use core::mem::size_of;
use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;
use super::InstructionInputParsing;
use crate::SettlementInstruction;
pub struct WithdrawNativeSol {
pub program_id: Pubkey,
pub state_pda: Pubkey,
pub authority: Pubkey,
pub native_sol_buffer: Pubkey,
pub recipient: Pubkey,
pub amount: u64,
}
impl From<WithdrawNativeSol> for Instruction {
fn from(builder: WithdrawNativeSol) -> Self {
let mut data = vec![SettlementInstruction::WithdrawNativeSol.discriminator()];
data.extend_from_slice(&builder.amount.to_le_bytes());
Instruction {
program_id: builder.program_id,
accounts: vec![
AccountMeta::new_readonly(builder.state_pda, false),
AccountMeta::new_readonly(builder.authority, true),
AccountMeta::new(builder.native_sol_buffer, false),
AccountMeta::new(builder.recipient, false),
],
data,
}
}
}
pub struct WithdrawNativeSolInput<'a, A> {
pub state_pda: &'a A,
pub authority: &'a A,
pub native_sol_buffer: &'a A,
pub recipient: &'a A,
pub amount: u64,
}
impl<'a, A> InstructionInputParsing<'a, A> for WithdrawNativeSolInput<'a, A> {
const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::WithdrawNativeSol;
fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
let amount: &[u8; size_of::<u64>()] = instruction_data
.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?;
let amount = u64::from_le_bytes(*amount);
let [state_pda, authority, native_sol_buffer, recipient, ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
Ok(Self {
state_pda,
authority,
native_sol_buffer,
recipient,
amount,
})
}
}
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use solana_address::Address;
use super::{Instruction, WithdrawNativeSol};
pub const NUM_ACCOUNTS: usize = 4;
pub fn withdraw_native_sol_data(amount: u64) -> Vec<u8> {
let zero = Address::new_from_array([0; 32]);
Instruction::from(WithdrawNativeSol {
program_id: zero,
state_pda: zero,
authority: zero,
native_sol_buffer: zero,
recipient: zero,
amount,
})
.data
}
}
#[cfg(test)]
mod tests {
use super::fixtures::{withdraw_native_sol_data, NUM_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,
};
fn sample() -> WithdrawNativeSol {
WithdrawNativeSol {
program_id: pubkey_from_seed("program id"),
state_pda: pubkey_from_seed("state pda"),
authority: pubkey_from_seed("authority"),
native_sol_buffer: pubkey_from_seed("native sol buffer"),
recipient: pubkey_from_seed("recipient"),
amount: 0x0102_0304_0506_0708,
}
}
#[test]
fn withdraw_native_sol_input_parses_valid_input() {
let builder = sample();
let accounts = [
fake_account(builder.state_pda),
fake_account(builder.authority),
fake_account(builder.native_sol_buffer),
fake_account(builder.recipient),
];
let (state_pda, authority, native_sol_buffer, recipient, amount) = (
builder.state_pda,
builder.authority,
builder.native_sol_buffer,
builder.recipient,
builder.amount,
);
let data = Instruction::from(builder).data;
let WithdrawNativeSolInput {
state_pda: parsed_state_pda,
authority: parsed_authority,
native_sol_buffer: parsed_native_sol_buffer,
recipient: parsed_recipient,
amount: parsed_amount,
} = WithdrawNativeSolInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(*parsed_state_pda.address(), state_pda);
assert_eq!(*parsed_authority.address(), authority);
assert_eq!(*parsed_native_sol_buffer.address(), native_sol_buffer);
assert_eq!(*parsed_recipient.address(), recipient);
assert_eq!(parsed_amount, amount);
}
#[test]
fn withdraw_native_sol_input_rejects_short_data() {
let mut data = withdraw_native_sol_data(1);
data.pop();
let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
assert_eq!(
WithdrawNativeSolInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn withdraw_native_sol_input_rejects_long_data() {
let mut data = withdraw_native_sol_data(1);
data.push(0);
let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
assert_eq!(
WithdrawNativeSolInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn withdraw_native_sol_input_rejects_missing_accounts() {
let data = withdraw_native_sol_data(1);
let accounts = fake_sequential_accounts::<{ NUM_ACCOUNTS - 1 }>();
assert_eq!(
WithdrawNativeSolInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn instruction_data_has_expected_layout() {
let Instruction { data, .. } = WithdrawNativeSol {
amount: 0x0102_0304_0506_0708,
..sample()
}
.into();
assert_eq!(
data,
[
SettlementInstruction::WithdrawNativeSol.discriminator(),
8,
7,
6,
5,
4,
3,
2,
1,
]
);
}
#[test]
fn instruction_has_expected_accounts() {
let builder = sample();
let ix = Instruction::from(sample());
assert_eq!(ix.program_id, builder.program_id);
assert_eq!(ix.accounts.len(), NUM_ACCOUNTS);
assert_readonly_nonsigner(&ix.accounts[0], builder.state_pda);
assert_readonly_signer(&ix.accounts[1], builder.authority);
assert_writable_nonsigner(&ix.accounts[2], builder.native_sol_buffer);
assert_writable_nonsigner(&ix.accounts[3], builder.recipient);
}
}