Published on
Solana

เขียน Solana Program ด้วย Anchor Framework

hello-anchor-solana

Anchor เป็น Solana Framework สำหรับเขียน Program (Smart Contract) ที่ช่วยให้เขียน Solana Program ง่ายและสะดวกขึ้น ปกติถ้าเราไม่ใช้ Anchor เวลาที่เราจะใช้ Client SDK ติดต่อ Program ต้องกำหนด Schema ทำ Deserialize/Serialize ซึ่งค่อนข้างยุ่งยาก เหมือนกับบทความที่แล้ว ที่ผมเขียนไว้ มาลองหัดเขียน Smart Contract บน Solana กัน ด้วยแอพ Hello World

ข้อดีของ Anchor คือ

  • Rust eDSL สำหรับเขียน Solana Program
  • Generate ไฟล์ IDL คล้ายๆกับ ABI เพื่อเอาไว้ติดต่อกับ Program ผ่าน JSON RPC.
  • คล้ายๆกับ Truffle/ web3.js หรือ Hardhat/ethers.js
  • รองรับ Rust ที่เป็น Official Library และ TypeScript (ฝั่ง Client)

Anchor ยังอยู่ในขั้นตอนการพัฒนา ฉะนั้น API หรือโค๊ดต่างๆ อาจจะมีการเปลี่ยนแปลงได้ตลอดเวลา ฉะนั้นดูเวอร์ชั่นที่ใช้งานด้วยนะครับ และก็ลองดู Changelog ว่ามี breaking changes อะไรมั้ย

Prerequisites

  • ควรมีพื้นฐาน Rust เบื้องต้นครับ - แนะนำลองอ่านนี้ประมาณ 30-60 นาที Tour of Rust
  • เข้าใจ Solana Program เบื้องต้น (รู้ว่า transactions, instructions, program, account คืออะไร) - อ่านเพิ่มเติม
  • ใช้งาน JavaScript / TypeScript เบื้องต้นได้ (ต้องใช้เขียนฝั่ง Client เพื่อต่อ JSON RPC)
  • ติดตั้ง Rust, Solana CLI, Yarn และ Node.js เรียบร้อยแล้ว ถ้าไม่มีแนะนำ Step 1 - ติดตั้งโปรแกรม Solana

หนังสือ Anchor Framework

  • The Anchor Book - น่าจะเป็น Official book แล้วแทน docs เก่า ปัจจุบัน v0.23.0
  • Getting Started - Anchor - Doc เวอร์ชั่นก่อนหน้านี้ ซึ่งคาดว่าน่าจะย้ายไปเขียนใน Anchor Book แทน แต่เนื้อหาทั้งสอง ณ​ ตอนนี้ก็ยังอ่านได้ครับ

ติดตั้ง Anchor

ขั้นตอนการติดตั้ง Anchor เราจะใช้ avm (Anchor Version Manager) นะครับ เผื่ออนาคตมีเวอร์ชั่นใหม่ๆ เราสามารถสลับเวอร์ชั่นได้ง่ายๆ

ติดตั้ง avm ก่อน

cargo install --git https://github.com/project-serum/anchor avm --locked --force

จากนั้นใช้ avm เพื่อติดตั้ง Anchor เวอร์ชั่นล่าสุด แล้ว set Anchor เป็น latest

avm install latest
avm use latest

ทดลองเช็คว่า Anchor ติดตั้งเรียบร้อยมั้ย

anchor --Version

# ผลลัพธ์ ณ วันที่เขียนบทความ
anchor-cli 0.23.0

สร้างโปรเจ็คด้วย Anchor

เราจะใช้คำสั่ง anchor init <program_name> เพื่อสร้างโปรเจ็คขึ้นมาใหม่ด้วย Anchor นะครับ ตัวอย่างผมตั้งชื่อโปรเจ็คว่า hello-anchor ก็จะได้เป็นแบบนี้

anchor init hello-anchor

ตัว Anchor จะทำการ generate folder ให้เรา โครงสร้างไฟล์ประมาณนี้

tree -L 4 -I node_modules

├── Anchor.toml
├── Cargo.toml
├── app
├── migrations
│   └── deploy.ts
├── package.json
├── programs
│   └── hello-anchor
│       ├── Cargo.toml
│       ├── Xargo.toml
│       └── src
│           └── lib.rs
├── tests
│   └── hello-anchor.ts
├── tsconfig.json
└── yarn.lock

6 directories, 10 files
  • programs - โฟลเดอร์นี้จะเป็นไฟล์ Solana Program ของเรา
  • Anchor.toml และ Cargo.toml - เป็นไฟล์ config ของ Cargo และ Anchor ครับ ตัว Cargo.toml ข้างนอกแค่ระบุ workspace ส่วน metadata จะอยู่ที่ programs/hello-anchor/Cargo.toml ครับ
  • tests - ไฟล์สำหรับ test

ข้างในไฟล์ hello-anchor มีอะไรบ้างนะ

programs/hello-anchor/src/lib.rs
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod hello_anchor {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}
  • use anchor_lang::prelude::*; - เป็นเหมือนกับการ import macro และ attributes ต่างๆ ของ Anchor
  • declare_id! - กำหนด Program addrses เพื่อเอาไว้ให้ Anchor generate
  • #[program] - เป็น attribute ที่กำหนดให้เป็น entrypoint ของ Program และ function ข้างใน ก็จะเอาไว้ handle RPC request.
  • Context<Initialize> - เป็น paramter แรก ของทุกๆ RPC handler ต้องมี โดยเป็น Generic ตาม struct ที่เรากำหนด ตัวอย่างคือ struct Initialize
  • #[derive(Accounts)] - attribute นี้รับ Accounts macro มองง่ายๆ คือ ช่วยให้ struct นี้ deserialized input accounts ได้

ทดลอง build:

anchor build

 --> programs/hello-anchor/src/lib.rs:9:23
  |
9 |     pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
  |                       ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `hello-anchor` (lib) generated 1 warning
    Finished release [optimized] target(s) in 0.39s

จะเห็นว่า build ผ่าน แค่มี warning เพราะมีตัวแปรที่ไม่ได้ใช้ เฉยๆ

ต่อมาสร้าง Wallet (Keypair) ขึ้นมา เพื่อเอาไว้ใช้ทดสอบ ตัวอย่างผมสร้างไฟล์ชื่อ dev-wallet.json

solana-keygen new -o ./dev-wallet.json

ไม่ควรนำ Wallet ที่ generate ไปใช้งานจริงนะครับ แต่ถ้าจะใช้งานจริงๆ ห้ามเปิดเผยไฟล์ dev-wallet.json รวมถึงอย่าลืมจด seed phrase ไว้ด้วยนะครับ

จากนั้นไฟล์ Anchor.toml ให้เปลี่ยน wallet เป็นกระเป๋าที่เราเพิ่งสร้าง

Anchor.toml
[provider]
cluster = "localnet"
wallet = "./dev-wallet.json"

จากนั้น Start local cluster validator ครับ

solana-test-validator

ถ้าเราไม่ได้ใช้ test-ledger เดิม หรือ account เราไม่มี Sol ก็ต้องทำการ airdrop ให้มันก่อนนะครับ

solana airdrop 10 $(solana address -k ./dev-wallet.json)
# มันคือการแสดง address จากไฟล์ dev-wallet.json และ solana airdrop 10 <address> นะครับ

จากนั้นลอง Deploy ด้วย anchor

anchor deploy

ผลลัพธ์หน้าตาประมาณนี้

Deploying workspace: http://localhost:8899
Upgrade authority: ./dev-wallet.json
Deploying program "hello-anchor"...
Program path: /your-folder/hello-anchor/target/deploy/hello_anchor.so...
Program Id: GXFtM6h99kckybSfyLBDPwh583mDKjSZqTqDYHR5ix5Z

Deploy success

เอา Program Id ที่ได้ อย่างตัวอย่างคือ GXFtM6h99kckybSfyLBDPwh583mDKjSZqTqDYHR5ix5Z ไปเปลี่ยนที่ไฟล์ Anchor.toml และ lib.rs และเดี๋ยวจะเอาไปใช้ในไฟล์ Client ด้วย

Anchor.toml
[features]
seeds = false
[programs.localnet]
hello_anchor = "GXFtM6h99kckybSfyLBDPwh583mDKjSZqTqDYHR5ix5Z"
src/lib.rs
declare_id!("GXFtM6h99kckybSfyLBDPwh583mDKjSZqTqDYHR5ix5Z");

จากนั้น Build อีกรอบ

anchor build

สร้างไฟล์ app/client.js

ต่อมาสร้างไฟล์ app/client.js เพื่อติดต่อกับ Program ที่เราเขียน

หากใครที่ใช้ Anchor v0.24.x อาจจะมีบางฟังค์ชั่นที่ไม่ตรงกัน แนะนำอ่าน - Notes - อัพเดท Anchor v0.24 เพิ่มเติมครับ

app/client.js
const anchor = require("@project-serum/anchor");
require("dotenv").config();

// Configure the local cluster.
anchor.setProvider(anchor.Provider.local());

async function main() {
  // #region main
  // Read the generated IDL.
  const idl = JSON.parse(
    require("fs").readFileSync("./target/idl/hello_anchor.json", "utf8")
  );

  // Address of the deployed program.
  const programId = new anchor.web3.PublicKey(
    "<YOUR_PROGRAM_ID>"
  );

  // Generate the program client from IDL.
  const program = new anchor.Program(idl, programId);

  // Execute the RPC.
  await program.methods.initialize().rpc();
  // #endregion main
}

console.log("Running client.");
main().then(() => console.log("Success"));
  • สิ่งที่ต้องเปลี่ยนคือ <YOUR_PROGRAM_ID> - เป็น Program Id ของเราครับ
  • ดูว่า file target IDL ตัว path ถูกต้องหรือไม่ ถ้าตั้งชื่อโปรเจ็คคนละชื่อ ต้องเปลี่ยนด้วยนะครับ

สุดท้ายลองรันด้วยคำสั่ง

ANCHOR_WALLET=./dev-wallet.json node app/client.js

จะได้ผลลัพธ์

Running client.
Success

จะเห็นว่า ตัว Client เราต้องใช้ ANCHOR_WALLET เวลาที่เรา setProvider ฉะนั้นย้ายไปใช้ .env แทน น่าจะสะดวกกว่า

yarn add dotenv --dev

สร้างไฟล์ .env และเก็บค่า

.env
ANCHOR_WALLET=./dev-wallet.json

ไฟล์ app/client.js ก็ให้โหลด dotenv ก่อน ทีนี้เวลารัน client ก็ให้อ่านจาก .env แทน

app/client.js
const anchor = require("@project-serum/anchor");
require("dotenv").config();

ลองรันใหม่

node app/client.js

ลอง test

ต่อมาสุดท้ายแล้ว ลองมาดูไฟล์เทส ที่ตัว Anchor generate มาให้ครับ

tests/hello-anchor.ts
import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";

describe("hello-anchor", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.Provider.env());

  const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;

  it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.rpc.initialize({});
    console.log("Your transaction signature", tx);
  });
});
  • มีการเรียก HelloAnchor ที่เป็น type ที่ Anchor auto generate ให้
  • anchor.workspace.* สามารถเรียก program instance ได้จากคำสั่งนี้เลย (workspace ใช้ได้เฉพาะตอนใช้ anchor test, build และ deploy นะครับ)

ลองเทส

anchor test

จะเห็นว่าเวลาเรารัน anchor test ไม่ต้องใช้ solana-test-validator เพราะตัว anchor มันจะรัน local cluster ให้ตอนเทส แต่ถ้าเราเรียก node app/client.js เราต้องมี local cluster เอง

พอมาดูไฟล์ใน target/types จะเห็นว่า Anchor ทำการ generate ทั้ง Type แล้วก็ IDL ให้เราแล้ว

export type HelloAnchor = {
  version: '0.1.0';
  name: 'hello_anchor';
  instructions: [
    {
      name: 'initialize';
      accounts: [];
      args: [];
    }
  ];
};

export const IDL: HelloAnchor = {
  version: '0.1.0',
  name: 'hello_anchor',
  instructions: [
    {
      name: 'initialize',
      accounts: [],
      args: []
    }
  ]
};

ถ้าเขียน Solidity มา ก็จะคุ้นๆ ว่ามันคล้ายๆ ABI เลย

สุดท้าย เปลี่ยนไปใช้ methods ซักนิด พอดีเห็นว่ามันแจ้งเตือน deprecated อนาคต อาจจะอัพเดทแล้วเอาออกไป

จาก

tests/hello-anchor.ts
const tx = await program.rpc.initialize({});

เป็น

tests/hello-anchor.ts
const tx = await program.methods.initialize().rpc();

สุดท้ายไฟล์ tests/hello-anchor.ts

tests/hello-anchor.ts
import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";

describe("hello-anchor", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.Provider.env());

  const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;

  it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
  });
});

ลองเทสใหม่ ผลลัพธ์ต้องเหมือนเดิม

anchor test

p ./tsconfig.json -t 1000000 'tests/**/*.ts'

  hello-anchor
Your transaction signature 463wu91g6KnFYr2J82u5d5DZaGgbBH4iuQV7XV3vRcmLC74dHQwQHcKHak4hm9RYavtfBxQNgZ9STsV1McBXXUH7
    ✔ Is initialized! (158ms)

  1 passing (160ms)

🎉 เป็นอันเรียบร้อย โปรเจ็คนี้ก็เป็น Basic เริ่มต้น ยังไม่มีอะไรมาก แค่เห็น flow การทำงานของมัน เดี๋ยวบทความหน้า จะมีการรับ Input และเก็บ state (Account) นะครับ

สรุป

จะเห็นว่าใช้ Anchor แล้วดูสะดวกสบายหลายๆอย่างเลย บทความนี้ก็เป็น Example ให้เห็นภาพ Anchor ง่ายๆ ครับ หากใครสนใจลองอ่านเพิ่มเติมครับ

หรือถ้าใครอยาก challenge ก็ลองดู Example basic1-4 ดูเพิ่มเติมครับ สุดท้าย ถ้าใครเคยเขียน Web3/Etheres.js มาเห็นส่วน Client ก็น่าจะเข้าใจไม่ยากครับ

Happy Coding ❤️

Authors
Discord