From 63282cb32152dbf7828fdef02d80362f48f32170 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 10:40:27 -0500 Subject: [PATCH 01/11] receipts: add new FetchReceiptTokenTransfers method to fetch and decode logs --- receipts/token_transfers.go | 99 ++++++++++++++++++++++++++++++++ receipts/token_transfers_test.go | 27 +++++++++ 2 files changed, 126 insertions(+) create mode 100644 receipts/token_transfers.go create mode 100644 receipts/token_transfers_test.go diff --git a/receipts/token_transfers.go b/receipts/token_transfers.go new file mode 100644 index 00000000..9e6b1c89 --- /dev/null +++ b/receipts/token_transfers.go @@ -0,0 +1,99 @@ +package receipts + +import ( + "context" + "math/big" + + "github.com/0xsequence/ethkit/ethrpc" + "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/ethkit/go-ethereum/core/types" + "github.com/0xsequence/go-sequence/contracts/gen/tokens" +) + +// FetchReceiptTokenTransfers fetches the transaction receipt for the given transaction hash +// and decodes any token transfer events (ERC20) that occurred within that transaction. TODOXXX: we +// currently only support ERC20 token transfers, but we can extend this to support ERC721 and ERC1155 as well. +func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, transactionHash common.Hash) (*types.Receipt, TokenTransfers, error) { + receipt, err := provider.TransactionReceipt(ctx, transactionHash) + if err != nil { + return nil, nil, err + } + if receipt == nil { + return nil, nil, nil + } + + transferTopic := common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") + polLogTransferTopic := common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4") + + var decoded []*TokenTransfer + + for _, log := range receipt.Logs { + if len(log.Topics) == 0 { + continue + } + if log.Topics[0] != transferTopic && log.Topics[0] != polLogTransferTopic { + continue + } + + if log.Topics[0] == transferTopic { + filterer, err := tokens.NewIERC20Filterer(log.Address, provider) + if err == nil { + if ev, err := filterer.ParseTransfer(*log); err == nil && ev != nil { + decoded = append(decoded, &TokenTransfer{From: ev.From, To: ev.To, Value: ev.Value, Raw: *log}) + continue + } + } + } + + // TODO: need to try all of the various versions of this.. and we may as well support ERC721 and ERC1155 too + // note: "indexed" args, etc. + + if len(log.Topics) >= 3 { + from := common.BytesToAddress(log.Topics[1].Bytes()) + to := common.BytesToAddress(log.Topics[2].Bytes()) + value := new(big.Int).SetBytes(log.Data) + decoded = append(decoded, &TokenTransfer{From: from, To: to, Value: value, Raw: *log}) + } + } + + return receipt, decoded, nil +} + +type TokenTransfer struct { + From common.Address + To common.Address + Value *big.Int + Raw types.Log +} + +type TokenTransfers []*TokenTransfer + +func (t TokenTransfers) FilterTokenTransfersByContractAddress(ctx context.Context, contract common.Address) TokenTransfers { + var out TokenTransfers + for _, transfer := range t { + if transfer.Raw.Address == contract { + out = append(out, transfer) + } + } + return out +} + +func (t TokenTransfers) FilterTokenTransfersByFromAddress(ctx context.Context, from common.Address) TokenTransfers { + var out TokenTransfers + for _, transfer := range t { + if transfer.From == from { + out = append(out, transfer) + } + } + return out +} + +func (t TokenTransfers) FilterTokenTransfersByToAddress(ctx context.Context, to common.Address) TokenTransfers { + var out TokenTransfers + for _, transfer := range t { + if transfer.To == to { + out = append(out, transfer) + } + } + return out +} diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go new file mode 100644 index 00000000..b24a0419 --- /dev/null +++ b/receipts/token_transfers_test.go @@ -0,0 +1,27 @@ +package receipts_test + +import ( + "context" + "testing" + + "github.com/0xsequence/ethkit/ethrpc" + "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/go-sequence/receipts" + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +func TestFetchReceiptTokenTransfers(t *testing.T) { + // txnHash := https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon") + require.NoError(t, err) + + txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5") + + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + + spew.Dump(transfers) +} From 35de40cb950121a643666afad2a2abd7cc926a61 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 11:50:19 -0500 Subject: [PATCH 02/11] update --- receipts/token_transfers.go | 21 ++++++++++++--- receipts/token_transfers_test.go | 46 +++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/receipts/token_transfers.go b/receipts/token_transfers.go index 9e6b1c89..1de4b58c 100644 --- a/receipts/token_transfers.go +++ b/receipts/token_transfers.go @@ -10,6 +10,11 @@ import ( "github.com/0xsequence/go-sequence/contracts/gen/tokens" ) +var tokenTransferTopicHashes = []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), // ERC20 Transfer + common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4"), // Polygon POL LogTransfer (custom) +} + // FetchReceiptTokenTransfers fetches the transaction receipt for the given transaction hash // and decodes any token transfer events (ERC20) that occurred within that transaction. TODOXXX: we // currently only support ERC20 token transfers, but we can extend this to support ERC721 and ERC1155 as well. @@ -68,7 +73,7 @@ type TokenTransfer struct { type TokenTransfers []*TokenTransfer -func (t TokenTransfers) FilterTokenTransfersByContractAddress(ctx context.Context, contract common.Address) TokenTransfers { +func (t TokenTransfers) FilterByContractAddress(ctx context.Context, contract common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { if transfer.Raw.Address == contract { @@ -78,7 +83,17 @@ func (t TokenTransfers) FilterTokenTransfersByContractAddress(ctx context.Contex return out } -func (t TokenTransfers) FilterTokenTransfersByFromAddress(ctx context.Context, from common.Address) TokenTransfers { +func (t TokenTransfers) FilterByAccountAddress(ctx context.Context, account common.Address) TokenTransfers { + var out TokenTransfers + for _, transfer := range t { + if transfer.From == account || transfer.To == account { + out = append(out, transfer) + } + } + return out +} + +func (t TokenTransfers) FilterByFromAddress(ctx context.Context, from common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { if transfer.From == from { @@ -88,7 +103,7 @@ func (t TokenTransfers) FilterTokenTransfersByFromAddress(ctx context.Context, f return out } -func (t TokenTransfers) FilterTokenTransfersByToAddress(ctx context.Context, to common.Address) TokenTransfers { +func (t TokenTransfers) FilterByToAddress(ctx context.Context, to common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { if transfer.To == to { diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index b24a0419..2cb82a62 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -12,16 +12,44 @@ import ( ) func TestFetchReceiptTokenTransfers(t *testing.T) { - // txnHash := https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 - provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon") - require.NoError(t, err) - txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5") + t.Run("ERC20 Transfer on Arbitrum", func(t *testing.T) { + // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") + require.NoError(t, err) - receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) - require.NoError(t, err) - require.NotNil(t, receipt) - require.Greater(t, len(transfers), 0) + txnHash := common.HexToHash("0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9") - spew.Dump(transfers) + // TODO: lets find a very simple metamask erc20 transfer, and check it in the test + // TODO2: find a batch of different erc20 transfers to test against.. for example + // its possible there can be multiple erc20 transfers in a single tx that come and go from an individual + // so what we want to see is the delta, the diff, etc. .. aka, the "Result" .. aka... "TokenTransferResult" + // and not just all of the TokenTransferLogs .. + // TODO3: vault bridge USDC .. lets check the token transfer event, prob just erc20 too + + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + + spew.Dump(transfers) + + }) + + t.Run("Polygon POL LogTransfer", func(t *testing.T) { + t.Skip("POL") + + // txnHash := https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon") + require.NoError(t, err) + + txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5") + + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + + spew.Dump(transfers) + }) } From 6175c85322d92a3f9220a4bc1100136dd9c02cf3 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 12:57:50 -0500 Subject: [PATCH 03/11] update --- receipts/token_transfers_test.go | 79 ++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index 2cb82a62..c69962b7 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -13,38 +13,34 @@ import ( func TestFetchReceiptTokenTransfers(t *testing.T) { - t.Run("ERC20 Transfer on Arbitrum", func(t *testing.T) { - // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 - provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") - require.NoError(t, err) + // TODO: lets find a very simple metamask erc20 transfer, and check it in the test + // https://arbiscan.io/tx/0x6753a97203d159702e594662729b06608cf3b9c99c0cce177b9d7b66e6456150 - txnHash := common.HexToHash("0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9") + // TODO2: a txn with a bunch of different erc20 transfers inside of it, ie. batch send + // this is a sequence v2 send of usdc and magic on arbitrum.. batch send + // https://arbiscan.io/tx/0x65c70290232207a21ef6805ae50622def8d52b7a8f381e1c3eac24d5423e0657 - // TODO: lets find a very simple metamask erc20 transfer, and check it in the test - // TODO2: find a batch of different erc20 transfers to test against.. for example - // its possible there can be multiple erc20 transfers in a single tx that come and go from an individual - // so what we want to see is the delta, the diff, etc. .. aka, the "Result" .. aka... "TokenTransferResult" - // and not just all of the TokenTransferLogs .. - // TODO3: vault bridge USDC .. lets check the token transfer event, prob just erc20 too + // TODO3: a trails intent call, with bunch of other actions inside of the txn, including erc20 transfers + // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 + // .. lets get another one using zerox + cctp for example - receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) - require.NoError(t, err) - require.NotNil(t, receipt) - require.Greater(t, len(transfers), 0) + // TODO4: vault bridge USDC .. lets check the token transfer event, prob just erc20 too + // https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52 - spew.Dump(transfers) - - }) + // TODO5: polygon POL LogTransfer event + // https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 - t.Run("Polygon POL LogTransfer", func(t *testing.T) { - t.Skip("POL") + // TODO6: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call + // and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ? + // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 - // txnHash := https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 - provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon") + // Test 1: Simple ERC20 Transfer via EOA, which shows just a simple transfer event + // https://arbiscan.io/tx/0x6753a97203d159702e594662729b06608cf3b9c99c0cce177b9d7b66e6456150 + t.Run("Simple ERC20 Transfer via EOA", func(t *testing.T) { + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") require.NoError(t, err) - txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5") - + txnHash := common.HexToHash("0x6753a97203d159702e594662729b06608cf3b9c99c0cce177b9d7b66e6456150") receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) require.NoError(t, err) require.NotNil(t, receipt) @@ -52,4 +48,39 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { spew.Dump(transfers) }) + + //-- + + // t.Run("Simple ERC20 Transfer", func(t *testing.T) { + // // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 + // provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") + // require.NoError(t, err) + + // txnHash := common.HexToHash("0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9") + + // receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + // require.NoError(t, err) + // require.NotNil(t, receipt) + // require.Greater(t, len(transfers), 0) + + // spew.Dump(transfers) + + // }) + + // t.Run("Polygon POL LogTransfer", func(t *testing.T) { + // t.Skip("POL") + + // // txnHash := https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 + // provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon") + // require.NoError(t, err) + + // txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5") + + // receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + // require.NoError(t, err) + // require.NotNil(t, receipt) + // require.Greater(t, len(transfers), 0) + + // spew.Dump(transfers) + // }) } From 7414031f7b5168ff2dd73e1d860b27bcbb416871 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 13:16:19 -0500 Subject: [PATCH 04/11] update --- receipts/token_transfers.go | 10 +++++++--- receipts/token_transfers_test.go | 30 +++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/receipts/token_transfers.go b/receipts/token_transfers.go index 1de4b58c..1ede3656 100644 --- a/receipts/token_transfers.go +++ b/receipts/token_transfers.go @@ -40,11 +40,13 @@ func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, continue } + tokenAddress := log.Address + if log.Topics[0] == transferTopic { filterer, err := tokens.NewIERC20Filterer(log.Address, provider) if err == nil { if ev, err := filterer.ParseTransfer(*log); err == nil && ev != nil { - decoded = append(decoded, &TokenTransfer{From: ev.From, To: ev.To, Value: ev.Value, Raw: *log}) + decoded = append(decoded, &TokenTransfer{Token: tokenAddress, From: ev.From, To: ev.To, Value: ev.Value, Raw: *log}) continue } } @@ -53,11 +55,12 @@ func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, // TODO: need to try all of the various versions of this.. and we may as well support ERC721 and ERC1155 too // note: "indexed" args, etc. + // TODO: this is wrong, etc. if len(log.Topics) >= 3 { from := common.BytesToAddress(log.Topics[1].Bytes()) to := common.BytesToAddress(log.Topics[2].Bytes()) value := new(big.Int).SetBytes(log.Data) - decoded = append(decoded, &TokenTransfer{From: from, To: to, Value: value, Raw: *log}) + decoded = append(decoded, &TokenTransfer{Token: tokenAddress, From: from, To: to, Value: value, Raw: *log}) } } @@ -65,9 +68,10 @@ func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, } type TokenTransfer struct { + Token common.Address From common.Address To common.Address - Value *big.Int + Value *big.Int // TODO: check the erc20 log spec to see if we should call this value or amount Raw types.Log } diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index c69962b7..c8b2244f 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -2,6 +2,7 @@ package receipts_test import ( "context" + "math/big" "testing" "github.com/0xsequence/ethkit/ethrpc" @@ -13,9 +14,6 @@ import ( func TestFetchReceiptTokenTransfers(t *testing.T) { - // TODO: lets find a very simple metamask erc20 transfer, and check it in the test - // https://arbiscan.io/tx/0x6753a97203d159702e594662729b06608cf3b9c99c0cce177b9d7b66e6456150 - // TODO2: a txn with a bunch of different erc20 transfers inside of it, ie. batch send // this is a sequence v2 send of usdc and magic on arbitrum.. batch send // https://arbiscan.io/tx/0x65c70290232207a21ef6805ae50622def8d52b7a8f381e1c3eac24d5423e0657 @@ -34,9 +32,9 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ? // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 - // Test 1: Simple ERC20 Transfer via EOA, which shows just a simple transfer event + // Case 1: Simple ERC20 Transfer via EOA, which shows just a simple transfer event // https://arbiscan.io/tx/0x6753a97203d159702e594662729b06608cf3b9c99c0cce177b9d7b66e6456150 - t.Run("Simple ERC20 Transfer via EOA", func(t *testing.T) { + t.Run("Case 1: Simple ERC20 Transfer via EOA", func(t *testing.T) { provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") require.NoError(t, err) @@ -46,7 +44,29 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { require.NotNil(t, receipt) require.Greater(t, len(transfers), 0) + // spew.Dump(transfers) + require.Equal(t, 1, len(transfers)) + require.Equal(t, common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), transfers[0].Token) // USDC + require.Equal(t, common.HexToAddress("0x1D17C0F90A0b3dFb5124C2FF56B33a0D2E202e1d"), transfers[0].From) + require.Equal(t, common.HexToAddress("0x5646E2424A7b7d43740EF14bc5b4f1e00Bf9B6Ba"), transfers[0].To) + require.Equal(t, big.NewInt(184840), transfers[0].Value) + }) + + // Case 2: a txn with a bunch of different erc20 transfers inside of it, ie. batch send + // this is a sequence v2 send of usdc and magic on arbitrum.. batch send + // https://arbiscan.io/tx/0x65c70290232207a21ef6805ae50622def8d52b7a8f381e1c3eac24d5423e0657 + t.Run("Case 2: Batch of ERC20 Transfers via SCW", func(t *testing.T) { + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") + require.NoError(t, err) + + txnHash := common.HexToHash("0x65c70290232207a21ef6805ae50622def8d52b7a8f381e1c3eac24d5423e0657") + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + spew.Dump(transfers) + require.Equal(t, 2, len(transfers)) }) //-- From 1fd2c10f0ae1c1bd99bf60b9eb31835856bee378 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 13:27:40 -0500 Subject: [PATCH 05/11] update --- receipts/token_transfers_test.go | 53 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index c8b2244f..9bb1bfa3 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -13,25 +13,6 @@ import ( ) func TestFetchReceiptTokenTransfers(t *testing.T) { - - // TODO2: a txn with a bunch of different erc20 transfers inside of it, ie. batch send - // this is a sequence v2 send of usdc and magic on arbitrum.. batch send - // https://arbiscan.io/tx/0x65c70290232207a21ef6805ae50622def8d52b7a8f381e1c3eac24d5423e0657 - - // TODO3: a trails intent call, with bunch of other actions inside of the txn, including erc20 transfers - // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 - // .. lets get another one using zerox + cctp for example - - // TODO4: vault bridge USDC .. lets check the token transfer event, prob just erc20 too - // https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52 - - // TODO5: polygon POL LogTransfer event - // https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 - - // TODO6: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call - // and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ? - // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 - // Case 1: Simple ERC20 Transfer via EOA, which shows just a simple transfer event // https://arbiscan.io/tx/0x6753a97203d159702e594662729b06608cf3b9c99c0cce177b9d7b66e6456150 t.Run("Case 1: Simple ERC20 Transfer via EOA", func(t *testing.T) { @@ -67,6 +48,40 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { spew.Dump(transfers) require.Equal(t, 2, len(transfers)) + + // USDC + require.Equal(t, common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), transfers[0].Token) + require.Equal(t, common.HexToAddress("0x8e3E38fe7367dd3b52D1e281E4e8400447C8d8B9"), transfers[0].From) + require.Equal(t, common.HexToAddress("0x9b1A542f3C455E8d6057C3478EB945B48D8e17fF"), transfers[0].To) + require.Equal(t, big.NewInt(100000), transfers[0].Value) + + // MAGIC + require.Equal(t, common.HexToAddress("0x539bdE0d7Dbd336b79148AA742883198BBF60342"), transfers[1].Token) + require.Equal(t, common.HexToAddress("0x8e3E38fe7367dd3b52D1e281E4e8400447C8d8B9"), transfers[1].From) + require.Equal(t, common.HexToAddress("0x9b1A542f3C455E8d6057C3478EB945B48D8e17fF"), transfers[1].To) + require.Equal(t, big.NewInt(200000000000000000), transfers[1].Value) + }) + + // Case 3: a trails intent call, with bunch of other actions inside of the txn, including erc20 transfers + // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 + // .. lets get another one using zerox + cctp for example + t.Run("Case 3: ..", func(t *testing.T) { + }) + + // Case 4: vault bridge USDC .. lets check the token transfer event, prob just erc20 too + // https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52 + t.Run("Case 4: ..", func(t *testing.T) { + }) + + // Case 5: polygon POL LogTransfer event + // https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 + t.Run("Case 5: ..", func(t *testing.T) { + }) + + // Case 6: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call + // and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ? + // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 + t.Run("Case 6: ..", func(t *testing.T) { }) //-- From c0009baca2a8cec0dd51cd78b5ff64bb2e39cdbb Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 14:50:12 -0500 Subject: [PATCH 06/11] update --- receipts/token_transfers.go | 84 +++++++++++++++++++++++ receipts/token_transfers_test.go | 112 +++++++++++++++++++------------ 2 files changed, 154 insertions(+), 42 deletions(-) diff --git a/receipts/token_transfers.go b/receipts/token_transfers.go index 1ede3656..16e902be 100644 --- a/receipts/token_transfers.go +++ b/receipts/token_transfers.go @@ -3,6 +3,7 @@ package receipts import ( "context" "math/big" + "sort" "github.com/0xsequence/ethkit/ethrpc" "github.com/0xsequence/ethkit/go-ethereum/common" @@ -77,6 +78,14 @@ type TokenTransfer struct { type TokenTransfers []*TokenTransfer +type TokenBalance struct { + Token common.Address + Account common.Address + Balance *big.Int +} + +type TokenBalances []*TokenBalance + func (t TokenTransfers) FilterByContractAddress(ctx context.Context, contract common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { @@ -116,3 +125,78 @@ func (t TokenTransfers) FilterByToAddress(ctx context.Context, to common.Address } return out } + +func (t TokenTransfers) Delta() TokenTransfers { + out := TokenTransfers{} + return out +} + +// ComputeBalances aggregates net balance changes per token per account from the transfers. +// For each transfer, it subtracts `Value` from `From` and adds `Value` to `To`. +// Accounts with a resulting zero balance change for a given token are omitted. +func (s TokenTransfers) ComputeBalanceOutputs() TokenBalances { + // key: token address + account address + type key struct { + token common.Address + account common.Address + } + + balances := make(map[key]*big.Int) + + for _, tr := range s { + if tr == nil || tr.Value == nil { + continue + } + + // From: subtract value + kFrom := key{token: tr.Token, account: tr.From} + if _, ok := balances[kFrom]; !ok { + balances[kFrom] = new(big.Int) + } + balances[kFrom].Sub(balances[kFrom], tr.Value) + + // To: add value + kTo := key{token: tr.Token, account: tr.To} + if _, ok := balances[kTo]; !ok { + balances[kTo] = new(big.Int) + } + balances[kTo].Add(balances[kTo], tr.Value) + } + + // Convert to slice, excluding zero balances + out := TokenBalances{} + zero := big.NewInt(0) + + for k, v := range balances { + if v == nil || v.Cmp(zero) == 0 { + continue + } + out = append(out, &TokenBalance{ + Token: k.token, + Account: k.account, + Balance: new(big.Int).Set(v), + }) + } + + sort.Slice(out, func(i, j int) bool { + bi := out[i].Balance + bj := out[j].Balance + // ascending by numeric value (negative first) + cmp := bi.Cmp(bj) + if cmp != 0 { + return cmp < 0 + } + // account lexicographic + ai := out[i].Account.Hex() + aj := out[j].Account.Hex() + if ai != aj { + return ai < aj + } + // token lexicographic + ti := out[i].Token.Hex() + tj := out[j].Token.Hex() + return ti < tj + }) + + return out +} diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index 9bb1bfa3..1dfed10f 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -64,58 +64,86 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // Case 3: a trails intent call, with bunch of other actions inside of the txn, including erc20 transfers // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 - // .. lets get another one using zerox + cctp for example t.Run("Case 3: ..", func(t *testing.T) { + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") + require.NoError(t, err) + + txnHash := common.HexToHash("0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9") + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + require.Equal(t, 13, len(receipt.Logs)) + + // Trails intent + usdc := common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831") + owner := common.HexToAddress("0x9DAB7A98C207f01A35DF00257949a27609b93ad7") + trailsRouter := common.HexToAddress("0xF8A739B9F24E297a98b7aba7A9cdFDBD457F6fF8") + bridge := common.HexToAddress("0xf70da97812CB96acDF810712Aa562db8dfA3dbEF") + collector := common.HexToAddress("0x76008498f26789dd8b691Bebe24C889A3dd1A2fc") + + // spew.Dump(transfers) + require.Equal(t, 3, len(transfers)) + + // Log 1, USDC owner to trailsRouter + require.Equal(t, usdc, transfers[0].Token) + require.Equal(t, owner, transfers[0].From) + require.Equal(t, trailsRouter, transfers[0].To) + require.Equal(t, big.NewInt(175353), transfers[0].Value) + + // Log 2, USDC trailsRouter from bridge + require.Equal(t, usdc, transfers[1].Token) + require.Equal(t, trailsRouter, transfers[1].From) + require.Equal(t, bridge, transfers[1].To) + require.Equal(t, big.NewInt(175353), transfers[1].Value) + + // Log 3, USDC owner to collector + require.Equal(t, usdc, transfers[2].Token) + require.Equal(t, owner, transfers[2].From) + require.Equal(t, collector, transfers[2].To) + require.Equal(t, big.NewInt(9979), transfers[2].Value) + + // Get the delta / net effects + balances := transfers.ComputeBalanceOutputs() + require.NotNil(t, balances) + require.Equal(t, len(balances), 3) + // spew.Dump(balances) + + require.Equal(t, usdc, balances[0].Token) + require.Equal(t, owner, balances[0].Account) + require.Equal(t, big.NewInt(-185332), balances[0].Balance) + + require.Equal(t, usdc, balances[1].Token) + require.Equal(t, collector, balances[1].Account) + require.Equal(t, big.NewInt(9979), balances[1].Balance) + + require.Equal(t, usdc, balances[2].Token) + require.Equal(t, bridge, balances[2].Account) + require.Equal(t, big.NewInt(175353), balances[2].Balance) }) - // Case 4: vault bridge USDC .. lets check the token transfer event, prob just erc20 too - // https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52 + // Case 4: a trails cross-chain swap where we use 0x + cctp to swap from MAGIC to USDC then bridge + // over CCTP. This includes many calls with USDC and MAGIC. + // https://arbiscan.io/tx/0xa5c17e51443c8a8ce60cdcbe84b89fd2570f073bbb3b9ec8cdc9361aa1ca984f t.Run("Case 4: ..", func(t *testing.T) { }) - // Case 5: polygon POL LogTransfer event - // https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 + // Case 5: vault bridge USDC .. lets check the token transfer event, prob just erc20 too + // https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52 t.Run("Case 5: ..", func(t *testing.T) { }) - // Case 6: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call - // and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ? - // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 + // Case 6: polygon POL LogTransfer event + // https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 t.Run("Case 6: ..", func(t *testing.T) { }) - //-- - - // t.Run("Simple ERC20 Transfer", func(t *testing.T) { - // // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 - // provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") - // require.NoError(t, err) - - // txnHash := common.HexToHash("0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9") - - // receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) - // require.NoError(t, err) - // require.NotNil(t, receipt) - // require.Greater(t, len(transfers), 0) - - // spew.Dump(transfers) - - // }) - - // t.Run("Polygon POL LogTransfer", func(t *testing.T) { - // t.Skip("POL") - - // // txnHash := https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 - // provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon") - // require.NoError(t, err) - - // txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5") - - // receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) - // require.NoError(t, err) - // require.NotNil(t, receipt) - // require.Greater(t, len(transfers), 0) - - // spew.Dump(transfers) - // }) + // Case 7: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call + // and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ? + // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 + t.Run("Case 7: ..", func(t *testing.T) { + }) } + +// TODO: lets test the TokenTransfers directly with mock +// data we write by hand to ensure aggregation works properly, etc. From da565fcc48d0f189a332c037b6d7a6b827fb1bd3 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 15:41:17 -0500 Subject: [PATCH 07/11] update --- receipts/token_transfers.go | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/receipts/token_transfers.go b/receipts/token_transfers.go index 16e902be..6240a9e7 100644 --- a/receipts/token_transfers.go +++ b/receipts/token_transfers.go @@ -11,11 +11,6 @@ import ( "github.com/0xsequence/go-sequence/contracts/gen/tokens" ) -var tokenTransferTopicHashes = []common.Hash{ - common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), // ERC20 Transfer - common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4"), // Polygon POL LogTransfer (custom) -} - // FetchReceiptTokenTransfers fetches the transaction receipt for the given transaction hash // and decodes any token transfer events (ERC20) that occurred within that transaction. TODOXXX: we // currently only support ERC20 token transfers, but we can extend this to support ERC721 and ERC1155 as well. @@ -27,13 +22,25 @@ func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, if receipt == nil { return nil, nil, nil } + transfers, err := DecodeTokenTransfersFromLogs(ctx, receipt.Logs) + if err != nil { + return receipt, nil, err + } + return receipt, transfers, nil +} + +var tokenTransferTopicHashes = []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), // ERC20 Transfer + common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4"), // Polygon POL LogTransfer (custom) +} +func DecodeTokenTransfersFromLogs(ctx context.Context, logs []*types.Log) (TokenTransfers, error) { transferTopic := common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") polLogTransferTopic := common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4") var decoded []*TokenTransfer - for _, log := range receipt.Logs { + for _, log := range logs { if len(log.Topics) == 0 { continue } @@ -44,7 +51,7 @@ func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, tokenAddress := log.Address if log.Topics[0] == transferTopic { - filterer, err := tokens.NewIERC20Filterer(log.Address, provider) + filterer, err := tokens.NewIERC20Filterer(log.Address, nil) if err == nil { if ev, err := filterer.ParseTransfer(*log); err == nil && ev != nil { decoded = append(decoded, &TokenTransfer{Token: tokenAddress, From: ev.From, To: ev.To, Value: ev.Value, Raw: *log}) @@ -65,14 +72,14 @@ func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, } } - return receipt, decoded, nil + return decoded, nil } type TokenTransfer struct { Token common.Address From common.Address To common.Address - Value *big.Int // TODO: check the erc20 log spec to see if we should call this value or amount + Value *big.Int Raw types.Log } @@ -131,7 +138,7 @@ func (t TokenTransfers) Delta() TokenTransfers { return out } -// ComputeBalances aggregates net balance changes per token per account from the transfers. +// ComputeBalanceOutputs aggregates net balance changes per token per account from the transfers. // For each transfer, it subtracts `Value` from `From` and adds `Value` to `To`. // Accounts with a resulting zero balance change for a given token are omitted. func (s TokenTransfers) ComputeBalanceOutputs() TokenBalances { From 60e772f98caef6871d4ef1b315afaf64a92cb852 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 15:48:20 -0500 Subject: [PATCH 08/11] update --- receipts/token_transfers_test.go | 52 ++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index 1dfed10f..35e66f97 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -8,7 +8,6 @@ import ( "github.com/0xsequence/ethkit/ethrpc" "github.com/0xsequence/ethkit/go-ethereum/common" "github.com/0xsequence/go-sequence/receipts" - "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/require" ) @@ -31,6 +30,20 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { require.Equal(t, common.HexToAddress("0x1D17C0F90A0b3dFb5124C2FF56B33a0D2E202e1d"), transfers[0].From) require.Equal(t, common.HexToAddress("0x5646E2424A7b7d43740EF14bc5b4f1e00Bf9B6Ba"), transfers[0].To) require.Equal(t, big.NewInt(184840), transfers[0].Value) + + // Get the balance outputs from the transfer logs + balances := transfers.ComputeBalanceOutputs() + require.NotNil(t, balances) + require.Equal(t, len(balances), 2) + // spew.Dump(balances) + + require.Equal(t, common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), balances[0].Token) // USDC + require.Equal(t, common.HexToAddress("0x1D17C0F90A0b3dFb5124C2FF56B33a0D2E202e1d"), balances[0].Account) + require.Equal(t, big.NewInt(-184840), balances[0].Balance) + + require.Equal(t, common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), balances[1].Token) // USDC + require.Equal(t, common.HexToAddress("0x5646E2424A7b7d43740EF14bc5b4f1e00Bf9B6Ba"), balances[1].Account) + require.Equal(t, big.NewInt(184840), balances[1].Balance) }) // Case 2: a txn with a bunch of different erc20 transfers inside of it, ie. batch send @@ -46,7 +59,7 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { require.NotNil(t, receipt) require.Greater(t, len(transfers), 0) - spew.Dump(transfers) + // spew.Dump(transfers) require.Equal(t, 2, len(transfers)) // USDC @@ -60,11 +73,33 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { require.Equal(t, common.HexToAddress("0x8e3E38fe7367dd3b52D1e281E4e8400447C8d8B9"), transfers[1].From) require.Equal(t, common.HexToAddress("0x9b1A542f3C455E8d6057C3478EB945B48D8e17fF"), transfers[1].To) require.Equal(t, big.NewInt(200000000000000000), transfers[1].Value) + + // Get the balance outputs from the transfer logs + balances := transfers.ComputeBalanceOutputs() + require.NotNil(t, balances) + require.Equal(t, len(balances), 4) + // spew.Dump(balances) + + require.Equal(t, common.HexToAddress("0x539bdE0d7Dbd336b79148AA742883198BBF60342"), balances[0].Token) // MAGIC + require.Equal(t, common.HexToAddress("0x8e3E38fe7367dd3b52D1e281E4e8400447C8d8B9"), balances[0].Account) + require.Equal(t, big.NewInt(-200000000000000000), balances[0].Balance) + + require.Equal(t, common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), balances[1].Token) // USDC + require.Equal(t, common.HexToAddress("0x8e3E38fe7367dd3b52D1e281E4e8400447C8d8B9"), balances[1].Account) + require.Equal(t, big.NewInt(-100000), balances[1].Balance) + + require.Equal(t, common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), balances[2].Token) // USDC + require.Equal(t, common.HexToAddress("0x9b1A542f3C455E8d6057C3478EB945B48D8e17fF"), balances[2].Account) + require.Equal(t, big.NewInt(100000), balances[2].Balance) + + require.Equal(t, common.HexToAddress("0x539bdE0d7Dbd336b79148AA742883198BBF60342"), balances[3].Token) // MAGIC + require.Equal(t, common.HexToAddress("0x9b1A542f3C455E8d6057C3478EB945B48D8e17fF"), balances[3].Account) + require.Equal(t, big.NewInt(200000000000000000), balances[3].Balance) }) // Case 3: a trails intent call, with bunch of other actions inside of the txn, including erc20 transfers // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9 - t.Run("Case 3: ..", func(t *testing.T) { + t.Run("Case 3: Trails intent origin call", func(t *testing.T) { provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") require.NoError(t, err) @@ -103,7 +138,7 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { require.Equal(t, collector, transfers[2].To) require.Equal(t, big.NewInt(9979), transfers[2].Value) - // Get the delta / net effects + // Get the balance outputs from the transfer logs balances := transfers.ComputeBalanceOutputs() require.NotNil(t, balances) require.Equal(t, len(balances), 3) @@ -125,23 +160,22 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // Case 4: a trails cross-chain swap where we use 0x + cctp to swap from MAGIC to USDC then bridge // over CCTP. This includes many calls with USDC and MAGIC. // https://arbiscan.io/tx/0xa5c17e51443c8a8ce60cdcbe84b89fd2570f073bbb3b9ec8cdc9361aa1ca984f - t.Run("Case 4: ..", func(t *testing.T) { + t.Run("Case 4: Trails swap intent call", func(t *testing.T) { }) // Case 5: vault bridge USDC .. lets check the token transfer event, prob just erc20 too // https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52 - t.Run("Case 5: ..", func(t *testing.T) { + t.Run("Case 5: Vault bridge USDC transfer", func(t *testing.T) { }) // Case 6: polygon POL LogTransfer event // https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 - t.Run("Case 6: ..", func(t *testing.T) { + t.Run("Case 6: POL with LogTransfer", func(t *testing.T) { }) // Case 7: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call - // and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ? // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 - t.Run("Case 7: ..", func(t *testing.T) { + t.Run("Case 7: Random txn with swap and many tokens", func(t *testing.T) { }) } From 7f4d86d49a63a1f261597cd94587403e4430333e Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 16:47:25 -0500 Subject: [PATCH 09/11] update --- receipts/token_transfers.go | 12 ++++ receipts/token_transfers_test.go | 100 +++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/receipts/token_transfers.go b/receipts/token_transfers.go index 6240a9e7..1567d72f 100644 --- a/receipts/token_transfers.go +++ b/receipts/token_transfers.go @@ -207,3 +207,15 @@ func (s TokenTransfers) ComputeBalanceOutputs() TokenBalances { return out } + +func (b TokenBalances) FilterByAccount(account common.Address, optToken ...common.Address) TokenBalances { + var out TokenBalances + for _, bal := range b { + if bal.Account == account { + if len(optToken) == 0 || bal.Token == optToken[0] { + out = append(out, bal) + } + } + } + return out +} diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index 35e66f97..af8434b5 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -161,6 +161,100 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // over CCTP. This includes many calls with USDC and MAGIC. // https://arbiscan.io/tx/0xa5c17e51443c8a8ce60cdcbe84b89fd2570f073bbb3b9ec8cdc9361aa1ca984f t.Run("Case 4: Trails swap intent call", func(t *testing.T) { + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum") + require.NoError(t, err) + + txnHash := common.HexToHash("0xa5c17e51443c8a8ce60cdcbe84b89fd2570f073bbb3b9ec8cdc9361aa1ca984f") + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + require.Equal(t, 31, len(receipt.Logs)) + + // Trails intent + require.Equal(t, 10, len(transfers)) + // spew.Dump(transfers) + + // Get the balance outputs from the transfer logs + balances := transfers.ComputeBalanceOutputs() + require.NotNil(t, balances) + require.Equal(t, len(balances), 9) + // spew.Dump(balances) + + usdc := common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831") + magic := common.HexToAddress("0x539bdE0d7Dbd336b79148AA742883198BBF60342") + owner := common.HexToAddress("0x8f2951E6b9Bd9F8cf3522A7Fa98A0F9bC767b155") + trailsRouter := common.HexToAddress("0xF8A739B9F24E297a98b7aba7A9cdFDBD457F6fF8") + collector := common.HexToAddress("0x76008498f26789dd8b691Bebe24C889A3dd1A2fc") + + // intermediary token used via uniswap + weth := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") + uniswap := common.HexToAddress("0xB7E50106A5bd3Cf21AF210A755F9C8740890A8c9") + + // some rando token used in the swap + liqBook := common.HexToAddress("0xb7236B927e03542AC3bE0A054F2bEa8868AF9508") + + // NOTE: these balances are not in order of operations + // it is the outputs, and sorted by smallest to highest + // in terms of numeric value (not USD value obviously) + // as decimals are not factored in here per token. + + // owner sending magic + require.Equal(t, magic, balances[0].Token) + require.Equal(t, owner, balances[0].Account) + require.Equal(t, makeBigInt(t, "-10686807000000000000"), balances[0].Balance) + + // uniswap sending out weth + require.Equal(t, weth, balances[1].Token) + require.Equal(t, uniswap, balances[1].Account) + require.Equal(t, makeBigInt(t, "-383769729558864"), balances[1].Balance) + + // liqbook sending usdc + require.Equal(t, usdc, balances[2].Token) + require.Equal(t, liqBook, balances[2].Account) + require.Equal(t, makeBigInt(t, "-1191946"), balances[2].Balance) + + // balance of some 0x related wallet, probably a fee collector for 0x + require.Equal(t, usdc, balances[3].Token) + require.Equal(t, common.HexToAddress("0xaD01C20d5886137e056775af56915de824c8fCe5"), balances[3].Account) + require.Equal(t, makeBigInt(t, "1787"), balances[3].Balance) + + // trailsRouter receiving usdc + // TODO: this must be a bug in trails router or calls, as there shouldn't be any + // dust left in the router. + require.Equal(t, usdc, balances[4].Token) + require.Equal(t, trailsRouter, balances[4].Account) + require.Equal(t, makeBigInt(t, "36299"), balances[4].Balance) + + // usdc burn for cctp + require.Equal(t, usdc, balances[5].Token) + require.Equal(t, common.HexToAddress("0x0000000000000000000000000000000000000000"), balances[5].Account) + require.Equal(t, makeBigInt(t, "1153860"), balances[5].Balance) + + // liqBook got the weth from the swap flow + require.Equal(t, weth, balances[6].Token) + require.Equal(t, liqBook, balances[6].Account) + require.Equal(t, makeBigInt(t, "383769729558864"), balances[6].Balance) + + // fee collector paid in magic + require.Equal(t, magic, balances[7].Token) + require.Equal(t, collector, balances[7].Account) + require.Equal(t, makeBigInt(t, "109223258035414205"), balances[7].Balance) + + // uniswap ending up with the magic from swap in + require.Equal(t, magic, balances[8].Token) + require.Equal(t, uniswap, balances[8].Account) + require.Equal(t, makeBigInt(t, "10577583741964585795"), balances[8].Balance) + + // Get balance of just the cctp burn address + cctpBurnAddress := balances.FilterByAccount(common.HexToAddress("0x0000000000000000000000000000000000000000"), usdc) + require.Equal(t, 1, len(cctpBurnAddress)) + require.Equal(t, usdc, cctpBurnAddress[0].Token) + require.Equal(t, makeBigInt(t, "1153860"), cctpBurnAddress[0].Balance) + + // Get balance of uniswap only + uniswapBalances := balances.FilterByAccount(uniswap) + require.Equal(t, 2, len(uniswapBalances)) }) // Case 5: vault bridge USDC .. lets check the token transfer event, prob just erc20 too @@ -181,3 +275,9 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // TODO: lets test the TokenTransfers directly with mock // data we write by hand to ensure aggregation works properly, etc. + +func makeBigInt(t *testing.T, s string) *big.Int { + bi, ok := new(big.Int).SetString(s, 10) + require.True(t, ok) + return bi +} From 120a21dbefc2951bf0f17913b03ab6098753aad5 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 16:50:03 -0500 Subject: [PATCH 10/11] update --- receipts/token_transfers_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index af8434b5..eabd35f0 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -260,6 +260,33 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // Case 5: vault bridge USDC .. lets check the token transfer event, prob just erc20 too // https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52 t.Run("Case 5: Vault bridge USDC transfer", func(t *testing.T) { + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/katana") + require.NoError(t, err) + + txnHash := common.HexToHash("0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52") + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + require.Equal(t, 1, len(receipt.Logs)) + + // Trails intent + require.Equal(t, 1, len(transfers)) + // spew.Dump(transfers) + + // Get the balance outputs from the transfer logs + balances := transfers.ComputeBalanceOutputs() + require.NotNil(t, balances) + require.Equal(t, len(balances), 2) + // spew.Dump(balances) + + require.Equal(t, common.HexToAddress("0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36"), balances[0].Token) + require.Equal(t, common.HexToAddress("0x1D17C0F90A0b3dFb5124C2FF56B33a0D2E202e1d"), balances[0].Account) + require.Equal(t, makeBigInt(t, "-177353"), balances[0].Balance) + + require.Equal(t, common.HexToAddress("0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36"), balances[1].Token) + require.Equal(t, common.HexToAddress("0xE766bc22e31097940FFF8A73B240055EFB2424C8"), balances[1].Account) + require.Equal(t, makeBigInt(t, "177353"), balances[1].Balance) }) // Case 6: polygon POL LogTransfer event From c9abfafbef2c4d5b5db205838eef9983af135985 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Sat, 13 Dec 2025 17:40:01 -0500 Subject: [PATCH 11/11] update --- receipts/token_transfers.go | 71 ++++++++++++++++++++------------ receipts/token_transfers_test.go | 50 +++++++++++++++++----- 2 files changed, 83 insertions(+), 38 deletions(-) diff --git a/receipts/token_transfers.go b/receipts/token_transfers.go index 1567d72f..8e0d9463 100644 --- a/receipts/token_transfers.go +++ b/receipts/token_transfers.go @@ -29,28 +29,23 @@ func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, return receipt, transfers, nil } -var tokenTransferTopicHashes = []common.Hash{ - common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), // ERC20 Transfer - common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4"), // Polygon POL LogTransfer (custom) -} +var ( + erc20TransferTopic = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") + polLogTransferTopic = common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4") +) func DecodeTokenTransfersFromLogs(ctx context.Context, logs []*types.Log) (TokenTransfers, error) { - transferTopic := common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") - polLogTransferTopic := common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4") - var decoded []*TokenTransfer for _, log := range logs { if len(log.Topics) == 0 { continue } - if log.Topics[0] != transferTopic && log.Topics[0] != polLogTransferTopic { - continue - } - tokenAddress := log.Address - if log.Topics[0] == transferTopic { + switch log.Topics[0] { + case erc20TransferTopic: + // ERC20 Transfer filterer, err := tokens.NewIERC20Filterer(log.Address, nil) if err == nil { if ev, err := filterer.ParseTransfer(*log); err == nil && ev != nil { @@ -58,17 +53,28 @@ func DecodeTokenTransfersFromLogs(ctx context.Context, logs []*types.Log) (Token continue } } - } - // TODO: need to try all of the various versions of this.. and we may as well support ERC721 and ERC1155 too - // note: "indexed" args, etc. + case polLogTransferTopic: + // Polygon POL LogTransfer (custom) + // https://polygonscan.com/address/0x0000000000000000000000000000000000001010#code + // + // ABI: + // event LogTransfer(address indexed token, address indexed from, address indexed to, + // uint256 amount, uint256 input1, uint256 input2, uint256 output1, uint256 output2) + if len(log.Topics) >= 4 { + from := common.BytesToAddress(log.Topics[2].Bytes()) + to := common.BytesToAddress(log.Topics[3].Bytes()) + var value *big.Int + if len(log.Data) >= 32 { + value = new(big.Int).SetBytes(log.Data[:32]) + } else { + value = new(big.Int) + } + decoded = append(decoded, &TokenTransfer{Token: tokenAddress, From: from, To: to, Value: value, Raw: *log}) + } - // TODO: this is wrong, etc. - if len(log.Topics) >= 3 { - from := common.BytesToAddress(log.Topics[1].Bytes()) - to := common.BytesToAddress(log.Topics[2].Bytes()) - value := new(big.Int).SetBytes(log.Data) - decoded = append(decoded, &TokenTransfer{Token: tokenAddress, From: from, To: to, Value: value, Raw: *log}) + default: + continue } } @@ -93,7 +99,7 @@ type TokenBalance struct { type TokenBalances []*TokenBalance -func (t TokenTransfers) FilterByContractAddress(ctx context.Context, contract common.Address) TokenTransfers { +func (t TokenTransfers) FilterByContractAddress(contract common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { if transfer.Raw.Address == contract { @@ -103,7 +109,7 @@ func (t TokenTransfers) FilterByContractAddress(ctx context.Context, contract co return out } -func (t TokenTransfers) FilterByAccountAddress(ctx context.Context, account common.Address) TokenTransfers { +func (t TokenTransfers) FilterByAccountAddress(account common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { if transfer.From == account || transfer.To == account { @@ -113,7 +119,7 @@ func (t TokenTransfers) FilterByAccountAddress(ctx context.Context, account comm return out } -func (t TokenTransfers) FilterByFromAddress(ctx context.Context, from common.Address) TokenTransfers { +func (t TokenTransfers) FilterByFromAddress(from common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { if transfer.From == from { @@ -123,7 +129,7 @@ func (t TokenTransfers) FilterByFromAddress(ctx context.Context, from common.Add return out } -func (t TokenTransfers) FilterByToAddress(ctx context.Context, to common.Address) TokenTransfers { +func (t TokenTransfers) FilterByToAddress(to common.Address) TokenTransfers { var out TokenTransfers for _, transfer := range t { if transfer.To == to { @@ -141,7 +147,7 @@ func (t TokenTransfers) Delta() TokenTransfers { // ComputeBalanceOutputs aggregates net balance changes per token per account from the transfers. // For each transfer, it subtracts `Value` from `From` and adds `Value` to `To`. // Accounts with a resulting zero balance change for a given token are omitted. -func (s TokenTransfers) ComputeBalanceOutputs() TokenBalances { +func (s TokenTransfers) ComputeBalanceOutputs(omitZeroBalances ...bool) TokenBalances { // key: token address + account address type key struct { token common.Address @@ -175,7 +181,7 @@ func (s TokenTransfers) ComputeBalanceOutputs() TokenBalances { zero := big.NewInt(0) for k, v := range balances { - if v == nil || v.Cmp(zero) == 0 { + if v == nil || (len(omitZeroBalances) > 0 && omitZeroBalances[0] && v.Cmp(zero) == 0) { continue } out = append(out, &TokenBalance{ @@ -208,6 +214,17 @@ func (s TokenTransfers) ComputeBalanceOutputs() TokenBalances { return out } +func (b TokenBalances) OmitZeroBalances() TokenBalances { + var out TokenBalances + zero := big.NewInt(0) + for _, bal := range b { + if bal.Balance != nil && bal.Balance.Cmp(zero) != 0 { + out = append(out, bal) + } + } + return out +} + func (b TokenBalances) FilterByAccount(account common.Address, optToken ...common.Address) TokenBalances { var out TokenBalances for _, bal := range b { diff --git a/receipts/token_transfers_test.go b/receipts/token_transfers_test.go index eabd35f0..e59c46e7 100644 --- a/receipts/token_transfers_test.go +++ b/receipts/token_transfers_test.go @@ -34,7 +34,7 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // Get the balance outputs from the transfer logs balances := transfers.ComputeBalanceOutputs() require.NotNil(t, balances) - require.Equal(t, len(balances), 2) + require.Equal(t, 2, len(balances)) // spew.Dump(balances) require.Equal(t, common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), balances[0].Token) // USDC @@ -77,7 +77,7 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // Get the balance outputs from the transfer logs balances := transfers.ComputeBalanceOutputs() require.NotNil(t, balances) - require.Equal(t, len(balances), 4) + require.Equal(t, 4, len(balances)) // spew.Dump(balances) require.Equal(t, common.HexToAddress("0x539bdE0d7Dbd336b79148AA742883198BBF60342"), balances[0].Token) // MAGIC @@ -139,9 +139,9 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { require.Equal(t, big.NewInt(9979), transfers[2].Value) // Get the balance outputs from the transfer logs - balances := transfers.ComputeBalanceOutputs() + balances := transfers.ComputeBalanceOutputs().OmitZeroBalances() require.NotNil(t, balances) - require.Equal(t, len(balances), 3) + require.Equal(t, 3, len(balances)) // spew.Dump(balances) require.Equal(t, usdc, balances[0].Token) @@ -176,9 +176,9 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // spew.Dump(transfers) // Get the balance outputs from the transfer logs - balances := transfers.ComputeBalanceOutputs() + balances := transfers.ComputeBalanceOutputs().OmitZeroBalances() require.NotNil(t, balances) - require.Equal(t, len(balances), 9) + require.Equal(t, 9, len(balances)) // spew.Dump(balances) usdc := common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831") @@ -270,14 +270,13 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { require.Greater(t, len(transfers), 0) require.Equal(t, 1, len(receipt.Logs)) - // Trails intent require.Equal(t, 1, len(transfers)) // spew.Dump(transfers) // Get the balance outputs from the transfer logs balances := transfers.ComputeBalanceOutputs() require.NotNil(t, balances) - require.Equal(t, len(balances), 2) + require.Equal(t, 2, len(balances)) // spew.Dump(balances) require.Equal(t, common.HexToAddress("0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36"), balances[0].Token) @@ -292,17 +291,46 @@ func TestFetchReceiptTokenTransfers(t *testing.T) { // Case 6: polygon POL LogTransfer event // https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5 t.Run("Case 6: POL with LogTransfer", func(t *testing.T) { + provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon") + require.NoError(t, err) + + txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5") + receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Greater(t, len(transfers), 0) + require.Equal(t, 25, len(receipt.Logs)) // actually a ton of stuff in here + + // Trails intent + transfers = transfers.FilterByContractAddress(common.HexToAddress("0x0000000000000000000000000000000000001010")) // POL token + require.Equal(t, 2, len(transfers)) + // spew.Dump(transfers) + + // Get the balance outputs from the transfer logs + balances := transfers.ComputeBalanceOutputs(true) // omit zero balances + require.NotNil(t, balances) + require.Equal(t, 2, len(balances)) + // spew.Dump(balances) + + polPsuedoToken := common.HexToAddress("0x0000000000000000000000000000000000001010") + + require.Equal(t, polPsuedoToken, balances[0].Token) + require.Equal(t, common.HexToAddress("0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270"), balances[0].Account) + require.Equal(t, makeBigInt(t, "-3965683724100320759"), balances[0].Balance) + + require.Equal(t, polPsuedoToken, balances[1].Token) + require.Equal(t, common.HexToAddress("0x1D17C0F90A0b3dFb5124C2FF56B33a0D2E202e1d"), balances[1].Account) + require.Equal(t, makeBigInt(t, "3965683724100320759"), balances[1].Balance) }) // Case 7: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call // https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4 t.Run("Case 7: Random txn with swap and many tokens", func(t *testing.T) { + // NOTE, skippig writing this for now as its pretty similar + // to case 4 }) } -// TODO: lets test the TokenTransfers directly with mock -// data we write by hand to ensure aggregation works properly, etc. - func makeBigInt(t *testing.T, s string) *big.Int { bi, ok := new(big.Int).SetString(s, 10) require.True(t, ok)