最初のSolanaプログラムを構築する

このクイックスタートでは、anchor initによって生成されたAnchor フレームワークのスタータープロジェクトを使用します。プロジェクトをローカルに作成し、テストを実行してプログラムをビルドし、Anchorが生成するプログラムコードを詳しく見ていきます。

前提条件

開始する前に、Solana開発ツールをインストールしてください。インストールにはRust、Solana CLI、およびAnchor CLIが含まれます。

このテンプレートにはAnchor CLIバージョン1.1.2以降を使用してください。インストール済みのバージョンを確認するには:

Terminal
$
anchor --version

プロジェクトの作成

ターミナルで以下のコマンドを実行してください:

Terminal
$
anchor init my-program
$
cd my-program

スタータープロジェクトには、programs/my-program配下にSolanaプログラムが1つ含まれています。このプログラムにはinstructionsが2つあります:カウンターアカウントを初期化するものと、カウンターをインクリメントするものです。

テンプレートの一部には、一般的なSolanaプログラムのパターンが示されています:PDAアカウントアドレスの導出、SOLを転送するためのCross Program Invocation(CPI)の実行、そして条件が満たされない場合にinstructionsを停止するカスタムエラーチェックの使用です。

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

プログラムのビルド

anchor buildを実行してスタータープログラムをコンパイルします:

Terminal
$
anchor build

コンパイルされたプログラムはtarget/deploy/my_program.soに書き込まれます。プログラムがデプロイされると、この.soファイルの内容がオンチェーンのアカウントに保存されます。

テストの実行

デフォルトのテストを実行します:

Terminal
$
anchor test

このテンプレートのAnchor.tomlはRustのテストコマンドを使用します:

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

このテストはコンパイル済みのプログラムをLiteSVMに読み込み、ペイヤーを作成し、initializeincrement のinstructionsを送信し、その後カウンターアカウントの状態を確認します。

anchor testを実行するとプログラムもコンパイルされるため、ローカルでテストする際に anchor buildを先に実行する必要はありません。

プログラムのデプロイ

ローカルテストは最も素早いフィードバックループです。devnetなどのネットワークへのデプロイの準備ができたら、まずビルドし、その後クラスターへデプロイします。

SolanaプログラムのデプロイにはSOLが必要です。プログラムはアカウントに保存され、そのアカウントは使用するスペースの料金を支払う必要があるためです。devnetでは、Solana FaucetまたはSolana CLIを使って無料のdevnet SOLをリクエストできます:

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

ソースファイル

srcディレクトリにはSolanaプログラムが含まれています。Anchorの Program Structure ドキュメントでは、declare_id!#[program]#[derive(Accounts)]、*rs#[account]*などここで使用されているコアマクロについて説明しています。このセクションではテンプレートファイルを順に解説します。

lib.rs

lib.rsはプログラムのエントリーポイントです。ソースファイルを接続し、プログラムアドレスを定義し、ユーザーが呼び出せるプログラムのinstructionsを定義します。

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

同じプログラムアドレスが設定とコードの両方に現れます。Anchor.tomlは、クラスターに対してデプロイまたは呼び出すアドレスをAnchorに伝えます。*rsdeclare_id!*は、セキュリティチェックのためにプログラム内のプログラムアドレスを定義します。

constants.rs

constants.rsは、共有する値を一か所にまとめます。このテンプレートでは、*rsCOUNTER_SEED*がカウンターPDAを導出し、*rsHELLO_WORLD_LAMPORTS*が初期化時に転送され、*rsMAX_COUNT*がインクリメント前にチェックされます。

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

initialize.rs は2つの定数を使用します:

  • COUNTER_SEED はカウンターPDAアドレスを導出します。
  • HELLO_WORLD_LAMPORTS は支払者からカウンターアカウントへ転送される金額を設定します。
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

increment.rsMAX_COUNT をカウンターの上限として使用します。現在のカウントがすでに最大値に達している場合、require!CounterOverflow を返し、アカウントデータは変更されません。

state.rs

state.rs は、プログラムが作成・所有するアカウントのカスタムデータ型を定義します。プログラムはそのデータを作成・初期化・更新するためのinstructionsを定義しますが、カウンターデータはプログラム自体の内部には保存されません。独自のアドレスを持つ別のアカウントに保存されます。

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rsCounter アカウントデータを定義します。instructionsファイルは、アカウントの作成と更新の際にその型を使用します:

  • Counter::INIT_SPACEstate.rs で定義されたフィールドに合わせてアカウントのサイズを設定します。
  • countauthority は、アカウントの初期化時に書き込まれるフィールド値です。
  • count += 1 は、バリデーションが通過した後にカウンターの保存値を更新します。

error.rs

error.rs はプログラムのカスタムエラーを定義します。このテンプレートでは、呼び出し元がカウンターを更新する権限を持たない場合、またはカウンターがすでに MAX_COUNT に達している場合に、instructionハンドラーが停止する仕組みをエラーで示しています。

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs は、instructionが返す可能性のあるエラーを定義します:

  • ErrorCode::Unauthorized は、署名者がカウンターアカウントに保存されている権限者でない場合に返されます。
  • ErrorCode::CounterOverflow は、カウンターがすでに MAX_COUNT に達している場合に返されます。

instructions.rs

instructions.rs は、instructionsファイルをプログラムクレートに接続し、 lib.rsinitialize および increment のinstructionsコードにアクセスできるようにします。各instructionsファイルは、そのinstructionsが必要とするアカウントと、Anchor がそれらのアカウントを検証した後に実行されるハンドラーロジックを定義します。

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs は、カウンターアカウントを作成するために必要なアカウントを定義し、アカウントの初期値を書き込みます。#[derive(Accounts)] 構造体は、Anchor の アカウント制約 を使用して、必要なアカウントと新しいカウンターアカウントの作成方法を指定します。

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

アカウントコンテキスト

Initialize 構造体は、ユーザーが initialize instructionsを呼び出す際に含めなければならないアカウントを定義します。Anchor はハンドラーが実行される前にこれらのアカウントを検証します。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

ペイヤーアカウント

payer アカウントは、カウンターアカウントの作成費用を支払います。 Signer<'info> 型はペイヤーがトランザクションに署名する必要があることを意味し、 #[account(mut)] は lamport が差し引かれるため、ペイヤーアカウントが変更可能であることを意味します。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

カウンターアカウント

counter アカウントは、state.rs からの Counter データを格納します。 init はハンドラーが実行される前にこのアカウントを作成するよう Anchor に指示し、 payer = payer は作成費用を支払うアカウントを Anchor に伝えます。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

アカウントサイズ

space 制約は、割り当てるアカウントデータの量をAnchorに伝えます。Anchorはまず8バイトのディスクリミネーターを格納し、次に Counter フィールドに必要なバイト数を格納します。ディスクリミネーターにより、Anchorはアカウントデータをデシリアライズする前に、このアカウントを Counter アカウントとして識別できます。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

カウンターアドレス

seedsbump 制約は、カウンターアカウントに期待されるPDAアドレスを定義します。Anchorは、提供された counter アカウントがそのアドレスと一致することを検証します。このテンプレートはPDAを使用しているため、ユーザーはプログラムIDとseedからカウンターアドレスを導出でき、カウンターアドレスが決定論的になります。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

新しいアカウントの作成にはSystem Programを使用するため、system_program アカウントが必要です。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

ハンドラー関数

handle_initialize 関数は、Anchorが Initialize 内のアカウントを検証した後に実行されます。ctx の値により、ハンドラーはそれらの検証済みアカウントにアクセスできます。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

初期データ

ハンドラーは新しいカウンターアカウントに初期値を書き込みます。カウントは 0 から始まり、支払者は後でカウンターをインクリメントする権限を持つauthorityになります。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

転送アカウント

この転送CPIは、CPIが別のプログラムにアカウントを渡す方法を示すためだけに含まれています。Transfer 構造体は、System Programの転送で使用されるアカウントを一覧表示します。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

CPIコンテキスト

CpiContext::new は、呼び出されるプログラムと、そのプログラムに渡されるアカウントを組み合わせます。これがCPIの基本的な形です。呼び出すプログラムを選択し、そのプログラムが必要とするアカウントを収集し、両方を呼び出しに渡します。ここで呼び出されるプログラムはSystem Programです。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

転送の呼び出し

anchor_lang::system_program::transfer はSystem Programの転送instructionsを呼び出します。このテンプレートでは、転送は自分のプログラムから別のプログラムを呼び出す簡単な例です。転送CPIが失敗すると、initialize instructionsも失敗します。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

メッセージのログ記録

msg! はプログラムログにメッセージを書き込みます。Ok(()) はinstructionsが正常に返されたことを示します。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

アカウントコンテキスト

Initialize 構造体は、ユーザーが initialize instructionsを呼び出す際に含めなければならないアカウントを定義します。Anchor はハンドラーが実行される前にこれらのアカウントを検証します。

ペイヤーアカウント

payer アカウントは、カウンターアカウントの作成費用を支払います。 Signer<'info> 型はペイヤーがトランザクションに署名する必要があることを意味し、 #[account(mut)] は lamport が差し引かれるため、ペイヤーアカウントが変更可能であることを意味します。

カウンターアカウント

counter アカウントは、state.rs からの Counter データを格納します。 init はハンドラーが実行される前にこのアカウントを作成するよう Anchor に指示し、 payer = payer は作成費用を支払うアカウントを Anchor に伝えます。

アカウントサイズ

space 制約は、割り当てるアカウントデータの量をAnchorに伝えます。Anchorはまず8バイトのディスクリミネーターを格納し、次に Counter フィールドに必要なバイト数を格納します。ディスクリミネーターにより、Anchorはアカウントデータをデシリアライズする前に、このアカウントを Counter アカウントとして識別できます。

カウンターアドレス

seedsbump 制約は、カウンターアカウントに期待されるPDAアドレスを定義します。Anchorは、提供された counter アカウントがそのアドレスと一致することを検証します。このテンプレートはPDAを使用しているため、ユーザーはプログラムIDとseedからカウンターアドレスを導出でき、カウンターアドレスが決定論的になります。

System Program

新しいアカウントの作成にはSystem Programを使用するため、system_program アカウントが必要です。

ハンドラー関数

handle_initialize 関数は、Anchorが Initialize 内のアカウントを検証した後に実行されます。ctx の値により、ハンドラーはそれらの検証済みアカウントにアクセスできます。

初期データ

ハンドラーは新しいカウンターアカウントに初期値を書き込みます。カウントは 0 から始まり、支払者は後でカウンターをインクリメントする権限を持つauthorityになります。

転送アカウント

この転送CPIは、CPIが別のプログラムにアカウントを渡す方法を示すためだけに含まれています。Transfer 構造体は、System Programの転送で使用されるアカウントを一覧表示します。

CPIコンテキスト

CpiContext::new は、呼び出されるプログラムと、そのプログラムに渡されるアカウントを組み合わせます。これがCPIの基本的な形です。呼び出すプログラムを選択し、そのプログラムが必要とするアカウントを収集し、両方を呼び出しに渡します。ここで呼び出されるプログラムはSystem Programです。

転送の呼び出し

anchor_lang::system_program::transfer はSystem Programの転送instructionsを呼び出します。このテンプレートでは、転送は自分のプログラムから別のプログラムを呼び出す簡単な例です。転送CPIが失敗すると、initialize instructionsも失敗します。

メッセージのログ記録

msg! はプログラムログにメッセージを書き込みます。Ok(()) はinstructionsが正常に返されたことを示します。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs は、既存のcounterアカウントを更新するために必要なアカウントを定義します。ハンドラーは、署名者が保存されたauthorityと一致するかを確認し、countが指定された*rsMAX_COUNT*に達していないかをチェックしたうえで、countをインクリメントします。

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

アカウントコンテキスト

Increment 構造体は、ユーザーが increment instructions を呼び出す際に含める必要があるアカウントを定義します。Anchor はハンドラーが実行される前にこれらのアカウントを検証します。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Counterアカウント

counter アカウントは Counter データを保存します。mut 制約によりハンドラーは保存されたcountを更新でき、seeds および bump 制約はcounterのPDAアドレスを検証します。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Authorityの署名者

authority アカウントはトランザクションに署名する必要があります。ハンドラーは後に、この署名者がcounterアカウントに保存されたauthorityと一致するかを確認します。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

ハンドラー関数

handle_increment 関数は、Anchor が Increment 内のアカウントを検証した後に実行されます。ctx の値により、ハンドラーはそれらの検証済みアカウントにアクセスできます。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

権限チェック

最初のチェックでは、署名者がこのカウンターを更新する権限を持っているかを確認します。署名者のアドレスが counter.authority と一致しない場合、instructions は ErrorCode::Unauthorized で停止します。これはアプリケーションレベルの認可を示しています。プログラムはカウンターデータを所有しますが、どの署名者がそのデータを変更できるかのルールを実装しています。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

最大カウントチェック

2 番目のチェックでは、カウンターが MAX_COUNT を超えないようにします。カウンターがすでに上限に達している場合、instructions は ErrorCode::CounterOverflow で停止します。この上限はテンプレート内の人為的なルールであり、カスタムエラーがアカウントデータが変更される前に instructions を停止する仕組みを確認できます。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

カウントの更新

両方のチェックが通過した後にのみ、ハンドラーはアカウントデータを更新します。この行は保存されたカウンター値に 1 を加算します。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

ログメッセージ

msg! は更新されたカウントをプログラムログに書き込みます。Ok(()) は instructions が正常に返されたことを示します。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

アカウントコンテキスト

Increment 構造体は、ユーザーが increment instructions を呼び出す際に含める必要があるアカウントを定義します。Anchor はハンドラーが実行される前にこれらのアカウントを検証します。

Counterアカウント

counter アカウントは Counter データを保存します。mut 制約によりハンドラーは保存されたcountを更新でき、seeds および bump 制約はcounterのPDAアドレスを検証します。

Authorityの署名者

authority アカウントはトランザクションに署名する必要があります。ハンドラーは後に、この署名者がcounterアカウントに保存されたauthorityと一致するかを確認します。

ハンドラー関数

handle_increment 関数は、Anchor が Increment 内のアカウントを検証した後に実行されます。ctx の値により、ハンドラーはそれらの検証済みアカウントにアクセスできます。

権限チェック

最初のチェックでは、署名者がこのカウンターを更新する権限を持っているかを確認します。署名者のアドレスが counter.authority と一致しない場合、instructions は ErrorCode::Unauthorized で停止します。これはアプリケーションレベルの認可を示しています。プログラムはカウンターデータを所有しますが、どの署名者がそのデータを変更できるかのルールを実装しています。

最大カウントチェック

2 番目のチェックでは、カウンターが MAX_COUNT を超えないようにします。カウンターがすでに上限に達している場合、instructions は ErrorCode::CounterOverflow で停止します。この上限はテンプレート内の人為的なルールであり、カスタムエラーがアカウントデータが変更される前に instructions を停止する仕組みを確認できます。

カウントの更新

両方のチェックが通過した後にのみ、ハンドラーはアカウントデータを更新します。この行は保存されたカウンター値に 1 を加算します。

ログメッセージ

msg! は更新されたカウントをプログラムログに書き込みます。Ok(()) は instructions が正常に返されたことを示します。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

テストファイル

programs/my-program/tests/test_initialize.rs は Rust の統合テストです。ローカル validator を起動しません。代わりに、コンパイルされた .so ファイルを LiteSVM に読み込み、プログラムを呼び出すトランザクションを構築し、各トランザクション後にカウンターアカウントを読み取ります。このテストは、呼び出すプログラムのプログラム ID を指定し、instruction data を提供し、必要なアカウントを渡すことで、Solana トランザクション用の instructions を構築します。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

Initialize アカウントコンテキストは、initialize instructionに必要なアカウントを定義します。テストはそれらと同じアカウントを生成された my_program::accounts::Initialize ヘルパーに渡します:

  • payerpayer: payer.pubkey() として渡されます。
  • countercounter として渡されます。
  • system_programsystem_program::ID として渡されます。

my_program::instruction::Initialize {}.data() は instruction data を作成します。ここでinstructionの引数がエンコードされますが、この initialize instructionは引数を必要としません。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

Increment アカウントコンテキストは、increment instructionに必要なアカウントを定義します。テストはそれらと同じアカウントを生成された my_program::accounts::Increment ヘルパーに渡します:

  • countercounter として渡されます。
  • authorityauthority: payer.pubkey() として渡されます。

my_program::instruction::Increment {}.data() は instruction data を作成します。ここでinstructionの引数がエンコードされますが、この increment instructionは引数を必要としません。

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

プロジェクトの設定

ルートプロジェクトファイルは、AnchorとCargoにプログラムのビルド、テスト、デプロイの方法を指示します。完全なリファレンスについては、Anchor.toml の設定および Anchor CLI に関する Anchor ドキュメントをご参照ください。

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?

目次

ページを編集
© 2026 Solana Foundation. 無断転載を禁じます。