use reqwest::{
header::{HeaderMap, HeaderValue},
StatusCode,
};
use url::form_urlencoded;
use crate::{
api_types::{
ASSEMBLE_EXTERNAL_MATCH_MALLEABLE_ROUTE, ASSEMBLE_EXTERNAL_MATCH_ROUTE,
REQUEST_EXTERNAL_MATCH_ROUTE,
},
http::RelayerHttpClient,
util::HmacKey,
GAS_REFUND_NATIVE_ETH_QUERY_PARAM,
};
use super::{
api_types::{
ApiSignedQuote, AssembleExternalMatchRequest, ExternalMatchRequest, ExternalMatchResponse,
ExternalOrder, ExternalQuoteRequest, ExternalQuoteResponse, GetSupportedTokensResponse,
MalleableExternalMatchResponse, SignedExternalQuote, GET_SUPPORTED_TOKENS_ROUTE,
REQUEST_EXTERNAL_QUOTE_ROUTE,
},
error::ExternalMatchClientError,
GAS_REFUND_ADDRESS_QUERY_PARAM, GAS_SPONSORSHIP_QUERY_PARAM,
};
pub const RENEGADE_API_KEY_HEADER: &str = "X-Renegade-Api-Key";
const SEPOLIA_AUTH_BASE_URL: &str = "https://testnet.auth-server.renegade.fi";
const MAINNET_AUTH_BASE_URL: &str = "https://mainnet.auth-server.renegade.fi";
const SEPOLIA_RELAYER_BASE_URL: &str = "https://testnet.cluster0.renegade.fi";
const MAINNET_RELAYER_BASE_URL: &str = "https://mainnet.cluster0.renegade.fi";
#[derive(Clone, Default)]
pub struct RequestQuoteOptions {
pub disable_gas_sponsorship: bool,
pub gas_refund_address: Option<String>,
pub refund_native_eth: bool,
}
impl RequestQuoteOptions {
pub fn new() -> Self {
Default::default()
}
pub fn disable_gas_sponsorship(mut self) -> Self {
self.disable_gas_sponsorship = true;
self
}
pub fn with_gas_refund_address(mut self, gas_refund_address: String) -> Self {
self.gas_refund_address = Some(gas_refund_address);
self
}
pub fn with_refund_native_eth(mut self) -> Self {
self.refund_native_eth = true;
self
}
pub(crate) fn build_request_path(&self) -> String {
let mut query = form_urlencoded::Serializer::new(String::new());
query.append_pair(GAS_SPONSORSHIP_QUERY_PARAM, &self.disable_gas_sponsorship.to_string());
query.append_pair(GAS_REFUND_NATIVE_ETH_QUERY_PARAM, &self.refund_native_eth.to_string());
if let Some(addr) = &self.gas_refund_address {
query.append_pair(GAS_REFUND_ADDRESS_QUERY_PARAM, addr);
}
format!("{}?{}", REQUEST_EXTERNAL_QUOTE_ROUTE, query.finish())
}
}
#[deprecated(
since = "0.1.0",
note = "This endpoint will soon be removed, use `request_quote` and `assemble_quote` instead"
)]
#[derive(Clone, Default)]
pub struct ExternalMatchOptions {
pub do_gas_estimation: bool,
pub sponsor_gas: bool,
pub gas_refund_address: Option<String>,
pub receiver_address: Option<String>,
}
#[allow(deprecated)]
impl ExternalMatchOptions {
pub fn new() -> Self {
Default::default()
}
pub fn with_gas_estimation(mut self, do_gas_estimation: bool) -> Self {
self.do_gas_estimation = do_gas_estimation;
self
}
pub fn with_receiver_address(mut self, receiver_address: String) -> Self {
self.receiver_address = Some(receiver_address);
self
}
pub fn request_gas_sponsorship(mut self) -> Self {
self.sponsor_gas = true;
self
}
pub fn with_gas_refund_address(mut self, gas_refund_address: String) -> Self {
self.gas_refund_address = Some(gas_refund_address);
self
}
pub(crate) fn build_request_path(&self) -> String {
let mut query = form_urlencoded::Serializer::new(String::new());
query.append_pair(GAS_SPONSORSHIP_QUERY_PARAM, &(!self.sponsor_gas).to_string());
if let Some(addr) = &self.gas_refund_address {
query.append_pair(GAS_REFUND_ADDRESS_QUERY_PARAM, addr);
}
format!("{}?{}", REQUEST_EXTERNAL_MATCH_ROUTE, query.finish())
}
}
#[derive(Clone, Default)]
pub struct AssembleQuoteOptions {
pub do_gas_estimation: bool,
pub allow_shared: bool,
#[deprecated(
since = "0.1.0",
note = "This option will soon be removed, request gas sponsorship when requesting a quote instead"
)]
pub sponsor_gas: bool,
#[deprecated(
since = "0.1.0",
note = "This option will soon be removed, request gas sponsorship when requesting a quote instead"
)]
pub gas_refund_address: Option<String>,
pub receiver_address: Option<String>,
pub updated_order: Option<ExternalOrder>,
}
impl AssembleQuoteOptions {
pub fn new() -> Self {
Default::default()
}
pub fn with_gas_estimation(mut self, do_gas_estimation: bool) -> Self {
self.do_gas_estimation = do_gas_estimation;
self
}
pub fn with_allow_shared(mut self, allow_shared: bool) -> Self {
self.allow_shared = allow_shared;
self
}
#[deprecated(
since = "0.1.0",
note = "This option will soon be removed, request gas sponsorship when requesting a quote instead"
)]
#[allow(deprecated)]
pub fn request_gas_sponsorship(mut self) -> Self {
self.sponsor_gas = true;
self
}
#[deprecated(
since = "0.1.0",
note = "This option will soon be removed, request gas sponsorship when requesting a quote instead"
)]
#[allow(deprecated)]
pub fn with_gas_refund_address(mut self, gas_refund_address: String) -> Self {
self.gas_refund_address = Some(gas_refund_address);
self
}
pub fn with_receiver_address(mut self, receiver_address: String) -> Self {
self.receiver_address = Some(receiver_address);
self
}
pub fn with_updated_order(mut self, updated_order: ExternalOrder) -> Self {
self.updated_order = Some(updated_order);
self
}
#[allow(deprecated)]
pub(crate) fn build_request_path(&self) -> String {
let mut query = form_urlencoded::Serializer::new(String::new());
if self.sponsor_gas {
query.append_pair(GAS_SPONSORSHIP_QUERY_PARAM, &(!self.sponsor_gas).to_string());
}
if let Some(addr) = &self.gas_refund_address {
query.append_pair(GAS_REFUND_ADDRESS_QUERY_PARAM, addr);
}
let query_str = query.finish();
if query_str.is_empty() {
return ASSEMBLE_EXTERNAL_MATCH_ROUTE.to_string();
}
format!("{}?{}", ASSEMBLE_EXTERNAL_MATCH_ROUTE, query_str)
}
}
#[derive(Clone)]
pub struct ExternalMatchClient {
api_key: String,
auth_http_client: RelayerHttpClient,
relayer_http_client: RelayerHttpClient,
}
impl ExternalMatchClient {
pub fn new(
api_key: &str,
api_secret: &str,
auth_base_url: &str,
relayer_base_url: &str,
) -> Result<Self, ExternalMatchClientError> {
let api_secret = HmacKey::from_base64_string(api_secret)
.map_err(|_| ExternalMatchClientError::InvalidApiSecret)?;
Ok(Self {
api_key: api_key.to_string(),
auth_http_client: RelayerHttpClient::new(auth_base_url.to_string(), api_secret),
relayer_http_client: RelayerHttpClient::new(relayer_base_url.to_string(), api_secret),
})
}
pub fn new_sepolia_client(
api_key: &str,
api_secret: &str,
) -> Result<Self, ExternalMatchClientError> {
Self::new(api_key, api_secret, SEPOLIA_AUTH_BASE_URL, SEPOLIA_RELAYER_BASE_URL)
}
pub fn new_mainnet_client(
api_key: &str,
api_secret: &str,
) -> Result<Self, ExternalMatchClientError> {
Self::new(api_key, api_secret, MAINNET_AUTH_BASE_URL, MAINNET_RELAYER_BASE_URL)
}
pub async fn get_supported_tokens(
&self,
) -> Result<GetSupportedTokensResponse, ExternalMatchClientError> {
let path = GET_SUPPORTED_TOKENS_ROUTE;
let resp = self.relayer_http_client.get(path).await?;
Ok(resp)
}
pub async fn request_quote(
&self,
order: ExternalOrder,
) -> Result<Option<SignedExternalQuote>, ExternalMatchClientError> {
self.request_quote_with_options(order, RequestQuoteOptions::default()).await
}
pub async fn request_quote_with_options(
&self,
order: ExternalOrder,
options: RequestQuoteOptions,
) -> Result<Option<SignedExternalQuote>, ExternalMatchClientError> {
let request = ExternalQuoteRequest { external_order: order };
let path = options.build_request_path();
let headers = self.get_headers()?;
let resp = self.auth_http_client.post_with_headers_raw(&path, request, headers).await?;
let quote_resp = Self::handle_optional_response::<ExternalQuoteResponse>(resp).await?;
Ok(quote_resp.map(|r| {
let ApiSignedQuote { quote, signature } = r.signed_quote;
SignedExternalQuote { quote, signature, gas_sponsorship_info: r.gas_sponsorship_info }
}))
}
pub async fn assemble_quote(
&self,
quote: SignedExternalQuote,
) -> Result<Option<ExternalMatchResponse>, ExternalMatchClientError> {
self.assemble_quote_with_options(quote, AssembleQuoteOptions::default()).await
}
pub async fn assemble_quote_with_options(
&self,
quote: SignedExternalQuote,
options: AssembleQuoteOptions,
) -> Result<Option<ExternalMatchResponse>, ExternalMatchClientError> {
let path = options.build_request_path();
let signed_quote = ApiSignedQuote { quote: quote.quote, signature: quote.signature };
let request = AssembleExternalMatchRequest {
signed_quote,
receiver_address: options.receiver_address,
do_gas_estimation: options.do_gas_estimation,
allow_shared: options.allow_shared,
updated_order: options.updated_order,
};
let headers = self.get_headers()?;
let resp =
self.auth_http_client.post_with_headers_raw(path.as_str(), request, headers).await?;
let match_resp = Self::handle_optional_response::<ExternalMatchResponse>(resp).await?;
Ok(match_resp)
}
pub async fn assemble_malleable_quote(
&self,
quote: SignedExternalQuote,
) -> Result<Option<MalleableExternalMatchResponse>, ExternalMatchClientError> {
self.assemble_malleable_quote_with_options(quote, AssembleQuoteOptions::default()).await
}
pub async fn assemble_malleable_quote_with_options(
&self,
quote: SignedExternalQuote,
options: AssembleQuoteOptions,
) -> Result<Option<MalleableExternalMatchResponse>, ExternalMatchClientError> {
let path = ASSEMBLE_EXTERNAL_MATCH_MALLEABLE_ROUTE;
let signed_quote = ApiSignedQuote { quote: quote.quote, signature: quote.signature };
let request = AssembleExternalMatchRequest {
signed_quote,
receiver_address: options.receiver_address.clone(),
do_gas_estimation: options.do_gas_estimation,
allow_shared: options.allow_shared,
updated_order: options.updated_order.clone(),
};
let headers = self.get_headers()?;
let resp = self.auth_http_client.post_with_headers_raw(path, request, headers).await?;
let match_resp =
Self::handle_optional_response::<MalleableExternalMatchResponse>(resp).await?;
Ok(match_resp)
}
#[deprecated(
since = "0.1.0",
note = "This endpoint will soon be removed, use `request_quote` and `assemble_quote` instead"
)]
#[allow(deprecated)]
pub async fn request_external_match(
&self,
order: ExternalOrder,
) -> Result<Option<ExternalMatchResponse>, ExternalMatchClientError> {
self.request_external_match_with_options(order, Default::default()).await
}
#[deprecated(
since = "0.1.0",
note = "This endpoint will soon be removed, use `request_quote` and `assemble_quote` instead"
)]
#[allow(deprecated)]
pub async fn request_external_match_with_options(
&self,
order: ExternalOrder,
options: ExternalMatchOptions,
) -> Result<Option<ExternalMatchResponse>, ExternalMatchClientError> {
let path = options.build_request_path();
let do_gas_estimation = options.do_gas_estimation;
let request = ExternalMatchRequest {
external_order: order,
do_gas_estimation,
receiver_address: options.receiver_address,
};
let headers = self.get_headers()?;
let resp =
self.auth_http_client.post_with_headers_raw(path.as_str(), request, headers).await?;
let match_resp = Self::handle_optional_response::<ExternalMatchResponse>(resp).await?;
Ok(match_resp)
}
async fn handle_optional_response<T>(
response: reqwest::Response,
) -> Result<Option<T>, ExternalMatchClientError>
where
T: serde::de::DeserializeOwned,
{
if response.status() == StatusCode::NO_CONTENT {
Ok(None)
} else if response.status() == StatusCode::OK {
let resp = response.json::<T>().await?;
Ok(Some(resp))
} else {
let status = response.status();
let msg = response.text().await?;
Err(ExternalMatchClientError::http(status, msg))
}
}
fn get_headers(&self) -> Result<HeaderMap, ExternalMatchClientError> {
let mut headers = HeaderMap::new();
let api_key = HeaderValue::from_str(&self.api_key)
.map_err(|_| ExternalMatchClientError::InvalidApiKey)?;
headers.insert(RENEGADE_API_KEY_HEADER, api_key);
Ok(headers)
}
}