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"
"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

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
}
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 := &ethereumTypeAddressData{}
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))

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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