Token values in explorer

This commit is contained in:
Martin Boehm 2022-11-29 09:40:17 +01:00 committed by Martin
parent 03f72f07f2
commit b118bbcd6b
13 changed files with 488 additions and 243 deletions

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"math/big" "math/big"
"sort" "sort"
"strings"
"time" "time"
"github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain"
@ -64,6 +65,21 @@ func IsZeroBigInt(b *big.Int) bool {
return len(b.Bits()) == 0 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 // MarshalJSON Amount serialization
func (a *Amount) MarshalJSON() (out []byte, err error) { func (a *Amount) MarshalJSON() (out []byte, err error) {
if a == nil { if a == nil {
@ -151,6 +167,8 @@ type Token struct {
Symbol string `json:"symbol,omitempty"` Symbol string `json:"symbol,omitempty"`
Decimals int `json:"decimals,omitempty"` Decimals int `json:"decimals,omitempty"`
BalanceSat *Amount `json:"balance,omitempty"` BalanceSat *Amount `json:"balance,omitempty"`
BaseValue float64 `json:"baseValue,omitempty"`
FiatValue float64 `json:"fiatValue,omitempty"`
Ids []Amount `json:"ids,omitempty"` // multiple ERC721 tokens Ids []Amount `json:"ids,omitempty"` // multiple ERC721 tokens
MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty"` // multiple ERC1155 tokens MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty"` // multiple ERC1155 tokens
TotalReceivedSat *Amount `json:"totalReceived,omitempty"` TotalReceivedSat *Amount `json:"totalReceived,omitempty"`
@ -158,6 +176,30 @@ type Token struct {
ContractIndex string `json:"-"` 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 // TokenTransfer contains info about a token transfer done in a transaction
type TokenTransfer struct { type TokenTransfer struct {
Type bchain.TokenTypeName `json:"type"` Type bchain.TokenTypeName `json:"type"`
@ -286,7 +328,11 @@ type Address struct {
Txids []string `json:"txids,omitempty"` Txids []string `json:"txids,omitempty"`
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
UsedTokens int `json:"usedTokens,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"` ContractInfo *bchain.ContractInfo `json:"contractInfo,omitempty"`
AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"`
// helpers for explorer // helpers for explorer

View File

@ -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)
}
})
}
}

View File

@ -833,7 +833,7 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) {
}, from, to, page }, 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] typeName := bchain.EthereumTokenTypeMap[c.Type]
ci, validContract, err := w.getContractDescriptorInfo(c.Contract, typeName) ci, validContract, err := w.getContractDescriptorInfo(c.Contract, typeName)
if err != nil { 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) glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, c.Contract, err)
} else { } else {
t.BalanceSat = (*Amount)(b) 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 { } else {
if len(c.Ids) > 0 { if len(c.Ids) > 0 {
@ -910,30 +925,57 @@ func (w *Worker) getEthereumContractBalanceFromBlockchain(addrDesc, contract bch
}, nil }, nil
} }
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.ContractInfo, uint64, int, int, int, error) { // GetContractBaseRate returns contract rate in base coin from the ticker or DB at the blocktime. Zero blocktime means now.
var ( func (w *Worker) GetContractBaseRate(ticker *common.CurrencyRatesTicker, contract string, blocktime int64) (float64, bool) {
ba *db.AddrBalance if ticker == nil {
tokens []Token return 0, false
ci *bchain.ContractInfo }
n uint64 rate, found := ticker.GetTokenRate(contract)
nonContractTxs int if !found {
internalTxs int var date time.Time
) if blocktime == 0 {
// unknown number of results for paging date = time.Now().UTC()
totalResults := -1 } 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) ca, err := w.db.GetAddrDescContracts(addrDesc)
if err != nil { 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) b, err := w.chain.EthereumTypeGetBalance(addrDesc)
if err != nil { 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 var filterDesc bchain.AddressDescriptor
if filter.Contract != "" { if filter.Contract != "" {
filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract) filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract)
if err != nil { 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 { if ca != nil {
@ -943,12 +985,14 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
if b != nil { if b != nil {
ba.BalanceSat = *b ba.BalanceSat = *b
} }
n, err = w.chain.EthereumTypeGetNonce(addrDesc) n, err := w.chain.EthereumTypeGetNonce(addrDesc)
if err != nil { 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 { if details > AccountDetailsBasic {
tokens = make([]Token, len(ca.Contracts)) d.tokens = make([]Token, len(ca.Contracts))
var j int var j int
for i := range ca.Contracts { for i := range ca.Contracts {
c := &ca.Contracts[i] c := &ca.Contracts[i]
@ -959,35 +1003,38 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
// filter only transactions of this contract // filter only transactions of this contract
filter.Vout = i + db.ContractIndexOffset 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 { 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++ 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 { if err != nil {
return nil, nil, nil, 0, 0, 0, 0, err return nil, nil, err
} }
if filter.FromHeight == 0 && filter.ToHeight == 0 { if filter.FromHeight == 0 && filter.ToHeight == 0 {
// compute total results for paging // compute total results for paging
if filter.Vout == AddressFilterVoutOff { if filter.Vout == AddressFilterVoutOff {
totalResults = int(ca.TotalTxs) d.totalResults = int(ca.TotalTxs)
} else if filter.Vout == 0 { } else if filter.Vout == 0 {
totalResults = int(ca.NonContractTxs) d.totalResults = int(ca.NonContractTxs)
} else if filter.Vout == db.InternalTxIndexOffset { } 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) { } 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 { } else if filter.Vout == AddressFilterVoutQueryNotNecessary {
totalResults = 0 d.totalResults = 0
} }
} }
nonContractTxs = int(ca.NonContractTxs) d.nonContractTxs = int(ca.NonContractTxs)
internalTxs = int(ca.InternalTxs) d.internalTxs = int(ca.InternalTxs)
} else { } else {
// addresses without any normal transactions can have internal transactions that were not processed and therefore balance // addresses without any normal transactions can have internal transactions that were not processed and therefore balance
if b != nil { 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 // 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) t, err := w.getEthereumContractBalanceFromBlockchain(addrDesc, filterDesc, details)
if err != nil { 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 // switch off query for transactions, there are no transactions
filter.Vout = AddressFilterVoutQueryNotNecessary 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 tx *Tx
var err error var err error
// only ChainBitcoinType supports TxHistoryLight // only ChainBitcoinType supports TxHistoryLight
@ -1038,7 +1085,7 @@ func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetail
blockInfo = &db.BlockInfo{} blockInfo = &db.BlockInfo{}
} }
} }
tx = w.txFromTxAddress(txid, ta, blockInfo, bestheight, addresses) tx = w.txFromTxAddress(txid, ta, blockInfo, bestHeight, addresses)
} }
} else { } else {
tx, err = w.getTransaction(txid, false, false, addresses) 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 // 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() start := time.Now()
page-- page--
if page < 0 { if page < 0 {
@ -1101,31 +1148,26 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
} }
var ( var (
ba *db.AddrBalance ba *db.AddrBalance
tokens []Token
contractInfo *bchain.ContractInfo
txm []string txm []string
txs []*Tx txs []*Tx
txids []string txids []string
pg Paging pg Paging
uBalSat big.Int uBalSat big.Int
totalReceived, totalSent *big.Int totalReceived, totalSent *big.Int
nonce string
unconfirmedTxs int unconfirmedTxs int
nonTokenTxs int
internalTxs int
totalResults int totalResults int
) )
ed := &ethereumTypeAddressData{}
addrDesc, address, err := w.getAddrDescAndNormalizeAddress(address) addrDesc, address, err := w.getAddrDescAndNormalizeAddress(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if w.chainType == bchain.ChainEthereumType { if w.chainType == bchain.ChainEthereumType {
var n uint64 ba, ed, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter, secondaryCoin)
ba, tokens, contractInfo, n, nonTokenTxs, internalTxs, totalResults, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nonce = strconv.Itoa(int(n)) totalResults = ed.totalResults
} else { } else {
// ba can be nil if the address is only in mempool! // ba can be nil if the address is only in mempool!
ba, err = w.db.GetAddrDescBalance(addrDesc, db.AddressBalanceDetailNoUTXO) 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 { if w.chainType == bchain.ChainBitcoinType {
totalReceived = ba.ReceivedSat() totalReceived = ba.ReceivedSat()
totalSent = &ba.SentSat totalSent = &ba.SentSat
} else {
totalBaseValue += ed.tokensBaseValue
} }
totalFiatValue = secondaryRate * totalBaseValue
r := &Address{ r := &Address{
Paging: pg, Paging: pg,
AddrStr: address, AddrStr: address,
@ -1225,15 +1279,19 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
TotalReceivedSat: (*Amount)(totalReceived), TotalReceivedSat: (*Amount)(totalReceived),
TotalSentSat: (*Amount)(totalSent), TotalSentSat: (*Amount)(totalSent),
Txs: int(ba.Txs), Txs: int(ba.Txs),
NonTokenTxs: nonTokenTxs, NonTokenTxs: ed.nonContractTxs,
InternalTxs: internalTxs, InternalTxs: ed.internalTxs,
UnconfirmedBalanceSat: (*Amount)(&uBalSat), UnconfirmedBalanceSat: (*Amount)(&uBalSat),
UnconfirmedTxs: unconfirmedTxs, UnconfirmedTxs: unconfirmedTxs,
Transactions: txs, Transactions: txs,
Txids: txids, Txids: txids,
Tokens: tokens, Tokens: ed.tokens,
ContractInfo: contractInfo, TokensBaseValue: ed.tokensBaseValue,
Nonce: nonce, TokensFiatValue: ed.tokensFiatValue,
TotalBaseValue: totalBaseValue,
TotalFiatValue: totalFiatValue,
ContractInfo: ed.contractInfo,
Nonce: ed.nonce,
AddressAliases: w.getAddressAliases(addresses), AddressAliases: w.getAddressAliases(addresses),
} }
glog.Info("GetAddress ", address, ", ", time.Since(start)) glog.Info("GetAddress ", address, ", ", time.Since(start))

View File

@ -1,6 +1,9 @@
package common package common
import "time" import (
"strings"
"time"
)
// CurrencyRatesTicker contains coin ticker data fetched from API // CurrencyRatesTicker contains coin ticker data fetched from API
type CurrencyRatesTicker struct { 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 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 // Convert converts value in base currency to toCurrency
func (t *CurrencyRatesTicker) Convert(baseValue float64, toCurrency string) float64 { func (t *CurrencyRatesTicker) Convert(baseValue float64, toCurrency string) float64 {
rate, found := t.Rates[toCurrency] 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 // ConvertTokenToBase converts token value to base currency
func (t *CurrencyRatesTicker) ConvertTokenToBase(value float64, token string) float64 { func (t *CurrencyRatesTicker) ConvertTokenToBase(value float64, token string) float64 {
if t.TokenRates != nil { rate, found := t.GetTokenRate(token)
rate, found := t.TokenRates[token] if found {
if found { return value * float64(rate)
return value * float64(rate)
}
} }
return 0 return 0
} }
@ -40,13 +50,11 @@ func (t *CurrencyRatesTicker) ConvertToken(value float64, token string, toCurren
// TokenRateInCurrency return token rate in toCurrency currency // TokenRateInCurrency return token rate in toCurrency currency
func (t *CurrencyRatesTicker) TokenRateInCurrency(token string, toCurrency string) float32 { func (t *CurrencyRatesTicker) TokenRateInCurrency(token string, toCurrency string) float32 {
if t.TokenRates != nil { rate, found := t.GetTokenRate(token)
rate, found := t.TokenRates[token] if found {
baseRate, found := t.Rates[toCurrency]
if found { if found {
baseRate, found := t.Rates[toCurrency] return baseRate * rate
if found {
return baseRate * rate
}
} }
} }
return 0 return 0

View File

@ -501,7 +501,6 @@ type TemplateData struct {
UseSecondaryCoin bool UseSecondaryCoin bool
CurrentSecondaryCoinRate float64 CurrentSecondaryCoinRate float64
CurrentTicker *common.CurrencyRatesTicker CurrentTicker *common.CurrencyRatesTicker
TxBlocktime int64
TxDate string TxDate string
TxSecondaryCoinRate float64 TxSecondaryCoinRate float64
TxTicker *common.CurrencyRatesTicker TxTicker *common.CurrencyRatesTicker
@ -514,6 +513,9 @@ func (s *PublicServer) parseTemplates() []*template.Template {
"amountSpan": s.amountSpan, "amountSpan": s.amountSpan,
"tokenAmountSpan": s.tokenAmountSpan, "tokenAmountSpan": s.tokenAmountSpan,
"amountSatsSpan": s.amountSatsSpan, "amountSatsSpan": s.amountSatsSpan,
"formattedAmountSpan": s.formattedAmountSpan,
"summaryValuesSpan": s.summaryValuesSpan,
"addressAlias": addressAlias,
"addressAliasSpan": addressAliasSpan, "addressAliasSpan": addressAliasSpan,
"formatAmount": s.formatAmount, "formatAmount": s.formatAmount,
"formatAmountWithDecimals": formatAmountWithDecimals, "formatAmountWithDecimals": formatAmountWithDecimals,
@ -680,9 +682,12 @@ func formatAmountWithDecimals(a *api.Amount, d int) string {
} }
func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) { func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) {
rv.WriteString(`<span class="`) rv.WriteString(`<span`)
rv.WriteString(class) if class != "" {
rv.WriteString(`"`) rv.WriteString(` class="`)
rv.WriteString(class)
rv.WriteString(`"`)
}
if txDate != "" { if txDate != "" {
rv.WriteString(` tm="`) rv.WriteString(` tm="`)
rv.WriteString(txDate) rv.WriteString(txDate)
@ -699,12 +704,14 @@ func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate strin
appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns") appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
rv.WriteString("</span>") rv.WriteString("</span>")
} }
rv.WriteString(" ") if shortcut != "" {
rv.WriteString(shortcut) rv.WriteString(" ")
rv.WriteString(shortcut)
}
rv.WriteString("</span>") 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`) rv.WriteString(`<span class="amt`)
if classes != "" { if classes != "" {
rv.WriteString(` `) 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 { func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML {
primary := s.formatAmount(a) primary := s.formatAmount(a)
var rv strings.Builder var rv strings.Builder
appendWrappingAmountSpan(&rv, primary, td.CoinShortcut, classes) appendAmountWrapperSpan(&rv, primary, td.CoinShortcut, classes)
appendAmountSpan(&rv, "prim-amt", primary, td.CoinShortcut, "") appendAmountSpan(&rv, "prim-amt", primary, td.CoinShortcut, "")
if td.SecondaryCoin != "" { if td.SecondaryCoin != "" {
p, err := strconv.ParseFloat(primary, 64) p, err := strconv.ParseFloat(primary, 64)
@ -729,8 +736,7 @@ func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes strin
txSecondary := "" 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 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 != nil {
if td.Tx.Blocktime != td.TxBlocktime { if td.TxTicker == nil {
td.TxBlocktime = td.Tx.Blocktime
date := time.Unix(td.Tx.Blocktime, 0).UTC() date := time.Unix(td.Tx.Blocktime, 0).UTC()
secondary := strings.ToLower(td.SecondaryCoin) secondary := strings.ToLower(td.SecondaryCoin)
ticker, _ := s.db.FiatRatesFindTicker(&date, secondary, "") 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 var rv strings.Builder
rv.WriteString(`<span`) rv.WriteString(`<span`)
if classes != "" { if classes != "" {
rv.WriteString(` class="`) rv.WriteString(` class="`)
rv.WriteString(classes) rv.WriteString(classes)
rv.WriteString(`"`) rv.WriteString(`"`)
} }
@ -779,45 +785,26 @@ func (s *PublicServer) amountSatsSpan(a *api.Amount, td *TemplateData, classes s
return template.HTML(rv.String()) 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 { func (s *PublicServer) tokenAmountSpan(t *api.TokenTransfer, td *TemplateData, classes string) template.HTML {
primary := formatAmountWithDecimals(t.Value, t.Decimals) primary := formatAmountWithDecimals(t.Value, t.Decimals)
var rv strings.Builder var rv strings.Builder
appendWrappingAmountSpan(&rv, primary, td.CoinShortcut, classes) appendAmountWrapperSpan(&rv, primary, td.CoinShortcut, classes)
appendAmountSpan(&rv, "prim-amt", primary, t.Symbol, "") appendAmountSpan(&rv, "prim-amt", primary, t.Symbol, "")
if td.SecondaryCoin != "" { if td.SecondaryCoin != "" {
var currentBase, currentSecondary, txBase, txSecondary string var currentBase, currentSecondary, txBase, txSecondary string
p, err := strconv.ParseFloat(primary, 64) p, err := strconv.ParseFloat(primary, 64)
if err == nil { if err == nil {
ticker := td.CurrentTicker baseRate, found := s.api.GetContractBaseRate(td.CurrentTicker, t.Contract, 0)
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)
}
if found { if found {
base := p * baseRate 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) currentSecondary = strconv.FormatFloat(base*td.CurrentSecondaryCoinRate, 'f', 2, 64)
} }
ticker = td.TxTicker baseRate, found = s.api.GetContractBaseRate(td.TxTicker, t.Contract, td.Tx.Blocktime)
baseRate, found = getContractRate(ticker, t.Contract)
if !found {
ticker, _ = s.db.FiatRatesFindTicker(&td.TxTicker.Timestamp, "", t.Contract)
baseRate, found = getContractRate(ticker, t.Contract)
}
if found { if found {
base := p * baseRate base := p * baseRate
txBase = strconv.FormatFloat(base, 'g', s.chainParser.AmountDecimals(), 64) txBase = strconv.FormatFloat(base, 'f', 6, 64)
txSecondary = strconv.FormatFloat(base*td.CurrentSecondaryCoinRate, 'f', 2, 64) txSecondary = strconv.FormatFloat(base*td.TxSecondaryCoinRate, 'f', 2, 64)
} }
} }
if txBase != "" { if txBase != "" {
@ -843,6 +830,33 @@ func (s *PublicServer) tokenAmountSpan(t *api.TokenTransfer, td *TemplateData, c
return template.HTML(rv.String()) 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 { func formatInt(i int) template.HTML {
return formatInt64(int64(i)) return formatInt64(int64(i))
} }
@ -905,7 +919,7 @@ func formatBigInt(i *big.Int) template.HTML {
return template.HTML(rv.String()) 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 alias api.AddressAlias
var found bool var found bool
if td.Block != nil { if td.Block != nil {
@ -915,8 +929,24 @@ func addressAliasSpan(a string, td *TemplateData) template.HTML {
} else if td.Tx != nil { } else if td.Tx != nil {
alias, found = td.Tx.AddressAliases[a] alias, found = td.Tx.AddressAliases[a]
} }
var rv strings.Builder
if !found { 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(`<span class="copyable">`)
rv.WriteString(a) rv.WriteString(a)
} else { } else {
@ -934,9 +964,8 @@ func addressAliasSpan(a string, td *TemplateData) template.HTML {
// called from template to support txdetail.html functionality // called from template to support txdetail.html functionality
func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData { func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
td.Tx = tx td.Tx = tx
// reset the TxBlocktimeSecondaryCoinRate if different Blocktime // reset the TxTicker if different Blocktime
if td.TxBlocktime != tx.Blocktime { if td.TxTicker != nil && td.TxTicker.Timestamp.Unix() != tx.Blocktime {
td.TxBlocktime = 0
td.TxSecondaryCoinRate = 0 td.TxSecondaryCoinRate = 0
td.TxTicker = nil 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() s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage) page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
// do not allow details to be changed by query params // 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 { if err != nil {
return errorTpl, nil, err return errorTpl, nil, err
} }
data := s.newTemplateData(r)
data.AddrStr = address.AddrStr data.AddrStr = address.AddrStr
data.Address = address data.Address = address
data.Page = address.Page 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) http.Redirect(w, r, joinURL("/tx/", tx.Txid), http.StatusFound)
return noTpl, nil, nil 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 { if err == nil {
http.Redirect(w, r, joinURL("/address/", address.AddrStr), http.StatusFound) http.Redirect(w, r, joinURL("/address/", address.AddrStr), http.StatusFound)
return noTpl, nil, nil return noTpl, nil, nil
@ -1451,7 +1480,8 @@ func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{},
var err error var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) 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 { if err == nil && apiVersion == apiV1 {
return s.api.AddressToV1(address), nil 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

View File

@ -63,7 +63,6 @@ type fiatRatesSubscription struct {
// WebsocketServer is a handle to websocket server // WebsocketServer is a handle to websocket server
type WebsocketServer struct { type WebsocketServer struct {
socket *websocket.Conn
upgrader *websocket.Upgrader upgrader *websocket.Upgrader
db *db.RocksDB db *db.RocksDB
txCache *db.TxCache txCache *db.TxCache
@ -483,15 +482,16 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) {
} }
type accountInfoReq struct { type accountInfoReq struct {
Descriptor string `json:"descriptor"` Descriptor string `json:"descriptor"`
Details string `json:"details"` Details string `json:"details"`
Tokens string `json:"tokens"` Tokens string `json:"tokens"`
PageSize int `json:"pageSize"` PageSize int `json:"pageSize"`
Page int `json:"page"` Page int `json:"page"`
FromHeight int `json:"from"` FromHeight int `json:"from"`
ToHeight int `json:"to"` ToHeight int `json:"to"`
ContractFilter string `json:"contractFilter"` ContractFilter string `json:"contractFilter"`
Gap int `json:"gap"` SecondaryCurrency string `json:"secondaryCurrency"`
Gap int `json:"gap"`
} }
func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) { 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) a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, req.Gap)
if err != nil { 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 return a, nil
} }
@ -825,6 +825,8 @@ func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, d *fiatRatesSu
currency := d.Currency currency := d.Currency
if currency == "" { if currency == "" {
currency = allFiatRates currency = allFiatRates
} else {
currency = strings.ToLower(currency)
} }
as, ok := s.fiatRatesSubscriptions[currency] as, ok := s.fiatRatesSubscriptions[currency]
if !ok { if !ok {

View File

@ -69,6 +69,12 @@ select {
border-color: #00854d; border-color: #00854d;
} }
.base-value {
color: #757575 !important;
padding-left: 0.5rem;
font-weight: normal;
}
.badge { .badge {
vertical-align: middle; vertical-align: middle;
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.15)); filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.15));
@ -79,6 +85,10 @@ select {
--bs-badge-border-radius: 0.6rem; --bs-badge-border-radius: 0.6rem;
} }
.bg-secondary {
background-color: #757575 !important;
}
.accordion { .accordion {
--bs-accordion-border-radius: 10px; --bs-accordion-border-radius: 10px;
--bs-accordion-inner-border-radius: calc(10px - 1px); --bs-accordion-inner-border-radius: calc(10px - 1px);
@ -93,6 +103,10 @@ select {
box-shadow: none; box-shadow: none;
} }
.accordion-body {
letter-spacing: -0.01em;
}
.bb-group { .bb-group {
border: 0.6rem solid #f6f6f6; border: 0.6rem solid #f6f6f6;
background-color: #f6f6f6; background-color: #f6f6f6;
@ -464,7 +478,7 @@ span.btn-paging:hover {
} }
.txerror { .txerror {
background-color: #c51f13b3; background-color: #c51f13a0;
color: white !important; color: white !important;
} }

View File

@ -1,9 +1,12 @@
{{define "specific"}}{{$addr := .Address}}{{$data := .}} {{define "specific"}}{{$addr := .Address}}{{$data := .}}
<div class="row"> <div class="row">
<div class="col-md-10 order-2 order-md-1"> <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> <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>
<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 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> <div id="qrcode"></div>
@ -20,6 +23,26 @@
<td></td> <td></td>
</tr> </tr>
{{if eq .ChainType 1}} {{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}}
{{if $addr.ContractInfo.Type}} {{if $addr.ContractInfo.Type}}
<tr> <tr>
@ -30,116 +53,16 @@
{{if $addr.ContractInfo.CreatedInBlock}} {{if $addr.ContractInfo.CreatedInBlock}}
<tr> <tr>
<td style="width: 25%;">Created in Block</td> <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> </tr>
{{end}} {{end}}
{{if $addr.ContractInfo.DestructedInBlock}} {{if $addr.ContractInfo.DestructedInBlock}}
<tr> <tr>
<td style="width: 25%;">Destructed in Block</td> <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> </tr>
{{end}} {{end}}
{{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}} {{else}}
<tr> <tr>
<td style="width: 25%;">Total Received</td> <td style="width: 25%;">Total Received</td>
@ -178,6 +101,120 @@
</tbody> </tbody>
</table> </table>
{{end}} {{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}} {{if or $addr.Transactions $addr.Filter}}
<div class="row pt-3 pb-1"> <div class="row pt-3 pb-1">
<h3 class="col-sm-6 col-lg-3 m-0 align-self-center">Transactions</h3> <h3 class="col-sm-6 col-lg-3 m-0 align-self-center">Transactions</h3>

View File

@ -6,11 +6,11 @@
<tbody> <tbody>
<tr> <tr>
<td style="width: 25%;">Token ID</td> <td style="width: 25%;">Token ID</td>
<td>{{$data.TokenId}}</td> <td><span class="copyable">{{$data.TokenId}}</span></td>
</tr> </tr>
<tr id="name" style="display: none;"> <tr id="name" style="display: none;">
<td>NTF Name</td> <td>NTF Name</td>
<td></td> <td class="copyable"></td>
</tr> </tr>
<tr id="description" style="display: none;"> <tr id="description" style="display: none;">
<td>NTF Description</td> <td>NTF Description</td>
@ -18,7 +18,7 @@
</tr> </tr>
<tr> <tr>
<td>Contract</td> <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>
<tr> <tr>
<td>Contract type</td> <td>Contract type</td>

View File

@ -48,7 +48,7 @@
<div class="col-12">No Outputs</div> <div class="col-12">No Outputs</div>
{{end}} {{end}}
</div> </div>
</div> </div>
<div class="col-md-3 amt-out">{{amountSpan $tx.ValueOutSat $data "tx-out copyable"}}</div> <div class="col-md-3 amt-out">{{amountSpan $tx.ValueOutSat $data "tx-out copyable"}}</div>
</div> </div>
@ -175,7 +175,7 @@
</div> </div>
<div class="col-md-3 amt-out"> <div class="col-md-3 amt-out">
{{range $i, $iv := $tt.MultiTokenValues}} {{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}} {{end}}
</div> </div>
</div> </div>

View File

@ -141,6 +141,7 @@
const from = parseInt(document.getElementById("getAccountInfoFrom").value); const from = parseInt(document.getElementById("getAccountInfoFrom").value);
const to = parseInt(document.getElementById("getAccountInfoTo").value); const to = parseInt(document.getElementById("getAccountInfoTo").value);
const contractFilter = document.getElementById("getAccountInfoContract").value.trim(); const contractFilter = document.getElementById("getAccountInfoContract").value.trim();
const secondaryCurrency = document.getElementById("getAccountInfoSecondaryCurrency").value.trim();
const pageSize = 10; const pageSize = 10;
const method = 'getAccountInfo'; const method = 'getAccountInfo';
const tokens = "derived"; // could be "nonzero", "used", default is "derived" i.e. all const tokens = "derived"; // could be "nonzero", "used", default is "derived" i.e. all
@ -152,7 +153,8 @@
pageSize, pageSize,
from, from,
to, to,
contractFilter contractFilter,
secondaryCurrency,
// default gap=20 // default gap=20
}; };
send(method, params, function (result) { send(method, params, function (result) {
@ -471,9 +473,10 @@
</div> </div>
<div class="row" style="margin: 0; margin-top: 5px;"> <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="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="from" style="width: 13%;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="to" style="width: 13%; 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="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> </div>
<div class="col form-inline"></div> <div class="col form-inline"></div>