init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
616
webapp-back/holders/scanner.go
Normal file
616
webapp-back/holders/scanner.go
Normal file
@@ -0,0 +1,616 @@
|
||||
package holders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
db "github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
)
|
||||
|
||||
// Config holds scanner configuration
|
||||
type Config struct {
|
||||
ChainID int
|
||||
RPCURL string
|
||||
YTVaults []VaultConfig
|
||||
YTLPAddress string
|
||||
DeploymentBlocks DeploymentBlocks
|
||||
PollInterval time.Duration
|
||||
BatchSize int64
|
||||
}
|
||||
|
||||
type VaultConfig struct {
|
||||
Name string
|
||||
Address string
|
||||
DeployBlock uint64
|
||||
}
|
||||
|
||||
type DeploymentBlocks struct {
|
||||
YTLP uint64
|
||||
}
|
||||
|
||||
// Scanner handles blockchain event scanning
|
||||
type Scanner struct {
|
||||
config Config
|
||||
client *ethclient.Client
|
||||
lastScannedBlock uint64
|
||||
isScanning bool
|
||||
mu sync.Mutex
|
||||
|
||||
// Address tracking
|
||||
ytAddresses map[string]map[string]int64 // vault -> address -> firstSeen
|
||||
lpAddresses map[string]int64 // address -> firstSeen
|
||||
}
|
||||
|
||||
// Event topics
|
||||
var (
|
||||
transferEventTopic = crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)"))
|
||||
)
|
||||
|
||||
// NewScanner creates a new blockchain scanner
|
||||
func NewScanner(config Config) (*Scanner, error) {
|
||||
client, err := ethclient.Dial(config.RPCURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Ethereum client: %w", err)
|
||||
}
|
||||
|
||||
return &Scanner{
|
||||
config: config,
|
||||
client: client,
|
||||
ytAddresses: make(map[string]map[string]int64),
|
||||
lpAddresses: make(map[string]int64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loadStateFromDB loads the last scanned block from database
|
||||
func (s *Scanner) loadStateFromDB() uint64 {
|
||||
database := db.GetDB()
|
||||
var state models.ScannerState
|
||||
result := database.Where("chain_id = ? AND scanner_type = ?", s.config.ChainID, "holder").First(&state)
|
||||
if result.Error != nil {
|
||||
return 0
|
||||
}
|
||||
return state.LastScannedBlock
|
||||
}
|
||||
|
||||
// saveStateToDB persists the last scanned block to database
|
||||
func (s *Scanner) saveStateToDB(block uint64) {
|
||||
database := db.GetDB()
|
||||
state := models.ScannerState{
|
||||
ScannerType: "holder",
|
||||
ChainID: s.config.ChainID,
|
||||
LastScannedBlock: block,
|
||||
}
|
||||
database.Where("chain_id = ? AND scanner_type = ?", s.config.ChainID, "holder").Assign(state).FirstOrCreate(&state)
|
||||
database.Model(&state).Update("last_scanned_block", block)
|
||||
}
|
||||
|
||||
// loadAddressesFromDB restores in-memory address maps from holder_snapshots
|
||||
func (s *Scanner) loadAddressesFromDB() {
|
||||
database := db.GetDB()
|
||||
var snapshots []models.HolderSnapshot
|
||||
database.Where("chain_id = ?", s.config.ChainID).Find(&snapshots)
|
||||
|
||||
for _, snap := range snapshots {
|
||||
switch snap.TokenType {
|
||||
case "ytLP":
|
||||
s.lpAddresses[snap.HolderAddress] = snap.FirstSeen
|
||||
default:
|
||||
for _, vault := range s.config.YTVaults {
|
||||
if vault.Name == snap.TokenType {
|
||||
if s.ytAddresses[vault.Address] == nil {
|
||||
s.ytAddresses[vault.Address] = make(map[string]int64)
|
||||
}
|
||||
s.ytAddresses[vault.Address][snap.HolderAddress] = snap.FirstSeen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("▶️ 从数据库加载了 %d 个历史地址", len(snapshots))
|
||||
}
|
||||
|
||||
// Start begins the scanning process
|
||||
func Start(config Config) error {
|
||||
scanner, err := NewScanner(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("=== Holder Scanner Started ===")
|
||||
log.Printf("RPC: %s\n", config.RPCURL)
|
||||
log.Printf("Poll Interval: %v\n", config.PollInterval)
|
||||
|
||||
// Check if we can resume from a previous scan
|
||||
lastBlock := scanner.loadStateFromDB()
|
||||
if lastBlock > 0 {
|
||||
log.Printf("▶️ 发现上次扫描记录,从区块 %d 续扫...", lastBlock)
|
||||
scanner.loadAddressesFromDB()
|
||||
scanner.lastScannedBlock = lastBlock
|
||||
} else {
|
||||
// Fresh start: full initial scan from deploy blocks
|
||||
log.Println("📊 首次扫描,从部署区块开始...")
|
||||
startTime := time.Now()
|
||||
if err := scanner.scanAll(context.Background(), true); err != nil {
|
||||
return fmt.Errorf("initial scan failed: %w", err)
|
||||
}
|
||||
log.Printf("✓ 初始扫描完成,耗时 %v\n", time.Since(startTime))
|
||||
}
|
||||
|
||||
// Start polling
|
||||
ticker := time.NewTicker(config.PollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("⏰ 开始轮询,每 %v 扫一次新区块...\n", config.PollInterval)
|
||||
|
||||
for range ticker.C {
|
||||
if err := scanner.incrementalScan(context.Background()); err != nil {
|
||||
log.Printf("✗ Incremental scan error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// minUint64 returns the smaller of two uint64 values.
|
||||
func minUint64(a, b uint64) uint64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// scanBatch scans ALL contract types for a single block range [fromBlock, toBlock].
|
||||
// The range must be ≤ BatchSize. After this call, every contract has been scanned to toBlock.
|
||||
func (s *Scanner) scanBatch(ctx context.Context, fromBlock, toBlock uint64) {
|
||||
log.Printf(" [Batch] Scanning blocks %d → %d\n", fromBlock, toBlock)
|
||||
s.scanYTVaultTransfers(ctx, fromBlock, toBlock)
|
||||
s.scanYTLPTransfers(ctx, fromBlock, toBlock)
|
||||
s.scanSwapEvents(ctx, fromBlock, toBlock)
|
||||
|
||||
// Checkpoint: all contracts have been scanned up to toBlock
|
||||
s.mu.Lock()
|
||||
s.lastScannedBlock = toBlock
|
||||
s.mu.Unlock()
|
||||
s.saveStateToDB(toBlock)
|
||||
}
|
||||
|
||||
// scanAll performs a full scan from deployment blocks, saving a checkpoint after every batch.
|
||||
// All contract types are scanned together per batch, so the checkpoint is always safe.
|
||||
func (s *Scanner) scanAll(ctx context.Context, isInitial bool) error {
|
||||
s.mu.Lock()
|
||||
if s.isScanning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("scan already in progress")
|
||||
}
|
||||
s.isScanning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
s.isScanning = false
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
latestBlock, err := s.client.BlockNumber(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Current block: %d\n", latestBlock)
|
||||
|
||||
// Determine the earliest block we need to scan across all contracts
|
||||
startBlock := s.lastScannedBlock + 1
|
||||
if isInitial {
|
||||
startBlock = latestBlock // will be lowered below
|
||||
for _, v := range s.config.YTVaults {
|
||||
startBlock = minUint64(startBlock, v.DeployBlock)
|
||||
}
|
||||
if s.config.YTLPAddress != "" && s.config.DeploymentBlocks.YTLP > 0 {
|
||||
startBlock = minUint64(startBlock, s.config.DeploymentBlocks.YTLP)
|
||||
}
|
||||
}
|
||||
if startBlock > latestBlock {
|
||||
log.Printf("📌 No new blocks to scan (startBlock %d > latestBlock %d)\n", startBlock, latestBlock)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("📊 Scanning blocks %d → %d in batches of %d\n", startBlock, latestBlock, s.config.BatchSize)
|
||||
|
||||
// Outer loop: one checkpoint per batch, all contracts scanned together
|
||||
for batchFrom := startBlock; batchFrom <= latestBlock; {
|
||||
batchTo := minUint64(batchFrom+uint64(s.config.BatchSize)-1, latestBlock)
|
||||
s.scanBatch(ctx, batchFrom, batchTo)
|
||||
batchFrom = batchTo + 1
|
||||
|
||||
// Rate limiting between batches
|
||||
if batchFrom <= latestBlock {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Balance snapshots: run once after all event scanning is done
|
||||
log.Printf("📊 Updating balance snapshots...\n")
|
||||
for _, vault := range s.config.YTVaults {
|
||||
if err := s.saveYTHolders(ctx, vault); err != nil {
|
||||
log.Printf(" [Snapshot] %s error: %v", vault.Name, err)
|
||||
}
|
||||
}
|
||||
if err := s.saveYTLPHolders(ctx); err != nil {
|
||||
log.Printf(" [Snapshot] ytLP error: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("📌 Scan complete. Last scanned block: %d\n", latestBlock)
|
||||
return nil
|
||||
}
|
||||
|
||||
// incrementalScan scans new blocks since last scan.
|
||||
// Incremental ranges are small (seconds of blocks), so a single batch suffices.
|
||||
func (s *Scanner) incrementalScan(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
if s.isScanning {
|
||||
s.mu.Unlock()
|
||||
log.Println("⏰ Skipping scan (previous scan still running)")
|
||||
return nil
|
||||
}
|
||||
s.isScanning = true
|
||||
lastBlock := s.lastScannedBlock
|
||||
s.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
s.isScanning = false
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
latestBlock, err := s.client.BlockNumber(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if latestBlock <= lastBlock {
|
||||
log.Printf("⏰ [%s] No new blocks (current: %d)\n", time.Now().Format("15:04:05"), latestBlock)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("\n%s\n", strings.Repeat("=", 60))
|
||||
log.Printf("⏰ [%s] Found new blocks %d → %d\n", time.Now().Format("15:04:05"), lastBlock+1, latestBlock)
|
||||
log.Printf("%s\n", strings.Repeat("=", 60))
|
||||
|
||||
startTime := time.Now()
|
||||
fromBlock := lastBlock + 1
|
||||
|
||||
// Incremental range may exceed BatchSize if server was down for a while; use batches
|
||||
for batchFrom := fromBlock; batchFrom <= latestBlock; {
|
||||
batchTo := minUint64(batchFrom+uint64(s.config.BatchSize)-1, latestBlock)
|
||||
s.scanBatch(ctx, batchFrom, batchTo)
|
||||
batchFrom = batchTo + 1
|
||||
if batchFrom <= latestBlock {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Update balance snapshots
|
||||
for _, vault := range s.config.YTVaults {
|
||||
if err := s.saveYTHolders(ctx, vault); err != nil {
|
||||
log.Printf(" [Snapshot] %s error: %v", vault.Name, err)
|
||||
}
|
||||
}
|
||||
if err := s.saveYTLPHolders(ctx); err != nil {
|
||||
log.Printf(" [Snapshot] ytLP error: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ Incremental scan completed in %v\n", time.Since(startTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanYTVaultTransfers scans Transfer events for all YT vaults in the given block range.
|
||||
// fromBlock/toBlock must already be a single batch (≤ BatchSize blocks).
|
||||
// Each vault skips blocks before its own deployBlock.
|
||||
// Does NOT save balance snapshots (call saveYTHolders separately after all batches complete).
|
||||
func (s *Scanner) scanYTVaultTransfers(ctx context.Context, fromBlock, toBlock uint64) error {
|
||||
for _, vault := range s.config.YTVaults {
|
||||
// Skip if vault not yet deployed in this range
|
||||
if vault.DeployBlock > toBlock {
|
||||
continue
|
||||
}
|
||||
effectiveFrom := fromBlock
|
||||
if vault.DeployBlock > effectiveFrom {
|
||||
effectiveFrom = vault.DeployBlock
|
||||
}
|
||||
|
||||
logs, err := s.queryBlockRange(ctx, common.HexToAddress(vault.Address), transferEventTopic, effectiveFrom, toBlock)
|
||||
if err != nil {
|
||||
log.Printf(" [Transfer] %s query error: %v", vault.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if s.ytAddresses[vault.Address] == nil {
|
||||
s.ytAddresses[vault.Address] = make(map[string]int64)
|
||||
}
|
||||
|
||||
for _, l := range logs {
|
||||
toAddress := common.BytesToAddress(l.Topics[2].Bytes()).Hex()
|
||||
if toAddress == "0x0000000000000000000000000000000000000000" {
|
||||
continue
|
||||
}
|
||||
if _, exists := s.ytAddresses[vault.Address][toAddress]; !exists {
|
||||
block, err := s.client.BlockByNumber(ctx, big.NewInt(int64(l.BlockNumber)))
|
||||
if err == nil {
|
||||
s.ytAddresses[vault.Address][toAddress] = int64(block.Time())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanYTLPTransfers scans Transfer events for the YT LP token in the given block range.
|
||||
// Does NOT save balance snapshots (call saveYTLPHolders separately after all batches complete).
|
||||
func (s *Scanner) scanYTLPTransfers(ctx context.Context, fromBlock, toBlock uint64) error {
|
||||
if s.config.YTLPAddress == "" {
|
||||
return nil
|
||||
}
|
||||
lpDeployBlock := s.config.DeploymentBlocks.YTLP
|
||||
if lpDeployBlock > toBlock {
|
||||
return nil
|
||||
}
|
||||
effectiveFrom := fromBlock
|
||||
if lpDeployBlock > effectiveFrom {
|
||||
effectiveFrom = lpDeployBlock
|
||||
}
|
||||
|
||||
logs, err := s.queryBlockRange(ctx, common.HexToAddress(s.config.YTLPAddress), transferEventTopic, effectiveFrom, toBlock)
|
||||
if err != nil {
|
||||
log.Printf(" [Transfer] ytLP query error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, l := range logs {
|
||||
toAddress := common.BytesToAddress(l.Topics[2].Bytes()).Hex()
|
||||
if toAddress == "0x0000000000000000000000000000000000000000" {
|
||||
continue
|
||||
}
|
||||
if _, exists := s.lpAddresses[toAddress]; !exists {
|
||||
block, err := s.client.BlockByNumber(ctx, big.NewInt(int64(l.BlockNumber)))
|
||||
if err == nil {
|
||||
s.lpAddresses[toAddress] = int64(block.Time())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// queryBlockRange queries logs for a single block range (no internal batching).
|
||||
// The caller is responsible for keeping the range within RPC limits (BatchSize).
|
||||
func (s *Scanner) queryBlockRange(ctx context.Context, contractAddr common.Address, topic common.Hash, fromBlock, toBlock uint64) ([]types.Log, error) {
|
||||
query := ethereum.FilterQuery{
|
||||
FromBlock: big.NewInt(int64(fromBlock)),
|
||||
ToBlock: big.NewInt(int64(toBlock)),
|
||||
Addresses: []common.Address{contractAddr},
|
||||
Topics: [][]common.Hash{{topic}},
|
||||
}
|
||||
logs, err := s.client.FilterLogs(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query logs [%d-%d]: %w", fromBlock, toBlock, err)
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// queryLogsInBatches queries logs in batches to avoid RPC limits.
|
||||
// Used only by incremental scans where range size is unpredictable.
|
||||
func (s *Scanner) queryLogsInBatches(ctx context.Context, contractAddr common.Address, topic common.Hash, fromBlock, toBlock uint64) ([]types.Log, error) {
|
||||
var allLogs []types.Log
|
||||
currentBlock := fromBlock
|
||||
|
||||
log.Printf(" Querying blocks %d -> %d (total: %d blocks)\n", fromBlock, toBlock, toBlock-fromBlock+1)
|
||||
|
||||
for currentBlock <= toBlock {
|
||||
endBlock := currentBlock + uint64(s.config.BatchSize)
|
||||
if endBlock > toBlock {
|
||||
endBlock = toBlock
|
||||
}
|
||||
|
||||
log.Printf(" Querying blocks %d - %d...\n", currentBlock, endBlock)
|
||||
|
||||
logs, err := s.queryBlockRange(ctx, contractAddr, topic, currentBlock, endBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allLogs = append(allLogs, logs...)
|
||||
log.Printf(" ✓ Got %d events\n", len(logs))
|
||||
|
||||
currentBlock = endBlock + 1
|
||||
|
||||
// Rate limiting
|
||||
if currentBlock <= toBlock {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(" Total: %d events\n\n", len(allLogs))
|
||||
return allLogs, nil
|
||||
}
|
||||
|
||||
// saveYTHolders queries balances and saves to database
|
||||
func (s *Scanner) saveYTHolders(ctx context.Context, vault VaultConfig) error {
|
||||
// ERC20 balanceOf ABI
|
||||
balanceOfABI, _ := abi.JSON(strings.NewReader(`[{"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]`))
|
||||
|
||||
database := db.GetDB()
|
||||
now := time.Now().Unix()
|
||||
holders := 0
|
||||
totalAddresses := len(s.ytAddresses[vault.Address])
|
||||
|
||||
contractAddr := common.HexToAddress(vault.Address)
|
||||
log.Printf("📞 [%s] 开始查询 %d 个地址的余额", vault.Name, totalAddresses)
|
||||
log.Printf("📍 [%s] 合约地址: %s", vault.Name, vault.Address)
|
||||
|
||||
processedCount := 0
|
||||
errorCount := 0
|
||||
zeroBalanceCount := 0
|
||||
|
||||
for address, firstSeen := range s.ytAddresses[vault.Address] {
|
||||
processedCount++
|
||||
|
||||
// Call balanceOf
|
||||
data, err := balanceOfABI.Pack("balanceOf", common.HexToAddress(address))
|
||||
if err != nil {
|
||||
log.Printf("❌ [%s] Pack balanceOf 失败: %s - %v", vault.Name, address, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := s.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &contractAddr,
|
||||
Data: data,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
log.Printf("❌ [%s] CallContract 失败: %s - %v", vault.Name, address, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
balance := new(big.Int).SetBytes(result)
|
||||
|
||||
// Log balance query result
|
||||
if processedCount <= 5 || balance.Cmp(big.NewInt(0)) > 0 {
|
||||
log.Printf(" [%s] 地址: %s, 余额: %s", vault.Name, address[:10]+"...", balance.String())
|
||||
}
|
||||
|
||||
if balance.Cmp(big.NewInt(0)) == 0 {
|
||||
zeroBalanceCount++
|
||||
continue
|
||||
}
|
||||
|
||||
holders++
|
||||
|
||||
// Upsert to database: create if not exists, only update balance/last_updated if exists
|
||||
holder := models.HolderSnapshot{
|
||||
HolderAddress: address,
|
||||
TokenType: vault.Name,
|
||||
TokenAddress: vault.Address,
|
||||
Balance: balance.String(),
|
||||
ChainID: s.config.ChainID,
|
||||
FirstSeen: firstSeen,
|
||||
LastUpdated: now,
|
||||
}
|
||||
var existing models.HolderSnapshot
|
||||
res := database.Where("holder_address = ? AND token_type = ?", address, vault.Name).First(&existing)
|
||||
if res.Error != nil {
|
||||
database.Create(&holder)
|
||||
} else {
|
||||
database.Model(&existing).Updates(map[string]interface{}{
|
||||
"balance": balance.String(),
|
||||
"last_updated": now,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
log.Printf("📊 [%s] 查询完成统计:", vault.Name)
|
||||
log.Printf(" • 总地址数: %d", totalAddresses)
|
||||
log.Printf(" • 已处理: %d", processedCount)
|
||||
log.Printf(" • 余额>0: %d ✅", holders)
|
||||
log.Printf(" • 余额=0: %d", zeroBalanceCount)
|
||||
log.Printf(" • 错误数: %d", errorCount)
|
||||
log.Printf(" • 已保存到数据库: %d", holders)
|
||||
log.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveYTLPHolders saves ytLP holders to database
|
||||
func (s *Scanner) saveYTLPHolders(ctx context.Context) error {
|
||||
balanceOfABI, _ := abi.JSON(strings.NewReader(`[{"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]`))
|
||||
|
||||
database := db.GetDB()
|
||||
now := time.Now().Unix()
|
||||
holders := 0
|
||||
totalAddresses := len(s.lpAddresses)
|
||||
|
||||
contractAddr := common.HexToAddress(s.config.YTLPAddress)
|
||||
log.Printf("📞 [ytLP] 开始查询 %d 个地址的余额", totalAddresses)
|
||||
log.Printf("📍 [ytLP] 合约地址: %s", s.config.YTLPAddress)
|
||||
|
||||
processedCount := 0
|
||||
errorCount := 0
|
||||
zeroBalanceCount := 0
|
||||
|
||||
for address, firstSeen := range s.lpAddresses {
|
||||
processedCount++
|
||||
|
||||
data, err := balanceOfABI.Pack("balanceOf", common.HexToAddress(address))
|
||||
if err != nil {
|
||||
log.Printf("❌ [ytLP] Pack balanceOf 失败: %s - %v", address, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := s.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &contractAddr,
|
||||
Data: data,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
log.Printf("❌ [ytLP] CallContract 失败: %s - %v", address, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
balance := new(big.Int).SetBytes(result)
|
||||
|
||||
// Log balance query result
|
||||
if processedCount <= 5 || balance.Cmp(big.NewInt(0)) > 0 {
|
||||
log.Printf(" [ytLP] 地址: %s, 余额: %s", address[:10]+"...", balance.String())
|
||||
}
|
||||
|
||||
if balance.Cmp(big.NewInt(0)) == 0 {
|
||||
zeroBalanceCount++
|
||||
continue
|
||||
}
|
||||
|
||||
holders++
|
||||
|
||||
holder := models.HolderSnapshot{
|
||||
HolderAddress: address,
|
||||
TokenType: "ytLP",
|
||||
TokenAddress: s.config.YTLPAddress,
|
||||
Balance: balance.String(),
|
||||
ChainID: s.config.ChainID,
|
||||
FirstSeen: firstSeen,
|
||||
LastUpdated: now,
|
||||
}
|
||||
var existing models.HolderSnapshot
|
||||
res := database.Where("holder_address = ? AND token_type = ?", address, "ytLP").First(&existing)
|
||||
if res.Error != nil {
|
||||
database.Create(&holder)
|
||||
} else {
|
||||
database.Model(&existing).Updates(map[string]interface{}{
|
||||
"balance": balance.String(),
|
||||
"last_updated": now,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
log.Printf("📊 [ytLP] 查询完成统计:")
|
||||
log.Printf(" • 总地址数: %d", totalAddresses)
|
||||
log.Printf(" • 已处理: %d", processedCount)
|
||||
log.Printf(" • 余额>0: %d ✅", holders)
|
||||
log.Printf(" • 余额=0: %d", zeroBalanceCount)
|
||||
log.Printf(" • 错误数: %d", errorCount)
|
||||
log.Printf(" • 已保存到数据库: %d", holders)
|
||||
log.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user