查询地址的所有代币转账记录,支持 ERC20/ERC721/ERC1155
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Clients │────▶│ Gin API │────▶│ Blockchain │
│ (REST/HTTP) │ │ Service │ │ (Event Logs) │
└──────────────┘ └─────────────┘ └──────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│PostgreSQL│ │Elasticsearch││ Indexer │
│ (Storage)│ │ (Search) │ │ (Worker) │
└──────────┘ └──────────┘ └──────────┘
1// Transfer 事件签名2var (3 // ERC20/ERC721: Transfer(address,address,uint256)4 TransferEventSig = crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)"))5 // ERC1155: TransferSingle(address,address,address,uint256,uint256)6 TransferSingleSig = crypto.Keccak256Hash([]byte("TransferSingle(address,address,address,uint256,uint256)"))7)8 9type TokenTransfer struct {10 TxHash string `json:"tx_hash"`11 BlockNumber uint64 `json:"block_number"`12 Timestamp time.Time `json:"timestamp"`13 Contract string `json:"contract"`14 TokenType string `json:"token_type"`15 From string `json:"from"`16 To string `json:"to"`17 TokenID *big.Int `json:"token_id,omitempty"`18 Amount *big.Int `json:"amount"`19}20 21func (s *TransferService) ParseTransferLog(log *types.Log) (*TokenTransfer, error) {22 switch log.Topics[0] {23 case TransferEventSig:24 return s.parseERC20or721Transfer(log)25 case TransferSingleSig:26 return s.parseERC1155Single(log)27 default:28 return nil, fmt.Errorf("unknown event")29 }30}1func (i *Indexer) IndexRange(ctx context.Context, from, to uint64) error {2 for start := from; start <= to; start += uint64(i.batch) {3 end := min(start+uint64(i.batch)-1, to)4 5 query := ethereum.FilterQuery{6 FromBlock: big.NewInt(int64(start)),7 ToBlock: big.NewInt(int64(end)),8 Topics: [][]common.Hash{{TransferEventSig, TransferSingleSig}},9 }10 11 logs, err := i.client.FilterLogs(ctx, query)12 if err != nil {13 return fmt.Errorf("filter logs failed: %w", err)14 }15 16 transfers := make([]*TokenTransfer, 0, len(logs))17 for _, log := range logs {18 transfer, err := i.parseTransferLog(&log)19 if err != nil {20 continue21 }22 transfers = append(transfers, transfer)23 }24 25 if err := i.storage.BatchSave(ctx, transfers); err != nil {26 return err27 }28 log.Printf("Indexed blocks %d-%d: %d transfers", start, end, len(transfers))29 }30 return nil31}1type TransferQuery struct {2 Address string `form:"address"`3 Contract string `form:"contract"`4 TokenType string `form:"token_type"`5 Direction string `form:"direction"` // in, out, all6 StartTime *time.Time `form:"start_time"`7 EndTime *time.Time `form:"end_time"`8 Page int `form:"page"`9 PageSize int `form:"page_size"`10}11 12func (s *TransferStorage) Query(ctx context.Context, q *TransferQuery) (*TransferResult, error) {13 query := s.db.Model(&TokenTransfer{})14 15 if q.Address != "" {16 addr := strings.ToLower(q.Address)17 switch q.Direction {18 case "in":19 query = query.Where("LOWER(to_address) = ?", addr)20 case "out":21 query = query.Where("LOWER(from_address) = ?", addr)22 default:23 query = query.Where("LOWER(from_address) = ? OR LOWER(to_address) = ?", addr, addr)24 }25 }26 27 var total int6428 query.Count(&total)29 30 var transfers []*TokenTransfer31 offset := (q.Page - 1) * q.PageSize32 query.Order("timestamp DESC").Offset(offset).Limit(q.PageSize).Find(&transfers)33 34 return &TransferResult{Transfers: transfers, Total: total}, nil35}