From 2a00fcd13273b07ef96735ee0eedcb2ea5668851 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 18 Dec 2024 09:49:55 +0100 Subject: [PATCH 01/34] Copied and renamed base files --- src/infra/cli.rs | 5 + src/infra/config/dex/mod.rs | 1 + src/infra/config/dex/okx/file.rs | 82 +++++++++++++ src/infra/config/dex/okx/mod.rs | 6 + src/infra/dex/mod.rs | 14 +++ src/infra/dex/okx/dto.rs | 167 ++++++++++++++++++++++++++ src/infra/dex/okx/mod.rs | 197 +++++++++++++++++++++++++++++++ src/run.rs | 9 ++ 8 files changed, 481 insertions(+) create mode 100644 src/infra/config/dex/okx/file.rs create mode 100644 src/infra/config/dex/okx/mod.rs create mode 100644 src/infra/dex/okx/dto.rs create mode 100644 src/infra/dex/okx/mod.rs 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..9ab804e 100644 --- a/src/infra/config/dex/mod.rs +++ b/src/infra/config/dex/mod.rs @@ -3,6 +3,7 @@ mod file; pub mod oneinch; pub mod paraswap; pub mod zeroex; +pub mod okx; use { crate::domain::{dex::slippage, eth}, diff --git a/src/infra/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs new file mode 100644 index 0000000..9c8bc9c --- /dev/null +++ b/src/infra/config/dex/okx/file.rs @@ -0,0 +1,82 @@ +use { + crate::{ + domain::eth, + infra::{config::dex::file, contracts, dex::okx}, + }, + ethereum_types::H160, + 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 0x swap API. + #[serde(default = "default_endpoint")] + #[serde_as(as = "serde_with::DisplayFromStr")] + endpoint: reqwest::Url, + + /// This is needed when configuring 0x to use + /// the gated API for partners. + api_key: String, + + /// The list of excluded liquidity sources. Liquidity from these sources + /// will not be considered when solving. + #[serde(default)] + excluded_sources: Vec, + + /// The affiliate address to use. Defaults to the mainnet CoW Protocol + /// settlement contract address. + #[serde(default = "default_affiliate")] + affiliate: H160, + + /// Whether or not to enable 0x RFQ-T liquidity. + #[serde(default)] + enable_rfqt: bool, + + /// Whether or not to enable slippage protection. The slippage protection + /// considers average negative slippage paid out in MEV when quoting, + /// preferring private market maker orders when they are close to what you + /// would get with on-chain liquidity pools. + #[serde(default)] + enable_slippage_protection: bool, +} + +fn default_endpoint() -> reqwest::Url { + "https://api.0x.org/swap/v1/".parse().unwrap() +} + +fn default_affiliate() -> H160 { + contracts::Contracts::for_chain(eth::ChainId::Mainnet) + .settlement + .0 +} + +/// Load the 0x 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; + + // Note that we just assume Mainnet here - this is because this is the + // only chain that the 0x solver supports anyway. + let settlement = contracts::Contracts::for_chain(eth::ChainId::Mainnet).settlement; + + super::Config { + okx: okx::Config { + endpoint: config.endpoint, + api_key: config.api_key, + excluded_sources: config.excluded_sources, + affiliate: config.affiliate, + settlement, + enable_rfqt: config.enable_rfqt, + enable_slippage_protection: config.enable_slippage_protection, + 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..2d2d336 100644 --- a/src/infra/dex/mod.rs +++ b/src/infra/dex/mod.rs @@ -9,6 +9,7 @@ pub mod oneinch; pub mod paraswap; pub mod simulator; pub mod zeroex; +pub mod okx; pub use self::simulator::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::NotFound => Self::NotFound, + okx::Error::RateLimited => Self::RateLimited, + okx::Error::UnavailableForLegalReasons => Self::UnavailableForLegalReasons, + _ => 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..21e8062 --- /dev/null +++ b/src/infra/dex/okx/dto.rs @@ -0,0 +1,167 @@ +//! DTOs for the 0x swap API. Full documentation for the API can be found +//! [here](https://docs.0x.org/0x-api-swap/api-references/get-swap-v1-quote). + +use { + crate::{ + domain::{dex, order}, + util::serialize, + }, + bigdecimal::BigDecimal, + ethereum_types::{H160, U256}, + serde::{Deserialize, Serialize}, + serde_with::serde_as, +}; + +/// A 0x API quote query parameters. +/// +/// See [API](https://docs.0x.org/0x-api-swap/api-references/get-swap-v1-quote) +/// documentation for more detailed information on each parameter. +#[serde_as] +#[derive(Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Query { + /// Contract address of a token to sell. + pub sell_token: H160, + + /// Contract address of a token to buy. + pub buy_token: H160, + + /// Amount of a token to sell, set in atoms. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option")] + pub sell_amount: Option, + + /// Amount of a token to sell, set in atoms. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option")] + pub buy_amount: Option, + + /// Limit of price slippage you are willing to accept. + #[serde(skip_serializing_if = "Option::is_none")] + pub slippage_percentage: Option, + + /// The target gas price for the swap transaction. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option")] + pub gas_price: Option, + + /// The address which will fill the quote. + #[serde(skip_serializing_if = "Option::is_none")] + pub taker_address: Option, + + /// List of sources to exclude. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde_as(as = "serialize::CommaSeparated")] + pub excluded_sources: Vec, + + /// Whether or not to skip quote validation. + pub skip_validation: bool, + + /// Wether or not you intend to actually fill the quote. Setting this flag + /// enables RFQ-T liquidity. + /// + /// + pub intent_on_filling: bool, + + /// The affiliate address to use for tracking and analytics purposes. + pub affiliate_address: H160, + + /// Requests trade routes which aim to protect against high slippage and MEV + /// attacks. + pub enable_slippage_protection: bool, +} + +/// A 0x slippage amount. +#[derive(Clone, Debug, Serialize)] +pub struct Slippage(BigDecimal); + +impl Query { + pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Self { + let (sell_amount, buy_amount) = match order.side { + order::Side::Buy => (None, Some(order.amount.get())), + order::Side::Sell => (Some(order.amount.get()), None), + }; + + Self { + sell_token: order.sell.0, + buy_token: order.buy.0, + sell_amount, + buy_amount, + // Note that the API calls this "slippagePercentage", but it is **not** a + // percentage but a factor. + slippage_percentage: Some(Slippage(slippage.as_factor().clone())), + ..self + } + } +} + +/// A Ox API quote response. +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Quote { + /// The address of the contract to call in order to execute the swap. + pub to: H160, + + /// The swap calldata. + #[serde_as(as = "serialize::Hex")] + pub data: Vec, + + /// The estimate for the amount of gas that will actually be used in the + /// transaction. + #[serde_as(as = "serialize::U256")] + pub estimated_gas: U256, + + /// The amount of sell token (in atoms) that would be sold in this swap. + #[serde_as(as = "serialize::U256")] + pub sell_amount: U256, + + /// The amount of buy token (in atoms) that would be bought in this swap. + #[serde_as(as = "serialize::U256")] + pub buy_amount: U256, + + /// The target contract address for which the user needs to have an + /// allowance in order to be able to complete the swap. + #[serde(with = "address_none_when_zero")] + pub allowance_target: Option, +} + +/// The 0x API uses the 0-address to indicate that no approvals are needed for a +/// swap. Use a custom deserializer to turn that into `None`. +mod address_none_when_zero { + use { + ethereum_types::H160, + serde::{Deserialize, Deserializer}, + }; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let value = H160::deserialize(deserializer)?; + Ok((!value.is_zero()).then_some(value)) + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +pub enum Response { + Ok(Quote), + Err(Error), +} + +impl Response { + /// Turns the API response into a [`std::result::Result`]. + pub fn into_result(self) -> Result { + match self { + Response::Ok(quote) => Ok(quote), + Response::Err(err) => Err(err), + } + } +} + +#[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..dfb9bea --- /dev/null +++ b/src/infra/dex/okx/mod.rs @@ -0,0 +1,197 @@ +use { + crate::{ + domain::{dex, eth, order}, + util, + }, + ethereum_types::H160, + ethrpc::block_stream::CurrentBlockWatcher, + hyper::StatusCode, + std::sync::atomic::{self, AtomicU64}, + tracing::Instrument, +}; + +mod dto; + +/// Bindings to the 0x swap API. +pub struct Okx { + client: super::Client, + endpoint: reqwest::Url, + defaults: dto::Query, +} + +pub struct Config { + /// The base URL for the 0x swap API. + pub endpoint: reqwest::Url, + + /// 0x provides a gated API for partners that requires authentication + /// by specifying this as header in the HTTP request. + pub api_key: String, + + /// The list of excluded liquidity sources. Liquidity from these sources + /// will not be considered when solving. + pub excluded_sources: Vec, + + /// The affiliate address to use. + /// + /// This is used by 0x for tracking and analytic purposes. + pub affiliate: H160, + + /// The address of the settlement contract. + pub settlement: eth::ContractAddress, + + /// Wether or not to enable RFQ-T liquidity. + pub enable_rfqt: bool, + + /// Whether or not to enable slippage protection. + pub enable_slippage_protection: bool, + + /// The stream that yields every new block. + pub block_stream: Option, +} + +impl Okx { + pub fn new(config: Config) -> Result { + let client = { + let mut key = reqwest::header::HeaderValue::from_str(&config.api_key)?; + key.set_sensitive(true); + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert("0x-api-key", key); + + let client = reqwest::Client::builder() + .default_headers(headers) + .build()?; + super::Client::new(client, config.block_stream) + }; + let defaults = dto::Query { + taker_address: Some(config.settlement.0), + excluded_sources: config.excluded_sources, + skip_validation: true, + intent_on_filling: config.enable_rfqt, + affiliate_address: config.affiliate, + enable_slippage_protection: config.enable_slippage_protection, + ..Default::default() + }; + + Ok(Self { + client, + endpoint: config.endpoint, + defaults, + }) + } + + pub async fn swap( + &self, + order: &dex::Order, + slippage: &dex::Slippage, + ) -> Result { + let query = self.defaults.clone().with_domain(order, slippage); + 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? + }; + + let max_sell_amount = match order.side { + order::Side::Buy => slippage.add(quote.sell_amount), + order::Side::Sell => quote.sell_amount, + }; + + Ok(dex::Swap { + call: dex::Call { + to: eth::ContractAddress(quote.to), + calldata: quote.data, + }, + input: eth::Asset { + token: order.sell, + amount: quote.sell_amount, + }, + output: eth::Asset { + token: order.buy, + amount: quote.buy_amount, + }, + allowance: dex::Allowance { + spender: quote + .allowance_target + .ok_or(Error::MissingSpender) + .map(eth::ContractAddress)?, + amount: dex::Amount::new(max_sell_amount), + }, + gas: eth::Gas(quote.estimated_gas), + }) + } + + async fn quote(&self, query: &dto::Query) -> Result { + let quote = util::http::roundtrip!( + ; + self.client + .request(reqwest::Method::GET, util::url::join(&self.endpoint, "quote")) + .query(query) + ) + .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("unable to find a quote")] + NotFound, + #[error("quote does not specify an approval spender")] + MissingSpender, + #[error("rate limited")] + RateLimited, + #[error("sell token or buy token are banned from trading")] + UnavailableForLegalReasons, + #[error("api error code {code}: {reason}")] + Api { code: i64, reason: String }, + #[error(transparent)] + Http(util::http::Error), +} + +impl From> for 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, + StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => { + Self::UnavailableForLegalReasons + } + _ => Self::Http(err), + } + } else { + Self::Http(err) + } + } + util::http::RoundtripError::Api(err) => { + // Unfortunately, AFAIK these codes aren't documented anywhere. These + // based on empirical observations of what the API has returned in the + // past. + match err.code { + 100 => Self::NotFound, + 429 => Self::RateLimited, + 451 => Self::UnavailableForLegalReasons, + _ => Self::Api { + code: err.code, + reason: err.reason, + }, + } + } + } + } +} diff --git a/src/run.rs b/src/run.rs index 66e7d83..f07c894 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::new(config.okx).expect("invalid OKX configuration"), + ), + config.base.clone(), + )) + } }; crate::api::Api { From 6b0e1c278e23a8337deb2fdecf770c6f7d24f81b Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 18 Dec 2024 19:21:22 +0100 Subject: [PATCH 02/34] Added query dto and config --- src/infra/config/dex/okx/file.rs | 40 +++----- src/infra/dex/okx/dto.rs | 161 +++++++++++++++++++------------ src/infra/dex/okx/mod.rs | 56 +++++------ 3 files changed, 139 insertions(+), 118 deletions(-) diff --git a/src/infra/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index 9c8bc9c..343e2ed 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -2,6 +2,7 @@ use { crate::{ domain::eth, infra::{config::dex::file, contracts, dex::okx}, + util::serialize, }, ethereum_types::H160, serde::Deserialize, @@ -18,30 +19,17 @@ struct Config { #[serde_as(as = "serde_with::DisplayFromStr")] endpoint: reqwest::Url, - /// This is needed when configuring 0x to use - /// the gated API for partners. - api_key: String, + /// Chain ID used to automatically determine contract addresses. + #[serde_as(as = "serialize::ChainId")] + chain_id: eth::ChainId, - /// The list of excluded liquidity sources. Liquidity from these sources - /// will not be considered when solving. - #[serde(default)] - excluded_sources: Vec, + pub api_project_id: String, - /// The affiliate address to use. Defaults to the mainnet CoW Protocol - /// settlement contract address. - #[serde(default = "default_affiliate")] - affiliate: H160, + pub api_key: String, - /// Whether or not to enable 0x RFQ-T liquidity. - #[serde(default)] - enable_rfqt: bool, + pub api_secret_key: String, - /// Whether or not to enable slippage protection. The slippage protection - /// considers average negative slippage paid out in MEV when quoting, - /// preferring private market maker orders when they are close to what you - /// would get with on-chain liquidity pools. - #[serde(default)] - enable_slippage_protection: bool, + pub api_passphrase: String, } fn default_endpoint() -> reqwest::Url { @@ -62,19 +50,17 @@ fn default_affiliate() -> H160 { pub async fn load(path: &Path) -> super::Config { let (base, config) = file::load::(path).await; - // Note that we just assume Mainnet here - this is because this is the - // only chain that the 0x solver supports anyway. let settlement = contracts::Contracts::for_chain(eth::ChainId::Mainnet).settlement; super::Config { okx: okx::Config { - endpoint: config.endpoint, + chain_id: config.chain_id, + project_id: config.api_project_id, api_key: config.api_key, - excluded_sources: config.excluded_sources, - affiliate: config.affiliate, + api_secret_key: config.api_secret_key, + api_passphrase: config.api_passphrase, + endpoint: config.endpoint, settlement, - enable_rfqt: config.enable_rfqt, - enable_slippage_protection: config.enable_slippage_protection, block_stream: base.block_stream.clone(), }, base, diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 21e8062..83d1d64 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -1,5 +1,5 @@ -//! DTOs for the 0x swap API. Full documentation for the API can be found -//! [here](https://docs.0x.org/0x-api-swap/api-references/get-swap-v1-quote). +//! 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::{ @@ -12,84 +12,135 @@ use { serde_with::serde_as, }; -/// A 0x API quote query parameters. +/// A OKX API swap query parameters. /// -/// See [API](https://docs.0x.org/0x-api-swap/api-references/get-swap-v1-quote) +/// 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 Query { - /// Contract address of a token to sell. - pub sell_token: H160, - /// Contract address of a token to buy. - pub buy_token: H160, + /// Chain ID + #[serde_as(as = "serde_with::DisplayFromStr")] + pub chain_id: u64, - /// Amount of a token to sell, set in atoms. + /// 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 send + 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 + pub user_wallet_address: H160, + + /// The fromToken address that receives the commission. + /// Only for SOL or SPL-Token commissions. #[serde(skip_serializing_if = "Option::is_none")] - #[serde_as(as = "Option")] - pub sell_amount: Option, + pub referrer_address: Option, - /// Amount of a token to sell, set in atoms. + /// Recipient address of a purchased token if not set, + /// user_wallet_address will receive a purchased token. #[serde(skip_serializing_if = "Option::is_none")] - #[serde_as(as = "Option")] - pub buy_amount: Option, + pub swap_receiver_address: Option, - /// Limit of price slippage you are willing to accept. + /// The percentage of from_token_address will be sent to the referrer's address, + /// the rest will be set as the input amount to be sold. + /// Min percentage:0 + /// Max percentage:3 #[serde(skip_serializing_if = "Option::is_none")] - pub slippage_percentage: Option, + #[serde_as(as = "Option")] + pub fee_percent: Option, - /// The target gas price for the swap transaction. + /// The gas limit (in wei) for the swap transaction. #[serde(skip_serializing_if = "Option::is_none")] #[serde_as(as = "Option")] - pub gas_price: Option, + pub gas_limit: Option, - /// The address which will fill the quote. + /// The target gas price level for the swap transaction. + /// Default value: average #[serde(skip_serializing_if = "Option::is_none")] - pub taker_address: Option, + pub gas_level: Option, - /// List of sources to exclude. + /// List of DexId of the liquidity pool for limited quotes. #[serde(skip_serializing_if = "Vec::is_empty")] #[serde_as(as = "serialize::CommaSeparated")] - pub excluded_sources: Vec, + pub dex_ids: Vec, + + /// The percentage of the price impact allowed. + /// Min value: 0 + /// Max value:1 (100%) + /// Default value: 0.9 (90%) + #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option")] + pub price_impact_protection_percentage: Option, + + /// Customized parameters sent on the blockchain in callData. + /// Hex encoded 128-characters string. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde_as(as = "serialize::Hex")] + pub call_data_memo: Vec, + + /// Address that receives the commission. + /// Only for SOL or SPL-Token commissions. + #[serde(skip_serializing_if = "Option::is_none")] + pub to_token_referrer_address: Option, - /// Whether or not to skip quote validation. - pub skip_validation: bool, + /// Used for transactions on the Solana network and similar to gas_price on Ethereum. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option")] + pub compute_unit_price: Option, - /// Wether or not you intend to actually fill the quote. Setting this flag - /// enables RFQ-T liquidity. - /// - /// - pub intent_on_filling: bool, + /// Used for transactions on the Solana network and analogous to gas_limit on Ethereum. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option")] + pub compute_unit_limit: Option, - /// The affiliate address to use for tracking and analytics purposes. - pub affiliate_address: H160, + /// The wallet address to receive the commission fee from the from_token. + #[serde(skip_serializing_if = "Option::is_none")] + pub from_token_referrer_wallet_address: Option, - /// Requests trade routes which aim to protect against high slippage and MEV - /// attacks. - pub enable_slippage_protection: bool, + /// The wallet address to receive the commission fee from the to_token + #[serde(skip_serializing_if = "Option::is_none")] + pub to_token_referrer_wallet_address: Option, } -/// A 0x slippage amount. -#[derive(Clone, Debug, Serialize)] +/// A OKX slippage amount. +#[derive(Clone, Debug, Default, Serialize)] pub struct Slippage(BigDecimal); +/// A OKX gas level. +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum GasLevel { + #[default] + Average, + Fast, + Slow +} + + + impl Query { pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Self { - let (sell_amount, buy_amount) = match order.side { - order::Side::Buy => (None, Some(order.amount.get())), - order::Side::Sell => (Some(order.amount.get()), None), + let (from_token_address, to_token_address, amount) = match order.side { + order::Side::Sell => (order.sell.0, order.buy.0, order.amount.get()), + order::Side::Buy => (order.buy.0, order.sell.0, order.amount.get()), }; Self { - sell_token: order.sell.0, - buy_token: order.buy.0, - sell_amount, - buy_amount, - // Note that the API calls this "slippagePercentage", but it is **not** a - // percentage but a factor. - slippage_percentage: Some(Slippage(slippage.as_factor().clone())), + chain_id: 1, // todo ms: from config + from_token_address, + to_token_address, + amount, + slippage: Slippage(slippage.as_factor().clone()), ..self } } @@ -122,27 +173,9 @@ pub struct Quote { /// The target contract address for which the user needs to have an /// allowance in order to be able to complete the swap. - #[serde(with = "address_none_when_zero")] pub allowance_target: Option, } -/// The 0x API uses the 0-address to indicate that no approvals are needed for a -/// swap. Use a custom deserializer to turn that into `None`. -mod address_none_when_zero { - use { - ethereum_types::H160, - serde::{Deserialize, Deserializer}, - }; - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let value = H160::deserialize(deserializer)?; - Ok((!value.is_zero()).then_some(value)) - } -} - #[derive(Deserialize)] #[serde(untagged)] pub enum Response { diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index dfb9bea..f077ff2 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -3,7 +3,6 @@ use { domain::{dex, eth, order}, util, }, - ethereum_types::H160, ethrpc::block_stream::CurrentBlockWatcher, hyper::StatusCode, std::sync::atomic::{self, AtomicU64}, @@ -12,7 +11,7 @@ use { mod dto; -/// Bindings to the 0x swap API. +/// Bindings to the OKX swap API. pub struct Okx { client: super::Client, endpoint: reqwest::Url, @@ -20,31 +19,30 @@ pub struct Okx { } pub struct Config { - /// The base URL for the 0x swap API. + /// The base URL for the 0KX swap API. pub endpoint: reqwest::Url, - /// 0x provides a gated API for partners that requires authentication - /// by specifying this as header in the HTTP request. + pub chain_id: eth::ChainId, + + /// OKX project ID to use. Instruction on how to create project: + /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#create-project + pub project_id: String, + + /// OKX API key. Instruction on how to generate API key: + /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys pub api_key: String, - /// The list of excluded liquidity sources. Liquidity from these sources - /// will not be considered when solving. - pub excluded_sources: Vec, + /// OKX API key additional security token. Instruction on how to get security token: + /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key + pub api_secret_key: String, - /// The affiliate address to use. - /// - /// This is used by 0x for tracking and analytic purposes. - pub affiliate: H160, + /// OKX API key passphrase used to encrypt secrety key. Instruction on how to get passhprase: + /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + pub api_passphrase: String, /// The address of the settlement contract. pub settlement: eth::ContractAddress, - /// Wether or not to enable RFQ-T liquidity. - pub enable_rfqt: bool, - - /// Whether or not to enable slippage protection. - pub enable_slippage_protection: bool, - /// The stream that yields every new block. pub block_stream: Option, } @@ -52,11 +50,19 @@ pub struct Config { impl Okx { pub fn new(config: Config) -> Result { let client = { - let mut key = reqwest::header::HeaderValue::from_str(&config.api_key)?; - key.set_sensitive(true); + let mut api_key = reqwest::header::HeaderValue::from_str(&config.api_key)?; + api_key.set_sensitive(true); + let mut api_secret_key = reqwest::header::HeaderValue::from_str(&config.api_secret_key)?; + api_secret_key.set_sensitive(true); + let mut api_passphrase = reqwest::header::HeaderValue::from_str(&config.api_passphrase)?; + api_passphrase.set_sensitive(true); let mut headers = reqwest::header::HeaderMap::new(); - headers.insert("0x-api-key", key); + headers.insert("OK-ACCESS-PROJECT", reqwest::header::HeaderValue::from_str(&config.project_id)?); + headers.insert("OK-ACCESS-KEY", api_key); + headers.insert("OK-ACCESS-SIGN", api_secret_key); + headers.insert("OK-ACCESS-PASSPHRASE", api_passphrase); + headers.insert("OK-ACCESS-TIMESTAMP", reqwest::header::HeaderValue::from_str(&chrono::Utc::now().to_string())?); let client = reqwest::Client::builder() .default_headers(headers) @@ -64,12 +70,8 @@ impl Okx { super::Client::new(client, config.block_stream) }; let defaults = dto::Query { - taker_address: Some(config.settlement.0), - excluded_sources: config.excluded_sources, - skip_validation: true, - intent_on_filling: config.enable_rfqt, - affiliate_address: config.affiliate, - enable_slippage_protection: config.enable_slippage_protection, + chain_id: config.chain_id as u64, + user_wallet_address: config.settlement.0, ..Default::default() }; From dbc9cc0c97deaba48adb40eb81d4bdbd45dcca1d Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 19 Dec 2024 00:33:04 +0100 Subject: [PATCH 03/34] Added response dto --- src/infra/config/dex/mod.rs | 2 +- src/infra/config/dex/okx/file.rs | 13 +-- src/infra/dex/mod.rs | 2 +- src/infra/dex/okx/dto.rs | 185 +++++++++++++++++++++++++------ src/infra/dex/okx/mod.rs | 55 +++++---- src/run.rs | 4 +- 6 files changed, 189 insertions(+), 72 deletions(-) diff --git a/src/infra/config/dex/mod.rs b/src/infra/config/dex/mod.rs index 9ab804e..4a054be 100644 --- a/src/infra/config/dex/mod.rs +++ b/src/infra/config/dex/mod.rs @@ -1,9 +1,9 @@ pub mod balancer; mod file; +pub mod okx; pub mod oneinch; pub mod paraswap; pub mod zeroex; -pub mod okx; use { crate::domain::{dex::slippage, eth}, diff --git a/src/infra/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index 343e2ed..89172b0 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -4,7 +4,6 @@ use { infra::{config::dex::file, contracts, dex::okx}, util::serialize, }, - ethereum_types::H160, serde::Deserialize, serde_with::serde_as, std::path::Path, @@ -33,13 +32,9 @@ struct Config { } fn default_endpoint() -> reqwest::Url { - "https://api.0x.org/swap/v1/".parse().unwrap() -} - -fn default_affiliate() -> H160 { - contracts::Contracts::for_chain(eth::ChainId::Mainnet) - .settlement - .0 + "https://www.okx.com/api/v5/dex/aggregator/" + .parse() + .unwrap() } /// Load the 0x solver configuration from a TOML file. @@ -50,7 +45,7 @@ fn default_affiliate() -> H160 { pub async fn load(path: &Path) -> super::Config { let (base, config) = file::load::(path).await; - let settlement = contracts::Contracts::for_chain(eth::ChainId::Mainnet).settlement; + let settlement = contracts::Contracts::for_chain(config.chain_id).settlement; super::Config { okx: okx::Config { diff --git a/src/infra/dex/mod.rs b/src/infra/dex/mod.rs index 2d2d336..508729f 100644 --- a/src/infra/dex/mod.rs +++ b/src/infra/dex/mod.rs @@ -5,11 +5,11 @@ use { }; pub mod balancer; +pub mod okx; pub mod oneinch; pub mod paraswap; pub mod simulator; pub mod zeroex; -pub mod okx; pub use self::simulator::Simulator; diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 83d1d64..5a5a361 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -12,15 +12,14 @@ use { serde_with::serde_as, }; -/// A OKX API swap query parameters. +/// A OKX API swap request parameters. /// /// 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 Query { - +pub struct SwapRequest { /// Chain ID #[serde_as(as = "serde_with::DisplayFromStr")] pub chain_id: u64, @@ -46,14 +45,14 @@ pub struct Query { #[serde(skip_serializing_if = "Option::is_none")] pub referrer_address: Option, - /// Recipient address of a purchased token if not set, + /// Recipient address of a purchased token if not set, /// user_wallet_address will receive a purchased token. #[serde(skip_serializing_if = "Option::is_none")] pub swap_receiver_address: Option, - /// The percentage of from_token_address will be sent to the referrer's address, - /// the rest will be set as the input amount to be sold. - /// Min percentage:0 + /// The percentage of from_token_address will be sent to the referrer's + /// address, the rest will be set as the input amount to be sold. + /// Min percentage:0 /// Max percentage:3 #[serde(skip_serializing_if = "Option::is_none")] #[serde_as(as = "Option")] @@ -75,7 +74,7 @@ pub struct Query { pub dex_ids: Vec, /// The percentage of the price impact allowed. - /// Min value: 0 + /// Min value: 0 /// Max value:1 (100%) /// Default value: 0.9 (90%) #[serde(skip_serializing_if = "Option::is_none")] @@ -93,12 +92,14 @@ pub struct Query { #[serde(skip_serializing_if = "Option::is_none")] pub to_token_referrer_address: Option, - /// Used for transactions on the Solana network and similar to gas_price on Ethereum. + /// Used for transactions on the Solana network and similar to gas_price on + /// Ethereum. #[serde(skip_serializing_if = "Option::is_none")] #[serde_as(as = "Option")] pub compute_unit_price: Option, - /// Used for transactions on the Solana network and analogous to gas_limit on Ethereum. + /// Used for transactions on the Solana network and analogous to gas_limit + /// on Ethereum. #[serde(skip_serializing_if = "Option::is_none")] #[serde_as(as = "Option")] pub compute_unit_limit: Option, @@ -123,12 +124,10 @@ pub enum GasLevel { #[default] Average, Fast, - Slow + Slow, } - - -impl Query { +impl SwapRequest { pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Self { let (from_token_address, to_token_address, amount) = match order.side { order::Side::Sell => (order.sell.0, order.buy.0, order.amount.get()), @@ -136,7 +135,6 @@ impl Query { }; Self { - chain_id: 1, // todo ms: from config from_token_address, to_token_address, amount, @@ -146,46 +144,165 @@ impl Query { } } -/// A Ox API quote response. +/// A OKX API quote response. #[serde_as] #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Quote { - /// The address of the contract to call in order to execute the swap. - pub to: H160, +pub struct SwapResponse { + pub code: String, - /// The swap calldata. - #[serde_as(as = "serialize::Hex")] - pub data: Vec, + pub data: Vec, + + pub msg: String, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseInner { + pub router_result: SwapResponseRouterResult, + + pub tx: SwapResponseTx, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseRouterResult { + #[serde_as(as = "serde_with::DisplayFromStr")] + pub chain_id: u64, + + #[serde_as(as = "serialize::U256")] + pub from_token_amount: U256, - /// The estimate for the amount of gas that will actually be used in the - /// transaction. #[serde_as(as = "serialize::U256")] - pub estimated_gas: U256, + pub to_token_amount: U256, - /// The amount of sell token (in atoms) that would be sold in this swap. #[serde_as(as = "serialize::U256")] - pub sell_amount: U256, + pub trade_fee: U256, - /// The amount of buy token (in atoms) that would be bought in this swap. #[serde_as(as = "serialize::U256")] - pub buy_amount: U256, + pub estimate_gas_fee: U256, + + pub dex_router_list: Vec, + + pub quote_compare_list: Vec, - /// The target contract address for which the user needs to have an - /// allowance in order to be able to complete the swap. - pub allowance_target: Option, + pub to_token: SwapResponseFromToToken, + + pub from_token: SwapResponseFromToToken, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseDexRouterList { + pub router: String, + + #[serde_as(as = "serde_with::DisplayFromStr")] + pub router_percent: f64, + + pub sub_router_list: Vec, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseDexSubRouterList { + pub dex_protocol: Vec, + + pub from_token: SwapResponseFromToToken, + + pub to_token: SwapResponseFromToToken, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseDexProtocol { + pub dex_name: String, + + #[serde_as(as = "serde_with::DisplayFromStr")] + pub percent: f64, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseFromToToken { + pub token_contract_address: H160, + + pub token_symbol: String, + + #[serde_as(as = "serde_with::DisplayFromStr")] + pub token_unit_price: f64, + + #[serde_as(as = "serde_with::DisplayFromStr")] + pub decimal: u8, + + pub is_honey_pot: bool, + + #[serde_as(as = "serde_with::DisplayFromStr")] + pub tax_rate: f64, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseQuoteCompareList { + pub dex_name: String, + + pub dex_logo: String, + + #[serde_as(as = "serde_with::DisplayFromStr")] + pub trade_fee: f64, + + #[serde_as(as = "serialize::U256")] + pub receive_amount: U256, + + #[serde_as(as = "serde_with::DisplayFromStr")] + pub price_impact_percentage: f64, +} + +#[serde_as] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapResponseTx { + pub signature_data: Vec, + + pub from: H160, + + #[serde_as(as = "serialize::U256")] + pub gas: U256, + + #[serde_as(as = "serialize::U256")] + pub gas_price: U256, + + #[serde_as(as = "serialize::U256")] + pub max_priority_fee_per_gas: U256, + + pub to: H160, + + #[serde_as(as = "serialize::U256")] + pub value: U256, + + #[serde_as(as = "serialize::U256")] + pub min_receive_amount: U256, + + #[serde_as(as = "serialize::Hex")] + pub data: Vec, } #[derive(Deserialize)] #[serde(untagged)] pub enum Response { - Ok(Quote), + Ok(SwapResponse), Err(Error), } impl Response { /// Turns the API response into a [`std::result::Result`]. - pub fn into_result(self) -> Result { + pub fn into_result(self) -> Result { match self { Response::Ok(quote) => Ok(quote), Response::Err(err) => Err(err), diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index f077ff2..4728b36 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -15,7 +15,7 @@ mod dto; pub struct Okx { client: super::Client, endpoint: reqwest::Url, - defaults: dto::Query, + defaults: dto::SwapRequest, } pub struct Config { @@ -32,12 +32,12 @@ pub struct Config { /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys pub api_key: String, - /// OKX API key additional security token. Instruction on how to get security token: - /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key + /// OKX API key additional security token. Instruction on how to get + /// security token: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key pub api_secret_key: String, - /// OKX API key passphrase used to encrypt secrety key. Instruction on how to get passhprase: - /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + /// OKX API key passphrase used to encrypt secrety key. Instruction on how + /// to get passhprase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys pub api_passphrase: String, /// The address of the settlement contract. @@ -52,24 +52,32 @@ impl Okx { let client = { let mut api_key = reqwest::header::HeaderValue::from_str(&config.api_key)?; api_key.set_sensitive(true); - let mut api_secret_key = reqwest::header::HeaderValue::from_str(&config.api_secret_key)?; + let mut api_secret_key = + reqwest::header::HeaderValue::from_str(&config.api_secret_key)?; api_secret_key.set_sensitive(true); - let mut api_passphrase = reqwest::header::HeaderValue::from_str(&config.api_passphrase)?; + let mut api_passphrase = + reqwest::header::HeaderValue::from_str(&config.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.project_id)?); + headers.insert( + "OK-ACCESS-PROJECT", + reqwest::header::HeaderValue::from_str(&config.project_id)?, + ); headers.insert("OK-ACCESS-KEY", api_key); headers.insert("OK-ACCESS-SIGN", api_secret_key); headers.insert("OK-ACCESS-PASSPHRASE", api_passphrase); - headers.insert("OK-ACCESS-TIMESTAMP", reqwest::header::HeaderValue::from_str(&chrono::Utc::now().to_string())?); + headers.insert( + "OK-ACCESS-TIMESTAMP", + reqwest::header::HeaderValue::from_str(&chrono::Utc::now().to_string())?, + ); let client = reqwest::Client::builder() .default_headers(headers) .build()?; super::Client::new(client, config.block_stream) }; - let defaults = dto::Query { + let defaults = dto::SwapRequest { chain_id: config.chain_id as u64, user_wallet_address: config.settlement.0, ..Default::default() @@ -99,40 +107,39 @@ impl Okx { .await? }; + let quote_result = quote.data.first().ok_or(Error::NotFound)?; + let max_sell_amount = match order.side { - order::Side::Buy => slippage.add(quote.sell_amount), - order::Side::Sell => quote.sell_amount, + order::Side::Buy => slippage.add(quote_result.router_result.from_token_amount), + order::Side::Sell => quote_result.router_result.from_token_amount, }; Ok(dex::Swap { call: dex::Call { - to: eth::ContractAddress(quote.to), - calldata: quote.data, + to: eth::ContractAddress(quote_result.tx.to), + calldata: quote_result.tx.data.clone(), }, input: eth::Asset { token: order.sell, - amount: quote.sell_amount, + amount: quote_result.router_result.from_token_amount, }, output: eth::Asset { token: order.buy, - amount: quote.buy_amount, + amount: quote_result.router_result.to_token_amount, }, allowance: dex::Allowance { - spender: quote - .allowance_target - .ok_or(Error::MissingSpender) - .map(eth::ContractAddress)?, + spender: eth::ContractAddress(quote_result.tx.to), amount: dex::Amount::new(max_sell_amount), }, - gas: eth::Gas(quote.estimated_gas), + gas: eth::Gas(quote_result.tx.gas), // todo ms: increase by 50% according to docs? }) } - async fn quote(&self, query: &dto::Query) -> Result { + async fn quote(&self, query: &dto::SwapRequest) -> Result { let quote = util::http::roundtrip!( - ; + ; self.client - .request(reqwest::Method::GET, util::url::join(&self.endpoint, "quote")) + .request(reqwest::Method::GET, util::url::join(&self.endpoint, "swap")) .query(query) ) .await?; diff --git a/src/run.rs b/src/run.rs index f07c894..1f84508 100644 --- a/src/run.rs +++ b/src/run.rs @@ -64,9 +64,7 @@ async fn run_with(args: cli::Args, bind: Option>) { cli::Command::Okx { config } => { let config = config::dex::okx::file::load(&config).await; Solver::Dex(solver::Dex::new( - dex::Dex::Okx( - dex::okx::Okx::new(config.okx).expect("invalid OKX configuration"), - ), + dex::Dex::Okx(dex::okx::Okx::new(config.okx).expect("invalid OKX configuration")), config.base.clone(), )) } From 6764d614beaad33b60287b9c4d45f63b6c9c89b4 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Sat, 4 Jan 2025 00:27:04 +0100 Subject: [PATCH 04/34] Working sign message function --- Cargo.lock | 3 +++ Cargo.toml | 3 +++ src/infra/dex/okx/mod.rs | 49 ++++++++++++++++++++++++++++++---------- src/tests/mod.rs | 1 + src/util/http.rs | 13 +++++++++-- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7519e97..f03489a 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 54ca53e..d7d028a 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/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 4728b36..6c60f42 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -4,9 +4,13 @@ use { util, }, ethrpc::block_stream::CurrentBlockWatcher, - hyper::StatusCode, + hyper::{StatusCode, header::HeaderValue}, std::sync::atomic::{self, AtomicU64}, + chrono::SecondsFormat, tracing::Instrument, + sha2::Sha256, + hmac::{Hmac, Mac}, + base64::prelude::*, }; mod dto; @@ -15,6 +19,7 @@ mod dto; pub struct Okx { client: super::Client, endpoint: reqwest::Url, + api_secret_key: String, defaults: dto::SwapRequest, } @@ -52,9 +57,6 @@ impl Okx { let client = { let mut api_key = reqwest::header::HeaderValue::from_str(&config.api_key)?; api_key.set_sensitive(true); - let mut api_secret_key = - reqwest::header::HeaderValue::from_str(&config.api_secret_key)?; - api_secret_key.set_sensitive(true); let mut api_passphrase = reqwest::header::HeaderValue::from_str(&config.api_passphrase)?; api_passphrase.set_sensitive(true); @@ -65,12 +67,7 @@ impl Okx { reqwest::header::HeaderValue::from_str(&config.project_id)?, ); headers.insert("OK-ACCESS-KEY", api_key); - headers.insert("OK-ACCESS-SIGN", api_secret_key); headers.insert("OK-ACCESS-PASSPHRASE", api_passphrase); - headers.insert( - "OK-ACCESS-TIMESTAMP", - reqwest::header::HeaderValue::from_str(&chrono::Utc::now().to_string())?, - ); let client = reqwest::Client::builder() .default_headers(headers) @@ -86,10 +83,28 @@ impl Okx { Ok(Self { client, endpoint: config.endpoint, + api_secret_key: config.api_secret_key, defaults, }) } + fn sign_request(&self, request: &reqwest::Request) -> String { + let mut data = String::new(); + data.push_str(request.headers().get("OK-ACCESS-TIMESTAMP").unwrap().to_str().unwrap()); + data.push_str(request.method().as_str()); + data.push_str(request.url().path()); + data.push('?'); + data.push_str(request.url().query().unwrap()); + println!("{:?}", data); + + type HmacSha256 = Hmac; + let mut mac = HmacSha256::new_from_slice(self.api_secret_key.as_bytes()).unwrap(); + mac.update(data.as_bytes()); + let signature = mac.finalize().into_bytes(); + + BASE64_STANDARD.encode(signature) + } + pub async fn swap( &self, order: &dex::Order, @@ -136,11 +151,21 @@ impl Okx { } async fn quote(&self, query: &dto::SwapRequest) -> Result { + let request_builder = self.client + .request(reqwest::Method::GET, util::url::join(&self.endpoint, "swap")) + .query(query); + let quote = util::http::roundtrip!( ; - self.client - .request(reqwest::Method::GET, util::url::join(&self.endpoint, "swap")) - .query(query) + request_builder, + |request| { + request.headers_mut().insert( + "OK-ACCESS-TIMESTAMP", + reqwest::header::HeaderValue::from_str(&chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true).to_string()).unwrap(), + ); + let signature = self.sign_request(request); + request.headers_mut().insert("OK-ACCESS-SIGN", HeaderValue::from_str(&signature).unwrap()); + } ) .await?; Ok(quote) diff --git a/src/tests/mod.rs b/src/tests/mod.rs index a32105c..7809e15 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -17,6 +17,7 @@ mod mock; mod oneinch; mod paraswap; mod zeroex; +mod okx; /// A solver engine handle for E2E testing. pub struct SolverEngine { diff --git a/src/util/http.rs b/src/util/http.rs index 946e56e..66cc2aa 100644 --- a/src/util/http.rs +++ b/src/util/http.rs @@ -7,7 +7,7 @@ use { crate::util, - reqwest::{Method, RequestBuilder, StatusCode, Url}, + reqwest::{Method, Request, RequestBuilder, StatusCode, Url}, serde::de::DeserializeOwned, std::str, }; @@ -20,6 +20,10 @@ use { /// the HTTP roundtripping is happening. macro_rules! roundtrip { (<$t:ty, $e:ty>; $request:expr) => { + $crate::util::http::roundtrip!(<$t, $e>; $request, |_|{}) + }; + // Pass additional opeartion which will be executed on built request. + (<$t:ty, $e:ty>; $request:expr, $operation:expr) => { $crate::util::http::roundtrip_internal::<$t, $e>( $request, |method, url, body, message| { @@ -32,12 +36,14 @@ macro_rules! roundtrip { |status, body, message| { tracing::trace!(%status, %body, "{message}"); }, + $operation, ) }; ($request:expr) => { $crate::util::http::roundtrip!(<_, _>; $request) }; } + pub(crate) use roundtrip; #[doc(hidden)] @@ -45,6 +51,7 @@ pub async fn roundtrip_internal( mut request: RequestBuilder, log_request: impl FnOnce(&Method, &Url, Option<&str>, &str), log_response: impl FnOnce(StatusCode, &str, &str), + mut operation: impl FnMut(&mut Request), ) -> Result> where T: DeserializeOwned, @@ -54,7 +61,9 @@ where request = request.header("X-REQUEST-ID", id); } let (client, request) = request.build_split(); - let request = request.map_err(Error::from)?; + let mut request = request.map_err(Error::from)?; + + operation(&mut request); let body = request .body() From 9a48e7a109770f8139a73d21345398aa30ce676f Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Sat, 4 Jan 2025 00:28:46 +0100 Subject: [PATCH 05/34] Added simple test --- src/tests/okx/mod.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/tests/okx/mod.rs diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs new file mode 100644 index 0000000..b9da90a --- /dev/null +++ b/src/tests/okx/mod.rs @@ -0,0 +1,58 @@ +#![allow(unreachable_code)] +#![allow(unused_imports)] +use { + crate::domain::dex::*, + crate::domain::eth, + crate::domain::eth::*, + crate::{ + infra::{config::dex::okx as okx_config, dex::okx as okx_dex}, + tests::{self, mock, okx}, + }, + bigdecimal::BigDecimal, + ethereum_types::H160, + serde_json::json, + std::default, + std::num::NonZeroUsize, + std::str::FromStr, + std::env, +}; + +#[ignore] +#[tokio::test] +// To run this test set following environment variables accordingly to your OKX setup: +// OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn simple() { + let okx_config = okx_dex::Config { + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/") + .unwrap(), + chain_id: crate::domain::eth::ChainId::Mainnet, + 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(), + settlement: eth::ContractAddress(H160::from_slice( + &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").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::Buy, + amount: Amount::new(U256::from_str("10000000000000").unwrap()), + owner: H160::from_slice( + &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), + ), + }; + + let slippage = Slippage::one_percent(); + + let okx = crate::infra::dex::okx::Okx::new(okx_config).unwrap(); + let swap_result = okx.swap(&order, &slippage).await; + swap_result.unwrap(); +} From 8a464e7c0c0fc36de3af7c630827b761e0feda99 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Sat, 4 Jan 2025 00:29:47 +0100 Subject: [PATCH 06/34] Updated formatting --- src/infra/dex/okx/mod.rs | 27 +++++++++++++++++++-------- src/tests/mod.rs | 2 +- src/tests/okx/mod.rs | 23 +++++++++-------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 6c60f42..c4010aa 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -3,14 +3,14 @@ use { domain::{dex, eth, order}, util, }, + base64::prelude::*, + chrono::SecondsFormat, ethrpc::block_stream::CurrentBlockWatcher, - hyper::{StatusCode, header::HeaderValue}, + hmac::{Hmac, Mac}, + hyper::{header::HeaderValue, StatusCode}, + sha2::Sha256, std::sync::atomic::{self, AtomicU64}, - chrono::SecondsFormat, tracing::Instrument, - sha2::Sha256, - hmac::{Hmac, Mac}, - base64::prelude::*, }; mod dto; @@ -90,7 +90,14 @@ impl Okx { fn sign_request(&self, request: &reqwest::Request) -> String { let mut data = String::new(); - data.push_str(request.headers().get("OK-ACCESS-TIMESTAMP").unwrap().to_str().unwrap()); + data.push_str( + request + .headers() + .get("OK-ACCESS-TIMESTAMP") + .unwrap() + .to_str() + .unwrap(), + ); data.push_str(request.method().as_str()); data.push_str(request.url().path()); data.push('?'); @@ -151,8 +158,12 @@ impl Okx { } async fn quote(&self, query: &dto::SwapRequest) -> Result { - let request_builder = self.client - .request(reqwest::Method::GET, util::url::join(&self.endpoint, "swap")) + let request_builder = self + .client + .request( + reqwest::Method::GET, + util::url::join(&self.endpoint, "swap"), + ) .query(query); let quote = util::http::roundtrip!( diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 7809e15..09d6fa3 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -14,10 +14,10 @@ use { mod balancer; mod dex; mod mock; +mod okx; mod oneinch; mod paraswap; mod zeroex; -mod okx; /// A solver engine handle for E2E testing. pub struct SolverEngine { diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index b9da90a..730e362 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -1,30 +1,27 @@ #![allow(unreachable_code)] #![allow(unused_imports)] use { - crate::domain::dex::*, - crate::domain::eth, - crate::domain::eth::*, crate::{ + domain::{ + dex::*, + eth::{self, *}, + }, infra::{config::dex::okx as okx_config, dex::okx as okx_dex}, tests::{self, mock, okx}, }, bigdecimal::BigDecimal, ethereum_types::H160, serde_json::json, - std::default, - std::num::NonZeroUsize, - std::str::FromStr, - std::env, + std::{default, env, num::NonZeroUsize, str::FromStr}, }; #[ignore] #[tokio::test] -// To run this test set following environment variables accordingly to your OKX setup: -// OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +// To run this test set following environment variables accordingly to your OKX +// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE async fn simple() { let okx_config = okx_dex::Config { - endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/") - .unwrap(), + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/").unwrap(), chain_id: crate::domain::eth::ChainId::Mainnet, project_id: env::var("OKX_PROJECT_ID").unwrap(), api_key: env::var("OKX_API_KEY").unwrap(), @@ -45,9 +42,7 @@ async fn simple() { )), side: crate::domain::order::Side::Buy, amount: Amount::new(U256::from_str("10000000000000").unwrap()), - owner: H160::from_slice( - &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), - ), + owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), }; let slippage = Slippage::one_percent(); From 82623bcd3b5d873abc586d5c8961bcbe50d275fc Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Tue, 7 Jan 2025 21:28:53 +0100 Subject: [PATCH 07/34] Updated handling requets signing --- src/infra/dex/okx/mod.rs | 43 ++++++++++++++++++++++++---------------- src/run.rs | 4 +++- src/tests/okx/mod.rs | 2 +- src/util/http.rs | 2 +- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index c4010aa..6febe47 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -6,7 +6,7 @@ use { base64::prelude::*, chrono::SecondsFormat, ethrpc::block_stream::CurrentBlockWatcher, - hmac::{Hmac, Mac}, + hmac::{digest::crypto_common::KeySizeUser, Hmac, Mac}, hyper::{header::HeaderValue, StatusCode}, sha2::Sha256, std::sync::atomic::{self, AtomicU64}, @@ -15,6 +15,8 @@ use { mod dto; +type HmacSha256 = Hmac; + /// Bindings to the OKX swap API. pub struct Okx { client: super::Client, @@ -53,7 +55,7 @@ pub struct Config { } impl Okx { - pub fn new(config: Config) -> Result { + pub fn try_new(config: Config) -> Result { let client = { let mut api_key = reqwest::header::HeaderValue::from_str(&config.api_key)?; api_key.set_sensitive(true); @@ -74,6 +76,11 @@ impl Okx { .build()?; super::Client::new(client, config.block_stream) }; + + if config.api_secret_key.as_bytes().len() != HmacSha256::key_size() { + return Err(CreationError::ConfigWrongSecretKeyLength); + } + let defaults = dto::SwapRequest { chain_id: config.chain_id as u64, user_wallet_address: config.settlement.0, @@ -88,23 +95,20 @@ impl Okx { }) } - fn sign_request(&self, request: &reqwest::Request) -> String { + fn sign_request(&self, request: &reqwest::Request, timestamp: &str) -> String { let mut data = String::new(); - data.push_str( - request - .headers() - .get("OK-ACCESS-TIMESTAMP") - .unwrap() - .to_str() - .unwrap(), - ); + 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().unwrap()); - println!("{:?}", data); + data.push_str( + request + .url() + .query() + .expect("Request query cannot be empty."), + ); - type HmacSha256 = Hmac; + // Safe to unwrap as we checked key size in the constructor let mut mac = HmacSha256::new_from_slice(self.api_secret_key.as_bytes()).unwrap(); mac.update(data.as_bytes()); let signature = mac.finalize().into_bytes(); @@ -170,12 +174,15 @@ impl Okx { ; request_builder, |request| { + let timestamp = &chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true).to_string(); + let signature = self.sign_request(request, timestamp); request.headers_mut().insert( "OK-ACCESS-TIMESTAMP", - reqwest::header::HeaderValue::from_str(&chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true).to_string()).unwrap(), + // Safe to unwrap as timestamp in RFC3339 format is a valid HTTP header value. + reqwest::header::HeaderValue::from_str(×tamp).unwrap(), ); - let signature = self.sign_request(request); - request.headers_mut().insert("OK-ACCESS-SIGN", HeaderValue::from_str(&signature).unwrap()); + request.headers_mut().insert("OK-ACCESS-SIGN", HeaderValue::from_str(&signature) + .expect("Request sign header value has invalid characters: {signature}")); } ) .await?; @@ -189,6 +196,8 @@ pub enum CreationError { Header(#[from] reqwest::header::InvalidHeaderValue), #[error(transparent)] Client(#[from] reqwest::Error), + #[error("Secret key length is wrong")] + ConfigWrongSecretKeyLength, } #[derive(Debug, thiserror::Error)] diff --git a/src/run.rs b/src/run.rs index 1f84508..3cca959 100644 --- a/src/run.rs +++ b/src/run.rs @@ -64,7 +64,9 @@ async fn run_with(args: cli::Args, bind: Option>) { cli::Command::Okx { config } => { let config = config::dex::okx::file::load(&config).await; Solver::Dex(solver::Dex::new( - dex::Dex::Okx(dex::okx::Okx::new(config.okx).expect("invalid OKX configuration")), + dex::Dex::Okx( + dex::okx::Okx::try_new(config.okx).expect("invalid OKX configuration"), + ), config.base.clone(), )) } diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index 730e362..ecc1bb8 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -47,7 +47,7 @@ async fn simple() { let slippage = Slippage::one_percent(); - let okx = crate::infra::dex::okx::Okx::new(okx_config).unwrap(); + let okx = crate::infra::dex::okx::Okx::try_new(okx_config).unwrap(); let swap_result = okx.swap(&order, &slippage).await; swap_result.unwrap(); } diff --git a/src/util/http.rs b/src/util/http.rs index 66cc2aa..64b8edd 100644 --- a/src/util/http.rs +++ b/src/util/http.rs @@ -20,7 +20,7 @@ use { /// the HTTP roundtripping is happening. macro_rules! roundtrip { (<$t:ty, $e:ty>; $request:expr) => { - $crate::util::http::roundtrip!(<$t, $e>; $request, |_|{}) + $crate::util::http::roundtrip!(<$t, $e>; $request, |_|()) }; // Pass additional opeartion which will be executed on built request. (<$t:ty, $e:ty>; $request:expr, $operation:expr) => { From a5a39d1a594bbeec9641b74f3578951a1b38b35c Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Tue, 7 Jan 2025 23:49:54 +0100 Subject: [PATCH 08/34] Fixed dto according to swap response --- src/infra/dex/okx/dto.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 5a5a361..7fe88de 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -178,8 +178,8 @@ pub struct SwapResponseRouterResult { #[serde_as(as = "serialize::U256")] pub to_token_amount: U256, - #[serde_as(as = "serialize::U256")] - pub trade_fee: U256, + #[serde_as(as = "serde_with::DisplayFromStr")] + pub trade_fee: f64, #[serde_as(as = "serialize::U256")] pub estimate_gas_fee: U256, @@ -257,11 +257,17 @@ pub struct SwapResponseQuoteCompareList { #[serde_as(as = "serde_with::DisplayFromStr")] pub trade_fee: f64, - #[serde_as(as = "serialize::U256")] - pub receive_amount: U256, - + // todo: missing in docs? #[serde_as(as = "serde_with::DisplayFromStr")] - pub price_impact_percentage: f64, + pub amount_out: f64, + + // todo: missing from response? + //#[serde_as(as = "serialize::U256")] + //pub receive_amount: U256, + + // todo: missing from response? + //#[serde_as(as = "serde_with::DisplayFromStr")] + //pub price_impact_percentage: f64, } #[serde_as] From f821ca64989d83d79461136d860224e56ec0158d Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Tue, 7 Jan 2025 23:50:58 +0100 Subject: [PATCH 09/34] Refactored errors handling --- src/infra/dex/okx/mod.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 6febe47..25e754e 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -6,7 +6,7 @@ use { base64::prelude::*, chrono::SecondsFormat, ethrpc::block_stream::CurrentBlockWatcher, - hmac::{digest::crypto_common::KeySizeUser, Hmac, Mac}, + hmac::{Hmac, Mac}, hyper::{header::HeaderValue, StatusCode}, sha2::Sha256, std::sync::atomic::{self, AtomicU64}, @@ -15,8 +15,6 @@ use { mod dto; -type HmacSha256 = Hmac; - /// Bindings to the OKX swap API. pub struct Okx { client: super::Client, @@ -77,10 +75,6 @@ impl Okx { super::Client::new(client, config.block_stream) }; - if config.api_secret_key.as_bytes().len() != HmacSha256::key_size() { - return Err(CreationError::ConfigWrongSecretKeyLength); - } - let defaults = dto::SwapRequest { chain_id: config.chain_id as u64, user_wallet_address: config.settlement.0, @@ -108,7 +102,8 @@ impl Okx { .expect("Request query cannot be empty."), ); - // Safe to unwrap as we checked key size in the constructor + type HmacSha256 = Hmac; + // Safe to unwrap as HMAC can take key of any size let mut mac = HmacSha256::new_from_slice(self.api_secret_key.as_bytes()).unwrap(); mac.update(data.as_bytes()); let signature = mac.finalize().into_bytes(); @@ -196,8 +191,6 @@ pub enum CreationError { Header(#[from] reqwest::header::InvalidHeaderValue), #[error(transparent)] Client(#[from] reqwest::Error), - #[error("Secret key length is wrong")] - ConfigWrongSecretKeyLength, } #[derive(Debug, thiserror::Error)] From 5c067c733b506f8c74c2bc05e1367d172a4799cc Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Tue, 7 Jan 2025 23:51:39 +0100 Subject: [PATCH 10/34] Fixed simple test --- src/tests/okx/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index ecc1bb8..b54160a 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -40,7 +40,7 @@ async fn simple() { buy: TokenAddress::from(H160::from_slice( &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), )), - side: crate::domain::order::Side::Buy, + side: crate::domain::order::Side::Sell, amount: Amount::new(U256::from_str("10000000000000").unwrap()), owner: H160::from_slice(&hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap()), }; From ba35f0bd5910e25c9c21691b109bf32d8a79c7e9 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Tue, 7 Jan 2025 23:52:56 +0100 Subject: [PATCH 11/34] Fixed formatting --- src/infra/dex/okx/dto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 7fe88de..4cea04a 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -260,7 +260,6 @@ pub struct SwapResponseQuoteCompareList { // todo: missing in docs? #[serde_as(as = "serde_with::DisplayFromStr")] pub amount_out: f64, - // todo: missing from response? //#[serde_as(as = "serialize::U256")] //pub receive_amount: U256, From 9a7f88b347a2e1e7eaca27b8d2ee65ca2b0c2efb Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 00:03:36 +0100 Subject: [PATCH 12/34] Updated test --- src/infra/dex/okx/mod.rs | 2 +- src/tests/okx/mod.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 25e754e..a287be1 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -149,7 +149,7 @@ impl Okx { amount: quote_result.router_result.to_token_amount, }, allowance: dex::Allowance { - spender: eth::ContractAddress(quote_result.tx.to), + spender: eth::ContractAddress(quote_result.tx.from), amount: dex::Amount::new(max_sell_amount), }, gas: eth::Gas(quote_result.tx.gas), // todo ms: increase by 50% according to docs? diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index b54160a..cf4be9e 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -48,6 +48,11 @@ async fn simple() { let slippage = Slippage::one_percent(); let okx = crate::infra::dex::okx::Okx::try_new(okx_config).unwrap(); - let swap_result = okx.swap(&order, &slippage).await; - swap_result.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); + assert_eq!(swap.allowance.spender.0, order.owner); } From a34372c199ece223a256612041b0516e33561293 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 00:06:45 +0100 Subject: [PATCH 13/34] Fixed clippy warning --- 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 a287be1..24248af 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -174,7 +174,7 @@ impl Okx { request.headers_mut().insert( "OK-ACCESS-TIMESTAMP", // Safe to unwrap as timestamp in RFC3339 format is a valid HTTP header value. - reqwest::header::HeaderValue::from_str(×tamp).unwrap(), + reqwest::header::HeaderValue::from_str(timestamp).unwrap(), ); request.headers_mut().insert("OK-ACCESS-SIGN", HeaderValue::from_str(&signature) .expect("Request sign header value has invalid characters: {signature}")); From 4d02c5f1f8d7e02acabb1bc8834d5d6beb4a3d03 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 10:12:56 +0100 Subject: [PATCH 14/34] Properly handling sell/buy orders --- src/infra/dex/okx/mod.rs | 36 ++++++++++++++++++++++---------- src/tests/okx/mod.rs | 44 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 24248af..3fa743c 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -130,9 +130,29 @@ impl Okx { let quote_result = quote.data.first().ok_or(Error::NotFound)?; - let max_sell_amount = match order.side { - order::Side::Buy => slippage.add(quote_result.router_result.from_token_amount), - order::Side::Sell => quote_result.router_result.from_token_amount, + let (input, output, max_sell_amount) = match order.side { + order::Side::Buy => ( + eth::Asset { + token: order.buy, + amount: quote_result.router_result.from_token_amount, + }, + eth::Asset { + token: order.sell, + amount: quote_result.router_result.to_token_amount, + }, + slippage.add(quote_result.router_result.from_token_amount), + ), + order::Side::Sell => ( + eth::Asset { + token: order.sell, + amount: quote_result.router_result.from_token_amount, + }, + eth::Asset { + token: order.buy, + amount: quote_result.router_result.to_token_amount, + }, + quote_result.router_result.from_token_amount, + ), }; Ok(dex::Swap { @@ -140,14 +160,8 @@ impl Okx { to: eth::ContractAddress(quote_result.tx.to), calldata: quote_result.tx.data.clone(), }, - input: eth::Asset { - token: order.sell, - amount: quote_result.router_result.from_token_amount, - }, - output: eth::Asset { - token: order.buy, - amount: quote_result.router_result.to_token_amount, - }, + input, + output, allowance: dex::Allowance { spender: eth::ContractAddress(quote_result.tx.from), amount: dex::Amount::new(max_sell_amount), diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index cf4be9e..9f8f99a 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -19,7 +19,7 @@ use { #[tokio::test] // To run this test set following environment variables accordingly to your OKX // setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE -async fn simple() { +async fn simple_sell() { let okx_config = okx_dex::Config { endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/").unwrap(), chain_id: crate::domain::eth::ChainId::Mainnet, @@ -56,3 +56,45 @@ async fn simple() { assert_eq!(swap.output.token, order.buy); assert_eq!(swap.allowance.spender.0, order.owner); } + +#[ignore] +#[tokio::test] +// To run this test set following environment variables accordingly to your OKX +// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn simple_buy() { + let okx_config = okx_dex::Config { + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/").unwrap(), + chain_id: crate::domain::eth::ChainId::Mainnet, + 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(), + settlement: eth::ContractAddress(H160::from_slice( + &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), + )), + 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_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.sell); + assert_eq!(swap.allowance.spender.0, order.owner); +} From baabdbead37646caeb75f2fbe5a97cf245b3c209 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 10:14:07 +0100 Subject: [PATCH 15/34] Updated endpoint --- src/infra/dex/okx/mod.rs | 5 +---- src/tests/okx/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 3fa743c..d1045a9 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -173,10 +173,7 @@ impl Okx { async fn quote(&self, query: &dto::SwapRequest) -> Result { let request_builder = self .client - .request( - reqwest::Method::GET, - util::url::join(&self.endpoint, "swap"), - ) + .request(reqwest::Method::GET, self.endpoint.clone()) .query(query); let quote = util::http::roundtrip!( diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index 9f8f99a..1928c28 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -21,7 +21,7 @@ use { // setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE async fn simple_sell() { let okx_config = okx_dex::Config { - endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/").unwrap(), + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/swap").unwrap(), chain_id: crate::domain::eth::ChainId::Mainnet, project_id: env::var("OKX_PROJECT_ID").unwrap(), api_key: env::var("OKX_API_KEY").unwrap(), @@ -63,7 +63,7 @@ async fn simple_sell() { // setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE async fn simple_buy() { let okx_config = okx_dex::Config { - endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/").unwrap(), + endpoint: reqwest::Url::parse("https://www.okx.com/api/v5/dex/aggregator/swap").unwrap(), chain_id: crate::domain::eth::ChainId::Mainnet, project_id: env::var("OKX_PROJECT_ID").unwrap(), api_key: env::var("OKX_API_KEY").unwrap(), From a2e5d00d7dd5c68f543a84b3a8fbb3e764fa7b1e Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 10:16:47 +0100 Subject: [PATCH 16/34] Tests warnings cleanup --- src/tests/okx/mod.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index 1928c28..4e4b1b7 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -1,18 +1,13 @@ -#![allow(unreachable_code)] -#![allow(unused_imports)] use { crate::{ domain::{ dex::*, eth::{self, *}, }, - infra::{config::dex::okx as okx_config, dex::okx as okx_dex}, - tests::{self, mock, okx}, + infra::dex::okx as okx_dex, }, - bigdecimal::BigDecimal, ethereum_types::H160, - serde_json::json, - std::{default, env, num::NonZeroUsize, str::FromStr}, + std::{env, str::FromStr}, }; #[ignore] From 9df0b37c60cf810b6f6bc4e09457025628dfc5a4 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 15:12:26 +0100 Subject: [PATCH 17/34] Reverted http roundtrip operation --- src/infra/dex/okx/mod.rs | 38 +++++++++++++++++++++++++------------- src/util/http.rs | 10 +--------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index d1045a9..c6c0e79 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -171,25 +171,35 @@ impl Okx { } async fn quote(&self, query: &dto::SwapRequest) -> Result { - let request_builder = self + let mut request_builder = self .client .request(reqwest::Method::GET, self.endpoint.clone()) .query(query); + let request = request_builder + .try_clone() + .ok_or(Error::SignRequestFailed)? + .build() + .map_err(|_| Error::SignRequestFailed)?; + + let timestamp = &chrono::Utc::now() + .to_rfc3339_opts(SecondsFormat::Millis, true) + .to_string(); + let signature = self.sign_request(&request, timestamp); + + request_builder = request_builder.header( + "OK-ACCESS-TIMESTAMP", + reqwest::header::HeaderValue::from_str(timestamp) + .map_err(|_| Error::SignRequestFailed)?, + ); + request_builder = request_builder.header( + "OK-ACCESS-SIGN", + HeaderValue::from_str(&signature).map_err(|_| Error::SignRequestFailed)?, + ); + let quote = util::http::roundtrip!( ; - request_builder, - |request| { - let timestamp = &chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true).to_string(); - let signature = self.sign_request(request, timestamp); - request.headers_mut().insert( - "OK-ACCESS-TIMESTAMP", - // Safe to unwrap as timestamp in RFC3339 format is a valid HTTP header value. - reqwest::header::HeaderValue::from_str(timestamp).unwrap(), - ); - request.headers_mut().insert("OK-ACCESS-SIGN", HeaderValue::from_str(&signature) - .expect("Request sign header value has invalid characters: {signature}")); - } + request_builder ) .await?; Ok(quote) @@ -206,6 +216,8 @@ pub enum CreationError { #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("failed to sign the request")] + SignRequestFailed, #[error("unable to find a quote")] NotFound, #[error("quote does not specify an approval spender")] diff --git a/src/util/http.rs b/src/util/http.rs index 64b8edd..2424a55 100644 --- a/src/util/http.rs +++ b/src/util/http.rs @@ -20,10 +20,6 @@ use { /// the HTTP roundtripping is happening. macro_rules! roundtrip { (<$t:ty, $e:ty>; $request:expr) => { - $crate::util::http::roundtrip!(<$t, $e>; $request, |_|()) - }; - // Pass additional opeartion which will be executed on built request. - (<$t:ty, $e:ty>; $request:expr, $operation:expr) => { $crate::util::http::roundtrip_internal::<$t, $e>( $request, |method, url, body, message| { @@ -36,7 +32,6 @@ macro_rules! roundtrip { |status, body, message| { tracing::trace!(%status, %body, "{message}"); }, - $operation, ) }; ($request:expr) => { @@ -51,7 +46,6 @@ pub async fn roundtrip_internal( mut request: RequestBuilder, log_request: impl FnOnce(&Method, &Url, Option<&str>, &str), log_response: impl FnOnce(StatusCode, &str, &str), - mut operation: impl FnMut(&mut Request), ) -> Result> where T: DeserializeOwned, @@ -61,9 +55,7 @@ where request = request.header("X-REQUEST-ID", id); } let (client, request) = request.build_split(); - let mut request = request.map_err(Error::from)?; - - operation(&mut request); + let request = request.map_err(Error::from)?; let body = request .body() From 10f2347be76f3867eaa4fd9e44926630a7fb8c80 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 15:16:39 +0100 Subject: [PATCH 18/34] Updated errors handling --- src/infra/dex/okx/mod.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index c6c0e79..a67a54e 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -89,26 +89,21 @@ impl Okx { }) } - fn sign_request(&self, request: &reqwest::Request, timestamp: &str) -> String { + fn sign_request(&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() - .expect("Request query cannot be empty."), - ); + data.push_str(request.url().query().ok_or(Error::SignRequestFailed)?); type HmacSha256 = Hmac; - // Safe to unwrap as HMAC can take key of any size - let mut mac = HmacSha256::new_from_slice(self.api_secret_key.as_bytes()).unwrap(); + let mut mac = HmacSha256::new_from_slice(self.api_secret_key.as_bytes()) + .map_err(|_| Error::SignRequestFailed)?; mac.update(data.as_bytes()); let signature = mac.finalize().into_bytes(); - BASE64_STANDARD.encode(signature) + Ok(BASE64_STANDARD.encode(signature)) } pub async fn swap( @@ -178,23 +173,23 @@ impl Okx { let request = request_builder .try_clone() - .ok_or(Error::SignRequestFailed)? + .ok_or(Error::RequestBuildFailed)? .build() - .map_err(|_| Error::SignRequestFailed)?; + .map_err(|_| Error::RequestBuildFailed)?; let timestamp = &chrono::Utc::now() .to_rfc3339_opts(SecondsFormat::Millis, true) .to_string(); - let signature = self.sign_request(&request, timestamp); + let signature = self.sign_request(&request, timestamp)?; request_builder = request_builder.header( "OK-ACCESS-TIMESTAMP", reqwest::header::HeaderValue::from_str(timestamp) - .map_err(|_| Error::SignRequestFailed)?, + .map_err(|_| Error::RequestBuildFailed)?, ); request_builder = request_builder.header( "OK-ACCESS-SIGN", - HeaderValue::from_str(&signature).map_err(|_| Error::SignRequestFailed)?, + HeaderValue::from_str(&signature).map_err(|_| Error::RequestBuildFailed)?, ); let quote = util::http::roundtrip!( @@ -216,6 +211,8 @@ pub enum CreationError { #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("failed to build the request")] + RequestBuildFailed, #[error("failed to sign the request")] SignRequestFailed, #[error("unable to find a quote")] From fdcf9512c10c4dc5842b5cfe749a61ad5b8b872f Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Wed, 8 Jan 2025 23:08:34 +0100 Subject: [PATCH 19/34] Updated errors --- src/infra/dex/okx/dto.rs | 3 ++- src/infra/dex/okx/mod.rs | 12 ++++++++++ src/tests/okx/mod.rs | 47 ++++++++++++++++++++++++++++++++++++++++ src/util/http.rs | 2 +- 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 4cea04a..36ffba9 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -149,7 +149,8 @@ impl SwapRequest { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SwapResponse { - pub code: String, + #[serde_as(as = "serde_with::DisplayFromStr")] + pub code: i64, pub data: Vec, diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index a67a54e..10e5b1d 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -106,6 +106,17 @@ impl Okx { Ok(BASE64_STANDARD.encode(signature)) } + /// Error codes: https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code + fn handle_api_error(code: i64, message: &str) -> Result<(), Error> { + match code { + 0 => Ok(()), + _ => Err(Error::Api { + code, + reason: message.to_string(), + }), + } + } + pub async fn swap( &self, order: &dex::Order, @@ -123,6 +134,7 @@ impl Okx { .await? }; + Self::handle_api_error(quote.code, "e.msg)?; let quote_result = quote.data.first().ok_or(Error::NotFound)?; let (input, output, max_sell_amount) = match order.side { diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index 4e4b1b7..8acb885 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -93,3 +93,50 @@ async fn simple_buy() { assert_eq!(swap.output.token, order.sell); assert_eq!(swap.allowance.spender.0, order.owner); } + +#[ignore] +#[tokio::test] +// To run this test set following environment variables accordingly to your OKX +// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn simple_api_error() { + init_logging(); + env::set_var("OKX_PROJECT_ID", "5d0b6cbaf8e9cedb7eb6836a0f35d961"); + env::set_var("OKX_API_KEY", "4b2ba8b8-4201-4f53-9587-74420a702be6"); + env::set_var("OKX_SECRET_KEY", "E5BFA3CC41B40A52BF78AA75D07DD148"); + env::set_var("OKX_PASSPHRASE", "xjmgdqY6ApuZdfQFCnvR$"); + + 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, + 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(), + settlement: eth::ContractAddress(H160::from_slice( + &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").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::tests::okx::okx_dex::Error::Api { .. } + )); +} diff --git a/src/util/http.rs b/src/util/http.rs index 2424a55..34d9855 100644 --- a/src/util/http.rs +++ b/src/util/http.rs @@ -7,7 +7,7 @@ use { crate::util, - reqwest::{Method, Request, RequestBuilder, StatusCode, Url}, + reqwest::{Method, RequestBuilder, StatusCode, Url}, serde::de::DeserializeOwned, std::str, }; From ab490bb2a3f4930b96e93ef1f08c003fa4d9eb8f Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 12:00:22 +0100 Subject: [PATCH 20/34] Config update --- src/infra/config/dex/okx/file.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/infra/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index 89172b0..cc23f9a 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -1,7 +1,7 @@ use { crate::{ domain::eth, - infra::{config::dex::file, contracts, dex::okx}, + infra::{config::dex::file, dex::okx}, util::serialize, }, serde::Deserialize, @@ -22,22 +22,26 @@ struct Config { #[serde_as(as = "serialize::ChainId")] chain_id: eth::ChainId, - pub api_project_id: String, + /// OKX Project ID. + api_project_id: String, - pub api_key: String, + /// OKX API Key. + api_key: String, - pub api_secret_key: String, + /// OKX Secret key used for signing request. + api_secret_key: String, - pub api_passphrase: String, + /// OKX Secret key passphrase. + api_passphrase: String, } fn default_endpoint() -> reqwest::Url { - "https://www.okx.com/api/v5/dex/aggregator/" + "https://www.okx.com/api/v5/dex/aggregator/swap" .parse() .unwrap() } -/// Load the 0x solver configuration from a TOML file. +/// Load the OKX solver configuration from a TOML file. /// /// # Panics /// @@ -45,8 +49,6 @@ fn default_endpoint() -> reqwest::Url { pub async fn load(path: &Path) -> super::Config { let (base, config) = file::load::(path).await; - let settlement = contracts::Contracts::for_chain(config.chain_id).settlement; - super::Config { okx: okx::Config { chain_id: config.chain_id, @@ -55,7 +57,6 @@ pub async fn load(path: &Path) -> super::Config { api_secret_key: config.api_secret_key, api_passphrase: config.api_passphrase, endpoint: config.endpoint, - settlement, block_stream: base.block_stream.clone(), }, base, From ce4b6ffb0501074babfcdbbd30b8470ef37d3b39 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 12:01:31 +0100 Subject: [PATCH 21/34] Added market order test --- src/tests/okx/api_calls.rs | 193 +++++++++++++++++++++++++++ src/tests/okx/market_order.rs | 237 ++++++++++++++++++++++++++++++++++ src/tests/okx/mod.rs | 158 +++-------------------- 3 files changed, 450 insertions(+), 138 deletions(-) create mode 100644 src/tests/okx/api_calls.rs create mode 100644 src/tests/okx/market_order.rs diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs new file mode 100644 index 0000000..f879e35 --- /dev/null +++ b/src/tests/okx/api_calls.rs @@ -0,0 +1,193 @@ +use { + crate::{ + domain::{ + dex::*, + eth::*, + }, + infra::dex::okx as okx_dex, + }, + ethereum_types::H160, + std::{env, str::FromStr}, +}; + + +#[cfg(test)] +fn init_logging() { + use tracing_subscriber::{layer::*, util::*}; + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "trace"); + } + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::Layer::default().compact()) + .init(); + env::set_var("OKX_PROJECT_ID", "5d0b6cbaf8e9cedb7eb6836a0f35d961"); + env::set_var("OKX_API_KEY", "4b2ba8b8-4201-4f53-9587-74420a702be6"); + env::set_var("OKX_SECRET_KEY", "E5BFA3CC41B40A52BF78AA75D07DD148"); + env::set_var("OKX_PASSPHRASE", "xjmgdqY6ApuZdfQFCnvR$"); +} + +#[ignore] +#[tokio::test] +// To run this test set following environment variables accordingly to your OKX +// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn swap_sell() { + init_logging(); + + 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, + 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); + assert_eq!(swap.allowance.spender.0, order.owner); +} + +#[ignore] +#[tokio::test] +// To run this test set following environment variables accordingly to your OKX +// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn swap_buy() { + init_logging(); + + 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, + 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 { + 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; + 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.sell); + assert_eq!(swap.allowance.spender.0, order.owner); +} + +#[ignore] +#[tokio::test] +// To run this test set following environment variables accordingly to your OKX +// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn swap_api_error() { + init_logging(); + + 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, + 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 following environment variables accordingly to your OKX +// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +async fn simple_sell2() { + init_logging(); + + 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, + 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("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), + )), + buy: TokenAddress::from(H160::from_slice( + &hex::decode("e41d2489571d322189246dafa5ebde1f4699f498").unwrap(), + )), + side: crate::domain::order::Side::Sell, + amount: Amount::new(U256::from_dec_str("1000000000000000000").unwrap()), + owner: H160::from_slice(&hex::decode("2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a").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); + assert_eq!(swap.allowance.spender.0, order.owner); +} diff --git a/src/tests/okx/market_order.rs b/src/tests/okx/market_order.rs new file mode 100644 index 0000000..1c49d22 --- /dev/null +++ b/src/tests/okx/market_order.rs @@ -0,0 +1,237 @@ +//! 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":308891, + "id":0, + "interactions":[ + { + "allowances":[ + { + "amount":"1000000000000000000", + "spender":"0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + "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" + } + ] + } + ] + }), + ); +} + diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index 8acb885..c0ec530 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -1,142 +1,24 @@ use { - crate::{ - domain::{ - dex::*, - eth::{self, *}, - }, - infra::dex::okx as okx_dex, - }, - ethereum_types::H160, - std::{env, str::FromStr}, + crate::tests, + std::net::SocketAddr, }; -#[ignore] -#[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE -async fn simple_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, - 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(), - settlement: eth::ContractAddress(H160::from_slice( - &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").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("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); - assert_eq!(swap.allowance.spender.0, order.owner); -} - -#[ignore] -#[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE -async fn simple_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, - 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(), - settlement: eth::ContractAddress(H160::from_slice( - &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").unwrap(), - )), - 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_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.sell); - assert_eq!(swap.allowance.spender.0, order.owner); -} - -#[ignore] -#[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE -async fn simple_api_error() { - init_logging(); - env::set_var("OKX_PROJECT_ID", "5d0b6cbaf8e9cedb7eb6836a0f35d961"); - env::set_var("OKX_API_KEY", "4b2ba8b8-4201-4f53-9587-74420a702be6"); - env::set_var("OKX_SECRET_KEY", "E5BFA3CC41B40A52BF78AA75D07DD148"); - env::set_var("OKX_PASSPHRASE", "xjmgdqY6ApuZdfQFCnvR$"); - - 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, - 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(), - settlement: eth::ContractAddress(H160::from_slice( - &hex::decode("6f9ffea7370310cd0f890dfde5e0e061059dcfb8").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::tests::okx::okx_dex::Error::Api { .. } - )); +mod api_calls; +mod market_order; + + +/// 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' +", + )) } From 74594acca25d19881393dd455fd91c63fa8f1bce Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 12:03:44 +0100 Subject: [PATCH 22/34] Updated dto initialization --- src/infra/dex/okx/dto.rs | 3 +- src/infra/dex/okx/mod.rs | 4 --- src/tests/okx/api_calls.rs | 66 -------------------------------------- 3 files changed, 2 insertions(+), 71 deletions(-) diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 36ffba9..d9482d3 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -28,7 +28,7 @@ pub struct SwapRequest { #[serde_as(as = "serialize::U256")] pub amount: U256, - /// Contract address of a token to be send + /// Contract address of a token to be sent pub from_token_address: H160, /// Contract address of a token to be received @@ -139,6 +139,7 @@ impl SwapRequest { to_token_address, amount, slippage: Slippage(slippage.as_factor().clone()), + user_wallet_address: order.owner, ..self } } diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 10e5b1d..e21c07a 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -45,9 +45,6 @@ pub struct Config { /// to get passhprase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys pub api_passphrase: String, - /// The address of the settlement contract. - pub settlement: eth::ContractAddress, - /// The stream that yields every new block. pub block_stream: Option, } @@ -77,7 +74,6 @@ impl Okx { let defaults = dto::SwapRequest { chain_id: config.chain_id as u64, - user_wallet_address: config.settlement.0, ..Default::default() }; diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs index f879e35..4a2a438 100644 --- a/src/tests/okx/api_calls.rs +++ b/src/tests/okx/api_calls.rs @@ -10,30 +10,11 @@ use { std::{env, str::FromStr}, }; - -#[cfg(test)] -fn init_logging() { - use tracing_subscriber::{layer::*, util::*}; - if std::env::var("RUST_LOG").is_err() { - std::env::set_var("RUST_LOG", "trace"); - } - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::from_default_env()) - .with(tracing_subscriber::fmt::Layer::default().compact()) - .init(); - env::set_var("OKX_PROJECT_ID", "5d0b6cbaf8e9cedb7eb6836a0f35d961"); - env::set_var("OKX_API_KEY", "4b2ba8b8-4201-4f53-9587-74420a702be6"); - env::set_var("OKX_SECRET_KEY", "E5BFA3CC41B40A52BF78AA75D07DD148"); - env::set_var("OKX_PASSPHRASE", "xjmgdqY6ApuZdfQFCnvR$"); -} - #[ignore] #[tokio::test] // To run this test set following environment variables accordingly to your OKX // setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE async fn swap_sell() { - init_logging(); - 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, @@ -73,8 +54,6 @@ async fn swap_sell() { // To run this test set following environment variables accordingly to your OKX // setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE async fn swap_buy() { - init_logging(); - 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, @@ -114,8 +93,6 @@ async fn swap_buy() { // To run this test set following environment variables accordingly to your OKX // setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE async fn swap_api_error() { - init_logging(); - 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, @@ -148,46 +125,3 @@ async fn swap_api_error() { crate::infra::dex::okx::Error::Api { .. } )); } - - - - -#[ignore] -#[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE -async fn simple_sell2() { - init_logging(); - - 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, - 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("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), - )), - buy: TokenAddress::from(H160::from_slice( - &hex::decode("e41d2489571d322189246dafa5ebde1f4699f498").unwrap(), - )), - side: crate::domain::order::Side::Sell, - amount: Amount::new(U256::from_dec_str("1000000000000000000").unwrap()), - owner: H160::from_slice(&hex::decode("2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a").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); - assert_eq!(swap.allowance.spender.0, order.owner); -} From ec9572583364c8070e20bc8f8af4b6644bdc047a Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 12:27:47 +0100 Subject: [PATCH 23/34] Removed unneeded code --- src/infra/dex/okx/mod.rs | 46 +++++++--------- src/tests/okx/api_calls.rs | 7 +-- src/tests/okx/market_order.rs | 98 +++++++++++++++++------------------ src/tests/okx/mod.rs | 6 +-- 4 files changed, 71 insertions(+), 86 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index e21c07a..8e2dcf7 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -133,29 +133,9 @@ impl Okx { Self::handle_api_error(quote.code, "e.msg)?; let quote_result = quote.data.first().ok_or(Error::NotFound)?; - let (input, output, max_sell_amount) = match order.side { - order::Side::Buy => ( - eth::Asset { - token: order.buy, - amount: quote_result.router_result.from_token_amount, - }, - eth::Asset { - token: order.sell, - amount: quote_result.router_result.to_token_amount, - }, - slippage.add(quote_result.router_result.from_token_amount), - ), - order::Side::Sell => ( - eth::Asset { - token: order.sell, - amount: quote_result.router_result.from_token_amount, - }, - eth::Asset { - token: order.buy, - amount: quote_result.router_result.to_token_amount, - }, - quote_result.router_result.from_token_amount, - ), + let max_sell_amount = match order.side { + order::Side::Buy => slippage.add(quote_result.router_result.from_token_amount), + order::Side::Sell => quote_result.router_result.from_token_amount, }; Ok(dex::Swap { @@ -163,10 +143,24 @@ impl Okx { to: eth::ContractAddress(quote_result.tx.to), calldata: quote_result.tx.data.clone(), }, - input, - output, + 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.from), + spender: eth::ContractAddress(quote_result.tx.to), amount: dex::Amount::new(max_sell_amount), }, gas: eth::Gas(quote_result.tx.gas), // todo ms: increase by 50% according to docs? diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs index 4a2a438..5d6f178 100644 --- a/src/tests/okx/api_calls.rs +++ b/src/tests/okx/api_calls.rs @@ -1,9 +1,6 @@ use { crate::{ - domain::{ - dex::*, - eth::*, - }, + domain::{dex::*, eth::*}, infra::dex::okx as okx_dex, }, ethereum_types::H160, @@ -46,7 +43,6 @@ 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.0, order.owner); } #[ignore] @@ -85,7 +81,6 @@ async fn swap_buy() { assert_eq!(swap.input.token, order.amount().token); assert_eq!(swap.input.amount, order.amount().amount); assert_eq!(swap.output.token, order.sell); - assert_eq!(swap.allowance.spender.0, order.owner); } #[ignore] diff --git a/src/tests/okx/market_order.rs b/src/tests/okx/market_order.rs index 1c49d22..cc8d2ab 100644 --- a/src/tests/okx/market_order.rs +++ b/src/tests/okx/market_order.rs @@ -156,7 +156,7 @@ async fn sell() { "sellToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "buyToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", "sellAmount": "1000000000000000000", - "buyAmount": "200000000000000000000", + "buyAmount": "200000000000000000000", "fullSellAmount": "1000000000000000000", "fullBuyAmount": "200000000000000000000", "kind": "sell", @@ -184,54 +184,54 @@ async fn sell() { assert_eq!( solution, json!({ - "solutions":[ - { - "gas":308891, - "id":0, - "interactions":[ - { - "allowances":[ - { - "amount":"1000000000000000000", - "spender":"0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", - "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" - } - ] - } - ] - }), + "solutions":[ + { + "gas":308891, + "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" + } + ] + } + ] + }), ); } diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index c0ec530..ff928ef 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -1,12 +1,8 @@ -use { - crate::tests, - std::net::SocketAddr, -}; +use {crate::tests, std::net::SocketAddr}; mod api_calls; mod market_order; - /// Creates a temporary file containing the config of the given solver. pub fn config(solver_addr: &SocketAddr) -> tests::Config { tests::Config::String(format!( From 60d9186283157a852959f1fc0459678bbbc58a80 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 12:53:44 +0100 Subject: [PATCH 24/34] Removed buy order type support --- src/infra/dex/okx/dto.rs | 16 ++++++--- src/infra/dex/okx/mod.rs | 22 +++++++------ src/tests/okx/api_calls.rs | 20 +++++------- src/tests/okx/market_order.rs | 61 +++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index d9482d3..9f3b000 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -13,6 +13,7 @@ use { }; /// A OKX API swap request parameters. +/// Only sell orders are supported by OKX. /// /// See [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap) /// documentation for more detailed information on each parameter. @@ -24,7 +25,7 @@ pub struct SwapRequest { #[serde_as(as = "serde_with::DisplayFromStr")] pub chain_id: u64, - /// Input amount of a token to be sold set in minimal divisible units + /// Input amount of a token to be sold set in minimal divisible units. #[serde_as(as = "serialize::U256")] pub amount: U256, @@ -37,7 +38,7 @@ pub struct SwapRequest { /// Limit of price slippage you are willing to accept pub slippage: Slippage, - /// User's wallet address + /// User's wallet address. Where the sell tokens will be taken from. pub user_wallet_address: H160, /// The fromToken address that receives the commission. @@ -128,20 +129,25 @@ pub enum GasLevel { } impl SwapRequest { - pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Self { + 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; + }; + let (from_token_address, to_token_address, amount) = match order.side { order::Side::Sell => (order.sell.0, order.buy.0, order.amount.get()), order::Side::Buy => (order.buy.0, order.sell.0, order.amount.get()), }; - Self { + Some(Self { from_token_address, to_token_address, amount, slippage: Slippage(slippage.as_factor().clone()), user_wallet_address: order.owner, ..self - } + }) } } diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 8e2dcf7..7eb4082 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -1,6 +1,6 @@ use { crate::{ - domain::{dex, eth, order}, + domain::{dex, eth}, util, }, base64::prelude::*, @@ -85,6 +85,9 @@ 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 sign_request(&self, request: &reqwest::Request, timestamp: &str) -> Result { let mut data = String::new(); data.push_str(timestamp); @@ -102,7 +105,7 @@ impl Okx { Ok(BASE64_STANDARD.encode(signature)) } - /// Error codes: https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code + /// OKX Error codes: https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code fn handle_api_error(code: i64, message: &str) -> Result<(), Error> { match code { 0 => Ok(()), @@ -118,7 +121,11 @@ impl Okx { order: &dex::Order, slippage: &dex::Slippage, ) -> Result { - let query = self.defaults.clone().with_domain(order, slippage); + 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 @@ -133,11 +140,6 @@ impl Okx { Self::handle_api_error(quote.code, "e.msg)?; let quote_result = quote.data.first().ok_or(Error::NotFound)?; - let max_sell_amount = match order.side { - order::Side::Buy => slippage.add(quote_result.router_result.from_token_amount), - order::Side::Sell => quote_result.router_result.from_token_amount, - }; - Ok(dex::Swap { call: dex::Call { to: eth::ContractAddress(quote_result.tx.to), @@ -161,7 +163,7 @@ impl Okx { }, allowance: dex::Allowance { spender: eth::ContractAddress(quote_result.tx.to), - amount: dex::Amount::new(max_sell_amount), + amount: dex::Amount::new(quote_result.router_result.from_token_amount), }, gas: eth::Gas(quote_result.tx.gas), // todo ms: increase by 50% according to docs? }) @@ -219,6 +221,8 @@ pub enum Error { SignRequestFailed, #[error("unable to find a quote")] NotFound, + #[error("order type is not supported")] + OrderNotSupported, #[error("quote does not specify an approval spender")] MissingSpender, #[error("rate limited")] diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs index 5d6f178..53f2251 100644 --- a/src/tests/okx/api_calls.rs +++ b/src/tests/okx/api_calls.rs @@ -45,18 +45,15 @@ async fn swap_sell() { assert_eq!(swap.output.token, order.buy); } -#[ignore] #[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE 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, - 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(), + project_id: String::new(), + api_key: String::new(), + api_secret_key: String::new(), + api_passphrase: String::new(), block_stream: None, }; @@ -76,11 +73,10 @@ async fn swap_buy() { 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.sell); + assert!(matches!( + swap_response.unwrap_err(), + crate::infra::dex::okx::Error::OrderNotSupported + )); } #[ignore] diff --git a/src/tests/okx/market_order.rs b/src/tests/okx/market_order.rs index cc8d2ab..72700d5 100644 --- a/src/tests/okx/market_order.rs +++ b/src/tests/okx/market_order.rs @@ -235,3 +235,64 @@ async fn sell() { ); } +#[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": [] }),); +} From 028b28a7aa1275e0c07b8b7ae955a3b7f1b1ed38 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 13:13:21 +0100 Subject: [PATCH 25/34] Moved OKX credentials config to separate struct --- src/infra/config/dex/okx/file.rs | 12 +++++++----- src/infra/dex/okx/mod.rs | 22 ++++++++++++++-------- src/tests/okx/api_calls.rs | 30 ++++++++++++++++++------------ 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/infra/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index cc23f9a..d7a3f53 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -51,12 +51,14 @@ pub async fn load(path: &Path) -> super::Config { super::Config { okx: okx::Config { - chain_id: config.chain_id, - project_id: config.api_project_id, - api_key: config.api_key, - api_secret_key: config.api_secret_key, - api_passphrase: config.api_passphrase, endpoint: config.endpoint, + chain_id: config.chain_id, + okx_credentials: okx::OkxCredentialsConfig { + project_id: config.api_project_id, + api_key: config.api_key, + api_secret_key: config.api_secret_key, + api_passphrase: config.api_passphrase, + }, block_stream: base.block_stream.clone(), }, base, diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 7eb4082..0d1bc04 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -24,11 +24,19 @@ pub struct Okx { } pub struct Config { - /// The base URL for the 0KX swap API. + /// 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. Instruction on how to create project: /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#create-project pub project_id: String, @@ -44,24 +52,22 @@ pub struct Config { /// OKX API key passphrase used to encrypt secrety key. Instruction on how /// to get passhprase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys pub api_passphrase: String, - - /// The stream that yields every new block. - pub block_stream: Option, } impl Okx { pub fn try_new(config: Config) -> Result { let client = { - let mut api_key = reqwest::header::HeaderValue::from_str(&config.api_key)?; + 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.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.project_id)?, + reqwest::header::HeaderValue::from_str(&config.okx_credentials.project_id)?, ); headers.insert("OK-ACCESS-KEY", api_key); headers.insert("OK-ACCESS-PASSPHRASE", api_passphrase); @@ -80,7 +86,7 @@ impl Okx { Ok(Self { client, endpoint: config.endpoint, - api_secret_key: config.api_secret_key, + api_secret_key: config.okx_credentials.api_secret_key, defaults, }) } diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs index 53f2251..7f1dbe4 100644 --- a/src/tests/okx/api_calls.rs +++ b/src/tests/okx/api_calls.rs @@ -15,10 +15,12 @@ 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, - 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(), + 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, }; @@ -50,10 +52,12 @@ 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, - project_id: String::new(), - api_key: String::new(), - api_secret_key: String::new(), - api_passphrase: String::new(), + 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, }; @@ -87,10 +91,12 @@ 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, - 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(), + 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, }; From 5a6e1b6b5a366c3f00d3fc8502e067350187a70f Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 13:38:15 +0100 Subject: [PATCH 26/34] Updated config file definition --- config/example.okx.toml | 28 +++++++++++++++++++++ src/infra/config/dex/okx/file.rs | 42 +++++++++++++++++++++++--------- src/infra/dex/okx/mod.rs | 12 +++------ src/tests/okx/mod.rs | 2 +- 4 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 config/example.okx.toml diff --git a/config/example.okx.toml b/config/example.okx.toml new file mode 100644 index 0000000..e9b866e --- /dev/null +++ b/config/example.okx.toml @@ -0,0 +1,28 @@ +node-url = "http://localhost:8545" + +[dex] +# See here how to get a free key: https://0x.org/docs/introduction/getting-started +api-key = "$YOUR_API_KEY" + +# 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 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 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 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 passhprase: +# 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/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index d7a3f53..ad5b27f 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -13,7 +13,7 @@ use { #[derive(Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] struct Config { - /// The versioned URL endpoint for the 0x swap API. + /// The versioned URL endpoint for the OKX swap API. #[serde(default = "default_endpoint")] #[serde_as(as = "serde_with::DisplayFromStr")] endpoint: reqwest::Url, @@ -22,19 +22,44 @@ struct Config { #[serde_as(as = "serialize::ChainId")] chain_id: eth::ChainId, - /// OKX Project ID. + /// 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 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. + /// OKX API Key. Instruction on how to generate 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. + /// OKX Secret key used for signing request. Instruction on how to get + /// 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. + /// OKX Secret key passphrase. Instruction on how + /// to get passhprase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys api_passphrase: String, } +// Implementing Into<> is enough as opposite conversion will never be used. +#[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() @@ -53,12 +78,7 @@ pub async fn load(path: &Path) -> super::Config { okx: okx::Config { endpoint: config.endpoint, chain_id: config.chain_id, - okx_credentials: okx::OkxCredentialsConfig { - project_id: config.api_project_id, - api_key: config.api_key, - api_secret_key: config.api_secret_key, - api_passphrase: config.api_passphrase, - }, + okx_credentials: config.okx_credentials.into(), block_stream: base.block_stream.clone(), }, base, diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 0d1bc04..dd9232a 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -37,20 +37,16 @@ pub struct Config { } pub struct OkxCredentialsConfig { - /// OKX project ID to use. Instruction on how to create project: - /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#create-project + /// OKX project ID to use. pub project_id: String, - /// OKX API key. Instruction on how to generate API key: - /// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + /// OKX API key. pub api_key: String, - /// OKX API key additional security token. Instruction on how to get - /// security token: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key + /// OKX API key additional security token. pub api_secret_key: String, - /// OKX API key passphrase used to encrypt secrety key. Instruction on how - /// to get passhprase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + /// OKX API key passphrase used to encrypt secrety key. pub api_passphrase: String, } diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index ff928ef..463c98f 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -14,7 +14,7 @@ endpoint = 'http://{solver_addr}' api-project-id = '1' api-key = '1234' api-secret-key = '1234567890123456' -api-passphrase ='pass' +api-passphrase = 'pass' ", )) } From 8ec77b738592a827684bae46f415f162e1c72e05 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 15:16:01 +0100 Subject: [PATCH 27/34] Updated gas calculation --- src/infra/dex/okx/mod.rs | 12 +++++++++++- src/tests/okx/market_order.rs | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index dd9232a..2a2e4d3 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -142,6 +142,14 @@ impl Okx { 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 >> 1) + .ok_or(Error::GasCalculationFailed)?; + Ok(dex::Swap { call: dex::Call { to: eth::ContractAddress(quote_result.tx.to), @@ -167,7 +175,7 @@ impl Okx { spender: eth::ContractAddress(quote_result.tx.to), amount: dex::Amount::new(quote_result.router_result.from_token_amount), }, - gas: eth::Gas(quote_result.tx.gas), // todo ms: increase by 50% according to docs? + gas: eth::Gas(gas), }) } @@ -231,6 +239,8 @@ pub enum Error { RateLimited, #[error("sell token or buy token are banned from trading")] UnavailableForLegalReasons, + #[error("calculating output gas failed")] + GasCalculationFailed, #[error("api error code {code}: {reason}")] Api { code: i64, reason: String }, #[error(transparent)] diff --git a/src/tests/okx/market_order.rs b/src/tests/okx/market_order.rs index 72700d5..7e068dd 100644 --- a/src/tests/okx/market_order.rs +++ b/src/tests/okx/market_order.rs @@ -186,7 +186,7 @@ async fn sell() { json!({ "solutions":[ { - "gas":308891, + "gas":410141, "id":0, "interactions":[ { From 3267648ef700776e8f17028b9b71399b2dea73c6 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 15:27:58 +0100 Subject: [PATCH 28/34] Updated errors handling --- src/infra/dex/mod.rs | 2 +- src/infra/dex/okx/mod.rs | 46 ++++++++++++++++------------------------ 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/infra/dex/mod.rs b/src/infra/dex/mod.rs index 508729f..5509a88 100644 --- a/src/infra/dex/mod.rs +++ b/src/infra/dex/mod.rs @@ -148,9 +148,9 @@ 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, - okx::Error::UnavailableForLegalReasons => Self::UnavailableForLegalReasons, _ => Self::Other(Box::new(err)), } } diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 2a2e4d3..f510b55 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -109,13 +109,14 @@ impl Okx { /// OKX Error codes: https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code fn handle_api_error(code: i64, message: &str) -> Result<(), Error> { - match code { - 0 => Ok(()), - _ => Err(Error::Api { + Err(match code { + 0 => return Ok(()), + 50011 => Error::RateLimited, + _ => Error::Api { code, reason: message.to_string(), - }), - } + }, + }) } pub async fn swap( @@ -229,18 +230,14 @@ pub enum Error { 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("quote does not specify an approval spender")] - MissingSpender, #[error("rate limited")] RateLimited, - #[error("sell token or buy token are banned from trading")] - UnavailableForLegalReasons, - #[error("calculating output gas failed")] - GasCalculationFailed, #[error("api error code {code}: {reason}")] Api { code: i64, reason: String }, #[error(transparent)] @@ -248,35 +245,28 @@ pub enum 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 funcion: 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, - StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => { - Self::UnavailableForLegalReasons - } _ => Self::Http(err), } } else { Self::Http(err) } } - util::http::RoundtripError::Api(err) => { - // Unfortunately, AFAIK these codes aren't documented anywhere. These - // based on empirical observations of what the API has returned in the - // past. - match err.code { - 100 => Self::NotFound, - 429 => Self::RateLimited, - 451 => Self::UnavailableForLegalReasons, - _ => Self::Api { - code: err.code, - reason: err.reason, - }, - } - } + util::http::RoundtripError::Api(err) => match err.code { + 429 => Self::RateLimited, + _ => Self::Api { + code: err.code, + reason: err.reason, + }, + }, } } } From 60948eda3b116fd3cd091db2088c444b000b970a Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 15:32:25 +0100 Subject: [PATCH 29/34] Removed optional fields from swap request --- src/infra/dex/okx/dto.rs | 72 ---------------------------------------- 1 file changed, 72 deletions(-) diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 9f3b000..7ace630 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -40,78 +40,6 @@ pub struct SwapRequest { /// User's wallet address. Where the sell tokens will be taken from. pub user_wallet_address: H160, - - /// The fromToken address that receives the commission. - /// Only for SOL or SPL-Token commissions. - #[serde(skip_serializing_if = "Option::is_none")] - pub referrer_address: Option, - - /// Recipient address of a purchased token if not set, - /// user_wallet_address will receive a purchased token. - #[serde(skip_serializing_if = "Option::is_none")] - pub swap_receiver_address: Option, - - /// The percentage of from_token_address will be sent to the referrer's - /// address, the rest will be set as the input amount to be sold. - /// Min percentage:0 - /// Max percentage:3 - #[serde(skip_serializing_if = "Option::is_none")] - #[serde_as(as = "Option")] - pub fee_percent: Option, - - /// The gas limit (in wei) for the swap transaction. - #[serde(skip_serializing_if = "Option::is_none")] - #[serde_as(as = "Option")] - pub gas_limit: Option, - - /// The target gas price level for the swap transaction. - /// Default value: average - #[serde(skip_serializing_if = "Option::is_none")] - pub gas_level: Option, - - /// List of DexId of the liquidity pool for limited quotes. - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde_as(as = "serialize::CommaSeparated")] - pub dex_ids: Vec, - - /// The percentage of the price impact allowed. - /// Min value: 0 - /// Max value:1 (100%) - /// Default value: 0.9 (90%) - #[serde(skip_serializing_if = "Option::is_none")] - #[serde_as(as = "Option")] - pub price_impact_protection_percentage: Option, - - /// Customized parameters sent on the blockchain in callData. - /// Hex encoded 128-characters string. - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde_as(as = "serialize::Hex")] - pub call_data_memo: Vec, - - /// Address that receives the commission. - /// Only for SOL or SPL-Token commissions. - #[serde(skip_serializing_if = "Option::is_none")] - pub to_token_referrer_address: Option, - - /// Used for transactions on the Solana network and similar to gas_price on - /// Ethereum. - #[serde(skip_serializing_if = "Option::is_none")] - #[serde_as(as = "Option")] - pub compute_unit_price: Option, - - /// Used for transactions on the Solana network and analogous to gas_limit - /// on Ethereum. - #[serde(skip_serializing_if = "Option::is_none")] - #[serde_as(as = "Option")] - pub compute_unit_limit: Option, - - /// The wallet address to receive the commission fee from the from_token. - #[serde(skip_serializing_if = "Option::is_none")] - pub from_token_referrer_wallet_address: Option, - - /// The wallet address to receive the commission fee from the to_token - #[serde(skip_serializing_if = "Option::is_none")] - pub to_token_referrer_wallet_address: Option, } /// A OKX slippage amount. From 652e8d6ab347188853f9bc3b2cf5b86b3df30d2e Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 16:02:54 +0100 Subject: [PATCH 30/34] Removed unused response fields, updated comments --- config/example.okx.toml | 2 +- src/infra/config/dex/okx/file.rs | 2 +- src/infra/dex/okx/dto.rs | 178 +++++++------------------------ src/infra/dex/okx/mod.rs | 6 +- 4 files changed, 44 insertions(+), 144 deletions(-) diff --git a/config/example.okx.toml b/config/example.okx.toml index e9b866e..6f0b6ae 100644 --- a/config/example.okx.toml +++ b/config/example.okx.toml @@ -23,6 +23,6 @@ api-key = "$OKX_API_KEY" # 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 passhprase: +# OKX Secret key passphrase. Instruction on how to get 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/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index ad5b27f..6d237ba 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -43,7 +43,7 @@ struct OkxCredentialsConfig { api_secret_key: String, /// OKX Secret key passphrase. Instruction on how - /// to get passhprase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + /// to get passphrase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys api_passphrase: String, } diff --git a/src/infra/dex/okx/dto.rs b/src/infra/dex/okx/dto.rs index 7ace630..16345e9 100644 --- a/src/infra/dex/okx/dto.rs +++ b/src/infra/dex/okx/dto.rs @@ -12,8 +12,8 @@ use { serde_with::serde_as, }; -/// A OKX API swap request parameters. -/// Only sell orders are supported by OKX. +/// 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. @@ -46,16 +46,6 @@ pub struct SwapRequest { #[derive(Clone, Debug, Default, Serialize)] pub struct Slippage(BigDecimal); -/// A OKX gas level. -#[derive(Clone, Debug, Default, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum GasLevel { - #[default] - Average, - Fast, - Slow, -} - impl SwapRequest { pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Option { // Buy orders are not supported on OKX @@ -63,15 +53,10 @@ impl SwapRequest { return None; }; - let (from_token_address, to_token_address, amount) = match order.side { - order::Side::Sell => (order.sell.0, order.buy.0, order.amount.get()), - order::Side::Buy => (order.buy.0, order.sell.0, order.amount.get()), - }; - Some(Self { - from_token_address, - to_token_address, - amount, + 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 @@ -79,178 +64,93 @@ impl SwapRequest { } } -/// A OKX API quote response. +/// 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 { - #[serde_as(as = "serde_with::DisplayFromStr")] - pub chain_id: u64, + /// 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, - - #[serde_as(as = "serde_with::DisplayFromStr")] - pub trade_fee: f64, - - #[serde_as(as = "serialize::U256")] - pub estimate_gas_fee: U256, - - pub dex_router_list: Vec, - - pub quote_compare_list: Vec, - - pub to_token: SwapResponseFromToToken, - - pub from_token: SwapResponseFromToToken, -} - -#[serde_as] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SwapResponseDexRouterList { - pub router: String, - - #[serde_as(as = "serde_with::DisplayFromStr")] - pub router_percent: f64, - - pub sub_router_list: Vec, -} - -#[serde_as] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SwapResponseDexSubRouterList { - pub dex_protocol: Vec, - - pub from_token: SwapResponseFromToToken, - - pub to_token: SwapResponseFromToToken, -} - -#[serde_as] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SwapResponseDexProtocol { - pub dex_name: String, - - #[serde_as(as = "serde_with::DisplayFromStr")] - pub percent: f64, } +/// 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, - - pub token_symbol: String, - - #[serde_as(as = "serde_with::DisplayFromStr")] - pub token_unit_price: f64, - - #[serde_as(as = "serde_with::DisplayFromStr")] - pub decimal: u8, - - pub is_honey_pot: bool, - - #[serde_as(as = "serde_with::DisplayFromStr")] - pub tax_rate: f64, -} - -#[serde_as] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SwapResponseQuoteCompareList { - pub dex_name: String, - - pub dex_logo: String, - - #[serde_as(as = "serde_with::DisplayFromStr")] - pub trade_fee: f64, - - // todo: missing in docs? - #[serde_as(as = "serde_with::DisplayFromStr")] - pub amount_out: f64, - // todo: missing from response? - //#[serde_as(as = "serialize::U256")] - //pub receive_amount: U256, - - // todo: missing from response? - //#[serde_as(as = "serde_with::DisplayFromStr")] - //pub price_impact_percentage: f64, } +/// 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 { - pub signature_data: Vec, - - pub from: H160, - + /// Estimated amount of the gas limit. #[serde_as(as = "serialize::U256")] pub gas: U256, - #[serde_as(as = "serialize::U256")] - pub gas_price: U256, - - #[serde_as(as = "serialize::U256")] - pub max_priority_fee_per_gas: U256, - + /// The contract address of OKX DEX router. pub to: H160, - #[serde_as(as = "serialize::U256")] - pub value: U256, - - #[serde_as(as = "serialize::U256")] - pub min_receive_amount: U256, - + /// Call data. #[serde_as(as = "serialize::Hex")] pub data: Vec, } -#[derive(Deserialize)] -#[serde(untagged)] -pub enum Response { - Ok(SwapResponse), - Err(Error), -} - -impl Response { - /// Turns the API response into a [`std::result::Result`]. - pub fn into_result(self) -> Result { - match self { - Response::Ok(quote) => Ok(quote), - Response::Err(err) => Err(err), - } - } -} - #[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 f510b55..51f0d58 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -46,7 +46,7 @@ pub struct OkxCredentialsConfig { /// OKX API key additional security token. pub api_secret_key: String, - /// OKX API key passphrase used to encrypt secrety key. + /// OKX API key passphrase used to encrypt secret key. pub api_passphrase: String, } @@ -107,7 +107,7 @@ impl Okx { Ok(BASE64_STANDARD.encode(signature)) } - /// OKX Error codes: https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code + /// 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(()), @@ -247,7 +247,7 @@ pub enum 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 funcion: handle_api_error(). + // dedicated function: handle_api_error(). fn from(err: util::http::RoundtripError) -> Self { match err { util::http::RoundtripError::Http(err) => { From 34f7d771a3aa48edd88f340cb7908c219b5ce875 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 16:25:39 +0100 Subject: [PATCH 31/34] Added not-found test --- src/infra/dex/okx/mod.rs | 2 + src/tests/okx/api_calls.rs | 40 ++++++++++++++++++++ src/tests/okx/mod.rs | 1 + src/tests/okx/not_found.rs | 77 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 src/tests/okx/not_found.rs diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index 51f0d58..b8c7393 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -111,6 +111,8 @@ impl Okx { 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, diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs index 7f1dbe4..207737a 100644 --- a/src/tests/okx/api_calls.rs +++ b/src/tests/okx/api_calls.rs @@ -122,3 +122,43 @@ async fn swap_api_error() { crate::infra::dex::okx::Error::Api { .. } )); } + +#[ignore] +#[tokio::test] +// To run this test set 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/mod.rs b/src/tests/okx/mod.rs index 463c98f..8995aa9 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -2,6 +2,7 @@ use {crate::tests, std::net::SocketAddr}; mod api_calls; mod market_order; +mod not_found; /// Creates a temporary file containing the config of the given solver. pub fn config(solver_addr: &SocketAddr) -> tests::Config { 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": [] }),); +} From e22c6544ccdd4eb7e57c928b1574dcac7c71119f Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Thu, 9 Jan 2025 16:34:55 +0100 Subject: [PATCH 32/34] Added out of price test --- src/tests/okx/mod.rs | 1 + src/tests/okx/out_of_price.rs | 189 ++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 src/tests/okx/out_of_price.rs diff --git a/src/tests/okx/mod.rs b/src/tests/okx/mod.rs index 8995aa9..eba6274 100644 --- a/src/tests/okx/mod.rs +++ b/src/tests/okx/mod.rs @@ -3,6 +3,7 @@ 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 { 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": [] }),); +} From 9911a20b32fe265cfc621c52fa9172c5f0a47aad Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 10 Jan 2025 14:24:16 +0100 Subject: [PATCH 33/34] Small refactoring --- src/infra/dex/okx/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/infra/dex/okx/mod.rs b/src/infra/dex/okx/mod.rs index b8c7393..60a4cd3 100644 --- a/src/infra/dex/okx/mod.rs +++ b/src/infra/dex/okx/mod.rs @@ -90,7 +90,11 @@ 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 sign_request(&self, request: &reqwest::Request, timestamp: &str) -> Result { + 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()); @@ -98,8 +102,7 @@ impl Okx { data.push('?'); data.push_str(request.url().query().ok_or(Error::SignRequestFailed)?); - type HmacSha256 = Hmac; - let mut mac = HmacSha256::new_from_slice(self.api_secret_key.as_bytes()) + 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(); @@ -150,7 +153,7 @@ impl Okx { let gas = quote_result .tx .gas - .checked_add(quote_result.tx.gas >> 1) + .checked_add(quote_result.tx.gas / 2) .ok_or(Error::GasCalculationFailed)?; Ok(dex::Swap { @@ -197,7 +200,7 @@ impl Okx { let timestamp = &chrono::Utc::now() .to_rfc3339_opts(SecondsFormat::Millis, true) .to_string(); - let signature = self.sign_request(&request, timestamp)?; + let signature = self.generate_signature(&request, timestamp)?; request_builder = request_builder.header( "OK-ACCESS-TIMESTAMP", From 97f3309ca2447828d349825fbcf138ed8630b268 Mon Sep 17 00:00:00 2001 From: Michal Strug Date: Fri, 10 Jan 2025 14:31:42 +0100 Subject: [PATCH 34/34] Updated comments --- config/example.okx.toml | 11 ++++------- src/infra/config/dex/okx/file.rs | 11 +++++------ src/tests/okx/api_calls.rs | 12 ++++++------ 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/config/example.okx.toml b/config/example.okx.toml index 6f0b6ae..b8bf297 100644 --- a/config/example.okx.toml +++ b/config/example.okx.toml @@ -1,9 +1,6 @@ node-url = "http://localhost:8545" [dex] -# See here how to get a free key: https://0x.org/docs/introduction/getting-started -api-key = "$YOUR_API_KEY" - # 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" @@ -11,18 +8,18 @@ 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 project: +# 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 API key: +# 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 security token: +# 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 passphrase: +# 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/config/dex/okx/file.rs b/src/infra/config/dex/okx/file.rs index 6d237ba..c1c41bf 100644 --- a/src/infra/config/dex/okx/file.rs +++ b/src/infra/config/dex/okx/file.rs @@ -30,24 +30,23 @@ struct Config { #[derive(Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] struct OkxCredentialsConfig { - /// OKX Project ID. Instruction on how to create project: + /// 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 API key: + /// 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 + /// 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 passphrase: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys + /// 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, } -// Implementing Into<> is enough as opposite conversion will never be used. #[allow(clippy::from_over_into)] impl Into for OkxCredentialsConfig { fn into(self) -> okx::OkxCredentialsConfig { diff --git a/src/tests/okx/api_calls.rs b/src/tests/okx/api_calls.rs index 207737a..9c0648d 100644 --- a/src/tests/okx/api_calls.rs +++ b/src/tests/okx/api_calls.rs @@ -9,8 +9,8 @@ use { #[ignore] #[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +// 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(), @@ -85,8 +85,8 @@ async fn swap_buy() { #[ignore] #[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +// 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(), @@ -125,8 +125,8 @@ async fn swap_api_error() { #[ignore] #[tokio::test] -// To run this test set following environment variables accordingly to your OKX -// setup: OKX_PROJECT_ID, OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE +// 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(),