BARU: Helius mengakuisisi Light Protocol
Pengantar Anchor
Blog/Pengembangan

Pengantar Anchor: Panduan Pemula untuk Membangun Program Solana

Developer Experience Engineer0xIchigo di X0xIchigo di LinkedIn0xIchigo di GitHub
Bacaan 46 menit

Terima kasih banyak kepada Noah, Mike, Jonas, Ryan, Prames, dan bl0ckpain yang telah meninjau artikel ini.

Apa yang dibahas dalam artikel ini?

Rust umumnya disebut sebagai lingua franca dalam pengembangan program Solana. Namun, Anchor lebih tepat disebut demikian karena sebagian besar pengembangan dengan Rust menggunakan framework ini. Anchor adalah framework yang terarah dan andal untuk membangun program Solana yang aman dengan cepat. Anchor menyederhanakan proses pengembangan dengan mengurangi boilerplate untuk berbagai aspek seperti (de)serialisasi account dan data instruction, menjalankan pemeriksaan keamanan penting, membuat library client secara otomatis, serta menyediakan lingkungan pengujian yang lengkap.

Artikel ini membahas cara mengembangkan program Anchor. Pembahasannya mencakup instalasi Anchor, penggunaan Solana Playground, serta pembuatan, build, dan deployment program Hello, World! sederhana. Setelah itu, kita akan mempelajari lebih dalam cara Anchor menyederhanakan proses pengembangan dengan membahas IDL, macro, struktur program Anchor, jenis dan constraint account, serta penanganan error. Kita juga akan membahas Cross-Program Invocation dan Program Derived Address secara singkat. Artikel ini akan memberikan semua yang perlu Anda ketahui untuk mulai menggunakan Anchor hari ini.

Pengetahuan Prasyarat

Artikel ini mengasumsikan Anda memahami model pemrograman Solana. Jika Anda baru mulai membangun di Solana, saya sarankan membaca artikel blog saya sebelumnya, Model Pemrograman Solana: Pengantar Pengembangan di Solana. 

Jangan khawatir jika Anda baru mengenal Rust—Anda tidak memerlukan pengetahuan tingkat lanjut untuk mulai mengembangkan dengan Anchor. Dokumentasi Anchor menjelaskan bahwa developer hanya perlu memahami dasar-dasar Rust (yaitu sembilan bab pertama dalam Rust Book). Saya menyarankan Anda menonton Panduan Bertahan Hidup dengan Rust untuk memahami konsep-konsep penting pemrograman Rust dengan baik. Memahami aturan memori, ownership, dan borrowing Rust juga sangat penting.

Untuk mempermudah proses belajar, saya menyarankan developer yang baru mengenal bahasa pemrograman tingkat rendah agar mempelajari berbagai konsep khusus pemrograman sistem yang sering tidak dibahas dalam materi Rust. Misalnya, pelajari topik seperti ukuran variabel, pointer, dan kebocoran memori. Saya juga merekomendasikan Rust By Example dan repositori saya tentang berbagai struktur data dan algoritma yang ditulis dalam Rust untuk melihat contoh penerapan Rust.

Ingin menggunakan TypeScript? Pelajari cara menulis program Solana dalam TypeScript menggunakan framework Poseidon untuk melakukan transpile TypeScript menjadi Rust dan menghasilkan program Anchor yang valid.

Artikel ini hanya berfokus pada pengembangan dengan Anchor. Kita tidak akan membahas cara mengembangkan program dalam Native Rust, dan artikel ini juga tidak mengasumsikan Anda memiliki pengetahuan tersebut. Selain itu, artikel ini tidak akan membahas pengembangan sisi client dengan Anchor—cara menguji dan berinteraksi dengan program Anchor melalui TypeScript akan dibahas dalam artikel mendatang.

Dengan demikian, mari mulai menggunakan Anchor!

Menginstal Anchor

Menyiapkan Anchor hanya memerlukan beberapa langkah sederhana untuk menginstal alat dan package yang diperlukan. Bagian ini membahas instalasi alat dan package tersebut (yaitu Rust, Solana Tool Suite, Yarn, dan Anchor Version Manager).

Menginstal Rust

Rust dapat diinstal dari situs web resmi Rust atau melalui command line:

Kode
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Menginstal Solana Tool Suite

Anchor juga memerlukan Solana Tool Suite. Rilis terbaru (1.17.16—saat artikel ini ditulis) dapat diinstal dengan perintah berikut untuk macOS dan Linux:

Kode
sh -c "$(curl -sSfL https://release.solana.com/v1.17.16/install)"

Pengguna Windows dapat menginstal Solana Tool Suite dengan perintah berikut:

Kode
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"

Namun, Anda sangat disarankan untuk menggunakan Windows Subsystem for Linux (WSL). Dengan WSL, Anda dapat menjalankan lingkungan Linux di komputer Windows tanpa melakukan dual boot atau menjalankan mesin virtual terpisah. Jika memilih cara ini, ikuti petunjuk instalasi untuk Linux (yaitu perintah curl).

Developer juga dapat mengganti v1.17.16 dengan tag rilis versi yang ingin diunduh. Anda juga dapat menggunakan nama channel stable, beta, atau edge. Setelah instalasi selesai, jalankan solana –-version untuk memastikan versi solana yang diinginkan telah terinstal.

Menginstal Yarn

Anchor juga memerlukan Yarn. Yarn dapat digunakan melalui Corepack, yang disertakan dalam semua rilis resmi Node.js mulai dari Node.js 14.9/16.9. Namun, selama masih dalam tahap eksperimental, Corepack harus diaktifkan secara manual. Karena itu, kita perlu menjalankan corepack enable sebelum menggunakannya. Beberapa distributor pihak ketiga mungkin tidak menyertakan Corepack secara default. Oleh karena itu, Anda mungkin perlu menjalankan npm install -g corepack sebelum corepack enable.

Menginstal Anchor Menggunakan AVM

Dokumentasi Anchor menyarankan instalasi Anchor melalui Anchor Version Manager (AVM). AVM menyederhanakan pengelolaan dan pemilihan beberapa instalasi binary anchor-cli. Hal ini mungkin diperlukan untuk menghasilkan build yang dapat diverifikasi, atau untuk menggunakan versi berbeda di berbagai program. AVM dapat diinstal menggunakan Cargo dengan perintah: cargo install --git [https://github.com/coral-xyz/anchor](https://github.com/coral-xyz/anchor) avm --locked --force. Kemudian, instal dan gunakan versi terbaru:

Kode
avm install latest
avm use latest

# Verify the installation
avm --version

Untuk melihat daftar versi anchor-cli yang tersedia, gunakan perintah avm list. Developer dapat menggunakan avm use <version> untuk memakai versi tertentu. Versi tersebut akan tetap digunakan hingga diubah. Developer dapat menghapus instalasi versi tertentu menggunakan perintah avm uninstall <version>.

Menginstal Anchor Menggunakan Binary dan Melakukan Build dari Source

Di Linux, binary Anchor tersedia melalui package npm @coral-xyz/anchor-cli. Saat ini, hanya Linux x86_64 yang didukung. Karena itu, developer harus melakukan build dari source untuk sistem operasi lain. Developer dapat menggunakan Cargo untuk menginstal CLI secara langsung. Contohnya:

Kode
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --locked

Ubah argumen --tag untuk menginstal versi Anchor lain yang diinginkan. Jika instalasi Cargo gagal, Anda mungkin perlu menginstal dependency tambahan. Contohnya, di Ubuntu:

Kode
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev

Setelah itu, developer dapat memverifikasi instalasi Anchor dengan perintah anchor --version.

Solana Playground

Sebagai alternatif, developer dapat mulai menggunakan Anchor melalui Solana Playground (Solpg). Solana Playground adalah IDE berbasis browser yang memudahkan pengembangan, pengujian, dan deployment program Solana secara cepat. 

Saat pertama kali menggunakan Solana Playground, developer harus membuat Playground Wallet. Klik indikator status merah berlabel Tidak terhubung di kiri bawah layar. Modal berikut akan muncul:

Sebaiknya simpan file keypair wallet sebagai cadangan sebelum mengeklik Lanjutkan. Playground Wallet disimpan di penyimpanan lokal browser. Menghapus cache browser akan menghapus wallet tersebut. 

Klik Lanjutkan untuk membuat wallet devnet yang siap digunakan di IDE.

Untuk mengisi saldo wallet, developer dapat menjalankan perintah solana airdrop <amount> di terminal Playground, dengan <amount> diganti dengan jumlah SOL devnet yang diinginkan. Sebagai alternatif, kunjungi faucet ini untuk mendapatkan SOL devnet. Saya menyarankan Anda membaca panduan cara mendapatkan SOL devnet berikut.

Perhatikan bahwa Anda mungkin mengalami error berikut:

Kode
Error: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer funds

Hal ini sering terjadi karena faucet devnet kehabisan dana dan/atau Anda meminta terlalu banyak SOL. Batas saat ini adalah 5 SOL, yang lebih dari cukup untuk men-deploy program ini. Karena itu, sebaiknya minta 5 SOL dari faucet atau jalankan perintah solana airdrop 5. Meminta jumlah yang lebih kecil secara bertahap berpotensi memicu pembatasan laju.

Hello, World!

Program Hello, World! dianggap sebagai pengantar yang sangat baik untuk framework atau bahasa pemrograman baru. Kesederhanaannya membuat program ini mudah dipahami oleh developer dari semua tingkat keahlian. Program ini juga memperjelas struktur dan sintaks dasar model pemrograman baru tanpa memperkenalkan logika atau fungsi yang kompleks. Hello, World! dengan cepat menjadi program pemula yang cukup standar dalam pemrograman, sehingga wajar jika kita membuatnya sendiri dengan Anchor. Bagian ini membahas cara melakukan build dan deployment program Hello, World! menggunakan penyiapan Anchor lokal maupun Solana Playground.

Membuat Project Baru dengan Penyiapan Anchor Lokal

Membuat project baru setelah Anchor terinstal sangat mudah:

Kode
anchor init hello-world
cd hello-world

Perintah-perintah ini akan menginisialisasi project Anchor baru bernama hello-world, lalu membuka direktorinya. Di dalam direktori tersebut, buka hello-world/programs/hello-world/src/lib.rs. File ini berisi kode awal berikut:

Kode
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 telah menyiapkan sejumlah file dan direktori untuk kita. Yaitu,

  • app kosong untuk client program
  • Folder programs yang akan menampung semua program Solana kita
  • Folder tests untuk pengujian JavaScript. Folder ini dilengkapi file pengujian yang dibuat otomatis untuk kode awal
  • File konfigurasi Anchor.toml. Jika Anda baru mengenal Rust, file TOML adalah format file konfigurasi minimal yang mudah dibaca berkat semantiknya. File Anchor.toml digunakan untuk mengatur cara Anchor berinteraksi dengan program. Misalnya, cluster tempat program harus di-deploy.

Membuat Project Baru dengan Solana Playground

Membuat project baru di Solana Playground sangat mudah. Buka sudut kiri atas, lalu klik Buat Project Baru:

Modal berikut akan muncul:

Beri nama program Anda, pilih Anchor(Rust), lalu klik Buat. Tindakan ini akan membuat project Anchor baru langsung di browser Anda. Di bagian Program sebelah kiri, Anda akan melihat direktori src. Direktori ini berisi lib.rs dengan kode awal berikut:

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

Perhatikan bahwa Solana Playground hanya membuat file client.ts dan anchor.test.ts. Saya menyarankan Anda membaca bagian tentang membuat program dengan Anchor secara lokal untuk melihat perincian file yang biasanya dibuat untuk project Anchor baru.

Baik Anda menggunakan Anchor secara lokal maupun melalui Solana Playground, untuk membuat program Hello, World! yang sangat sederhana, ganti kode awal dengan kode berikut:

Kode
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 {}
}

Kita akan membahas detail setiap bagiannya pada bagian berikutnya. Untuk saat ini, perhatikan penggunaan macro dan trait untuk menyederhanakan proses pengembangan. Macro declare_id! menetapkan public key program. Untuk pengembangan lokal, perintah anchor init yang digunakan untuk menyiapkan program akan membuat keypair di direktori target/deploy dan mengisi macro ini. Solana Playground juga akan melakukannya secara otomatis untuk kita.

Dalam modul utama hello_world, kita membuat fungsi yang mencatat Hello, World! Fungsi ini juga mengembalikan Ok(()) untuk menandakan bahwa program berhasil dijalankan. Perhatikan bahwa kita menambahkan garis bawah di awal ctx untuk menghindari peringatan variabel yang tidak digunakan di konsol. Hello adalah struct account yang tidak mengharuskan account apa pun diteruskan karena program hanya mencatat pesan baru.

Selesai! Kita tidak perlu menerima account apa pun atau menjalankan logika yang kompleks. Kode di atas membuat program yang mencatat Hello, World!

Melakukan Build dan Deployment Secara Lokal

Bagian ini berfokus pada deployment ke Localhost. Meskipun Solana Playground menggunakan devnet secara default, lingkungan pengembangan lokal menawarkan pengalaman developer yang jauh lebih baik. Selain lebih cepat, lingkungan ini juga menghindari beberapa masalah yang umum ditemui saat melakukan pengujian di devnet. Contohnya termasuk saldo SOL yang tidak cukup untuk transaction, deployment yang lambat, dan tidak dapat melakukan pengujian saat devnet tidak aktif. Sebaliknya, pengembangan lokal dapat menjamin state baru pada setiap pengujian. Hal ini menciptakan lingkungan developer yang lebih terkontrol dan efisien.

Mengonfigurasi Alat Kita

Pertama, kita perlu memastikan Solana Tool Suite dikonfigurasi dengan benar untuk pengembangan di Localhost. Jalankan perintah solana config set --url localhost untuk memastikan semua konfigurasi mengarah ke URL Localhost. 

Pastikan juga Anda memiliki keypair lokal untuk berinteraksi dengan Solana secara lokal. Anda harus memiliki wallet Solana dengan saldo SOL untuk men-deploy program menggunakan Solana CLI. Jalankan perintah solana address untuk memeriksa apakah Anda sudah memiliki keypair lokal. Jika muncul error, jalankan perintah solana-keygen new. Secara default, wallet sistem file baru akan dibuat di path ~/.config/solana/id.json. Perintah tersebut juga akan memberikan frasa pemulihan yang dapat digunakan untuk memulihkan public key dan private key. Sebaiknya simpan keypair ini meskipun hanya digunakan secara lokal. Perhatikan juga bahwa jika Anda sudah memiliki wallet sistem file yang tersimpan di lokasi default, perintah solana-keygen new tidak akan menimpanya kecuali ditentukan dengan perintah --force.

Mengonfigurasi Anchor.toml

Berikutnya, kita perlu memastikan file Anchor.toml mengarah ke Localhost dengan benar. Pastikan file tersebut berisi kode berikut:

Kode
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Localnet"
wallet = '~config/solana/id.json'

Di sini, [programs.localnet] mengacu pada ID program di localnet (yaitu Localhost). ID program selalu ditentukan berdasarkan cluster. Alasannya, program yang sama dapat di-deploy ke alamat berbeda di cluster yang berbeda. Dari perspektif pengalaman developer, mendeklarasikan ID program baru untuk program yang di-deploy di beberapa cluster dapat merepotkan. 

ID program bersifat publik. Namun, keypair-nya disimpan dalam folder target/deploy. Penamaannya mengikuti konvensi tertentu berdasarkan nama program. Misalnya, jika nama program adalah hello_world, Anchor akan mencari keypair di target/deploy/hello-world-keypair.json. Anchor akan membuat keypair baru jika file ini tidak ditemukan saat deployment. Akibatnya, ID program baru akan dibuat. Karena itu, memperbarui ID program setelah deployment pertama sangatlah penting. File hello-world-keypair.json berfungsi sebagai bukti kepemilikan program. Jika keypair bocor, pihak jahat dapat melakukan perubahan tanpa izin pada program. 

Dengan [provider], kita memberi tahu Anchor agar menggunakan Localhost dan wallet yang ditentukan untuk membayar penyimpanan dan transaction.

Melakukan Build, Deployment, dan Menjalankan Ledger Lokal

Gunakan perintah anchor build untuk melakukan build program. Untuk melakukan build program tertentu berdasarkan namanya, gunakan perintah anchor build -p <program name> dan ganti <program name> dengan nama program. Karena kita melakukan pengembangan di localnet, kita dapat menggunakan perintah localnet dari Anchor CLI untuk menyederhanakan proses pengembangan. Misalnya, anchor localnet --skip-build sangat berguna untuk melewati proses build program di workspace. Opsi ini dapat menghemat waktu ketika menjalankan pengujian dan kode program tidak berubah.

Jika kita mencoba menjalankan perintah anchor deploy sekarang, kita akan mendapatkan error. Hal ini karena tidak ada cluster Solana yang berjalan di komputer kita untuk digunakan dalam pengujian. Kita dapat menjalankan ledger lokal untuk menyimulasikan cluster di komputer. Solana CLI sudah dilengkapi validator pengujian. Menjalankan perintah solana-test-validator akan memulai cluster node tunggal berfitur lengkap di workstation Anda. Hal ini memberikan berbagai manfaat, seperti tidak adanya batas laju RPC, tidak ada batas airdrop, deployment program on-chain secara langsung, pemuatan account dari file, dan cloning account dari cluster publik. Validator pengujian harus berjalan di jendela terminal terpisah yang tetap terbuka agar cluster localhost tetap online dan dapat digunakan untuk berinteraksi. 

Sekarang kita dapat menjalankan anchor deploy untuk men-deploy program ke ledger lokal. Semua data yang dikirim ke ledger lokal akan disimpan dalam folder test-ledger yang dibuat di direktori kerja saat ini. Sebaiknya tambahkan folder ini ke file .gitignore agar folder tersebut tidak di-commit ke repositori. Selain itu, menghentikan ledger lokal (yaitu menekan Ctrl + C di terminal) tidak akan menghapus data yang telah dikirim ke cluster. Data tersebut akan dihapus jika Anda menghapus folder test-ledger atau menjalankan solana-test-validator --reset.

Selamat! Anda baru saja men-deploy program Solana pertama Anda ke Localhost!

Solana Explorer

Developer juga dapat mengonfigurasi Solana Explorer agar terhubung ke ledger lokal. Buka Solana Explorer. Di bilah navigasi, klik tombol hijau yang menampilkan cluster saat ini:

Tindakan ini akan membuka sidebar yang memungkinkan Anda memilih cluster. Klik URL RPC Kustom. Kolom tersebut seharusnya terisi otomatis dengan http://localhost:8899. Jika tidak, masukkan URL tersebut agar explorer mengarah ke komputer Anda pada port 8899:

Hal ini sangat berguna karena beberapa alasan:

  • Memungkinkan developer memeriksa transaction di ledger lokal secara real-time, menyerupai kemampuan yang biasanya tersedia melalui block explorer yang menganalisis devnet atau mainnet
  • Mempermudah visualisasi state account, token, dan program seolah-olah semuanya berjalan di cluster aktif
  • Memberikan informasi mendetail tentang error dan kegagalan transaction
  • Memberikan pengalaman pengembangan yang konsisten di berbagai cluster melalui antarmuka yang sudah dikenal

Men-deploy ke Devnet

Meskipun kami menyarankan pengembangan di Localhost, developer juga dapat men-deploy ke devnet jika ingin melakukan pengujian secara khusus pada cluster tersebut. Prosesnya pada dasarnya sama, kecuali Anda tidak perlu menjalankan ledger lokal karena tersedia cluster Solana lengkap yang dapat digunakan untuk berinteraksi.

Jalankan perintah solana config set --url devnet untuk mengubah cluster yang dipilih menjadi devnet. Setiap perintah solana yang dijalankan di terminal sekarang akan dieksekusi di devnet. Kemudian, di file Anchor.toml, duplikasi bagian [programs.localnet] dan ubah namanya menjadi [programs.devnet]. Ubah juga [provider] agar mengarah ke devnet:

Kode
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"

[programs.devnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Devnet"
wallet = '~config/solana/id.json'

Developer harus memastikan bahwa mereka memiliki SOL devnet untuk men-deploy program. Gunakan perintah solana airdrop <amount> untuk melakukan airdrop ke lokasi keypair default di ~/.config/solana/id.json. Alamat wallet juga dapat ditentukan menggunakan solana aidrop <amount> <wallet address>. Sebagai alternatif, kunjungi faucet ini untuk mendapatkan SOL devnet. Saya menyarankan Anda membaca panduan cara mendapatkan SOL devnet berikut.

Hal ini sering terjadi karena faucet devnet kehabisan dana dan/atau Anda meminta terlalu banyak SOL sekaligus. Batas saat ini adalah 5 SOL, yang lebih dari cukup untuk men-deploy program ini. Karena itu, sebaiknya minta 5 SOL dari faucet atau jalankan perintah solana airdrop 5. Meminta jumlah yang lebih kecil secara bertahap berpotensi memicu pembatasan laju.

Sekarang, lakukan build dan deployment program menggunakan perintah berikut:

Kode
anchor build
anchor deploy

Selamat! Anda baru saja men-deploy program Solana pertama Anda ke devnet secara lokal!

Melakukan Build dan Deployment di Solana Playground

Di Solana Playground, buka ikon Alat di sidebar kiri. Klik Build. Di konsol, Anda akan melihat hal berikut:

Kode
Building...
Build successful. Completed in 2.20s..

Perhatikan bahwa ID dalam macro declare_id! telah ditimpa. Alamat baru ini adalah tempat kita akan men-deploy program. Sekarang, klik Deploy. Anda akan melihat tampilan serupa berikut di konsol:

Kode

Deploying... This could take a while depending on the program size and network conditions.
Warning: 41 transactions not confirmed, retrying...
Deployment successful. Completed in 17s

Selamat! Anda baru saja men-deploy program Solana pertama Anda ke devnet melalui Solana Playground!

Abstraksi Efektif: IDL dan Makro

Anchor menyederhanakan pengembangan program melalui abstraksi yang efektif. Artinya, Anchor menyederhanakan konsep pemrograman blockchain yang kompleks sehingga lebih mudah dipahami dan digunakan. Misalnya, Anchor menggunakan Interface Definition Language (IDL) untuk mendefinisikan antarmuka program. Saat membangun program, Anchor akan menghasilkan file JSON yang merepresentasikan IDL program tersebut. Pada dasarnya, struktur ini dapat digunakan di sisi klien untuk menentukan cara berinteraksi dengan fungsi dan struktur data program. Anchor juga menyediakan abstraksi tingkat tinggi untuk menangani pengelolaan status. Anchor memungkinkan pengembang mendefinisikan status program menggunakan struct Rust, yang lebih intuitif daripada bekerja dengan array byte mentah atau serialisasi manual. Dengan demikian, pengembang dapat mendefinisikan status seperti pada struktur data Rust biasa, lalu Anchor menangani serialisasi yang mendasarinya dan penyimpanan ke dalam akun.

Menerbitkan IDL secara on-chain juga sangat mudah. Pengembang dapat menerbitkan IDL dengan perintah berikut:

Kode
anchor idl init --filepath   --provider.cluster  --provider.wallet

Pastikan wallet yang diberikan merupakan otoritas program dan memiliki cukup SOL untuk transaksi. Kini pengembang dapat melihat IDL mereka di block explorer seperti Orb.

Sebagai contoh, berikut adalah IDL aggregator v4 DFlow di Orb.

Makro Anchor merupakan salah satu abstraksi terpenting, bahkan mungkin yang paling penting. Dalam Rust, makro adalah potongan kode yang menghasilkan potongan kode lain. Ini merupakan bentuk metapemrograman. Makro deklaratif adalah bentuk makro yang paling banyak digunakan dalam Rust. Makro ini memungkinkan pengembang menulis sesuatu yang menyerupai ekspresi match melalui konstruksi macro_rules!. Makro prosedural bekerja lebih seperti fungsi: menerima kode sebagai input, memproses kode tersebut, lalu menghasilkan output. Dalam Anchor, misalnya, makro #[account] mendefinisikan dan memberlakukan batasan pada akun Solana. Hal ini membantu mengurangi kompleksitas dan potensi kesalahan dalam pengelolaan akun. Pembahasan tentang makro Anchor tentu perlu mencakup struktur program Anchor.

Struktur Program Anchor

Struktur program Anchor dirancang untuk memanfaatkan kombinasi makro dan trait guna menghasilkan kode boilerplate serta memberlakukan logika program. Filosofi desain ini berperan besar dalam menyederhanakan proses pengembangan serta memastikan konsistensi dan keandalan perilaku program.

Deklarasi use berada di bagian atas file. Perhatikan bahwa deklarasi ini merupakan semantik umum bahasa Rust dan tidak khusus untuk Anchor. Deklarasi tersebut membuat satu atau beberapa binding nama lokal yang identik dengan path lain—deklarasi use mempersingkat path yang diperlukan untuk merujuk ke item modul. Deklarasi ini dapat muncul dalam modul atau blok. Selain itu, kata kunci self dapat melakukan binding pada daftar path dengan prefiks yang sama beserta modul induknya. Sebagai contoh, semua deklarasi use berikut valid: 

Kode
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};

Makro Anchor pertama yang akan dijumpai pengembang adalah declare_id!. Makro ini digunakan untuk mendeklarasikan alamat program (ID program), sehingga semua interaksi diarahkan dengan benar ke program. Anchor akan menghasilkan keypair baru saat pengembang membangun program Anchor untuk pertama kalinya. Keypair ini digunakan untuk men-deploy program kecuali ditentukan lain. Public key keypair tersebut harus diberikan sebagai ID program untuk makro declare_id!:

Kode
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

Makro atribut #[program] menandai modul logika instruksi program. Makro ini berfungsi sebagai titik masuk yang menentukan cara program menafsirkan dan mengeksekusi instruksi masuk. Makro ini menyederhanakan perutean instruksi tersebut ke fungsi yang sesuai dalam program, sehingga kode program lebih teratur dan mudah dikelola. Setiap fungsi dalam modul ini diperlakukan sebagai instruksi terpisah. Setiap fungsi menerima parameter konteks (ctx) bertipe Context sebagai argumen pertamanya. Pengembang dapat mengakses akun, ID program dari program yang sedang dieksekusi, dan akun yang tersisa.

Tipe Context didefinisikan sebagai:

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

Ini membantu menyediakan input non-argumen bagi suatu program. Field program_id bertipe Pubkey dan merepresentasikan ID program yang sedang dieksekusi. accounts merujuk pada akun yang diserialisasi, sedangkan remaining_accounts merujuk pada akun lain yang diberikan tetapi tidak dideserialisasi atau divalidasi—berhati-hatilah saat menggunakannya secara langsung. Field bumps bertipe Bumps yang dihasilkan oleh #[derive(Accounts)]. Field ini merepresentasikan bump seed yang ditemukan selama validasi batasan. Kita akan membahas batasan akun pada bagian berikutnya. Untuk saat ini, hal penting yang perlu diketahui adalah field ini disediakan demi kemudahan agar handler tidak perlu menghitung ulang bump seed atau meneruskannya sebagai argumen.

Perhatikan bahwa Context merupakan tipe generik. Dalam Rust, generik memungkinkan pengembang menulis kode fleksibel dan dapat digunakan kembali yang bekerja dengan tipe data apa pun. Generik memungkinkan definisi tipe untuk struct, enum, fungsi, dan metode tanpa menentukan tipe persis yang akan digunakan. Sebagai gantinya, placeholder digunakan untuk tipe tersebut, biasanya ditandai sebagai T. Generik membantu mengurangi kode berulang dan meningkatkan kejelasan. Sebagai contoh, enum dapat didefinisikan untuk menampung tipe data generik:

Kode
enum Option<T> {
  Some(T),
  None,
}

Cuplikan kode di atas menampilkan enum Option<T>. Ini adalah enum Rust standar yang dapat mengenkapsulasi nilai dari tipe apa pun (yaitu Some(T)) atau tanpa tipe (None).

Untuk keperluan kita, Context merupakan tipe generik dengan T yang menentukan akun yang diperlukan untuk suatu instruksi (yaitu tipe apa pun yang ingin dibuat pengembang untuk menyimpan data). Pengembang dapat mendefinisikan T sebagai struct yang mengimplementasikan trait Accounts saat menggunakan Context. Misalnya, Context<SetData>. Pengembang dapat mengakses field dalam tipe Context menggunakan notasi titik. Sebagai contoh, ctx.accounts mengakses field accounts dari struct Context.

Seperti disebutkan sebelumnya, makro #[account] mendefinisikan tipe akun khusus. Pada bagian berikutnya, kita akan mempelajari tipe dan batasan akun menggunakan #[account(...)]. Untuk saat ini, penting untuk diketahui bahwa struct Accounts merupakan tempat pengembang mendefinisikan akun yang harus diterima instruksi dan batasan yang harus dipatuhi akun tersebut.

Tipe Akun

Tipe Account digunakan ketika instruksi ingin mengakses data akun yang telah dideserialisasi. Struct Account bersifat generik terhadap T dan didefinisikan sebagai: 

Kode
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> { /* private fields */ }

Tipe ini merupakan wrapper untuk AccountInfo yang memverifikasi kepemilikan program dan mendeserialisasi data yang mendasarinya menjadi tipe Rust. Tipe ini memeriksa kepemilikan program sehingga Account.info.owner == T::owner(). Artinya, tipe ini memeriksa apakah pemilik data sama dengan ID (yang sebelumnya dibuat dengan declare_id!) dari crate tempat #[account] digunakan. Ini berarti tipe data yang dibungkus Account (=T) harus mengimplementasikan trait Owner. Atribut #[account] mengimplementasikan trait tersebut untuk struct menggunakan crate::ID yang dideklarasikan oleh declare_id! dalam program yang sama. Dalam sebagian besar kasus, pengembang cukup menggunakan atribut #[account] untuk menambahkan trait dan implementasi yang diperlukan ke data mereka. Atribut #[account] menghasilkan implementasi untuk trait berikut:

Delapan byte awal dialokasikan untuk diskriminator akun unik saat mengimplementasikan trait untuk serialisasi akun. Diskriminator ini ditentukan oleh 8 byte pertama hash SHA-256 dari identifier Rust akun. Setiap pemanggilan try_deserialize milik AccountDeserialize akan memeriksa diskriminator ini dan menghentikan deserialisasi akun dengan error jika akun yang diberikan tidak valid.

Ada kalanya pengembang perlu berinteraksi dengan program non-Anchor. Dalam hal ini, pengembang dapat memperoleh semua manfaat Account jika mereka membuat tipe wrapper khusus sendiri, alih-alih menggunakan #[account]. Perhatikan cuplikan kode berikut sebagai contoh:

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

Sebagian besar validasi akun dilakukan melalui batasan akun, yang akan dibahas pada bagian berikutnya. Namun untuk saat ini, perhatikan bagaimana tipe TokenAccount digunakan untuk memastikan akun yang masuk dimiliki oleh program token. TokenAccount membungkus struct Account milik program token dan menambahkan fungsi yang diperlukan. Hal ini memastikan Anchor dapat mendeserialisasi akun, dan pengembang dapat menggunakan field-nya dalam batasan akun serta fungsi instruksi.

Perhatikan juga dalam cuplikan kode di atas bahwa makro derive mengenkapsulasi seluruh struct. Makro ini mengimplementasikan deserializer Accounts pada SetData dan digunakan untuk memvalidasi akun yang masuk.

Beberapa tipe Account dapat digunakan dalam struct validasi akun, termasuk:

Batasan Akun

Batasan akun sangat penting untuk mengembangkan program Anchor yang aman. Dalam artikel mendatang, kita akan membahas keamanan program Solana dan peretasan program Anchor secara lebih mendalam. Namun, penting untuk membahas batasan di sini. Batasan memungkinkan pengembang memverifikasi bahwa akun tertentu atau data yang disimpannya sesuai dengan persyaratan yang telah ditentukan. Beberapa jenis batasan dapat diterapkan menggunakan atribut #[account(...)], yang juga dapat merujuk pada struktur data lain. Formatnya adalah sebagai berikut:

Kode
#[account(constraint goes here)]
pub account: AccountType

Penting juga untuk diperhatikan bahwa dalam makro Accounts, pengembang dapat mengakses argumen instruksi menggunakan atribut #[instruction(...)]. Pengembang harus mencantumkan argumen instruksi dalam urutan yang sama seperti pada instruksi, tetapi dapat menghilangkan semua argumen setelah argumen terakhir yang diperlukan. Sebagai contoh, dari dokumentasi Anchor: 

Kode
...
pub fn initialize(ctx: Context, bump: u8, authority: Pubkey, data: u64) -> anchor_lang::Result<()> {
    ...
    Ok(())
}
...
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct Initialize<'info> {
    ...
}

Batasan akun dapat dibagi menjadi Batasan Normal dan Batasan SPL. Kita akan membahas batasan tertentu di sepanjang bagian selanjutnya dari artikel ini. Dalam contoh-contoh ini, <expr> merepresentasikan ekspresi arbitrer yang dapat diberikan selama menghasilkan nilai dengan tipe yang diharapkan. Misalnya, owner = token_program.key().

Menganalisis Batasan Program

Saya menyarankan Anda membaca dokumentasi Anchor tentang akun untuk memperoleh daftar batasan yang lebih lengkap. Akan terlalu rumit untuk membahas setiap batasan dan memberikan definisi formal dalam format tabel. Untuk keperluan kita, akan lebih bermanfaat untuk menganalisis program berikut agar memahami penerapan batasan akun:

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

Ini adalah program Fanout milik Helium. Program ini cukup kompleks dan digunakan untuk mendistribusikan token secara proporsional kepada pemegang token berdasarkan jumlah kepemilikannya. Saat ini, proyek ini belum terlihat berguna bagi kita karena tidak memiliki batasan. Namun, jika kita menganalisis struct StakeV0 dari instruksi stake_v0, ada banyak batasan yang dapat dipelajari.

mut

Batasan pertama dalam instruksi ini adalah batasan akun mut. mut didefinisikan sebagai #[account(mut)] atau #[account(mut @ <custom_error>)], dengan dukungan untuk error khusus melalui notasi @. Batasan ini memeriksa apakah akun tertentu bersifat mutable dan membuat Anchor menyimpan setiap perubahan status. Dalam program Helium, batasan ini memastikan bahwa akun payer bersifat mutable:

Kode
...
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

Batasan has_one didefinisikan sebagai #[account(has_one = <target_account)] atau #[account(has_one = <target_account> @ <custom_error>)]. Batasan ini memeriksa field target_account untuk melihat apakah akun sesuai dengan key field target_account dalam struct Accounts. Error khusus didukung melalui anotasi @.

Dalam konteks struct StakeV0, batasan has_one digunakan untuk memeriksa apakah akun memiliki membership_mint, token_account, dan membership_collection:

Kode
...
#[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>>,
...

Perhatikan bahwa terdapat beberapa batasan has_one, dan batasan mut juga digunakan. Dengan batasan akun, beberapa batasan dapat digunakan secara bersamaan pada satu akun.

seeds, bump

Batasan seeds dan bump digunakan untuk memeriksa bahwa akun tertentu merupakan PDA yang diturunkan dari program yang sedang dieksekusi, seed, dan bump jika diberikan:

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

Jika bump tidak diberikan, Anchor akan menggunakan bump kanonis. Seeds::program = <expr> dapat digunakan untuk menurunkan PDA dari program yang berbeda dari program yang sedang dieksekusi.

Dalam program fanout Helium, batasan seeds memeriksa apakah teks “metadata”, key token_metadata_program, key membership_collection, dan teks “edition” merupakan seed yang digunakan untuk menurunkan PDA ini. Batasan seeds::program memastikan bahwa token_metadata_program digunakan untuk menurunkan PDA, bukan program saat ini:

Kode
...
#[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

Batasan token::mint dan token::authority didefinisikan sebagai berikut:

  • #[account(token::mint = <target account>, token::authority = <target account>)]
  • #[account(token::mint = <target account>, token::authority = <target account>, token::token_program = <target account>)]

Batasan token mint dan authority digunakan untuk memverifikasi alamat mint dan otoritas TokenAccount. Batasan ini dapat digunakan sebagai pemeriksaan atau bersama batasan init untuk membuat akun token dengan alamat mint dan otoritas yang diberikan. Saat digunakan sebagai pemeriksaan, hanya sebagian batasan yang dapat ditentukan. 

Dalam konteks program Helium, batasan ini digunakan untuk memeriksa apakah mint milik associated_token sama dengan membership_mint, dan apakah otoritas token ditetapkan ke staker:

Kode
...
#[account(
  mut,
  associated_token::mint = membership_mint,
  associated_token::authority = staker,
)]
pub from_account: Box<Account<'info, TokenAccount>>,
...

init, payer, space

Pada tahap ini, sebaiknya kita melompat sedikit ke bagian kode berikutnya untuk menganalisis batasan init, payer, dan space. Batasan init didefinisikan sebagai [#account(init, payer = <target_account>, space = <num_bytes>)]. Batasan ini membuat akun melalui CPI ke System Program dan menginisialisasinya dengan menetapkan diskriminator akun. Ini akan menandai akun sebagai mutable dan tidak dapat digunakan bersama mut. Untuk akun yang lebih besar dari 10 Kibibyte, gunakan #[account(zero)].

Batasan init harus digunakan bersama beberapa batasan tambahan. Batasan ini memerlukan payer, yang menentukan akun yang akan membayar pembuatan akun. Batasan ini juga mengharuskan System Program tersedia dalam struct dan diberi nama system_program. Batasan space juga harus didefinisikan. Pada bagian Ruang Akun, kita akan membahas batasan ini dan kebutuhan ruang secara lebih mendalam.

Dalam program fanout Helium, perintah init membuat akun baru. payer ditetapkan sebagai payer, yang sebelumnya didefinisikan dalam struct sebagai pub payer: Signer<'info>. Ruang akun ditetapkan sebesar ukuran FanoutVoucherV0, ditambah 8 byte untuk diskriminator dan tambahan ruang sebesar 61 byte:

Kode
...
#[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

Batasan init_if_needed didefinisikan sebagai #[account(init_if_nedded, payer = <target_Account>)] atau #[account(init)if_needed, payer = <target_account>, space = <num_bytes>)]. Batasan ini memiliki fungsi yang sama persis dengan init. Namun, batasan ini hanya dijalankan jika akun belum ada. Jika akun sudah ada, init_if_needed tetap memverifikasi bahwa semua batasan inisialisasi terpenuhi, seperti akun memiliki alokasi ruang yang tepat atau seed yang tepat untuk PDA.

init_if_needed harus digunakan dengan hati-hati karena berada di balik feature flag akibat potensi risikonya. Untuk mengaktifkannya, impor anchor-lang dengan fitur cargo init-if-needed. Sangat penting untuk melindungi program dari serangan inisialisasi ulang saat menggunakan init_if_needed. Pengembang harus memastikan kode mereka mencakup pemeriksaan untuk mencegah akun dikembalikan ke status awal setelah diinisialisasi, kecuali perilaku ini memang diinginkan. Menjaga jalur eksekusi instruksi tetap sederhana dianggap sebagai praktik terbaik untuk memitigasi serangan tersebut. Pertimbangkan untuk membagi instruksi menjadi satu instruksi untuk inisialisasi dan instruksi lain untuk operasi berikutnya.

Program Fanout Helium menggunakan batasan init_if_needed untuk menginisialisasi recipient_account jika akun tersebut belum ada:

Kode
...
#[account(
  init_if_needed,
  payer = payer,
  associated_token::mint = mint,
  associated_token::authority = recipient,
)]
pub receipt_account: Box<Account<'info, TokenAccount>>,
...

constraint

Batasan constraint didefinisikan sebagai #[account(constraint = <expr>)] atau #[account(constraint = <expr> @ <custom_error>)]. Batasan ini memeriksa apakah ekspresi yang diberikan menghasilkan nilai true. Ini berguna ketika tidak ada batasan lain yang sesuai dengan kasus penggunaan yang dimaksud. Batasan ini juga mendukung error khusus melalui anotasi @.

Program Fanout menggunakan constraint untuk memeriksa apakah supply mint ditetapkan ke nol:

Kode
...
#[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 

Dalam cuplikan kode di atas, batasan mint::decimals, mint::authority, dan mint::freeze_authority digunakan untuk memeriksa apakah desimal mint ditetapkan ke nol serta apakah voucher memiliki otoritas dan freeze authority.

Sebagai konteks, batasan mint::authority, mint::decimals, dan mint::freeze_authority didefinisikan sebagai:

  • #[account(mint::authority = <target account>, mint::decimals = <expr>)]
  • #[account(mint::authority = <target account>, mint::decimals = <expr>, mint::freeze_authority = <target account>)]

Batasan ini sudah cukup jelas—masing-masing memeriksa otoritas token, desimal, dan freeze authority. Batasan ini dapat digunakan sebagai pemeriksaan atau bersama init untuk membuat akun mint dengan desimal mint dan otoritas mint yang diberikan. Freeze authority sepenuhnya opsional saat digunakan bersama init. Saat digunakan sebagai pemeriksaan, hanya sebagian batasan yang dapat ditentukan.

Ruang Akun 

Setiap akun di Solana yang digunakan oleh program harus memiliki ruang penyimpanan yang dialokasikan secara eksplisit. Alokasi ini sangat penting untuk pengelolaan sumber daya yang efisien, sehingga hanya data yang diperlukan yang disimpan secara on-chain. Hal ini juga membuat biaya transaksi lebih mudah diprediksi dan meningkatkan efisiensi eksekusi transaksi karena transaksi dapat diproses tanpa perlu mengalokasikan atau mengubah ukuran penyimpanan akun secara dinamis. Selain itu, pra-alokasi data menjamin akun memiliki ruang yang cukup untuk menyimpan semua data yang diperlukan, sehingga mengurangi risiko kegagalan transaksi atau potensi kerentanan keamanan.

Menentukan Ukuran Variabel

Jenis data yang berbeda memiliki kebutuhan ruang yang berbeda. Berikut panduan sederhana untuk membantu memperkirakan kebutuhan ruang:

  • Tipe Dasar: jenis data sederhana seperti bool, u8, i8, u16, i16, u32, i32, u64, i64, u128, dan i128 memiliki ukuran tetap. Ukurannya berkisar dari 1 byte untuk bool (meskipun hanya menggunakan 1 bit) hingga 16 byte untuk u128 / i128
  • Array: untuk array [T;amount], ruang dihitung sebagai ukuran T dikalikan jumlah elemen (yaitu, amount). Misalnya, array yang berisi 16 u16 akan memerlukan 32 byte
  • Pubkey:  public key selalu menempati 32 byte di Solana
  • Tipe Dinamis: String dan Vec<T> perlu dipertimbangkan dengan cermat. Keduanya memerlukan 4 byte untuk menyimpan panjangnya, ditambah ruang untuk konten sebenarnya. Anda harus mengalokasikan ruang yang cukup untuk ukuran maksimum yang diperkirakan. Untuk String, ini berarti 4 byte ditambah panjang String dalam byte. Untuk Vec<T>, ini berarti 4 byte ditambah ruang dari tipe yang diberikan, dikalikan jumlah elemen yang diperkirakan (yaitu, 4 + space(T) * amount)
  • Option dan Enum: tipe Option<T> memerlukan 1 byte ditambah ruang untuk tipe T. Enum memerlukan 1 byte untuk diskriminator enum ditambah ruang yang diperlukan oleh varian terbesar
  • Bilangan Floating Point: tipe seperti f32 dan f64 masing-masing menempati 4 dan 8 byte. Berhati-hatilah dengan nilai NaN, karena nilai tersebut dapat menyebabkan serialisasi gagal

Panduan berikut hanya berlaku untuk akun yang tidak menggunakan serialisasi zero-copy. Serialisasi zero-copy ditandai dengan atribut #[zero_copy]. Serialisasi ini memanfaatkan atribut repr(c) untuk tata letak memori, yang memungkinkan casting pointer langsung untuk mengakses data. Ini merupakan cara yang efisien untuk bekerja dengan data on-chain tanpa overhead deserialisasi tradisional. #[zero_copy] adalah bentuk singkat untuk menerapkan #[derive(Copy, Clone)], #[derive(bytemuck::Zeroable)], #[derive(bytemuck::Pod)], dan #[repr(C)]. Atribut ini memastikan akun dapat diperlakukan dengan aman sebagai urutan byte dan kompatibel dengan deserialisasi zero-copy. Deserialisasi zero-copy sangat penting untuk akun yang memerlukan ukuran sangat besar, yaitu akun yang tidak dapat diserialisasi secara efisien menggunakan Borsh atau mekanisme serialisasi default Anchor tanpa melampaui batas heap atau stack.

Diskriminator Internal Anchor

Developer harus menambahkan 8 ke batasan space untuk diskriminator internal Anchor. Misalnya, jika sebuah akun memerlukan 32 byte, akun tersebut akan memerlukan 40 byte. Menetapkan batasan ruang sebagai space = 8 + <account size> dianggap sebagai praktik yang baik agar jelas bahwa diskriminator internal telah diperhitungkan dalam kalkulasi ruang.  

Sebagai tambahan, diskriminator adalah pengidentifikasi unik yang digunakan untuk membedakan berbagai jenis data. Ini berguna untuk membedakan berbagai jenis struktur data akun saat runtime. Diskriminator juga digunakan sebagai prefiks instruksi, yang membantu mengarahkan instruksi tersebut ke metode terkait dalam program Anchor. Diskriminator adalah array 8 byte yang merepresentasikan pengidentifikasi unik jenis data.

Menghitung Ruang Awal

Menghitung kebutuhan ruang awal untuk sebuah akun dapat menjadi sulit. Makro InitSpace menambahkan konstanta INIT_SPACE yang dapat digunakan pada struktur akun. Struktur tersebut tidak harus memuat makro #[account] untuk menghasilkan konstanta. Dokumentasi Anchor memberikan contoh berikut:

Kode
#[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>,
}

Dalam contoh ini, ExampleAccount::INIT_SPACE secara otomatis menghitung ruang yang diperlukan untuk ExampleAccount. Kalkulasi ruang tersebut juga memperhitungkan diskriminator internal Anchor.

Mengubah Ukuran Ruang Program

Batasan realloc digunakan untuk menyesuaikan ruang akun program pada awal instruksi. Batasan ini mengharuskan akun bersifat mutable (yaitu, mut) dan dapat diterapkan pada tipe Account atau AccountLoader. Batasan ini didefinisikan sebagai #[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)]. Saat panjang data akun bertambah, lamport ditransfer dari realloc::payer ke akun program untuk mempertahankan pengecualian sewa. Jika panjang data berkurang, lamport dipindahkan kembali dari akun program ke realloc::payer. Batasan realloc::zero menentukan apakah memori yang baru dialokasikan harus diinisialisasi dengan nol. Inisialisasi nol memastikan memori baru bersih dan bebas dari data sisa atau data yang tidak diinginkan.

Penggunaan AccountInfo::realloc secara manual tidak disarankan dibandingkan batasan realloc. Alasannya adalah tidak adanya pemeriksaan runtime yang memastikan realokasi tidak melampaui batas MAX_PERMITTED_DATA_INCREASE, yang dapat menyebabkan data di akun lain tertimpa. Batasan tersebut juga memeriksa dan mencegah realokasi berulang dalam satu instruksi.

Contoh:

Kode
#[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>,
}

Error

Penanganan error merupakan aspek penting dalam pengembangan program. Ini adalah mekanisme untuk mengidentifikasi dan mengelola error yang dapat menghentikan eksekusi program. Penanganan error harus dilakukan secara sengaja dan terencana untuk memastikan kualitas, pemeliharaan, dan fungsionalitas kode. Anchor menyederhanakannya dengan mekanisme penanganan error yang tangguh. Error dalam program Anchor dapat dibagi menjadi AnchorErrors dan error non-Anchor. Bagian ini akan berfokus pada AnchorErrors karena error non-Anchor mencakup beragam error Rust. Untuk error non-Anchor, saya menyarankan Anda membaca bab Penanganan Error di Rust Book dan bagian Penanganan Error di Rust By Example.

struct berikut mendefinisikan AnchorError:

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

Field ini cukup mudah dipahami. error_name adalah string yang merepresentasikan nama error. error_code_number adalah pengidentifikasi unik (yaitu, bilangan bulat unik tanpa tanda yang menempati ruang 32 bit) untuk error tersebut. error_msg adalah pesan deskriptif yang menjelaskan error. error_origin adalah field opsional yang memberikan informasi tentang asal error, seperti file sumber atau akun terkait. compared_values adalah field opsional yang merinci nilai-nilai yang dibandingkan saat error terjadi. Ini sangat berguna untuk debugging.

AnchorError mengimplementasikan metode log. Metode ini mencakup informasi tentang asal error dan nilai-nilai yang terlibat, sehingga berguna untuk debugging dan penyelesaian error. Metode ini menggunakan error_origin dan compared_values untuk memberikan informasi tersebut.

AnchorError dapat dibagi lebih lanjut menjadi error internal Anchor dan error khusus. Anchor memiliki daftar panjang kode error internal yang dapat dikembalikan. Error internal ini tidak dimaksudkan untuk digunakan oleh pengguna. Namun, memahami pemetaan antara kode dan penyebabnya tetap berguna. Error tersebut biasanya dilempar saat sebuah batasan dilanggar. Kode error internal mengikuti skema berikut:

  • >= 100 adalah kode error instruksi
  • >= 1000 adalah kode error IDL
  • >= 2000 adalah kode error batasan
  • >= 3000 adalah kode error akun
  • >= 4100 adalah kode error lain-lain
  • = 5000 adalah kode error yang tidak lagi digunakan.

Error khusus dimulai dari ERROR_CODE_OFFSET (yaitu, 6000).

Developer dapat mengimplementasikan error khusus mereka sendiri menggunakan atribut error_code. Atribut ini digunakan pada enum, dan varian enum tersebut dapat digunakan sebagai error di seluruh program. Pesan dapat ditambahkan untuk setiap varian. Klien dapat menampilkan pesan ini jika error terjadi. Contoh:

Kode
#[error_code]
pub enum HeliusError {
  #[msg(“This RPC provider is too good”)]
  RPCTooGood
}

Makro err! dan error! dapat digunakan untuk melempar error ini. Contoh:

Kode
require!(rpc.speed > 9000, HeliusError::RPCTooGood);

Penting untuk diperhatikan bahwa ada beberapa makro require yang dapat dipilih. Sebagian besar makro ini berkaitan dengan nilai non-public key. Misalnya, makro require_gte memeriksa apakah nilai non-public key pertama lebih besar dari atau sama dengan nilai non-public key kedua:

Kode
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
    require_gte!(ctx.accounts.data.data, 1);
    ctx.accounts.data.data = data;
    Ok(());
}

Ada juga beberapa hal yang perlu diperhatikan saat membandingkan public key. Misalnya, developer sebaiknya menggunakan require_keys_eq alih-alih require_eq karena opsi kedua lebih mahal.

Semua program akan mengembalikan ProgramError. Tipe error ini mencakup field khusus untuk nomor error khusus, yang digunakan Anchor untuk menyimpan kode error internal dan khususnya. Namun, ini hanya berupa satu angka sehingga tidak terlalu berguna. Logging Anchor yang telah disebutkan sebelumnya dengan AnchorError jauh lebih membantu. Klien Anchor dirancang untuk mengurai log ini. Namun, ada skenario ketika hal ini dapat menjadi sulit. Misalnya, mengambil log untuk transaksi yang telah diproses dengan pemeriksaan preflight yang dinonaktifkan tidaklah mudah. Demikian pula, Anchor juga menggunakan mekanisme fallback untuk program non-Anchor atau lama yang tidak mencatat AnchorError dengan cara standar. Dalam hal ini, Anchor memeriksa apakah nomor error yang dikembalikan oleh transaksi sesuai dengan kode error internal Anchor atau nomor error yang didefinisikan dalam IDL program. Anchor memperkaya informasi error untuk memberikan lebih banyak konteks ketika menemukan kecocokan. Anchor juga akan mencoba mengurai stack error program jika memungkinkan untuk melacak penyebab awal error program. ProgramError berfungsi sebagai tipe error dasar, dengan kegunaan yang ditingkatkan oleh mekanisme logging dan penguraian Anchor untuk memberikan informasi error yang terperinci.

Pemanggilan Lintas Program (CPI)

Pemanggilan Lintas Program (CPI) telah disinggung di sepanjang artikel ini, sehingga sudah selayaknya kita menyediakan bagian khusus untuk membahasnya. CPI merupakan dasar komposabilitas Solana karena memungkinkan program memanggil program lain secara langsung. Dengan demikian, ekosistem Solana menjadi API luas yang saling terhubung bagi developer. Agar ringkas, saya menyarankan Anda membaca dokumentasi Anchor tentang CPI, yang memberikan contoh berguna tentang penerapan CPI dengan program puppet dan puppet master.

CPI dapat didefinisikan sebagai panggilan dari satu program ke program lain yang menargetkan instruksi tertentu dalam program yang dipanggil. Program pemanggil dihentikan sementara hingga program yang dipanggil selesai memproses instruksi. 

Eskalasi Hak Istimewa

CPI memungkinkan program pemanggil memperluas hak istimewa penandatangannya kepada program yang dipanggil. Perluasan hak istimewa memang praktis, tetapi berpotensi sangat berbahaya. Jika CPI secara tidak sengaja menargetkan program berbahaya, program tersebut memperoleh hak istimewa yang sama dengan pemanggil. Anchor mengurangi risiko ini dengan dua perlindungan:

  • Tipe Program<’info, T> memastikan akun yang ditentukan sesuai dengan program yang diharapkan (T)
  • Meskipun tipe Program tidak digunakan, fungsi CPI yang dibuat secara otomatis akan memverifikasi bahwa argumen cpi_program sesuai dengan program yang diharapkan

Menjalankan CPI

Program dapat menjalankan CPI menggunakan invoke atau invoke_signed dari crate solana_program. Anchor juga menyediakan struct CpiContext untuk menentukan input non-argumen bagi CPI.

invoke

Fungsi invoke digunakan ketika PDA tidak diperlukan sebagai tanda tangan. Dalam hal ini, runtime memperluas tanda tangan asli dari program pemanggil ke program yang dipanggil. Fungsi ini didefinisikan sebagai:

Kode
pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
) -> ProgramResult

Memanggil program lain melibatkan pembuatan Instruction yang mencakup ID program, data instruksi untuk program yang dipanggil, dan daftar akun yang akan diakses oleh program tersebut. Program hanya menerima nilai AccountInfo dari runtime pada entrypoint program. Setiap akun yang diperlukan oleh program yang dipanggil untuk pemanggilannya harus disertakan dan disediakan oleh program yang memanggilnya. Misalnya, jika program yang dipanggil perlu mengubah akun tertentu, program pemanggil harus menyertakan akun tersebut dalam daftar nilai AccountInfo. Hal ini juga berlaku untuk ID program dari program yang dipanggil (yaitu, pemanggil harus secara eksplisit menentukan program yang dipanggil dengan menyertakan ID programnya).

Instruction biasanya dibuat di dalam program pemanggil, meskipun dapat dideserialisasi dari output eksternal.

Seluruh transaksi akan langsung gagal jika program yang dipanggil mengalami error atau berhenti. Alasannya, fungsi invoke tidak akan kembali kecuali berhasil. Gunakan fungsi set_return_data atau get_return_data untuk mengembalikan data sebagai hasil CPI. Perhatikan bahwa tipe yang dikembalikan harus mengimplementasikan trait AnchorSerialize dan AnchorDeserialize. Sebagai alternatif, minta program yang dipanggil menulis ke akun khusus untuk menyimpan data

Meskipun program dapat memanggil dirinya sendiri secara rekursif, panggilan rekursif tidak langsung (yaitu, reentrancy) oleh program lain akan langsung menyebabkan transaksi gagal.

Misalnya, jika kita memiliki program yang mentransfer token melalui CPI, kita akan menggunakan invoke sebagai berikut:

Kode
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 digunakan untuk CPI yang memerlukan PDA sebagai penanda tangan. Fungsi ini memungkinkan program pemanggil bertindak atas nama PDA dengan menyediakan seed yang diperlukan untuk menurunkannya:

Kode
pub fn invoke_signed(
	instruction: &Instruction,
	account_infos: &[AccountInfo<'_>],
  signers_seeds: &[&[&[u8]]]
) -> ProgramResult

PDA juga dapat bertindak sebagai penanda tangan dalam CPI. Runtime akan menggunakan seed yang diberikan dan program_id milik program pemanggil untuk menghasilkan PDA secara internal melalui create_program_address. PDA kemudian divalidasi terhadap alamat yang diteruskan bersama instruksi (yaitu, account_infos) untuk memastikan bahwa PDA tersebut merupakan penanda tangan yang valid.

Dengan fungsi ini, pemanggilan dapat menandatangani atas nama satu atau beberapa PDA yang dikendalikan oleh program pemanggil. Hal ini memungkinkan program yang dipanggil berinteraksi dengan akun yang diberikan seolah-olah akun tersebut ditandatangani secara kriptografis. signer_seeds terdiri dari potongan seed yang digunakan untuk menurunkan PDA. Selama pemanggilan, runtime menganggap setiap akun yang cocok di account_info sebagai “ditandatangani”. Misalnya, jika kita memiliki program yang membuat akun untuk PDA, kita akan memanggil invoke_signed sebagai berikut:

Kode
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 menyediakan CpiContext sebagai cara yang lebih sederhana untuk membuat CPI dibandingkan menggunakan invoke atau invoke_signed. Struct ini menentukan input non-argumen yang diperlukan untuk CPI, dengan fungsi yang sangat menyerupai Context. Struct ini memberikan informasi tentang akun yang diperlukan untuk instruksi, akun tambahan yang terlibat, ID program yang dipanggil, dan seed untuk menurunkan PDA jika diperlukan. Gunakan CpiContext::new untuk CPI tanpa PDA, dan CpiContext::new_with_signer untuk CPI yang memerlukan penanda tangan PDA.

CpiContext didefinisikan sebagai berikut, dengan T sebagai tipe generik yang mencakup objek apa pun yang mengimplementasikan trait ToAccountMetas dan ToAccountInfos<’info>:

Kode
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 adalah tipe generik, sehingga dapat berupa objek apa pun yang mengimplementasikan trait ToAccountMetas dan ToAccountInfos<’info>. Hal ini dimungkinkan oleh makro atribut #[derive(Accounts)] untuk memudahkan pengorganisasian kode dan meningkatkan keamanan tipe. 

CpiContext menyederhanakan pemanggilan program Anchor dan non-Anchor. Untuk program Anchor, cukup deklarasikan dependensi dalam file Cargo.toml proyek dan gunakan modul cpi yang dihasilkan oleh Anchor:

Kode
[dependencies]
callee = { path = "../callee", features = ["cpi"]}

Menetapkan features = [“cpi”] memberikan program akses ke modul callee::cpi. Anchor menghasilkan modul ini secara otomatis dan mengekspos instruksi program sebagai fungsi Rust. Fungsi ini menerima CpiContext dan data instruksi tambahan, dengan format yang menyerupai fungsi instruksi biasa dalam program Anchor, tetapi menggunakan CpiContext untuk menggantikan Context. Modul cpi juga menyediakan struct akun yang diperlukan untuk memanggil instruksi.

Misalnya, jika program yang dipanggil memiliki instruksi bernama hello_there yang memerlukan akun tertentu sebagaimana didefinisikan dalam struct GeneralKenobi, panggil instruksi tersebut sebagai berikut:

Kode
// 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
}

Dalam modul fight_on_utapau, CPI dilakukan menggunakan CpiContext. Fungsi call_hello_there dirancang untuk berinteraksi dengan program jedi. Fungsi ini membuat CpiContext dengan informasi akun yang diperlukan oleh struct akun GeneralKenobi dari program jedi dan informasi akun program jedi. Konteks ini memanggil hello_there dengan meneruskan parameter tambahan yang diperlukan sebagaimana ditentukan oleh struct GreetingParams. Struct CallGeneralKenobi mendefinisikan akun yang diperlukan untuk fungsi ini, sehingga menyederhanakan prosesnya.

Terakhir, saat memanggil instruksi dari program non-Anchor, periksa apakah pengelola program telah menerbitkan crate mereka sendiri dengan fungsi helper untuk memanggil program tersebut. Jika tidak ada fungsi helper untuk program yang instruksinya harus dipanggil, gunakan invoke dan invoke_signer sebagai fallback untuk mengatur dan menyiapkan CPI.

Program Derived Addresses (PDA)

Ingat, PDA berada di luar kurva dan tidak memiliki private key terkait. PDA memungkinkan program menandatangani instruksi dan developer membangun struktur menyerupai hashmap secara on-chain. PDA diturunkan menggunakan daftar seed opsional, bump seed, dan ID program. 

Sebagai penegasan, batasan berikut digunakan untuk memeriksa bahwa akun tertentu merupakan PDA yang diturunkan dari program yang sedang dieksekusi, seed, dan bump jika diberikan:

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

Jika bump tidak diberikan, Anchor akan menggunakan bump kanonis. Seeds::program = <expr> dapat digunakan untuk menurunkan PDA dari program yang berbeda dari program yang sedang dieksekusi.

Penggunaan batasan seeds dan bump menyederhanakan proses penurunan:

Kode
#[derive(Accounts)]
struct ExamplePDA<'info> {
	#[account(seeds = [b"example"], bump)]
	pub example_pda: Account<'info, AccountType>,
}

Di sini, batasan seeds digunakan untuk menurunkan PDA. Anchor secara otomatis memverifikasi bahwa akun yang diteruskan ke instruksi cocok dengan PDA yang diturunkan dari seed. Anchor secara default menggunakan bump kanonis saat batasan bump digunakan tanpa nilai tertentu.

Anchor juga memungkinkan seed dinamis berdasarkan field akun lain atau data instruksi. Hal ini dilakukan dengan mereferensikan field lain di dalam struct atau menggunakan makro atribut #[instruction(...)] untuk menyertakan data instruksi yang telah dideserialisasi. Misalnya, dalam struct berikut, example_pda dibatasi untuk menggunakan kombinasi seed statis, data instruksi, dan public key penanda tangan:

Kode
#[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>
}

Kesimpulan

Menyebut Anchor sebagai framework yang canggih masih belum cukup menggambarkan kemampuannya. Kemampuannya dalam menyederhanakan proses pengembangan terlihat jelas dari pembahasan kita tentang berbagai macro dan trait yang digunakan Anchor untuk mengurangi kode. Anchor didukung oleh dokumentasi yang terpelihara dengan baik serta ekosistem tutorial dan crate terkait yang tangguh. Anchor disukai dan digunakan oleh sebagian besar developer Solana.

Artikel ini merupakan panduan yang sangat, sangat komprehensif untuk mengembangkan program dengan Anchor. Artikel ini membahas instalasi Anchor, penggunaan Solana Playground, serta pembuatan, build, dan deployment program Hello, World!. Selanjutnya, kita membahas metode abstraksi efektif Anchor, struktur program Anchor pada umumnya, serta beragam jenis dan batasan account yang tersedia. Artikel ini juga membahas pentingnya mengalokasikan ruang account dan menangani error. Terakhir, kita mempelajari CPI dan PDA. Inilah artikel Anchor yang paling lengkap—semua yang Anda perlukan untuk mulai mengembangkan program di Solana hari ini tersedia di sini.

Jika Anda sudah membaca sejauh ini, terima kasih, anon! Pastikan Anda memasukkan alamat email di bawah agar tidak melewatkan informasi terbaru tentang Solana. Siap mempelajari lebih dalam? Bergabunglah dengan Discord kami untuk mulai mengembangkan program Anchor.

Referensi Tambahan

Berlangganan Helius

Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan

Gambar diperbesar