修复tron

This commit is contained in:
zhx 2026-07-23 11:27:12 +08:00
parent 9a27923d69
commit cb0cacdcc5
2 changed files with 78 additions and 15 deletions

View File

@ -1312,7 +1312,8 @@ func socialRequirementMetricDefinitions(section string) []MetricDefinition {
default: default:
return []MetricDefinition{ return []MetricDefinition{
{Metric: "app_download_users", Definition: "客户端 app_first_open 去重人数;不代表应用商店真实下载量。"}, {Metric: "app_download_users", Definition: "客户端 app_first_open 去重人数;不代表应用商店真实下载量。"},
{Metric: "registered_users", Definition: "UserRegistered 服务端事实去重人数。"}, // registered_users 保留历史字段兼容,业务展示统一为“新增用户”;资格和幂等边界由 UserRegistered 生产、消费链路保证。
{Metric: "registered_users", Definition: "注册成功的新用户人数:仅统计资料完成后由 user-service 发布的 UserRegistered 服务端事实按用户去重未完成资料、quick_account、game_robot 不计MQ 重投不重复。"},
{Metric: "new_user_d1_retention_rate", Definition: "新普通用户次日活跃留存。"}, {Metric: "new_user_d1_retention_rate", Definition: "新普通用户次日活跃留存。"},
} }
} }
@ -1421,7 +1422,7 @@ func socialRequirementColumns(section string) []SocialRequirementColumn {
default: default:
return []SocialRequirementColumn{ return []SocialRequirementColumn{
{Key: "app_download_users", Label: "下载APP人数", Type: "count", Tooltip: "客户端 app_first_open 去重人数"}, {Key: "app_download_users", Label: "下载APP人数", Type: "count", Tooltip: "客户端 app_first_open 去重人数"},
{Key: "registered_users", Label: "注册成功用户", Type: "count"}, {Key: "registered_users", Label: "新增用户", Type: "count", Tooltip: "注册成功的新用户人数:资料完成后产生 UserRegistered 服务端事实并按用户去重未完成资料、quick_account、game_robot 不计MQ 重投不重复"},
{Key: "new_host_users", Label: "新主播用户", Type: "count"}, {Key: "new_host_users", Label: "新主播用户", Type: "count"},
{Key: "new_normal_users", Label: "新普通用户", Type: "count"}, {Key: "new_normal_users", Label: "新普通用户", Type: "count"},
{Key: "registration_success_rate", Label: "注册成功率", Type: "ratio"}, {Key: "registration_success_rate", Label: "注册成功率", Type: "ratio"},

View File

@ -1,9 +1,13 @@
package trongrid package trongrid
import ( import (
"bytes"
"context" "context"
"crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"io" "io"
"math/big"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -32,7 +36,8 @@ func New(cfg config.TronGridConfig) *Client {
httpClient: &http.Client{Timeout: timeout}, httpClient: &http.Client{Timeout: timeout},
apiBaseURL: strings.TrimRight(strings.TrimSpace(cfg.APIBaseURL), "/"), apiBaseURL: strings.TrimRight(strings.TrimSpace(cfg.APIBaseURL), "/"),
apiKey: strings.TrimSpace(cfg.APIKey), apiKey: strings.TrimSpace(cfg.APIKey),
contractAddress: strings.ToLower(strings.TrimSpace(cfg.USDTContractAddress)), // Base58Check 区分大小写;保存原始形式,比较时再统一解码为账户 Hex。
contractAddress: strings.TrimSpace(cfg.USDTContractAddress),
} }
} }
@ -42,10 +47,13 @@ func (c *Client) VerifyUSDTTransfer(ctx context.Context, txHash string, toAddres
return "", xerr.New(xerr.Unavailable, "tron grid client is not configured") return "", xerr.New(xerr.Unavailable, "tron grid client is not configured")
} }
txHash = strings.TrimSpace(txHash) txHash = strings.TrimSpace(txHash)
toAddress = strings.ToLower(strings.TrimSpace(toAddress))
if txHash == "" || toAddress == "" || amountMinor <= 0 { if txHash == "" || toAddress == "" || amountMinor <= 0 {
return "", xerr.New(xerr.InvalidArgument, "usdt transfer verification request is incomplete") return "", xerr.New(xerr.InvalidArgument, "usdt transfer verification request is incomplete")
} }
expectedAddress, ok := normalizeTronAddress(toAddress)
if !ok {
return "", xerr.New(xerr.InvalidArgument, "tron receive address is invalid")
}
// H5 商品金额以 USD cent 保存USDT 使用 6 位小数1 cent = 0.01 USDT = 10,000 atomic units。 // H5 商品金额以 USD cent 保存USDT 使用 6 位小数1 cent = 0.01 USDT = 10,000 atomic units。
requiredAtomicAmount := amountMinor * 10_000 requiredAtomicAmount := amountMinor * 10_000
events, rawJSON, err := c.listTransferEvents(ctx, txHash) events, rawJSON, err := c.listTransferEvents(ctx, txHash)
@ -59,7 +67,10 @@ func (c *Client) VerifyUSDTTransfer(ctx context.Context, txHash string, toAddres
if !c.contractMatches(event.ContractAddress) { if !c.contractMatches(event.ContractAddress) {
continue continue
} }
if strings.ToLower(strings.TrimSpace(event.Result.To)) != toAddress { // TronGrid 的 event result.to 可能返回 0x/41 Hex而后台保存的是 Base58Check。
// 两端先归一为去除 TRON 网络前缀的 20-byte Hex避免同一地址因编码形式不同被误判。
eventAddress, validAddress := normalizeTronAddress(event.Result.To)
if !validAddress || eventAddress != expectedAddress {
continue continue
} }
value, err := strconv.ParseInt(strings.TrimSpace(event.Result.Value), 10, 64) value, err := strconv.ParseInt(strings.TrimSpace(event.Result.Value), 10, 64)
@ -102,15 +113,66 @@ func (c *Client) listTransferEvents(ctx context.Context, txHash string) ([]trans
} }
func (c *Client) contractMatches(value string) bool { func (c *Client) contractMatches(value string) bool {
value = strings.ToLower(strings.TrimSpace(value)) configured, configuredOK := normalizeTronAddress(c.contractAddress)
if value == "" || c.contractAddress == "" { actual, actualOK := normalizeTronAddress(value)
return false return configuredOK && actualOK && configured == actual
}
const tronBase58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
// normalizeTronAddress 接受后台常用的 Base58Check以及 TronGrid 可能返回的
// 0x + 20-byte、41 + 20-byte 或裸 20-byte Hex统一输出账户主体的 40 位小写 Hex。
// Base58 输入必须通过网络前缀和双 SHA-256 checksum 校验,避免把格式正确但已损坏的地址用于放币判断。
func normalizeTronAddress(value string) (string, bool) {
value = strings.TrimSpace(value)
if value == "" {
return "", false
} }
if value == c.contractAddress { hexValue := strings.TrimPrefix(strings.ToLower(value), "0x")
return true if len(hexValue) == 42 && strings.HasPrefix(hexValue, "41") {
hexValue = hexValue[2:]
} }
// TronGrid 的不同接口可能返回 base58 或 hex 合约地址;这里保留主网 USDT hex 兼容,避免官方返回格式差异导致误拒。 if len(hexValue) == 40 {
return c.contractAddress == "tr7nhqjekqxgtci8q8zyk4pyl8otszgjlj6t" && value == "a614f803b6fd780986a42c78ec9c7f77e6ded13c" if _, err := hex.DecodeString(hexValue); err == nil {
return hexValue, true
}
}
payload, ok := decodeTronBase58Check(value)
if !ok || len(payload) != 21 || payload[0] != 0x41 {
return "", false
}
return hex.EncodeToString(payload[1:]), true
}
func decodeTronBase58Check(value string) ([]byte, bool) {
number := new(big.Int)
base := big.NewInt(58)
for _, char := range value {
index := strings.IndexRune(tronBase58Alphabet, char)
if index < 0 {
return nil, false
}
number.Mul(number, base)
number.Add(number, big.NewInt(int64(index)))
}
decoded := number.Bytes()
leadingZeroes := 0
for leadingZeroes < len(value) && value[leadingZeroes] == '1' {
leadingZeroes++
}
if leadingZeroes > 0 {
decoded = append(make([]byte, leadingZeroes), decoded...)
}
if len(decoded) < 5 {
return nil, false
}
payload, checksum := decoded[:len(decoded)-4], decoded[len(decoded)-4:]
firstHash := sha256.Sum256(payload)
secondHash := sha256.Sum256(firstHash[:])
if !bytes.Equal(checksum, secondHash[:4]) {
return nil, false
}
return payload, true
} }
type transferEvent struct { type transferEvent struct {