diff --git a/crates/autopilot/src/driver_model.rs b/crates/autopilot/src/driver_model.rs index f2c26b6e81..c2cd99fc65 100644 --- a/crates/autopilot/src/driver_model.rs +++ b/crates/autopilot/src/driver_model.rs @@ -75,7 +75,7 @@ pub mod solve { #[serde(rename_all = "camelCase")] pub struct Token { pub address: H160, - #[serde_as(as = "Option")] + #[serde_as(as = "Option")] pub price: Option, pub trusted: bool, pub decimals: Option, diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index b46c697c14..bf323ad395 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -79,13 +79,13 @@ impl Settlement { let mut settlement = solver::settlement::Settlement::new( solution - .prices()? + .clearing_prices()? .into_iter() - .map(|asset| (asset.token.into(), asset.amount)) + .map(|asset| (asset.token.into(), asset.amount.into())) .collect(), ); - for trade in &solution.trades { + for trade in solution.trades() { let (boundary_order, execution) = match trade { competition::solution::Trade::Fulfillment(trade) => { // TODO: The `http_solver` module filters out orders with 0 @@ -130,13 +130,13 @@ impl Settlement { } let slippage_calculator = SlippageCalculator { - relative: to_big_decimal(solution.solver.slippage().relative.clone()), - absolute: solution.solver.slippage().absolute.map(Into::into), + relative: to_big_decimal(solution.solver().slippage().relative.clone()), + absolute: solution.solver().slippage().absolute.map(Into::into), }; let external_prices = ExternalPrices::try_from_auction_prices( native_token.address(), auction - .tokens + .tokens() .iter() .filter_map(|token| { token @@ -147,7 +147,7 @@ impl Settlement { )?; let slippage_context = slippage_calculator.context(&external_prices); - for interaction in &solution.interactions { + for interaction in solution.interactions() { let boundary_interaction = to_boundary_interaction( &slippage_context, settlement_contract.address().into(), @@ -161,8 +161,8 @@ impl Settlement { Ok(Self { inner: settlement, - solver: solution.solver.address(), - risk: solution.risk, + solver: solution.solver().address(), + risk: solution.risk(), }) } @@ -204,7 +204,7 @@ impl Settlement { let prices = ExternalPrices::try_from_auction_prices( eth.contracts().weth().address(), auction - .tokens + .tokens() .iter() .filter_map(|token| { token @@ -213,7 +213,7 @@ impl Settlement { }) .collect(), )?; - let gas_price = u256_to_big_rational(&auction.gas_price.effective().into()); + let gas_price = u256_to_big_rational(&auction.gas_price().effective().into()); let inputs = solver::objective_value::Inputs::from_settlement( &self.inner, &prices, @@ -239,8 +239,8 @@ fn to_boundary_order(order: &competition::Order) -> Order { data: OrderData { sell_token: order.sell.token.into(), buy_token: order.buy.token.into(), - sell_amount: order.sell.amount, - buy_amount: order.buy.amount, + sell_amount: order.sell.amount.into(), + buy_amount: order.buy.amount.into(), fee_amount: order.fee.user.into(), receiver: order.receiver.map(Into::into), valid_to: order.valid_to.into(), @@ -319,8 +319,8 @@ fn to_boundary_jit_order(domain: &DomainSeparator, order: &order::Jit) -> Order sell_token: order.sell.token.into(), buy_token: order.buy.token.into(), receiver: Some(order.receiver.into()), - sell_amount: order.sell.amount, - buy_amount: order.buy.amount, + sell_amount: order.sell.amount.into(), + buy_amount: order.buy.amount.into(), valid_to: order.valid_to.into(), app_data: AppDataHash(order.app_data.into()), fee_amount: order.fee.into(), @@ -407,11 +407,11 @@ pub fn to_boundary_interaction( let input = liquidity::MaxInput(eth::Asset { token: boundary_execution.input_max.token.into(), - amount: boundary_execution.input_max.amount, + amount: boundary_execution.input_max.amount.into(), }); let output = liquidity::ExactOutput(eth::Asset { token: boundary_execution.output.token.into(), - amount: boundary_execution.output.amount, + amount: boundary_execution.output.amount.into(), }); let interaction = match &liquidity.liquidity.kind { diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 0a742692fa..6769a6a8be 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -3,6 +3,7 @@ use { competition::{self, solution}, eth, }, + std::collections::HashMap, thiserror::Error, }; @@ -11,25 +12,112 @@ use { /// solving them. #[derive(Debug)] pub struct Auction { - /// [`None`] if this auction applies to a quote. - pub id: Option, - pub tokens: Vec, - pub orders: Vec, - pub gas_price: eth::GasPrice, - pub deadline: Deadline, + /// See the [`Self::id`] method. + id: Option, + /// See the [`Self::orders`] method. + orders: Vec, + /// The tokens that are used in the orders of this auction. + tokens: Tokens, + gas_price: eth::GasPrice, + deadline: Deadline, } impl Auction { - pub fn is_trusted(&self, token: eth::TokenAddress) -> bool { - self.tokens - .iter() - .find(|t| t.address == token) - .map(|token| token.trusted) - .unwrap_or(false) + pub fn new( + id: Option, + mut orders: Vec, + tokens: impl Iterator, + gas_price: eth::GasPrice, + deadline: Deadline, + weth: eth::WethAddress, + ) -> Result { + let tokens = Tokens(tokens.map(|token| (token.address, token)).collect()); + + // Ensure that tokens are included for each order. + if !orders.iter().all(|order| { + tokens.0.contains_key(&order.buy.token.wrap(weth)) + && tokens.0.contains_key(&order.sell.token.wrap(weth)) + }) { + return Err(InvalidTokens); + } + + // Sort orders such that most likely to be fulfilled come first. + orders.sort_by_key(|order| { + // Market orders are preferred over limit orders, as the expectation is that + // they should be immediately fulfillable. Liquidity orders come last, as they + // are the most niche and rarely used. + let class = match order.kind { + competition::order::Kind::Market => 2, + competition::order::Kind::Limit { .. } => 1, + competition::order::Kind::Liquidity => 0, + }; + std::cmp::Reverse(( + class, + // If the orders are of the same kind, then sort by likelihood of fulfillment + // based on token prices. + order.likelihood(&tokens), + )) + }); + + // TODO Filter out orders based on user balance + + Ok(Self { + id, + orders, + tokens, + gas_price, + deadline, + }) + } + + /// [`None`] if this auction applies to a quote. See + /// [`crate::domain::quote`]. + pub fn id(&self) -> Option { + self.id + } + + /// The orders for the auction. The orders are sorted such that those which + /// are more likely to be fulfilled come before less likely orders. + pub fn orders(&self) -> &[competition::Order] { + &self.orders + } + + /// The tokens used in the auction. + pub fn tokens(&self) -> &Tokens { + &self.tokens + } + + pub fn gas_price(&self) -> eth::GasPrice { + self.gas_price + } + + pub fn deadline(&self) -> Deadline { + self.deadline } } -#[derive(Debug)] +/// The tokens that are used in an auction. +#[derive(Debug, Default)] +pub struct Tokens(HashMap); + +impl Tokens { + pub fn get(&self, address: eth::TokenAddress) -> Token { + self.0.get(&address).cloned().unwrap_or(Token { + decimals: None, + symbol: None, + address, + price: None, + available_balance: Default::default(), + trusted: false, + }) + } + + pub fn iter(&self) -> impl Iterator { + self.0.values() + } +} + +#[derive(Debug, Clone)] pub struct Token { pub decimals: Option, pub symbol: Option, @@ -45,7 +133,22 @@ pub struct Token { /// The price of a token in wei. This represents how much wei is needed to buy /// 10**18 of another token. #[derive(Debug, Clone, Copy)] -pub struct Price(pub eth::Ether); +pub struct Price(eth::Ether); + +impl Price { + pub fn new(value: eth::Ether) -> Result { + if value.0.is_zero() { + Err(InvalidPrice) + } else { + Ok(Self(value)) + } + } + + /// Apply this price to some token amount, converting that token into ETH. + pub fn apply(self, amount: eth::TokenAmount) -> eth::Ether { + (amount.0 * self.0 .0).into() + } +} impl From for eth::U256 { fn from(value: Price) -> Self { @@ -121,3 +224,11 @@ pub struct DeadlineExceeded; #[derive(Debug, Error)] #[error("invalid auction id")] pub struct InvalidId; + +#[derive(Debug, Error)] +#[error("invalid auction tokens")] +pub struct InvalidTokens; + +#[derive(Debug, Error)] +#[error("price cannot be zero")] +pub struct InvalidPrice; diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 6386496539..bb681f37c1 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -51,7 +51,7 @@ impl Competition { .liquidity .fetch( &auction - .orders + .orders() .iter() .filter_map(|order| match order.kind { order::Kind::Market | order::Kind::Limit { .. } => { @@ -66,13 +66,13 @@ impl Competition { // Fetch the solutions from the solver. let solutions = self .solver - .solve(auction, &liquidity, auction.deadline.timeout()?) + .solve(auction, &liquidity, auction.deadline().timeout()?) .await?; // Empty solutions aren't useful, so discard them. let solutions = solutions.into_iter().filter(|solution| { if solution.is_empty() { - observe::empty_solution(self.solver.name(), solution.id); + observe::empty_solution(self.solver.name(), solution.id()); false } else { true @@ -81,9 +81,9 @@ impl Competition { // Encode the solutions into settlements. let settlements = join_all(solutions.map(|solution| async move { - observe::encoding(self.solver.name(), solution.id); + observe::encoding(self.solver.name(), solution.id()); ( - solution.id, + solution.id(), solution.encode(auction, &self.eth, &self.simulator).await, ) })) diff --git a/crates/driver/src/domain/competition/order/mod.rs b/crates/driver/src/domain/competition/order/mod.rs index ddedd5ab33..22fbac5238 100644 --- a/crates/driver/src/domain/competition/order/mod.rs +++ b/crates/driver/src/domain/competition/order/mod.rs @@ -1,12 +1,13 @@ use crate::{ domain::eth, infra::{blockchain, Ethereum}, - util::{self, Bytes}, + util::{self, conv, Bytes}, }; pub mod signature; pub use signature::Signature; +use {super::auction, bigdecimal::Zero, num::CheckedDiv}; /// An order in the auction. #[derive(Debug, Clone)] @@ -70,6 +71,18 @@ impl From for eth::U256 { } } +impl From for TargetAmount { + fn from(value: eth::TokenAmount) -> Self { + Self(value.0) + } +} + +impl From for eth::TokenAmount { + fn from(value: TargetAmount) -> Self { + Self(value.0) + } +} + /// Order fee denominated in the sell token. #[derive(Debug, Default, Clone)] pub struct Fee { @@ -129,7 +142,7 @@ impl Order { pub fn solver_sell(&self) -> eth::Asset { if let Kind::Limit { surplus_fee } = self.kind { eth::Asset { - amount: self.sell.amount - surplus_fee.0, + amount: (self.sell.amount.0 - surplus_fee.0).into(), token: self.sell.token, } } else { @@ -159,6 +172,25 @@ impl Order { pub fn solver_determines_fee(&self) -> bool { self.is_partial() && matches!(self.kind, Kind::Limit { .. }) } + + /// The likelihood that this order will be fulfilled, based on token prices. + /// A larger value means that the order is more likely to be fulfilled. + /// This is used to prioritize orders when solving. + pub fn likelihood(&self, tokens: &auction::Tokens) -> num::BigRational { + match ( + tokens.get(self.buy.token).price, + tokens.get(self.sell.token).price, + ) { + (Some(buy_price), Some(sell_price)) => { + let buy = buy_price.apply(self.buy.amount); + let sell = sell_price.apply(self.sell.amount); + conv::u256::to_big_rational(buy.0) + .checked_div(&conv::u256::to_big_rational(sell.0)) + .unwrap_or_else(num::BigRational::zero) + } + _ => num::BigRational::zero(), + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/driver/src/domain/competition/solution/interaction.rs b/crates/driver/src/domain/competition/solution/interaction.rs index ef702344da..3dc6651808 100644 --- a/crates/driver/src/domain/competition/solution/interaction.rs +++ b/crates/driver/src/domain/competition/solution/interaction.rs @@ -58,7 +58,7 @@ impl Interaction { address, token: interaction.input.token, }, - amount: interaction.input.amount, + amount: interaction.input.amount.into(), } .into()] } diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 197f46a800..4261097c00 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -25,23 +25,78 @@ pub mod trade; pub use {interaction::Interaction, settlement::Settlement, trade::Trade}; +// TODO Add a constructor and ensure that the clearing prices are included for +// each trade /// A solution represents a set of orders which the solver has found an optimal /// way to settle. A [`Solution`] is generated by a solver as a response to a /// [`competition::Auction`]. See also [`settlement::Settlement`]. #[derive(Debug, Clone)] pub struct Solution { - pub id: Id, - /// Trades settled by this solution. - pub trades: Vec, - pub prices: ClearingPrices, - pub interactions: Vec, - pub weth: eth::WethAddress, - /// The solver which generated this solution. - pub solver: Solver, - pub risk: Risk, + id: Id, + trades: Vec, + prices: HashMap, + interactions: Vec, + solver: Solver, + risk: Risk, + weth: eth::WethAddress, } impl Solution { + pub fn new( + id: Id, + trades: Vec, + prices: HashMap, + interactions: Vec, + solver: Solver, + risk: Risk, + weth: eth::WethAddress, + ) -> Result { + let solution = Self { + id, + trades, + prices, + interactions, + solver, + risk, + weth, + }; + + // Check that the solution includes clearing prices for all user trades. + if solution.user_trades().all(|trade| { + solution.clearing_price(trade.order().sell.token).is_some() + && solution.clearing_price(trade.order().buy.token).is_some() + }) { + Ok(solution) + } else { + Err(InvalidClearingPrices) + } + } + + /// The ID of this solution. + pub fn id(&self) -> Id { + self.id + } + + /// Trades settled by this solution. + pub fn trades(&self) -> &[Trade] { + &self.trades + } + + /// Interactions executed by this solution. + pub fn interactions(&self) -> &[Interaction] { + &self.interactions + } + + /// The solver which generated this solution. + pub fn solver(&self) -> &Solver { + &self.solver + } + + /// The risk of this solution. + pub fn risk(&self) -> Risk { + self.risk + } + /// Approval interactions necessary for encoding the settlement. pub async fn approvals( &self, @@ -115,14 +170,19 @@ impl Solution { Settlement::encode(self, auction, eth, simulator).await } - /// The clearing prices, represented as a list of assets. If there are any - /// orders which buy ETH, this will contain the correct ETH price. - pub fn prices(&self) -> Result, Error> { - let prices = self - .prices - .0 - .iter() - .map(|(&token, &amount)| eth::Asset { token, amount }); + /// Token prices settled by this solution, expressed using an arbitrary + /// reference unit chosen by the solver. These values are only + /// meaningful in relation to each others. + /// + /// The rule which relates two prices for tokens X and Y is: + /// ``` + /// amount_x * price_x = amount_y * price_y + /// ``` + pub fn clearing_prices(&self) -> Result, Error> { + let prices = self.prices.iter().map(|(&token, &amount)| eth::Asset { + token, + amount: amount.into(), + }); if self.user_trades().any(|trade| trade.order().buys_eth()) { // The solution contains an order which buys ETH. Solvers only produce solutions @@ -148,44 +208,22 @@ impl Solution { // Add a clearing price for ETH equal to WETH. prices.push(eth::Asset { token: eth::ETH_TOKEN, - amount: self - .prices - .0 - .get(&self.weth.into()) - .ok_or(Error::MissingWethClearingPrice)? - .to_owned(), + amount: self.prices[&self.weth.into()].to_owned().into(), }); return Ok(prices); } - // TODO: We should probably filter out all unused prices. + // TODO: We should probably filter out all unused prices to save gas. Ok(prices.collect_vec()) } /// Clearing price for the given token. - pub fn price(&self, token: eth::TokenAddress) -> Option { + pub fn clearing_price(&self, token: eth::TokenAddress) -> Option { // The clearing price of ETH is equal to WETH. let token = token.wrap(self.weth); - self.prices.0.get(&token).map(ToOwned::to_owned) - } -} - -/// Token prices for this solution, expressed using an arbitrary reference -/// unit chosen by the solver. These values are only meaningful in relation -/// to each others. -/// -/// The rule which relates two prices for tokens X and Y is: -/// ``` -/// amount_x * price_x = amount_y * price_y -/// ``` -#[derive(Debug, Clone)] -pub struct ClearingPrices(HashMap); - -impl ClearingPrices { - pub fn new(prices: HashMap) -> Self { - Self(prices) + self.prices.get(&token).map(ToOwned::to_owned) } } @@ -293,8 +331,6 @@ pub enum Error { Blockchain(#[from] blockchain::Error), #[error("boundary error: {0:?}")] Boundary(#[from] boundary::Error), - #[error("missing weth clearing price")] - MissingWethClearingPrice, #[error("simulation error: {0:?}")] Simulation(#[from] simulator::Error), #[error( @@ -319,3 +355,7 @@ pub enum Error { #[derive(Debug, Error)] #[error("the solution deadline has been exceeded")] pub struct DeadlineExceeded; + +#[derive(Debug, Error)] +#[error("invalid clearing prices")] +pub struct InvalidClearingPrices; diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 9142e27627..8c83940bc4 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -73,7 +73,8 @@ impl Settlement { .iter() .flat_map(|interaction| interaction.inputs()) { - *flow.entry(input.token).or_default() -= util::conv::u256::to_big_int(input.amount); + *flow.entry(input.token).or_default() -= + util::conv::u256::to_big_int(input.amount.into()); } // Interaction outputs represent flow into the contract, i.e. positive flow. @@ -82,7 +83,8 @@ impl Settlement { .iter() .flat_map(|interaction| interaction.outputs()) { - *flow.entry(output.token).or_default() += util::conv::u256::to_big_int(output.amount); + *flow.entry(output.token).or_default() += + util::conv::u256::to_big_int(output.amount.into()); } // For trades, the sold amounts are always entering the contract (positive @@ -90,11 +92,12 @@ impl Settlement { // (negative flow). for trade in solution.trades.iter() { let trade::Execution { sell, buy } = trade.execution(&solution)?; - *flow.entry(sell.token).or_default() += util::conv::u256::to_big_int(sell.amount); + *flow.entry(sell.token).or_default() += + util::conv::u256::to_big_int(sell.amount.into()); // Within the settlement contract, the orders which buy ETH are wrapped into // WETH, and hence contribute to WETH flow. *flow.entry(buy.token.wrap(solution.weth)).or_default() -= - util::conv::u256::to_big_int(buy.amount); + util::conv::u256::to_big_int(buy.amount.into()); } if flow.values().any(|v| v.is_negative()) { @@ -111,7 +114,7 @@ impl Settlement { interaction .inputs() .iter() - .all(|asset| auction.is_trusted(asset.token)) + .all(|asset| auction.tokens().get(asset.token).trusted) }) { return Err(Error::UntrustedInternalization); @@ -120,7 +123,7 @@ impl Settlement { // Encode the solution into a settlement. let boundary = boundary::Settlement::encode(eth, &solution, auction).await?; Self::new( - auction.id.unwrap(), + auction.id().unwrap(), [(solution.id, solution)].into(), boundary, eth, diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index bf164aed63..10276957c9 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -180,6 +180,15 @@ impl Trade { &self, solution: &competition::Solution, ) -> Result { + #[derive(Debug, Clone, Copy)] + struct ExecutionParams { + side: order::Side, + kind: order::Kind, + sell: eth::Asset, + buy: eth::Asset, + executed: order::TargetAmount, + } + // Values needed to calculate the executed amounts. let ExecutionParams { side, @@ -218,11 +227,11 @@ impl Trade { // Market orders use clearing prices to calculate the executed amounts. See the // [`competition::Solution::prices`] field for an explanation of how these work. let sell_price = solution - .price(sell.token) + .clearing_price(sell.token) .ok_or(ExecutionError::ClearingPriceMissing(sell.token))? .to_owned(); let buy_price = solution - .price(buy.token) + .clearing_price(buy.token) .ok_or(ExecutionError::ClearingPriceMissing(buy.token))? .to_owned(); match side { @@ -237,7 +246,8 @@ impl Trade { .checked_mul(buy_price) .ok_or(ExecutionError::Overflow)? .checked_div(sell_price) - .ok_or(ExecutionError::Overflow)?, + .ok_or(ExecutionError::Overflow)? + .into(), token: sell.token, }, }, @@ -252,7 +262,8 @@ impl Trade { .checked_mul(sell_price) .ok_or(ExecutionError::Overflow)? .checked_ceil_div(&buy_price) - .ok_or(ExecutionError::Overflow)?, + .ok_or(ExecutionError::Overflow)? + .into(), token: buy.token, }, }, @@ -270,10 +281,12 @@ impl Trade { sell: eth::Asset { amount: sell .amount + .0 .checked_mul(executed.into()) .ok_or(ExecutionError::Overflow)? - .checked_div(buy.amount) - .ok_or(ExecutionError::Overflow)?, + .checked_div(buy.amount.into()) + .ok_or(ExecutionError::Overflow)? + .into(), token: sell.token, }, }, @@ -285,10 +298,12 @@ impl Trade { buy: eth::Asset { amount: buy .amount + .0 .checked_mul(executed.into()) .ok_or(ExecutionError::Overflow)? - .checked_div(sell.amount) - .ok_or(ExecutionError::Overflow)?, + .checked_div(sell.amount.into()) + .ok_or(ExecutionError::Overflow)? + .into(), token: buy.token, }, }, @@ -314,11 +329,11 @@ impl Trade { .expect("all limit orders must have a surplus fee"); let sell_price = solution - .price(sell.token) + .clearing_price(sell.token) .ok_or(ExecutionError::ClearingPriceMissing(sell.token))? .to_owned(); let buy_price = solution - .price(buy.token) + .clearing_price(buy.token) .ok_or(ExecutionError::ClearingPriceMissing(buy.token))? .to_owned(); match side { @@ -339,7 +354,8 @@ impl Trade { // amount by the surplus fee. We know that the user placed an order // big enough to cover the surplus fee. .checked_add(surplus_fee.into()) - .ok_or(ExecutionError::Overflow)?, + .ok_or(ExecutionError::Overflow)? + .into(), token: sell.token, }, }, @@ -361,7 +377,8 @@ impl Trade { .checked_mul(sell_price) .ok_or(ExecutionError::Overflow)? .checked_ceil_div(&buy_price) - .ok_or(ExecutionError::Overflow)?, + .ok_or(ExecutionError::Overflow)? + .into(), token: buy.token, }, }, @@ -380,15 +397,6 @@ pub struct Execution { pub buy: eth::Asset, } -#[derive(Debug, Clone, Copy)] -struct ExecutionParams { - side: order::Side, - kind: order::Kind, - sell: eth::Asset, - buy: eth::Asset, - executed: order::TargetAmount, -} - #[derive(Debug, thiserror::Error)] #[error("invalid executed amount")] pub struct InvalidExecutedAmount; diff --git a/crates/driver/src/domain/eth/mod.rs b/crates/driver/src/domain/eth/mod.rs index 05190ac618..4b233fc374 100644 --- a/crates/driver/src/domain/eth/mod.rs +++ b/crates/driver/src/domain/eth/mod.rs @@ -182,6 +182,36 @@ impl TokenAddress { } } +/// An ERC20 token amount. +/// +/// https://eips.ethereum.org/EIPS/eip-20 +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TokenAmount(pub U256); + +impl From for TokenAmount { + fn from(value: U256) -> Self { + Self(value) + } +} + +impl From for U256 { + fn from(value: TokenAmount) -> Self { + value.0 + } +} + +impl From for TokenAmount { + fn from(value: u128) -> Self { + Self(value.into()) + } +} + +impl std::fmt::Display for TokenAmount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + /// The address of the WETH contract. #[derive(Debug, Clone, Copy)] pub struct WethAddress(pub TokenAddress); @@ -220,7 +250,7 @@ impl From for ContractAddress { /// particular token. #[derive(Debug, Clone, Copy)] pub struct Asset { - pub amount: U256, + pub amount: TokenAmount, pub token: TokenAddress, } diff --git a/crates/driver/src/domain/quote.rs b/crates/driver/src/domain/quote.rs index e5425182f7..d937aee5cb 100644 --- a/crates/driver/src/domain/quote.rs +++ b/crates/driver/src/domain/quote.rs @@ -1,4 +1,5 @@ use { + super::competition::auction, crate::{ boundary, domain::{ @@ -29,10 +30,10 @@ pub struct Quote { impl Quote { fn new(eth: &Ethereum, order: &Order, solution: competition::Solution) -> Result { let sell_price = solution - .price(order.tokens.sell) + .clearing_price(order.tokens.sell) .ok_or(QuotingFailed::ClearingSellMissing)?; let buy_price = solution - .price(order.tokens.buy) + .clearing_price(order.tokens.buy) .ok_or(QuotingFailed::ClearingBuyMissing)?; let amount = match order.side { order::Side::Sell => conv::u256::from_big_rational( @@ -48,8 +49,8 @@ impl Quote { }; Ok(Self { amount, - interactions: boundary::quote::encode_interactions(eth, &solution.interactions)?, - solver: solution.solver.address(), + interactions: boundary::quote::encode_interactions(eth, solution.interactions())?, + solver: solution.solver().address(), }) } } @@ -78,7 +79,11 @@ impl Order { let gas_price = eth.gas_price().await?; let timeout = self.deadline.timeout()?; let solutions = solver - .solve(&self.fake_auction(gas_price), &liquidity, timeout) + .solve( + &self.fake_auction(gas_price, eth.contracts().weth_address()), + &liquidity, + timeout, + ) .await?; Quote::new( eth, @@ -92,11 +97,14 @@ impl Order { ) } - fn fake_auction(&self, gas_price: eth::GasPrice) -> competition::Auction { - competition::Auction { - id: None, - tokens: Default::default(), - orders: vec![competition::Order { + fn fake_auction( + &self, + gas_price: eth::GasPrice, + weth: eth::WethAddress, + ) -> competition::Auction { + competition::Auction::new( + None, + vec![competition::Order { uid: Default::default(), receiver: None, valid_to: util::Timestamp::MAX, @@ -107,7 +115,6 @@ impl Order { kind: competition::order::Kind::Market, app_data: Default::default(), partial: competition::order::Partial::No, - // TODO add actual pre- and post-interactions (#1491) pre_interactions: Default::default(), post_interactions: Default::default(), sell_token_balance: competition::order::SellTokenBalance::Erc20, @@ -118,9 +125,30 @@ impl Order { signer: Default::default(), }, }], - gas_price: gas_price.effective().into(), - deadline: Default::default(), - } + [ + auction::Token { + decimals: None, + symbol: None, + address: self.tokens.sell, + price: None, + available_balance: Default::default(), + trusted: false, + }, + auction::Token { + decimals: None, + symbol: None, + address: self.tokens.buy, + price: None, + available_balance: Default::default(), + trusted: false, + }, + ] + .into_iter(), + gas_price.effective().into(), + Default::default(), + weth, + ) + .unwrap() } /// The asset being bought, or [`eth::U256::one`] if this is a sell, to @@ -128,7 +156,7 @@ impl Order { fn buy(&self) -> eth::Asset { match self.side { order::Side::Sell => eth::Asset { - amount: eth::U256::one(), + amount: eth::U256::one().into(), token: self.tokens.buy, }, order::Side::Buy => eth::Asset { @@ -151,7 +179,7 @@ impl Order { // contract, so buy orders requiring excessively large sell amounts // would not work anyway. order::Side::Buy => eth::Asset { - amount: eth::U256::one() << 192, + amount: (eth::U256::one() << 192).into(), token: self.tokens.sell, }, } diff --git a/crates/driver/src/infra/api/error.rs b/crates/driver/src/infra/api/error.rs index 59edbf3f41..7ce098986a 100644 --- a/crates/driver/src/infra/api/error.rs +++ b/crates/driver/src/infra/api/error.rs @@ -16,6 +16,7 @@ enum Kind { Unknown, InvalidAuctionId, MissingSurplusFee, + InvalidTokens, QuoteSameTokens, } @@ -37,6 +38,9 @@ impl From for (hyper::StatusCode, axum::Json) { Kind::InvalidAuctionId => "Invalid ID specified in the auction", Kind::MissingSurplusFee => "Auction contains a limit order with no surplus fee", Kind::QuoteSameTokens => "Invalid quote with same buy and sell tokens", + Kind::InvalidTokens => { + "Invalid tokens specified in the auction, the tokens for some orders are missing" + } }; ( hyper::StatusCode::BAD_REQUEST, @@ -79,6 +83,7 @@ impl From for (hyper::StatusCode, axum::Json) let error = match value { api::routes::AuctionError::InvalidAuctionId => Kind::InvalidAuctionId, api::routes::AuctionError::MissingSurplusFee => Kind::MissingSurplusFee, + api::routes::AuctionError::InvalidTokens => Kind::InvalidTokens, api::routes::AuctionError::GasPrice(_) => Kind::Unknown, }; error.into() diff --git a/crates/driver/src/infra/api/routes/solve/dto/auction.rs b/crates/driver/src/infra/api/routes/solve/dto/auction.rs index 1dcf2dab4c..8edf75ef85 100644 --- a/crates/driver/src/infra/api/routes/solve/dto/auction.rs +++ b/crates/driver/src/infra/api/routes/solve/dto/auction.rs @@ -15,22 +15,9 @@ use { impl Auction { pub async fn into_domain(self, eth: &Ethereum) -> Result { - Ok(competition::Auction { - id: Some(self.id.try_into()?), - tokens: self - .tokens - .into_iter() - .map(|token| competition::auction::Token { - decimals: token.decimals, - symbol: token.symbol, - address: token.address.into(), - price: token.price.map(Into::into), - available_balance: Default::default(), - trusted: token.trusted, - }) - .collect(), - orders: self - .orders + competition::Auction::new( + Some(self.id.try_into()?), + self.orders .into_iter() .map(|order| { Ok(competition::Order { @@ -38,11 +25,11 @@ impl Auction { receiver: order.receiver.map(Into::into), valid_to: order.valid_to.into(), buy: eth::Asset { - amount: order.buy_amount, + amount: order.buy_amount.into(), token: order.buy_token.into(), }, sell: eth::Asset { - amount: order.sell_amount, + amount: order.sell_amount.into(), token: order.sell_token.into(), }, side: match order.kind { @@ -129,10 +116,22 @@ impl Auction { }, }) }) - .try_collect::<_, _, Error>()?, - gas_price: eth.gas_price().await.map_err(Error::GasPrice)?, - deadline: self.deadline.into(), - }) + .try_collect::<_, Vec<_>, Error>()?, + self.tokens + .into_iter() + .map(|token| competition::auction::Token { + decimals: token.decimals, + symbol: token.symbol, + address: token.address.into(), + price: token.price.map(Into::into), + available_balance: Default::default(), + trusted: token.trusted, + }), + eth.gas_price().await.map_err(Error::GasPrice)?, + self.deadline.into(), + eth.contracts().weth_address(), + ) + .map_err(Into::into) } } @@ -142,6 +141,8 @@ pub enum Error { InvalidAuctionId, #[error("surplus fee is missing for limit order")] MissingSurplusFee, + #[error("invalid tokens in auction")] + InvalidTokens, #[error("error getting gas price")] GasPrice(#[source] crate::infra::blockchain::Error), } @@ -152,6 +153,12 @@ impl From for Error { } } +impl From for Error { + fn from(_value: auction::InvalidTokens) -> Self { + Self::InvalidTokens + } +} + #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/crates/driver/src/infra/solver/dto/auction.rs b/crates/driver/src/infra/solver/dto/auction.rs index 31e3275aa1..c00807e250 100644 --- a/crates/driver/src/infra/solver/dto/auction.rs +++ b/crates/driver/src/infra/solver/dto/auction.rs @@ -17,7 +17,7 @@ impl Auction { weth: eth::WethAddress, ) -> Self { let mut tokens: HashMap = auction - .tokens + .tokens() .iter() .map(|token| { ( @@ -34,16 +34,16 @@ impl Auction { .collect(); Self { - id: auction.id.as_ref().map(ToString::to_string), + id: auction.id().as_ref().map(ToString::to_string), orders: auction - .orders + .orders() .iter() .map(|order| Order { uid: order.uid.into(), sell_token: order.solver_sell().token.into(), buy_token: order.solver_buy(weth).token.into(), - sell_amount: order.solver_sell().amount, - buy_amount: order.solver_buy(weth).amount, + sell_amount: order.solver_sell().amount.into(), + buy_amount: order.solver_buy(weth).amount.into(), fee_amount: order.fee.solver.into(), kind: match order.side { competition::order::Side::Buy => Kind::Buy, @@ -76,7 +76,7 @@ impl Auction { ( asset.token.into(), ConstantProductReserve { - balance: asset.amount, + balance: asset.amount.into(), }, ) }) @@ -124,7 +124,7 @@ impl Auction { ( asset.token.into(), ConstantProductReserve { - balance: asset.amount, + balance: asset.amount.into(), }, ) }) @@ -136,7 +136,7 @@ impl Auction { }) .collect(), tokens, - effective_gas_price: auction.gas_price.effective().into(), + effective_gas_price: auction.gas_price().effective().into(), deadline: timeout.deadline(), } } diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index bc392ad0cd..121bb2da5b 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -21,15 +21,15 @@ impl Solutions { self.solutions .into_iter() .map(|solution| { - Ok(competition::Solution { - id: solution.id.into(), - trades: solution + competition::Solution::new( + solution.id.into(), + solution .trades .into_iter() .map(|trade| match trade { Trade::Fulfillment(fulfillment) => { let order = auction - .orders + .orders() .iter() .find(|order| order.uid == fulfillment.order) // TODO this error should reference the UID @@ -57,11 +57,11 @@ impl Solutions { competition::solution::trade::Jit::new( competition::order::Jit { sell: eth::Asset { - amount: jit.order.sell_amount, + amount: jit.order.sell_amount.into(), token: jit.order.sell_token.into(), }, buy: eth::Asset { - amount: jit.order.buy_amount, + amount: jit.order.buy_amount.into(), token: jit.order.buy_token.into(), }, fee: jit.order.fee_amount.into(), @@ -121,14 +121,12 @@ impl Solutions { )), }) .try_collect()?, - prices: competition::solution::ClearingPrices::new( - solution - .prices - .into_iter() - .map(|(address, price)| (address.into(), price)) - .collect(), - ), - interactions: solution + solution + .prices + .into_iter() + .map(|(address, price)| (address.into(), price)) + .collect(), + solution .interactions .into_iter() .map(|interaction| match interaction { @@ -156,7 +154,7 @@ impl Solutions { .inputs .into_iter() .map(|input| eth::Asset { - amount: input.amount, + amount: input.amount.into(), token: input.token.into(), }) .collect(), @@ -164,7 +162,7 @@ impl Solutions { .outputs .into_iter() .map(|input| eth::Asset { - amount: input.amount, + amount: input.amount.into(), token: input.token.into(), }) .collect(), @@ -184,11 +182,11 @@ impl Solutions { competition::solution::interaction::Liquidity { liquidity, input: eth::Asset { - amount: interaction.input_amount, + amount: interaction.input_amount.into(), token: interaction.input_token.into(), }, output: eth::Asset { - amount: interaction.output_amount, + amount: interaction.output_amount.into(), token: interaction.output_token.into(), }, internalize: interaction.internalize, @@ -197,10 +195,10 @@ impl Solutions { } }) .try_collect()?, + solver.clone(), + solution.risk.into(), weth, - solver: solver.clone(), - risk: solution.risk.into(), - }) + ).map_err(|competition::solution::InvalidClearingPrices| super::Error("invalid clearing prices")) }) .collect() } diff --git a/crates/driver/src/infra/solver/mod.rs b/crates/driver/src/infra/solver/mod.rs index 214cc09803..dc153ebff1 100644 --- a/crates/driver/src/infra/solver/mod.rs +++ b/crates/driver/src/infra/solver/mod.rs @@ -147,7 +147,7 @@ impl Solver { let solutions = res.into_domain(auction, liquidity, weth, self.clone())?; // Ensure that solution IDs are unique. - let ids: HashSet<_> = solutions.iter().map(|solution| solution.id).collect(); + let ids: HashSet<_> = solutions.iter().map(|solution| solution.id()).collect(); if ids.len() != solutions.len() { return Err(Error::RepeatedSolutionIds); } diff --git a/crates/driver/src/tests/cases/merge_settlements.rs b/crates/driver/src/tests/cases/merge_settlements.rs index 4d61c6b1dc..f38302172a 100644 --- a/crates/driver/src/tests/cases/merge_settlements.rs +++ b/crates/driver/src/tests/cases/merge_settlements.rs @@ -8,12 +8,12 @@ use crate::tests::{ #[ignore] async fn possible() { let test = setup() - .pool(ab_pool()) .pool(cd_pool()) - .order(ab_order()) + .pool(ab_pool()) .order(cd_order()) - .solution(ab_solution()) + .order(ab_order()) .solution(cd_solution()) + .solution(ab_solution()) .done() .await; @@ -39,13 +39,13 @@ async fn possible() { async fn impossible() { let test = setup() .pool(ab_pool()) + .order(ab_order().rename("reduced order").reduce_amount(1000000000000000u128.into())) .order(ab_order()) - .order(ab_order().rename("second order").reduce_amount(1000000000000000u128.into())) // These two solutions result in different clearing prices (due to different surplus), // so they can't be merged. .solution(ab_solution()) .solution(Solution { - orders: vec!["second order"], + orders: vec!["reduced order"], ..ab_solution().reduce_score() }) .done() diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index 5a9288def8..6249e41638 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -7,6 +7,7 @@ pub mod internalization; pub mod merge_settlements; pub mod multiple_solutions; pub mod negative_scores; +pub mod order_sorting; pub mod quote; pub mod risk; pub mod settle; diff --git a/crates/driver/src/tests/cases/negative_scores.rs b/crates/driver/src/tests/cases/negative_scores.rs index 82d361f07f..b3e1a91249 100644 --- a/crates/driver/src/tests/cases/negative_scores.rs +++ b/crates/driver/src/tests/cases/negative_scores.rs @@ -26,8 +26,8 @@ async fn no_valid_solutions() { async fn one_valid_solution() { let test = setup() .pool(ab_pool()) - .order(ab_order()) .order(ab_order().rename("no surplus").no_surplus()) + .order(ab_order()) .solution(ab_solution()) // This solution has no surplus, and hence a negative score, so it gets skipped. .solution(Solution { diff --git a/crates/driver/src/tests/cases/order_sorting.rs b/crates/driver/src/tests/cases/order_sorting.rs new file mode 100644 index 0000000000..813ddf0fd9 --- /dev/null +++ b/crates/driver/src/tests/cases/order_sorting.rs @@ -0,0 +1,36 @@ +use crate::tests::{ + setup, + setup::{ab_order, ab_pool, ab_solution}, +}; + +/// Test that orders are sorted correctly before being sent to the solver: +/// market orders come before limit orders, and orders that are more likely to +/// fulfill come before orders that are less likely (according to token prices +/// in ETH). +#[tokio::test] +#[ignore] +async fn test() { + let test = setup() + .pool(ab_pool()) + // Orders with better price ratios come first. + .order( + ab_order() + .reduce_amount(1000000000000000u128.into()), + ) + .order(ab_order().rename("second order")) + // Limit orders come after market orders. + .order( + ab_order() + .rename("third order") + .limit() + .reduce_amount(1000000000000000u128.into()), + ) + .order(ab_order().rename("fourth order").limit()) + .solution(ab_solution()) + .done() + .await; + + // Only check that the solve endpoint can be called successfully, which means + // that the solver received the orders sorted. + test.solve().await.ok(); +} diff --git a/crates/driver/src/tests/setup/blockchain.rs b/crates/driver/src/tests/setup/blockchain.rs index bf967dc8ee..cb15f1b5b9 100644 --- a/crates/driver/src/tests/setup/blockchain.rs +++ b/crates/driver/src/tests/setup/blockchain.rs @@ -81,19 +81,19 @@ pub struct Solution { #[derive(Debug, Clone)] pub struct Fulfillment { - pub quote: Quote, + pub quoted_order: QuotedOrder, pub interactions: Vec, } /// An order for which buy and sell amounts have been calculated. #[derive(Debug, Clone)] -pub struct Quote { +pub struct QuotedOrder { pub order: Order, pub buy: eth::U256, pub sell: eth::U256, } -impl Quote { +impl QuotedOrder { /// The buy amount with the surplus factor. pub fn buy_amount(&self) -> eth::U256 { match self.order.side { @@ -507,14 +507,14 @@ impl Blockchain { /// Quote an order using a UniswapV2 pool. This determines the buy and sell /// amount of the order. - pub async fn quote(&self, order: &Order) -> Quote { + pub async fn quote(&self, order: &Order) -> QuotedOrder { let pair = self.find_pair(order); let executed_sell = order.sell_amount; let executed_buy = pair.pool.out(Asset { amount: order.sell_amount, token: order.sell_token, }); - Quote { + QuotedOrder { order: order.clone(), buy: executed_buy, sell: executed_sell, @@ -608,7 +608,7 @@ impl Blockchain { .unwrap() .0; fulfillments.push(Fulfillment { - quote: quote.clone(), + quoted_order: quote.clone(), interactions: vec![ Interaction { address: sell_token.address(), @@ -634,14 +634,16 @@ impl Blockchain { inputs: vec![eth::Asset { token: sell_token.address().into(), // Surplus fees stay in the contract. - amount: quote.sell - quote.order.surplus_fee() + amount: (quote.sell - quote.order.surplus_fee() + quote.order.execution_diff.increase_sell - - quote.order.execution_diff.decrease_sell, + - quote.order.execution_diff.decrease_sell) + .into(), }], outputs: vec![eth::Asset { token: buy_token.address().into(), - amount: quote.buy + quote.order.execution_diff.increase_buy - - quote.order.execution_diff.decrease_buy, + amount: (quote.buy + quote.order.execution_diff.increase_buy + - quote.order.execution_diff.decrease_buy) + .into(), }], internalize: order.internalize, }, diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index bd84b7e74c..b4d22350e9 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -9,6 +9,7 @@ use { infra::time, tests::hex_address, }, + rand::seq::SliceRandom, secp256k1::SecretKey, serde_json::json, std::{io::Write, net::SocketAddr, path::PathBuf}, @@ -64,7 +65,11 @@ impl Driver { pub fn solve_req(test: &Test) -> serde_json::Value { let mut tokens_json = Vec::new(); let mut orders_json = Vec::new(); - for quote in test.quotes.iter() { + // The orders are shuffled before being sent to the driver, to ensure that the + // driver sorts them correctly before forwarding them to the solver. + let mut quotes = test.quoted_orders.clone(); + quotes.shuffle(&mut rand::thread_rng()); + for quote in quotes.iter() { orders_json.push(json!({ "uid": quote.order_uid(&test.blockchain), "sellToken": hex_address(test.blockchain.get_token(quote.order.sell_token)), @@ -102,14 +107,14 @@ pub fn solve_req(test: &Test) -> serde_json::Value { } for fulfillment in test.fulfillments.iter() { tokens_json.push(json!({ - "address": hex_address(test.blockchain.get_token_wrapped(fulfillment.quote.order.sell_token)), + "address": hex_address(test.blockchain.get_token_wrapped(fulfillment.quoted_order.order.sell_token)), "price": "1000000000000000000", - "trusted": test.trusted.contains(fulfillment.quote.order.sell_token), + "trusted": test.trusted.contains(fulfillment.quoted_order.order.sell_token), })); tokens_json.push(json!({ - "address": hex_address(test.blockchain.get_token_wrapped(fulfillment.quote.order.buy_token)), + "address": hex_address(test.blockchain.get_token_wrapped(fulfillment.quoted_order.order.buy_token)), "price": "1000000000000000000", - "trusted": test.trusted.contains(fulfillment.quote.order.buy_token), + "trusted": test.trusted.contains(fulfillment.quoted_order.order.buy_token), })); } json!({ @@ -122,11 +127,11 @@ pub fn solve_req(test: &Test) -> serde_json::Value { /// Create a request for the driver /quote endpoint. pub fn quote_req(test: &Test) -> serde_json::Value { - if test.quotes.len() != 1 { + if test.quoted_orders.len() != 1 { panic!("when testing /quote, there must be exactly one order"); } - let quote = test.quotes.first().unwrap(); + let quote = test.quoted_orders.first().unwrap(); json!({ "sellToken": hex_address(test.blockchain.get_token(quote.order.sell_token)), "buyToken": hex_address(test.blockchain.get_token(quote.order.buy_token)), diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 0e672cf956..078652b11e 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -533,7 +533,7 @@ impl Setup { blockchain: &blockchain, solutions: &solutions, trusted: &trusted, - quotes: "es, + quoted_orders: "es, deadline, quote: self.quote, }) @@ -560,7 +560,7 @@ impl Setup { fulfillments: solutions.into_iter().flat_map(|s| s.fulfillments).collect(), trusted, deadline, - quotes, + quoted_orders: quotes, quote: self.quote, } } @@ -579,7 +579,7 @@ impl Setup { } pub struct Test { - quotes: Vec, + quoted_orders: Vec, blockchain: Blockchain, driver: Driver, client: reqwest::Client, @@ -762,14 +762,14 @@ impl SolveOk<'_> { .map(|name| { self.fulfillments .iter() - .find(|f| f.quote.order.name == *name) + .find(|f| f.quoted_order.order.name == *name) .unwrap_or_else(|| { panic!( "unexpected orders {order_names:?}: fulfillment not found in {:?}", self.fulfillments, ) }) - .quote + .quoted_order .order_uid(self.blockchain) .to_string() }) @@ -841,11 +841,11 @@ impl QuoteOk<'_> { let fulfillment = &self.fulfillments[0]; let result: serde_json::Value = serde_json::from_str(&self.body).unwrap(); let amount = result.get("amount").unwrap().as_str().unwrap().to_owned(); - let expected = match fulfillment.quote.order.side { - order::Side::Buy => { - (fulfillment.quote.sell - fulfillment.quote.order.surplus_fee()).to_string() - } - order::Side::Sell => fulfillment.quote.buy.to_string(), + let expected = match fulfillment.quoted_order.order.side { + order::Side::Buy => (fulfillment.quoted_order.sell + - fulfillment.quoted_order.order.surplus_fee()) + .to_string(), + order::Side::Sell => fulfillment.quoted_order.buy.to_string(), }; assert_eq!(amount, expected); self diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 332e2a8472..cf89ec16da 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -25,7 +25,7 @@ pub struct Config<'a> { pub blockchain: &'a Blockchain, pub solutions: &'a [blockchain::Solution], pub trusted: &'a HashSet<&'static str>, - pub quotes: &'a [super::blockchain::Quote], + pub quoted_orders: &'a [super::blockchain::QuotedOrder], pub deadline: chrono::DateTime, /// Is this a test for the /quote endpoint? pub quote: bool, @@ -36,7 +36,7 @@ impl Solver { pub async fn new(config: Config<'_>) -> Self { let mut solutions_json = Vec::new(); let mut orders_json = Vec::new(); - for quote in config.quotes { + for quote in config.quoted_orders { // ETH orders get unwrapped into WETH by the driver before being passed to the // solver. let sell_token = if quote.order.sell_token == "ETH" { @@ -105,25 +105,26 @@ impl Solver { prices_json.insert( config .blockchain - .get_token_wrapped(fulfillment.quote.order.sell_token), - fulfillment.quote.buy.to_string(), + .get_token_wrapped(fulfillment.quoted_order.order.sell_token), + fulfillment.quoted_order.buy.to_string(), ); prices_json.insert( config .blockchain - .get_token_wrapped(fulfillment.quote.order.buy_token), - (fulfillment.quote.sell - fulfillment.quote.order.surplus_fee()).to_string(), + .get_token_wrapped(fulfillment.quoted_order.order.buy_token), + (fulfillment.quoted_order.sell - fulfillment.quoted_order.order.surplus_fee()) + .to_string(), ); trades_json.push(json!({ "kind": "fulfillment", - "order": if config.quote { Default::default() } else { fulfillment.quote.order_uid(config.blockchain) }, + "order": if config.quote { Default::default() } else { fulfillment.quoted_order.order_uid(config.blockchain) }, "executedAmount": - match fulfillment.quote.order.executed { + match fulfillment.quoted_order.order.executed { Some(executed) => executed.to_string(), - None => match fulfillment.quote.order.side { + None => match fulfillment.quoted_order.order.side { order::Side::Sell => - (fulfillment.quote.sell_amount() - fulfillment.quote.order.surplus_fee()).to_string(), - order::Side::Buy => fulfillment.quote.buy_amount().to_string(), + (fulfillment.quoted_order.sell_amount() - fulfillment.quoted_order.order.surplus_fee()).to_string(), + order::Side::Buy => fulfillment.quoted_order.buy_amount().to_string(), }, } })) @@ -136,42 +137,39 @@ impl Solver { "risk": solution.risk.to_string(), })); } - let tokens_json = if config.quote { - Default::default() - } else { - config - .solutions - .iter() - .flat_map(|s| s.fulfillments.iter()) - .flat_map(|f| { - let quote = &f.quote; - [ - ( - hex_address( - config.blockchain.get_token_wrapped(quote.order.sell_token), - ), - json!({ - "decimals": null, - "symbol": null, - "referencePrice": "1000000000000000000", - "availableBalance": "0", - "trusted": config.trusted.contains(quote.order.sell_token), - }), - ), - ( - hex_address(config.blockchain.get_token_wrapped(quote.order.buy_token)), - json!({ - "decimals": null, - "symbol": null, - "referencePrice": "1000000000000000000", - "availableBalance": "0", - "trusted": config.trusted.contains(quote.order.buy_token), - }), + + let tokens_json = config + .solutions + .iter() + .flat_map(|s| s.fulfillments.iter()) + .flat_map(|f| { + let quote = &f.quoted_order; + [ + ( + hex_address( + config.blockchain.get_token_wrapped(quote.order.sell_token), ), - ] - }) - .collect::>() - }; + json!({ + "decimals": null, + "symbol": null, + "referencePrice": if config.quote { None } else { Some("1000000000000000000") }, + "availableBalance": "0", + "trusted": config.trusted.contains(quote.order.sell_token), + }), + ), + ( + hex_address(config.blockchain.get_token_wrapped(quote.order.buy_token)), + json!({ + "decimals": null, + "symbol": null, + "referencePrice": if config.quote { None } else { Some("1000000000000000000") }, + "availableBalance": "0", + "trusted": config.trusted.contains(quote.order.buy_token), + }), + ), + ] + }) + .collect::>(); let url = config.blockchain.web3_url.parse().unwrap(); let eth = Ethereum::ethrpc(