Show ERC20 contracts for address
This commit is contained in:
parent
c96c357013
commit
fead52881f
35
api/types.go
35
api/types.go
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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] == '-' {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 -}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user