181 lines
5.0 KiB
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
|
|
}
|
|
|