Skip to content

Commit

Permalink
OKX integration (#97)
Browse files Browse the repository at this point in the history
# Description
OKX dex integration.
See also dedicated issue: #93

# Changes
Added new `okx` modules in `dex`, `config` and `tests`.
Created dto basing on documentation:
https://www.okx.com/en-au/web3/build/docs/waas/dex-swap.

OKX requires signing of swap request, it is implemented in function:
`sign_request()`, basing on documentation:
https://www.okx.com/en-au/web3/build/docs/waas/rest-authentication#signature

Also implemented custom error handling function: `handle_api_error()`,
as OKX in error cases returns valid json http body which causes that
trait implementation from `RoundtripError` to `dto::Error` is not used.

## How to test
Created dedicated tests which uses http mocking and real endpoint tests
(ignored) which requires providing API credentials. If API credentials
are needed for running ignored test I can provide that.
  • Loading branch information
mstrug authored Jan 10, 2025
1 parent 6b9aff1 commit b76dcc2
Show file tree
Hide file tree
Showing 18 changed files with 1,338 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
25 changes: 25 additions & 0 deletions config/example.okx.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
node-url = "http://localhost:8545"

[dex]
# Specify which chain to use, 1 for Ethereum.
# More info here: https://www.okx.com/en-au/web3/build/docs/waas/walletapi-resources-supported-networks
chain-id = "1"

# Optionally specify a custom OKX Swap API endpoint
# endpoint = "https://www.okx.com/api/v5/dex/aggregator/swap"

# OKX Project ID. Instruction on how to create a project:
# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#create-project
api-project-id = "$OKX_PROJECT_ID"

# OKX API Key. Instruction on how to generate an API key:
# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys
api-key = "$OKX_API_KEY"

# OKX Secret key used for signing request. Instruction on how to get a security token:
# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key
api-secret-key = "$OKX_SECRET_KEY"

# OKX Secret key passphrase. Instruction on how to get a passphrase:
# https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys
api-passphrase = "$OKX_PASSPHRASE"
5 changes: 5 additions & 0 deletions src/infra/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@ pub enum Command {
#[clap(long, env)]
config: PathBuf,
},
/// solve individual orders using OKX API
Okx {
#[clap(long, env)]
config: PathBuf,
},
}
1 change: 1 addition & 0 deletions src/infra/config/dex/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod balancer;
mod file;
pub mod okx;
pub mod oneinch;
pub mod paraswap;
pub mod zeroex;
Expand Down
85 changes: 85 additions & 0 deletions src/infra/config/dex/okx/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use {
crate::{
domain::eth,
infra::{config::dex::file, dex::okx},
util::serialize,
},
serde::Deserialize,
serde_with::serde_as,
std::path::Path,
};

#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct Config {
/// The versioned URL endpoint for the OKX swap API.
#[serde(default = "default_endpoint")]
#[serde_as(as = "serde_with::DisplayFromStr")]
endpoint: reqwest::Url,

/// Chain ID used to automatically determine contract addresses.
#[serde_as(as = "serialize::ChainId")]
chain_id: eth::ChainId,

/// OKX API credentials
#[serde(flatten)]
okx_credentials: OkxCredentialsConfig,
}

#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct OkxCredentialsConfig {
/// OKX Project ID. Instruction on how to create a project:
/// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#create-project
api_project_id: String,

/// OKX API Key. Instruction on how to generate an API key:
/// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys
api_key: String,

/// OKX Secret key used for signing request. Instruction on how to get a
/// security token: https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#view-the-secret-key
api_secret_key: String,

/// OKX Secret key passphrase. Instruction on how to get a passphrase:
/// https://www.okx.com/en-au/web3/build/docs/waas/introduction-to-developer-portal-interface#generate-api-keys
api_passphrase: String,
}

#[allow(clippy::from_over_into)]
impl Into<okx::OkxCredentialsConfig> for OkxCredentialsConfig {
fn into(self) -> okx::OkxCredentialsConfig {
okx::OkxCredentialsConfig {
project_id: self.api_project_id,
api_key: self.api_key,
api_secret_key: self.api_secret_key,
api_passphrase: self.api_passphrase,
}
}
}

fn default_endpoint() -> reqwest::Url {
"https://www.okx.com/api/v5/dex/aggregator/swap"
.parse()
.unwrap()
}

/// Load the OKX solver configuration from a TOML file.
///
/// # Panics
///
/// This method panics if the config is invalid or on I/O errors.
pub async fn load(path: &Path) -> super::Config {
let (base, config) = file::load::<Config>(path).await;

super::Config {
okx: okx::Config {
endpoint: config.endpoint,
chain_id: config.chain_id,
okx_credentials: config.okx_credentials.into(),
block_stream: base.block_stream.clone(),
},
base,
}
}
6 changes: 6 additions & 0 deletions src/infra/config/dex/okx/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod file;

pub struct Config {
pub okx: crate::infra::dex::okx::Config,
pub base: super::Config,
}
14 changes: 14 additions & 0 deletions src/infra/dex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use {
};

pub mod balancer;
pub mod okx;
pub mod oneinch;
pub mod paraswap;
pub mod simulator;
Expand All @@ -18,6 +19,7 @@ pub enum Dex {
OneInch(oneinch::OneInch),
ZeroEx(zeroex::ZeroEx),
ParaSwap(paraswap::ParaSwap),
Okx(okx::Okx),
}

impl Dex {
Expand All @@ -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)
}
Expand Down Expand Up @@ -141,3 +144,14 @@ impl From<paraswap::Error> for Error {
}
}
}

impl From<okx::Error> for Error {
fn from(err: okx::Error) -> Self {
match err {
okx::Error::OrderNotSupported => Self::OrderNotSupported,
okx::Error::NotFound => Self::NotFound,
okx::Error::RateLimited => Self::RateLimited,
_ => Self::Other(Box::new(err)),
}
}
}
158 changes: 158 additions & 0 deletions src/infra/dex/okx/dto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! DTOs for the OKX swap API. Full documentation for the API can be found
//! [here](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap).
use {
crate::{
domain::{dex, order},
util::serialize,
},
bigdecimal::BigDecimal,
ethereum_types::{H160, U256},
serde::{Deserialize, Serialize},
serde_with::serde_as,
};

/// A OKX API swap request parameters (only mandatory fields).
/// OKX supports only sell orders.
///
/// See [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap)
/// documentation for more detailed information on each parameter.
#[serde_as]
#[derive(Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapRequest {
/// Chain ID
#[serde_as(as = "serde_with::DisplayFromStr")]
pub chain_id: u64,

/// Input amount of a token to be sold set in minimal divisible units.
#[serde_as(as = "serialize::U256")]
pub amount: U256,

/// Contract address of a token to be sent
pub from_token_address: H160,

/// Contract address of a token to be received
pub to_token_address: H160,

/// Limit of price slippage you are willing to accept
pub slippage: Slippage,

/// User's wallet address. Where the sell tokens will be taken from.
pub user_wallet_address: H160,
}

/// A OKX slippage amount.
#[derive(Clone, Debug, Default, Serialize)]
pub struct Slippage(BigDecimal);

impl SwapRequest {
pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Option<Self> {
// Buy orders are not supported on OKX
if order.side == order::Side::Buy {
return None;
};

Some(Self {
from_token_address: order.sell.0,
to_token_address: order.buy.0,
amount: order.amount.get(),
slippage: Slippage(slippage.as_factor().clone()),
user_wallet_address: order.owner,
..self
})
}
}

/// A OKX API swap response - generic wrapper for success and failure cases.
///
/// See [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap)
/// documentation for more detailed information on each parameter.
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapResponse {
/// Error code, 0 for success, otherwise one of:
/// [error codes](https://www.okx.com/en-au/web3/build/docs/waas/dex-error-code)
#[serde_as(as = "serde_with::DisplayFromStr")]
pub code: i64,

/// Response data.
pub data: Vec<SwapResponseInner>,

/// Error code text message.
pub msg: String,
}

/// A OKX API swap response.
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapResponseInner {
/// Quote execution path.
pub router_result: SwapResponseRouterResult,

/// Contract related response.
pub tx: SwapResponseTx,
}

/// A OKX API swap response - quote execution path.
/// Deserializing fields which are only used by the implementation.
/// For all possible fields look into the documentation:
/// [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap)
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapResponseRouterResult {
/// The information of a token to be sold.
pub from_token: SwapResponseFromToToken,

/// The information of a token to be bought.
pub to_token: SwapResponseFromToToken,

/// The input amount of a token to be sold.
#[serde_as(as = "serialize::U256")]
pub from_token_amount: U256,

/// The resulting amount of a token to be bought.
#[serde_as(as = "serialize::U256")]
pub to_token_amount: U256,
}

/// A OKX API swap response - token information.
/// Deserializing fields which are only used by the implementation.
/// For all possible fields look into the documentation:
/// [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap)
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapResponseFromToToken {
/// Address of the token smart contract.
pub token_contract_address: H160,
}

/// A OKX API swap response - contract related information.
/// Deserializing fields which are only used by the implementation.
/// For all possible fields look into the documentation:
/// [API](https://www.okx.com/en-au/web3/build/docs/waas/dex-swap)
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapResponseTx {
/// Estimated amount of the gas limit.
#[serde_as(as = "serialize::U256")]
pub gas: U256,

/// The contract address of OKX DEX router.
pub to: H160,

/// Call data.
#[serde_as(as = "serialize::Hex")]
pub data: Vec<u8>,
}

#[derive(Deserialize)]
pub struct Error {
pub code: i64,
pub reason: String,
}
Loading

0 comments on commit b76dcc2

Please sign in to comment.