Skip to content

Commit c0009ba

Browse files
committed
update
1 parent 1fd2c10 commit c0009ba

File tree

2 files changed

+154
-42
lines changed

2 files changed

+154
-42
lines changed

receipts/token_transfers.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package receipts
33
import (
44
"context"
55
"math/big"
6+
"sort"
67

78
"github.com/0xsequence/ethkit/ethrpc"
89
"github.com/0xsequence/ethkit/go-ethereum/common"
@@ -77,6 +78,14 @@ type TokenTransfer struct {
7778

7879
type TokenTransfers []*TokenTransfer
7980

81+
type TokenBalance struct {
82+
Token common.Address
83+
Account common.Address
84+
Balance *big.Int
85+
}
86+
87+
type TokenBalances []*TokenBalance
88+
8089
func (t TokenTransfers) FilterByContractAddress(ctx context.Context, contract common.Address) TokenTransfers {
8190
var out TokenTransfers
8291
for _, transfer := range t {
@@ -116,3 +125,78 @@ func (t TokenTransfers) FilterByToAddress(ctx context.Context, to common.Address
116125
}
117126
return out
118127
}
128+
129+
func (t TokenTransfers) Delta() TokenTransfers {
130+
out := TokenTransfers{}
131+
return out
132+
}
133+
134+
// ComputeBalances aggregates net balance changes per token per account from the transfers.
135+
// For each transfer, it subtracts `Value` from `From` and adds `Value` to `To`.
136+
// Accounts with a resulting zero balance change for a given token are omitted.
137+
func (s TokenTransfers) ComputeBalanceOutputs() TokenBalances {
138+
// key: token address + account address
139+
type key struct {
140+
token common.Address
141+
account common.Address
142+
}
143+
144+
balances := make(map[key]*big.Int)
145+
146+
for _, tr := range s {
147+
if tr == nil || tr.Value == nil {
148+
continue
149+
}
150+
151+
// From: subtract value
152+
kFrom := key{token: tr.Token, account: tr.From}
153+
if _, ok := balances[kFrom]; !ok {
154+
balances[kFrom] = new(big.Int)
155+
}
156+
balances[kFrom].Sub(balances[kFrom], tr.Value)
157+
158+
// To: add value
159+
kTo := key{token: tr.Token, account: tr.To}
160+
if _, ok := balances[kTo]; !ok {
161+
balances[kTo] = new(big.Int)
162+
}
163+
balances[kTo].Add(balances[kTo], tr.Value)
164+
}
165+
166+
// Convert to slice, excluding zero balances
167+
out := TokenBalances{}
168+
zero := big.NewInt(0)
169+
170+
for k, v := range balances {
171+
if v == nil || v.Cmp(zero) == 0 {
172+
continue
173+
}
174+
out = append(out, &TokenBalance{
175+
Token: k.token,
176+
Account: k.account,
177+
Balance: new(big.Int).Set(v),
178+
})
179+
}
180+
181+
sort.Slice(out, func(i, j int) bool {
182+
bi := out[i].Balance
183+
bj := out[j].Balance
184+
// ascending by numeric value (negative first)
185+
cmp := bi.Cmp(bj)
186+
if cmp != 0 {
187+
return cmp < 0
188+
}
189+
// account lexicographic
190+
ai := out[i].Account.Hex()
191+
aj := out[j].Account.Hex()
192+
if ai != aj {
193+
return ai < aj
194+
}
195+
// token lexicographic
196+
ti := out[i].Token.Hex()
197+
tj := out[j].Token.Hex()
198+
return ti < tj
199+
})
200+
201+
return out
202+
}

receipts/token_transfers_test.go

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -64,58 +64,86 @@ func TestFetchReceiptTokenTransfers(t *testing.T) {
6464

6565
// Case 3: a trails intent call, with bunch of other actions inside of the txn, including erc20 transfers
6666
// https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9
67-
// .. lets get another one using zerox + cctp for example
6867
t.Run("Case 3: ..", func(t *testing.T) {
68+
provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum")
69+
require.NoError(t, err)
70+
71+
txnHash := common.HexToHash("0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9")
72+
receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash)
73+
require.NoError(t, err)
74+
require.NotNil(t, receipt)
75+
require.Greater(t, len(transfers), 0)
76+
require.Equal(t, 13, len(receipt.Logs))
77+
78+
// Trails intent
79+
usdc := common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831")
80+
owner := common.HexToAddress("0x9DAB7A98C207f01A35DF00257949a27609b93ad7")
81+
trailsRouter := common.HexToAddress("0xF8A739B9F24E297a98b7aba7A9cdFDBD457F6fF8")
82+
bridge := common.HexToAddress("0xf70da97812CB96acDF810712Aa562db8dfA3dbEF")
83+
collector := common.HexToAddress("0x76008498f26789dd8b691Bebe24C889A3dd1A2fc")
84+
85+
// spew.Dump(transfers)
86+
require.Equal(t, 3, len(transfers))
87+
88+
// Log 1, USDC owner to trailsRouter
89+
require.Equal(t, usdc, transfers[0].Token)
90+
require.Equal(t, owner, transfers[0].From)
91+
require.Equal(t, trailsRouter, transfers[0].To)
92+
require.Equal(t, big.NewInt(175353), transfers[0].Value)
93+
94+
// Log 2, USDC trailsRouter from bridge
95+
require.Equal(t, usdc, transfers[1].Token)
96+
require.Equal(t, trailsRouter, transfers[1].From)
97+
require.Equal(t, bridge, transfers[1].To)
98+
require.Equal(t, big.NewInt(175353), transfers[1].Value)
99+
100+
// Log 3, USDC owner to collector
101+
require.Equal(t, usdc, transfers[2].Token)
102+
require.Equal(t, owner, transfers[2].From)
103+
require.Equal(t, collector, transfers[2].To)
104+
require.Equal(t, big.NewInt(9979), transfers[2].Value)
105+
106+
// Get the delta / net effects
107+
balances := transfers.ComputeBalanceOutputs()
108+
require.NotNil(t, balances)
109+
require.Equal(t, len(balances), 3)
110+
// spew.Dump(balances)
111+
112+
require.Equal(t, usdc, balances[0].Token)
113+
require.Equal(t, owner, balances[0].Account)
114+
require.Equal(t, big.NewInt(-185332), balances[0].Balance)
115+
116+
require.Equal(t, usdc, balances[1].Token)
117+
require.Equal(t, collector, balances[1].Account)
118+
require.Equal(t, big.NewInt(9979), balances[1].Balance)
119+
120+
require.Equal(t, usdc, balances[2].Token)
121+
require.Equal(t, bridge, balances[2].Account)
122+
require.Equal(t, big.NewInt(175353), balances[2].Balance)
69123
})
70124

71-
// Case 4: vault bridge USDC .. lets check the token transfer event, prob just erc20 too
72-
// https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52
125+
// Case 4: a trails cross-chain swap where we use 0x + cctp to swap from MAGIC to USDC then bridge
126+
// over CCTP. This includes many calls with USDC and MAGIC.
127+
// https://arbiscan.io/tx/0xa5c17e51443c8a8ce60cdcbe84b89fd2570f073bbb3b9ec8cdc9361aa1ca984f
73128
t.Run("Case 4: ..", func(t *testing.T) {
74129
})
75130

76-
// Case 5: polygon POL LogTransfer event
77-
// https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5
131+
// Case 5: vault bridge USDC .. lets check the token transfer event, prob just erc20 too
132+
// https://katanascan.com/tx/0x7bcd0068a5c3352cf4e1d75c7c4f78d99f02b8b2f5f96b2c407972f43e724f52
78133
t.Run("Case 5: ..", func(t *testing.T) {
79134
})
80135

81-
// Case 6: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call
82-
// and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ?
83-
// https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4
136+
// Case 6: polygon POL LogTransfer event
137+
// https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5
84138
t.Run("Case 6: ..", func(t *testing.T) {
85139
})
86140

87-
//--
88-
89-
// t.Run("Simple ERC20 Transfer", func(t *testing.T) {
90-
// // https://arbiscan.io/tx/0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9
91-
// provider, err := ethrpc.NewProvider("https://nodes.sequence.app/arbitrum")
92-
// require.NoError(t, err)
93-
94-
// txnHash := common.HexToHash("0xb88cc2fea7cd26c88e169f6244fea76f590fc0797ba4c424669d1b74643f1dc9")
95-
96-
// receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash)
97-
// require.NoError(t, err)
98-
// require.NotNil(t, receipt)
99-
// require.Greater(t, len(transfers), 0)
100-
101-
// spew.Dump(transfers)
102-
103-
// })
104-
105-
// t.Run("Polygon POL LogTransfer", func(t *testing.T) {
106-
// t.Skip("POL")
107-
108-
// // txnHash := https://polygonscan.com/tx/0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5
109-
// provider, err := ethrpc.NewProvider("https://nodes.sequence.app/polygon")
110-
// require.NoError(t, err)
111-
112-
// txnHash := common.HexToHash("0x252419983224542bfb07dab75808fa57186a7a269d0d267ae655eb7ef037fdd5")
113-
114-
// receipt, transfers, err := receipts.FetchReceiptTokenTransfers(context.Background(), provider, txnHash)
115-
// require.NoError(t, err)
116-
// require.NotNil(t, receipt)
117-
// require.Greater(t, len(transfers), 0)
118-
119-
// spew.Dump(transfers)
120-
// })
141+
// Case 7: bunch of logs for the same erc20 token, and we need to sum it up, ie. a big uniswap call
142+
// and we have to do a delta/diff, and return the "result" maybe "TokenTransferResult" ?
143+
// https://etherscan.io/tx/0xb11ff491495e145b07a1d3cc304f7d04b235b80af51b50da9a54095a6882fca4
144+
t.Run("Case 7: ..", func(t *testing.T) {
145+
})
121146
}
147+
148+
// TODO: lets test the TokenTransfers directly with mock
149+
// data we write by hand to ensure aggregation works properly, etc.

0 commit comments

Comments
 (0)