From 98928ece37d42689a777e48786e4039182ee1faa Mon Sep 17 00:00:00 2001 From: ilya Date: Thu, 4 Jul 2024 12:25:28 +0300 Subject: [PATCH] Balancer SOR API V3 (#37) --- Cargo.lock | 2 + Cargo.toml | 2 + src/domain/solver/dex/mod.rs | 5 +- src/infra/config/dex/balancer/file.rs | 17 +- src/infra/dex/balancer/dto.rs | 360 +++++++++++++++++-- src/infra/dex/balancer/mod.rs | 57 ++- src/infra/dex/mod.rs | 3 +- src/tests/balancer/market_order.rs | 137 ++++--- src/tests/balancer/mod.rs | 30 ++ src/tests/balancer/not_found.rs | 30 +- src/tests/balancer/out_of_price.rs | 136 ++++--- src/tests/dex/partial_fill.rs | 500 +++++++++++++++----------- src/tests/dex/wrong_execution.rs | 54 ++- src/tests/mock/http.rs | 12 + 14 files changed, 918 insertions(+), 427 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8833fe7..c702696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3474,7 +3474,9 @@ dependencies = [ "humantime-serde", "hyper", "itertools 0.11.0", + "maplit", "num", + "number", "observe", "prometheus", "prometheus-metric-storage", diff --git a/Cargo.toml b/Cargo.toml index 629414e..773981c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,9 @@ observe = { git = "https://github.com/cowprotocol/services.git", tag = "v2.265.0 shared = { git = "https://github.com/cowprotocol/services.git", tag = "v2.265.0", package = "shared" } dto = { git = "https://github.com/cowprotocol/services.git", tag = "v2.265.0", package = "solvers-dto" } rate-limit = { git = "https://github.com/cowprotocol/services.git", tag = "v2.265.0", package = "rate-limit" } +number = { git = "https://github.com/cowprotocol/services.git", tag = "v2.265.0", package = "number" } [dev-dependencies] glob = "0.3" +maplit = "1" tempfile = "3" diff --git a/src/domain/solver/dex/mod.rs b/src/domain/solver/dex/mod.rs index a7eec1f..3e4af6b 100644 --- a/src/domain/solver/dex/mod.rs +++ b/src/domain/solver/dex/mod.rs @@ -115,7 +115,6 @@ impl Dex { order: &Order, dex_order: &dex::Order, tokens: &auction::Tokens, - gas_price: auction::GasPrice, ) -> Option { let dex_err_handler = |err: infra::dex::Error| { infra::metrics::solve_error(err.format_variant()); @@ -149,7 +148,7 @@ impl Dex { let swap = async { let slippage = self.slippage.relative(&dex_order.amount(), tokens); self.dex - .swap(dex_order, &slippage, tokens, gas_price) + .swap(dex_order, &slippage, tokens) .await .map_err(dex_err_handler) }; @@ -183,7 +182,7 @@ impl Dex { ) -> Option { let order = order.get(); let dex_order = self.fills.dex_order(order, tokens)?; - let swap = self.try_solve(order, &dex_order, tokens, gas_price).await?; + let swap = self.try_solve(order, &dex_order, tokens).await?; let sell = tokens.reference_price(&order.sell.token); let Some(solution) = swap .into_solution( diff --git a/src/infra/config/dex/balancer/file.rs b/src/infra/config/dex/balancer/file.rs index cb07d93..9231266 100644 --- a/src/infra/config/dex/balancer/file.rs +++ b/src/infra/config/dex/balancer/file.rs @@ -2,6 +2,7 @@ use { crate::{ domain::eth, infra::{config::dex::file, contracts, dex}, + util::serialize, }, ethereum_types::H160, serde::Deserialize, @@ -20,6 +21,15 @@ struct Config { /// Optional Balancer V2 Vault contract address. If not specified, the /// default Vault contract address will be used. vault: Option, + + /// Chain ID used to automatically determine contract addresses and send to + /// the SOR API. + #[serde_as(as = "serialize::ChainId")] + chain_id: eth::ChainId, + + /// Whether to run `queryBatchSwap` to update the return amount with most + /// up-to-date on-chain values. + query_batch_swap: Option, } /// Load the driver configuration from a TOML file. @@ -29,10 +39,7 @@ struct Config { /// This method panics if the config is invalid or on I/O errors. pub async fn load(path: &Path) -> super::Config { let (base, config) = file::load::(path).await; - - // Take advantage of the fact that deterministic deployment means that all - // CoW Protocol and Balancer Vault contracts have the same address. - let contracts = contracts::Contracts::for_chain(eth::ChainId::Mainnet); + let contracts = contracts::Contracts::for_chain(config.chain_id); super::Config { sor: dex::balancer::Config { @@ -43,6 +50,8 @@ pub async fn load(path: &Path) -> super::Config { .unwrap_or(contracts.balancer_vault), settlement: base.contracts.settlement, block_stream: base.block_stream.clone(), + chain_id: config.chain_id, + query_batch_swap: config.query_batch_swap.unwrap_or(false), }, base, } diff --git a/src/infra/dex/balancer/dto.rs b/src/infra/dex/balancer/dto.rs index 39a3f87..62dd821 100644 --- a/src/infra/dex/balancer/dto.rs +++ b/src/infra/dex/balancer/dto.rs @@ -1,58 +1,242 @@ use { crate::{ - domain::{auction, dex, order}, + domain::{auction, dex, eth, order}, + infra::dex::balancer::Error, util::serialize, }, + bigdecimal::{num_bigint::BigInt, BigDecimal}, ethereum_types::{H160, H256, U256}, - serde::{Deserialize, Serialize}, + number::conversions::u256_to_big_decimal, + serde::{Deserialize, Serialize, Serializer}, serde_with::serde_as, }; -#[derive(Debug, Serialize)] +/// Get swap quote from the SOR v2 for the V2 vault. +const QUERY: &str = r#" +query sorGetSwapPaths($callDataInput: GqlSwapCallDataInput!, $chain: GqlChain!, $queryBatchSwap: Boolean!, $swapAmount: AmountHumanReadable!, $swapType: GqlSorSwapType!, $tokenIn: String!, $tokenOut: String!, $useProtocolVersion: Int) { + sorGetSwapPaths( + callDataInput: $callDataInput, + chain: $chain, + queryBatchSwap: $queryBatchSwap, + swapAmount: $swapAmount, + swapType: $swapType, + tokenIn: $tokenIn, + tokenOut: $tokenOut, + useProtocolVersion: $useProtocolVersion + ) { + tokenAddresses + swaps { + poolId + assetInIndex + assetOutIndex + amount + userData + } + swapAmountRaw + returnAmountRaw + tokenIn + tokenOut + } +} +"#; + +#[derive(Serialize)] #[serde(rename_all = "camelCase")] -pub enum OrderKind { - Sell, - Buy, +pub struct Query<'a> { + query: &'a str, + variables: Variables, } -/// An SOR query. +impl Query<'_> { + pub fn from_domain( + order: &dex::Order, + tokens: &auction::Tokens, + slippage: &dex::Slippage, + chain_id: eth::ChainId, + contract_address: eth::ContractAddress, + query_batch_swap: bool, + swap_deadline: Option, + ) -> Result { + let token_decimals = match order.side { + order::Side::Buy => tokens + .decimals(&order.buy) + .ok_or(Error::MissingDecimals(order.buy)), + order::Side::Sell => tokens + .decimals(&order.sell) + .ok_or(Error::MissingDecimals(order.sell)), + }?; + let variables = Variables { + call_data_input: CallDataInput { + deadline: swap_deadline, + receiver: contract_address.0, + sender: contract_address.0, + slippage_percentage: slippage.as_factor().clone(), + }, + chain: Chain::from_domain(chain_id)?, + query_batch_swap, + swap_amount: HumanReadableAmount::from_u256(&order.amount.get(), token_decimals), + swap_type: SwapType::from_domain(order.side), + token_in: order.sell.0, + token_out: order.buy.0, + use_protocol_version: Some(ProtocolVersion::V2.into()), + }; + Ok(Self { + query: QUERY, + variables, + }) + } +} + +/// Refers to the SOR API V3's `AmountHumanReadable` type and represents a token +/// amount without decimals. #[serde_as] -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Query { - /// The sell token to quote. - pub sell_token: H160, - /// The buy token to quote. - pub buy_token: H160, - /// The order kind to use. - pub order_kind: OrderKind, - /// The amount to quote - /// - /// For sell orders this is the exact amount of sell token to trade, for buy - /// orders, this is the amount of buy tokens to buy. - #[serde_as(as = "serialize::U256")] - pub amount: U256, - /// The current gas price estimate used for determining how the trading - /// route should be split. - #[serde_as(as = "serialize::U256")] - pub gas_price: U256, +#[derive(Debug, Default, PartialEq)] +pub struct HumanReadableAmount { + amount: U256, + decimals: u8, } -impl Query { - pub fn from_domain(order: &dex::Order, gas_price: auction::GasPrice) -> Self { +impl HumanReadableAmount { + /// Convert a `U256` amount to a human form. + pub fn from_u256(amount: &U256, decimals: u8) -> HumanReadableAmount { Self { - sell_token: order.sell.0, - buy_token: order.buy.0, - order_kind: match order.side { - order::Side::Buy => OrderKind::Buy, - order::Side::Sell => OrderKind::Sell, - }, - amount: order.amount().amount, - gas_price: gas_price.0 .0, + amount: *amount, + decimals, + } + } + + pub fn value(&self) -> BigDecimal { + let decimals: BigDecimal = BigInt::from(10).pow(self.decimals as u32).into(); + u256_to_big_decimal(&self.amount) / decimals + } + + /// Convert the human readable amount to a `U256` with the token's decimals. + pub fn as_wei(&self) -> U256 { + self.amount + } +} + +impl Serialize for HumanReadableAmount { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.value().serialize(serializer) + } +} + +#[serde_as] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Variables { + call_data_input: CallDataInput, + /// The Chain to query. + chain: Chain, + /// Whether to run `queryBatchSwap` to update the return amount with most + /// up-to-date on-chain values. + query_batch_swap: bool, + /// The amount to swap in human form. + swap_amount: HumanReadableAmount, + /// SwapType either exact_in or exact_out (also givenIn or givenOut). + swap_type: SwapType, + /// Token address of the tokenIn. + token_in: H160, + /// Token address of the tokenOut. + token_out: H160, + /// Which vault version to use. If none provided, will chose the better + /// return from either version. + use_protocol_version: Option, +} + +/// Inputs for the call data to create the swap transaction. If this input is +/// given, call data is added to the response. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CallDataInput { + /// How long the swap should be valid, provide a timestamp. `999999999` for + /// infinite. Default: infinite. + #[serde(skip_serializing_if = "Option::is_none")] + deadline: Option, + /// Who receives the output amount. + receiver: H160, + /// Who sends the input amount. + sender: H160, + /// The max slippage in percent 0.01 -> 0.01%. + slippage_percentage: BigDecimal, +} + +/// Balancer SOR API supported chains. +#[derive(Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum Chain { + Arbitrum, + Avalanche, + Base, + Fantom, + Fraxtal, + Gnosis, + Mainnet, + Mode, + Optimism, + Polygon, + Sepolia, + ZkEvm, +} + +impl Chain { + fn from_domain(chain_id: eth::ChainId) -> Result { + match chain_id { + eth::ChainId::Mainnet => Ok(Self::Mainnet), + eth::ChainId::Gnosis => Ok(Self::Gnosis), + eth::ChainId::ArbitrumOne => Ok(Self::Arbitrum), + unsupported => Err(Error::UnsupportedChainId(unsupported)), } } } +#[derive(Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum SwapType { + ExactIn, + ExactOut, +} + +impl SwapType { + fn from_domain(side: order::Side) -> Self { + match side { + order::Side::Buy => Self::ExactOut, + order::Side::Sell => Self::ExactIn, + } + } +} + +#[repr(u8)] +enum ProtocolVersion { + V2 = 2, +} + +impl From for u8 { + fn from(value: ProtocolVersion) -> Self { + value as u8 + } +} + +/// The response from the Balancer SOR service. +#[serde_as] +#[derive(Debug, Default, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSwapPathsResponse { + pub data: Data, +} + +/// The data field in the Balancer SOR response. +#[serde_as] +#[derive(Debug, Default, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Data { + pub sor_get_swap_paths: Quote, +} + /// The swap route found by the Balancer SOR service. #[serde_as] #[derive(Debug, Default, PartialEq, Deserialize)] @@ -66,12 +250,12 @@ pub struct Quote { /// /// In sell token for sell orders or buy token for buy orders. #[serde_as(as = "serialize::U256")] - pub swap_amount: U256, + pub swap_amount_raw: U256, /// The returned token amount. /// /// In buy token for sell orders or sell token for buy orders. #[serde_as(as = "serialize::U256")] - pub return_amount: U256, + pub return_amount_raw: U256, /// The input (sell) token. #[serde(with = "address_default_when_empty")] pub token_in: H160, @@ -160,3 +344,103 @@ mod value_or_string { } } } + +#[cfg(test)] +mod tests { + use { + super::*, + maplit::hashmap, + number::conversions::big_decimal_to_u256, + serde_json::json, + std::str::FromStr, + }; + + #[test] + fn test_query_serialization() { + let tokens = auction::Tokens(hashmap! { + eth::TokenAddress(H160::from_str("0x2170ed0880ac9a755fd29b2688956bd959f933f8").unwrap()) => auction::Token { + decimals: Some(18), + symbol: Some("ETH".to_string()), + reference_price: None, + available_balance: U256::from(1000), + trusted: true, + }, + eth::TokenAddress(H160::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap()) => auction::Token { + decimals: Some(24), + symbol: Some("USDT".to_string()), + reference_price: None, + available_balance: U256::from(1000), + trusted: true, + }, + }); + let order = dex::Order { + sell: H160::from_str("0x2170ed0880ac9a755fd29b2688956bd959f933f8") + .unwrap() + .into(), + buy: H160::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7") + .unwrap() + .into(), + side: order::Side::Buy, + amount: dex::Amount::new(U256::from(1000)), + owner: H160::from_str("0x9008d19f58aabd9ed0d60971565aa8510560ab41").unwrap(), + }; + let slippage = dex::Slippage::one_percent(); + let chain_id = eth::ChainId::Mainnet; + let contract_address = eth::ContractAddress( + H160::from_str("0x9008d19f58aabd9ed0d60971565aa8510560ab41").unwrap(), + ); + let query = Query::from_domain( + &order, + &tokens, + &slippage, + chain_id, + contract_address, + false, + Some(12345_u64), + ) + .unwrap(); + + let actual = serde_json::to_value(query).unwrap(); + let expected = json!({ + "query": QUERY, + "variables": { + "callDataInput": { + "deadline": 12345, + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "0.000000000000000000001", + "swapType": "EXACT_OUT", + "tokenIn": "0x2170ed0880ac9a755fd29b2688956bd959f933f8", + "tokenOut": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "useProtocolVersion": 2 + } + }); + + assert_eq!(actual, expected); + } + + #[test] + fn test_human_readable_amount() { + let amount = + big_decimal_to_u256(&BigDecimal::from_str("1230000000000000000021").unwrap()).unwrap(); + let human_readable_amount = HumanReadableAmount::from_u256(&amount, 18); + assert_eq!( + human_readable_amount.value(), + BigDecimal::from_str("1230.000000000000000021").unwrap() + ); + assert_eq!(human_readable_amount.as_wei(), amount); + + let amount = + big_decimal_to_u256(&BigDecimal::from_str("1230000000000021").unwrap()).unwrap(); + let human_readable_amount = HumanReadableAmount::from_u256(&amount, 18); + assert_eq!( + human_readable_amount.value(), + BigDecimal::from_str("0.001230000000000021").unwrap() + ); + assert_eq!(human_readable_amount.as_wei(), amount); + } +} diff --git a/src/infra/dex/balancer/mod.rs b/src/infra/dex/balancer/mod.rs index 890f594..e23e42e 100644 --- a/src/infra/dex/balancer/mod.rs +++ b/src/infra/dex/balancer/mod.rs @@ -1,12 +1,22 @@ use { crate::{ - domain::{auction, dex, eth, order}, + domain::{ + auction, + dex, + eth::{self, TokenAddress}, + order, + }, util, }, contracts::ethcontract::I256, ethereum_types::U256, ethrpc::current_block::CurrentBlockStream, - std::sync::atomic::{self, AtomicU64}, + num::ToPrimitive, + std::{ + ops::Add, + sync::atomic::{self, AtomicU64}, + time::Duration, + }, tracing::Instrument, }; @@ -19,6 +29,8 @@ pub struct Sor { endpoint: reqwest::Url, vault: vault::Vault, settlement: eth::ContractAddress, + chain_id: eth::ChainId, + query_batch_swap: bool, } pub struct Config { @@ -33,6 +45,13 @@ pub struct Config { /// The address of the Settlement contract. pub settlement: eth::ContractAddress, + + /// For which chain the solver is configured. + pub chain_id: eth::ChainId, + + /// Whether to run `queryBatchSwap` to update the return amount with most + /// up-to-date on-chain values. + pub query_batch_swap: bool, } impl Sor { @@ -48,6 +67,8 @@ impl Sor { endpoint: config.endpoint, vault: vault::Vault::new(config.vault), settlement: config.settlement, + chain_id: config.chain_id, + query_batch_swap: config.query_batch_swap, } } @@ -55,9 +76,21 @@ impl Sor { &self, order: &dex::Order, slippage: &dex::Slippage, - gas_price: auction::GasPrice, + tokens: &auction::Tokens, ) -> Result { - let query = dto::Query::from_domain(order, gas_price); + let query = dto::Query::from_domain( + order, + tokens, + slippage, + self.chain_id, + self.settlement, + self.query_batch_swap, + // 2 minutes from now + chrono::Utc::now() + .add(Duration::from_secs(120)) + .timestamp() + .to_u64(), + )?; let quote = { // Set up a tracing span to make debugging of API requests easier. // Historically, debugging API requests to external DEXs was a bit @@ -74,8 +107,8 @@ impl Sor { } let (input, output) = match order.side { - order::Side::Buy => (quote.return_amount, quote.swap_amount), - order::Side::Sell => (quote.swap_amount, quote.return_amount), + order::Side::Buy => (quote.return_amount_raw, quote.swap_amount_raw), + order::Side::Sell => (quote.swap_amount_raw, quote.return_amount_raw), }; let (max_input, min_output) = match order.side { @@ -151,15 +184,15 @@ impl Sor { }) } - async fn quote(&self, query: &dto::Query) -> Result { - let quote = util::http::roundtrip!( - ; + async fn quote(&self, query: &dto::Query<'_>) -> Result { + let response = util::http::roundtrip!( + ; self.client .request(reqwest::Method::POST, self.endpoint.clone()) .json(query) ) .await?; - Ok(quote) + Ok(response.data.sor_get_swap_paths) } } @@ -171,6 +204,10 @@ pub enum Error { RateLimited, #[error(transparent)] Http(util::http::Error), + #[error("unsupported chain: {0:?}")] + UnsupportedChainId(eth::ChainId), + #[error("decimals are missing for the swapped token: {0:?}")] + MissingDecimals(TokenAddress), } impl From> for Error { diff --git a/src/infra/dex/mod.rs b/src/infra/dex/mod.rs index f9c0455..61929ea 100644 --- a/src/infra/dex/mod.rs +++ b/src/infra/dex/mod.rs @@ -30,10 +30,9 @@ impl Dex { order: &dex::Order, slippage: &dex::Slippage, tokens: &auction::Tokens, - gas_price: auction::GasPrice, ) -> Result { let swap = match self { - Dex::Balancer(balancer) => balancer.swap(order, slippage, gas_price).await?, + Dex::Balancer(balancer) => balancer.swap(order, slippage, tokens).await?, Dex::OneInch(oneinch) => oneinch.swap(order, slippage).await?, Dex::ZeroEx(zeroex) => zeroex.swap(order, slippage).await?, Dex::ParaSwap(paraswap) => paraswap.swap(order, slippage, tokens).await?, diff --git a/src/tests/balancer/market_order.rs b/src/tests/balancer/market_order.rs index 7b7ef79..12b8873 100644 --- a/src/tests/balancer/market_order.rs +++ b/src/tests/balancer/market_order.rs @@ -2,7 +2,11 @@ //! market orders, turning Balancer SOR responses into CoW Protocol solutions. use { - crate::tests::{self, balancer, mock}, + crate::tests::{ + self, + balancer::{self, SWAP_QUERY}, + mock, + }, serde_json::json, }; @@ -10,36 +14,46 @@ use { async fn sell() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::exact("sor"), - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "sell", - "amount": "1000000000000000000", - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial(json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "1", + "swapType": "EXACT_IN", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), vec!["variables.callDataInput.deadline"]), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "1000000000000000000", - "userData": "0x", - "returnAmount": "227598784442065388110" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "1000000000000000000", + "userData": "0x", + "returnAmount": "227598784442065388110" + } + ], + "swapAmountRaw": "1000000000000000000", + "returnAmountRaw": "227598784442065388110", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "1000000000000000000", - "swapAmountForSwaps": "1000000000000000000", - "returnAmount": "227598784442065388110", - "returnAmountFromSwaps": "227598784442065388110", - "returnAmountConsideringFees": "227307710853355710706", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }), }]) .await; @@ -179,36 +193,45 @@ async fn sell() { async fn buy() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::exact("sor"), - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "buy", - "amount": "100000000000000000000", - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial(json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "100", + "swapType": "EXACT_OUT", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), vec!["variables.callDataInput.deadline"]), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "100000000000000000000", - "userData": "0x", - "returnAmount": "439470293178110675" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "100000000000000000000", + "userData": "0x", + } + ], + "swapAmountRaw": "100000000000000000000", + "returnAmountRaw": "439470293178110675", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "100000000000000000000", - "swapAmountForSwaps": "100000000000000000000", - "returnAmount": "439470293178110675", - "returnAmountFromSwaps": "439470293178110675", - "returnAmountConsideringFees": "440745919677086983", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004394663712203829" + } }), }]) .await; diff --git a/src/tests/balancer/mod.rs b/src/tests/balancer/mod.rs index c71bf72..104b260 100644 --- a/src/tests/balancer/mod.rs +++ b/src/tests/balancer/mod.rs @@ -11,6 +11,36 @@ pub fn config(solver_addr: &SocketAddr) -> tests::Config { node-url = 'http://localhost:8545' [dex] endpoint = 'http://{solver_addr}/sor' +chain-id = '1' ", )) } + +// Copy from src/infra/dex/balancer/dto.rs +pub const SWAP_QUERY: &str = r#" +query sorGetSwapPaths($callDataInput: GqlSwapCallDataInput!, $chain: GqlChain!, $queryBatchSwap: Boolean!, $swapAmount: AmountHumanReadable!, $swapType: GqlSorSwapType!, $tokenIn: String!, $tokenOut: String!, $useProtocolVersion: Int) { + sorGetSwapPaths( + callDataInput: $callDataInput, + chain: $chain, + queryBatchSwap: $queryBatchSwap, + swapAmount: $swapAmount, + swapType: $swapType, + tokenIn: $tokenIn, + tokenOut: $tokenOut, + useProtocolVersion: $useProtocolVersion + ) { + tokenAddresses + swaps { + poolId + assetInIndex + assetOutIndex + amount + userData + } + swapAmountRaw + returnAmountRaw + tokenIn + tokenOut + } +} +"#; diff --git a/src/tests/balancer/not_found.rs b/src/tests/balancer/not_found.rs index 086848f..15b1294 100644 --- a/src/tests/balancer/not_found.rs +++ b/src/tests/balancer/not_found.rs @@ -2,38 +2,16 @@ //! swap was found for the specified quoted order. use { - crate::tests::{self, balancer, mock}, + crate::tests::{self, balancer}, serde_json::json, + std::{net::SocketAddr, str::FromStr}, }; /// Tests that orders get marked as "mandatory" in `/quote` requests. #[tokio::test] async fn test() { - let api = mock::http::setup(vec![mock::http::Expectation::Post { - path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0x1111111111111111111111111111111111111111", - "buyToken": "0x2222222222222222222222222222222222222222", - "orderKind": "sell", - "amount": "1000000000000000000", - "gasPrice": "15000000000", - })), - res: json!({ - "tokenAddresses": [], - "swaps": [], - "swapAmount": "0", - "swapAmountForSwaps": "0", - "returnAmount": "0", - "returnAmountFromSwaps": "0", - "returnAmountConsideringFees": "0", - "tokenIn": "", - "tokenOut": "", - "marketSp": "0", - }), - }]) - .await; - - let engine = tests::SolverEngine::new("balancer", balancer::config(&api.address)).await; + let api_address = SocketAddr::from_str("127.0.0.1:8080").unwrap(); + let engine = tests::SolverEngine::new("balancer", balancer::config(&api_address)).await; let solution = engine .solve(json!({ diff --git a/src/tests/balancer/out_of_price.rs b/src/tests/balancer/out_of_price.rs index e95fecd..0fce6b3 100644 --- a/src/tests/balancer/out_of_price.rs +++ b/src/tests/balancer/out_of_price.rs @@ -5,7 +5,11 @@ //! test cases with exuberant limit prices. use { - crate::tests::{self, balancer, mock}, + crate::tests::{ + self, + balancer::{self, SWAP_QUERY}, + mock, + }, serde_json::json, }; @@ -13,36 +17,45 @@ use { async fn sell() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "sell", - "amount": "1000000000000000000", - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial(json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "1", + "swapType": "EXACT_IN", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), vec!["variables.callDataInput.deadline"]), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "1000000000000000000", - "userData": "0x", - "returnAmount": "227598784442065388110" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "1000000000000000000", + "userData": "0x", + } + ], + "swapAmountRaw": "1000000000000000000", + "returnAmount": "227598784442065388110", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "1000000000000000000", - "swapAmountForSwaps": "1000000000000000000", - "returnAmount": "227598784442065388110", - "returnAmountFromSwaps": "227598784442065388110", - "returnAmountConsideringFees": "227307710853355710706", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }), }]) .await; @@ -114,36 +127,45 @@ async fn sell() { async fn buy() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "buy", - "amount": "100000000000000000000", - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial(json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "100", + "swapType": "EXACT_OUT", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), vec!["variables.callDataInput.deadline"]), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "100000000000000000000", - "userData": "0x", - "returnAmount": "439470293178110675" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "100000000000000000000", + "userData": "0x", + } + ], + "swapAmountRaw": "100000000000000000000", + "returnAmountRaw": "439470293178110675", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "100000000000000000000", - "swapAmountForSwaps": "100000000000000000000", - "returnAmount": "439470293178110675", - "returnAmountFromSwaps": "439470293178110675", - "returnAmountConsideringFees": "440745919677086983", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004394663712203829" + } }), }]) .await; diff --git a/src/tests/dex/partial_fill.rs b/src/tests/dex/partial_fill.rs index 4370126..f3d5f0e 100644 --- a/src/tests/dex/partial_fill.rs +++ b/src/tests/dex/partial_fill.rs @@ -1,5 +1,9 @@ use { - crate::tests::{self, balancer, mock}, + crate::tests::{ + self, + balancer::{self, SWAP_QUERY}, + mock, + }, serde_json::json, }; @@ -12,111 +16,122 @@ use { #[tokio::test] async fn tested_amounts_adjust_depending_on_response() { // observe::tracing::initialize_reentrant("solvers=trace"); - let inner_request = |amount| { - mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "sell", - "amount": amount, - "gasPrice": "15000000000", - })) + let inner_request = |ether_amount| { + mock::http::RequestBody::Partial( + json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": ether_amount, + "swapType": "EXACT_IN", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), + vec!["variables.callDataInput.deadline"], + ) }; let no_swap_found_response = json!({ - "tokenAddresses": [], - "swaps": [], - "swapAmount": "0", - "swapAmountForSwaps": "0", - "returnAmount": "0", - "returnAmountFromSwaps": "0", - "returnAmountConsideringFees": "0", - "tokenIn": "0x0000000000000000000000000000000000000000", - "tokenOut": "0x0000000000000000000000000000000000000000", - "marketSp": "0", + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [], + "swaps": [], + "swapAmountRaw": "0", + "returnAmountRaw": "0", + "tokenIn": "0x0000000000000000000000000000000000000000", + "tokenOut": "0x0000000000000000000000000000000000000000", + } + } }); - let limit_price_violation_response = |in_amount| { + let limit_price_violation_response = |in_wei_amount| { json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ - db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": in_amount, - "userData": "0x", - "returnAmount": "1" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ + db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": in_wei_amount, + "userData": "0x", + } + ], + "swapAmountRaw": in_wei_amount, + "returnAmountRaw": "1", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": in_amount, - "swapAmountForSwaps": in_amount, - "returnAmount": "1", - "returnAmountFromSwaps": "1", - "returnAmountConsideringFees": "1", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }) }; let api = mock::http::setup(vec![ mock::http::Expectation::Post { path: mock::http::Path::Any, - req: inner_request("16000000000000000000"), + req: inner_request("16"), res: no_swap_found_response.clone(), }, mock::http::Expectation::Post { path: mock::http::Path::Any, - req: inner_request("8000000000000000000"), + req: inner_request("8"), res: no_swap_found_response.clone(), }, mock::http::Expectation::Post { path: mock::http::Path::Any, - req: inner_request("4000000000000000000"), + req: inner_request("4"), res: limit_price_violation_response("4000000000000000000").clone(), }, mock::http::Expectation::Post { path: mock::http::Path::Any, - req: inner_request("2000000000000000000"), + req: inner_request("2"), res: limit_price_violation_response("2000000000000000000").clone(), }, mock::http::Expectation::Post { path: mock::http::Path::Any, - req: inner_request("1000000000000000000"), + req: inner_request("1"), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ - db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "1000000000000000000", - "userData": "0x", - "returnAmount": "227598784442065388110" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ + db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "1000000000000000000", + "userData": "0x", + } + ], + "swapAmountRaw": "1000000000000000000", + "returnAmountRaw": "227598784442065388110", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "1000000000000000000", - "swapAmountForSwaps": "1000000000000000000", - "returnAmount": "227598784442065388110", - "returnAmountFromSwaps": "227598784442065388110", - "returnAmountConsideringFees": "227307710853355710706", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }), }, // After a successful response we try the next time with a bigger amount. mock::http::Expectation::Post { path: mock::http::Path::Any, - req: inner_request("2000000000000000000"), + req: inner_request("2"), res: no_swap_found_response.clone(), }, ]) @@ -140,6 +155,7 @@ async fn tested_amounts_adjust_depending_on_response() { node-url = 'http://{}' [dex] endpoint = 'http://{}/sor' +chain-id = '1' ", simulation_node.address, api.address, )); @@ -302,48 +318,59 @@ async fn tested_amounts_wrap_around() { // Test is set up such that 2.5 BAL or exactly 0.01 ETH. // And the lowest amount we are willing to fill is 0.01 ETH. let fill_attempts = [ - "16000000000000000000", // 16 BAL == 0.064 ETH - "8000000000000000000", // 8 BAL == 0.032 ETH - "4000000000000000000", // 4 BAL == 0.016 ETH - // Next would be 2 BAL == 0.008 ETH which is below - // the minimum fill of 0.01 ETH so instead we start over. - "16000000000000000000", // 16 BAL == 0.06 ETH + ("16", "16000000000000000000"), // 16 BAL == 0.064 ETH + ("8", "8000000000000000000"), // 8 BAL == 0.032 ETH + ("4", "4000000000000000000"), // 4 BAL == 0.016 ETH + ("16", "16000000000000000000"), // 16 BAL == 0.064 ETH ] .into_iter() - .map(|amount| mock::http::Expectation::Post { + .map(|(amount_in, amount_in_wei)| mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "buy", - "amount": amount, - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial( + json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": amount_in, + "swapType": "EXACT_OUT", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), + vec!["variables.callDataInput.deadline"], + ), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ - db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": amount, - "userData": "0x", - "returnAmount": "70000000000000000" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ + db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": amount_in_wei, + "userData": "0x", + } + ], + "swapAmountRaw": amount_in_wei, + // Does not satisfy limit price of any chunk... + "returnAmountRaw": "700000000000000000", + "returnAmountConsideringFees": "1", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": amount, - "swapAmountForSwaps": amount, - // Does not satisfy limit price of any chunk... - "returnAmount": "70000000000000000", - "returnAmountFromSwaps": "70000000000000000", - "returnAmountConsideringFees": "1", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }), }) .collect(); @@ -429,59 +456,84 @@ async fn moves_surplus_fee_to_buy_token() { let api = mock::http::setup(vec![ mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "sell", - "amount": "2000000000000000000", - "gasPrice": "6000000000000", - })), + req: mock::http::RequestBody::Partial( + json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "2", + "swapType": "EXACT_IN", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), + vec!["variables.callDataInput.deadline"], + ), res: json!({ - "tokenAddresses": [], - "swaps": [], - "swapAmount": "0", - "swapAmountForSwaps": "0", - "returnAmount": "0", - "returnAmountFromSwaps": "0", - "returnAmountConsideringFees": "0", - "tokenIn": "0x0000000000000000000000000000000000000000", - "tokenOut": "0x0000000000000000000000000000000000000000", - "marketSp": "0", + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [], + "swaps": [], + "swapAmountRaw": "0", + "returnAmountRaw": "0", + "tokenIn": "0x0000000000000000000000000000000000000000", + "tokenOut": "0x0000000000000000000000000000000000000000", + } + } }), }, mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "sell", - "amount": "1000000000000000000", - "gasPrice": "6000000000000", - })), + req: mock::http::RequestBody::Partial( + json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "1", + "swapType": "EXACT_IN", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), + vec!["variables.callDataInput.deadline"], + ), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ - db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "1000000000000000000", - "userData": "0x", - "returnAmount": "227598784442065388110" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ + db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "1000000000000000000", + "userData": "0x", + } + ], + "swapAmountRaw": "1000000000000000000", + "returnAmountRaw": "227598784442065388110", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "1000000000000000000", - "swapAmountForSwaps": "1000000000000000000", - "returnAmount": "227598784442065388110", - "returnAmountFromSwaps": "227598784442065388110", - "returnAmountConsideringFees": "227307710853355710706", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }), }, ]) @@ -510,6 +562,7 @@ async fn moves_surplus_fee_to_buy_token() { node-url = 'http://{}' [dex] endpoint = 'http://{}/sor' +chain-id = '1' ", simulation_node.address, api.address, )); @@ -665,37 +718,49 @@ endpoint = 'http://{}/sor' async fn insufficient_room_for_surplus_fee() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "sell", - "amount": "1000000000000000000", - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial( + json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "1", + "swapType": "EXACT_IN", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), + vec!["variables.callDataInput.deadline"], + ), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ - db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "1000000000000000000", - "userData": "0x", - "returnAmount": "227598784442065388110" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ + db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "1000000000000000000", + "userData": "0x", + } + ], + "swapAmountRaw": "1000000000000000000", + "returnAmountRaw": "227598784442065388110", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "1000000000000000000", - "swapAmountForSwaps": "1000000000000000000", - "returnAmount": "227598784442065388110", - "returnAmountFromSwaps": "227598784442065388110", - "returnAmountConsideringFees": "227307710853355710706", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }), }]) .await; @@ -778,37 +843,50 @@ async fn insufficient_room_for_surplus_fee() { async fn market() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": "sell", - "amount": "1000000000000000000", - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial( + json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": "1", + "swapType": "EXACT_IN", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), + vec!["variables.callDataInput.deadline"], + ), res: json!({ - "tokenAddresses": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba100000625a3754423978a60c9317c58a424e3d" - ], - "swaps": [ - { - "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ - db8f56000200000000000000000014", - "assetInIndex": 0, - "assetOutIndex": 1, - "amount": "1000000000000000000", - "userData": "0x", - "returnAmount": "227598784442065388110" + "data": { + "sorGetSwapPaths": { + "tokenAddresses": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "swaps": [ + { + "poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820\ + db8f56000200000000000000000014", + "assetInIndex": 0, + "assetOutIndex": 1, + "amount": "1000000000000000000", + "userData": "0x", + "returnAmount": "227598784442065388110" + } + ], + "swapAmountRaw": "1000000000000000000", + "returnAmountRaw": "227598784442065388110", + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", } - ], - "swapAmount": "1000000000000000000", - "swapAmountForSwaps": "1000000000000000000", - "returnAmount": "227598784442065388110", - "returnAmountFromSwaps": "227598784442065388110", - "returnAmountConsideringFees": "227307710853355710706", - "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", + } }), }]) .await; diff --git a/src/tests/dex/wrong_execution.rs b/src/tests/dex/wrong_execution.rs index 1c6254c..86a8a05 100644 --- a/src/tests/dex/wrong_execution.rs +++ b/src/tests/dex/wrong_execution.rs @@ -1,5 +1,9 @@ use { - crate::tests::{self, balancer, mock}, + crate::tests::{ + self, + balancer::{self, SWAP_QUERY}, + mock, + }, serde_json::json, }; @@ -42,17 +46,34 @@ async fn test() { ] { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: mock::http::RequestBody::Exact(json!({ - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", - "orderKind": side, - "amount": if side == "sell" { - "1000000000000000000" - } else { - "227598784442065388110" - }, - "gasPrice": "15000000000", - })), + req: mock::http::RequestBody::Partial( + json!({ + "query": serde_json::to_value(SWAP_QUERY).unwrap(), + "variables": { + "callDataInput": { + "receiver": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "sender": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", + "slippagePercentage": "0.01" + }, + "chain": "MAINNET", + "queryBatchSwap": false, + "swapAmount": if side == "sell" { + "1" + } else { + "227.59878444206538811" + }, + "swapType": if side == "sell" { + "EXACT_IN" + } else { + "EXACT_OUT" + }, + "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", + "useProtocolVersion": 2 + } + }), + vec!["variables.callDataInput.deadline"], + ), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", @@ -66,17 +87,12 @@ async fn test() { "assetOutIndex": 1, "amount": input_amount, "userData": "0x", - "returnAmount": output_amount, } ], - "swapAmount": input_amount, - "swapAmountForSwaps": input_amount, - "returnAmount": output_amount, - "returnAmountFromSwaps": output_amount, - "returnAmountConsideringFees": output_amount, + "swapAmountRaw": input_amount, + "returnAmountRaw": output_amount, "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "tokenOut": "0xba100000625a3754423978a60c9317c58a424e3d", - "marketSp": "0.004393607339632106", }), }]) .await; diff --git a/src/tests/mock/http.rs b/src/tests/mock/http.rs index f91f59d..0e27555 100644 --- a/src/tests/mock/http.rs +++ b/src/tests/mock/http.rs @@ -1,4 +1,5 @@ use { + shared::assert_json_matches_excluding, std::{ fmt::{self, Debug, Formatter}, net::SocketAddr, @@ -80,6 +81,10 @@ pub enum Expectation { pub enum RequestBody { /// The received `[RequestBody]` has to match the provided value exactly. Exact(serde_json::Value), + /// The received `[RequestBody]` has to match the provided value partially + /// excluding the specified paths which are represented as dot-separated + /// strings. + Partial(serde_json::Value, Vec<&'static str>), /// Any `[RequestBody]` will be accepted. Any, } @@ -240,6 +245,13 @@ fn post( assert_eq!(full_path, expected_path, "POST request has unexpected path"); match expected_req { RequestBody::Exact(value) => assert_eq!(req, value, "POST request has unexpected body"), + RequestBody::Partial(value, exclude_paths) => { + let exclude_paths = exclude_paths + .iter() + .map(AsRef::as_ref) + .collect::>(); + assert_json_matches_excluding!(req, value, &exclude_paths) + } RequestBody::Any => (), } res