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"
)
// 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
type APIError struct {
Text string
@ -125,16 +139,17 @@ type Paging struct {
// Address holds information about address and its transactions
type Address struct {
Paging
AddrStr string `json:"addrStr"`
Balance string `json:"balance"`
TotalReceived string `json:"totalReceived"`
TotalSent string `json:"totalSent"`
UnconfirmedBalance string `json:"unconfirmedBalance"`
UnconfirmedTxApperances int `json:"unconfirmedTxApperances"`
TxApperances int `json:"txApperances"`
Transactions []*Tx `json:"txs,omitempty"`
Txids []string `json:"transactions,omitempty"`
Erc20Tokens []*Erc20Token `json:"erc20tokens,omitempty"`
AddrStr string `json:"addrStr"`
Balance string `json:"balance"`
TotalReceived string `json:"totalReceived,omitempty"`
TotalSent string `json:"totalSent,omitempty"`
UnconfirmedBalance string `json:"unconfirmedBalance"`
UnconfirmedTxApperances int `json:"unconfirmedTxApperances"`
TxApperances int `json:"txApperances"`
Transactions []*Tx `json:"txs,omitempty"`
Txids []string `json:"transactions,omitempty"`
Erc20Contract *bchain.Erc20Contract `json:"erc20contract,omitempty"`
Erc20Tokens []Erc20Token `json:"erc20tokens,omitempty"`
}
// 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)
if err != nil {
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{
Contract: e.Contract,
@ -418,8 +420,67 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) {
}, 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
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()
page--
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)
}
var (
ba *db.AddrBalance
ca *db.AddrContracts
ba *db.AddrBalance
erc20t []Erc20Token
erc20c *bchain.Erc20Contract
)
if w.chainType == bchain.ChainEthereumType {
ca, err = w.db.GetAddrDescContracts(addrDesc)
ba, erc20t, erc20c, err = w.getEthereumTypeAddressBalances(addrDesc, option)
if err != nil {
return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
return nil, err
}
glog.Infof("%+v", ca)
} else {
// ba can be nil if the address is only in mempool!
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 existOnly && ba != nil {
if option == ExistOnly && ba != nil {
return &Address{AddrStr: address}, nil
}
// 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)
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v false", address)
return nil, errors.Annotatef(err, "getAddressTxids %v false", addrDesc)
}
txc = UniqueTxidsInReverse(txc)
var txm []string
@ -471,11 +532,11 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
}
txm, err = w.getAddressTxids(addrDesc, true)
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v true", address)
return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc)
}
txm = UniqueTxidsInReverse(txm)
// check if the address exist
if len(txc)+len(txm) == 0 {
if len(txc)+len(txm) == 0 || option == ExistOnly {
return &Address{
AddrStr: address,
}, 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)
var txs []*Tx
var txids []string
if onlyTxids {
if option == TxidHistory {
txids = make([]string, len(txm)+to-from)
} else {
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.Sub(&uBalSat, tx.getAddrVinValue(addrDesc))
if page == 0 {
if onlyTxids {
if option == TxidHistory {
txids[txi] = tx.Txid
} else {
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)
}
for i := from; i < to; i++ {
txid := txc[i]
if onlyTxids {
if option == TxidHistory {
txids[txi] = txid
} else {
if w.chainType == bchain.ChainEthereumType {
@ -548,7 +609,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
}
txi++
}
if onlyTxids {
if option == TxidHistory {
txids = txids[:txi]
} else {
txs = txs[:txi]
@ -564,6 +625,8 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids,
UnconfirmedTxApperances: len(txm),
Transactions: txs,
Txids: txids,
Erc20Contract: erc20c,
Erc20Tokens: erc20t,
}
glog.Info("GetAddress ", address, " finished in ", time.Since(start))
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
func AmountToDecimalString(a *big.Int, d int) string {
if a == nil {
return ""
}
n := a.String()
var s string
if n[0] == '-' {

View File

@ -43,7 +43,7 @@ type Erc20Transfer struct {
Tokens big.Int
}
var cachedContracts = make(map[string]bchain.Erc20Contract)
var cachedContracts = make(map[string]*bchain.Erc20Contract)
var cachedContractsMux sync.Mutex
func addressFromPaddedHex(s string) (string, error) {
@ -146,30 +146,38 @@ func (b *EthereumRPC) EthereumTypeGetErc20ContractInfo(contractDesc bchain.Addre
return nil, err
}
name := parseErc20StringProperty(contractDesc, data)
data, err = b.ethCall(erc20SymbolSignature, address)
if err != nil {
return nil, err
}
symbol := parseErc20StringProperty(contractDesc, data)
data, err = b.ethCall(erc20DecimalsSignature, address)
if err != nil {
return nil, err
}
contract = bchain.Erc20Contract{
Name: name,
Symbol: symbol,
}
d := parseErc20NumericProperty(contractDesc, data)
if d != nil {
contract.Decimals = int(uint8(d.Uint64()))
if name != "" {
data, err = b.ethCall(erc20SymbolSignature, address)
if err != nil {
return nil, err
}
symbol := parseErc20StringProperty(contractDesc, data)
data, err = b.ethCall(erc20DecimalsSignature, address)
if err != nil {
return nil, err
}
if name == "" {
name = address
}
contract = &bchain.Erc20Contract{
Contract: address,
Name: name,
Symbol: symbol,
}
d := parseErc20NumericProperty(contractDesc, data)
if d != nil {
contract.Decimals = int(uint8(d.Uint64()))
} else {
contract.Decimals = EtherAmountDecimalPoint
}
} else {
contract.Decimals = EtherAmountDecimalPoint
contract = nil
}
cachedContractsMux.Lock()
cachedContracts[cds] = contract
cachedContractsMux.Unlock()
}
return &contract, nil
return contract, nil
}
// 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
type Erc20Contract struct {
Contract string
Name string
Symbol string
Decimals int
Contract string `json:"contract"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
}
// 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,
CoinShortcut: s.is.CoinShortcut,
CoinLabel: s.is.CoinLabel,
ChainType: s.chainParser.GetChainType(),
InternalExplorer: s.internalExplorer && !s.is.InitialSync,
TOSLink: api.Text.TOSLink,
}
@ -336,6 +337,7 @@ type TemplateData struct {
CoinShortcut string
CoinLabel string
InternalExplorer bool
ChainType bchain.ChainType
Address *api.Address
AddrStr string
Tx *api.Tx
@ -447,7 +449,7 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
if ec != nil {
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 {
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)
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 {
http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
return noTpl, nil, nil
@ -686,7 +688,7 @@ func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
if ec != nil {
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
}

View File

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

View File

@ -1,5 +1,5 @@
{{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>
</h1>
<div class="alert alert-data ellipsis">
@ -10,6 +10,40 @@
<div class="col-md-10">
<table class="table data-table">
<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>
<td style="width: 25%;">Total Received</td>
<td class="data">{{formatAmount $addr.TotalReceived}} {{$cs}}</td>
@ -26,6 +60,7 @@
<td>No. Transactions</td>
<td class="data">{{$addr.TxApperances}}</td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
@ -54,9 +89,9 @@
</table>
</div>
{{- end}}{{if $addr.Transactions -}}
<div class="h-container">
<h3 class="h-container-6">Transactions</h3>
<nav class="h-container-6">{{template "paging" $data}}</nav>
<div class="row h-container">
<h3 class="col-md-6 col-sm-12">Transactions</h3>
<nav class="col-md-6 col-sm-12">{{template "paging" $data}}</nav>
</div>
<div class="data-div">
{{- range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}}