From 72e0ac23bc203fee69840a291490668e94dc508f Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Mon, 24 Jan 2022 01:11:18 +0100 Subject: [PATCH] Return internal data and ERC721 and ERC1155 tokens from API and explorer --- api/types.go | 26 +++- api/worker.go | 64 ++++++-- bchain/coins/eth/ethrpc.go | 29 ++-- server/public.go | 12 ++ server/public_ethereumtype_test.go | 2 +- static/templates/address.html | 4 + static/templates/txdetail_ethereumtype.html | 158 +++++++++++++++++++- 7 files changed, 255 insertions(+), 40 deletions(-) diff --git a/api/types.go b/api/types.go index 3e7ffed5..6a0bd165 100644 --- a/api/types.go +++ b/api/types.go @@ -187,16 +187,25 @@ type TokenTransfer struct { Values []TokenTransferValues `json:"values,omitempty"` } +type EthereumInternalTransfer struct { + Type bchain.EthereumInternalTransactionType `json:"type"` + From string `json:"from"` + To string `json:"to"` + Value *Amount `json:"value"` +} + // EthereumSpecific contains ethereum specific transaction data type EthereumSpecific struct { - TxType string `json:"txType,omitempty"` - Status eth.TxStatus `json:"status"` // 1 OK, 0 Fail, -1 pending - Nonce uint64 `json:"nonce"` - GasLimit *big.Int `json:"gasLimit"` - GasUsed *big.Int `json:"gasUsed"` - GasPrice *Amount `json:"gasPrice"` - Data string `json:"data,omitempty"` - InternalTransfers []bchain.EthereumInternalTransfer `json:"internalTransfers,omitempty"` + Type bchain.EthereumInternalTransactionType `json:"type,omitempty"` + CreatedContract string `json:"createdContract,omitempty"` + Status eth.TxStatus `json:"status"` // 1 OK, 0 Fail, -1 pending + Error string `json:"error,omitempty"` + Nonce uint64 `json:"nonce"` + GasLimit *big.Int `json:"gasLimit"` + GasUsed *big.Int `json:"gasUsed"` + GasPrice *Amount `json:"gasPrice"` + Data string `json:"data,omitempty"` + InternalTransfers []EthereumInternalTransfer `json:"internalTransfers,omitempty"` } // Tx holds information about a transaction @@ -279,6 +288,7 @@ type Address struct { UnconfirmedTxs int `json:"unconfirmedTxs"` Txs int `json:"txs"` NonTokenTxs int `json:"nonTokenTxs,omitempty"` + InternalTxs int `json:"internalTxs,omitempty"` Transactions []*Tx `json:"transactions,omitempty"` Txids []string `json:"txids,omitempty"` Nonce string `json:"nonce,omitempty"` diff --git a/api/worker.go b/api/worker.go index 19583aa9..ac9cdb90 100644 --- a/api/worker.go +++ b/api/worker.go @@ -261,6 +261,15 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe } tokens = w.getEthereumTokensTransfers(tokenTransfers) ethTxData := eth.GetEthereumTxData(bchainTx) + + var internalData *bchain.EthereumInternalData + if eth.ProcessInternalTransactions { + internalData, err = w.db.GetEthereumInternalData(bchainTx.Txid) + if err != nil { + return nil, err + } + } + // mempool txs do not have fees yet if ethTxData.GasUsed != nil { feesSat.Mul(ethTxData.GasPrice, ethTxData.GasUsed) @@ -276,6 +285,21 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe Status: ethTxData.Status, Data: ethTxData.Data, } + if internalData != nil { + ethSpecific.Type = internalData.Type + ethSpecific.CreatedContract = internalData.Contract + ethSpecific.Error = internalData.Error + ethSpecific.InternalTransfers = make([]EthereumInternalTransfer, len(internalData.Transfers)) + for i := range internalData.Transfers { + f := &internalData.Transfers[i] + t := ðSpecific.InternalTransfers[i] + t.From = f.From + t.To = f.To + t.Type = f.Type + t.Value = (*Amount)(&f.Value) + } + } + } // for now do not return size, we would have to compute vsize of segwit transactions // size:=len(bchainTx.Hex) / 2 @@ -427,13 +451,25 @@ func (w *Worker) getEthereumTokensTransfers(transfers bchain.TokenTransfers) []T if erc20c == nil { erc20c = &bchain.Erc20Contract{Name: t.Contract} } + var value *Amount + var values []TokenTransferValues + if t.Type == bchain.ERC1155 { + values = make([]TokenTransferValues, len(t.IdValues)) + for j := range values { + values[j].Id = (*Amount)(&t.IdValues[j].Id) + values[j].Value = (*Amount)(&t.IdValues[j].Value) + } + } else { + value = (*Amount)(&t.Value) + } tokens[i] = TokenTransfer{ Type: TokenTypeMap[t.Type], Token: t.Contract, From: t.From, To: t.To, + Value: value, + Values: values, Decimals: erc20c.Decimals, - Value: (*Amount)(&t.Value), Name: erc20c.Name, Symbol: erc20c.Symbol, } @@ -657,29 +693,30 @@ func (w *Worker) getEthereumToken(index int, addrDesc, contract bchain.AddressDe }, nil } -func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.Erc20Contract, uint64, int, int, error) { +func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.Erc20Contract, uint64, int, int, int, error) { var ( ba *db.AddrBalance tokens []Token ci *bchain.Erc20Contract n uint64 nonContractTxs int + internalTxs int ) // unknown number of results for paging totalResults := -1 ca, err := w.db.GetAddrDescContracts(addrDesc) if err != nil { - return nil, nil, nil, 0, 0, 0, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) + return nil, nil, nil, 0, 0, 0, 0, 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, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc) + return nil, nil, nil, 0, 0, 0, 0, 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, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true) + return nil, nil, nil, 0, 0, 0, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true) } } if ca != nil { @@ -691,7 +728,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto } n, err = w.chain.EthereumTypeGetNonce(addrDesc) if err != nil { - return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc) + return nil, nil, nil, 0, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc) } if details > AccountDetailsBasic { tokens = make([]Token, len(ca.Contracts)) @@ -706,7 +743,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto } t, err := w.getEthereumToken(i+db.ContractIndexOffset, addrDesc, c.Contract, details, int(c.Txs)) if err != nil { - return nil, nil, nil, 0, 0, 0, err + return nil, nil, nil, 0, 0, 0, 0, err } tokens[j] = *t j++ @@ -716,7 +753,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto 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, err + return nil, nil, nil, 0, 0, 0, 0, err } tokens = []Token{*t} // switch off query for transactions, there are no transactions @@ -727,7 +764,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto } ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc) if err != nil { - return nil, nil, nil, 0, 0, 0, err + return nil, nil, nil, 0, 0, 0, 0, err } if filter.FromHeight == 0 && filter.ToHeight == 0 { // compute total results for paging @@ -742,6 +779,7 @@ 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 if b != nil { @@ -753,14 +791,14 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto if len(filterDesc) > 0 && details >= AccountDetailsTokens { t, err := w.getEthereumToken(0, addrDesc, filterDesc, details, 0) if err != nil { - return nil, nil, nil, 0, 0, 0, err + return nil, nil, nil, 0, 0, 0, 0, err } tokens = []Token{*t} // switch off query for transactions, there are no transactions filter.Vout = AddressFilterVoutQueryNotNecessary } } - return ba, tokens, ci, n, nonContractTxs, totalResults, nil + return ba, tokens, ci, n, nonContractTxs, internalTxs, totalResults, nil } func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetails, blockInfo *db.BlockInfo) (*Tx, error) { @@ -865,6 +903,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco nonce string unconfirmedTxs int nonTokenTxs int + internalTxs int totalResults int ) addrDesc, address, err := w.getAddrDescAndNormalizeAddress(address) @@ -873,7 +912,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco } if w.chainType == bchain.ChainEthereumType { var n uint64 - ba, tokens, erc20c, n, nonTokenTxs, totalResults, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter) + ba, tokens, erc20c, n, nonTokenTxs, internalTxs, totalResults, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter) if err != nil { return nil, err } @@ -977,6 +1016,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco TotalSentSat: (*Amount)(totalSent), Txs: int(ba.Txs), NonTokenTxs: nonTokenTxs, + InternalTxs: internalTxs, UnconfirmedBalanceSat: (*Amount)(&uBalSat), UnconfirmedTxs: unconfirmedTxs, Transactions: txs, diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index 1b6f2803..8c4630c2 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -36,14 +36,15 @@ const ( // Configuration represents json config file type Configuration struct { - CoinName string `json:"coin_name"` - CoinShortcut string `json:"coin_shortcut"` - RPCURL string `json:"rpc_url"` - RPCTimeout int `json:"rpc_timeout"` - BlockAddressesToKeep int `json:"block_addresses_to_keep"` - MempoolTxTimeoutHours int `json:"mempoolTxTimeoutHours"` - QueryBackendOnMempoolResync bool `json:"queryBackendOnMempoolResync"` - ProcessInternalTransactions bool `json:"processInternalTransactions"` + CoinName string `json:"coin_name"` + CoinShortcut string `json:"coin_shortcut"` + RPCURL string `json:"rpc_url"` + RPCTimeout int `json:"rpc_timeout"` + BlockAddressesToKeep int `json:"block_addresses_to_keep"` + MempoolTxTimeoutHours int `json:"mempoolTxTimeoutHours"` + QueryBackendOnMempoolResync bool `json:"queryBackendOnMempoolResync"` + ProcessInternalTransactions bool `json:"processInternalTransactions"` + ProcessZeroInternalTransactions bool `json:"processZeroInternalTransactions"` } // EthereumRPC is an interface to JSON-RPC eth service. @@ -65,6 +66,9 @@ type EthereumRPC struct { ChainConfig *Configuration } +// ProcessInternalTransactions specifies if internal transactions are processed +var ProcessInternalTransactions bool + // NewEthereumRPC returns new EthRPC instance. func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { var err error @@ -90,6 +94,8 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification ChainConfig: &c, } + ProcessInternalTransactions = c.ProcessInternalTransactions + // always create parser s.Parser = NewEthereumParser(c.BlockAddressesToKeep) s.timeout = time.Duration(c.RPCTimeout) * time.Second @@ -498,7 +504,7 @@ func (b *EthereumRPC) getTokenTransferEventsForBlock(blockNumber string) (map[st err := b.rpc.CallContext(ctx, &logs, "eth_getLogs", map[string]interface{}{ "fromBlock": blockNumber, "toBlock": blockNumber, - "topics": []string{tokenTransferEventSignature, tokenERC1155TransferSingleEventSignature, tokenERC1155TransferBatchEventSignature}, + // "topics": []string{tokenTransferEventSignature, tokenERC1155TransferSingleEventSignature, tokenERC1155TransferBatchEventSignature}, }) if err != nil { return nil, errors.Annotatef(err, "blockNumber %v", blockNumber) @@ -543,7 +549,7 @@ func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInt From: call.From, To: call.To, }) - } else if err == nil && value.BitLen() > 0 { + } else if err == nil && (value.BitLen() > 0 || b.ChainConfig.ProcessZeroInternalTransactions) { d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{ Value: *value, From: call.From, @@ -561,7 +567,7 @@ func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInt // getInternalDataForBlock fetches debug trace using callTracer, extracts internal transfers and creations and destructions of contracts func (b *EthereumRPC) getInternalDataForBlock(blockHash string, transactions []bchain.RpcTransaction) ([]bchain.EthereumInternalData, error) { data := make([]bchain.EthereumInternalData, len(transactions)) - if b.ChainConfig.ProcessInternalTransactions { + if ProcessInternalTransactions { ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() var trace []rpcTraceResult @@ -640,6 +646,7 @@ func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error internalData, err := b.getInternalDataForBlock(head.Hash, body.Transactions) if err != nil { blockSpecificData = &bchain.EthereumBlockSpecificData{InternalDataError: err.Error()} + glog.Info("InternalDataError ", bbh.Height, ": ", err.Error()) } btxs := make([]bchain.Tx, len(body.Transactions)) diff --git a/server/public.go b/server/public.go index 125c273a..e778306b 100644 --- a/server/public.go +++ b/server/public.go @@ -456,6 +456,7 @@ func (s *PublicServer) parseTemplates() []*template.Template { "setTxToTemplateData": setTxToTemplateData, "isOwnAddress": isOwnAddress, "toJSON": toJSON, + "tokenTransfersCount": tokenTransfersCount, } var createTemplate func(filenames ...string) *template.Template if s.debug { @@ -558,6 +559,17 @@ func isOwnAddress(td *TemplateData, a string) bool { return a == td.AddrStr } +// called from template, returns count of token transfers of given type +func tokenTransfersCount(tx *api.Tx, t api.TokenType) int { + count := 0 + for i := range tx.TokenTransfers { + if tx.TokenTransfers[i].Type == t { + count++ + } + } + return count +} + func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { var tx *api.Tx var err error diff --git a/server/public_ethereumtype_test.go b/server/public_ethereumtype_test.go index 4c9c4878..55ed9dc5 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,"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},{"type":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":2,"symbol":"S74","decimals":18}],"erc20Contract":{"contract":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","name":"Contract 75","symbol":"S75","decimals":18}}`, }, }, } diff --git a/static/templates/address.html b/static/templates/address.html index 208a2b44..b5b67d3d 100644 --- a/static/templates/address.html +++ b/static/templates/address.html @@ -22,6 +22,10 @@ Non-contract Transactions {{$addr.NonTokenTxs}} + + Internal Transactions + {{$addr.InternalTxs}} + Nonce {{$addr.Nonce}} diff --git a/static/templates/txdetail_ethereumtype.html b/static/templates/txdetail_ethereumtype.html index c8c9f8bf..36aed548 100644 --- a/static/templates/txdetail_ethereumtype.html +++ b/static/templates/txdetail_ethereumtype.html @@ -6,6 +6,7 @@ {{if eq $tx.EthereumSpecific.Status 1}}{{end}}{{if eq $tx.EthereumSpecific.Status 0}}{{end}} {{- if $tx.Blocktime}}
{{if $tx.Confirmations}}mined{{else}}first seen{{end}} {{formatUnixTime $tx.Blocktime}}
{{end -}} + {{if $tx.EthereumSpecific.Error}}
Error: {{$tx.EthereumSpecific.Error}}
{{end}}
@@ -67,19 +68,20 @@ {{formatAmount $tx.ValueOutSat}} {{$cs}}
- {{- if $tx.TokenTransfers -}} + + {{if $tx.EthereumSpecific.InternalTransfers}}
- ERC20 Token Transfers + Internal Transactions
- {{- range $erc20 := $tx.TokenTransfers -}} + {{- range $tt := $tx.EthereumSpecific.InternalTransfers -}}
- + @@ -95,20 +97,160 @@
- {{if ne $erc20.From $addr}}{{$erc20.From}}{{else}}{{$erc20.From}}{{end}} + {{if ne $tt.From $addr}}{{$tt.From}}{{else}}{{$tt.From}}{{end}}
- +
- {{if ne $erc20.To $addr}}{{$erc20.To}}{{else}}{{$erc20.To}}{{end}} + {{if ne $tt.To $addr}}{{$tt.To}}{{else}}{{$tt.To}}{{end}}
-
{{formatAmountWithDecimals $erc20.Value $erc20.Decimals}} {{$erc20.Symbol}}
+
{{formatAmount $tt.Value}} {{$cs}}
{{- end -}}
{{- end -}} + + {{- if tokenTransfersCount $tx "ERC20" -}} +
+ ERC20 Token Transfers +
+ {{- range $tt := $tx.TokenTransfers -}} + {{if eq $tt.Type "ERC20"}} +
+
+
+ + + + + + +
+ {{if ne $tt.From $addr}}{{$tt.From}}{{else}}{{$tt.From}}{{end}} +
+
+
+
+ + + +
+
+
+ + + + + + +
+ {{if ne $tt.To $addr}}{{$tt.To}}{{else}}{{$tt.To}}{{end}} +
+
+
+
{{formatAmountWithDecimals $tt.Value $tt.Decimals}} {{$tt.Symbol}}
+
+ {{- end -}} + {{- end -}} +
+ {{- end -}} + + {{- if tokenTransfersCount $tx "ERC721" -}} +
+ ERC721 Token Transfers +
+ {{- range $tt := $tx.TokenTransfers -}} + {{if eq $tt.Type "ERC721"}} +
+
+
+ + + + + + +
+ {{if ne $tt.From $addr}}{{$tt.From}}{{else}}{{$tt.From}}{{end}} +
+
+
+
+ + + +
+
+
+ + + + + + +
+ {{if ne $tt.To $addr}}{{$tt.To}}{{else}}{{$tt.To}}{{end}} +
+
+
+
ID {{formatAmountWithDecimals $tt.Value 0}} {{$tt.Symbol}}
+
+ {{- end -}} + {{- end -}} +
+ {{- end -}} + + {{- if tokenTransfersCount $tx "ERC1155" -}} +
+ ERC1155 Token Transfers +
+ {{- range $tt := $tx.TokenTransfers -}} + {{if eq $tt.Type "ERC1155"}} +
+
+
+ + + + + + +
+ {{if ne $tt.From $addr}}{{$tt.From}}{{else}}{{$tt.From}}{{end}} +
+
+
+
+ + + +
+
+
+ + + + + + +
+ {{if ne $tt.To $addr}}{{$tt.To}}{{else}}{{$tt.To}}{{end}} +
+
+
+
+ {{- range $iv := $tt.Values -}} + {{formatAmountWithDecimals $iv.Id 0}}:{{formatAmountWithDecimals $iv.Value $tt.Decimals}} {{$tt.Symbol}} + {{- end -}} +
+
+ {{- end -}} + {{- end -}} +
+ {{- end -}} +
{{- if $tx.FeesSat -}}