use std::vec;
use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;
use crate::instruction::InstructionInputParsing;
use crate::{recover_discriminator, SettlementError, SettlementInstruction};
use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID};
pub const FINALIZE_FIXED_ACCOUNTS: usize = 3;
fn split_push_bytes(body: &[u8]) -> Result<(&[u8], &[[u8; 8]]), ProgramError> {
if !body.len().is_multiple_of(9) {
return Err(ProgramError::InvalidInstructionData);
}
let push_count = body.len() / 9;
let (bumps, amount_bytes) = body.split_at(push_count);
let (amounts, []) = amount_bytes.as_chunks::<8>() else {
return Err(ProgramError::InvalidInstructionData);
};
Ok((bumps, amounts))
}
fn split_pushes(body: &[u8]) -> Result<(&[u8], impl Iterator<Item = u64> + '_), ProgramError> {
let (bumps, amounts) = split_push_bytes(body)?;
Ok((bumps, amounts.iter().copied().map(u64::from_le_bytes)))
}
pub fn finalize_push_amounts(
instruction_data: &[u8],
) -> Result<impl Iterator<Item = u64> + '_, ProgramError> {
let (_discriminator, rest) = recover_discriminator(instruction_data)?;
let (_begin_ix_index, body) = recover_counterpart(rest)?;
let (_bumps, amounts) = split_pushes(body)?;
Ok(amounts)
}
pub struct FinalizeSettle<'a> {
pub program_id: Pubkey,
pub state_pda: Pubkey,
pub begin_ix_index: u16,
pub source_buffers: &'a [Pubkey],
pub destinations: &'a [Pubkey],
pub bumps: &'a [u8],
pub amounts: &'a [u64],
}
impl From<FinalizeSettle<'_>> for Instruction {
fn from(builder: FinalizeSettle<'_>) -> Self {
let FinalizeSettle {
program_id,
state_pda,
begin_ix_index,
source_buffers,
destinations,
bumps,
amounts,
} = builder;
let data: Vec<u8> = core::iter::once(SettlementInstruction::FinalizeSettle.discriminator())
.chain(begin_ix_index.to_le_bytes())
.chain(bumps.iter().copied())
.chain(amounts.iter().flat_map(|amount| amount.to_le_bytes()))
.collect();
let mut accounts = vec![
AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
AccountMeta::new_readonly(state_pda, false),
AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
];
for (source, destination) in source_buffers.iter().zip(destinations) {
accounts.push(AccountMeta::new(*source, false));
accounts.push(AccountMeta::new(*destination, false));
}
Instruction {
program_id,
accounts,
data,
}
}
}
pub struct Push<'a, A> {
pub source_buffer: &'a A,
pub destination: &'a A,
pub bump: u8,
pub amount: u64,
}
pub struct Pushes<'a, A> {
push_accounts: &'a [A],
bumps: &'a [u8],
amounts: &'a [[u8; 8]],
}
impl<'a, A> Pushes<'a, A> {
#[allow(
clippy::arithmetic_side_effects,
reason = "offsets are bounded by tx limits"
)]
pub fn iter(&self) -> impl Iterator<Item = Push<'a, A>> + '_ {
let push_count = self.bumps.len();
let mut i = 0usize;
let mut account_offset = 0usize;
std::iter::from_fn(move || {
if i >= push_count {
return None;
}
let bump = self.bumps[i];
let amount = u64::from_le_bytes(self.amounts[i]);
i += 1;
let source_buffer = &self.push_accounts[account_offset];
let destination = &self.push_accounts[account_offset + 1];
account_offset += 2;
Some(Push {
source_buffer,
destination,
bump,
amount,
})
})
}
}
pub struct FinalizeSettleInput<'a, A> {
pub begin_ix_index: u16,
pub instructions_sysvar_account: &'a A,
pub state_pda_account: &'a A,
pub token_program_account: &'a A,
pub pushes: Pushes<'a, A>,
}
impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> {
const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::FinalizeSettle;
fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
let (begin_ix_index, body) = recover_counterpart(instruction_data)?;
let [instructions_sysvar_account, state_pda_account, token_program_account, push_accounts @ ..] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
let (bumps, amounts) = split_push_bytes(body)?;
let push_count = bumps.len();
let expected_accounts = push_count
.checked_mul(2)
.ok_or(ProgramError::InvalidInstructionData)?;
if push_accounts.len() != expected_accounts {
return Err(SettlementError::AccountCountNotMatchingPushCount.into());
}
Ok(Self {
begin_ix_index,
instructions_sysvar_account,
state_pda_account,
token_program_account,
pushes: Pushes {
push_accounts,
bumps,
amounts,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instruction::fixtures::{
fake_account, fake_account_from_array, fake_sequential_accounts,
};
use crate::instruction::settle::tests::ix_data;
use crate::instruction::tests::assert_readonly_nonsigner;
use hex_literal::hex;
use proptest::prelude::*;
use solana_account_view::AccountView;
use solana_address::Address;
#[test]
fn finalize_fixed_accounts_matches_builder() {
let ix = Instruction::from(FinalizeSettle {
program_id: Pubkey::new_unique(),
state_pda: Pubkey::new_unique(),
begin_ix_index: 0,
source_buffers: &[],
destinations: &[],
bumps: &[],
amounts: &[],
});
assert_eq!(ix.accounts.len(), FINALIZE_FIXED_ACCOUNTS);
}
#[test]
fn expected_encoding_finalize_settle_no_pushes() {
let program_id = Pubkey::new_unique();
let state_pda = Pubkey::new_unique();
let Instruction {
program_id: ix_program_id,
accounts,
data,
} = FinalizeSettle {
program_id,
state_pda,
begin_ix_index: 0x1337,
source_buffers: &[],
destinations: &[],
bumps: &[],
amounts: &[],
}
.into();
assert_eq!(ix_program_id, program_id);
assert_eq!(
data,
ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
hex!("3713"), ],
);
assert_eq!(accounts.len(), 3);
assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID);
assert_readonly_nonsigner(&accounts[1], state_pda);
assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID);
}
#[test]
fn finalize_settle_encodes_pushes() {
let program_id = Pubkey::new_unique();
let state_pda = Pubkey::new_unique();
let source_a = Pubkey::new_from_array([0x01; 32]);
let dest_a = Pubkey::new_from_array([0x02; 32]);
let source_b = Pubkey::new_from_array([0x03; 32]);
let dest_b = Pubkey::new_from_array([0x04; 32]);
let ix = Instruction::from(FinalizeSettle {
program_id,
state_pda,
begin_ix_index: 0x1337,
source_buffers: &[source_a, source_b],
destinations: &[dest_a, dest_b],
bumps: &[0xa1, 0xb1],
amounts: &[0x01020304, 0x05060708],
});
assert_eq!(
ix.data,
ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
hex!("3713"), [0xa1, 0xb1], hex!("0403020100000000"),
hex!("0807060500000000"),
],
);
let actual: Vec<Pubkey> = ix.accounts.iter().map(|meta| meta.pubkey).collect();
assert_eq!(
actual,
vec![
INSTRUCTIONS_SYSVAR_ID,
state_pda,
SPL_TOKEN_PROGRAM_ID,
source_a,
dest_a,
source_b,
dest_b,
],
);
let writable: Vec<Pubkey> = ix
.accounts
.iter()
.filter(|meta| meta.is_writable)
.map(|meta| meta.pubkey)
.collect();
assert_eq!(writable, vec![source_a, dest_a, source_b, dest_b]);
assert!(ix.accounts.iter().all(|meta| !meta.is_signer));
}
#[test]
fn finalize_settle_input_parses_no_pushes() {
let sysvar = Address::new_from_array([0x42u8; 32]);
let state = Address::new_from_array([0x43u8; 32]);
let token_program = Address::new_from_array([0x44u8; 32]);
let accounts = [
fake_account(sysvar),
fake_account(state),
fake_account(token_program),
];
let data = ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
[0x37, 0x13], ];
let FinalizeSettleInput {
begin_ix_index,
instructions_sysvar_account,
state_pda_account,
token_program_account,
pushes,
} = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(begin_ix_index, 0x1337);
assert_eq!(instructions_sysvar_account.address(), &sysvar);
assert_eq!(state_pda_account.address(), &state);
assert_eq!(token_program_account.address(), &token_program);
assert_eq!(pushes.iter().count(), 0);
}
#[test]
fn finalize_settle_input_parses_pushes() {
let sysvar = Address::new_from_array([1u8; 32]);
let state = Address::new_from_array([0xa1u8; 32]);
let token_program = Address::new_from_array([0xa2u8; 32]);
let source = Address::new_from_array([3u8; 32]);
let dest0 = Address::new_from_array([4u8; 32]);
let dest1 = Address::new_from_array([5u8; 32]);
let accounts = [
fake_account(sysvar),
fake_account(state),
fake_account(token_program),
fake_account(source),
fake_account(dest0),
fake_account(source),
fake_account(dest1),
];
let data = ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
[0x13, 0x37], [0xfe, 0xfd], 0x1122u64.to_le_bytes(),
0x3344u64.to_le_bytes(),
];
let FinalizeSettleInput { pushes, .. } =
FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
let parsed: Vec<(&Address, &Address, u8, u64)> = pushes
.iter()
.map(|push| {
(
push.source_buffer.address(),
push.destination.address(),
push.bump,
push.amount,
)
})
.collect();
assert_eq!(
parsed,
vec![
(&source, &dest0, 0xfe, 0x1122),
(&source, &dest1, 0xfd, 0x3344),
],
);
}
#[test]
fn finalize_settle_input_parses_many_pushes() {
const PUSH_COUNT: usize = 16;
struct ExpectedPush {
source: Address,
dest: Address,
bump: u8,
amount: u64,
}
let mut expected: Vec<ExpectedPush> = Vec::new();
for i in 0..PUSH_COUNT {
let source = Address::new_from_array([i as u8; 32]);
let dest = Address::new_from_array([(i + PUSH_COUNT) as u8; 32]);
let bump = (i + 2 * PUSH_COUNT) as u8;
let amount = u64::from_le_bytes([(i + 3 * PUSH_COUNT) as u8; 8]);
expected.push(ExpectedPush {
source,
dest,
bump,
amount,
});
}
let mut accounts = vec![
fake_account_from_array([0xff; 32]),
fake_account_from_array([0xfe; 32]),
fake_account_from_array([0xfd; 32]),
];
let mut bump_bytes = Vec::new();
let mut amount_bytes = Vec::new();
for push in &expected {
accounts.push(fake_account(push.source));
accounts.push(fake_account(push.dest));
bump_bytes.push(push.bump);
amount_bytes.extend_from_slice(&push.amount.to_le_bytes());
}
let data = ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
[0x13, 0x37], bump_bytes,
amount_bytes,
];
let parsed = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
let pushes: Vec<_> = parsed.pushes.iter().collect();
assert_eq!(pushes.len(), PUSH_COUNT);
for (push, expected) in pushes.iter().zip(&expected) {
assert_eq!(push.source_buffer.address(), &expected.source);
assert_eq!(push.destination.address(), &expected.dest);
assert_eq!(push.bump, expected.bump);
assert_eq!(push.amount, expected.amount);
}
}
#[test]
fn finalize_settle_input_rejects_different_discriminator() {
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], ];
let accounts: [AccountView; 0] = [];
assert_eq!(
FinalizeSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn finalize_settle_input_rejects_empty_accounts() {
let data = ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
[0, 0], ];
let accounts: [AccountView; 0] = [];
assert_eq!(
FinalizeSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn finalize_settle_input_rejects_account_count_mismatch() {
let data: Vec<u8> = ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
[13, 37], [0xff], 31337u64.to_le_bytes(),
];
let too_few = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 1 }>();
assert_eq!(
FinalizeSettleInput::parse(&data, &too_few).err(),
Some(SettlementError::AccountCountNotMatchingPushCount.into()),
);
let too_many = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 3 }>();
assert_eq!(
FinalizeSettleInput::parse(&data, &too_many).err(),
Some(SettlementError::AccountCountNotMatchingPushCount.into()),
);
}
#[test]
fn finalize_settle_input_rejects_partial_push() {
let accounts = fake_sequential_accounts::<FINALIZE_FIXED_ACCOUNTS>();
let data = ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
[37, 13], [0xff], [0x11, 0x22, 0x33, 0x44], ];
assert_eq!(
FinalizeSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn finalize_push_amounts_extracts_amounts() {
let amounts = [0x0102, 0x0304];
let ix = Instruction::from(FinalizeSettle {
program_id: Pubkey::new_unique(),
state_pda: Pubkey::new_unique(),
begin_ix_index: 0x1337,
source_buffers: &[Pubkey::new_unique(), Pubkey::new_unique()],
destinations: &[Pubkey::new_unique(), Pubkey::new_unique()],
bumps: &[0xa1, 0xb1],
amounts: &amounts,
});
let recovered = finalize_push_amounts(&ix.data).expect("valid finalize data");
let decoded: Vec<u64> = recovered.collect();
assert_eq!(decoded, amounts);
}
#[test]
fn finalize_push_amounts_handles_no_pushes() {
let ix = Instruction::from(FinalizeSettle {
program_id: Pubkey::new_unique(),
state_pda: Pubkey::new_unique(),
begin_ix_index: 0,
source_buffers: &[],
destinations: &[],
bumps: &[],
amounts: &[],
});
let mut amounts = finalize_push_amounts(&ix.data).expect("valid empty finalize data");
assert!(amounts.next().is_none());
}
#[test]
fn finalize_push_amounts_rejects_incorrect_bytes() {
let mut ix = Instruction::from(FinalizeSettle {
program_id: Pubkey::new_unique(),
state_pda: Pubkey::new_unique(),
begin_ix_index: 0,
source_buffers: &[Pubkey::new_unique()],
destinations: &[Pubkey::new_unique()],
bumps: &[0xff],
amounts: &[31337],
});
ix.data.pop();
assert_eq!(
finalize_push_amounts(&ix.data).err(),
Some(ProgramError::InvalidInstructionData),
);
}
fn arb_finalize_instruction(
push_count: impl Into<prop::collection::SizeRange>,
) -> impl Strategy<Value = Instruction> {
(
any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
any::<u16>(),
crate::instruction::settle::fixtures::arb_pushes(push_count),
)
.prop_map(
|(
program_id,
state_pda,
begin_ix_index,
(source_buffers, destinations, bumps, amounts),
)| {
Instruction::from(FinalizeSettle {
program_id,
state_pda,
begin_ix_index,
source_buffers: &source_buffers,
destinations: &destinations,
bumps: &bumps,
amounts: &amounts,
})
},
)
}
proptest! {
#[test]
fn finalize_push_amounts_matches_parser(ix in arb_finalize_instruction(0..=16usize)) {
let recovered: Vec<u64> =
finalize_push_amounts(&ix.data).expect("well-formed finalize data").collect();
let accounts: Vec<AccountView> =
ix.accounts.iter().map(|meta| fake_account(meta.pubkey)).collect();
let parsed = FinalizeSettleInput::parse(&ix.data, &accounts)
.expect("a well-formed finalize parses");
let parsed_amounts: Vec<u64> =
parsed.pushes.iter().map(|push| push.amount).collect();
prop_assert_eq!(recovered, parsed_amounts);
}
}
}