Skip to content

Commit be4e227

Browse files
authored
receipts: decode token transfers and balance outputs from any receipt or logs array (#333)
1 parent 2eebc81 commit be4e227

File tree

2 files changed

+576
-0
lines changed

2 files changed

+576
-0
lines changed

receipts/token_transfers.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package receipts
2+
3+
import (
4+
"context"
5+
"math/big"
6+
"sort"
7+
8+
"github.com/0xsequence/ethkit/ethrpc"
9+
"github.com/0xsequence/ethkit/go-ethereum/common"
10+
"github.com/0xsequence/ethkit/go-ethereum/core/types"
11+
"github.com/0xsequence/go-sequence/contracts/gen/tokens"
12+
)
13+
14+
// FetchReceiptTokenTransfers fetches the transaction receipt for the given transaction hash
15+
// and decodes any token transfer events (ERC20) that occurred within that transaction. TODOXXX: we
16+
// currently only support ERC20 token transfers, but we can extend this to support ERC721 and ERC1155 as well.
17+
func FetchReceiptTokenTransfers(ctx context.Context, provider *ethrpc.Provider, transactionHash common.Hash) (*types.Receipt, TokenTransfers, error) {
18+
receipt, err := provider.TransactionReceipt(ctx, transactionHash)
19+
if err != nil {
20+
return nil, nil, err
21+
}
22+
if receipt == nil {
23+
return nil, nil, nil
24+
}
25+
transfers, err := DecodeTokenTransfersFromLogs(ctx, receipt.Logs)
26+
if err != nil {
27+
return receipt, nil, err
28+
}
29+
return receipt, transfers, nil
30+
}
31+
32+
var (
33+
erc20TransferTopic = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
34+
polLogTransferTopic = common.HexToHash("0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4")
35+
)
36+
37+
func DecodeTokenTransfersFromLogs(ctx context.Context, logs []*types.Log) (TokenTransfers, error) {
38+
var decoded []*TokenTransfer
39+
40+
for _, log := range logs {
41+
if len(log.Topics) == 0 {
42+
continue
43+
}
44+
tokenAddress := log.Address
45+
46+
switch log.Topics[0] {
47+
case erc20TransferTopic:
48+
// ERC20 Transfer
49+
filterer, err := tokens.NewIERC20Filterer(log.Address, nil)
50+
if err == nil {
51+
if ev, err := filterer.ParseTransfer(*log); err == nil && ev != nil {
52+
decoded = append(decoded, &TokenTransfer{Token: tokenAddress, From: ev.From, To: ev.To, Value: ev.Value, Raw: *log})
53+
continue
54+
}
55+
}
56+
57+
case polLogTransferTopic:
58+
// Polygon POL LogTransfer (custom)
59+
// https://polygonscan.com/address/0x0000000000000000000000000000000000001010#code
60+
//
61+
// ABI:
62+
// event LogTransfer(address indexed token, address indexed from, address indexed to,
63+
// uint256 amount, uint256 input1, uint256 input2, uint256 output1, uint256 output2)
64+
if len(log.Topics) >= 4 {
65+
from := common.BytesToAddress(log.Topics[2].Bytes())
66+
to := common.BytesToAddress(log.Topics[3].Bytes())
67+
var value *big.Int
68+
if len(log.Data) >= 32 {
69+
value = new(big.Int).SetBytes(log.Data[:32])
70+
} else {
71+
value = new(big.Int)
72+
}
73+
decoded = append(decoded, &TokenTransfer{Token: tokenAddress, From: from, To: to, Value: value, Raw: *log})
74+
}
75+
76+
default:
77+
continue
78+
}
79+
}
80+
81+
return decoded, nil
82+
}
83+
84+
type TokenTransfer struct {
85+
Token common.Address
86+
From common.Address
87+
To common.Address
88+
Value *big.Int
89+
Raw types.Log
90+
}
91+
92+
type TokenTransfers []*TokenTransfer
93+
94+
type TokenBalance struct {
95+
Token common.Address
96+
Account common.Address
97+
Balance *big.Int
98+
}
99+
100+
type TokenBalances []*TokenBalance
101+
102+
func (t TokenTransfers) FilterByContractAddress(contract common.Address) TokenTransfers {
103+
var out TokenTransfers
104+
for _, transfer := range t {
105+
if transfer.Raw.Address == contract {
106+
out = append(out, transfer)
107+
}
108+
}
109+
return out
110+
}
111+
112+
func (t TokenTransfers) FilterByAccountAddress(account common.Address) TokenTransfers {
113+
var out TokenTransfers
114+
for _, transfer := range t {
115+
if transfer.From == account || transfer.To == account {
116+
out = append(out, transfer)
117+
}
118+
}
119+
return out
120+
}
121+
122+
func (t TokenTransfers) FilterByFromAddress(from common.Address) TokenTransfers {
123+
var out TokenTransfers
124+
for _, transfer := range t {
125+
if transfer.From == from {
126+
out = append(out, transfer)
127+
}
128+
}
129+
return out
130+
}
131+
132+
func (t TokenTransfers) FilterByToAddress(to common.Address) TokenTransfers {
133+
var out TokenTransfers
134+
for _, transfer := range t {
135+
if transfer.To == to {
136+
out = append(out, transfer)
137+
}
138+
}
139+
return out
140+
}
141+
142+
func (t TokenTransfers) Delta() TokenTransfers {
143+
out := TokenTransfers{}
144+
return out
145+
}
146+
147+
// ComputeBalanceOutputs aggregates net balance changes per token per account from the transfers.
148+
// For each transfer, it subtracts `Value` from `From` and adds `Value` to `To`.
149+
// Accounts with a resulting zero balance change for a given token are omitted.
150+
func (s TokenTransfers) ComputeBalanceOutputs(omitZeroBalances ...bool) TokenBalances {
151+
// key: token address + account address
152+
type key struct {
153+
token common.Address
154+
account common.Address
155+
}
156+
157+
balances := make(map[key]*big.Int)
158+
159+
for _, tr := range s {
160+
if tr == nil || tr.Value == nil {
161+
continue
162+
}
163+
164+
// From: subtract value
165+
kFrom := key{token: tr.Token, account: tr.From}
166+
if _, ok := balances[kFrom]; !ok {
167+
balances[kFrom] = new(big.Int)
168+
}
169+
balances[kFrom].Sub(balances[kFrom], tr.Value)
170+
171+
// To: add value
172+
kTo := key{token: tr.Token, account: tr.To}
173+
if _, ok := balances[kTo]; !ok {
174+
balances[kTo] = new(big.Int)
175+
}
176+
balances[kTo].Add(balances[kTo], tr.Value)
177+
}
178+
179+
// Convert to slice, excluding zero balances
180+
out := TokenBalances{}
181+
zero := big.NewInt(0)
182+
183+
for k, v := range balances {
184+
if v == nil || (len(omitZeroBalances) > 0 && omitZeroBalances[0] && v.Cmp(zero) == 0) {
185+
continue
186+
}
187+
out = append(out, &TokenBalance{
188+
Token: k.token,
189+
Account: k.account,
190+
Balance: new(big.Int).Set(v),
191+
})
192+
}
193+
194+
sort.Slice(out, func(i, j int) bool {
195+
bi := out[i].Balance
196+
bj := out[j].Balance
197+
// ascending by numeric value (negative first)
198+
cmp := bi.Cmp(bj)
199+
if cmp != 0 {
200+
return cmp < 0
201+
}
202+
// account lexicographic
203+
ai := out[i].Account.Hex()
204+
aj := out[j].Account.Hex()
205+
if ai != aj {
206+
return ai < aj
207+
}
208+
// token lexicographic
209+
ti := out[i].Token.Hex()
210+
tj := out[j].Token.Hex()
211+
return ti < tj
212+
})
213+
214+
return out
215+
}
216+
217+
func (b TokenBalances) OmitZeroBalances() TokenBalances {
218+
var out TokenBalances
219+
zero := big.NewInt(0)
220+
for _, bal := range b {
221+
if bal.Balance != nil && bal.Balance.Cmp(zero) != 0 {
222+
out = append(out, bal)
223+
}
224+
}
225+
return out
226+
}
227+
228+
func (b TokenBalances) FilterByAccount(account common.Address, optToken ...common.Address) TokenBalances {
229+
var out TokenBalances
230+
for _, bal := range b {
231+
if bal.Account == account {
232+
if len(optToken) == 0 || bal.Token == optToken[0] {
233+
out = append(out, bal)
234+
}
235+
}
236+
}
237+
return out
238+
}

0 commit comments

Comments
 (0)