renegade_sdk/external_match_client/
client.rs

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