Workshop 6: Xây dựng Program với PDAs và SPLT

Tham khảo ngay bài viết này để hiểu rõ hơn về cơ chế và cách sử dụng PDA, seed và bump để làm việc với PDA trong Anchor, init_if_needed constraint và tương tác với SPLT.

Workshop 6: Xây dựng Program với PDAs và SPLT

Bài viết này với tấtc cả các thông tin chi tiết sẽ giúp bạn:

  • Hiểu được cơ chế và cách sử dụng PDA
  • Sử dụng seedbump để làm việc với PDA accounts trong Anchor
  • Sử dụng init_if_needed constraint
  • Tương tác với SPLT

Program Derived Addresses (PDAs)

Program Derived Addresses (PDAs) là những accounts được thiết kế và điều khiển bởi một chương trình nhất định. Với PDAs, chương trình có thể ký từ những địa chỉ nhất định và không cần private key. PDAs đóng vài trò nền tảng cho Cross-Program Invacation cho phép các chương trình có thể kết hợp với nhau.

Đặc điểm

  • PDAs là chuỗi 32 byte tương tự như public keys, nhưng không có private keys.
  • findProgramAddress sẽ suy ra địa chỉ PDA từ programId và seeds (collection of bytes)
  • Một bump được sử dụng để lấy PDA của ed25519 elliptic curve
  • Chương trình có thể ký PDAs của nó bằng cách cung cấp seed và bump cho invoke_signed
  • Một PDA chỉ có thể được ký bởi chính chương trình tạo ra nó.

PDAs với Anchor

Cùng nhớ lại PDAs được suy ra từ danh sách không bắt buộc seeds, bump seed và program ID. Anchor cực kì tiện lợi trong việc xác nhận PDA với seedsbump.

#[derive(Accounts)]
struct ExampleAccounts {
  #[account(
    seeds = [b"example_seed"],
    bump
  )]
  pub pda_account: Account<'info, AccountType>,
}

Trong quá trình xác thực tài khoản, Anchor sẽ suy ra PDA bằng cách sử dụng seeds được chỉ định trong seeds contraint và kiểm tra account trong instruction khớp với PDA định nghĩa.

Anchor sẽ tự động sử dụng.

Bạn có thể chỉ định seeds phụ thuộc vào những account khác như signer's public key.

#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct Example<'info> {
    #[account(
        seeds = [b"example_seed", user.key().as_ref(), instruction_data.as_ref()],
        bump
    )]
    pub pda_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>
}

Nếu địa chỉ pda_account không khớp với PDA được suy ra từ seeds và bump, việc kiểm tra account sẽ thất bại.

Sử dụng PDAs với init constraint

Bạn có thể kết hợp seedsbump contraints với init contraints cho việc khở tạo tài khoản sử dụng PDA.

#[derive(Accounts)]
pub struct InitializePda<'info> {
    #[account(
        init,
        seeds = [b"example_seed", user.key().as_ref()],
        bump,
        payer = user,
        space = 8 + 8
    )]
    pub pda_account: Account<'info, AccountType>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct AccountType {
    pub data: u64,
}

Khi sử dụng init kết hợp với seeds và bump, chủ sở hữu phải là chương trình đang thực thi. Bởi vì khởi tạo account cho PDA đòi hỏi chữ ký mà chỉ có chương trình thi hành mới có thể cung cấp.

Khi định nghĩa giá trị space cho việc khở tạo bởi Anchor program, hay buôn nhớ 8 bytes đầu dành cho việc định danh loại tài khoản. Bạn có thể sử dụng reference cho việc tính toán cần bao nhiêu space.

Sử dụng #[instruction(...)] attribute macro

Khi sử dụng #[instruction(...)], bạn có thể bỏ qua đối số không sử dụng, tuy nhiên bạn phải khai báo đúng thứ tự arguments nếu không bạn sẽ gặp lỗi.

Ví dụ một instruction có các đối số input_one, input_two, và input_three, nếu bạn chỉ cần input_oneinput_three bạn cần phải liệt kê cả 3 đối số trong #[instruction(...)] attribute macro.

Tuy nhiên, nếu bạn chỉ sử dụng input_oneinput_two bạn có thể bỏ qua input_three

pub fn example_instruction(
    ctx: Context<Example>,
    input_one: String,
    input_two: String,
    input_three: String,
) -> Result<()> {
    ...
    Ok(())
}

#[derive(Accounts)]
#[instruction(input_one:String, input_two:String)]
pub struct Example<'info> {
    ...
}

Bạn sẽ gặp lỗi nếu liệt kê không đúng thứ tự:

#[derive(Accounts)]
#[instruction(input_two:String, input_one:String)]
pub struct Example<'info> {
    ...
}

Khi initialize instruction được gọi trong ví dụ, Anchor sẽ kiểm tra nếu token_account tồn tại và khởi tại nếu chưa. Nếu đã tồn tại, instruction sẽ tiếp tục mà không cần khởi tạo tài khoản. Cũng tương tự như init, cũng có thể sử dụng init_if_needed kết hợp với seedsbump nếu là account PDA.

Init-if-needed

Anchor cung cấp init_if_needed constraint để tạo account nêys account chưa được khởi tạo. Khi sử dụng init_if_needed, bạn cần đảm bảo bảo vệ đúng cách chương trình của mình trước các cuộc tấn công tái khởi tạo. Bạn cần bao gồm các kiểm tra trong mã để kiểm tra rằng tài khoản đã được khởi tạo không thể được đặt lại về cài đặt ban đầu sau lần đầu tiên nó được khở tạo.

Để sử dụng tính init_if_needed, trước tiên bạn cần bật tính năng này trong Cargo.toml.

[dependencies]
anchor-lang = { version = "0.25.0", features = ["init-if-needed"] }

Khi đã bật tính năng này, bạn có thể sử dụng chúng trong #[account(…)] attribute macro. Đây là một ví dụ sử dụng init_if_needed để khởi tạo associated token account nếu chưa tồn tại.

#[program]
mod example {
    use super::*;
    pub fn transfer_token(ctx: Context<TransferToken>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct TransferToken<'info> {
    #[account(mut)]
    pub src_token_account: Account<'info, TokenAccount>,
    #[account(
        init_if_needed,
        payer = payer,
        associated_token::mint = mint,
        associated_token::authority = payer
    )]
    pub dst_token_account: Account<'info, TokenAccount>,
    pub mint: Account<'info, Mint>,
     #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
}



SPLT với Anchor

  • SPL-Tokens đại diện cho tất cả các tokens trên mạng Solana kể cả NFTs.
  • Token Program là chương trình chứa các instructions giúp tạo và tương tác với SPL-Tokens
  • Mint Account là tài khoản lưu trữ thông tin về một Token cụ thể, nhưng không chứa Tokens.
  • Token Accounts được sử dụng để chứa Tokens của một Token Mint cụ thể
  • Việc tại Token MintsToken Accounts bắt buộc sẽ tốn một khoản fee SOL, tuy nhiên tiền thuê Token Account sẽ được hoàn trả lại khi tài khoản bị đóng.
  • Hiện tại Token Mint không thể bị đóng.

Bạn có thể tìm hiểu sâu hơn về Token Program ở Tham khảo

Cài đặt

Để sử dụng Anchor SPL, trước tiên cần khai báo dependencies trong Cargo.toml.

[dependencies]
anchor-spl = { version = "0.25.0", features = ["default"] }

Khi đã bật tính năng này, bạn có thể sử dụng anchor_spl.

Thử nghiệm

Trong thử nghiệm này, chúng ta sẽ cũng xây dựng một chương trình như sau:

  • Chương trình tạo ra mint mới  và in một lượng cho người tạo
  • User là người ký transaction, người thanh toán cho các khoản fee khởi tạo
  • Mint là PDA account, được tạo thành từ seeds: “mint", địa chỉ user và số lượng in.
    Đầu tiên tại một project mới bằng anchor init.
anchor init anchor-program

Khai báo dependencies trong file Cargo.toml

[dependencies]
anchor-lang = { version = "0.25.0", features = ["init-if-needed"] }
anchor-spl = { version = "0.25.0", features = ["default"] }

Tiếp theo, cập nhật lại file lib.rs trong folder programs như sau:

use anchor_lang::prelude::*;
use anchor_spl::{associated_token, token};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

}

Hiện thực Context InitializeMint

#[derive(Accounts)]
#[instruction(amount:u64)]
pub struct InitializeMint<'info> {
  #[account(mut)]
  pub user: Signer<'info>,

  #[account(
      init,
      seeds = [
        b"mint",
        user.key().as_ref(),
        &amount.to_le_bytes()
      ],
      bump,
      payer = user,
      mint::decimals = 9,
      mint::authority = mint,
  )]
  pub mint: Account<'info, token::Mint>,

  #[account(
      init_if_needed,
      payer = user,
      associated_token::mint = mint,
      associated_token::authority = user
  )]
  pub token_account: Account<'info, token::TokenAccount>,

  // Program
  pub system_program: Program<'info, System>,
  pub token_program: Program<'info, token::Token>,
  pub associated_token_program: Program<'info, associated_token::AssociatedToken>,
  pub rent: Sysvar<'info, Rent>,
}

Hiện thực initializeMint instruction

pub fn initialize_mint(ctx: Context<InitializeMint>, amount: u64) -> Result<()> {
    let seeds: &[&[&[u8]]] = &[&[
      "mint".as_ref(),
      &ctx.accounts.user.key().to_bytes(),
      &amount.to_le_bytes(),
      &[*ctx.bumps.get("mint").unwrap()],
    ]];

    let mint_to_ctx = CpiContext::new_with_signer(
      ctx.accounts.token_program.to_account_info(),
      token::MintTo {
        to: ctx.accounts.token_account.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        authority: ctx.accounts.mint.to_account_info(),
      },
      seeds,
    );

    token::mint_to(mint_to_ctx, amount)?;

    Ok(())
  }

Build

Chạy anchor build để build chương trình

anchor build

Testing

Di chuyển đến file anchor-program.ts và thay thế đoạn mã mặc định thành như sau:


   const [mint] = anchor.web3.PublicKey.findProgramAddressSync(
     [
       Buffer.from('mint'),
       provider.wallet.publicKey.toBuffer(),
       amount.toArrayLike(Buffer, 'le', 8),
     ],
     program.programId,
   )

   const tokenAccount = await anchor.utils.token.associatedAddress({
     mint,
     owner: provider.wallet.publicKey,
   })
   // Add your test here.
   const tx = await program.methods
     .initializeMint(amount)
     .accounts({
       user: provider.wallet.publicKey,
       mint,
       tokenAccount,
       tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
       associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
       systemProgram: anchor.web3.SystemProgram.programId,
       rent: anchor.web3.SYSVAR_RENT_PUBKEY,
     })
     .rpc()
   console.log('Your transaction signature', tx)
 })
})


Cuối cùng, chạy anchor test và bạn sẽ thấy kết quả tương tự như sau:

 anchor-program
Your transaction signature 2ixreMMMnCzwxD6h5pJMtfSvjKVn39SbMajjUhgBDF1scBHNi4uygdVnn8FhszhSLEiu89TjJhJQbuW3wKoZeD6r
    ✔ Is initialized mint! (196ms)


  1 passing (199ms)

✨  Done in 6.09s.

Bạn có thể tham khảo mã nguồn  tại đây.

Các bài viết liên quan

Workshop 1: Introduction to Solana

Workshop 2: Xây dựng giao diện tương tác với Solana

Workshop 3: Client interaction with common Solana programs (SPLT, Metaplex)

Workshop 4: Use the Rust programming language, Anchor

Về Sentre Protocol

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

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