diff --git a/bench-report.json b/bench-report.json index 6dc671d..23dffb3 100644 --- a/bench-report.json +++ b/bench-report.json @@ -32,24 +32,24 @@ "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, - "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, + "add_solver/add_with_many_existing_solvers": 5075, + "add_solver/adds_a_solver": 4623, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 7348, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17248, + "create_buffers/max_buffers_in_one_instruction": 169572, + "create_order/happy_path_creates_order_pda_with_expected_body": 4987, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4531, + "reclaim_buffer/funded_buffer_is_skipped": 4834, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 5981, + "reclaim_buffer/max_buffers_in_one_instruction": 124621, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7580, + "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2204, + "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2073, + "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2081, "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, + "remove_solver/remove_with_many_existing_solvers": 3759, + "remove_solver/removes_a_solver": 3494, "settle/finalizes_with_no_pushes": 7154, "settle/pulls_from_multiple_orders": 20059, "settle/pulls_funds_to_destination": 13640, @@ -59,9 +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": 3173, - "transfer_authority/manager_can_transfer_reclaim_authority": 3175, - "transfer_authority/reclaim_authority_can_transfer_itself": 3179 + "transfer_authority/manager_can_transfer_manager": 3175, + "transfer_authority/manager_can_transfer_reclaim_authority": 3177, + "transfer_authority/reclaim_authority_can_transfer_itself": 3181 }, "transaction_bytes": { "add_solver/add_with_many_existing_solvers": 366, diff --git a/client/src/instruction/add_solver.rs b/client/src/instruction/add_solver.rs new file mode 100644 index 0000000..6f48579 --- /dev/null +++ b/client/src/instruction/add_solver.rs @@ -0,0 +1,27 @@ +//! Builder for the `AddSolver` instruction. + +use cow_settlement_interface::{pda::state::find_state_pda, Instruction, Pubkey}; + +/// Inserts `solver` into the state PDA's solver list. `manager` authorizes the +/// change and must be the current manager; `payer` funds the account's growth. +/// Both sign. +pub struct AddSolver { + pub program_id: Pubkey, + pub manager: Pubkey, + pub payer: Pubkey, + pub solver: Pubkey, +} + +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 { + program_id: builder.program_id, + manager: builder.manager, + payer: builder.payer, + state_pda, + solver: builder.solver, + } + .into() + } +} diff --git a/client/src/instruction/begin_settle.rs b/client/src/instruction/begin_settle.rs new file mode 100644 index 0000000..cf7aab5 --- /dev/null +++ b/client/src/instruction/begin_settle.rs @@ -0,0 +1,134 @@ +//! Builder for the `BeginSettle` instruction. + +use cow_settlement_interface::{ + data::intent::OrderIntent, + pda::{order::find_order_pda, state::find_state_pda}, + Instruction, Pubkey, +}; + +// Reexport the interface's `Pull` so the client provides all the types a caller +// needs to build a settlement. +pub use cow_settlement_interface::instruction::settle::Pull; + +/// An order ready to be settled, together with the funds to pull from it: +/// `intent` identifies the order and `pulls` lists the [`Pull`]s to make from +/// its sell token account. +pub struct InitializedIntent<'a> { + pub intent: &'a OrderIntent, + pub pulls: &'a [Pull], +} + +/// Builder for a `BeginSettle` instruction settling the given orders. +pub struct BeginSettle<'a> { + pub program_id: Pubkey, + pub solver: Pubkey, + pub finalize_ix_index: u16, + /// The off-chain auction this settlement executes, carried so it can be tied + /// back to its auction off-chain. + pub auction_id: i64, + pub orders: &'a [InitializedIntent<'a>], +} + +impl From> for Instruction { + fn from(builder: BeginSettle<'_>) -> Self { + let mut order_pdas = Vec::with_capacity(builder.orders.len()); + let mut sell_token_accounts = Vec::with_capacity(builder.orders.len()); + let mut pull_lists: Vec<&[Pull]> = Vec::with_capacity(builder.orders.len()); + for order in builder.orders { + let (order_pda, _bump) = find_order_pda(&builder.program_id, &order.intent.uid()); + order_pdas.push(order_pda); + sell_token_accounts.push(order.intent.sell_token_account); + pull_lists.push(order.pulls); + } + let (state_pda, _bump) = find_state_pda(&builder.program_id); + cow_settlement_interface::instruction::settle::BeginSettle { + program_id: builder.program_id, + state_pda, + solver: builder.solver, + finalize_ix_index: builder.finalize_ix_index, + auction_id: builder.auction_id, + order_pdas: &order_pdas, + sell_token_accounts: &sell_token_accounts, + pulls: &pull_lists, + } + .into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ::proptest::{prelude::*, test_runner::TestCaseError}; + use cow_settlement_interface::{ + data::intent::fixtures::arb_order_intent, + fixtures::pubkey_from_seed, + instruction::{ + fixtures::fake_account_from_array, + settle::{BeginSettleInput, INSTRUCTIONS_SYSVAR_ID}, + InstructionInputParsing, + }, + }; + + proptest! { + // `BeginSettle` derives each order's PDA from its intent and forwards to + // the interface builder so that the on-chain parser recovers exactly + // those orders. + #[test] + fn begin_settle_derives_orders_from_intents( + finalize_ix_index in any::(), + intents in prop::collection::vec(arb_order_intent(), 1..=5), + ) { + let program_id = pubkey_from_seed("program id"); + // No pulls here: this test only checks that orders are derived and + // laid out correctly. + let orders: Vec = intents + .iter() + .map(|intent| InitializedIntent { intent, pulls: &[] }) + .collect(); + let ix = Instruction::from(BeginSettle { + program_id, + solver: pubkey_from_seed("solver"), + finalize_ix_index, + auction_id: 0, + orders: &orders, + }); + + // Expected orders: each intent's canonical PDA paired with its sell + // token account, sorted by PDA address (the builder's order). + let mut expected: Vec<(Pubkey, Pubkey)> = intents + .iter() + .map(|intent| { + let (order_pda, _bump) = find_order_pda(&program_id, &intent.uid()); + (order_pda, intent.sell_token_account) + }) + .collect(); + expected.sort_by_key(|(order_pda, _)| *order_pda); + + let accounts: Vec<_> = ix + .accounts + .iter() + .map(|meta| fake_account_from_array(meta.pubkey.to_bytes())) + .collect(); + let parsed = BeginSettleInput::parse(&ix.data, &accounts) + .map_err(|e| TestCaseError::fail(format!("parse failed: {e:?}")))?; + + prop_assert_eq!(parsed.finalize_ix_index, finalize_ix_index); + prop_assert_eq!( + parsed.instructions_sysvar_account.address(), + &INSTRUCTIONS_SYSVAR_ID, + ); + + let actual: Vec<(Pubkey, Pubkey)> = parsed + .orders + .iter() + .map(|order| { + ( + *order.order_pda.address(), + *order.sell_token_account.address(), + ) + }) + .collect(); + prop_assert_eq!(actual, expected); + } + } +} diff --git a/client/src/instruction/create_buffer.rs b/client/src/instruction/create_buffer.rs new file mode 100644 index 0000000..425f870 --- /dev/null +++ b/client/src/instruction/create_buffer.rs @@ -0,0 +1,25 @@ +//! Builder for the `CreateBuffer` instruction. + +use cow_settlement_interface::{pda::buffer::find_buffer_pda, Instruction, Pubkey}; + +pub struct CreateBuffers<'a> { + pub program_id: Pubkey, + pub payer: Pubkey, + pub mints: &'a [Pubkey], +} + +impl From> for Instruction { + fn from(builder: CreateBuffers<'_>) -> Self { + let buffers: Vec<(Pubkey, Pubkey)> = builder + .mints + .iter() + .map(|mint| (find_buffer_pda(&builder.program_id, mint).0, *mint)) + .collect(); + cow_settlement_interface::instruction::create_buffer::CreateBuffers { + program_id: builder.program_id, + payer: builder.payer, + buffers: &buffers, + } + .into() + } +} diff --git a/client/src/instruction/create_order.rs b/client/src/instruction/create_order.rs new file mode 100644 index 0000000..ad9a688 --- /dev/null +++ b/client/src/instruction/create_order.rs @@ -0,0 +1,30 @@ +//! Builder for the `CreateOrder` instruction. + +use cow_settlement_interface::{ + data::intent::{EncodedOrderIntent, OrderIntent}, + pda::order::find_order_pda, + Instruction, Pubkey, +}; + +pub struct CreateOrder<'a> { + pub program_id: Pubkey, + pub owner: Pubkey, + pub created_by: Pubkey, + pub intent: &'a OrderIntent, +} + +impl From> for Instruction { + fn from(builder: CreateOrder<'_>) -> Self { + 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 { + program_id: builder.program_id, + owner: builder.owner, + created_by: builder.created_by, + order_pda, + intent_bytes, + } + .into() + } +} diff --git a/client/src/instruction/finalize_settle.rs b/client/src/instruction/finalize_settle.rs new file mode 100644 index 0000000..7525fa5 --- /dev/null +++ b/client/src/instruction/finalize_settle.rs @@ -0,0 +1,169 @@ +//! Builder for the `FinalizeSettle` instruction. + +use cow_settlement_interface::{ + data::intent::OrderIntent, + pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, + Instruction, Pubkey, +}; + +/// A settled order whose proceeds are pushed to it: `intent` identifies the +/// order (its `buy_token_account` is the push destination and its `buy_mint` +/// selects the canonical source buffer) and `amount` is the quantity to push. +pub struct FinalizedIntent<'a> { + pub intent: &'a OrderIntent, + pub amount: u64, +} + +/// Builder for a `FinalizeSettle` instruction pushing each order's proceeds to +/// its buy token account. +/// +/// The destination is the order intent's `buy_token_account` and the source is +/// the canonical buffer PDA for its `buy_mint` (see [`find_buffer_pda`]), the +/// only buffer `BeginSettle` accepts as the source of that order's push. The +/// orders are sorted by their canonical order PDA (the same key +/// [`BeginSettle`](super::begin_settle::BeginSettle) orders its settled-order +/// list by) so the two instructions present the orders +/// in the same order and their lists line up. +pub struct FinalizeSettle<'a> { + pub program_id: Pubkey, + pub begin_ix_index: u16, + pub orders: &'a [FinalizedIntent<'a>], +} + +impl From> for Instruction { + fn from(builder: FinalizeSettle<'_>) -> Self { + // Sort the orders by their canonical order PDA, the key `BeginSettle` + // lays its settled orders out by, so the two instruction lists align. + // For BeginSettle, sorting can take place in the interface. But the + // order PDAs don't appear in the actual FinalizeSettle instruction, so + // the sorting can only happen here. + let num_orders = builder.orders.len(); + let mut orders: Vec = (0..num_orders).collect(); + orders.sort_by_key(|&i| { + find_order_pda(&builder.program_id, &builder.orders[i].intent.uid()).0 + }); + + let mut source_buffers: Vec = Vec::with_capacity(num_orders); + let mut destinations = Vec::with_capacity(num_orders); + let mut bumps = Vec::with_capacity(num_orders); + let mut amounts = Vec::with_capacity(num_orders); + for &i in &orders { + let (buffer_pda, bump) = + find_buffer_pda(&builder.program_id, &builder.orders[i].intent.buy_mint); + source_buffers.push(buffer_pda); + destinations.push(builder.orders[i].intent.buy_token_account); + bumps.push(bump); + amounts.push(builder.orders[i].amount); + } + let (state_pda, _bump) = find_state_pda(&builder.program_id); + cow_settlement_interface::instruction::settle::FinalizeSettle { + program_id: builder.program_id, + state_pda, + begin_ix_index: builder.begin_ix_index, + source_buffers: &source_buffers, + destinations: &destinations, + bumps: &bumps, + amounts: &amounts, + } + .into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ::proptest::{prelude::*, test_runner::TestCaseError}; + use cow_settlement_interface::{ + data::intent::fixtures::arb_order_intent, + fixtures::pubkey_from_seed, + instruction::{ + fixtures::fake_account_from_array, + settle::{FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID}, + InstructionInputParsing, + }, + }; + + proptest! { + // `FinalizeSettle` derives each order's source buffer from its buy mint + // and destination from the intent, sorting by canonical order PDA like + // `BeginSettle` so the on-chain parser recovers exactly those pushes in + // that order. + #[test] + fn finalize_settle_derives_buffers_from_mints( + begin_ix_index in any::(), + cases in prop::collection::vec( + (arb_order_intent(), any::()), + 1..=5, + ), + ) { + let program_id = pubkey_from_seed("program id"); + let orders: Vec = cases + .iter() + .map(|(intent, amount)| FinalizedIntent { + intent, + amount: *amount, + }) + .collect(); + let ix = Instruction::from(FinalizeSettle { + program_id, + begin_ix_index, + orders: &orders, + }); + + // Expected pushes: each order's buffer PDA (and its canonical bump), + // buy token account, and amount, sorted by the order's canonical PDA + // (the builder's order). + struct ExpectedPush { + order_pda: Pubkey, + buffer: Pubkey, + bump: u8, + destination: Pubkey, + amount: u64, + } + let mut expected: Vec = orders + .iter() + .map(|order| { + let (order_pda, _bump) = find_order_pda(&program_id, &order.intent.uid()); + let (buffer, bump) = find_buffer_pda(&program_id, &order.intent.buy_mint); + ExpectedPush { + order_pda, + buffer, + bump, + destination: order.intent.buy_token_account, + amount: order.amount, + } + }) + .collect(); + expected.sort_by_key(|push| push.order_pda); + + let accounts: Vec<_> = ix + .accounts + .iter() + .map(|meta| fake_account_from_array(meta.pubkey.to_bytes())) + .collect(); + let parsed = FinalizeSettleInput::parse(&ix.data, &accounts) + .map_err(|e| TestCaseError::fail(format!("parse failed: {e:?}")))?; + + prop_assert_eq!(parsed.begin_ix_index, begin_ix_index); + prop_assert_eq!( + parsed.instructions_sysvar_account.address(), + &INSTRUCTIONS_SYSVAR_ID, + ); + let (state_pda, _bump) = find_state_pda(&program_id); + prop_assert_eq!(parsed.state_pda_account.address(), &state_pda); + prop_assert_eq!( + parsed.token_program_account.address(), + &SPL_TOKEN_PROGRAM_ID, + ); + + let parsed_pushes: Vec<_> = parsed.pushes.iter().collect(); + prop_assert_eq!(parsed_pushes.len(), expected.len()); + for (push, expected) in parsed_pushes.iter().zip(&expected) { + prop_assert_eq!(push.source_buffer.address(), &expected.buffer); + prop_assert_eq!(push.destination.address(), &expected.destination); + prop_assert_eq!(push.bump, expected.bump); + prop_assert_eq!(push.amount, expected.amount); + } + } + } +} diff --git a/client/src/instruction/initialize.rs b/client/src/instruction/initialize.rs new file mode 100644 index 0000000..6bc76a2 --- /dev/null +++ b/client/src/instruction/initialize.rs @@ -0,0 +1,24 @@ +//! Builder for the `Initialize` instruction. + +use cow_settlement_interface::{pda::state::find_state_pda, Instruction, Pubkey}; + +pub struct Initialize { + pub program_id: Pubkey, + pub payer: Pubkey, + pub manager: Pubkey, + pub reclaim_authority: Pubkey, +} + +impl From for Instruction { + fn from(builder: Initialize) -> Self { + let (state_pda, _bump) = find_state_pda(&builder.program_id); + cow_settlement_interface::instruction::initialize::Initialize { + program_id: builder.program_id, + payer: builder.payer, + state_pda, + manager: builder.manager, + reclaim_authority: builder.reclaim_authority, + } + .into() + } +} diff --git a/client/src/instruction/mod.rs b/client/src/instruction/mod.rs new file mode 100644 index 0000000..66ef70b --- /dev/null +++ b/client/src/instruction/mod.rs @@ -0,0 +1,26 @@ +//! Instruction builders for the settlement program. +//! +//! A single place for client callers to reach for instruction constructors. +//! The instruction builders are the same as those in the interface, but they +//! provide a simplified interface at the price of more computation done +//! by the function, making it more suitable for off-chain use. + +pub mod add_solver; +pub mod begin_settle; +pub mod create_buffer; +pub mod create_order; +pub mod finalize_settle; +pub mod initialize; +pub mod reclaim_buffer; +pub mod remove_solver; +pub mod transfer_authority; + +pub use add_solver::AddSolver; +pub use begin_settle::{BeginSettle, InitializedIntent, Pull}; +pub use create_buffer::CreateBuffers; +pub use create_order::CreateOrder; +pub use finalize_settle::{FinalizeSettle, FinalizedIntent}; +pub use initialize::Initialize; +pub use reclaim_buffer::ReclaimBuffer; +pub use remove_solver::RemoveSolver; +pub use transfer_authority::TransferAuthority; diff --git a/client/src/instruction/reclaim_buffer.rs b/client/src/instruction/reclaim_buffer.rs new file mode 100644 index 0000000..af86ef8 --- /dev/null +++ b/client/src/instruction/reclaim_buffer.rs @@ -0,0 +1,42 @@ +//! Builder for the `ReclaimBuffer` instruction. + +use cow_settlement_interface::{ + pda::{buffer::find_buffer_pda, state::find_state_pda}, + Instruction, Pubkey, +}; + +/// Builder for a `ReclaimBuffer` instruction closing the buffer for each of +/// `mints` and sending their rent lamports to `reclaim_recipient`, which +/// `reclaim_authority` picks freely and may set to itself. +/// +/// A buffer that still holds a token balance is silently skipped rather than +/// closed, so a successful instruction is no guarantee that any buffer went +/// away. This is done to prevent accidental loss of funds. +pub struct ReclaimBuffer<'a> { + pub program_id: Pubkey, + pub reclaim_authority: Pubkey, + pub reclaim_recipient: Pubkey, + pub mints: &'a [Pubkey], +} + +impl From> for Instruction { + fn from(builder: ReclaimBuffer<'_>) -> Self { + let (state_pda, _bump) = find_state_pda(&builder.program_id); + let buffers: Vec<(Pubkey, Pubkey)> = builder + .mints + .iter() + .map(|mint| { + let (buffer_pda, _bump) = find_buffer_pda(&builder.program_id, mint); + (buffer_pda, *mint) + }) + .collect(); + cow_settlement_interface::instruction::reclaim_buffer::ReclaimBuffer { + program_id: builder.program_id, + state_pda, + reclaim_authority: builder.reclaim_authority, + reclaim_recipient: builder.reclaim_recipient, + buffers: &buffers, + } + .into() + } +} diff --git a/client/src/instruction/remove_solver.rs b/client/src/instruction/remove_solver.rs new file mode 100644 index 0000000..90422af --- /dev/null +++ b/client/src/instruction/remove_solver.rs @@ -0,0 +1,26 @@ +//! Builder for the `RemoveSolver` instruction. + +use cow_settlement_interface::{pda::state::find_state_pda, Instruction, Pubkey}; + +/// 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() + } +} diff --git a/client/src/instruction/transfer_authority.rs b/client/src/instruction/transfer_authority.rs new file mode 100644 index 0000000..6a3d843 --- /dev/null +++ b/client/src/instruction/transfer_authority.rs @@ -0,0 +1,26 @@ +//! Builder for the `TransferAuthority` instruction. + +use cow_settlement_interface::{pda::state::find_state_pda, Instruction, Pubkey, Role}; + +/// Transfers `role` to `new_authority` in a single step. Signed by `signer`, +/// which must be the manager or the current holder of `role`. +pub struct TransferAuthority { + pub program_id: Pubkey, + pub signer: Pubkey, + pub role: Role, + pub new_authority: Pubkey, +} + +impl From for Instruction { + fn from(builder: TransferAuthority) -> Self { + let (state_pda, _bump) = find_state_pda(&builder.program_id); + cow_settlement_interface::instruction::transfer_authority::TransferAuthority { + program_id: builder.program_id, + signer: builder.signer, + state_pda, + role: builder.role, + new_authority: builder.new_authority, + } + .into() + } +} diff --git a/client/src/instructions.rs b/client/src/instructions.rs deleted file mode 100644 index 207016e..0000000 --- a/client/src/instructions.rs +++ /dev/null @@ -1,458 +0,0 @@ -//! Instruction builders for the settlement program. -//! -//! A single place for client callers to reach for instruction constructors. -//! The instruction builders are the same as those in the interface, but they -//! provide a simplified interface at the price of more computation done -//! by the function, making it more suitable for off-chain use. - -use cow_settlement_interface::{ - data::intent::{EncodedOrderIntent, OrderIntent}, - pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, - Instruction, Pubkey, Role, -}; - -// Reexport the instruction builders that don't change from the interface. -// We want the client to provide all instruction builders. -pub use cow_settlement_interface::instruction::settle::Pull; - -/// An order ready to be settled, together with the funds to pull from it: -/// `intent` identifies the order and `pulls` lists the [`Pull`]s to make from -/// its sell token account. -pub struct InitializedIntent<'a> { - pub intent: &'a OrderIntent, - pub pulls: &'a [Pull], -} - -/// Builder for a `BeginSettle` instruction settling the given orders. -pub struct BeginSettle<'a> { - pub program_id: Pubkey, - pub solver: Pubkey, - pub finalize_ix_index: u16, - /// The off-chain auction this settlement executes, carried so it can be tied - /// back to its auction off-chain. - pub auction_id: i64, - pub orders: &'a [InitializedIntent<'a>], -} - -impl From> for Instruction { - fn from(builder: BeginSettle<'_>) -> Self { - let mut order_pdas = Vec::with_capacity(builder.orders.len()); - let mut sell_token_accounts = Vec::with_capacity(builder.orders.len()); - let mut pull_lists: Vec<&[Pull]> = Vec::with_capacity(builder.orders.len()); - for order in builder.orders { - let (order_pda, _bump) = find_order_pda(&builder.program_id, &order.intent.uid()); - order_pdas.push(order_pda); - sell_token_accounts.push(order.intent.sell_token_account); - pull_lists.push(order.pulls); - } - let (state_pda, _bump) = find_state_pda(&builder.program_id); - cow_settlement_interface::instruction::settle::BeginSettle { - program_id: builder.program_id, - state_pda, - solver: builder.solver, - finalize_ix_index: builder.finalize_ix_index, - auction_id: builder.auction_id, - order_pdas: &order_pdas, - sell_token_accounts: &sell_token_accounts, - pulls: &pull_lists, - } - .into() - } -} - -/// A settled order whose proceeds are pushed to it: `intent` identifies the -/// order (its `buy_token_account` is the push destination and its `buy_mint` -/// selects the canonical source buffer) and `amount` is the quantity to push. -pub struct FinalizedIntent<'a> { - pub intent: &'a OrderIntent, - pub amount: u64, -} - -/// Builder for a `FinalizeSettle` instruction pushing each order's proceeds to -/// its buy token account. -/// -/// The destination is the order intent's `buy_token_account` and the source is -/// the canonical buffer PDA for its `buy_mint` (see [`find_buffer_pda`]), the -/// only buffer `BeginSettle` accepts as the source of that order's push. The -/// orders are sorted by their canonical order PDA (the same key [`BeginSettle`] -/// orders its settled-order list by) so the two instructions present the orders -/// in the same order and their lists line up. -pub struct FinalizeSettle<'a> { - pub program_id: Pubkey, - pub begin_ix_index: u16, - pub orders: &'a [FinalizedIntent<'a>], -} - -impl From> for Instruction { - fn from(builder: FinalizeSettle<'_>) -> Self { - // Sort the orders by their canonical order PDA, the key `BeginSettle` - // lays its settled orders out by, so the two instruction lists align. - // For BeginSettle, sorting can take place in the interface. But the - // order PDAs don't appear in the actual FinalizeSettle instruction, so - // the sorting can only happen here. - let num_orders = builder.orders.len(); - let mut orders: Vec = (0..num_orders).collect(); - orders.sort_by_key(|&i| { - find_order_pda(&builder.program_id, &builder.orders[i].intent.uid()).0 - }); - - let mut source_buffers: Vec = Vec::with_capacity(num_orders); - let mut destinations = Vec::with_capacity(num_orders); - let mut bumps = Vec::with_capacity(num_orders); - let mut amounts = Vec::with_capacity(num_orders); - for &i in &orders { - let (buffer_pda, bump) = - find_buffer_pda(&builder.program_id, &builder.orders[i].intent.buy_mint); - source_buffers.push(buffer_pda); - destinations.push(builder.orders[i].intent.buy_token_account); - bumps.push(bump); - amounts.push(builder.orders[i].amount); - } - let (state_pda, _bump) = find_state_pda(&builder.program_id); - cow_settlement_interface::instruction::settle::FinalizeSettle { - program_id: builder.program_id, - state_pda, - begin_ix_index: builder.begin_ix_index, - source_buffers: &source_buffers, - destinations: &destinations, - bumps: &bumps, - amounts: &amounts, - } - .into() - } -} - -pub struct CreateOrder<'a> { - pub program_id: Pubkey, - pub owner: Pubkey, - pub created_by: Pubkey, - pub intent: &'a OrderIntent, -} - -impl From> for Instruction { - fn from(builder: CreateOrder<'_>) -> Self { - 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 { - program_id: builder.program_id, - owner: builder.owner, - created_by: builder.created_by, - order_pda, - intent_bytes, - } - .into() - } -} - -pub struct CreateBuffers<'a> { - pub program_id: Pubkey, - pub payer: Pubkey, - pub mints: &'a [Pubkey], -} - -impl From> for Instruction { - fn from(builder: CreateBuffers<'_>) -> Self { - let buffers: Vec<(Pubkey, Pubkey)> = builder - .mints - .iter() - .map(|mint| (find_buffer_pda(&builder.program_id, mint).0, *mint)) - .collect(); - cow_settlement_interface::instruction::create_buffer::CreateBuffers { - program_id: builder.program_id, - payer: builder.payer, - buffers: &buffers, - } - .into() - } -} - -pub struct Initialize { - pub program_id: Pubkey, - pub payer: Pubkey, - pub manager: Pubkey, - pub reclaim_authority: Pubkey, -} - -impl From for Instruction { - fn from(builder: Initialize) -> Self { - let (state_pda, _bump) = find_state_pda(&builder.program_id); - cow_settlement_interface::instruction::initialize::Initialize { - program_id: builder.program_id, - payer: builder.payer, - state_pda, - manager: builder.manager, - reclaim_authority: builder.reclaim_authority, - } - .into() - } -} - -/// Builder for a `ReclaimBuffer` instruction closing the buffer for each of -/// `mints` and sending their rent lamports to `reclaim_recipient`, which -/// `reclaim_authority` picks freely and may set to itself. -/// -/// A buffer that still holds a token balance is silently skipped rather than -/// closed, so a successful instruction is no guarantee that any buffer went -/// away. This is done to prevent accidental loss of funds. -pub struct ReclaimBuffer<'a> { - pub program_id: Pubkey, - pub reclaim_authority: Pubkey, - pub reclaim_recipient: Pubkey, - pub mints: &'a [Pubkey], -} - -impl From> for Instruction { - fn from(builder: ReclaimBuffer<'_>) -> Self { - let (state_pda, _bump) = find_state_pda(&builder.program_id); - let buffers: Vec<(Pubkey, Pubkey)> = builder - .mints - .iter() - .map(|mint| { - let (buffer_pda, _bump) = find_buffer_pda(&builder.program_id, mint); - (buffer_pda, *mint) - }) - .collect(); - cow_settlement_interface::instruction::reclaim_buffer::ReclaimBuffer { - program_id: builder.program_id, - state_pda, - reclaim_authority: builder.reclaim_authority, - reclaim_recipient: builder.reclaim_recipient, - buffers: &buffers, - } - .into() - } -} - -/// Transfers `role` to `new_authority` in a single step. Signed by `signer`, -/// which must be the manager or the current holder of `role`. -pub struct TransferAuthority { - pub program_id: Pubkey, - pub signer: Pubkey, - pub role: Role, - pub new_authority: Pubkey, -} - -impl From for Instruction { - fn from(builder: TransferAuthority) -> Self { - let (state_pda, _bump) = find_state_pda(&builder.program_id); - cow_settlement_interface::instruction::transfer_authority::TransferAuthority { - program_id: builder.program_id, - signer: builder.signer, - state_pda, - role: builder.role, - new_authority: builder.new_authority, - } - .into() - } -} - -/// Inserts `solver` into the state PDA's solver list. `manager` authorizes the -/// change and must be the current manager; `payer` funds the account's growth. -/// Both sign. -pub struct AddSolver { - pub program_id: Pubkey, - pub manager: Pubkey, - pub payer: Pubkey, - pub solver: Pubkey, -} - -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 { - program_id: builder.program_id, - manager: builder.manager, - payer: builder.payer, - state_pda, - solver: builder.solver, - } - .into() - } -} - -/// 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::*; - use ::proptest::{prelude::*, test_runner::TestCaseError}; - use cow_settlement_interface::{ - data::intent::fixtures::arb_order_intent, - fixtures::pubkey_from_seed, - instruction::{ - fixtures::fake_account_from_array, - settle::{ - BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID, - }, - InstructionInputParsing, - }, - pda::order::find_order_pda, - }; - - proptest! { - // `BeginSettle` derives each order's PDA from its intent and forwards to - // the interface builder so that the on-chain parser recovers exactly - // those orders. - #[test] - fn begin_settle_derives_orders_from_intents( - finalize_ix_index in any::(), - intents in prop::collection::vec(arb_order_intent(), 1..=5), - ) { - let program_id = pubkey_from_seed("program id"); - // No pulls here: this test only checks that orders are derived and - // laid out correctly. - let orders: Vec = intents - .iter() - .map(|intent| InitializedIntent { intent, pulls: &[] }) - .collect(); - let ix = Instruction::from(BeginSettle { - program_id, - solver: pubkey_from_seed("solver"), - finalize_ix_index, - auction_id: 0, - orders: &orders, - }); - - // Expected orders: each intent's canonical PDA paired with its sell - // token account, sorted by PDA address (the builder's order). - let mut expected: Vec<(Pubkey, Pubkey)> = intents - .iter() - .map(|intent| { - let (order_pda, _bump) = find_order_pda(&program_id, &intent.uid()); - (order_pda, intent.sell_token_account) - }) - .collect(); - expected.sort_by_key(|(order_pda, _)| *order_pda); - - let accounts: Vec<_> = ix - .accounts - .iter() - .map(|meta| fake_account_from_array(meta.pubkey.to_bytes())) - .collect(); - let parsed = BeginSettleInput::parse(&ix.data, &accounts) - .map_err(|e| TestCaseError::fail(format!("parse failed: {e:?}")))?; - - prop_assert_eq!(parsed.finalize_ix_index, finalize_ix_index); - prop_assert_eq!( - parsed.instructions_sysvar_account.address(), - &INSTRUCTIONS_SYSVAR_ID, - ); - - let actual: Vec<(Pubkey, Pubkey)> = parsed - .orders - .iter() - .map(|order| { - ( - *order.order_pda.address(), - *order.sell_token_account.address(), - ) - }) - .collect(); - prop_assert_eq!(actual, expected); - } - - // `FinalizeSettle` derives each order's source buffer from its buy mint - // and destination from the intent, sorting by canonical order PDA like - // `BeginSettle` so the on-chain parser recovers exactly those pushes in - // that order. - #[test] - fn finalize_settle_derives_buffers_from_mints( - begin_ix_index in any::(), - cases in prop::collection::vec( - (arb_order_intent(), any::()), - 1..=5, - ), - ) { - let program_id = pubkey_from_seed("program id"); - let orders: Vec = cases - .iter() - .map(|(intent, amount)| FinalizedIntent { - intent, - amount: *amount, - }) - .collect(); - let ix = Instruction::from(FinalizeSettle { - program_id, - begin_ix_index, - orders: &orders, - }); - - // Expected pushes: each order's buffer PDA (and its canonical bump), - // buy token account, and amount, sorted by the order's canonical PDA - // (the builder's order). - struct ExpectedPush { - order_pda: Pubkey, - buffer: Pubkey, - bump: u8, - destination: Pubkey, - amount: u64, - } - let mut expected: Vec = orders - .iter() - .map(|order| { - let (order_pda, _bump) = find_order_pda(&program_id, &order.intent.uid()); - let (buffer, bump) = find_buffer_pda(&program_id, &order.intent.buy_mint); - ExpectedPush { - order_pda, - buffer, - bump, - destination: order.intent.buy_token_account, - amount: order.amount, - } - }) - .collect(); - expected.sort_by_key(|push| push.order_pda); - - let accounts: Vec<_> = ix - .accounts - .iter() - .map(|meta| fake_account_from_array(meta.pubkey.to_bytes())) - .collect(); - let parsed = FinalizeSettleInput::parse(&ix.data, &accounts) - .map_err(|e| TestCaseError::fail(format!("parse failed: {e:?}")))?; - - prop_assert_eq!(parsed.begin_ix_index, begin_ix_index); - prop_assert_eq!( - parsed.instructions_sysvar_account.address(), - &INSTRUCTIONS_SYSVAR_ID, - ); - let (state_pda, _bump) = find_state_pda(&program_id); - prop_assert_eq!(parsed.state_pda_account.address(), &state_pda); - prop_assert_eq!( - parsed.token_program_account.address(), - &SPL_TOKEN_PROGRAM_ID, - ); - - let parsed_pushes: Vec<_> = parsed.pushes.iter().collect(); - prop_assert_eq!(parsed_pushes.len(), expected.len()); - for (push, expected) in parsed_pushes.iter().zip(&expected) { - prop_assert_eq!(push.source_buffer.address(), &expected.buffer); - prop_assert_eq!(push.destination.address(), &expected.destination); - prop_assert_eq!(push.bump, expected.bump); - prop_assert_eq!(push.amount, expected.amount); - } - } - } -} diff --git a/client/src/lib.rs b/client/src/lib.rs index 9cca48a..434bd64 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2,6 +2,6 @@ pub use cow_settlement_interface; -pub mod instructions; +pub mod instruction; pub mod parse; pub mod pda; diff --git a/client/src/parse.rs b/client/src/parse.rs index 7f0c235..984aa96 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -77,7 +77,7 @@ pub fn parse_instruction<'a, A>( #[cfg(test)] mod tests { use super::*; - use crate::instructions::{ + use crate::instruction::{ AddSolver, BeginSettle, CreateBuffers, CreateOrder, FinalizeSettle, Initialize, InitializedIntent, RemoveSolver, }; diff --git a/interface/src/error.rs b/interface/src/error.rs new file mode 100644 index 0000000..c0e078d --- /dev/null +++ b/interface/src/error.rs @@ -0,0 +1,143 @@ +//! Program-side errors surfaced by the settlement program. + +/// Program-side errors surfaced by the settlement program. +/// The discriminant value is the on-chain `ProgramError::Custom` code. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum SettlementError { + /// The `FinalizeSettle` included as input to `BeginSettle` isn't before + /// the actual `BeginSettle` index. + FinalizeBeforeInitialize = 0, + /// Another `BeginSettle`/`FinalizeSettle` of this program appears strictly + /// between this pair's bounds, nesting or overlapping two settlements. + BeginFinalizePairOverlap = 1, + /// The counterpart index points past the end of the transaction's + /// instruction list, so no instruction sits there. + MissingCounterpartInstruction = 2, + /// The instruction at the counterpart index belongs to a different program. + CounterpartIsExternal = 3, + /// The counterpart instruction's discriminator byte couldn't be recovered + /// from its data. + InvalidCounterpartDiscriminator = 4, + /// The counterpart instruction's own counterpart index couldn't be + /// recovered from its data. + InvalidCounterpartCounterpart = 5, + /// The counterpart's discriminator isn't the expected + /// `BeginSettle`/`FinalizeSettle` kind, or its counterpart index doesn't + /// point back at this instruction. + MismatchedCounterpartDiscriminator = 6, + /// `CreateOrder` instruction wasn't signed by the created `OrderIntent` + /// owner. + OwnerMismatch = 7, + /// An account was provided that cannot be derived from the seeds recognized by the program + AccountNotDerivable = 8, + /// `BeginSettle`'s order accounts aren't passed strictly increasing by + /// address. + OrdersNotStrictlyIncreasing = 9, + /// A `BeginSettle` sell token account doesn't match the + /// `sell_token_account` recorded in the order's intent. + SellTokenAccountMismatch = 10, + /// A `BeginSettle` sell token account isn't a valid SPL token account + /// (wrong data length or not owned by the token program). + SellTokenAccountInvalid = 11, + /// A `BeginSettle` sell token account's SPL owner isn't the order's intent + /// owner. + SellTokenOwnerMismatch = 12, + /// `BeginSettle`'s order-account count doesn't match the structure its + /// instruction data expects: `n` orders each contribute an order PDA and a + /// sell token account, plus one destination account per transfer. + AccountCountNotMatchingOrderCount = 13, + /// `BeginSettle` or `FinalizeSettle` was invoked via CPI rather than as a + /// top-level transaction instruction. + CalledViaCpi = 14, + /// A `BeginSettle` order has been cancelled by its owner and can no longer + /// be settled. + OrderCancelled = 15, + /// A `BeginSettle` order's `valid_to` lies in the past: the order has + /// expired and can no longer be settled. + OrderExpired = 16, + /// The transfer counts in `BeginSettle` don't sum to the number of transfer + /// amounts, so destinations and amounts can't be paired up exactly. + TransferCountMismatch = 17, + /// `BeginSettle`'s state account isn't the canonical settlement state PDA, + /// which must sign the pulls as the user's token delegate. + StateAccountMismatch = 18, + /// `FinalizeSettle`'s push-account count doesn't match its instruction + /// data: each push contributes a source buffer and a destination account, + /// so the count must be twice the number of push amounts. + AccountCountNotMatchingPushCount = 19, + /// `BeginSettle`: the number of pushes carried by the paired `FinalizeSettle` + /// doesn't equal the number of settled orders. Each order must be paid by + /// exactly one push. + SettledOrderPushCountMismatch = 20, + /// `BeginSettle`: a paired `FinalizeSettle` push doesn't send its proceeds + /// to the order's buy token account; its destination differs from the + /// `buy_token_account` in the order's intent. + PushDestinationMismatch = 21, + /// `BeginSettle`: a paired `FinalizeSettle` push doesn't draw funds from the + /// canonical buffer for the order's `buy_mint`. + PushSourceNotBuffer = 22, + /// `BeginSettle`: the OrderIntent `sell_token_account` holds a different + /// mint than the declared `sell_mint`. + SellMintMismatch = 23, + /// `BeginSettle`: a settled order's executed price (`amount_out/amount_in`) + /// is worse than the order's limit price (`buy_amount/sell_amount`). + LimitPriceViolated = 24, + /// `BeginSettle`: an order's pull amounts sum to more than `u64::MAX`. + PullAmountOverflow = 25, + /// `BeginSettle`: filling this order would consume more tokens than the + /// maximum the user is willing to trade on this intent. + /// Sell: `amount_in > sell_amount`; buy: `amount_out > buy_amount`. + FillExceedsOrderAmount = 26, + /// `BeginSettle`: a non-`partially_fillable` order isn't filled exactly to + /// its amount (either under- or over-filled). + /// Sell: `amount_in != sell_amount`; buy: total `amount_out != buy_amount`. + OrderNotExactlyFilled = 27, + /// `BeginSettle`: the order's cumulative `amount_withdrawn` would exceed + /// `u64::MAX` once this settlement's pulls are added. + AmountWithdrawnOverflow = 28, + /// `BeginSettle`: the order's cumulative `amount_received` would exceed + /// `u64::MAX` once this settlement's push is added. + AmountReceivedOverflow = 29, + /// `ReclaimOrder` was called on an order that has is not yet eligible for reclaim. + OrderNotReclaimable = 30, + /// `ReclaimOrder`'s `reclaim_recipient` account doesn't match the + /// `created_by` address recorded in the order. + ReclaimRecipientMismatch = 31, + /// `ReclaimBuffer`'s `reclaim_authority` account isn't a signer, or doesn't + /// match the `reclaim_authority` address recorded in the settlement state + /// PDA. + ReclaimAuthorityMismatch = 32, + /// A `ReclaimBuffer` `buffer_pda` doesn't sit at the canonical buffer PDA + /// derived from its paired `mint`. + ReclaimBufferNotCanonical = 33, + /// `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`/`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`'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 = 38, + /// A created order's intent isn't set with the `created_on_chain` flag + /// corresponding to the behavior of the invoked order creation instruction. + OrderCreatedOnChainMismatch = 39, +} + +impl From for u32 { + fn from(e: SettlementError) -> Self { + e as u32 + } +} + +impl From for solana_program_error::ProgramError { + fn from(e: SettlementError) -> Self { + Self::Custom(e.into()) + } +} diff --git a/interface/src/instruction/mod.rs b/interface/src/instruction/mod.rs index 2ff4200..27570d6 100644 --- a/interface/src/instruction/mod.rs +++ b/interface/src/instruction/mod.rs @@ -6,8 +6,6 @@ use solana_program_error::ProgramError; -use crate::{recover_discriminator, SettlementInstruction}; - pub mod add_solver; pub mod create_buffer; pub mod create_order; @@ -18,6 +16,67 @@ pub mod remove_solver; pub mod settle; pub mod transfer_authority; +#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] +#[repr(u8)] +#[num_enum(error_type( + name = ProgramError, + constructor = SettlementInstruction::unknown_discriminator, +))] +pub enum SettlementInstruction { + /// Pulls funds for a batch of orders. Must be paired in the same + /// transaction with a `FinalizeSettle` at `finalize_ix_index`. + BeginSettle = 0, + /// Validates that a `BeginSettle` at `begin_ix_index` exists and points + /// back at this instruction. Must not be called via CPI. + FinalizeSettle = 1, + /// Allocates a per-order PDA and writes the initial `OrderAccount` body. + CreateOrder = 2, + /// Creates the singleton settlement state PDA. Succeeds only once. + Initialize = 3, + /// Creates one or more per-token buffer PDAs (SPL token accounts) in a + /// single instruction. + /// + /// Each buffer_pda_i must be the canonical PDA for seeds + /// [SETTLEMENT_SEED, mint_i, "buffer"]. + CreateBuffer = 4, + /// Closes an expired order PDA and returns its rent lamports to the + /// created_by account recorded in the order body. The instruction may only + /// be executed after the order's valid_to timestamp has elapsed. + /// + /// No signature requirement: anyone may reclaim an expired order on behalf + /// of its reclaim_recipient. + ReclaimOrder = 5, + ReclaimBuffer = 6, + TransferAuthority = 7, + AddSolver = 8, + RemoveSolver = 9, +} + +impl SettlementInstruction { + pub fn discriminator(self) -> u8 { + self as u8 + } + + fn unknown_discriminator(_: u8) -> ProgramError { + ProgramError::InvalidInstructionData + } +} + +/// Recover the discriminator from the first byte of the payload and the +/// remaining bytes to parse. +/// Returns `InvalidInstructionData` for an insufficient length or an +/// unknown discriminator. +pub fn recover_discriminator( + instruction_data: &[u8], +) -> Result<(SettlementInstruction, &[u8]), ProgramError> { + let discriminator = instruction_data + .first() + .copied() + .ok_or(ProgramError::InvalidInstructionData) + .and_then(SettlementInstruction::try_from)?; + Ok((discriminator, &instruction_data[1..])) +} + /// Shared components for parsing an instruction's input (data fields and /// accounts). /// @@ -261,4 +320,50 @@ mod tests { Some(ProgramError::InvalidInstructionData), ); } + + #[test] + fn rejects_empty_payload() { + assert_eq!( + recover_discriminator(&[]), + Err(ProgramError::InvalidInstructionData), + ); + } + + #[test] + fn rejects_unknown_discriminator() { + // 42 is outside the set of valid discriminators. + assert_eq!( + recover_discriminator(&[42]), + Err(ProgramError::InvalidInstructionData), + ); + } + + #[test] + fn forwards_trailing_bytes() { + assert!(matches!( + recover_discriminator(&[ + SettlementInstruction::BeginSettle.discriminator(), + 42 // unused + ]), + Ok((SettlementInstruction::BeginSettle, [42])), + )); + } + + #[test] + fn settlement_instruction_try_from_partitions_all_bytes() { + for i in u8::MIN..=u8::MAX { + match SettlementInstruction::try_from(i) { + Ok(ix) => assert_eq!(ix as u8, i), + Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData), + } + } + } + + #[test] + fn settlement_instruction_try_from_matches_begin_settle() { + assert_eq!( + SettlementInstruction::try_from(0), + Ok(SettlementInstruction::BeginSettle) + ); + } } diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 82398ba..6682eda 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -1,272 +1,20 @@ //! Shared types and instruction builders for the CoW Protocol settlement program. pub use solana_instruction::{AccountMeta, Instruction}; -use solana_program_error::ProgramError; pub use solana_pubkey::Pubkey; solana_pubkey::declare_id!("FYp8R5K4B3B1Kfr7QuWzMz4TwoT7wptjYtxgCrY5sRXb"); pub mod data; +pub mod error; pub mod instruction; pub mod pda; +pub mod role; -#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] -#[repr(u8)] -#[num_enum(error_type( - name = ProgramError, - constructor = SettlementInstruction::unknown_discriminator, -))] -pub enum SettlementInstruction { - /// Pulls funds for a batch of orders. Must be paired in the same - /// transaction with a `FinalizeSettle` at `finalize_ix_index`. - BeginSettle = 0, - /// Validates that a `BeginSettle` at `begin_ix_index` exists and points - /// back at this instruction. Must not be called via CPI. - FinalizeSettle = 1, - /// Allocates a per-order PDA and writes the initial `OrderAccount` body. - CreateOrder = 2, - /// Creates the singleton settlement state PDA. Succeeds only once. - Initialize = 3, - /// Creates one or more per-token buffer PDAs (SPL token accounts) in a - /// single instruction. - /// - /// Each buffer_pda_i must be the canonical PDA for seeds - /// [SETTLEMENT_SEED, mint_i, "buffer"]. - CreateBuffer = 4, - /// Closes an expired order PDA and returns its rent lamports to the - /// created_by account recorded in the order body. The instruction may only - /// be executed after the order's valid_to timestamp has elapsed. - /// - /// No signature requirement: anyone may reclaim an expired order on behalf - /// of its reclaim_recipient. - ReclaimOrder = 5, - ReclaimBuffer = 6, - TransferAuthority = 7, - AddSolver = 8, - RemoveSolver = 9, -} - -impl SettlementInstruction { - pub fn discriminator(self) -> u8 { - self as u8 - } - - fn unknown_discriminator(_: u8) -> ProgramError { - ProgramError::InvalidInstructionData - } -} - -/// A transferable authority stored in the state PDA. -/// -/// The discriminant is the wire value carried by the authority-transfer -/// instruction (see [`transfer_authority`](instruction::transfer_authority)). -#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] -#[repr(u8)] -#[num_enum(error_type(name = ProgramError, constructor = Role::unknown_role))] -pub enum Role { - /// The account authorized to add and remove solvers and to transfer roles. - /// It is the highest authority: it may transfer any role. - Manager = 0, - /// The account authorized to close buffer accounts and reclaim their rent, - /// choosing where that rent goes. - ReclaimAuthority, -} - -impl Role { - /// Every [`Role`] variant, in discriminant order. - pub const ALL: [Self; 2] = [Role::Manager, Role::ReclaimAuthority]; - - /// The single wire byte that selects this role in the authority-transfer - /// instruction. - pub fn discriminator(self) -> u8 { - self as u8 - } - - fn unknown_role(_: u8) -> ProgramError { - ProgramError::InvalidInstructionData - } -} - -/// Identifies the account type a given account's data belongs to, via the -/// single discriminator byte stored at its front. Starts at 128 to keep -/// account discriminators visually distinct from instruction discriminators. -#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] -#[repr(u8)] -#[num_enum(error_type( - name = ProgramError, - constructor = SettlementAccount::unknown_discriminator, -))] -pub enum SettlementAccount { - OrderAccount = 128, - SettlementState = 129, -} - -impl SettlementAccount { - pub const fn discriminator(self) -> u8 { - self as u8 - } - - fn unknown_discriminator(_: u8) -> ProgramError { - ProgramError::InvalidAccountData - } -} - -/// Recover the discriminator from the first byte of the payload and the -/// remaining bytes to parse. -/// Returns `InvalidInstructionData` for an insufficient length or an -/// unknown discriminator. -pub fn recover_discriminator( - instruction_data: &[u8], -) -> Result<(SettlementInstruction, &[u8]), ProgramError> { - let discriminator = instruction_data - .first() - .copied() - .ok_or(ProgramError::InvalidInstructionData) - .and_then(SettlementInstruction::try_from)?; - Ok((discriminator, &instruction_data[1..])) -} - -/// Program-side errors surfaced by the settlement program. -/// The discriminant value is the on-chain `ProgramError::Custom` code. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(u32)] -pub enum SettlementError { - /// The `FinalizeSettle` included as input to `BeginSettle` isn't before - /// the actual `BeginSettle` index. - FinalizeBeforeInitialize = 0, - /// Another `BeginSettle`/`FinalizeSettle` of this program appears strictly - /// between this pair's bounds, nesting or overlapping two settlements. - BeginFinalizePairOverlap = 1, - /// The counterpart index points past the end of the transaction's - /// instruction list, so no instruction sits there. - MissingCounterpartInstruction = 2, - /// The instruction at the counterpart index belongs to a different program. - CounterpartIsExternal = 3, - /// The counterpart instruction's discriminator byte couldn't be recovered - /// from its data. - InvalidCounterpartDiscriminator = 4, - /// The counterpart instruction's own counterpart index couldn't be - /// recovered from its data. - InvalidCounterpartCounterpart = 5, - /// The counterpart's discriminator isn't the expected - /// `BeginSettle`/`FinalizeSettle` kind, or its counterpart index doesn't - /// point back at this instruction. - MismatchedCounterpartDiscriminator = 6, - /// `CreateOrder` instruction wasn't signed by the created `OrderIntent` - /// owner. - OwnerMismatch = 7, - /// An account was provided that cannot be derived from the seeds recognized by the program - AccountNotDerivable = 8, - /// `BeginSettle`'s order accounts aren't passed strictly increasing by - /// address. - OrdersNotStrictlyIncreasing = 9, - /// A `BeginSettle` sell token account doesn't match the - /// `sell_token_account` recorded in the order's intent. - SellTokenAccountMismatch = 10, - /// A `BeginSettle` sell token account isn't a valid SPL token account - /// (wrong data length or not owned by the token program). - SellTokenAccountInvalid = 11, - /// A `BeginSettle` sell token account's SPL owner isn't the order's intent - /// owner. - SellTokenOwnerMismatch = 12, - /// `BeginSettle`'s order-account count doesn't match the structure its - /// instruction data expects: `n` orders each contribute an order PDA and a - /// sell token account, plus one destination account per transfer. - AccountCountNotMatchingOrderCount = 13, - /// `BeginSettle` or `FinalizeSettle` was invoked via CPI rather than as a - /// top-level transaction instruction. - CalledViaCpi = 14, - /// A `BeginSettle` order has been cancelled by its owner and can no longer - /// be settled. - OrderCancelled = 15, - /// A `BeginSettle` order's `valid_to` lies in the past: the order has - /// expired and can no longer be settled. - OrderExpired = 16, - /// The transfer counts in `BeginSettle` don't sum to the number of transfer - /// amounts, so destinations and amounts can't be paired up exactly. - TransferCountMismatch = 17, - /// `BeginSettle`'s state account isn't the canonical settlement state PDA, - /// which must sign the pulls as the user's token delegate. - StateAccountMismatch = 18, - /// `FinalizeSettle`'s push-account count doesn't match its instruction - /// data: each push contributes a source buffer and a destination account, - /// so the count must be twice the number of push amounts. - AccountCountNotMatchingPushCount = 19, - /// `BeginSettle`: the number of pushes carried by the paired `FinalizeSettle` - /// doesn't equal the number of settled orders. Each order must be paid by - /// exactly one push. - SettledOrderPushCountMismatch = 20, - /// `BeginSettle`: a paired `FinalizeSettle` push doesn't send its proceeds - /// to the order's buy token account; its destination differs from the - /// `buy_token_account` in the order's intent. - PushDestinationMismatch = 21, - /// `BeginSettle`: a paired `FinalizeSettle` push doesn't draw funds from the - /// canonical buffer for the order's `buy_mint`. - PushSourceNotBuffer = 22, - /// `BeginSettle`: the OrderIntent `sell_token_account` holds a different - /// mint than the declared `sell_mint`. - SellMintMismatch = 23, - /// `BeginSettle`: a settled order's executed price (`amount_out/amount_in`) - /// is worse than the order's limit price (`buy_amount/sell_amount`). - LimitPriceViolated = 24, - /// `BeginSettle`: an order's pull amounts sum to more than `u64::MAX`. - PullAmountOverflow = 25, - /// `BeginSettle`: filling this order would consume more tokens than the - /// maximum the user is willing to trade on this intent. - /// Sell: `amount_in > sell_amount`; buy: `amount_out > buy_amount`. - FillExceedsOrderAmount = 26, - /// `BeginSettle`: a non-`partially_fillable` order isn't filled exactly to - /// its amount (either under- or over-filled). - /// Sell: `amount_in != sell_amount`; buy: total `amount_out != buy_amount`. - OrderNotExactlyFilled = 27, - /// `BeginSettle`: the order's cumulative `amount_withdrawn` would exceed - /// `u64::MAX` once this settlement's pulls are added. - AmountWithdrawnOverflow = 28, - /// `BeginSettle`: the order's cumulative `amount_received` would exceed - /// `u64::MAX` once this settlement's push is added. - AmountReceivedOverflow = 29, - /// `ReclaimOrder` was called on an order that has is not yet eligible for reclaim. - OrderNotReclaimable = 30, - /// `ReclaimOrder`'s `reclaim_recipient` account doesn't match the - /// `created_by` address recorded in the order. - ReclaimRecipientMismatch = 31, - /// `ReclaimBuffer`'s `reclaim_authority` account isn't a signer, or doesn't - /// match the `reclaim_authority` address recorded in the settlement state - /// PDA. - ReclaimAuthorityMismatch = 32, - /// A `ReclaimBuffer` `buffer_pda` doesn't sit at the canonical buffer PDA - /// derived from its paired `mint`. - ReclaimBufferNotCanonical = 33, - /// `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`/`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`'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 = 38, - /// A created order's intent isn't set with the `created_on_chain` flag - /// corresponding to the behavior of the invoked order creation instruction. - OrderCreatedOnChainMismatch = 39, -} - -impl From for u32 { - fn from(e: SettlementError) -> Self { - e as u32 - } -} - -impl From for solana_program_error::ProgramError { - fn from(e: SettlementError) -> Self { - Self::Custom(e.into()) - } -} +pub use error::SettlementError; +pub use instruction::{recover_discriminator, SettlementInstruction}; +pub use pda::SettlementAccount; +pub use role::Role; /// Test fixtures for building settlement values with stable, readable /// addresses. Exposed under the `test-fixtures` feature (and unconditionally @@ -288,99 +36,3 @@ pub mod fixtures { /// declared on-chain id. pub static PROGRAM_ID: LazyLock = LazyLock::new(|| pubkey_from_seed("program id")); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rejects_empty_payload() { - assert_eq!( - recover_discriminator(&[]), - Err(ProgramError::InvalidInstructionData), - ); - } - - #[test] - fn rejects_unknown_discriminator() { - // 42 is outside the set of valid discriminators. - assert_eq!( - recover_discriminator(&[42]), - Err(ProgramError::InvalidInstructionData), - ); - } - - #[test] - fn forwards_trailing_bytes() { - assert!(matches!( - recover_discriminator(&[ - SettlementInstruction::BeginSettle.discriminator(), - 42 // unused - ]), - Ok((SettlementInstruction::BeginSettle, [42])), - )); - } - - #[test] - fn settlement_instruction_try_from_partitions_all_bytes() { - for i in u8::MIN..=u8::MAX { - match SettlementInstruction::try_from(i) { - Ok(ix) => assert_eq!(ix as u8, i), - Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData), - } - } - } - - #[test] - fn settlement_instruction_try_from_matches_begin_settle() { - assert_eq!( - SettlementInstruction::try_from(0), - Ok(SettlementInstruction::BeginSettle) - ); - } - - #[test] - fn settlement_account_try_from_partitions_all_bytes() { - for i in u8::MIN..=u8::MAX { - match SettlementAccount::try_from(i) { - Ok(account) => assert_eq!(account as u8, i), - Err(err) => assert_eq!(err, ProgramError::InvalidAccountData), - } - } - } - - #[test] - fn settlement_account_discriminators_are_distinct() { - assert_ne!( - SettlementAccount::OrderAccount.discriminator(), - SettlementAccount::SettlementState.discriminator(), - ); - } - - #[test] - fn role_try_from_partitions_all_bytes() { - for i in u8::MIN..=u8::MAX { - match Role::try_from(i) { - Ok(role) => assert_eq!(role as u8, i), - Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData), - } - } - } - - #[test] - fn role_try_from_matches_manager() { - assert_eq!(Role::try_from(0), Ok(Role::Manager)); - } - - #[test] - fn all_roles_lists_every_role_in_discriminator_order() { - // The roles `try_from` accepts, discovered independently of `Role::ALL`. - // The scan runs over ascending bytes, so this is every role that exists, - // in discriminant order. - let every_role: Vec = (u8::MIN..=u8::MAX) - .filter_map(|byte| Role::try_from(byte).ok()) - .collect(); - - assert_eq!(Role::ALL.as_slice(), every_role.as_slice()); - } -} diff --git a/interface/src/pda/mod.rs b/interface/src/pda/mod.rs index c146138..8878e62 100644 --- a/interface/src/pda/mod.rs +++ b/interface/src/pda/mod.rs @@ -5,6 +5,7 @@ //! derivation helper for one kind of PDA. use solana_address::Address; +use solana_program_error::ProgramError; pub mod buffer; pub mod order; @@ -69,6 +70,30 @@ pub fn is_pda_with_signer_seeds( .is_ok_and(|derived| account == &derived) } +/// Identifies the account type a given account's data belongs to, via the +/// single discriminator byte stored at its front. Starts at 128 to keep +/// account discriminators visually distinct from instruction discriminators. +#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] +#[repr(u8)] +#[num_enum(error_type( + name = ProgramError, + constructor = SettlementAccount::unknown_discriminator, +))] +pub enum SettlementAccount { + OrderAccount = 128, + SettlementState = 129, +} + +impl SettlementAccount { + pub const fn discriminator(self) -> u8 { + self as u8 + } + + fn unknown_discriminator(_: u8) -> ProgramError { + ProgramError::InvalidAccountData + } +} + #[cfg(test)] mod tests { use std::collections::HashSet; @@ -76,9 +101,10 @@ mod tests { use solana_pubkey::Pubkey; use super::{ - build_padded_settlement_seed, SETTLEMENT_SEED, SETTLEMENT_SEED_LEN, + build_padded_settlement_seed, SettlementAccount, SETTLEMENT_SEED, SETTLEMENT_SEED_LEN, SETTLEMENT_SEED_VERSION_LEN, }; + use solana_program_error::ProgramError; pub(crate) const SAMPLE_VERSIONS: &[&str] = &[ "0.0", "0.1", "0.2", "0.10", "0.11", "0.12", "0.13", "0.14", "0.15", "0.16", "0.17", @@ -198,4 +224,22 @@ mod tests { assert!(seen_pdas.insert(other_pda)); } } + + #[test] + fn settlement_account_try_from_partitions_all_bytes() { + for i in u8::MIN..=u8::MAX { + match SettlementAccount::try_from(i) { + Ok(account) => assert_eq!(account as u8, i), + Err(err) => assert_eq!(err, ProgramError::InvalidAccountData), + } + } + } + + #[test] + fn settlement_account_discriminators_are_distinct() { + assert_ne!( + SettlementAccount::OrderAccount.discriminator(), + SettlementAccount::SettlementState.discriminator(), + ); + } } diff --git a/interface/src/role.rs b/interface/src/role.rs new file mode 100644 index 0000000..5bb1382 --- /dev/null +++ b/interface/src/role.rs @@ -0,0 +1,66 @@ +//! Transferable authority roles stored in the settlement state PDA. + +use solana_program_error::ProgramError; + +/// A transferable authority stored in the state PDA. +/// +/// The discriminant is the wire value carried by the authority-transfer +/// instruction (see [`transfer_authority`](crate::instruction::transfer_authority)). +#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] +#[repr(u8)] +#[num_enum(error_type(name = ProgramError, constructor = Role::unknown_role))] +pub enum Role { + /// The account authorized to add and remove solvers and to transfer roles. + /// It is the highest authority: it may transfer any role. + Manager = 0, + /// The account authorized to close buffer accounts and reclaim their rent, + /// choosing where that rent goes. + ReclaimAuthority, +} + +impl Role { + /// Every [`Role`] variant, in discriminant order. + pub const ALL: [Self; 2] = [Role::Manager, Role::ReclaimAuthority]; + + /// The single wire byte that selects this role in the authority-transfer + /// instruction. + pub fn discriminator(self) -> u8 { + self as u8 + } + + fn unknown_role(_: u8) -> ProgramError { + ProgramError::InvalidInstructionData + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn role_try_from_partitions_all_bytes() { + for i in u8::MIN..=u8::MAX { + match Role::try_from(i) { + Ok(role) => assert_eq!(role as u8, i), + Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData), + } + } + } + + #[test] + fn role_try_from_matches_manager() { + assert_eq!(Role::try_from(0), Ok(Role::Manager)); + } + + #[test] + fn all_roles_lists_every_role_in_discriminator_order() { + // The roles `try_from` accepts, discovered independently of `Role::ALL`. + // The scan runs over ascending bytes, so this is every role that exists, + // in discriminant order. + let every_role: Vec = (u8::MIN..=u8::MAX) + .filter_map(|byte| Role::try_from(byte).ok()) + .collect(); + + assert_eq!(Role::ALL.as_slice(), every_role.as_slice()); + } +} diff --git a/programs/settlement/src/lib.rs b/programs/settlement/src/lib.rs index 1fc2f41..f940bdc 100644 --- a/programs/settlement/src/lib.rs +++ b/programs/settlement/src/lib.rs @@ -1,67 +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; -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 create_buffer::process_create_buffer; -use create_order::process_create_order; -use initialize::process_initialize; -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; +use pinocchio::entrypoint; +pub use processor::process_instruction; entrypoint!(process_instruction); - -pub fn process_instruction( - program_id: &Address, - accounts: &mut [AccountView], - instruction_data: &[u8], -) -> ProgramResult { - let (discriminator, _) = recover_discriminator(instruction_data)?; - match discriminator { - SettlementInstruction::BeginSettle => { - process_begin_settle(program_id, accounts, instruction_data) - } - SettlementInstruction::FinalizeSettle => { - process_finalize_settle(program_id, accounts, instruction_data) - } - SettlementInstruction::CreateOrder => { - process_create_order(program_id, accounts, instruction_data) - } - SettlementInstruction::Initialize => { - process_initialize(program_id, accounts, instruction_data) - } - SettlementInstruction::CreateBuffer => { - process_create_buffer(program_id, accounts, instruction_data) - } - SettlementInstruction::ReclaimOrder => { - process_reclaim_order(program_id, accounts, instruction_data) - } - SettlementInstruction::ReclaimBuffer => { - process_reclaim_buffer(program_id, accounts, instruction_data) - } - SettlementInstruction::TransferAuthority => { - process_transfer_authority(program_id, accounts, instruction_data) - } - 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/add_solver.rs b/programs/settlement/src/processor/add_solver.rs similarity index 98% rename from programs/settlement/src/add_solver.rs rename to programs/settlement/src/processor/add_solver.rs index 4491245..4f776be 100644 --- a/programs/settlement/src/add_solver.rs +++ b/programs/settlement/src/processor/add_solver.rs @@ -16,7 +16,7 @@ use pinocchio::{ }; use pinocchio_system::instructions::Transfer; -use crate::processor::check_state_pda; +use crate::processor::utils::auth::check_state_pda; pub fn process_add_solver( program_id: &Address, diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/processor/begin_settle.rs similarity index 99% rename from programs/settlement/src/settle/begin.rs rename to programs/settlement/src/processor/begin_settle.rs index 4100403..4ab7ddd 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/processor/begin_settle.rs @@ -29,12 +29,13 @@ use pinocchio::{ }; use pinocchio_token::{instructions::Transfer, state::Account as TokenAccount}; -use crate::processor::{ - check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump, +use crate::processor::utils::{ + auth::{check_state_pda, require_solver, with_state_pda_signer_from_bump}, + cpi::is_cpi_call, + settle::validate_counterpart, + token::validate_token_program_account, }; -use super::{validate_counterpart, validate_token_program_account}; - pub fn process_begin_settle( program_id: &Address, accounts: &mut [AccountView], diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/processor/create_buffer.rs similarity index 93% rename from programs/settlement/src/create_buffer.rs rename to programs/settlement/src/processor/create_buffer.rs index 2dab7e0..761a38b 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/processor/create_buffer.rs @@ -7,10 +7,10 @@ use cow_settlement_interface::{ }, pda::{buffer::buffer_pda_seeds, state::state_pda_seeds}, }; -use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; +use pinocchio::{AccountView, Address, ProgramResult}; use pinocchio_token::{instructions::InitializeAccount3, state::Account as TokenAccount}; -use crate::processor::CanonicalPda; +use crate::processor::utils::{pda::CanonicalPda, token::validate_token_program_account}; pub fn process_create_buffer( program_id: &Address, @@ -22,9 +22,7 @@ pub fn process_create_buffer( // Only the legacy SPL Token program is supported. The InitializeAccount3 // CPI targets that program unconditionally; reject a mismatching account // up front so the caller gets a clear error. - if input.token_program.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } + validate_token_program_account(input.token_program)?; // The buffers' token authority is the settlement state PDA, the single // authority over every buffer. Derive it once for all buffers. @@ -69,6 +67,7 @@ mod tests { create_buffer_data, NUM_SHARED_ACCOUNTS, }; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; + use pinocchio::error::ProgramError; #[test] fn process_create_buffer_propagates_error() { diff --git a/programs/settlement/src/create_order.rs b/programs/settlement/src/processor/create_order.rs similarity index 99% rename from programs/settlement/src/create_order.rs rename to programs/settlement/src/processor/create_order.rs index 6d08fd8..5f477b0 100644 --- a/programs/settlement/src/create_order.rs +++ b/programs/settlement/src/processor/create_order.rs @@ -11,7 +11,7 @@ use cow_settlement_interface::{ }; use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; -use crate::processor::CanonicalPda; +use crate::processor::utils::pda::CanonicalPda; pub fn process_create_order( program_id: &Address, diff --git a/programs/settlement/src/settle/finalize.rs b/programs/settlement/src/processor/finalize_settle.rs similarity index 94% rename from programs/settlement/src/settle/finalize.rs rename to programs/settlement/src/processor/finalize_settle.rs index df190ee..8e9ea94 100644 --- a/programs/settlement/src/settle/finalize.rs +++ b/programs/settlement/src/processor/finalize_settle.rs @@ -12,9 +12,10 @@ use pinocchio::{ }; use pinocchio_token::instructions::Transfer; -use crate::processor::{is_cpi_call, with_state_pda_signer}; - -use super::{validate_counterpart, validate_token_program_account}; +use crate::processor::utils::{ + auth::with_state_pda_signer, cpi::is_cpi_call, settle::validate_counterpart, + token::validate_token_program_account, +}; pub fn process_finalize_settle( program_id: &Address, diff --git a/programs/settlement/src/initialize.rs b/programs/settlement/src/processor/initialize.rs similarity index 97% rename from programs/settlement/src/initialize.rs rename to programs/settlement/src/processor/initialize.rs index 6920dea..6f40a1c 100644 --- a/programs/settlement/src/initialize.rs +++ b/programs/settlement/src/processor/initialize.rs @@ -7,7 +7,7 @@ use cow_settlement_interface::{ }; use pinocchio::{AccountView, Address, ProgramResult}; -use crate::processor::CanonicalPda; +use crate::processor::utils::pda::CanonicalPda; pub fn process_initialize( program_id: &Address, diff --git a/programs/settlement/src/processor/mod.rs b/programs/settlement/src/processor/mod.rs new file mode 100644 index 0000000..4dc9216 --- /dev/null +++ b/programs/settlement/src/processor/mod.rs @@ -0,0 +1,67 @@ +//! Instruction dispatch for the settlement program. + +mod add_solver; +mod begin_settle; +mod create_buffer; +mod create_order; +mod finalize_settle; +mod initialize; +mod reclaim_buffer; +mod reclaim_order; +mod remove_solver; +mod transfer_authority; +pub mod utils; + +use add_solver::process_add_solver; +use begin_settle::process_begin_settle; +use create_buffer::process_create_buffer; +use create_order::process_create_order; +use finalize_settle::process_finalize_settle; +use initialize::process_initialize; +use reclaim_buffer::process_reclaim_buffer; +use reclaim_order::process_reclaim_order; +use remove_solver::process_remove_solver; +use transfer_authority::process_transfer_authority; + +use cow_settlement_interface::{recover_discriminator, SettlementInstruction}; +use pinocchio::{AccountView, Address, ProgramResult}; + +pub fn process_instruction( + program_id: &Address, + accounts: &mut [AccountView], + instruction_data: &[u8], +) -> ProgramResult { + let (discriminator, _) = recover_discriminator(instruction_data)?; + match discriminator { + SettlementInstruction::BeginSettle => { + process_begin_settle(program_id, accounts, instruction_data) + } + SettlementInstruction::FinalizeSettle => { + process_finalize_settle(program_id, accounts, instruction_data) + } + SettlementInstruction::CreateOrder => { + process_create_order(program_id, accounts, instruction_data) + } + SettlementInstruction::Initialize => { + process_initialize(program_id, accounts, instruction_data) + } + SettlementInstruction::CreateBuffer => { + process_create_buffer(program_id, accounts, instruction_data) + } + SettlementInstruction::ReclaimOrder => { + process_reclaim_order(program_id, accounts, instruction_data) + } + SettlementInstruction::ReclaimBuffer => { + process_reclaim_buffer(program_id, accounts, instruction_data) + } + SettlementInstruction::TransferAuthority => { + process_transfer_authority(program_id, accounts, instruction_data) + } + 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/reclaim_buffer.rs b/programs/settlement/src/processor/reclaim_buffer.rs similarity index 96% rename from programs/settlement/src/reclaim_buffer.rs rename to programs/settlement/src/processor/reclaim_buffer.rs index 6a1b045..49826fe 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/processor/reclaim_buffer.rs @@ -7,17 +7,14 @@ use cow_settlement_interface::{ data::state::StateAccount, - instruction::{ - create_buffer::SPL_TOKEN_PROGRAM_ID, reclaim_buffer::ReclaimBufferInput, - InstructionInputParsing, - }, + instruction::{reclaim_buffer::ReclaimBufferInput, InstructionInputParsing}, pda::buffer::find_buffer_pda, Pubkey, Role, SettlementError, }; use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; use pinocchio_token::{instructions::CloseAccount, state::Account as TokenAccount}; -use crate::processor::with_state_pda_signer; +use crate::processor::utils::{auth::with_state_pda_signer, token::validate_token_program_account}; pub fn process_reclaim_buffer( program_id: &Address, @@ -32,9 +29,7 @@ pub fn process_reclaim_buffer( buffers, } = ReclaimBufferInput::parse(instruction_data, accounts)?; - if token_program.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } + validate_token_program_account(token_program)?; with_state_pda_signer(program_id, state_pda, |state_signer| { let reclaim_authority_pubkey: Pubkey = @@ -75,6 +70,7 @@ 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::create_buffer::SPL_TOKEN_PROGRAM_ID; use cow_settlement_interface::instruction::fixtures::{ fake_account, fake_account_owned_by, fake_account_with_data, fake_sequential_accounts, fake_signer, diff --git a/programs/settlement/src/reclaim_order.rs b/programs/settlement/src/processor/reclaim_order.rs similarity index 100% rename from programs/settlement/src/reclaim_order.rs rename to programs/settlement/src/processor/reclaim_order.rs diff --git a/programs/settlement/src/remove_solver.rs b/programs/settlement/src/processor/remove_solver.rs similarity index 98% rename from programs/settlement/src/remove_solver.rs rename to programs/settlement/src/processor/remove_solver.rs index f5d795c..3e7f77a 100644 --- a/programs/settlement/src/remove_solver.rs +++ b/programs/settlement/src/processor/remove_solver.rs @@ -17,7 +17,7 @@ use pinocchio::{ AccountView, Address, ProgramResult, Resize, }; -use crate::processor::{check_state_pda, move_lamports}; +use crate::processor::utils::{auth::check_state_pda, lamports::move_lamports}; pub fn process_remove_solver( program_id: &Address, diff --git a/programs/settlement/src/transfer_authority.rs b/programs/settlement/src/processor/transfer_authority.rs similarity index 98% rename from programs/settlement/src/transfer_authority.rs rename to programs/settlement/src/processor/transfer_authority.rs index 72e2666..e1f2fd8 100644 --- a/programs/settlement/src/transfer_authority.rs +++ b/programs/settlement/src/processor/transfer_authority.rs @@ -10,7 +10,7 @@ use cow_settlement_interface::{ }; use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; -use crate::processor::check_state_pda; +use crate::processor::utils::auth::check_state_pda; pub fn process_transfer_authority( program_id: &Address, diff --git a/programs/settlement/src/processor/utils/auth.rs b/programs/settlement/src/processor/utils/auth.rs new file mode 100644 index 0000000..12dec7e --- /dev/null +++ b/programs/settlement/src/processor/utils/auth.rs @@ -0,0 +1,74 @@ +//! State-PDA validation, signing, and solver authentication shared across +//! instruction handlers. + +use pinocchio::{ + cpi::{Seed, Signer}, + error::ProgramError, + AccountView, Address, ProgramResult, +}; + +use cow_settlement_interface::{ + data::state::StateAccount, + pda::state::{state_pda_seeds, state_pda_signer_seeds}, + SettlementError, +}; + +/// Confirm `state_pda_account` sits at the canonical state PDA for `program_id`, +/// returning its canonical bump. +#[must_use = "ignoring the result skips the canonical state-PDA check"] +pub fn check_state_pda( + program_id: &Address, + state_pda_account: &AccountView, +) -> Result { + let (state_pda, state_bump) = Address::find_program_address(&state_pda_seeds(), program_id); + if state_pda_account.address() != &state_pda { + return Err(SettlementError::StateAccountMismatch.into()); + } + Ok(state_bump) +} + +/// Run `f` with a signer for the state PDA, given its already-derived canonical +/// `state_bump`. +/// +/// This function is to be used as an alternative for [`with_state_pda_signer`] +/// in the case where the state PDA has been checked in an earlier call. +/// The caller is responsible for having validated the bump against the state +/// PDA, via [`check_state_pda`]. +/// +/// If state PDA validation is needed, use [`with_state_pda_signer`]. +pub fn with_state_pda_signer_from_bump( + state_bump: u8, + f: impl FnOnce(&Signer) -> ProgramResult, +) -> ProgramResult { + let state_bump = [state_bump]; + let signer_seeds = state_pda_signer_seeds(&state_bump).map(Seed::from); + f(&Signer::from(&signer_seeds)) +} +/// Validate that `state_pda_account` is the canonical state PDA and run `f` +/// with a signer for it, in one step. Use [`with_state_pda_signer_from_bump`] +/// directly when the bump has already been derived (as settling does, via +/// [`check_state_pda`]) to avoid re-deriving the PDA. +pub fn with_state_pda_signer( + program_id: &Address, + state_pda_account: &AccountView, + f: impl FnOnce(&Signer) -> ProgramResult, +) -> ProgramResult { + with_state_pda_signer_from_bump(check_state_pda(program_id, state_pda_account)?, f) +} + +/// Confirm that `solver_account` signed the transaction and is in the solver +/// list held by `state_pda_account`. +/// +/// Confirming the state account sits at the canonical state PDA (and deriving +/// its bump for the signer) is left to the caller, via [`check_state_pda`]. +#[must_use = "ignoring the result skips solver authentication"] +pub fn require_solver( + state_pda_account: &AccountView, + solver_account: &AccountView, +) -> ProgramResult { + let state = StateAccount::attach(state_pda_account.try_borrow()?)?; + if !solver_account.is_signer() || !state.is_solver(solver_account.address()) { + return Err(SettlementError::UnauthorizedSolver.into()); + } + Ok(()) +} diff --git a/programs/settlement/src/processor/utils/cpi.rs b/programs/settlement/src/processor/utils/cpi.rs new file mode 100644 index 0000000..af88f3c --- /dev/null +++ b/programs/settlement/src/processor/utils/cpi.rs @@ -0,0 +1,17 @@ +//! Detection of cross-program invocation, shared across instruction handlers. + +use solana_instruction::{syscalls::get_stack_height, TRANSACTION_LEVEL_STACK_HEIGHT}; + +pub fn is_cpi_call() -> bool { + get_stack_height() > TRANSACTION_LEVEL_STACK_HEIGHT +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_cpi_false_outside_solana_lib() { + assert!(!is_cpi_call()); + } +} diff --git a/programs/settlement/src/processor/utils/lamports.rs b/programs/settlement/src/processor/utils/lamports.rs new file mode 100644 index 0000000..8aa6164 --- /dev/null +++ b/programs/settlement/src/processor/utils/lamports.rs @@ -0,0 +1,23 @@ +//! Direct lamport movement shared across instruction handlers. + +use pinocchio::{error::ProgramError, AccountView, ProgramResult}; + +/// 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(()) +} diff --git a/programs/settlement/src/processor/utils/mod.rs b/programs/settlement/src/processor/utils/mod.rs new file mode 100644 index 0000000..1737f5b --- /dev/null +++ b/programs/settlement/src/processor/utils/mod.rs @@ -0,0 +1,8 @@ +//! Plumbing shared across the settlement program's instruction handlers. + +pub mod auth; +pub mod cpi; +pub mod lamports; +pub mod pda; +pub mod settle; +pub mod token; diff --git a/programs/settlement/src/processor.rs b/programs/settlement/src/processor/utils/pda.rs similarity index 57% rename from programs/settlement/src/processor.rs rename to programs/settlement/src/processor/utils/pda.rs index 0872c62..fe29afe 100644 --- a/programs/settlement/src/processor.rs +++ b/programs/settlement/src/processor/utils/pda.rs @@ -1,21 +1,14 @@ -//! Shared program plumbing: canonical PDA creation. +//! Canonical PDA creation shared across instruction handlers. use pinocchio::{ address::MAX_SEEDS, cpi::{Seed, Signer}, error::ProgramError, - AccountView, Address, ProgramResult, + AccountView, Address, }; use pinocchio_system::instructions::CreateAccountAllowPrefund; -use cow_settlement_interface::{ - data::state::StateAccount, - pda::state::{state_pda_seeds, state_pda_signer_seeds}, - SettlementError, -}; -use solana_instruction::{syscalls::get_stack_height, TRANSACTION_LEVEL_STACK_HEIGHT}; - /// Description of a canonical PDA to create: the account at `pda`, assigned to /// `owner` and funded by `payer`. /// @@ -111,97 +104,3 @@ impl CanonicalPda<'_, N> { } } } - -/// Confirm `state_pda_account` sits at the canonical state PDA for `program_id`, -/// returning its canonical bump. -#[must_use = "ignoring the result skips the canonical state-PDA check"] -pub fn check_state_pda( - program_id: &Address, - state_pda_account: &AccountView, -) -> Result { - let (state_pda, state_bump) = Address::find_program_address(&state_pda_seeds(), program_id); - if state_pda_account.address() != &state_pda { - return Err(SettlementError::StateAccountMismatch.into()); - } - Ok(state_bump) -} - -/// Run `f` with a signer for the state PDA, given its already-derived canonical -/// `state_bump`. -/// -/// This function is to be used as an alternative for [`with_state_pda_signer`] -/// in the case where the state PDA has been checked in an earlier call. -/// The caller is responsible for having validated the bump against the state -/// PDA, via [`check_state_pda`]. -/// -/// If state PDA validation is needed, use [`with_state_pda_signer`]. -pub fn with_state_pda_signer_from_bump( - state_bump: u8, - f: impl FnOnce(&Signer) -> ProgramResult, -) -> ProgramResult { - let state_bump = [state_bump]; - let signer_seeds = state_pda_signer_seeds(&state_bump).map(Seed::from); - f(&Signer::from(&signer_seeds)) -} -/// Validate that `state_pda_account` is the canonical state PDA and run `f` -/// with a signer for it, in one step. Use [`with_state_pda_signer_from_bump`] -/// directly when the bump has already been derived (as settling does, via -/// [`check_state_pda`]) to avoid re-deriving the PDA. -pub fn with_state_pda_signer( - program_id: &Address, - state_pda_account: &AccountView, - f: impl FnOnce(&Signer) -> ProgramResult, -) -> ProgramResult { - with_state_pda_signer_from_bump(check_state_pda(program_id, state_pda_account)?, f) -} - -/// Confirm that `solver_account` signed the transaction and is in the solver -/// list held by `state_pda_account`. -/// -/// Confirming the state account sits at the canonical state PDA (and deriving -/// its bump for the signer) is left to the caller, via [`check_state_pda`]. -#[must_use = "ignoring the result skips solver authentication"] -pub fn require_solver( - state_pda_account: &AccountView, - solver_account: &AccountView, -) -> ProgramResult { - let state = StateAccount::attach(state_pda_account.try_borrow()?)?; - if !solver_account.is_signer() || !state.is_solver(solver_account.address()) { - return Err(SettlementError::UnauthorizedSolver.into()); - } - Ok(()) -} - -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::*; - - #[test] - fn is_cpi_false_outside_solana_lib() { - assert!(!is_cpi_call()); - } -} diff --git a/programs/settlement/src/settle/mod.rs b/programs/settlement/src/processor/utils/settle.rs similarity index 60% rename from programs/settlement/src/settle/mod.rs rename to programs/settlement/src/processor/utils/settle.rs index 25a01d7..4908691 100644 --- a/programs/settlement/src/settle/mod.rs +++ b/programs/settlement/src/processor/utils/settle.rs @@ -1,27 +1,20 @@ -//! `BeginSettle`/`FinalizeSettle` instruction handlers. +//! Counterpart validation shared across the `BeginSettle`/`FinalizeSettle` +//! handlers. use std::ops::Deref; use cow_settlement_interface::{ - instruction::{create_buffer::SPL_TOKEN_PROGRAM_ID, settle::recover_counterpart}, - recover_discriminator, SettlementError, SettlementInstruction, + instruction::settle::recover_counterpart, recover_discriminator, SettlementError, + SettlementInstruction, }; -use pinocchio::{ - error::ProgramError, sysvars::instructions::Instructions, AccountView, Address, ProgramResult, -}; - -mod begin; -mod finalize; - -pub use begin::process_begin_settle; -pub use finalize::process_finalize_settle; +use pinocchio::{sysvars::instructions::Instructions, Address, ProgramResult}; /// Load the counterpart instruction at `counterpart_index` and verify it /// belongs to `program_id`, carries `expected_discriminator`, and points /// back at the current instruction. Ordering (before/after) is the caller's /// responsibility. #[must_use = "ignoring the output may lead to an unintended on-chain state"] -fn validate_counterpart>( +pub fn validate_counterpart>( program_id: &Address, instructions: &Instructions, current_index: u16, @@ -44,13 +37,3 @@ fn validate_counterpart>( } Ok(()) } - -/// Validate that `token_program_account` is the legacy SPL Token program, which -/// every settlement transfer is issued against. -#[must_use = "ignoring the output may lead to an unintended on-chain state"] -fn validate_token_program_account(token_program_account: &AccountView) -> ProgramResult { - if token_program_account.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} diff --git a/programs/settlement/src/processor/utils/token.rs b/programs/settlement/src/processor/utils/token.rs new file mode 100644 index 0000000..e79e856 --- /dev/null +++ b/programs/settlement/src/processor/utils/token.rs @@ -0,0 +1,14 @@ +//! SPL Token program validation shared across instruction handlers. + +use cow_settlement_interface::instruction::create_buffer::SPL_TOKEN_PROGRAM_ID; +use pinocchio::{error::ProgramError, AccountView, ProgramResult}; + +/// Validate that `token_program_account` is the legacy SPL Token program, the +/// only token program the settlement program issues CPIs against. +#[must_use = "ignoring the output may lead to an unintended on-chain state"] +pub fn validate_token_program_account(token_program_account: &AccountView) -> ProgramResult { + if token_program_account.address() != &SPL_TOKEN_PROGRAM_ID { + return Err(ProgramError::IncorrectProgramId); + } + Ok(()) +} diff --git a/programs/settlement/tests/add_solvers.rs b/programs/settlement/tests/add_solvers.rs index d00bf79..d5a8d85 100644 --- a/programs/settlement/tests/add_solvers.rs +++ b/programs/settlement/tests/add_solvers.rs @@ -6,7 +6,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::state::{WIDTH_HEADER, WIDTH_PUBKEY}, Instruction, SettlementError, }; -use cow_settlement_client::instructions::AddSolver; +use cow_settlement_client::instruction::AddSolver; use litesvm::LiteSVM; use solana_sdk::{ instruction::InstructionError, diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 3a2965f..3372d1c 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -35,7 +35,7 @@ use cow_settlement_client::cow_settlement_interface::{ pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, Instruction, SettlementError, SettlementInstruction, }; -use cow_settlement_client::instructions::{ +use cow_settlement_client::instruction::{ BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, }; use cow_settlement_interface::data::intent::OrderIntent; diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index 2dddd41..5dd6d75 100644 --- a/programs/settlement/tests/common/buffer.rs +++ b/programs/settlement/tests/common/buffer.rs @@ -2,7 +2,7 @@ use cow_settlement_client::cow_settlement_interface::pda::buffer::find_buffer_pda; use cow_settlement_client::cow_settlement_interface::Instruction; -use cow_settlement_client::instructions::CreateBuffers; +use cow_settlement_client::instruction::CreateBuffers; use litesvm::LiteSVM; use solana_sdk::{ pubkey::Pubkey, diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index cb54dcc..d5fccf0 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -14,7 +14,7 @@ pub mod settlement; pub mod state; pub mod token; -use cow_settlement_client::instructions::{AddSolver, Initialize}; +use cow_settlement_client::instruction::{AddSolver, Initialize}; use cow_settlement_interface::pda::state::find_state_pda; use cow_settlement_interface::Instruction; use cow_settlement_interface::SettlementError; diff --git a/programs/settlement/tests/common/order.rs b/programs/settlement/tests/common/order.rs index dc0f831..0fc805e 100644 --- a/programs/settlement/tests/common/order.rs +++ b/programs/settlement/tests/common/order.rs @@ -3,7 +3,7 @@ use cow_settlement_client::cow_settlement_interface::data::intent::{ Flags, OrderIntent, OrderKind, }; -use cow_settlement_client::instructions::CreateOrder; +use cow_settlement_client::instruction::CreateOrder; use litesvm::LiteSVM; use solana_sdk::{ pubkey::Pubkey, diff --git a/programs/settlement/tests/common/settlement.rs b/programs/settlement/tests/common/settlement.rs index c3065bf..c9cdafb 100644 --- a/programs/settlement/tests/common/settlement.rs +++ b/programs/settlement/tests/common/settlement.rs @@ -1,6 +1,6 @@ //! Scaffolding for building `[BeginSettle, FinalizeSettle]` settlement pairs. -use cow_settlement_client::instructions::{ +use cow_settlement_client::instruction::{ BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, }; use cow_settlement_interface::{data::intent::OrderIntent, Instruction}; diff --git a/programs/settlement/tests/common/state.rs b/programs/settlement/tests/common/state.rs index 0f0709c..e19f7d4 100644 --- a/programs/settlement/tests/common/state.rs +++ b/programs/settlement/tests/common/state.rs @@ -1,5 +1,5 @@ use cow_settlement_client::cow_settlement_interface::data::state::StateAccount; -use cow_settlement_client::instructions::Initialize; +use cow_settlement_client::instruction::Initialize; use litesvm::LiteSVM; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Keypair; diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index f0aee17..3a41934 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -5,7 +5,7 @@ use cow_settlement_client::cow_settlement_interface::{ state::find_state_pda, }, }; -use cow_settlement_client::instructions::CreateBuffers; +use cow_settlement_client::instruction::CreateBuffers; use litesvm::LiteSVM; use litesvm_token::{ get_spl_account, diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index daa291f..c4c870f 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -21,7 +21,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::intent::OrderIntent, instruction::settle::SPL_TOKEN_PROGRAM_ID, pda::state::find_state_pda, Instruction, SettlementError, }; -use cow_settlement_client::instructions::{FinalizeSettle, FinalizedIntent}; +use cow_settlement_client::instruction::{FinalizeSettle, FinalizedIntent}; use litesvm_token::spl_token::error::TokenError; use solana_sdk::{ instruction::InstructionError, program_error::ProgramError, pubkey::Pubkey, signer::Signer, diff --git a/programs/settlement/tests/idl/generate.rs b/programs/settlement/tests/idl/generate.rs index 0e4cb71..76b0857 100644 --- a/programs/settlement/tests/idl/generate.rs +++ b/programs/settlement/tests/idl/generate.rs @@ -127,7 +127,7 @@ const STRUCT_TYPES: &[(&Source, &str, &str)] = &[ /// The enum types the IDL defines, as `(source, name)`. pub const ENUM_TYPES: &[(&Source, &str)] = &[ (&parse_rust::INTENT_RS, "OrderKind"), - (&parse_rust::INTERFACE_LIB_RS, "Role"), + (&parse_rust::ROLE_RS, "Role"), ]; /// The enum whose variants are the IDL's `errors[]`. @@ -168,7 +168,7 @@ pub fn partial_idl() -> Value { /// One entry per `SettlementInstruction` variant, in discriminator order. fn instructions() -> Vec { - discriminator_variants(&parse_rust::INTERFACE_LIB_RS.find_enum("SettlementInstruction")) + discriminator_variants(&parse_rust::INSTRUCTION_MOD_RS.find_enum("SettlementInstruction")) .map(|(byte, variant)| { let instruction = INSTRUCTIONS .iter() @@ -278,7 +278,7 @@ fn struct_type(rust_struct: &syn::ItemStruct, rust_name: &str) -> Value { /// One entry per `SettlementAccount` variant, in discriminator order. fn accounts() -> Vec { - discriminator_variants(&parse_rust::INTERFACE_LIB_RS.find_enum("SettlementAccount")) + discriminator_variants(&parse_rust::PDA_MOD_RS.find_enum("SettlementAccount")) .map(|(byte, variant)| { let mut entry = Map::new(); entry.insert("name".into(), json!(variant.ident.to_string())); @@ -322,7 +322,7 @@ fn type_entry(idl_name: &str, docs: Vec, ty: Value) -> Value { /// `ProgramError::Custom` code the program returns, and its doc comment is the /// message the IDL publishes for that code. fn errors() -> Vec { - parse_rust::INTERFACE_LIB_RS + parse_rust::ERROR_RS .find_enum(ERRORS) .variants .iter() diff --git a/programs/settlement/tests/idl/parse_rust.rs b/programs/settlement/tests/idl/parse_rust.rs index b8b21b7..bf1ab2d 100644 --- a/programs/settlement/tests/idl/parse_rust.rs +++ b/programs/settlement/tests/idl/parse_rust.rs @@ -15,9 +15,24 @@ pub struct Source { text: &'static str, } -pub const INTERFACE_LIB_RS: Source = Source { - display: "interface/src/lib.rs", - text: include_str!("../../../../interface/src/lib.rs"), +pub const PDA_MOD_RS: Source = Source { + display: "interface/src/pda/mod.rs", + text: include_str!("../../../../interface/src/pda/mod.rs"), +}; + +pub const ERROR_RS: Source = Source { + display: "interface/src/error.rs", + text: include_str!("../../../../interface/src/error.rs"), +}; + +pub const ROLE_RS: Source = Source { + display: "interface/src/role.rs", + text: include_str!("../../../../interface/src/role.rs"), +}; + +pub const INSTRUCTION_MOD_RS: Source = Source { + display: "interface/src/instruction/mod.rs", + text: include_str!("../../../../interface/src/instruction/mod.rs"), }; pub const INTENT_RS: Source = Source { diff --git a/programs/settlement/tests/initialize.rs b/programs/settlement/tests/initialize.rs index 244ff0e..1d129c3 100644 --- a/programs/settlement/tests/initialize.rs +++ b/programs/settlement/tests/initialize.rs @@ -2,7 +2,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::state::WIDTH_HEADER, instruction::initialize::Initialize as InitializeRaw, pda::state::find_state_pda, }; -use cow_settlement_client::instructions::Initialize; +use cow_settlement_client::instruction::Initialize; use cow_settlement_client::pda::state::DecodedStateAccount; use solana_sdk::signature::Signer; diff --git a/programs/settlement/tests/matching_begin_finalize.rs b/programs/settlement/tests/matching_begin_finalize.rs index 958cea0..69fd47f 100644 --- a/programs/settlement/tests/matching_begin_finalize.rs +++ b/programs/settlement/tests/matching_begin_finalize.rs @@ -1,5 +1,5 @@ use cow_settlement_client::cow_settlement_interface::{SettlementError, SettlementInstruction}; -use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle}; use litesvm::{types::FailedTransactionMetadata, LiteSVM}; use solana_sdk::{ instruction::{AccountMeta, Instruction, InstructionError}, diff --git a/programs/settlement/tests/program_deployment.rs b/programs/settlement/tests/program_deployment.rs index bbd3a8d..dd376c9 100644 --- a/programs/settlement/tests/program_deployment.rs +++ b/programs/settlement/tests/program_deployment.rs @@ -1,4 +1,4 @@ -use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle}; use solana_sdk::{ instruction::{Instruction, InstructionError}, signature::Signer, diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index ad5607d..46365e3 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -1,4 +1,4 @@ -use cow_settlement_client::instructions::ReclaimBuffer; +use cow_settlement_client::instruction::ReclaimBuffer; use cow_settlement_interface::Instruction; use cow_settlement_interface::{ instruction::reclaim_buffer::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, diff --git a/programs/settlement/tests/remove_solvers.rs b/programs/settlement/tests/remove_solvers.rs index 2de3a22..014ffad 100644 --- a/programs/settlement/tests/remove_solvers.rs +++ b/programs/settlement/tests/remove_solvers.rs @@ -7,7 +7,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::state::{WIDTH_HEADER, WIDTH_PUBKEY}, Instruction, SettlementError, }; -use cow_settlement_client::instructions::RemoveSolver; +use cow_settlement_client::instruction::RemoveSolver; use litesvm::LiteSVM; use solana_sdk::{ instruction::InstructionError, diff --git a/programs/settlement/tests/settle_solver_auth.rs b/programs/settlement/tests/settle_solver_auth.rs index 82e8742..02e11a5 100644 --- a/programs/settlement/tests/settle_solver_auth.rs +++ b/programs/settlement/tests/settle_solver_auth.rs @@ -4,7 +4,7 @@ //! unauthorized caller is rejected before any settlement work happens. use cow_settlement_client::cow_settlement_interface::{Instruction, SettlementError}; -use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle}; use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; use crate::common::{ diff --git a/programs/settlement/tests/transfer_authority.rs b/programs/settlement/tests/transfer_authority.rs index f26ec2c..340d757 100644 --- a/programs/settlement/tests/transfer_authority.rs +++ b/programs/settlement/tests/transfer_authority.rs @@ -4,7 +4,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::state::StateAccount, instruction::transfer_authority::fixtures::ROLE_OFFSET, Instruction, Role, SettlementError, }; -use cow_settlement_client::instructions::TransferAuthority; +use cow_settlement_client::instruction::TransferAuthority; use litesvm::LiteSVM; use solana_sdk::{ instruction::InstructionError, diff --git a/test-cli/src/cmd/create_order.rs b/test-cli/src/cmd/create_order.rs index 3465203..99b55a6 100644 --- a/test-cli/src/cmd/create_order.rs +++ b/test-cli/src/cmd/create_order.rs @@ -5,13 +5,13 @@ use cow_settlement_client::{ data::intent::{Flags, OrderIntent, OrderKind}, pda::order::find_order_pda, }, - instructions::CreateOrder, + instruction::CreateOrder, }; use solana_sdk::{signature::Signer, transaction::Transaction}; use std::time::{SystemTime, UNIX_EPOCH}; use super::Context; -use crate::{helpers::print_summary, token::ResolvedToken}; +use crate::utils::{self, output::print_summary, token::ResolvedToken}; #[derive(ClapArgs)] struct CommonArgs { @@ -103,8 +103,8 @@ fn parse(ctx: &Context, kind: OrderKind, terms: &[String]) -> anyhow::Result (b_tok, b_amount, *a_tok, *a_amount), }; - let sell = crate::token::resolve(&ctx.rpc, &ctx.payer.pubkey(), sell_tok)?; - let buy = crate::token::resolve(&ctx.rpc, &ctx.payer.pubkey(), buy_tok)?; + let sell = utils::token::resolve(&ctx.rpc, &ctx.payer.pubkey(), sell_tok)?; + let buy = utils::token::resolve(&ctx.rpc, &ctx.payer.pubkey(), buy_tok)?; let sell_amount = spl_token::try_ui_amount_into_amount(sell_amount_str.to_string(), sell.mint_data.decimals) @@ -139,7 +139,7 @@ fn execute(ctx: Context, parsed: ParsedOrder, common: CommonArgs) -> anyhow::Res if sell_is_sol { let (wsol_ata, wrap_ixs) = - crate::instructions::wrap_sol(&ctx.rpc, &ctx.payer.pubkey(), sell_amount)?; + utils::spl_instructions::wrap_sol(&ctx.rpc, &ctx.payer.pubkey(), sell_amount)?; assert_eq!(wsol_ata, sell.ta, "resolved WSOL ATA mismatch"); ixs.extend(wrap_ixs); } @@ -148,7 +148,7 @@ fn execute(ctx: Context, parsed: ParsedOrder, common: CommonArgs) -> anyhow::Res ixs.extend(buy.create_ata_ix(&ctx.payer.pubkey())); // Approve the settlement state PDA to pull sell tokens on the user's behalf. - ixs.push(crate::instructions::approve( + ixs.push(utils::spl_instructions::approve( &ctx.program_id, &sell.ta, &ctx.payer.pubkey(), diff --git a/test-cli/src/cmd/initialize.rs b/test-cli/src/cmd/initialize.rs index 6e06df0..e020b40 100644 --- a/test-cli/src/cmd/initialize.rs +++ b/test-cli/src/cmd/initialize.rs @@ -1,11 +1,11 @@ use anyhow::Context as _; use clap::Args as ClapArgs; use cow_settlement_client::{ - cow_settlement_interface::pda::state::find_state_pda, instructions::Initialize, + cow_settlement_interface::pda::state::find_state_pda, instruction::Initialize, }; use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; -use crate::helpers::print_summary; +use crate::utils::output::print_summary; use super::Context; diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 2d7bb5a..dc99807 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -6,7 +6,7 @@ use cow_settlement_client::{ pda::buffer::find_buffer_pda, Pubkey, }, - instructions::{ + instruction::{ BeginSettle, CreateBuffers, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, }, }; @@ -19,7 +19,7 @@ use solana_sdk::{ }; use std::collections::{HashMap, HashSet}; -use crate::token::{resolve_from_token_account, ResolvedToken}; +use crate::utils::token::{resolve_from_token_account, ResolvedToken}; use super::Context; diff --git a/test-cli/src/cmd/solver.rs b/test-cli/src/cmd/solver.rs index 12e2d5f..867b69f 100644 --- a/test-cli/src/cmd/solver.rs +++ b/test-cli/src/cmd/solver.rs @@ -2,14 +2,14 @@ use anyhow::Context as _; use clap::{Args as ClapArgs, Parser, Subcommand}; use cow_settlement_client::{ cow_settlement_interface::{pda::state::find_state_pda, Pubkey}, - instructions::AddSolver, + instruction::AddSolver, }; use solana_sdk::{ signature::{read_keypair_file, Signer}, transaction::Transaction, }; -use crate::helpers::print_summary; +use crate::utils::output::print_summary; use super::Context; diff --git a/test-cli/src/main.rs b/test-cli/src/main.rs index e045a95..d6bd120 100644 --- a/test-cli/src/main.rs +++ b/test-cli/src/main.rs @@ -2,9 +2,7 @@ use clap::{Parser, Subcommand}; use cow_settlement_client::cow_settlement_interface::Pubkey; mod cmd; -mod helpers; -mod instructions; -mod token; +mod utils; fn home_dir() -> String { std::env::var("HOME").expect("`HOME` env not available") diff --git a/test-cli/src/utils/mod.rs b/test-cli/src/utils/mod.rs new file mode 100644 index 0000000..80cd4a3 --- /dev/null +++ b/test-cli/src/utils/mod.rs @@ -0,0 +1,5 @@ +//! Assorted helpers for the CLI. + +pub mod output; +pub mod spl_instructions; +pub mod token; diff --git a/test-cli/src/helpers.rs b/test-cli/src/utils/output.rs similarity index 100% rename from test-cli/src/helpers.rs rename to test-cli/src/utils/output.rs diff --git a/test-cli/src/instructions.rs b/test-cli/src/utils/spl_instructions.rs similarity index 94% rename from test-cli/src/instructions.rs rename to test-cli/src/utils/spl_instructions.rs index 665b373..581d999 100644 --- a/test-cli/src/instructions.rs +++ b/test-cli/src/utils/spl_instructions.rs @@ -1,6 +1,6 @@ -//! Instruction builders for operations the CLI needs to compose. +//! Builders for the SPL token instructions the CLI needs in a settlement. -use crate::token; +use super::token; use anyhow::Context as _; use cow_settlement_client::cow_settlement_interface::{pda::state::find_state_pda, Pubkey}; use solana_instruction::Instruction; diff --git a/test-cli/src/token.rs b/test-cli/src/utils/token.rs similarity index 100% rename from test-cli/src/token.rs rename to test-cli/src/utils/token.rs