はじめに
2024年、Solanaは総ロック額(TVL)で4位のパブリックブロックチェーンに躍り出て、投資家や開発者の関心を集めました。
BlockSecは、Solanaの基本概念、Solanaスマートコントラクトの書き方チュートリアル、Solanaトランザクション分析ガイドなどを掲載した「Solana Simplified」シリーズをキュレーションしました。このシリーズの目標は、読者がSolanaエコシステムを理解し、Solanaでのプロジェクト開発やトランザクション実行に必要なスキルを習得することです。
このシリーズの最初の記事では、Solanaネットワークのオペレーションメカニズム、アカウントモデル、トランザクションなど、Solanaネットワークの主要な概念を掘り下げ、正確で高性能なSolanaコントラクトを作成するための強固な基盤を築きました。
この記事では、記事の投稿と表示を行うSolanaプログラム(Solanaスマートコントラクト)の作成方法を説明します。これにより、最初の記事の概念を定着させ、まだ説明していないSolanaのいくつかの機能を紹介します。関連するプログラムとテストコードはGitHubで公開されています。
環境設定
注:以下のコマンドはUbuntuシステムのみを対象としています。WindowsおよびmacOSシステムでは一部のコマンドが機能しない場合があります。
👉 代替コマンドを使用するか、ローカル開発環境のセットアップとSolana CLIのインストールを参照してください。
ローカル環境でSolanaプログラムをコンパイルおよびデプロイします。Solanaプログラム開発に進む前に、いくつかのコマンドと依存関係をインストールする必要があります。
Rust
Solanaプログラムは主にRustプログラミング言語で記述されるため、Rustをインストールするには次のコマンドを実行します。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
node.js & TypeScript
テストスクリプトはTypeScriptで記述されているため、Node.jsとTypeScriptツールチェーンをインストールする必要があります。
curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash -
sudo apt-get install -y nodejs npm
npm install -g typescript
npm install -g ts-node
Solana CLI
次に、ウォレットの作成やコントラクトのデプロイなどのタスクにコマンドを提供するSolana CLIツールをインストールする必要があります。
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
現在のターミナルにこれらの変更を適用するには、次のコマンドを実行します。
source ~/.profile
solana --versionを実行して、Solana CLIが正常にインストールされたことを確認できます。
$ solana --version
solana-cli 1.17.25 (src:d0ed878d; feat:3580551090, client:SolanaLabs)
ローカルウォレット
次に、ローカルファイルシステムウォレットを生成するには、次のコマンドを実行します。
solana-keygen new
デフォルトでは、このコマンドは~/.config/solana/id.jsonに秘密鍵ファイルを作成します。このファイルは後でテストスクリプトで使用します。solana addressコマンドを使用して、生成されたアドレスを確認できます。
$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy
Devnet
プログラムはdevnetにデプロイされるため、Solana CLIをdevnetに切り替えるには次のコマンドを実行します。
solana config set --url https://api.devnet.solana.com
SOLトークン
コントラクトのデプロイやテストトランザクションの送信は無料ではないため、SOLトークンをリクエストする必要があります。solana airdrop 2を実行するか、パブリックWebフォーセットから直接トークンをリクエストできます。
プログラムの記述
これから紹介するプログラムは、ユーザーが記事を投稿し、現在投稿されているすべての記事を表示できるようにします。3種類の命令を処理します。
- init 命令:記事の現在の最大インデックスを保存するデータアカウントを作成してプログラムを初期化します。
- post 命令:投稿された記事を新しいデータアカウントに保存します。
- list 命令:ログに投稿されたすべて記事を表示します。
プログラムの構造は以下のとおりです。
$ tree program/
program/
├── Cargo.lock
├── Cargo.toml
└── src
├── instructions
│ ├── init.rs
│ ├── list.rs
│ ├── mod.rs
│ └── post.rs
├── lib.rs
└── processor.rs
Cargo.tomlはプロジェクトの外部ライブラリを指定します。
[dependencies]
borsh = "0.10.0"
borsh-derive = "0.10.0"
solana-program = "=1.17.25"
注:
solana-programのバージョンは、インストールされているSolana CLIのバージョンと一致する必要があります(solana --versionで確認できます)。一致しない場合、コンパイル中にエラーが発生する可能性があります。
コントラクトのエントリ関数はprocessor.rsで定義されています。ファイルは、必要な依存関係をインポートすることから始まります(他のRustファイルでは同様の部分は省略します)。

次に、コントラクトのエントリ関数の定義が続きます。

プログラムの10行目では、Solanaライブラリのマクロentrypoint!が、エントリ関数がprocess_instructionであることを指定します。この関数は3つのパラメータを受け取ります。
- program_id:現在のプログラムのデプロイ済みアドレス。
- accounts:命令に関与するすべてのアカウント。
- instruction_data:命令の処理に使用されるバイト配列。
process_instructionはinstruction_dataの最初のバイトを抽出して命令の種類を識別し、対応する関数を呼び出して処理します。次に、これらの3つの関数がどのように実装されているかを見ていきましょう。
init
3つの関数のそれぞれは、program/src/instructionsディレクトリ内の同じ名前のファイルで定義されています。まずinit.rsから見ていきましょう。

init命令は追加のバイト配列を必要としないため、program_idとaccountsの2つのパラメータのみを受け取ります。
12行目から15行目にかけて、プログラムは必要なアカウント情報を順次抽出し、次にfind_program_address関数を呼び出して、現在の記事インデックスの最大値を保存するために使用されるデータアカウントのアドレスであるindex_pda_keyを計算します。その後、プログラムはindex_pda_keyがindex_pdaのアドレスと等しいことをアサートします。
ここで、index_pdaのアドレスは、PDA(Program Derived Addresses)と呼ばれる特別なアドレスです。公開鍵から生成される「ウォレット」アカウントアドレスとは異なり、PDAはオプションのバイト配列、バイト「bump」、およびプログラムのアドレスから導出されます。バイト配列とプログラムアドレスは呼び出し元によって明示的に提供され、bumpはfind_program_address関数によって自動的に生成されて返されます。bumpにより、このように作成されたアドレスは公開鍵によって生成されたアドレスと衝突しません。

index_pdaのアドレスが期待通りであることを確認した後、SystemProgramから提供されるcreate_account命令を使用します。index_pdaを作成します。22行目では、プログラムはIndexAccountData型の変数を生成します。この変数は記事の現在の最大インデックス(初期値は0)を記録します。下の図に示すように、この型はBorshSerializeおよびBorshDeserializeトレイトを実装しており、Borsh形式でシリアライズおよびデシリアライズできます。

23行目から24行目にかけて、アカウントデータ保存に必要なスペースとアカウント作成のための最小レントが計算されます。
27行目から37行目にかけて、SystemProgramからcreate_account命令が作成され、invoke_signedメソッドを使用して呼び出されます。あるプログラム内で別のプログラムの命令を呼び出すアクションは、CPI(Cross-Program Invocation)と呼ばれます。Solanaでは、invokeとinvoke_signedの2つの関数を使用してCPIを実行できます。
invoke関数は指定された命令を直接処理し、その関数シグネチャは次のとおりです。
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> Result<(), ProgramError>
invoke_signedとinvokeの唯一の違いは、invoke_signedがPDAアカウントへの署名方法を提供することです。PDAアカウントには公開鍵/秘密鍵のペアがないため、直接署名を提供できません。invoke_signedはこの問題を解決します。この関数はsigners_seedsと呼ばれる追加のパラメータを受け取ります。その関数シグネチャは次のとおりです。
find_program_address関数の動作と同様に、invoke_signedは提供されたsigners_seedsと呼び出し元のprogram_idに基づいてPDAアドレスのセットを計算します。account_infosにこれらのPDAアドレスが含まれている場合、それらは署名者としてマークされ、署名を提供したかのように扱われます。
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>
ここでは、create_account命令は作成されるアカウントを署名者として要求するため、invoke_signedを使用してindex_pdaの署名を提供する必要があります。

プログラムの最後に、コントラクトはindex_dataのserializeメソッドを呼び出してデータをシリアライズし、index_pdaのdataフィールドに書き込みます。
post_article

次に、post命令の実装を見てみましょう。init命令とは異なり、post命令は投稿された記事を受け取るため、instruction_data_innerという追加のパラメータがあり、記事のシリアライズされたデータが格納されています。
いつものように、プログラムは命令に必要なアカウント情報を抽出することから始まります。記事ごとに個別のPDAアカウントを生成するため、プログラムは23行目でnew_article_pdaという追加のアカウントを受け取ります。
init命令と同様に、27行目から30行目ではfind_program_addressを使用して、渡されたindex_pdaのアドレスが正しいことを確認します。次に、そのdataフィールドのデータをデシリアライズして、現在割り当てられている最大インデックスを読み取ります。
33行目から40行目でもnew_article_pdaのアドレスを確認します。プログラムは文字列「ARTICLE_PDA」と現在のインデックスを使用して新しいアドレスを生成します。インデックスは記事のアップロードごとにインクリメントされ、生成されるアドレスが一意であることを保証します。

次に、プログラムはinstruction_data_innerをPostArticleData型の記事データにデシリアライズします。

この構造体にはtitleとcontentの2つのフィールドしかありません。45行目から49行目にかけて、プログラムは記事が単一のアカウントに収まらないほど大きくないことを確認するチェックを実行します。

次の手順もinit命令と同様です。プログラムはまずアカウントに必要なスペースとレントを計算し、次にSystemProgramのcreate_account命令を呼び出して記事を保存するためのPDAアカウントを作成します。最後に、投稿された記事はnew_article_pdaのdataフィールドにシリアライズされ、現在の最大インデックスは1つインクリメントされてindex_pdaのdataフィールドにシリアライズされます。
list_articles

最後に、list命令の実装を見てみましょう。この命令はすべてのアカウントをリストする必要があるため、15行目では記事を保存しているすべてのアカウントPDAを表すためにベクターが使用され、index_pdaアカウントのアドレスも検証されます。

その後、プログラムはベクターのサイズが現在の最大インデックスと同じであることを確認し、各アカウントのアドレスの正しさをチェックします。

プログラムの最後では、各アカウントを反復処理し、solana-programのmsg!マクロを使用して、デシリアライズされた記事のコンテンツを1つずつ出力します。
トランザクションテスト
コントラクトをテストするために、リポジトリのclient/main.tsにあるTypeScriptスクリプトを使用します。

スクリプトの先頭では、必要なすべてのライブラリをインポートし、3つのグローバル定数を定義します。KEYPAIR_PATHはsolana-keygen newを使用して生成された秘密鍵ファイルのパスを示し、PROGRAM_IDは前のセクションで記述したデプロイ済みプログラムのアドレスであり、POST_ARTICLE_SCHEMAは記事のシリアライズに使用されるオブジェクトです。


スクリプトのメインボディでは、まずloadKeyFromFile関数を呼び出して秘密鍵ファイルを解析し、トランザクション手数料の支払い者としてKeypairオブジェクトを取得します。次に、@solana/web3.jsが提供するfindProgramAddressSyncメソッドを使用してindexPdaのアドレスを計算します。このメソッドはRustコントラクトと同じアルゴリズムを使用しており、同じパラメータで同じアドレスを計算します。connectionオブジェクトは、テストにdevnetを使用することを示しています。

次に、最初の初期化トランザクションを送信します。単一の命令を含むトランザクションを作成します。この命令のdataフィールドにはバイト「0」のみが含まれており、これはinit命令への呼び出しであることを示しています。次に、トランザクションはsendAndConfirmTransactionを使用して送信および確認されます。

2番目のトランザクションは、post命令を呼び出して記事を投稿します。56行目から63行目にかけて、スクリプトは2つの記事をシリアライズし、対応するPDAアドレスを計算します。次に、2つのpost命令が1つのトランザクションに追加され、これらの記事が作成されます。

最後のトランザクションはlist命令を呼び出し、トランザクションが確認された後、ログが出力されます。
スクリプトを使用してプログラムをテストするには、KEYPAIR_PATHを秘密鍵のパスに置き換えます(デフォルトパスでない場合)。次に、次のコマンドを実行してRustスマートコントラクトをコンパイルおよびデプロイします。
cd program
cargo build-bpf
solana program deploy ./target/deploy/hello_solana.so
次に、デプロイされたアドレスをコピーし、PROGRAM_IDフィールドに貼り付けます。プロジェクトのルートディレクトリでnpm installを実行して依存関係をインストールし、次にnpm startを実行してスクリプトを実行します。
その後、Solscanを使用してテストトランザクションの実行詳細を確認できます。

記事発行のための2番目のトランザクションを直接見てみましょう。これには2つの命令が含まれており、各命令のInstruction Data Rawの最初のバイトは0x01です。さらに、各命令には、System ProgramのCreateAccount命令の呼び出しである内部命令が含まれています。

同様に、すべてのアカウントをリストするための3番目のトランザクションを検査できます。Program Logsセクションでは、2つの記事のインデックス、タイトル、コンテンツが出力されていることがわかります。
結論
この記事では、まずSolanaプログラミングおよびランタイム環境をローカルでセットアップする方法を説明しました。次に、Solanaコントラクトの実装ロジックを詳細に説明しました。最後に、TypeScriptスクリプトを使用してコントラクトの機能をテストしました。このステップバイステップのチュートリアルにより、簡単なSolanaスマートコントラクトの作成方法を学べたことと思います🥳。
次の記事では、Phalcon Explorer(Phalcon Explorerは現在Solanaをサポートしています)を使用してSolanaトランザクションを表示および分析する方法について詳細なガイドを提供します。お楽しみに!



