Files
explorer-monorepo/backend/indexer/track2/token_indexer.go

137 lines
4.2 KiB
Go

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
}