
TONプロジェクト開発チュートリアル(1):ソースコードの観点から、TONチェーン上にNFTを作成する方法
TechFlow厳選深潮セレクト

TONプロジェクト開発チュートリアル(1):ソースコードの観点から、TONチェーン上にNFTを作成する方法
TONエコシステムにおけるDApp開発は本当に面白いことだ。
著者:@Web3Mario
概要:前回のTON技術紹介記事に続き、ここ数日TON公式開発ドキュメントを深く研究してきましたが、学習にはある程度のハードルがあると感じました。現在のドキュメントはやや内部向けの開発資料のように見え、新しく参入する開発者にとってはあまり親しみやすいものではないようです。そこで、自分の学習軌跡に基づき、TON Chain上でのプロジェクト開発に関する一連の記事を整理してみることにしました。これにより、皆さんがTON DApp開発に素早く入門できるようになればと思います。記述に誤りがあれば、ぜひご指摘ください。一緒に学んでいきましょう。
EVMでのNFT開発とTON Chain上でのNFT開発の違い
DApp開発者にとって、FTまたはNFTを発行することは通常最も基本的なニーズです。そのため、私もこれを学習の入り口として選びました。まず、EVM技術スタックでNFTを開発することと、TON Chain上でNFTを開発することの違いについて理解しましょう。EVMベースのNFTでは、通常ERC-721標準を継承します。「NFT」とは分割不可能な暗号資産タイプであり、各資産は唯一無二の特性を持つという点が特徴です。ERC-721はこの種の資産に対する一般的な開発パターンです。よくあるERC721コントラクトがどのような関数を実装し、どのような情報を記録すべきかを見てみましょう。以下の図はERC721インターフェースです。FTとは異なり、送金インターフェースでは数量ではなく、送金対象のtokenIdを入力する必要があります。このtokenIdこそがNFT資産の一意性の最も基本的な表現です。さらに多くの属性を保持するために、通常は各tokenIdに対してmetadataを記録します。このmetadataは外部リンクであり、そのNFTの拡張可能なデータ(例:PFP画像のリンクや特定の属性名など)を保存しています。

Solidityに慣れ親しんでいる、あるいはオブジェクト指向に詳しい開発者にとっては、このようなスマートコントラクトを実装するのは簡単です。必要なデータ型(重要なマッピング関係など)を定義し、必要な機能に応じてこれらのデータを変更するロジックを開発すれば、NFTを実現できます。
しかし、TON Chainでは状況が異なります。その主な理由は二つあります:
l TONではデータの保存がCellによって実現されており、同じアカウントのCellは有向非巡回グラフ(DAG)によって接続されています。これにより、永続化されるデータは無制限に増加できないという問題が生じます。なぜなら、DAGにおいてはデータの深さが検索コストを決定し、深さが無限に伸びると検索コストが過度に高くなり、コントラクトがデッドロック状態に陥る可能性があるためです。
l 高い並列処理性能を追求するため、TONは逐次実行のアーキテクチャを放棄し、代わりに並列処理に特化したアクターモデルを採用して実行環境を再構築しています。この結果、スマートコントラクト間の呼び出しは、「内部メッセージ」を送信する非同期方式でのみ可能になります。状態変更操作であろうと読み取り専用操作であろうと、すべてこの原則に従う必要があります。また、非同期呼び出しが失敗した場合のデータロールバック処理も慎重に検討する必要があります。
他の技術的相違点については前の記事で詳しく説明しましたので、本稿ではスマートコントラクト開発に焦点を当て、それらについては深入りしません。上記の二つの設計原則により、TONにおけるスマートコントラクト開発はEVMとは大きく異なります。前述した通り、NFTコントラクトではマッピング(mapping)を定義してNFTに関連するデータを保存する必要があります。特に重要なのは「owners」で、これは特定のtokenIdに対応するNFTの所有者アドレスを記録するマッピングであり、NFTの所有権を決定します。送金とはこの所有権の変更に他なりません。理論的にはこのデータ構造は無限に拡大可能ですが、それを避けるべきです。そのため、公式では境界なしのデータ構造の有無をシャーディングの基準として推奨しています。つまり、同様のデータ保存ニーズがある場合、マスター・スレーブ契約パターンを使用し、子コントラクトを作成して各キーに対応するデータを管理します。マスターコントラクトはグローバルパラメータを管理したり、子コントラクト間の内部メッセージ交換を支援したりします。
つまり、TONにおけるNFTも同様のアーキテクチャで設計する必要があります。各NFTは独立したサブコントラクトとして、所有者アドレスやmetadataなどの固有データを保持し、マスターコントラクトがNFTのname、symbol、総供給量などのグローバルデータを管理します。
アーキテクチャを明確にした後は、次に主要な機能要件に取り組む必要があります。このマスター・スレーブ方式を採用したことで、どの機能をマスターコントラクトが担い、どの機能を子コントラクトが担うかを明確にする必要があります。また、両者がどのように内部メッセージで通信するか、エラー発生時のデータロールバック方法も検討しなければなりません。通常、複雑で大規模なプロジェクトを開発する前に、クラス図を作成し、相互の情報フローを明確にし、内部呼び出しが失敗した際のロールバックロジックを慎重に検討することが必要です。もちろん、今回のNFT開発は比較的単純ですが、同様の検証を行うことは有効です。

ソースコードからTONスマートコントラクトの開発を学ぶ
TONはC言語風の静的型付け言語「Func」をスマートコントラクト開発言語として採用しています。それでは、実際にソースコードを通してTONスマートコントラクトの開発方法を学びましょう。ここではTON公式ドキュメントのNFTサンプルを紹介します。興味のある方はこちらをご覧ください。このケースではシンプルなTON NFTの実装例が示されています。コントラクト構造を見てみると、2つの主要な機能コントラクトと3つの必須ライブラリに分かれています。

これら2つの主要な機能コントラクトは前述の原則に基づいて設計されています。まず、マスターコントラクトnft-collectionのコードを見てみましょう:

ここで最初の知識ポイントが登場します。TONスマートコントラクトでデータを永続的に保存する方法です。Solidityでは、EVMがパラメータの型に基づいて自動的にデータの永続化を処理します。通常、スマートコントラクトの状態変数は実行終了後に最新の値で自動的に永続化され、開発者はこのプロセスを意識する必要はありません。しかし、Funcではそうではなく、開発者が自ら処理ロジックを実装する必要があります。これはCやC++でガベージコレクション(GC)を考慮するのに似ていますが、最近の他の開発言語ではこの部分のロジックは自動化されています。コードを見てみましょう。まず必要なライブラリをインポートし、その後、永続化されたデータを読み取るための関数load_dataがあります。そのロジックは、get_dataを使って永続化されたコントラクトストレージcellを返すことです。これは標準ライブラリstdlib.fcによって実装されていることに注意してください。通常、一部の関数はシステム関数として扱うことができます。
この関数の戻り値の型はcellで、TVMのcell型です。以前の説明で、TONブロックチェーンのすべての永続データはcellツリーに格納されていることを知っています。各cellは最大1023ビットの任意データと、最大4つの他のcellへの参照を持ちます。cellはスタックベースのTVMにおいてメモリとして使用されます。cellには緊密にエンコードされたデータが保存されており、その中の具体的なプレーンテキストデータを得るには、cellを「slice」と呼ばれる型に変換する必要があります。cellはbegin_parse関数によってslice型に変換でき、sliceからデータビットや他のcellへの参照を読み出すことでcell内のデータを取得できます。15行目の呼び出し方法はfuncにおける構文糖であり、第一関数の戻り値に対して第二関数を直接呼び出せます。最後に、データの永続化順序に従ってそれぞれのデータを読み込みます。このプロセスはSolidityとは異なり、ハッシュマップによる呼び出しではないため、呼び出し順序を間違えてはいけません。
save_data関数では、ロジックは類似していますが、逆方向のプロセスです。ここで次の知識ポイントが登場します。新しい型「builder」です。これはcellビルダーの型です。データビットや他のcellへの参照はbuilderに保存でき、最終的にbuilderは新しいcellに確定されます。まず標準関数begin_cellを使ってbuilderを作成し、store関連関数を使って関連データを順次保存します。前述の呼び出し順序と保存順序は一致させる必要があります。最後にend_cellで新しいcellの構築を完了します。この時点でcellはメモリ内に管理され、最外層のset_dataを通じてcellの永続化が完了します。

次に、業務関連の関数を見ていきましょう。まず、次の知識ポイントを紹介します。コントラクトから新しいコントラクトを作成する方法です。これは先ほど紹介したマスター・スレーブアーキテクチャで頻繁に使用されます。TONでは、スマートコントラクト間の呼び出しは内部メッセージを送信することで実現されます。これはsend_raw_messageという関数で行われます。最初の引数はメッセージをエンコードしたcell、2番目の引数はフラグで、トランザクションの実行方法の違いを示します。TONでは異なる内部メッセージ送信方式が設定されており、現在3種類のModesと3種類のFlagsがあります。単一のModeと複数(あるいはなし)のフラグを組み合わせて目的のmodeを得られます。組み合わせとは、それらの値の合計を埋め込むだけでよいです。以下にModesとFlagsの説明表を示します:

では最初の主要な関数deploy_nft_itemを見てみましょう。名前の通り、これは新しいNFTインスタンスを作成(または鋳造)する関数です。いくつかの操作でメッセージをエンコードした後、send_raw_messageで内部コントラクトを送信し、フラグ1の送信識別子を選択します。これにより、エンコードで指定されたfeeのみが今回の実行のgas feeとして使われます。前述の説明から容易に推測できるように、このエンコードルールは新しいスマートコントラクトを作成する方法に対応しているはずです。
では、具体的にどう実装されているか見ていきましょう。
51行目から見てみましょう。上の2つの関数はメッセージ作成に必要な情報を生成するヘルパー関数なので、後ほど見ることにします。これはスマートコントラクト作成の内部メッセージをエンコードするプロセスです。途中にあるいくつかの数字も識別子であり、内部メッセージの要求内容を示しています。ここで次の知識ポイントが登場します。TONは「TL-B」というバイナリ言語を使用してメッセージの実行方法を記述しており、異なるフラグを設定することで特定の機能を持つ内部メッセージを実現します。最も思い浮かぶ使用例は、新規コントラクトの作成と既存デプロイ済みコントラクトの関数呼び出しです。51行目の方式は前者、つまり新しいnft itemコントラクトの作成に対応しています。これは主に55、56、57行で指定されています。まず55行の長い数字列は一連の識別子です。store_uintの最初の引数は値、2番目はビット長です。この中で、後ろの3つのフラグが内部メッセージがコントラクト作成であることを決定します。対応するバイナリ値は111(10進数では4+2+1)です。前の2つはメッセージがStateInitデータを付帯することを意味し、このデータは新規コントラクトのソースコードおよび初期化に必要なデータです。最後のフラグは内部メッセージペイロードを意味し、関連ロジックの実行と必要なパラメータを希望していることを示します。そのため、66行目のコードではこの3つのデータを設定していないことから、これはデプロイ済みコントラクトの関数呼び出しであることがわかります。詳細なエンコードルールはこちらをご覧ください。
StateInitのエンコードルールは49行目のコードに対応し、calculate_nft_item_state_initで計算されます。stateinitデータのエンコードも所定のTL-Bエンコードルールに従います。いくつかのフラグに加え、主に新規コントラクトのcodeと初期化dataの2つの部分が関与します。dataのエンコード順序は、新規コントラクトで指定された永続化cellの保存順序と一致させる必要があります。36行目では、初期化データにitem_index(ERC721のtokenIdに類似)と、標準関数my_addressが返す現在のコントラクトアドレス(collection_address)が含まれており、このデータ順序はnft-itemで宣言されたものと一致しています。
次の知識ポイントは、TONでは未生成のスマートコントラクトであっても、生成後のアドレスを事前に計算できる点です。これはSolidityのcreate2関数と似ています。TONでは、新規アドレスはworkchain識別子とstateinitのハッシュ値を連結して構成されます。前者は前述の通り、TONの無限シャーディングアーキテクチャに対応するために指定されるもので、現在は固定値です。標準関数workchainで取得します。後者は標準関数cell_hashで取得します。よって、この例に戻ると、calculate_nft_item_addressは新規コントラクトアドレスを事前に計算する関数であり、生成された値は53行目でメッセージにエンコードされ、内部メッセージの受信アドレスとして使用されます。nft_contentは作成されるコントラクトの初期化呼び出しに対応し、具体的な実装は次の記事で紹介します。
send_royalty_paramsは、特定の読み取り専用リクエストに対する内部メッセージの応答です。前述の説明で強調したように、TONの内部メッセージはデータ変更操作だけでなく、読み取り操作もこの方式で実現する必要があります。このコントラクトはまさにそのような操作です。まず67行目に注目すると、リクエストに応答した後に呼び出し元にコールバックするためのマークが示されています。その後に続くのは返却データで、リクエストされたitem indexと対応するroyaltyデータです。
次に次の知識ポイントを導入します。TONのスマートコントラクトにはrecv_internalとrecv_externalという2つの統一されたエントリポイントしかありません。前者はすべての内部メッセージの統一呼び出しエントリ、後者はすべての外部メッセージの統一呼び出しエントリです。開発者は関数内で、メッセージの異なるフラグに応じて、switchのような方式で異なるリクエストに応答する必要があります。ここで言うフラグは67行目のコールバック関数フラグのことです。この例に戻ると、まずメッセージの空チェックを行い、その後メッセージ内の情報を解析します。83行目でsender_addressを解析し、後続の権限チェックに使用します。ここで~演算子は別の構文糖です。ここでは詳述しません。次にop操作フラグを解析し、フラグに応じてそれぞれのリクエストを処理します。その中で前述の関数を論理的に呼び出します。例えば、royaltyパラメータリクエストに応答したり、新しいNFTを鋳造してグローバルindexをインクリメントしたりします。
次の知識ポイントは108行目に該当します。おそらく名前から処理ロジックがわかるでしょう。Solidityのrequire関数と同様に、Funcでは標準関数throw_unlessを使って例外をスローします。最初の引数はエラーコード、2番目はチェック用のブール値で、falseの場合例外をスローし、そのエラーコードを付加します。この行ではequal_slicesを使って、前述で解析されたsender_addressがこのコントラクトの永続化ストレージに保存されたowner_addressと等しいかどうかを判断し、権限チェックを行います。

最後に、コード構造をより明確にするために、永続化情報の取得を支援する一連のヘルパー関数を導入しました。ここでは詳述しませんが、開発者はこのような構造を参考にして自身のスマートコントラクトを開発できます。

TONエコシステムにおけるDApp開発は非常に興味深いものです。EVMの開発パターンとは大きく異なります。そのため、私は一連の記事を通じてTON ChainでのDApp開発方法を紹介していきます。皆さんと一緒に学び、このチャンスを掴みましょう。Twitterで私とやり取りして、新しい面白いDAppのアイデアを共有し、一緒に開発しましょう。
TechFlow公式コミュニティへようこそ
Telegram購読グループ:https://t.me/TechFlowDaily
Twitter公式アカウント:https://x.com/TechFlowPost
Twitter英語アカウント:https://x.com/BlockFlow_News














