package bscusdt import ( "bytes" "context" "encoding/hex" "encoding/json" "io" "math/big" "net/http" "strings" "time" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/config" ) const transferEventTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" // Client 通过 BSC JSON-RPC 校验 USDT-BEP20 Transfer 事件;它只读取公开 receipt,不触碰私钥。 type Client struct { httpClient *http.Client rpcURL string contractAddress string } // New 创建 BSC USDT 只读客户端。RPCURL 可以是自建节点、BscScan proxy 或兼容网关。 func New(cfg config.BSCUSDTConfig) *Client { timeout := cfg.HTTPTimeout if timeout <= 0 { timeout = 10 * time.Second } return &Client{ httpClient: &http.Client{Timeout: timeout}, rpcURL: strings.TrimSpace(cfg.RPCURL), contractAddress: normalizeEVMAddress(cfg.USDTContractAddress), } } // VerifyUSDTTransfer 校验 tx_hash 已成功执行,且 USDT Transfer 足额转入平台共享地址。 func (c *Client) VerifyUSDTTransfer(ctx context.Context, txHash string, toAddress string, amountMinor int64) (string, error) { if c == nil || c.rpcURL == "" { return "", xerr.New(xerr.Unavailable, "bsc rpc client is not configured") } txHash = strings.TrimSpace(txHash) toAddress = normalizeEVMAddress(toAddress) if txHash == "" || toAddress == "" || amountMinor <= 0 { return "", xerr.New(xerr.InvalidArgument, "bsc usdt transfer verification request is incomplete") } // 订单金额以 USD cent 保存,USDT 使用 6 位精度;1 cent = 10,000 atomic units。 requiredAtomicAmount := big.NewInt(amountMinor) requiredAtomicAmount.Mul(requiredAtomicAmount, big.NewInt(10_000)) receipt, rawJSON, err := c.transactionReceipt(ctx, txHash) if err != nil { return rawJSON, err } if !strings.EqualFold(strings.TrimSpace(receipt.Status), "0x1") { return rawJSON, xerr.New(xerr.Conflict, "bsc transaction is not successful") } for _, log := range receipt.Logs { if !strings.EqualFold(normalizeEVMAddress(log.Address), c.contractAddress) { continue } if len(log.Topics) < 3 || !strings.EqualFold(strings.TrimSpace(log.Topics[0]), transferEventTopic) { continue } if topicAddress(log.Topics[2]) != toAddress { continue } amount, ok := parseHexBigInt(log.Data) if !ok || amount.Cmp(requiredAtomicAmount) < 0 { continue } return rawJSON, nil } return rawJSON, xerr.New(xerr.Conflict, "bsc usdt transfer does not match order") } func (c *Client) transactionReceipt(ctx context.Context, txHash string) (ethReceipt, string, error) { body, err := json.Marshal(jsonRPCRequest{ JSONRPC: "2.0", ID: 1, Method: "eth_getTransactionReceipt", Params: []string{txHash}, }) if err != nil { return ethReceipt{}, "", err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.rpcURL, bytes.NewReader(body)) if err != nil { return ethReceipt{}, "", err } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return ethReceipt{}, "", xerr.New(xerr.Unavailable, "bsc rpc request failed") } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) if err != nil { return ethReceipt{}, "", err } rawJSON := string(raw) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return ethReceipt{}, rawJSON, xerr.New(xerr.Unavailable, "bsc rpc returned non-success") } var parsed struct { Result *ethReceipt `json:"result"` Error *struct { Code int64 `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(raw, &parsed); err != nil { return ethReceipt{}, rawJSON, err } if parsed.Error != nil { return ethReceipt{}, rawJSON, xerr.New(xerr.Unavailable, "bsc rpc error: "+strings.TrimSpace(parsed.Error.Message)) } if parsed.Result == nil { return ethReceipt{}, rawJSON, xerr.New(xerr.Conflict, "bsc transaction receipt not found") } return *parsed.Result, rawJSON, nil } func normalizeEVMAddress(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if strings.HasPrefix(value, "0x") && len(value) == 42 { return value } return "" } func topicAddress(topic string) string { topic = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(topic)), "0x") if len(topic) < 40 { return "" } return "0x" + topic[len(topic)-40:] } func parseHexBigInt(value string) (*big.Int, bool) { value = strings.TrimPrefix(strings.TrimSpace(value), "0x") if value == "" { return nil, false } if _, err := hex.DecodeString(padEvenHex(value)); err != nil { return nil, false } amount := new(big.Int) if _, ok := amount.SetString(value, 16); !ok { return nil, false } return amount, true } func padEvenHex(value string) string { if len(value)%2 == 0 { return value } return "0" + value } type jsonRPCRequest struct { JSONRPC string `json:"jsonrpc"` ID int64 `json:"id"` Method string `json:"method"` Params []string `json:"params"` } type ethReceipt struct { Status string `json:"status"` Logs []struct { Address string `json:"address"` Topics []string `json:"topics"` Data string `json:"data"` } `json:"logs"` }