124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
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"`
|
||
}
|