From 64567fef237dc5edfe25a43f9d23427b1ec2a0b0 Mon Sep 17 00:00:00 2001 From: Joey Kraut Date: Thu, 8 Aug 2024 14:01:59 -0700 Subject: [PATCH] funds-manager: execution-client: Add quote and price helpers --- .../src/execution_client/error.rs | 41 ++++++++++ .../src/execution_client/mod.rs | 64 +++++++++++++++ .../src/execution_client/quotes.rs | 81 +++++++++++++++++++ .../funds-manager-server/src/main.rs | 1 + 4 files changed, 187 insertions(+) create mode 100644 funds-manager/funds-manager-server/src/execution_client/error.rs create mode 100644 funds-manager/funds-manager-server/src/execution_client/mod.rs create mode 100644 funds-manager/funds-manager-server/src/execution_client/quotes.rs diff --git a/funds-manager/funds-manager-server/src/execution_client/error.rs b/funds-manager/funds-manager-server/src/execution_client/error.rs new file mode 100644 index 0000000..c2af0f6 --- /dev/null +++ b/funds-manager/funds-manager-server/src/execution_client/error.rs @@ -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(e: T) -> Self { + ExecutionClientError::Http(e.to_string()) + } + + /// Create a new parse error + pub fn parse(e: T) -> Self { + 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 for ExecutionClientError { + fn from(e: reqwest::Error) -> Self { + ExecutionClientError::http(e) + } +} diff --git a/funds-manager/funds-manager-server/src/execution_client/mod.rs b/funds-manager/funds-manager-server/src/execution_client/mod.rs new file mode 100644 index 0000000..00e77b1 --- /dev/null +++ b/funds-manager/funds-manager-server/src/execution_client/mod.rs @@ -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, +} + +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 { + 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 Deserialize<'de>>( + &self, + endpoint: &str, + params: &[(&str, &str)], + ) -> Result { + let url = self.build_url(endpoint, params)?; + self.http_client + .get(url) + .header(API_KEY_HEADER, &self.api_key) + .send() + .await? + .json::() + .await + .map_err(ExecutionClientError::http) + } +} diff --git a/funds-manager/funds-manager-server/src/execution_client/quotes.rs b/funds-manager/funds-manager-server/src/execution_client/quotes.rs new file mode 100644 index 0000000..92c55f9 --- /dev/null +++ b/funds-manager/funds-manager-server/src/execution_client/quotes.rs @@ -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 { + 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, ¶ms).await?; + resp.price.parse::().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 { + 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, ¶ms).await + } +} diff --git a/funds-manager/funds-manager-server/src/main.rs b/funds-manager/funds-manager-server/src/main.rs index 4b63f1b..b71749b 100644 --- a/funds-manager/funds-manager-server/src/main.rs +++ b/funds-manager/funds-manager-server/src/main.rs @@ -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;