1use 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
31pub const RENEGADE_API_KEY_HEADER: &str = "X-Renegade-Api-Key";
37
38const ARBITRUM_SEPOLIA_AUTH_BASE_URL: &str = "https://arbitrum-sepolia.auth-server.renegade.fi";
40const ARBITRUM_ONE_AUTH_BASE_URL: &str = "https://arbitrum-one.auth-server.renegade.fi";
42const BASE_SEPOLIA_AUTH_BASE_URL: &str = "https://base-sepolia.auth-server.renegade.fi";
44const BASE_MAINNET_AUTH_BASE_URL: &str = "https://base-mainnet.auth-server.renegade.fi";
46const ARBITRUM_SEPOLIA_RELAYER_BASE_URL: &str = "https://arbitrum-sepolia.relayer.renegade.fi";
48const ARBITRUM_ONE_RELAYER_BASE_URL: &str = "https://arbitrum-one.relayer.renegade.fi";
50const BASE_SEPOLIA_RELAYER_BASE_URL: &str = "https://base-sepolia.relayer.renegade.fi";
52const BASE_MAINNET_RELAYER_BASE_URL: &str = "https://base-mainnet.relayer.renegade.fi";
54
55#[derive(Clone)]
61pub struct ExternalMatchClient {
62 api_key: String,
64 auth_http_client: RelayerHttpClient,
66 relayer_http_client: RelayerHttpClient,
70}
71
72impl ExternalMatchClient {
73 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 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 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 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 #[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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 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 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}