From 67bcf1046600dccf834d1309cead9bab6c806ce2 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Tue, 28 Feb 2023 00:09:40 -0300 Subject: [PATCH] feat(Wallet): handle erc721 transfers --- services/wallet/transfer/downloader.go | 23 +++++--- services/wallet/transfer/view.go | 72 +++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/services/wallet/transfer/downloader.go b/services/wallet/transfer/downloader.go index 46fbd10ad..6b7b300bc 100644 --- a/services/wallet/transfer/downloader.go +++ b/services/wallet/transfer/downloader.go @@ -21,10 +21,15 @@ type Type string type MultiTransactionIDType int64 const ( - ethTransfer Type = "eth" - erc20Transfer Type = "erc20" + ethTransfer Type = "eth" + erc20Transfer Type = "erc20" + erc721Transfer Type = "erc721" + unknownTokenTransfer Type = "unknown" - erc20TransferEventSignature = "Transfer(address,address,uint256)" + erc20_721TransferEventSignature = "Transfer(address,address,uint256)" + + erc20TransferEventIndexedParameters = 3 // signature, from, to + erc721TransferEventIndexedParameters = 4 // signature, from, to, tokenId NoMultiTransactionID = MultiTransactionIDType(0) ) @@ -208,7 +213,7 @@ func (d *ETHDownloader) getTransfersInBlock(ctx context.Context, blk *types.Bloc // NewERC20TransfersDownloader returns new instance. func NewERC20TransfersDownloader(client *chain.ClientWithFallback, accounts []common.Address, signer types.Signer) *ERC20TransfersDownloader { - signature := crypto.Keccak256Hash([]byte(erc20TransferEventSignature)) + signature := crypto.Keccak256Hash([]byte(erc20_721TransferEventSignature)) return &ERC20TransfersDownloader{ client: client, accounts: accounts, @@ -217,7 +222,11 @@ func NewERC20TransfersDownloader(client *chain.ClientWithFallback, accounts []co } } -// ERC20TransfersDownloader is a downloader for erc20 tokens transfers. +// ERC20TransfersDownloader is a downloader for erc20 and erc721 tokens transfers. +// Since both transaction types share the same signature, both will be assigned +// type erc20Transfer. Until the downloader gets refactored and a migration of the +// database gets implemented, differentiation between erc20 and erc721 will handled +// in the controller. type ERC20TransfersDownloader struct { client *chain.ClientWithFallback accounts []common.Address @@ -478,7 +487,7 @@ func (d *ERC20TransfersDownloader) GetHeadersInRange(parent context.Context, fro } func IsTokenTransfer(logs []*types.Log) bool { - signature := crypto.Keccak256Hash([]byte(erc20TransferEventSignature)) + signature := crypto.Keccak256Hash([]byte(erc20_721TransferEventSignature)) for _, l := range logs { if len(l.Topics) > 0 && l.Topics[0] == signature { return true @@ -488,7 +497,7 @@ func IsTokenTransfer(logs []*types.Log) bool { } func getTokenLog(logs []*types.Log) *types.Log { - signature := crypto.Keccak256Hash([]byte(erc20TransferEventSignature)) + signature := crypto.Keccak256Hash([]byte(erc20_721TransferEventSignature)) for _, l := range logs { if len(l.Topics) > 0 && l.Topics[0] == signature { return l diff --git a/services/wallet/transfer/view.go b/services/wallet/transfer/view.go index 438edea9c..9e6d842b2 100644 --- a/services/wallet/transfer/view.go +++ b/services/wallet/transfer/view.go @@ -29,7 +29,8 @@ type View struct { TxStatus hexutil.Uint64 `json:"txStatus"` Input hexutil.Bytes `json:"input"` TxHash common.Hash `json:"txHash"` - Value *hexutil.Big `json:"value"` + Value *hexutil.Big `json:"value"` // Only used for Type ethTransfer and erc20Transfer + TokenID *hexutil.Big `json:"tokenId"` // Only used for Type erc721Transfer From common.Address `json:"from"` To common.Address `json:"to"` Contract common.Address `json:"contract"` @@ -49,7 +50,7 @@ func castToTransferViews(transfers []Transfer) []View { func CastToTransferView(t Transfer) View { view := View{} view.ID = t.ID - view.Type = t.Type + view.Type = getFixedTransferType(t) view.Address = t.Address view.BlockNumber = (*hexutil.Big)(t.BlockNumber) view.BlockHash = t.BlockHash @@ -74,25 +75,54 @@ func CastToTransferView(t Transfer) View { view.Input = hexutil.Bytes(t.Transaction.Data()) view.TxHash = t.Transaction.Hash() view.NetworkID = t.NetworkID - switch t.Type { + + value := new(hexutil.Big) + tokenId := new(hexutil.Big) + + switch view.Type { case ethTransfer: view.From = t.From if t.Transaction.To() != nil { view.To = *t.Transaction.To() } - view.Value = (*hexutil.Big)(t.Transaction.Value()) + value = (*hexutil.Big)(t.Transaction.Value()) view.Contract = t.Receipt.ContractAddress case erc20Transfer: view.Contract = t.Log.Address - from, to, amount := parseLog(t.Log) - view.From, view.To, view.Value = from, to, (*hexutil.Big)(amount) + from, to, valueInt := parseErc20Log(t.Log) + view.From, view.To, value = from, to, (*hexutil.Big)(valueInt) + case erc721Transfer: + view.Contract = t.Log.Address + from, to, tokenIdInt := parseErc721Log(t.Log) + view.From, view.To, tokenId = from, to, (*hexutil.Big)(tokenIdInt) } view.MultiTransactionID = int64(t.MultiTransactionID) + view.Value = value + view.TokenID = tokenId + return view } -func parseLog(ethlog *types.Log) (from, to common.Address, amount *big.Int) { +func getFixedTransferType(tx Transfer) Type { + // erc721 transfers share signature with erc20 ones, so they are both (cached and new) + // categorized as erc20 by the Downloader. We fix this on the fly for the moment, until + // the Downloader gets refactored. + if tx.Type == erc20Transfer { + switch len(tx.Log.Topics) { + case erc20TransferEventIndexedParameters: + // do nothing + case erc721TransferEventIndexedParameters: + return erc721Transfer + default: + return unknownTokenTransfer + } + } + return tx.Type +} + +func parseErc20Log(ethlog *types.Log) (from, to common.Address, amount *big.Int) { + amount = new(big.Int) if len(ethlog.Topics) < 3 { log.Warn("not enough topics for erc20 transfer", "topics", ethlog.Topics) return @@ -111,6 +141,32 @@ func parseLog(ethlog *types.Log) (from, to common.Address, amount *big.Int) { log.Warn("data is not padded to 32 byts big int", "data", ethlog.Data) return } - amount = new(big.Int).SetBytes(ethlog.Data) + amount.SetBytes(ethlog.Data) + + return +} + +func parseErc721Log(ethlog *types.Log) (from, to common.Address, tokenId *big.Int) { + tokenId = new(big.Int) + if len(ethlog.Topics) < 4 { + log.Warn("not enough topics for erc721 transfer", "topics", ethlog.Topics) + return + } + if len(ethlog.Topics[1]) != 32 { + log.Warn("second topic is not padded to 32 byte address", "topic", ethlog.Topics[1]) + return + } + if len(ethlog.Topics[2]) != 32 { + log.Warn("third topic is not padded to 32 byte address", "topic", ethlog.Topics[2]) + return + } + if len(ethlog.Topics[3]) != 32 { + log.Warn("fourth topic is not 32 byte tokenId", "topic", ethlog.Topics[3]) + return + } + copy(from[:], ethlog.Topics[1][12:]) + copy(to[:], ethlog.Topics[2][12:]) + tokenId.SetBytes(ethlog.Topics[3][:]) + return }