// Package tron reads confirmed incoming TRC20 (USDT) transfers from TronGrid. // Only reads — the watcher never signs or moves funds (that's offline sweeping). package tron import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strconv" "time" ) // USDTContractMainnet is the TRON mainnet USDT (TRC20) contract. 6 decimals. // ⚠️ Verify before relying on it in production (Phase-level constant check). const USDTContractMainnet = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" // Transfer is one confirmed incoming TRC20 transfer to a watched address. // Value is the raw integer amount (micro-USDT, since USDT has 6 decimals). type Transfer struct { TxID string To string Value int64 } // Fetcher returns confirmed incoming USDT transfers to a given address. type Fetcher interface { IncomingTransfers(ctx context.Context, address string) ([]Transfer, error) } // Client talks to the TronGrid HTTP API. type Client struct { base string usdtContract string apiKey string hc *http.Client } func NewClient(base, usdtContract, apiKey string) *Client { if base == "" { base = "https://api.trongrid.io" } if usdtContract == "" { usdtContract = USDTContractMainnet } return &Client{base: base, usdtContract: usdtContract, apiKey: apiKey, hc: &http.Client{Timeout: 15 * time.Second}} } func (c *Client) IncomingTransfers(ctx context.Context, address string) ([]Transfer, error) { u := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20?only_confirmed=true&contract_address=%s&limit=50", c.base, url.PathEscape(address), url.QueryEscape(c.usdtContract)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, err } if c.apiKey != "" { req.Header.Set("TRON-PRO-API-KEY", c.apiKey) } resp, err := c.hc.Do(req) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("tron: trongrid status %d", resp.StatusCode) } var body struct { Data []struct { TransactionID string `json:"transaction_id"` To string `json:"to"` Value string `json:"value"` Type string `json:"type"` } `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, fmt.Errorf("tron: decode: %w", err) } out := make([]Transfer, 0, len(body.Data)) for _, d := range body.Data { if d.To != address || d.Type != "Transfer" { continue } v, err := strconv.ParseInt(d.Value, 10, 64) if err != nil { continue // skip malformed value rather than fail the whole batch } out = append(out, Transfer{TxID: d.TransactionID, To: d.To, Value: v}) } return out, nil }