package track2 import ( "context" "fmt" "math/big" "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/jackc/pgx/v5/pgxpool" ) // TokenIndexer indexes ERC-20 token transfers for Track 2 type TokenIndexer struct { db *pgxpool.Pool client *ethclient.Client chainID int } // NewTokenIndexer creates a new token indexer func NewTokenIndexer(db *pgxpool.Pool, client *ethclient.Client, chainID int) *TokenIndexer { return &TokenIndexer{ db: db, client: client, chainID: chainID, } } // ERC20TransferEventSignature is the signature for ERC-20 Transfer event const ERC20TransferEventSignature = "Transfer(address,address,uint256)" // IndexTokenTransfers indexes token transfers from a transaction receipt func (ti *TokenIndexer) IndexTokenTransfers(ctx context.Context, receipt *types.Receipt, blockNumber uint64, blockHash common.Hash, timestamp time.Time) error { // Parse Transfer event signature transferEventSig := common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") // keccak256("Transfer(address,address,uint256)") for _, log := range receipt.Logs { // Check if this is a Transfer event if len(log.Topics) != 3 || log.Topics[0] != transferEventSig { continue } // Extract token contract, from, to, and value tokenContract := log.Address.Hex() from := common.BytesToAddress(log.Topics[1].Bytes()).Hex() to := common.BytesToAddress(log.Topics[2].Bytes()).Hex() // Decode value from data value := new(big.Int).SetBytes(log.Data) // Insert token transfer insertQuery := ` INSERT INTO token_transfers ( chain_id, transaction_hash, log_index, block_number, block_hash, timestamp, token_contract, from_address, to_address, value ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING ` _, err := ti.db.Exec(ctx, insertQuery, ti.chainID, receipt.TxHash.Hex(), log.Index, blockNumber, blockHash.Hex(), timestamp, tokenContract, from, to, value.String(), ) if err != nil { return fmt.Errorf("failed to insert token transfer: %w", err) } // Update token balances ti.updateTokenBalances(ctx, tokenContract, from, to, value) } return nil } // updateTokenBalances updates token balances for addresses func (ti *TokenIndexer) updateTokenBalances(ctx context.Context, tokenContract, from, to string, value *big.Int) { // Decrease from balance if from != "" && from != "0x0000000000000000000000000000000000000000" { updateFromQuery := ` INSERT INTO token_balances (address, token_contract, chain_id, balance, last_updated_timestamp) VALUES ($1, $2, $3, 0, NOW()) ON CONFLICT (address, token_contract, chain_id) DO UPDATE SET balance = GREATEST(0, token_balances.balance - $4::numeric), last_updated_timestamp = NOW(), updated_at = NOW() ` ti.db.Exec(ctx, updateFromQuery, strings.ToLower(from), strings.ToLower(tokenContract), ti.chainID, value.String()) } // Increase to balance if to != "" && to != "0x0000000000000000000000000000000000000000" { updateToQuery := ` INSERT INTO token_balances (address, token_contract, chain_id, balance, last_updated_timestamp) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (address, token_contract, chain_id) DO UPDATE SET balance = token_balances.balance + $4::numeric, last_updated_timestamp = NOW(), updated_at = NOW() ` ti.db.Exec(ctx, updateToQuery, strings.ToLower(to), strings.ToLower(tokenContract), ti.chainID, value.String()) } } // IndexBlockTokenTransfers indexes all token transfers in a block func (ti *TokenIndexer) IndexBlockTokenTransfers(ctx context.Context, blockNumber uint64) error { block, err := ti.client.BlockByNumber(ctx, big.NewInt(int64(blockNumber))) if err != nil { return fmt.Errorf("failed to get block: %w", err) } for _, tx := range block.Transactions() { receipt, err := ti.client.TransactionReceipt(ctx, tx.Hash()) if err != nil { continue } if err := ti.IndexTokenTransfers(ctx, receipt, blockNumber, block.Hash(), time.Unix(int64(block.Time()), 0)); err != nil { fmt.Printf("Failed to index token transfers for tx %s: %v\n", tx.Hash().Hex(), err) } } return nil }