Xây Dựng Chương Trình Solana Đầu Tiên

Quickstart này sử dụng dự án khởi đầu của Anchor framework được tạo bởi anchor init. Bạn sẽ tạo dự án trên máy cục bộ, chạy các bài kiểm tra, build chương trình và xem qua mã nguồn chương trình mà Anchor tạo ra.

Điều Kiện Tiên Quyết

Trước khi bắt đầu, hãy cài đặt các công cụ phát triển Solana. Bộ cài đặt bao gồm Rust, Solana CLI và Anchor CLI.

Sử dụng Anchor CLI phiên bản 1.1.2 trở lên cho template này. Kiểm tra phiên bản đã cài đặt của bạn:

Terminal
$
anchor --version

Tạo Dự Án

Chạy các lệnh sau trong terminal của bạn:

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

Dự án khởi đầu bao gồm một chương trình Solana nằm trong programs/my-program. Chương trình này có hai instruction: một để khởi tạo tài khoản counter và một để tăng giá trị counter.

Một số phần của template minh họa các mẫu chương trình Solana phổ biến: dẫn xuất địa chỉ tài khoản PDA, thực hiện Cross Program Invocation (CPI) để chuyển SOL, và sử dụng kiểm tra lỗi tùy chỉnh để dừng một instruction khi một điều kiện không được thỏa mãn.

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

Build Chương Trình

Chạy anchor build để biên dịch chương trình khởi đầu:

Terminal
$
anchor build

Chương trình đã biên dịch được ghi vào target/deploy/my_program.so. Khi chương trình được triển khai, nội dung của tệp .so này sẽ được lưu trữ trong một tài khoản trên chuỗi.

Chạy Kiểm Thử

Chạy kiểm thử mặc định:

Terminal
$
anchor test

Anchor.toml của template này sử dụng lệnh kiểm thử Rust:

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

Kiểm thử tải chương trình đã biên dịch vào LiteSVM, tạo một payer, gửi các lệnh initializeincrement, sau đó kiểm tra trạng thái của tài khoản counter.

Chạy anchor test cũng sẽ biên dịch chương trình, vì vậy bạn không cần chạy anchor build trước khi kiểm thử cục bộ.

Triển Khai Chương Trình

Kiểm thử cục bộ là vòng phản hồi nhanh nhất. Khi bạn sẵn sàng triển khai lên một mạng, ví dụ như devnet, hãy build trước, sau đó triển khai lên một cluster.

Việc triển khai một chương trình Solana yêu cầu SOL vì chương trình được lưu trữ trong một account, và account đó phải trả phí cho không gian lưu trữ. Trên devnet, hãy yêu cầu devnet SOL miễn phí từ Solana Faucet hoặc qua Solana CLI:

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

Các tệp nguồn

Thư mục src chứa chương trình Solana. Tài liệu Cấu Trúc Chương Trình của Anchor giải thích các macro cốt lõi được sử dụng ở đây, bao gồm declare_id!, #[program], #[derive(Accounts)], và #[account]. Phần này hướng dẫn qua các tệp template.

lib.rs

lib.rs là điểm vào của chương trình. Nó kết nối các tệp nguồn, định nghĩa địa chỉ chương trình, và định nghĩa các lệnh của chương trình mà người dùng có thể gọi.

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");

Địa chỉ chương trình giống nhau xuất hiện trong cả cấu hình và mã nguồn. Anchor.toml cho Anchor biết địa chỉ cần triển khai hoặc gọi cho một cluster. declare_id! định nghĩa địa chỉ chương trình trong chương trình để kiểm tra bảo mật.

constants.rs

constants.rs lưu các giá trị dùng chung ở một nơi. Trong template này, COUNTER_SEED dẫn xuất PDA của bộ đếm, HELLO_WORLD_LAMPORTS được chuyển trong quá trình khởi tạo, và MAX_COUNT được kiểm tra trước khi tăng giá trị.

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 sử dụng hai hằng số:

  • COUNTER_SEED lấy địa chỉ PDA của bộ đếm.
  • HELLO_WORLD_LAMPORTS đặt số lượng được chuyển từ người thanh toán sang tài khoản bộ đếm.
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.rs sử dụng MAX_COUNT làm giới hạn trên của bộ đếm. Nếu giá trị hiện tại đã đạt tối đa, require! trả về CounterOverflow và dữ liệu tài khoản không bị thay đổi.

state.rs

state.rs định nghĩa các kiểu dữ liệu tùy chỉnh cho các tài khoản mà chương trình tạo ra và sở hữu. Chương trình định nghĩa các lệnh để tạo, khởi tạo và cập nhật dữ liệu đó, nhưng dữ liệu bộ đếm không được lưu trữ bên trong chính chương trình. Nó được lưu trữ trong một tài khoản riêng biệt với địa chỉ của chính nó.

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.rs định nghĩa dữ liệu tài khoản Counter. Các tệp lệnh sử dụng kiểu đó khi tạo và cập nhật tài khoản:

  • Counter::INIT_SPACE xác định kích thước tài khoản cho các trường được định nghĩa trong state.rs.
  • countauthority là các giá trị trường được ghi khi tài khoản được khởi tạo.
  • count += 1 cập nhật giá trị bộ đếm đã lưu sau khi xác thực thành công.

error.rs

error.rs định nghĩa các lỗi tùy chỉnh của chương trình. Trong mẫu này, các lỗi minh họa cách các trình xử lý lệnh dừng lại khi người gọi không được phép cập nhật bộ đếm hoặc bộ đếm đã đạt đến MAX_COUNT.

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 liệt kê các lỗi mà lệnh có thể trả về:

  • ErrorCode::Unauthorized được trả về khi người ký không phải là authority được lưu trong tài khoản bộ đếm.
  • ErrorCode::CounterOverflow được trả về khi bộ đếm đã đạt đến MAX_COUNT.

instructions.rs

instructions.rs kết nối các tệp lệnh với crate chương trình để lib.rs có thể truy cập mã lệnh initializeincrement. Mỗi tệp lệnh định nghĩa các tài khoản mà lệnh đó yêu cầu và logic xử lý chạy sau khi Anchor xác thực các tài khoản đó.

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

initialize.rs

initialize.rs định nghĩa các tài khoản cần thiết để tạo tài khoản counter, sau đó ghi các giá trị đầu tiên của tài khoản. Struct #[derive(Accounts)] sử dụng các ràng buộc tài khoản của Anchor để chỉ định các tài khoản nào là bắt buộc và cách tạo tài khoản counter mới.

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(())
}

Ngữ cảnh tài khoản

Struct Initialize định nghĩa các tài khoản phải được đưa vào khi người dùng gọi lệnh initialize. Anchor kiểm tra các tài khoản này trước khi trình xử lý chạy.

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>,
}

Tài khoản người thanh toán

Tài khoản payer trả phí để tạo tài khoản counter. Kiểu Signer<'info> có nghĩa là người thanh toán phải ký giao dịch, và #[account(mut)] có nghĩa là tài khoản người thanh toán có thể bị thay đổi vì lamport sẽ bị trừ đi.

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>,
}

Tài khoản Counter

Tài khoản counter lưu trữ dữ liệu Counter từ state.rs. init yêu cầu Anchor tạo tài khoản này trước khi trình xử lý chạy, và payer = payer cho Anchor biết tài khoản nào trả phí tạo.

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>,
}

Kích Thước Tài Khoản

Ràng buộc space cho Anchor biết cần cấp phát bao nhiêu dữ liệu cho tài khoản. Anchor lưu trữ một bộ phân biệt 8 byte trước, sau đó là các byte cần thiết cho các trường Counter. Bộ phân biệt cho phép Anchor nhận dạng tài khoản này là tài khoản Counter trước khi giải mã hóa dữ liệu tài khoản.

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>,
}

Địa Chỉ Bộ Đếm

Các ràng buộc seedsbump xác định địa chỉ PDA dự kiến cho tài khoản bộ đếm. Anchor xác minh rằng tài khoản counter được cung cấp khớp với địa chỉ đó. Mẫu sử dụng PDA để người dùng có thể suy ra địa chỉ bộ đếm từ ID chương trình và seed, giúp địa chỉ bộ đếm trở nên xác định.

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

Tài khoản system_program là bắt buộc vì việc tạo tài khoản mới sử dụng 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>,
}

Hàm Xử Lý

Hàm handle_initialize chạy sau khi Anchor xác thực các tài khoản trong Initialize. Giá trị ctx cho phép hàm xử lý truy cập vào các tài khoản đã được kiểm tra đó.

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(())
}

Dữ Liệu Ban Đầu

Hàm xử lý ghi các giá trị đầu tiên vào tài khoản bộ đếm mới. Số đếm bắt đầu tại 0, và người thanh toán trở thành quyền hạn được phép tăng bộ đếm sau này.

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(())
}

Tài Khoản Chuyển Khoản

CPI chuyển khoản này được đưa vào chỉ để minh họa cách một CPI truyền tài khoản đến chương trình khác. Cấu trúc Transfer liệt kê các tài khoản được sử dụng bởi lệnh chuyển khoản 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(())
}

Ngữ Cảnh CPI

CpiContext::new kết hợp chương trình được gọi với các tài khoản được truyền vào chương trình đó. Đây là cấu trúc cơ bản của một CPI: chọn chương trình cần gọi, thu thập các tài khoản mà chương trình đó yêu cầu, sau đó truyền cả hai vào lời gọi. Ở đây, chương trình được gọi là 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(())
}

Gọi Transfer

anchor_lang::system_program::transfer gọi lệnh transfer của System Program. Trong mẫu này, transfer là một ví dụ đơn giản về việc gọi một chương trình khác từ chương trình của bạn. Nếu CPI transfer thất bại, lệnh initialize cũng sẽ thất bại theo.

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(())
}

Ghi Log

msg! ghi một thông báo vào log của chương trình. Ok(()) cho biết lệnh đã trả về thành công.

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(())
}

Ngữ cảnh tài khoản

Struct Initialize định nghĩa các tài khoản phải được đưa vào khi người dùng gọi lệnh initialize. Anchor kiểm tra các tài khoản này trước khi trình xử lý chạy.

Tài khoản người thanh toán

Tài khoản payer trả phí để tạo tài khoản counter. Kiểu Signer<'info> có nghĩa là người thanh toán phải ký giao dịch, và #[account(mut)] có nghĩa là tài khoản người thanh toán có thể bị thay đổi vì lamport sẽ bị trừ đi.

Tài khoản Counter

Tài khoản counter lưu trữ dữ liệu Counter từ state.rs. init yêu cầu Anchor tạo tài khoản này trước khi trình xử lý chạy, và payer = payer cho Anchor biết tài khoản nào trả phí tạo.

Kích Thước Tài Khoản

Ràng buộc space cho Anchor biết cần cấp phát bao nhiêu dữ liệu cho tài khoản. Anchor lưu trữ một bộ phân biệt 8 byte trước, sau đó là các byte cần thiết cho các trường Counter. Bộ phân biệt cho phép Anchor nhận dạng tài khoản này là tài khoản Counter trước khi giải mã hóa dữ liệu tài khoản.

Địa Chỉ Bộ Đếm

Các ràng buộc seedsbump xác định địa chỉ PDA dự kiến cho tài khoản bộ đếm. Anchor xác minh rằng tài khoản counter được cung cấp khớp với địa chỉ đó. Mẫu sử dụng PDA để người dùng có thể suy ra địa chỉ bộ đếm từ ID chương trình và seed, giúp địa chỉ bộ đếm trở nên xác định.

System Program

Tài khoản system_program là bắt buộc vì việc tạo tài khoản mới sử dụng System Program.

Hàm Xử Lý

Hàm handle_initialize chạy sau khi Anchor xác thực các tài khoản trong Initialize. Giá trị ctx cho phép hàm xử lý truy cập vào các tài khoản đã được kiểm tra đó.

Dữ Liệu Ban Đầu

Hàm xử lý ghi các giá trị đầu tiên vào tài khoản bộ đếm mới. Số đếm bắt đầu tại 0, và người thanh toán trở thành quyền hạn được phép tăng bộ đếm sau này.

Tài Khoản Chuyển Khoản

CPI chuyển khoản này được đưa vào chỉ để minh họa cách một CPI truyền tài khoản đến chương trình khác. Cấu trúc Transfer liệt kê các tài khoản được sử dụng bởi lệnh chuyển khoản System Program.

Ngữ Cảnh CPI

CpiContext::new kết hợp chương trình được gọi với các tài khoản được truyền vào chương trình đó. Đây là cấu trúc cơ bản của một CPI: chọn chương trình cần gọi, thu thập các tài khoản mà chương trình đó yêu cầu, sau đó truyền cả hai vào lời gọi. Ở đây, chương trình được gọi là System Program.

Gọi Transfer

anchor_lang::system_program::transfer gọi lệnh transfer của System Program. Trong mẫu này, transfer là một ví dụ đơn giản về việc gọi một chương trình khác từ chương trình của bạn. Nếu CPI transfer thất bại, lệnh initialize cũng sẽ thất bại theo.

Ghi Log

msg! ghi một thông báo vào log của chương trình. Ok(()) cho biết lệnh đã trả về thành công.

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 xác định các tài khoản cần thiết để cập nhật một tài khoản counter hiện có. Hàm xử lý kiểm tra rằng người ký là authority đã lưu trữ, kiểm tra rằng count chưa đạt đến MAX_COUNT được chỉ định, sau đó tăng count lên.

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(())
}

Account Context

Struct Increment xác định các tài khoản phải được đưa vào khi người dùng gọi lệnh increment. Anchor kiểm tra các tài khoản này trước khi hàm xử lý chạy.

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

Counter Account

Tài khoản counter lưu trữ dữ liệu Counter. Ràng buộc mut cho phép hàm xử lý cập nhật count đã lưu trữ, còn các ràng buộc seedsbump xác minh địa chỉ PDA của counter.

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

Authority Signer

Tài khoản authority phải ký giao dịch. Hàm xử lý sau đó kiểm tra rằng người ký này khớp với authority được lưu trong tài khoản counter.

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

Hàm Handler

Hàm handle_increment chạy sau khi Anchor xác thực các tài khoản trong Increment. Giá trị ctx cho phép handler truy cập vào các tài khoản đã được kiểm tra đó.

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(())
}

Kiểm Tra Quyền

Kiểm tra đầu tiên đảm bảo rằng người ký được phép cập nhật bộ đếm này. Nếu địa chỉ của người ký không khớp với counter.authority, lệnh sẽ dừng lại với ErrorCode::Unauthorized. Điều này minh họa cho việc phân quyền ở cấp độ ứng dụng: chương trình sở hữu dữ liệu bộ đếm, nhưng nó thực thi một quy tắc xác định người ký nào có thể thay đổi dữ liệu đó.

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(())
}

Kiểm Tra Giá Trị Tối Đa

Kiểm tra thứ hai ngăn bộ đếm vượt quá MAX_COUNT. Nếu bộ đếm đã đạt đến giới hạn, lệnh sẽ dừng lại với ErrorCode::CounterOverflow. Giới hạn này là một quy tắc nhân tạo trong template để bạn có thể thấy cách các lỗi tùy chỉnh dừng một lệnh trước khi dữ liệu tài khoản bị thay đổi.

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(())
}

Cập Nhật Bộ Đếm

Chỉ sau khi cả hai kiểm tra đều vượt qua, handler mới cập nhật dữ liệu tài khoản. Dòng này cộng thêm một vào giá trị bộ đếm đã lưu trữ.

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(())
}

Ghi Log

msg! ghi số đếm đã cập nhật vào nhật ký chương trình. Ok(()) cho biết lệnh đã trả về thành công.

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(())
}

Account Context

Struct Increment xác định các tài khoản phải được đưa vào khi người dùng gọi lệnh increment. Anchor kiểm tra các tài khoản này trước khi hàm xử lý chạy.

Counter Account

Tài khoản counter lưu trữ dữ liệu Counter. Ràng buộc mut cho phép hàm xử lý cập nhật count đã lưu trữ, còn các ràng buộc seedsbump xác minh địa chỉ PDA của counter.

Authority Signer

Tài khoản authority phải ký giao dịch. Hàm xử lý sau đó kiểm tra rằng người ký này khớp với authority được lưu trong tài khoản counter.

Hàm Handler

Hàm handle_increment chạy sau khi Anchor xác thực các tài khoản trong Increment. Giá trị ctx cho phép handler truy cập vào các tài khoản đã được kiểm tra đó.

Kiểm Tra Quyền

Kiểm tra đầu tiên đảm bảo rằng người ký được phép cập nhật bộ đếm này. Nếu địa chỉ của người ký không khớp với counter.authority, lệnh sẽ dừng lại với ErrorCode::Unauthorized. Điều này minh họa cho việc phân quyền ở cấp độ ứng dụng: chương trình sở hữu dữ liệu bộ đếm, nhưng nó thực thi một quy tắc xác định người ký nào có thể thay đổi dữ liệu đó.

Kiểm Tra Giá Trị Tối Đa

Kiểm tra thứ hai ngăn bộ đếm vượt quá MAX_COUNT. Nếu bộ đếm đã đạt đến giới hạn, lệnh sẽ dừng lại với ErrorCode::CounterOverflow. Giới hạn này là một quy tắc nhân tạo trong template để bạn có thể thấy cách các lỗi tùy chỉnh dừng một lệnh trước khi dữ liệu tài khoản bị thay đổi.

Cập Nhật Bộ Đếm

Chỉ sau khi cả hai kiểm tra đều vượt qua, handler mới cập nhật dữ liệu tài khoản. Dòng này cộng thêm một vào giá trị bộ đếm đã lưu trữ.

Ghi Log

msg! ghi số đếm đã cập nhật vào nhật ký chương trình. Ok(()) cho biết lệnh đã trả về thành công.

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

Tệp kiểm thử

programs/my-program/tests/test_initialize.rs là một bài kiểm thử tích hợp Rust. Nó không khởi động một validator cục bộ. Thay vào đó, nó tải tệp .so đã biên dịch vào LiteSVM, xây dựng các giao dịch gọi chương trình và đọc tài khoản counter sau mỗi giao dịch. Bài kiểm thử xây dựng các lệnh cho một giao dịch Solana bằng cách chỉ định ID chương trình cần gọi, cung cấp instruction data và truyền các tài khoản cần thiết.

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),
);

Ngữ cảnh tài khoản Initialize xác định các tài khoản cần thiết cho lệnh initialize. Bài kiểm tra truyền những tài khoản đó vào hàm trợ giúp my_program::accounts::Initialize được tạo tự động:

  • payer được truyền vào là payer: payer.pubkey().
  • counter được truyền vào là counter.
  • system_program được truyền vào là system_program::ID.

my_program::instruction::Initialize {}.data() tạo ra instruction data. Đây là nơi các đối số lệnh sẽ được mã hóa, nhưng lệnh initialize này không yêu cầu bất kỳ đối số nào.

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),
);

Ngữ cảnh tài khoản Increment xác định các tài khoản cần thiết cho lệnh increment. Bài kiểm tra truyền những tài khoản đó vào hàm trợ giúp my_program::accounts::Increment được tạo tự động:

  • counter được truyền vào là counter.
  • authority được truyền vào là authority: payer.pubkey().

my_program::instruction::Increment {}.data() tạo ra instruction data. Đây là nơi các đối số lệnh sẽ được mã hóa, nhưng lệnh increment này không yêu cầu bất kỳ đối số nào.

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());
}

Cấu hình dự án

Các tệp dự án gốc cho Anchor và Cargo biết cách build, test và triển khai chương trình. Để tham khảo đầy đủ, xem tài liệu Anchor cho cấu hình Anchor.tomlAnchor CLI.

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?

Mục lục

Chỉnh sửa trang