-
Notifications
You must be signed in to change notification settings - Fork 181
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
Open
squadgazzz
wants to merge
16
commits into
main
Choose a base branch
from
solana-solvers/pr3-solution-assembly
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
c05ec9a
feat(solana-solvers): PR 3 solution assembly (custom interactions, pr…
squadgazzz 822a212
Drop spec references from code comments
squadgazzz 2a43350
Trim solution module doc to the three deliberate absences
squadgazzz efe1c02
Introduce OrderUid newtype matching the indexer convention
squadgazzz 5f18acf
Move absence notes to where the decisions happen, drop tautological t…
squadgazzz c5104e6
Assemble a solution end to end in the live Jupiter sell test
squadgazzz 1df3840
Validate the assembled solution in the live test instead of printing it
squadgazzz 3aa1656
refactor(solana-solvers): match EVM OrderUid hex Display, plainer int…
squadgazzz 72452aa
feat(solana-solvers): add optional cu_estimate to the solution DTO, u…
squadgazzz d25f8ac
refactor(solana-solvers): flatten interactions to Vec<Instruction> ma…
squadgazzz 832bd62
refactor(solana-solvers): serialize solution amounts as decimal strings
squadgazzz 2f430df
refactor(solana-solvers): trim solution doc comments, drop redundant …
squadgazzz 7385b72
docs(solana-solvers): describe shared solution DTO by contract, not s…
squadgazzz 9887f1b
refactor(solana-solvers): move swap instructions into the solution in…
squadgazzz 030aca4
refactor(solana-solvers): use const_hex::Buffer for OrderUid, tidy so…
squadgazzz 18ba577
refactor(solana-solvers): drop clearing prices from the solve DTO
squadgazzz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| //! 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 buffer = const_hex::Buffer::<32, true>::new(); | ||
| f.write_str(buffer.format(&self.0)) | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,260 @@ | ||
| //! 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 as SolInstruction, pubkey::Pubkey}, | ||
| }; | ||
|
|
||
| /// A solution in the driver's `/solve` DTO. Trades fulfill auction orders, with | ||
| /// no JIT. | ||
| #[serde_as] | ||
| #[derive(Debug, Serialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| pub struct Solution { | ||
| pub id: u64, | ||
| pub trades: Vec<Trade>, | ||
| pub interactions: Vec<Instruction>, | ||
| /// Optional solver estimate of total settlement compute units. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub cu_estimate: Option<u64>, | ||
| /// 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. | ||
| #[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. | ||
| #[serde_as(as = "serde_with::DisplayFromStr")] | ||
| pub fee: u64, | ||
| } | ||
|
|
||
| /// A Solana instruction the solver supplies, carried verbatim. | ||
| #[serde_as] | ||
| #[derive(Debug, Serialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| pub struct Instruction { | ||
| #[serde_as(as = "serde_with::DisplayFromStr")] | ||
| pub program_id: Pubkey, | ||
| pub accounts: Vec<AccountMeta>, | ||
| /// Base64-encoded instruction data. | ||
| #[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, which is not a real trade. | ||
| #[error("sell and buy mint are the same")] | ||
| SameMint, | ||
| /// A zero quoted amount means the swap fills nothing. | ||
| #[error("quoted amount is zero")] | ||
| ZeroAmount, | ||
| } | ||
|
|
||
| impl Solution { | ||
| /// Wraps one quoted swap into a single-order 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( | ||
| 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, | ||
| trades: vec![Trade { | ||
| order_uid, | ||
| executed_amount, | ||
| fee: 0, | ||
| }], | ||
| interactions: swap | ||
| .instructions | ||
| .into_iter() | ||
| .map(Instruction::from_sdk) | ||
| .collect(), | ||
| cu_estimate: None, | ||
| address_lookup_tables: swap.address_lookup_tables, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl Instruction { | ||
| fn from_sdk(instruction: SolInstruction) -> Self { | ||
| Self { | ||
| program_id: instruction.program_id, | ||
| accounts: instruction | ||
| .accounts | ||
| .into_iter() | ||
| .map(|meta| AccountMeta { | ||
| pubkey: meta.pubkey, | ||
| is_signer: meta.is_signer, | ||
| is_writable: meta.is_writable, | ||
| }) | ||
| .collect(), | ||
| instruction_data: instruction.data, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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}; | ||
|
|
||
| const ORDER_UID: OrderUid = OrderUid([8; 32]); | ||
|
|
||
| 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![SolInstruction { | ||
| 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, ORDER_UID, &order, swap()).unwrap(); | ||
|
|
||
| assert_eq!(solution.id, 42); | ||
| 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. | ||
| 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] | ||
| fn buy_swap_executes_in_buy_token_units() { | ||
| let solution = Solution::single(0, ORDER_UID, &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, ORDER_UID, &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, 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 json = serde_json::to_value(&solution).unwrap(); | ||
|
|
||
| assert_eq!( | ||
| json, | ||
| serde_json::json!({ | ||
| "id": 1, | ||
| "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()], | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| #[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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.