use borsh::{BorshDeserialize, BorshSerialize}; | |
use solana_program::{ | |
account_info::{next_account_info, AccountInfo}, | |
entrypoint, | |
entrypoint::ProgramResult, | |
msg, | |
program_error::ProgramError, | |
pubkey::Pubkey, | |
}; | |
/// Define the type of state stored in accounts | |
#[derive(BorshSerialize, BorshDeserialize, Debug)] | |
pub struct GreetingAccount { | |
/// number of greetings | |
pub counter: u32, | |
} | |
// Declare and export the program's entrypoint | |
entrypoint!(process_instruction); | |
// Program entrypoint's implementation | |
pub fn process_instruction( | |
program_id: &Pubkey, // Public key of the account the hello world program was loaded into | |
accounts: &[AccountInfo], // The account to say hello to | |
_instruction_data: &[u8], // Ignored, all helloworld instructions are hellos | |
) -> ProgramResult { | |
msg!("Hello World Rust program entrypoint"); | |
// Iterating accounts is safer than indexing | |
let accounts_iter = &mut accounts.iter(); | |
// Get the account to say hello to | |
let account = next_account_info(accounts_iter)?; | |
// The account must be owned by the program in order to modify its data | |
if account.owner != program_id { | |
msg!("Greeted account does not have the correct program id"); | |
return Err(ProgramError::IncorrectProgramId); | |
} | |
// Increment and store the number of times the account has been greeted | |
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?; | |
greeting_account.counter += 1; | |
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?; | |
msg!("Greeted {} time(s)!", greeting_account.counter); | |
Ok(()) | |
} | |
// Sanity tests | |
#[cfg(test)] | |
mod test { | |
use super::*; | |
use solana_program::clock::Epoch; | |
use std::mem; | |
#[test] | |
fn test_sanity() { | |
let program_id = Pubkey::default(); | |
let key = Pubkey::default(); | |
let mut lamports = 0; | |
let mut data = vec![0; mem::size_of::<u32>()]; | |
let owner = Pubkey::default(); | |
let account = AccountInfo::new( | |
&key, | |
false, | |
true, | |
&mut lamports, | |
&mut data, | |
&owner, | |
false, | |
Epoch::default(), | |
); | |
let instruction_data: Vec<u8> = Vec::new(); | |
let accounts = vec![account]; | |
assert_eq!( | |
GreetingAccount::try_from_slice(&accounts[0].data.borrow()) | |
.unwrap() | |
.counter, | |
0 | |
); | |
process_instruction(&program_id, &accounts, &instruction_data).unwrap(); | |
assert_eq!( | |
GreetingAccount::try_from_slice(&accounts[0].data.borrow()) | |
.unwrap() | |
.counter, | |
1 | |
); | |
process_instruction(&program_id, &accounts, &instruction_data).unwrap(); | |
assert_eq!( | |
GreetingAccount::try_from_slice(&accounts[0].data.borrow()) | |
.unwrap() | |
.counter, | |
2 | |
); | |
} | |
} |
/// Define the type of state stored in accounts
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
/// number of greetings
pub counter: u32,
}
次に、deriveマクロを使用してGreetingAccount構造体をラップするために必要な定型コードをすべて生成します。これはコンパイル時に#[derive()]マクロを使用することで裏側で行われます。Rustのマクロはかなり大きなトピックですが、理解するために努力する価値は十分にあります。とりあえず、これはコンパイル時に挿入される定型的なコードのショートカットであることだけは知っておいてください。
構造体宣言自体は簡単で、pubキーワードを使用して構造体をパブリックにアクセス可能であることを宣言しています。この構造体キーワードは、GreetingAccountという構造体を定義していることをコンパイラに知らせています。この構造体は、u32(符号なし32ビット整数)の型を持つcounterという単一のフィールドを持っています。つまり、カウンタは4,294,967,295より大きくてはいけないということです。
次に、エントリポイントであるprocess_instruction関数を宣言します。
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Hello World Rust program entrypoint");
let accounts_iter = &mut accounts.iter();
let account = next_account_info(accounts_iter)?;
- process_instruction エントリポイントの戻り値は ProgramResult になります。
Result は std クレートに由来し、エラーの可能性を表現するために使用されます。 - デバッグのために、ネットワークの計算コストが高くつくprintln!()を使わず、msg!()マクロでProgram Logにメッセージを出力することができます。
- Rustのletキーワードは変数の値を不変にします。イテレータを使ってアカウントをループすることで、accounts_iterはaccountsの各値のミュータブルリファレンスを取っています。そしてnext_account_info(account_iter)?は次のAccountInfoを返すかNotEnoughAccountKeysエラーを返します。最後の?に注目してください。これはRustのエラー伝播のためのショートカット式です。
// The account must be owned by the program in order to modify its data
if account.owner != program_id {
msg!("Greeted account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}
アカウントの所有者が許可されているかどうか、セキュリティチェックを行います。もし account.owner の公開鍵が program_id と等しくない場合は、エラーを返します。
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
msg!("Greeted {} time(s)!", greeting_account.counter);
Ok(())
最後に、既存のアカウントデータを「借りて」、カウンターの値を1つ増やし、ストレージに書き戻すという、いいとこ取りの処理をします。
- GreetingAccount構造体はcounterという1つのフィールドしか持っていません。これを変更できるようにするには、&borrow演算子でaccount.dataの参照を借用する必要があります。
- BorshDeserialize の try_from_slice() 関数は、 account.data を mutable で参照し、デシリアライズします。
- borrow()関数はRustコアライブラリに由来し、ラップされた値を不変に借用するために存在します。
つまり、アカウントデータを借りてきて、それをデシリアライズする関数に渡し、エラーが発生した場合はエラーを返すということです。?はエラー伝搬のためのものであることを思い出してください。
次に、カウンターの値を1つ増やすのは、加算代入演算子+=を使えば簡単です。
BorshSerializeのserialize()関数により、新しいカウンタ値が正しいフォーマットでSolanaに送り返されます。このメカニズムは、std::ioクレートのWrite traitによって実現されています。
そして、そのカウントが何回インクリメントされたかは、msg!を使って見ることができる。
Set up Solana CLI
Solana Programをデプロイする前に、Solanaクラスターを設定し、アカウントを作成し、エアドロップを要求し、すべてが正しく機能していることを確認する必要があります。
CLI の設定 URL を devnet クラスターに設定します。
solana config set --url https://api.devnet.solana.com
次に、CLIを使用して新しいキーペアを生成します。ターミナルで以下のコマンドを実行します。
mkdir solana-wallet
solana-keygen new --outfile solana-wallet/keypair.json
デプロイには、アカウントで利用できるSOLが必要ですので、AirDrop withを入手してください。
solana airdrop 1 $(solana-keygen pubkey solana-wallet/keypair.json)
すべての設定が完了し、アドレスに1SOLの資金が投入されたことを確認します。
solana config get
solana account $(solana-keygen pubkey solana-wallet/keypair.json)
Solana programデプロイする前にRustのインストールが必要
https://doc.rust-jp.rs/book-ja/ch01-01-installation.html
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
Filesize limit exceededのエラー出たらとりあえずこれで対応
ulimit -f unlimited
インストールしたらPATH通す
export PATH="$HOME/.cargo/bin:$PATH"
rustを最新版にアップデート
rustup update
rustcのバージョン確認
rustc --version
Deploy a Solana program
これからデプロイするプログラムは、あるアカウントが挨拶の指示を送った回数を記録しています。これは、Solana上でストレージがどのように機能するかを効果的に示すものです。
Building the program
yarn run solana:build:program
成功すると、helloworld.soというコンパイル済みのプログラムへのパスを指定してdeployコマンドを実行するように指示されます。このままでもいいのですが、この目的のためだけに生成したキーペアを指定したいので、読み進めてください。
To deploy this program:
$ solana program deploy /home/zu/project/figment/learn-web3-dapp/dist/solana/program/helloworld.so
Done in 1.39s.
Deploying the program
CLIのsolana deployを使って、プログラムをdevnetクラスタにデプロイする
solana deploy -v --keypair solana-wallet/keypair.json dist/solana/program/helloworld.so
vフラグはオプションですが、RPC URLやデフォルトの署名者鍵ペアのパス、予想されるコミットメントレベルなどの関連情報が表示されます。プロセスが完了すると、プログラムIDが表示されます。
成功した場合、CLIはデプロイされたコントラクトのprogramIdを表示します。
RPC URL: https://api.devnet.solana.com
Default Signer Path: solana-wallet/keypair.json
Commitment: confirmed
Program Id: 8LwWMLn37RKFLFz84HwaigLfpeoXCaXSFhcXooeDQBpW
Deploying the program to a test validator inside Gitpod
クラスタの変更
solana config set --url http://127.0.0.1:8899
テストバリデーターの実行
solana-test-validator
Gitpodで新しいターミナルを開きます (あるいはテストバリデータを実行しているターミナルを分割します)。この新しいターミナルで、Solana CLI の場所をコマンドで PATH に追加する必要があります。
export PATH="/home/gitpod/.local/share/solana/install/active_release/bin:$PATH"
/solana-wallet/keypair.json にあるキーペアに SOL の残高があることを確認し、SOL をいくらかエアドロップすることでデプロイの代金を支払います (テストバリデータ上なので、もっと高額な SOL を指定することも可能です)。
solana airdrop 100 $(solana-keygen pubkey solana-wallet/keypair.json)
これで、プログラムをテストバリデータにデプロイするコマンドを実行することができます。
solana deploy -v --keypair solana-wallet/keypair.json dist/solana/program/helloworld.so
Refs
% solana --version
solana-cli 1.10.5 (src:5eb085fc; feat:3235626988)
% rustc --version
rustc 1.60.0 (7737e0b5c 2022-04-04)
Rustでは、use宣言は他のコードへの便利なショートカットです。この場合、borshクレートのシリアライズ関数とデシリアライズ関数です。borshはBinary Object Representation Serializer for Hashingの略です。
Solana Programのクレートの一部を使用する。クレートとは、配布やコンパイルが可能なソースコードの集まりのこと(バイナリやライブラリ)です。