use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;
use super::InstructionInputParsing;
use crate::SettlementInstruction;
pub struct ReclaimBuffer<'a> {
pub program_id: Pubkey,
pub state_pda: Pubkey,
pub reclaim_authority: Pubkey,
pub reclaim_recipient: Pubkey,
pub token_program: Pubkey,
pub buffers: &'a [(Pubkey, Pubkey)],
}
impl From<ReclaimBuffer<'_>> for Instruction {
fn from(builder: ReclaimBuffer<'_>) -> Self {
let mut accounts = vec![
AccountMeta::new_readonly(builder.state_pda, false),
AccountMeta::new_readonly(builder.reclaim_authority, true),
AccountMeta::new(builder.reclaim_recipient, 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::ReclaimBuffer.discriminator()],
}
}
}
pub struct ReclaimBufferInput<'a, A> {
pub state_pda: &'a A,
pub reclaim_authority: &'a A,
pub reclaim_recipient: &'a A,
pub buffers: &'a [[A; 2]],
}
impl<'a, A> InstructionInputParsing<'a, A> for ReclaimBufferInput<'a, A> {
const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::ReclaimBuffer;
fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
if !instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
let [state_pda, reclaim_authority, reclaim_recipient, _token_program, rest @ ..] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
let (buffers, remainder) = rest.as_chunks::<2>();
if !remainder.is_empty() || buffers.is_empty() {
return Err(ProgramError::NotEnoughAccountKeys);
}
Ok(Self {
state_pda,
reclaim_authority,
reclaim_recipient,
buffers,
})
}
}
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use solana_address::Address;
use super::{Instruction, ReclaimBuffer};
pub const NUM_SHARED_ACCOUNTS: usize = 4;
pub fn reclaim_buffer_data() -> Vec<u8> {
let zero = Address::new_from_array([0; 32]);
Instruction::from(ReclaimBuffer {
program_id: zero,
state_pda: zero,
reclaim_authority: zero,
reclaim_recipient: zero,
token_program: zero,
buffers: &[(zero, zero)],
})
.data
}
}
#[cfg(test)]
mod tests {
use super::fixtures::{reclaim_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_readonly_signer, assert_writable_nonsigner,
};
#[test]
fn reclaim_buffer_input_parses_valid_input() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let reclaim_authority = pubkey_from_seed("reclaim authority");
let reclaim_recipient = pubkey_from_seed("reclaim recipient");
let token_program = pubkey_from_seed("token program");
let buffer_pda = pubkey_from_seed("buffer pda");
let mint = pubkey_from_seed("mint");
let data = Instruction::from(ReclaimBuffer {
program_id,
state_pda,
reclaim_authority,
reclaim_recipient,
token_program,
buffers: &[(buffer_pda, mint)],
})
.data;
let token_program = pubkey_from_seed("token program");
let accounts = [
fake_account(state_pda),
fake_account(reclaim_authority),
fake_account(reclaim_recipient),
fake_account(token_program),
fake_account(buffer_pda),
fake_account(mint),
];
let ReclaimBufferInput {
state_pda: parsed_state_pda,
reclaim_authority: parsed_reclaim_authority,
reclaim_recipient: parsed_reclaim_recipient,
buffers,
} = ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(*parsed_state_pda.address(), state_pda);
assert_eq!(*parsed_reclaim_authority.address(), reclaim_authority);
assert_eq!(*parsed_reclaim_recipient.address(), reclaim_recipient);
assert_eq!(buffers.len(), 1, "one buffer is one pair");
assert_eq!(*buffers[0][0].address(), buffer_pda);
assert_eq!(*buffers[0][1].address(), mint);
}
#[test]
fn reclaim_buffer_input_parses_multiple_buffers() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let reclaim_authority = pubkey_from_seed("reclaim authority");
let reclaim_recipient = pubkey_from_seed("reclaim recipient");
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(ReclaimBuffer {
program_id,
state_pda,
token_program,
reclaim_authority,
reclaim_recipient,
buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
})
.data;
let accounts = [
fake_account(state_pda),
fake_account(reclaim_authority),
fake_account(reclaim_recipient),
fake_account(token_program),
fake_account(buffer_a),
fake_account(mint_a),
fake_account(buffer_b),
fake_account(mint_b),
];
let ReclaimBufferInput { buffers, .. } =
ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(buffers.len(), 2, "two buffers are two pairs");
assert_eq!(
buffers[0].each_ref().map(|a| *a.address()),
[buffer_a, mint_a]
);
assert_eq!(
buffers[1].each_ref().map(|a| *a.address()),
[buffer_b, mint_b]
);
}
#[test]
fn reclaim_buffer_input_rejects_zero_buffers() {
let data = reclaim_buffer_data();
let accounts = fake_sequential_accounts::<NUM_SHARED_ACCOUNTS>();
assert_eq!(
ReclaimBufferInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
"an instruction that reclaims no buffers is rejected",
);
}
#[test]
fn reclaim_buffer_input_rejects_long_data() {
let mut data = reclaim_buffer_data();
data.push(0); assert_eq!(
ReclaimBufferInput::parse(&data, &[0]).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn reclaim_buffer_input_rejects_missing_accounts() {
let data = reclaim_buffer_data();
let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS - 1 }>();
assert_eq!(
ReclaimBufferInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn reclaim_buffer_input_rejects_incomplete_pair() {
let data = reclaim_buffer_data();
let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + 3 }>();
assert_eq!(
ReclaimBufferInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn instruction_data_has_expected_layout() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let reclaim_authority = pubkey_from_seed("reclaim authority");
let reclaim_recipient = pubkey_from_seed("reclaim recipient");
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, .. } = ReclaimBuffer {
program_id,
state_pda,
reclaim_authority,
reclaim_recipient,
token_program,
buffers: &[(buffer_pda, mint)],
}
.into();
assert_eq!(
data,
vec![SettlementInstruction::ReclaimBuffer.discriminator()]
);
}
#[test]
fn single_buffer_has_expected_accounts() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let reclaim_authority = pubkey_from_seed("reclaim authority");
let reclaim_recipient = pubkey_from_seed("reclaim recipient");
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, .. } = ReclaimBuffer {
program_id,
state_pda,
reclaim_authority,
reclaim_recipient,
token_program,
buffers: &[(buffer_pda, mint)],
}
.into();
assert_eq!(accounts.len(), 6);
assert_readonly_nonsigner(&accounts[0], state_pda);
assert_readonly_signer(&accounts[1], reclaim_authority);
assert_writable_nonsigner(&accounts[2], reclaim_recipient);
assert_readonly_nonsigner(&accounts[3], token_program);
assert_writable_nonsigner(&accounts[4], buffer_pda);
assert_readonly_nonsigner(&accounts[5], mint);
}
#[test]
fn recipient_may_be_the_reclaim_authority_itself() {
let reclaim_authority = pubkey_from_seed("reclaim authority");
let Instruction { accounts, .. } = ReclaimBuffer {
program_id: pubkey_from_seed("program id"),
state_pda: pubkey_from_seed("state pda"),
reclaim_authority,
reclaim_recipient: reclaim_authority,
token_program: pubkey_from_seed("token program"),
buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"))],
}
.into();
assert_readonly_signer(&accounts[1], reclaim_authority);
assert_writable_nonsigner(&accounts[2], reclaim_authority);
}
#[test]
fn multiple_buffers_append_pairs_after_shared_accounts() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let reclaim_authority = pubkey_from_seed("reclaim authority");
let reclaim_recipient = pubkey_from_seed("reclaim recipient");
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, .. } = ReclaimBuffer {
program_id,
state_pda,
reclaim_authority,
reclaim_recipient,
token_program,
buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
}
.into();
assert_eq!(accounts.len(), 4 + 2 * 2);
assert_writable_nonsigner(&accounts[4], buffer_a);
assert_readonly_nonsigner(&accounts[5], mint_a);
assert_writable_nonsigner(&accounts[6], buffer_b);
assert_readonly_nonsigner(&accounts[7], mint_b);
}
#[test]
fn empty_buffers_has_only_shared_accounts() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let reclaim_authority = pubkey_from_seed("reclaim authority");
let reclaim_recipient = pubkey_from_seed("reclaim recipient");
let token_program = pubkey_from_seed("token program");
let Instruction { accounts, .. } = ReclaimBuffer {
program_id,
state_pda,
reclaim_authority,
reclaim_recipient,
token_program,
buffers: &[],
}
.into();
assert_eq!(accounts.len(), NUM_SHARED_ACCOUNTS);
}
}