From 74ef087d4b9596a562865ff7cc414f8c996f7c27 Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Sat, 26 Mar 2022 19:16:35 +0100 Subject: [PATCH] Return token balances from API --- api/types.go | 32 ++++---- api/worker.go | 92 +++++++++++++++++----- server/public_ethereumtype_test.go | 2 +- tests/dbtestdata/fakechain_ethereumtype.go | 5 ++ 4 files changed, 94 insertions(+), 37 deletions(-) diff --git a/api/types.go b/api/types.go index 6a0bd165..dc6644d0 100644 --- a/api/types.go +++ b/api/types.go @@ -153,27 +153,29 @@ const ( // the map must match all bchain.TokenTransferTypes to avoid index out of range panic var TokenTypeMap []TokenType = []TokenType{ERC20TokenType, ERC771TokenType, ERC1155TokenType} -// Token contains info about tokens held by an address -type Token struct { - Type TokenType `json:"type"` - Name string `json:"name"` - Path string `json:"path,omitempty"` - Contract string `json:"contract,omitempty"` - Transfers int `json:"transfers"` - Symbol string `json:"symbol,omitempty"` - Decimals int `json:"decimals,omitempty"` - BalanceSat *Amount `json:"balance,omitempty"` - TotalReceivedSat *Amount `json:"totalReceived,omitempty"` - TotalSentSat *Amount `json:"totalSent,omitempty"` - ContractIndex string `json:"-"` -} - // TokenTransferValues contains values for ERC1155 contract type TokenTransferValues struct { Id *Amount `json:"id,omitempty"` Value *Amount `json:"value,omitempty"` } +// Token contains info about tokens held by an address +type Token struct { + Type TokenType `json:"type"` + Name string `json:"name"` + Path string `json:"path,omitempty"` + Contract string `json:"contract,omitempty"` + Transfers int `json:"transfers"` + Symbol string `json:"symbol,omitempty"` + Decimals int `json:"decimals,omitempty"` + BalanceSat *Amount `json:"balance,omitempty"` + Ids []Amount `json:"ids,omitempty"` // multiple ERC721 tokens + IdValues []TokenTransferValues `json:"idValues,omitempty"` // multiple ERC1155 tokens + TotalReceivedSat *Amount `json:"totalReceived,omitempty"` + TotalSentSat *Amount `json:"totalSent,omitempty"` + ContractIndex string `json:"-"` +} + // TokenTransfer contains info about a token transfer done in a transaction type TokenTransfer struct { Type TokenType `json:"type"` diff --git a/api/worker.go b/api/worker.go index ac9cdb90..c5f2e054 100644 --- a/api/worker.go +++ b/api/worker.go @@ -655,7 +655,68 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) { }, from, to, page } -func (w *Worker) getEthereumToken(index int, addrDesc, contract bchain.AddressDescriptor, details AccountDetails, txs int) (*Token, error) { +func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, index int, c *db.AddrContract, details AccountDetails) (*Token, error) { + // TODO use db.contracts + validContract := true + + ci, err := w.chain.EthereumTypeGetErc20ContractInfo(c.Contract) + if err != nil { + return 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] + } + validContract = false + } + + t := Token{ + Type: ERC20TokenType, + Contract: ci.Contract, + Name: ci.Name, + Symbol: ci.Symbol, + Transfers: int(c.Txs), + Decimals: ci.Decimals, + ContractIndex: strconv.Itoa(index), + } + // return contract balances/values only at or above AccountDetailsTokenBalances + if details >= AccountDetailsTokenBalances && validContract { + if c.Type == bchain.ERC20 { + // get Erc20 Contract Balance from blockchain, balance obtained from adding and subtracting transfers is not correct + b, err := w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract) + if err != nil { + // return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract) + glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, c.Contract, err) + } else { + t.BalanceSat = (*Amount)(b) + } + } else { + if len(t.Ids) > 0 { + ids := make([]Amount, len(t.Ids)) + for j := range ids { + ids[j] = (Amount)(c.Ids[j]) + } + t.Ids = ids + } + if len(t.IdValues) > 0 { + idValues := make([]TokenTransferValues, len(t.IdValues)) + for j := range idValues { + idValues[j].Id = (*Amount)(&c.IdValues[j].Id) + idValues[j].Value = (*Amount)(&c.IdValues[j].Value) + } + t.IdValues = idValues + } + } + } + + return &t, nil +} + +// a fallback method in case internal transactions are not processed and there is no indexed info about contract balance for an address +func (w *Worker) getEthereumContractBalanceFromBlockchain(addrDesc, contract bchain.AddressDescriptor, details AccountDetails) (*Token, error) { var b *big.Int validContract := true ci, err := w.chain.EthereumTypeGetErc20ContractInfo(contract) @@ -687,9 +748,9 @@ func (w *Worker) getEthereumToken(index int, addrDesc, contract bchain.AddressDe Contract: ci.Contract, Name: ci.Name, Symbol: ci.Symbol, - Transfers: txs, + Transfers: 0, Decimals: ci.Decimals, - ContractIndex: strconv.Itoa(index), + ContractIndex: "0", }, nil } @@ -733,7 +794,8 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto if details > AccountDetailsBasic { tokens = make([]Token, len(ca.Contracts)) var j int - for i, c := range ca.Contracts { + for i := range ca.Contracts { + c := &ca.Contracts[i] if len(filterDesc) > 0 { if !bytes.Equal(filterDesc, c.Contract) { continue @@ -741,26 +803,14 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto // filter only transactions of this contract filter.Vout = i + db.ContractIndexOffset } - t, err := w.getEthereumToken(i+db.ContractIndexOffset, addrDesc, c.Contract, details, int(c.Txs)) + t, err := w.getEthereumContractBalance(addrDesc, i+db.ContractIndexOffset, c, details) if err != nil { return nil, nil, nil, 0, 0, 0, 0, err } tokens[j] = *t j++ } - // special handling if filter has contract - // if the address has no transactions with given contract, check the balance, the address may have some balance even without transactions - if len(filterDesc) > 0 && j == 0 && details >= AccountDetailsTokens { - t, err := w.getEthereumToken(0, addrDesc, filterDesc, details, 0) - if err != nil { - return nil, nil, nil, 0, 0, 0, 0, err - } - tokens = []Token{*t} - // switch off query for transactions, there are no transactions - filter.Vout = AddressFilterVoutQueryNotNecessary - } else { - tokens = tokens[:j] - } + tokens = tokens[:j] } ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc) if err != nil { @@ -781,15 +831,15 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto nonContractTxs = int(ca.NonContractTxs) internalTxs = int(ca.InternalTxs) } else { - // addresses without any normal transactions can have internal transactions and therefore balance + // addresses without any normal transactions can have internal transactions that were not processed and therefore balance if b != nil { ba = &db.AddrBalance{ BalanceSat: *b, } } - // special handling if filtering for a contract, check the ballance of it + // special handling if filtering for a contract, check the ballance of it in the blockchain if len(filterDesc) > 0 && details >= AccountDetailsTokens { - t, err := w.getEthereumToken(0, addrDesc, filterDesc, details, 0) + t, err := w.getEthereumContractBalanceFromBlockchain(addrDesc, filterDesc, details) if err != nil { return nil, nil, nil, 0, 0, 0, 0, err } diff --git a/server/public_ethereumtype_test.go b/server/public_ethereumtype_test.go index 55ed9dc5..c6a02055 100644 --- a/server/public_ethereumtype_test.go +++ b/server/public_ethereumtype_test.go @@ -34,7 +34,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","balance":"123450075","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":1,"nonTokenTxs":1,"internalTxs":1,"txids":["0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2"],"nonce":"75","tokens":[{"type":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":2,"symbol":"S13","decimals":18},{"type":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":2,"symbol":"S74","decimals":18}],"erc20Contract":{"contract":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","name":"Contract 75","symbol":"S75","decimals":18}}`, + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","balance":"123450075","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":1,"nonTokenTxs":1,"internalTxs":1,"txids":["0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2"],"nonce":"75","tokens":[{"type":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":2,"symbol":"S13","decimals":18,"balance":"1000075013"},{"type":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":2,"symbol":"S74","decimals":18,"balance":"1000075074"}],"erc20Contract":{"contract":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","name":"Contract 75","symbol":"S75","decimals":18}}`, }, }, } diff --git a/tests/dbtestdata/fakechain_ethereumtype.go b/tests/dbtestdata/fakechain_ethereumtype.go index 800ff03f..b1927616 100644 --- a/tests/dbtestdata/fakechain_ethereumtype.go +++ b/tests/dbtestdata/fakechain_ethereumtype.go @@ -126,3 +126,8 @@ func (c *fakeBlockChainEthereumType) EthereumTypeGetErc20ContractInfo(contractDe Decimals: 18, }, nil } + +// EthereumTypeGetErc20ContractBalance is not supported +func (c *fakeBlockChainEthereumType) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) { + return big.NewInt(1000000000 + int64(addrDesc[0])*1000 + int64(contractDesc[0])), nil +}