導入
プログラムはそれ自体には状態を持てません(レッスン14で見たとおり、コードとデータは別アカウント)。「カウンタの値」のような状態は、必ず別のデータアカウントの data に読み書きします。ここでは、そのカウンタを実際に+1して保存するまでの一連の流れを見ます。
説明
データを保存するアカウントの中身も、あらかじめ構造体として定義しておき、borsh でシリアライズ・デシリアライズします。
use borsh::{BorshDeserialize, BorshSerialize};
// データアカウントのdataに保存する中身
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u32,
}
「読み込む → 値を変える → 書き戻す」の一連の流れはこうなります。
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
msg,
};
use borsh::{BorshDeserialize, BorshSerialize};
fn increment_counter(accounts: &[AccountInfo], amount: u32) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
// 1. アカウントのdataからCounterAccountを復元する
let mut counter = CounterAccount::try_from_slice(&counter_account.data.borrow())?;
// 2. Rustの値として普通に更新する
counter.count += amount;
msg!("カウンタを{}に更新します", counter.count);
// 3. 更新した値をアカウントのdataに書き戻す(シリアライズ)
counter.serialize(&mut &mut counter_account.data.borrow_mut()[..])?;
Ok(())
}
sequenceDiagram
participant Client as クライアント(TypeScript)
participant Prog as プログラム(Rust)
participant Acc as データアカウント(オンチェーン)
Client->>Prog: Increment { amount: 1 } を送信
Prog->>Acc: dataを読み込む
Acc-->>Prog: count: 5
Prog->>Prog: count += amount → 6
Prog->>Acc: dataへ書き戻す(count: 6)
Prog-->>Client: Ok(()) (トランザクション成功)
ここで大事な前提が2つあります。
- 保存先のアカウントは、あらかじめ必要なバイト数ぶんの領域が確保されていなければならない(後述の System Program でアカウント自体を先に作る必要があります。第6章で扱います)。
- アカウントを維持するには一定額以上の SOL 残高が必要です。これを レント免除(rent-exempt) な残高と呼びます。データサイズに応じた最低残高を下回らないようにアカウントを作成することで、削除されずにデータを保持し続けられます。
演習
「カウンタの値そのもの」と「カウンタを操作するロジック(プログラムのコード)」が、それぞれ別々のアカウントに分かれていることのメリットを、レッスン14の内容も思い出しながら1つ挙げてみましょう。
ヒント1を見る
同じプログラムを使う人が100人いたら、カウンタのデータアカウントはいくつ必要でしょうか。
ヒント2を見る
プログラムのコード(.so)はアップグレードしない限り変わりませんが、データはユーザーごとに毎回変わります。