func2tolk について
func2tolkは、旧来のFunCスマートコントラクトをTolkへ移行するためのコーディングスキルで、TONコントラクト開発者向けです。TL-Bレイアウト、オペコード、エラーコード、観測可能な動作の維持を重視し、Actonのワークフローで変換、テスト、検証を進めます。
主な機能
- 01TL-Bレイアウトとコントラクト動作の維持
- 02型付きメッセージとユニオンによるディスパッチ
- 03構造体と型付きマップによるストレージ表現
- 04Actonによるビルドとテスト
- 05移行チェックリストと参照例
活用シーン
- 01FunCコントラクトの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)を必ず使用してください。 - ビジネスロジックで命令的にフィールドを1つずつ解析するのではなく、ストレージとメッセージのデコードには必ず構造体を使用してください。
- コアロジックでは型付きマップ(
map<K, V>)と型付きヘルパーを必ず使用してください。生の dict 解析が許可されるのは境界部分のみで、直後に必ずラップしてください。 - 慣用的なコードから逸脱する場合は、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 コントラクト、ネイティブテスト、スクリプト、生成されたラッパー、該当する場合はベンチマークスナップショットが含まれ、README から元の FunC 実装にリンクされています。
- 特に参考になる Acton/Tolk スイート:
jetton-v2とhttps://github.com/ton-blockchain/jetton-contract/tree/jetton-2.0の組み合わせnftとhttps://github.com/ton-blockchain/nft-contractの組み合わせw5とhttps://github.com/ton-blockchain/wallet-contract-v5の組み合わせdnsとhttps://github.com/ton-blockchain/dns-contractの組み合わせmultisig-v2とhttps://github.com/ton-blockchain/multisig-contract-v2の組み合わせhighload-v3とhttps://github.com/ton-blockchain/highload-wallet-contract-v3の組み合わせnotcoinとhttps://github.com/OpenBuilders/notcoin-contractの組み合わせconfigとhttps://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/config-with-ownable-params.fcの組み合わせelectorと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 ...を実行する前に、Acton.tomlがあるディレクトリへcdしてください(Acton はそこを基準にコントラクトと設定を解決します)。
移植ワークフロー(Acton を優先)
1) 動作仕様を特定する
- FunC のエントリーポイント(
recv_internal、recv_external、run_ticktock、getter)を特定し、処理対象のオペコードを一覧にします。 - 維持すべき不変条件を抽出します。
- 正確なストレージレイアウト(ビット幅、フィールドの順序、参照の有無)
- メッセージの TL-B レイアウト(
EitherペイロードやMaybeフィールドを含む) - ガス/リザーブの動作と送信モード
- 既存の JS/TS テストを仕様として扱い、移植時にネイティブの Tolk テストへ書き換えます。
2) Acton プロジェクトの構成をセットアップする
- プロジェクトを作成するか、既存のものを再利用します。
acton new PROJECT --template empty|counter|jetton
[contracts.*]の下に、Acton.tomlでコントラクトを定義します。- 深い相対インポートを避けるために
[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) 低レベルのメッセージ構築を置き換える
- 手動の
builderエンコーディングより、createMessage({ ... })と.send(SEND_MODE_...)を優先します。 - 送信モードとバウンスフラグは、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) オペコードのディスパッチ:if (op == ...) の連鎖を型付きユニオンと match に置き換える
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
}
}
これが優れている理由:
- 手動解析のずれをなくしながら、オペコードの互換性を維持します
- 分岐ごとの解析を強い型付けで保ちます
- オペコードの種類が多い場合も、より適切に拡張できます(wallet-v5/vesting/telemint 形式)
B) アドホックな読み込みではなく、本文を構造体に解析する
推奨:
- 外部から見えるオペコードには
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 レイアウト(フィールド順序と ref/inline の形状)を同一に保ちます
struct Storage { totalSupply: coins adminAddress: address? }
fun Storage.load() { return Storage.fromCell(contract.getData()) }
fun Storage.save(self) { contract.setData(self.toCell()) }
E) 複数形状の状態を明示的にモデル化する(初期化済み/未初期化)
NFT/DNS/Telemint のアイテムでよく見られます:
- FunC では、形状を判定するために
slice_bits() > 0や ref の有無を確認します。
イディオマティックな 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) 手動の begin_cell() によるメッセージ組み立てより createMessage(...) を優先する
デフォルト:
createMessage({ ... })で送信メッセージを構築します.send(SEND_MODE_...)で送信します
手動ビルダーを使用するのは、次の場合に限ります:
- 過去の癖を正確に再現する場合
- inline/no-ref の本文レイアウトを厳密に指定する場合
H) 外部署名のフロー:サフィックス/残りのデータを意図的に解析する
ウォレット形式の外部メッセージの場合:
- 署名を署名対象のペイロードから分離します(
getLastBits/removeLastBitsまたは明示的なフィールド順序を使用します) - まずハッシュ/署名を検証します
- リプレイ防止状態(
seqno/クエリマップ)を更新し、永続化してコミットします - その後、アクション/メッセージを実行します
これは、強化された wallet-v5/highload のパターンに沿っています。
I)バウンスメッセージの処理:型付きのバウンスユニオンで onBouncedMessage を優先します
FunC の基本形:
recv_internal内のバウンス分岐+手動のload_uint
慣用的な Tolk:
onBouncedMessage(in: InMessageBounced)- バウンス時に処理するオペコードのサブセットに、必要に応じてユニオンを使用します:
type BounceOpToHandle = A | B
- 型付きの解析済みメッセージから残高/供給量を復元します
J)レガシー TL-B/動作を維持するための互換性オプション
FunC のテストと一致させる場合は、以下を意図的に使用します:
- 参照なしのボディの互換性には
UnsafeBodyNoRef { forceInline: ... } - レガシーのレイアウトが
address?より広範な場合はany_address? - ペイロードの正確性検証には、明示的な
(Either Cell ^Cell)の残り部分検証ヘルパー - 未知のオペコードの動作(
throw 0xFFFFと空/ボディの無視)を同一に保ちます
K)実際の移植から得られたドメイン固有のレシピ
Jetton ファミリー:
AllowedMessageToMinter/AllowedMessageToWallet- 型付きのバウンス復元(
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 スイートを、モジュール構成、型付きストレージ、メッセージユニオン、ラッパー、スクリプト、テスト、ベンチマークスナップショットのスタイルの基準として使用します
以下を使って検索することを推奨します:
- オペコードの 16 進数(例:
0xd53276db) - メッセージ型名(例:
InternalTransferStep) - ストレージフィールド名(例:
totalSupply、ownerAddress)
スイートの対応関係、元の FunC へのリンク、検索対象については、references/repo-examples.md を開いてください。
ドキュメントを優先した参照
- Tolk 言語の詳細および標準ライブラリについては、TON Docs の
languages/tolk/以下のページ(概要、シリアライズ、標準ライブラリ、FunC との比較)を使用してください。 - Acton 固有の情報(コマンド、ラッパー、テスト、スクリプト)については、Acton のドキュメントと
acton --helpを使用してください。