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
37 changes: 9 additions & 28 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 @@ -64,17 +47,15 @@ pub async fn load(path: &Path) -> super::Config {

// 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
128 changes: 56 additions & 72 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,55 @@ 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,

/// The address which will fill the quote.
pub taker: H160,

/// Limit of price slippage you are willing to accept.
#[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 with_domain(self, order: &dex::Order, slippage: &dex::Slippage) -> Option<Self> {
// Buy orders are not supported on 0x
if order.side == order::Side::Buy {
return None;
};

Self {
Some(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 +76,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 +104,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