TONプロジェクト開発チュートリアル(1):ソースコードの観点からTONチェーン上でNFTを作成する方法
著者 :@Web3Mario(https://x.com/web3_mario)
要約 :前回のTON技術紹介に続き、この期間にTON公式開発ドキュメントを深く研究しましたが、学ぶには少しハードルがあると感じました。現在のドキュメント内容は、内部開発ドキュメントのように見え、新しく入門する開発者にはあまり優しくありません。そのため、自分の学習の軌跡をもとに、TON Chainプロジェクト開発に関する一連の記事を整理してみました。皆さんがTON DApp開発に迅速に入門する手助けになれば幸いです。文中に誤りがあれば、ぜひご指摘いただき、一緒に学びましょう。
EVMでのNFT開発とTON ChainでのNFT開発の違い
FTやNFTを発行することは、DApp開発者にとって通常最も基本的なニーズです。そこで、これを学習の入り口としました。まず、EVM技術スタックでNFTを開発することとTON Chainで開発することの違いを理解しましょう。EVMベースのNFTは通常、ERC-721標準を継承することを選択します。NFTとは、分割不可能な暗号資産の一種であり、各資産はユニークであり、特定の専有特性を持っています。ERC-721は、このタイプの資産に対する一般的な開発パラダイムです。一般的なERC721コントラクトが実装する必要がある関数や記録する情報を見てみましょう。下の図はERC721インターフェースです。FTとは異なり、転送インターフェースでは、転送するtokenIdを入力する必要があります。このtokenIdはNFT資産のユニーク性を最も基本的に示すものであり、もちろん、より多くの属性を保持するために、通常は各tokenIdにメタデータを記録します。このメタデータは外部リンクであり、そのNFTの他の拡張データを保存します。例えば、PFP画像のリンクや特定の属性名などです。
Solidityに精通しているか、オブジェクト指向開発に慣れている開発者にとって、このようなスマートコントラクトを実装することは容易なことです。必要なデータ型、例えばいくつかの重要なマッピング関係を定義し、必要な機能に応じてこれらのデータの変更ロジックを開発すれば、NFTを実現できます。
しかし、TON Chainではすべてが少し異なります。その主な理由は2つあります:
- TONではデータの保存がCellに基づいて実装されており、同じアカウントのCellは有向非循環グラフを通じて実現されています。これにより、永続的に保存する必要があるデータは無限に増加することができません。有向非循環グラフでは、データの深さがクエリコストを決定するため、深さが無限に延びると、クエリコストが高くなり、コントラクトがデッドロックに陥る可能性があります。
- 高い同時実行性能を追求するために、TONは逐次実行のアーキテクチャを放棄し、並行処理のために生まれた開発パラダイムであるActorモデルを採用して実行環境を再構築しました。これにより、スマートコントラクト間は、いわゆる内部メッセージを送信することによって非同期に呼び出すことしかできなくなります。状態変更型または読み取り型の呼び出しのいずれも、この原則に従う必要があります。さらに、非同期呼び出しが失敗した場合のデータのロールバック方法についても慎重に考慮する必要があります。
もちろん、技術的な他の違いについては前回の記事で詳しく述べました。本記事ではスマートコントラクト開発に焦点を当てたいと思いますので、詳細には触れません。上記の2つの設計原則により、TONにおけるスマートコントラクト開発はEVMとは大きく異なります。最初の議論で、NFTコントラクトにはいくつかのマッピング関係、つまりNFTに関連するデータを保存するためのmappingを定義する必要があることがわかりました。その中で最も重要なのはownersであり、このmappingは特定のtokenIDに対応するNFTの所有者アドレスのマッピング関係を保存し、NFTの所有権を決定します。転送はその所有権の変更です。理論的には、これは無限に増加可能なデータ構造であるため、できるだけ避ける必要があります。そのため、公式は無限データ構造の存在をシャーディングの基準として推奨しています。つまり、同様のデータ保存のニーズがある場合、マスター・スレーブコントラクトのパラダイムを代替し、サブコントラクトを作成することで各keyに対応するデータを管理します。そして、マスターコントラクトがグローバルパラメータを管理したり、サブコントラクト間の内部情報のやり取りを助けたりします。
これは、TONのNFTも同様のアーキテクチャで設計する必要があることを意味します。各NFTは独立したサブコントラクトであり、所有者アドレスやメタデータなどの専有データを保存し、マスターコントラクトを通じてグローバルデータを管理します。例えば、NFTの名前、シンボル、総供給量などです。
アーキテクチャが明確になった後、次にコア機能のニーズを解決する必要があります。このマスター・スレーブコントラクトの方式を採用するため、どの機能がマスターコントラクトによって担われ、どの機能がサブコントラクトによって担われるのか、また両者がどのように内部情報を通信するのかを明確にする必要があります。同時に、実行エラーが発生した場合に、以前のデータをどのようにロールバックするかを考慮する必要があります。通常、複雑な大規模プロジェクトを開発する前に、クラス図を作成し、相互の情報フローを明確にし、内部呼び出しが失敗した場合のロールバックロジックを慎重に考えることが必要です。もちろん、上記のNFT開発はシンプルですが、同様の検証を行うこともできます。
ソースコードからTONスマートコントラクトの開発を学ぶ
TONは、C言語に似た静的型付け言語であるFuncをスマートコントラクト開発言語として設計しました。それでは、ソースコードからTONスマートコントラクトの開発方法を学びましょう。私はTON公式ドキュメントのNFTの例を選んで紹介します。興味のある方は自分で調べてみてください。このケースでは、シンプルなTON NFTの例を実装しています。コントラクト構造を見てみましょう。2つの機能コントラクトと3つの必要なライブラリに分かれています。
これらの2つの主要な機能コントラクトは、上記の原則に従って設計されています。まず、主コントラクトnft-collectionのコードを見てみましょう。
これにより、最初の知識点が導入されます。TONスマートコントラクトでデータを永続的に保存する方法です。Solidityでは、データの永続的な保存はEVMがパラメータの型に基づいて自動的に処理します。通常、スマートコントラクトの状態変数は、実行終了後に最新の値に基づいて自動的に永続的に保存され、開発者はこのプロセスを考慮する必要はありません。しかし、Funcでは状況が異なり、開発者は相応の処理ロジックを自分で実装する必要があります。この状況は、CやC++がGCのプロセスを考慮する必要があるのに似ていますが、他の新しい開発言語では通常、この部分のロジックが自動化されています。コードを見てみましょう。まず、いくつかの必要なライブラリをインポートし、次に最初の関数loaddataが永続的に保存されたデータを読み取るために使用されます。そのロジックは、まずgetdataを通じて永続的なコントラクトストレージ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とは異なり、hashmapに基づいて呼び出すのではないため、呼び出し順序を乱すことはできません。
savedata関数では、ロジックは似ていますが、これは逆のプロセスです。これにより、次の知識点が導入されます。新しい型builder、これはcellビルダーの型です。データビットや他のcellへの参照はビルダーに保存でき、ビルダーは最終的に新しいcellに最終化されます。まず、標準関数begincellを通じてビルダーを作成し、次にstore関連関数を通じて関連関数を保存します。注意すべきは、上文での呼び出し順序とここでの保存順序を一致させる必要があることです。最後に、endcellを通じて新しいcellの構築を完了します。この時、cellはメモリ内で管理され、最終的に最外層のsetdataを通じて、そのcellの永続的な保存が完了します。
次に、ビジネス関連の関数を見てみましょう。まず、コントラクトを通じて新しいコントラクトを作成する方法についての知識点を紹介する必要があります。これは、先ほど紹介したマスター・スレーブアーキテクチャで頻繁に使用されます。TONでは、スマートコントラクト間の呼び出しは内部メッセージを送信することで実現されます。これは、sendrawmessageという名前の関数を通じて実現されます。注意すべきは、最初のパラメータがメッセージエンコードされたcellであり、第二のパラメータがフラグで、トランザクションの実行方法の違いを示します。TONでは、異なる内部メッセージ送信の実行方法が設定されており、現在3種類のメッセージモードと3種類のメッセージフラグがあります。単一のモードを複数の(場合によってはない)フラグと組み合わせて、必要なモードを得ることができます。 以下に示すモードとフラグの説明表:
それでは、最初の主要な関数deploynftitemを見てみましょう。これは、新しいNFTインスタンスを作成または鋳造するための関数です。いくつかの操作を経てmsgをエンコードし、sendrawmessageを通じて内部コントラクトを送信し、フラグ1の送信フラグを選択しました。これは、エンコード中に指定されたfeeを今回の実行のガス費用としてのみ使用します。上文の紹介から、このエンコードルールは新しいスマートコントラクトを作成する方法に対応していることが容易に理解できます。それでは、具体的にどのように実装されているのか見てみましょう。
51行目を直接見てみましょう。上の2つの関数はメッセージ生成に必要な情報を生成する補助関数ですので、後で見てみます。これは、スマートコントラクトの内部メッセージのエンコードプロセスです。中間のいくつかの数字は、実際にはいくつかのフラグであり、この内部メッセージの要求を示すために使用されます。ここで次の知識点を導入します。TONは、メッセージの実行方法を記述するためにTL-Bという名前のバイナリ言語を選択し、異なるフラグを設定することで特定の機能を持つ内部メッセージを実現します。最も容易に思いつく2つの使用シーンは、新しいコントラクトの作成と既にデプロイされたコントラクトの関数呼び出しです。そして、51行目のこの方法は前者に対応し、新しいnft itemコントラクトを作成します。これは主に55、56、57行で指定されています。まず55行のこの長い数字は一連のフラグであり、注意すべきはstore_uintの最初の引数が数値であり、第二の引数がビット長であり、これによりこの内部メッセージがコントラクト作成であることが決定され、最後の3つのフラグは、内部メッセージがStateInitデータを伴うことを示します。このデータは新しいコントラクトのソースコードと初期化に必要なデータです。そして、最後のフラグは内部メッセージの付加を示し、関連するロジックと必要なパラメータを実行したいことを示します。したがって、66行目のコードにはこの3つのデータが設定されていないことがわかります。これは、すでにデプロイされたコントラクトの関数呼び出しを示しています。具体的なエンコードルールはここで確認できます。
StateInitのエンコードルールは49行目のコードに対応し、calculatenftitemstateinitを通じて計算されます。注意すべきは、stateinitデータのエンコードも既定のTL-Bエンコードルールに従い、いくつかのフラグを除いて、新しいコントラクトのcodeと初期化dataの2つの部分に関わります。dataのエンコード順序は、新しいコントラクトが指定した永続cellの保存順序と一致させる必要があります。36行目では、初期化データにitemindexがあり、これはERC721のtokenIdに似ており、標準関数myaddressが返す現在のコントラクトアドレス、つまりcollection_addressです。このデータの順序はnft-itemの宣言と一致しています。
次の知識点は、TONではすべての未生成のスマートコントラクトの生成後のアドレスを事前に計算できることです。これはSolidityのcreate2関数に似ています。TONでは新しいアドレスの生成は2つの部分で構成されており、workchain識別子とstateinitのハッシュ値が結合されます。前者は、TONの無限シャーディングアーキテクチャに対応するために指定する必要があることがわかります。現在は統一値です。これは標準関数workchainによって取得されます。後者は標準関数cellhashによって取得されます。したがって、この例に戻ると、calculatenftitemaddressは新しいコントラクトアドレスを事前に計算する関数です。そして、生成された値は53行目でメッセージにエンコードされ、この内部メッセージの受信アドレスとして使用されます。nft_contentは、作成されたコントラクトの初期化呼び出しに対応し、具体的な実装は次の記事で紹介します。
sendroyaltyparamsは、特定の読み取りリクエストに対する内部メッセージの応答である必要があります。前回の紹介で、TONでは内部メッセージがデータを変更する操作だけでなく、読み取り操作もこの方法で実現する必要があることを特に強調しました。したがって、このコントラクトはそのような操作に対応しています。まず、67行目はこのリクエストに応じたリクエスト者のコールバック関数のフラグを示しています。これは、リクエストされたitem indexおよび関連するroyaltyデータを返すことになります。
次に、TONのスマートコントラクトには2つの統一されたエントリポイントがあり、recvinternalとrecvexternalと呼ばれています。前者はすべての内部メッセージの統一された呼び出しエントリであり、後者はすべての外部メッセージの統一された呼び出しエントリです。開発者は、関数内部でのニーズに応じて、switchのような方法を用いてmessageで指定された異なるフラグに基づいて異なるリクエストに応答する必要があります。ここでのフラグは、上記67行目のコールバック関数のフラグです。この例に戻ると、まずmessageの空位チェックを行い、次にmessage内の情報を解析します。まず83行目でsender_addressを解析し、このパラメータは後続の権限チェックに使用されます。注意すべきは、ここでの~演算子は別の構文糖です。ここでは詳しくは触れません。次に、op操作フラグを解析し、その後、異なるフラグに基づいてそれぞれのリクエストを処理します。ここでは、特定のロジックに基づいて上記の関数を呼び出しています。例えば、royaltyパラメータのリクエストに応じたり、新しいnftを鋳造したり、グローバルindexを自動的に増加させたりします。
次の知識点は108行目に対応し、名前からもわかるように、この関数の処理ロジックはSolidityのrequire関数に似ています。Funcでは、標準関数throwunlessを通じて例外を投げます。最初の引数はエラーコードで、第二の引数はチェックビットのブール値です。ビットがfalseの場合、例外が投げられ、エラーコードが付加されます。この行では、equalslicesを通じて上記で解析したsenderaddressがコントラクトの永続的な保存のowneraddressと等しいかどうかを判断し、権限を確認します。
最後に、コード構造をより明確にするために、永続的な情報を取得するための一連の補助関数を作成しました。ここでは詳しくは触れませんが、開発者はこの構造を参考にして自分のスマートコントラクトを開発できます。
TONエコシステムのDApp開発は非常に興味深いものであり、EVMの開発パラダイムとは大きく異なります。そのため、TON ChainでのDApp開発方法を紹介する一連の記事を通じて、皆さんと共に学び、この機会をつかみたいと思います。また、Twitterで私と交流し、新しい興味深いDAppアイデアを共有し、一緒に開発することも歓迎します。