From 8f8ef93bf6bdf27a77995f1897559010dbd8d004 Mon Sep 17 00:00:00 2001 From: ilya Date: Fri, 23 Feb 2024 19:25:54 +0000 Subject: [PATCH] Driver tests refactoring for protocol fees (#2404) # Description Refactors driver tests to be able to use them in UT fashion by providing custom quotes and explicit expected results without replicating original protocol fees logic in the tests. The easiest approach is to continue using Uniswap on-chain interactions, but it turned out that it has some limitations where the pool's executed amounts ratio should be the same as the pool's reserves ratio. The proposed approach addresses this limitation by adjusting the pool's reserves by providing precalculated executed amounts using a reversed function. The precision issue with calculations without a floating point adds complexity to the implemented functions. # Changes - [ ] Adds ability to provide custom pool's executed amounts - [ ] Results are now compared with precalculated values - [ ] Minor refactoring to operate with WEI values for better readability - [ ] Pool's reserve token `a` or `b` adjustment functions could be used interchangeably ## Related Issues Fixed #2267 --- crates/driver/src/tests/cases/buy_eth.rs | 5 +- crates/driver/src/tests/cases/fees.rs | 18 +- .../src/tests/cases/merge_settlements.rs | 18 +- crates/driver/src/tests/cases/mod.rs | 1 + .../src/tests/cases/multiple_solutions.rs | 18 +- .../driver/src/tests/cases/negative_scores.rs | 11 +- .../driver/src/tests/cases/protocol_fees.rs | 189 ++++++++++++++++ .../src/tests/cases/score_competition.rs | 10 +- crates/driver/src/tests/setup/blockchain.rs | 1 - crates/driver/src/tests/setup/driver.rs | 7 +- crates/driver/src/tests/setup/mod.rs | 207 ++++++++++++++++-- 11 files changed, 421 insertions(+), 64 deletions(-) create mode 100644 crates/driver/src/tests/cases/protocol_fees.rs diff --git a/crates/driver/src/tests/cases/buy_eth.rs b/crates/driver/src/tests/cases/buy_eth.rs index a3541c91f9..ec2932b8f9 100644 --- a/crates/driver/src/tests/cases/buy_eth.rs +++ b/crates/driver/src/tests/cases/buy_eth.rs @@ -7,13 +7,14 @@ use crate::tests::{ #[tokio::test] #[ignore] async fn test() { + let order = eth_order(); let test = setup() .pool(weth_pool()) - .order(eth_order()) + .order(order.clone()) .solution(eth_solution()) .done() .await; - test.solve().await.ok().orders(&[eth_order().name]); + test.solve().await.ok().orders(&[order]); test.settle().await.ok().await.eth_order_executed().await; } diff --git a/crates/driver/src/tests/cases/fees.rs b/crates/driver/src/tests/cases/fees.rs index 820199a4d9..719fb93085 100644 --- a/crates/driver/src/tests/cases/fees.rs +++ b/crates/driver/src/tests/cases/fees.rs @@ -29,20 +29,19 @@ async fn rejects_unwarranted_solver_fee() { #[ignore] async fn solver_fee() { for side in [order::Side::Buy, order::Side::Sell] { + let order = ab_order() + .kind(order::Kind::Limit) + .side(side) + .solver_fee(Some(500.into())); let test = tests::setup() .name(format!("Solver Fee: {side:?}")) .pool(ab_pool()) - .order( - ab_order() - .kind(order::Kind::Limit) - .side(side) - .solver_fee(Some(500.into())), - ) + .order(order.clone()) .solution(ab_solution()) .done() .await; - test.solve().await.ok().orders(&[ab_order().name]); + test.solve().await.ok().orders(&[order]); } } @@ -50,14 +49,15 @@ async fn solver_fee() { #[ignore] async fn user_fee() { for side in [order::Side::Buy, order::Side::Sell] { + let order = ab_order().side(side).user_fee(1000.into()); let test = tests::setup() .name(format!("User Fee: {side:?}")) .pool(ab_pool()) - .order(ab_order().side(side).user_fee(1000.into())) + .order(order.clone()) .solution(ab_solution()) .done() .await; - test.solve().await.ok().orders(&[ab_order().name]); + test.solve().await.ok().orders(&[order]); } } diff --git a/crates/driver/src/tests/cases/merge_settlements.rs b/crates/driver/src/tests/cases/merge_settlements.rs index f151f8e522..e4496cee3d 100644 --- a/crates/driver/src/tests/cases/merge_settlements.rs +++ b/crates/driver/src/tests/cases/merge_settlements.rs @@ -7,20 +7,19 @@ use crate::tests::{ #[tokio::test] #[ignore] async fn possible() { + let ab_order = ab_order(); + let cd_order = cd_order(); let test = setup() .pool(cd_pool()) .pool(ab_pool()) - .order(ab_order()) - .order(cd_order()) + .order(ab_order.clone()) + .order(cd_order.clone()) .solution(cd_solution()) .solution(ab_solution()) .done() .await; - test.solve() - .await - .ok() - .orders(&[ab_order().name, cd_order().name]); + test.solve().await.ok().orders(&[ab_order, cd_order]); test.reveal().await.ok().calldata(); test.settle() .await @@ -38,10 +37,11 @@ async fn possible() { #[tokio::test] #[ignore] async fn impossible() { + let order = ab_order(); let test = setup() .pool(ab_pool()) - .order(ab_order()) - .order(ab_order().rename("reduced order").reduce_amount(1000000000000000u128.into())) + .order(order.clone()) + .order(order.clone().rename("reduced 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()) @@ -54,7 +54,7 @@ async fn impossible() { // Only the first A-B order gets settled. - test.solve().await.ok().orders(&[ab_order().name]); + test.solve().await.ok().orders(&[order]); test.reveal().await.ok().calldata(); test.settle().await.ok().await.ab_order_executed().await; } diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index 4a3cf67d64..faf250da6e 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -9,6 +9,7 @@ pub mod multiple_drivers; pub mod multiple_solutions; pub mod negative_scores; pub mod order_prioritization; +pub mod protocol_fees; pub mod quote; pub mod score_competition; pub mod settle; diff --git a/crates/driver/src/tests/cases/multiple_solutions.rs b/crates/driver/src/tests/cases/multiple_solutions.rs index dfac3c90d6..d247f0a71b 100644 --- a/crates/driver/src/tests/cases/multiple_solutions.rs +++ b/crates/driver/src/tests/cases/multiple_solutions.rs @@ -8,19 +8,16 @@ use crate::tests::{ #[tokio::test] #[ignore] async fn valid() { + let order = ab_order(); let test = setup() .pool(ab_pool()) - .order(ab_order()) + .order(order.clone()) .solution(ab_solution()) .solution(ab_solution().reduce_score()) .done() .await; - test.solve() - .await - .ok() - .default_score() - .orders(&[ab_order().name]); + test.solve().await.ok().default_score().orders(&[order]); test.reveal().await.ok().calldata(); } @@ -29,18 +26,15 @@ async fn valid() { #[tokio::test] #[ignore] async fn invalid() { + let order = ab_order(); let test = setup() .pool(ab_pool()) - .order(ab_order()) + .order(order.clone()) .solution(ab_solution().reduce_score()) .solution(ab_solution().invalid()) .done() .await; - test.solve() - .await - .ok() - .default_score() - .orders(&[ab_order().name]); + test.solve().await.ok().default_score().orders(&[order]); test.reveal().await.ok().calldata(); } diff --git a/crates/driver/src/tests/cases/negative_scores.rs b/crates/driver/src/tests/cases/negative_scores.rs index d34c6c8c8d..f1299cdc15 100644 --- a/crates/driver/src/tests/cases/negative_scores.rs +++ b/crates/driver/src/tests/cases/negative_scores.rs @@ -24,10 +24,11 @@ async fn no_valid_solutions() { #[tokio::test] #[ignore] async fn one_valid_solution() { + let order = ab_order(); let test = setup() .pool(ab_pool()) - .order(ab_order()) - .order(ab_order().rename("no surplus").no_surplus()) + .order(order.clone()) + .order(order.clone().rename("no surplus").no_surplus()) .solution(ab_solution()) // This solution has no surplus, and hence a negative score, so it gets skipped. .solution(Solution { @@ -36,10 +37,6 @@ async fn one_valid_solution() { }) .done() .await; - test.solve() - .await - .ok() - .default_score() - .orders(&[ab_order().name]); + test.solve().await.ok().default_score().orders(&[order]); test.reveal().await.ok().calldata(); } diff --git a/crates/driver/src/tests/cases/protocol_fees.rs b/crates/driver/src/tests/cases/protocol_fees.rs new file mode 100644 index 0000000000..42e526e32f --- /dev/null +++ b/crates/driver/src/tests/cases/protocol_fees.rs @@ -0,0 +1,189 @@ +use crate::{ + domain::{competition::order, eth}, + tests::{ + self, + setup::{ + ab_adjusted_pool, + ab_liquidity_quote, + ab_order, + ab_solution, + ExpectedOrderAmounts, + FeePolicy, + Test, + }, + }, +}; + +struct TestCase { + order_side: order::Side, + fee_policy: FeePolicy, + order_sell_amount: eth::U256, + solver_fee: Option, + quote_sell_amount: eth::U256, + quote_buy_amount: eth::U256, + executed: eth::U256, + executed_sell_amount: eth::U256, + executed_buy_amount: eth::U256, +} + +async fn protocol_fee_test_case(test_case: TestCase) { + let test_name = format!( + "Protocol Fee: {:?} {:?}", + test_case.order_side, test_case.fee_policy + ); + let quote = ab_liquidity_quote() + .sell_amount(test_case.quote_sell_amount) + .buy_amount(test_case.quote_buy_amount); + let pool = ab_adjusted_pool(quote); + let expected_amounts = ExpectedOrderAmounts { + sell: test_case.executed_sell_amount, + buy: test_case.executed_buy_amount, + }; + let order = ab_order() + .kind(order::Kind::Limit) + .sell_amount(test_case.order_sell_amount) + .side(test_case.order_side) + .solver_fee(test_case.solver_fee) + .fee_policy(test_case.fee_policy) + .executed(test_case.executed) + .expected_amounts(expected_amounts); + let test: Test = tests::setup() + .name(test_name) + .pool(pool) + .order(order.clone()) + .solution(ab_solution()) + .done() + .await; + + test.solve().await.ok().orders(&[order]); +} + +#[tokio::test] +#[ignore] +async fn surplus_protocol_fee_buy_order_not_capped() { + let fee_policy = FeePolicy::Surplus { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 1.0, + }; + let test_case = TestCase { + order_side: order::Side::Buy, + fee_policy, + order_sell_amount: 50000000000000000000u128.into(), + solver_fee: Some(10000000000000000000u128.into()), + quote_sell_amount: 50000000000000000000u128.into(), + quote_buy_amount: 40000000000000000000u128.into(), + executed: 40000000000000000000u128.into(), + executed_sell_amount: 100000000000000000000u128.into(), + executed_buy_amount: 40000000000000000000u128.into(), + }; + + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn surplus_protocol_fee_sell_order_not_capped() { + let fee_policy = FeePolicy::Surplus { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 1.0, + }; + let test_case = TestCase { + order_side: order::Side::Sell, + fee_policy, + order_sell_amount: 50000000000000000000u128.into(), + solver_fee: Some(10000000000000000000u128.into()), + quote_sell_amount: 50000000000000000000u128.into(), + quote_buy_amount: 40000000000000000000u128.into(), + executed: 40000000000000000000u128.into(), + executed_sell_amount: 50000000000000000000u128.into(), + executed_buy_amount: 20000000002000000000u128.into(), + }; + + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn surplus_protocol_fee_buy_order_capped() { + let fee_policy = FeePolicy::Surplus { + factor: 0.5, + // low enough so we get capped by volume fee + max_volume_factor: 0.1, + }; + let test_case = TestCase { + order_side: order::Side::Buy, + fee_policy, + order_sell_amount: 50000000000000000000u128.into(), + solver_fee: Some(10000000000000000000u128.into()), + quote_sell_amount: 50000000000000000000u128.into(), + quote_buy_amount: 40000000000000000000u128.into(), + executed: 40000000000000000000u128.into(), + executed_sell_amount: 55000000000000000000u128.into(), + executed_buy_amount: 40000000000000000000u128.into(), + }; + + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn surplus_protocol_fee_sell_order_capped() { + let fee_policy = FeePolicy::Surplus { + factor: 0.5, + // low enough so we get capped by volume fee + max_volume_factor: 0.1, + }; + let test_case = TestCase { + order_side: order::Side::Sell, + fee_policy, + order_sell_amount: 50000000000000000000u128.into(), + solver_fee: Some(10000000000000000000u128.into()), + quote_sell_amount: 50000000000000000000u128.into(), + quote_buy_amount: 40000000000000000000u128.into(), + executed: 40000000000000000000u128.into(), + executed_sell_amount: 50000000000000000000u128.into(), + executed_buy_amount: 35000000000000000000u128.into(), + }; + + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn volume_protocol_fee_buy_order() { + let fee_policy = FeePolicy::Volume { factor: 0.5 }; + let test_case = TestCase { + order_side: order::Side::Buy, + fee_policy, + order_sell_amount: 50000000000000000000u128.into(), + solver_fee: Some(10000000000000000000u128.into()), + quote_sell_amount: 50000000000000000000u128.into(), + quote_buy_amount: 40000000000000000000u128.into(), + executed: 40000000000000000000u128.into(), + executed_sell_amount: 75000000000000000000u128.into(), + executed_buy_amount: 40000000000000000000u128.into(), + }; + + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn volume_protocol_fee_sell_order() { + let fee_policy = FeePolicy::Volume { factor: 0.5 }; + let test_case = TestCase { + order_side: order::Side::Sell, + fee_policy, + order_sell_amount: 50000000000000000000u128.into(), + solver_fee: Some(10000000000000000000u128.into()), + quote_sell_amount: 50000000000000000000u128.into(), + quote_buy_amount: 40000000000000000000u128.into(), + executed: 40000000000000000000u128.into(), + executed_sell_amount: 50000000000000000000u128.into(), + executed_buy_amount: 15000000000000000000u128.into(), + }; + + protocol_fee_test_case(test_case).await; +} diff --git a/crates/driver/src/tests/cases/score_competition.rs b/crates/driver/src/tests/cases/score_competition.rs index bdd4ce82ae..ef95dd0494 100644 --- a/crates/driver/src/tests/cases/score_competition.rs +++ b/crates/driver/src/tests/cases/score_competition.rs @@ -8,9 +8,10 @@ use crate::tests::{ #[tokio::test] #[ignore] async fn solver_score_winner() { + let order = ab_order(); let test = setup() .pool(ab_pool()) - .order(ab_order()) + .order(order.clone()) .solution(ab_solution().score(Score::Solver { score: 2902421280589416499u128.into()})) // higher than objective value .solution(ab_solution().score(Score::RiskAdjusted{ success_probability: 0.4})) .done() @@ -18,16 +19,17 @@ async fn solver_score_winner() { let solve = test.solve().await.ok(); assert_eq!(solve.score(), 2902421280589416499u128.into()); - solve.orders(&[ab_order().name]); + solve.orders(&[order]); test.reveal().await.ok().calldata(); } #[tokio::test] #[ignore] async fn risk_adjusted_score_winner() { + let order = ab_order(); let test = setup() .pool(ab_pool()) - .order(ab_order()) + .order(order.clone()) .solution(ab_solution().score(Score::Solver { score: DEFAULT_SCORE_MIN.into(), })) @@ -39,6 +41,6 @@ async fn risk_adjusted_score_winner() { let solve = test.solve().await.ok(); assert!(solve.score() != DEFAULT_SCORE_MIN.into()); - solve.orders(&[ab_order().name]); + solve.orders(&[order]); test.reveal().await.ok().calldata(); } diff --git a/crates/driver/src/tests/setup/blockchain.rs b/crates/driver/src/tests/setup/blockchain.rs index 3de1f79f1d..864d622a85 100644 --- a/crates/driver/src/tests/setup/blockchain.rs +++ b/crates/driver/src/tests/setup/blockchain.rs @@ -325,7 +325,6 @@ impl Blockchain { tokens.insert(pool.reserve_b.token, token); } } - // Create the uniswap factory. let uniswap_factory = wait_for( &web3, diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 8a8c08656b..e10893422f 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -76,12 +76,7 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "protocolFees": match quote.order.kind { order::Kind::Market => json!([]), order::Kind::Liquidity => json!([]), - order::Kind::Limit { .. } => json!([{ - "surplus": { - "factor": 0.0, - "maxVolumeFactor": 0.06 - } - }]), + order::Kind::Limit { .. } => json!([quote.order.fee_policy.to_json_value()]), }, "validTo": u32::try_from(time::now().timestamp()).unwrap() + quote.order.valid_for.0, "kind": match quote.order.side { diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index ac97b81a7d..bde85ce61d 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -30,6 +30,7 @@ use { futures::future::join_all, hyper::StatusCode, secp256k1::SecretKey, + serde_json::json, serde_with::serde_as, std::{ collections::{HashMap, HashSet}, @@ -71,6 +72,37 @@ pub enum Score { RiskAdjusted { success_probability: f64 }, } +#[serde_as] +#[derive(Debug, Clone, PartialEq, serde::Serialize)] +#[serde(rename_all = "camelCase", tag = "kind")] +pub enum FeePolicy { + #[serde(rename_all = "camelCase")] + Surplus { factor: f64, max_volume_factor: f64 }, + #[serde(rename_all = "camelCase")] + Volume { factor: f64 }, +} + +impl FeePolicy { + pub fn to_json_value(&self) -> serde_json::Value { + match self { + FeePolicy::Surplus { + factor, + max_volume_factor, + } => json!({ + "surplus": { + "factor": factor, + "maxVolumeFactor": max_volume_factor + } + }), + FeePolicy::Volume { factor } => json!({ + "volume": { + "factor": factor + } + }), + } + } +} + impl Default for Score { fn default() -> Self { Self::RiskAdjusted { @@ -79,6 +111,27 @@ impl Default for Score { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct LiquidityQuote { + pub sell_token: &'static str, + pub buy_token: &'static str, + pub sell_amount: eth::U256, + pub buy_amount: eth::U256, +} + +impl LiquidityQuote { + pub fn buy_amount(self, buy_amount: eth::U256) -> Self { + Self { buy_amount, ..self } + } + + pub fn sell_amount(self, sell_amount: eth::U256) -> Self { + Self { + sell_amount, + ..self + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct Order { pub name: &'static str, @@ -105,12 +158,14 @@ pub struct Order { /// Override the executed amount of the order. Useful for testing liquidity /// orders. Otherwise [`execution_diff`] is probably more suitable. pub executed: Option, - + /// Provides explicit expected order executed amounts. + pub expected_amounts: Option, /// Should this order be filtered out before being sent to the solver? pub filtered: bool, /// Should the trader account be funded with enough tokens to place this /// order? True by default. pub funded: bool, + pub fee_policy: FeePolicy, } impl Order { @@ -200,6 +255,31 @@ impl Order { } } + pub fn fee_policy(self, fee_policy: FeePolicy) -> Self { + Self { fee_policy, ..self } + } + + pub fn executed(self, executed_price: eth::U256) -> Self { + Self { + executed: Some(executed_price), + ..self + } + } + + pub fn expected_amounts(self, expected_amounts: ExpectedOrderAmounts) -> Self { + Self { + expected_amounts: Some(expected_amounts), + ..self + } + } + + pub fn sell_amount(self, sell_amount: eth::U256) -> Self { + Self { + sell_amount, + ..self + } + } + fn surplus_fee(&self) -> eth::U256 { match self.kind { order::Kind::Limit => self.solver_fee.unwrap_or_default(), @@ -224,8 +304,13 @@ impl Default for Order { name: Default::default(), surplus_factor: DEFAULT_SURPLUS_FACTOR.into(), executed: Default::default(), + expected_amounts: Default::default(), filtered: Default::default(), funded: true, + fee_policy: FeePolicy::Surplus { + factor: 0.0, + max_volume_factor: 0.06, + }, } } } @@ -299,6 +384,74 @@ pub struct Pool { pub amount_b: eth::U256, } +impl Pool { + /// Restores reserve_a value from the given reserve_b and the quote. Reverse + /// operation for the `blockchain::Pool::out` function. + /// + #[allow(dead_code)] + pub fn adjusted_reserve_a(self, quote: &LiquidityQuote) -> Self { + let (quote_sell_amount, quote_buy_amount) = if quote.sell_token == self.token_a { + (quote.sell_amount, quote.buy_amount) + } else { + (quote.buy_amount, quote.sell_amount) + }; + let reserve_a_min = ceil_div( + eth::U256::from(997) + * quote_sell_amount + * (self.amount_b - quote_buy_amount - eth::U256::from(1)), + eth::U256::from(1000) * quote_buy_amount, + ); + let reserve_a_max = + (eth::U256::from(997) * quote_sell_amount * (self.amount_b - quote_buy_amount)) + / (eth::U256::from(1000) * quote_buy_amount); + if reserve_a_min > reserve_a_max { + panic!( + "Unexpected calculated reserves. min: {:?}, max: {:?}", + reserve_a_min, reserve_a_max + ); + } + Self { + amount_a: reserve_a_min, + ..self + } + } + + /// Restores reserve_b value from the given reserve_a and the quote. Reverse + /// operation for the `blockchain::Pool::out` function + /// + pub fn adjusted_reserve_b(self, quote: &LiquidityQuote) -> Self { + let (quote_sell_amount, quote_buy_amount) = if quote.sell_token == self.token_a { + (quote.sell_amount, quote.buy_amount) + } else { + (quote.buy_amount, quote.sell_amount) + }; + let reserve_b_min = ceil_div( + quote_buy_amount + * (eth::U256::from(1000) * self.amount_a + + eth::U256::from(997) * quote_sell_amount), + eth::U256::from(997) * quote_sell_amount, + ); + let reserve_b_max = ((quote_buy_amount + eth::U256::from(1)) + * (eth::U256::from(1000) * self.amount_a + eth::U256::from(997) * quote_sell_amount) + - eth::U256::from(1)) + / (eth::U256::from(997) * quote_sell_amount); + if reserve_b_min > reserve_b_max { + panic!( + "Unexpected calculated reserves. min: {:?}, max: {:?}", + reserve_b_min, reserve_b_max + ); + } + Self { + amount_b: reserve_b_min, + ..self + } + } +} + +fn ceil_div(x: eth::U256, y: eth::U256) -> eth::U256 { + (x + y - eth::U256::from(1)) / y +} + #[derive(Debug)] pub enum Mempool { Public, @@ -433,6 +586,10 @@ pub fn ab_pool() -> Pool { } } +pub fn ab_adjusted_pool(quote: LiquidityQuote) -> Pool { + ab_pool().adjusted_reserve_b("e) +} + /// An example order which sells token "A" for token "B". pub fn ab_order() -> Order { Order { @@ -444,6 +601,15 @@ pub fn ab_order() -> Order { } } +pub fn ab_liquidity_quote() -> LiquidityQuote { + LiquidityQuote { + sell_token: "A", + buy_token: "B", + sell_amount: AB_ORDER_AMOUNT.into(), + buy_amount: 40000000000000000000u128.into(), + } +} + /// A solution solving the [`ab_order`]. pub fn ab_solution() -> Solution { Solution { @@ -923,7 +1089,7 @@ impl<'a> SolveOk<'a> { } /// Check that the solution contains the expected orders. - pub fn orders(self, order_names: &[&str]) -> Self { + pub fn orders(self, orders: &[Order]) -> Self { let solution = self.solution(); assert!(solution.get("orders").is_some()); let trades = serde_json::from_value::>( @@ -931,29 +1097,36 @@ impl<'a> SolveOk<'a> { ) .unwrap(); - for expected in order_names.iter().map(|name| { - self.fulfillments + for (expected, fulfillment) in orders.iter().map(|expected_order| { + let fulfillment = self + .fulfillments .iter() - .find(|f| f.quoted_order.order.name == *name) + .find(|f| f.quoted_order.order.name == expected_order.name) .unwrap_or_else(|| { panic!( - "unexpected orders {order_names:?}: fulfillment not found in {:?}", - self.fulfillments, + "unexpected order {:?}: fulfillment not found in {:?}", + expected_order.name, self.fulfillments, ) - }) + }); + (expected_order, fulfillment) }) { - let uid = expected.quoted_order.order_uid(self.blockchain); + let uid = fulfillment.quoted_order.order_uid(self.blockchain); let trade = trades .get(&uid.to_string()) .expect("Didn't find expected trade in solution"); let u256 = |value: &serde_json::Value| { eth::U256::from_dec_str(value.as_str().unwrap()).unwrap() }; - assert!(u256(trade.get("buyAmount").unwrap()) == expected.quoted_order.buy); - assert!( - u256(trade.get("sellAmount").unwrap()) - == expected.quoted_order.sell + expected.quoted_order.order.user_fee - ); + + let (expected_sell, expected_buy) = match &expected.expected_amounts { + Some(executed_amounts) => (executed_amounts.sell, executed_amounts.buy), + None => ( + fulfillment.quoted_order.sell + fulfillment.quoted_order.order.user_fee, + fulfillment.quoted_order.buy, + ), + }; + assert!(u256(trade.get("sellAmount").unwrap()) == expected_sell); + assert!(u256(trade.get("buyAmount").unwrap()) == expected_buy); } self } @@ -973,6 +1146,12 @@ impl Reveal { } } +#[derive(Debug, Clone, PartialEq)] +pub struct ExpectedOrderAmounts { + pub sell: eth::U256, + pub buy: eth::U256, +} + pub struct RevealOk { body: String, }