Ton 转账原理拆解
背景
TON 是消息驱动的账户抽象链:所有操作都是智能合约之间通过异步消息(外部消息、内部消息)互动完成。
钱包是合约:用户持有的钱包其实是一个部署在链上的合约(如 v3、v4、v5 钱包),由私钥签名外部消息驱动执行转账等操作。
Toncoin 与 Jetton 的差异:
Toncoin 是链的原生代币,余额挂在账户状态上,转账通过向目标地址发送“内部消息”(带 Toncoin value)完成。
Jetton 是代币标准(类似 ERC-20),每个代币有一个 Jetton 根合约与每个账户的 Jetton 钱包合约;转账不是直接“携带代币”,而是通过调用 Jetton 钱包合约的 transfer 方法来减少/增加余额。
Gas/费用模型:
Toncoin 转账:发送消息需要内含足够 Toncoin 支付消息费和目标合约执行费。
Jetton 转账:需要支付两类费用:执行
transfer
的消息费与随消息附带的 Toncoin,用于目标 Jetton 钱包合约执行和可能的回执(notification)链路。
工作链/分片:TON 有多链分片架构,消息投递是异步且可能跨分片,最终性靠区块确认(通常秒级)。
原生 token与其他 token 的关系
总览关系
Toncoin 转账:你的钱包合约发送一个“内部消息”到目标地址,消息里携带
value
(Toncoin 数量),链直接结算余额。无 Toncoin 合约:Toncoin 是原生资产,没有“代币合约”。
Jetton(代币标准)结构
以某个代币(如 USDT-jetton)为例,一个 Root 对应全网一类代币,每个账户有该代币下的专属 Jetton Wallet。
每个代币一个 Root 合约:负责代币元数据、规则、以及计算每个“拥有者地址”的专属 Jetton Wallet 地址。
每个“账户×代币”一个 Jetton Wallet 合约:存储该账户对该代币的余额。你和对方各自拥有该代币的子钱包。
转账链路:
你的钱包合约向“你的 Jetton Wallet”发送内部消息,调用
transfer
。你的 Jetton Wallet 扣减余额,并向“对方 Jetton Wallet”发送内部消息,附带少量 Toncoin 作为执行费。
对方 Jetton Wallet 增加余额,并可向你回执(可选)。
同一账户持有多种 Jetton 的情形
同一个“你的钱包地址”,针对不同代币(A/B/C),各自拥有一个对应的 Jetton Wallet。
这些 Jetton Wallet 地址可由各自的 Root 确定性推导,未使用时可能尚未部署,但地址可预先计算。
Toncoin 与 Jetton 的对比一览
Toncoin(原生):
- 无代币合约
- 余额直接在账户状态上
- 转账 = 钱包发内部消息携带 value 到目标地址 Jetton(标准代币):
- 1 个 Jetton Root(每种代币)
- 多个 Jetton Wallet(每个账户各 1 个)
- 转账 = 钱包 -> 自己的 Jetton Wallet.transfer -> 对方 Jetton Wallet
- 需要附带少量 Toncoin 用作执行/转发费用
单次转账:Toncoin(原生)原理
关键对象与消息
钱包合约接收外部消息(由用户本地签名)。
钱包合约根据指令构造内部消息(internal message)并发送至目标地址。
内部消息包含:
value
: 发送的 Toncoin 数量(nanoTON)bounce
: 是否允许弹回(默认对合约通常设为true
)stateInit
: 可选,若目标地址是未部署合约,可以携带其初始代码与数据以冷启动部署body
: 可选应用层负载(如评论、op code 等)
典型流程
用户构造外部消息:包含到期时间、seqno、转账参数,使用私钥签名。
钱包合约校验签名与
seqno
,防重放。钱包合约发送一个内部消息到收款地址,内含
value
与可选body
。目标地址接收:
若是普通钱包地址:余额直接增加;如果
bounce=true
且地址不存在,则可能回弹。若是合约地址:进入合约
recv_internal
,执行逻辑,消耗 gas,可能触发子消息或回执。
手续费扣除:
发送方支付网络与执行费用(包含消息大小和执行成本),从发送方余额中扣除。
如消息执行触发回执,回执的费用也需要在消息中预留。
伪代码(Toncoin 单次转账)
def send_toncoin(wallet, to_addr, amount, comment=None, bounce=True):
# 1. 构造内部消息体
body = None
if comment:
# 常见做法是在 body 里放 UTF-8 文本或专用 opcode + cell
body = build_comment_body(comment)
internal_msg = {
"to": to_addr,
"value": amount, # nanoTON
"bounce": bounce,
"stateInit": None, # 如需首次部署对端合约,可填
"body": body
}
# 2. 构造外部消息让钱包合约执行
ext_msg = wallet.build_external_message(
ops=[internal_msg], # v4/v5 可一次性传多个内部消息(批量)
timeout=now()+60,
seqno=wallet.get_seqno()
)
# 3. 签名并发送
signed = sign(ext_msg, wallet.private_key)
send_external_message(signed)
# 要点:需要保证钱包余额足以覆盖 amount + 所有消息/执行费用
要点:
钱包合约版本支持一次发送多条内部消息,这为“批量转账”提供基础。
bounce
设为false
可避免给不可反弹的地址造成失败;但一般对合约地址建议true
以免误转。
单次转账:Jetton(标准代币)原理
Jetton 标准核心合约角色:
Jetton Root:代币元数据与铸造权限,负责派生每个账户的 Jetton Wallet 地址。
Jetton Wallet(每个账户一个):实际存储该账户的代币余额,处理
transfer
/accept
/通知等。
转账不是“搬运代币”,而是:
发起者调用“自己名下的 Jetton Wallet 合约”的
transfer
,该合约减少发起者余额,同时通过消息驱动“接收方的 Jetton Wallet 合约”增加余额。
关键消息与字段(概念化)
op_transfer
: Jetton 转账操作码amount
: 代币数量(通常是自然数,精度通过 decimals 解释)destination
: 接收者地址(用户钱包地址或直接 Jetton Wallet 地址)response_destination
: 回执接收地址(可为发起方钱包)forward_ton_amount
: 附加转发所需 Toncoin,用于对端执行与可能的通知forward_payload
: 附带给接收方的钱包合约的业务负载(可空)custom_payload
: 可选自定义负载(路由/权限等)
典型流程
查出发起者的 Jetton Wallet 地址(由 Root 派生,或本地缓存/链上查询)。
钱包合约向“发起者 Jetton Wallet”发送一条内部消息,调用其
transfer
。发起者 Jetton Wallet 合约:
校验调用者与签名(通过钱包合约发来的内部消息可视为授权)。
扣减余额,构造并发送一条内部消息给“接收方 Jetton Wallet”。
附带必要的 Toncoin(
forward_ton_amount
)让对端能执行成功并可能向response_destination
回执。
接收方 Jetton Wallet 合约:
若不存在,可由
stateInit
携带初始化(有些实现自动创建)。增加余额,按标准发送接收通知或回执。
费用:
发起方需准备足够 Toncoin,用于多跳消息的 gas 与执行费,常见做法通过参数控制
forward_ton_amount
。
伪代码(Jetton 单次转账)
def send_jetton(wallet, jetton_root, to_owner_addr, jetton_amount, note=None):
# 1. 解析/计算本人的 Jetton Wallet 地址
my_jetton_wallet = derive_jetton_wallet(jetton_root, wallet.owner_addr)
# 2. 准备 transfer 调用参数
op_transfer = 0x0f8a7ea5 # 示例 opcode,实际看标准
forward_ton_amount = to_nano(0.05) # 预估:够对端执行与回执(视实现调整)
forward_payload = build_comment_body(note) if note else None
transfer_body = build_cell(
op=op_transfer,
query_id=random_u64(),
amount=jetton_amount,
destination=to_owner_addr, # 对方“拥有者地址”,钱包合约可据此派生其 Jetton Wallet
response_destination=wallet.owner_addr, # 回执地址
custom_payload=None,
forward_ton_amount=forward_ton_amount,
forward_payload=forward_payload
)
# 3. 钱包合约发内部消息到 my_jetton_wallet,携带足够 Toncoin 覆盖执行与转发
internal_msg = {
"to": my_jetton_wallet,
"value": to_nano(0.2), # 需要覆盖本次 transfer 与对子钱包转发、回执的费用
"bounce": True,
"body": transfer_body
}
ext_msg = wallet.build_external_message(
ops=[internal_msg],
timeout=now()+60,
seqno=wallet.get_seqno()
)
signed = sign(ext_msg, wallet.private_key)
send_external_message(signed)
要点:
必须为 Jetton 转账附带足够 Toncoin 作为执行费与转发费(尤其跨多跳)。
部分实现支持“直接向对方 Jetton Wallet 地址”转账,但标准推荐通过“对方拥有者地址”,由合约自行解析派生。
批量转账:Toncoin 原理
批量转账的本质是:钱包合约一次接收外部消息后,内部构造并发送多条内部消息(N 笔 Toncoin 转账),以实现单交易多转发。
流程与考虑
用户外部消息中携带多个“转账指令”列表。
钱包合约迭代构造 N 条内部消息并发送。
费用评估:
外部消息大小随 N 增加,费用线性增长。
每条内部消息的基费 + 目标执行费 + 潜在回执费。
失败处理:
某一条转账 bounce 并不自动回滚其他转账(异步、无原子性)。
可通过合理
bounce
与余额检查、分批发送降低风险。
伪代码(Toncoin 批量)
def batch_send_toncoin(wallet, transfers):
# transfers: list of {to, amount, comment?, bounce?}
ops = []
for t in transfers:
body = build_comment_body(t.get("comment")) if t.get("comment") else None
ops.append({
"to": t["to"],
"value": t["amount"],
"bounce": t.get("bounce", True),
"stateInit": None,
"body": body
})
ext_msg = wallet.build_external_message(
ops=ops,
timeout=now()+120,
seqno=wallet.get_seqno()
)
ensure_enough_balance(wallet, sum(t["amount"] for t in transfers) + estimate_fees(ops))
signed = sign(ext_msg, wallet.private_key)
send_external_message(signed)
要点:
批量并非原子事务;每条内部消息独立投递、独立结算。
钱包合约版本需支持多操作列表(如 v4/v5)。
批量转账:Jetton 原理
批量 Jetton 转账常见有两种策略:
钱包合约一次外部消息,发起多次对“自己 Jetton Wallet”的
transfer
调用(N 次)
优点:保持标准、每次语义清晰。
成本:链上执行与消息数量线性增长。
使用定制批量转账扩展(如多转账入口、批量打包 body)
优点:压缩外部消息与签名成本。
风险:需目标 Jetton 钱包/中间合约支持自定义批量接口;不是所有 Jetton 标准钱包都支持。
典型(标准做法)仍是方式 1:循环调用 transfer
。
伪代码(Jetton 批量,标准循环)
def batch_send_jetton(wallet, jetton_root, items):
# items: list of {to_owner_addr, amount, note?}
my_jetton_wallet = derive_jetton_wallet(jetton_root, wallet.owner_addr)
ops = []
for it in items:
body = build_jetton_transfer_body(
amount=it["amount"],
destination=it["to_owner_addr"],
response_destination=wallet.owner_addr,
forward_ton_amount=to_nano(0.05),
forward_payload=build_comment_body(it["note"]) if it.get("note") else None
)
ops.append({
"to": my_jetton_wallet,
"value": to_nano(0.2), # 为本次 transfer + 转发/回执预留
"bounce": True,
"body": body
})
ext_msg = wallet.build_external_message(
ops=ops,
timeout=now()+180,
seqno=wallet.get_seqno()
)
ensure_enough_balance(wallet, estimate_total_ton_for_fees(ops))
signed = sign(ext_msg, wallet.private_key)
send_external_message(signed)
要点:
批量的 Toncoin成本全部由发起者承担,包含每次
transfer
的执行与对端回执费用。大批量时要注意外部消息大小限制与区块 gas 限额;可分批发送。
Bounce、StateInit 与地址存在性
bounce=true
:当目标地址为合约且处理失败,消息可回弹,Toncoin 返还(扣除部分费);对普通未部署地址,bounce 可能导致回弹或丢失取决于目标存在性与规则。stateInit
:可以为“目标 Jetton 钱包”或其他合约的首次部署携带初始化数据,从而在首次转账时创建目标合约。这在 Jetton 转账中尤为常见(自动创建对方 Jetton 钱包)。地址类型:标准地址、原始地址(workchain + hash),对分片透明;合约是否已部署影响 bounce 与 gas 消耗。
费用与安全边界
费用估计要覆盖:
外部消息大小费
每条内部消息大小费
发送方与接收方执行费
Jetton 转账中的 forward ton 与回执链路费
安全建议:
对未知合约地址,建议
bounce=true
,并先小额试转。批量时优先分批发送,避免单条失败导致队列拥塞或余额不足引发连锁失败。
记录
query_id
以便在回执中对账与重试。注意钱包
seqno
单调递增、防重放;外部消息设置timeout
。
全流程贯穿(从构思到落地)
以下以“一个统一的批量转账入口,既可发 Toncoin 又可发 Jetton”为例,贯穿整个执行链路:
准备阶段(离线)
解析配置:待转账清单 items,包含类型(ton 或 jetton)、目标地址、数量、备注。
对 Jetton 项:
解析对应 Jetton Root,预先派生本方 Jetton Wallet 地址。
为每个接收方预估是否需要
stateInit
创建其 Jetton Wallet(可选,很多实现由对端自动创建)。
费用估算:按 Toncoin/Jetton 两类分别计算,确保钱包 Toncoin 余额足以覆盖所有费用与转发。
构造外部消息
遍历 items:
若为 Toncoin:构造一条内部消息 -> 收款地址,附加评论或业务 body。
若为 Jetton:构造一条内部消息 -> 本方 Jetton Wallet,body 为
transfer
调用,设置forward_ton_amount
与response_destination
。
将所有内部消息集合成一个外部消息(钱包支持多操作时)。
设置
seqno
、timeout
,签名。
上链与投递
外部消息送达钱包合约。
钱包合约校验签名、递增
seqno
,按序发送内部消息队列。各内部消息被目标分片路由、执行:
Toncoin:目标地址余额更新或合约执行。
Jetton:我的 Jetton Wallet 执行
transfer
,路由到对方 Jetton Wallet,完成余额更新,回执可选返回。
回执与对账
监听事件或解析交易:
Toncoin:交易成功状态、目标账户余额变化确认。
Jetton:根据
query_id
/response_destination
收到回执消息;或直接从目标 Jetton Wallet 查询余额变更。
对失败(超时、bounce、余额不足)项进行重试或人工审核。
终态
所有成功项有链上交易哈希可追溯。
批量与单次仅在“内部消息条目数”上不同,协议原理一致。