支持多链多代币余额查询,批量地址查询,历史余额快照
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Clients │────▶│ Gin API │────▶│ Blockchain │
│ (REST/HTTP) │ │ Service │ │ (go-ethereum)│
└──────────────┘ └─────────────┘ └──────────────┘
│
┌──────┴──────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Redis │ │PostgreSQL│
│ (Cache) │ │ (History)│
└──────────┘ └──────────┘
1package main2 3import (4 "github.com/gin-gonic/gin"5)6 7func main() {8 r := gin.Default()9 10 // 中间件11 r.Use(RateLimitMiddleware())12 r.Use(CacheMiddleware())13 14 // 路由组15 v1 := r.Group("/api/v1")16 {17 v1.GET("/balance/:chain/:address", GetBalance)18 v1.POST("/balance/batch", GetBatchBalance)19 v1.GET("/tokens/:chain/:address", GetTokenBalances)20 v1.GET("/history/:chain/:address", GetBalanceHistory)21 }22 23 r.Run(":8080")24}25 26type BalanceResponse struct {27 Address string `json:"address"`28 Chain string `json:"chain"`29 Native *BalanceInfo `json:"native"`30 Tokens []*TokenBalance `json:"tokens,omitempty"`31}32 33type BalanceInfo struct {34 Balance string `json:"balance"`35 Symbol string `json:"symbol"`36 Decimals int `json:"decimals"`37 USD float64 `json:"usd,omitempty"`38}1type BalanceService struct {2 clients map[string]*ethclient.Client3 cache *redis.Client4}5 6func (s *BalanceService) GetNativeBalance(ctx context.Context, chain, address string) (*BalanceInfo, error) {7 // 检查缓存8 cacheKey := fmt.Sprintf("balance:%s:%s:native", chain, address)9 if cached, err := s.cache.Get(ctx, cacheKey).Result(); err == nil {10 var info BalanceInfo11 json.Unmarshal([]byte(cached), &info)12 return &info, nil13 }14 15 client, ok := s.clients[chain]16 if !ok {17 return nil, fmt.Errorf("unsupported chain: %s", chain)18 }19 20 addr := common.HexToAddress(address)21 balance, err := client.BalanceAt(ctx, addr, nil)22 if err != nil {23 return nil, err24 }25 26 info := &BalanceInfo{27 Balance: weiToEther(balance),28 Symbol: getChainSymbol(chain),29 Decimals: 18,30 }31 32 // 缓存 30 秒33 data, _ := json.Marshal(info)34 s.cache.Set(ctx, cacheKey, data, 30*time.Second)35 36 return info, nil37}38 39func weiToEther(wei *big.Int) string {40 fbal := new(big.Float).SetInt(wei)41 ethValue := new(big.Float).Quo(fbal, big.NewFloat(1e18))42 return ethValue.Text('f', 8)43}1// ERC20 ABI (简化版)2const erc20ABI = `[3 {"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"},4 {"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"type":"function"},5 {"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"type":"function"}6]`7 8func (s *BalanceService) GetTokenBalance(ctx context.Context, chain, wallet, token string) (*TokenBalance, error) {9 client := s.clients[chain]10 11 parsed, _ := abi.JSON(strings.NewReader(erc20ABI))12 contract := common.HexToAddress(token)13 walletAddr := common.HexToAddress(wallet)14 15 // 查询余额16 balanceData, _ := parsed.Pack("balanceOf", walletAddr)17 result, err := client.CallContract(ctx, ethereum.CallMsg{18 To: &contract,19 Data: balanceData,20 }, nil)21 if err != nil {22 return nil, err23 }24 25 balance := new(big.Int).SetBytes(result)26 27 // 查询 decimals 和 symbol28 decimals := s.getDecimals(ctx, client, contract)29 symbol := s.getSymbol(ctx, client, contract)30 31 return &TokenBalance{32 Token: token,33 Symbol: symbol,34 Balance: formatBalance(balance, decimals),35 Decimals: decimals,36 }, nil37}1// Multicall3 合约地址 (多链通用)2const Multicall3Address = "0xcA11bde05977b3631167028862bE2a173976CA11"3 4type MulticallService struct {5 client *ethclient.Client6 contract *bind.BoundContract7}8 9func (s *MulticallService) BatchGetBalances(ctx context.Context, wallet string, tokens []string) ([]*TokenBalance, error) {10 calls := make([]Multicall3Call, len(tokens))11 12 for i, token := range tokens {13 data, _ := erc20ABI.Pack("balanceOf", common.HexToAddress(wallet))14 calls[i] = Multicall3Call{15 Target: common.HexToAddress(token),16 CallData: data,17 }18 }19 20 // 一次 RPC 调用获取所有余额21 results, err := s.contract.Aggregate(ctx, calls)22 if err != nil {23 return nil, err24 }25 26 balances := make([]*TokenBalance, len(tokens))27 for i, result := range results {28 balance := new(big.Int).SetBytes(result)29 balances[i] = &TokenBalance{30 Token: tokens[i],31 Balance: balance.String(),32 }33 }34 35 return balances, nil36}