From c05ec9ae53896d0c9283bf9daa2ceaea3fb792e8 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:07:28 +0000 Subject: [PATCH 01/21] feat(solana-solvers): PR 3 solution assembly (custom interactions, prices, lookup tables) --- crates/solana-solvers/src/domain/mod.rs | 3 + crates/solana-solvers/src/domain/solution.rs | 288 +++++++++++++++++++ crates/solana-solvers/src/lib.rs | 1 + 3 files changed, 292 insertions(+) create mode 100644 crates/solana-solvers/src/domain/mod.rs create mode 100644 crates/solana-solvers/src/domain/solution.rs diff --git a/crates/solana-solvers/src/domain/mod.rs b/crates/solana-solvers/src/domain/mod.rs new file mode 100644 index 0000000000..2d50cf862b --- /dev/null +++ b/crates/solana-solvers/src/domain/mod.rs @@ -0,0 +1,3 @@ +//! Domain types the solver engine emits: the solution DTO the driver consumes. + +pub mod solution; diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs new file mode 100644 index 0000000000..ce922e92dd --- /dev/null +++ b/crates/solana-solvers/src/domain/solution.rs @@ -0,0 +1,288 @@ +//! Solution assembly: one quoted swap becomes one single-order solution in the +//! driver's `/solve` DTO (driver spec §2). +//! +//! The solver controls only the `interactions` array. Slippage is already +//! baked into the instruction data by the aggregator and the driver applies +//! none to `custom` interactions, so nothing is re-applied here. Compute +//! budget sizing is the driver's job (it derives the CU limit from +//! simulation), so the solution carries no compute-unit estimate and the +//! aggregator's compute-budget instructions are never included. The buy side +//! is funded by the swap output landing in the settlement's per-token buffer +//! (the adapter's `destination_token_account`), and `FinalizeSettle` pushes +//! each order's amount to the user, so the solver emits no transfer or credit +//! interaction. + +use { + crate::dex, + base64::prelude::*, + serde::Serialize, + serde_with::serde_as, + solana_sdk::{instruction::Instruction, pubkey::Pubkey}, + std::collections::HashMap, +}; + +/// A single-order solution in the driver DTO. Trades fulfill auction orders +/// only, with no JIT. +#[serde_as] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Solution { + pub id: u64, + /// Uniform clearing prices keyed by mint. + #[serde_as(as = "HashMap")] + pub prices: HashMap, + pub trades: Vec, + pub interactions: Vec, + /// The address lookup tables the interactions assume, carried through so + /// the driver can build the v0 transaction around them. + #[serde_as(as = "Vec")] + pub address_lookup_tables: Vec, +} + +/// A fulfillment of one auction order. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Trade { + /// The order's 32-byte intent hash. + #[serde(serialize_with = "serialize_hex")] + pub order_uid: [u8; 32], + /// Sell-token units for sell orders, buy-token units for buy orders. + pub executed_amount: u64, + /// Fee in sell-token units. Always zero at MVP: the solver prices the + /// full quoted amounts into the clearing prices instead. + pub fee: u64, +} + +/// A solver-supplied settlement interaction. Only `custom` exists: the +/// dormant liquidity variant of the EVM DTO is never emitted on Solana +/// (jupiter-solver spec §3.1), so the type does not carry it. +#[derive(Debug, Serialize)] +#[serde(tag = "kind", rename_all = "camelCase")] +pub enum Interaction { + Custom(CustomInteraction), +} + +/// The aggregator's instruction carried verbatim: program ID, full account +/// metas (writable and signer flags), and the instruction data. +#[serde_as] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomInteraction { + #[serde_as(as = "serde_with::DisplayFromStr")] + pub program_id: Pubkey, + pub accounts: Vec, + /// Base64, matching the aggregator wire encoding. + #[serde(serialize_with = "serialize_base64")] + pub instruction_data: Vec, +} + +/// Account meta in the driver DTO shape. +#[serde_as] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountMeta { + #[serde_as(as = "serde_with::DisplayFromStr")] + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, +} + +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum Error { + /// Sell and buy mint coincide, so a uniform clearing-price map (one price + /// per mint) cannot represent the trade. + #[error("sell and buy mint are the same")] + SameMint, + /// A clearing price of zero would make the trade worthless downstream. + #[error("quoted amount is zero")] + ZeroAmount, +} + +impl Solution { + /// Wraps one quoted swap into a single-order solution. + /// + /// Clearing prices derive from the quoted amounts: the sell mint is + /// priced at the swap's output amount and the buy mint at its input + /// amount, so `executed × price` matches on both sides. The swap's + /// instructions are carried verbatim as `custom` interactions, and its + /// address lookup tables travel along so the driver can build the v0 + /// transaction the instructions assume. + pub fn single( + id: u64, + order_uid: [u8; 32], + order: &dex::Order, + swap: dex::Swap, + ) -> Result { + if order.sell_mint == order.buy_mint { + return Err(Error::SameMint); + } + if swap.in_amount == 0 || swap.out_amount == 0 { + return Err(Error::ZeroAmount); + } + let executed_amount = match order.side { + dex::Side::Sell => swap.in_amount, + dex::Side::Buy => swap.out_amount, + }; + Ok(Self { + id, + prices: HashMap::from([ + (order.sell_mint, swap.out_amount), + (order.buy_mint, swap.in_amount), + ]), + trades: vec![Trade { + order_uid, + executed_amount, + fee: 0, + }], + interactions: swap.instructions.iter().map(Interaction::custom).collect(), + address_lookup_tables: swap.address_lookup_tables, + }) + } +} + +impl Interaction { + fn custom(instruction: &Instruction) -> Self { + Self::Custom(CustomInteraction { + program_id: instruction.program_id, + accounts: instruction + .accounts + .iter() + .map(|meta| AccountMeta { + pubkey: meta.pubkey, + is_signer: meta.is_signer, + is_writable: meta.is_writable, + }) + .collect(), + instruction_data: instruction.data.clone(), + }) + } +} + +fn serialize_base64(data: &[u8], serializer: S) -> Result { + serializer.serialize_str(&BASE64_STANDARD.encode(data)) +} + +fn serialize_hex(data: &[u8; 32], serializer: S) -> Result { + serializer.serialize_str(&data.map(|byte| format!("{byte:02x}")).concat()) +} + +#[cfg(test)] +mod tests { + use {super::*, solana_sdk::instruction::AccountMeta as SdkAccountMeta, std::str::FromStr}; + + fn pubkey(byte: u8) -> Pubkey { + Pubkey::new_from_array([byte; 32]) + } + + fn order(side: dex::Side) -> dex::Order { + dex::Order { + sell_mint: pubkey(1), + buy_mint: pubkey(2), + buy_destination: pubkey(3), + amount: 1_000, + side, + } + } + + fn swap() -> dex::Swap { + dex::Swap { + in_amount: 1_000, + out_amount: 2_000, + instructions: vec![Instruction { + program_id: pubkey(9), + accounts: vec![SdkAccountMeta { + pubkey: pubkey(4), + is_signer: true, + is_writable: false, + }], + data: vec![0xde, 0xad], + }], + address_lookup_tables: vec![pubkey(7)], + } + } + + #[test] + fn sell_swap_maps_to_single_order_solution() { + let order = order(dex::Side::Sell); + let solution = Solution::single(42, [8; 32], &order, swap()).unwrap(); + + assert_eq!(solution.id, 42); + // Clearing prices: sell mint priced at the output amount, buy mint at + // the input amount, so executed × price matches on both sides. + assert_eq!(solution.prices[&order.sell_mint], 2_000); + assert_eq!(solution.prices[&order.buy_mint], 1_000); + assert_eq!(solution.trades.len(), 1); + assert_eq!(solution.trades[0].order_uid, [8; 32]); + assert_eq!(solution.trades[0].executed_amount, 1_000); + assert_eq!(solution.trades[0].fee, 0); + assert_eq!(solution.address_lookup_tables, vec![pubkey(7)]); + + // The instruction is carried verbatim, flags included. + let Interaction::Custom(custom) = &solution.interactions[0]; + assert_eq!(custom.program_id, pubkey(9)); + assert_eq!(custom.accounts[0].pubkey, pubkey(4)); + assert!(custom.accounts[0].is_signer); + assert!(!custom.accounts[0].is_writable); + assert_eq!(custom.instruction_data, vec![0xde, 0xad]); + } + + #[test] + fn buy_swap_executes_in_buy_token_units() { + let solution = Solution::single(0, [0; 32], &order(dex::Side::Buy), swap()).unwrap(); + assert_eq!(solution.trades[0].executed_amount, 2_000); + } + + #[test] + fn same_mint_order_is_rejected() { + let mut order = order(dex::Side::Sell); + order.buy_mint = order.sell_mint; + assert_eq!( + Solution::single(0, [0; 32], &order, swap()).unwrap_err(), + Error::SameMint + ); + } + + #[test] + fn zero_quoted_amount_is_rejected() { + let mut swap = swap(); + swap.out_amount = 0; + assert_eq!( + Solution::single(0, [0; 32], &order(dex::Side::Sell), swap).unwrap_err(), + Error::ZeroAmount + ); + } + + #[test] + fn wire_format_is_stable() { + let solution = Solution::single(1, [8; 32], &order(dex::Side::Sell), swap()).unwrap(); + let json = serde_json::to_value(&solution).unwrap(); + + assert_eq!( + json["prices"][pubkey(1).to_string()], + serde_json::json!(2_000) + ); + assert_eq!(json["trades"][0]["orderUid"], "08".repeat(32)); + assert_eq!(json["trades"][0]["executedAmount"], 1_000); + assert_eq!(json["interactions"][0]["kind"], "custom"); + assert_eq!(json["interactions"][0]["programId"], pubkey(9).to_string()); + assert_eq!( + json["interactions"][0]["instructionData"], + BASE64_STANDARD.encode([0xde, 0xad]) + ); + assert!( + json["interactions"][0]["accounts"][0]["isSigner"] + .as_bool() + .unwrap() + ); + assert_eq!(json["addressLookupTables"][0], pubkey(7).to_string()); + // No cu_estimate on the wire: CU sizing is the driver's job. + assert!(json.get("cuEstimate").is_none()); + } + + #[test] + fn from_str_roundtrip_for_wire_keys() { + // Pubkeys serialize as base58 and parse back. + let key = pubkey(5); + assert_eq!(Pubkey::from_str(&key.to_string()).unwrap(), key); + } +} diff --git a/crates/solana-solvers/src/lib.rs b/crates/solana-solvers/src/lib.rs index 98f1a02c77..d3eb34ba42 100644 --- a/crates/solana-solvers/src/lib.rs +++ b/crates/solana-solvers/src/lib.rs @@ -7,6 +7,7 @@ pub mod api; mod cli; pub mod config; pub mod dex; +pub mod domain; mod run; pub use run::start; From 822a212a17975644341d6a06b08a8311fd4e1260 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:21:54 +0000 Subject: [PATCH 02/21] Drop spec references from code comments --- crates/solana-solvers/src/domain/solution.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index ce922e92dd..f7584ce203 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -1,5 +1,5 @@ //! Solution assembly: one quoted swap becomes one single-order solution in the -//! driver's `/solve` DTO (driver spec §2). +//! driver's `/solve` DTO. //! //! The solver controls only the `interactions` array. Slippage is already //! baked into the instruction data by the aggregator and the driver applies @@ -54,8 +54,8 @@ pub struct Trade { } /// A solver-supplied settlement interaction. Only `custom` exists: the -/// dormant liquidity variant of the EVM DTO is never emitted on Solana -/// (jupiter-solver spec §3.1), so the type does not carry it. +/// dormant liquidity variant of the EVM DTO is never emitted on Solana, so +/// the type does not carry it. #[derive(Debug, Serialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum Interaction { From 2a43350eaa802d0a6b5e195d2e538bcfa7b6adaf Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:24:34 +0000 Subject: [PATCH 03/21] Trim solution module doc to the three deliberate absences --- crates/solana-solvers/src/domain/solution.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index f7584ce203..18ebd935a4 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -1,16 +1,10 @@ //! Solution assembly: one quoted swap becomes one single-order solution in the //! driver's `/solve` DTO. //! -//! The solver controls only the `interactions` array. Slippage is already -//! baked into the instruction data by the aggregator and the driver applies -//! none to `custom` interactions, so nothing is re-applied here. Compute -//! budget sizing is the driver's job (it derives the CU limit from -//! simulation), so the solution carries no compute-unit estimate and the -//! aggregator's compute-budget instructions are never included. The buy side -//! is funded by the swap output landing in the settlement's per-token buffer -//! (the adapter's `destination_token_account`), and `FinalizeSettle` pushes -//! each order's amount to the user, so the solver emits no transfer or credit -//! interaction. +//! Deliberately absent: slippage (the aggregator bakes it into the +//! instruction data), compute-unit estimates (the driver sizes the CU limit +//! from simulation), and payout instructions (the swap output funds the +//! settlement's buffer, which pays the user out). use { crate::dex, From efe1c02db416ffac7a65e82cd533660131c66d1c Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:28:47 +0000 Subject: [PATCH 04/21] Introduce OrderUid newtype matching the indexer convention --- crates/solana-solvers/src/domain/mod.rs | 1 + crates/solana-solvers/src/domain/order.rs | 25 +++++++++++++++++ crates/solana-solvers/src/domain/solution.rs | 29 ++++++++++---------- 3 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 crates/solana-solvers/src/domain/order.rs diff --git a/crates/solana-solvers/src/domain/mod.rs b/crates/solana-solvers/src/domain/mod.rs index 2d50cf862b..60a0328cb3 100644 --- a/crates/solana-solvers/src/domain/mod.rs +++ b/crates/solana-solvers/src/domain/mod.rs @@ -1,3 +1,4 @@ //! Domain types the solver engine emits: the solution DTO the driver consumes. +pub mod order; pub mod solution; diff --git a/crates/solana-solvers/src/domain/order.rs b/crates/solana-solvers/src/domain/order.rs new file mode 100644 index 0000000000..d8604858a9 --- /dev/null +++ b/crates/solana-solvers/src/domain/order.rs @@ -0,0 +1,25 @@ +//! CoW Protocol order identifier. + +use std::fmt; + +/// A 32-byte CoW Protocol order identifier, equal to `hash(intent)`. The +/// same bytes the indexer and the settlement program's order-lifecycle +/// events carry, serialized as hex on the wire. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct OrderUid(pub [u8; 32]); + +impl fmt::Display for OrderUid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x")?; + for byte in self.0 { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +impl serde::Serialize for OrderUid { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(&self) + } +} diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index 18ebd935a4..93749ce1b8 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -7,6 +7,7 @@ //! settlement's buffer, which pays the user out). use { + super::order::OrderUid, crate::dex, base64::prelude::*, serde::Serialize, @@ -38,8 +39,7 @@ pub struct Solution { #[serde(rename_all = "camelCase")] pub struct Trade { /// The order's 32-byte intent hash. - #[serde(serialize_with = "serialize_hex")] - pub order_uid: [u8; 32], + pub order_uid: OrderUid, /// Sell-token units for sell orders, buy-token units for buy orders. pub executed_amount: u64, /// Fee in sell-token units. Always zero at MVP: the solver prices the @@ -103,7 +103,7 @@ impl Solution { /// transaction the instructions assume. pub fn single( id: u64, - order_uid: [u8; 32], + order_uid: OrderUid, order: &dex::Order, swap: dex::Swap, ) -> Result { @@ -156,10 +156,6 @@ fn serialize_base64(data: &[u8], serializer: S) -> Result< serializer.serialize_str(&BASE64_STANDARD.encode(data)) } -fn serialize_hex(data: &[u8; 32], serializer: S) -> Result { - serializer.serialize_str(&data.map(|byte| format!("{byte:02x}")).concat()) -} - #[cfg(test)] mod tests { use {super::*, solana_sdk::instruction::AccountMeta as SdkAccountMeta, std::str::FromStr}; @@ -198,7 +194,7 @@ mod tests { #[test] fn sell_swap_maps_to_single_order_solution() { let order = order(dex::Side::Sell); - let solution = Solution::single(42, [8; 32], &order, swap()).unwrap(); + let solution = Solution::single(42, OrderUid([8; 32]), &order, swap()).unwrap(); assert_eq!(solution.id, 42); // Clearing prices: sell mint priced at the output amount, buy mint at @@ -206,7 +202,7 @@ mod tests { assert_eq!(solution.prices[&order.sell_mint], 2_000); assert_eq!(solution.prices[&order.buy_mint], 1_000); assert_eq!(solution.trades.len(), 1); - assert_eq!(solution.trades[0].order_uid, [8; 32]); + assert_eq!(solution.trades[0].order_uid, OrderUid([8; 32])); assert_eq!(solution.trades[0].executed_amount, 1_000); assert_eq!(solution.trades[0].fee, 0); assert_eq!(solution.address_lookup_tables, vec![pubkey(7)]); @@ -222,7 +218,8 @@ mod tests { #[test] fn buy_swap_executes_in_buy_token_units() { - let solution = Solution::single(0, [0; 32], &order(dex::Side::Buy), swap()).unwrap(); + let solution = + Solution::single(0, OrderUid([0; 32]), &order(dex::Side::Buy), swap()).unwrap(); assert_eq!(solution.trades[0].executed_amount, 2_000); } @@ -231,7 +228,7 @@ mod tests { let mut order = order(dex::Side::Sell); order.buy_mint = order.sell_mint; assert_eq!( - Solution::single(0, [0; 32], &order, swap()).unwrap_err(), + Solution::single(0, OrderUid([0; 32]), &order, swap()).unwrap_err(), Error::SameMint ); } @@ -241,21 +238,25 @@ mod tests { let mut swap = swap(); swap.out_amount = 0; assert_eq!( - Solution::single(0, [0; 32], &order(dex::Side::Sell), swap).unwrap_err(), + Solution::single(0, OrderUid([0; 32]), &order(dex::Side::Sell), swap).unwrap_err(), Error::ZeroAmount ); } #[test] fn wire_format_is_stable() { - let solution = Solution::single(1, [8; 32], &order(dex::Side::Sell), swap()).unwrap(); + let solution = + Solution::single(1, OrderUid([8; 32]), &order(dex::Side::Sell), swap()).unwrap(); let json = serde_json::to_value(&solution).unwrap(); assert_eq!( json["prices"][pubkey(1).to_string()], serde_json::json!(2_000) ); - assert_eq!(json["trades"][0]["orderUid"], "08".repeat(32)); + assert_eq!( + json["trades"][0]["orderUid"], + format!("0x{}", "08".repeat(32)) + ); assert_eq!(json["trades"][0]["executedAmount"], 1_000); assert_eq!(json["interactions"][0]["kind"], "custom"); assert_eq!(json["interactions"][0]["programId"], pubkey(9).to_string()); From 5f18acfb3ad41a2cf210ae20bd1cb1ce439112b3 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:32:11 +0000 Subject: [PATCH 05/21] Move absence notes to where the decisions happen, drop tautological test assertion --- crates/solana-solvers/src/dex/jupiter/dto.rs | 5 +++++ crates/solana-solvers/src/dex/jupiter/mod.rs | 2 ++ crates/solana-solvers/src/domain/solution.rs | 7 ------- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/solana-solvers/src/dex/jupiter/dto.rs b/crates/solana-solvers/src/dex/jupiter/dto.rs index db9b971b02..56244fea20 100644 --- a/crates/solana-solvers/src/dex/jupiter/dto.rs +++ b/crates/solana-solvers/src/dex/jupiter/dto.rs @@ -48,6 +48,11 @@ impl<'a> SwapInstructionsRequest<'a> { /// The parts of the `/swap-instructions` response we need to build a [`Swap`]. /// Amounts come from the `/quote` response. +/// +/// The response's `computeBudgetInstructions` are deliberately not read: the +/// driver emits its own ComputeBudget instructions sized by simulating the +/// full settlement transaction, and a transaction with two of them is +/// rejected by the runtime. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SwapInstructionsResponse { diff --git a/crates/solana-solvers/src/dex/jupiter/mod.rs b/crates/solana-solvers/src/dex/jupiter/mod.rs index 1c2ba9aab8..0de7a37d08 100644 --- a/crates/solana-solvers/src/dex/jupiter/mod.rs +++ b/crates/solana-solvers/src/dex/jupiter/mod.rs @@ -78,6 +78,8 @@ impl Jupiter { .append_pair("outputMint", &order.buy_mint.to_string()) .append_pair("amount", &order.amount.to_string()) .append_pair("swapMode", swap_mode.as_str()) + // Jupiter bakes the resulting bounds into the returned instruction + // data; nothing downstream re-applies slippage. .append_pair("slippageBps", &self.slippage_bps.to_string()); self.send(self.with_key(self.client.get(url))).await } diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index 93749ce1b8..ade3c22600 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -1,10 +1,5 @@ //! Solution assembly: one quoted swap becomes one single-order solution in the //! driver's `/solve` DTO. -//! -//! Deliberately absent: slippage (the aggregator bakes it into the -//! instruction data), compute-unit estimates (the driver sizes the CU limit -//! from simulation), and payout instructions (the swap output funds the -//! settlement's buffer, which pays the user out). use { super::order::OrderUid, @@ -270,8 +265,6 @@ mod tests { .unwrap() ); assert_eq!(json["addressLookupTables"][0], pubkey(7).to_string()); - // No cu_estimate on the wire: CU sizing is the driver's job. - assert!(json.get("cuEstimate").is_none()); } #[test] From c5104e6511a368ccc367a893db47e1d5755beaeb Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:34:49 +0000 Subject: [PATCH 06/21] Assemble a solution end to end in the live Jupiter sell test --- crates/solana-solvers/src/dex/jupiter/mod.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/solana-solvers/src/dex/jupiter/mod.rs b/crates/solana-solvers/src/dex/jupiter/mod.rs index 0de7a37d08..78c46fb43c 100644 --- a/crates/solana-solvers/src/dex/jupiter/mod.rs +++ b/crates/solana-solvers/src/dex/jupiter/mod.rs @@ -198,20 +198,32 @@ mod tests { } /// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY` - /// for headroom. + /// for headroom. Run with `--nocapture` to see the assembled solution + /// JSON. #[tokio::test] #[ignore] async fn jupiter_live_sell() { let jupiter = Jupiter::new(&config(false)).unwrap(); // Any valid pubkey works for building instructions, the swap only runs // for real once the driver supplies its settlement signer. + let sell = order(Side::Sell); let swap = jupiter - .swap(&order(Side::Sell), &Pubkey::from_str(WSOL).unwrap()) + .swap(&sell, &Pubkey::from_str(WSOL).unwrap()) .await .unwrap(); assert_eq!(swap.in_amount, 1_000_000); assert!(swap.out_amount > 0); assert!(!swap.instructions.is_empty()); + + // End to end: the live swap assembles into a valid solution. + let solution = crate::domain::solution::Solution::single( + 0, + crate::domain::order::OrderUid([1; 32]), + &sell, + swap, + ) + .unwrap(); + println!("{}", serde_json::to_string_pretty(&solution).unwrap()); } /// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY` From 1df384000b6e6c071410c52e3cbb8920b9982aaf Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:39:30 +0000 Subject: [PATCH 07/21] Validate the assembled solution in the live test instead of printing it --- crates/solana-solvers/src/dex/jupiter/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/solana-solvers/src/dex/jupiter/mod.rs b/crates/solana-solvers/src/dex/jupiter/mod.rs index 78c46fb43c..6d29b9aa2d 100644 --- a/crates/solana-solvers/src/dex/jupiter/mod.rs +++ b/crates/solana-solvers/src/dex/jupiter/mod.rs @@ -198,8 +198,7 @@ mod tests { } /// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY` - /// for headroom. Run with `--nocapture` to see the assembled solution - /// JSON. + /// for headroom. #[tokio::test] #[ignore] async fn jupiter_live_sell() { @@ -215,7 +214,8 @@ mod tests { assert!(swap.out_amount > 0); assert!(!swap.instructions.is_empty()); - // End to end: the live swap assembles into a valid solution. + // End to end: the live swap assembles into a valid solution that + // serializes. let solution = crate::domain::solution::Solution::single( 0, crate::domain::order::OrderUid([1; 32]), @@ -223,7 +223,9 @@ mod tests { swap, ) .unwrap(); - println!("{}", serde_json::to_string_pretty(&solution).unwrap()); + assert_eq!(solution.trades.len(), 1); + assert!(!solution.interactions.is_empty()); + serde_json::to_string(&solution).unwrap(); } /// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY` From 3aa165641d8ff3bcfb99c9bae150f5abd13927e4 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 07:47:23 +0000 Subject: [PATCH 08/21] refactor(solana-solvers): match EVM OrderUid hex Display, plainer interaction doc --- Cargo.lock | 1 + crates/solana-solvers/Cargo.toml | 1 + crates/solana-solvers/src/domain/order.rs | 26 ++++++++++++-------- crates/solana-solvers/src/domain/solution.rs | 6 ++--- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ac4105e4a..1a16335142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10835,6 +10835,7 @@ dependencies = [ "axum 0.8.8", "base64 0.22.1", "clap", + "const-hex", "observe", "reqwest 0.13.4", "serde", diff --git a/crates/solana-solvers/Cargo.toml b/crates/solana-solvers/Cargo.toml index fbf4331ece..d05629b790 100644 --- a/crates/solana-solvers/Cargo.toml +++ b/crates/solana-solvers/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" axum = { workspace = true } base64 = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } +const-hex = { workspace = true } observe = { workspace = true } reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/solana-solvers/src/domain/order.rs b/crates/solana-solvers/src/domain/order.rs index d8604858a9..84d459196c 100644 --- a/crates/solana-solvers/src/domain/order.rs +++ b/crates/solana-solvers/src/domain/order.rs @@ -2,24 +2,30 @@ use std::fmt; -/// A 32-byte CoW Protocol order identifier, equal to `hash(intent)`. The -/// same bytes the indexer and the settlement program's order-lifecycle -/// events carry, serialized as hex on the wire. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// A 32-byte CoW Protocol order identifier, equal to `hash(intent)`, +/// serialized as a `0x`-prefixed hex string on the wire. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct OrderUid(pub [u8; 32]); impl fmt::Display for OrderUid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0x")?; - for byte in self.0 { - write!(f, "{byte:02x}")?; - } - Ok(()) + let mut bytes = [0u8; 2 + 32 * 2]; + bytes[..2].copy_from_slice(b"0x"); + // Unwrap: the destination length always matches the input. + const_hex::encode_to_slice(self.0.as_slice(), &mut bytes[2..]).unwrap(); + // Unwrap: hex output is always valid UTF-8. + f.write_str(std::str::from_utf8(&bytes).unwrap()) + } +} + +impl fmt::Debug for OrderUid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") } } impl serde::Serialize for OrderUid { fn serialize(&self, serializer: S) -> Result { - serializer.collect_str(&self) + serializer.collect_str(self) } } diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index ade3c22600..a9f90895bb 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -42,9 +42,9 @@ pub struct Trade { pub fee: u64, } -/// A solver-supplied settlement interaction. Only `custom` exists: the -/// dormant liquidity variant of the EVM DTO is never emitted on Solana, so -/// the type does not carry it. +/// A solver-supplied settlement interaction. Only `custom` exists: the EVM +/// DTO's other interaction kinds aren't produced on Solana, so the type +/// carries just this one. #[derive(Debug, Serialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum Interaction { From 72452aa331db68605fb21257c7f30c7d0c139264 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 10:23:04 +0000 Subject: [PATCH 09/21] feat(solana-solvers): add optional cu_estimate to the solution DTO, unset for Jupiter --- crates/solana-solvers/src/domain/solution.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index a9f90895bb..0764f0564b 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -23,6 +23,12 @@ pub struct Solution { pub prices: HashMap, pub trades: Vec, pub interactions: Vec, + /// The solver's estimate of total settlement compute units. Optional and + /// left unset here: the driver sizes the CU limit from simulating the whole + /// settlement transaction, which this solver never sees, so it has no total + /// to report. Mirrors the EVM solution's optional `gas`. + #[serde(skip_serializing_if = "Option::is_none")] + pub cu_estimate: Option, /// The address lookup tables the interactions assume, carried through so /// the driver can build the v0 transaction around them. #[serde_as(as = "Vec")] @@ -124,6 +130,7 @@ impl Solution { fee: 0, }], interactions: swap.instructions.iter().map(Interaction::custom).collect(), + cu_estimate: None, address_lookup_tables: swap.address_lookup_tables, }) } @@ -265,6 +272,8 @@ mod tests { .unwrap() ); assert_eq!(json["addressLookupTables"][0], pubkey(7).to_string()); + // cu_estimate is optional and unset, so it is omitted from the wire. + assert!(json.get("cuEstimate").is_none()); } #[test] From d25f8acc38b1154a7ab667320448c3c431dd6479 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 10:31:01 +0000 Subject: [PATCH 10/21] refactor(solana-solvers): flatten interactions to Vec matching driver spec --- crates/solana-solvers/src/domain/solution.rs | 53 +++++++++----------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index 0764f0564b..7847ab4c9c 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -7,7 +7,7 @@ use { base64::prelude::*, serde::Serialize, serde_with::serde_as, - solana_sdk::{instruction::Instruction, pubkey::Pubkey}, + solana_sdk::{instruction::Instruction as SolInstruction, pubkey::Pubkey}, std::collections::HashMap, }; @@ -22,7 +22,7 @@ pub struct Solution { #[serde_as(as = "HashMap")] pub prices: HashMap, pub trades: Vec, - pub interactions: Vec, + pub interactions: Vec, /// The solver's estimate of total settlement compute units. Optional and /// left unset here: the driver sizes the CU limit from simulating the whole /// settlement transaction, which this solver never sees, so it has no total @@ -48,21 +48,15 @@ pub struct Trade { pub fee: u64, } -/// A solver-supplied settlement interaction. Only `custom` exists: the EVM -/// DTO's other interaction kinds aren't produced on Solana, so the type -/// carries just this one. -#[derive(Debug, Serialize)] -#[serde(tag = "kind", rename_all = "camelCase")] -pub enum Interaction { - Custom(CustomInteraction), -} - -/// The aggregator's instruction carried verbatim: program ID, full account -/// metas (writable and signer flags), and the instruction data. +/// One settlement instruction the solver supplies, carried verbatim: program +/// ID, full account metas (writable and signer flags), and the instruction +/// data. The driver splices these between `BeginSettle` and `FinalizeSettle`. +/// Solana has a single interaction kind at MVP, so this is a plain instruction, +/// not a tagged enum. #[serde_as] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct CustomInteraction { +pub struct Instruction { #[serde_as(as = "serde_with::DisplayFromStr")] pub program_id: Pubkey, pub accounts: Vec, @@ -99,7 +93,7 @@ impl Solution { /// Clearing prices derive from the quoted amounts: the sell mint is /// priced at the swap's output amount and the buy mint at its input /// amount, so `executed × price` matches on both sides. The swap's - /// instructions are carried verbatim as `custom` interactions, and its + /// instructions are carried verbatim as interactions, and its /// address lookup tables travel along so the driver can build the v0 /// transaction the instructions assume. pub fn single( @@ -129,16 +123,20 @@ impl Solution { executed_amount, fee: 0, }], - interactions: swap.instructions.iter().map(Interaction::custom).collect(), + interactions: swap + .instructions + .iter() + .map(Instruction::from_sdk) + .collect(), cu_estimate: None, address_lookup_tables: swap.address_lookup_tables, }) } } -impl Interaction { - fn custom(instruction: &Instruction) -> Self { - Self::Custom(CustomInteraction { +impl Instruction { + fn from_sdk(instruction: &SolInstruction) -> Self { + Self { program_id: instruction.program_id, accounts: instruction .accounts @@ -150,7 +148,7 @@ impl Interaction { }) .collect(), instruction_data: instruction.data.clone(), - }) + } } } @@ -180,7 +178,7 @@ mod tests { dex::Swap { in_amount: 1_000, out_amount: 2_000, - instructions: vec![Instruction { + instructions: vec![SolInstruction { program_id: pubkey(9), accounts: vec![SdkAccountMeta { pubkey: pubkey(4), @@ -210,12 +208,12 @@ mod tests { assert_eq!(solution.address_lookup_tables, vec![pubkey(7)]); // The instruction is carried verbatim, flags included. - let Interaction::Custom(custom) = &solution.interactions[0]; - assert_eq!(custom.program_id, pubkey(9)); - assert_eq!(custom.accounts[0].pubkey, pubkey(4)); - assert!(custom.accounts[0].is_signer); - assert!(!custom.accounts[0].is_writable); - assert_eq!(custom.instruction_data, vec![0xde, 0xad]); + let interaction = &solution.interactions[0]; + assert_eq!(interaction.program_id, pubkey(9)); + assert_eq!(interaction.accounts[0].pubkey, pubkey(4)); + assert!(interaction.accounts[0].is_signer); + assert!(!interaction.accounts[0].is_writable); + assert_eq!(interaction.instruction_data, vec![0xde, 0xad]); } #[test] @@ -260,7 +258,6 @@ mod tests { format!("0x{}", "08".repeat(32)) ); assert_eq!(json["trades"][0]["executedAmount"], 1_000); - assert_eq!(json["interactions"][0]["kind"], "custom"); assert_eq!(json["interactions"][0]["programId"], pubkey(9).to_string()); assert_eq!( json["interactions"][0]["instructionData"], From 832bd624baec8fcb94e69cad470344fd7fa0d4bd Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 11:20:50 +0000 Subject: [PATCH 11/21] refactor(solana-solvers): serialize solution amounts as decimal strings --- crates/solana-solvers/src/domain/solution.rs | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index 7847ab4c9c..1e6f82e75f 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -18,15 +18,14 @@ use { #[serde(rename_all = "camelCase")] pub struct Solution { pub id: u64, - /// Uniform clearing prices keyed by mint. - #[serde_as(as = "HashMap")] + /// Uniform clearing prices keyed by mint. Amounts go out as decimal strings + /// (a `u64` can exceed 2^53 and lose precision as a JSON number). + #[serde_as(as = "HashMap")] pub prices: HashMap, pub trades: Vec, pub interactions: Vec, - /// The solver's estimate of total settlement compute units. Optional and - /// left unset here: the driver sizes the CU limit from simulating the whole - /// settlement transaction, which this solver never sees, so it has no total - /// to report. Mirrors the EVM solution's optional `gas`. + /// Solver's estimate of total settlement compute units. Unset: the driver + /// sizes the real CU limit from its own simulation. #[serde(skip_serializing_if = "Option::is_none")] pub cu_estimate: Option, /// The address lookup tables the interactions assume, carried through so @@ -36,15 +35,18 @@ pub struct Solution { } /// A fulfillment of one auction order. +#[serde_as] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Trade { /// The order's 32-byte intent hash. pub order_uid: OrderUid, /// Sell-token units for sell orders, buy-token units for buy orders. + #[serde_as(as = "serde_with::DisplayFromStr")] pub executed_amount: u64, /// Fee in sell-token units. Always zero at MVP: the solver prices the /// full quoted amounts into the clearing prices instead. + #[serde_as(as = "serde_with::DisplayFromStr")] pub fee: u64, } @@ -249,15 +251,12 @@ mod tests { Solution::single(1, OrderUid([8; 32]), &order(dex::Side::Sell), swap()).unwrap(); let json = serde_json::to_value(&solution).unwrap(); - assert_eq!( - json["prices"][pubkey(1).to_string()], - serde_json::json!(2_000) - ); + assert_eq!(json["prices"][pubkey(1).to_string()], "2000"); assert_eq!( json["trades"][0]["orderUid"], format!("0x{}", "08".repeat(32)) ); - assert_eq!(json["trades"][0]["executedAmount"], 1_000); + assert_eq!(json["trades"][0]["executedAmount"], "1000"); assert_eq!(json["interactions"][0]["programId"], pubkey(9).to_string()); assert_eq!( json["interactions"][0]["instructionData"], From 2f430dfc3c62e25bfcfe1b776607eaa6cf061a13 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 11:36:05 +0000 Subject: [PATCH 12/21] refactor(solana-solvers): trim solution doc comments, drop redundant cu_estimate assertion --- crates/solana-solvers/src/domain/solution.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index 1e6f82e75f..84b5ea1b2d 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -18,8 +18,7 @@ use { #[serde(rename_all = "camelCase")] pub struct Solution { pub id: u64, - /// Uniform clearing prices keyed by mint. Amounts go out as decimal strings - /// (a `u64` can exceed 2^53 and lose precision as a JSON number). + /// Uniform clearing prices keyed by mint. Values are decimal strings. #[serde_as(as = "HashMap")] pub prices: HashMap, pub trades: Vec, @@ -44,17 +43,12 @@ pub struct Trade { /// Sell-token units for sell orders, buy-token units for buy orders. #[serde_as(as = "serde_with::DisplayFromStr")] pub executed_amount: u64, - /// Fee in sell-token units. Always zero at MVP: the solver prices the - /// full quoted amounts into the clearing prices instead. + /// Fee in sell-token units. #[serde_as(as = "serde_with::DisplayFromStr")] pub fee: u64, } -/// One settlement instruction the solver supplies, carried verbatim: program -/// ID, full account metas (writable and signer flags), and the instruction -/// data. The driver splices these between `BeginSettle` and `FinalizeSettle`. -/// Solana has a single interaction kind at MVP, so this is a plain instruction, -/// not a tagged enum. +/// A Solana instruction the solver supplies, carried verbatim. #[serde_as] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -268,8 +262,6 @@ mod tests { .unwrap() ); assert_eq!(json["addressLookupTables"][0], pubkey(7).to_string()); - // cu_estimate is optional and unset, so it is omitted from the wire. - assert!(json.get("cuEstimate").is_none()); } #[test] From 7385b72f47e0552c26e0c719e1dfa2803baa30ec Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 11:43:16 +0000 Subject: [PATCH 13/21] docs(solana-solvers): describe shared solution DTO by contract, not solver behavior --- crates/solana-solvers/src/domain/solution.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index 84b5ea1b2d..f76f05f3e9 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -11,8 +11,8 @@ use { std::collections::HashMap, }; -/// A single-order solution in the driver DTO. Trades fulfill auction orders -/// only, with no JIT. +/// A solution in the driver's `/solve` DTO. Trades fulfill auction orders, with +/// no JIT. #[serde_as] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -23,8 +23,7 @@ pub struct Solution { pub prices: HashMap, pub trades: Vec, pub interactions: Vec, - /// Solver's estimate of total settlement compute units. Unset: the driver - /// sizes the real CU limit from its own simulation. + /// Optional solver estimate of total settlement compute units. #[serde(skip_serializing_if = "Option::is_none")] pub cu_estimate: Option, /// The address lookup tables the interactions assume, carried through so @@ -56,7 +55,7 @@ pub struct Instruction { #[serde_as(as = "serde_with::DisplayFromStr")] pub program_id: Pubkey, pub accounts: Vec, - /// Base64, matching the aggregator wire encoding. + /// Base64-encoded instruction data. #[serde(serialize_with = "serialize_base64")] pub instruction_data: Vec, } From 9887f1bac91a09b18791f2e253e2c122b7ae5643 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 14:03:12 +0000 Subject: [PATCH 14/21] refactor(solana-solvers): move swap instructions into the solution instead of cloning --- crates/solana-solvers/src/domain/solution.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index f76f05f3e9..e6dad4ed78 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -120,7 +120,7 @@ impl Solution { }], interactions: swap .instructions - .iter() + .into_iter() .map(Instruction::from_sdk) .collect(), cu_estimate: None, @@ -130,19 +130,19 @@ impl Solution { } impl Instruction { - fn from_sdk(instruction: &SolInstruction) -> Self { + fn from_sdk(instruction: SolInstruction) -> Self { Self { program_id: instruction.program_id, accounts: instruction .accounts - .iter() + .into_iter() .map(|meta| AccountMeta { pubkey: meta.pubkey, is_signer: meta.is_signer, is_writable: meta.is_writable, }) .collect(), - instruction_data: instruction.data.clone(), + instruction_data: instruction.data, } } } From 030aca4ef95bbf097db02a02c67448a8cec3780a Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Wed, 22 Jul 2026 08:07:22 +0000 Subject: [PATCH 15/21] refactor(solana-solvers): use const_hex::Buffer for OrderUid, tidy solution tests --- crates/solana-solvers/src/domain/order.rs | 8 +-- crates/solana-solvers/src/domain/solution.rs | 57 +++++++++++--------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/crates/solana-solvers/src/domain/order.rs b/crates/solana-solvers/src/domain/order.rs index 84d459196c..487faeb762 100644 --- a/crates/solana-solvers/src/domain/order.rs +++ b/crates/solana-solvers/src/domain/order.rs @@ -9,12 +9,8 @@ pub struct OrderUid(pub [u8; 32]); impl fmt::Display for OrderUid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut bytes = [0u8; 2 + 32 * 2]; - bytes[..2].copy_from_slice(b"0x"); - // Unwrap: the destination length always matches the input. - const_hex::encode_to_slice(self.0.as_slice(), &mut bytes[2..]).unwrap(); - // Unwrap: hex output is always valid UTF-8. - f.write_str(std::str::from_utf8(&bytes).unwrap()) + let mut buffer = const_hex::Buffer::<32, true>::new(); + f.write_str(buffer.format(&self.0)) } } diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index e6dad4ed78..2d8e4c1673 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -18,7 +18,8 @@ use { #[serde(rename_all = "camelCase")] pub struct Solution { pub id: u64, - /// Uniform clearing prices keyed by mint. Values are decimal strings. + /// Uniform clearing prices keyed by token mint (an SPL token's on-chain + /// address). Values are decimal strings. #[serde_as(as = "HashMap")] pub prices: HashMap, pub trades: Vec, @@ -155,6 +156,8 @@ fn serialize_base64(data: &[u8], serializer: S) -> Result< mod tests { use {super::*, solana_sdk::instruction::AccountMeta as SdkAccountMeta, std::str::FromStr}; + const ORDER_UID: OrderUid = OrderUid([8; 32]); + fn pubkey(byte: u8) -> Pubkey { Pubkey::new_from_array([byte; 32]) } @@ -189,7 +192,7 @@ mod tests { #[test] fn sell_swap_maps_to_single_order_solution() { let order = order(dex::Side::Sell); - let solution = Solution::single(42, OrderUid([8; 32]), &order, swap()).unwrap(); + let solution = Solution::single(42, ORDER_UID, &order, swap()).unwrap(); assert_eq!(solution.id, 42); // Clearing prices: sell mint priced at the output amount, buy mint at @@ -197,7 +200,7 @@ mod tests { assert_eq!(solution.prices[&order.sell_mint], 2_000); assert_eq!(solution.prices[&order.buy_mint], 1_000); assert_eq!(solution.trades.len(), 1); - assert_eq!(solution.trades[0].order_uid, OrderUid([8; 32])); + assert_eq!(solution.trades[0].order_uid, ORDER_UID); assert_eq!(solution.trades[0].executed_amount, 1_000); assert_eq!(solution.trades[0].fee, 0); assert_eq!(solution.address_lookup_tables, vec![pubkey(7)]); @@ -213,8 +216,7 @@ mod tests { #[test] fn buy_swap_executes_in_buy_token_units() { - let solution = - Solution::single(0, OrderUid([0; 32]), &order(dex::Side::Buy), swap()).unwrap(); + let solution = Solution::single(0, ORDER_UID, &order(dex::Side::Buy), swap()).unwrap(); assert_eq!(solution.trades[0].executed_amount, 2_000); } @@ -223,7 +225,7 @@ mod tests { let mut order = order(dex::Side::Sell); order.buy_mint = order.sell_mint; assert_eq!( - Solution::single(0, OrderUid([0; 32]), &order, swap()).unwrap_err(), + Solution::single(0, ORDER_UID, &order, swap()).unwrap_err(), Error::SameMint ); } @@ -233,34 +235,41 @@ mod tests { let mut swap = swap(); swap.out_amount = 0; assert_eq!( - Solution::single(0, OrderUid([0; 32]), &order(dex::Side::Sell), swap).unwrap_err(), + Solution::single(0, ORDER_UID, &order(dex::Side::Sell), swap).unwrap_err(), Error::ZeroAmount ); } #[test] fn wire_format_is_stable() { - let solution = - Solution::single(1, OrderUid([8; 32]), &order(dex::Side::Sell), swap()).unwrap(); + let solution = Solution::single(1, ORDER_UID, &order(dex::Side::Sell), swap()).unwrap(); let json = serde_json::to_value(&solution).unwrap(); - assert_eq!(json["prices"][pubkey(1).to_string()], "2000"); - assert_eq!( - json["trades"][0]["orderUid"], - format!("0x{}", "08".repeat(32)) - ); - assert_eq!(json["trades"][0]["executedAmount"], "1000"); - assert_eq!(json["interactions"][0]["programId"], pubkey(9).to_string()); assert_eq!( - json["interactions"][0]["instructionData"], - BASE64_STANDARD.encode([0xde, 0xad]) - ); - assert!( - json["interactions"][0]["accounts"][0]["isSigner"] - .as_bool() - .unwrap() + json, + serde_json::json!({ + "id": 1, + "prices": { + pubkey(1).to_string(): "2000", + pubkey(2).to_string(): "1000", + }, + "trades": [{ + "orderUid": format!("0x{}", "08".repeat(32)), + "executedAmount": "1000", + "fee": "0", + }], + "interactions": [{ + "programId": pubkey(9).to_string(), + "accounts": [{ + "pubkey": pubkey(4).to_string(), + "isSigner": true, + "isWritable": false, + }], + "instructionData": BASE64_STANDARD.encode([0xde, 0xad]), + }], + "addressLookupTables": [pubkey(7).to_string()], + }) ); - assert_eq!(json["addressLookupTables"][0], pubkey(7).to_string()); } #[test] From 18ba577df5ab66149cb263d4abb3071ca768667e Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Wed, 22 Jul 2026 08:48:54 +0000 Subject: [PATCH 16/21] refactor(solana-solvers): drop clearing prices from the solve DTO --- crates/solana-solvers/src/domain/solution.rs | 31 ++++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/domain/solution.rs index 2d8e4c1673..047d86c014 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/domain/solution.rs @@ -8,7 +8,6 @@ use { serde::Serialize, serde_with::serde_as, solana_sdk::{instruction::Instruction as SolInstruction, pubkey::Pubkey}, - std::collections::HashMap, }; /// A solution in the driver's `/solve` DTO. Trades fulfill auction orders, with @@ -18,10 +17,6 @@ use { #[serde(rename_all = "camelCase")] pub struct Solution { pub id: u64, - /// Uniform clearing prices keyed by token mint (an SPL token's on-chain - /// address). Values are decimal strings. - #[serde_as(as = "HashMap")] - pub prices: HashMap, pub trades: Vec, pub interactions: Vec, /// Optional solver estimate of total settlement compute units. @@ -74,11 +69,10 @@ pub struct AccountMeta { #[derive(Debug, thiserror::Error, PartialEq)] pub enum Error { - /// Sell and buy mint coincide, so a uniform clearing-price map (one price - /// per mint) cannot represent the trade. + /// Sell and buy mint coincide, which is not a real trade. #[error("sell and buy mint are the same")] SameMint, - /// A clearing price of zero would make the trade worthless downstream. + /// A zero quoted amount means the swap fills nothing. #[error("quoted amount is zero")] ZeroAmount, } @@ -86,12 +80,9 @@ pub enum Error { impl Solution { /// Wraps one quoted swap into a single-order solution. /// - /// Clearing prices derive from the quoted amounts: the sell mint is - /// priced at the swap's output amount and the buy mint at its input - /// amount, so `executed × price` matches on both sides. The swap's - /// instructions are carried verbatim as interactions, and its - /// address lookup tables travel along so the driver can build the v0 - /// transaction the instructions assume. + /// The swap's instructions are carried verbatim as interactions, and + /// its address lookup tables travel along so the driver can build the + /// v0 transaction the instructions assume. pub fn single( id: u64, order_uid: OrderUid, @@ -110,10 +101,6 @@ impl Solution { }; Ok(Self { id, - prices: HashMap::from([ - (order.sell_mint, swap.out_amount), - (order.buy_mint, swap.in_amount), - ]), trades: vec![Trade { order_uid, executed_amount, @@ -195,10 +182,6 @@ mod tests { let solution = Solution::single(42, ORDER_UID, &order, swap()).unwrap(); assert_eq!(solution.id, 42); - // Clearing prices: sell mint priced at the output amount, buy mint at - // the input amount, so executed × price matches on both sides. - assert_eq!(solution.prices[&order.sell_mint], 2_000); - assert_eq!(solution.prices[&order.buy_mint], 1_000); assert_eq!(solution.trades.len(), 1); assert_eq!(solution.trades[0].order_uid, ORDER_UID); assert_eq!(solution.trades[0].executed_amount, 1_000); @@ -249,10 +232,6 @@ mod tests { json, serde_json::json!({ "id": 1, - "prices": { - pubkey(1).to_string(): "2000", - pubkey(2).to_string(): "1000", - }, "trades": [{ "orderUid": format!("0x{}", "08".repeat(32)), "executedAmount": "1000", From 72867ba86a03c535f0d6310dc095ffc03cf314ab Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 21 Jul 2026 13:18:16 +0000 Subject: [PATCH 17/21] feat(solana-solvers): solve loop, order handling, engine wiring --- Cargo.lock | 1 + crates/solana-solvers/Cargo.toml | 1 + crates/solana-solvers/src/api.rs | 20 ++- crates/solana-solvers/src/dex/mod.rs | 3 +- crates/solana-solvers/src/domain/auction.rs | 59 +++++++ crates/solana-solvers/src/domain/mod.rs | 5 +- crates/solana-solvers/src/domain/order.rs | 10 ++ .../solana-solvers/src/domain/solver/mod.rs | 163 ++++++++++++++++++ crates/solana-solvers/src/run.rs | 6 +- 9 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 crates/solana-solvers/src/domain/auction.rs create mode 100644 crates/solana-solvers/src/domain/solver/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1a16335142..26fdbf50e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10836,6 +10836,7 @@ dependencies = [ "base64 0.22.1", "clap", "const-hex", + "futures", "observe", "reqwest 0.13.4", "serde", diff --git a/crates/solana-solvers/Cargo.toml b/crates/solana-solvers/Cargo.toml index d05629b790..0eacadea52 100644 --- a/crates/solana-solvers/Cargo.toml +++ b/crates/solana-solvers/Cargo.toml @@ -18,6 +18,7 @@ axum = { workspace = true } base64 = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } const-hex = { workspace = true } +futures = { workspace = true } observe = { workspace = true } reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/solana-solvers/src/api.rs b/crates/solana-solvers/src/api.rs index 26a6024690..29f3a80f37 100644 --- a/crates/solana-solvers/src/api.rs +++ b/crates/solana-solvers/src/api.rs @@ -1,10 +1,12 @@ //! HTTP API for the solver engine. //! -//! Serves the `/solve` contract the driver calls. The handler is a scaffold: it -//! accepts any auction and returns no solutions. +//! Serves the `/solve` contract the driver calls. use { - crate::config::Config, + crate::{ + dex::Dex, + domain::{auction::Auction, solver}, + }, axum::{ Json, Router, @@ -20,7 +22,7 @@ const REQUEST_BODY_LIMIT: usize = 10 * 1024 * 1024; pub struct Api { pub addr: SocketAddr, - pub config: Config, + pub dex: Arc, } impl Api { @@ -32,7 +34,7 @@ impl Api { let app = Router::new() .route("/healthz", get(healthz)) .route("/solve", post(solve)) - .with_state(Arc::new(self.config)) + .with_state(self.dex) .layer(RequestBodyLimitLayer::new(REQUEST_BODY_LIMIT)) .layer(axum::extract::DefaultBodyLimit::disable()); @@ -48,8 +50,8 @@ async fn healthz() -> &'static str { "ok" } -/// Scaffold `/solve`: accepts any auction and returns no solutions, so the -/// driver wiring can be exercised against an empty result. -async fn solve(State(_config): State>, Json(_auction): Json) -> Json { - Json(json!({ "solutions": [] })) +/// Quote every order in the auction and return the single-order solutions. +async fn solve(State(dex): State>, Json(auction): Json) -> Json { + let solutions = solver::solve(dex.as_ref(), &auction).await; + Json(json!({ "solutions": solutions })) } diff --git a/crates/solana-solvers/src/dex/mod.rs b/crates/solana-solvers/src/dex/mod.rs index ccdc1bdd4a..96c5f7fa6b 100644 --- a/crates/solana-solvers/src/dex/mod.rs +++ b/crates/solana-solvers/src/dex/mod.rs @@ -20,7 +20,8 @@ pub struct Order { pub side: Side, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] pub enum Side { Buy, Sell, diff --git a/crates/solana-solvers/src/domain/auction.rs b/crates/solana-solvers/src/domain/auction.rs new file mode 100644 index 0000000000..f850a55439 --- /dev/null +++ b/crates/solana-solvers/src/domain/auction.rs @@ -0,0 +1,59 @@ +//! Inbound `/solve` auction: the orders the driver asks the solver to fill. +//! +//! Proposed shape. The driver spec pins the solution response, not the auction +//! request, so these are the fields the solve loop needs and will be reconciled +//! with the driver's request DTO. + +use { + super::order::OrderUid, + crate::dex, + serde::Deserialize, + serde_with::serde_as, + solana_sdk::pubkey::Pubkey, +}; + +/// The auction the driver posts to `/solve`. +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Auction { + pub id: u64, + /// Settlement signer the swap instructions are built for. + #[serde_as(as = "serde_with::DisplayFromStr")] + pub taker: Pubkey, + pub orders: Vec, +} + +/// One order to quote. +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Order { + #[serde_as(as = "serde_with::DisplayFromStr")] + pub uid: OrderUid, + #[serde_as(as = "serde_with::DisplayFromStr")] + pub sell_mint: Pubkey, + #[serde_as(as = "serde_with::DisplayFromStr")] + pub buy_mint: Pubkey, + /// The buy-mint buffer the swap output lands in. + #[serde_as(as = "serde_with::DisplayFromStr")] + pub buy_destination: Pubkey, + /// Sell amount for a sell, buy amount for a buy. Decimal string on the + /// wire. + #[serde_as(as = "serde_with::DisplayFromStr")] + pub amount: u64, + pub side: dex::Side, +} + +impl Order { + /// The adapter-facing view of this order. + pub fn to_dex_order(&self) -> dex::Order { + dex::Order { + sell_mint: self.sell_mint, + buy_mint: self.buy_mint, + buy_destination: self.buy_destination, + amount: self.amount, + side: self.side, + } + } +} diff --git a/crates/solana-solvers/src/domain/mod.rs b/crates/solana-solvers/src/domain/mod.rs index 60a0328cb3..70042e4da1 100644 --- a/crates/solana-solvers/src/domain/mod.rs +++ b/crates/solana-solvers/src/domain/mod.rs @@ -1,4 +1,7 @@ -//! Domain types the solver engine emits: the solution DTO the driver consumes. +//! Domain types and the solve loop: parse the auction, quote each order, and +//! assemble the solutions the driver consumes. +pub mod auction; pub mod order; pub mod solution; +pub mod solver; diff --git a/crates/solana-solvers/src/domain/order.rs b/crates/solana-solvers/src/domain/order.rs index 487faeb762..0a83e50ce8 100644 --- a/crates/solana-solvers/src/domain/order.rs +++ b/crates/solana-solvers/src/domain/order.rs @@ -25,3 +25,13 @@ impl serde::Serialize for OrderUid { serializer.collect_str(self) } } + +impl std::str::FromStr for OrderUid { + type Err = const_hex::FromHexError; + + fn from_str(s: &str) -> Result { + let mut bytes = [0u8; 32]; + const_hex::decode_to_slice(s.strip_prefix("0x").unwrap_or(s), &mut bytes)?; + Ok(Self(bytes)) + } +} diff --git a/crates/solana-solvers/src/domain/solver/mod.rs b/crates/solana-solvers/src/domain/solver/mod.rs new file mode 100644 index 0000000000..ac600c483d --- /dev/null +++ b/crates/solana-solvers/src/domain/solver/mod.rs @@ -0,0 +1,163 @@ +//! Solve loop: quote each auction order and assemble single-order solutions. + +use { + super::{auction::Auction, solution::Solution}, + crate::dex::{self, Dex}, + futures::future::join_all, + solana_sdk::pubkey::Pubkey, + std::future::Future, +}; + +/// Quotes one order into a swap. A seam over [`Dex`] so the loop is testable +/// without the network. +pub trait Quote { + fn quote( + &self, + order: &dex::Order, + taker: &Pubkey, + ) -> impl Future> + Send; +} + +impl Quote for Dex { + fn quote( + &self, + order: &dex::Order, + taker: &Pubkey, + ) -> impl Future> + Send { + self.swap(order, taker) + } +} + +/// Quote every order concurrently and return one single-order solution per +/// routable order. Buys (when disabled) and orders the aggregator cannot route +/// yield no candidate, the rest of the auction still proceeds. +/// +/// Order counts are small (bounded by the settlement account budget), so every +/// order is quoted at once. +pub async fn solve(quoter: &Q, auction: &Auction) -> Vec { + let candidates = auction.orders.iter().enumerate().map(|(index, order)| { + let dex_order = order.to_dex_order(); + async move { + let swap = quoter.quote(&dex_order, &auction.taker).await.ok()?; + Solution::single(index as u64, order.uid, &dex_order, swap).ok() + } + }); + join_all(candidates).await.into_iter().flatten().collect() +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + config::JupiterConfig, + domain::{auction, order::OrderUid}, + }, + std::str::FromStr, + }; + + // USDC and wrapped SOL mints for the live test. + const USDC: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; + const WSOL: &str = "So11111111111111111111111111111111111111112"; + + fn pubkey(byte: u8) -> Pubkey { + Pubkey::new_from_array([byte; 32]) + } + + fn order(uid: u8, side: dex::Side, sell_mint: Pubkey) -> auction::Order { + auction::Order { + uid: OrderUid([uid; 32]), + sell_mint, + buy_mint: pubkey(2), + buy_destination: pubkey(3), + amount: 1_000, + side, + } + } + + /// Routes any sell except the `0xff` sell mint, rejects buys. + struct MockQuote; + + impl Quote for MockQuote { + fn quote( + &self, + order: &dex::Order, + _taker: &Pubkey, + ) -> impl Future> + Send { + let result = match order.side { + dex::Side::Buy => Err(dex::jupiter::Error::OrderNotSupported), + dex::Side::Sell if order.sell_mint == pubkey(0xff) => { + Err(dex::jupiter::Error::NotFound) + } + dex::Side::Sell => Ok(dex::Swap { + in_amount: 1_000, + out_amount: 2_000, + instructions: vec![], + address_lookup_tables: vec![], + }), + }; + async move { result } + } + } + + #[tokio::test] + async fn emits_one_solution_per_routable_order() { + let auction = Auction { + id: 1, + taker: pubkey(1), + orders: vec![ + order(0x01, dex::Side::Sell, pubkey(0x10)), // routable + order(0x02, dex::Side::Sell, pubkey(0xff)), // no route + order(0x03, dex::Side::Buy, pubkey(0x11)), // buys disabled + ], + }; + + let solutions = solve(&MockQuote, &auction).await; + + assert_eq!(solutions.len(), 1); + assert_eq!(solutions[0].trades[0].order_uid, OrderUid([0x01; 32])); + } + + #[tokio::test] + async fn empty_auction_yields_no_solutions() { + let auction = Auction { + id: 1, + taker: pubkey(1), + orders: vec![], + }; + assert!(solve(&MockQuote, &auction).await.is_empty()); + } + + /// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY` + /// for headroom. + #[tokio::test] + #[ignore] + async fn jupiter_live_solve() { + let dex = Dex::Jupiter( + dex::jupiter::Jupiter::new(&JupiterConfig { + endpoint: "https://api.jup.ag".parse().unwrap(), + api_key: std::env::var("JUPITER_API_KEY").ok(), + slippage_bps: 50, + enable_buy_orders: false, + }) + .unwrap(), + ); + let auction = Auction { + id: 1, + taker: Pubkey::from_str(WSOL).unwrap(), + orders: vec![auction::Order { + uid: OrderUid([7; 32]), + sell_mint: Pubkey::from_str(USDC).unwrap(), + buy_mint: Pubkey::from_str(WSOL).unwrap(), + buy_destination: Pubkey::from_str(WSOL).unwrap(), + amount: 1_000_000, + side: dex::Side::Sell, + }], + }; + + let solutions = solve(&dex, &auction).await; + + assert_eq!(solutions.len(), 1); + assert!(!solutions[0].interactions.is_empty()); + } +} diff --git a/crates/solana-solvers/src/run.rs b/crates/solana-solvers/src/run.rs index 1168f474c4..4c504fb07f 100644 --- a/crates/solana-solvers/src/run.rs +++ b/crates/solana-solvers/src/run.rs @@ -7,8 +7,10 @@ use { api::Api, cli::{Args, Command}, config, + dex, }, clap::Parser, + std::sync::Arc, }; /// Parse args and run the selected solver engine until shutdown. @@ -28,9 +30,11 @@ pub async fn start(args: impl IntoIterator) { match args.command { Command::Jupiter { config: path } => { let config = config::load(&path).await; + let jupiter = dex::jupiter::Jupiter::new(&config.dex) + .unwrap_or_else(|err| panic!("build jupiter dex: {err}")); let api = Api { addr: args.addr, - config, + dex: Arc::new(dex::Dex::Jupiter(jupiter)), }; if let Err(err) = api.serve(shutdown_signal()).await { tracing::error!(?err, "server error"); From 106bc865bef06341cc87c006ee3288b25ad1467a Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Fri, 24 Jul 2026 09:15:13 +0000 Subject: [PATCH 18/21] refactor(solana-solvers): move solve DTOs to a dto module and rename single to new --- crates/solana-solvers/src/dex/jupiter/mod.rs | 4 ++-- crates/solana-solvers/src/domain/mod.rs | 4 ---- crates/solana-solvers/src/dto/mod.rs | 4 ++++ crates/solana-solvers/src/{domain => dto}/order.rs | 0 .../solana-solvers/src/{domain => dto}/solution.rs | 12 ++++++------ crates/solana-solvers/src/lib.rs | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 crates/solana-solvers/src/domain/mod.rs create mode 100644 crates/solana-solvers/src/dto/mod.rs rename crates/solana-solvers/src/{domain => dto}/order.rs (100%) rename crates/solana-solvers/src/{domain => dto}/solution.rs (94%) diff --git a/crates/solana-solvers/src/dex/jupiter/mod.rs b/crates/solana-solvers/src/dex/jupiter/mod.rs index 6d29b9aa2d..7f019e9dc6 100644 --- a/crates/solana-solvers/src/dex/jupiter/mod.rs +++ b/crates/solana-solvers/src/dex/jupiter/mod.rs @@ -216,9 +216,9 @@ mod tests { // End to end: the live swap assembles into a valid solution that // serializes. - let solution = crate::domain::solution::Solution::single( + let solution = crate::dto::solution::Solution::new( 0, - crate::domain::order::OrderUid([1; 32]), + crate::dto::order::OrderUid([1; 32]), &sell, swap, ) diff --git a/crates/solana-solvers/src/domain/mod.rs b/crates/solana-solvers/src/domain/mod.rs deleted file mode 100644 index 60a0328cb3..0000000000 --- a/crates/solana-solvers/src/domain/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Domain types the solver engine emits: the solution DTO the driver consumes. - -pub mod order; -pub mod solution; diff --git a/crates/solana-solvers/src/dto/mod.rs b/crates/solana-solvers/src/dto/mod.rs new file mode 100644 index 0000000000..1c2cdc1648 --- /dev/null +++ b/crates/solana-solvers/src/dto/mod.rs @@ -0,0 +1,4 @@ +//! Wire DTOs the solver engine emits: the solution the driver consumes. + +pub mod order; +pub mod solution; diff --git a/crates/solana-solvers/src/domain/order.rs b/crates/solana-solvers/src/dto/order.rs similarity index 100% rename from crates/solana-solvers/src/domain/order.rs rename to crates/solana-solvers/src/dto/order.rs diff --git a/crates/solana-solvers/src/domain/solution.rs b/crates/solana-solvers/src/dto/solution.rs similarity index 94% rename from crates/solana-solvers/src/domain/solution.rs rename to crates/solana-solvers/src/dto/solution.rs index 047d86c014..421efb9892 100644 --- a/crates/solana-solvers/src/domain/solution.rs +++ b/crates/solana-solvers/src/dto/solution.rs @@ -83,7 +83,7 @@ impl Solution { /// The swap's instructions are carried verbatim as interactions, and /// its address lookup tables travel along so the driver can build the /// v0 transaction the instructions assume. - pub fn single( + pub fn new( id: u64, order_uid: OrderUid, order: &dex::Order, @@ -179,7 +179,7 @@ mod tests { #[test] fn sell_swap_maps_to_single_order_solution() { let order = order(dex::Side::Sell); - let solution = Solution::single(42, ORDER_UID, &order, swap()).unwrap(); + let solution = Solution::new(42, ORDER_UID, &order, swap()).unwrap(); assert_eq!(solution.id, 42); assert_eq!(solution.trades.len(), 1); @@ -199,7 +199,7 @@ mod tests { #[test] fn buy_swap_executes_in_buy_token_units() { - let solution = Solution::single(0, ORDER_UID, &order(dex::Side::Buy), swap()).unwrap(); + let solution = Solution::new(0, ORDER_UID, &order(dex::Side::Buy), swap()).unwrap(); assert_eq!(solution.trades[0].executed_amount, 2_000); } @@ -208,7 +208,7 @@ mod tests { let mut order = order(dex::Side::Sell); order.buy_mint = order.sell_mint; assert_eq!( - Solution::single(0, ORDER_UID, &order, swap()).unwrap_err(), + Solution::new(0, ORDER_UID, &order, swap()).unwrap_err(), Error::SameMint ); } @@ -218,14 +218,14 @@ mod tests { let mut swap = swap(); swap.out_amount = 0; assert_eq!( - Solution::single(0, ORDER_UID, &order(dex::Side::Sell), swap).unwrap_err(), + Solution::new(0, ORDER_UID, &order(dex::Side::Sell), swap).unwrap_err(), Error::ZeroAmount ); } #[test] fn wire_format_is_stable() { - let solution = Solution::single(1, ORDER_UID, &order(dex::Side::Sell), swap()).unwrap(); + let solution = Solution::new(1, ORDER_UID, &order(dex::Side::Sell), swap()).unwrap(); let json = serde_json::to_value(&solution).unwrap(); assert_eq!( diff --git a/crates/solana-solvers/src/lib.rs b/crates/solana-solvers/src/lib.rs index d3eb34ba42..3e7996bf05 100644 --- a/crates/solana-solvers/src/lib.rs +++ b/crates/solana-solvers/src/lib.rs @@ -7,7 +7,7 @@ pub mod api; mod cli; pub mod config; pub mod dex; -pub mod domain; +pub mod dto; mod run; pub use run::start; From 7febcb7c0629e05d72d55d93b470a796bb8bb35e Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Fri, 24 Jul 2026 10:38:55 +0000 Subject: [PATCH 19/21] refactor(solana-solvers): drop the unused fee field from the solve DTO --- crates/solana-solvers/src/dto/solution.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/solana-solvers/src/dto/solution.rs b/crates/solana-solvers/src/dto/solution.rs index 421efb9892..77f6cd51f9 100644 --- a/crates/solana-solvers/src/dto/solution.rs +++ b/crates/solana-solvers/src/dto/solution.rs @@ -38,9 +38,6 @@ pub struct Trade { /// Sell-token units for sell orders, buy-token units for buy orders. #[serde_as(as = "serde_with::DisplayFromStr")] pub executed_amount: u64, - /// Fee in sell-token units. - #[serde_as(as = "serde_with::DisplayFromStr")] - pub fee: u64, } /// A Solana instruction the solver supplies, carried verbatim. @@ -104,7 +101,6 @@ impl Solution { trades: vec![Trade { order_uid, executed_amount, - fee: 0, }], interactions: swap .instructions @@ -185,7 +181,6 @@ mod tests { assert_eq!(solution.trades.len(), 1); assert_eq!(solution.trades[0].order_uid, ORDER_UID); assert_eq!(solution.trades[0].executed_amount, 1_000); - assert_eq!(solution.trades[0].fee, 0); assert_eq!(solution.address_lookup_tables, vec![pubkey(7)]); // The instruction is carried verbatim, flags included. @@ -235,7 +230,6 @@ mod tests { "trades": [{ "orderUid": format!("0x{}", "08".repeat(32)), "executedAmount": "1000", - "fee": "0", }], "interactions": [{ "programId": pubkey(9).to_string(), From 0190efcca18e22e72fabfcaf74fff5cc9c014f72 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Fri, 24 Jul 2026 11:54:20 +0000 Subject: [PATCH 20/21] test(solana-solvers): drop redundant jupiter_live_solve, adapter live tests cover it --- .../solana-solvers/src/domain/solver/mod.rs | 43 +------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/crates/solana-solvers/src/domain/solver/mod.rs b/crates/solana-solvers/src/domain/solver/mod.rs index 4a9d00c739..0c908fb44f 100644 --- a/crates/solana-solvers/src/domain/solver/mod.rs +++ b/crates/solana-solvers/src/domain/solver/mod.rs @@ -51,17 +51,9 @@ pub async fn solve(quoter: &Q, auction: &Auction) -> Vec { mod tests { use { super::*, - crate::{ - config::JupiterConfig, - dto::{auction, order::OrderUid}, - }, - std::str::FromStr, + crate::dto::{auction, order::OrderUid}, }; - // USDC and wrapped SOL mints for the live test. - const USDC: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; - const WSOL: &str = "So11111111111111111111111111111111111111112"; - fn pubkey(byte: u8) -> Pubkey { Pubkey::new_from_array([byte; 32]) } @@ -129,37 +121,4 @@ mod tests { }; assert!(solve(&MockQuote, &auction).await.is_empty()); } - - /// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY` - /// for headroom. - #[tokio::test] - #[ignore] - async fn jupiter_live_solve() { - let dex = Dex::Jupiter( - dex::jupiter::Jupiter::new(&JupiterConfig { - endpoint: "https://api.jup.ag".parse().unwrap(), - api_key: std::env::var("JUPITER_API_KEY").ok(), - slippage_bps: 50, - enable_buy_orders: false, - }) - .unwrap(), - ); - let auction = Auction { - id: 1, - taker: Pubkey::from_str(WSOL).unwrap(), - orders: vec![auction::Order { - uid: OrderUid([7; 32]), - sell_mint: Pubkey::from_str(USDC).unwrap(), - buy_mint: Pubkey::from_str(WSOL).unwrap(), - buy_destination: Pubkey::from_str(WSOL).unwrap(), - amount: 1_000_000, - side: dex::Side::Sell, - }], - }; - - let solutions = solve(&dex, &auction).await; - - assert_eq!(solutions.len(), 1); - assert!(!solutions[0].interactions.is_empty()); - } } From 941d38f838bb32d28392ed13ce99f930333b4841 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 4 Aug 2026 07:19:18 +0000 Subject: [PATCH 21/21] Document where the swap route spends its input from --- crates/solana-solvers/src/dex/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/solana-solvers/src/dex/mod.rs b/crates/solana-solvers/src/dex/mod.rs index 96c5f7fa6b..b951cb3da8 100644 --- a/crates/solana-solvers/src/dex/mod.rs +++ b/crates/solana-solvers/src/dex/mod.rs @@ -46,6 +46,11 @@ pub enum Dex { impl Dex { /// Quote `order` for settlement signer `user`. + /// + /// The route spends its input from `user`'s ATA for the sell mint. + /// Jupiter has no source-account override, so the settlement must pull + /// the sell funds into that ATA, creating it if missing, before the + /// swap executes. pub async fn swap(&self, order: &Order, user: &Pubkey) -> Result { match self { Dex::Jupiter(jupiter) => jupiter.swap(order, user).await,