use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;
pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
use super::InstructionInputParsing;
use crate::SettlementInstruction;
pub struct CreateBuffers<'a> {
pub program_id: Pubkey,
pub payer: Pubkey,
pub token_program: Pubkey,
pub buffers: &'a [(Pubkey, Pubkey)],
}
impl From<CreateBuffers<'_>> for Instruction {
fn from(builder: CreateBuffers<'_>) -> Self {
let mut accounts = vec![
AccountMeta::new(builder.payer, true),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(builder.token_program, false),
];
for (buffer_pda, mint) in builder.buffers {
accounts.push(AccountMeta::new(*buffer_pda, false));
accounts.push(AccountMeta::new_readonly(*mint, false));
}
Instruction {
program_id: builder.program_id,
accounts,
data: vec![SettlementInstruction::CreateBuffer.discriminator()],
}
}
}
pub struct BufferAccounts<'a, A> {
pub buffer_pda: &'a A,
pub mint: &'a A,
}
pub struct CreateBufferInput<'a, A> {
pub payer: &'a A,
buffer_pairs: &'a [[A; 2]],
}
impl<'a, A> CreateBufferInput<'a, A> {
pub fn buffers(&self) -> impl Iterator<Item = BufferAccounts<'a, A>> + '_ {
self.buffer_pairs
.iter()
.map(|[buffer_pda, mint]| BufferAccounts { buffer_pda, mint })
}
}
impl<'a, A> InstructionInputParsing<'a, A> for CreateBufferInput<'a, A> {
const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::CreateBuffer;
fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
if !instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
let [payer, _system, _token_program, rest @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
let rest: &'a [A] = rest;
let (buffers, remainder) = rest.as_chunks::<2>();
if !remainder.is_empty() || buffers.is_empty() {
return Err(ProgramError::NotEnoughAccountKeys);
}
Ok(Self {
payer,
buffer_pairs: buffers,
})
}
}
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use solana_address::Address;
use super::{CreateBuffers, Instruction};
pub const NUM_SHARED_ACCOUNTS: usize = 3;
pub fn create_buffer_data() -> Vec<u8> {
let zero = Address::new_from_array([0; 32]);
Instruction::from(CreateBuffers {
program_id: zero,
payer: zero,
token_program: zero,
buffers: &[(zero, zero)],
})
.data
}
}
#[cfg(test)]
mod tests {
use super::fixtures::{create_buffer_data, NUM_SHARED_ACCOUNTS};
use super::*;
use crate::fixtures::pubkey_from_seed;
use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
use crate::instruction::tests::{
assert_readonly_nonsigner, assert_writable_nonsigner, assert_writable_signer,
};
use solana_account_view::AccountView;
#[test]
fn create_buffer_input_parses_valid_input() {
let program_id = pubkey_from_seed("program id");
let payer = pubkey_from_seed("payer");
let token_program = pubkey_from_seed("token program");
let system_program = fake_account(pubkey_from_seed("system program"));
let buffer_pda = pubkey_from_seed("buffer pda");
let mint = pubkey_from_seed("mint");
let data = Instruction::from(CreateBuffers {
program_id,
payer,
token_program,
buffers: &[(buffer_pda, mint)],
})
.data;
let accounts = [
fake_account(payer),
system_program,
fake_account(token_program),
fake_account(buffer_pda),
fake_account(mint),
];
let input = CreateBufferInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(*input.payer.address(), payer);
let buffers: Vec<_> = input.buffers().collect();
assert_eq!(buffers.len(), 1, "one buffer is one (pda, mint) pair");
assert_eq!(*buffers[0].buffer_pda.address(), buffer_pda);
assert_eq!(*buffers[0].mint.address(), mint);
}
#[test]
fn create_buffer_input_parses_multiple_buffers() {
let program_id = pubkey_from_seed("program id");
let payer = pubkey_from_seed("payer");
let token_program = pubkey_from_seed("token program");
let buffer_a = pubkey_from_seed("buffer a");
let mint_a = pubkey_from_seed("mint a");
let buffer_b = pubkey_from_seed("buffer b");
let mint_b = pubkey_from_seed("mint b");
let data = Instruction::from(CreateBuffers {
program_id,
payer,
token_program,
buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
})
.data;
let accounts = [
fake_account(payer),
fake_account(pubkey_from_seed("system program")),
fake_account(token_program),
fake_account(buffer_a),
fake_account(mint_a),
fake_account(buffer_b),
fake_account(mint_b),
];
let input = CreateBufferInput::parse(&data, &accounts).expect("parse should succeed");
let buffers: Vec<_> = input.buffers().collect();
assert_eq!(
[*buffers[0].buffer_pda.address(), *buffers[0].mint.address()],
[buffer_a, mint_a]
);
assert_eq!(
[*buffers[1].buffer_pda.address(), *buffers[1].mint.address()],
[buffer_b, mint_b]
);
}
#[test]
fn create_buffer_input_rejects_zero_buffers() {
let data = vec![SettlementInstruction::CreateBuffer.discriminator()];
let accounts = fake_sequential_accounts::<NUM_SHARED_ACCOUNTS>();
assert_eq!(
CreateBufferInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
"an instruction that creates no buffers is rejected",
);
}
#[test]
fn create_buffer_input_rejects_long_data() {
let mut data = create_buffer_data();
data.push(0); let accounts: [AccountView; 0] = [];
assert_eq!(
CreateBufferInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn create_buffer_input_rejects_missing_accounts() {
let data = create_buffer_data();
let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS - 1 }>();
assert_eq!(
CreateBufferInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn create_buffer_input_rejects_odd_pair_accounts() {
let data = create_buffer_data();
let accounts = fake_sequential_accounts::<4>();
assert_eq!(
CreateBufferInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn instruction_data_has_expected_layout() {
let program_id = pubkey_from_seed("program id");
let payer = pubkey_from_seed("payer");
let token_program = pubkey_from_seed("token program");
let buffer_pda = pubkey_from_seed("buffer pda");
let mint = pubkey_from_seed("mint");
let Instruction { data, .. } = CreateBuffers {
program_id,
payer,
token_program,
buffers: &[(buffer_pda, mint)],
}
.into();
assert_eq!(
data,
vec![SettlementInstruction::CreateBuffer.discriminator()]
);
}
#[test]
fn single_buffer_has_expected_accounts() {
let program_id = pubkey_from_seed("program id");
let payer = pubkey_from_seed("payer");
let token_program = pubkey_from_seed("token program");
let buffer_pda = pubkey_from_seed("buffer pda");
let mint = pubkey_from_seed("mint");
let Instruction { accounts, .. } = CreateBuffers {
program_id,
payer,
token_program,
buffers: &[(buffer_pda, mint)],
}
.into();
assert_eq!(accounts.len(), 5);
assert_writable_signer(&accounts[0], payer);
assert_readonly_nonsigner(&accounts[1], SYSTEM_PROGRAM_ID);
assert_readonly_nonsigner(&accounts[2], token_program);
assert_writable_nonsigner(&accounts[3], buffer_pda);
assert_readonly_nonsigner(&accounts[4], mint);
}
#[test]
fn multiple_buffers_append_pairs_after_shared_accounts() {
let program_id = pubkey_from_seed("program id");
let payer = pubkey_from_seed("payer");
let token_program = pubkey_from_seed("token program");
let buffer_a = pubkey_from_seed("buffer a");
let mint_a = pubkey_from_seed("mint a");
let buffer_b = pubkey_from_seed("buffer b");
let mint_b = pubkey_from_seed("mint b");
let Instruction { accounts, .. } = CreateBuffers {
program_id,
payer,
token_program,
buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
}
.into();
assert_eq!(accounts.len(), 3 + 2 * 2);
assert_writable_nonsigner(&accounts[3], buffer_a);
assert_readonly_nonsigner(&accounts[4], mint_a);
assert_writable_nonsigner(&accounts[5], buffer_b);
assert_readonly_nonsigner(&accounts[6], mint_b);
}
#[test]
fn empty_buffers_has_only_shared_accounts() {
let program_id = pubkey_from_seed("program id");
let payer = pubkey_from_seed("payer");
let token_program = pubkey_from_seed("token program");
let Instruction { accounts, .. } = CreateBuffers {
program_id,
payer,
token_program,
buffers: &[],
}
.into();
assert_eq!(accounts.len(), 3);
}
}