From 61125855c45223972eac09608e69badfa8c9d03f Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:40:45 +0200 Subject: [PATCH 1/8] Add instruction to remove a solver --- bench-report.json | 28 +- client/src/instructions.rs | 23 ++ client/src/parse.rs | 16 +- interface/src/data/state.rs | 104 ++++++- interface/src/instruction/mod.rs | 1 + interface/src/instruction/remove_solver.rs | 243 +++++++++++++++++ interface/src/lib.rs | 14 +- programs/settlement/src/add_solver.rs | 4 +- programs/settlement/src/lib.rs | 10 +- programs/settlement/src/remove_solver.rs | 165 ++++++++++++ programs/settlement/tests/common/benchmark.rs | 2 + programs/settlement/tests/remove_solvers.rs | 255 ++++++++++++++++++ 12 files changed, 842 insertions(+), 23 deletions(-) create mode 100644 interface/src/instruction/remove_solver.rs create mode 100644 programs/settlement/src/remove_solver.rs create mode 100644 programs/settlement/tests/remove_solvers.rs diff --git a/bench-report.json b/bench-report.json index 738f6ee..1d0e2ce 100644 --- a/bench-report.json +++ b/bench-report.json @@ -12,6 +12,8 @@ "reclaim_buffer/max_buffers_in_one_instruction": 64, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9, "reclaim_order/happy_path_returns_lamports_and_closes_pda": 4, + "remove_solver/remove_with_many_existing_solvers": 5, + "remove_solver/removes_a_solver": 5, "settle/finalizes_with_no_pushes": 5, "settle/pulls_from_multiple_orders": 15, "settle/pulls_funds_to_destination": 10, @@ -28,16 +30,18 @@ "compute_units": { "add_solver/add_with_many_existing_solvers": 5074, "add_solver/adds_a_solver": 4622, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 10345, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21743, - "create_buffers/max_buffers_in_one_instruction": 177040, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 10347, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21747, + "create_buffers/max_buffers_in_one_instruction": 177071, "create_order/happy_path_creates_order_pda_with_expected_body": 4978, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4529, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4530, "reclaim_buffer/funded_buffer_is_skipped": 6335, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7483, - "reclaim_buffer/max_buffers_in_one_instruction": 136652, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18082, - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2183, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7482, + "reclaim_buffer/max_buffers_in_one_instruction": 136622, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18081, + "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2182, + "remove_solver/remove_with_many_existing_solvers": 3763, + "remove_solver/removes_a_solver": 3498, "settle/finalizes_with_no_pushes": 7154, "settle/pulls_from_multiple_orders": 20043, "settle/pulls_funds_to_destination": 13632, @@ -47,9 +51,9 @@ "settle/pushes_several_orders_from_one_buffer": 17750, "settle/settles_a_single_order": 12505, "settle/settles_multiple_orders": 23062, - "transfer_authority/manager_can_transfer_manager": 3174, - "transfer_authority/manager_can_transfer_reclaim_authority": 3176, - "transfer_authority/reclaim_authority_can_transfer_itself": 3180 + "transfer_authority/manager_can_transfer_manager": 3173, + "transfer_authority/manager_can_transfer_reclaim_authority": 3175, + "transfer_authority/reclaim_authority_can_transfer_itself": 3179 }, "transaction_bytes": { "add_solver/add_with_many_existing_solvers": 366, @@ -64,6 +68,8 @@ "reclaim_buffer/max_buffers_in_one_instruction": 332, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466, "reclaim_order/happy_path_returns_lamports_and_closes_pda": 236, + "remove_solver/remove_with_many_existing_solvers": 365, + "remove_solver/removes_a_solver": 365, "settle/finalizes_with_no_pushes": 290, "settle/pulls_from_multiple_orders": 656, "settle/pulls_funds_to_destination": 473, diff --git a/client/src/instructions.rs b/client/src/instructions.rs index e1c7976..207016e 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -271,6 +271,29 @@ impl From for Instruction { } } +/// Removes `solver` from the state PDA's solver list. Authorized by `manager`; +/// the freed rent is paid to `rent_recipient`. +pub struct RemoveSolver { + pub program_id: Pubkey, + pub manager: Pubkey, + pub rent_recipient: Pubkey, + pub solver: Pubkey, +} + +impl From for Instruction { + fn from(builder: RemoveSolver) -> Self { + let (state_pda, _bump) = find_state_pda(&builder.program_id); + cow_settlement_interface::instruction::remove_solver::RemoveSolver { + program_id: builder.program_id, + manager: builder.manager, + rent_recipient: builder.rent_recipient, + state_pda, + solver: builder.solver, + } + .into() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/client/src/parse.rs b/client/src/parse.rs index 6088546..7f0c235 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -11,6 +11,7 @@ use cow_settlement_interface::{ initialize::InitializeInput, reclaim_buffer::ReclaimBufferInput, reclaim_order::ReclaimOrderInput, + remove_solver::RemoveSolverInput, settle::{BeginSettleInput, FinalizeSettleInput}, transfer_authority::TransferAuthorityInput, InstructionInputParsing, @@ -30,6 +31,7 @@ pub enum ParsedInstruction<'a, A> { ReclaimBuffer(ReclaimBufferInput<'a, A>), TransferAuthority(TransferAuthorityInput<'a, A>), AddSolver(AddSolverInput<'a, A>), + RemoveSolver(RemoveSolverInput<'a, A>), } /// Parses any settlement instruction by its discriminator. @@ -66,6 +68,9 @@ pub fn parse_instruction<'a, A>( SettlementInstruction::AddSolver => { ParsedInstruction::AddSolver(AddSolverInput::parse_body(remaining_data, accounts)?) } + SettlementInstruction::RemoveSolver => ParsedInstruction::RemoveSolver( + RemoveSolverInput::parse_body(remaining_data, accounts)?, + ), }) } @@ -74,7 +79,7 @@ mod tests { use super::*; use crate::instructions::{ AddSolver, BeginSettle, CreateBuffers, CreateOrder, FinalizeSettle, Initialize, - InitializedIntent, + InitializedIntent, RemoveSolver, }; use cow_settlement_interface::{ data::intent::fixtures::sample_intent, @@ -159,6 +164,13 @@ mod tests { solver: pubkey_from_seed("solver"), } .into(), + SettlementInstruction::RemoveSolver => RemoveSolver { + program_id, + manager: payer, + rent_recipient: payer, + solver: pubkey_from_seed("solver"), + } + .into(), } } @@ -177,6 +189,7 @@ mod tests { SettlementInstruction::ReclaimBuffer, SettlementInstruction::TransferAuthority, SettlementInstruction::AddSolver, + SettlementInstruction::RemoveSolver, ] { let ix = build(expected); let accounts: Vec<_> = ix @@ -196,6 +209,7 @@ mod tests { ParsedInstruction::ReclaimBuffer(_) => SettlementInstruction::ReclaimBuffer, ParsedInstruction::TransferAuthority(_) => SettlementInstruction::TransferAuthority, ParsedInstruction::AddSolver(_) => SettlementInstruction::AddSolver, + ParsedInstruction::RemoveSolver(_) => SettlementInstruction::RemoveSolver, }; assert_eq!(actual, expected); } diff --git a/interface/src/data/state.rs b/interface/src/data/state.rs index 3850384..c21809e 100644 --- a/interface/src/data/state.rs +++ b/interface/src/data/state.rs @@ -172,6 +172,19 @@ impl> StateAccount { .checked_add(WIDTH_PUBKEY) .ok_or(ProgramError::ArithmeticOverflow) } + + /// The account's data length after shrinking it by one solver slot: the size + /// it must be resized to once [`remove_solver`](Self::remove_solver) has + /// shifted the tail over the removed slot. + /// + /// Returns [`ProgramError::ArithmeticOverflow`] if that length underflows, + /// which a caller that located a solver to remove can treat as unreachable. + pub fn shrunk_len(&self) -> Result { + self.0 + .len() + .checked_sub(WIDTH_PUBKEY) + .ok_or(ProgramError::ArithmeticOverflow) + } } impl<'a> StateAccount> { @@ -258,6 +271,37 @@ impl> StateAccount { data[gap..gap_end].copy_from_slice(&solver.to_bytes()); Ok(()) } + + /// Remove `solver` from the sorted solver list, or fail with + /// [`SettlementError::SolverNotFound`] if it isn't stored. + /// + /// The entries after the removed one are shifted one slot left to close the + /// gap; the now-stale trailing slot is left in place for the caller to drop + /// by resizing the account down to [`shrunk_len`](Self::shrunk_len). + pub fn remove_solver(&mut self, solver: &Pubkey) -> Result<(), ProgramError> { + let index = match self.solver_region().binary_search(&solver.to_bytes()) { + Ok(index) => index, + Err(_) => return Err(SettlementError::SolverNotFound.into()), + }; + + // Shift the entries after `index` down one slot; the trailing slot is + // left unchanged. + let data: &mut [u8] = &mut self.0; + let len = data.len(); + let offset = WIDTH_HEADER + .checked_add( + index + .checked_mul(WIDTH_PUBKEY) + .expect("removal index bound by data length"), + ) + .expect("removal offset bound by data length"); + let slot_end = offset + .checked_add(WIDTH_PUBKEY) + .expect("removal slot bound by data length"); + + data.copy_within(slot_end..len, offset); + Ok(()) + } } /// Test scaffolding for building state-account bytes, shared by this crate's @@ -555,8 +599,6 @@ mod tests { prop_assert_eq!(state.solvers().collect::>(), expected); } - /// `insert_solver` rejects a solver that is already stored and leaves - /// the live list untouched. #[test] fn insert_solver_rejects_an_existing_solver( header in fixtures::arb_init_params(), @@ -583,6 +625,64 @@ mod tests { let state = StateAccount::attach(&bytes[..]).expect("valid header"); prop_assert_eq!(state.solvers().take(stored.len()).collect::>(), stored); } + + #[test] + fn remove_solver_drops_a_present_solver( + header in fixtures::arb_init_params(), + // Unique and already sorted, being a `BTreeSet`. + raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 1..50), + pick in any::(), + ) { + let stored: Vec = + raw_solvers.into_iter().map(Pubkey::new_from_array).collect(); + let index = pick.index(stored.len()); + let removed = stored[index]; + + // Remove the solver, then shrink to the length `shrunk_len` + // reports, exactly as the handler resizes the account. + let mut bytes = fixtures::state_account_bytes(&header, &stored); + let shrunk_len = StateAccount::attach(&bytes[..]) + .expect("valid header") + .shrunk_len() + .expect("shrunk length fits"); + prop_assert_eq!(shrunk_len, bytes.len().strict_sub(WIDTH_PUBKEY)); + StateAccount::attach(&mut bytes[..]) + .expect("valid header") + .remove_solver(&removed) + .expect("a present solver is removed"); + bytes.truncate(shrunk_len); + + let mut expected = stored; + expected.remove(index); + let state = StateAccount::attach(&bytes[..]).expect("valid header"); + prop_assert_eq!(state.solvers().collect::>(), expected); + prop_assert_eq!(state.solver_search(&removed), Err(index)); + } + + #[test] + fn remove_solver_rejects_an_absent_solver( + header in fixtures::arb_init_params(), + // Unique and already sorted, being a `BTreeSet`. + raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 0..50), + raw_absent in any::<[u8; 32]>(), + ) { + prop_assume!(!raw_solvers.contains(&raw_absent)); + let stored: Vec = + raw_solvers.into_iter().map(Pubkey::new_from_array).collect(); + let absent = Pubkey::new_from_array(raw_absent); + + let mut bytes = fixtures::state_account_bytes(&header, &stored); + prop_assert_eq!( + StateAccount::attach(&mut bytes[..]) + .expect("valid header") + .remove_solver(&absent), + Err(SettlementError::SolverNotFound.into()), + ); + + // Nothing was removed: the stored solvers still read back in order. + let state = StateAccount::attach(&bytes[..]).expect("valid header"); + prop_assert_eq!(state.solvers().collect::>(), stored); + } } } } diff --git a/interface/src/instruction/mod.rs b/interface/src/instruction/mod.rs index 6da3a24..2ff4200 100644 --- a/interface/src/instruction/mod.rs +++ b/interface/src/instruction/mod.rs @@ -14,6 +14,7 @@ pub mod create_order; pub mod initialize; pub mod reclaim_buffer; pub mod reclaim_order; +pub mod remove_solver; pub mod settle; pub mod transfer_authority; diff --git a/interface/src/instruction/remove_solver.rs b/interface/src/instruction/remove_solver.rs new file mode 100644 index 0000000..233fa8c --- /dev/null +++ b/interface/src/instruction/remove_solver.rs @@ -0,0 +1,243 @@ +//! `RemoveSolver` instruction builder and parser. +//! +//! It removes a solver from the sorted solver list stored in the state PDA (see +//! [`crate::data::state`]). Only the manager may authorize it. The state PDA +//! shrinks by one solver and the freed rent is paid to `rent_recipient`. + +use core::mem::size_of; + +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +use crate::instruction::InstructionInputParsing; +use crate::SettlementInstruction; + +/// Builder for a `RemoveSolver` instruction. +/// +/// `manager` authorizes the change and must be the state PDA's current manager; +/// it signs but doesn't receive anything. `rent_recipient` receives the freed +/// rent. `solver` is removed from the sorted solver list; removing one that +/// isn't present fails. +/// +/// Wire format: `[discriminator=9, solver (32 bytes)]`. +/// Required accounts: `[manager (S), rent_recipient (W), state_pda (W)]`. +pub struct RemoveSolver { + pub program_id: Pubkey, + pub manager: Pubkey, + pub rent_recipient: Pubkey, + pub state_pda: Pubkey, + pub solver: Pubkey, +} + +impl From for Instruction { + fn from(builder: RemoveSolver) -> Self { + let mut data = vec![SettlementInstruction::RemoveSolver.discriminator()]; + data.extend_from_slice(&builder.solver.to_bytes()); + Instruction { + program_id: builder.program_id, + accounts: vec![ + AccountMeta::new_readonly(builder.manager, true), + AccountMeta::new(builder.rent_recipient, false), + AccountMeta::new(builder.state_pda, false), + ], + data, + } + } +} + +/// Parsed inputs of a `RemoveSolver` instruction. +pub struct RemoveSolverInput<'a, A> { + pub manager: &'a A, + pub rent_recipient: &'a A, + pub state_pda: &'a A, + pub solver: Pubkey, +} + +impl<'a, A> InstructionInputParsing<'a, A> for RemoveSolverInput<'a, A> { + const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::RemoveSolver; + + fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result { + let solver: &[u8; size_of::()] = instruction_data + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + let solver = Pubkey::new_from_array(*solver); + + // Accounts: [manager (S), rent_recipient (W), state_pda (W)]. + let [manager, rent_recipient, state_pda, ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + Ok(Self { + manager, + rent_recipient, + state_pda, + solver, + }) + } +} + +/// Test scaffolding for `RemoveSolver` parsing and handling, shared by this +/// crate's tests and the settlement program's via the `test-fixtures` feature. +#[cfg(any(test, feature = "test-fixtures"))] +pub mod fixtures { + use solana_address::Address; + + use super::{Instruction, RemoveSolver}; + + /// Number of accounts `RemoveSolver` expects: manager, rent recipient, and + /// state PDA. + pub const NUM_ACCOUNTS: usize = 3; + + /// `RemoveSolver` instruction data with placeholder addresses, for failure + /// cases where the actual addresses don't matter. + pub fn remove_solver_data() -> Vec { + let zero = Address::new_from_array([0; 32]); + Instruction::from(RemoveSolver { + program_id: zero, + manager: zero, + rent_recipient: zero, + state_pda: zero, + solver: zero, + }) + .data + } +} + +#[cfg(test)] +mod tests { + use super::fixtures::{remove_solver_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_signer, assert_writable_nonsigner}; + use solana_account_view::AccountView; + + #[test] + fn remove_solver_input_parses_valid_input() { + let program_id = pubkey_from_seed("program id"); + let manager = fake_account(pubkey_from_seed("manager")); + let rent_recipient = fake_account(pubkey_from_seed("rent recipient")); + let state_pda = fake_account(pubkey_from_seed("state pda")); + let solver = pubkey_from_seed("solver"); + + let data = Instruction::from(RemoveSolver { + program_id, + manager: *manager.address(), + rent_recipient: *rent_recipient.address(), + state_pda: *state_pda.address(), + solver, + }) + .data; + let accounts = [manager, rent_recipient, state_pda]; + + let RemoveSolverInput { + manager: parsed_manager, + rent_recipient: parsed_rent_recipient, + state_pda: parsed_state_pda, + solver: parsed_solver, + } = RemoveSolverInput::parse(&data, &accounts).expect("parse should succeed"); + + assert_eq!(parsed_manager.address(), accounts[0].address()); + assert_eq!(parsed_rent_recipient.address(), accounts[1].address()); + assert_eq!(parsed_state_pda.address(), accounts[2].address()); + assert_eq!(parsed_solver, solver); + } + + #[test] + fn remove_solver_input_rejects_long_data() { + let mut data = remove_solver_data(); + data.push(0); // trailing byte + let accounts = fake_sequential_accounts::(); + assert_eq!( + RemoveSolverInput::parse(&data, &accounts).err(), + Some(ProgramError::InvalidInstructionData), + ); + } + + #[test] + fn remove_solver_input_rejects_short_data() { + let mut data = remove_solver_data(); + data.pop(); // one byte short + let accounts = fake_sequential_accounts::(); + assert_eq!( + RemoveSolverInput::parse(&data, &accounts).err(), + Some(ProgramError::InvalidInstructionData), + ); + } + + #[test] + fn remove_solver_input_rejects_missing_accounts() { + let data = remove_solver_data(); + let mut accounts: Vec = fake_sequential_accounts::().into(); + accounts.pop(); + assert_eq!( + RemoveSolverInput::parse(&data, &accounts).err(), + Some(ProgramError::NotEnoughAccountKeys), + ); + } + + #[test] + fn instruction_data_has_expected_layout() { + let solver = pubkey_from_seed("solver"); + let Instruction { data, .. } = RemoveSolver { + program_id: pubkey_from_seed("program id"), + manager: pubkey_from_seed("manager"), + rent_recipient: pubkey_from_seed("rent recipient"), + state_pda: pubkey_from_seed("state pda"), + solver, + } + .into(); + + assert_eq!(data.len(), 1 + size_of::()); + assert_eq!(data[0], SettlementInstruction::RemoveSolver.discriminator()); + assert_eq!(&data[1..], &solver.to_bytes()); + } + + #[test] + fn instruction_data_regression() { + let solver = Pubkey::new_from_array([0x11; 32]); + let Instruction { data, .. } = RemoveSolver { + program_id: pubkey_from_seed("program id"), + manager: pubkey_from_seed("manager"), + rent_recipient: pubkey_from_seed("rent recipient"), + state_pda: pubkey_from_seed("state pda"), + solver, + } + .into(); + + #[rustfmt::skip] + let expected: [u8; 1 + size_of::()] = [ + // discriminator (RemoveSolver = 9) + 0x09, + // solver + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + ]; + assert_eq!(data, expected); + } + + #[test] + fn instruction_has_expected_accounts() { + let manager = pubkey_from_seed("manager"); + let rent_recipient = pubkey_from_seed("rent recipient"); + let state_pda = pubkey_from_seed("state pda"); + let Instruction { accounts, .. } = RemoveSolver { + program_id: pubkey_from_seed("program id"), + manager, + rent_recipient, + state_pda, + solver: pubkey_from_seed("solver"), + } + .into(); + + assert_eq!(accounts.len(), 3); + // The manager authorizes the change; the freed rent is paid to the + // recipient; the state PDA is shrunk and written. + assert_readonly_signer(&accounts[0], manager); + assert_writable_nonsigner(&accounts[1], rent_recipient); + assert_writable_nonsigner(&accounts[2], state_pda); + } +} diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 9a294c8..e8867fa 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -26,6 +26,7 @@ pub enum SettlementInstruction { ReclaimBuffer = 6, TransferAuthority = 7, AddSolver = 8, + RemoveSolver = 9, } impl SettlementInstruction { @@ -222,14 +223,17 @@ pub enum SettlementError { /// `TransferAuthority`'s signer is neither the manager nor the current /// holder of the role being transferred, so it may not transfer it. UnauthorizedAuthorityTransfer = 34, - /// `AddSolver`'s manager account isn't a signer, or doesn't match the - /// `manager` recorded in the settlement state PDA. + /// `AddSolver`/`RemoveSolver`'s manager account isn't a signer, or doesn't + /// match the `manager` recorded in the settlement state PDA, so it may not + /// change the solver list. UnauthorizedSolverManagement = 35, /// `AddSolver`'s solver is already in the state PDA's solver list. SolverAlreadyExists = 36, - /// `BeginSettle`/`FinalizeSettle`'s solver account isn't a signer or isn't - /// in the state PDA's solver list, so it may not settle. - UnauthorizedSolver = 37, + /// `RemoveSolver`'s solver isn't in the state PDA's solver list. + SolverNotFound = 37, + /// `BeginSettle`'s solver account isn't a signer or isn't in the state PDA's + /// solver list, so it may not settle. + UnauthorizedSolver = 38, } impl From for u32 { diff --git a/programs/settlement/src/add_solver.rs b/programs/settlement/src/add_solver.rs index 244364b..2a51bff 100644 --- a/programs/settlement/src/add_solver.rs +++ b/programs/settlement/src/add_solver.rs @@ -93,8 +93,8 @@ mod tests { #[test] fn process_add_solver_rejects_non_canonical_state_pda() { - // `fake_sequential_accounts` puts the state PDA at `[3; 32]`, which is - // not the canonical state PDA for this program. + // `fake_sequential_accounts` puts the state PDA at some arbitrary + // address, which is not the canonical state PDA for this program. let data = add_solver_data(); let mut accounts = fake_sequential_accounts::(); assert_eq!( diff --git a/programs/settlement/src/lib.rs b/programs/settlement/src/lib.rs index d60fa23..1fc2f41 100644 --- a/programs/settlement/src/lib.rs +++ b/programs/settlement/src/lib.rs @@ -1,5 +1,8 @@ //! On-chain CoW Protocol settlement program. +use cow_settlement_interface::{recover_discriminator, SettlementInstruction}; +use pinocchio::{entrypoint, AccountView, Address, ProgramResult}; + mod add_solver; mod create_buffer; mod create_order; @@ -7,17 +10,17 @@ mod initialize; mod processor; mod reclaim_buffer; mod reclaim_order; +mod remove_solver; mod settle; mod transfer_authority; use add_solver::process_add_solver; -use cow_settlement_interface::{recover_discriminator, SettlementInstruction}; use create_buffer::process_create_buffer; use create_order::process_create_order; use initialize::process_initialize; -use pinocchio::{entrypoint, AccountView, Address, ProgramResult}; use reclaim_buffer::process_reclaim_buffer; use reclaim_order::process_reclaim_order; +use remove_solver::process_remove_solver; use settle::{process_begin_settle, process_finalize_settle}; use transfer_authority::process_transfer_authority; @@ -57,5 +60,8 @@ pub fn process_instruction( SettlementInstruction::AddSolver => { process_add_solver(program_id, accounts, instruction_data) } + SettlementInstruction::RemoveSolver => { + process_remove_solver(program_id, accounts, instruction_data) + } } } diff --git a/programs/settlement/src/remove_solver.rs b/programs/settlement/src/remove_solver.rs new file mode 100644 index 0000000..c393e6f --- /dev/null +++ b/programs/settlement/src/remove_solver.rs @@ -0,0 +1,165 @@ +//! `RemoveSolver` instruction handler. +//! +//! Removes a solver from the sorted solver list that follows the state PDA +//! header, shifting the tail left to close the gap and shrinking the account. +//! Only the manager may authorize it, and the freed rent is paid to +//! `rent_recipient`. The refund is a direct lamport move out of the +//! program-owned state PDA, so no system program is involved. + +use cow_settlement_interface::{ + data::state::StateAccount, + instruction::{remove_solver::RemoveSolverInput, InstructionInputParsing}, + Role, SettlementError, +}; +use pinocchio::{ + error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + AccountView, Address, ProgramResult, Resize, +}; + +use crate::processor::check_state_pda; + +pub fn process_remove_solver( + program_id: &Address, + accounts: &mut [AccountView], + instruction_data: &[u8], +) -> ProgramResult { + let RemoveSolverInput { + manager, + rent_recipient, + state_pda, + solver, + } = RemoveSolverInput::parse(instruction_data, accounts)?; + + check_state_pda(program_id, state_pda)?; + + let mut state_pda = *state_pda; + let new_len = { + let mut state = StateAccount::attach(state_pda.try_borrow_mut()?)?; + if !manager.is_signer() || *manager.address() != state.authority(Role::Manager) { + return Err(SettlementError::UnauthorizedSolverManagement.into()); + } + state.remove_solver(&solver)?; + state + .shrunk_len() + .expect("a solver has been removed, so the length doesn't underflow") + }; + state_pda.resize(new_len)?; + + // Refund the rent the smaller account no longer needs to `rent_recipient`. + // The state PDA is program-owned, so the program may debit it directly. + let surplus = state_pda + .lamports() + .checked_sub(Rent::get()?.try_minimum_balance(new_len)?) + // The failure case is basically unreachable unless there are some + // protocol changes to the rent mechanism. + .ok_or(ProgramError::AccountNotRentExempt)?; + let mut rent_recipient = *rent_recipient; + let refunded = rent_recipient + .lamports() + .checked_add(surplus) + .ok_or(ProgramError::ArithmeticOverflow)?; + let retained = state_pda + .lamports() + .checked_sub(surplus) + .ok_or(ProgramError::ArithmeticOverflow)?; + state_pda.set_lamports(retained); + rent_recipient.set_lamports(refunded); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; + use cow_settlement_interface::instruction::remove_solver::fixtures::{ + remove_solver_data, NUM_ACCOUNTS, + }; + use pinocchio::error::ProgramError; + + const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]); + + #[test] + fn process_remove_solver_propagates_parse_error() { + let mut data = remove_solver_data(); + data.push(0); // trailing byte triggers a parse error + let mut accounts = fake_sequential_accounts::(); + assert_eq!( + process_remove_solver(&PROGRAM_ID, &mut accounts, &data), + Err(ProgramError::InvalidInstructionData), + ); + } + + #[test] + fn process_remove_solver_rejects_non_canonical_state_pda() { + // `fake_sequential_accounts` puts the state PDA at some arbitrary + // address, which is not the canonical state PDA for this program. + let data = remove_solver_data(); + let mut accounts = fake_sequential_accounts::(); + assert_eq!( + process_remove_solver(&PROGRAM_ID, &mut accounts, &data), + Err(SettlementError::StateAccountMismatch.into()), + ); + } + + mod proptest { + use ::proptest::prelude::*; + + use super::*; + use cow_settlement_interface::data::state::fixtures::{ + arb_init_params, state_account_bytes, + }; + use cow_settlement_interface::fixtures::pubkey_from_seed; + use cow_settlement_interface::instruction::fixtures::{ + fake_account, fake_account_owned_by, fake_signer, + }; + use cow_settlement_interface::instruction::remove_solver::RemoveSolver; + use cow_settlement_interface::pda::state::find_state_pda; + use cow_settlement_interface::{Instruction, Pubkey}; + + proptest! { + #[test] + fn process_remove_solver_rejects_an_absent_solver( + header in arb_init_params(), + // Unique and already sorted, being a `BTreeSet`. + raw_solvers in ::proptest::collection::btree_set(any::<[u8; 32]>(), 0..50), + raw_absent in any::<[u8; 32]>(), + ) { + prop_assume!(!raw_solvers.contains(&raw_absent)); + let manager = header.manager; + let stored: Vec = + raw_solvers.into_iter().map(Pubkey::new_from_array).collect(); + let absent = Pubkey::new_from_array(raw_absent); + + // Mock the three accounts the handler parses. Only the manager + // signer and the state PDA carry meaning here; the rent recipient + // is never touched, since the reject happens before the refund. + let (state_pda_address, _bump) = find_state_pda(&PROGRAM_ID); + let mut accounts = [ + fake_signer(manager), + fake_account(pubkey_from_seed("rent recipient")), + fake_account_owned_by( + state_pda_address, + PROGRAM_ID, + &state_account_bytes(&header, &stored), + ), + ]; + + let data = Instruction::from(RemoveSolver { + program_id: PROGRAM_ID, + manager, + rent_recipient: pubkey_from_seed("rent recipient"), + state_pda: state_pda_address, + solver: absent, + }) + .data; + + prop_assert_eq!( + process_remove_solver(&PROGRAM_ID, &mut accounts, &data), + Err(SettlementError::SolverNotFound.into()), + ); + } + } + } +} diff --git a/programs/settlement/tests/common/benchmark.rs b/programs/settlement/tests/common/benchmark.rs index 23f0ee3..ced249d 100644 --- a/programs/settlement/tests/common/benchmark.rs +++ b/programs/settlement/tests/common/benchmark.rs @@ -25,6 +25,7 @@ pub enum BenchLabel { Settle, TransferAuthority, AddSolver, + RemoveSolver, } impl fmt::Display for BenchLabel { @@ -40,6 +41,7 @@ impl fmt::Display for BenchLabel { Self::Settle => "settle", Self::TransferAuthority => "transfer_authority", Self::AddSolver => "add_solver", + Self::RemoveSolver => "remove_solver", }) } } diff --git a/programs/settlement/tests/remove_solvers.rs b/programs/settlement/tests/remove_solvers.rs new file mode 100644 index 0000000..1de58e2 --- /dev/null +++ b/programs/settlement/tests/remove_solvers.rs @@ -0,0 +1,255 @@ +//! Integration tests for removing solvers from the state PDA's list (shrinking +//! the account and refunding rent) and the manager gate on removal. Adding +//! solvers is covered by `add_solvers.rs`; the solver gate on settling by +//! `settle_solver_auth.rs`. + +use cow_settlement_client::cow_settlement_interface::{ + data::state::{StateAccount, WIDTH_HEADER, WIDTH_PUBKEY}, + Instruction, SettlementError, +}; +use cow_settlement_client::instructions::RemoveSolver; +use litesvm::LiteSVM; +use solana_sdk::{ + instruction::InstructionError, + pubkey::Pubkey, + signature::Signer, + transaction::{Transaction, TransactionError}, +}; + +use crate::common::{ + assert_instruction_error, + benchmark::{send_transaction_metered, BenchLabel}, + lamports, setup_init, to_instruction_error, unique_keypair, unique_pubkey, InitializedParams, +}; + +mod common; + +/// [`setup_init`] plus a funded, dedicated `rent_recipient` for removals. A +/// removal refunds the rent to this account, and the recipient of a lamport +/// credit must itself end up rent-exempt, so it's airdropped here. +fn setup() -> (LiteSVM, InitializedParams, Pubkey) { + let (mut svm, params) = setup_init(); + let rent_recipient = unique_pubkey(); + svm.airdrop(&rent_recipient, 1_000_000_000) + .expect("airdrop to rent recipient should succeed"); + (svm, params, rent_recipient) +} + +#[track_caller] +fn assert_solver_invariant(solvers: &[Pubkey]) { + assert!( + solvers.is_sorted_by(|a, b| a < b), + "invariant violated: solver list must be strictly ascending by address: {solvers:?}", + ); +} + +/// The solver list currently stored in the state PDA, in stored order. Reading +/// it also re-checks the storage invariant (see [`assert_solver_invariant`]), so +/// every test that inspects the list enforces it, not just the ones that compare +/// against a sorted expectation. +#[track_caller] +fn solvers(svm: &LiteSVM, state_pda: &Pubkey) -> Vec { + let data = svm + .get_account(state_pda) + .expect("state PDA should exist") + .data; + let solvers: Vec = StateAccount::attach(&data[..]) + .expect("state PDA should be a valid state account") + .solvers() + .collect(); + assert_solver_invariant(&solvers); + solvers +} + +/// Build a `RemoveSolver` transaction authorized by the manager, refunding the +/// freed rent to `rent_recipient`. Signed by the payer and the manager. Split +/// from [`remove_solver`] so the happy-path test can meter the same transaction. +fn remove_solver_tx( + svm: &LiteSVM, + params: &InitializedParams, + rent_recipient: &Pubkey, + solver: &Pubkey, +) -> Transaction { + let ix = RemoveSolver { + program_id: params.program_id, + manager: params.manager.pubkey(), + rent_recipient: *rent_recipient, + solver: *solver, + }; + common::signed_tx(svm, ¶ms.payer, ¶ms.manager, ix) +} + +/// Send a [`remove_solver_tx`]. +fn remove_solver( + svm: &mut LiteSVM, + params: &InitializedParams, + rent_recipient: &Pubkey, + solver: &Pubkey, +) -> Result<(), TransactionError> { + let tx = remove_solver_tx(svm, params, rent_recipient, solver); + svm.send_transaction(tx).map(|_| ()).map_err(|e| e.err) +} + +#[test] +fn removes_a_solver() { + let (mut svm, params, rent_recipient) = setup(); + let keep = unique_keypair().pubkey(); + let drop = unique_keypair().pubkey(); + common::register_solver(&mut svm, ¶ms, &keep); + common::register_solver(&mut svm, ¶ms, &drop); + + let recipient_before = lamports(&svm, &rent_recipient); + let tx = remove_solver_tx(&svm, ¶ms, &rent_recipient, &drop); + send_transaction_metered(&mut svm, tx, BenchLabel::RemoveSolver) + .expect("removing a solver should succeed"); + + // Only `keep` remains, the account shrank by one solver and stayed exactly + // rent-exempt, and the freed rent went to the rent recipient. + assert_eq!(solvers(&svm, ¶ms.state_pda), vec![keep]); + let account = svm + .get_account(¶ms.state_pda) + .expect("state PDA exists"); + assert_eq!(account.data.len(), WIDTH_HEADER + WIDTH_PUBKEY); + assert_eq!( + account.lamports, + svm.minimum_balance_for_rent_exemption(account.data.len()), + ); + assert!( + lamports(&svm, &rent_recipient) > recipient_before, + "the rent recipient received the freed rent", + ); +} + +#[test] +fn rejects_removing_absent_solver() { + let (mut svm, params, rent_recipient) = setup(); + let absent = unique_keypair().pubkey(); + + assert_instruction_error( + remove_solver(&mut svm, ¶ms, &rent_recipient, &absent), + to_instruction_error(SettlementError::SolverNotFound), + ); +} + +#[test] +fn rejects_removing_solver_by_non_manager() { + let (mut svm, params, rent_recipient) = setup(); + let solver = unique_keypair().pubkey(); + common::register_solver(&mut svm, ¶ms, &solver); + + let stranger = unique_keypair(); + let ix = RemoveSolver { + program_id: params.program_id, + manager: stranger.pubkey(), + rent_recipient, + solver, + }; + let tx = common::signed_tx(&svm, ¶ms.payer, &stranger, ix); + assert_instruction_error( + svm.send_transaction(tx).map(|_| ()).map_err(|e| e.err), + to_instruction_error(SettlementError::UnauthorizedSolverManagement), + ); +} + +#[test] +fn rejects_removing_solver_if_manager_is_not_signer() { + let (mut svm, params, rent_recipient) = setup(); + let solver = unique_keypair().pubkey(); + common::register_solver(&mut svm, ¶ms, &solver); + + // The correct manager, but with its signer flag cleared: authorization must + // require the manager to actually sign, not just be named. + let mut ix: Instruction = RemoveSolver { + program_id: params.program_id, + manager: params.manager.pubkey(), + rent_recipient, + solver, + } + .into(); + + /// Index of the manager account in a `RemoveSolver` instruction. + const MANAGER_INDEX: usize = 0; + assert!( + ix.accounts[MANAGER_INDEX].is_signer + && ix.accounts[MANAGER_INDEX].pubkey == params.manager.pubkey(), + "sanity check: MANAGER_INDEX should point to the manager signer" + ); + ix.accounts[MANAGER_INDEX].is_signer = false; + + let res = common::send(&mut svm, ¶ms.payer, vec![ix]); + assert_instruction_error( + res, + to_instruction_error(SettlementError::UnauthorizedSolverManagement), + ); +} + +/// A state PDA holding less than its shrunk rent minimum is rejected with +/// [`InstructionError::AccountNotRentExempt`], not refunded. +/// The flow in this test isn't expected to be reachable unless there are +/// changes to how rent is handled. Still, if it does, this will be less of an +/// issue than it could be. +#[test] +fn rejects_removing_from_a_below_rent_state_pda() { + let (mut svm, params, rent_recipient) = setup(); + let solver = unique_keypair().pubkey(); + common::register_solver(&mut svm, ¶ms, &solver); + + // Reduce the state PDA to one lamport below the rent minimum for zero + // solvers (its size after the removal), so it can't hold the rent it needs to + // exist and the refund's `checked_sub` underflows. + let below_rent = svm + .minimum_balance_for_rent_exemption(WIDTH_HEADER) + .strict_sub(1); + let mut account = svm + .get_account(¶ms.state_pda) + .expect("state PDA exists"); + account.lamports = below_rent; + svm.set_account(params.state_pda, account) + .expect("set_account should succeed"); + + assert_instruction_error( + remove_solver(&mut svm, ¶ms, &rent_recipient, &solver), + InstructionError::AccountNotRentExempt, + ); +} + +/// Removing a solver still works, and stays sorted, when the list is already +/// large. This test also benchmarks moving a lot of account data. +#[test] +fn remove_with_many_existing_solvers() { + let (mut svm, params, rent_recipient) = setup(); + + /// A deterministic solver address holding `index` big-endian in its leading + /// two bytes and the rest zero, so their relative order is their index's + /// order. + fn indexed_solver(index: u16) -> Pubkey { + let mut bytes = [0u8; 32]; + let leading = index.to_be_bytes(); + bytes[0] = leading[0]; + bytes[1] = leading[1]; + Pubkey::new_from_array(bytes) + } + + // Existing solvers 0x0000, 0x0001, …, written straight into the state PDA after + // its header rather than added one transaction at a time. + const EXISTING: u16 = 1_000; + const REMOVE_INDEX: u16 = 42; + let mut expected: Vec = (0..=EXISTING).map(indexed_solver).collect(); + let mut account = svm + .get_account(¶ms.state_pda) + .expect("state PDA exists"); + for solver in &expected { + account.data.extend_from_slice(&solver.to_bytes()); + } + account.lamports = svm.minimum_balance_for_rent_exemption(account.data.len()); + svm.set_account(params.state_pda, account) + .expect("set_account should succeed"); + + let sacrifice = indexed_solver(REMOVE_INDEX); + let tx = remove_solver_tx(&svm, ¶ms, &rent_recipient, &sacrifice); + send_transaction_metered(&mut svm, tx, BenchLabel::RemoveSolver) + .expect("removing from a large list should succeed"); + + expected.retain(|&s| s != sacrifice); + assert_eq!(solvers(&svm, ¶ms.state_pda), expected); +} From 43f6fe624040bc5583ed1d0a077de7afbb444f2e Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:34:15 +0200 Subject: [PATCH 2/8] Explicit function to move rent --- bench-report.json | 4 ++-- programs/settlement/src/processor.rs | 20 ++++++++++++++++++++ programs/settlement/src/remove_solver.rs | 13 ++----------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/bench-report.json b/bench-report.json index 1d0e2ce..5910626 100644 --- a/bench-report.json +++ b/bench-report.json @@ -40,8 +40,8 @@ "reclaim_buffer/max_buffers_in_one_instruction": 136622, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18081, "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2182, - "remove_solver/remove_with_many_existing_solvers": 3763, - "remove_solver/removes_a_solver": 3498, + "remove_solver/remove_with_many_existing_solvers": 3758, + "remove_solver/removes_a_solver": 3493, "settle/finalizes_with_no_pushes": 7154, "settle/pulls_from_multiple_orders": 20043, "settle/pulls_funds_to_destination": 13632, diff --git a/programs/settlement/src/processor.rs b/programs/settlement/src/processor.rs index 76e1cc7..0872c62 100644 --- a/programs/settlement/src/processor.rs +++ b/programs/settlement/src/processor.rs @@ -176,6 +176,26 @@ pub fn is_cpi_call() -> bool { get_stack_height() > TRANSACTION_LEVEL_STACK_HEIGHT } +/// Move `amount` lamports from `from` to `to` by editing their balances +/// directly, with no system program involved. +/// +/// `from` must be program-owned so the program may debit it. Both edits are +/// checked, so a balance that would under- or overflow reverts instead of +/// wrapping. +pub fn move_lamports(from: &mut AccountView, to: &mut AccountView, amount: u64) -> ProgramResult { + let debited = from + .lamports() + .checked_sub(amount) + .ok_or(ProgramError::ArithmeticOverflow)?; + let credited = to + .lamports() + .checked_add(amount) + .ok_or(ProgramError::ArithmeticOverflow)?; + from.set_lamports(debited); + to.set_lamports(credited); + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/programs/settlement/src/remove_solver.rs b/programs/settlement/src/remove_solver.rs index c393e6f..430216f 100644 --- a/programs/settlement/src/remove_solver.rs +++ b/programs/settlement/src/remove_solver.rs @@ -17,7 +17,7 @@ use pinocchio::{ AccountView, Address, ProgramResult, Resize, }; -use crate::processor::check_state_pda; +use crate::processor::{check_state_pda, move_lamports}; pub fn process_remove_solver( program_id: &Address, @@ -55,16 +55,7 @@ pub fn process_remove_solver( // protocol changes to the rent mechanism. .ok_or(ProgramError::AccountNotRentExempt)?; let mut rent_recipient = *rent_recipient; - let refunded = rent_recipient - .lamports() - .checked_add(surplus) - .ok_or(ProgramError::ArithmeticOverflow)?; - let retained = state_pda - .lamports() - .checked_sub(surplus) - .ok_or(ProgramError::ArithmeticOverflow)?; - state_pda.set_lamports(retained); - rent_recipient.set_lamports(refunded); + move_lamports(&mut state_pda, &mut rent_recipient, surplus)?; Ok(()) } From 669f59266c9c1f80cce909db2bf93d06ebfd9b39 Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:48:18 +0200 Subject: [PATCH 3/8] Have `remove_solver` return the expected resized length --- bench-report.json | 4 +-- interface/src/data/state.rs | 38 ++++++++---------------- programs/settlement/src/remove_solver.rs | 5 +--- 3 files changed, 16 insertions(+), 31 deletions(-) diff --git a/bench-report.json b/bench-report.json index 5910626..32b6abe 100644 --- a/bench-report.json +++ b/bench-report.json @@ -40,8 +40,8 @@ "reclaim_buffer/max_buffers_in_one_instruction": 136622, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18081, "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2182, - "remove_solver/remove_with_many_existing_solvers": 3758, - "remove_solver/removes_a_solver": 3493, + "remove_solver/remove_with_many_existing_solvers": 3757, + "remove_solver/removes_a_solver": 3492, "settle/finalizes_with_no_pushes": 7154, "settle/pulls_from_multiple_orders": 20043, "settle/pulls_funds_to_destination": 13632, diff --git a/interface/src/data/state.rs b/interface/src/data/state.rs index c21809e..4324611 100644 --- a/interface/src/data/state.rs +++ b/interface/src/data/state.rs @@ -172,19 +172,6 @@ impl> StateAccount { .checked_add(WIDTH_PUBKEY) .ok_or(ProgramError::ArithmeticOverflow) } - - /// The account's data length after shrinking it by one solver slot: the size - /// it must be resized to once [`remove_solver`](Self::remove_solver) has - /// shifted the tail over the removed slot. - /// - /// Returns [`ProgramError::ArithmeticOverflow`] if that length underflows, - /// which a caller that located a solver to remove can treat as unreachable. - pub fn shrunk_len(&self) -> Result { - self.0 - .len() - .checked_sub(WIDTH_PUBKEY) - .ok_or(ProgramError::ArithmeticOverflow) - } } impl<'a> StateAccount> { @@ -273,12 +260,13 @@ impl> StateAccount { } /// Remove `solver` from the sorted solver list, or fail with - /// [`SettlementError::SolverNotFound`] if it isn't stored. + /// [`SettlementError::SolverNotFound`] if it isn't stored. Returns the + /// length the account must be resized down to. /// /// The entries after the removed one are shifted one slot left to close the /// gap; the now-stale trailing slot is left in place for the caller to drop - /// by resizing the account down to [`shrunk_len`](Self::shrunk_len). - pub fn remove_solver(&mut self, solver: &Pubkey) -> Result<(), ProgramError> { + /// by resizing the account down to the returned length. + pub fn remove_solver(&mut self, solver: &Pubkey) -> Result { let index = match self.solver_region().binary_search(&solver.to_bytes()) { Ok(index) => index, Err(_) => return Err(SettlementError::SolverNotFound.into()), @@ -300,7 +288,11 @@ impl> StateAccount { .expect("removal slot bound by data length"); data.copy_within(slot_end..len, offset); - Ok(()) + + let updated_length = len + .checked_sub(WIDTH_PUBKEY) + .expect("a solver was removed, so the length doesn't underflow"); + Ok(updated_length) } } @@ -638,18 +630,14 @@ mod tests { let index = pick.index(stored.len()); let removed = stored[index]; - // Remove the solver, then shrink to the length `shrunk_len` - // reports, exactly as the handler resizes the account. + // Remove the solver, then shrink to the length it reports, + // exactly as the handler resizes the account. let mut bytes = fixtures::state_account_bytes(&header, &stored); - let shrunk_len = StateAccount::attach(&bytes[..]) - .expect("valid header") - .shrunk_len() - .expect("shrunk length fits"); - prop_assert_eq!(shrunk_len, bytes.len().strict_sub(WIDTH_PUBKEY)); - StateAccount::attach(&mut bytes[..]) + let shrunk_len = StateAccount::attach(&mut bytes[..]) .expect("valid header") .remove_solver(&removed) .expect("a present solver is removed"); + prop_assert_eq!(shrunk_len, bytes.len().strict_sub(WIDTH_PUBKEY)); bytes.truncate(shrunk_len); let mut expected = stored; diff --git a/programs/settlement/src/remove_solver.rs b/programs/settlement/src/remove_solver.rs index 430216f..8ddf678 100644 --- a/programs/settlement/src/remove_solver.rs +++ b/programs/settlement/src/remove_solver.rs @@ -39,10 +39,7 @@ pub fn process_remove_solver( if !manager.is_signer() || *manager.address() != state.authority(Role::Manager) { return Err(SettlementError::UnauthorizedSolverManagement.into()); } - state.remove_solver(&solver)?; - state - .shrunk_len() - .expect("a solver has been removed, so the length doesn't underflow") + state.remove_solver(&solver)? }; state_pda.resize(new_len)?; From b618692f8876f1f916d3fb109336fbd088f4d664 Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:10:45 +0200 Subject: [PATCH 4/8] Use shared program address in interface tests --- interface/src/data/order.rs | 3 +-- interface/src/lib.rs | 7 +++++++ programs/settlement/src/add_solver.rs | 3 +-- programs/settlement/src/create_buffer.rs | 5 +---- programs/settlement/src/create_order.rs | 6 +----- programs/settlement/src/reclaim_buffer.rs | 3 +-- programs/settlement/src/reclaim_order.rs | 3 +-- programs/settlement/src/remove_solver.rs | 7 +++---- programs/settlement/src/transfer_authority.rs | 3 +-- 9 files changed, 17 insertions(+), 23 deletions(-) diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index 52567c2..8f277b7 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -485,11 +485,10 @@ mod tests { mod load_from_pda { use super::*; + use crate::fixtures::PROGRAM_ID; use crate::instruction::fixtures::fake_account_with_data; use crate::pda::order::find_order_pda; - const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]); - /// [`sample_account`] carrying its own canonical bump, plus the address /// of the PDA it belongs at. fn canonical_account(cancelled: bool) -> (OrderAccount, Address) { diff --git a/interface/src/lib.rs b/interface/src/lib.rs index e8867fa..b32608b 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -253,6 +253,8 @@ impl From for solana_program_error::ProgramError { /// for this crate's own `cargo test`) so other crates can reuse them. #[cfg(any(test, feature = "test-fixtures"))] pub mod fixtures { + use std::sync::LazyLock; + use crate::Pubkey; /// Deterministically generate a [`Pubkey`] by hashing a seed string, for @@ -260,6 +262,11 @@ pub mod fixtures { pub fn pubkey_from_seed(seed: &str) -> Pubkey { Pubkey::new_from_array(solana_sha256_hasher::hash(seed.as_bytes()).to_bytes()) } + + /// A deterministic stand-in program id shared by handler tests, so each + /// doesn't define its own. This is an arbitrary placeholder, not the + /// declared on-chain id. + pub static PROGRAM_ID: LazyLock = LazyLock::new(|| pubkey_from_seed("program id")); } #[cfg(test)] diff --git a/programs/settlement/src/add_solver.rs b/programs/settlement/src/add_solver.rs index 2a51bff..4491245 100644 --- a/programs/settlement/src/add_solver.rs +++ b/programs/settlement/src/add_solver.rs @@ -72,14 +72,13 @@ pub fn process_add_solver( #[cfg(test)] mod tests { use super::*; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::add_solver::fixtures::{ add_solver_data, NUM_ACCOUNTS, }; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; use pinocchio::error::ProgramError; - const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]); - #[test] fn process_add_solver_propagates_parse_error() { let mut data = add_solver_data(); diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/create_buffer.rs index c1a59ae..199e3b1 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/create_buffer.rs @@ -67,12 +67,9 @@ mod tests { use cow_settlement_interface::instruction::create_buffer::fixtures::{ create_buffer_data, NUM_SHARED_ACCOUNTS, }; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; - /// Arbitrary placeholder program id. The failure path exercised below - /// returns before the program id is used for any syscall. - const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]); - #[test] fn process_create_buffer_propagates_error() { let mut data = create_buffer_data(); diff --git a/programs/settlement/src/create_order.rs b/programs/settlement/src/create_order.rs index b7d6c59..92124fc 100644 --- a/programs/settlement/src/create_order.rs +++ b/programs/settlement/src/create_order.rs @@ -76,6 +76,7 @@ mod tests { use cow_settlement_interface::instruction::create_order::fixtures::{ default_order_data, valid_intent_bytes, DEFAULT_OWNER, NUM_ACCOUNTS, }; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::{ fake_account, fake_account_from, fake_sequential_accounts, }; @@ -84,11 +85,6 @@ mod tests { use super::*; - /// Arbitrary placeholder program id for handler-level tests. The - /// failure paths exercised below return before the program id is used - /// for any syscall, so any 32-byte value works. - const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]); - #[test] fn process_create_order_propagates_parse_error() { let intent_bytes = valid_intent_bytes(); diff --git a/programs/settlement/src/reclaim_buffer.rs b/programs/settlement/src/reclaim_buffer.rs index 8508891..6a1b045 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/reclaim_buffer.rs @@ -74,6 +74,7 @@ pub fn process_reclaim_buffer( #[cfg(test)] mod tests { use cow_settlement_interface::data::state::{StateAccount, StateInitArgs, WIDTH_HEADER}; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::{ fake_account, fake_account_owned_by, fake_account_with_data, fake_sequential_accounts, fake_signer, @@ -86,8 +87,6 @@ mod tests { use solana_program_pack::Pack; use super::*; - - const PROGRAM_ID: Address = Address::new_from_array([100; 32]); const AUTHORITY: Address = Address::new_from_array([101; 32]); const MANAGER: Address = Address::new_from_array([102; 32]); const UNRELATED: Address = Address::new_from_array([254; 32]); diff --git a/programs/settlement/src/reclaim_order.rs b/programs/settlement/src/reclaim_order.rs index 1a9c9de..fe17888 100644 --- a/programs/settlement/src/reclaim_order.rs +++ b/programs/settlement/src/reclaim_order.rs @@ -56,14 +56,13 @@ mod tests { fixtures::{fake_account, fake_account_with_data, fake_sequential_accounts}, reclaim_order::fixtures::{default_reclaim_data, NUM_ACCOUNTS}, }; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::pda::order::find_order_pda; use cow_settlement_interface::SettlementInstruction; use pinocchio::Address; use super::*; - const PROGRAM_ID: pinocchio::Address = pinocchio::Address::new_from_array([0xc0; 32]); - #[test] fn process_reclaim_order_propagates_parse_error() { let mut data = default_reclaim_data(); diff --git a/programs/settlement/src/remove_solver.rs b/programs/settlement/src/remove_solver.rs index 8ddf678..f5d795c 100644 --- a/programs/settlement/src/remove_solver.rs +++ b/programs/settlement/src/remove_solver.rs @@ -60,14 +60,13 @@ pub fn process_remove_solver( #[cfg(test)] mod tests { use super::*; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; use cow_settlement_interface::instruction::remove_solver::fixtures::{ remove_solver_data, NUM_ACCOUNTS, }; use pinocchio::error::ProgramError; - const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]); - #[test] fn process_remove_solver_propagates_parse_error() { let mut data = remove_solver_data(); @@ -129,13 +128,13 @@ mod tests { fake_account(pubkey_from_seed("rent recipient")), fake_account_owned_by( state_pda_address, - PROGRAM_ID, + *PROGRAM_ID, &state_account_bytes(&header, &stored), ), ]; let data = Instruction::from(RemoveSolver { - program_id: PROGRAM_ID, + program_id: *PROGRAM_ID, manager, rent_recipient: pubkey_from_seed("rent recipient"), state_pda: state_pda_address, diff --git a/programs/settlement/src/transfer_authority.rs b/programs/settlement/src/transfer_authority.rs index 8bd498f..72e2666 100644 --- a/programs/settlement/src/transfer_authority.rs +++ b/programs/settlement/src/transfer_authority.rs @@ -48,13 +48,12 @@ pub fn process_transfer_authority( #[cfg(test)] mod tests { use super::*; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; use cow_settlement_interface::instruction::transfer_authority::fixtures::{ transfer_authority_data, NUM_ACCOUNTS, }; - const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]); - #[test] fn process_transfer_authority_propagates_parse_error() { let mut data = transfer_authority_data(); From fffbe7f5d408d24ef88e4c95a0d271a6a8bb94db Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:13:40 +0200 Subject: [PATCH 5/8] Deduplicate shared add/remove helpers --- programs/settlement/tests/add_solvers.rs | 38 +++------------------ programs/settlement/tests/common/state.rs | 34 ++++++++++++++++++ programs/settlement/tests/remove_solvers.rs | 32 +++-------------- 3 files changed, 42 insertions(+), 62 deletions(-) diff --git a/programs/settlement/tests/add_solvers.rs b/programs/settlement/tests/add_solvers.rs index b76481d..d00bf79 100644 --- a/programs/settlement/tests/add_solvers.rs +++ b/programs/settlement/tests/add_solvers.rs @@ -3,7 +3,7 @@ //! gate on adding them. use cow_settlement_client::cow_settlement_interface::{ - data::state::{StateAccount, WIDTH_HEADER, WIDTH_PUBKEY}, + data::state::{WIDTH_HEADER, WIDTH_PUBKEY}, Instruction, SettlementError, }; use cow_settlement_client::instructions::AddSolver; @@ -19,43 +19,13 @@ use solana_system_interface::MAX_PERMITTED_DATA_LENGTH; use crate::common::{ assert_instruction_error, benchmark::{send_transaction_metered, BenchLabel}, - create_account_at, lamports, setup_init, to_instruction_error, unique_keypair, - InitializedParams, + create_account_at, lamports, setup_init, + state::solvers, + to_instruction_error, unique_keypair, InitializedParams, }; mod common; -/// Assert the solver list's storage invariant: solvers are stored strictly -/// ascending by address (sorted, with no duplicates). This is what lets the -/// program binary-search the list, so every read below re-checks it. -#[track_caller] -fn assert_solver_invariant(solvers: &[Pubkey]) { - assert!( - // We use `is_sorted_by` here instead of `is_sorted` because that - // doesn't catch duplicates. - solvers.is_sorted_by(|a, b| a < b), - "invariant violated: solver list must be strictly ascending by address: {solvers:?}", - ); -} - -/// The solver list currently stored in the state PDA, in stored order. Reading it -/// also re-checks the storage invariant (see [`assert_solver_invariant`]), so -/// every test that inspects the list enforces it, not just the ones that compare -/// against a sorted expectation. -#[track_caller] -fn solvers(svm: &LiteSVM, state_pda: &Pubkey) -> Vec { - let data = svm - .get_account(state_pda) - .expect("state PDA should exist") - .data; - let solvers: Vec = StateAccount::attach(&data[..]) - .expect("state PDA should be a valid state account") - .solvers() - .collect(); - assert_solver_invariant(&solvers); - solvers -} - /// Build an `AddSolver` transaction authorized by the manager and paid by the /// payer, both of which sign. Split from [`add_solver`] so the happy-path test /// can submit the same transaction through the metered send. diff --git a/programs/settlement/tests/common/state.rs b/programs/settlement/tests/common/state.rs index 0e04736..0f0709c 100644 --- a/programs/settlement/tests/common/state.rs +++ b/programs/settlement/tests/common/state.rs @@ -1,4 +1,7 @@ +use cow_settlement_client::cow_settlement_interface::data::state::StateAccount; use cow_settlement_client::instructions::Initialize; +use litesvm::LiteSVM; +use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Keypair; /// Send `ix` as the settlement's `Initialize`, signed by `payer`. @@ -6,3 +9,34 @@ pub fn initialize(svm: &mut litesvm::LiteSVM, payer: &Keypair, ix: Initialize) { let tx = super::signed_tx(svm, payer, payer, ix); svm.send_transaction(tx).expect("initialize should succeed"); } + +/// Assert the solver list's storage invariant: solvers are stored strictly +/// ascending by address (sorted, with no duplicates). This is what lets the +/// program binary-search the list, so every read below re-checks it. +#[track_caller] +pub fn assert_solver_invariant(solvers: &[Pubkey]) { + assert!( + // We use `is_sorted_by` here instead of `is_sorted` because that + // doesn't catch duplicates. + solvers.is_sorted_by(|a, b| a < b), + "invariant violated: solver list must be strictly ascending by address: {solvers:?}", + ); +} + +/// The solver list currently stored in the state PDA, in stored order. Reading it +/// also re-checks the storage invariant (see [`assert_solver_invariant`]), so +/// every test that inspects the list enforces it, not just the ones that compare +/// against a sorted expectation. +#[track_caller] +pub fn solvers(svm: &LiteSVM, state_pda: &Pubkey) -> Vec { + let data = svm + .get_account(state_pda) + .expect("state PDA should exist") + .data; + let solvers: Vec = StateAccount::attach(&data[..]) + .expect("state PDA should be a valid state account") + .solvers() + .collect(); + assert_solver_invariant(&solvers); + solvers +} diff --git a/programs/settlement/tests/remove_solvers.rs b/programs/settlement/tests/remove_solvers.rs index 1de58e2..2de3a22 100644 --- a/programs/settlement/tests/remove_solvers.rs +++ b/programs/settlement/tests/remove_solvers.rs @@ -4,7 +4,7 @@ //! `settle_solver_auth.rs`. use cow_settlement_client::cow_settlement_interface::{ - data::state::{StateAccount, WIDTH_HEADER, WIDTH_PUBKEY}, + data::state::{WIDTH_HEADER, WIDTH_PUBKEY}, Instruction, SettlementError, }; use cow_settlement_client::instructions::RemoveSolver; @@ -19,7 +19,9 @@ use solana_sdk::{ use crate::common::{ assert_instruction_error, benchmark::{send_transaction_metered, BenchLabel}, - lamports, setup_init, to_instruction_error, unique_keypair, unique_pubkey, InitializedParams, + lamports, setup_init, + state::solvers, + to_instruction_error, unique_keypair, unique_pubkey, InitializedParams, }; mod common; @@ -35,32 +37,6 @@ fn setup() -> (LiteSVM, InitializedParams, Pubkey) { (svm, params, rent_recipient) } -#[track_caller] -fn assert_solver_invariant(solvers: &[Pubkey]) { - assert!( - solvers.is_sorted_by(|a, b| a < b), - "invariant violated: solver list must be strictly ascending by address: {solvers:?}", - ); -} - -/// The solver list currently stored in the state PDA, in stored order. Reading -/// it also re-checks the storage invariant (see [`assert_solver_invariant`]), so -/// every test that inspects the list enforces it, not just the ones that compare -/// against a sorted expectation. -#[track_caller] -fn solvers(svm: &LiteSVM, state_pda: &Pubkey) -> Vec { - let data = svm - .get_account(state_pda) - .expect("state PDA should exist") - .data; - let solvers: Vec = StateAccount::attach(&data[..]) - .expect("state PDA should be a valid state account") - .solvers() - .collect(); - assert_solver_invariant(&solvers); - solvers -} - /// Build a `RemoveSolver` transaction authorized by the manager, refunding the /// freed rent to `rent_recipient`. Signed by the payer and the manager. Split /// from [`remove_solver`] so the happy-path test can meter the same transaction. From 75bf9a95549e1b7375570c16c826efdb57819f13 Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:18:56 +0200 Subject: [PATCH 6/8] Fix fmt --- programs/settlement/src/create_buffer.rs | 2 +- programs/settlement/src/create_order.rs | 2 +- programs/settlement/src/reclaim_order.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/create_buffer.rs index 199e3b1..2dab7e0 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/create_buffer.rs @@ -64,10 +64,10 @@ pub fn process_create_buffer( #[cfg(test)] mod tests { use super::*; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::create_buffer::fixtures::{ create_buffer_data, NUM_SHARED_ACCOUNTS, }; - use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; #[test] diff --git a/programs/settlement/src/create_order.rs b/programs/settlement/src/create_order.rs index 92124fc..fbd886a 100644 --- a/programs/settlement/src/create_order.rs +++ b/programs/settlement/src/create_order.rs @@ -73,10 +73,10 @@ pub fn process_create_order( #[cfg(test)] mod tests { use cow_settlement_interface::data::intent::{Flags, OrderIntent, OrderKind}; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::create_order::fixtures::{ default_order_data, valid_intent_bytes, DEFAULT_OWNER, NUM_ACCOUNTS, }; - use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::{ fake_account, fake_account_from, fake_sequential_accounts, }; diff --git a/programs/settlement/src/reclaim_order.rs b/programs/settlement/src/reclaim_order.rs index fe17888..6da0e77 100644 --- a/programs/settlement/src/reclaim_order.rs +++ b/programs/settlement/src/reclaim_order.rs @@ -52,11 +52,11 @@ pub fn process_reclaim_order( #[cfg(test)] mod tests { use cow_settlement_interface::data::order::EncodedOrderAccount; + use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::{ fixtures::{fake_account, fake_account_with_data, fake_sequential_accounts}, reclaim_order::fixtures::{default_reclaim_data, NUM_ACCOUNTS}, }; - use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::pda::order::find_order_pda; use cow_settlement_interface::SettlementInstruction; use pinocchio::Address; From c9e594340a0dd21f261538cd770d2f97d1c001a3 Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:35:15 +0200 Subject: [PATCH 7/8] Vibe IDL --- bench-report.json | 94 ++++----------------- programs/settlement/idl/cow_settlement.json | 79 ++++++++++++++++- programs/settlement/tests/idl/generate.rs | 5 ++ programs/settlement/tests/idl/parse_rust.rs | 5 ++ 4 files changed, 103 insertions(+), 80 deletions(-) diff --git a/bench-report.json b/bench-report.json index 7b23745..6dc671d 100644 --- a/bench-report.json +++ b/bench-report.json @@ -11,19 +11,13 @@ "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7, "reclaim_buffer/max_buffers_in_one_instruction": 64, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9, -<<<<<<< HEAD - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 4, - "remove_solver/remove_with_many_existing_solvers": 5, - "remove_solver/removes_a_solver": 5, -||||||| d88b453 - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 4, -======= "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 4, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 3, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 3, "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": 3, "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 3, ->>>>>>> main + "remove_solver/remove_with_many_existing_solvers": 5, + "remove_solver/removes_a_solver": 5, "settle/finalizes_with_no_pushes": 5, "settle/pulls_from_multiple_orders": 15, "settle/pulls_funds_to_destination": 10, @@ -40,72 +34,23 @@ "compute_units": { "add_solver/add_with_many_existing_solvers": 5074, "add_solver/adds_a_solver": 4622, -<<<<<<< HEAD - "create_buffers/happy_path_creates_initialized_buffer_token_account": 10347, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21747, - "create_buffers/max_buffers_in_one_instruction": 177071, - "create_order/happy_path_creates_order_pda_with_expected_body": 4978, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4530, - "reclaim_buffer/funded_buffer_is_skipped": 6335, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7482, - "reclaim_buffer/max_buffers_in_one_instruction": 136622, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18081, - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2182, - "remove_solver/remove_with_many_existing_solvers": 3757, - "remove_solver/removes_a_solver": 3492, -||||||| d88b453 - "create_buffers/happy_path_creates_initialized_buffer_token_account": 10345, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21743, - "create_buffers/max_buffers_in_one_instruction": 177040, - "create_order/happy_path_creates_order_pda_with_expected_body": 4978, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4529, - "reclaim_buffer/funded_buffer_is_skipped": 6335, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7483, - "reclaim_buffer/max_buffers_in_one_instruction": 136652, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18082, - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2183, -======= - "create_buffers/happy_path_creates_initialized_buffer_token_account": 7345, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17243, - "create_buffers/max_buffers_in_one_instruction": 169540, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 7347, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17247, + "create_buffers/max_buffers_in_one_instruction": 169571, "create_order/happy_path_creates_order_pda_with_expected_body": 4985, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4529, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4530, "reclaim_buffer/funded_buffer_is_skipped": 4835, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 5983, - "reclaim_buffer/max_buffers_in_one_instruction": 124652, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7582, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 5982, + "reclaim_buffer/max_buffers_in_one_instruction": 124622, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7581, "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2202, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2071, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2079, "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": null, "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, ->>>>>>> main + "remove_solver/remove_with_many_existing_solvers": 3757, + "remove_solver/removes_a_solver": 3492, "settle/finalizes_with_no_pushes": 7154, -<<<<<<< HEAD - "settle/pulls_from_multiple_orders": 20043, - "settle/pulls_funds_to_destination": 13632, - "settle/pulls_to_multiple_destinations": 14773, - "settle/pushes_a_single_order": 12487, - "settle/pushes_several_orders_from_different_buffers": 17751, - "settle/pushes_several_orders_from_one_buffer": 17750, - "settle/settles_a_single_order": 12505, - "settle/settles_multiple_orders": 23062, - "transfer_authority/manager_can_transfer_manager": 3173, - "transfer_authority/manager_can_transfer_reclaim_authority": 3175, - "transfer_authority/reclaim_authority_can_transfer_itself": 3179 -||||||| d88b453 - "settle/pulls_from_multiple_orders": 20043, - "settle/pulls_funds_to_destination": 13632, - "settle/pulls_to_multiple_destinations": 14773, - "settle/pushes_a_single_order": 12487, - "settle/pushes_several_orders_from_different_buffers": 17751, - "settle/pushes_several_orders_from_one_buffer": 17750, - "settle/settles_a_single_order": 12505, - "settle/settles_multiple_orders": 23062, - "transfer_authority/manager_can_transfer_manager": 3174, - "transfer_authority/manager_can_transfer_reclaim_authority": 3176, - "transfer_authority/reclaim_authority_can_transfer_itself": 3180 -======= "settle/pulls_from_multiple_orders": 20059, "settle/pulls_funds_to_destination": 13640, "settle/pulls_to_multiple_destinations": 14781, @@ -114,10 +59,9 @@ "settle/pushes_several_orders_from_one_buffer": 17766, "settle/settles_a_single_order": 12513, "settle/settles_multiple_orders": 23086, - "transfer_authority/manager_can_transfer_manager": 3174, - "transfer_authority/manager_can_transfer_reclaim_authority": 3176, - "transfer_authority/reclaim_authority_can_transfer_itself": 3180 ->>>>>>> main + "transfer_authority/manager_can_transfer_manager": 3173, + "transfer_authority/manager_can_transfer_reclaim_authority": 3175, + "transfer_authority/reclaim_authority_can_transfer_itself": 3179 }, "transaction_bytes": { "add_solver/add_with_many_existing_solvers": 366, @@ -131,19 +75,13 @@ "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 400, "reclaim_buffer/max_buffers_in_one_instruction": 332, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466, -<<<<<<< HEAD - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 236, - "remove_solver/remove_with_many_existing_solvers": 365, - "remove_solver/removes_a_solver": 365, -||||||| d88b453 - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 236, -======= "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 236, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 204, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 204, "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": 204, "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 204, ->>>>>>> main + "remove_solver/remove_with_many_existing_solvers": 365, + "remove_solver/removes_a_solver": 365, "settle/finalizes_with_no_pushes": 290, "settle/pulls_from_multiple_orders": 656, "settle/pulls_funds_to_destination": 473, diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json index bb11b0d..bdd9b90 100644 --- a/programs/settlement/idl/cow_settlement.json +++ b/programs/settlement/idl/cow_settlement.json @@ -596,6 +596,76 @@ ] } ] + }, + { + "name": "remove_solver", + "docs": [ + "Removes a solver, deleting it from the sorted solver list stored in the state PDA. Only the current holder of the Manager role may authorize this, and a solver that isn't registered is rejected.", + "The state PDA shrinks by one address, and the freed rent is paid to rent_recipient by a direct debit from the program-owned PDA, so no system program is involved." + ], + "discriminator": [ + 9 + ], + "accounts": [ + { + "name": "manager", + "signer": true, + "docs": [ + "Must match the manager address recorded in the state PDA. Authorizes the change but doesn't receive anything." + ] + }, + { + "name": "rent_recipient", + "writable": true, + "docs": [ + "Receives the rent freed by shrinking the state PDA." + ] + }, + { + "name": "state_pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 101, + 116, + 116, + 108, + 101, + 109, + 101, + 110, + 116, + 32, + 118, + 48, + 46, + 51, + 32, + 32, + 32, + 32 + ] + } + ] + }, + "docs": [ + "Holds the manager the signer is checked against and the solver list the removed solver is taken from; shrunk by one address. Canonical PDA seeded by SETTLEMENT_SEED alone. SETTLEMENT_SEED is the ASCII prefix \"settlement v\" followed by the program's major.minor version, right-padded with spaces to a fixed 19 bytes; the fixed width prevents one version's seeds from being a prefix of another's." + ] + } + ], + "args": [ + { + "name": "solver", + "type": "pubkey", + "docs": [ + "The solver to remove." + ] + } + ] } ], "accounts": [ @@ -950,7 +1020,7 @@ { "code": 35, "name": "UnauthorizedSolverManagement", - "msg": "AddSolver's manager account isn't a signer, or doesn't match the manager recorded in the settlement state PDA." + "msg": "AddSolver/RemoveSolver's manager account isn't a signer, or doesn't match the manager recorded in the settlement state PDA, so it may not change the solver list." }, { "code": 36, @@ -960,10 +1030,15 @@ { "code": 37, "name": "UnauthorizedSolver", - "msg": "BeginSettle/FinalizeSettle's solver account isn't a signer or isn't in the state PDA's solver list, so it may not settle." + "msg": "BeginSettle's solver account isn't a signer or isn't in the state PDA's solver list, so it may not settle." }, { "code": 38, + "name": "SolverNotFound", + "msg": "RemoveSolver's solver isn't in the state PDA's solver list." + }, + { + "code": 39, "name": "OrderCreatedOnChainMismatch", "msg": "A created order's intent isn't set with the created_on_chain flag corresponding to the behavior of the invoked order creation instruction." } diff --git a/programs/settlement/tests/idl/generate.rs b/programs/settlement/tests/idl/generate.rs index 8921156..0e4cb71 100644 --- a/programs/settlement/tests/idl/generate.rs +++ b/programs/settlement/tests/idl/generate.rs @@ -109,6 +109,11 @@ const INSTRUCTIONS: &[Instruction] = &[ input: &parse_rust::ADD_SOLVER_RS, pda_accounts: &[("state_pda", STATE_PDA)], }, + Instruction { + variant: SettlementInstruction::RemoveSolver, + input: &parse_rust::REMOVE_SOLVER_RS, + pda_accounts: &[("state_pda", STATE_PDA)], + }, ]; /// The struct types the IDL defines, as `(source, Rust name, IDL name)`. The diff --git a/programs/settlement/tests/idl/parse_rust.rs b/programs/settlement/tests/idl/parse_rust.rs index 35ca0e9..b8b21b7 100644 --- a/programs/settlement/tests/idl/parse_rust.rs +++ b/programs/settlement/tests/idl/parse_rust.rs @@ -80,6 +80,11 @@ pub const ADD_SOLVER_RS: Source = Source { text: include_str!("../../../../interface/src/instruction/add_solver.rs"), }; +pub const REMOVE_SOLVER_RS: Source = Source { + display: "interface/src/instruction/remove_solver.rs", + text: include_str!("../../../../interface/src/instruction/remove_solver.rs"), +}; + impl Source { fn parse(&self) -> syn::File { syn::parse_file(self.text) From 066604e5c86c1ecd808d4aacded279eb0efac5b6 Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:08:52 +0200 Subject: [PATCH 8/8] Refactor: organize components into folders --- bench-report.json | 156 +++++++++--------- client/src/instructions.rs | 10 +- client/src/parse.rs | 13 +- .../{create_buffer.rs => buffer/create.rs} | 2 +- interface/src/instruction/buffer/mod.rs | 4 + .../{reclaim_buffer.rs => buffer/reclaim.rs} | 4 +- interface/src/instruction/mod.rs | 9 +- .../{create_order.rs => order/create.rs} | 2 +- interface/src/instruction/order/mod.rs | 4 + .../{reclaim_order.rs => order/reclaim.rs} | 2 +- .../{add_solver.rs => solver_auth/add.rs} | 0 interface/src/instruction/solver_auth/mod.rs | 4 + .../remove.rs} | 0 .../{create_buffer.rs => buffer/create.rs} | 4 +- programs/settlement/src/buffer/mod.rs | 7 + .../{reclaim_buffer.rs => buffer/reclaim.rs} | 8 +- programs/settlement/src/lib.rs | 18 +- .../src/{create_order.rs => order/create.rs} | 8 +- programs/settlement/src/order/mod.rs | 7 + .../{reclaim_order.rs => order/reclaim.rs} | 4 +- programs/settlement/src/settle/mod.rs | 4 +- .../src/{add_solver.rs => solver_auth/add.rs} | 6 +- programs/settlement/src/solver_auth/mod.rs | 7 + .../remove.rs} | 6 +- .../{create_buffer.rs => buffer/create.rs} | 4 +- programs/settlement/tests/buffer/main.rs | 7 + .../{reclaim_buffer.rs => buffer/reclaim.rs} | 4 +- programs/settlement/tests/idl/parse_rust.rs | 24 +-- .../{create_order.rs => order/create.rs} | 4 +- programs/settlement/tests/order/main.rs | 7 + .../{reclaim_order.rs => order/reclaim.rs} | 4 +- .../begin_orders.rs} | 2 +- .../finalize_pushes.rs} | 2 - .../limit_prices.rs} | 2 - programs/settlement/tests/settle/main.rs | 10 ++ .../{ => settle}/matching_begin_finalize.rs | 2 +- .../solver_auth.rs} | 2 - .../{add_solvers.rs => solver_auth/add.rs} | 2 +- programs/settlement/tests/solver_auth/main.rs | 7 + .../remove.rs} | 2 +- 40 files changed, 210 insertions(+), 164 deletions(-) rename interface/src/instruction/{create_buffer.rs => buffer/create.rs} (99%) create mode 100644 interface/src/instruction/buffer/mod.rs rename interface/src/instruction/{reclaim_buffer.rs => buffer/reclaim.rs} (99%) rename interface/src/instruction/{create_order.rs => order/create.rs} (99%) create mode 100644 interface/src/instruction/order/mod.rs rename interface/src/instruction/{reclaim_order.rs => order/reclaim.rs} (99%) rename interface/src/instruction/{add_solver.rs => solver_auth/add.rs} (100%) create mode 100644 interface/src/instruction/solver_auth/mod.rs rename interface/src/instruction/{remove_solver.rs => solver_auth/remove.rs} (100%) rename programs/settlement/src/{create_buffer.rs => buffer/create.rs} (95%) create mode 100644 programs/settlement/src/buffer/mod.rs rename programs/settlement/src/{reclaim_buffer.rs => buffer/reclaim.rs} (98%) rename programs/settlement/src/{create_order.rs => order/create.rs} (97%) create mode 100644 programs/settlement/src/order/mod.rs rename programs/settlement/src/{reclaim_order.rs => order/reclaim.rs} (97%) rename programs/settlement/src/{add_solver.rs => solver_auth/add.rs} (95%) create mode 100644 programs/settlement/src/solver_auth/mod.rs rename programs/settlement/src/{remove_solver.rs => solver_auth/remove.rs} (95%) rename programs/settlement/tests/{create_buffer.rs => buffer/create.rs} (99%) create mode 100644 programs/settlement/tests/buffer/main.rs rename programs/settlement/tests/{reclaim_buffer.rs => buffer/reclaim.rs} (99%) rename programs/settlement/tests/{create_order.rs => order/create.rs} (99%) create mode 100644 programs/settlement/tests/order/main.rs rename programs/settlement/tests/{reclaim_order.rs => order/reclaim.rs} (99%) rename programs/settlement/tests/{begin_settle_orders.rs => settle/begin_orders.rs} (99%) rename programs/settlement/tests/{finalize_settle_pushes.rs => settle/finalize_pushes.rs} (99%) rename programs/settlement/tests/{settle_limit_prices.rs => settle/limit_prices.rs} (99%) create mode 100644 programs/settlement/tests/settle/main.rs rename programs/settlement/tests/{ => settle}/matching_begin_finalize.rs (99%) rename programs/settlement/tests/{settle_solver_auth.rs => settle/solver_auth.rs} (99%) rename programs/settlement/tests/{add_solvers.rs => solver_auth/add.rs} (99%) create mode 100644 programs/settlement/tests/solver_auth/main.rs rename programs/settlement/tests/{remove_solvers.rs => solver_auth/remove.rs} (99%) diff --git a/bench-report.json b/bench-report.json index 6dc671d..c5aaea8 100644 --- a/bench-report.json +++ b/bench-report.json @@ -1,96 +1,96 @@ { "accounts": { - "add_solver/add_with_many_existing_solvers": 5, - "add_solver/adds_a_solver": 5, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 6, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 10, - "create_buffers/max_buffers_in_one_instruction": 64, - "create_order/happy_path_creates_order_pda_with_expected_body": 4, + "add_solver/add::add_with_many_existing_solvers": 5, + "add_solver/add::adds_a_solver": 5, + "create_buffers/create::happy_path_creates_initialized_buffer_token_account": 6, + "create_buffers/create::happy_path_creates_multiple_buffers_in_one_instruction": 10, + "create_buffers/create::max_buffers_in_one_instruction": 64, + "create_order/create::happy_path_creates_order_pda_with_expected_body": 4, "initialize/happy_path_initializes_state_pda_with_expected_data": 4, - "reclaim_buffer/funded_buffer_is_skipped": 7, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7, - "reclaim_buffer/max_buffers_in_one_instruction": 64, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9, - "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 4, - "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 3, - "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 3, - "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": 3, - "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 3, - "remove_solver/remove_with_many_existing_solvers": 5, - "remove_solver/removes_a_solver": 5, - "settle/finalizes_with_no_pushes": 5, - "settle/pulls_from_multiple_orders": 15, - "settle/pulls_funds_to_destination": 10, - "settle/pulls_to_multiple_destinations": 11, - "settle/pushes_a_single_order": 9, - "settle/pushes_several_orders_from_different_buffers": 13, - "settle/pushes_several_orders_from_one_buffer": 12, - "settle/settles_a_single_order": 9, - "settle/settles_multiple_orders": 17, + "reclaim_buffer/reclaim::funded_buffer_is_skipped": 7, + "reclaim_buffer/reclaim::happy_path_reclaims_empty_buffer_to_the_authority_itself": 7, + "reclaim_buffer/reclaim::max_buffers_in_one_instruction": 64, + "reclaim_buffer/reclaim::reclaims_multiple_buffers_skipping_funded": 9, + "reclaim_order/reclaim::happy_path_expired_returns_lamports_and_closes_pda": 4, + "reclaim_order/reclaim::happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 3, + "reclaim_order/reclaim::happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 3, + "reclaim_order/reclaim::off_chain_order_is_reclaimable_only_once_expired": 3, + "reclaim_order/reclaim::on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 3, + "remove_solver/remove::remove_with_many_existing_solvers": 5, + "remove_solver/remove::removes_a_solver": 5, + "settle/begin_orders::pulls_from_multiple_orders": 15, + "settle/begin_orders::pulls_funds_to_destination": 10, + "settle/begin_orders::pulls_to_multiple_destinations": 11, + "settle/begin_orders::settles_a_single_order": 9, + "settle/begin_orders::settles_multiple_orders": 17, + "settle/finalize_pushes::finalizes_with_no_pushes": 5, + "settle/finalize_pushes::pushes_a_single_order": 9, + "settle/finalize_pushes::pushes_several_orders_from_different_buffers": 13, + "settle/finalize_pushes::pushes_several_orders_from_one_buffer": 12, "transfer_authority/manager_can_transfer_manager": 4, "transfer_authority/manager_can_transfer_reclaim_authority": 4, "transfer_authority/reclaim_authority_can_transfer_itself": 4 }, "compute_units": { - "add_solver/add_with_many_existing_solvers": 5074, - "add_solver/adds_a_solver": 4622, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 7347, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17247, - "create_buffers/max_buffers_in_one_instruction": 169571, - "create_order/happy_path_creates_order_pda_with_expected_body": 4985, + "add_solver/add::add_with_many_existing_solvers": 5074, + "add_solver/add::adds_a_solver": 4622, + "create_buffers/create::happy_path_creates_initialized_buffer_token_account": 7347, + "create_buffers/create::happy_path_creates_multiple_buffers_in_one_instruction": 17247, + "create_buffers/create::max_buffers_in_one_instruction": 169571, + "create_order/create::happy_path_creates_order_pda_with_expected_body": 4985, "initialize/happy_path_initializes_state_pda_with_expected_data": 4530, - "reclaim_buffer/funded_buffer_is_skipped": 4835, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 5982, - "reclaim_buffer/max_buffers_in_one_instruction": 124622, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7581, - "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2202, - "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2071, - "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2079, - "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": null, - "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, - "remove_solver/remove_with_many_existing_solvers": 3757, - "remove_solver/removes_a_solver": 3492, - "settle/finalizes_with_no_pushes": 7154, - "settle/pulls_from_multiple_orders": 20059, - "settle/pulls_funds_to_destination": 13640, - "settle/pulls_to_multiple_destinations": 14781, - "settle/pushes_a_single_order": 12495, - "settle/pushes_several_orders_from_different_buffers": 17767, - "settle/pushes_several_orders_from_one_buffer": 17766, - "settle/settles_a_single_order": 12513, - "settle/settles_multiple_orders": 23086, + "reclaim_buffer/reclaim::funded_buffer_is_skipped": 4835, + "reclaim_buffer/reclaim::happy_path_reclaims_empty_buffer_to_the_authority_itself": 5982, + "reclaim_buffer/reclaim::max_buffers_in_one_instruction": 124622, + "reclaim_buffer/reclaim::reclaims_multiple_buffers_skipping_funded": 7581, + "reclaim_order/reclaim::happy_path_expired_returns_lamports_and_closes_pda": 2202, + "reclaim_order/reclaim::happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2071, + "reclaim_order/reclaim::happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2079, + "reclaim_order/reclaim::off_chain_order_is_reclaimable_only_once_expired": null, + "reclaim_order/reclaim::on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, + "remove_solver/remove::remove_with_many_existing_solvers": 3757, + "remove_solver/remove::removes_a_solver": 3492, + "settle/begin_orders::pulls_from_multiple_orders": 20059, + "settle/begin_orders::pulls_funds_to_destination": 13640, + "settle/begin_orders::pulls_to_multiple_destinations": 14781, + "settle/begin_orders::settles_a_single_order": 12513, + "settle/begin_orders::settles_multiple_orders": 23086, + "settle/finalize_pushes::finalizes_with_no_pushes": 7154, + "settle/finalize_pushes::pushes_a_single_order": 12495, + "settle/finalize_pushes::pushes_several_orders_from_different_buffers": 17767, + "settle/finalize_pushes::pushes_several_orders_from_one_buffer": 17766, "transfer_authority/manager_can_transfer_manager": 3173, "transfer_authority/manager_can_transfer_reclaim_authority": 3175, "transfer_authority/reclaim_authority_can_transfer_itself": 3179 }, "transaction_bytes": { - "add_solver/add_with_many_existing_solvers": 366, - "add_solver/adds_a_solver": 366, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 303, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 435, - "create_buffers/max_buffers_in_one_instruction": 331, - "create_order/happy_path_creates_order_pda_with_expected_body": 452, + "add_solver/add::add_with_many_existing_solvers": 366, + "add_solver/add::adds_a_solver": 366, + "create_buffers/create::happy_path_creates_initialized_buffer_token_account": 303, + "create_buffers/create::happy_path_creates_multiple_buffers_in_one_instruction": 435, + "create_buffers/create::max_buffers_in_one_instruction": 331, + "create_order/create::happy_path_creates_order_pda_with_expected_body": 452, "initialize/happy_path_initializes_state_pda_with_expected_data": 301, - "reclaim_buffer/funded_buffer_is_skipped": 400, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 400, - "reclaim_buffer/max_buffers_in_one_instruction": 332, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466, - "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 236, - "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 204, - "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 204, - "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": 204, - "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 204, - "remove_solver/remove_with_many_existing_solvers": 365, - "remove_solver/removes_a_solver": 365, - "settle/finalizes_with_no_pushes": 290, - "settle/pulls_from_multiple_orders": 656, - "settle/pulls_funds_to_destination": 473, - "settle/pulls_to_multiple_destinations": 514, - "settle/pushes_a_single_order": 432, - "settle/pushes_several_orders_from_different_buffers": 574, - "settle/pushes_several_orders_from_one_buffer": 542, - "settle/settles_a_single_order": 432, - "settle/settles_multiple_orders": 716, + "reclaim_buffer/reclaim::funded_buffer_is_skipped": 400, + "reclaim_buffer/reclaim::happy_path_reclaims_empty_buffer_to_the_authority_itself": 400, + "reclaim_buffer/reclaim::max_buffers_in_one_instruction": 332, + "reclaim_buffer/reclaim::reclaims_multiple_buffers_skipping_funded": 466, + "reclaim_order/reclaim::happy_path_expired_returns_lamports_and_closes_pda": 236, + "reclaim_order/reclaim::happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 204, + "reclaim_order/reclaim::happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 204, + "reclaim_order/reclaim::off_chain_order_is_reclaimable_only_once_expired": 204, + "reclaim_order/reclaim::on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 204, + "remove_solver/remove::remove_with_many_existing_solvers": 365, + "remove_solver/remove::removes_a_solver": 365, + "settle/begin_orders::pulls_from_multiple_orders": 656, + "settle/begin_orders::pulls_funds_to_destination": 473, + "settle/begin_orders::pulls_to_multiple_destinations": 514, + "settle/begin_orders::settles_a_single_order": 432, + "settle/begin_orders::settles_multiple_orders": 716, + "settle/finalize_pushes::finalizes_with_no_pushes": 290, + "settle/finalize_pushes::pushes_a_single_order": 432, + "settle/finalize_pushes::pushes_several_orders_from_different_buffers": 574, + "settle/finalize_pushes::pushes_several_orders_from_one_buffer": 542, "transfer_authority/manager_can_transfer_manager": 333, "transfer_authority/manager_can_transfer_reclaim_authority": 333, "transfer_authority/reclaim_authority_can_transfer_itself": 333 diff --git a/client/src/instructions.rs b/client/src/instructions.rs index 207016e..ceb85fa 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -134,7 +134,7 @@ impl From> for Instruction { let encoded = EncodedOrderIntent::from(builder.intent); let (order_pda, _bump) = find_order_pda(&builder.program_id, &encoded.hash()); let intent_bytes: [u8; EncodedOrderIntent::SIZE] = (&encoded).into(); - cow_settlement_interface::instruction::create_order::CreateOrder { + cow_settlement_interface::instruction::order::create::CreateOrder { program_id: builder.program_id, owner: builder.owner, created_by: builder.created_by, @@ -158,7 +158,7 @@ impl From> for Instruction { .iter() .map(|mint| (find_buffer_pda(&builder.program_id, mint).0, *mint)) .collect(); - cow_settlement_interface::instruction::create_buffer::CreateBuffers { + cow_settlement_interface::instruction::buffer::create::CreateBuffers { program_id: builder.program_id, payer: builder.payer, buffers: &buffers, @@ -213,7 +213,7 @@ impl From> for Instruction { (buffer_pda, *mint) }) .collect(); - cow_settlement_interface::instruction::reclaim_buffer::ReclaimBuffer { + cow_settlement_interface::instruction::buffer::reclaim::ReclaimBuffer { program_id: builder.program_id, state_pda, reclaim_authority: builder.reclaim_authority, @@ -260,7 +260,7 @@ pub struct AddSolver { impl From for Instruction { fn from(builder: AddSolver) -> Self { let (state_pda, _bump) = find_state_pda(&builder.program_id); - cow_settlement_interface::instruction::add_solver::AddSolver { + cow_settlement_interface::instruction::solver_auth::add::AddSolver { program_id: builder.program_id, manager: builder.manager, payer: builder.payer, @@ -283,7 +283,7 @@ pub struct RemoveSolver { impl From for Instruction { fn from(builder: RemoveSolver) -> Self { let (state_pda, _bump) = find_state_pda(&builder.program_id); - cow_settlement_interface::instruction::remove_solver::RemoveSolver { + cow_settlement_interface::instruction::solver_auth::remove::RemoveSolver { program_id: builder.program_id, manager: builder.manager, rent_recipient: builder.rent_recipient, diff --git a/client/src/parse.rs b/client/src/parse.rs index 7f0c235..f3f019a 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -5,14 +5,11 @@ use cow_settlement_interface::{ instruction::{ - add_solver::AddSolverInput, - create_buffer::CreateBufferInput, - create_order::CreateOrderInput, + buffer::{create::CreateBufferInput, reclaim::ReclaimBufferInput}, initialize::InitializeInput, - reclaim_buffer::ReclaimBufferInput, - reclaim_order::ReclaimOrderInput, - remove_solver::RemoveSolverInput, + order::{create::CreateOrderInput, reclaim::ReclaimOrderInput}, settle::{BeginSettleInput, FinalizeSettleInput}, + solver_auth::{add::AddSolverInput, remove::RemoveSolverInput}, transfer_authority::TransferAuthorityInput, InstructionInputParsing, }, @@ -85,8 +82,8 @@ mod tests { data::intent::fixtures::sample_intent, fixtures::pubkey_from_seed, instruction::{ - fixtures::fake_account_from_array, reclaim_buffer::ReclaimBuffer, - reclaim_order::ReclaimOrder, transfer_authority::TransferAuthority, + buffer::reclaim::ReclaimBuffer, fixtures::fake_account_from_array, + order::reclaim::ReclaimOrder, transfer_authority::TransferAuthority, }, Instruction, Role, }; diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/buffer/create.rs similarity index 99% rename from interface/src/instruction/create_buffer.rs rename to interface/src/instruction/buffer/create.rs index 0d3b25e..d06bea5 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/buffer/create.rs @@ -11,7 +11,7 @@ use solana_pubkey::Pubkey; pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; -use super::InstructionInputParsing; +use crate::instruction::InstructionInputParsing; use crate::SettlementInstruction; /// The SPL Token program. Buffers are created as token accounts owned by this diff --git a/interface/src/instruction/buffer/mod.rs b/interface/src/instruction/buffer/mod.rs new file mode 100644 index 0000000..a0bef45 --- /dev/null +++ b/interface/src/instruction/buffer/mod.rs @@ -0,0 +1,4 @@ +//! Off-chain builders for the buffer instructions. + +pub mod create; +pub mod reclaim; diff --git a/interface/src/instruction/reclaim_buffer.rs b/interface/src/instruction/buffer/reclaim.rs similarity index 99% rename from interface/src/instruction/reclaim_buffer.rs rename to interface/src/instruction/buffer/reclaim.rs index 5a8559e..b74b7bc 100644 --- a/interface/src/instruction/reclaim_buffer.rs +++ b/interface/src/instruction/buffer/reclaim.rs @@ -15,8 +15,8 @@ use solana_instruction::{AccountMeta, Instruction}; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; -use super::InstructionInputParsing; -pub use crate::instruction::create_buffer::SPL_TOKEN_PROGRAM_ID; +pub use crate::instruction::buffer::create::SPL_TOKEN_PROGRAM_ID; +use crate::instruction::InstructionInputParsing; use crate::SettlementInstruction; /// Builder for a `ReclaimBuffer` instruction that closes one buffer per diff --git a/interface/src/instruction/mod.rs b/interface/src/instruction/mod.rs index 2ff4200..619991d 100644 --- a/interface/src/instruction/mod.rs +++ b/interface/src/instruction/mod.rs @@ -8,14 +8,11 @@ use solana_program_error::ProgramError; use crate::{recover_discriminator, SettlementInstruction}; -pub mod add_solver; -pub mod create_buffer; -pub mod create_order; +pub mod buffer; pub mod initialize; -pub mod reclaim_buffer; -pub mod reclaim_order; -pub mod remove_solver; +pub mod order; pub mod settle; +pub mod solver_auth; pub mod transfer_authority; /// Shared components for parsing an instruction's input (data fields and diff --git a/interface/src/instruction/create_order.rs b/interface/src/instruction/order/create.rs similarity index 99% rename from interface/src/instruction/create_order.rs rename to interface/src/instruction/order/create.rs index 7182eaa..f12b2e0 100644 --- a/interface/src/instruction/create_order.rs +++ b/interface/src/instruction/order/create.rs @@ -10,7 +10,7 @@ use solana_pubkey::Pubkey; pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; -use super::InstructionInputParsing; +use crate::instruction::InstructionInputParsing; use crate::{data::intent::EncodedOrderIntent, SettlementInstruction}; /// Builder for a `CreateOrder` instruction. diff --git a/interface/src/instruction/order/mod.rs b/interface/src/instruction/order/mod.rs new file mode 100644 index 0000000..d5333ab --- /dev/null +++ b/interface/src/instruction/order/mod.rs @@ -0,0 +1,4 @@ +//! Off-chain builders for the order instructions. + +pub mod create; +pub mod reclaim; diff --git a/interface/src/instruction/reclaim_order.rs b/interface/src/instruction/order/reclaim.rs similarity index 99% rename from interface/src/instruction/reclaim_order.rs rename to interface/src/instruction/order/reclaim.rs index 0824198..04492be 100644 --- a/interface/src/instruction/reclaim_order.rs +++ b/interface/src/instruction/order/reclaim.rs @@ -14,7 +14,7 @@ use solana_instruction::{AccountMeta, Instruction}; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; -use super::InstructionInputParsing; +use crate::instruction::InstructionInputParsing; use crate::SettlementInstruction; /// Builder for a `ReclaimOrder` instruction. diff --git a/interface/src/instruction/add_solver.rs b/interface/src/instruction/solver_auth/add.rs similarity index 100% rename from interface/src/instruction/add_solver.rs rename to interface/src/instruction/solver_auth/add.rs diff --git a/interface/src/instruction/solver_auth/mod.rs b/interface/src/instruction/solver_auth/mod.rs new file mode 100644 index 0000000..a98ee39 --- /dev/null +++ b/interface/src/instruction/solver_auth/mod.rs @@ -0,0 +1,4 @@ +//! Off-chain builders for the solver-authentication instructions. + +pub mod add; +pub mod remove; diff --git a/interface/src/instruction/remove_solver.rs b/interface/src/instruction/solver_auth/remove.rs similarity index 100% rename from interface/src/instruction/remove_solver.rs rename to interface/src/instruction/solver_auth/remove.rs diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/buffer/create.rs similarity index 95% rename from programs/settlement/src/create_buffer.rs rename to programs/settlement/src/buffer/create.rs index 2dab7e0..7047e67 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/buffer/create.rs @@ -2,7 +2,7 @@ use cow_settlement_interface::{ instruction::{ - create_buffer::{BufferAccounts, CreateBufferInput, SPL_TOKEN_PROGRAM_ID}, + buffer::create::{BufferAccounts, CreateBufferInput, SPL_TOKEN_PROGRAM_ID}, InstructionInputParsing, }, pda::{buffer::buffer_pda_seeds, state::state_pda_seeds}, @@ -65,7 +65,7 @@ pub fn process_create_buffer( mod tests { use super::*; use cow_settlement_interface::fixtures::PROGRAM_ID; - use cow_settlement_interface::instruction::create_buffer::fixtures::{ + use cow_settlement_interface::instruction::buffer::create::fixtures::{ create_buffer_data, NUM_SHARED_ACCOUNTS, }; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; diff --git a/programs/settlement/src/buffer/mod.rs b/programs/settlement/src/buffer/mod.rs new file mode 100644 index 0000000..7a03f1f --- /dev/null +++ b/programs/settlement/src/buffer/mod.rs @@ -0,0 +1,7 @@ +//! Instruction handlers for buffer operations. + +mod create; +mod reclaim; + +pub use create::process_create_buffer; +pub use reclaim::process_reclaim_buffer; diff --git a/programs/settlement/src/reclaim_buffer.rs b/programs/settlement/src/buffer/reclaim.rs similarity index 98% rename from programs/settlement/src/reclaim_buffer.rs rename to programs/settlement/src/buffer/reclaim.rs index 6a1b045..989c326 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/buffer/reclaim.rs @@ -8,7 +8,7 @@ use cow_settlement_interface::{ data::state::StateAccount, instruction::{ - create_buffer::SPL_TOKEN_PROGRAM_ID, reclaim_buffer::ReclaimBufferInput, + buffer::{create::SPL_TOKEN_PROGRAM_ID, reclaim::ReclaimBufferInput}, InstructionInputParsing, }, pda::buffer::find_buffer_pda, @@ -75,13 +75,13 @@ pub fn process_reclaim_buffer( mod tests { use cow_settlement_interface::data::state::{StateAccount, StateInitArgs, WIDTH_HEADER}; use cow_settlement_interface::fixtures::PROGRAM_ID; + use cow_settlement_interface::instruction::buffer::reclaim::fixtures::{ + reclaim_buffer_data, NUM_SHARED_ACCOUNTS, + }; use cow_settlement_interface::instruction::fixtures::{ fake_account, fake_account_owned_by, fake_account_with_data, fake_sequential_accounts, fake_signer, }; - use cow_settlement_interface::instruction::reclaim_buffer::fixtures::{ - reclaim_buffer_data, NUM_SHARED_ACCOUNTS, - }; use cow_settlement_interface::pda::state::state_pda_seeds; use litesvm_token::spl_token::state::{Account as SplTokenAccount, AccountState}; use solana_program_pack::Pack; diff --git a/programs/settlement/src/lib.rs b/programs/settlement/src/lib.rs index 1fc2f41..eab29b3 100644 --- a/programs/settlement/src/lib.rs +++ b/programs/settlement/src/lib.rs @@ -3,25 +3,19 @@ use cow_settlement_interface::{recover_discriminator, SettlementInstruction}; use pinocchio::{entrypoint, AccountView, Address, ProgramResult}; -mod add_solver; -mod create_buffer; -mod create_order; +mod buffer; mod initialize; +mod order; mod processor; -mod reclaim_buffer; -mod reclaim_order; -mod remove_solver; mod settle; +mod solver_auth; mod transfer_authority; -use add_solver::process_add_solver; -use create_buffer::process_create_buffer; -use create_order::process_create_order; +use buffer::{process_create_buffer, process_reclaim_buffer}; use initialize::process_initialize; -use reclaim_buffer::process_reclaim_buffer; -use reclaim_order::process_reclaim_order; -use remove_solver::process_remove_solver; +use order::{process_create_order, process_reclaim_order}; use settle::{process_begin_settle, process_finalize_settle}; +use solver_auth::{process_add_solver, process_remove_solver}; use transfer_authority::process_transfer_authority; entrypoint!(process_instruction); diff --git a/programs/settlement/src/create_order.rs b/programs/settlement/src/order/create.rs similarity index 97% rename from programs/settlement/src/create_order.rs rename to programs/settlement/src/order/create.rs index 6d08fd8..4718af1 100644 --- a/programs/settlement/src/create_order.rs +++ b/programs/settlement/src/order/create.rs @@ -5,7 +5,7 @@ use cow_settlement_interface::{ intent::EncodedOrderIntent, order::{self, EncodedOrderAccount}, }, - instruction::{create_order::CreateOrderInput, InstructionInputParsing}, + instruction::{order::create::CreateOrderInput, InstructionInputParsing}, pda::order::order_pda_seeds, SettlementError, }; @@ -79,12 +79,12 @@ pub fn process_create_order( mod tests { use cow_settlement_interface::data::intent::{Flags, OrderIntent, OrderKind}; use cow_settlement_interface::fixtures::PROGRAM_ID; - use cow_settlement_interface::instruction::create_order::fixtures::{ - default_order_data, valid_intent_bytes, DEFAULT_OWNER, NUM_ACCOUNTS, - }; use cow_settlement_interface::instruction::fixtures::{ fake_account, fake_account_from, fake_sequential_accounts, }; + use cow_settlement_interface::instruction::order::create::fixtures::{ + default_order_data, valid_intent_bytes, DEFAULT_OWNER, NUM_ACCOUNTS, + }; use pinocchio::account::RuntimeAccount; diff --git a/programs/settlement/src/order/mod.rs b/programs/settlement/src/order/mod.rs new file mode 100644 index 0000000..d47c38e --- /dev/null +++ b/programs/settlement/src/order/mod.rs @@ -0,0 +1,7 @@ +//! Instruction handlers for order operations. + +mod create; +mod reclaim; + +pub use create::process_create_order; +pub use reclaim::process_reclaim_order; diff --git a/programs/settlement/src/reclaim_order.rs b/programs/settlement/src/order/reclaim.rs similarity index 97% rename from programs/settlement/src/reclaim_order.rs rename to programs/settlement/src/order/reclaim.rs index 2482c4e..b04cd7b 100644 --- a/programs/settlement/src/reclaim_order.rs +++ b/programs/settlement/src/order/reclaim.rs @@ -2,7 +2,7 @@ use cow_settlement_interface::{ data::order::OrderAccount, - instruction::{reclaim_order::ReclaimOrderInput, InstructionInputParsing}, + instruction::{order::reclaim::ReclaimOrderInput, InstructionInputParsing}, SettlementError, }; use pinocchio::{ @@ -64,7 +64,7 @@ mod tests { use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::{ fixtures::{fake_account, fake_account_with_data, fake_sequential_accounts}, - reclaim_order::fixtures::{default_reclaim_data, NUM_ACCOUNTS}, + order::reclaim::fixtures::{default_reclaim_data, NUM_ACCOUNTS}, }; use cow_settlement_interface::pda::order::find_order_pda; use cow_settlement_interface::SettlementInstruction; diff --git a/programs/settlement/src/settle/mod.rs b/programs/settlement/src/settle/mod.rs index 25a01d7..37c47d8 100644 --- a/programs/settlement/src/settle/mod.rs +++ b/programs/settlement/src/settle/mod.rs @@ -1,9 +1,9 @@ -//! `BeginSettle`/`FinalizeSettle` instruction handlers. +//! Instruction handlers for the settlement. use std::ops::Deref; use cow_settlement_interface::{ - instruction::{create_buffer::SPL_TOKEN_PROGRAM_ID, settle::recover_counterpart}, + instruction::{buffer::create::SPL_TOKEN_PROGRAM_ID, settle::recover_counterpart}, recover_discriminator, SettlementError, SettlementInstruction, }; use pinocchio::{ diff --git a/programs/settlement/src/add_solver.rs b/programs/settlement/src/solver_auth/add.rs similarity index 95% rename from programs/settlement/src/add_solver.rs rename to programs/settlement/src/solver_auth/add.rs index 4491245..3f23b57 100644 --- a/programs/settlement/src/add_solver.rs +++ b/programs/settlement/src/solver_auth/add.rs @@ -7,7 +7,7 @@ use cow_settlement_interface::{ data::state::StateAccount, - instruction::{add_solver::AddSolverInput, InstructionInputParsing}, + instruction::{solver_auth::add::AddSolverInput, InstructionInputParsing}, Role, SettlementError, }; use pinocchio::{ @@ -73,10 +73,10 @@ pub fn process_add_solver( mod tests { use super::*; use cow_settlement_interface::fixtures::PROGRAM_ID; - use cow_settlement_interface::instruction::add_solver::fixtures::{ + use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; + use cow_settlement_interface::instruction::solver_auth::add::fixtures::{ add_solver_data, NUM_ACCOUNTS, }; - use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; use pinocchio::error::ProgramError; #[test] diff --git a/programs/settlement/src/solver_auth/mod.rs b/programs/settlement/src/solver_auth/mod.rs new file mode 100644 index 0000000..fe8696a --- /dev/null +++ b/programs/settlement/src/solver_auth/mod.rs @@ -0,0 +1,7 @@ +//! Instruction handlers for solver authentication. + +mod add; +mod remove; + +pub use add::process_add_solver; +pub use remove::process_remove_solver; diff --git a/programs/settlement/src/remove_solver.rs b/programs/settlement/src/solver_auth/remove.rs similarity index 95% rename from programs/settlement/src/remove_solver.rs rename to programs/settlement/src/solver_auth/remove.rs index f5d795c..c201634 100644 --- a/programs/settlement/src/remove_solver.rs +++ b/programs/settlement/src/solver_auth/remove.rs @@ -8,7 +8,7 @@ use cow_settlement_interface::{ data::state::StateAccount, - instruction::{remove_solver::RemoveSolverInput, InstructionInputParsing}, + instruction::{solver_auth::remove::RemoveSolverInput, InstructionInputParsing}, Role, SettlementError, }; use pinocchio::{ @@ -62,7 +62,7 @@ mod tests { use super::*; use cow_settlement_interface::fixtures::PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; - use cow_settlement_interface::instruction::remove_solver::fixtures::{ + use cow_settlement_interface::instruction::solver_auth::remove::fixtures::{ remove_solver_data, NUM_ACCOUNTS, }; use pinocchio::error::ProgramError; @@ -101,7 +101,7 @@ mod tests { use cow_settlement_interface::instruction::fixtures::{ fake_account, fake_account_owned_by, fake_signer, }; - use cow_settlement_interface::instruction::remove_solver::RemoveSolver; + use cow_settlement_interface::instruction::solver_auth::remove::RemoveSolver; use cow_settlement_interface::pda::state::find_state_pda; use cow_settlement_interface::{Instruction, Pubkey}; diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/buffer/create.rs similarity index 99% rename from programs/settlement/tests/create_buffer.rs rename to programs/settlement/tests/buffer/create.rs index f0aee17..520b6ec 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/buffer/create.rs @@ -1,5 +1,5 @@ use cow_settlement_client::cow_settlement_interface::{ - instruction::create_buffer::{CreateBuffers as CreateBuffersRaw, SPL_TOKEN_PROGRAM_ID}, + instruction::buffer::create::{CreateBuffers as CreateBuffersRaw, SPL_TOKEN_PROGRAM_ID}, pda::{ buffer::{buffer_pda_seeds, find_buffer_pda}, state::find_state_pda, @@ -31,7 +31,7 @@ use crate::common::{ unique_keypair, unique_pubkey, }; -mod common; +use crate::common; #[test] fn happy_path_creates_initialized_buffer_token_account() { diff --git a/programs/settlement/tests/buffer/main.rs b/programs/settlement/tests/buffer/main.rs new file mode 100644 index 0000000..83cdcc2 --- /dev/null +++ b/programs/settlement/tests/buffer/main.rs @@ -0,0 +1,7 @@ +//! Integration tests for the buffer instructions. + +#[path = "../common/mod.rs"] +mod common; + +mod create; +mod reclaim; diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/buffer/reclaim.rs similarity index 99% rename from programs/settlement/tests/reclaim_buffer.rs rename to programs/settlement/tests/buffer/reclaim.rs index ad5607d..8f8e747 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/buffer/reclaim.rs @@ -1,7 +1,7 @@ use cow_settlement_client::instructions::ReclaimBuffer; use cow_settlement_interface::Instruction; use cow_settlement_interface::{ - instruction::reclaim_buffer::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, + instruction::buffer::reclaim::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, pda::state::find_state_pda, SettlementError, }; use litesvm::LiteSVM; @@ -17,7 +17,7 @@ use crate::common::{ assert_instruction_error, to_instruction_error, unique_pubkey, InitializedParams, }; -mod common; +use crate::common; #[test] fn happy_path_reclaims_to_a_recipient_chosen_by_the_authority() { diff --git a/programs/settlement/tests/idl/parse_rust.rs b/programs/settlement/tests/idl/parse_rust.rs index b8b21b7..06eeb31 100644 --- a/programs/settlement/tests/idl/parse_rust.rs +++ b/programs/settlement/tests/idl/parse_rust.rs @@ -41,13 +41,13 @@ pub const INITIALIZE_RS: Source = Source { }; pub const CREATE_BUFFER_RS: Source = Source { - display: "interface/src/instruction/create_buffer.rs", - text: include_str!("../../../../interface/src/instruction/create_buffer.rs"), + display: "interface/src/instruction/buffer/create.rs", + text: include_str!("../../../../interface/src/instruction/buffer/create.rs"), }; pub const CREATE_ORDER_RS: Source = Source { - display: "interface/src/instruction/create_order.rs", - text: include_str!("../../../../interface/src/instruction/create_order.rs"), + display: "interface/src/instruction/order/create.rs", + text: include_str!("../../../../interface/src/instruction/order/create.rs"), }; pub const BEGIN_SETTLE_RS: Source = Source { @@ -61,13 +61,13 @@ pub const FINALIZE_SETTLE_RS: Source = Source { }; pub const RECLAIM_ORDER_RS: Source = Source { - display: "interface/src/instruction/reclaim_order.rs", - text: include_str!("../../../../interface/src/instruction/reclaim_order.rs"), + display: "interface/src/instruction/order/reclaim.rs", + text: include_str!("../../../../interface/src/instruction/order/reclaim.rs"), }; pub const RECLAIM_BUFFER_RS: Source = Source { - display: "interface/src/instruction/reclaim_buffer.rs", - text: include_str!("../../../../interface/src/instruction/reclaim_buffer.rs"), + display: "interface/src/instruction/buffer/reclaim.rs", + text: include_str!("../../../../interface/src/instruction/buffer/reclaim.rs"), }; pub const TRANSFER_AUTHORITY_RS: Source = Source { @@ -76,13 +76,13 @@ pub const TRANSFER_AUTHORITY_RS: Source = Source { }; pub const ADD_SOLVER_RS: Source = Source { - display: "interface/src/instruction/add_solver.rs", - text: include_str!("../../../../interface/src/instruction/add_solver.rs"), + display: "interface/src/instruction/solver_auth/add.rs", + text: include_str!("../../../../interface/src/instruction/solver_auth/add.rs"), }; pub const REMOVE_SOLVER_RS: Source = Source { - display: "interface/src/instruction/remove_solver.rs", - text: include_str!("../../../../interface/src/instruction/remove_solver.rs"), + display: "interface/src/instruction/solver_auth/remove.rs", + text: include_str!("../../../../interface/src/instruction/solver_auth/remove.rs"), }; impl Source { diff --git a/programs/settlement/tests/create_order.rs b/programs/settlement/tests/order/create.rs similarity index 99% rename from programs/settlement/tests/create_order.rs rename to programs/settlement/tests/order/create.rs index e30ae51..3e54dab 100644 --- a/programs/settlement/tests/create_order.rs +++ b/programs/settlement/tests/order/create.rs @@ -3,7 +3,7 @@ use cow_settlement_client::cow_settlement_interface::{ intent::{fixtures, EncodedOrderIntent, OrderIntent}, order::{EncodedOrderAccount, OrderAccount}, }, - instruction::create_order::CreateOrder, + instruction::order::create::CreateOrder, pda::order::{find_order_pda, order_pda_seeds}, SettlementError, }; @@ -20,7 +20,7 @@ use crate::common::{ signed_tx, to_instruction_error, unique_pubkey, }; -mod common; +use crate::common; fn sample_intent(owner: Pubkey) -> OrderIntent { OrderIntent { diff --git a/programs/settlement/tests/order/main.rs b/programs/settlement/tests/order/main.rs new file mode 100644 index 0000000..42231ee --- /dev/null +++ b/programs/settlement/tests/order/main.rs @@ -0,0 +1,7 @@ +//! Integration tests for the order instructions. + +#[path = "../common/mod.rs"] +mod common; + +mod create; +mod reclaim; diff --git a/programs/settlement/tests/reclaim_order.rs b/programs/settlement/tests/order/reclaim.rs similarity index 99% rename from programs/settlement/tests/reclaim_order.rs rename to programs/settlement/tests/order/reclaim.rs index 30ca4e1..40591d9 100644 --- a/programs/settlement/tests/reclaim_order.rs +++ b/programs/settlement/tests/order/reclaim.rs @@ -1,6 +1,6 @@ use cow_settlement_client::cow_settlement_interface::{ data::intent::{fixtures::sample_intent, EncodedOrderIntent, OrderIntent}, - instruction::{create_order::CreateOrder, reclaim_order::ReclaimOrder}, + instruction::{order::create::CreateOrder, order::reclaim::ReclaimOrder}, pda::order::find_order_pda, SettlementError, }; @@ -25,7 +25,7 @@ use crate::common::{ signed_tx, to_instruction_error, token, unique_keypair, unique_pubkey, }; -mod common; +use crate::common; const VALID_TO: u32 = 1_000; diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/settle/begin_orders.rs similarity index 99% rename from programs/settlement/tests/begin_settle_orders.rs rename to programs/settlement/tests/settle/begin_orders.rs index 3a2965f..1f1a83e 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/settle/begin_orders.rs @@ -48,7 +48,7 @@ use solana_sdk::{ transaction::{Transaction, TransactionError}, }; -mod common; +use crate::common; /// Assert the transaction failed in `BeginSettle` (at [`BEGIN_INDEX`]) with /// `expected`. diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/settle/finalize_pushes.rs similarity index 99% rename from programs/settlement/tests/finalize_settle_pushes.rs rename to programs/settlement/tests/settle/finalize_pushes.rs index daa291f..f34fe05 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/settle/finalize_pushes.rs @@ -28,8 +28,6 @@ use solana_sdk::{ transaction::TransactionError, }; -mod common; - /// Assert the transaction failed in `FinalizeSettle` (at [`FINALIZE_INDEX`]) /// with `expected`. fn assert_finalize_error(result: Result, expected: InstructionError) { diff --git a/programs/settlement/tests/settle_limit_prices.rs b/programs/settlement/tests/settle/limit_prices.rs similarity index 99% rename from programs/settlement/tests/settle_limit_prices.rs rename to programs/settlement/tests/settle/limit_prices.rs index fbd39d2..0506525 100644 --- a/programs/settlement/tests/settle_limit_prices.rs +++ b/programs/settlement/tests/settle/limit_prices.rs @@ -26,8 +26,6 @@ use solana_sdk::{ transaction::TransactionError, }; -mod common; - /// Convenience wrapper around [`assert_instruction_error_at`] for asserting a /// specific [`SettlementError`] at the instruction that produced it: settlements /// run as a `[BeginSettle, FinalizeSettle]` pair, so the failing instruction diff --git a/programs/settlement/tests/settle/main.rs b/programs/settlement/tests/settle/main.rs new file mode 100644 index 0000000..8d4bc16 --- /dev/null +++ b/programs/settlement/tests/settle/main.rs @@ -0,0 +1,10 @@ +//! Integration tests for the settle instructions. + +#[path = "../common/mod.rs"] +mod common; + +mod begin_orders; +mod finalize_pushes; +mod limit_prices; +mod matching_begin_finalize; +mod solver_auth; diff --git a/programs/settlement/tests/matching_begin_finalize.rs b/programs/settlement/tests/settle/matching_begin_finalize.rs similarity index 99% rename from programs/settlement/tests/matching_begin_finalize.rs rename to programs/settlement/tests/settle/matching_begin_finalize.rs index 958cea0..f4b12e1 100644 --- a/programs/settlement/tests/matching_begin_finalize.rs +++ b/programs/settlement/tests/settle/matching_begin_finalize.rs @@ -11,7 +11,7 @@ use solana_system_interface::instruction as system_instruction; use crate::common::to_instruction_error; -mod common; +use crate::common; #[derive(Clone, Copy, Debug)] /// Abstract representation of instructions in a settlement transaction. diff --git a/programs/settlement/tests/settle_solver_auth.rs b/programs/settlement/tests/settle/solver_auth.rs similarity index 99% rename from programs/settlement/tests/settle_solver_auth.rs rename to programs/settlement/tests/settle/solver_auth.rs index 82e8742..2871504 100644 --- a/programs/settlement/tests/settle_solver_auth.rs +++ b/programs/settlement/tests/settle/solver_auth.rs @@ -13,8 +13,6 @@ use crate::common::{ setup_init, to_instruction_error, unique_keypair, }; -mod common; - /// A minimal no-op settlement: a `[BeginSettle, FinalizeSettle]` pair with no /// orders, naming `solver` as `BeginSettle`'s settling solver. It moves no funds, /// so it isolates the solver gate from the rest of settlement. diff --git a/programs/settlement/tests/add_solvers.rs b/programs/settlement/tests/solver_auth/add.rs similarity index 99% rename from programs/settlement/tests/add_solvers.rs rename to programs/settlement/tests/solver_auth/add.rs index d00bf79..a5c3a07 100644 --- a/programs/settlement/tests/add_solvers.rs +++ b/programs/settlement/tests/solver_auth/add.rs @@ -24,7 +24,7 @@ use crate::common::{ to_instruction_error, unique_keypair, InitializedParams, }; -mod common; +use crate::common; /// Build an `AddSolver` transaction authorized by the manager and paid by the /// payer, both of which sign. Split from [`add_solver`] so the happy-path test diff --git a/programs/settlement/tests/solver_auth/main.rs b/programs/settlement/tests/solver_auth/main.rs new file mode 100644 index 0000000..317fdf2 --- /dev/null +++ b/programs/settlement/tests/solver_auth/main.rs @@ -0,0 +1,7 @@ +//! Integration tests for solver authentication. + +#[path = "../common/mod.rs"] +mod common; + +mod add; +mod remove; diff --git a/programs/settlement/tests/remove_solvers.rs b/programs/settlement/tests/solver_auth/remove.rs similarity index 99% rename from programs/settlement/tests/remove_solvers.rs rename to programs/settlement/tests/solver_auth/remove.rs index 2de3a22..1a31f43 100644 --- a/programs/settlement/tests/remove_solvers.rs +++ b/programs/settlement/tests/solver_auth/remove.rs @@ -24,7 +24,7 @@ use crate::common::{ to_instruction_error, unique_keypair, unique_pubkey, InitializedParams, }; -mod common; +use crate::common; /// [`setup_init`] plus a funded, dedicated `rent_recipient` for removals. A /// removal refunds the rent to this account, and the recipient of a lamport