Skip to content

Commit

Permalink
funds-manager: execution-client: Add quote and price helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Aug 8, 2024
1 parent 29c40c8 commit 64567fe
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 0 deletions.
41 changes: 41 additions & 0 deletions funds-manager/funds-manager-server/src/execution_client/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Error types for the execution client

use std::fmt::Display;

/// An error returned by the execution client
#[derive(Debug, Clone)]
pub enum ExecutionClientError {
/// An error returned by the execution client
Http(String),
/// An error parsing a value
Parse(String),
}

impl ExecutionClientError {
/// Create a new http error
pub fn http<T: ToString>(e: T) -> Self {

Check failure on line 16 in funds-manager/funds-manager-server/src/execution_client/error.rs

View workflow job for this annotation

GitHub Actions / clippy

this argument is passed by value, but not consumed in the function body

error: this argument is passed by value, but not consumed in the function body --> funds-manager/funds-manager-server/src/execution_client/error.rs:16:33 | 16 | pub fn http<T: ToString>(e: T) -> Self { | ^ help: consider taking a reference instead: `&T` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value = note: requested on the command line with `-D clippy::needless-pass-by-value`
ExecutionClientError::Http(e.to_string())
}

/// Create a new parse error
pub fn parse<T: ToString>(e: T) -> Self {

Check failure on line 21 in funds-manager/funds-manager-server/src/execution_client/error.rs

View workflow job for this annotation

GitHub Actions / clippy

this argument is passed by value, but not consumed in the function body

error: this argument is passed by value, but not consumed in the function body --> funds-manager/funds-manager-server/src/execution_client/error.rs:21:34 | 21 | pub fn parse<T: ToString>(e: T) -> Self { | ^ help: consider taking a reference instead: `&T` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
ExecutionClientError::Parse(e.to_string())
}
}

impl Display for ExecutionClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
ExecutionClientError::Http(e) => format!("HTTP error: {e}"),
ExecutionClientError::Parse(e) => format!("Parse error: {e}"),
};

write!(f, "{}", msg)
}
}

impl From<reqwest::Error> for ExecutionClientError {
fn from(e: reqwest::Error) -> Self {
ExecutionClientError::http(e)
}
}
64 changes: 64 additions & 0 deletions funds-manager/funds-manager-server/src/execution_client/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Client for interacting with execution venues, currently this is 0x Matcha
pub mod error;
pub mod quotes;

use std::sync::Arc;

use reqwest::{Client, Url};
use serde::Deserialize;

use self::error::ExecutionClientError;

/// The 0x api key header
const API_KEY_HEADER: &str = "0x-api-key";

/// The client for interacting with the execution venue
#[derive(Clone)]
pub struct ExecutionClient {
/// The API key to use for requests
api_key: String,
/// The base URL for the execution client
base_url: String,
/// The underlying HTTP client
http_client: Arc<Client>,
}

impl ExecutionClient {
/// Create a new client
pub fn new(api_key: String, base_url: String) -> Self {
Self { api_key, base_url, http_client: Arc::new(Client::new()) }
}

/// Get a full URL for a given endpoint
fn build_url(
&self,
endpoint: &str,
params: &[(&str, &str)],
) -> Result<Url, ExecutionClientError> {
let endpoint = if !endpoint.starts_with('/') {
format!("{}/{}", self.base_url, endpoint)
} else {
format!("{}{}", self.base_url, endpoint)
};
println!("endpoint: {}", endpoint);

Url::parse_with_params(&endpoint, params).map_err(ExecutionClientError::parse)
}

/// Send a get request to the execution venue
async fn get_execution<T: for<'de> Deserialize<'de>>(
&self,
endpoint: &str,
params: &[(&str, &str)],
) -> Result<T, ExecutionClientError> {
let url = self.build_url(endpoint, params)?;
self.http_client
.get(url)
.header(API_KEY_HEADER, &self.api_key)
.send()
.await?
.json::<T>()
.await
.map_err(ExecutionClientError::http)
}
}
81 changes: 81 additions & 0 deletions funds-manager/funds-manager-server/src/execution_client/quotes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! Client methods for fetching quotes and prices from the execution venue

use serde::Deserialize;

use super::{error::ExecutionClientError, ExecutionClient};

/// The price endpoint
const PRICE_ENDPOINT: &str = "swap/v1/price";
/// The quote endpoint
const QUOTE_ENDPOINT: &str = "swap/v1/quote";

/// The buy token url param
const BUY_TOKEN: &str = "buyToken";
/// The sell token url param
const SELL_TOKEN: &str = "sellToken";
/// The sell amount url param
const SELL_AMOUNT: &str = "sellAmount";
/// The taker address url param
const TAKER_ADDRESS: &str = "takerAddress";

/// The price response
#[derive(Debug, Deserialize)]
pub struct ZeroXPriceResponse {
/// The price
pub price: String,
}

/// The subset of the quote response necessary for submitting a quote for
/// execution
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionQuote {
/// The price of the quote
pub price: String,
/// The submitting address
pub from: String,
/// The to address
pub to: String,
/// The calldata for the swap
pub data: String,
/// The value of the tx; should be zero
pub value: String,
/// The gas price used in the swap
pub gas_price: String,
}

impl ExecutionClient {
/// Fetch a price for an asset
pub async fn get_price(
&self,
buy_asset: &str,
sell_asset: &str,
amount: u128,
) -> Result<f64, ExecutionClientError> {
let amount_str = amount.to_string();
let params =
[(BUY_TOKEN, buy_asset), (SELL_TOKEN, sell_asset), (SELL_AMOUNT, amount_str.as_str())];

let resp: ZeroXPriceResponse = self.get_execution(PRICE_ENDPOINT, &params).await?;
resp.price.parse::<f64>().map_err(ExecutionClientError::parse)
}

/// Fetch a quote for an asset
pub async fn get_quote(
&self,
buy_asset: &str,
sell_asset: &str,
amount: u128,
recipient: &str,
) -> Result<ExecutionQuote, ExecutionClientError> {
let amount_str = amount.to_string();
let params = [
(BUY_TOKEN, buy_asset),
(SELL_TOKEN, sell_asset),
(SELL_AMOUNT, amount_str.as_str()),
(TAKER_ADDRESS, recipient),
];

self.get_execution(QUOTE_ENDPOINT, &params).await
}
}
1 change: 1 addition & 0 deletions funds-manager/funds-manager-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
pub mod custody_client;
pub mod db;
pub mod error;
pub mod execution_client;
pub mod fee_indexer;
pub mod handlers;
pub mod helpers;
Expand Down

0 comments on commit 64567fe

Please sign in to comment.