Show ERC20 contracts for address

This commit is contained in:
Martin Boehm 2018-12-03 15:48:07 +01:00
parent c96c357013
commit fead52881f
8 changed files with 188 additions and 69 deletions

View File

@ -10,6 +10,20 @@ import (
"time" "time"
) )
// GetAddressOption specifies what data returns GetAddress api call
type GetAddressOption int
const (
// ExistOnly - only that address is indexed
ExistOnly GetAddressOption = iota
// BalancesOnly - only balances
BalancesOnly
// TxidHistory - balances and txids, subject to paging
TxidHistory
// TxHistory - balances and full tx data, subject to paging
TxHistory
)
// APIError extends error by information if the error details should be returned to the end user // APIError extends error by information if the error details should be returned to the end user
type APIError struct { type APIError struct {
Text string Text string
@ -125,16 +139,17 @@ type Paging struct {
// Address holds information about address and its transactions // Address holds information about address and its transactions
type Address struct { type Address struct {
Paging Paging
AddrStr string `json:"addrStr"` AddrStr string `json:"addrStr"`
Balance string `json:"balance"` Balance string `json:"balance"`
TotalReceived string `json:"totalReceived"` TotalReceived string `json:"totalReceived,omitempty"`
TotalSent string `json:"totalSent"` TotalSent string `json:"totalSent,omitempty"`
UnconfirmedBalance string `json:"unconfirmedBalance"` UnconfirmedBalance string `json:"unconfirmedBalance"`
UnconfirmedTxApperances int `json:"unconfirmedTxApperances"` UnconfirmedTxApperances int `json:"unconfirmedTxApperances"`
TxApperances int `json:"txApperances"` TxApperances int `json:"txApperances"`
Transactions []*Tx `json:"txs,omitempty"` Transactions []*Tx `json:"txs,omitempty"`
Txids []string `json:"transactions,omitempty"` Txids []string `json:"transactions,omitempty"`
Erc20Tokens []*Erc20Token `json:"erc20tokens,omitempty"` Erc20Contract *bchain.Erc20Contract `json:"erc20contract,omitempty"`
Erc20Tokens []Erc20Token `json:"erc20tokens,omitempty"`
} }
// AddressUtxo holds information about address and its transactions // AddressUtxo holds information about address and its transactions

View File

@ -229,7 +229,9 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool
erc20c, err := w.chain.EthereumTypeGetErc20ContractInfo(cd) erc20c, err := w.chain.EthereumTypeGetErc20ContractInfo(cd)
if err != nil { if err != nil {
glog.Errorf("GetErc20ContractInfo error %v, contract %v", err, e.Contract) glog.Errorf("GetErc20ContractInfo error %v, contract %v", err, e.Contract)
erc20c = &bchain.Erc20Contract{} }
if erc20c == nil {
erc20c = &bchain.Erc20Contract{Name: e.Contract}
} }
erc20t[i] = Erc20Transfer{ erc20t[i] = Erc20Transfer{
Contract: e.Contract, Contract: e.Contract,
@ -418,8 +420,67 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) {
}, from, to, page }, from, to, page
} }
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, option GetAddressOption) (*db.AddrBalance, []Erc20Token, *bchain.Erc20Contract, error) {
var (
ba *db.AddrBalance
erc20t []Erc20Token
ci *bchain.Erc20Contract
)
ca, err := w.db.GetAddrDescContracts(addrDesc)
if err != nil {
return nil, nil, nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
}
if ca != nil {
ba = &db.AddrBalance{
Txs: uint32(ca.EthTxs),
}
// do not read balances etc in case of ExistOnly option
if option != ExistOnly {
var b *big.Int
b, err = w.chain.EthereumTypeGetBalance(addrDesc)
if err != nil {
return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc)
}
if b != nil {
ba.BalanceSat = *b
}
erc20t = make([]Erc20Token, len(ca.Contracts))
for i, c := range ca.Contracts {
ci, err := w.chain.EthereumTypeGetErc20ContractInfo(c.Contract)
if err != nil {
return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractInfo %v", c.Contract)
}
if ci == nil {
ci = &bchain.Erc20Contract{}
addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(c.Contract)
if len(addresses) > 0 {
ci.Contract = addresses[0]
ci.Name = addresses[0]
}
}
b, err = w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract)
if err != nil {
return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract)
}
erc20t[i] = Erc20Token{
Balance: bchain.AmountToDecimalString(b, ci.Decimals),
Contract: ci.Contract,
Name: ci.Name,
Symbol: ci.Symbol,
Txs: int(c.Txs),
}
}
ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc)
if err != nil {
return nil, nil, nil, err
}
}
}
return ba, erc20t, ci, nil
}
// 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, onlyTxids, existOnly bool) (*Address, error) { func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetAddressOption) (*Address, error) {
start := time.Now() start := time.Now()
page-- page--
if page < 0 { if page < 0 {
@ -430,15 +491,15 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
return nil, NewAPIError(fmt.Sprintf("Invalid address, %v", err), true) return nil, NewAPIError(fmt.Sprintf("Invalid address, %v", err), true)
} }
var ( var (
ba *db.AddrBalance ba *db.AddrBalance
ca *db.AddrContracts erc20t []Erc20Token
erc20c *bchain.Erc20Contract
) )
if w.chainType == bchain.ChainEthereumType { if w.chainType == bchain.ChainEthereumType {
ca, err = w.db.GetAddrDescContracts(addrDesc) ba, erc20t, erc20c, err = w.getEthereumTypeAddressBalances(addrDesc, option)
if err != nil { if err != nil {
return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) return nil, err
} }
glog.Infof("%+v", ca)
} 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) ba, err = w.db.GetAddrDescBalance(addrDesc)
@ -447,7 +508,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
} }
} }
// if only check that the address exist, return if we have the address // if only check that the address exist, return if we have the address
if existOnly && ba != nil { if option == ExistOnly && ba != nil {
return &Address{AddrStr: address}, nil return &Address{AddrStr: address}, nil
} }
// convert the address to the format defined by the parser // convert the address to the format defined by the parser
@ -460,7 +521,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
} }
txc, err := w.getAddressTxids(addrDesc, false) txc, err := w.getAddressTxids(addrDesc, false)
if err != nil { if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v false", address) return nil, errors.Annotatef(err, "getAddressTxids %v false", addrDesc)
} }
txc = UniqueTxidsInReverse(txc) txc = UniqueTxidsInReverse(txc)
var txm []string var txm []string
@ -471,11 +532,11 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
} }
txm, err = w.getAddressTxids(addrDesc, true) txm, err = w.getAddressTxids(addrDesc, true)
if err != nil { if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v true", address) return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc)
} }
txm = UniqueTxidsInReverse(txm) txm = UniqueTxidsInReverse(txm)
// check if the address exist // check if the address exist
if len(txc)+len(txm) == 0 { if len(txc)+len(txm) == 0 || option == ExistOnly {
return &Address{ return &Address{
AddrStr: address, AddrStr: address,
}, nil }, nil
@ -487,7 +548,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
pg, from, to, page := computePaging(len(txc), page, txsOnPage) pg, from, to, page := computePaging(len(txc), page, txsOnPage)
var txs []*Tx var txs []*Tx
var txids []string var txids []string
if onlyTxids { if option == TxidHistory {
txids = make([]string, len(txm)+to-from) txids = make([]string, len(txm)+to-from)
} else { } else {
txs = make([]*Tx, len(txm)+to-from) txs = make([]*Tx, len(txm)+to-from)
@ -504,7 +565,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc)) uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc))
uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc)) uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc))
if page == 0 { if page == 0 {
if onlyTxids { if option == TxidHistory {
txids[txi] = tx.Txid txids[txi] = tx.Txid
} else { } else {
txs[txi] = tx txs[txi] = tx
@ -513,12 +574,12 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
} }
} }
} }
if len(txc) != int(ba.Txs) { if len(txc) != int(ba.Txs) && w.chainType == bchain.ChainBitcoinType {
glog.Warning("DB inconsistency for address ", address, ": number of txs from column addresses ", len(txc), ", from addressBalance ", ba.Txs) glog.Warning("DB inconsistency for address ", address, ": number of txs from column addresses ", len(txc), ", from addressBalance ", ba.Txs)
} }
for i := from; i < to; i++ { for i := from; i < to; i++ {
txid := txc[i] txid := txc[i]
if onlyTxids { if option == TxidHistory {
txids[txi] = txid txids[txi] = txid
} else { } else {
if w.chainType == bchain.ChainEthereumType { if w.chainType == bchain.ChainEthereumType {
@ -548,7 +609,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
} }
txi++ txi++
} }
if onlyTxids { if option == TxidHistory {
txids = txids[:txi] txids = txids[:txi]
} else { } else {
txs = txs[:txi] txs = txs[:txi]
@ -564,6 +625,8 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
UnconfirmedTxApperances: len(txm), UnconfirmedTxApperances: len(txm),
Transactions: txs, Transactions: txs,
Txids: txids, Txids: txids,
Erc20Contract: erc20c,
Erc20Tokens: erc20t,
} }
glog.Info("GetAddress ", address, " finished in ", time.Since(start)) glog.Info("GetAddress ", address, " finished in ", time.Since(start))
return r, nil return r, nil

View File

@ -56,6 +56,9 @@ func (p *BaseParser) AmountToBigInt(n json.Number) (big.Int, error) {
// AmountToDecimalString converts amount in big.Int to string with decimal point in the place defined by the parameter d // AmountToDecimalString converts amount in big.Int to string with decimal point in the place defined by the parameter d
func AmountToDecimalString(a *big.Int, d int) string { func AmountToDecimalString(a *big.Int, d int) string {
if a == nil {
return ""
}
n := a.String() n := a.String()
var s string var s string
if n[0] == '-' { if n[0] == '-' {

View File

@ -43,7 +43,7 @@ type Erc20Transfer struct {
Tokens big.Int Tokens big.Int
} }
var cachedContracts = make(map[string]bchain.Erc20Contract) var cachedContracts = make(map[string]*bchain.Erc20Contract)
var cachedContractsMux sync.Mutex var cachedContractsMux sync.Mutex
func addressFromPaddedHex(s string) (string, error) { func addressFromPaddedHex(s string) (string, error) {
@ -146,30 +146,38 @@ func (b *EthereumRPC) EthereumTypeGetErc20ContractInfo(contractDesc bchain.Addre
return nil, err return nil, err
} }
name := parseErc20StringProperty(contractDesc, data) name := parseErc20StringProperty(contractDesc, data)
data, err = b.ethCall(erc20SymbolSignature, address) if name != "" {
if err != nil { data, err = b.ethCall(erc20SymbolSignature, address)
return nil, err if err != nil {
} return nil, err
symbol := parseErc20StringProperty(contractDesc, data) }
data, err = b.ethCall(erc20DecimalsSignature, address) symbol := parseErc20StringProperty(contractDesc, data)
if err != nil { data, err = b.ethCall(erc20DecimalsSignature, address)
return nil, err if err != nil {
} return nil, err
contract = bchain.Erc20Contract{ }
Name: name, if name == "" {
Symbol: symbol, name = address
} }
d := parseErc20NumericProperty(contractDesc, data) contract = &bchain.Erc20Contract{
if d != nil { Contract: address,
contract.Decimals = int(uint8(d.Uint64())) Name: name,
Symbol: symbol,
}
d := parseErc20NumericProperty(contractDesc, data)
if d != nil {
contract.Decimals = int(uint8(d.Uint64()))
} else {
contract.Decimals = EtherAmountDecimalPoint
}
} else { } else {
contract.Decimals = EtherAmountDecimalPoint contract = nil
} }
cachedContractsMux.Lock() cachedContractsMux.Lock()
cachedContracts[cds] = contract cachedContracts[cds] = contract
cachedContractsMux.Unlock() cachedContractsMux.Unlock()
} }
return &contract, nil return contract, nil
} }
// EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address // EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address

View File

@ -163,10 +163,10 @@ func (ad AddressDescriptor) String() string {
// Erc20Contract contains info about ERC20 contract // Erc20Contract contains info about ERC20 contract
type Erc20Contract struct { type Erc20Contract struct {
Contract string Contract string `json:"contract"`
Name string Name string `json:"name"`
Symbol string Symbol string `json:"symbol"`
Decimals int Decimals int `json:"decimals"`
} }
// OnNewBlockFunc is used to send notification about a new block // OnNewBlockFunc is used to send notification about a new block

View File

@ -247,6 +247,7 @@ func (s *PublicServer) newTemplateData() *TemplateData {
CoinName: s.is.Coin, CoinName: s.is.Coin,
CoinShortcut: s.is.CoinShortcut, CoinShortcut: s.is.CoinShortcut,
CoinLabel: s.is.CoinLabel, CoinLabel: s.is.CoinLabel,
ChainType: s.chainParser.GetChainType(),
InternalExplorer: s.internalExplorer && !s.is.InitialSync, InternalExplorer: s.internalExplorer && !s.is.InitialSync,
TOSLink: api.Text.TOSLink, TOSLink: api.Text.TOSLink,
} }
@ -336,6 +337,7 @@ type TemplateData struct {
CoinShortcut string CoinShortcut string
CoinLabel string CoinLabel string
InternalExplorer bool InternalExplorer bool
ChainType bchain.ChainType
Address *api.Address Address *api.Address
AddrStr string AddrStr string
Tx *api.Tx Tx *api.Tx
@ -447,7 +449,7 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
if ec != nil { if ec != nil {
page = 0 page = 0
} }
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, false, false) address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, api.TxHistory)
if err != nil { if err != nil {
return errorTpl, nil, err return errorTpl, nil, err
} }
@ -531,7 +533,7 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t
http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302) http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302)
return noTpl, nil, nil return noTpl, nil, nil
} }
address, err = s.api.GetAddress(q, 0, 1, true, true) address, err = s.api.GetAddress(q, 0, 1, api.ExistOnly)
if err == nil { if err == nil {
http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302) http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
return noTpl, nil, nil return noTpl, nil, nil
@ -686,7 +688,7 @@ func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
if ec != nil { if ec != nil {
page = 0 page = 0
} }
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, true, false) address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.TxidHistory)
} }
return address, err return address, err
} }

View File

@ -214,6 +214,11 @@ h3 {
font-weight: bold; font-weight: bold;
} }
table.data-table table.data-table th {
border-top: 0;
font-weight: normal;
}
.alert .data-table { .alert .data-table {
margin: 0; margin: 0;
} }
@ -239,19 +244,7 @@ h3 {
font-size: 14px; font-size: 14px;
} }
.h-container { .h-container ul, .h-container h3 {
display: -webkit-box;
display: -ms-flexbox;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.h-container-6 {
width: 50%;
margin: 0;
}
.h-container ul {
margin: 0; margin: 0;
} }

View File

@ -1,5 +1,5 @@
{{define "specific"}}{{$cs := .CoinShortcut}}{{$addr := .Address}}{{$data := .}} {{define "specific"}}{{$cs := .CoinShortcut}}{{$addr := .Address}}{{$data := .}}
<h1>Address <h1>{{if $addr.Erc20Contract}}Contract {{$addr.Erc20Contract.Name}} ({{$addr.Erc20Contract.Symbol}}){{else}}Address{{end}}
<small class="text-muted">{{formatAmount $addr.Balance}} {{$cs}}</small> <small class="text-muted">{{formatAmount $addr.Balance}} {{$cs}}</small>
</h1> </h1>
<div class="alert alert-data ellipsis"> <div class="alert alert-data ellipsis">
@ -10,6 +10,40 @@
<div class="col-md-10"> <div class="col-md-10">
<table class="table data-table"> <table class="table data-table">
<tbody> <tbody>
{{- if eq .ChainType 1 -}}
<tr>
<td style="width: 25%;">Balance</td>
<td class="data">{{formatAmount $addr.Balance}} {{$cs}}</td>
</tr>
<tr>
<td>Non-contract Transactions</td>
<td class="data">{{$addr.TxApperances}}</td>
</tr>
{{- if $addr.Erc20Tokens -}}
<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%;">No. Txs</th>
</tr>
{{- range $et := $addr.Erc20Tokens -}}
<tr>
<td class="data ellipsis"><a href="/address/{{$et.Contract}}">{{$et.Name}}</a></td>
<td class="data">{{formatAmount $et.Balance}} {{$et.Symbol}}</td>
<td class="data">{{$et.Txs}}</td>
</tr>
{{- end -}}
</tbody>
</table>
</td>
</tr>
{{- end -}}
</tr>
{{- else -}}
<tr> <tr>
<td style="width: 25%;">Total Received</td> <td style="width: 25%;">Total Received</td>
<td class="data">{{formatAmount $addr.TotalReceived}} {{$cs}}</td> <td class="data">{{formatAmount $addr.TotalReceived}} {{$cs}}</td>
@ -26,6 +60,7 @@
<td>No. Transactions</td> <td>No. Transactions</td>
<td class="data">{{$addr.TxApperances}}</td> <td class="data">{{$addr.TxApperances}}</td>
</tr> </tr>
{{- end -}}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -54,9 +89,9 @@
</table> </table>
</div> </div>
{{- end}}{{if $addr.Transactions -}} {{- end}}{{if $addr.Transactions -}}
<div class="h-container"> <div class="row h-container">
<h3 class="h-container-6">Transactions</h3> <h3 class="col-md-6 col-sm-12">Transactions</h3>
<nav class="h-container-6">{{template "paging" $data}}</nav> <nav class="col-md-6 col-sm-12">{{template "paging" $data}}</nav>
</div> </div>
<div class="data-div"> <div class="data-div">
{{- range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}} {{- range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}}