Skip to content

Commit

Permalink
ParaSwap /swap API (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
squadgazzz authored Jun 7, 2024
1 parent 48d5202 commit 991ff20
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 485 deletions.
10 changes: 8 additions & 2 deletions src/domain/dex/slippage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use {
domain::{auction, eth},
util::conv,
},
bigdecimal::BigDecimal,
bigdecimal::{BigDecimal, ToPrimitive},
ethereum_types::U256,
num::{BigUint, Integer, One, Zero},
std::cmp,
Expand Down Expand Up @@ -53,7 +53,7 @@ impl Limits {
/// Relative slippage has saturating semantics. I.e. if adding slippage to a
/// token amount would overflow a `U256`, then `U256::max_value()` is returned
/// instead.
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Slippage(BigDecimal);

impl Slippage {
Expand Down Expand Up @@ -90,6 +90,12 @@ impl Slippage {
&self.0
}

/// Converts the relative slippage factor into basis points.
pub fn as_bps(&self) -> Option<u16> {
let basis_points = self.as_factor() * BigDecimal::from(10000);
basis_points.to_u16()
}

/// Rounds a relative slippage value to the specified decimal precision.
pub fn round(&self, arg: u32) -> Self {
Self(self.0.round(arg as _))
Expand Down
148 changes: 31 additions & 117 deletions src/infra/dex/paraswap/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@ use {
util::serialize,
},
ethereum_types::{H160, U256},
serde::{de, Deserialize, Deserializer, Serialize},
serde::{Deserialize, Serialize},
serde_with::serde_as,
};

/// ParaSwap query parameters for the `/prices` endpoint.
/// ParaSwap query parameters for the `/swap` endpoint.
///
/// See [API](https://developers.paraswap.network/api/get-rate-for-a-token-pair)
/// documentation for more detailed information on each parameter.
/// This API is not public, so no docs are available.
#[serde_as]
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceQuery {
pub struct SwapQuery {
/// Source token address.
pub src_token: H160,

Expand Down Expand Up @@ -52,13 +51,20 @@ pub struct PriceQuery {

/// The maximum price impact accepted (in percentage, 0-100)
pub max_impact: u8,

/// The address of the signer.
pub user_address: H160,

/// A relative slippage tolerance denominated in bps.
pub slippage: u16,
}

impl PriceQuery {
impl SwapQuery {
pub fn new(
config: &super::Config,
order: &dex::Order,
tokens: &auction::Tokens,
slippage: &dex::Slippage,
) -> Result<Self, super::Error> {
Ok(Self {
src_token: order.sell.0,
Expand All @@ -78,74 +84,10 @@ impl PriceQuery {
network: config.chain_id.network_id().to_string(),
partner: config.partner.clone(),
max_impact: 100,
})
}
}

/// ParaSwap API body parameters for the `/transactions` endpoint.
///
/// See [API](https://developers.paraswap.network/api/build-parameters-for-transaction)
/// documentation for more detailed information on each parameter.
#[serde_as]
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionBody {
/// Source token address.
pub src_token: H160,

/// Destination token address.
pub dest_token: H160,

// Source amount.
#[serde_as(as = "serialize::U256")]
pub src_amount: U256,

// Destination amount.
#[serde_as(as = "serialize::U256")]
pub dest_amount: U256,

/// The decimals of the source token.
pub src_decimals: u8,

/// The decimals of the destination token.
pub dest_decimals: u8,

/// Price route from `/prices` endpoint response (without any change).
pub price_route: serde_json::Value,

/// The address of the signer.
pub user_address: H160,

/// The partner name.
pub partner: String,
}

impl TransactionBody {
pub fn new(
price: &Price,
config: &super::Config,
order: &dex::Order,
tokens: &auction::Tokens,
slippage: &dex::Slippage,
) -> Result<Self, super::Error> {
let (src_amount, dest_amount) = match order.side {
order::Side::Sell => (price.src_amount, slippage.sub(price.dest_amount)),
order::Side::Buy => (slippage.add(price.src_amount), price.dest_amount),
};
Ok(Self {
src_token: order.sell.0,
dest_token: order.buy.0,
src_decimals: tokens
.decimals(&order.sell)
.ok_or(super::Error::MissingDecimals)?,
dest_decimals: tokens
.decimals(&order.buy)
.ok_or(super::Error::MissingDecimals)?,
src_amount,
dest_amount,
price_route: price.price_route.clone(),
user_address: config.address,
partner: config.partner.clone(),
slippage: slippage
.as_bps()
.ok_or(super::Error::InvalidSlippage(slippage.clone()))?,
})
}
}
Expand All @@ -157,64 +99,36 @@ pub enum Side {
Buy,
}

/// A ParaSwap API price response.
pub struct Price {
/// The price route. This should be passed on to the `/transactions`
/// endpoint.
pub price_route: serde_json::Value,
/// A ParaSwap swap API response.
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Swap {
pub price_route: PriceRoute,
pub tx_params: TxParams,
}

#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceRoute {
/// The source token amount in atoms.
#[serde_as(as = "serialize::U256")]
pub src_amount: U256,
/// The destination token amount in atoms.
#[serde_as(as = "serialize::U256")]
pub dest_amount: U256,
/// The (very) approximate gas cost for the swap.
#[serde_as(as = "serialize::U256")]
pub gas_cost: U256,
/// The token transfer proxy that requires an allowance.
pub token_transfer_proxy: H160,
}

impl<'de> Deserialize<'de> for Price {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Raw {
price_route: serde_json::Value,
}

#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Parsed {
#[serde_as(as = "serialize::U256")]
src_amount: U256,
#[serde_as(as = "serialize::U256")]
dest_amount: U256,
#[serde_as(as = "serialize::U256")]
gas_cost: U256,
token_transfer_proxy: H160,
}

let Raw { price_route } = Raw::deserialize(deserializer)?;
let parsed =
serde_json::from_value::<Parsed>(price_route.clone()).map_err(de::Error::custom)?;

Ok(Self {
price_route,
src_amount: parsed.src_amount,
dest_amount: parsed.dest_amount,
gas_cost: parsed.gas_cost,
token_transfer_proxy: parsed.token_transfer_proxy,
})
}
}

#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub struct TxParams {
pub from: H160,
pub to: H160,

Expand Down
63 changes: 17 additions & 46 deletions src/infra/dex/paraswap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,71 +62,40 @@ impl ParaSwap {
}
}

/// Make a request to the `/swap` endpoint.
pub async fn swap(
&self,
order: &dex::Order,
slippage: &dex::Slippage,
tokens: &auction::Tokens,
) -> Result<dex::Swap, Error> {
let price = self.price(order, tokens).await?;
let transaction = self.transaction(&price, order, tokens, slippage).await?;
let query = dto::SwapQuery::new(&self.config, order, tokens, slippage)?;
let swap = util::http::roundtrip!(
<dto::Swap, dto::Error>;
self.client.request(reqwest::Method::GET, util::url::join(&self.config.endpoint, "swap"))
.query(&query)
)
.await?;
Ok(dex::Swap {
call: dex::Call {
to: eth::ContractAddress(transaction.to),
calldata: transaction.data,
to: eth::ContractAddress(swap.tx_params.to),
calldata: swap.tx_params.data,
},
input: eth::Asset {
token: order.sell,
amount: price.src_amount,
amount: swap.price_route.src_amount,
},
output: eth::Asset {
token: order.buy,
amount: price.dest_amount,
amount: swap.price_route.dest_amount,
},
allowance: dex::Allowance {
spender: eth::ContractAddress(price.token_transfer_proxy),
amount: dex::Amount::new(price.src_amount),
spender: eth::ContractAddress(swap.price_route.token_transfer_proxy),
amount: dex::Amount::new(swap.price_route.src_amount),
},
gas: eth::Gas(price.gas_cost),
gas: eth::Gas(swap.price_route.gas_cost),
})
}

/// Make a request to the `/prices` endpoint.
async fn price(
&self,
order: &dex::Order,
tokens: &auction::Tokens,
) -> Result<dto::Price, Error> {
let price = util::http::roundtrip!(
<dto::Price, dto::Error>;
self.client.request(reqwest::Method::GET, util::url::join(&self.config.endpoint, "prices"))
.query(&dto::PriceQuery::new(&self.config, order, tokens)?)
)
.await?;
Ok(price)
}

/// Make a request to the `/transactions` endpoint.
async fn transaction(
&self,
price: &dto::Price,
order: &dex::Order,
tokens: &auction::Tokens,
slippage: &dex::Slippage,
) -> Result<dto::Transaction, Error> {
let body = dto::TransactionBody::new(price, &self.config, order, tokens, slippage)?;
let transaction = util::http::roundtrip!(
<dto::Transaction, dto::Error>;
self.client
.request(reqwest::Method::POST, util::url::join(
&self.config.endpoint,
&format!("transactions/{}?ignoreChecks=true", self.config.chain_id.network_id())
))
.json(&body)
)
.await?;
Ok(transaction)
}
}

#[derive(Debug, thiserror::Error)]
Expand All @@ -141,6 +110,8 @@ pub enum Error {
Api(String),
#[error(transparent)]
Http(util::http::Error),
#[error("unable to convert slippage to bps: {0:?}")]
InvalidSlippage(dex::Slippage),
}

impl From<util::http::RoundtripError<dto::Error>> for Error {
Expand Down
Loading

0 comments on commit 991ff20

Please sign in to comment.