-
Notifications
You must be signed in to change notification settings - Fork 183
feat(solana-solvers): PR 3 solution assembly (custom interactions, prices, lookup tables) #4652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
c05ec9a
822a212
2a43350
efe1c02
5f18acf
c5104e6
1df3840
3aa1656
72452aa
d25f8ac
832bd62
2f430df
7385b72
9887f1b
030aca4
18ba577
106bc86
7febcb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| //! Domain types the solver engine emits: the solution DTO the driver consumes. | ||
|
|
||
| pub mod order; | ||
| pub mod solution; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| //! CoW Protocol order identifier. | ||
|
|
||
| use std::fmt; | ||
|
|
||
| /// 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 { | ||
| 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()) | ||
|
jmg-duarte marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| impl fmt::Debug for OrderUid { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| write!(f, "{self}") | ||
| } | ||
| } | ||
|
|
||
| impl serde::Serialize for OrderUid { | ||
| fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | ||
| serializer.collect_str(self) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,276 @@ | ||
| //! Solution assembly: one quoted swap becomes one single-order solution in the | ||
| //! driver's `/solve` DTO. | ||
|
|
||
| use { | ||
| super::order::OrderUid, | ||
| 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<serde_with::DisplayFromStr, _>")] | ||
| pub prices: HashMap<Pubkey, u64>, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
#[serde_as(as = "HashMap<_, HexOrDecimalU256>")]
pub prices: HashMap<Address, U256>,
...
#[serde_as(as = "HexOrDecimalU256")]
pub executed_amount: U256,Two concerns:
Suggest serializing these as strings to match the EVM DTO. Fix this →
squadgazzz marked this conversation as resolved.
Outdated
|
||
| pub trades: Vec<Trade>, | ||
| pub interactions: Vec<Interaction>, | ||
| /// The address lookup tables the interactions assume, carried through so | ||
| /// the driver can build the v0 transaction around them. | ||
| #[serde_as(as = "Vec<serde_with::DisplayFromStr>")] | ||
| pub address_lookup_tables: Vec<Pubkey>, | ||
| } | ||
|
|
||
| /// A fulfillment of one auction order. | ||
| #[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. | ||
| 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 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. | ||
| #[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<AccountMeta>, | ||
| /// Base64, matching the aggregator wire encoding. | ||
| #[serde(serialize_with = "serialize_base64")] | ||
| pub instruction_data: Vec<u8>, | ||
| } | ||
|
|
||
| /// 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given all the effort done into removing UCPs, did we check with solvers if we should actually be adding the UCP concept back in?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! Dropped UCPs from the DTO. It looks like they won't be used at all. |
||
| /// 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( | ||
|
squadgazzz marked this conversation as resolved.
Outdated
|
||
| id: u64, | ||
| order_uid: OrderUid, | ||
| order: &dex::Order, | ||
| swap: dex::Swap, | ||
| ) -> Result<Self, Error> { | ||
| 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(), | ||
|
squadgazzz marked this conversation as resolved.
Outdated
squadgazzz marked this conversation as resolved.
Outdated
|
||
| 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<S: serde::Serializer>(data: &[u8], serializer: S) -> Result<S::Ok, S::Error> { | ||
| serializer.serialize_str(&BASE64_STANDARD.encode(data)) | ||
| } | ||
|
|
||
| #[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, OrderUid([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, OrderUid([8; 32])); | ||
|
squadgazzz marked this conversation as resolved.
Outdated
|
||
| 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, OrderUid([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, OrderUid([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, OrderUid([0; 32]), &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 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"], | ||
| 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"], | ||
| BASE64_STANDARD.encode([0xde, 0xad]) | ||
| ); | ||
| assert!( | ||
| json["interactions"][0]["accounts"][0]["isSigner"] | ||
| .as_bool() | ||
| .unwrap() | ||
| ); | ||
| assert_eq!(json["addressLookupTables"][0], pubkey(7).to_string()); | ||
| } | ||
|
|
||
| #[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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ pub mod api; | |
| mod cli; | ||
| pub mod config; | ||
| pub mod dex; | ||
| pub mod domain; | ||
| mod run; | ||
|
|
||
| pub use run::start; | ||
Uh oh!
There was an error while loading. Please reload this page.