From d7eeb89d2a2e9a9ca803c7b87764672d9cc23e4c Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 24 Jan 2025 03:45:08 +0100 Subject: [PATCH 01/11] Implementation --- src/infra/dex/okx/dto.rs | 87 +++++++++++++++++++++-------- src/infra/dex/okx/mod.rs | 116 +++++++++++++++++++++++---------------- 2 files changed, 133 insertions(+), 70 deletions(-) diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 16345e9..3ccedb8 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -64,31 +64,14 @@ impl SwapRequest { } } -/// A OKX API swap response - generic wrapper for success and failure cases. +/// A OKX API swap response. /// /// 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)] +#[derive(Deserialize, Clone)] #[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, @@ -101,7 +84,7 @@ pub struct SwapResponseInner { /// 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)] +#[derive(Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SwapResponseRouterResult { /// The information of a token to be sold. @@ -124,7 +107,7 @@ pub struct SwapResponseRouterResult { /// 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)] +#[derive(Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SwapResponseFromToToken { /// Address of the token smart contract. @@ -136,7 +119,7 @@ pub struct SwapResponseFromToToken { /// 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)] +#[derive(Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SwapResponseTx { /// Estimated amount of the gas limit. @@ -151,6 +134,66 @@ pub struct SwapResponseTx { pub data: Vec, } +/// A OKX API approve transaction request. +/// +/// See [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-approve-transaction) +/// documentation for more detailed information on each parameter. +#[serde_as] +#[derive(Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApproveTransactionRequest { + /// Chain ID + #[serde_as(as = "serde_with::DisplayFromStr")] + pub chain_id: u64, + + /// Contract address of a token to be permitted. + pub token_contract_address: H160, + + /// The amount of token that needs to be permitted (in minimal divisible + /// units). + #[serde_as(as = "serialize::U256")] + pub approve_amount: U256, +} + +impl ApproveTransactionRequest { + pub fn with_domain(chain_id: u64, order: &dex::Order) -> Self { + Self { + chain_id, + token_contract_address: order.sell.0, + approve_amount: order.amount.get(), + } + } +} + +/// A OKX API approve transaction response. +/// Deserializing fields which are only used by the implementation. +/// See [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-approve-transaction) +/// documentation for more detailed information on each parameter. +#[serde_as] +#[derive(Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApproveTransactionResponse { + /// The contract address of OKX DEX approve. + pub dex_contract_address: H160, +} + +/// A OKX API response - generic wrapper for success and failure cases. +#[serde_as] +#[derive(Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Response { + /// 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, +} + #[derive(Deserialize)] pub struct Error { pub code: i64, diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 60a4cd3..2ddb25d 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -8,6 +8,7 @@ use { ethrpc::block_stream::CurrentBlockWatcher, hmac::{Hmac, Mac}, hyper::{header::HeaderValue, StatusCode}, + serde::{de::DeserializeOwned, Serialize}, sha2::Sha256, std::sync::atomic::{self, AtomicU64}, tracing::Instrument, @@ -87,43 +88,6 @@ impl Okx { }) } - /// 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, @@ -134,19 +98,27 @@ impl Okx { .clone() .with_domain(order, slippage) .ok_or(Error::OrderNotSupported)?; - let quote = { + let query_approve_transaction = + dto::ApproveTransactionRequest::with_domain(self.defaults.chain_id, order); + let (quote_result, approve_result) = { // 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) + + let quote: dto::SwapResponse = self + .send("swap", &query) .instrument(tracing::trace_span!("quote", id = %id)) - .await? - }; + .await?; - Self::handle_api_error(quote.code, "e.msg)?; - let quote_result = quote.data.first().ok_or(Error::NotFound)?; + let approve_transaction: dto::ApproveTransactionResponse = self + .send("approve-transaction", &query_approve_transaction) + .instrument(tracing::trace_span!("approve_transaction", id = %id)) + .await?; + + (quote, approve_transaction) + }; // 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) @@ -178,17 +150,63 @@ impl Okx { amount: quote_result.router_result.to_token_amount, }, allowance: dex::Allowance { - spender: eth::ContractAddress(quote_result.tx.to), + spender: eth::ContractAddress(approve_result.dex_contract_address), amount: dex::Amount::new(quote_result.router_result.from_token_amount), }, gas: eth::Gas(gas), }) } - async fn quote(&self, query: &dto::SwapRequest) -> Result { + /// 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(), + }, + }) + } + + async fn send(&self, endpoint: &str, query: &T) -> Result + where + T: Serialize, + U: DeserializeOwned + Clone, + { let mut request_builder = self .client - .request(reqwest::Method::GET, self.endpoint.clone()) + .request( + reqwest::Method::GET, + self.endpoint + .join(endpoint) + .map_err(|_| Error::RequestBuildFailed)?, + ) .query(query); let request = request_builder @@ -213,11 +231,13 @@ impl Okx { ); let quote = util::http::roundtrip!( - ; + , dto::Error>; request_builder ) .await?; - Ok(quote) + + Self::handle_api_error(quote.code, "e.msg)?; + quote.data.first().cloned().ok_or(Error::NotFound) } } From 1d5aeb87606200e204305d43a8829c0e9fa9022c Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 24 Jan 2025 03:46:42 +0100 Subject: [PATCH 02/11] Config update --- config/example.okx.toml | 2 +- src/infra/config/dex/okx/file.rs | 2 +- src/tests/okx/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/example.okx.toml b/config/example.okx.toml index b8bf297..e662441 100644 --- a/config/example.okx.toml +++ b/config/example.okx.toml @@ -6,7 +6,7 @@ node-url = "http://localhost:8545" chain-id = "1" # Optionally specify a custom OKX Swap API endpoint -# endpoint = "https://www.okx.com/api/v5/dex/aggregator/swap" +# endpoint = "https://www.okx.com/api/v5/dex/aggregator/" # 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 diff --git a/src/infra/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index c1c41bf..bd3905d 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -60,7 +60,7 @@ impl Into for OkxCredentialsConfig { } fn default_endpoint() -> reqwest::Url { - "https://www.okx.com/api/v5/dex/aggregator/swap" + "https://www.okx.com/api/v5/dex/aggregator/" .parse() .unwrap() } diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index eba6274..3eded0d 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -12,7 +12,7 @@ pub fn config(solver_addr: &SocketAddr) -> tests::Config { node-url = 'http://localhost:8545' [dex] chain-id = '1' -endpoint = 'http://{solver_addr}' +endpoint = 'http://{solver_addr}/' api-project-id = '1' api-key = '1234' api-secret-key = '1234567890123456' From 5d0ad4b91e1ecf0d61857ee83810d794b01d000a Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 24 Jan 2025 03:48:07 +0100 Subject: [PATCH 03/11] Updated tests --- src/tests/okx/api_calls.rs | 62 ++++++----- src/tests/okx/market_order.rs | 20 +++- src/tests/okx/not_found.rs | 194 +++++++++++++++++++++++++++++++++- src/tests/okx/out_of_price.rs | 18 +++- 4 files changed, 259 insertions(+), 35 deletions(-) diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs index 9c0648d..2f5ed5a 100644 --- a/src/tests/okx/api_calls.rs +++ b/src/tests/okx/api_calls.rs @@ -13,7 +13,7 @@ use { // 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(), + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/").unwrap(), chain_id: crate::domain::eth::ChainId::Mainnet, okx_credentials: okx_dex::OkxCredentialsConfig { project_id: env::var("OKX_PROJECT_ID").unwrap(), @@ -25,15 +25,15 @@ async fn swap_sell() { }; let order = Order { - sell: TokenAddress::from(H160::from_slice( - &hex::decode("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), - )), - buy: TokenAddress::from(H160::from_slice( - &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), + sell: TokenAddress::from( + H160::from_str("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), + ), + buy: TokenAddress::from( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + ), side: crate::domain::order::Side::Sell, amount: Amount::new(U256::from_dec_str("10000000000000").unwrap()), - owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + owner: H160::from_str("0x6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), }; let slippage = Slippage::one_percent(); @@ -45,6 +45,10 @@ async fn swap_sell() { assert_eq!(swap.input.token, order.amount().token); assert_eq!(swap.input.amount, order.amount().amount); assert_eq!(swap.output.token, order.buy); + assert_eq!( + swap.allowance.spender, + ContractAddress(H160::from_str("0x40aA958dd87FC8305b97f2BA922CDdCa374bcD7f").unwrap()) + ); } #[tokio::test] @@ -62,15 +66,15 @@ async fn swap_buy() { }; let order = Order { - buy: TokenAddress::from(H160::from_slice( - &hex::decode("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), - )), - sell: TokenAddress::from(H160::from_slice( - &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), + buy: TokenAddress::from( + H160::from_str("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), + ), + sell: TokenAddress::from( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + ), side: crate::domain::order::Side::Buy, amount: Amount::new(U256::from_dec_str("10000000000000").unwrap()), - owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + owner: H160::from_str("0x6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), }; let slippage = Slippage::one_percent(); @@ -101,15 +105,15 @@ async fn swap_api_error() { }; let order = Order { - sell: TokenAddress::from(H160::from_slice( - &hex::decode("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), - )), - buy: TokenAddress::from(H160::from_slice( - &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), + sell: TokenAddress::from( + H160::from_str("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(), + ), + buy: TokenAddress::from( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + ), side: crate::domain::order::Side::Sell, amount: Amount::new(U256::from_str("0").unwrap()), - owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + owner: H160::from_str("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), }; let slippage = Slippage::one_percent(); @@ -141,15 +145,15 @@ async fn swap_sell_insufficient_liquidity() { }; let order = Order { - sell: TokenAddress::from(H160::from_slice( - &hex::decode("C8CD2BE653759aed7B0996315821AAe71e1FEAdF").unwrap(), - )), - buy: TokenAddress::from(H160::from_slice( - &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), - )), + sell: TokenAddress::from( + H160::from_str("0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF").unwrap(), + ), + buy: TokenAddress::from( + H160::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + ), side: crate::domain::order::Side::Sell, amount: Amount::new(U256::from_dec_str("10000000000000").unwrap()), - owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), + owner: H160::from_str("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), }; let slippage = Slippage::one_percent(); diff --git a/src/tests/okx/market_order.rs b/src/tests/okx/market_order.rs index 7e068dd..3626750 100644 --- a/src/tests/okx/market_order.rs +++ b/src/tests/okx/market_order.rs @@ -11,7 +11,7 @@ async fn sell() { let api = mock::http::setup(vec![ mock::http::Expectation::Get { path: mock::http::Path::exact( - "?chainId=1\ + "swap?chainId=1\ &amount=1000000000000000000\ &fromTokenAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\ &toTokenAddress=0xe41d2489571d322189246dafa5ebde1f4699f498\ @@ -123,7 +123,21 @@ async fn sell() { ], "msg":"" }), - } + }, + mock::http::Expectation::Get { + path: mock::http::Path::exact( + "approve-transaction?chainId=1\ + &tokenContractAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\ + &approveAmount=1000000000000000000" + ), + res: json!( + { + "code":"0", + "data":[{"data":"0x095ea7b300000000000000000000000040aa958dd87fc8305b97f2ba922cddca374bcd7f000000000000000000000000000000000000000000000000000009184e72a000","dexContractAddress":"0x40aA958dd87FC8305b97f2BA922CDdCa374bcD7f","gasLimit":"70000","gasPrice":"7424402761"}], + "msg":"" + } + ) + }, ]) .await; @@ -193,7 +207,7 @@ async fn sell() { "allowances":[ { "amount":"1000000000000000000", - "spender":"0x7d0ccaa3fac1e5a943c5168b6ced828691b46b36", + "spender":"0x40aa958dd87fc8305b97f2ba922cddca374bcd7f", "token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" } ], diff --git a/src/tests/okx/not_found.rs b/src/tests/okx/not_found.rs index 17f129a..66e4cc6 100644 --- a/src/tests/okx/not_found.rs +++ b/src/tests/okx/not_found.rs @@ -10,7 +10,7 @@ use { async fn sell() { let api = mock::http::setup(vec![mock::http::Expectation::Get { path: mock::http::Path::exact( - "?chainId=1&amount=1000000000000000000&\ + "swap?chainId=1&amount=1000000000000000000&\ fromTokenAddress=0xc8cd2be653759aed7b0996315821aae71e1feadf&\ toTokenAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&slippage=0.01&\ userWalletAddress=0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", @@ -75,3 +75,195 @@ async fn sell() { assert_eq!(solution, json!({ "solutions": [] }),); } + +#[tokio::test] +async fn sell_no_approve_transaction() { + let api = mock::http::setup(vec![ + mock::http::Expectation::Get { + path: mock::http::Path::exact( + "swap?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":"" + }), + }, + mock::http::Expectation::Get { + path: mock::http::Path::exact( + "approve-transaction?chainId=1\ + &tokenContractAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\ + &approveAmount=1000000000000000000" + ), + res: json!( + { + "code":"50011", + "data":[], + "msg":"Rate limit reached" + } + ) + }, + ]) + .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": [] }),); +} diff --git a/src/tests/okx/out_of_price.rs b/src/tests/okx/out_of_price.rs index 2a3f91b..df89dc2 100644 --- a/src/tests/okx/out_of_price.rs +++ b/src/tests/okx/out_of_price.rs @@ -14,7 +14,7 @@ async fn sell() { let api = mock::http::setup(vec![ mock::http::Expectation::Get { path: mock::http::Path::exact( - "?chainId=1\ + "swap?chainId=1\ &amount=1000000000000000000\ &fromTokenAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\ &toTokenAddress=0xe41d2489571d322189246dafa5ebde1f4699f498\ @@ -126,7 +126,21 @@ async fn sell() { ], "msg":"" }), - } + }, + mock::http::Expectation::Get { + path: mock::http::Path::exact( + "approve-transaction?chainId=1\ + &tokenContractAddress=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\ + &approveAmount=1000000000000000000" + ), + res: json!( + { + "code":"0", + "data":[{"data":"0x095ea7b300000000000000000000000040aa958dd87fc8305b97f2ba922cddca374bcd7f000000000000000000000000000000000000000000000000000009184e72a000","dexContractAddress":"0x40aA958dd87FC8305b97f2BA922CDdCa374bcD7f","gasLimit":"70000","gasPrice":"7424402761"}], + "msg":"" + } + ) + }, ]) .await; From 59ec3346c51b7a4a1e75c66bf74519d5ca2bcb84 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 24 Jan 2025 14:45:14 +0100 Subject: [PATCH 04/11] Added LRU chache for OKX dex approve addresses --- Cargo.toml | 1 + src/infra/dex/okx/mod.rs | 57 +++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c634e2..8c21981 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ tower = "0.4" tower-http = { version = "0.4", features = ["trace"] } tracing = "0.1" web3 = "0.19" +lru = "0.12.5" contracts = { git = "https://github.com/cowprotocol/services.git", tag = "v2.292.1", package = "contracts" } ethrpc = { git = "https://github.com/cowprotocol/services.git", tag = "v2.292.1", package = "ethrpc" } diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 2ddb25d..992a9f8 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -8,20 +8,32 @@ use { ethrpc::block_stream::CurrentBlockWatcher, hmac::{Hmac, Mac}, hyper::{header::HeaderValue, StatusCode}, + lru::LruCache, serde::{de::DeserializeOwned, Serialize}, sha2::Sha256, - std::sync::atomic::{self, AtomicU64}, + std::{ + num::NonZeroUsize, + sync::{ + atomic::{self, AtomicU64}, + Arc, + }, + }, + tokio::sync::RwLock, tracing::Instrument, }; mod dto; +const DEFAULT_DEX_APPROVED_ADDRESSES_CACHE_SIZE: usize = 1000; + /// Bindings to the OKX swap API. pub struct Okx { client: super::Client, endpoint: reqwest::Url, api_secret_key: String, defaults: dto::SwapRequest, + /// Cache to store map of Token Address to contract address of OKX DEX approve. + dex_approved_addresses: Arc>>, } pub struct Config { @@ -85,6 +97,9 @@ impl Okx { endpoint: config.endpoint, api_secret_key: config.okx_credentials.api_secret_key, defaults, + dex_approved_addresses: Arc::new(RwLock::new(LruCache::new( + NonZeroUsize::new(DEFAULT_DEX_APPROVED_ADDRESSES_CACHE_SIZE).unwrap(), + ))), }) } @@ -98,9 +113,7 @@ impl Okx { .clone() .with_domain(order, slippage) .ok_or(Error::OrderNotSupported)?; - let query_approve_transaction = - dto::ApproveTransactionRequest::with_domain(self.defaults.chain_id, order); - let (quote_result, approve_result) = { + let (quote_result, dex_contract_address) = { // 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. @@ -112,12 +125,36 @@ impl Okx { .instrument(tracing::trace_span!("quote", id = %id)) .await?; - let approve_transaction: dto::ApproveTransactionResponse = self - .send("approve-transaction", &query_approve_transaction) - .instrument(tracing::trace_span!("approve_transaction", id = %id)) - .await?; + let existing_dex_contract_address = self + .dex_approved_addresses + .write() + .await + .get(&order.sell) + .cloned(); + + let dex_contract_address = match existing_dex_contract_address { + Some(value) => value, + None => { + let query_approve_transaction = + dto::ApproveTransactionRequest::with_domain(self.defaults.chain_id, order); + + let approve_transaction: dto::ApproveTransactionResponse = self + .send("approve-transaction", &query_approve_transaction) + .instrument(tracing::trace_span!("approve_transaction", id = %id)) + .await?; + + let address = eth::ContractAddress(approve_transaction.dex_contract_address); + + self.dex_approved_addresses + .write() + .await + .put(order.sell, address); + + address + } + }; - (quote, approve_transaction) + (quote, dex_contract_address) }; // Increasing returned gas by 50% according to the documentation: @@ -150,7 +187,7 @@ impl Okx { amount: quote_result.router_result.to_token_amount, }, allowance: dex::Allowance { - spender: eth::ContractAddress(approve_result.dex_contract_address), + spender: dex_contract_address, amount: dex::Amount::new(quote_result.router_result.from_token_amount), }, gas: eth::Gas(gas), From fd54666cda37b2d531365b420127e3fd872cb2cc Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 24 Jan 2025 15:02:14 +0100 Subject: [PATCH 05/11] Fixed lint error --- src/infra/dex/okx/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 992a9f8..480239f 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -32,7 +32,8 @@ pub struct Okx { endpoint: reqwest::Url, api_secret_key: String, defaults: dto::SwapRequest, - /// Cache to store map of Token Address to contract address of OKX DEX approve. + /// Cache to store map of Token Address to contract address of OKX DEX + /// approve. dex_approved_addresses: Arc>>, } From 4fcdc21d603d952096267fdc104a44161135c685 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 24 Jan 2025 15:04:10 +0100 Subject: [PATCH 06/11] Fixed lint error --- src/infra/dex/okx/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 480239f..0c567e9 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -33,7 +33,7 @@ pub struct Okx { api_secret_key: String, defaults: dto::SwapRequest, /// Cache to store map of Token Address to contract address of OKX DEX - /// approve. + /// approve. dex_approved_addresses: Arc>>, } From 814df7b85a84eaeadff0a2701151e50fa12ae5fd Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 24 Jan 2025 15:18:22 +0100 Subject: [PATCH 07/11] Updated cargolock file --- Cargo.lock | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 28cc795..c20167d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1554,6 +1554,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1786,6 +1792,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashlink" version = "0.8.4" @@ -2413,6 +2430,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.2", +] + [[package]] name = "maplit" version = "1.0.2" @@ -3135,7 +3161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.66", @@ -3927,6 +3953,7 @@ dependencies = [ "humantime-serde", "hyper", "itertools 0.11.0", + "lru", "maplit", "num", "number", From c8f925cb2003513328b9e6758fbe3a1b08838d49 Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Mon, 27 Jan 2025 12:13:40 +0000 Subject: [PATCH 08/11] Replace RwLock with Mutex because we always take write lock anyway --- src/infra/dex/okx/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 0c567e9..aa51fd6 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -16,9 +16,9 @@ use { sync::{ atomic::{self, AtomicU64}, Arc, + Mutex, }, }, - tokio::sync::RwLock, tracing::Instrument, }; @@ -34,7 +34,7 @@ pub struct Okx { defaults: dto::SwapRequest, /// Cache to store map of Token Address to contract address of OKX DEX /// approve. - dex_approved_addresses: Arc>>, + dex_approved_addresses: Arc>>, } pub struct Config { @@ -98,7 +98,7 @@ impl Okx { endpoint: config.endpoint, api_secret_key: config.okx_credentials.api_secret_key, defaults, - dex_approved_addresses: Arc::new(RwLock::new(LruCache::new( + dex_approved_addresses: Arc::new(Mutex::new(LruCache::new( NonZeroUsize::new(DEFAULT_DEX_APPROVED_ADDRESSES_CACHE_SIZE).unwrap(), ))), }) @@ -128,8 +128,8 @@ impl Okx { let existing_dex_contract_address = self .dex_approved_addresses - .write() - .await + .lock() + .unwrap() .get(&order.sell) .cloned(); @@ -147,8 +147,8 @@ impl Okx { let address = eth::ContractAddress(approve_transaction.dex_contract_address); self.dex_approved_addresses - .write() - .await + .lock() + .unwrap() .put(order.sell, address); address From bfc1784646bed9412836b3dff5aa57fcfa9af473 Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Mon, 27 Jan 2025 12:31:48 +0000 Subject: [PATCH 09/11] Rename `send` function --- src/infra/dex/okx/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index aa51fd6..54c16d5 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -122,7 +122,7 @@ impl Okx { let id = ID.fetch_add(1, atomic::Ordering::Relaxed); let quote: dto::SwapResponse = self - .send("swap", &query) + .send_get_request("swap", &query) .instrument(tracing::trace_span!("quote", id = %id)) .await?; @@ -140,7 +140,7 @@ impl Okx { dto::ApproveTransactionRequest::with_domain(self.defaults.chain_id, order); let approve_transaction: dto::ApproveTransactionResponse = self - .send("approve-transaction", &query_approve_transaction) + .send_get_request("approve-transaction", &query_approve_transaction) .instrument(tracing::trace_span!("approve_transaction", id = %id)) .await?; @@ -232,7 +232,7 @@ impl Okx { }) } - async fn send(&self, endpoint: &str, query: &T) -> Result + async fn send_get_request(&self, endpoint: &str, query: &T) -> Result where T: Serialize, U: DeserializeOwned + Clone, From a9f5d62ec10e4667a2b5558e9d8cf0c901475f8d Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Mon, 27 Jan 2025 12:32:26 +0000 Subject: [PATCH 10/11] Reduce default cache size since we won't handle so many tokens at once --- src/infra/dex/okx/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 54c16d5..36e84f9 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -24,7 +24,7 @@ use { mod dto; -const DEFAULT_DEX_APPROVED_ADDRESSES_CACHE_SIZE: usize = 1000; +const DEFAULT_DEX_APPROVED_ADDRESSES_CACHE_SIZE: usize = 100; /// Bindings to the OKX swap API. pub struct Okx { From 6326445ebaa2ee92ac5c41432c03c72842e0355b Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Mon, 27 Jan 2025 12:33:41 +0000 Subject: [PATCH 11/11] Rename quote to response --- src/infra/dex/okx/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 36e84f9..44202b1 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -268,14 +268,14 @@ impl Okx { HeaderValue::from_str(&signature).map_err(|_| Error::RequestBuildFailed)?, ); - let quote = util::http::roundtrip!( + let response = util::http::roundtrip!( , dto::Error>; request_builder ) .await?; - Self::handle_api_error(quote.code, "e.msg)?; - quote.data.first().cloned().ok_or(Error::NotFound) + Self::handle_api_error(response.code, &response.msg)?; + response.data.first().cloned().ok_or(Error::NotFound) } }