代码 USDT TRC20 自动收款:链上有转账、系统却没加钱,先查这四处

2026-10-03 09:00:54

USDT TRC20 自动收款:链上有转账、系统却没加钱,先查这四处

先分清两种反向故障

对接 USDT 自动收款,报障通常长成两个相反的样子:

  • 漏单:链上确实有一笔 USDT 转到收款地址,系统没加钱。三种常见来源——只监听最新链头导致未固化交易被回滚;只查了 TRX 转账接口,没查 TRC20;分页只取第一页,旧交易被新交易挤掉。
  • 假充值:系统加了钱,链上其实没成功。TRC20 转账本质是一次 TriggerSmartContract 合约调用,执行失败(OUT_OF_ENERGY / REVERT)的交易照样会被区块打包、照样有 txid、照样能在浏览器搜到。只按「有 Transfer 事件」判成功,等于给失败交易放行。

黑客构造一笔真实的 USDT 转账、故意把能量留到不够,交易 Revert,块里仍然留下记录。日志上看着是「有人转 USDT 给我了」,实际一分钱没动。

第一步:拿 txid 问链,别问自己的日志

curl -X POST https://api.trongrid.io/walletsolidity/gettransactioninfobyid \
-H "Content-Type: application/json" \
-d '{"value":""}'

只看一个字段:

{ "receipt": { "result": "SUCCESS" } }

receipt.result 不是 SUCCESS 的,一律拒绝加款,OUT_OF_ENERGY 和 REVERT 都不例外。

只看到这一层还不够。合约地址必须核对,攻击者可以部署一个 symbol 也叫 USDT、decimals 也设 6 的假合约,字段看起来完全一致。官方 USDT 合约地址只有一个:

TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t

如果这笔钱是内部交易转进来的,还要看 internal_transactions:跳过 rejected == true 的条目,callValueInfo[].tokenId 为空且 callValue > 0 是内部 TRX 转账,tokenId 非空则是内部 TRC10。

还有一条分界线:构建和广播交易走 FullNode(:8090),确认交易、余额对账、读固化块走 SolidityNode(:8091,接口前缀 /walletsolidity/)。不要把最新链头当最终依据——固化区块通常比最新链头滞后约 1 分钟,这段窗口里看到的「到账」随时可能被回滚。

第二步:TronGrid v1 查 TRC20 历史的正确姿势

BASE_URL=https://api.trongrid.io
curl --request GET --url "${BASE_URL}/v1/accounts/TJmmqjb1DK9TTZbQXzRQ2AuA94z4gKAPFh/transactions/trc20?limit=20&contract_address=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
  • only_confirmed=true/false:false 时同时返回已确认和未确认;不能与 only_unconfirmed 同时用。
  • limit:默认 20,最大 200。
  • fingerprint:上一页最后一笔的指纹,用于翻页;翻页时其它过滤条件必须保持不变,否则指纹失效。
  • contract_address:TRC20 合约地址,Base58 或 hex 都行,用来过滤单个 Token。
  • 生产环境必须带请求头 TRON-PRO-API-KEY。

返回里值得盯的字段:

{
"transaction_id": "...",
"token_info": { "symbol": "USDT", "address": "TR7NH...Lj6t", "decimals": 6, "name": "Tether USD" },
"block_timestamp": 1700000000000,
"from": "...",
"to": "...",
"type": "Transfer",
"value": "5000000"
}

decimals = 6,value 是整数最小单位:5000000 就是 5 USDT。直接拿 value 当美元金额会差 1e6 倍,这是金额对不上的头号原因。block_timestamp 是毫秒。

查 TRX / TRC10 历史是另一个接口:GET /v1/accounts/{address}/transactions?only_confirmed=true,用 type 区分 TransferContract(TRX)和 TransferAssetContract(TRC10)。它不覆盖 TRC20。

限流方面,TronGrid 免费档是 20 QPS、每日 10 万次访问(官方文档与社区口径,我没有逐档压测),按 API key、账户、IP、接口类型、时间窗口分别限,超限返回 429 或 403。客户端要做退避、缓存、分页和断点续扫,别对同一地址、同一区块范围高频轮询。

四道入账校验

  1. contract == TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t(官方 USDT)
  2. receipt.result == SUCCESS(配合 gettransactioninfobyid 查;任何 OUT_OF_ENERGY / REVERT 一律拒绝加款)
  3. to == 预期收款地址
  4. amount >= 订单金额(value / 1e6,多付策略自行定义)
  5. txid 本地唯一索引,防重放、防重复回调

第 5 条单独强调:只按 amount + address 判定入账,会被小额多发或同额多笔刷穿。另外 block_timestamp 要落在订单有效期窗口内。

确认数建议:低价值数字商品 1 个块(约 3 秒);一般电商 3–12 块;高价值 19+ 块(约 1 分钟,TRON 上 19 个区块即 Finalized 不可逆)或转人工审批。也有实现建议 19–30 这个区间。

三种监听方案怎么选

方案 A:轮询账户历史 API——/v1/accounts/{addr}/transactions/trc20?only_confirmed=true。最轻量、开发最快;到账延迟等于轮询间隔;受 API 频次限制。适合中小体量、地址数量不多。本地必须记录每个地址已处理的最后 txid / 时间戳避免重复记账。地址一多 QPS 直接炸。

方案 B:订阅合约 Transfer 事件——/v1/contracts/{address}/events?event_name=Transfer,可带 min_timestamp、order_by=timestamp,asc。按合约维度拉,一次覆盖所有收款地址,适合地址池大的场景;但拿到之后仍要在本地按 to 过滤自己的地址池并做分页。

方案 C:自建全节点扫块——/walletsolidity/getnowblock → getblockbynum → 遍历 raw_data.contract[0].type。延迟最低、不受第三方限流;代价是自己维护索引器,而一笔 TriggerSmartContract 可能包含多个 Transfer 事件和内部交易,复杂度高,流量足够大才值得。

方案 D:客户提交 txid 后单笔核对——最简单,但要提供链上确认接口做校验。

归集(sweep)要预留 TRX

归集就是把收款地址里的 USDT 转到热钱包或财库,本质是 TRC20 合约调用,消耗能量(Energy)。能量不够会烧 TRX,TRX 也不够交易就失败,已消耗的资源不退。所以收款地址上要预留 TRX,或者质押 TRX 换能量,否则归集交易会因能量不足卡死。

服务端回调给业务系统要签名:Signature = HMAC-SHA256(SecretKey, order_id + amount + timestamp + nonce),防伪造回调;回调侧要幂等,并配指数退避重试。

参考链接

复制全文 生成海报 USDT TRC20 TronGrid 支付回调 幂等 链上收款

推荐文章

程序员茄子在线接单