renegade_sdk/external_match_client/
client.rs

1//! The client for requesting external matches
2
3use crate::auth::HmacKey;
4use reqwest::{
5    StatusCode,
6    header::{HeaderMap, HeaderValue},
7};
8
9use crate::{
10    ARBITRUM_ONE_RELAYER_BASE_URL, ARBITRUM_SEPOLIA_RELAYER_BASE_URL, AssembleQuoteOptionsV2,
11    BASE_MAINNET_RELAYER_BASE_URL, BASE_SEPOLIA_RELAYER_BASE_URL,
12    ETHEREUM_SEPOLIA_RELAYER_BASE_URL, ExternalMatchOptions, RequestQuoteOptions,
13    api_types::{
14        ASSEMBLE_MATCH_BUNDLE_ROUTE, AssemblyType, ExternalMatchResponseV2,
15        GET_MARKET_DEPTH_BY_MINT_ROUTE, GET_MARKETS_DEPTH_ROUTE, GET_MARKETS_ROUTE,
16        GetMarketDepthByMintResponse, GetMarketDepthsResponse, GetMarketsResponse,
17        exchange_metadata::ExchangeMetadataResponse,
18    },
19};
20
21#[allow(deprecated)]
22use crate::http::RelayerHttpClient;
23
24use super::{
25    api_types::{
26        ApiSignedQuoteV2, AssembleExternalMatchRequest, ExternalOrderV2, ExternalQuoteRequest,
27        ExternalQuoteResponse, GET_EXCHANGE_METADATA_ROUTE, SignedExternalQuoteV2,
28    },
29    error::ExternalMatchClientError,
30};
31
32// -------------
33// | Constants |
34// -------------
35
36/// The Renegade API key header
37pub const RENEGADE_API_KEY_HEADER: &str = "X-Renegade-Api-Key";
38
39/// The Arbitrum Sepolia auth server base URL
40const ARBITRUM_SEPOLIA_AUTH_BASE_URL: &str = "https://arbitrum-sepolia.v2.auth-server.renegade.fi";
41/// The Arbitrum One auth server base URL
42const ARBITRUM_ONE_AUTH_BASE_URL: &str = "https://arbitrum-one.v2.auth-server.renegade.fi";
43/// The Base Sepolia auth server base URL
44const BASE_SEPOLIA_AUTH_BASE_URL: &str = "https://base-sepolia.v2.auth-server.renegade.fi";
45/// The Base mainnet auth server base URL
46const BASE_MAINNET_AUTH_BASE_URL: &str = "https://base-mainnet.v2.auth-server.renegade.fi";
47/// The Ethereum Sepolia auth server base URL
48const ETHEREUM_SEPOLIA_AUTH_BASE_URL: &str = "https://ethereum-sepolia.v2.auth-server.renegade.fi";
49
50// ----------
51// | Client |
52// ----------
53
54/// A client for requesting external matches from the relayer
55#[derive(Clone, Debug)]
56pub struct ExternalMatchClient {
57    /// The api key for the external match client
58    pub(crate) api_key: String,
59    /// The HTTP client
60    pub(crate) auth_http_client: RelayerHttpClient,
61    /// The relayer HTTP client
62    ///
63    /// Separate from the auth client as they request different base URLs
64    pub(crate) relayer_http_client: RelayerHttpClient,
65}
66
67impl ExternalMatchClient {
68    /// Create a new client
69    pub fn new(
70        api_key: &str,
71        api_secret: &str,
72        auth_base_url: &str,
73        relayer_base_url: &str,
74    ) -> Result<Self, ExternalMatchClientError> {
75        let api_secret = HmacKey::from_base64_string(api_secret)
76            .map_err(|_| ExternalMatchClientError::InvalidApiSecret)?;
77
78        Ok(Self {
79            api_key: api_key.to_string(),
80            auth_http_client: RelayerHttpClient::new(auth_base_url.to_string(), api_secret),
81            relayer_http_client: RelayerHttpClient::new(relayer_base_url.to_string(), api_secret),
82        })
83    }
84
85    /// Create a new client with a custom HTTP client
86    pub fn new_with_client(
87        api_key: &str,
88        api_secret: &str,
89        auth_base_url: &str,
90        relayer_base_url: &str,
91        client: reqwest::Client,
92    ) -> Result<Self, ExternalMatchClientError> {
93        let api_secret = HmacKey::from_base64_string(api_secret)
94            .map_err(|_| ExternalMatchClientError::InvalidApiSecret)?;
95        let auth_http_client = RelayerHttpClient::new_with_client(
96            auth_base_url.to_string(),
97            api_secret,
98            client.clone(),
99        );
100        let relayer_http_client =
101            RelayerHttpClient::new_with_client(relayer_base_url.to_string(), api_secret, client);
102
103        Ok(Self { api_key: api_key.to_string(), auth_http_client, relayer_http_client })
104    }
105
106    /// Create a new client for the Ethereum Sepolia network
107    pub fn new_ethereum_sepolia_client(
108        api_key: &str,
109        api_secret: &str,
110    ) -> Result<Self, ExternalMatchClientError> {
111        Self::new(
112            api_key,
113            api_secret,
114            ETHEREUM_SEPOLIA_AUTH_BASE_URL,
115            ETHEREUM_SEPOLIA_RELAYER_BASE_URL,
116        )
117    }
118
119    /// Create a new client for the Ethereum Sepolia network with custom HTTP
120    /// client
121    pub fn new_ethereum_sepolia_with_client(
122        api_key: &str,
123        api_secret: &str,
124        client: reqwest::Client,
125    ) -> Result<Self, ExternalMatchClientError> {
126        Self::new_with_client(
127            api_key,
128            api_secret,
129            ETHEREUM_SEPOLIA_AUTH_BASE_URL,
130            ETHEREUM_SEPOLIA_RELAYER_BASE_URL,
131            client,
132        )
133    }
134
135    /// Create a new client for the Arbitrum Sepolia network
136    pub fn new_arbitrum_sepolia_client(
137        api_key: &str,
138        api_secret: &str,
139    ) -> Result<Self, ExternalMatchClientError> {
140        Self::new(
141            api_key,
142            api_secret,
143            ARBITRUM_SEPOLIA_AUTH_BASE_URL,
144            ARBITRUM_SEPOLIA_RELAYER_BASE_URL,
145        )
146    }
147
148    /// Create a new client for the Base Sepolia network
149    pub fn new_base_sepolia_client(
150        api_key: &str,
151        api_secret: &str,
152    ) -> Result<Self, ExternalMatchClientError> {
153        Self::new(api_key, api_secret, BASE_SEPOLIA_AUTH_BASE_URL, BASE_SEPOLIA_RELAYER_BASE_URL)
154    }
155
156    /// Create a new client for the Arbitrum One network
157    pub fn new_arbitrum_one_client(
158        api_key: &str,
159        api_secret: &str,
160    ) -> Result<Self, ExternalMatchClientError> {
161        Self::new(api_key, api_secret, ARBITRUM_ONE_AUTH_BASE_URL, ARBITRUM_ONE_RELAYER_BASE_URL)
162    }
163
164    /// Create a new client for the Arbitrum One network with custom HTTP client
165    pub fn new_arbitrum_one_with_client(
166        api_key: &str,
167        api_secret: &str,
168        client: reqwest::Client,
169    ) -> Result<Self, ExternalMatchClientError> {
170        Self::new_with_client(
171            api_key,
172            api_secret,
173            ARBITRUM_ONE_AUTH_BASE_URL,
174            ARBITRUM_ONE_RELAYER_BASE_URL,
175            client,
176        )
177    }
178
179    /// Create a new client for the Base mainnet network
180    pub fn new_base_mainnet_client(
181        api_key: &str,
182        api_secret: &str,
183    ) -> Result<Self, ExternalMatchClientError> {
184        Self::new(api_key, api_secret, BASE_MAINNET_AUTH_BASE_URL, BASE_MAINNET_RELAYER_BASE_URL)
185    }
186
187    /// Create a new client for the Base mainnet network with custom HTTP client
188    pub fn new_base_mainnet_with_client(
189        api_key: &str,
190        api_secret: &str,
191        client: reqwest::Client,
192    ) -> Result<Self, ExternalMatchClientError> {
193        Self::new_with_client(
194            api_key,
195            api_secret,
196            BASE_MAINNET_AUTH_BASE_URL,
197            BASE_MAINNET_RELAYER_BASE_URL,
198            client,
199        )
200    }
201
202    // ------------------
203    // | Markets Routes |
204    // ------------------
205
206    /// Get a list of tradable markets. Includes the tokens pair, current price,
207    /// and fee rates for each market.
208    pub async fn get_markets(&self) -> Result<GetMarketsResponse, ExternalMatchClientError> {
209        let path = GET_MARKETS_ROUTE;
210        let headers = self.get_headers()?;
211        let resp = self.auth_http_client.get_with_headers(path, headers).await?;
212
213        Ok(resp)
214    }
215
216    /// Get the market depth for the given token.
217    ///
218    /// The address is the address of the token
219    pub async fn get_market_depth(
220        &self,
221        address: &str,
222    ) -> Result<GetMarketDepthByMintResponse, ExternalMatchClientError> {
223        let path = GET_MARKET_DEPTH_BY_MINT_ROUTE.replace(":mint", address);
224        let headers = self.get_headers()?;
225        let resp = self.auth_http_client.get_with_headers(&path, headers).await?;
226
227        Ok(resp)
228    }
229
230    /// Get the market depths for all supported pairs
231    pub async fn get_market_depths_all_pairs(
232        &self,
233    ) -> Result<GetMarketDepthsResponse, ExternalMatchClientError> {
234        let path = GET_MARKETS_DEPTH_ROUTE;
235        let headers = self.get_headers()?;
236        let resp = self.auth_http_client.get_with_headers(path, headers).await?;
237
238        Ok(resp)
239    }
240
241    // -------------------------
242    // | External Match Routes |
243    // -------------------------
244
245    /// Request a quote for an external match (v2 API)
246    pub async fn request_quote_v2(
247        &self,
248        order: ExternalOrderV2,
249    ) -> Result<Option<SignedExternalQuoteV2>, ExternalMatchClientError> {
250        self.request_quote_with_options_v2(order, RequestQuoteOptions::default()).await
251    }
252
253    /// Request a quote for an external match, with options (v2 API)
254    pub async fn request_quote_with_options_v2(
255        &self,
256        order: ExternalOrderV2,
257        options: RequestQuoteOptions,
258    ) -> Result<Option<SignedExternalQuoteV2>, ExternalMatchClientError> {
259        let request = ExternalQuoteRequest { external_order: order };
260        let path = options.build_request_path();
261        let headers = self.get_headers()?;
262
263        let resp = self.auth_http_client.post_with_headers_raw(&path, request, headers).await?;
264        let quote_resp = Self::handle_optional_response::<ExternalQuoteResponse>(resp).await?;
265        Ok(quote_resp
266            .map(|r| SignedExternalQuoteV2::from_api_quote(r.signed_quote, r.gas_sponsorship_info)))
267    }
268
269    /// Assemble a quote into a match bundle, ready for settlement (v2 API)
270    pub async fn assemble_quote_v2(
271        &self,
272        quote: SignedExternalQuoteV2,
273    ) -> Result<Option<ExternalMatchResponseV2>, ExternalMatchClientError> {
274        self.assemble_quote_with_options_v2(quote, AssembleQuoteOptionsV2::default()).await
275    }
276
277    /// Assemble a quote into a match bundle, ready for settlement, with options
278    /// (v2 API)
279    pub async fn assemble_quote_with_options_v2(
280        &self,
281        quote: SignedExternalQuoteV2,
282        options: AssembleQuoteOptionsV2,
283    ) -> Result<Option<ExternalMatchResponseV2>, ExternalMatchClientError> {
284        let path = ASSEMBLE_MATCH_BUNDLE_ROUTE;
285
286        let signed_quote = ApiSignedQuoteV2::from(quote);
287        let order =
288            AssemblyType::QuotedOrder { signed_quote, updated_order: options.updated_order };
289
290        let request = AssembleExternalMatchRequest {
291            receiver_address: options.receiver_address,
292            do_gas_estimation: options.do_gas_estimation,
293            order,
294        };
295
296        let headers = self.get_headers()?;
297        let resp = self.auth_http_client.post_with_headers_raw(path, request, headers).await?;
298
299        let match_resp = Self::handle_optional_response::<ExternalMatchResponseV2>(resp).await?;
300        Ok(match_resp)
301    }
302
303    /// Request an external match (v2 API)
304    pub async fn request_external_match_v2(
305        &self,
306        order: ExternalOrderV2,
307    ) -> Result<Option<ExternalMatchResponseV2>, ExternalMatchClientError> {
308        self.request_external_match_with_options_v2(order, Default::default()).await
309    }
310
311    /// Request an external match and specify any options for the request (v2
312    /// API)
313    pub async fn request_external_match_with_options_v2(
314        &self,
315        order: ExternalOrderV2,
316        options: ExternalMatchOptions,
317    ) -> Result<Option<ExternalMatchResponseV2>, ExternalMatchClientError> {
318        let path = options.build_request_path();
319
320        let order = AssemblyType::DirectOrder { external_order: order };
321        let request = AssembleExternalMatchRequest {
322            receiver_address: options.receiver_address,
323            do_gas_estimation: options.do_gas_estimation,
324            order,
325        };
326
327        let headers = self.get_headers()?;
328        let resp =
329            self.auth_http_client.post_with_headers_raw(path.as_str(), request, headers).await?;
330
331        let match_resp = Self::handle_optional_response::<ExternalMatchResponseV2>(resp).await?;
332        Ok(match_resp)
333    }
334
335    // -------------------
336    // | Metadata Routes |
337    // -------------------
338
339    /// Get metadata about the Renegade exchange
340    pub async fn get_exchange_metadata(
341        &self,
342    ) -> Result<ExchangeMetadataResponse, ExternalMatchClientError> {
343        let path = GET_EXCHANGE_METADATA_ROUTE;
344        let headers = self.get_headers()?;
345        let resp = self.auth_http_client.get_with_headers(path, headers).await?;
346
347        Ok(resp)
348    }
349
350    // -----------
351    // | Helpers |
352    // -----------
353
354    /// Helper function to handle response that might be NO_CONTENT, OK with
355    /// json, or an error
356    pub(crate) async fn handle_optional_response<T>(
357        response: reqwest::Response,
358    ) -> Result<Option<T>, ExternalMatchClientError>
359    where
360        T: serde::de::DeserializeOwned,
361    {
362        if response.status() == StatusCode::NO_CONTENT {
363            Ok(None)
364        } else if response.status() == StatusCode::OK {
365            let resp = response.json::<T>().await?;
366            Ok(Some(resp))
367        } else {
368            let status = response.status();
369            let msg = response.text().await?;
370            Err(ExternalMatchClientError::http(status, msg))
371        }
372    }
373
374    /// Get a header map with the api key added
375    pub(crate) fn get_headers(&self) -> Result<HeaderMap, ExternalMatchClientError> {
376        let mut headers = HeaderMap::new();
377        let api_key = HeaderValue::from_str(&self.api_key)
378            .map_err(|_| ExternalMatchClientError::InvalidApiKey)?;
379        headers.insert(RENEGADE_API_KEY_HEADER, api_key);
380
381        Ok(headers)
382    }
383}