
Giới thiệu về Anchor: Hướng dẫn xây dựng chương trình Solana cho người mới bắt đầu
Mục lục
- Bài viết này nói về điều gì?
- Kiến thức cần có
- Cài đặt Anchor
- Cài đặt Rust
- Cài đặt Solana Tool Suite
- Cài đặt Yarn
- Cài đặt Anchor bằng AVM
- Cài đặt Anchor bằng tệp nhị phân và build từ mã nguồn
- Solana Playground
- Hello, World!
- Tạo dự án mới bằng thiết lập Anchor cục bộ
- Tạo dự án mới bằng Solana Playground
- Viết Hello, World!
- Build và triển khai cục bộ
- Triển khai lên Devnet
- Build và triển khai trên Solana Playground
- Trừu tượng hóa hiệu quả: IDL và macro
- Cấu trúc chương trình Anchor
- Các kiểu tài khoản
- Ràng buộc tài khoản
- Phân tích các ràng buộc của chương trình
- Không gian tài khoản
- Lỗi
- Lệnh gọi liên chương trình (CPI)
- Nâng quyền
- Thực thi CPI
- Địa chỉ dẫn xuất từ chương trình (PDA)
- Kết luận
- Tài nguyên bổ sung
Xin chân thành cảm ơn Noah, Mike, Jonas, Ryan, Prames và bl0ckpain đã đánh giá bài viết này.
Bài viết này nói về điều gì?
Rust thường được mô tả là ngôn ngữ chung trong quá trình phát triển chương trình Solana. Tuy nhiên, sẽ chính xác hơn nếu dùng cách mô tả này cho Anchor, vì phần lớn hoạt động phát triển bằng Rust đều sử dụng framework này. Anchor là một framework mạnh mẽ, có quy ước rõ ràng, được thiết kế để nhanh chóng xây dựng các chương trình Solana an toàn. Anchor hợp lý hóa quy trình phát triển bằng cách giảm mã soạn sẵn cho các tác vụ như tuần tự hóa và giải tuần tự hóa tài khoản cũng như dữ liệu chỉ thị, thực hiện các bước kiểm tra bảo mật thiết yếu, tự động tạo thư viện máy khách và cung cấp môi trường kiểm thử toàn diện.
Bài viết này trình bày cách phát triển chương trình Anchor. Nội dung bao gồm cài đặt Anchor, sử dụng Solana Playground cũng như tạo, build và triển khai một chương trình Hello, World! đơn giản. Sau đó, chúng ta sẽ tìm hiểu sâu hơn cách Anchor hợp lý hóa quy trình phát triển thông qua IDL, macro, cấu trúc chương trình Anchor, các loại tài khoản và ràng buộc cũng như xử lý lỗi. Chúng ta cũng sẽ điểm qua Cross-Program Invocation và Program Derived Address. Bài viết này cung cấp mọi kiến thức bạn cần để bắt đầu sử dụng Anchor ngay hôm nay.
Kiến thức cần có
Bài viết này giả định bạn đã hiểu mô hình lập trình của Solana. Nếu mới bắt đầu xây dựng trên Solana, bạn nên đọc bài viết trước của tôi, Mô hình lập trình Solana: Giới thiệu về phát triển trên Solana.
Đừng lo nếu bạn mới làm quen với Rust — bạn không cần kiến thức nâng cao để bắt đầu phát triển bằng Anchor. Tài liệu Anchor chỉ ra rằng nhà phát triển chỉ cần nắm vững kiến thức Rust cơ bản (tức chín chương đầu tiên của Rust Book). Bạn nên xem Hướng dẫn sinh tồn với Rust để hiểu rõ các khái niệm lập trình Rust thiết yếu. Việc hiểu các quy tắc về bộ nhớ, quyền sở hữu và mượn trong Rust cũng rất quan trọng.
Để giảm bớt độ khó khi học, các nhà phát triển mới làm quen với ngôn ngữ lập trình cấp thấp nên xem lại những khái niệm dành riêng cho lập trình hệ thống mà tài liệu Rust thường bỏ qua. Ví dụ, bạn nên tìm hiểu các chủ đề như kích thước biến, con trỏ và rò rỉ bộ nhớ. Tôi cũng đề xuất Rust By Example và kho lưu trữ của tôi về nhiều cấu trúc dữ liệu và thuật toán được viết bằng Rust để xem các ví dụ thực tế về Rust.
Bạn muốn dùng TypeScript thay thế? Hãy tìm hiểu cách viết chương trình Solana bằng TypeScript với framework của Poseidon để biên dịch chuyển đổi TypeScript sang Rust và tạo chương trình Anchor hợp lệ.
Bài viết này chỉ tập trung vào việc phát triển bằng Anchor. Chúng tôi sẽ không đề cập đến cách phát triển chương trình bằng Native Rust và cũng không giả định bạn có kiến thức về lĩnh vực đó. Ngoài ra, bài viết này sẽ không trình bày việc phát triển phía máy khách bằng Anchor — trong một bài viết sau, chúng tôi sẽ hướng dẫn cách kiểm thử và tương tác với chương trình Anchor qua TypeScript.
Sau phần giới thiệu, hãy bắt đầu với Anchor!
Cài đặt Anchor
Việc thiết lập Anchor gồm một vài bước đơn giản để cài đặt các công cụ và gói cần thiết. Phần này trình bày cách cài đặt những công cụ và gói đó (gồm Rust, Solana Tool Suite, Yarn và Anchor Version Manager).
Cài đặt Rust
Bạn có thể cài đặt Rust từ trang web chính thức của Rust hoặc qua dòng lệnh:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shCài đặt Solana Tool Suite
Anchor cũng yêu cầu Solana Tool Suite. Có thể cài đặt bản phát hành mới nhất (1.17.16 — tại thời điểm viết bài) bằng lệnh sau trên macOS và Linux:
sh -c "$(curl -sSfL https://release.solana.com/v1.17.16/install)"Người dùng Windows có thể cài đặt Solana Tool Suite bằng lệnh sau:
cmd /c "curl https://release.solana.com/v1.17.16/solana-install-init-x86_64-pc-windows-msvc.exe --output C:\solana-install-tmp\solana-install-init.exe --create-dirs"Tuy nhiên, bạn nên dùng Windows Subsystem for Linux (WSL) thay thế. Công cụ này cho phép chạy môi trường Linux trên máy Windows mà không cần khởi động kép hoặc thiết lập một máy ảo riêng. Nếu chọn cách này, hãy làm theo hướng dẫn cài đặt dành cho Linux ở trên (tức lệnh curl).
Nhà phát triển cũng có thể thay v1.17.16 bằng thẻ phát hành của phiên bản muốn tải xuống. Hoặc sử dụng tên kênh stable, beta hay edge. Sau khi cài đặt, hãy chạy solana –-version để xác nhận đã cài đúng phiên bản solana mong muốn.
Cài đặt Yarn
Anchor cũng yêu cầu Yarn. Bạn có thể cài đặt Yarn bằng Corepack, vốn được tích hợp trong mọi bản phát hành Node.js chính thức kể từ Node.js 14.9 / 16.9. Tuy nhiên, ở giai đoạn thử nghiệm hiện tại, bạn cần chủ động bật công cụ này. Vì vậy, chúng ta phải chạy corepack enable trước khi có thể sử dụng. Một số nhà phân phối bên thứ ba có thể không tích hợp Corepack theo mặc định. Do đó, bạn có thể cần chạy npm install -g corepack trước corepack enable.
Cài đặt Anchor bằng AVM
Tài liệu Anchor khuyên cài đặt Anchor qua Anchor Version Manager (AVM). AVM giúp đơn giản hóa việc quản lý và lựa chọn giữa nhiều bản cài đặt của tệp nhị phân anchor-cli. Điều này có thể cần thiết để tạo bản build có thể xác minh hoặc làm việc với các phiên bản khác nhau trên nhiều chương trình. Bạn có thể cài đặt AVM bằng Cargo với lệnh: cargo install --git [https://github.com/coral-xyz/anchor](https://github.com/coral-xyz/anchor) avm --locked --force. Sau đó, cài đặt và sử dụng phiên bản mới nhất:
avm install latest
avm use latest
# Verify the installation
avm --versionĐể xem danh sách các phiên bản anchor-cli hiện có, hãy dùng lệnh avm list. Nhà phát triển có thể dùng avm use <version> để sử dụng một phiên bản cụ thể. Phiên bản này sẽ tiếp tục được dùng cho đến khi được thay đổi. Nhà phát triển có thể gỡ cài đặt một phiên bản cụ thể bằng lệnh avm uninstall <version>.
Cài đặt Anchor bằng tệp nhị phân và build từ mã nguồn
Trên Linux, tệp nhị phân Anchor được cung cấp qua gói npm @coral-xyz/anchor-cli. Hiện tại, chỉ hỗ trợ Linux x86_64. Vì vậy, nhà phát triển phải build từ mã nguồn trên các hệ điều hành khác. Có thể dùng Cargo để cài đặt trực tiếp CLI. Ví dụ:
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --lockedThay đổi đối số --tag để cài đặt một phiên bản Anchor mong muốn khác. Bạn có thể cần cài đặt thêm các phần phụ thuộc nếu quá trình cài đặt bằng Cargo thất bại. Ví dụ, trên Ubuntu:
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-devSau đó, nhà phát triển có thể xác minh bản cài đặt Anchor bằng lệnh anchor --version.
Solana Playground
Ngoài ra, nhà phát triển có thể bắt đầu sử dụng Anchor với Solana Playground (Solpg). Solana Playground là IDE chạy trên trình duyệt, giúp nhanh chóng phát triển, kiểm thử và triển khai các chương trình Solana.
Nhà phát triển phải tạo Playground Wallet trong lần đầu sử dụng Solana Playground. Nhấp vào chỉ báo trạng thái màu đỏ có nhãn Chưa kết nối ở góc dưới bên trái màn hình. Hộp thoại sau sẽ xuất hiện:
Bạn nên lưu tệp cặp khóa của ví làm bản sao lưu trước khi nhấp vào Tiếp tục. Lý do là Playground Wallet được lưu trong bộ nhớ cục bộ của trình duyệt. Việc xóa bộ nhớ đệm của trình duyệt sẽ xóa ví.
Nhấp vào Tiếp tục để tạo một ví devnet sẵn sàng sử dụng trong IDE.
Để nạp tiền vào ví, nhà phát triển có thể chạy lệnh solana airdrop <amount> trong terminal của Playground, trong đó <amount> được thay bằng lượng SOL devnet mong muốn. Ngoài ra, hãy truy cập faucet này để nhận SOL devnet. Bạn nên tham khảo hướng dẫn cách nhận SOL devnet sau đây.
Lưu ý rằng bạn có thể gặp lỗi sau:
Error: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer fundsNguyên nhân thường là faucet devnet đã cạn và/hoặc bạn yêu cầu quá nhiều SOL. Giới hạn hiện tại là 5 SOL, quá đủ để triển khai chương trình này. Vì vậy, bạn nên yêu cầu 5 SOL từ faucet hoặc thực thi lệnh solana airdrop 5. Việc yêu cầu từng lượng nhỏ liên tiếp có thể dẫn đến giới hạn tốc độ.
Hello, World!
Chương trình Hello, World! được xem là cách tuyệt vời để làm quen với framework hoặc ngôn ngữ lập trình mới. Nhờ tính đơn giản, nhà phát triển ở mọi trình độ đều có thể hiểu được chương trình này. Chương trình cũng minh họa cấu trúc và cú pháp cơ bản của mô hình lập trình mới mà không đưa vào logic hoặc hàm phức tạp. Hello, World! đã nhanh chóng trở thành một chương trình nhập môn tiêu chuẩn trong lập trình, vì vậy việc tự viết một chương trình như vậy bằng Anchor là điều rất tự nhiên. Phần này trình bày cách build và triển khai chương trình Hello, World! bằng cả thiết lập Anchor cục bộ và Solana Playground.
Tạo dự án mới bằng thiết lập Anchor cục bộ
Sau khi cài đặt Anchor, việc tạo dự án mới chỉ đơn giản như sau:
anchor init hello-world
cd hello-worldCác lệnh này sẽ khởi tạo một dự án Anchor mới có tên hello-world và chuyển đến thư mục của dự án. Trong thư mục này, hãy mở hello-world/programs/hello-world/src/lib.rs. Tệp này chứa mã khởi đầu sau:
use anchor_lang::prelude::*;
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");
#[program]
pub mod hello-world {
use super::*;
pub fn initialize(ctx: Context) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}Anchor đã chuẩn bị sẵn một số tệp và thư mục cho chúng ta. Cụ thể gồm:
- Một thư mục app trống dành cho máy khách của chương trình
- Một thư mục programs chứa tất cả chương trình Solana của chúng ta
- Một thư mục tests để kiểm thử JavaScript. Thư mục này đi kèm một tệp kiểm thử được tự động tạo cho mã khởi đầu
- Một tệp cấu hình Anchor.toml. Nếu mới làm quen với Rust, tệp TOML là một định dạng tệp cấu hình tối giản, dễ đọc nhờ ngữ nghĩa rõ ràng. Tệp Anchor.toml được dùng để cấu hình cách Anchor tương tác với chương trình. Ví dụ: chương trình sẽ được triển khai lên cụm nào.
Tạo dự án mới bằng Solana Playground
Việc tạo dự án mới trên Solana Playground rất đơn giản. Chuyển đến góc trên bên trái và nhấp vào Tạo dự án mới:
Hộp thoại sau sẽ xuất hiện:
Đặt tên chương trình, chọn Anchor(Rust) rồi nhấp vào Tạo. Thao tác này sẽ tạo một dự án Anchor mới ngay trong trình duyệt. Trong mục Program ở bên trái, bạn sẽ thấy thư mục src. Thư mục này chứa lib.rs với mã khởi đầu sau:
use anchor_lang::prelude::*;
// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("11111111111111111111111111111111");
#[program]
mod hello_anchor {
use super::*;
pub fn initialize(ctx: Context, data: u64) -> Result<()> {
ctx.accounts.new_account.data = data;
msg!("Changed data to: {}!", data); // Message will show up in the tx logs
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
// We must specify the space in order to initialize an account.
// First 8 bytes are default account discriminator,
// next 8 bytes come from NewAccount.data being type u64.
// (u64 = 64 bits unsigned integer = 8 bytes)
#[account(init, payer = signer, space = 8 + 8)]
pub new_account: Account<'info, NewAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct NewAccount {
data: u64
}Lưu ý rằng Solana Playground chỉ tạo các tệp client.ts và anchor.test.ts. Bạn nên đọc phần tạo chương trình bằng Anchor trên máy cục bộ để xem nội dung chi tiết thường được tạo cho một dự án Anchor mới.
Viết Hello, World!
Dù sử dụng Anchor cục bộ hay qua Solana Playground, với một chương trình Hello, World! rất đơn giản, hãy thay mã khởi đầu bằng nội dung sau:
use anchor_lang::prelude::*;
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");
#[program]
mod hello_world {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
msg!("Hello, World!");
Ok(())
}
#[derive(Accounts)]
pub struct Hello {}
}Chúng ta sẽ xem xét chi tiết từng phần trong các mục tiếp theo. Hiện tại, điều quan trọng là lưu ý cách dùng macro và trait để đơn giản hóa quy trình phát triển. Macro declare_id! đặt khóa công khai cho chương trình. Khi phát triển cục bộ, lệnh anchor init dùng để thiết lập chương trình sẽ tạo một cặp khóa trong thư mục target/deploy và điền giá trị vào macro này. Solana Playground cũng tự động thực hiện việc này cho chúng ta.
Trong mô-đun hello_world chính, chúng ta tạo một hàm ghi Hello, World! vào nhật ký. Hàm cũng trả về Ok(()) để báo hiệu chương trình đã thực thi thành công. Lưu ý rằng chúng ta thêm dấu gạch dưới vào trước ctx để tránh cảnh báo biến không được sử dụng trong bảng điều khiển. Hello là một struct tài khoản không yêu cầu truyền tài khoản nào vì chương trình chỉ ghi một thông báo mới vào nhật ký.
Vậy là xong! Không cần nhận tài khoản hay thực hiện logic phức tạp. Đoạn mã trên tạo một chương trình ghi Hello, World! vào nhật ký.
Build và triển khai cục bộ
Phần này tập trung vào việc triển khai lên Localhost. Mặc dù Solana Playground mặc định sử dụng devnet, môi trường phát triển cục bộ mang lại trải nghiệm tốt hơn đáng kể cho nhà phát triển. Môi trường này không chỉ nhanh hơn mà còn tránh được một số vấn đề thường gặp khi kiểm thử trên devnet. Ví dụ: không đủ SOL cho giao dịch, triển khai chậm và không thể kiểm thử khi devnet ngừng hoạt động. Ngược lại, phát triển cục bộ có thể đảm bảo trạng thái mới sau mỗi lần kiểm thử. Điều này tạo ra môi trường phát triển được kiểm soát tốt hơn và hiệu quả hơn.
Cấu hình công cụ
Trước tiên, chúng ta cần đảm bảo Solana Tool Suite được cấu hình đúng để phát triển trên Localhost. Chạy lệnh solana config set --url localhost để đảm bảo mọi cấu hình đều trỏ đến URL của Localhost.
Ngoài ra, hãy đảm bảo bạn có một cặp khóa cục bộ để tương tác với Solana trên máy. Bạn phải có ví Solana với số dư SOL để triển khai chương trình bằng Solana CLI. Chạy lệnh solana address để kiểm tra xem bạn đã có cặp khóa cục bộ hay chưa. Nếu gặp lỗi, hãy chạy lệnh solana-keygen new. Theo mặc định, một ví mới trên hệ thống tệp sẽ được tạo tại đường dẫn ~/.config/solana/id.json. Lệnh cũng cung cấp một cụm từ khôi phục có thể dùng để khôi phục khóa công khai và khóa riêng tư. Bạn nên lưu cặp khóa này dù chỉ sử dụng cục bộ. Cũng cần lưu ý rằng nếu đã có ví trên hệ thống tệp được lưu tại vị trí mặc định, lệnh solana-keygen new sẽ không ghi đè ví đó, trừ khi bạn chỉ định bằng lệnh --force.
Cấu hình Anchor.toml
Tiếp theo, chúng ta cần đảm bảo tệp Anchor.toml trỏ đúng đến Localhost. Hãy đảm bảo tệp chứa đoạn mã sau:
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Localnet"
wallet = '~config/solana/id.json'Ở đây, [programs.localnet] chỉ ID của chương trình trên localnet (tức Localhost). ID chương trình luôn được chỉ định theo cụm. Lý do là cùng một chương trình có thể được triển khai đến một địa chỉ khác trên một cụm khác. Xét về trải nghiệm nhà phát triển, việc khai báo ID chương trình mới cho các chương trình được triển khai trên nhiều cụm khác nhau có thể khá phiền phức.
ID chương trình là công khai. Tuy nhiên, cặp khóa của chương trình được lưu trong thư mục target/deploy. Tên tệp tuân theo quy ước cụ thể dựa trên tên chương trình. Ví dụ, nếu chương trình có tên hello_world, Anchor sẽ tìm cặp khóa tại target/deploy/hello-world-keypair.json. Anchor sẽ tạo một cặp khóa mới nếu không tìm thấy tệp này trong quá trình triển khai. Điều này sẽ tạo ra một ID chương trình mới. Vì vậy, việc cập nhật ID chương trình sau lần triển khai đầu tiên là rất quan trọng. Tệp hello-world-keypair.json đóng vai trò là bằng chứng về quyền sở hữu chương trình. Nếu cặp khóa bị rò rỉ, kẻ xấu có thể thực hiện các thay đổi trái phép đối với chương trình.
Với [provider], chúng ta yêu cầu Anchor sử dụng Localhost và ví được chỉ định để thanh toán chi phí lưu trữ và giao dịch.
Build, triển khai và chạy sổ cái cục bộ
Dùng lệnh anchor build để build chương trình. Để build một chương trình cụ thể theo tên, hãy dùng lệnh anchor build -p <program name> và thay <program name> bằng tên chương trình. Vì đang phát triển trên localnet, chúng ta có thể dùng các lệnh localnet của Anchor CLI để hợp lý hóa quy trình phát triển. Ví dụ, anchor localnet --skip-build đặc biệt hữu ích để bỏ qua bước build một chương trình trong workspace. Cách này có thể tiết kiệm thời gian khi chạy kiểm thử nếu mã của chương trình không thay đổi.
Nếu chạy lệnh anchor deploy ngay bây giờ, chúng ta sẽ nhận được lỗi. Lý do là chưa có cụm Solana nào đang chạy trên máy để kiểm thử. Chúng ta có thể chạy một sổ cái cục bộ để mô phỏng cụm trên máy. Solana CLI đi kèm một trình xác thực kiểm thử. Chạy lệnh solana-test-validator sẽ khởi động một cụm đơn nút đầy đủ tính năng trên máy trạm. Điều này mang lại nhiều lợi ích như không có giới hạn tốc độ RPC, không có giới hạn airdrop, triển khai chương trình trực tiếp trên chuỗi, tải tài khoản từ tệp và sao chép tài khoản từ cụm công khai. Trình xác thực kiểm thử phải chạy trong một cửa sổ terminal riêng và tiếp tục hoạt động để cụm localhost luôn trực tuyến cũng như sẵn sàng cho việc tương tác.
Bây giờ, chúng ta có thể chạy thành công anchor deploy để triển khai chương trình lên sổ cái cục bộ. Mọi dữ liệu được truyền đến sổ cái cục bộ sẽ được lưu trong thư mục test-ledger được tạo tại thư mục làm việc hiện tại. Bạn nên thêm thư mục này vào tệp .gitignore để tránh commit thư mục vào kho lưu trữ. Ngoài ra, việc thoát khỏi sổ cái cục bộ (tức nhấn Ctrl + C trong terminal) sẽ không xóa bất kỳ dữ liệu nào đã gửi đến cụm. Việc xóa thư mục test-ledger hoặc chạy solana-test-validator --reset sẽ xóa dữ liệu.
Xin chúc mừng! Bạn vừa triển khai chương trình Solana đầu tiên của mình lên Localhost!
Solana Explorer
Nhà phát triển cũng có thể cấu hình Solana Explorer với sổ cái cục bộ. Truy cập Solana Explorer. Trên thanh điều hướng, nhấp vào nút màu xanh lá hiển thị cụm hiện tại:
Thao tác này sẽ mở thanh bên cho phép bạn chọn cụm. Nhấp vào URL RPC tùy chỉnh. Trường này sẽ tự động được điền bằng http://localhost:8899. Nếu không, hãy điền địa chỉ này để trình khám phá trỏ đến máy của bạn tại cổng 8899:
Tính năng này vô cùng hữu ích vì một số lý do:
- Cho phép nhà phát triển kiểm tra giao dịch trên sổ cái cục bộ theo thời gian thực, tương tự những chức năng thường có trên trình khám phá khối phân tích devnet hoặc mainnet
- Giúp dễ dàng trực quan hóa trạng thái của tài khoản, token và chương trình như thể chúng đang hoạt động trên một cụm trực tiếp
- Cung cấp thông tin chi tiết về lỗi và giao dịch thất bại
- Mang lại trải nghiệm phát triển nhất quán giữa các cụm nhờ giao diện quen thuộc
Triển khai lên Devnet
Mặc dù chúng tôi khuyến khích phát triển trên Localhost, nhà phát triển vẫn có thể triển khai lên devnet nếu muốn kiểm thử riêng trên cụm đó. Quy trình nhìn chung giống nhau, ngoại trừ việc không cần chạy sổ cái cục bộ vì chúng ta đã có một cụm Solana hoàn chỉnh để tương tác.
Chạy lệnh solana config set --url devnet để chuyển cụm được chọn sang devnet. Từ giờ, mọi lệnh solana chạy trong terminal đều sẽ được thực thi trên devnet. Sau đó, trong tệp Anchor.toml, hãy sao chép mục [programs.localnet] và đổi tên thành [programs.devnet]. Đồng thời, thay đổi [provider] để giờ đây trỏ đến devnet:
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
[programs.devnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Devnet"
wallet = '~config/solana/id.json'Nhà phát triển phải đảm bảo có SOL devnet để triển khai chương trình. Dùng lệnh solana airdrop <amount> để airdrop vào vị trí cặp khóa mặc định tại ~/.config/solana/id.json. Bạn cũng có thể chỉ định địa chỉ ví bằng solana aidrop <amount> <wallet address>. Ngoài ra, hãy truy cập faucet này để nhận SOL devnet. Bạn nên tham khảo hướng dẫn cách nhận SOL devnet sau đây.
Bạn có thể gặp lỗi sau: không thể xác nhận giao dịch. Điều này có thể xảy ra trong các trường hợp như giao dịch hết hạn và tài khoản trả phí không đủ tiền
Nguyên nhân thường là faucet devnet đã cạn và/hoặc bạn yêu cầu quá nhiều SOL cùng lúc. Giới hạn hiện tại là 5 SOL, quá đủ để triển khai chương trình này. Vì vậy, bạn nên yêu cầu 5 SOL từ faucet hoặc thực thi lệnh solana airdrop 5. Việc yêu cầu từng lượng nhỏ liên tiếp có thể dẫn đến giới hạn tốc độ.
Bây giờ, hãy build và triển khai chương trình bằng các lệnh sau:
anchor build
anchor deployXin chúc mừng! Bạn vừa triển khai cục bộ chương trình Solana đầu tiên của mình lên devnet!
Build và triển khai trên Solana Playground
Trên Solana Playground, mở biểu tượng Công cụ ở thanh bên trái. Nhấp vào Build. Trong bảng điều khiển, bạn sẽ thấy nội dung sau:
Building...
Build successful. Completed in 2.20s..Lưu ý rằng ID trong macro declare_id! đã bị ghi đè. Địa chỉ mới này là nơi chúng ta sẽ triển khai chương trình. Bây giờ, hãy nhấp vào Triển khai. Bạn sẽ thấy nội dung tương tự như sau trong bảng điều khiển:
Deploying... This could take a while depending on the program size and network conditions.
Warning: 41 transactions not confirmed, retrying...
Deployment successful. Completed in 17sXin chúc mừng! Bạn vừa triển khai chương trình Solana đầu tiên của mình lên devnet qua Solana Playground!
Trừu tượng hóa hiệu quả: IDL và macro
Anchor đơn giản hóa quá trình phát triển chương trình thông qua khả năng trừu tượng hóa hiệu quả. Nói cách khác, Anchor đơn giản hóa các khái niệm lập trình blockchain phức tạp, giúp chúng dễ tiếp cận và dễ sử dụng hơn. Ví dụ, Anchor sử dụng Ngôn ngữ định nghĩa giao diện (IDL) để định nghĩa giao diện của chương trình. Khi xây dựng chương trình, Anchor sẽ tạo một tệp JSON đại diện cho IDL của chương trình. Về cơ bản, cấu trúc này có thể được sử dụng ở phía máy khách để xác định cách tương tác với các hàm và cấu trúc dữ liệu của chương trình. Anchor cũng cung cấp các lớp trừu tượng cấp cao hơn để xử lý việc quản lý trạng thái. Anchor cho phép nhà phát triển định nghĩa trạng thái chương trình bằng các struct Rust, trực quan hơn so với làm việc với mảng byte thô hoặc tự tuần tự hóa. Nhờ đó, nhà phát triển có thể định nghĩa trạng thái như với bất kỳ cấu trúc dữ liệu Rust thông thường nào, còn Anchor sẽ xử lý việc tuần tự hóa và lưu trữ dữ liệu vào các tài khoản ở lớp bên dưới.
Việc phát hành IDL on-chain cũng rất đơn giản. Nhà phát triển có thể phát hành IDL bằng lệnh sau:
anchor idl init --filepath --provider.cluster --provider.walletHãy đảm bảo ví được cung cấp là authority của chương trình và có đủ SOL cho giao dịch. Giờ đây, nhà phát triển có thể xem IDL của mình trên một trình khám phá khối như Orb.
Ví dụ, đây là IDL aggregator v4 của DFlow trên Orb.
Macro của Anchor là một trong những lớp trừu tượng quan trọng nhất, nếu không muốn nói là quan trọng nhất. Trong Rust, macro là một đoạn mã tạo ra một đoạn mã khác. Đây là một dạng siêu lập trình. Macro khai báo là dạng macro được sử dụng rộng rãi nhất trong Rust. Chúng cho phép nhà phát triển viết nội dung tương tự biểu thức match thông qua cấu trúc macro_rules!. Macro thủ tục hoạt động giống một hàm hơn: nhận mã làm đầu vào, xử lý mã đó và tạo ra đầu ra. Ví dụ, trong Anchor, macro #[account] định nghĩa và thực thi các ràng buộc đối với tài khoản Solana. Điều này giúp giảm độ phức tạp và các lỗi tiềm ẩn liên quan đến việc quản lý tài khoản. Khi tìm hiểu các macro của Anchor, không thể không bàn đến cấu trúc chương trình của Anchor.
Cấu trúc chương trình Anchor
Cấu trúc chương trình của Anchor được thiết kế để kết hợp macro và trait nhằm tạo mã soạn sẵn và thực thi logic chương trình. Triết lý thiết kế này góp phần lớn vào việc tinh giản quy trình phát triển, đồng thời đảm bảo hành vi chương trình nhất quán và đáng tin cậy.
Các khai báo use nằm ở đầu tệp. Lưu ý rằng đây là ngữ nghĩa chung của ngôn ngữ Rust, không dành riêng cho Anchor. Các khai báo này tạo một hoặc nhiều liên kết tên cục bộ đồng nghĩa với một đường dẫn khác — khai báo use rút ngắn đường dẫn cần dùng để tham chiếu đến một mục trong mô-đun. Chúng có thể xuất hiện trong mô-đun hoặc khối. Ngoài ra, từ khóa self có thể liên kết một danh sách đường dẫn có cùng tiền tố và mô-đun cha chung. Ví dụ, tất cả các khai báo use sau đều hợp lệ:
use anchor_lang::prelude::*;
use std::collections::hash_map::{self, HashMap};
use a::b::{c, d, e::f, g::h::i};
use a::b::{self, c, d::e};Macro Anchor đầu tiên mà nhà phát triển gặp là declare_id!. Macro này dùng để khai báo địa chỉ của chương trình (program ID), đảm bảo mọi tương tác đều được định tuyến chính xác đến chương trình. Anchor sẽ tạo một keypair mới khi nhà phát triển build chương trình Anchor lần đầu. Đây là keypair dùng để triển khai chương trình, trừ khi có chỉ định khác. Public key của keypair phải được cung cấp làm program ID cho macro declare_id!:
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");Macro thuộc tính #[program] biểu thị mô-đun logic lệnh của chương trình. Nó đóng vai trò là điểm vào, xác định cách chương trình diễn giải và thực thi các lệnh đến. Macro này đơn giản hóa việc định tuyến các lệnh đến hàm phù hợp trong chương trình, giúp mã chương trình có tổ chức và dễ quản lý hơn. Mỗi hàm trong mô-đun này được xem là một lệnh riêng biệt. Mỗi hàm sẽ nhận tham số ngữ cảnh (ctx) thuộc kiểu Context làm đối số đầu tiên. Nhà phát triển có thể truy cập các tài khoản, program ID của chương trình đang thực thi và các tài khoản còn lại.
Kiểu Context được định nghĩa như sau:
pub struct Context<'a, 'b, 'c, 'info, T: Bumps> {
pub program_id: &'a Pubkey,
pub accounts: &'b mut T,
pub remaining_accounts: &'c [AccountInfo<'info>],
pub bumps: T::Bumps,
}Điều này giúp cung cấp các đầu vào không phải đối số cho một chương trình nhất định. Trường program_id thuộc kiểu Pubkey và đại diện cho program ID hiện đang thực thi. accounts chỉ các tài khoản đã được tuần tự hóa, còn remaining_accounts chỉ các tài khoản còn lại đã được cung cấp nhưng chưa được giải tuần tự hoặc xác thực — hãy hết sức thận trọng khi sử dụng trực tiếp trường này. Trường bumps thuộc kiểu Bumps do #[derive(Accounts)] tạo ra. Nó đại diện cho các bump seed được tìm thấy trong quá trình xác thực ràng buộc. Chúng ta sẽ tìm hiểu các ràng buộc tài khoản ở phần sau. Hiện tại, điều quan trọng cần biết là trường này được cung cấp để thuận tiện, nhờ đó các handler không phải tính lại bump seed hoặc truyền chúng dưới dạng đối số.
Lưu ý rằng Context là một kiểu generic. Trong Rust, generic cho phép nhà phát triển viết mã linh hoạt, có thể tái sử dụng và hoạt động với mọi kiểu dữ liệu. Chúng cho phép định nghĩa kiểu cho struct, enum, hàm và phương thức mà không cần chỉ định chính xác kiểu sẽ được sử dụng. Thay vào đó, một placeholder được dùng cho các kiểu đó, thường được biểu thị là T. Generic giúp giảm mã lặp lại và tăng tính rõ ràng. Ví dụ, có thể định nghĩa một enum để chứa các kiểu dữ liệu generic:
enum Option<T> {
Some(T),
None,
}Đoạn mã trên minh họa enum Option<T>. Đây là một enum Rust tiêu chuẩn có thể đóng gói một giá trị thuộc bất kỳ kiểu nào (tức Some(T)) hoặc không có kiểu nào (None).
Trong trường hợp này, Context là một kiểu generic, trong đó T chỉ định các tài khoản cần thiết cho một lệnh (tức bất kỳ kiểu nào nhà phát triển muốn tạo để lưu dữ liệu). Khi sử dụng Context, nhà phát triển có thể định nghĩa T dưới dạng một struct triển khai trait Accounts. Ví dụ: Context<SetData>. Nhà phát triển có thể truy cập các trường trong kiểu Context bằng ký hiệu dấu chấm. Chẳng hạn, ctx.accounts truy cập trường accounts của struct Context.
Như đã đề cập, macro #[account] định nghĩa các kiểu tài khoản tùy chỉnh. Trong các phần tiếp theo, chúng ta sẽ tìm hiểu các kiểu tài khoản và ràng buộc bằng #[account(...)]. Hiện tại, điều quan trọng cần lưu ý là struct Accounts là nơi nhà phát triển định nghĩa các tài khoản mà một lệnh cần nhận và những ràng buộc mà các tài khoản này phải tuân theo.
Các kiểu tài khoản
Kiểu Account được dùng khi một lệnh muốn truy cập dữ liệu đã giải tuần tự của tài khoản. Struct Account là generic theo T và được định nghĩa như sau:
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> { /* private fields */ }Đây là một wrapper cho AccountInfo , có chức năng xác minh quyền sở hữu của chương trình và giải tuần tự dữ liệu bên dưới thành một kiểu Rust. Nó kiểm tra quyền sở hữu chương trình theo điều kiện Account.info.owner == T::owner(). Nói cách khác, nó kiểm tra chủ sở hữu dữ liệu có trùng với ID (được tạo trước đó bằng declare_id!) của crate nơi sử dụng #[account] hay không. Điều này có nghĩa là kiểu dữ liệu được Account bọc (=T) phải triển khai trait Owner. Thuộc tính #[account] triển khai trait cho một struct bằng crate::ID được declare_id! khai báo trong cùng chương trình. Trong hầu hết trường hợp, nhà phát triển chỉ cần dùng thuộc tính #[account] để thêm các trait và phần triển khai cần thiết vào dữ liệu. Thuộc tính #[account] tạo phần triển khai cho các trait sau:
Khi triển khai các trait tuần tự hóa tài khoản, 8 byte đầu tiên được dành cho một discriminator tài khoản duy nhất. Discriminator này được xác định bằng 8 byte đầu tiên của hàm băm SHA-256 từ định danh Rust của tài khoản. Mọi lệnh gọi try_deserialize của AccountDeserialize đều kiểm tra discriminator này và dừng quá trình giải tuần tự tài khoản kèm lỗi nếu tài khoản được cung cấp không hợp lệ.
Sẽ có những trường hợp nhà phát triển cần tương tác với các chương trình không dùng Anchor. Khi đó, nhà phát triển có thể tận dụng mọi lợi ích của Account nếu tự tạo một kiểu wrapper tùy chỉnh thay vì dùng #[account]. Hãy xem đoạn mã sau làm ví dụ:
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
// Rest of the program
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>,
#[account(
constraint = my_account.mint == token_account.mint,
has_one = owner
)]
pub token_account: Account<'info, TokenAccount>,
pub owner: Signer<'info>
}Phần lớn việc xác thực tài khoản được thực hiện qua các ràng buộc tài khoản mà chúng ta sẽ tìm hiểu trong phần tiếp theo. Còn hiện tại, hãy xem cách kiểu TokenAccount được dùng để đảm bảo tài khoản đầu vào thuộc sở hữu của token program. TokenAccount bọc struct Account của token program và bổ sung các hàm cần thiết. Điều này đảm bảo Anchor có thể giải tuần tự tài khoản, đồng thời nhà phát triển có thể sử dụng các trường của tài khoản trong ràng buộc tài khoản và với hàm lệnh.
Ngoài ra, hãy lưu ý trong đoạn mã trên rằng macro derive đóng gói toàn bộ struct. Macro này triển khai một bộ giải tuần tự Accounts trên SetData và được dùng để xác thực các tài khoản đầu vào.
Có thể sử dụng một số kiểu Account trong struct xác thực tài khoản, bao gồm:
- Account<’info, T>: một container tài khoản kiểm tra quyền sở hữu khi giải tuần tự
- AccountInfo<’info>: một tài khoản chưa được kiểm tra có thể dùng làm kiểu. Tuy nhiên, nên dùng UncheckedAccount thay thế vì AccountInfo có thể sẽ bị loại bỏ trong một bản phát hành tương lai
- AccountLoader<’info, T>: một kiểu hỗ trợ giải tuần tự zero-copy theo nhu cầu. Cách này khác với việc dùng
Accountvì nhà phát triển phải gọiload_initsau khi khởi tạo tài khoản,loadkhi tài khoản không thể thay đổi vàload_mutkhi tài khoản có thể thay đổi - Box<Account<’info, T>> hoặc Box<InterfaceAccount<’info, T>>: một kiểu box giúp tiết kiệm không gian stack vì đôi khi tài khoản quá lớn đối với stack và có thể gây vi phạm stack — đưa tài khoản vào box có thể khắc phục vấn đề này
- Interface<’info, T>: một kiểu bọc
Program, dùng để xác thực rằng tài khoản thuộc một trong các chương trình đã cho. Nó kiểm tra xem chương trình dự kiến có chứa khóa của tài khoản hay không và tài khoản có thể thực thi hay không - InterfaceAccount<’info, T>: một container tài khoản kiểm tra quyền sở hữu chương trình và giải tuần tự dữ liệu bên dưới thành một kiểu Rust
- Option<Account<’info, T>>: một kiểu option dành cho tài khoản tùy chọn
- Program<’info, T>: một kiểu xác thực tài khoản có phải là chương trình đã cho hay không
- Signer<’info>: một kiểu xác thực tài khoản có ký giao dịch hay không
- SystemAccount<’info>: một kiểu xác thực tài khoản có thuộc sở hữu của System Program hay không
- Sysvar<’info, T>: một kiểu xác thực tài khoản có phải là sysvar hay không. Tức là tài khoản có phải kiểu đặc biệt chứa dữ liệu được cập nhật động về cụm mạng, lịch sử blockchain và giao dịch đang thực thi hay không. Các sysvar
clock,epoch_schedule,instructionsvàrentrất hữu ích khi phát triển chương trình - UncheckedAccount<’info>: một container tài khoản nhấn mạnh rõ rằng không có kiểm tra nào được thực hiện trên tài khoản đã chỉ định
Ràng buộc tài khoản
Ràng buộc tài khoản đóng vai trò thiết yếu trong việc phát triển các chương trình Anchor an toàn. Trong những bài viết sau, chúng ta sẽ tìm hiểu sâu hơn về bảo mật chương trình Solana và việc tấn công chương trình Anchor. Tuy nhiên, ở đây cần tìm hiểu về các ràng buộc. Ràng buộc cho phép nhà phát triển xác minh một số tài khoản hoặc dữ liệu mà chúng lưu giữ có đáp ứng các yêu cầu được định trước hay không. Có thể áp dụng nhiều loại ràng buộc khác nhau bằng thuộc tính #[account(...)], thuộc tính này cũng có thể tham chiếu đến các cấu trúc dữ liệu khác. Định dạng như sau:
#[account(constraint goes here)]
pub account: AccountTypeCũng cần lưu ý rằng trong macro Accounts, nhà phát triển có thể truy cập các đối số của lệnh bằng thuộc tính #[instruction(...)]. Nhà phát triển cần liệt kê các đối số của lệnh theo đúng thứ tự trong lệnh nhưng có thể bỏ qua mọi đối số sau đối số cuối cùng cần dùng. Ví dụ từ tài liệu Anchor:
...
pub fn initialize(ctx: Context, bump: u8, authority: Pubkey, data: u64) -> anchor_lang::Result<()> {
...
Ok(())
}
...
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct Initialize<'info> {
...
}Ràng buộc tài khoản có thể được chia thành Ràng buộc thông thường và Ràng buộc SPL. Chúng ta sẽ lần lượt xem xét các ràng buộc cụ thể trong phần còn lại của bài viết. Trong các ví dụ này, <expr> đại diện cho một biểu thức bất kỳ có thể được truyền vào, miễn là biểu thức đó cho ra giá trị thuộc kiểu dự kiến. Ví dụ: owner = token_program.key().
Phân tích các ràng buộc của chương trình
Tôi khuyên bạn nên tham khảo tài liệu Anchor về tài khoản để xem danh sách đầy đủ hơn về các ràng buộc có thể sử dụng. Việc duyệt qua từng ràng buộc và cung cấp định nghĩa chính thức dưới dạng bảng sẽ quá dài dòng. Trong phạm vi bài viết này, phân tích chương trình sau sẽ hữu ích hơn để hiểu cách các ràng buộc tài khoản hoạt động trong thực tế:
use anchor_lang::prelude::*;
#[cfg(not(feature = "no-entrypoint"))]
use {default_env::default_env, solana_security_txt::security_txt};
declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6");
pub mod errors;
pub mod instructions;
pub mod state;
pub use instructions::*;
pub use state::*;
#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
name: "Fanout",
project_url: "http://helium.com",
contacts: "email:hello@helium.foundation",
policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md",
// Optional Fields
preferred_languages: "en",
source_code: "https://github.com/helium/helium-program-library/tree/master/programs/fanout",
source_revision: default_env!("GITHUB_SHA", ""),
source_release: default_env!("GITHUB_REF_NAME", ""),
auditors: "Sec3"
}
#[program]
pub mod fanout {
use super::*;
pub fn initialize_fanout_v0(
ctx: Context<InitializeFanoutV0>,
args: InitializeFanoutArgsV0,
) -> Result<()> {
instructions::initialize_fanout_v0::handler(ctx, args)
}
pub fn stake_v0(ctx: Context<StakeV0>, args: StakeArgsV0) -> Result<()> {
instructions::stake_v0::handler(ctx, args)
}
pub fn unstake_v0(ctx: Context<UnstakeV0>) -> Result<()> {
instructions::unstake_v0::handler(ctx)
}
pub fn distribute_v0(ctx: Context<DistributeV0>) -> Result<()> {
instructions::distribute_v0::handler(ctx)
}
}Đây là chương trình Fanout của Helium. Đây là một chương trình tương đối phức tạp, dùng để phân phối token cho người nắm giữ token theo tỷ lệ dựa trên số lượng họ sở hữu. Hiện tại, dự án có vẻ chưa hữu ích lắm cho mục đích của chúng ta vì chưa có ràng buộc nào. Tuy nhiên, nếu phân tích struct StakeV0 của lệnh stake_v0, chúng ta sẽ có rất nhiều ràng buộc để khám phá.
mut
Ràng buộc đầu tiên trong lệnh này là ràng buộc tài khoản mut. mut được định nghĩa là #[account(mut)] hoặc #[account(mut @ <custom_error>)], hỗ trợ lỗi tùy chỉnh bằng ký hiệu @. Ràng buộc này kiểm tra một tài khoản nhất định có thể thay đổi hay không và yêu cầu Anchor lưu mọi thay đổi trạng thái. Trong chương trình của Helium, ràng buộc này đảm bảo tài khoản payer có thể thay đổi:
...
pub struct StakeV0<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub staker: Signer<'info>,
/// CHECK: Just needed to receive nft
pub recipient: AccountInfo<'info>,
...has_one
Ràng buộc has_one được định nghĩa là #[account(has_one = <target_account)] hoặc #[account(has_one = <target_account> @ <custom_error>)]. Nó kiểm tra trường target_account để xem tài khoản có khớp với khóa của trường target_account trong struct Accounts hay không. Lỗi tùy chỉnh được hỗ trợ qua annotation @.
Trong ngữ cảnh của struct StakeV0, ràng buộc has_one được dùng để kiểm tra tài khoản có membership_mint, token_account và membership_collection hay không:
...
#[account(
mut,
has_one = membership_mint,
has_one = token_account,
has_one = membership_collection
)]
pub fanout: Box<Account<'info, FanoutV0>>,
pub membership_mint: Box<Account<'info, Mint>>,
pub token_account: Box<Account<'info, TokenAccount>>,
pub membership_collection: Box<Account<'info, Mint>>,
...Lưu ý rằng có nhiều ràng buộc has_one và ràng buộc mut cũng đang được sử dụng. Với ràng buộc tài khoản, có thể áp dụng đồng thời nhiều ràng buộc cho một tài khoản.
seeds, bump
Các ràng buộc seeds và bump được dùng để kiểm tra một tài khoản nhất định có phải là PDA được dẫn xuất từ chương trình hiện đang thực thi, các seed và bump nếu được cung cấp hay không:
#[account(seeds = <seeds>, bump)]#[account(seeds = <seeds>, bump, seeds::program = <expr>)]#[account(seeds = <seeds>, bump = <expr>)]#[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]
Nếu không cung cấp bump, Anchor sẽ sử dụng bump chuẩn. Có thể dùng Seeds::program = <expr> để dẫn xuất PDA từ một chương trình khác với chương trình hiện đang thực thi.
Trong chương trình fanout của Helium, ràng buộc seeds kiểm tra xem văn bản “metadata”, khóa token_metadata_program, khóa membership_collection và văn bản “edition” có phải là các seed được dùng để dẫn xuất PDA này hay không. Ràng buộc seeds::program đảm bảo token_metadata_program được dùng để dẫn xuất PDA thay vì chương trình hiện tại:
...
#[account(
mut,
seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), membership_collection.key().as_ref()],
seeds::program = token_metadata_program.key(),
bump,
)]
pub collection_metadata: UncheckedAccount<'info>,
...token::mint, token::authority
Các ràng buộc token::mint và token::authority được định nghĩa như sau:
#[account(token::mint = <target account>, token::authority = <target account>)]#[account(token::mint = <target account>, token::authority = <target account>, token::token_program = <target account>)]
Các ràng buộc token mint và authority được dùng để xác minh địa chỉ mint và authority của TokenAccount. Có thể dùng các ràng buộc này để kiểm tra hoặc kết hợp với ràng buộc init nhằm tạo tài khoản token với địa chỉ mint và authority đã cho. Khi dùng để kiểm tra, chỉ cần chỉ định một tập con các ràng buộc.
Trong ngữ cảnh chương trình của Helium, các ràng buộc này được dùng để kiểm tra mint của associated_token có bằng membership_mint hay không và authority của token có được đặt thành staker hay không:
...
#[account(
mut,
associated_token::mint = membership_mint,
associated_token::authority = staker,
)]
pub from_account: Box<Account<'info, TokenAccount>>,
...init, payer, space
Ở thời điểm này, nên chuyển tiếp một chút trong mã để phân tích các ràng buộc init, payer và space. Ràng buộc init được định nghĩa là [#account(init, payer = <target_account>, space = <num_bytes>)]. Ràng buộc này tạo tài khoản thông qua CPI đến System Program và khởi tạo tài khoản bằng cách thiết lập discriminator. Thao tác này sẽ đánh dấu tài khoản là có thể thay đổi và loại trừ lẫn nhau với mut. Với các tài khoản lớn hơn 10 Kibibyte, hãy dùng #[account(zero)].
Ràng buộc init phải được dùng cùng một số ràng buộc bổ sung. Nó yêu cầu ràng buộc payer để chỉ định tài khoản sẽ thanh toán cho việc tạo tài khoản. Nó cũng yêu cầu System Program phải tồn tại trong struct và được gọi là system_program. Ràng buộc space cũng phải được định nghĩa. Trong phần Không gian tài khoản, chúng ta sẽ tìm hiểu sâu hơn về ràng buộc này và các yêu cầu không gian.
Đối với chương trình fanout của Helium, lệnh init tạo một tài khoản mới. payer được đặt làm payer, đã được thiết lập trước đó trong struct dưới dạng pub payer: Signer<'info>. Không gian của tài khoản được đặt bằng kích thước của FanoutVoucherV0, cộng thêm 8 byte cho discriminator và thêm 61 byte không gian:
...
#[account(
init,
payer = payer,
space = 60 + 8 + std::mem::size_of::<FanoutVoucherV0>() + 1,
seeds = ["fanout_voucher".as_bytes(), mint.key().as_ref()],
bump,
)]
pub voucher: Box<Account<'info, FanoutVoucherV0>>,
...init_if_needed
Ràng buộc init_if_needed được định nghĩa là #[account(init_if_nedded, payer = <target_Account>)] hoặc #[account(init)if_needed, payer = <target_account>, space = <num_bytes>)]. Ràng buộc này có chức năng hoàn toàn giống init. Tuy nhiên, nó chỉ chạy nếu tài khoản chưa tồn tại. Nếu tài khoản đã tồn tại, init_if_needed vẫn xác minh rằng mọi ràng buộc khởi tạo đều được đáp ứng, chẳng hạn tài khoản được cấp phát đúng dung lượng hoặc có đúng seed trong trường hợp PDA.
Cần thận trọng khi sử dụng init_if_needed vì tính năng này được kiểm soát bằng feature flag do có các rủi ro tiềm ẩn. Để bật tính năng, hãy import anchor-lang với cargo feature init-if-needed. Khi sử dụng init_if_needed, việc bảo vệ khỏi các cuộc tấn công khởi tạo lại là rất quan trọng. Nhà phát triển phải đảm bảo mã có các bước kiểm tra để ngăn tài khoản bị đặt lại về trạng thái ban đầu sau khi đã khởi tạo, trừ khi đây là hành vi chủ đích. Giữ đường dẫn thực thi lệnh đơn giản được xem là phương pháp hay nhất để giảm thiểu các cuộc tấn công này. Hãy cân nhắc chia các lệnh thành một lệnh để khởi tạo và các lệnh còn lại cho những thao tác tiếp theo.
Chương trình Fanout của Helium sử dụng ràng buộc init_if_needed để khởi tạo recipient_account nếu tài khoản chưa tồn tại:
...
#[account(
init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = recipient,
)]
pub receipt_account: Box<Account<'info, TokenAccount>>,
...constraint
Ràng buộc constraint được định nghĩa là #[account(constraint = <expr>)] hoặc #[account(constraint = <expr> @ <custom_error>)]. Nó kiểm tra xem biểu thức được cung cấp có cho kết quả true hay không. Ràng buộc này hữu ích khi không có ràng buộc nào khác phù hợp với trường hợp sử dụng dự kiến. Nó cũng hỗ trợ lỗi tùy chỉnh qua annotation @.
Chương trình Fanout sử dụng constraint để kiểm tra supply của mint có được đặt bằng 0 hay không:
...
#[account(
mut,
constraint = mint.supply == 0,
mint::decimals = 0,
mint::authority = voucher,
mint::freeze_authority = voucher,
)]
pub mint: Box<Account<'info, Mint>>,
...mint::authority, mint::decimals, mint::freeze_authority
Trong đoạn mã trên, các ràng buộc mint::decimals, mint::authority và mint::freeze_authority được dùng để kiểm tra số chữ số thập phân của mint có được đặt bằng 0 hay không, đồng thời voucher có authority và freeze authority hay không.
Để làm rõ ngữ cảnh, các ràng buộc mint::authority, mint::decimals và mint::freeze_authority được định nghĩa như sau:
#[account(mint::authority = <target account>, mint::decimals = <expr>)]#[account(mint::authority = <target account>, mint::decimals = <expr>, mint::freeze_authority = <target account>)]
Ý nghĩa của các ràng buộc này khá rõ ràng — chúng lần lượt kiểm tra authority, số chữ số thập phân và freeze authority của token. Có thể dùng chúng để kiểm tra hoặc kết hợp với init nhằm tạo một tài khoản mint với số chữ số thập phân và mint authority đã cho. Freeze authority hoàn toàn không bắt buộc khi dùng cùng init. Khi dùng để kiểm tra, chỉ cần chỉ định một tập con các ràng buộc này.
Không gian tài khoản
Mọi tài khoản mà một chương trình sử dụng trên Solana đều phải được cấp phát không gian lưu trữ rõ ràng. Việc cấp phát này rất quan trọng để quản lý tài nguyên hiệu quả, bảo đảm chỉ dữ liệu cần thiết mới được lưu trữ trên chuỗi. Điều này cũng giúp chi phí giao dịch dễ dự đoán hơn và tăng hiệu quả thực thi giao dịch — các giao dịch có thể được xử lý mà không cần cấp phát động hoặc thay đổi kích thước vùng lưu trữ của tài khoản. Ngoài ra, việc cấp phát trước bảo đảm tài khoản có đủ không gian để lưu mọi dữ liệu cần thiết, qua đó giảm nguy cơ giao dịch thất bại hoặc phát sinh lỗ hổng bảo mật.
Xác định kích thước biến
Các kiểu dữ liệu khác nhau có yêu cầu về không gian khác nhau. Dưới đây là hướng dẫn đơn giản để ước tính không gian cần thiết:
- Kiểu cơ bản: các kiểu dữ liệu đơn giản như bool, u8, i8, u16, i16, u32, i32, u64, i64, u128 và i128 đều có kích thước cố định. Kích thước dao động từ 1 byte cho
bool(mặc dù chỉ sử dụng 1 bit) đến 16 byte chou128/i128 - Mảng: với mảng
[T;amount], không gian được tính bằng kích thước củaTnhân với số phần tử (tức làamount). Ví dụ, một mảng gồm 16u16sẽ cần 32 byte - Pubkey: khóa công khai luôn chiếm 32 byte trên Solana
- Kiểu động:
StringvàVec<T>cần được cân nhắc kỹ. Cả hai đều cần 4 byte để lưu độ dài, cộng thêm không gian cho nội dung thực tế. Cần cấp phát đủ không gian cho kích thước tối đa dự kiến. VớiString, không gian này bằng 4 byte cộng với độ dài củaStringtính theo byte. VớiVec<T>, không gian này bằng 4 byte cộng với không gian của kiểu đã cho nhân với số phần tử dự kiến (tức là 4 + space(T) * amount) - Option và enum: kiểu
Option<T>cần 1 byte cộng với không gian dành cho kiểuT. Enum cần 1 byte cho bộ phân biệt enum, cộng với không gian cần thiết cho biến thể lớn nhất - Số dấu phẩy động: các kiểu như
f32vàf64lần lượt chiếm 4 và 8 byte. Hãy thận trọng với các giá trị NaN, vì chúng có thể khiến quá trình tuần tự hóa thất bại
Hướng dẫn sau chỉ áp dụng cho các tài khoản không sử dụng cơ chế tuần tự hóa zero-copy. Tuần tự hóa zero-copy được biểu thị bằng thuộc tính #[zero_copy]. Cơ chế này tận dụng thuộc tính repr(c) để bố trí bộ nhớ, cho phép ép kiểu con trỏ trực tiếp để truy cập dữ liệu. Đây là cách hiệu quả để làm việc với dữ liệu trên chuỗi mà không phải chịu chi phí của quá trình giải tuần tự truyền thống. #[zero_copy] là cách viết tắt để áp dụng #[derive(Copy, Clone)], #[derive(bytemuck::Zeroable)], #[derive(bytemuck::Pod)] và #[repr(C)]. Các thuộc tính này bảo đảm tài khoản có thể được xử lý an toàn như một chuỗi byte và tương thích với cơ chế giải tuần tự zero-copy. Giải tuần tự zero-copy rất quan trọng với các tài khoản cần kích thước đặc biệt lớn — những tài khoản không thể được tuần tự hóa hiệu quả bằng Borsh hoặc cơ chế tuần tự hóa mặc định của Anchor mà không chạm giới hạn heap hoặc stack.
Bộ phân biệt nội bộ của Anchor
Nhà phát triển phải cộng thêm 8 vào ràng buộc space cho bộ phân biệt nội bộ của Anchor. Ví dụ, nếu một tài khoản cần 32 byte thì tài khoản đó sẽ cần 40 byte. Việc đặt ràng buộc không gian thành space = 8 + <account size> được xem là phương pháp hay, vì cho thấy bộ phân biệt nội bộ đã được tính đến khi xác định không gian.
Ngoài ra, bộ phân biệt là một mã định danh duy nhất dùng để phân biệt các kiểu dữ liệu khác nhau. Nó hữu ích khi phân biệt các loại cấu trúc dữ liệu tài khoản tại thời gian chạy. Bộ phân biệt cũng được dùng làm tiền tố cho các chỉ thị, giúp định tuyến chúng đến phương thức tương ứng trong một chương trình Anchor. Bộ phân biệt là một mảng 8 byte đại diện cho mã định danh duy nhất của kiểu dữ liệu.
Tính toán không gian ban đầu
Việc tính toán không gian ban đầu cần thiết cho một tài khoản có thể khá khó khăn. Macro InitSpace thêm một hằng số INIT_SPACE có thể được sử dụng trên cấu trúc của tài khoản. Cấu trúc không nhất thiết phải chứa macro #[account] để tạo hằng số này. Tài liệu Anchor cung cấp ví dụ sau:
#[account]
#[derive(InitSpace)]
pub struct ExampleAccount {
pub data: u64,
// max_len represents the length of the structure
#[max_len(50)]
pub string_one: String,
#[max_len(10, 5)]
pub nested: Vec<Vec<u8>>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
#[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)]
pub data: Account<'info, ExampleAccount>,
}Trong ví dụ này, ExampleAccount::INIT_SPACE tự động tính toán không gian cần thiết cho ExampleAccount. Nó cũng tính đến bộ phân biệt nội bộ của Anchor khi xác định không gian.
Thay đổi kích thước không gian chương trình
Ràng buộc realloc được dùng để điều chỉnh không gian của một tài khoản chương trình khi bắt đầu chỉ thị. Tài khoản phải có thể thay đổi (tức là mut), và ràng buộc này áp dụng cho kiểu Account hoặc AccountLoader. Nó được định nghĩa là #[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)]. Khi tăng độ dài dữ liệu tài khoản, lamport được chuyển từ realloc::payer sang tài khoản chương trình để duy trì trạng thái miễn tiền thuê. Nếu độ dài dữ liệu giảm, lamport được chuyển ngược từ tài khoản chương trình về realloc::payer. Ràng buộc realloc::zero quyết định liệu vùng nhớ mới cấp phát có cần được khởi tạo bằng 0 hay không. Việc khởi tạo bằng 0 bảo đảm vùng nhớ mới sạch và không chứa dữ liệu còn sót lại hoặc không mong muốn.
Không nên sử dụng AccountInfo::realloc theo cách thủ công thay cho ràng buộc realloc. Nguyên nhân là không có các bước kiểm tra tại thời gian chạy để bảo đảm việc cấp phát lại không vượt quá giới hạn MAX_PERMITTED_DATA_INCREASE, điều có thể dẫn đến ghi đè dữ liệu trong các tài khoản khác. Ràng buộc này cũng kiểm tra và ngăn việc cấp phát lại nhiều lần trong cùng một chỉ thị.
Ví dụ:
#[derive(Accounts)]
pub struct Data {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [b"data"],
bump,
realloc = 8 + std::mem::size_of::<()>() + 48,
realloc::payer = payer,
realloc::zero = false
)]
pub update_account: Account<'info, NewData>,
system_program: Program<'info, System>,
}Lỗi
Xử lý lỗi là một phần thiết yếu trong quá trình phát triển chương trình. Đây là cơ chế xác định và quản lý các lỗi có thể làm dừng quá trình thực thi chương trình. Việc xử lý lỗi phải có chủ đích và được lên kế hoạch để bảo đảm chất lượng, khả năng bảo trì và chức năng của mã. Anchor đơn giản hóa việc này bằng các cơ chế xử lý lỗi mạnh mẽ. Lỗi trong chương trình Anchor có thể được chia thành AnchorErrors và lỗi không thuộc Anchor. Phần này sẽ tập trung vào AnchorErrors vì lỗi không thuộc Anchor bao gồm rất nhiều lỗi Rust. Với lỗi không thuộc Anchor, bạn nên tham khảo chương về Xử lý lỗi trong Rust Book và phần Xử lý lỗi trong Rust By Example.
struct sau định nghĩa AnchorError:
pub struct AnchorError {
pub error_name: String,
pub error_code_number: u32,
pub error_msg: String,
pub error_origin: Option<ErrorOrigin>,
pub compared_values: Option<ComparedValues>,
}Các trường này tương đối dễ hiểu. error_name là một chuỗi đại diện cho tên lỗi. error_code_number là mã định danh duy nhất của lỗi (tức là một số nguyên không dấu duy nhất chiếm 32 bit không gian). error_msg là thông báo mô tả giải thích lỗi. error_origin là trường tùy chọn cung cấp thông tin về nơi phát sinh lỗi, chẳng hạn như tệp nguồn hoặc tài khoản liên quan. compared_values là trường tùy chọn nêu chi tiết các giá trị đang được so sánh khi lỗi xảy ra. Trường này cực kỳ hữu ích khi gỡ lỗi.
AnchorError triển khai một phương thức ghi nhật ký. Nhật ký bao gồm thông tin về nguồn gốc của lỗi và các giá trị liên quan, hữu ích cho việc gỡ lỗi và khắc phục lỗi. Phương thức này sử dụng error_origin và compared_values để cung cấp thông tin đó.
AnchorError có thể được chia nhỏ thành lỗi nội bộ của Anchor và lỗi tùy chỉnh. Anchor có một danh sách dài các mã lỗi nội bộ có thể được trả về. Những lỗi nội bộ này không dành cho người dùng. Tuy nhiên, việc biết mối liên hệ giữa mã lỗi và nguyên nhân vẫn rất hữu ích. Chúng thường được phát sinh khi một ràng buộc bị vi phạm. Mã lỗi nội bộ tuân theo lược đồ sau:
- >= 100 là mã lỗi chỉ thị
- >= 1000 là mã lỗi IDL
- >= 2000 là mã lỗi ràng buộc
- >= 3000 là mã lỗi tài khoản
- >= 4100 là mã lỗi khác
- = 5000 là mã lỗi không còn được dùng.
Lỗi tùy chỉnh bắt đầu từ ERROR_CODE_OFFSET (tức là 6000).
Nhà phát triển có thể triển khai lỗi tùy chỉnh của riêng mình bằng thuộc tính error_code. Thuộc tính này được dùng trên một enum, và các biến thể của enum có thể được sử dụng làm lỗi trong toàn bộ chương trình. Có thể thêm thông báo cho từng biến thể. Máy khách có thể hiển thị thông báo này nếu lỗi xảy ra. Ví dụ:
#[error_code]
pub enum HeliusError {
#[msg(“This RPC provider is too good”)]
RPCTooGood
}Có thể sử dụng các macro err! và error! để phát sinh những lỗi này. Ví dụ:
require!(rpc.speed > 9000, HeliusError::RPCTooGood);Cần lưu ý rằng có nhiều macro require để lựa chọn. Phần lớn các macro này liên quan đến những giá trị không phải khóa công khai. Ví dụ, macro require_gte kiểm tra xem giá trị không phải khóa công khai đầu tiên có lớn hơn hoặc bằng giá trị không phải khóa công khai thứ hai hay không:
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
require_gte!(ctx.accounts.data.data, 1);
ctx.accounts.data.data = data;
Ok(());
}Ngoài ra còn có một số điểm cần lưu ý khi so sánh khóa công khai. Ví dụ, nhà phát triển nên sử dụng require_keys_eq thay cho require_eq vì lựa chọn sau tốn kém hơn.
Mọi chương trình đều trả về một ProgramError. Kiểu lỗi này có một trường dành riêng cho số lỗi tùy chỉnh, được Anchor sử dụng để lưu mã lỗi nội bộ và mã lỗi tùy chỉnh. Tuy nhiên, đây chỉ là một con số duy nhất nên không thực sự hữu ích. Cơ chế ghi nhật ký bằng AnchorError của Anchor đã đề cập ở trên hữu ích hơn nhiều. Các máy khách Anchor được thiết kế để phân tích những nhật ký này. Tuy nhiên, trong một số trường hợp, việc này có thể gặp khó khăn. Chẳng hạn, việc truy xuất nhật ký cho các giao dịch đã xử lý khi tắt kiểm tra preflight không đơn giản. Tương tự, Anchor cũng sử dụng cơ chế dự phòng cho các chương trình không thuộc Anchor hoặc chương trình cũ không ghi AnchorError theo cách tiêu chuẩn. Trong trường hợp này, Anchor kiểm tra xem số lỗi do giao dịch trả về có tương ứng với mã lỗi nội bộ của Anchor hoặc số lỗi được định nghĩa trong IDL của chương trình hay không. Khi tìm thấy kết quả khớp, Anchor bổ sung thông tin để cung cấp thêm ngữ cảnh cho lỗi. Khi có thể, Anchor cũng cố gắng phân tích stack lỗi của chương trình để truy ngược về nguyên nhân ban đầu. ProgramError đóng vai trò là kiểu lỗi nền tảng, được tăng cường tính hữu dụng nhờ cơ chế ghi nhật ký và phân tích của Anchor để cung cấp thông tin lỗi chi tiết.
Lệnh gọi liên chương trình (CPI)
Lệnh gọi liên chương trình (CPI) đã được nhắc đến xuyên suốt bài viết này, vì vậy cần có một phần riêng dành cho chúng. CPI là nền tảng cho khả năng kết hợp của Solana vì chúng cho phép các chương trình gọi trực tiếp chương trình khác. Có thể xem điều này như biến hệ sinh thái Solana thành một API rộng lớn và liên kết chặt chẽ dành cho nhà phát triển. Để ngắn gọn, bạn nên đọc tài liệu Anchor về CPI, trong đó có một ví dụ hữu ích về CPI với chương trình con rối và người điều khiển con rối.
Dù vậy, CPI có thể được định nghĩa là một lệnh gọi từ chương trình này sang chương trình khác, nhắm đến một chỉ thị cụ thể trong chương trình được gọi. Chương trình gọi sẽ tạm dừng cho đến khi chương trình được gọi xử lý xong chỉ thị.
Nâng quyền
CPI cho phép chương trình gọi mở rộng đặc quyền của bên ký sang chương trình được gọi. Việc mở rộng đặc quyền rất tiện lợi nhưng cũng có thể cực kỳ nguy hiểm. Nếu CPI vô tình nhắm đến một chương trình độc hại, chương trình đó sẽ có cùng đặc quyền với bên gọi. Anchor giảm thiểu rủi ro này bằng hai biện pháp bảo vệ:
- Kiểu
Program<’info, T>bảo đảm tài khoản được chỉ định khớp với chương trình dự kiến (T) - Ngay cả khi không sử dụng kiểu
Program, hàm CPI được tạo tự động vẫn xác minh rằng đối sốcpi_programtương ứng với chương trình dự kiến
Thực thi CPI
Một chương trình có thể thực thi CPI bằng invoke hoặc invoke_signed từ crate solana_program. Anchor cũng cung cấp struct CpiContext để chỉ định các đầu vào không phải đối số cho CPI.
invoke
Hàm invoke được sử dụng khi PDA không bắt buộc phải đóng vai trò chữ ký. Trong trường hợp này, môi trường chạy mở rộng chữ ký ban đầu từ chương trình gọi sang chương trình được gọi. Hàm được định nghĩa như sau:
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> ProgramResultViệc gọi một chương trình khác bao gồm tạo Instruction chứa ID chương trình, dữ liệu chỉ thị cho chương trình được gọi và danh sách tài khoản mà chương trình được gọi sẽ truy cập. Một chương trình chỉ nhận các giá trị AccountInfo từ môi trường chạy tại điểm vào chương trình. Mọi tài khoản mà chương trình được gọi cần để thực hiện lệnh gọi đều phải được chương trình gọi cung cấp và đưa vào. Ví dụ, nếu chương trình được gọi cần sửa đổi một tài khoản cụ thể, chương trình gọi phải đưa tài khoản đó vào danh sách các giá trị AccountInfo. Điều này cũng áp dụng cho ID chương trình của chương trình được gọi (tức là bên gọi phải chỉ định rõ chương trình nào đang được gọi bằng cách đưa ID chương trình của chương trình được gọi vào).
Instruction thường được tạo bên trong chương trình gọi, mặc dù nó cũng có thể được giải tuần tự từ đầu ra bên ngoài.
Toàn bộ giao dịch sẽ thất bại ngay lập tức nếu chương trình được gọi gặp lỗi hoặc bị hủy. Nguyên nhân là hàm invoke chỉ trả về khi thành công. Sử dụng các hàm set_return_data hoặc get_return_data để trả về dữ liệu dưới dạng kết quả của CPI. Lưu ý rằng kiểu được trả về phải triển khai các trait AnchorSerialize và AnchorDeserialize. Ngoài ra, có thể để chương trình được gọi ghi vào một tài khoản chuyên dụng nhằm lưu trữ dữ liệu
Mặc dù một chương trình có thể tự gọi chính nó theo cách đệ quy, các lệnh gọi đệ quy gián tiếp (tức là tái nhập) bởi một chương trình khác sẽ ngay lập tức khiến giao dịch thất bại.
Ví dụ, nếu có một chương trình chuyển token qua CPI, chúng ta sẽ sử dụng invoke như sau:
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
require_gte!(ctx.accounts.data.data, 1);
ctx.accounts.data.data = data;
Ok(());
}invoke_signed
invoke_signed được sử dụng cho các CPI cần PDA làm bên ký. Nó cho phép chương trình gọi hành động thay mặt PDA bằng cách cung cấp các seed cần thiết để dẫn xuất PDA đó:
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> ProgramResultPDA cũng có thể đóng vai trò bên ký trong CPI. Môi trường chạy sẽ sử dụng các seed được cung cấp và program_id của chương trình gọi để tạo PDA nội bộ thông qua create_program_address. Sau đó, PDA được xác thực dựa trên các địa chỉ được truyền cùng chỉ thị (tức là account_infos) để xác nhận đó là một bên ký hợp lệ.
Với hàm này, một lệnh gọi có thể ký thay mặt một hoặc nhiều PDA do chương trình gọi kiểm soát. Điều này cho phép chương trình được gọi tương tác với các tài khoản đã cho như thể chúng đã được ký bằng mật mã. signer_seeds bao gồm các lát cắt seed dùng để dẫn xuất PDA. Trong quá trình gọi, môi trường chạy coi mọi tài khoản khớp trong account_info là “đã ký”. Ví dụ, nếu có một chương trình tạo tài khoản cho PDA, chúng ta sẽ gọi invoke_signed như sau:
invoke_signed(
&system_instruction::create_account(
&payer.key,
&vault_pda.key,
lamports,
vault_size,
&program_id,
),
&[
payer.clone(),
vault_pda.clone(),
],
&[
&[
b"vault",
payer.key.as_ref(),
&[vault_bump_seed],
],
]
)?;CpiContext
Anchor cung cấp CpiContext như một cách đơn giản hơn để tạo CPI thay vì sử dụng invoke hoặc invoke_signed. Struct này chỉ định các đầu vào không phải đối số cần thiết cho CPI, mô phỏng sát chức năng của Context. Nó cung cấp thông tin về các tài khoản cần thiết cho chỉ thị, mọi tài khoản bổ sung có liên quan, ID chương trình được gọi và các seed để dẫn xuất PDA nếu cần. Sử dụng CpiContext::new cho CPI không có PDA và CpiContext::new_with_signer cho CPI cần PDA làm bên ký.
CpiContext được định nghĩa như sau, trong đó T là kiểu generic bao hàm mọi đối tượng triển khai các trait ToAccountMetas và ToAccountInfos<’info>:
pub struct CpiContext<'a, 'b, 'c, 'info, T>where
T: ToAccountMetas + ToAccountInfos<'info>,{
pub accounts: T,
pub remaining_accounts: Vec>,
pub program: AccountInfo<'info>,
pub signer_seeds: &'a [&'b [&'c [u8]]],
}Accounts là một kiểu generic, cho phép dùng mọi đối tượng triển khai các trait ToAccountMetas và ToAccountInfos<’info>. Điều này được hỗ trợ bởi macro thuộc tính #[derive(Accounts)] để giúp tổ chức mã và tăng độ an toàn kiểu.
CpiContext đơn giản hóa việc gọi các chương trình Anchor và không thuộc Anchor. Với chương trình Anchor, chỉ cần khai báo một dependency trong tệp Cargo.toml của dự án và sử dụng module cpi do Anchor tạo:
[dependencies]
callee = { path = "../callee", features = ["cpi"]}Việc đặt features = [“cpi”] cấp cho chương trình quyền truy cập module callee::cpi. Anchor tự động tạo module này và cung cấp các chỉ thị của chương trình dưới dạng hàm Rust. Hàm này nhận một CpiContext và mọi dữ liệu chỉ thị bổ sung, theo định dạng tương tự các hàm chỉ thị thông thường trong chương trình Anchor nhưng thay Context bằng CpiContext. Module cpi cũng cung cấp các struct tài khoản cần thiết để gọi chỉ thị.
Ví dụ, nếu chương trình được gọi có một chỉ thị tên là hello_there và cần các tài khoản cụ thể được định nghĩa trong struct GeneralKenobi, hãy gọi chỉ thị đó như sau:
// We assume "jedi" is an Anchor program with a published crate
use jedi::cpi::accounts::GeneralKenobi;
use jedi::cpi::hello_there;
use anchor_lang::prelude::*;
#[program]
pub mod fight_on_utapau {
use super::*;
pub fn call_hello_there(ctx: Context<CallGeneralKenobi>, data: GreetingParams) -> Result<()> {
let cpi_accounts = GeneralKenobi {
jedi: ctx.accounts.jedi.to_account_info(),
// Other account infos needed for the GeneralKenobi struct go here
};
let cpi_program = ctx.accounts.jedi_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
hello_there(cpi_ctx, data);
}
#[derive(Accounts)]
pub struct CallGeneralKenobi<'info> {
pub jedi: UncheckedAccount<'info>,
pub jedi_program: Program<'info, Jedi>,
// Other required accounts
}
pub struct GreetingParams {
// Params required for the hello_there function
}Trong module fight_on_utapau, CPI được thực hiện bằng CpiContext. Hàm call_hello_there được thiết kế để tương tác với chương trình jedi. Hàm này tạo một CpiContext với thông tin tài khoản cần thiết cho struct tài khoản GeneralKenobi từ chương trình jedi và thông tin tài khoản chương trình của jedi. Ngữ cảnh này gọi hello_there, đồng thời truyền mọi tham số bổ sung bắt buộc được chỉ định bởi struct GreetingParams. Struct CallGeneralKenobi định nghĩa các tài khoản cần thiết cho hàm này, giúp đơn giản hóa quy trình.
Cuối cùng, khi gọi chỉ thị từ các chương trình không thuộc Anchor, hãy kiểm tra xem bên bảo trì chương trình có phát hành crate riêng với các hàm hỗ trợ để gọi chương trình của họ hay không. Nếu không có hàm hỗ trợ cho chương trình có chỉ thị cần gọi, hãy dùng invoke và invoke_signer làm phương án dự phòng để tổ chức và chuẩn bị CPI.
Địa chỉ dẫn xuất từ chương trình (PDA)
Hãy nhớ rằng PDA nằm ngoài đường cong và không có khóa riêng tư tương ứng. Chúng cho phép các chương trình ký chỉ thị và cho phép nhà phát triển xây dựng các cấu trúc tương tự hashmap trên chuỗi. PDA được dẫn xuất bằng một danh sách seed tùy chọn, một bump seed và ID chương trình.
Nhắc lại, các ràng buộc sau được dùng để kiểm tra rằng một tài khoản nhất định là PDA được dẫn xuất từ chương trình hiện đang thực thi, các seed và bump nếu được cung cấp:
#[account(seeds = <seeds>, bump)]#[account(seeds = <seeds>, bump, seeds::program = <expr>)]#[account(seeds = <seeds>, bump = <expr>)]#[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]
Nếu không cung cấp bump, Anchor sẽ sử dụng bump chuẩn. Seeds::program = <expr> có thể được dùng để dẫn xuất PDA từ một chương trình khác với chương trình đang thực thi.
Việc sử dụng các ràng buộc seeds và bump giúp đơn giản hóa quá trình dẫn xuất:
#[derive(Accounts)]
struct ExamplePDA<'info> {
#[account(seeds = [b"example"], bump)]
pub example_pda: Account<'info, AccountType>,
}Ở đây, ràng buộc seeds được dùng để dẫn xuất PDA. Anchor tự động xác minh rằng tài khoản được truyền vào chỉ thị khớp với PDA được dẫn xuất từ các seed. Anchor mặc định dùng bump chuẩn khi ràng buộc bump được sử dụng mà không có giá trị cụ thể.
Anchor cũng cho phép seed động dựa trên các trường tài khoản khác hoặc dữ liệu chỉ thị. Việc này được thực hiện bằng cách tham chiếu các trường khác trong struct hoặc sử dụng macro thuộc tính #[instruction(...)] để đưa dữ liệu chỉ thị đã giải tuần tự vào. Ví dụ, trong struct sau, example_pda bị ràng buộc phải sử dụng kết hợp một seed tĩnh, dữ liệu chỉ thị và khóa công khai của bên ký:
#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct ExamplePDA<'info> {
#[account(seeds = [b"example", signor.key().as_ref(), instruction_data.as_bytes()], bump)]
pub example_pda: Account<'info, AccountType>,
#[account(mut)]
pub signoooorrr: Signer<'info>
}Kết luận
Gọi Anchor là một framework mạnh mẽ vẫn chưa thể hiện hết năng lực của nó. Khả năng tinh gọn quy trình phát triển được thể hiện rõ qua các macro và trait mà Anchor sử dụng để giảm lượng code. Framework này có tài liệu được duy trì tốt cùng một hệ sinh thái phong phú gồm các hướng dẫn và crate liên quan. Anchor được đại đa số nhà phát triển Solana yêu thích và sử dụng.
Bài viết này là một hướng dẫn cực kỳ, cực kỳ toàn diện về cách phát triển program bằng Anchor. Bài viết đề cập đến việc cài đặt Anchor, sử dụng Solana Playground, cũng như tạo, build và triển khai một program Hello, World!. Sau đó, chúng ta đã tìm hiểu các phương pháp trừu tượng hóa hiệu quả của Anchor, cấu trúc của một program Anchor điển hình, cùng nhiều loại account và constraint hiện có. Bài viết cũng trình bày tầm quan trọng của việc phân bổ không gian account và xử lý lỗi. Cuối cùng, chúng ta tìm hiểu CPI và PDA. Đây chính là bài viết toàn diện nhất về Anchor — có mọi thứ bạn cần để bắt đầu phát triển program trên Solana ngay hôm nay.
Nếu đã đọc đến đây, cảm ơn bạn! Hãy nhập địa chỉ email bên dưới để không bỏ lỡ bất kỳ thông tin cập nhật nào về những điểm mới trên Solana. Sẵn sàng tìm hiểu sâu hơn? Tham gia Discord của chúng tôi để bắt đầu phát triển program Anchor.
Tài nguyên bổ sung
Bài viết liên quan
Đăng ký nhận tin từ Helius
Luôn cập nhật những thông tin mới nhất về phát triển Solana và nhận thông báo khi chúng tôi đăng bài


