交易签名与广播——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 步——对交易进行签名

交易签名在客户端本地完成,通常包含三个子步骤:

  1. 计算 raw_data 的 SHA-256 哈希,结果与 txID 相同。
  2. 用发送方的 Secp256k1 私钥对该哈希签名。
  3. 把生成的 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、BroadcastHexresult: 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 工作流