Account Maps
Maps คือ data structures ที่เราใช้บ่อยๆ ตอนเขียน program ที่เกี่ยวกับ key และ value ของอะไรบางอย่าง.ซึ่ง value อาจจะเป็น type อะไรก็ได้ ส่วน key จะเป็นเหมือน identifier สำหรับ value นั้นๆ การมี key ก็จะทำให้เราสามารถแทรก (insert), อ่านค่ามา (retrieve) และ เปลี่ยนแปลง (update) values เหล่านี้ได้สะดวก
อย่างที่เรารู้ รูปแบบของ Solana's Account ต้องการ program data และ state data ที่เกี่ยวข้องเก็บอยู่ใน accounts แยกกัน ซึ่งการที่ accounts เหล่านี้มี address associated อยู่ทำให้มันมีลักษณะเหมือน map นั่นเอง! เรียนรู้เกี่ยวกับ Solana's Account mode ที่นี่.
ดังนั้นมันก็เหมาะสมแล้วที่เราจะเก็บ values ไว้ใน accounts แยก โดยต้องมี address เป็น key ในการดึง value ออกมา. แต่มันก็มีปัญหาตามมาอยู่บ้างเหมือนกัน เช่น,
พอใช้ addresses เป็น keys แล้วทำให้จำยาก
พอใช้ addresses ก็อย่างที่เรารู้ว่า public keys ของ Keypairs จะมี public key (หรือ address) และ private key เข้ามาเกี่ยวข้องด้วย ซึ่ง private key จะต้องมีเอาไว้ sign instructions ต่างๆ เวลาที่ต้องการ, ทำให้เราต้องเก็บ private key ไว้ด้วย ซึ่ง ไม่ใช่ทางที่ดี แน่ๆ!
ปัญหาเหล่าน้ีเป็นปัญหาที่นักพัฒนา Solana จะต้องเจอเวลาที่ต้องใช้ Map
ใน programs เราลองมาดูทางแก้กันดีกว่า
Deriving PDAs
PDA ย่อมาจาก Program Derived Address, และสั้นๆ คือ addresses จะได้รับสืบทอด (derived) จาก seeds และ a program id (หรือ address).
ที่พิเศษสำหรับ PDAs คือ addresses เหล่านี้จะไม่เกี่ยวข้องกับ private key ใดๆ ทั้งนี้เพราะ addresses เหล่านี้ไม่ได้อยู่บน ED25519 curve ดังนั้นจะมี เฉพาะ program ที่ address นี้ได้รับสืบทอดมาเท่านั้นถึงจะสามารถ sign instruction นี้ด้วย key ได้ ร่วมกับ seeds ที่เตรียมไว้ เรียนรู้เกี่ยวกับเรื่องนี้ได้ ที่นี่.
ตอนนี้เราก็ได้รู้จักกับ PDAs แล้วว่ามันคืออะไร, มาลองใช้พวกมันในการ map accounts กันเถอะ! เราจะใช้ตัวอย่างจาก Blog program เพื่อแสดงให้เห็นว่าเราจะใช้งานมันได้ยังไง
ใน Blog program เราต้องการให้แต่ละ User
มี Blog
คนละตัว และ blog นี้จะมีได้หลาย Posts
นั่นหมายความว่าเราต้องทำการ mapping แต่ละ user ไปที่ blog และแต่ละ post จะมีตัวชี้คอย mapped ไปที่ blog
พูดสั้นๆ ก็คือ, มี 1:1
mapping ระหว่าง user กับ blog และ 1:N
mapping ระหว่าง blog กับ posts.
สำหรับ 1:1
mapping, เราต้องการให้ blog address สืบทอดมาจาก user เท่านั้น, จะได้ทำให้เราดึง blog ได้ตาม authority (หรือตาม user นั่นเอง). ดังนั้น seed จึงต้องมี authority's key และควรจะขึ้นต้นด้วย "blog" เพื่อใช้เป็นตัวแยกประเภท (type identifier)
สำหรับ 1:N
mapping, เราต้องการให้แต่ละ post address สืบทอดได้จาก หลายๆ blog ที่เกี่ยวข้อง แต่ใช้คนละ identifier, เพื่อจะได้มีความแตกต่างระหว่าง posts สำหรับทุกๆ N
posts ใน blog ลองดูตัวอย่างด้านล่างจะเห็นว่าในแต่ละ post address จะถูกสืบทอด (derived from) มาจาก blog's key, slug เพื่อจำแนกแต่่ละ post, และขึ้นต้นด้วย "post" เพื่อใช้เป็นตัวแยกประเภท (type identifier) อีกทีนึง.
ตัวอย่าง code อยู่ด้านล่าง,
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,
}
#[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>
}
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);
}
}
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(())
}
ในฝั่ง client เราสามารถใช้ PublicKey.findProgramAddress()
เพื่อหา Blog
และ Post
account address เอาไปเรียก connection.getAccountInfo()
เพื่อดึงข้อมูล account data ตามตัวอย่างด้านล่าง
import * as borsh from "@project-serum/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);
};
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);
};
Single Map Account
วิธีอื่นที่จะทำ mapping ก็จะคือการใช้ data structure แบบ BTreeMap
เก็บไว้ใน account เดียว ซึ่ง account address นี้จะเป็น PDA หรือ public key ของ Keypair ที่สร้างขึ้นมาแล้วก็ได้
การทำ mapping accounts อาจจะไม่ใช่ทางที่ดีที่สุดเพราะเหตุผลต่อไปนี้
คุณต้อง initialize account ที่เก็บ
BTreeMap
อยู่ก่อนที่คุณจะ สามารถเพิ่ม key-value pairs ลงไปและคุณต้องเก็บ address ของ account นี้ไว้สักที่เพื่อเอาไว้ updatememory ของ account มีข้อจำกัดอยู่ที่ 10 megabytes ทำให้
BTreeMap
เก็บ key-value pairs ขนาดใหญ่ได้ไม่พอ.
ดังนั้นหลังจากเลือกได้แล้วว่าต้องการการใช้งานแบบไหนก็สามารถเริ่มทำได้ตามตัวอย่างด้านล่างนี้ได้เลย
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);
}
}
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(())
}
code ฝั่ง client-side เพื่อทดสอบ program ด้านบนว่าจะออกมาเหมือนด้านล่างนี้หรือไม่
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import * as borsh from "@project-serum/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);
};
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);