Files
explorer-monorepo/backend/indexer/tokens/extractor.go

181 lines
5.0 KiB
Go

package tokens
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/jackc/pgx/v5/pgxpool"
)
// Extractor extracts token transfers from transaction logs
type Extractor struct {
db *pgxpool.Pool
chainID int
}
// NewExtractor creates a new token extractor
func NewExtractor(db *pgxpool.Pool, chainID int) *Extractor {
return &Extractor{
db: db,
chainID: chainID,
}
}
// ERC20 Transfer event signature: Transfer(address,address,uint256)
var ERC20TransferSignature = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
// ERC721 Transfer event signature: Transfer(address,address,uint256)
var ERC721TransferSignature = ERC20TransferSignature
// ERC1155 TransferSingle event signature: TransferSingle(address,address,address,uint256,uint256)
var ERC1155TransferSingleSignature = common.HexToHash("0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62")
// ExtractTokenTransfers extracts token transfers from logs
func (e *Extractor) ExtractTokenTransfers(ctx context.Context, txHash common.Hash, blockNumber int64, logs []types.Log) error {
for i, log := range logs {
// Check for ERC20/ERC721 Transfer
if len(log.Topics) == 3 && log.Topics[0] == ERC20TransferSignature {
if err := e.extractERC20Transfer(ctx, txHash, blockNumber, i, log); err != nil {
return fmt.Errorf("failed to extract ERC20 transfer: %w", err)
}
}
// Check for ERC1155 TransferSingle
if len(log.Topics) == 4 && log.Topics[0] == ERC1155TransferSingleSignature {
if err := e.extractERC1155Transfer(ctx, txHash, blockNumber, i, log); err != nil {
return fmt.Errorf("failed to extract ERC1155 transfer: %w", err)
}
}
}
return nil
}
// extractERC20Transfer extracts ERC20 transfer
func (e *Extractor) extractERC20Transfer(ctx context.Context, txHash common.Hash, blockNumber int64, logIndex int, log types.Log) error {
if len(log.Topics) != 3 || len(log.Data) != 32 {
return fmt.Errorf("invalid ERC20 transfer log")
}
from := common.BytesToAddress(log.Topics[1].Bytes())
to := common.BytesToAddress(log.Topics[2].Bytes())
amount := new(big.Int).SetBytes(log.Data)
// Determine token type (ERC20 or ERC721)
tokenType := "ERC20"
if len(log.Data) == 0 {
tokenType = "ERC721"
}
query := `
INSERT INTO token_transfers (
chain_id, transaction_hash, block_number, log_index,
token_address, token_type, from_address, to_address, amount
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
`
_, err := e.db.Exec(ctx, query,
e.chainID,
txHash.Hex(),
blockNumber,
logIndex,
log.Address.Hex(),
tokenType,
from.Hex(),
to.Hex(),
amount.String(),
)
if err != nil {
return err
}
// Update token holder count
return e.updateTokenStats(ctx, log.Address)
}
// extractERC1155Transfer extracts ERC1155 transfer
func (e *Extractor) extractERC1155Transfer(ctx context.Context, txHash common.Hash, blockNumber int64, logIndex int, log types.Log) error {
if len(log.Topics) != 4 || len(log.Data) != 64 {
return fmt.Errorf("invalid ERC1155 transfer log")
}
operator := common.BytesToAddress(log.Topics[1].Bytes())
from := common.BytesToAddress(log.Topics[2].Bytes())
to := common.BytesToAddress(log.Topics[3].Bytes())
tokenID := new(big.Int).SetBytes(log.Data[:32])
amount := new(big.Int).SetBytes(log.Data[32:])
query := `
INSERT INTO token_transfers (
chain_id, transaction_hash, block_number, log_index,
token_address, token_type, from_address, to_address, amount, token_id, operator
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
`
_, err := e.db.Exec(ctx, query,
e.chainID,
txHash.Hex(),
blockNumber,
logIndex,
log.Address.Hex(),
"ERC1155",
from.Hex(),
to.Hex(),
amount.String(),
tokenID.String(),
operator.Hex(),
)
if err != nil {
return err
}
return e.updateTokenStats(ctx, log.Address)
}
// updateTokenStats updates token statistics
func (e *Extractor) updateTokenStats(ctx context.Context, tokenAddress common.Address) error {
// Count holders
var holderCount int
err := e.db.QueryRow(ctx, `
SELECT COUNT(DISTINCT to_address)
FROM token_transfers
WHERE chain_id = $1 AND token_address = $2
`, e.chainID, tokenAddress.Hex()).Scan(&holderCount)
if err != nil {
holderCount = 0
}
// Count transfers
var transferCount int
err = e.db.QueryRow(ctx, `
SELECT COUNT(*)
FROM token_transfers
WHERE chain_id = $1 AND token_address = $2
`, e.chainID, tokenAddress.Hex()).Scan(&transferCount)
if err != nil {
transferCount = 0
}
// Update token
query := `
INSERT INTO tokens (chain_id, address, type, holder_count, transfer_count)
VALUES ($1, $2, 'ERC20', $3, $4)
ON CONFLICT (chain_id, address) DO UPDATE SET
holder_count = $3,
transfer_count = $4,
updated_at = NOW()
`
_, err = e.db.Exec(ctx, query, e.chainID, tokenAddress.Hex(), holderCount, transferCount)
return err
}