Token values in explorer
This commit is contained in:
parent
03f72f07f2
commit
b118bbcd6b
48
api/types.go
48
api/types.go
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/trezor/blockbook/bchain"
|
||||
@ -64,6 +65,21 @@ func IsZeroBigInt(b *big.Int) bool {
|
||||
return len(b.Bits()) == 0
|
||||
}
|
||||
|
||||
// Compare returns an integer comparing two Amounts. The result will be 0 if a == b, -1 if a < b, and +1 if a > b.
|
||||
// Nil Amount is always less then non nil amount, two nil Amounts are equal
|
||||
func (a *Amount) Compare(b *Amount) int {
|
||||
if b == nil {
|
||||
if a == nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if a == nil {
|
||||
return -1
|
||||
}
|
||||
return (*big.Int)(a).Cmp((*big.Int)(b))
|
||||
}
|
||||
|
||||
// MarshalJSON Amount serialization
|
||||
func (a *Amount) MarshalJSON() (out []byte, err error) {
|
||||
if a == nil {
|
||||
@ -151,6 +167,8 @@ type Token struct {
|
||||
Symbol string `json:"symbol,omitempty"`
|
||||
Decimals int `json:"decimals,omitempty"`
|
||||
BalanceSat *Amount `json:"balance,omitempty"`
|
||||
BaseValue float64 `json:"baseValue,omitempty"`
|
||||
FiatValue float64 `json:"fiatValue,omitempty"`
|
||||
Ids []Amount `json:"ids,omitempty"` // multiple ERC721 tokens
|
||||
MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty"` // multiple ERC1155 tokens
|
||||
TotalReceivedSat *Amount `json:"totalReceived,omitempty"`
|
||||
@ -158,6 +176,30 @@ type Token struct {
|
||||
ContractIndex string `json:"-"`
|
||||
}
|
||||
|
||||
// Tokens is array of Token
|
||||
type Tokens []Token
|
||||
|
||||
func (a Tokens) Len() int { return len(a) }
|
||||
func (a Tokens) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a Tokens) Less(i, j int) bool {
|
||||
ti := &a[i]
|
||||
tj := &a[j]
|
||||
// sort by BaseValue descending and then Name and then by Contract
|
||||
if ti.BaseValue < tj.BaseValue {
|
||||
return false
|
||||
} else if ti.BaseValue > tj.BaseValue {
|
||||
return true
|
||||
}
|
||||
c := strings.Compare(ti.Name, tj.Name)
|
||||
if c == 1 {
|
||||
return false
|
||||
} else if c == -1 {
|
||||
return true
|
||||
}
|
||||
c = strings.Compare(ti.Contract, tj.Contract)
|
||||
return c == -1
|
||||
}
|
||||
|
||||
// TokenTransfer contains info about a token transfer done in a transaction
|
||||
type TokenTransfer struct {
|
||||
Type bchain.TokenTypeName `json:"type"`
|
||||
@ -286,7 +328,11 @@ type Address struct {
|
||||
Txids []string `json:"txids,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
UsedTokens int `json:"usedTokens,omitempty"`
|
||||
Tokens []Token `json:"tokens,omitempty"`
|
||||
Tokens Tokens `json:"tokens,omitempty"`
|
||||
TokensBaseValue float64 `json:"tokensBaseValue,omitempty"`
|
||||
TokensFiatValue float64 `json:"tokensFiatValue,omitempty"`
|
||||
TotalBaseValue float64 `json:"totalBaseValue,omitempty"`
|
||||
TotalFiatValue float64 `json:"totalFiatValue,omitempty"`
|
||||
ContractInfo *bchain.ContractInfo `json:"contractInfo,omitempty"`
|
||||
AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"`
|
||||
// helpers for explorer
|
||||
|
||||
@ -172,3 +172,50 @@ func TestBalanceHistories_SortAndAggregate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAmount_Compare(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a *Amount
|
||||
b *Amount
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "nil-nil",
|
||||
a: nil,
|
||||
b: nil,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "20-nil",
|
||||
a: (*Amount)(big.NewInt(20)),
|
||||
b: nil,
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "nil-20",
|
||||
a: nil,
|
||||
b: (*Amount)(big.NewInt(20)),
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "18-20",
|
||||
a: (*Amount)(big.NewInt(18)),
|
||||
b: (*Amount)(big.NewInt(20)),
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "20-20",
|
||||
a: (*Amount)(big.NewInt(20)),
|
||||
b: (*Amount)(big.NewInt(20)),
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.a.Compare(tt.b); got != tt.want {
|
||||
t.Errorf("Amount.Compare() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
162
api/worker.go
162
api/worker.go
@ -833,7 +833,7 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) {
|
||||
}, from, to, page
|
||||
}
|
||||
|
||||
func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, index int, c *db.AddrContract, details AccountDetails) (*Token, error) {
|
||||
func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, index int, c *db.AddrContract, details AccountDetails, ticker *common.CurrencyRatesTicker, secondaryCoin string) (*Token, error) {
|
||||
typeName := bchain.EthereumTokenTypeMap[c.Type]
|
||||
ci, validContract, err := w.getContractDescriptorInfo(c.Contract, typeName)
|
||||
if err != nil {
|
||||
@ -858,6 +858,21 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
|
||||
glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, c.Contract, err)
|
||||
} else {
|
||||
t.BalanceSat = (*Amount)(b)
|
||||
if secondaryCoin != "" {
|
||||
baseRate, found := w.GetContractBaseRate(ticker, t.Contract, 0)
|
||||
if found {
|
||||
value, err := strconv.ParseFloat(t.BalanceSat.DecimalString(t.Decimals), 64)
|
||||
if err == nil {
|
||||
t.BaseValue = value * baseRate
|
||||
if ticker != nil {
|
||||
secondaryRate, found := ticker.Rates[secondaryCoin]
|
||||
if found {
|
||||
t.FiatValue = t.BaseValue * float64(secondaryRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(c.Ids) > 0 {
|
||||
@ -910,30 +925,57 @@ func (w *Worker) getEthereumContractBalanceFromBlockchain(addrDesc, contract bch
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.ContractInfo, uint64, int, int, int, error) {
|
||||
var (
|
||||
ba *db.AddrBalance
|
||||
tokens []Token
|
||||
ci *bchain.ContractInfo
|
||||
n uint64
|
||||
nonContractTxs int
|
||||
internalTxs int
|
||||
)
|
||||
// unknown number of results for paging
|
||||
totalResults := -1
|
||||
// GetContractBaseRate returns contract rate in base coin from the ticker or DB at the blocktime. Zero blocktime means now.
|
||||
func (w *Worker) GetContractBaseRate(ticker *common.CurrencyRatesTicker, contract string, blocktime int64) (float64, bool) {
|
||||
if ticker == nil {
|
||||
return 0, false
|
||||
}
|
||||
rate, found := ticker.GetTokenRate(contract)
|
||||
if !found {
|
||||
var date time.Time
|
||||
if blocktime == 0 {
|
||||
date = time.Now().UTC()
|
||||
} else {
|
||||
date = time.Unix(blocktime, 0).UTC()
|
||||
}
|
||||
ticker, _ = w.db.FiatRatesFindTicker(&date, "", contract)
|
||||
if ticker == nil {
|
||||
return 0, false
|
||||
}
|
||||
rate, found = ticker.GetTokenRate(contract)
|
||||
}
|
||||
|
||||
return float64(rate), found
|
||||
}
|
||||
|
||||
type ethereumTypeAddressData struct {
|
||||
tokens Tokens
|
||||
contractInfo *bchain.ContractInfo
|
||||
nonce string
|
||||
nonContractTxs int
|
||||
internalTxs int
|
||||
totalResults int
|
||||
tokensBaseValue float64
|
||||
tokensFiatValue float64
|
||||
}
|
||||
|
||||
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter, secondaryCoin string) (*db.AddrBalance, *ethereumTypeAddressData, error) {
|
||||
var ba *db.AddrBalance
|
||||
// unknown number of results for paging initially
|
||||
d := ethereumTypeAddressData{totalResults: -1}
|
||||
ca, err := w.db.GetAddrDescContracts(addrDesc)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, 0, 0, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
|
||||
return nil, nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
|
||||
}
|
||||
b, err := w.chain.EthereumTypeGetBalance(addrDesc)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc)
|
||||
return nil, nil, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc)
|
||||
}
|
||||
var filterDesc bchain.AddressDescriptor
|
||||
if filter.Contract != "" {
|
||||
filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, 0, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true)
|
||||
return nil, nil, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true)
|
||||
}
|
||||
}
|
||||
if ca != nil {
|
||||
@ -943,12 +985,14 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
|
||||
if b != nil {
|
||||
ba.BalanceSat = *b
|
||||
}
|
||||
n, err = w.chain.EthereumTypeGetNonce(addrDesc)
|
||||
n, err := w.chain.EthereumTypeGetNonce(addrDesc)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc)
|
||||
return nil, nil, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc)
|
||||
}
|
||||
d.nonce = strconv.Itoa(int(n))
|
||||
ticker := w.is.GetCurrentTicker("", "")
|
||||
if details > AccountDetailsBasic {
|
||||
tokens = make([]Token, len(ca.Contracts))
|
||||
d.tokens = make([]Token, len(ca.Contracts))
|
||||
var j int
|
||||
for i := range ca.Contracts {
|
||||
c := &ca.Contracts[i]
|
||||
@ -959,35 +1003,38 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
|
||||
// filter only transactions of this contract
|
||||
filter.Vout = i + db.ContractIndexOffset
|
||||
}
|
||||
t, err := w.getEthereumContractBalance(addrDesc, i+db.ContractIndexOffset, c, details)
|
||||
t, err := w.getEthereumContractBalance(addrDesc, i+db.ContractIndexOffset, c, details, ticker, secondaryCoin)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, 0, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
tokens[j] = *t
|
||||
d.tokens[j] = *t
|
||||
d.tokensBaseValue += t.BaseValue
|
||||
d.tokensFiatValue += t.FiatValue
|
||||
j++
|
||||
}
|
||||
tokens = tokens[:j]
|
||||
d.tokens = d.tokens[:j]
|
||||
sort.Sort(d.tokens)
|
||||
}
|
||||
ci, err = w.db.GetContractInfo(addrDesc, "")
|
||||
d.contractInfo, err = w.db.GetContractInfo(addrDesc, "")
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, 0, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if filter.FromHeight == 0 && filter.ToHeight == 0 {
|
||||
// compute total results for paging
|
||||
if filter.Vout == AddressFilterVoutOff {
|
||||
totalResults = int(ca.TotalTxs)
|
||||
d.totalResults = int(ca.TotalTxs)
|
||||
} else if filter.Vout == 0 {
|
||||
totalResults = int(ca.NonContractTxs)
|
||||
d.totalResults = int(ca.NonContractTxs)
|
||||
} else if filter.Vout == db.InternalTxIndexOffset {
|
||||
totalResults = int(ca.InternalTxs)
|
||||
d.totalResults = int(ca.InternalTxs)
|
||||
} else if filter.Vout >= db.ContractIndexOffset && filter.Vout-db.ContractIndexOffset < len(ca.Contracts) {
|
||||
totalResults = int(ca.Contracts[filter.Vout-db.ContractIndexOffset].Txs)
|
||||
d.totalResults = int(ca.Contracts[filter.Vout-db.ContractIndexOffset].Txs)
|
||||
} else if filter.Vout == AddressFilterVoutQueryNotNecessary {
|
||||
totalResults = 0
|
||||
d.totalResults = 0
|
||||
}
|
||||
}
|
||||
nonContractTxs = int(ca.NonContractTxs)
|
||||
internalTxs = int(ca.InternalTxs)
|
||||
d.nonContractTxs = int(ca.NonContractTxs)
|
||||
d.internalTxs = int(ca.InternalTxs)
|
||||
} else {
|
||||
// addresses without any normal transactions can have internal transactions that were not processed and therefore balance
|
||||
if b != nil {
|
||||
@ -997,20 +1044,20 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
|
||||
}
|
||||
}
|
||||
// special handling if filtering for a contract, return the contract details even though the address had no transactions with it
|
||||
if len(tokens) == 0 && len(filterDesc) > 0 && details >= AccountDetailsTokens {
|
||||
if len(d.tokens) == 0 && len(filterDesc) > 0 && details >= AccountDetailsTokens {
|
||||
t, err := w.getEthereumContractBalanceFromBlockchain(addrDesc, filterDesc, details)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0, 0, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
tokens = []Token{*t}
|
||||
d.tokens = []Token{*t}
|
||||
// switch off query for transactions, there are no transactions
|
||||
filter.Vout = AddressFilterVoutQueryNotNecessary
|
||||
totalResults = -1
|
||||
d.totalResults = -1
|
||||
}
|
||||
return ba, tokens, ci, n, nonContractTxs, internalTxs, totalResults, nil
|
||||
return ba, &d, nil
|
||||
}
|
||||
|
||||
func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetails, blockInfo *db.BlockInfo, addresses map[string]struct{}) (*Tx, error) {
|
||||
func (w *Worker) txFromTxid(txid string, bestHeight uint32, option AccountDetails, blockInfo *db.BlockInfo, addresses map[string]struct{}) (*Tx, error) {
|
||||
var tx *Tx
|
||||
var err error
|
||||
// only ChainBitcoinType supports TxHistoryLight
|
||||
@ -1038,7 +1085,7 @@ func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetail
|
||||
blockInfo = &db.BlockInfo{}
|
||||
}
|
||||
}
|
||||
tx = w.txFromTxAddress(txid, ta, blockInfo, bestheight, addresses)
|
||||
tx = w.txFromTxAddress(txid, ta, blockInfo, bestHeight, addresses)
|
||||
}
|
||||
} else {
|
||||
tx, err = w.getTransaction(txid, false, false, addresses)
|
||||
@ -1093,7 +1140,7 @@ func setIsOwnAddress(tx *Tx, address string) {
|
||||
}
|
||||
|
||||
// GetAddress computes address value and gets transactions for given address
|
||||
func (w *Worker) GetAddress(address string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter) (*Address, error) {
|
||||
func (w *Worker) GetAddress(address string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, secondaryCoin string) (*Address, error) {
|
||||
start := time.Now()
|
||||
page--
|
||||
if page < 0 {
|
||||
@ -1101,31 +1148,26 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
|
||||
}
|
||||
var (
|
||||
ba *db.AddrBalance
|
||||
tokens []Token
|
||||
contractInfo *bchain.ContractInfo
|
||||
txm []string
|
||||
txs []*Tx
|
||||
txids []string
|
||||
pg Paging
|
||||
uBalSat big.Int
|
||||
totalReceived, totalSent *big.Int
|
||||
nonce string
|
||||
unconfirmedTxs int
|
||||
nonTokenTxs int
|
||||
internalTxs int
|
||||
totalResults int
|
||||
)
|
||||
ed := ðereumTypeAddressData{}
|
||||
addrDesc, address, err := w.getAddrDescAndNormalizeAddress(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if w.chainType == bchain.ChainEthereumType {
|
||||
var n uint64
|
||||
ba, tokens, contractInfo, n, nonTokenTxs, internalTxs, totalResults, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter)
|
||||
ba, ed, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter, secondaryCoin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce = strconv.Itoa(int(n))
|
||||
totalResults = ed.totalResults
|
||||
} else {
|
||||
// ba can be nil if the address is only in mempool!
|
||||
ba, err = w.db.GetAddrDescBalance(addrDesc, db.AddressBalanceDetailNoUTXO)
|
||||
@ -1214,10 +1256,22 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
|
||||
}
|
||||
}
|
||||
}
|
||||
var secondaryRate, totalFiatValue float64
|
||||
ticker := w.is.GetCurrentTicker("", "")
|
||||
totalBaseValue, err := strconv.ParseFloat((*Amount)(&ba.BalanceSat).DecimalString(w.chainParser.AmountDecimals()), 64)
|
||||
if ticker != nil && err == nil {
|
||||
r, found := ticker.Rates[secondaryCoin]
|
||||
if found {
|
||||
secondaryRate = float64(r)
|
||||
}
|
||||
}
|
||||
if w.chainType == bchain.ChainBitcoinType {
|
||||
totalReceived = ba.ReceivedSat()
|
||||
totalSent = &ba.SentSat
|
||||
} else {
|
||||
totalBaseValue += ed.tokensBaseValue
|
||||
}
|
||||
totalFiatValue = secondaryRate * totalBaseValue
|
||||
r := &Address{
|
||||
Paging: pg,
|
||||
AddrStr: address,
|
||||
@ -1225,15 +1279,19 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
|
||||
TotalReceivedSat: (*Amount)(totalReceived),
|
||||
TotalSentSat: (*Amount)(totalSent),
|
||||
Txs: int(ba.Txs),
|
||||
NonTokenTxs: nonTokenTxs,
|
||||
InternalTxs: internalTxs,
|
||||
NonTokenTxs: ed.nonContractTxs,
|
||||
InternalTxs: ed.internalTxs,
|
||||
UnconfirmedBalanceSat: (*Amount)(&uBalSat),
|
||||
UnconfirmedTxs: unconfirmedTxs,
|
||||
Transactions: txs,
|
||||
Txids: txids,
|
||||
Tokens: tokens,
|
||||
ContractInfo: contractInfo,
|
||||
Nonce: nonce,
|
||||
Tokens: ed.tokens,
|
||||
TokensBaseValue: ed.tokensBaseValue,
|
||||
TokensFiatValue: ed.tokensFiatValue,
|
||||
TotalBaseValue: totalBaseValue,
|
||||
TotalFiatValue: totalFiatValue,
|
||||
ContractInfo: ed.contractInfo,
|
||||
Nonce: ed.nonce,
|
||||
AddressAliases: w.getAddressAliases(addresses),
|
||||
}
|
||||
glog.Info("GetAddress ", address, ", ", time.Since(start))
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package common
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CurrencyRatesTicker contains coin ticker data fetched from API
|
||||
type CurrencyRatesTicker struct {
|
||||
@ -9,6 +12,15 @@ type CurrencyRatesTicker struct {
|
||||
TokenRates map[string]float32 `json:"tokenRates"` // rates of the tokens (identified by the address of the contract) against the base currency
|
||||
}
|
||||
|
||||
// Convert returns token rate in base currency
|
||||
func (t *CurrencyRatesTicker) GetTokenRate(token string) (float32, bool) {
|
||||
if t.TokenRates != nil {
|
||||
rate, found := t.TokenRates[strings.ToLower(token)]
|
||||
return rate, found
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Convert converts value in base currency to toCurrency
|
||||
func (t *CurrencyRatesTicker) Convert(baseValue float64, toCurrency string) float64 {
|
||||
rate, found := t.Rates[toCurrency]
|
||||
@ -20,11 +32,9 @@ func (t *CurrencyRatesTicker) Convert(baseValue float64, toCurrency string) floa
|
||||
|
||||
// ConvertTokenToBase converts token value to base currency
|
||||
func (t *CurrencyRatesTicker) ConvertTokenToBase(value float64, token string) float64 {
|
||||
if t.TokenRates != nil {
|
||||
rate, found := t.TokenRates[token]
|
||||
if found {
|
||||
return value * float64(rate)
|
||||
}
|
||||
rate, found := t.GetTokenRate(token)
|
||||
if found {
|
||||
return value * float64(rate)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@ -40,13 +50,11 @@ func (t *CurrencyRatesTicker) ConvertToken(value float64, token string, toCurren
|
||||
|
||||
// TokenRateInCurrency return token rate in toCurrency currency
|
||||
func (t *CurrencyRatesTicker) TokenRateInCurrency(token string, toCurrency string) float32 {
|
||||
if t.TokenRates != nil {
|
||||
rate, found := t.TokenRates[token]
|
||||
rate, found := t.GetTokenRate(token)
|
||||
if found {
|
||||
baseRate, found := t.Rates[toCurrency]
|
||||
if found {
|
||||
baseRate, found := t.Rates[toCurrency]
|
||||
if found {
|
||||
return baseRate * rate
|
||||
}
|
||||
return baseRate * rate
|
||||
}
|
||||
}
|
||||
return 0
|
||||
|
||||
120
server/public.go
120
server/public.go
@ -501,7 +501,6 @@ type TemplateData struct {
|
||||
UseSecondaryCoin bool
|
||||
CurrentSecondaryCoinRate float64
|
||||
CurrentTicker *common.CurrencyRatesTicker
|
||||
TxBlocktime int64
|
||||
TxDate string
|
||||
TxSecondaryCoinRate float64
|
||||
TxTicker *common.CurrencyRatesTicker
|
||||
@ -514,6 +513,9 @@ func (s *PublicServer) parseTemplates() []*template.Template {
|
||||
"amountSpan": s.amountSpan,
|
||||
"tokenAmountSpan": s.tokenAmountSpan,
|
||||
"amountSatsSpan": s.amountSatsSpan,
|
||||
"formattedAmountSpan": s.formattedAmountSpan,
|
||||
"summaryValuesSpan": s.summaryValuesSpan,
|
||||
"addressAlias": addressAlias,
|
||||
"addressAliasSpan": addressAliasSpan,
|
||||
"formatAmount": s.formatAmount,
|
||||
"formatAmountWithDecimals": formatAmountWithDecimals,
|
||||
@ -680,9 +682,12 @@ func formatAmountWithDecimals(a *api.Amount, d int) string {
|
||||
}
|
||||
|
||||
func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) {
|
||||
rv.WriteString(`<span class="`)
|
||||
rv.WriteString(class)
|
||||
rv.WriteString(`"`)
|
||||
rv.WriteString(`<span`)
|
||||
if class != "" {
|
||||
rv.WriteString(` class="`)
|
||||
rv.WriteString(class)
|
||||
rv.WriteString(`"`)
|
||||
}
|
||||
if txDate != "" {
|
||||
rv.WriteString(` tm="`)
|
||||
rv.WriteString(txDate)
|
||||
@ -699,12 +704,14 @@ func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate strin
|
||||
appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
rv.WriteString(" ")
|
||||
rv.WriteString(shortcut)
|
||||
if shortcut != "" {
|
||||
rv.WriteString(" ")
|
||||
rv.WriteString(shortcut)
|
||||
}
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
|
||||
func appendWrappingAmountSpan(rv *strings.Builder, primary, symbol, classes string) {
|
||||
func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) {
|
||||
rv.WriteString(`<span class="amt`)
|
||||
if classes != "" {
|
||||
rv.WriteString(` `)
|
||||
@ -720,7 +727,7 @@ func appendWrappingAmountSpan(rv *strings.Builder, primary, symbol, classes stri
|
||||
func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML {
|
||||
primary := s.formatAmount(a)
|
||||
var rv strings.Builder
|
||||
appendWrappingAmountSpan(&rv, primary, td.CoinShortcut, classes)
|
||||
appendAmountWrapperSpan(&rv, primary, td.CoinShortcut, classes)
|
||||
appendAmountSpan(&rv, "prim-amt", primary, td.CoinShortcut, "")
|
||||
if td.SecondaryCoin != "" {
|
||||
p, err := strconv.ParseFloat(primary, 64)
|
||||
@ -729,8 +736,7 @@ func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes strin
|
||||
txSecondary := ""
|
||||
// if tx is specified, compute secondary amount is at the time of tx and amount with current rate is returned with class "csec-amt"
|
||||
if td.Tx != nil {
|
||||
if td.Tx.Blocktime != td.TxBlocktime {
|
||||
td.TxBlocktime = td.Tx.Blocktime
|
||||
if td.TxTicker == nil {
|
||||
date := time.Unix(td.Tx.Blocktime, 0).UTC()
|
||||
secondary := strings.ToLower(td.SecondaryCoin)
|
||||
ticker, _ := s.db.FiatRatesFindTicker(&date, secondary, "")
|
||||
@ -767,7 +773,7 @@ func (s *PublicServer) amountSatsSpan(a *api.Amount, td *TemplateData, classes s
|
||||
var rv strings.Builder
|
||||
rv.WriteString(`<span`)
|
||||
if classes != "" {
|
||||
rv.WriteString(` class="`)
|
||||
rv.WriteString(` class="`)
|
||||
rv.WriteString(classes)
|
||||
rv.WriteString(`"`)
|
||||
}
|
||||
@ -779,45 +785,26 @@ func (s *PublicServer) amountSatsSpan(a *api.Amount, td *TemplateData, classes s
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func getContractRate(ticker *common.CurrencyRatesTicker, contract string) (float64, bool) {
|
||||
if ticker == nil {
|
||||
return 0, false
|
||||
}
|
||||
rate, found := ticker.TokenRates[contract]
|
||||
return float64(rate), found
|
||||
}
|
||||
|
||||
func (s *PublicServer) tokenAmountSpan(t *api.TokenTransfer, td *TemplateData, classes string) template.HTML {
|
||||
primary := formatAmountWithDecimals(t.Value, t.Decimals)
|
||||
var rv strings.Builder
|
||||
appendWrappingAmountSpan(&rv, primary, td.CoinShortcut, classes)
|
||||
appendAmountWrapperSpan(&rv, primary, td.CoinShortcut, classes)
|
||||
appendAmountSpan(&rv, "prim-amt", primary, t.Symbol, "")
|
||||
if td.SecondaryCoin != "" {
|
||||
var currentBase, currentSecondary, txBase, txSecondary string
|
||||
p, err := strconv.ParseFloat(primary, 64)
|
||||
if err == nil {
|
||||
ticker := td.CurrentTicker
|
||||
baseRate, found := getContractRate(ticker, t.Contract)
|
||||
if !found {
|
||||
now := time.Now().UTC()
|
||||
ticker, _ = s.db.FiatRatesFindTicker(&now, "", t.Contract)
|
||||
baseRate, found = getContractRate(ticker, t.Contract)
|
||||
}
|
||||
baseRate, found := s.api.GetContractBaseRate(td.CurrentTicker, t.Contract, 0)
|
||||
if found {
|
||||
base := p * baseRate
|
||||
currentBase = strconv.FormatFloat(base, 'g', s.chainParser.AmountDecimals(), 64)
|
||||
currentBase = strconv.FormatFloat(base, 'f', 6, 64)
|
||||
currentSecondary = strconv.FormatFloat(base*td.CurrentSecondaryCoinRate, 'f', 2, 64)
|
||||
}
|
||||
ticker = td.TxTicker
|
||||
baseRate, found = getContractRate(ticker, t.Contract)
|
||||
if !found {
|
||||
ticker, _ = s.db.FiatRatesFindTicker(&td.TxTicker.Timestamp, "", t.Contract)
|
||||
baseRate, found = getContractRate(ticker, t.Contract)
|
||||
}
|
||||
baseRate, found = s.api.GetContractBaseRate(td.TxTicker, t.Contract, td.Tx.Blocktime)
|
||||
if found {
|
||||
base := p * baseRate
|
||||
txBase = strconv.FormatFloat(base, 'g', s.chainParser.AmountDecimals(), 64)
|
||||
txSecondary = strconv.FormatFloat(base*td.CurrentSecondaryCoinRate, 'f', 2, 64)
|
||||
txBase = strconv.FormatFloat(base, 'f', 6, 64)
|
||||
txSecondary = strconv.FormatFloat(base*td.TxSecondaryCoinRate, 'f', 2, 64)
|
||||
}
|
||||
}
|
||||
if txBase != "" {
|
||||
@ -843,6 +830,33 @@ func (s *PublicServer) tokenAmountSpan(t *api.TokenTransfer, td *TemplateData, c
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func (s *PublicServer) formattedAmountSpan(a *api.Amount, d int, symbol string, td *TemplateData, classes string) template.HTML {
|
||||
if symbol == td.CoinShortcut {
|
||||
d = s.chainParser.AmountDecimals()
|
||||
}
|
||||
value := formatAmountWithDecimals(a, d)
|
||||
var rv strings.Builder
|
||||
appendAmountSpan(&rv, classes, value, symbol, "")
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float64, td *TemplateData) template.HTML {
|
||||
var rv strings.Builder
|
||||
if secondaryValue > 0 {
|
||||
appendAmountSpan(&rv, "", strconv.FormatFloat(secondaryValue, 'f', 2, 64), td.SecondaryCoin, "")
|
||||
if baseValue > 0 && s.chainParser.GetChainType() == bchain.ChainEthereumType {
|
||||
rv.WriteString(`<span class="base-value">(`)
|
||||
appendAmountSpan(&rv, "", strconv.FormatFloat(baseValue, 'f', 6, 64), td.CoinShortcut, "")
|
||||
rv.WriteString(")</span>")
|
||||
}
|
||||
} else {
|
||||
if td.SecondaryCoin != "" {
|
||||
rv.WriteString("-")
|
||||
}
|
||||
}
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func formatInt(i int) template.HTML {
|
||||
return formatInt64(int64(i))
|
||||
}
|
||||
@ -905,7 +919,7 @@ func formatBigInt(i *big.Int) template.HTML {
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func addressAliasSpan(a string, td *TemplateData) template.HTML {
|
||||
func getAddressAlias(a string, td *TemplateData) *api.AddressAlias {
|
||||
var alias api.AddressAlias
|
||||
var found bool
|
||||
if td.Block != nil {
|
||||
@ -915,8 +929,24 @@ func addressAliasSpan(a string, td *TemplateData) template.HTML {
|
||||
} else if td.Tx != nil {
|
||||
alias, found = td.Tx.AddressAliases[a]
|
||||
}
|
||||
var rv strings.Builder
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return &alias
|
||||
}
|
||||
|
||||
func addressAlias(a string, td *TemplateData) string {
|
||||
alias := getAddressAlias(a, td)
|
||||
if alias == nil {
|
||||
return ""
|
||||
}
|
||||
return alias.Alias
|
||||
}
|
||||
|
||||
func addressAliasSpan(a string, td *TemplateData) template.HTML {
|
||||
var rv strings.Builder
|
||||
alias := getAddressAlias(a, td)
|
||||
if alias == nil {
|
||||
rv.WriteString(`<span class="copyable">`)
|
||||
rv.WriteString(a)
|
||||
} else {
|
||||
@ -934,9 +964,8 @@ func addressAliasSpan(a string, td *TemplateData) template.HTML {
|
||||
// called from template to support txdetail.html functionality
|
||||
func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
|
||||
td.Tx = tx
|
||||
// reset the TxBlocktimeSecondaryCoinRate if different Blocktime
|
||||
if td.TxBlocktime != tx.Blocktime {
|
||||
td.TxBlocktime = 0
|
||||
// reset the TxTicker if different Blocktime
|
||||
if td.TxTicker != nil && td.TxTicker.Timestamp.Unix() != tx.Blocktime {
|
||||
td.TxSecondaryCoinRate = 0
|
||||
td.TxTicker = nil
|
||||
}
|
||||
@ -1104,11 +1133,11 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
|
||||
page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
|
||||
// do not allow details to be changed by query params
|
||||
address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter)
|
||||
data := s.newTemplateData(r)
|
||||
address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, strings.ToLower(data.SecondaryCoin))
|
||||
if err != nil {
|
||||
return errorTpl, nil, err
|
||||
}
|
||||
data := s.newTemplateData(r)
|
||||
data.AddrStr = address.AddrStr
|
||||
data.Address = address
|
||||
data.Page = address.Page
|
||||
@ -1253,7 +1282,7 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t
|
||||
http.Redirect(w, r, joinURL("/tx/", tx.Txid), http.StatusFound)
|
||||
return noTpl, nil, nil
|
||||
}
|
||||
address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
|
||||
address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, "")
|
||||
if err == nil {
|
||||
http.Redirect(w, r, joinURL("/address/", address.AddrStr), http.StatusFound)
|
||||
return noTpl, nil, nil
|
||||
@ -1451,7 +1480,8 @@ func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{},
|
||||
var err error
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
|
||||
page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
|
||||
address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter)
|
||||
secondaryCoin := strings.ToLower(r.URL.Query().Get("secondary"))
|
||||
address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter, secondaryCoin)
|
||||
if err == nil && apiVersion == apiV1 {
|
||||
return s.api.AddressToV1(address), nil
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -63,7 +63,6 @@ type fiatRatesSubscription struct {
|
||||
|
||||
// WebsocketServer is a handle to websocket server
|
||||
type WebsocketServer struct {
|
||||
socket *websocket.Conn
|
||||
upgrader *websocket.Upgrader
|
||||
db *db.RocksDB
|
||||
txCache *db.TxCache
|
||||
@ -483,15 +482,16 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) {
|
||||
}
|
||||
|
||||
type accountInfoReq struct {
|
||||
Descriptor string `json:"descriptor"`
|
||||
Details string `json:"details"`
|
||||
Tokens string `json:"tokens"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Page int `json:"page"`
|
||||
FromHeight int `json:"from"`
|
||||
ToHeight int `json:"to"`
|
||||
ContractFilter string `json:"contractFilter"`
|
||||
Gap int `json:"gap"`
|
||||
Descriptor string `json:"descriptor"`
|
||||
Details string `json:"details"`
|
||||
Tokens string `json:"tokens"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Page int `json:"page"`
|
||||
FromHeight int `json:"from"`
|
||||
ToHeight int `json:"to"`
|
||||
ContractFilter string `json:"contractFilter"`
|
||||
SecondaryCurrency string `json:"secondaryCurrency"`
|
||||
Gap int `json:"gap"`
|
||||
}
|
||||
|
||||
func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) {
|
||||
@ -540,7 +540,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address,
|
||||
}
|
||||
a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, req.Gap)
|
||||
if err != nil {
|
||||
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter)
|
||||
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, strings.ToLower(req.SecondaryCurrency))
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
@ -825,6 +825,8 @@ func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, d *fiatRatesSu
|
||||
currency := d.Currency
|
||||
if currency == "" {
|
||||
currency = allFiatRates
|
||||
} else {
|
||||
currency = strings.ToLower(currency)
|
||||
}
|
||||
as, ok := s.fiatRatesSubscriptions[currency]
|
||||
if !ok {
|
||||
|
||||
@ -69,6 +69,12 @@ select {
|
||||
border-color: #00854d;
|
||||
}
|
||||
|
||||
.base-value {
|
||||
color: #757575 !important;
|
||||
padding-left: 0.5rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.badge {
|
||||
vertical-align: middle;
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.15));
|
||||
@ -79,6 +85,10 @@ select {
|
||||
--bs-badge-border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: #757575 !important;
|
||||
}
|
||||
|
||||
.accordion {
|
||||
--bs-accordion-border-radius: 10px;
|
||||
--bs-accordion-inner-border-radius: calc(10px - 1px);
|
||||
@ -93,6 +103,10 @@ select {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.bb-group {
|
||||
border: 0.6rem solid #f6f6f6;
|
||||
background-color: #f6f6f6;
|
||||
@ -464,7 +478,7 @@ span.btn-paging:hover {
|
||||
}
|
||||
|
||||
.txerror {
|
||||
background-color: #c51f13b3;
|
||||
background-color: #c51f13a0;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
{{define "specific"}}{{$addr := .Address}}{{$data := .}}
|
||||
<div class="row">
|
||||
<div class="col-md-10 order-2 order-md-1">
|
||||
<h1>{{if $addr.ContractInfo}}Contract {{$addr.ContractInfo.Name}}{{if $addr.ContractInfo.Symbol}} ({{$addr.ContractInfo.Symbol}}){{end}}{{else}}Address{{end}}</h1>
|
||||
<h1>{{if $addr.ContractInfo}}Contract {{$addr.ContractInfo.Name}}{{if $addr.ContractInfo.Symbol}} ({{$addr.ContractInfo.Symbol}}){{end}}{{else}}Address {{addressAlias $addr.AddrStr $data}}{{end}}</h1>
|
||||
<h5 class="col-12 d-flex h-data pb-2"><span class="ellipsis copyable">{{$addr.AddrStr}}</span></h5>
|
||||
<h4>{{amountSpan $addr.BalanceSat $data "copyable"}}</h4>
|
||||
<h4>
|
||||
{{formattedAmountSpan $addr.BalanceSat 0 $data.CoinShortcut $data "copyable"}}
|
||||
{{if $addr.TotalFiatValue}}<span class="ps-5"{{if eq .ChainType 1}} tt="Address value including tokens"{{end}}>{{summaryValuesSpan $addr.TotalBaseValue $addr.TotalFiatValue $data}}</span>{{end}}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-md-2 order-1 order-md-2 d-flex justify-content-center justify-content-md-end mb-3 mb-md-0">
|
||||
<div id="qrcode"></div>
|
||||
@ -20,6 +23,26 @@
|
||||
<td></td>
|
||||
</tr>
|
||||
{{if eq .ChainType 1}}
|
||||
<tr>
|
||||
<td style="width: 25%;">Balance</td>
|
||||
<td>{{amountSpan $addr.BalanceSat $data "copyable"}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Transactions</td>
|
||||
<td>{{formatInt $addr.Txs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Non-contract Transactions</td>
|
||||
<td>{{formatInt $addr.NonTokenTxs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Internal Transactions</td>
|
||||
<td>{{formatInt $addr.InternalTxs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nonce</td>
|
||||
<td>{{$addr.Nonce}}</td>
|
||||
</tr>
|
||||
{{if $addr.ContractInfo}}
|
||||
{{if $addr.ContractInfo.Type}}
|
||||
<tr>
|
||||
@ -30,116 +53,16 @@
|
||||
{{if $addr.ContractInfo.CreatedInBlock}}
|
||||
<tr>
|
||||
<td style="width: 25%;">Created in Block</td>
|
||||
<td><a href="/block/{{$addr.ContractInfo.CreatedInBlock}}">{{$addr.ContractInfo.CreatedInBlock}}</a></td>
|
||||
<td><a href="/block/{{$addr.ContractInfo.CreatedInBlock}}">{{formatUint32 $addr.ContractInfo.CreatedInBlock}}</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if $addr.ContractInfo.DestructedInBlock}}
|
||||
<tr>
|
||||
<td style="width: 25%;">Destructed in Block</td>
|
||||
<td><a href="/block/{{$addr.ContractInfo.DestructedInBlock}}">{{$addr.ContractInfo.DestructedInBlock}}</a></td>
|
||||
<td><a href="/block/{{$addr.ContractInfo.DestructedInBlock}}">{{formatUint32 $addr.ContractInfo.DestructedInBlock}}</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<tr>
|
||||
<td style="width: 25%;">Balance</td>
|
||||
<td>{{amountSpan $addr.BalanceSat $data "copyable"}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Transactions</td>
|
||||
<td>{{$addr.Txs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Non-contract Transactions</td>
|
||||
<td>{{$addr.NonTokenTxs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Internal Transactions</td>
|
||||
<td>{{$addr.InternalTxs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nonce</td>
|
||||
<td>{{$addr.Nonce}}</td>
|
||||
</tr>
|
||||
{{if tokenCount $addr.Tokens "ERC20"}}
|
||||
<tr>
|
||||
<td>ERC20 Tokens</td>
|
||||
<td style="padding: 0;">
|
||||
<table class="table data-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Contract</th>
|
||||
<th>Tokens</th>
|
||||
<th style="width: 15%;">Transfers</th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC20"}}
|
||||
<tr>
|
||||
<td class="ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}}</a></td>
|
||||
<td>{{formatAmountWithDecimals $t.BalanceSat $t.Decimals}} {{$t.Symbol}}</td>
|
||||
<td>{{$t.Transfers}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if tokenCount $addr.Tokens "ERC721"}}
|
||||
<tr>
|
||||
<td>ERC721 Tokens</td>
|
||||
<td style="padding: 0;">
|
||||
<table class="table data-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Contract</th>
|
||||
<th>Tokens</th>
|
||||
<th style="width: 15%;">Transfers</th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC721"}}
|
||||
<tr>
|
||||
<td class="ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}}</a></td>
|
||||
<td>
|
||||
{{range $i, $iv := $t.Ids}}{{if $i}}, {{end}}<a href="/nft/{{$t.Contract}}/{{formatAmountWithDecimals $iv 0}}">{{formatAmountWithDecimals $iv 0}}</a>{{end}}
|
||||
</td>
|
||||
<td>{{$t.Transfers}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if tokenCount $addr.Tokens "ERC1155"}}
|
||||
<tr>
|
||||
<td>ERC1155 Tokens</td>
|
||||
<td style="padding: 0;">
|
||||
<table class="table data-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Contract</th>
|
||||
<th>Tokens</th>
|
||||
<th style="width: 15%;">Transfers</th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC1155"}}
|
||||
<tr>
|
||||
<td class="ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}}</a></td>
|
||||
<td>
|
||||
{{range $i, $iv := $t.MultiTokenValues}}{{if $i}}, {{end}}{{$iv.Value}} of ID <a href="/nft/{{$t.Contract}}/{{$iv.Id}}">{{$iv.Id}}</a>{{end}}
|
||||
</td>
|
||||
<td>{{$t.Transfers}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td style="width: 25%;">Total Received</td>
|
||||
@ -178,6 +101,120 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
{{if eq .ChainType 1}}
|
||||
{{if tokenCount $addr.Tokens "ERC20"}}
|
||||
<div class="accordion mt-2 mb-2" id="erc20">
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header" id="erc20Heading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#erc20Body" aria-expanded="false" aria-controls="erc20Body">
|
||||
<div class="row g-0 w-100">
|
||||
<h5 class="col-md-4 mb-md-0">ERC20 Tokens <span class="badge bg-secondary">{{tokenCount $addr.Tokens "ERC20"}}</span></h5>
|
||||
<h5 class="col-md-8 mb-md-0"><span tt="Total value of tokens">{{summaryValuesSpan $addr.TokensBaseValue $addr.TokensFiatValue $data}}</span></h5>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div id="erc20Body" class="accordion-collapse collapse" aria-labelledby="erc20Heading" data-bs-parent="#erc20">
|
||||
<div class="accordion-body">
|
||||
<table class="table data-table mt-0 mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 25%;">Contract</th>
|
||||
<th style="width: 30%;">Quantity</th>
|
||||
<th style="width: 35%;">Value</th>
|
||||
<th class="text-end" style="width: 10%;">Transfers</th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC20"}}
|
||||
<tr>
|
||||
<td class="ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}<span class="copyable" cc="{{$t.Contract}}" alias-type="Contract">{{$t.Name}}</span>{{else}}<span class="copyable">{{$t.Contract}}</span>{{end}}</a></td>
|
||||
<td>{{formattedAmountSpan $t.BalanceSat $t.Decimals $t.Symbol $data "copyable"}}</td>
|
||||
<td>{{summaryValuesSpan $t.BaseValue $t.FiatValue $data}}</span></td>
|
||||
<td class="text-end">{{formatInt $t.Transfers}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if tokenCount $addr.Tokens "ERC721"}}
|
||||
<div class="accordion mt-2 mb-2" id="erc721">
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header" id="erc721Heading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#erc721Body" aria-expanded="false" aria-controls="erc721Body">
|
||||
<div class="row g-0 w-100">
|
||||
<h5 class="col-12 mb-md-0">ERC721 Tokens <span class="badge bg-secondary">{{tokenCount $addr.Tokens "ERC721"}}</span></h5>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div id="erc721Body" class="accordion-collapse collapse" aria-labelledby="erc721Heading" data-bs-parent="#erc721">
|
||||
<div class="accordion-body">
|
||||
<table class="table data-table mt-0 mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 25%;">Contract</th>
|
||||
<th style="width: 65%;">Tokens</th>
|
||||
<th class="text-end" style="width: 10%;">Transfers</th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC721"}}
|
||||
<tr>
|
||||
<td class="ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}<span class="copyable" cc="{{$t.Contract}}" alias-type="Contract">{{$t.Name}}</span>{{else}}<span class="copyable">{{$t.Contract}}</span>{{end}}</a></td>
|
||||
<td>
|
||||
{{range $i, $iv := $t.Ids}}{{if $i}}, {{end}}<a href="/nft/{{$t.Contract}}/{{formatAmountWithDecimals $iv 0}}">{{formatAmountWithDecimals $iv 0}}</a>{{end}}
|
||||
</td>
|
||||
<td class="text-end">{{$t.Transfers}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if tokenCount $addr.Tokens "ERC1155"}}
|
||||
<div class="accordion mt-2 mb-2" id="erc1155">
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header" id="erc1155Heading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#erc1155Body" aria-expanded="false" aria-controls="erc1155Body">
|
||||
<div class="row g-0 w-100">
|
||||
<h5 class="col-12 mb-md-0">ERC1155 Tokens <span class="badge bg-secondary">{{tokenCount $addr.Tokens "ERC1155"}}</span></h5>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div id="erc1155Body" class="accordion-collapse collapse" aria-labelledby="erc1155Heading" data-bs-parent="#erc1155">
|
||||
<div class="accordion-body">
|
||||
<table class="table data-table mt-0 mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 25%;">Contract</th>
|
||||
<th style="width: 65%;">Tokens</th>
|
||||
<th class="text-end" style="width: 10%;">Transfers</th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC1155"}}
|
||||
<tr>
|
||||
<td class="ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}<span class="copyable" cc="{{$t.Contract}}" alias-type="Contract">{{$t.Name}}</span>{{else}}<span class="copyable">{{$t.Contract}}</span>{{end}}</a></td>
|
||||
<td>
|
||||
{{range $i, $iv := $t.MultiTokenValues}}{{if $i}}, {{end}}{{formattedAmountSpan $iv.Value 0 $t.Symbol $data ""}} of ID <a href="/nft/{{$t.Contract}}/{{$iv.Id}}">{{$iv.Id}}</a>{{end}}
|
||||
</td>
|
||||
<td class="text-end">{{formatInt $t.Transfers}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if or $addr.Transactions $addr.Filter}}
|
||||
<div class="row pt-3 pb-1">
|
||||
<h3 class="col-sm-6 col-lg-3 m-0 align-self-center">Transactions</h3>
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 25%;">Token ID</td>
|
||||
<td>{{$data.TokenId}}</td>
|
||||
<td><span class="copyable">{{$data.TokenId}}</span></td>
|
||||
</tr>
|
||||
<tr id="name" style="display: none;">
|
||||
<td>NTF Name</td>
|
||||
<td></td>
|
||||
<td class="copyable"></td>
|
||||
</tr>
|
||||
<tr id="description" style="display: none;">
|
||||
<td>NTF Description</td>
|
||||
@ -18,7 +18,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contract</td>
|
||||
<td><a href="/address/{{$data.ContractInfo.Contract}}">{{$data.ContractInfo.Contract}}</a><br>{{$data.ContractInfo.Name}}</td>
|
||||
<td><a href="/address/{{$data.ContractInfo.Contract}}"><span class="copyable">{{$data.ContractInfo.Contract}}</span></a><br>{{$data.ContractInfo.Name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contract type</td>
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
<div class="col-12">No Outputs</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 amt-out">{{amountSpan $tx.ValueOutSat $data "tx-out copyable"}}</div>
|
||||
</div>
|
||||
|
||||
@ -175,7 +175,7 @@
|
||||
</div>
|
||||
<div class="col-md-3 amt-out">
|
||||
{{range $i, $iv := $tt.MultiTokenValues}}
|
||||
{{if $i}}, {{end}}{{$iv.Value}} {{$tt.Symbol}} of ID <a href="/nft/{{$tt.Contract}}/{{$iv.Id}}">{{$iv.Id}}</a>
|
||||
{{if $i}}, {{end}}{{formattedAmountSpan $iv.Value 0 $tt.Symbol $data ""}} of ID <a href="/nft/{{$tt.Contract}}/{{$iv.Id}}">{{$iv.Id}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -141,6 +141,7 @@
|
||||
const from = parseInt(document.getElementById("getAccountInfoFrom").value);
|
||||
const to = parseInt(document.getElementById("getAccountInfoTo").value);
|
||||
const contractFilter = document.getElementById("getAccountInfoContract").value.trim();
|
||||
const secondaryCurrency = document.getElementById("getAccountInfoSecondaryCurrency").value.trim();
|
||||
const pageSize = 10;
|
||||
const method = 'getAccountInfo';
|
||||
const tokens = "derived"; // could be "nonzero", "used", default is "derived" i.e. all
|
||||
@ -152,7 +153,8 @@
|
||||
pageSize,
|
||||
from,
|
||||
to,
|
||||
contractFilter
|
||||
contractFilter,
|
||||
secondaryCurrency,
|
||||
// default gap=20
|
||||
};
|
||||
send(method, params, function (result) {
|
||||
@ -471,9 +473,10 @@
|
||||
</div>
|
||||
<div class="row" style="margin: 0; margin-top: 5px;">
|
||||
<input type="text" placeholder="page" style="width: 10%; margin-right: 5px;" class="form-control" id="getAccountInfoPage">
|
||||
<input type="text" placeholder="from" style="width: 15%;margin-left: 5px;margin-right: 5px;" class="form-control" id="getAccountInfoFrom">
|
||||
<input type="text" placeholder="to" style="width: 15%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoTo">
|
||||
<input type="text" placeholder="contract" style="width: 55%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoContract">
|
||||
<input type="text" placeholder="from" style="width: 13%;margin-left: 5px;margin-right: 5px;" class="form-control" id="getAccountInfoFrom">
|
||||
<input type="text" placeholder="to" style="width: 13%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoTo">
|
||||
<input type="text" placeholder="contract" style="width: 50%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoContract">
|
||||
<input type="text" placeholder="usd" style="width: 8%; margin-left: 5px;" class="form-control" id="getAccountInfoSecondaryCurrency">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form-inline"></div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user