Workshop 5: Xây dựng chương trình đầu tiên với Anchor
Tham khảo thông tin từ bài viết này để làm quen với lập trình Smart Contract và quy trình xậy dựng project Smart Contract bằng Anchor. Đồng thời giúp bạn biết được cách kiểm thử ứng dụng và nắm được cách kết nối đến Program thông qua Anchor Program

Tham khảo thông tin từ bài viết này để làm quen với lập trình Smart Contract và quy trình xậy dựng project Smart Contract bằng Anchor. Đồng thời giúp bạn biết được cách kiểm thử ứng dụng và nắm được cách kết nối đến Program thông qua Anchor Program
Cấu trúc chương trình Anchor
Anchor sử dụng các macros và traits để tạo ra mã Rust cho bạn. Điều này cung cấp một cấu trúc chương trình rõ ràng, giúp bạn dễ bảo trì và phát triển. Các macros và attributes quan trọng sau:
- declare_id - là macro để khai báo địa chỉ chương trình
- #[program] - là attribute macro để biểu thị module chứa logic hướng dẫn thực thi chương trình.
- Accounts - là trait áp dụng cho cấu trúc đại diện danh sách accounts được yêu cầu cho một instruction
- #[account] - là attribute macro sử dụng xác định loại toài khoản tuỳ chỉnh cho chương trình
Định nghĩa instruction logic
#[program] attribute macro định nghĩa mudule chứng tất các instructions thuộc chương trình của bạn. Đây là nơi bạn triển khai logic cho mỗi instruction trong chương trình của bạn.
Mỗi instruction bắt buộc phải có tham số kiểContext, ngoài ra bạn có thể bổ sung các tham số khác nếu cần thiết.
#[program]
mod program_module_name {
use super::*;
pub fn instruction_one(ctx: Context<InstructionAccounts>, instruction_data: u64) -> Result<()> {
ctx.accounts.account_name.data = instruction_data;
Ok(())
}
}
Instruction Context
Context là loại hiển thị siêu dữ liệu và accounts của instruction.
pub struct Context<'a, 'b, 'c, 'info, T> {
/// Currently executing program id.
pub program_id: &'a Pubkey,
/// Deserialized accounts.
pub accounts: &'b mut T,
/// Remaining accounts given but not deserialized or validated.
/// Be very careful when using this directly.
pub remaining_accounts: &'c [AccountInfo<'info>],
/// Bump seeds found during constraint validation. This is provided as a
/// convenience so that handlers don't have to recalculate bump seeds or
/// pass them in as arguments.
pub bumps: BTreeMap<String, u8>,
}
- Các accounts được chuyển vào instruction nằm trong ctx.accounts
- Program ID ctx.program_id là địa chỉ chương trình đang thực thi
- Remaining accounts ctx.remaining_accounts là vector chưa tất cả các accounts chuyển vào instruction nhưng không được khai báo trong Accounts struct
- Bump ctx.bumps chưa bumps của các PDA accounts
Định nghĩa instruction accounts
Account trait định nghĩa cho cấu trúc dữ liệu của các tài khoản. Đây là các tài khoản bắt buộc cho instruction.
Một ví dụ, instruction_one đòi hỏi Context argument của InstructionAccounts. #[derive(Accounts)] macro sử dụng cho InstructionAccounts bao gồm 3 accounts: account_name, user, and system_program.
#[program]
mod program_module_name {
use super::*;
pub fn instruction_one(ctx: Context<InstructionAccounts>, instruction_data: u64) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InstructionAccounts {
#[account(init, payer = user, space = 8 + 8)]
pub account_name: Account<'info, AccountStruct>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
Khi instruction_one được gọi, chương trình:
- Kiểm tra xem các tài khoản được truyền vào vào khớp với các tài khoản được định nghĩa trong cấu trúc InstructionAccounts không.
- Kiểm tra các tài khoản theo ràng buộc bổ sung
Account validation
Bạn có thể nhận thấy trong ví dụ trước, một tài khoản có kiểu Account, một cho Signer và một Program.
Anchor có một số loại tài khoản khác nhau, với mỗi loại thực hiện cách xác thực khác nhau. Ban có thể xem toàn bộ ở: full list of account types.
Loại tài khoản Account
Account là wrapper xung quanh AccountInfo, nó xác thực quyền sử hữu của chương trình và deserializes data thành một kiểu dữ liệu Rust.
// Deserializes this info
pub struct AccountInfo<'a> {
pub key: &'a Pubkey,
pub is_signer: bool,
pub is_writable: bool,
pub lamports: Rc<RefCell<&'a mut u64>>,
pub data: Rc<RefCell<&'a mut [u8]>>, // <---- deserializes account data
pub owner: &'a Pubkey, // <---- checks owner program
pub executable: bool,
pub rent_epoch: u64,
}
Trong ví dụ trước, InstructionAccounts có trường account_name
Account wrapper sẽ thực hiện những việc sau:
- Deserializes account data và format lại thành AccountStruct
- Kiểm tra program owner của khớp với program được chỉ định của AccountStruct
Đây là cách kiểm tra được thực hiện:
// Checks
Account.info.owner == T::owner()
!(Account.info.owner == SystemProgram && Account.info.lamports() == 0)
Loại tài khoản Signer
Signer xác thực tài khoản đã kí trên transaction. Không có kiểm tra nào khác được thực hiện.
Đối với tài khoản user trong ví dụ trước, Signer xác định rằng user chắc chắn phải kí lên instruction.
Kiểm tra sau được thực hiện:
// Checks
Signer.info.is_signer == true
Loại tài khoản Program
Program xác thực đó là một chương trình nhất định.
Đối với system_program trong ví dụ trước, Program được dùng để chỉ định đây phải là system program. Anchor Program sẽ bao gồm programId để kiểm tra.
Kiểm tra sau được thực hiện:
//Checks
account_info.key == expected_program
account_info.executable == true
Thêm ràng buộc với #[account(..)]
#[account(..)] attribute macro được dùng để thêm ràng buộc cho accounts. Trong bài này chỉ nhắc đến một vài ví dụ cho ràng buộc. Bạn có thể xem toàn bộ ở đây: list of possible constraints.
Ở ví dụ trước, account_name của InstructionAccounts trong ví dụ:
#[account(init, payer = user, space = 8 + 8)]
pub account_name: Account<'info, AccountStruct>,
#[account(mut)]
pub user: Signer<'info>,
Để ý rằng thấy #[account(..)] chưa 3 giá trị phân cách nhau bằng dấu phẩy.
- init: khởi tạo account thông qua system program (đặt account discriminator).
- payer: chỉ định người trả tiền cho việc khởi tạo tài khoản user
- space: chỉ định không gian phân bổ cho tài khoản bằng 8 + 8 bytes. 8 byte đầu cho account discriminator, anchor tự động thêm vào để xác định loại tài khoản. 8 byte tiếp theo dành cho không gian lưu trữ trên tại khoản, được định nghĩa trong loại AccountStruct.
Đối với user, mut để chỉ định rằng tài khoản này có thể thay đổi. Tài khoản phải đánh dấu có thể thay đổi để trừ lamports khi thanh toán cho tạo account_name.
#[account(mut)]
pub user: Signer<'info>,
Lưu ý rằng init constraint bao gồm cả mut constraint, vì vậy cả account_name và user đều là mutable accounts.
Kết hợp tất cả lại với nhau
Khai bạn kết hợp tất cả những điều vừa rồi với nhau, bạn sẽ có một chương trình hoàn chỉnh. Dưới đây là một ví dụ về một chương trình Anchor cơ bản với một instruction duy nhất:
// Use this import to gain access to common anchor features
use anchor_lang::prelude::*;
// Program on-chain address
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
// Instruction logic
#[program]
mod program_module_name {
use super::*;
pub fn instruction_one(ctx: Context<InstructionAccounts>, instruction_data: u64) -> Result<()> {
ctx.accounts.account_name.data = instruction_data;
Ok(())
}
}
// Validate incoming accounts for instructions
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub account_name: Account<'info, AccountStruct>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Define custom program account type
#[account]
pub struct AccountStruct {
data: u64
}
Bây giờ bạn đã sẵn sàng để xây dựng chương trình Solana của riêng mình bằng cách sử framework Anchor.
Ví dụ
Setup
Khởi tạo project mới anchor-counter bằng câu lệnh anchor init:
anchor init anchor-counter
Sau cho chạy câu lệnh anchor build
anchor build
Đợi một khoảng thời gian cho việc tự động cài đặt và build xong thì chạy câu lệnh anchor keys list
anchor keys list
Sao chép địa chỉ xuất ra từ anchor keys list, ví dụ:
anchor_counter: BouTUP7a3MZLtXqMAm1NrkJSKwAjmid8abqiNjUyBJSr
Cập nhật declare_id! trong lib.rs tương tự như sau:
declare_id!("BouTUP7a3MZLtXqMAm1NrkJSKwAjmid8abqiNjUyBJSr");
Cập nhật luôn file Anchor.toml
[programs.localnet]
anchor_counter = "BouTUP7a3MZLtXqMAm1NrkJSKwAjmid8abqiNjUyBJSr"
Cuối cùng xoá code mặc định trong lib.rs để có nội dung tương tự như sau:
use anchor_lang::prelude::*;
declare_id!("BouTUP7a3MZLtXqMAm1NrkJSKwAjmid8abqiNjUyBJSr");
#[program]
pub mod anchor_counter {
use super::*;
}
Hiện thực Context Initialize
Sử dụng #[derive(Accounts)] macro cho Initialize, thực hiện định nghĩa các accounts sử dụng và validates cho chúng. Chúng ta cần 3 loại accounts sau:
- Counter: Account dùng chưa dữ liệu để đếm.
- User: Người thanh toán cho việc khởi tạo Counter.
- System_program: System Program là bắt buộc để khởi tạo account mới.
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
Hiện thực Counter
#[account]
pub struct Counter {
pub count: u64,
}
Hiện thực initialize instruction
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
msg!("Counter Account Created");
msg!("Current Count: { }", counter.count);
Ok(())
}
Thêm increment instruction
pub fn increment(ctx: Context<Update>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
msg!("Previous counter: {}", counter.count);
counter.count = counter.count.checked_add(1).unwrap();
msg!("Counter incremented. Current count: {}", counter.count);
Ok(())
}
Thêm Context Update cho increment
#[derive(Accounts)]
pub struct Update<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
pub user: Signer<'info>,
}
Build
Tổng quát lại, chúng ta sẽ có một chương trình trông như sau:
use anchor_lang::prelude::*;
declare_id!("BouTUP7a3MZLtXqMAm1NrkJSKwAjmid8abqiNjUyBJSr");
#[program]
pub mod anchor_counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
msg!("Counter account created. Current count: {}", counter.count);
Ok(())
}
pub fn increment(ctx: Context<Update>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
msg!("Previous counter: {}", counter.count);
counter.count = counter.count.checked_add(1).unwrap();
msg!("Counter incremented. Current count: {}", counter.count);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Update<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
pub user: Signer<'info>,
}
#[account]
pub struct Counter {
pub count: u64,
}
Chạy anchor build để build chương trình
anchor build
Testing
Anchor tích hợp test với typescript mà mocha test framework. Di chuyển đến file anchor-counter.ts và thay thế đoạn mã mặc định thành như sau:
import * as anchor from "@project-serum/anchor"
import { Program } from "@project-serum/anchor"
import { expect } from "chai"
import { AnchorCounter } from "../target/types/anchor_counter"
describe("anchor-counter", () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
const program = anchor.workspace.AnchorCounter as Program<AnchorCounter>
const counter = anchor.web3.Keypair.generate()
it("Is initialized!", async () => {})
it("Incremented the count", async () => {})
})
Đoạn mã trên tạo keypair cho counter account. Tiếp theo, tạo test cho initialize instruction
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({ counter: counter.publicKey })
.signers([counter])
.rpc()
const account = await program.account.counter.fetch(counter.publicKey)
expect(account.count.toNumber() === 0)
})
Tiếp theo, tạo thử nghiệm thứ hai cho increment instruction:
it("Incremented the count", async () => {
const tx = await program.methods
.increment()
.accounts({ counter: counter.publicKey, user: provider.wallet.publicKey })
.rpc()
const account = await program.account.counter.fetch(counter.publicKey)
expect(account.count.toNumber() === 1)
})
Cuối cùng, chạy anchor test và bạn sẽ thấy kết quả tương tự như sau:
anchor-counter
✔ Is initialized! (290ms)
✔ Incremented the count (403ms)
2 passing (696ms)
Khi chạy anchor test sẽ tự động tạo trình thử nghiệm cục bộ (local test validator), deploy chương trình của bạn, và chạy mo cha tests. Đừng lo lắng nếu bạn cảm thấy bối rối ở đây, chúng ra sẽ tìm hiểu sâu hơn sau.
Chúc mừng bạn vừa xây dựngt hành công một chương trình Solana sử dụng Anchor Framework. Bạn có thể tham khảo thêm mã nguồn ở đây nếu bạn cần chúng.
Thử Thách
Bây giờ đến lượt bạn xây dựng một cách độc lập. Bởi vì đây là một chương trình rất đơn giản, nó sẽ trông giống với những gì chúng ta vừa học, sẽ rất tuyệt vời nếu bạn có thể hoàn thành nó mà không copy code.
- Viết một chương trình khởi tạo counter account
- Thực hiện 2 instruction: increment và decrement
- Build và deploy chương trình của bạn giống ở ví dụ
- Test chương trình bạn và kiểm tra program logs.
Cố gắng hoàn thành độc lập nếu bạn có thể, tuy nhiên nếu bạn gặp vấn đề bạn có thể tham khảo giải pháp ở solution code
Các bài viết liên quan
Workshop 1: Introduction to Solana
Workshop 2: Xây dựng giao diện tương tác với Solana
Workshop 3: Client interaction with common Solana programs (SPLT, Metaplex)
Workshop 4: Use the Rust programming language, Anchor
Về Sentre Protocol
Sentre Protocol là nền tảng mở All-in-One hoạt động trên mạng lưới Solana, cung cấp DApp Store và giao thức mở giúp tích lũy thanh khoản. Với Sentre:
- Người dùng có thể cài đặt các DApp yêu thích của mình và trải nghiệm thế giới DeFi trên một nền tảng duy nhất;
- Lập trình viên và đối tác có thể phát hành DApp thông qua Sen Store, tận dụng tài nguyên có sẵn và tự do đóng góp cho nền tảng.