在 TON 里,你钱包里那串地址背后不是一个裸的密钥对,而是一个智能合约:由代码和数据组成,和其他任何合约一样能收外部消息、收内部消息、发内部消息、跑只读的 get 方法。官方文档在钱包历史页写得很干脆:钱包在 TON 中不是某种独立实体。理解这一点,才能读懂后面那些字段。
合约里存什么:公钥和一个序号
多数基于 seqno 的钱包合约在持久化数据里放两样东西:一把公钥,和一个叫 seqno 的序号。公钥用来认证签了名的请求,seqno 用来防重放。官方给的重放场景很具体:Alice 有 100 Gram,给 Bob 转 10 Gram 当礼物,Bob 想再拿一次,就把同样一条消息再转给 Alice 的钱包——如果钱包不防重放,他就会再收到 10 Gram。
机制是这样:一条签名请求里放一个 seqno(哪怕这条请求里打包了好几条对外消息),合约拿它和自己存储里的状态比对,按合约规则决定何时把它消费掉;一旦被消费,旧的请求就过不了 seqno 检查。文档另外强调了两条容易忽略的性质。第一,钱包合约不存余额字段:账户余额是协议层的账户状态,由验证者在收到、发出价值和扣手续费时自动维护,合约里再存一份既多余又会和真实状态脱节,外部通过 RPC 与区块浏览器读到的余额就来自这个公共状态。第二,公钥不是地址:地址是从合约的 StateInit 派生出来的,签名方案是 Ed25519,外部签名请求携带 512 位签名与负载,钱包先验签再处理。

两个时间/序号条件与重试规则
除了 seqno,用到该机制的合约还认 valid_until:一个 Unix 时间戳,划定签名请求的有效期,过期的请求直接被拒。官方的重试建议可以直接当排障步骤用:只有在结果的 seqno 仍是当前值、且请求尚未过期时,才重试那条未改动的已签名请求;如果结果未知,先读钱包状态再重试,因为另一条被接受的请求可能已经把 seqno 推进了;如果请求过期而没被处理,就用当前的 seqno 和一个新的期限签一条替换请求。
这套规则解释了两类常见现象。同一笔签名被重复提交时,只有第一次会成功,其余会卡在 seqno 校验上;批量并发的多条交易如果各自用旧的 seqno,也会互相把对方的序号作废。
subwallet_id 与 wallet_id:为什么同一个密钥能有好几个地址
subwallet_id 是一个 32 位标识,与某个钱包实例绑定,在建钱包时就定下,并被写进待签名的消息里。之后每条签名请求,钱包都会核对传入的 subwallet_id 与自己的是否一致,不一致就拒绝这条消息。于是同一把公钥可以派生出多个互相独立的钱包合约,各用自己的 subwallet_id、各自的地址和各自的 seqno——适合按账户、按环境、按业务线分开管理。官方给的注意事项是:在最初的 StateInit 里选一个不同的 subwallet_id,导出的就是另一个地址,即使公钥相同,资金也不共享。
钱包 V5 做了整合:用一个派生的 32 位 wallet_id 取代 subwallet_id,把网络、钱包版本、工作链和子钱包编号编码进同一个值里;为了兼容,链上的读取方法名字仍叫 get_subwallet_id(),但对 V5 返回的是这个派生值。
发交易前该读什么
官方 API 文档里 getWalletInformation 的定位就是这件事:查询某个地址的钱包专属信息,若是已知钱包合约则返回钱包类型、当前 seqno 和 wallet_id。两条硬性提示值得抄进操作清单——用任何钱包专属字段之前,先确认响应里的 wallet 为 true;发送任何交易之前,先调用它取当前的 seqno。响应的字段形态包括 wallet_type(形如 wallet v1 r1)、seqno、wallet_id、is_signature_allowed,以及账户状态;未部署的账户会返回 uninitialized 的账户状态。这也和 TON 转账的退与不退:bounce 标志、弹回消息与四种失败退出码 里”目标账户未初始化”那类失败原因连上了:钱包侧先读状态,能提前发现大半问题。想核对某个地址上跑的到底是哪一版钱包合约,官方钱包历史页按版本列出了各合约代码的哈希值,可与之对照。TON 上的 dApp 怎么连钱包:TON Connect 的桥、会话与账号锁定 讲的是连接层,本篇是合约层。涉及资产操作均有风险,本文不构成投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。