Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0x v2 support #106

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 4 additions & 14 deletions config/example.zeroex.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,11 @@ relative-slippage = "0.001" # Percentage in the [0, 1] range
# See here how to get a free key: https://0x.org/docs/introduction/getting-started
api-key = "$YOUR_API_KEY"

# Optionally specify a custom 0x API endpoint
# endpoint = "https://gated.api.0x.org/swap/v1/"
# Specify chain ID
chain-id = 1

# Optionally specify a custom affiliate address.
# affiliate = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
# Optionally specify a custom 0x API endpoint
# endpoint = "https://gated.api.0x.org/swap/allowance-holder/"

# Optionally specify which liquidity sources to exclude
# excluded_sources = ["Balancer_V2"]

# Optionally specify whether to enable RFQ-T liquidity
# enable-rfqt = true

# Optionally specify whether 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.
# enable-slippage-protection = true
39 changes: 9 additions & 30 deletions src/infra/config/dex/zeroex/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use {
crate::{
domain::eth,
infra::{config::dex::file, contracts, dex::zeroex},
util::serialize,
},
ethereum_types::H160,
serde::Deserialize,
serde_with::serde_as,
std::path::Path,
Expand All @@ -13,6 +13,11 @@ use {
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct Config {
/// Chain ID used to automatically determine the address of the settlement
/// contract and for metrics.
#[serde_as(as = "serialize::ChainId")]
chain_id: eth::ChainId,

/// The versioned URL endpoint for the 0x swap API.
#[serde(default = "default_endpoint")]
#[serde_as(as = "serde_with::DisplayFromStr")]
Expand All @@ -26,32 +31,10 @@ struct Config {
/// will not be considered when solving.
#[serde(default)]
excluded_sources: Vec<String>,

/// 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
"https://api.0x.org/swap/allowance-holder/".parse().unwrap()
}

/// Load the 0x solver configuration from a TOML file.
Expand All @@ -62,19 +45,15 @@ fn default_affiliate() -> H160 {
pub async fn load(path: &Path) -> super::Config {
let (base, config) = file::load::<Config>(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;
let settlement = contracts::Contracts::for_chain(config.chain_id).settlement;

super::Config {
zeroex: zeroex::Config {
chain_id: config.chain_id,
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,
Expand Down
1 change: 1 addition & 0 deletions src/infra/dex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ impl From<zeroex::Error> for Error {
zeroex::Error::NotFound => Self::NotFound,
zeroex::Error::RateLimited => Self::RateLimited,
zeroex::Error::UnavailableForLegalReasons => Self::UnavailableForLegalReasons,
zeroex::Error::OrderNotSupported => Self::OrderNotSupported,
_ => Self::Other(Box::new(err)),
}
}
Expand Down
10 changes: 7 additions & 3 deletions src/infra/dex/okx/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,17 @@ pub struct SwapRequest {
pub struct Slippage(BigDecimal);

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

Some(Self {
Ok(Self {
from_token_address: order.sell.0,
to_token_address: order.buy.0,
amount: order.amount.get(),
Expand Down
7 changes: 1 addition & 6 deletions src/infra/dex/okx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,7 @@ impl Okx {
slippage: &dex::Slippage,
) -> Result<(dto::SwapResponse, eth::ContractAddress), Error> {
let swap_request_future = async {
let swap_request = self
.defaults
.clone()
.with_domain(order, slippage)
.ok_or(Error::OrderNotSupported)?;

let swap_request = self.defaults.clone().try_with_domain(order, slippage)?;
self.send_get_request("swap", &swap_request).await
};

Expand Down
10 changes: 7 additions & 3 deletions src/infra/dex/oneinch/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,14 @@ pub struct Query {
}

impl Query {
pub fn with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Option<Self> {
pub fn try_with_domain(
self,
order: &dex::Order,
slippage: &dex::Slippage,
) -> Result<Self, super::Error> {
// Buy orders are not supported on 1Inch
if order.side == order::Side::Buy {
return None;
return Err(super::Error::OrderNotSupported);
};

// 1Inch checks `origin` for legal reasons.
Expand All @@ -107,7 +111,7 @@ impl Query {
false => order.owner,
};

Some(Self {
Ok(Self {
from_token_address: order.sell.0,
to_token_address: order.buy.0,
amount: order.amount.get(),
Expand Down
6 changes: 1 addition & 5 deletions src/infra/dex/oneinch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,7 @@ impl OneInch {
order: &dex::Order,
slippage: &dex::Slippage,
) -> Result<dex::Swap, Error> {
let query = self
.defaults
.clone()
.with_domain(order, slippage)
.ok_or(Error::OrderNotSupported)?;
let query = self.defaults.clone().try_with_domain(order, slippage)?;
let swap = {
// Set up a tracing span to make debugging of API requests easier.
// Historically, debugging API requests to external DEXs was a bit
Expand Down
135 changes: 62 additions & 73 deletions src/infra/dex/zeroex/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use {
domain::{dex, order},
util::serialize,
},
bigdecimal::BigDecimal,
ethereum_types::{H160, U256},
serde::{Deserialize, Serialize},
serde_with::serde_as,
Expand All @@ -20,78 +19,60 @@ use {
#[derive(Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Query {
/// Contract address of a token to sell.
pub sell_token: H160,
/// The chain ID of the network the query is prepared for.
pub chain_id: u64,

/// 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<serialize::U256>")]
pub sell_amount: Option<U256>,
/// Contract address of a token to sell.
pub sell_token: H160,

/// Amount of a token to sell, set in atoms.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<serialize::U256>")]
pub buy_amount: Option<U256>,
#[serde_as(as = "serialize::U256")]
pub sell_amount: U256,

/// Limit of price slippage you are willing to accept.
/// The address which will fill the quote.
pub taker: H160,

/// Limit of price slippage you are willing to accept. Values are in basis
/// points [ 0 .. 10000 ].
#[serde(skip_serializing_if = "Option::is_none")]
pub slippage_percentage: Option<Slippage>,
pub slippage_bps: Option<Slippage>,

/// The target gas price for the swap transaction.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<serialize::U256>")]
pub gas_price: Option<U256>,

/// The address which will fill the quote.
#[serde(skip_serializing_if = "Option::is_none")]
pub taker_address: Option<H160>,

/// List of sources to exclude.
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde_as(as = "serialize::CommaSeparated")]
pub excluded_sources: Vec<String>,

/// 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.
///
/// <https://docs.0x.org/market-makers/docs/introduction>
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);
pub struct Slippage(u16);

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),
pub fn try_with_domain(
self,
order: &dex::Order,
slippage: &dex::Slippage,
) -> Result<Self, super::Error> {
// Buy orders are not supported on 0x
if order.side == order::Side::Buy {
return Err(super::Error::OrderNotSupported);
};

Self {
Ok(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())),
sell_amount: order.amount.get(),
slippage_bps: slippage.as_bps().map(Slippage),
..self
}
})
}
}

Expand All @@ -100,6 +81,25 @@ impl Query {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Quote {
/// 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 transaction details for the swap.
pub transaction: QuoteTransaction,

/// Issues containing the allowance data
pub issues: Issues,
}

#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuoteTransaction {
/// The address of the contract to call in order to execute the swap.
pub to: H160,

Expand All @@ -109,38 +109,27 @@ pub struct Quote {

/// 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,
#[serde_as(as = "Option<serialize::U256>")]
pub gas: Option<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<H160>,
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Issues {
/// Allowance data for the sell token.
pub allowance: Option<Allowance>,
}

/// 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<Option<H160>, D::Error>
where
D: Deserializer<'de>,
{
let value = H160::deserialize(deserializer)?;
Ok((!value.is_zero()).then_some(value))
}
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Allowance {
/// The taker's current allowance of the spender
#[serde_as(as = "serialize::U256")]
pub actual: U256,
/// The address to set the allowance on
pub spender: H160,
}

#[derive(Deserialize)]
Expand Down
Loading
Loading