diff --git a/Cargo.lock b/Cargo.lock index b438a7e..28cc795 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3912,6 +3912,7 @@ dependencies = [ "anyhow", "async-trait", "axum", + "base64 0.22.1", "bigdecimal", "chrono", "clap", @@ -3921,6 +3922,7 @@ dependencies = [ "futures", "glob", "hex", + "hmac", "humantime", "humantime-serde", "hyper", @@ -3936,6 +3938,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sha2", "shared", "solvers-dto", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index cd3d214..0c634e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,14 @@ path = "src/main.rs" anyhow = "1" async-trait = "0.1.80" axum = "0.6" +base64 = "0.22.1" bigdecimal = { version = "0.3", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"], default-features = false } clap = { version = "4", features = ["derive", "env"] } ethereum-types = "0.14" futures = "0.3.30" hex = "0.4" +hmac = "0.12.1" humantime = "2.1.0" humantime-serde = "1.1.1" hyper = "0.14" @@ -34,6 +36,7 @@ reqwest = "0.11" serde = "1" serde_json = "1" serde_with = "3" +sha2 = "0.10.8" thiserror = "1" tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time"] } toml = "0.7" diff --git a/config/example.okx.toml b/config/example.okx.toml new file mode 100644 index 0000000..b8bf297 --- /dev/null +++ b/config/example.okx.toml @@ -0,0 +1,25 @@ +node-url = "http://localhost:8545" + +[dex] +# Specify which chain to use, 1 for Ethereum. +# More info here: https://www.okx.com/en-au/web3/build/docs/waas/walletapi-resources-supported-networks +chain-id = "1" + +# Optionally specify a custom OKX Swap API endpoint +# endpoint = "https://www.okx.com/api/v5/dex/aggregator/swap" + +# OKX Project ID. Instruction on how to create a project: +# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#create-project +api-project-id = "$OKX_PROJECT_ID" + +# OKX API Key. Instruction on how to generate an API key: +# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys +api-key = "$OKX_API_KEY" + +# OKX Secret key used for signing request. Instruction on how to get a security token: +# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key +api-secret-key = "$OKX_SECRET_KEY" + +# OKX Secret key passphrase. Instruction on how to get a passphrase: +# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys +api-passphrase = "$OKX_PASSPHRASE" diff --git a/src/infra/cli.rs b/src/infra/cli.rs index b8352e7..7107cc7 100644 --- a/src/infra/cli.rs +++ b/src/infra/cli.rs @@ -50,4 +50,9 @@ pub enum Command { #[clap(long, env)] config: PathBuf, }, + /// solve individual orders using OKX API + Okx { + #[clap(long, env)] + config: PathBuf, + }, } diff --git a/src/infra/config/dex/mod.rs b/src/infra/config/dex/mod.rs index 088b62a..4a054be 100644 --- a/src/infra/config/dex/mod.rs +++ b/src/infra/config/dex/mod.rs @@ -1,5 +1,6 @@ pub mod balancer; mod file; +pub mod okx; pub mod oneinch; pub mod paraswap; pub mod zeroex; diff --git a/src/infra/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs new file mode 100644 index 0000000..c1c41bf --- /dev/null +++ b/src/infra/config/dex/okx/file.rs @@ -0,0 +1,85 @@ +use { + crate::{ + domain::eth, + infra::{config::dex::file, dex::okx}, + util::serialize, + }, + serde::Deserialize, + serde_with::serde_as, + std::path::Path, +}; + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct Config { + /// The versioned URL endpoint for the OKX swap API. + #[serde(default = "default_endpoint")] + #[serde_as(as = "serde_with::DisplayFromStr")] + endpoint: reqwest::Url, + + /// Chain ID used to automatically determine contract addresses. + #[serde_as(as = "serialize::ChainId")] + chain_id: eth::ChainId, + + /// OKX API credentials + #[serde(flatten)] + okx_credentials: OkxCredentialsConfig, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct OkxCredentialsConfig { + /// OKX Project ID. Instruction on how to create a project: + /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#create-project + api_project_id: String, + + /// OKX API Key. Instruction on how to generate an API key: + /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + api_key: String, + + /// OKX Secret key used for signing request. Instruction on how to get a + /// security token: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key + api_secret_key: String, + + /// OKX Secret key passphrase. Instruction on how to get a passphrase: + /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + api_passphrase: String, +} + +#[allow(clippy::from_over_into)] +impl Into for OkxCredentialsConfig { + fn into(self) -> okx::OkxCredentialsConfig { + okx::OkxCredentialsConfig { + project_id: self.api_project_id, + api_key: self.api_key, + api_secret_key: self.api_secret_key, + api_passphrase: self.api_passphrase, + } + } +} + +fn default_endpoint() -> reqwest::Url { + "https://www.okx.com/api/v5/dex/aggregator/swap" + .parse() + .unwrap() +} + +/// Load the OKX solver configuration from a TOML file. +/// +/// # Panics +/// +/// 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; + + super::Config { + okx: okx::Config { + endpoint: config.endpoint, + chain_id: config.chain_id, + okx_credentials: config.okx_credentials.into(), + block_stream: base.block_stream.clone(), + }, + base, + } +} diff --git a/src/infra/config/dex/okx/mod.rs b/src/infra/config/dex/okx/mod.rs new file mode 100644 index 0000000..df781cf --- /dev/null +++ b/src/infra/config/dex/okx/mod.rs @@ -0,0 +1,6 @@ +pub mod file; + +pub struct Config { + pub okx: crate::infra::dex::okx::Config, + pub base: super::Config, +} diff --git a/src/infra/dex/mod.rs b/src/infra/dex/mod.rs index cad5afc..5509a88 100644 --- a/src/infra/dex/mod.rs +++ b/src/infra/dex/mod.rs @@ -5,6 +5,7 @@ use { }; pub mod balancer; +pub mod okx; pub mod oneinch; pub mod paraswap; pub mod simulator; @@ -18,6 +19,7 @@ pub enum Dex { OneInch(oneinch::OneInch), ZeroEx(zeroex::ZeroEx), ParaSwap(paraswap::ParaSwap), + Okx(okx::Okx), } impl Dex { @@ -36,6 +38,7 @@ impl Dex { 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?, + Dex::Okx(okx) => okx.swap(order, slippage).await?, }; Ok(swap) } @@ -141,3 +144,14 @@ impl From for Error { } } } + +impl From for Error { + fn from(err: okx::Error) -> Self { + match err { + okx::Error::OrderNotSupported => Self::OrderNotSupported, + okx::Error::NotFound => Self::NotFound, + okx::Error::RateLimited => Self::RateLimited, + _ => Self::Other(Box::new(err)), + } + } +} diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs new file mode 100644 index 0000000..16345e9 --- /dev/null +++ b/src/infra/dex/okx/dto.rs @@ -0,0 +1,158 @@ +//! DTOs for the OKX swap API. Full documentation for the API can be found +//! [here](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap). + +use { + crate::{ + domain::{dex, order}, + util::serialize, + }, + bigdecimal::BigDecimal, + ethereum_types::{H160, U256}, + serde::{Deserialize, Serialize}, + serde_with::serde_as, +}; + +/// A OKX API swap request parameters (only mandatory fields). +/// OKX supports only sell orders. +/// +/// See [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap) +/// documentation for more detailed information on each parameter. +#[serde_as] +#[derive(Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapRequest { + /// Chain ID + #[serde_as(as = "serde_with::DisplayFromStr")] + pub chain_id: u64, + + /// Input amount of a token to be sold set in minimal divisible units. + #[serde_as(as = "serialize::U256")] + pub amount: U256, + + /// Contract address of a token to be sent + pub from_token_address: H160, + + /// Contract address of a token to be received + pub to_token_address: H160, + + /// Limit of price slippage you are willing to accept + pub slippage: Slippage, + + /// User's wallet address. Where the sell tokens will be taken from. + pub user_wallet_address: H160, +} + +/// A OKX slippage amount. +#[derive(Clone, Debug, Default, Serialize)] +pub struct Slippage(BigDecimal); + +impl SwapRequest { + pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Option { + // Buy orders are not supported on OKX + if order.side == order::Side::Buy { + return None; + }; + + Some(Self { + from_token_address: order.sell.0, + to_token_address: order.buy.0, + amount: order.amount.get(), + slippage: Slippage(slippage.as_factor().clone()), + user_wallet_address: order.owner, + ..self + }) + } +} + +/// A OKX API swap response - generic wrapper for success and failure cases. +/// +/// See [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap) +/// documentation for more detailed information on each parameter. +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponse { + /// Error code, 0 for success, otherwise one of: + /// [error codes](https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code) + #[serde_as(as = "serde_with::DisplayFromStr")] + pub code: i64, + + /// Response data. + pub data: Vec, + + /// Error code text message. + pub msg: String, +} + +/// A OKX API swap response. +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseInner { + /// Quote execution path. + pub router_result: SwapResponseRouterResult, + + /// Contract related response. + pub tx: SwapResponseTx, +} + +/// A OKX API swap response - quote execution path. +/// Deserializing fields which are only used by the implementation. +/// For all possible fields look into the documentation: +/// [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap) +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseRouterResult { + /// The information of a token to be sold. + pub from_token: SwapResponseFromToToken, + + /// The information of a token to be bought. + pub to_token: SwapResponseFromToToken, + + /// The input amount of a token to be sold. + #[serde_as(as = "serialize::U256")] + pub from_token_amount: U256, + + /// The resulting amount of a token to be bought. + #[serde_as(as = "serialize::U256")] + pub to_token_amount: U256, +} + +/// A OKX API swap response - token information. +/// Deserializing fields which are only used by the implementation. +/// For all possible fields look into the documentation: +/// [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap) +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseFromToToken { + /// Address of the token smart contract. + pub token_contract_address: H160, +} + +/// A OKX API swap response - contract related information. +/// Deserializing fields which are only used by the implementation. +/// For all possible fields look into the documentation: +/// [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap) +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseTx { + /// Estimated amount of the gas limit. + #[serde_as(as = "serialize::U256")] + pub gas: U256, + + /// The contract address of OKX DEX router. + pub to: H160, + + /// Call data. + #[serde_as(as = "serialize::Hex")] + pub data: Vec, +} + +#[derive(Deserialize)] +pub struct Error { + pub code: i64, + pub reason: String, +} diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs new file mode 100644 index 0000000..60a4cd3 --- /dev/null +++ b/src/infra/dex/okx/mod.rs @@ -0,0 +1,277 @@ +use { + crate::{ + domain::{dex, eth}, + util, + }, + base64::prelude::*, + chrono::SecondsFormat, + ethrpc::block_stream::CurrentBlockWatcher, + hmac::{Hmac, Mac}, + hyper::{header::HeaderValue, StatusCode}, + sha2::Sha256, + std::sync::atomic::{self, AtomicU64}, + tracing::Instrument, +}; + +mod dto; + +/// Bindings to the OKX swap API. +pub struct Okx { + client: super::Client, + endpoint: reqwest::Url, + api_secret_key: String, + defaults: dto::SwapRequest, +} + +pub struct Config { + /// The URL for the 0KX swap API. + pub endpoint: reqwest::Url, + + pub chain_id: eth::ChainId, + + /// Credentials used to access OKX API. + pub okx_credentials: OkxCredentialsConfig, + + /// The stream that yields every new block. + pub block_stream: Option, +} + +pub struct OkxCredentialsConfig { + /// OKX project ID to use. + pub project_id: String, + + /// OKX API key. + pub api_key: String, + + /// OKX API key additional security token. + pub api_secret_key: String, + + /// OKX API key passphrase used to encrypt secret key. + pub api_passphrase: String, +} + +impl Okx { + pub fn try_new(config: Config) -> Result { + let client = { + let mut api_key = + reqwest::header::HeaderValue::from_str(&config.okx_credentials.api_key)?; + api_key.set_sensitive(true); + let mut api_passphrase = + reqwest::header::HeaderValue::from_str(&config.okx_credentials.api_passphrase)?; + api_passphrase.set_sensitive(true); + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + "OK-ACCESS-PROJECT", + reqwest::header::HeaderValue::from_str(&config.okx_credentials.project_id)?, + ); + headers.insert("OK-ACCESS-KEY", api_key); + headers.insert("OK-ACCESS-PASSPHRASE", api_passphrase); + + let client = reqwest::Client::builder() + .default_headers(headers) + .build()?; + super::Client::new(client, config.block_stream) + }; + + let defaults = dto::SwapRequest { + chain_id: config.chain_id as u64, + ..Default::default() + }; + + Ok(Self { + client, + endpoint: config.endpoint, + api_secret_key: config.okx_credentials.api_secret_key, + defaults, + }) + } + + /// OKX requires signature of the request to be added as dedicated HTTP + /// Header. More information on generating the signature can be found in + /// OKX documentation: https://www.okx.com/en-au/web3/build/docs/waas/rest-authentication#signature + fn generate_signature( + &self, + request: &reqwest::Request, + timestamp: &str, + ) -> Result { + let mut data = String::new(); + data.push_str(timestamp); + data.push_str(request.method().as_str()); + data.push_str(request.url().path()); + data.push('?'); + data.push_str(request.url().query().ok_or(Error::SignRequestFailed)?); + + let mut mac = Hmac::::new_from_slice(self.api_secret_key.as_bytes()) + .map_err(|_| Error::SignRequestFailed)?; + mac.update(data.as_bytes()); + let signature = mac.finalize().into_bytes(); + + Ok(BASE64_STANDARD.encode(signature)) + } + + /// OKX Error codes: [link](https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code) + fn handle_api_error(code: i64, message: &str) -> Result<(), Error> { + Err(match code { + 0 => return Ok(()), + 82000 => Error::NotFound, // Insufficient liquidity + 82104 => Error::NotFound, // Token not supported + 50011 => Error::RateLimited, + _ => Error::Api { + code, + reason: message.to_string(), + }, + }) + } + + pub async fn swap( + &self, + order: &dex::Order, + slippage: &dex::Slippage, + ) -> Result { + let query = self + .defaults + .clone() + .with_domain(order, slippage) + .ok_or(Error::OrderNotSupported)?; + let quote = { + // Set up a tracing span to make debugging of API requests easier. + // Historically, debugging API requests to external DEXs was a bit + // of a headache. + static ID: AtomicU64 = AtomicU64::new(0); + let id = ID.fetch_add(1, atomic::Ordering::Relaxed); + self.quote(&query) + .instrument(tracing::trace_span!("quote", id = %id)) + .await? + }; + + Self::handle_api_error(quote.code, "e.msg)?; + let quote_result = quote.data.first().ok_or(Error::NotFound)?; + + // Increasing returned gas by 50% according to the documentation: + // https://www.okx.com/en-au/web3/build/docs/waas/dex-swap (gas field description in Response param) + let gas = quote_result + .tx + .gas + .checked_add(quote_result.tx.gas / 2) + .ok_or(Error::GasCalculationFailed)?; + + Ok(dex::Swap { + call: dex::Call { + to: eth::ContractAddress(quote_result.tx.to), + calldata: quote_result.tx.data.clone(), + }, + input: eth::Asset { + token: quote_result + .router_result + .from_token + .token_contract_address + .into(), + amount: quote_result.router_result.from_token_amount, + }, + output: eth::Asset { + token: quote_result + .router_result + .to_token + .token_contract_address + .into(), + amount: quote_result.router_result.to_token_amount, + }, + allowance: dex::Allowance { + spender: eth::ContractAddress(quote_result.tx.to), + amount: dex::Amount::new(quote_result.router_result.from_token_amount), + }, + gas: eth::Gas(gas), + }) + } + + async fn quote(&self, query: &dto::SwapRequest) -> Result { + let mut request_builder = self + .client + .request(reqwest::Method::GET, self.endpoint.clone()) + .query(query); + + let request = request_builder + .try_clone() + .ok_or(Error::RequestBuildFailed)? + .build() + .map_err(|_| Error::RequestBuildFailed)?; + + let timestamp = &chrono::Utc::now() + .to_rfc3339_opts(SecondsFormat::Millis, true) + .to_string(); + let signature = self.generate_signature(&request, timestamp)?; + + request_builder = request_builder.header( + "OK-ACCESS-TIMESTAMP", + reqwest::header::HeaderValue::from_str(timestamp) + .map_err(|_| Error::RequestBuildFailed)?, + ); + request_builder = request_builder.header( + "OK-ACCESS-SIGN", + HeaderValue::from_str(&signature).map_err(|_| Error::RequestBuildFailed)?, + ); + + let quote = util::http::roundtrip!( + ; + request_builder + ) + .await?; + Ok(quote) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CreationError { + #[error(transparent)] + Header(#[from] reqwest::header::InvalidHeaderValue), + #[error(transparent)] + Client(#[from] reqwest::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("failed to build the request")] + RequestBuildFailed, + #[error("failed to sign the request")] + SignRequestFailed, + #[error("calculating output gas failed")] + GasCalculationFailed, + #[error("unable to find a quote")] + NotFound, + #[error("order type is not supported")] + OrderNotSupported, + #[error("rate limited")] + RateLimited, + #[error("api error code {code}: {reason}")] + Api { code: i64, reason: String }, + #[error(transparent)] + Http(util::http::Error), +} + +impl From> for Error { + // This function is only called when swap response body is not a valid json. + // OKX is returning valid json for 4xx HTTP codes, and the errors are handled in + // dedicated function: handle_api_error(). + fn from(err: util::http::RoundtripError) -> Self { + match err { + util::http::RoundtripError::Http(err) => { + if let util::http::Error::Status(code, _) = err { + match code { + StatusCode::TOO_MANY_REQUESTS => Self::RateLimited, + _ => Self::Http(err), + } + } else { + Self::Http(err) + } + } + util::http::RoundtripError::Api(err) => match err.code { + 429 => Self::RateLimited, + _ => Self::Api { + code: err.code, + reason: err.reason, + }, + }, + } + } +} diff --git a/src/run.rs b/src/run.rs index 66e7d83..3cca959 100644 --- a/src/run.rs +++ b/src/run.rs @@ -61,6 +61,15 @@ async fn run_with(args: cli::Args, bind: Option>) { config.base.clone(), )) } + cli::Command::Okx { config } => { + let config = config::dex::okx::file::load(&config).await; + Solver::Dex(solver::Dex::new( + dex::Dex::Okx( + dex::okx::Okx::try_new(config.okx).expect("invalid OKX configuration"), + ), + config.base.clone(), + )) + } }; crate::api::Api { diff --git a/src/tests/mod.rs b/src/tests/mod.rs index a32105c..09d6fa3 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -14,6 +14,7 @@ use { mod balancer; mod dex; mod mock; +mod okx; mod oneinch; mod paraswap; mod zeroex; diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs new file mode 100644 index 0000000..9c0648d --- /dev/null +++ b/src/tests/okx/api_calls.rs @@ -0,0 +1,164 @@ +use { + crate::{ + domain::{dex::*, eth::*}, + infra::dex::okx as okx_dex, + }, + ethereum_types::H160, + std::{env, str::FromStr}, +}; + +#[ignore] +#[tokio::test] +// To run this test, set the following environment variables accordingly to your +// OKX setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn swap_sell() { + let okx_config = okx_dex::Config { + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/swap").unwrap(), + chain_id: crate::domain::eth::ChainId::Mainnet, + okx_credentials: okx_dex::OkxCredentialsConfig { + project_id: env::var("OKX_PROJECT_ID").unwrap(), + api_key: env::var("OKX_API_KEY").unwrap(), + api_secret_key: env::var("OKX_SECRET_KEY").unwrap(), + api_passphrase: env::var("OKX_PASSPHRASE").unwrap(), + }, + block_stream: None, + }; + + let order = Order { + sell: TokenAddress::from(H160::from_slice( + &hex::decode("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), + )), + buy: TokenAddress::from(H160::from_slice( + &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + side: crate::domain::order::Side::Sell, + amount: Amount::new(U256::from_dec_str("10000000000000").unwrap()), + owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + }; + + let slippage = Slippage::one_percent(); + + let okx = crate::infra::dex::okx::Okx::try_new(okx_config).unwrap(); + let swap_response = okx.swap(&order, &slippage).await; + let swap = swap_response.unwrap(); + + assert_eq!(swap.input.token, order.amount().token); + assert_eq!(swap.input.amount, order.amount().amount); + assert_eq!(swap.output.token, order.buy); +} + +#[tokio::test] +async fn swap_buy() { + let okx_config = okx_dex::Config { + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/swap").unwrap(), + chain_id: crate::domain::eth::ChainId::Mainnet, + okx_credentials: okx_dex::OkxCredentialsConfig { + project_id: String::new(), + api_key: String::new(), + api_secret_key: String::new(), + api_passphrase: String::new(), + }, + block_stream: None, + }; + + let order = Order { + buy: TokenAddress::from(H160::from_slice( + &hex::decode("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), + )), + sell: TokenAddress::from(H160::from_slice( + &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + side: crate::domain::order::Side::Buy, + amount: Amount::new(U256::from_dec_str("10000000000000").unwrap()), + owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + }; + + let slippage = Slippage::one_percent(); + + let okx = crate::infra::dex::okx::Okx::try_new(okx_config).unwrap(); + let swap_response = okx.swap(&order, &slippage).await; + assert!(matches!( + swap_response.unwrap_err(), + crate::infra::dex::okx::Error::OrderNotSupported + )); +} + +#[ignore] +#[tokio::test] +// To run this test, set the following environment variables accordingly to your +// OKX setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn swap_api_error() { + let okx_config = okx_dex::Config { + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/swap").unwrap(), + chain_id: crate::domain::eth::ChainId::Mainnet, + okx_credentials: okx_dex::OkxCredentialsConfig { + project_id: env::var("OKX_PROJECT_ID").unwrap(), + api_key: env::var("OKX_API_KEY").unwrap(), + api_secret_key: env::var("OKX_SECRET_KEY").unwrap(), + api_passphrase: env::var("OKX_PASSPHRASE").unwrap(), + }, + block_stream: None, + }; + + let order = Order { + sell: TokenAddress::from(H160::from_slice( + &hex::decode("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), + )), + buy: TokenAddress::from(H160::from_slice( + &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + side: crate::domain::order::Side::Sell, + amount: Amount::new(U256::from_str("0").unwrap()), + owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + }; + + let slippage = Slippage::one_percent(); + + let okx = crate::infra::dex::okx::Okx::try_new(okx_config).unwrap(); + let swap_response = okx.swap(&order, &slippage).await; + + assert!(matches!( + swap_response.unwrap_err(), + crate::infra::dex::okx::Error::Api { .. } + )); +} + +#[ignore] +#[tokio::test] +// To run this test, set the following environment variables accordingly to your +// OKX setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn swap_sell_insufficient_liquidity() { + let okx_config = okx_dex::Config { + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/swap").unwrap(), + chain_id: crate::domain::eth::ChainId::Mainnet, + okx_credentials: okx_dex::OkxCredentialsConfig { + project_id: env::var("OKX_PROJECT_ID").unwrap(), + api_key: env::var("OKX_API_KEY").unwrap(), + api_secret_key: env::var("OKX_SECRET_KEY").unwrap(), + api_passphrase: env::var("OKX_PASSPHRASE").unwrap(), + }, + block_stream: None, + }; + + let order = Order { + sell: TokenAddress::from(H160::from_slice( + &hex::decode("C8CD2BE653759aed7B0996315821AAe71e1FEAdF").unwrap(), + )), + buy: TokenAddress::from(H160::from_slice( + &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + )), + side: crate::domain::order::Side::Sell, + amount: Amount::new(U256::from_dec_str("10000000000000").unwrap()), + owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + }; + + let slippage = Slippage::one_percent(); + + let okx = crate::infra::dex::okx::Okx::try_new(okx_config).unwrap(); + let swap_response = okx.swap(&order, &slippage).await; + + assert!(matches!( + swap_response.unwrap_err(), + crate::infra::dex::okx::Error::NotFound + )); +} diff --git a/src/tests/okx/market_order.rs b/src/tests/okx/market_order.rs new file mode 100644 index 0000000..7e068dd --- /dev/null +++ b/src/tests/okx/market_order.rs @@ -0,0 +1,298 @@ +//! This test ensures that the OKX solver properly handles market sell +//! orders, turning OKX swap responses into CoW Protocol solutions. + +use { + crate::tests::{self, mock}, + serde_json::json, +}; + +#[tokio::test] +async fn sell() { + let api = mock::http::setup(vec![ + mock::http::Expectation::Get { + path: mock::http::Path::exact( + "?chainId=1\ + &amount=1000000000000000000\ + &fromTokenAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\ + &toTokenAddress=0xe41d2489571d322189246dafa5ebde1f4699f498\ + &slippage=0.01\ + &userWalletAddress=0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + ), + res: json!( + { + "code":"0", + "data":[ + { + "routerResult":{ + "chainId":"1", + "dexRouterList":[ + { + "router":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2--0xe41d2489571d322189246dafa5ebde1f4699f498", + "routerPercent":"100", + "subRouterList":[ + { + "dexProtocol":[ + { + "dexName":"Uniswap V3", + "percent":"100" + } + ], + "fromToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenSymbol":"WETH", + "tokenUnitPrice":"3315.553196726842565048" + }, + "toToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xe41d2489571d322189246dafa5ebde1f4699f498", + "tokenSymbol":"ZRX", + "tokenUnitPrice":"0.504455838152300152" + } + } + ] + } + ], + "estimateGasFee":"135000", + "fromToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenSymbol":"WETH", + "tokenUnitPrice":"3315.553196726842565048" + }, + "fromTokenAmount":"1000000000000000000", + "priceImpactPercentage":"-0.25", + "quoteCompareList":[ + { + "amountOut":"6556.259156432631386442", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/UNI.png", + "dexName":"Uniswap V3", + "tradeFee":"2.3554356342513966" + }, + { + "amountOut":"6375.198002761542738881", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/UNI.png", + "dexName":"Uniswap V2", + "tradeFee":"3.34995290204643072" + }, + { + "amountOut":"4456.799978982369793812", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/UNI.png", + "dexName":"Uniswap V1", + "tradeFee":"4.64638467513839940864" + }, + { + "amountOut":"2771.072269036022134969", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/SUSHI.png", + "dexName":"SushiSwap", + "tradeFee":"3.34995290204643072" + } + ], + "toToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xe41d2489571d322189246dafa5ebde1f4699f498", + "tokenSymbol":"ZRX", + "tokenUnitPrice":"0.504455838152300152" + }, + "toTokenAmount":"6556259156432631386442", + "tradeFee":"2.3554356342513966" + }, + "tx":{ + "data":"0x0d5f0e3b00000000000000000001a0cf2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000015fdc8278903f7f31c10000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000014424eeecbff345b38187d0b8b749e56faa68539", + "from":"0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + "gas":"202500", + "gasPrice":"6756286873", + "maxPriorityFeePerGas":"1000000000", + "minReceiveAmount":"6490696564868305072578", + "signatureData":[ + "" + ], + "slippage":"0.01", + "to":"0x7D0CcAa3Fac1e5A943c5168b6CEd828691b46B36", + "value":"0" + } + } + ], + "msg":"" + }), + } + ]) + .await; + + let engine = tests::SolverEngine::new("okx", super::config(&api.address)).await; + + let solution = engine + .solve(json!({ + "id": "1", + "tokens": { + "0xe41d2489571d322189246dafa5ebde1f4699f498": { + "decimals": 18, + "symbol": "ZRX", + "referencePrice": "4327903683155778", + "availableBalance": "1583034704488033979459", + "trusted": true, + }, + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": { + "decimals": 18, + "symbol": "WETH", + "referencePrice": "1000000000000000000", + "availableBalance": "482725140468789680", + "trusted": true, + }, + }, + "orders": [ + { + "uid": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a", + "sellToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", + "sellAmount": "1000000000000000000", + "buyAmount": "200000000000000000000", + "fullSellAmount": "1000000000000000000", + "fullBuyAmount": "200000000000000000000", + "kind": "sell", + "partiallyFillable": false, + "class": "market", + "sellTokenSource": "erc20", + "buyTokenDestination": "erc20", + "preInteractions": [], + "postInteractions": [], + "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", + "validTo": 0, + "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "signingScheme": "presign", + "signature": "0x", + } + ], + "liquidity": [], + "effectiveGasPrice": "15000000000", + "deadline": "2106-01-01T00:00:00.000Z", + "surplusCapturingJitOrderOwners": [] + })) + .await + .unwrap(); + + assert_eq!( + solution, + json!({ + "solutions":[ + { + "gas":410141, + "id":0, + "interactions":[ + { + "allowances":[ + { + "amount":"1000000000000000000", + "spender":"0x7d0ccaa3fac1e5a943c5168b6ced828691b46b36", + "token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + } + ], + "callData":"0x0d5f0e3b00000000000000000001a0cf2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000015fdc8278903f7f31c10000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000014424eeecbff345b38187d0b8b749e56faa68539", + "inputs":[ + { + "amount":"1000000000000000000", + "token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + } + ], + "internalize":false, + "kind":"custom", + "outputs":[ + { + "amount":"6556259156432631386442", + "token":"0xe41d2489571d322189246dafa5ebde1f4699f498" + } + ], + "target":"0x7d0ccaa3fac1e5a943c5168b6ced828691b46b36", + "value":"0" + } + ], + "postInteractions":[], + "preInteractions":[], + "prices":{ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2":"6556259156432631386442", + "0xe41d2489571d322189246dafa5ebde1f4699f498":"1000000000000000000" + }, + "trades":[ + { + "executedAmount":"1000000000000000000", + "kind":"fulfillment", + "order":"0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + } + ] + } + ] + }), + ); +} + +#[tokio::test] +async fn buy() { + let api = mock::http::setup(vec![]).await; + + let engine = tests::SolverEngine::new("okx", super::config(&api.address)).await; + + let solution = engine + .solve(json!({ + "id": "1", + "tokens": { + "0xe41d2489571d322189246dafa5ebde1f4699f498": { + "decimals": 18, + "symbol": "ZRX", + "referencePrice": "4327903683155778", + "availableBalance": "1583034704488033979459", + "trusted": true, + }, + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": { + "decimals": 18, + "symbol": "WETH", + "referencePrice": "1000000000000000000", + "availableBalance": "482725140468789680", + "trusted": true, + }, + }, + "orders": [ + { + "uid": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a", + "sellToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", + "sellAmount": "1000000000000000000", + "buyAmount": "200000000000000000000", + "fullSellAmount": "1000000000000000000", + "fullBuyAmount": "200000000000000000000", + "kind": "buy", + "partiallyFillable": false, + "class": "market", + "sellTokenSource": "erc20", + "buyTokenDestination": "erc20", + "preInteractions": [], + "postInteractions": [], + "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", + "validTo": 0, + "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "signingScheme": "presign", + "signature": "0x", + } + ], + "liquidity": [], + "effectiveGasPrice": "15000000000", + "deadline": "2106-01-01T00:00:00.000Z", + "surplusCapturingJitOrderOwners": [] + })) + .await + .unwrap(); + + // Buy order is not supported on OKX. + assert_eq!(solution, json!({ "solutions": [] }),); +} diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs new file mode 100644 index 0000000..eba6274 --- /dev/null +++ b/src/tests/okx/mod.rs @@ -0,0 +1,22 @@ +use {crate::tests, std::net::SocketAddr}; + +mod api_calls; +mod market_order; +mod not_found; +mod out_of_price; + +/// Creates a temporary file containing the config of the given solver. +pub fn config(solver_addr: &SocketAddr) -> tests::Config { + tests::Config::String(format!( + r" +node-url = 'http://localhost:8545' +[dex] +chain-id = '1' +endpoint = 'http://{solver_addr}' +api-project-id = '1' +api-key = '1234' +api-secret-key = '1234567890123456' +api-passphrase = 'pass' +", + )) +} diff --git a/src/tests/okx/not_found.rs b/src/tests/okx/not_found.rs new file mode 100644 index 0000000..17f129a --- /dev/null +++ b/src/tests/okx/not_found.rs @@ -0,0 +1,77 @@ +//! This test ensures that the OKX solver properly handles cases where no swap +//! was found for the specified order. + +use { + crate::tests::{self, mock}, + serde_json::json, +}; + +#[tokio::test] +async fn sell() { + let api = mock::http::setup(vec![mock::http::Expectation::Get { + path: mock::http::Path::exact( + "?chainId=1&amount=1000000000000000000&\ + fromTokenAddress=0xc8cd2be653759aed7b0996315821aae71e1feadf&\ + toTokenAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&slippage=0.01&\ + userWalletAddress=0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + ), + res: json!({"code":"82000","data":[],"msg":"Insufficient liquidity."}), + }]) + .await; + + let engine = tests::SolverEngine::new("okx", super::config(&api.address)).await; + + let solution = engine + .solve(json!({ + "id": "1", + "tokens": { + "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF": { + "decimals": 18, + "symbol": "TETH", + "referencePrice": "4327903683155778", + "availableBalance": "1583034704488033979459", + "trusted": true, + }, + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": { + "decimals": 18, + "symbol": "WETH", + "referencePrice": "1000000000000000000", + "availableBalance": "482725140468789680", + "trusted": true, + }, + }, + "orders": [ + { + "uid": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a", + "sellToken": "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF", + "buyToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "sellAmount": "1000000000000000000", + "buyAmount": "200000000000000000000", + "fullSellAmount": "1000000000000000000", + "fullBuyAmount": "200000000000000000000", + "kind": "sell", + "partiallyFillable": false, + "class": "market", + "sellTokenSource": "erc20", + "buyTokenDestination": "erc20", + "preInteractions": [], + "postInteractions": [], + "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", + "validTo": 0, + "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "signingScheme": "presign", + "signature": "0x", + } + ], + "liquidity": [], + "effectiveGasPrice": "15000000000", + "deadline": "2106-01-01T00:00:00.000Z", + "surplusCapturingJitOrderOwners": [] + })) + .await + .unwrap(); + + assert_eq!(solution, json!({ "solutions": [] }),); +} diff --git a/src/tests/okx/out_of_price.rs b/src/tests/okx/out_of_price.rs new file mode 100644 index 0000000..2a3f91b --- /dev/null +++ b/src/tests/okx/out_of_price.rs @@ -0,0 +1,189 @@ +//! This test verifies that the OKX solver does not generate solutions when +//! the swap returned from the API does not satisfy an order's limit price. +//! +//! The actual test case is a modified version of the [`super::market_order`] +//! test with an exuberant buy amount. + +use { + crate::tests::{self, mock}, + serde_json::json, +}; + +#[tokio::test] +async fn sell() { + let api = mock::http::setup(vec![ + mock::http::Expectation::Get { + path: mock::http::Path::exact( + "?chainId=1\ + &amount=1000000000000000000\ + &fromTokenAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\ + &toTokenAddress=0xe41d2489571d322189246dafa5ebde1f4699f498\ + &slippage=0.01\ + &userWalletAddress=0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + ), + res: json!( + { + "code":"0", + "data":[ + { + "routerResult":{ + "chainId":"1", + "dexRouterList":[ + { + "router":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2--0xe41d2489571d322189246dafa5ebde1f4699f498", + "routerPercent":"100", + "subRouterList":[ + { + "dexProtocol":[ + { + "dexName":"Uniswap V3", + "percent":"100" + } + ], + "fromToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenSymbol":"WETH", + "tokenUnitPrice":"3315.553196726842565048" + }, + "toToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xe41d2489571d322189246dafa5ebde1f4699f498", + "tokenSymbol":"ZRX", + "tokenUnitPrice":"0.504455838152300152" + } + } + ] + } + ], + "estimateGasFee":"135000", + "fromToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "tokenSymbol":"WETH", + "tokenUnitPrice":"3315.553196726842565048" + }, + "fromTokenAmount":"1000000000000000000", + "priceImpactPercentage":"-0.25", + "quoteCompareList":[ + { + "amountOut":"6556.259156432631386442", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/UNI.png", + "dexName":"Uniswap V3", + "tradeFee":"2.3554356342513966" + }, + { + "amountOut":"6375.198002761542738881", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/UNI.png", + "dexName":"Uniswap V2", + "tradeFee":"3.34995290204643072" + }, + { + "amountOut":"4456.799978982369793812", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/UNI.png", + "dexName":"Uniswap V1", + "tradeFee":"4.64638467513839940864" + }, + { + "amountOut":"2771.072269036022134969", + "dexLogo":"https://static.okx.com/cdn/wallet/logo/SUSHI.png", + "dexName":"SushiSwap", + "tradeFee":"3.34995290204643072" + } + ], + "toToken":{ + "decimal":"18", + "isHoneyPot":false, + "taxRate":"0", + "tokenContractAddress":"0xe41d2489571d322189246dafa5ebde1f4699f498", + "tokenSymbol":"ZRX", + "tokenUnitPrice":"0.504455838152300152" + }, + "toTokenAmount":"6556259156432631386442", + "tradeFee":"2.3554356342513966" + }, + "tx":{ + "data":"0x0d5f0e3b00000000000000000001a0cf2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000015fdc8278903f7f31c10000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000014424eeecbff345b38187d0b8b749e56faa68539", + "from":"0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + "gas":"202500", + "gasPrice":"6756286873", + "maxPriorityFeePerGas":"1000000000", + "minReceiveAmount":"6490696564868305072578", + "signatureData":[ + "" + ], + "slippage":"0.01", + "to":"0x7D0CcAa3Fac1e5A943c5168b6CEd828691b46B36", + "value":"0" + } + } + ], + "msg":"" + }), + } + ]) + .await; + + let engine = tests::SolverEngine::new("okx", super::config(&api.address)).await; + + let solution = engine + .solve(json!({ + "id": "1", + "tokens": { + "0xe41d2489571d322189246dafa5ebde1f4699f498": { + "decimals": 18, + "symbol": "ZRX", + "referencePrice": "4327903683155778", + "availableBalance": "1583034704488033979459", + "trusted": true, + }, + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": { + "decimals": 18, + "symbol": "WETH", + "referencePrice": "1000000000000000000", + "availableBalance": "482725140468789680", + "trusted": true, + }, + }, + "orders": [ + { + "uid": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ + 2a2a2a2a", + "sellToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", + "sellAmount": "1000000000000000000", + // Way too much... + "buyAmount": "1000000000000000000000000000000000000", + "fullSellAmount": "1000000000000000000", + "fullBuyAmount": "1000000000000000000000000000000000000", + "kind": "sell", + "partiallyFillable": false, + "class": "market", + "sellTokenSource": "erc20", + "buyTokenDestination": "erc20", + "preInteractions": [], + "postInteractions": [], + "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", + "validTo": 0, + "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "signingScheme": "presign", + "signature": "0x", + } + ], + "liquidity": [], + "effectiveGasPrice": "15000000000", + "deadline": "2106-01-01T00:00:00.000Z", + "surplusCapturingJitOrderOwners": [] + })) + .await + .unwrap(); + + assert_eq!(solution, json!({ "solutions": [] }),); +} diff --git a/src/util/http.rs b/src/util/http.rs index 946e56e..34d9855 100644 --- a/src/util/http.rs +++ b/src/util/http.rs @@ -38,6 +38,7 @@ macro_rules! roundtrip { $crate::util::http::roundtrip!(<_, _>; $request) }; } + pub(crate) use roundtrip; #[doc(hidden)]