账户映射
在编程中,我们经常使用映射(Map)这种数据结构,将一个键与某种值关联起来。键和值可以是任意类型的数据,键用作标识要保存的特定值的标识符。通过键,我们可以高效地插入、检索和更新这些值。
正如我们所了解的,Solana的账户模型要求程序数据和相关状态数据存储在不同的账户中。这些账户都有与之关联的地址,这本身就有映射的作用!在这里了解更多关于Solana账户模型的信息。
因此,将值存储在单独的账户中,以其地址作为检索值所需的键是有意义的。但这也带来了一些问题,比如:
*上述地址很可能不是理想的键,你可能难以记住并检索所需的值。
*上述地址是不同Keypair的公钥,每个公钥(或地址)都有与之关联的私钥。如果需要,这个私钥将用于对不同的指令进行签名,这意味着我们需要在某个地方存储私钥,这绝对不是推荐的做法!
这给许多Solana开发者带来了一个问题,即如何在他们的程序中实现类似Map
的逻辑。让我们看看几种解决这个问题的方法。
派生PDA
PDA的全称是“程序派生地址” - Program Derived Address,简而言之,它们是从一组种子和程序ID(或地址)派生出来的地址。
PDAs的独特之处在于,这些地址不与任何私钥相关联。这是因为这些地址不位于ED25519曲线上。因此,只有派生此地址的程序可以使用提供的密钥和种子对指令进行签名。在这里了解更多信息。
现在我们对PDAs有了一个概念,让我们使用它们来映射一些账户!我们以一个博客程序作为示例,演示如何实现这一点。
在这个博客程序中,我们希望每个User
都拥有一个Blog
。这个博客可以有任意数量的Posts
。这意味着我们将每个用户映射到一个博客,每个帖子映射到某个博客。
简而言之,用户和他/她的博客之间是1:1
的映射,而博客和其帖子之间是1:N
的映射。
对于1:1
的映射,我们希望一个博客的地址仅从其用户派生,这样我们可以通过其权限(或用户)来检索博客。因此,博客的种子将包括其权限的密钥,可能还有一个前缀博客,作为类型标识符。
对于1:N
的映射,我们希望每个帖子的地址不仅从它所关联的博客派生,还从另一个标识符派生,以区分博客中的多个帖子。在下面的示例中,每个帖子的地址是从博客的密钥、一个用于标识每个帖子的slug和一个前缀帖子派生出来的,作为类型标识符。
代码如下所示:
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(())
}
在客户端,你可以使用PublicKey.findProgramAddress()
来获取所需的Blog
和Post
账户地址,然后将其传递给connection.getAccountInfo()
来获取账户数据。下面是一个示例:
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);
};
单个映射账户
另一种实现映射的方法是在单个账户中显式存储一个BTreeMap
数据结构。这个账户的地址本身可以是一个PDA,或者是生成的Keypair的公钥。
这种账户映射的方法并不理想,原因如下:
*首先,你需要初始化存储BTreeMap
的账户,然后才能向其中插入必要的键值对。然后,你还需要将这个账户的地址存储在某个地方,以便每次更新时进行更新。
*账户存在内存限制,每个账户的最大大小为10兆字节,这限制了BTreeMap
存储大量键值对的能力。
因此,在考虑你的用例后,可以按照以下方式实现这种方法:
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(())
}
上述程序的客户端测试代码可能如下所示:
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);