简介提供一份基于TRON官方API的JAVA对接TRC20/TRX转账与地址生成Demo适合需要快速集成USDT-TRC20收付款、或实现链上交易功能的后端开发者。资源内含两个Java源文件、两个Maven配置xml以及两个可直接引用的jar依赖并附带说明文档覆盖从生成地址、查询余额到构造并广播TRC20转账的完整调用链路。压缩包共8个文件大小2.78MB结构精简包含Maven工程文件与依赖目录便于导入现有Spring或普通Java工程。目前已有75人学习/下载适合有一定Java基础、希望避开重复踩坑的区块链应用开发人员。通过示例代码可快速掌握官方API的参数组织方式与签名逻辑整体代码结构清晰便于二次开发可有效缩短对接TronGrid或节点RPC的调试周期。1. 用JAVA对接TRC20的TRX交易为什么“生成地址”只是一小步接到一个需求用 JAVA 实现 TRC20 链上的 TRX 交易转账还要能生成地址。听起来简单真正做完才发现生成地址只是入口后面还压着离线签名、接口选型、金额精度、带宽与能量消耗这几座山。网上搜“java 生成 trx地址”能搜到一堆 demo可一旦把转账搬到生产环境翻车的往往是那些 demo 里没写的东西官方 API 文档里接口的真正边界在哪里、交易广播后怎么确认到账、私钥到底该放在哪个环节。这篇笔记按我实际落地的顺序来写先讲怎么读 TRON 官方 API 文档再给生成地址和转账的完整 JAVA 实现最后是踩坑记录和到账验证策略。新手可以照着代码一步步跑通熟手可以直接跳到第 5 章和第 6 章看参数边界。2. 读透TRON官方API文档再动手先用一张表圈定接口边界2.1 官方API文档到底在讲什么TRON 的官方 API 文档描述的是节点对外暴露的 HTTP 接口路径统一以/wallet开头比如创建交易、广播交易、查询账户都是独立接口。这一点和以太坊的 JSON-RPC 思路类似但比它更直白请求和响应都是 JSON字段含义在文档里写得比较清楚JAVA 对接时不需要引入任何链上 SDK用普通的 HTTP 客户端就能完成全部流程。有一个认知必须在一开始就立住官方 API 只负责“构建交易”和“广播交易”不负责“为交易签名”。节点收到签名前的交易数据会返回给你一个txID签名的动作必须在你自己的服务端完成然后把签名结果拼回交易里再广播。谁持有私钥谁才有权签名这套模型保证了私钥不会离开你的服务器。理解了这一点后面排查问题就能快速定位报错如果出现在广播之后问题多半在签名而不是在接口调用。2.2 先记住这张接口清单对接 TRC20 和 TRX 转账实际用到的接口不会超过 7 个我把它们列成了一张表开发时对照着看就行。接口路径作用请求要点/wallet/generateaddress在线生成地址官方不建议生产使用私钥会经过网络/wallet/getaccount查询 TRX 余额传addressBase58/wallet/createtransaction构建 TRX 转账交易传owner_address、to_address、amount/wallet/broadcasttransaction广播签名后的交易传完整交易 JSON含signature/wallet/triggersmartcontract调用 TRC20 合约转账传合约地址、方法签名、ABI 编码参数/wallet/gettransactioninfobyid查询交易上链结果传txID看 receipt 与块高/wallet/triggerconstantcontract调用合约只读方法查余额适合查 USDT 等代币余额重点说两个容易混淆的地方。第一generateaddress虽然能一把生成地址和私钥但它走的是网络请求私钥在传输过程中会被节点知道所以我在生产环境里从来不碰它地址生成全部离线完成。第二TRC20 代币转账和 TRX 主币转账是两个完全不同的接口TRX 用createtransaction代币用triggersmartcontract两者的手续费计算也不一样。后面第 4 章会分别给出代码。2.3 环境准备与 JAVA 依赖选型开发环境基本上就是 JDK 1.8 以上加一个 Spring Boot 工程JAVA_HOME 配好就行没有特殊要求。依赖方面我习惯只引入三样东西OkHttp发 HTTP 请求、Gson 或 Jackson处理 JSON、一个能算 secp256k1 签名的库。签名库的选择是个关键点常见做法是直接用 tronj 里的ECKey类它针对 TRON 做了适配地址推导和签名方法都是现成的省去自己处理 Keccak 和 Base58Check 的功夫。用 Maven 管理的话pom 里大致长这样dependency groupIdio.github.tronprotocol/groupId artifactIdtronj/artifactId version按你工程实际情况选稳定版/version /dependency dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId /dependency dependency groupIdcom.google.code.gson/groupId artifactIdgson/artifactId /dependency参数说明tronj这个库我只看中它的ECKey和地址工具类网络请求部分不依赖它因为节点地址随时要换直接用 OkHttp 调接口反而灵活。版本号不建议追新选一个在你的 JDK 版本下编译通过的稳定版就行这一点上不要有“版本越新越好”的执念。另外准备一个配置文件把节点地址、私钥、合约地址都放进去。我一般用application.ymltrx: node: http://your-node-address:8090 private-key: 你的测试私钥 usdt-contract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6tnode那一行写的是你自己部署的全节点地址或者你选用的公共节点地址。生产环境我强烈建议自建全节点理由在第 5 章讲调用量限制时会说到。3. 生成TRON地址离线生成才是生产环境的正解3.1 地址生成原理私钥、公钥、Base58CheckTRON 地址的生成链路是私钥 → secp256k1 椭圆曲线算出公钥 → 对公钥做 Keccak-256 哈希 → 取哈希后 20 字节 → 前面拼一个字节的前缀0x41→ 得到 21 字节的地址字节数组 → 再做 Base58Check 编码最终得到以T开头、长度 34 位的字符串。这里有两个特别容易踩的点。第一TRON 的地址哈希用的是 Keccak-256不是 SHA-256也不是以太坊现在用的那个带0x前缀的 Keccak 变体。如果用 SHA-256 去推地址算出来的结果一定对不上而且排查起来很隐蔽。第二Base58Check 的编码规则与比特币地址类似但 TRON 的版本字节是0x41这一点别照搬 BTC 的实现。如果只是调用现成库里封装好的toBase58Address()这些细节库已经处理了但你最好知道它背后做了什么否则线上出了诡异问题时无从下手。3.2 离线生成地址的 JAVA 实现我常用的生成方式是用ECKey的随机构造器直接在服务器内存里生成私钥和地址完全不走网络。完整代码可以这样写import io.github.tronprotocol.tronj.crypto.ECKey; import io.github.tronprotocol.tronj.utils.ByteArray; public class TrxAddressGenerator { public static void main(String[] args) { // 1. 随机生成一个密钥对这一步不依赖任何网络 ECKey key new ECKey(); // 2. 取出 16 进制私钥长度应为 64 位 String privateKey ByteArray.toHexString(key.getPrivKeyBytes()); System.out.println(privateKey: privateKey); // 3. 直接输出 Base58Check 格式的 T 地址 String address key.toBase58Address(); System.out.println(address: address); // 4. 同时输出地址的 Hex 形式后面调合约接口会用到 String addressHex ByteArray.toHexString(key.getAddress()); System.out.println(addressHex: 0x addressHex); } }逻辑说明new ECKey()内部会生成一个随机的 secp256k1 密钥对getPrivKeyBytes()返回 32 字节私钥转成 Hex 就是 64 位字符串这串字符是后续签名的唯一凭证getAddress()返回的是带0x41前缀的 21 字节地址数组toBase58Address()是最终展示给用户的地址。参数说明privateKey的 64 位 Hex 字符串要妥善保存千万别打日志addressHex以0x41开头但实际调 TRON 接口时有些接口要求不带前缀的 40 位 Hex甚至要求把41也去掉这个我在第 4 章 TRC20 转账的参数拼接里会再强调。3.3 生成地址后的三项自检地址生成完不要急着用我建议花一分钟做三项自检能挡掉大多数低级问题。第一检查私钥长度。标准私钥一定是 64 位 Hex 字符串如果打印出来是 63 位或 65 位说明十六进制转换出了问题这时候生成的地址和后续签名全部不可用。第二检查地址前缀和长度。T 开头的 Base58 地址固定 34 位如果不是要么是编码实现不对要么是前缀字节写错。第三把私钥再跑一次生成流程对比两次得到的地址是否一致。地址推导是确定性的同一个私钥必须得到同一个地址这一步能验证你的私钥和地址工具类配合没有问题。另外强调一句生产环境里不要把生成地址和签名放在同一个接口里对外暴露更不要在浏览器端生成。我之前接过一个项目对方把私钥生成逻辑放在了前端 JS 里私钥直接暴露给了用户浏览器这等于把资产控制权交给了客户端风险非常大。正确姿势是服务端生成、服务端保管、服务端签名客户端只拿到地址字符串。4. TRX与TRC20转账签名、广播、参数精度一个都不能错4.1 TRX 主币转账构建交易、本地签名、广播TRX 转账的完整流程分为四步构建交易 → 本地签名 → 广播交易 → 查询结果。我先给出核心实现再逐行解释。public String transferTrx(String fromAddress, String toAddress, BigDecimal amountTrx, String privateKeyHex) throws Exception { // 1. 金额换算1 TRX 1_000_000 SUN接口只认 SUN long amountSun amountTrx.multiply(BigDecimal.valueOf(1_000_000L)).longValueExact(); // 2. 构建交易请求节点返回未签名的交易对象 JSONObject txReq new JSONObject(); txReq.put(owner_address, fromAddress); txReq.put(to_address, toAddress); txReq.put(amount, amountSun); String txBody postJson(node /wallet/createtransaction, txReq.toJSONString()); JSONObject tx JSON.parseObject(txBody); // 3. 关键对 raw_data_hex 做 SHA-256得到待签名的哈希 byte[] rawData HexUtil.decode(tx.getString(raw_data_hex)); byte[] txIdHash Sha256Hash.hash(rawData); // 4. 用私钥签发得到签名串 ECKey key ECKey.fromPrivate(HexUtil.decode(privateKeyHex)); ECKey.ECDSASignature signature key.sign(txIdHash); String signatureHex HexUtil.encode(signature.toByteArray()); // 5. 把签名拼回交易对象广播 tx.put(signature, new String[] {signatureHex}); String broadcastBody postJson(node /wallet/broadcasttransaction, tx.toJSONString()); JSONObject result JSON.parseObject(broadcastBody); if (!result.getBooleanValue(result)) { throw new RuntimeException(broadcast failed, code result.getString(code)); } return tx.getString(txID); }逻辑说明第 1 步的金额换算是整个函数里最容易出错的地方。createtransaction接口的amount字段单位是SUN不是TRX1 TRX 等于 100 万 SUN。如果你直接传1进去转出去的就是 0.000001 TRX。第 3 步的raw_data_hex是节点返回的交易原始数据签名前要先对这个原始数据做 SHA-256 得到待签名哈希再用 ECKey 签名。TRON 和以太坊不同它的签名对象就是这个txID对应的哈希不需要拼 chainId 之类的字段。参数说明fromAddress和toAddress都传 Base58 格式T 开头。amountTrx用BigDecimal接收避免 double 的精度问题multiply配合longValueExact()能在金额小数位超过 6 位时直接抛异常这比静默截断安全得多。广播返回的code字段很重要常见的SIGERROR表示签名不对BANDWITH_ERROR表示手续费不够第 5 章我会专门展开。4.2 关键参数amount、expiration、fee_limitcreatetransaction的响应里有几个字段值得提前关注。txID是这笔交易的哈希后续查询就靠它raw_data里有expiration默认是当前时间往后 60 秒超时后这笔交易不会被打包。我处理过期的方式是拿到expiration后和当前服务端时间对比如果距离过期小于 10 秒就重新构建一笔交易。这个策略在异步场景里尤其重要因为你的服务可能在拿到交易后做了很久的签名或审批才广播结果发现交易已经过期了。fee_limit这个参数在 TRX 主币转账里用不到但 TRC20 转账一定会用到。它代表你愿意为这笔合约调用支付的最大能量费用上限单位同样是 SUN。比如fee_limit 10_000_000表示最多消耗 10 TRX 的能量费。具体值要根据合约复杂度来定USDT 的 transfer 大多在 5 到 15 TRX 之间我会在 4.3 节给一个建议值。4.3 TRC20 代币转账triggerSmartContract 与 ABI 编码TRC20 转账走的是合约调用接口路径是/wallet/triggersmartcontract。和 TRX 转账最大的区别是你需要自己拼transfer(address,uint256)的 ABI 编码参数。这一步是新手最容易卡住的地方我给出完整代码。public String transferTrc20(String fromAddress, String toAddress, BigDecimal amount, String privateKeyHex) throws Exception { // 1. 拼接 ABI 参数地址去掉 0x41 前缀补成 32 字节金额补成 32 字节 String toAddressHex base58ToHexNoPrefix(toAddress); // 40 位 Hex String amountHex amount.multiply(BigDecimal.valueOf(1_000_000L)) .toBigInteger().toString(16); StringBuilder params new StringBuilder(); params.append(leftPad(toAddressHex, 64, 0)); // 左补 0 到 64 位 params.append(leftPad(amountHex, 64, 0)); // 2. 构造合约调用请求 JSONObject triggerReq new JSONObject(); triggerReq.put(owner_address, fromAddress); triggerReq.put(contract_address, usdtContract); // Base58 格式 triggerReq.put(function_selector, transfer(address,uint256)); triggerReq.put(parameter, params.toString()); triggerReq.put(fee_limit, 10_000_000); triggerReq.put(call_value, 0); // 3. 节点返回未签名的合约交易后续签名广播流程与 TRX 完全一致 String triggerBody postJson(node /wallet/triggersmartcontract, triggerReq.toJSONString()); JSONObject tx JSON.parseObject(triggerBody); // 4. 同样的 SHA-256 本地签名 广播 byte[] rawData HexUtil.decode(tx.getString(raw_data_hex)); byte[] hash Sha256Hash.hash(rawData); ECKey key ECKey.fromPrivate(HexUtil.decode(privateKeyHex)); ECKey.ECDSASignature signature key.sign(hash); tx.put(signature, new String[] {HexUtil.encode(signature.toByteArray())}); String broadcastBody postJson(node /wallet/broadcasttransaction, tx.toJSONString()); JSONObject result JSON.parseObject(broadcastBody); if (!result.getBooleanValue(result)) { throw new RuntimeException(broadcast failed, code result.getString(code)); } return tx.getString(txID); }逻辑说明第 1 步的leftPad是核心。Solidity 的address和uint256在 ABI 编码里都固定占 32 字节地址要右对齐数字要左补零。如果漏了补位合约调用大概率会报CONTRACT_VALIDATE_ERROR。第 2 步的function_selector告诉节点你要调用合约的哪个方法TRON 节点会帮你算方法选择器不需要手动算 Keccak。第 4 步的签名和 TRX 主币转账完全一样因为triggersmartcontract返回的也是一个标准交易对象。参数说明fee_limit我填的是 1000 万 SUN也就是 10 TRX。对 USDT 来说如果接收方不是新地址这个值通常够用但如果目标地址从未激活过能量消耗会高一些可以适当调到 1500 万到 2000 万。call_value必须传 0因为 TRC20 的 transfer 不接收主币如果你想同时转入 TRX那是另一套逻辑。5. 对接必读的避坑清单从精度到带宽的6个真实翻车点5.1 地址格式传错base58 和 hex 各有一半接口会报错现象同样的地址在getaccount接口里能查到余额在triggersmartcontract里就报INVALID_ADDRESS。原因TRON 接口对地址格式的要求并不统一。getaccount、createtransaction这些老接口接受 Base58T 开头而一部分合约接口或者需要拼 ABI 参数的地方要的是去掉前缀的 40 位 Hex。解决我在工具类里同时维护两个字段address和addressHexNoPrefix调接口时按文档要求选对格式不要临时做转换。我见过有人把 Base58 地址直接塞进 ABI 编码里结果参数长度多了一倍合约直接拒绝执行。5.2 签名看似成功广播返回 SIGERROR现象本地签名正常生成了 65 字节的签名串广播后返回SIGERROR。原因签名对象搞错了。TRON 要求对raw_data做 SHA-256 之后再签名如果你直接把节点返回的txID拿来签名或者用了 Keccak-256 做哈希验签一定失败。解决严格按照Sha256Hash.hash(rawData)来算待签名哈希这里的rawData是响应里raw_data_hex字段的字节数组。另外注意签名后不要把txID改掉广播时用的是原交易数据加签名任何字段的变动都会导致验签失败。5.3 TRC20 转账返回 BANDWITH_ERROR现象广播 TRC20 转账时返回BANDWITH_ERROR但 TRX 主币转账却正常。原因调用合约不仅需要带宽还需要能量Energy。TRON 的规则是优先消耗你账户里的免费能量和质押获得的能量不够的部分会从你的 TRX 余额里按比例扣除。如果账户里 TRX 不够支付能量费用就会报这个错。解决给发起转账的账户预留一些 TRX或者使用能量租赁服务。我一般建议封一个检查方法在转账前先查一下账户 TRX 余额小于 15 TRX 就直接拒绝本次转账把错误提示返回给调用方。5.4 公共节点调用量限制现象测试环境一切正常一上生产就开始随机报超时和SERVER_BUSY。原因公共节点为了稳定性会限制单 IP 的 QPS而且对同一笔交易还可能做缓存你的服务一旦并发上来请求就会被限流。解决生产环境自建全节点通过快照同步数据然后用 Nginx 做负载均衡。如果短期没有自建条件至少要在代码里给节点请求加上带超时和重试的 HTTP 客户端并且把节点地址做成多节点配置一个节点挂了自动切换。5.5 交易过期导致丢单现象用户付款成功但系统里查不到交易记录。原因交易在expiration之前没有被广播或者广播了但没被打包过期后节点直接丢弃。解决构建交易后不要做耗时的异步签名尽量在 5 秒内完成签名并广播。广播后如果返回成功不代表交易一定上链要配合下一章的轮询确认。我在代码里会把expiration存到日志里一旦发现接近过期就重新构建这个值比依赖直觉判断可靠得多。5.6 幂等缺失导致重复扣款现象网络超时后重试广播用户被扣了两次款。原因broadcasttransaction返回超时但交易其实已经上链重试时同一笔交易再次广播成功收款地址收到两笔。解决在业务层记录txID作为幂等键。构建交易成功后就先落库广播失败需要重试时先查库如果txID已经存在且链上查询有结果就不再广播。这笔交易的唯一性由txID保证代码里永远不要用“用户订单号”去关联链上交易。6. 验证交易是否真正到账轮询、确认数与幂等设计6.1 主动轮询与确认块数广播成功只代表节点接受了这笔交易不代表已经打包出块。我吃过一次亏广播返回成功后直接给用户标记“已支付”结果那笔交易因为能量不足被节点丢弃了用户转账记录和链上状态对不上。从那以后我接的所有链上项目都强制增加一个确认流程核心就是用交易哈希去查链上结果。public TransactionInfo pollTransaction(String txId, int maxAttempts, long intervalMs) throws InterruptedException { for (int i 0; i maxAttempts; i) { JSONObject req new JSONObject(); req.put(txID, txId); String body postJson(node /wallet/gettransactioninfobyid, req.toJSONString()); JSONObject info JSON.parseObject(body); if (info ! null !info.isEmpty()) { // 取块高和打包结果 long blockNumber info.getLongValue(blockNumber); String result info.getJSONObject(receipt).getString(result); if (SUCCESS.equals(result)) { System.out.println(confirmed at block blockNumber); return info; } } Thread.sleep(intervalMs); } throw new RuntimeException(tx not confirmed after maxAttempts attempts); }参数说明maxAttempts我一般设 10intervalMs设 3000也就是最多轮询 30 秒。TRON 的出块时间约 3 秒正常情况下 10 次以内就能查到结果。判定标准不要只看blockNumber存在必须看receipt.result是否为SUCCESS。我建议业务上至少等 6 个确认再更新用户账本余额也就是大约 18 秒这样可以避免链上临时分叉造成的回滚。6.2 幂等设计与最终一致性轮询确认和业务幂等要配合使用。我的标准做法是收到台回调或前端轮询请求时先查自己数据库里的txID查不到就调用链上接口确认确认成功后更新订单状态并落库再返回给调用方。如果同一个txID被重复请求第二次进来时数据库已经有了直接返回成功即可不需要再次调链上接口。最后聊一个经验性的细节配置里的节点地址和私钥不要写死在代码里用环境变量注入并且私钥只放在服务端配置中心日志里打地址即可千万不要打印私钥和签名串。我见过有人把私钥打到异常日志里排查问题最后不得不紧急轮换密钥这种教训一次就够。这个方向本身不难难点全在细节把控上把接口边界、参数精度、确认流程这三件事做好整套对接就能稳定跑起来。希望帮到你。本文还有配套的精品资源点击获取