|
| 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