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, }