use std::vec;
use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;
use crate::instruction::InstructionInputParsing;
use crate::{SettlementError, SettlementInstruction};
use super::{
recover_counterpart, MaybeMint, TokenProgram, INSTRUCTIONS_SYSVAR_ID, MINT_PLACEHOLDER,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Pull {
pub destination: Pubkey,
pub amount: u64,
}
#[cfg_attr(any(test, feature = "test-fixtures"), derive(Default))]
pub struct BeginSettle<'a> {
pub program_id: Pubkey,
pub state_pda: Pubkey,
pub solver: Pubkey,
pub finalize_ix_index: u16,
pub auction_id: i64,
pub only_token_program: Option<TokenProgram>,
pub order_pdas: &'a [Pubkey],
pub sell_token_accounts: &'a [Pubkey],
pub sell_mints: &'a [Option<Pubkey>],
pub pulls: &'a [&'a [Pull]],
}
impl From<BeginSettle<'_>> for Instruction {
fn from(builder: BeginSettle<'_>) -> Self {
let BeginSettle {
program_id,
state_pda,
solver,
finalize_ix_index,
auction_id,
only_token_program,
order_pdas,
sell_token_accounts,
sell_mints,
pulls,
} = builder;
let mut order: Vec<usize> = (0..order_pdas.len()).collect();
order.sort_by_key(|&i| order_pdas[i]);
let counts: Vec<u8> = order.iter().map(|&i| pulls[i].len() as u8).collect();
let amounts: Vec<u8> = order
.iter()
.flat_map(|&i| pulls[i].iter())
.flat_map(|pull| pull.amount.to_le_bytes())
.collect();
let data = [
&[SettlementInstruction::BeginSettle.discriminator()][..],
&finalize_ix_index.to_le_bytes()[..],
&auction_id.to_le_bytes()[..],
&[order_pdas.len() as u8][..],
&counts[..],
&amounts[..],
]
.concat();
let mut accounts = vec![
AccountMeta::new_readonly(solver, true),
AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
AccountMeta::new_readonly(state_pda, false),
];
accounts.extend(TokenProgram::ALL.map(|program| {
let address = if only_token_program.is_none_or(|only| only == program) {
program.address()
} else {
INSTRUCTIONS_SYSVAR_ID
};
AccountMeta::new_readonly(address, false)
}));
for &i in &order {
accounts.push(AccountMeta::new(order_pdas[i], false));
accounts.push(AccountMeta::new(sell_token_accounts[i], false));
accounts.push(AccountMeta::new_readonly(
sell_mints[i].unwrap_or(MINT_PLACEHOLDER),
false,
));
for pull in pulls[i] {
accounts.push(AccountMeta::new(pull.destination, false));
}
}
Instruction {
program_id,
accounts,
data,
}
}
}
pub struct SettledOrder<'a, A> {
pub order_pda: &'a A,
pub sell_token_account: &'a A,
pub sell_mint: MaybeMint<'a, A>,
pub destinations: &'a [A],
pub amounts: &'a [[u8; 8]],
}
pub struct SettledOrders<'a, A> {
order_accounts: &'a [A],
counts: &'a [u8],
amounts: &'a [[u8; 8]],
}
impl<'a, A> SettledOrders<'a, A> {
pub fn len(&self) -> usize {
self.counts.len()
}
pub fn is_empty(&self) -> bool {
self.counts.is_empty()
}
#[allow(
clippy::arithmetic_side_effects,
reason = "offsets are bounded by tx limits"
)]
pub fn iter(&self) -> impl Iterator<Item = SettledOrder<'a, A>> + '_ {
let (counts, amounts) = (self.counts, self.amounts);
let mut rest: &'a [A] = self.order_accounts;
let mut i = 0usize;
let mut amount_offset = 0usize;
std::iter::from_fn(move || {
if i >= counts.len() {
return None;
}
let count = usize::from(counts[i]);
i += 1;
let (order_pda, tail) = rest.split_first()?;
let (sell_token_account, tail) = tail.split_first()?;
let (sell_mint, tail) = tail.split_first()?;
let (destinations, remainder) = tail.split_at(count);
rest = remainder;
let amount_end = amount_offset + count;
let order_amounts = &amounts[amount_offset..amount_end];
amount_offset = amount_end;
Some(SettledOrder {
order_pda,
sell_token_account,
sell_mint: MaybeMint::new(sell_mint),
destinations,
amounts: order_amounts,
})
})
}
}
pub struct BeginSettleInput<'a, A> {
pub finalize_ix_index: u16,
pub auction_id: i64,
pub solver_account: &'a A,
pub instructions_sysvar_account: &'a A,
pub state_pda_account: &'a A,
pub orders: SettledOrders<'a, A>,
}
impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> {
const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::BeginSettle;
fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
let (finalize_ix_index, body) = recover_counterpart(instruction_data)?;
let [solver_account, instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, order_accounts @ ..] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
let (auction_id, body) = body
.split_first_chunk::<8>()
.ok_or(ProgramError::InvalidInstructionData)?;
let auction_id = i64::from_le_bytes(*auction_id);
let (&order_count, body) = body
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
let order_count = usize::from(order_count);
let (counts, amount_bytes) = body
.split_at_checked(order_count)
.ok_or(ProgramError::InvalidInstructionData)?;
let (amounts, []) = amount_bytes.as_chunks::<8>() else {
return Err(ProgramError::InvalidInstructionData);
};
let transfer_count = amounts.len();
let expected_accounts = order_count
.checked_mul(3)
.and_then(|three_n| three_n.checked_add(transfer_count))
.ok_or(ProgramError::InvalidInstructionData)?;
if order_accounts.len() != expected_accounts {
return Err(SettlementError::AccountCountNotMatchingOrderCount.into());
}
let counts_sum: usize = counts.iter().map(|&c| usize::from(c)).sum();
if counts_sum != transfer_count {
return Err(SettlementError::TransferCountMismatch.into());
}
Ok(Self {
finalize_ix_index,
auction_id,
instructions_sysvar_account,
state_pda_account,
solver_account,
orders: SettledOrders {
order_accounts,
counts,
amounts,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures::pubkey_from_seed;
use crate::instruction::fixtures::{
fake_account, fake_account_from_array, fake_sequential_accounts,
};
use crate::instruction::settle::tests::{ix_data, mint_address};
use crate::instruction::tests::{assert_readonly_nonsigner, assert_readonly_signer};
use crate::token_program::TokenProgram;
use hex_literal::hex;
use solana_account_view::AccountView;
use solana_address::Address;
const FIXED_ACCOUNTS: usize = 5;
const AUCTION_ID: i64 = 0x0102_0304_0506_0708;
#[test]
fn expected_encoding_begin_settle_no_orders() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let solver = pubkey_from_seed("solver");
let Instruction {
program_id: ix_program_id,
accounts,
data,
} = BeginSettle {
program_id,
state_pda,
solver,
finalize_ix_index: 0x1337,
auction_id: 0x0102_0304_0506_0708,
..Default::default()
}
.into();
assert_eq!(ix_program_id, program_id);
assert_eq!(
data,
ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
hex!("3713"), hex!("0807060504030201"), [0], ],
);
assert_eq!(accounts.len(), FIXED_ACCOUNTS);
assert_readonly_signer(&accounts[0], solver);
assert_readonly_nonsigner(&accounts[1], INSTRUCTIONS_SYSVAR_ID);
assert_readonly_nonsigner(&accounts[2], state_pda);
assert_readonly_nonsigner(&accounts[3], TokenProgram::SplToken.address());
assert_readonly_nonsigner(&accounts[4], TokenProgram::Token2022.address());
}
#[test]
fn begin_settle_carries_the_token_program_slots_it_is_given() {
for (only_token_program, expected) in [
(
None,
[
TokenProgram::SplToken.address(),
TokenProgram::Token2022.address(),
],
),
(
Some(TokenProgram::SplToken),
[TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID],
),
(
Some(TokenProgram::Token2022),
[INSTRUCTIONS_SYSVAR_ID, TokenProgram::Token2022.address()],
),
] {
let Instruction { accounts, .. } = Instruction::from(BeginSettle {
program_id: Pubkey::new_unique(),
state_pda: Pubkey::new_unique(),
solver: Pubkey::new_unique(),
only_token_program,
..Default::default()
});
let slots: Vec<Pubkey> = accounts[3..].iter().map(|meta| meta.pubkey).collect();
assert_eq!(
slots, expected,
"{only_token_program:?} should name just the programs it settles against",
);
}
}
#[test]
fn begin_settle_sorts_orders_by_pda() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let solver = pubkey_from_seed("solver");
let high_order_pda = Pubkey::new_from_array([0xbb; 32]);
let high_sell_token_account = Pubkey::new_from_array([0xa0; 32]);
let low_order_pda = Pubkey::new_from_array([0xaa; 32]);
let low_sell_token_account = Pubkey::new_from_array([0xb0; 32]);
let high_sell_mint = Pubkey::new_from_array([0xc0; 32]);
let Instruction { data, accounts, .. } = BeginSettle {
program_id,
state_pda,
solver,
finalize_ix_index: 0x1337,
auction_id: AUCTION_ID,
order_pdas: &[high_order_pda, low_order_pda],
sell_token_accounts: &[high_sell_token_account, low_sell_token_account],
sell_mints: &[Some(high_sell_mint), None],
pulls: &[&[], &[]],
..Default::default()
}
.into();
assert_eq!(
data,
ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
hex!("3713"), AUCTION_ID.to_le_bytes(), [2], [0, 0], ],
);
let expected: Vec<Pubkey> = vec![
solver,
INSTRUCTIONS_SYSVAR_ID,
state_pda,
TokenProgram::SplToken.address(),
TokenProgram::Token2022.address(),
low_order_pda,
low_sell_token_account,
MINT_PLACEHOLDER,
high_order_pda,
high_sell_token_account,
high_sell_mint,
];
let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
assert_eq!(actual, expected);
let writable: Vec<Pubkey> = accounts
.iter()
.filter(|account| account.is_writable)
.map(|account| account.pubkey)
.collect();
assert_eq!(
writable,
vec![
low_order_pda,
low_sell_token_account,
high_order_pda,
high_sell_token_account,
],
);
let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS);
assert!(order_accounts.iter().all(|account| !account.is_signer));
}
#[test]
fn begin_settle_encodes_grouped_transfers() {
let program_id = pubkey_from_seed("program id");
let state_pda = pubkey_from_seed("state pda");
let solver = pubkey_from_seed("solver");
let order_a = Pubkey::new_from_array([0x01; 32]);
let sell_a = Pubkey::new_from_array([0x02; 32]);
let order_b = Pubkey::new_from_array([0x03; 32]);
let sell_b = Pubkey::new_from_array([0x04; 32]);
let dest_a0 = Pubkey::new_from_array([0x05; 32]);
let dest_a1 = Pubkey::new_from_array([0x06; 32]);
let dest_b0 = Pubkey::new_from_array([0x07; 32]);
let mint_b = Pubkey::new_from_array([0x08; 32]);
let Instruction { data, accounts, .. } = BeginSettle {
program_id,
state_pda,
solver,
finalize_ix_index: 0x1337,
auction_id: AUCTION_ID,
order_pdas: &[order_a, order_b],
sell_token_accounts: &[sell_a, sell_b],
sell_mints: &[None, Some(mint_b)],
pulls: &[
&[
Pull {
destination: dest_a0,
amount: 0x0102,
},
Pull {
destination: dest_a1,
amount: 0x0304,
},
],
&[Pull {
destination: dest_b0,
amount: 0x0506,
}],
],
..Default::default()
}
.into();
assert_eq!(
data,
ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
hex!("3713"), AUCTION_ID.to_le_bytes(), [2], [2, 1], hex!("0201000000000000"),
hex!("0403000000000000"),
hex!("0605000000000000"),
],
);
let expected: Vec<Pubkey> = vec![
solver,
INSTRUCTIONS_SYSVAR_ID,
state_pda,
TokenProgram::SplToken.address(),
TokenProgram::Token2022.address(),
order_a,
sell_a,
MINT_PLACEHOLDER,
dest_a0,
dest_a1,
order_b,
sell_b,
mint_b,
dest_b0,
];
let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
assert_eq!(actual, expected);
let writable: Vec<Pubkey> = accounts
.iter()
.filter(|account| account.is_writable)
.map(|account| account.pubkey)
.collect();
assert_eq!(
writable,
vec![order_a, sell_a, dest_a0, dest_a1, order_b, sell_b, dest_b0,],
);
let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS);
assert!(order_accounts.iter().all(|account| !account.is_signer));
}
#[test]
fn begin_settle_input_parses_valid_input() {
let sysvar = pubkey_from_seed("sysvar");
let state = pubkey_from_seed("state pda");
let spl_token_program = pubkey_from_seed("spl token program");
let token_2022_program = pubkey_from_seed("token 2022 program");
let solver = pubkey_from_seed("solver");
let accounts = [
fake_account(solver),
fake_account(sysvar),
fake_account(state),
fake_account(spl_token_program),
fake_account(token_2022_program),
];
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x37, 0x13], hex!("0807060504030201"), [0x00], ];
let BeginSettleInput {
finalize_ix_index,
auction_id,
solver_account,
instructions_sysvar_account,
orders,
state_pda_account,
} = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(finalize_ix_index, 0x1337);
assert_eq!(auction_id, 0x0102_0304_0506_0708);
assert_eq!(instructions_sysvar_account.address(), &sysvar);
assert_eq!(orders.iter().count(), 0);
assert_eq!(state_pda_account.address(), &state);
assert_eq!(solver_account.address(), &solver);
}
#[test]
fn begin_settle_input_rejects_different_discriminator() {
let data = ix_data![
[SettlementInstruction::FinalizeSettle.discriminator()],
[0, 0], ];
let accounts: [AccountView; 0] = [];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn begin_settle_input_rejects_empty_accounts() {
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], ];
let accounts: [AccountView; 0] = [];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn begin_settle_input_rejects_missing_auction_id() {
let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], [0x01, 0x02, 0x03], ];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn begin_settle_input_pairs_orders_with_their_accounts() {
let sysvar = pubkey_from_seed("sysvar");
let state = pubkey_from_seed("state pda");
let spl_token_program = pubkey_from_seed("spl token program");
let token_2022_program = pubkey_from_seed("token 2022 program");
let solver = pubkey_from_seed("solver");
let order_pda = pubkey_from_seed("order pda");
let sell_token = pubkey_from_seed("sell token");
let sell_mint = pubkey_from_seed("sell mint");
let accounts = [
fake_account(solver),
fake_account(sysvar),
fake_account(state),
fake_account(spl_token_program),
fake_account(token_2022_program),
fake_account(order_pda),
fake_account(sell_token),
fake_account(sell_mint),
];
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x37, 0x13], AUCTION_ID.to_le_bytes(), [0x01], [0x00], ];
let BeginSettleInput {
finalize_ix_index,
auction_id,
solver_account,
instructions_sysvar_account,
orders,
state_pda_account,
} = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(finalize_ix_index, 0x1337);
assert_eq!(auction_id, AUCTION_ID);
assert_eq!(instructions_sysvar_account.address(), &sysvar);
assert_eq!(state_pda_account.address(), &state);
assert_eq!(solver_account.address(), &solver);
let mut orders = orders.iter();
let order = orders.next().expect("one settled order");
assert_eq!(order.order_pda.address(), &order_pda);
assert_eq!(order.sell_token_account.address(), &sell_token);
assert_eq!(mint_address(order.sell_mint), &sell_mint);
assert_eq!(order.destinations.len(), 0);
assert!(orders.next().is_none());
}
#[test]
fn begin_settle_input_parses_transfers() {
let sysvar = pubkey_from_seed("sysvar");
let state = pubkey_from_seed("state pda");
let spl_token_program = pubkey_from_seed("spl token program");
let token_2022_program = pubkey_from_seed("token 2022 program");
let solver = pubkey_from_seed("solver");
let order_pda = pubkey_from_seed("order pda");
let sell_token = pubkey_from_seed("sell token");
let sell_mint = pubkey_from_seed("sell mint");
let dest0 = pubkey_from_seed("destination 0");
let dest1 = pubkey_from_seed("destination 1");
let accounts = [
fake_account(solver),
fake_account(sysvar),
fake_account(state),
fake_account(spl_token_program),
fake_account(token_2022_program),
fake_account(order_pda),
fake_account(sell_token),
fake_account(sell_mint),
fake_account(dest0),
fake_account(dest1),
];
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x37, 0x13], AUCTION_ID.to_le_bytes(), [0x01], [0x02], 0x1122u64.to_le_bytes(),
0x3344u64.to_le_bytes(),
];
let BeginSettleInput { orders, .. } =
BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
let mut orders = orders.iter();
let order = orders.next().expect("one settled order");
assert_eq!(order.order_pda.address(), &order_pda);
assert_eq!(order.sell_token_account.address(), &sell_token);
assert_eq!(mint_address(order.sell_mint), &sell_mint);
let transfers: Vec<(&Address, u64)> = order
.destinations
.iter()
.zip(order.amounts)
.map(|(destination, amount)| (destination.address(), u64::from_le_bytes(*amount)))
.collect();
assert_eq!(transfers, vec![(&dest0, 0x1122), (&dest1, 0x3344)]);
assert!(orders.next().is_none());
}
#[test]
fn begin_settle_input_pairs_every_order_with_its_sell_token_account() {
const ORDER_COUNT: usize = 16;
let mut expected: Vec<(Address, Address, Address)> = Vec::new();
for i in 0..ORDER_COUNT {
let order_pda = pubkey_from_seed(&format!("order pda {i}"));
let sell_token = pubkey_from_seed(&format!("sell token {i}"));
let sell_mint = pubkey_from_seed(&format!("sell mint {i}"));
expected.push((order_pda, sell_token, sell_mint));
}
let mut accounts = vec![
fake_account_from_array([0xff; 32]),
fake_account_from_array([0xfe; 32]),
fake_account_from_array([0xfd; 32]),
fake_account_from_array([0xfc; 32]),
fake_account_from_array([0xfb; 32]),
];
for &(order_pda, sell_token, sell_mint) in &expected {
accounts.push(fake_account(order_pda));
accounts.push(fake_account(sell_token));
accounts.push(fake_account(sell_mint));
}
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x37, 0x13], AUCTION_ID.to_le_bytes(), [ORDER_COUNT as u8], [0u8; ORDER_COUNT],
];
let parsed = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
let actual: Vec<(Address, Address, Address)> = parsed
.orders
.iter()
.map(|order| {
assert_eq!(order.destinations.len(), 0);
(
*order.order_pda.address(),
*order.sell_token_account.address(),
*mint_address(order.sell_mint),
)
})
.collect();
assert_eq!(actual, expected);
}
#[test]
fn begin_settle_input_rejects_too_few_accounts() {
let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 2 }>();
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x00], ];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(SettlementError::AccountCountNotMatchingOrderCount.into()),
);
}
#[test]
fn begin_settle_input_rejects_too_many_accounts() {
let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 5 }>();
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x01], 0x1122u64.to_le_bytes(),
];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(SettlementError::AccountCountNotMatchingOrderCount.into()),
);
}
#[test]
fn begin_settle_input_rejects_counts_not_summing_to_destinations() {
let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 5 }>();
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x01], 0u64.to_le_bytes(),
0u64.to_le_bytes(),
];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(SettlementError::TransferCountMismatch.into()),
);
}
#[test]
fn begin_settle_input_rejects_missing_order_count() {
let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], AUCTION_ID.to_le_bytes(), ];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn begin_settle_input_rejects_body_too_short_for_counts() {
let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], AUCTION_ID.to_le_bytes(), [0x02], [0x00], ];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn begin_settle_input_rejects_partial_amount() {
let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x00], [0x11, 0x22, 0x33, 0x44], ];
assert_eq!(
BeginSettleInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
}