Bài 4: Viết phần mềm Solana đầu tiên

Bài viết này sẽ hướng dẫn bạn lập trình phần mềm trên Solana blockchain bằng Anchor. Tham khảo ngay!

Bài 4: Viết phần mềm Solana đầu tiên

Đối với lập trình viên, quá trình đi từ ý tưởng đến sản phẩm thực tiễn là một con đường gian nan với vô số rào cản, trong đó phần lập trình thô gây nhàm chán nhất.

Anchor ra đời với các bản soạn sẵn phong phú và cơ số bước kiểm tra bảo mật, giúp developer rút ngắn thời gian lập trình thô và tập trung vào công đoạn quan trọng nhất - phát triển sản phẩm của chính mình.

Tìm hiểu về Anchor

Anchor là gì?

Anchor(1) là một framework giúp các developer phát triển Smart Contract trên Solana nhanh chóng, dễ dàng và an toàn hơn.

Anchor có cách đặc điểm sau:

  • Sử dụng ngôn ngữ Rust(2)
  • IDL để chỉ rõ cách hoạt động, sử dụng chương trình
  • Tự động tạo TypeScript Package từ IDL
  • Hỗ trợ nhiều câu lệnh, CLI ngắn gọn dễ dùng

Cài đặt môi trường

Tìm hiểu thêm thông tin tại (3).

Bước 1. Cài đặt Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustup component add rustfmt

Bước 2. Cài đặt Solana

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

Bước 3. Cài đặt Yarn

npm install -g yarn

Bước 4. Cài đặt Anchor

npm i -g @project-serum/anchor-cli

Tạo ứng dụng đầu tiên

Để khởi tạo một project mới, bạn hãy chạy lệnh sau:

anchor init my_program
cd my_program

Trong đó my_app là tên project của bạn.

Sau khi tạo thành công, cấu trúc project sẽ như sau:

  • programs/my_program/src/lib.rs: Nơi định nghĩa, lập trình Smart Contract
  • programs/my_program/Cargo.toml: Nơi cài đặt các thư viện sử dụng
  • target: Nơi chứa dữ liệu sau khi build. Bao gồm IDL
  • test: Viết test cho Smart Contract

Chạy test chương trình bằng dòng lệnh:

anchor test

Hệ thống sẽ tự động thực thi các bước sau và cho ra kết quả:

  • Build smart contract từ file src/lib.rs
  • Tự động Deploy localhost
  • Ghi kết quả vào thư mục target
  • Chạy file test/my_program.ts → phân tích cấu trúc Smart Contract được build từ target/idl/my_program.json → tạo data phù hợp gọi lên Program đã được tự động deploy ở bước 2
  • Trả về kết quả

Khi chạy anchor test, hệ thống đã tự build giúp mình. Nếu muốn build thủ công, bạn có thể dùng câu lệnh: anchor build

Sau khi build IDL được tạo ra ở thư mục target/idl/my_program.json

Một instruction cũng giống như một hàm thông thường, bao gồm ba thông tin quan trọng là:

  • name: Tên hàm, dùng programe.rpc để gọi
  • accounts: Danh sách tài khoản sẽ tương tác
  • arg: tham số truyền vào hàm

Để hiểu rõ hơn, chúng ta sẽ cùng thực hành một ví dụ: Viết một chương trình tính tổng bao gồm:

Tên hàm

Logic thực thi

Tham số

Accounts

InitializeSum

Thuê tài khoản để lưu kết quả tính tổng

sum_init: Giá trị ban đầu của tổng

sumAccount: địa chỉ lưu kết quả tính tổng

user: Người trả phí thuê tài khoản

systemProgram: địa chỉ chương trình giúp thuê tài khoản 

updateSum

Cập nhật mới tổng

number:  giá trị cộng thêm vào.

sumAccount: địa chỉ lưu kết quả tính tổng

Một hàm như vậy còn được gọi là intruction. Để viết và sử dụng một intruction, chúng ta cần đi qua các bước sau:

  • Khai báo cấu trúc dữ liệu cho trường hợp tạo mới
  • Khai báo các tài khoản tương tác
  • Khai bao intruction trong main

Bước 1. Khởi tạo

1. Khai báo cấu trúc dữ liệu

#[account]
pub struct SumAccount {
   pub sum: u64,
}

2. Khai báo các tài khoản sẽ tương tác

#[derive(Accounts)]
pub struct Initialize<'info> {
   // địa chỉ thuê
   #[account(init, payer = user, space = 8 + 8)]
   pub my_account: Account<'info, MyAccount>,
   // người trả phí giao dịch
   #[account(mut)]
   pub user: Signer<'info>,
   // địa chỉ chương trình giúp thuê tài khoản
   pub system_program: Program<'info, System>,
}

3. Khai báo hàm thực thi

pub fn initialize(ctx: Context<Initialize>, sum_init: u64) -> Result<()> {
       let my_account = &mut ctx.accounts.my_account;
       my_account.sum = sum_init;
       Ok(())
   }

Chương trình của chúng ta sẽ trông như thế này:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod my_program {
   use super::*;
   pub fn initialize_sum(ctx: Context<InitializeSum>, sum_init: u64) -> Result<()> {
       ctx.accounts.sum_account.sum = sum_init;
       Ok(())
   }
}

#[derive(Accounts)]
pub struct InitializeSum<'info> {
   #[account(init, payer = user, space = 8 + 8)]
   pub sum_account: Account<'info, SumAccount>,
   #[account(mut)]
   pub user: Signer<'info>,
   pub system_program: Program<'info, System>,
}

#[account]
pub struct SumAccount {
   pub sum: u64,
}




4. Chạy anchor build để chương trình tạo IDL

"instructions": [
   {
     "name": "initializeSum",
     "accounts": [
       {
         "name": "sumAccount",
         "isMut": true,
         "isSigner": true
       },
       {
         "name": "user",
         "isMut": true,
         "isSigner": true
       },
       {
         "name": "systemProgram",
         "isMut": false,
         "isSigner": false
       }
     ],
     "args": [
       {
         "name": "sumInit",
         "type": "u64"
       }
     ]
   }
 ],

Dựa vào IDL, chúng ta biết được cách sử dụng hàm này từ client bằng typescript. Cập nhật file test như sau:

import { Program, setProvider, web3, Provider, workspace, BN } from "@project-serum/anchor";
import { SystemProgram } from "@solana/web3.js";
import { MyProgram } from "../target/types/my_program";

describe("my_program", () => {
 setProvider(Provider.env());
 const program = workspace.MyProgram as Program<MyProgram>;
 // Tạo địa chỉ thuê
 const sumAccount = web3.Keypair.generate();
 it("Is initialized!", async () => {
   await program.rpc.initializeSum(new BN(1), {
     accounts: {
       sumAccount: sumAccount.publicKey,
       user: program.provider.wallet.publicKey,
       systemProgram: SystemProgram.programId,
     },
     signers: [sumAccount],
   });
   let sumAccountData = await program.account.sumAccount.fetch(sumAccount.publicKey);
   console.log("sumAccountData", sumAccountData.sum);
 });
});



Chạy anchor test cho ra kết quả:

Vậy là chúng ta đã thuê thành công một tài khoản để chứa dữ liệu tính tổng.

Bước 2. Cập nhật dữ liệu

Lúc này, account chứa dữ liệu đã được tạo nên không cần khai báo cấu trúc SumAccount nữa. Cập nhật các tài khoản tương tác và khai báo intruction:

#[derive(Accounts)]
pub struct UpdateSum<'info> {
   #[account(mut)]
   pub sum_account: Account<'info, SumAccount>,
}

  pub fn update_sum(ctx: Context<UpdateSum>, number: u64) -> Result<()> {
       ctx.accounts.sum_account.sum += number;
       Ok(())
   }

Chương trình sẽ trông như thế này:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod my_program {
   use super::*;
   pub fn initialize_sum(ctx: Context<InitializeSum>, sum_init: u64) -> Result<()> {
       ctx.accounts.sum_account.sum = sum_init;
       Ok(())
   }
   pub fn update_sum(ctx: Context<UpdateSum>, number: u64) -> Result<()> {
       ctx.accounts.sum_account.sum += number;
       Ok(())
   }
}

#[derive(Accounts)]
pub struct InitializeSum<'info> {
   #[account(init, payer = user, space = 8 + 8)]
   pub sum_account: Account<'info, SumAccount>,
   #[account(mut)]
   pub user: Signer<'info>,
   pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateSum<'info> {
   #[account(mut)]
   pub sum_account: Account<'info, SumAccount>,
}

#[account]
pub struct SumAccount {
   pub sum: u64,
}



Gọi từ client:

 it("Update sum!", async () => {
   await program.rpc.updateSum(new BN(2), {
     accounts: {
       sumAccount: sumAccount.publicKey,
     },
   });
   let sumAccountData = await program.account.sumAccount.fetch(sumAccount.publicKey);
   console.log("sumAccountData", sumAccountData.sum);
 });

Chạy anchor test và cho ra kết quả:

Sum đã được cập nhật từ 1 lên 3.

Vậy là chúng ta vừa đi qua 2 ví dụ cơ bản của lập trình trên blockchain. Hãy xem video để thực hành và bình luận nếu có thắc mắc nhé!

Các tài liệu tham khảo

  1. https://project-serum.github.io/anchor/getting-started/introduction.html
  2. https://doc.rust-lang.org/book/
  3. https://project-serum.github.io/anchor/getting-started/installation.html

Về Sentre Protocol

Sentre Protocol là nền tảng mở All-in-One hoạt động trên mạng lưới Solana, cung cấp DApp Store và giao thức mở giúp tích lũy thanh khoản. Với Sentre:

  • Người dùng có thể cài đặt các DApp yêu thích của mình và trải nghiệm thế giới DeFi trên một nền tảng duy nhất;
  • Lập trình viên và đối tác có thể phát hành DApp thông qua Sen Store, tận dụng tài nguyên có sẵn và tự do đóng góp cho nền tảng.