2026-06-11 01:02:16 +08:00

124 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package trongrid
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/config"
)
// Client 校验用户提交的 USDT-TRC20 tx_hash它只读链上公开数据不保存也不接触任何私钥。
type Client struct {
httpClient *http.Client
apiBaseURL string
apiKey string
contractAddress string
}
// New 创建 TronGrid 兼容客户端base_url 可以指向官方 TronGrid、自建节点代理或兼容网关。
func New(cfg config.TronGridConfig) *Client {
timeout := cfg.HTTPTimeout
if timeout <= 0 {
timeout = 10 * time.Second
}
return &Client{
httpClient: &http.Client{Timeout: timeout},
apiBaseURL: strings.TrimRight(strings.TrimSpace(cfg.APIBaseURL), "/"),
apiKey: strings.TrimSpace(cfg.APIKey),
contractAddress: strings.ToLower(strings.TrimSpace(cfg.USDTContractAddress)),
}
}
// VerifyUSDTTransfer 校验 tx_hash 指向的 confirmed Transfer 是否把足额 USDT 打到平台共享地址。
func (c *Client) VerifyUSDTTransfer(ctx context.Context, txHash string, toAddress string, amountMinor int64) (string, error) {
if c == nil || c.apiBaseURL == "" {
return "", xerr.New(xerr.Unavailable, "tron grid client is not configured")
}
txHash = strings.TrimSpace(txHash)
toAddress = strings.ToLower(strings.TrimSpace(toAddress))
if txHash == "" || toAddress == "" || amountMinor <= 0 {
return "", xerr.New(xerr.InvalidArgument, "usdt transfer verification request is incomplete")
}
// H5 商品金额以 USD cent 保存USDT 使用 6 位小数1 cent = 0.01 USDT = 10,000 atomic units。
requiredAtomicAmount := amountMinor * 10_000
events, rawJSON, err := c.listTransferEvents(ctx, txHash)
if err != nil {
return "", err
}
for _, event := range events {
if !strings.EqualFold(event.EventName, "Transfer") {
continue
}
if !c.contractMatches(event.ContractAddress) {
continue
}
if strings.ToLower(strings.TrimSpace(event.Result.To)) != toAddress {
continue
}
value, err := strconv.ParseInt(strings.TrimSpace(event.Result.Value), 10, 64)
if err != nil || value < requiredAtomicAmount {
continue
}
return rawJSON, nil
}
return rawJSON, xerr.New(xerr.Conflict, "usdt transfer is not confirmed or does not match order")
}
func (c *Client) listTransferEvents(ctx context.Context, txHash string) ([]transferEvent, string, error) {
endpoint := c.apiBaseURL + "/v1/transactions/" + url.PathEscape(txHash) + "/events?event_name=Transfer&only_confirmed=true&limit=50"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, "", err
}
if c.apiKey != "" {
req.Header.Set("TRON-PRO-API-KEY", c.apiKey)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, "", xerr.New(xerr.Unavailable, "tron grid request failed")
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if err != nil {
return nil, "", err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, string(body), xerr.New(xerr.Unavailable, "tron grid returned non-success")
}
var parsed struct {
Data []transferEvent `json:"data"`
}
if err := json.Unmarshal(body, &parsed); err != nil {
return nil, string(body), err
}
return parsed.Data, string(body), nil
}
func (c *Client) contractMatches(value string) bool {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" || c.contractAddress == "" {
return false
}
if value == c.contractAddress {
return true
}
// TronGrid 的不同接口可能返回 base58 或 hex 合约地址;这里保留主网 USDT hex 兼容,避免官方返回格式差异导致误拒。
return c.contractAddress == "tr7nhqjekqxgtci8q8zyk4pyl8otszgjlj6t" && value == "a614f803b6fd780986a42c78ec9c7f77e6ded13c"
}
type transferEvent struct {
EventName string `json:"event_name"`
ContractAddress string `json:"contract_address"`
Result struct {
To string `json:"to"`
Value string `json:"value"`
} `json:"result"`
}