133 lines
3.8 KiB
Go
133 lines
3.8 KiB
Go
|
|
package holders
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"log"
|
|||
|
|
"net/http"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
|||
|
|
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// RegisterRoutes registers holders API routes
|
|||
|
|
func RegisterRoutes(router *gin.RouterGroup) {
|
|||
|
|
router.GET("/stats", GetStats)
|
|||
|
|
router.GET("/:tokenType", GetHoldersByType)
|
|||
|
|
router.POST("/update", UpdateHolders)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStats returns aggregated statistics for all token types
|
|||
|
|
func GetStats(c *gin.Context) {
|
|||
|
|
db := common.GetDB()
|
|||
|
|
|
|||
|
|
log.Printf("📊 [API] 收到获取统计数据请求 - IP: %s", c.ClientIP())
|
|||
|
|
|
|||
|
|
var stats []models.HolderStats
|
|||
|
|
err := db.Raw(`
|
|||
|
|
SELECT
|
|||
|
|
token_type,
|
|||
|
|
COUNT(DISTINCT holder_address) as holder_count
|
|||
|
|
FROM holder_snapshots
|
|||
|
|
WHERE balance != '0' AND balance != '' AND LENGTH(balance) > 0
|
|||
|
|
GROUP BY token_type
|
|||
|
|
`).Scan(&stats).Error
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("❌ [API] 查询统计数据失败: %v", err)
|
|||
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|||
|
|
"success": false,
|
|||
|
|
"error": "Failed to fetch stats",
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("✅ [API] 成功返回统计数据 - %d 个token类型", len(stats))
|
|||
|
|
for _, stat := range stats {
|
|||
|
|
log.Printf(" - %s: %d 个持有者", stat.TokenType, stat.HolderCount)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true,
|
|||
|
|
"data": stats,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetHoldersByType returns holders for a specific token type
|
|||
|
|
func GetHoldersByType(c *gin.Context) {
|
|||
|
|
tokenType := c.Param("tokenType")
|
|||
|
|
db := common.GetDB()
|
|||
|
|
|
|||
|
|
// 记录请求日志
|
|||
|
|
log.Printf("🔍 [API] 收到获取持有者请求 - Token类型: %s, IP: %s", tokenType, c.ClientIP())
|
|||
|
|
|
|||
|
|
var holders []models.HolderSnapshot
|
|||
|
|
err := db.Where("token_type = ? AND balance != ? AND balance != '' AND LENGTH(balance) > 0", tokenType, "0").
|
|||
|
|
Order("LENGTH(balance) DESC, balance DESC").
|
|||
|
|
Find(&holders).Error
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("❌ [API] 查询持有者失败 - Token类型: %s, 错误: %v", tokenType, err)
|
|||
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|||
|
|
"success": false,
|
|||
|
|
"error": "Failed to fetch holders",
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("✅ [API] 成功返回 %d 个持有者 - Token类型: %s", len(holders), tokenType)
|
|||
|
|
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true,
|
|||
|
|
"data": holders,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Global scanner instance
|
|||
|
|
var globalScanner *Scanner
|
|||
|
|
|
|||
|
|
// StartAllScanners 为所有支持的链启动 holder 扫描器(后台常驻)
|
|||
|
|
func StartAllScanners() {
|
|||
|
|
supportedChains := []int64{97, 421614}
|
|||
|
|
for _, chainID := range supportedChains {
|
|||
|
|
config, err := LoadConfigFromDB(chainID)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("[HolderScanner] Chain %d 配置加载失败: %v", chainID, err)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if len(config.YTVaults) == 0 && config.YTLPAddress == "" {
|
|||
|
|
log.Printf("[HolderScanner] Chain %d 无活跃资产,跳过", chainID)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
go func(cfg Config) {
|
|||
|
|
log.Printf("[HolderScanner] Chain %d 启动,YTVaults=%d ytLP=%s",
|
|||
|
|
cfg.ChainID, len(cfg.YTVaults), cfg.YTLPAddress)
|
|||
|
|
if err := Start(cfg); err != nil {
|
|||
|
|
log.Printf("[HolderScanner] Chain %d 异常退出: %v", cfg.ChainID, err)
|
|||
|
|
}
|
|||
|
|
}(config)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UpdateHolders triggers blockchain data update (admin only)
|
|||
|
|
func UpdateHolders(c *gin.Context) {
|
|||
|
|
if globalScanner == nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true,
|
|||
|
|
"message": "Scanner is running in background. It will process new blocks on next tick.",
|
|||
|
|
"timestamp": time.Now().Unix(),
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
go func() {
|
|||
|
|
if err := globalScanner.incrementalScan(c.Request.Context()); err != nil {
|
|||
|
|
log.Printf("[HolderScanner] Manual update error: %v", err)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true,
|
|||
|
|
"message": "Manual scan triggered.",
|
|||
|
|
"timestamp": time.Now().Unix(),
|
|||
|
|
})
|
|||
|
|
}
|