交易签名与广播——API 工作流
TRON 中所有修改链上状态的操作都遵循三步工作流:构建未签名交易、在客户端用发送方私钥签名、广播已签名交易。本页介绍交易完整生命周期,附操作示例与常见开发陷阱。
TRON 中所有改变链上状态的操作(转账、智能合约交互、质押、投票等)都遵循同一套交易流程:
- 构建(Create):生成包含业务操作和链上时序信息的未签名交易。
- 签名(Sign):在客户端用发送方私钥对交易签名。
- 广播(Broadcast):把已签名交易发送到 TRON 网络,等待超级代表(SR)打包。
查询余额、读取合约状态、获取区块信息等只读操作不改变链上状态,不需要签名和广播。下面拆解写入交易的完整工作流:
前置阅读
三步工作流
构建未签名交易有多种方式:调用节点 API、用各语言 SDK、用本地加密工具包。下面以调用节点 API 为例:
flowchart LR
%% 全局样式定义
classDef boxStyle fill:#fff,stroke:#2b7a78,stroke-width:1px,color:#333;
classDef textStyle fill:none,stroke:none,color:#666,font-size:13px;
subgraph Step1 ["1. 构建"]
direction TB
A1["通过 POST 方法<br>调用节点 API<br>获取未签名的 raw_data"]
A2["⬇ 未签名交易"]:::textStyle
A1 ~~~ A2
end
subgraph Step2 ["2. 签名"]
direction TB
B1["SHA-256(raw_data)<br>+ secp256k1 签名"]
B2["⬇ 已签名交易"]:::textStyle
B1 ~~~ B2
end
subgraph Step3 ["3. 广播"]
direction TB
C1["将已签名交易 POST 到节点<br>/wallet/broadcasttransaction"]
C2["⬇ txid"]:::textStyle
C1 ~~~ C2
end
Step1 --> Step2 --> Step3
%% 应用样式
class A1,B1,C1 boxStyle;
注意交易签名必须在客户端本地执行。私钥不应离开本地设备,TRON 节点和 TronGrid 都不需要接触私钥。
第 1 步——构建未签名交易
TRON 节点为不同交易类型提供对应的构建 Endpoint。客户端提交描述操作的 JSON 请求,节点返回完整但未签名的交易对象。
| 业务操作 | 构建 Endpoint |
|---|---|
| 转账 TRX | /wallet/createtransaction |
| 转账 TRC-10 Token | /wallet/transferasset |
| 调用智能合约(包含 TRC-20 转账) | /wallet/triggersmartcontract |
| 质押 TRX(Stake 2.0) | /wallet/freezebalancev2 |
| 为超级代表(SR)投票 | /wallet/votewitnessaccount |
| 部署智能合约 | /wallet/deploycontract |
返回的 JSON 有三个关键顶层字段:
txID:raw_data的SHA-256哈希,即交易哈希,也是密码学签名的对象。raw_data:结构化的交易体(包含交易类型、参数、TAPOS 引用块及过期时间等)。raw_data_hex:raw_data经 Protobuf 序列化后的十六进制字符串。
节点还会在交易体里自动填充两个相互独立的时序字段:
- TAPOS 引用(
ref_block_bytes和ref_block_hash):把交易锚定到近期区块,限制其在不含该参考区块的分支上重放。若两个分支共享该参考区块,交易在引用和过期时间仍有效时可能同时通过 TAPOS 校验。 - 过期时间(
expiration):交易的独立截止时间戳,默认有效期约 60 秒,在构建交易时确定。如果交易由节点构建(如/wallet/createtransaction等接口),默认值取自该节点config.conf中的trx.expiration.timeInMilliseconds;如果使用 SDK(TronWeb、Trident、GoTron 等)在客户端构建,expiration由 SDK 设置,多数 SDK 支持按交易覆盖。交易若未在截止前打包进区块,会被网络判定为过期;该截止时间相对链头区块的上限约为 24 小时。
重要在构建交易后的 50 秒内完成签名并广播,否则交易很可能因超时过期被网络拒绝。
第 2 步——对交易进行签名
交易签名在客户端本地完成,通常包含三个子步骤:
- 计算
raw_data的SHA-256哈希,结果与txID相同。 - 用发送方的 Secp256k1 私钥对该哈希签名。
- 把生成的 65 字节签名追加到交易对象的
signature数组(transaction.signature[])。
主流 SDK 把这三个子步骤封装为单一方法。以 TronWeb 为例,直接调用签名方法即可完成签名:
| SDK | 签名调用方法 |
|---|---|
| TronWeb (JavaScript) | tronWeb.trx.sign(unsignedTx, privateKey) |
| Trident (Java) | wrapper.signTransaction(unsignedTx) |
在无法直接用主流 SDK 的环境(如硬件安全模块 HSM、硬件钱包),可从 raw_data_hex 自行计算交易哈希,用标准 Secp256k1 密码学库签名。签名原语的协议底层细节,参见交易数据结构说明。
多重签名交易遵循相同流程,区别在于:所有共同签名者针对同一个 txID 分别签名,广播前把全部签名收集进 signature 数组。详情参见账户权限管理。
第 3 步——广播已签名交易
把已签名的交易对象 POST 到 /wallet/broadcasttransaction:
POST /wallet/broadcasttransaction
Content-Type: application/json
{
"txID": "...",
"raw_data": { ... },
"raw_data_hex": "...",
"signature": ["..."]
}广播接口返回 result: true 时,响应示例如下:
{ "result": true, "txid": "f947f1283f7b43c111eba662..." }收到该响应后,仍需继续查询原 txID。对于直接发起的 TRX 或 TRC-10 转账,通过 /walletsolidity/gettransactionbyid 确认交易本体已进入固化主链;对于智能合约交易,还需通过 /walletsolidity/gettransactioninfobyid 检查执行结果。
注意广播接口未返回错误不等于交易已经确认。
result: true只表示节点处理本次广播请求时未返回错误;它不证明交易仍保留在待处理池、已传播到其他节点、已打包、执行成功或已固化。排障路径参见广播与 RPC 错误。
第 4 步——确认交易结果
将已签名交易发送给节点后,还需要继续查询原 txID,确认交易是否已进入区块、是否执行成功以及是否已经固化。下表列出了各阶段对应的接口及其返回含义。
| 阶段 | 推荐接口 | 返回语义 | 下一步 |
|---|---|---|---|
| 构建未签名交易 | CreateTransaction、TriggerSmartContract 等构建接口 | 返回 raw_data、raw_data_hex 和 txID,交易尚未签名、尚未进入网络 | 本地签名 |
| 收集签名权重 | GetSignWeight | 返回当前签名权重是否达到目标 permission 的 threshold | 达到 threshold 后广播 |
| 广播已签名交易 | BroadcastTransaction、BroadcastHex | result: true 只表示本次广播调用未返回错误 | 继续查询交易状态 |
| 查询交易本体 | GetTransactionById | 返回 FullNode 当前可见的交易原始内容;不包含执行收据 | 需要固化确认时查询 /walletsolidity/gettransactionbyid |
| 查询执行收据 | GetTransactionInfoById | 返回 FullNode 当前可见的手续费、Energy、合约执行状态、Event 和内部交易等信息 | 不能据此判断交易已经固化 |
| 查询已固化交易本体 | /walletsolidity/gettransactionbyid | 仅返回已固化主链中的交易本体 | 确认直接发起的 TRX 或 TRC-10 转账已经上链 |
| 查询已固化执行收据 | GetTransactionInfoById(固化查询) | 仅返回已固化区块中的交易执行信息 | 确认顶层 result 未标记为 FAILED,且 receipt.result 为 SUCCESS |
如果业务需要确认交易的最终状态,先通过 /walletsolidity/gettransactionbyid 确认交易本体已进入固化主链;对于智能合约交易,还应通过 /walletsolidity/gettransactioninfobyid 检查已固化的执行结果。如果所需结果暂未返回,应保留原 txID 并继续查询,不要立即判定交易失败。
如果需要确认原交易未上链,例如准备创建替代交易,应先等待可靠、已同步节点的最新固化区块时间达到或超过原交易的 raw_data.expiration,再使用 /walletsolidity/gettransactionbyid 查询原 txID。接口返回空结果时,不能立即判断原交易未上链。如果所用节点或 RPC 服务能够提供原交易从第一次发送到 raw_data.expiration 期间的全部已固化区块,应通过 /walletsolidity/getblockbynum 逐块检查;如果无法确定原交易第一次发送的时间,或当前服务无法提供完整的区块历史,则应改用保留完整历史的数据源复核。只有完成上述检查且仍未发现原 txID,才能确认原交易未进入固化主链。
确认资产转移结果时,应先根据协议层交易类型确定数据来源:
- 系统合约转账交易:
TransferContract用于转移 TRX,TransferAssetContract用于转移 TRC-10。收款地址和金额记录在交易本体中;交易固化后,如果ret[0].contractRet存在,其值应为SUCCESS。 - 智能合约调用交易: 对于
TriggerSmartContract交易,应先查询已固化 receipt,确认顶层result未标记为FAILED且receipt.result为SUCCESS,再解析本次调用涉及的资产转移:- 调用交易通过
call_value向目标合约发送 TRX;通过call_token_value和token_id向目标合约发送 TRC-10。 - 合约执行过程中继续产生的 TRX 或 TRC-10 转移记录在
internal_transactions[]中。应排除rejected = true的记录,并校验transferTo_address和callValueInfo[]。 - TRC-20 转账通过 Token 合约的状态变化实现。只处理由业务已审核并纳入允许列表的 Token 合约发出的
Transfer(address,address,uint256)Event,并校验发送方、接收方和金额。对于未经审核的合约,不能仅根据 Event 判断转账成功。
- 调用交易通过
等待交易固化并检查执行结果
下面的 Node.js 脚本用于在交易广播后确认两件事:交易是否已进入固化主链,以及智能合约交易是否执行成功。脚本始终跟踪原 txID,不会创建或广播新交易。
脚本首先通过 /walletsolidity/gettransactionbyid 查询交易本体。返回匹配的交易后,即可确认该交易已进入固化主链;如果 ret[0].contractRet 存在且不为 SUCCESS,则判定交易执行失败。对于 CreateSmartContract 和 TriggerSmartContract 交易,脚本还会通过 /walletsolidity/gettransactioninfobyid 查询执行收据,只有顶层 result 未标记为 FAILED 且 receipt.result 为 SUCCESS 时,才判定合约执行成功。
如果达到重试上限后仍未查询到交易本体,或仍未查询到智能合约交易所需的执行收据,脚本不会直接判定交易失败。此时应保留原 txID 并继续查询,不要立即创建可能造成重复扣款的替代交易。
const solidityNode = (process.env.SOLIDITY_NODE_URL || 'https://api.trongrid.io')
.replace(/\/$/, '');
const txID = process.env.TX_ID;
if (!/^[0-9a-fA-F]{64}$/.test(txID || '')) {
throw new Error('请通过 TX_ID 提供 64 个十六进制字符组成的交易 ID');
}
async function query(path) {
const response = await fetch(`${solidityNode}/${path}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ value: txID }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
for (let attempt = 1; attempt <= 30; attempt += 1) {
const transaction = await query('walletsolidity/gettransactionbyid');
if (transaction.txID?.toLowerCase() === txID.toLowerCase()) {
const contractResult = transaction.ret?.[0]?.contractRet;
const transactionFailed = contractResult && contractResult !== 'SUCCESS';
if (transactionFailed) {
throw new Error(`交易已固化,但执行失败:${contractResult}`);
}
const contractTypes = transaction.raw_data?.contract?.map(({ type }) => type) || [];
const needsReceipt = contractTypes.some((type) =>
type === 'CreateSmartContract' || type === 'TriggerSmartContract');
if (!needsReceipt) {
console.log('交易已进入固化主链');
process.exit(0);
}
const info = await query('walletsolidity/gettransactioninfobyid');
if (info.id?.toLowerCase() !== txID.toLowerCase()) {
await new Promise((resolve) => setTimeout(resolve, 3000));
continue;
}
const executionResult = info.receipt?.result;
if (info.result === 'FAILED') {
throw new Error('智能合约交易执行失败:FAILED');
}
if (executionResult === undefined) {
await new Promise((resolve) => setTimeout(resolve, 3000));
continue;
}
if (executionResult !== 'SUCCESS') {
throw new Error(`智能合约交易执行失败:${executionResult}`);
}
console.log(`智能合约交易已固化并执行成功:${info.blockNumber}`);
process.exit(0);
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
throw new Error('尚未查询到所需的已固化结果;请使用同一 TX_ID 继续查询');脚本默认使用 TronGrid 主网端点。如需使用其他提供 /walletsolidity 接口的节点服务,可通过 SOLIDITY_NODE_URL 指定其 HTTP 地址。将脚本保存为 confirm-transaction.mjs。运行时建议使用仍在维护的 Node.js LTS 版本,并先更新到该版本的最新补丁版:
export TX_ID="替换为广播响应中的 txid"
# 可选:export SOLIDITY_NODE_URL="https://提供-walletsolidity-接口的节点地址"
node confirm-transaction.mjs实际示例:质押 TRX
下面是通过 Stake 2.0 质押 TRX 获取能量的完整交易生命周期示例:
1. 构建交易
POST /wallet/freezebalancev2
Content-Type: application/json
{
"owner_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0",
"frozen_balance": 1000000,
"resource": "ENERGY"
}接口响应如下:
{
"visible": false,
"txID": "f947f1283f7b43c111eba662ab5d27b31dc906b77fdb0b3e52ee626335b21108",
"raw_data": {
"contract": [{
"parameter": {
"value": {
"resource": "ENERGY",
"frozen_balance": 1000000,
"owner_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0"
},
"type_url": "type.googleapis.com/protocol.FreezeBalanceV2Contract"
},
"type": "FreezeBalanceV2Contract"
}],
"ref_block_bytes": "a59c",
"ref_block_hash": "aef359052c4aa176",
"expiration": 1681117668000,
"timestamp": 1681117610802
},
"raw_data_hex": "0a02a59c2208aef359052c4aa17640a0c5afd3f6305a59083612550a34..."
}2. 签名交易
在本地用客户端 SDK 对上一步返回的交易对象签名:
const signedTx = await tronWeb.trx.sign(unsignedTx, privateKey);
// 此时,signedTx 对象的 signature 数组中已填入签名数据:signature[0]3. 广播交易
把签名后的完整交易对象发送到网络:
POST /wallet/broadcasttransaction
Content-Type: application/json
{ ...signedTx }接口响应如下:
{ "result": true, "txid": "f947f1283f7b43c111eba662ab5d27b31dc906b77fdb0b3e52ee626335b21108" }该响应只表示这笔质押 1 TRX(等于 1,000,000 sun)的交易广播调用未返回错误。应继续使用原 txID 查询交易;交易被超级代表打包入账并固化后,质押额度和对应的能量可通过 /wallet/getaccountresource 确认。
常见陷阱
- TAPOS 引用失效:
ref_block_bytes和ref_block_hash必须指向规范主链上的近期区块。从生成交易到打包期间若发生链重组(如微分叉)使被引用区块失效,交易打包时会因TAPOS_ERROR被拒绝。解决:构建交易时尽量引用已固化区块,而非链头区块。 - 交易超时过期:与 TAPOS 独立,交易体的
expiration字段(默认约 60 秒)限定交易打包前的生存周期。节点构建的交易默认值取自节点config.conf中的trx.expiration.timeInMilliseconds;客户端 SDK 构建的交易由 SDK 设置,多数 SDK 支持按交易覆盖。expiration相对链头区块的上限约为 24 小时。构建到广播之间延迟过长会触发过期被拒。解决:构建后立即签名并广播。 visible标志不一致:visible=true时请求参数和响应中的地址为 Base58 格式(以T开头);visible=false时为十六进制 Hex 格式(以41开头)。构建交易和本地签名两环节的visible格式不一致,会导致签名校验失败。详情参见地址与数据编码。- 对错误的哈希算法签名:TRON 签名的哈希对象是
raw_data经SHA-256的结果,不是以太坊常用的 Keccak-256,也不是对raw_data_hex字符串本身做哈希(尽管两者算出的字节序列有逻辑关联)。推荐:始终用 SDK 封装的签名 API,避免自行实现哈希与签名。 - 仅因本地时间超过
raw_data.expiration就立即创建替代交易:本地时间超过该值,既不能证明原交易未上链,也不能替代基于固化区块时间的检查。对于已经发送过或发送结果未知的交易,应先按交易安全重播与替换流程,等待固化视图覆盖原交易的有效期并核对相关固化区块。只有确认原txID未进入已固化主链后,才可重新构建并签名。如果能够确定原交易从未发送给任何节点,只是在首次发送前已经过期,则可以直接重建。 - 把广播调用未返回错误当作交易确认:接口返回
{"result": true}只表示本次广播调用未返回错误,不证明交易仍保留在待处理池、已传播、打包、执行成功或固化。直接发起的 TRX 或 TRC-10 转账应通过/walletsolidity/gettransactionbyid确认已进入固化主链;智能合约交易还应通过/walletsolidity/gettransactioninfobyid检查执行结果。 - 重复广播同一笔交易:因网络波动重试时,可以重播完全相同的已签名交易。这笔交易的
txID不变,TRON 会对重复交易进行去重;接收节点可能返回DUP_TRANSACTION_ERROR,而不是再次返回成功结果。 - 在服务端托管或使用用户私钥签名:不要在中心化服务器上代持或使用用户私钥。私钥应保留在客户端、钱包插件(如 TronLink、WalletAdapter)或硬件加密模块(HSM)中。
相关资源
- 确认语义 —— 区分广播响应、交易上链、Receipt 执行结果和已固化状态
- 地址与数据编码 —— 详细了解 Base58 与 Hex 地址转换、
visible参数的影响以及 ABI 编码规范 - 广播与 RPC 错误 —— 排查
TAPOS_ERROR、TRANSACTION_EXPIRATION_ERROR、资源不足和广播失败 - 账户管理 —— TRON 地址格式、生成算法与密钥派生机制
- 交易结构解析 —— 深入剖析 TRON 交易的原生数据结构与其完整生命周期
- 账户多重签名管理 —— 多重签名配置及多个签名收集的 API 工作流
Updated 5 days ago
