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"` }