关于 func2tolk
func2tolk 是将旧版 FunC 智能合约迁移到 Tolk 的编码技能,面向 TON 合约开发者。它强调保留 TL-B 布局、操作码、错误码和可观察行为,并使用 Acton 工作流辅助转换、测试与验证。
主要功能
- 01保留 TL-B 布局和合约行为
- 02用类型化消息与联合类型处理分发
- 03将存储建模为结构体和类型化映射
- 04使用 Acton 构建和测试
- 05提供迁移清单与参考示例
适用场景
- 01将 FunC 合约迁移为 Tolk
- 02核对消息和存储布局兼容性
- 03把现有测试迁移为原生 Tolk 测试
- 04构建、测试并验证合约
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ton-blockchain/ skills --skill func2tolk安装后在 Claude Code 中输入/func2tolk确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/porting-checklist.mdrepo-examples.md详细介绍
func2tolk
概述
将旧版 FunC 智能合约移植到 Tolk,同时保留:
- TL-B 布局(存储和消息体)
- 操作码编号和错误代码
- 可观察行为(包括退回处理和发送模式)
开发框架优先使用 Acton(构建、包装器、原生 Tolk 测试、脚本、部署、验证)。
如果 acton func2tolk 能加快工作进度,可将其用作第一轮转换工具;随后应将输出视为草稿,进行审核并重构为符合 Tolk 惯用风格、可测试的代码。
常见映射和注意事项,请打开 references/porting-checklist.md。公开的惯用示例和原始 FunC 基线,请打开 references/repo-examples.md。
不可妥协的规则(必须遵守)
每次 FunC -> Tolk 移植都必须遵守以下规则。
- 必须遵循本技能以及以下文档中的惯用写法:
https://docs.ton.org/languages/tolk/idioms-conventions
- 对已知操作码族,必须使用类型化消息架构和联合分派(
type Allowed...+lazy ...fromSlice(...)+match)。 - 必须使用结构体进行存储和消息解码,而不是在业务逻辑中逐字段命令式解析。
- 核心逻辑中必须使用类型化映射(
map<K, V>)和类型化辅助函数;仅允许在边界处进行原始字典解析,并且必须立即封装。 - 偏离惯用代码时,必须明确说明 TL-B 兼容性约束(例如:
UnsafeBodyNoRef、any_address?、旧版内联消息体的特殊行为)。 - 移植期间必须打开并应用
references/porting-checklist.md。 - 在完成本文件中的强制完成检查之前,禁止结束移植。
除非为兼容性提供明确理由,否则禁止:
- 对可建模为带标签结构体 + 联合 +
match的消息,使用手动操作码if/elseif分支链 - 在可使用类型化结构体的领域逻辑中,临时进行消息解析(
load_uint/load_msg_addr/...) - 在核心业务分支中散布原始
udict_*操作,而不使用类型化映射辅助函数 - 未按照检查清单进行自审就结束移植
可移植参考资料与示例集
- 本技能目录中的参考资料:
references/porting-checklist.mdreferences/repo-examples.md
- 主要公开风格参考:
https://github.com/ton-blockchain/acton-contracts- 每个合约套件都包含 Acton/Tolk 合约、原生测试、脚本、生成的包装器、相关基准快照,以及指向原始 FunC 实现的 README。
- 值得重点研究的 Acton/Tolk 套件:
jetton-v2,配套https://github.com/ton-blockchain/jetton-contract/tree/jetton-2.0nft,配套https://github.com/ton-blockchain/nft-contractw5,配套https://github.com/ton-blockchain/wallet-contract-v5dns,配套https://github.com/ton-blockchain/dns-contractmultisig-v2,配套https://github.com/ton-blockchain/multisig-contract-v2highload-v3,配套https://github.com/ton-blockchain/highload-wallet-contract-v3notcoin,配套https://github.com/OpenBuilders/notcoin-contractconfig,配套https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/config-with-ownable-params.fcelector,配套https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/elector-code.fc
acton-contracts中的counter没有对应的原始 FunC 实现;仅将其用于了解 Acton 项目的基本机制。- Acton 文档:
https://github.com/ton-blockchain/acton/tree/master/docs/content/docs- 否则请使用
acton --help和官方托管文档
- Tolk 惯用写法与约定(以文档为先的风格目标):
https://docs.ton.org/languages/tolk/idioms-conventions
- 运行
acton ...之前,先cd到包含Acton.toml的目录(Acton 会相对于该目录解析合约和配置)。
移植工作流(Acton 优先)
1) 确定行为契约
- 定位 FunC 入口点(
recv_internal、recv_external、run_ticktock、getters),并列出已处理的操作码。 - 提取必须保留的不变量:
- 精确的存储布局(位宽、字段顺序、引用的有无)
- 消息 TL-B 布局(包括
Either负载和Maybe字段) - Gas 和预留行为,以及发送模式
- 将现有 JS/TS 测试视为规范;移植时将其转换为原生 Tolk 测试。
2) 设置 Acton 项目结构
- 创建脚手架或复用现有项目:
acton new PROJECT --template empty|counter|jetton
- 在
Acton.toml的[contracts.*]下定义合约。 - 使用
[mappings]避免深层相对导入(采用@stdlib/...导入风格)。 - 如果系统包含多个合约(Jetton 铸币合约 + 钱包、集合 + 项目等),请在
Acton.toml中建模依赖,并使用acton build --graph检查依赖 DAG。
3) 优先转换存储和 TL-B 类型
- 将类型拆分到可由测试/封装导入的文件中(避免将消息/存储结构体放在合约入口文件内):
errors.tolk— 错误码messages.tolk— 消息 TL-B 结构体和联合类型storage.tolk— 持久化结构体和辅助函数
- 将合约数据建模为
struct Storage { ... }(及相关结构体)。 - 实现存储辅助函数:
fun Storage.load() { return Storage.fromCell(contract.getData()) }fun Storage.save(self) { contract.setData(self.toCell()) }
- 如果 FunC 使用“可能尚未初始化”的存储,请明确建模(例如,使用一个从
slice开始解析、检查剩余引用,再解析 Initialized 或 NotInitialized 变体的封装类型)。
4) 使用带标签的结构体转换消息体
- 使用带标签的结构体替代 FunC 的
op::...和手动解析:struct (0x...) SomeMessage { ... }
- 创建用于分发的联合类型:
type AllowedMessage = A | B | C
- 使用惰性解码进行解析:
val msg = lazy AllowedMessage.fromSlice(in.body);
- 为匹配旧版编码,优先使用正确的类型,而不是临时位操作:
RemainingBitsAndRefs用于“剩余”负载/蛇形格式Cell<T>用于引用负载- 当旧版编码与
address?不兼容时,使用any_address?
5) 按 Acton 风格实现入口点
- 内部消息:
fun onInternalMessage(in: InMessage) { ... }
- 弹回消息:
fun onBouncedMessage(in: InMessageBounced) { ... }- 用它在发送失败时恢复余额/供应量(Jetton 中很常见)。
- 外部消息(钱包、签名):
fun onExternalMessage(inMsgBody: slice) { ... }
- 优先使用
match进行分发;明确决定是忽略未知/空消息体,还是抛出0xFFFF。 - 如果必须在
onInternalMessage内手动处理弹回,请使用编译器策略(例如@on_bounced_policy("manual")),并说明原因。
6) 替换低级消息构建方式
- 优先使用
createMessage({ ... })+.send(SEND_MODE_...),而非手动进行builder编码。 - 保持发送模式和弹回标志与 FunC 行为一致。
- 移植依赖
raw_reserve、accept_message、存储费预留等功能的合约时,优先使用@stdlib/gas-payments辅助函数。
7) 迁移测试(JS/TS → 原生 Tolk)
- 在
tests/*.test.tolk中编写 Acton 测试。 - 使用
net.treasury("name")创建已注资的模拟账户。 - 对返回的交易列表使用
expect(res)匹配器。 - 生成封装(推荐):
acton wrapper CONTRACT_ID --test
- 将共享类型放在单独的文件中(例如
contracts/types.tolk),以便封装/测试可以导入它们,而无需导入合约入口文件(避免重复定义onInternalMessage)。
8) 使用 Acton 工具迭代
- 构建:
acton build(调试时添加--clear-cache)。 - 测试:
acton test(按需使用--filter、--coverage、--debug)。 - 调试编译差异:
acton disasm(可用时使用--source-map)。 - 在本地试运行部署逻辑:
acton script scripts/deploy.tolk(不会实际消耗 TON)。 - 仅在本地运行成功后广播:
acton script scripts/deploy.tolk --broadcast --net testnet|mainnet
9) 在链上验证
- 部署后:
acton verify CONTRACT_ID --address EQ... --net testnet|mainnet
高价值 FunC → 符合 Tolk 惯用写法的模式(来自 acton-contracts)
除非兼容性限制要求使用更底层的代码,否则请将本节作为默认的转换策略。
A) Opcode 分发:用带类型的联合类型 + match 替换 if (op == ...) 链
FunC 基线:
op = in_msg_body~load_uint(32); if (op == op::x) { ... } if (op == op::y) { ... }
符合 Tolk 惯用写法:
- 在
messages.tolk中声明带标签的消息结构体 - 将它们组合成联合类型(
type AllowedMessage = A | B | C) - 延迟解析并使用
match分发
type AllowedMessage = Transfer | Burn | TopUp
fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedMessage.fromSlice(in.body);
match (msg) {
Transfer => { ... }
Burn => { ... }
TopUp => { ... }
else => throw 0xFFFF
}
}
这样做的好处:
- 保持 opcode 兼容性,同时避免手动解析偏差
- 让各分支的本地解析具有强类型
- 更适用于大型 opcode 集合(wallet-v5/vesting/telemint 风格)
B) 将消息体解析为结构体,而不是临时加载
优先使用:
- 对外可见的 opcode 使用
struct (0x...) - 对分组负载使用嵌套结构体(
TransferOwnershipData、MsgInner等) - 负载明确按引用传递时使用
Cell<T> - 对真正的剩余负载 / snake / either 包装器使用
RemainingBitsAndRefs
避免:
- 在业务逻辑中反复使用
load_uint/load_msg_addr/load_ref,而结构体本可以表示相同的布局。
C) 使用专用的 messages.tolk 模块定义入站格式
移植中广泛使用的模式:
- 将所有传入消息的模式保存在
messages.tolk中 - 将消息类型导入合约入口
- 在消息定义附近定义
Allowed...联合类型
这样可让入口文件专注于行为,而非字节解析。
D) 存储转换:将元组加载器替换为类型化存储结构体 + 方法
FunC 基线:
(a, b, c) load_data()和save_data(a, b, c)
符合 Tolk 惯用写法:
struct Storage { ... }Storage.load()/Storage.save()- 保持 TL-B 布局一致(字段顺序 + 引用/内联形态)
struct Storage { totalSupply: coins adminAddress: address? }
fun Storage.load() { return Storage.fromCell(contract.getData()) }
fun Storage.save(self) { contract.setData(self.toCell()) }
E) 显式建模多形态状态(已初始化 vs 未初始化)
NFT/DNS/Telemint 项目中常见:
- FunC 会检查
slice_bits() > 0或引用是否存在,以判断形态。
符合 Tolk 惯用写法:
- 加载器包装器 + 显式解析器:
startLoading...().isInitialized().parseNotInitialized().parseInitialized()
这样可明确状态转换,并避免脆弱的剩余内容解析。
F) 字典:将原始 udict_* 操作替换为类型化映射
FunC 基线:
- 手动使用
udict_get?、udict_set_ref、udict_delete?、udict_delete_get_min
符合 Tolk 惯用写法:
map<K, V>别名 + 类型化辅助方法(exists、set、delete、findFirst、iterateNext)- 仅在边界处转换底层字典(
createMapFromLowLevelDict(...))
示例:
- DNS 记录:
type DnsRecords = map<uint256, cell> - 钱包扩展:
map<uint256, bool> - 查询位图:
map<uint13, Bitmap>
G) 优先使用 createMessage(...),而不是手动组装 begin_cell() 消息
默认做法:
- 使用
createMessage({ ... })构建传出消息 - 通过
.send(SEND_MODE_...)发送
仅在以下情况下保留手动构建器:
- 精确模拟历史行为差异
- 强制使用精确的内联/无引用消息体布局
H) 外部签名流程:有意解析后缀/剩余内容
对于钱包风格的外部消息:
- 将签名与签名负载分离(使用
getLastBits/removeLastBits或显式字段顺序) - 先验证哈希/签名
- 更新重放状态(
seqno/ 查询映射),持久化并提交 - 然后执行操作/发送消息
这与经过强化的钱包 v5/高负载模式一致。
I) Bounce 处理:优先使用 onBouncedMessage 和带类型的 bounce 联合类型
FunC 基线:
recv_internal中的 bounce 分支 + 手动load_uint
符合 Tolk 惯用风格的写法:
onBouncedMessage(in: InMessageBounced)- 可选的 bounce 操作码子集联合类型:
type BounceOpToHandle = A | B
- 从带类型的解析消息中恢复余额/供应量
J) 保留旧版 TL-B/行为的兼容性选项
与 FunC 测试保持一致时,有意使用以下选项:
- 使用
UnsafeBodyNoRef { forceInline: ... }保持无引用体的兼容性 - 当旧版布局比
address?更宽泛时,使用any_address? - 使用显式的
(Either Cell ^Cell)剩余部分验证辅助函数确保载荷正确性 - 保持未知操作码的行为一致(
throw 0xFFFF与忽略空消息体)
K) 从实际移植中提炼的领域专用方案
Jetton 系列:
AllowedMessageToMinter/AllowedMessageToWallet- 带类型的 bounce 恢复(
InternalTransferStep | BurnNotification...) - 带类型的转发载荷(
RemainingBitsAndRefs)+ 验证器辅助函数
NFT + DNS 项:
- 明确的未初始化->已初始化转换路径
- 将所有权转移载荷作为嵌套结构体
- 使用基于映射的内容记录,而不是命令式地修改字典位
Highload 钱包:
- 尽早验证最小结构,然后进行带类型解析
- 将重放位图建模为带类型的映射 + 辅助方法
- 转发前对出站原始消息结构进行带类型验证
Vesting / 策略钱包:
- 将白名单映射表示为带类型的
map<address, ()> - 使用联合类型解析 +
match验证允许的操作码(而不是在各分支中分散数值操作码白名单)
强制完成门槛(每次移植任务结束前)
完成以下所有检查项后,才能结束任务。
- 已打开并应用检查清单:
references/porting-checklist.md
- 分发逻辑符合惯用风格:
- 已知消息系列使用带标签的结构体 + 联合类型 +
match
- 已知消息系列使用带标签的结构体 + 联合类型 +
- 解析采用类型化方式:
- 业务分支中没有可避免的命令式解析
- 存储采用类型化方式:
- 使用
Storage.load()/Storage.save()(或显式的已初始化/未初始化加载器模式)
- 使用
- Dict 使用采用类型化方式:
- 逻辑中使用
map<K,V>+ 辅助函数;边界处的原始 dict 解码已封装
- 逻辑中使用
- 已记录兼容性例外:
- 任何非惯用结构均已说明其作为 TL-B/行为一致性要求的理由
- 已报告构建/测试状态:
- 运行相关的
acton build/acton test(或报告确切阻碍)
- 运行相关的
- 最终回复包含针对上述门槛的简短自审摘要。
如果任何门槛项目无法完成,不要悄然结束。请报告阻碍,并明确列出未完成的门槛项目。
将 acton-contracts 用作风格参考
对于生产级移植,默认使用 https://github.com/ton-blockchain/acton-contracts 作为公开风格参考:
- 使用各套件的 README 查找原始 FunC 仓库或文件
- 使用原始 FunC 源码作为行为基线,尤其要关注边缘情况
- 使用对应的 Acton/Tolk 套件作为模块布局、类型化存储、消息联合类型、包装器、脚本、测试和基准快照的目标风格
优先按以下内容搜索:
- 操作码十六进制值(例如
0xd53276db) - 消息类型名称(例如
InternalTransferStep) - 存储字段名称(例如
totalSupply、ownerAddress)
打开 references/repo-examples.md 查看套件映射、原始 FunC 链接和搜索目标。
文档优先查阅
- 对于 Tolk 语言细节和标准库,请使用 TON Docs 中
languages/tolk/下的页面(概览、序列化、标准库、FunC 对比)。 - 对于 Acton 的具体用法(命令、包装器、测试、脚本),请查阅 Acton 文档和
acton --help。