Files
assetx/webapp-back/holders/README.md
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

11 KiB
Raw Permalink Blame History

Holders API - 持有者统计服务

📋 概述

这个模块提供了区块链代币持有者的统计和查询功能,包括:

  • YT 代币持有者 (YT-A, YT-B, YT-C)
  • ytLP 代币持有者
  • Lending 提供者

🏗️ 架构

┌─────────────────┐      ┌──────────────────┐      ┌──────────────┐
│   Frontend      │─────▶│   API Server     │─────▶│   Database   │
│  (Ant Design)   │      │  (Gin + Gorm)    │      │  (MySQL/PG)  │
└─────────────────┘      └──────────────────┘      └──────────────┘
                                  │                        ▲
                                  │                        │
                                  ▼                        │
                         ┌──────────────────┐            │
                         │  Scanner Service │────────────┘
                         │  (Blockchain)    │
                         └──────────────────┘
                                  │
                                  ▼
                         ┌──────────────────┐
                         │   Arbitrum RPC   │
                         │   (Sepolia)      │
                         └──────────────────┘

📁 文件结构

holders/
├── routers.go      # API 路由和处理函数
├── scanner.go      # 区块链扫描器核心逻辑
└── README.md       # 本文档

cmd/scanner/
└── main.go         # 扫描器服务启动入口

models/
└── holder.go       # 数据库模型定义

🔧 安装依赖

1. 安装 Go 依赖

cd /home/coder/myprojects/assetx/golang-gin-realworld

# 添加 ethereum 依赖
go get github.com/ethereum/go-ethereum
go get github.com/ethereum/go-ethereum/ethclient
go get github.com/ethereum/go-ethereum/accounts/abi
go get github.com/ethereum/go-ethereum/common
go get github.com/ethereum/go-ethereum/core/types
go get github.com/ethereum/go-ethereum/crypto

# 更新依赖
go mod tidy

2. 数据库迁移

数据库表会在启动时自动创建(通过 GORM AutoMigrate

CREATE TABLE holder_snapshots (
    id BIGSERIAL PRIMARY KEY,
    holder_address VARCHAR(42) NOT NULL,
    token_type VARCHAR(50) NOT NULL,
    token_address VARCHAR(42) NOT NULL,
    balance VARCHAR(78) NOT NULL,
    chain_id INTEGER NOT NULL,
    first_seen BIGINT NOT NULL,
    last_updated BIGINT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_holder_token (holder_address, token_type),
    INDEX idx_token_time (token_type, last_updated)
);

🚀 启动服务

方法一:分别启动(推荐开发环境)

1. 启动 API 服务器

cd /home/coder/myprojects/assetx/golang-gin-realworld

# 设置环境变量
export DB_TYPE=mysql
export DB_HOST=localhost
export DB_PORT=3306
export DB_USER=root
export DB_PASSWORD=your_password
export DB_NAME=assetx
export GIN_MODE=release
export PORT=8080

# 启动 API 服务
go run main.go

2. 启动扫描器服务

cd /home/coder/myprojects/assetx/golang-gin-realworld

# 设置 RPC URL可选有默认值
export RPC_URL=https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07

# 启动扫描器
go run cmd/scanner/main.go

方法二:使用 PM2推荐生产环境

1. 创建 PM2 配置文件

// ecosystem.holders.config.js
module.exports = {
  apps: [
    {
      name: 'assetx-api',
      script: './golang-gin-realworld-example-app',
      env: {
        DB_TYPE: 'mysql',
        DB_HOST: 'localhost',
        DB_PORT: '3306',
        DB_USER: 'root',
        DB_PASSWORD: 'your_password',
        DB_NAME: 'assetx',
        GIN_MODE: 'release',
        PORT: '8080'
      }
    },
    {
      name: 'holder-scanner',
      script: 'go',
      args: 'run cmd/scanner/main.go',
      env: {
        DB_TYPE: 'mysql',
        DB_HOST: 'localhost',
        DB_PORT: '3306',
        DB_USER: 'root',
        DB_PASSWORD: 'your_password',
        DB_NAME: 'assetx',
        RPC_URL: 'https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07'
      }
    }
  ]
};

2. 启动服务

# 先编译 API 服务器
go build -o golang-gin-realworld-example-app

# 使用 PM2 启动
pm2 start ecosystem.holders.config.js
pm2 logs
pm2 status

📡 API 接口

1. GET /api/holders/stats

获取所有代币类型的统计信息。

请求

curl http://localhost:8080/api/holders/stats

响应

{
  "success": true,
  "data": [
    {
      "token_type": "YT-A",
      "holder_count": 156,
      "total_balance": "1250000000000000000000"
    },
    {
      "token_type": "YT-B",
      "holder_count": 89,
      "total_balance": "780000000000000000000"
    }
  ]
}

2. GET /api/holders/:tokenType

获取特定代币的持有者列表。

参数

  • tokenType: YT-A | YT-B | YT-C | ytLP | Lending

请求

curl http://localhost:8080/api/holders/YT-A

响应

{
  "success": true,
  "data": [
    {
      "id": 1,
      "holder_address": "0x1234567890abcdef1234567890abcdef12345678",
      "token_type": "YT-A",
      "token_address": "0x97204190B35D9895a7a47aa7BaC61ac08De3cF05",
      "balance": "100000000000000000000",
      "chain_id": 421614,
      "first_seen": 1707800000,
      "last_updated": 1707900000
    }
  ]
}

3. POST /api/holders/update

手动触发数据更新(需要管理员权限)。

请求

curl -X POST http://localhost:8080/api/holders/update \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN"

响应

{
  "success": true,
  "message": "Update triggered successfully",
  "timestamp": 1707900000
}

⚙️ 配置说明

扫描器配置

cmd/scanner/main.go 中可以修改以下配置:

scannerConfig := holders.Config{
    RPCURL: "your_rpc_url",           // RPC 节点地址
    PollInterval: 10 * time.Second,   // 轮询间隔10秒
    BatchSize: 9999,                  // 每批次查询的区块数

    // 代币合约地址
    YTVaults: []holders.VaultConfig{
        {Name: "YT-A", Address: "0x..."},
        {Name: "YT-B", Address: "0x..."},
        {Name: "YT-C", Address: "0x..."},
    },
    YTLPAddress: "0x...",
    LendingAddress: "0x...",

    // 部署区块号(从这里开始扫描)
    DeploymentBlocks: holders.DeploymentBlocks{
        YTVaults: 227339300,
        YTLP:     227230270,
        Lending:  227746053,
    },
}

环境变量

变量名 说明 默认值
RPC_URL Arbitrum RPC 节点地址 zan.top URL
DB_TYPE 数据库类型 sqlite
DB_HOST 数据库主机 localhost
DB_PORT 数据库端口 3306
DB_USER 数据库用户名 root
DB_PASSWORD 数据库密码 -
DB_NAME 数据库名称 assetx

🔍 工作原理

扫描流程

  1. 初始扫描

    • 从合约部署区块开始
    • 扫描所有历史 Transfer/Supply 事件
    • 记录所有曾经持有代币的地址
    • 查询当前余额并保存到数据库
  2. 增量扫描

    • 每 10 秒检查一次新区块
    • 只扫描自上次扫描后的新区块
    • 更新新地址和余额变化
    • 持续追踪地址首次出现时间
  3. 数据存储

    • 使用 holder_snapshots 表存储快照
    • 余额以 wei 格式字符串存储(避免精度丢失)
    • 记录首次持有时间和最后更新时间
    • 支持唯一约束address + token_type

性能优化

  • 批量查询区块事件(每次最多 9999 个区块)
  • 增量扫描(只查询新区块)
  • 地址缓存(减少重复查询)
  • 并发控制(防止重复扫描)
  • 速率限制(避免 RPC 限流)

📊 监控和日志

查看扫描器日志

# 实时查看日志
pm2 logs holder-scanner

# 或者直接运行时查看
go run cmd/scanner/main.go

日志示例

=== Holder Scanner Service ===
✓ Configuration loaded
✓ Database tables checked
🚀 Starting blockchain scanner...
=== Holder Scanner Started ===
RPC: https://api.zan.top/node/v1/arb/sepolia/...
Poll Interval: 10s
📊 Starting initial scan...
Current block: 227950000

1. Scanning YT Vaults...
  Scanning YT-A (0x9720...)...
    Querying blocks 227339300 -> 227950000 (total: 610700 blocks)
    Querying blocks 227339300 - 227349299...
    ✓ Got 45 events
    ...
    Found 23 new addresses, total tracking: 156
  YT-A: 156 holders saved

2. Scanning ytLP...
  ytLP: 67 holders saved

3. Scanning Lending...
  Lending: 43 holders saved

📌 Last scanned block: 227950000
✓ Initial scan completed in 2m34s

⏰ Starting polling every 10s...
⏰ [15:30:45] No new blocks (current: 227950000)
⏰ [15:30:55] Found new blocks
🔄 Incremental scan: blocks 227950001 -> 227950015
✓ Incremental scan completed in 1.2s

🐛 故障排查

常见问题

1. RPC 连接失败

Error: failed to connect to Ethereum client

解决: 检查 RPC_URL 是否正确,网络是否通畅

2. 数据库连接失败

Error: failed to connect to database

解决: 检查数据库配置和权限

3. 查询超时

Error: context deadline exceeded

解决: 减小 BatchSize 参数(如改为 5000

4. 内存占用过高

OOM or high memory usage

解决: 增加批次间隔时间或减小批次大小

数据验证

检查数据库中的数据

-- 查看所有代币类型的持有者数量
SELECT token_type, COUNT(*) as count
FROM holder_snapshots
GROUP BY token_type;

-- 查看 YT-A 的前 10 名持有者
SELECT holder_address, balance
FROM holder_snapshots
WHERE token_type = 'YT-A'
ORDER BY CAST(balance AS DECIMAL) DESC
LIMIT 10;

-- 查看最近更新的记录
SELECT * FROM holder_snapshots
ORDER BY last_updated DESC
LIMIT 10;

🔐 安全注意事项

  1. RPC 密钥保护

    • 不要在代码中硬编码 RPC URL
    • 使用环境变量或配置文件
    • 限制 RPC API Key 的权限
  2. 数据库安全

    • 使用强密码
    • 限制数据库访问权限
    • 定期备份数据
  3. API 权限

    • /update 接口需要管理员权限
    • 建议添加请求频率限制
    • 记录操作日志

📝 TODO

  • 添加 WebSocket 支持(实时推送)
  • 实现手动触发扫描的实际逻辑
  • 添加 Prometheus 监控指标
  • 支持多链配置
  • 添加数据导出功能

📞 联系方式

如有问题请联系开发团队或查看项目文档。


最后更新: 2024-02-13