Account Maps

Map là một kiểu cấu trúc thường dùng trong lập trình và bao gồm một key tương ứng với một value. Cặp key-value có thể là bất kỳ kiểu dữ liệu nào trong đó key như là chìa khoá định danh dữ liệu được lưu trong value. Do đó, với mỗi key, chũng ta có thể thêm, xoá, cập nhật dữ liệu vào value một cách hiệu quả.

Mô hình Account trong Solana, như đã biết, yêu cầu dữ liệu của Program và các trạng thái của nó phải được lưu ở những Account riêng biệt. Những Account này có một địa chỉ định danh tương ứng và mô hình đó rất giống với map! Tìm hiểu thêm về mô hình Account trong Solana tại đâyopen in new window.

Như vậy, dễ hiểu khi mà ta lưu values vào những Account tách biệt và dùng key để truy vấn dữ liệu trong values. Tuy nhiên, điều này lại gây ra một số vấn đề như là:

  • Những địa chỉ đề cập hầu hết không phải là một keys lý tưởng, khi mà bạn cần phải ghi nhớ tất cả chúng để truy vấn các dữ liệu tương ứng.

  • Những địa chỉ đề cập bên trên được tham chiếu bằng khoá công khai của những Keypairs (cặp khoá) khác nhau, trong đó khoá công khai bắt buộc phải có khoá riêng tư tương ứng. Khoá riêng tư lại cần thiết để ký các chỉ thị và lại bắt buộc chúng ta phải lưu ở một nơi nào đó, điều mà thật sự không được khuyến khích trong thực tiễn.

Điếu đó dẫn đến rất nhiều vấn đề cho lập trình viên muốn hiện thực Map trực tiếp vào Program trên Solana. Giờ hãy quan sát một vài cách để giải quyết vấn đề trên.

Tìm PDA

PDA là viết tắt Program Derived Addressopen in new window. Chúng là những địa chỉ được tìm thấy thông qua tập hợp gồm seedsprogram_id.

Điểm đặc biệt của PDA là chúng không tồn tại khoá riêng tư tương ứng. Điều này bởi vì những địa chỉ này không nằm trên đường cong ED25519. Vì vậy, duy nhất Program sinh ra PDA mới có thể ký lên các chỉ thị cho các PDA đó bằng seeds. Tìm hiểu thêm tại đâyopen in new window.

Sau khi đã nắm được khái niệm PDA, chúng ta có thể sử dụng để tạo kiểu Map! Hãy lấy ví dụ một Blog Program để hiểu rõ hơn cách sử dụng.

Trong Blog Program, chúng ta muốn mỗi User sẽ có một trang Blog. Bài blog có thể có nhiều Posts. Cụ thể hơn, mỗi User sẽ map đến một trang Blog. Nhiều bài Posts sẽ được map về một trang Blog.

User sẽ có kết nối 1:1 với Blog trong khi Blog sẽ có kết nối 1:N với Posts.

Với 1:1, chúng ta mong mốn địa chỉ của trang blog có thể được suy ra độc nhất từ địa chỉ người dùng. Cơ chế này sẽ giúp chúng ta lấy được dữ liệu của blog khi biết được địa chỉ chử sở hữu blog đó. Hiển nhiên, seeds cho Blog phải chứa địa chỉ chủ sở hữu, và có thể thêm một tiền tố như "blog" để giúp chú thích.

Với 1:N, chúng ta mong muốn địa chỉ mỗi bài post sẽ được suy ra từ không chỉ địa chỉ trang blog mà còn từ cách thành tố khác giúp tạo ra N địa chỉ bài post trong một trang blog. Trong ví dụ bên dưới, mỗi địa chỉ bài post được suy ra bằng địa chỉ trang blog, một thành tố phụ - slug - để định danh cho mỗi bài post, và tiền tố "post" để chú thích.

Code mẫu được viết như sau:

Press </> button to view full source
use anchor_lang::prelude::*;

declare_id!("2vD2HBhLnkcYcKxnxLjFYXokHdcsgJnyEXGnSpAX376e");

#[program]
pub mod mapping_pda {
    use super::*;
    pub fn initialize_blog(ctx: Context<InitializeBlog>, _blog_account_bump: u8, blog: Blog) -> ProgramResult {
        ctx.accounts.blog_account.set_inner(blog);
        Ok(())
    }

    pub fn create_post(ctx: Context<CreatePost>, _post_account_bump: u8, post: Post) -> ProgramResult {
        if (post.title.len() > 20) || (post.content.len() > 50) {
            return Err(ErrorCode::InvalidContentOrTitle.into());
        }

        ctx.accounts.post_account.set_inner(post);
        ctx.accounts.blog_account.post_count += 1;

        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(blog_account_bump: u8)]
pub struct InitializeBlog<'info> {
    #[account(
        init,
        seeds = [
            b"blog".as_ref(),
            authority.key().as_ref()
        ],
        bump = blog_account_bump,
        payer = authority,
        space = Blog::LEN
    )]
    pub blog_account: Account<'info, Blog>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>
}

#[derive(Accounts)]
#[instruction(post_account_bump: u8, post: Post)]
pub struct CreatePost<'info> {
    #[account(mut, has_one = authority)]
    pub blog_account: Account<'info, Blog>,

    #[account(
        init,
        seeds = [
            b"post".as_ref(),
            blog_account.key().as_ref(),
            post.slug.as_ref(),
        ],
        bump = post_account_bump,
        payer = authority,
        space = Post::LEN
    )]
    pub post_account: Account<'info, Post>,

    #[account(mut)]
    pub authority: Signer<'info>,
    
    pub system_program: Program<'info, System>
}

#[account]
pub struct Blog {
    pub authority: Pubkey,
    pub bump: u8,
    pub post_count: u8,
}

#[account]
pub struct Post {
    pub author: Pubkey,
    pub slug: String, // 10 characters max
    pub title: String, // 20 characters max
    pub content: String // 50 characters max
}

impl Blog {
    const LEN: usize = 8 + 32 + 1 + (4 + (10 * 32));
}

impl Post {
    const LEN: usize = 8 + 32 + 32 + (4 + 10) + (4 + 20) + (4 + 50); 
}

#[error]
pub enum ErrorCode {
    #[msg("Invalid Content or Title.")]
    InvalidContentOrTitle,
}
use std::convert::TryInto;
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    sysvar::{rent::Rent, Sysvar},
    borsh::try_from_slice_unchecked,
    account_info::{AccountInfo, next_account_info},
    entrypoint,
    entrypoint::ProgramResult, 
    pubkey::Pubkey, 
    msg,
    program_error::ProgramError, system_instruction, program::invoke_signed,
};
use thiserror::Error;


entrypoint!(process_instruction);
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    Processor::process(program_id, accounts, instruction_data)
}

pub enum BlogInstruction {

    /// Accounts expected:
    /// 
    /// 0. `[signer]` User account who is creating the blog
    /// 1. `[writable]` Blog account derived from PDA
    /// 2. `[]` The System Program
    InitBlog {},

    /// Accounts expected:
    /// 
    /// 0. `[signer]` User account who is creating the post
    /// 1. `[writable]` Blog account for which post is being created
    /// 2. `[writable]` Post account derived from PDA
    /// 3. `[]` System Program
    CreatePost {
        slug: String,
        title: String,
        content: String,
    }
}

pub struct Processor;
impl Processor {
    pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
        
        let instruction = BlogInstruction::unpack(instruction_data)?;

        match instruction {
            BlogInstruction::InitBlog {} => {
                msg!("Instruction: InitBlog");
                Self::process_init_blog(accounts, program_id)
            },
            BlogInstruction::CreatePost { slug, title, content} => {
                msg!("Instruction: CreatePost");
                Self::process_create_post(accounts, slug, title, content, program_id)
            }
        }
    }

    fn process_create_post(
        accounts: &[AccountInfo],
        slug: String,
        title: String,
        content: String,
        program_id: &Pubkey
    ) -> ProgramResult {
        if slug.len() > 10 || content.len() > 20 || title.len() > 50 {
            return Err(BlogError::InvalidPostData.into())
        }

        let account_info_iter = &mut accounts.iter();

        let authority_account = next_account_info(account_info_iter)?;
        let blog_account = next_account_info(account_info_iter)?;
        let post_account = next_account_info(account_info_iter)?;
        let system_program = next_account_info(account_info_iter)?;

        if !authority_account.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }

        let (blog_pda, _blog_bump) = Pubkey::find_program_address(
            &[b"blog".as_ref(), authority_account.key.as_ref()],
            program_id
        );
        if blog_pda != *blog_account.key || !blog_account.is_writable || blog_account.data_is_empty() {
            return Err(BlogError::InvalidBlogAccount.into())
        }

        let (post_pda, post_bump) = Pubkey::find_program_address(
            &[b"post".as_ref(), slug.as_ref(), authority_account.key.as_ref()],
            program_id
        );
        if post_pda != *post_account.key {
            return Err(BlogError::InvalidPostAccount.into())
        }

        let post_len: usize = 32 + 32 + 1 + (4 + slug.len()) + (4 + title.len()) + (4 + content.len());

        let rent = Rent::get()?;
        let rent_lamports = rent.minimum_balance(post_len);

        let create_post_pda_ix = &system_instruction::create_account(
            authority_account.key,
            post_account.key,
            rent_lamports,
            post_len.try_into().unwrap(),
            program_id
        );
        msg!("Creating post account!");
        invoke_signed(
            create_post_pda_ix, 
            &[
                authority_account.clone(),
                post_account.clone(),
                system_program.clone()
            ],
            &[&[
                b"post".as_ref(),
                slug.as_ref(),
                authority_account.key.as_ref(),
                &[post_bump]
            ]]
        )?;

        let mut post_account_state = try_from_slice_unchecked::<Post>(&post_account.data.borrow()).unwrap();
        post_account_state.author = *authority_account.key;
        post_account_state.blog = *blog_account.key;
        post_account_state.bump = post_bump;
        post_account_state.slug = slug;
        post_account_state.title = title;
        post_account_state.content = content;

        msg!("Serializing Post data");
        post_account_state.serialize(&mut &mut post_account.data.borrow_mut()[..])?;


        let mut blog_account_state = Blog::try_from_slice(&blog_account.data.borrow())?;
        blog_account_state.post_count += 1;

        msg!("Serializing Blog data");
        blog_account_state.serialize(&mut &mut blog_account.data.borrow_mut()[..])?;

        Ok(())
    }

    fn process_init_blog(
        accounts: &[AccountInfo],
        program_id: &Pubkey
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        
        let authority_account = next_account_info(account_info_iter)?;
        let blog_account = next_account_info(account_info_iter)?;
        let system_program = next_account_info(account_info_iter)?;

        if !authority_account.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }

        let (blog_pda, blog_bump) = Pubkey::find_program_address(
            &[b"blog".as_ref(), authority_account.key.as_ref()],
            program_id 
        );
        if blog_pda != *blog_account.key {
            return Err(BlogError::InvalidBlogAccount.into())
        }

        let rent = Rent::get()?;
        let rent_lamports = rent.minimum_balance(Blog::LEN);
        
        let create_blog_pda_ix = &system_instruction::create_account(
            authority_account.key,
            blog_account.key,
            rent_lamports,
            Blog::LEN.try_into().unwrap(),
            program_id
        );
        msg!("Creating blog account!");
        invoke_signed(
            create_blog_pda_ix, 
            &[
                authority_account.clone(),
                blog_account.clone(),
                system_program.clone()
            ],
            &[&[
                b"blog".as_ref(),
                authority_account.key.as_ref(),
                &[blog_bump]
            ]]
        )?;

        let mut blog_account_state = Blog::try_from_slice(&blog_account.data.borrow())?;
        blog_account_state.authority = *authority_account.key;
        blog_account_state.bump = blog_bump;
        blog_account_state.post_count = 0;
        blog_account_state.serialize(&mut &mut blog_account.data.borrow_mut()[..])?;
        

        Ok(())
    }
}



#[derive(BorshDeserialize, Debug)]
struct PostIxPayload {
    slug: String,
    title: String,
    content: String
}


impl BlogInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
        let (variant, rest) = input.split_first().ok_or(BlogError::InvalidInstruction)?;
        let payload = PostIxPayload::try_from_slice(rest).unwrap();

        Ok(match variant {
            0 => Self::InitBlog {},
            1 => Self::CreatePost {
                slug: payload.slug,
                title: payload.title,
                content: payload.content
            },
            _ => return Err(BlogError::InvalidInstruction.into()),
        })
    }
}

#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
pub struct Blog {
    pub authority: Pubkey,
    pub bump: u8,
    pub post_count: u8 // 10 posts max
}

#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
pub struct Post {
    pub author: Pubkey,
    pub blog: Pubkey,
    pub bump: u8,
    pub slug: String, // 10 chars max
    pub title: String, // 20 chars max
    pub content: String, // 50 chars max
}

impl Blog {
    pub const LEN: usize = 32 + 1 + 1;
}

#[derive(Error, Debug, Copy, Clone)]
pub enum BlogError {
    #[error("Invalid Instruction")]
    InvalidInstruction,

    #[error("Invalid Blog Account")]
    InvalidBlogAccount,

    #[error("Invalid Post Account")]
    InvalidPostAccount,

    #[error("Invalid Post Data")]
    InvalidPostData,

    #[error("Account not Writable")]
    AccountNotWritable,
}

impl From<BlogError> for ProgramError {
    fn from(e: BlogError) -> Self {
        return ProgramError::Custom(e as u32);
    }
}

Ở phía người dùng, họ có thể sử dụng PublicKey.findProgramAddress() để tìm ra địa chỉ của BlogPost mong muốn thông qua địa chỉ ví đầu vào. Các địa chỉ vừa tìm thấy có thể được truyền vào connection.getAccountInfo() để truy vấn dữ liệu trong Account tương ứng. Ví dụ bên dưới sẽ minh hoạ điều đó:

Press </> button to view full source
import * as borsh from "@project-OpenBook/borsh";
import { PublicKey } from "@solana/web3.js";

export const BLOG_ACCOUNT_DATA_LAYOUT = borsh.struct([
  borsh.publicKey("authorityPubkey"),
  borsh.u8("bump"),
  borsh.u8("postCount"),
]);

export const POST_ACCOUNT_DATA_LAYOUT = borsh.struct([
  borsh.publicKey("author"),
  borsh.publicKey("blog"),
  borsh.u8("bump"),
  borsh.str("slug"),
  borsh.str("title"),
  borsh.str("content"),
]);

async () => {
  const connection = new Connection("http://localhost:8899", "confirmed");

  const [blogAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("blog"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
  );

  const [postAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("post"), Buffer.from("slug-1"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
  );

  const blogAccountInfo = await connection.getAccountInfo(blogAccount);
  const blogAccountState = BLOG_ACCOUNT_DATA_LAYOUT.decode(
    blogAccountInfo.data
  );
  console.log("Blog account state: ", blogAccountState);

  const postAccountInfo = await connection.getAccountInfo(postAccount);
  const postAccountState = POST_ACCOUNT_DATA_LAYOUT.decode(
    postAccountInfo.data
  );
  console.log("Post account state: ", postAccountState);
};

Map bằng Account đơn

Một cách khác hơn đển hiện thực map là dùng cấu trúc BTreeMap để lưu dữ liệu lên một Account duy nhất. Địa chỉ của Account này có thể là PDA, hoặc có thể là khoá công khai của một cặp khoá được sinh ra thủ công.

Phương pháp này thường có một vài hạn chế:

  • Bước đầu tiên phải khởi tạo Account để lưu BTreeMap trước khi có thể thêm bất kỳ key-value nào vào bên trong nó. Sau đó, bạn sẽ phải lưu địa chỉ này một nơi nào đó để dùng cho việc cập nhật dữ liệu về sau.

  • Có nhiều giới hạn lưu trữ cho một Account như là dung lượng tối đa của một Account là 10 megabytes và không thể cho phép BTreeMap có thể lưu trữ một số lượng lớn các cặp key-value.

Tuy vào tính huống của riêng ứng dụng, bạn có thể cân nhắc sử dụng nó như sau:

Press </> button to view full source
use std::{collections::BTreeMap};
use thiserror::Error;
use borsh::{BorshSerialize, BorshDeserialize};
use num_traits::FromPrimitive;
use solana_program::{sysvar::{rent::Rent, Sysvar}, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, account_info::{AccountInfo, next_account_info}, program_error::ProgramError, system_instruction, msg, program::{invoke_signed}, borsh::try_from_slice_unchecked};

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    msg!("instruction_data: {:?}", instruction_data);
    Processor::process(program_id, accounts, instruction_data)
}

pub struct Processor;

impl Processor {
    pub fn process(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        instruction_data: &[u8]
    ) -> ProgramResult {
        let instruction = FromPrimitive::from_u8(instruction_data[0]).ok_or(ProgramError::InvalidInstructionData)?;

        match instruction {
            0 => {
                msg!("Initializing map!");
                Self::process_init_map(accounts, program_id)?;
            },
            1 => {
                msg!("Inserting entry!");
                Self::process_insert_entry(accounts, program_id)?;
            },
            _ => {
                return Err(ProgramError::InvalidInstructionData)
            }
        }
        Ok(())
    }

    fn process_init_map(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();

        let authority_account = next_account_info(account_info_iter)?;
        let map_account = next_account_info(account_info_iter)?;
        let system_program = next_account_info(account_info_iter)?;

        if !authority_account.is_signer {
            return Err(ProgramError::MissingRequiredSignature)
        }

        let (map_pda, map_bump) = Pubkey::find_program_address(
            &[b"map".as_ref()],
            program_id
        );

        if map_pda != *map_account.key || !map_account.is_writable || !map_account.data_is_empty() {
            return Err(BlogError::InvalidMapAccount.into())
        }

        let rent = Rent::get()?;
        let rent_lamports = rent.minimum_balance(MapAccount::LEN);

        let create_map_ix = &system_instruction::create_account(
            authority_account.key, 
            map_account.key, 
            rent_lamports, 
            MapAccount::LEN.try_into().unwrap(), 
            program_id
        );

        msg!("Creating MapAccount account");
        invoke_signed(
            create_map_ix, 
            &[
                authority_account.clone(),
                map_account.clone(),
                system_program.clone()
            ],
            &[&[
                b"map".as_ref(),
                &[map_bump]
            ]]
        )?;

        msg!("Deserializing MapAccount account");
        let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow()).unwrap();
        let empty_map: BTreeMap<Pubkey, Pubkey> = BTreeMap::new();

        map_state.is_initialized = 1;
        map_state.map = empty_map;

        msg!("Serializing MapAccount account");
        map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

        Ok(())
    }

    fn process_insert_entry(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
        
        let account_info_iter = &mut accounts.iter();

        let a_account = next_account_info(account_info_iter)?;
        let b_account = next_account_info(account_info_iter)?;
        let map_account = next_account_info(account_info_iter)?;

        if !a_account.is_signer {
            return Err(ProgramError::MissingRequiredSignature)
        }

        if map_account.data.borrow()[0] == 0 || *map_account.owner != *program_id {
            return Err(BlogError::InvalidMapAccount.into())
        }

        msg!("Deserializing MapAccount account");
        let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow())?;

        if map_state.map.contains_key(a_account.key) {
            return Err(BlogError::AccountAlreadyHasEntry.into())
        }

        map_state.map.insert(*a_account.key, *b_account.key);
        
        msg!("Serializing MapAccount account");
        map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

        Ok(())
    }
}

#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct MapAccount {
    pub is_initialized: u8,
    pub map: BTreeMap<Pubkey, Pubkey> // 100
}

impl MapAccount {
    const LEN: usize = 1 + (4 + (10 * 64)); // 10 user -> blog
}

#[derive(Error, Debug, Copy, Clone)]
pub enum BlogError {
    #[error("Invalid MapAccount account")]
    InvalidMapAccount,

    #[error("Invalid Blog account")]
    InvalidBlogAccount,

    #[error("Account already has entry in Map")]
    AccountAlreadyHasEntry,
}

impl From<BlogError> for ProgramError {
    fn from(e: BlogError) -> Self {
        return ProgramError::Custom(e as u32);
    }
}

Về phía người dùng có thể kiểm tra Program trên bằng đoạn code mẫu bên dưới:

Press </> button to view full source
import {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";

import * as borsh from "@project-OpenBook/borsh";

const MY_PROGRAM_ID = new PublicKey(
  "FwcG3yKuAkCfX68q9GPykNWDaaPjdZFaR1Tgr8qSxaEa"
);

const MAP_DATA_LAYOUT = borsh.struct([
  borsh.u8("is_initialized"),
  borsh.map(borsh.publicKey("user_a"), borsh.publicKey("user_b"), "blogs"),
]);

async () => {
  const connection = new Connection("http://localhost:8899", "confirmed");

  const userA = Keypair.generate();
  const userB = Keypair.generate();
  const userC = Keypair.generate();

  const [mapKey] = await PublicKey.findProgramAddress(
    [Buffer.from("map")],
    MY_PROGRAM_ID
  );

  const airdropASig = await connection.requestAirdrop(
    userA.publicKey,
    5 * LAMPORTS_PER_SOL
  );
  const airdropBSig = await connection.requestAirdrop(
    userB.publicKey,
    5 * LAMPORTS_PER_SOL
  );
  const airdropCSig = await connection.requestAirdrop(
    userC.publicKey,
    5 * LAMPORTS_PER_SOL
  );
  const promiseA = connection.confirmTransaction(airdropASig);
  const promiseB = connection.confirmTransaction(airdropBSig);
  const promiseC = connection.confirmTransaction(airdropCSig);

  await Promise.all([promiseA, promiseB, promiseC]);

  const initMapIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
      {
        pubkey: userA.publicKey,
        isSigner: true,
        isWritable: true,
      },
      {
        pubkey: mapKey,
        isSigner: false,
        isWritable: true,
      },
      {
        pubkey: SystemProgram.programId,
        isSigner: false,
        isWritable: false,
      },
    ],
    data: Buffer.from(Uint8Array.of(0)),
  });

  const insertABIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
      {
        pubkey: userA.publicKey,
        isSigner: true,
        isWritable: true,
      },
      {
        pubkey: userB.publicKey,
        isSigner: false,
        isWritable: false,
      },
      {
        pubkey: mapKey,
        isSigner: false,
        isWritable: true,
      },
    ],
    data: Buffer.from(Uint8Array.of(1)),
  });

  const insertBCIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
      {
        pubkey: userB.publicKey,
        isSigner: true,
        isWritable: true,
      },
      {
        pubkey: userC.publicKey,
        isSigner: false,
        isWritable: false,
      },
      {
        pubkey: mapKey,
        isSigner: false,
        isWritable: true,
      },
    ],
    data: Buffer.from(Uint8Array.of(1)),
  });

  const insertCAIx = new TransactionInstruction({
    programId: MY_PROGRAM_ID,
    keys: [
      {
        pubkey: userC.publicKey,
        isSigner: true,
        isWritable: true,
      },
      {
        pubkey: userA.publicKey,
        isSigner: false,
        isWritable: false,
      },
      {
        pubkey: mapKey,
        isSigner: false,
        isWritable: true,
      },
    ],
    data: Buffer.from(Uint8Array.of(1)),
  });

  const tx = new Transaction();
  tx.add(initMapIx);
  tx.add(insertABIx);
  tx.add(insertBCIx);
  tx.add(insertCAIx);

  const sig = await connection.sendTransaction(tx, [userA, userB, userC], {
    skipPreflight: false,
    preflightCommitment: "confirmed",
  });
  await connection.confirmTransaction(sig);

  const mapAccount = await connection.getAccountInfo(mapKey);
  const mapData = MAP_DATA_LAYOUT.decode(mapAccount.data);
  console.log("MapData: ", mapData);
};
Last Updated:
Contributors: Trần Minh Quang, tuphan-dn