1use polyoxide_core::{HttpClient, QueryBuilder};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5 account::{Credentials, Signer, Wallet},
6 error::ClobError,
7 request::{AuthMode, Request},
8 types::SignedOrder,
9};
10
11#[derive(Clone)]
13pub struct Rfq {
14 pub(crate) http_client: HttpClient,
15 pub(crate) wallet: Wallet,
16 pub(crate) credentials: Credentials,
17 pub(crate) signer: Signer,
18 pub(crate) chain_id: u64,
19}
20
21impl Rfq {
22 fn l2_auth(&self) -> AuthMode {
23 AuthMode::L2 {
24 address: self.wallet.address(),
25 credentials: self.credentials.clone(),
26 signer: self.signer.clone(),
27 }
28 }
29
30 pub async fn create_request(
34 &self,
35 params: &CreateRfqRequestParams,
36 ) -> Result<RfqRequestResponse, ClobError> {
37 Request::<RfqRequestResponse>::post(
38 self.http_client.clone(),
39 "/rfq/request".to_string(),
40 self.l2_auth(),
41 self.chain_id,
42 )
43 .body(params)?
44 .send()
45 .await
46 }
47
48 pub async fn cancel_request(
50 &self,
51 request_id: impl Into<String>,
52 ) -> Result<serde_json::Value, ClobError> {
53 #[derive(Serialize)]
54 #[serde(rename_all = "camelCase")]
55 struct Body {
56 request_id: String,
57 }
58
59 Request::<serde_json::Value>::delete(
60 self.http_client.clone(),
61 "/rfq/request",
62 self.l2_auth(),
63 self.chain_id,
64 )
65 .body(&Body {
66 request_id: request_id.into(),
67 })?
68 .send()
69 .await
70 }
71
72 pub async fn create_quote(
74 &self,
75 params: &CreateRfqQuoteParams,
76 ) -> Result<RfqQuoteResponse, ClobError> {
77 Request::<RfqQuoteResponse>::post(
78 self.http_client.clone(),
79 "/rfq/quote".to_string(),
80 self.l2_auth(),
81 self.chain_id,
82 )
83 .body(params)?
84 .send()
85 .await
86 }
87
88 pub async fn cancel_quote(
90 &self,
91 quote_id: impl Into<String>,
92 ) -> Result<serde_json::Value, ClobError> {
93 #[derive(Serialize)]
94 #[serde(rename_all = "camelCase")]
95 struct Body {
96 quote_id: String,
97 }
98
99 Request::<serde_json::Value>::delete(
100 self.http_client.clone(),
101 "/rfq/quote",
102 self.l2_auth(),
103 self.chain_id,
104 )
105 .body(&Body {
106 quote_id: quote_id.into(),
107 })?
108 .send()
109 .await
110 }
111
112 pub async fn accept_request(
116 &self,
117 request_id: impl Into<String>,
118 quote_id: impl Into<String>,
119 signed_order: &SignedOrder,
120 ) -> Result<serde_json::Value, ClobError> {
121 let payload = serde_json::json!({
122 "requestId": request_id.into(),
123 "quoteId": quote_id.into(),
124 "owner": self.credentials.key,
125 "order": signed_order,
126 });
127
128 Request::<serde_json::Value>::post(
129 self.http_client.clone(),
130 "/rfq/request/accept".to_string(),
131 self.l2_auth(),
132 self.chain_id,
133 )
134 .body(&payload)?
135 .send()
136 .await
137 }
138
139 pub async fn approve_quote(
141 &self,
142 request_id: impl Into<String>,
143 quote_id: impl Into<String>,
144 signed_order: &SignedOrder,
145 ) -> Result<serde_json::Value, ClobError> {
146 let payload = serde_json::json!({
147 "requestId": request_id.into(),
148 "quoteId": quote_id.into(),
149 "owner": self.credentials.key,
150 "order": signed_order,
151 });
152
153 Request::<serde_json::Value>::post(
154 self.http_client.clone(),
155 "/rfq/quote/approve".to_string(),
156 self.l2_auth(),
157 self.chain_id,
158 )
159 .body(&payload)?
160 .send()
161 .await
162 }
163
164 pub fn list_requests(&self) -> ListRfqRequests {
168 let request = Request::get(
169 self.http_client.clone(),
170 "/rfq/data/requests",
171 self.l2_auth(),
172 self.chain_id,
173 );
174 ListRfqRequests { request }
175 }
176
177 pub fn requester_quotes(&self) -> ListRfqQuotes {
179 let request = Request::get(
180 self.http_client.clone(),
181 "/rfq/data/requester/quotes",
182 self.l2_auth(),
183 self.chain_id,
184 );
185 ListRfqQuotes { request }
186 }
187
188 pub fn quoter_quotes(&self) -> ListRfqQuotes {
190 let request = Request::get(
191 self.http_client.clone(),
192 "/rfq/data/quoter/quotes",
193 self.l2_auth(),
194 self.chain_id,
195 );
196 ListRfqQuotes { request }
197 }
198
199 pub fn best_quote(&self, request_id: impl Into<String>) -> Request<RfqQuote> {
201 Request::get(
202 self.http_client.clone(),
203 "/rfq/data/best-quote",
204 self.l2_auth(),
205 self.chain_id,
206 )
207 .query("requestId", request_id.into())
208 }
209
210 pub fn config(&self) -> Request<RfqConfig> {
212 Request::get(
213 self.http_client.clone(),
214 "/rfq/config",
215 AuthMode::None,
216 self.chain_id,
217 )
218 }
219}
220
221pub struct ListRfqRequests {
225 request: Request<RfqPaginatedResponse<RfqRequest>>,
226}
227
228impl ListRfqRequests {
229 pub fn request_ids(mut self, ids: impl Into<Vec<String>>) -> Self {
231 self.request = self.request.query_many("request_ids", ids.into());
232 self
233 }
234
235 pub fn state(mut self, state: impl Into<String>) -> Self {
237 self.request = self.request.query("state", state.into());
238 self
239 }
240
241 pub fn markets(mut self, markets: impl Into<Vec<String>>) -> Self {
243 self.request = self.request.query_many("markets", markets.into());
244 self
245 }
246
247 pub fn size_min(mut self, min: f64) -> Self {
249 self.request = self.request.query("size_min", min);
250 self
251 }
252
253 pub fn size_max(mut self, max: f64) -> Self {
255 self.request = self.request.query("size_max", max);
256 self
257 }
258
259 pub fn size_usdc_min(mut self, min: f64) -> Self {
261 self.request = self.request.query("size_usdc_min", min);
262 self
263 }
264
265 pub fn size_usdc_max(mut self, max: f64) -> Self {
267 self.request = self.request.query("size_usdc_max", max);
268 self
269 }
270
271 pub fn price_min(mut self, min: f64) -> Self {
273 self.request = self.request.query("price_min", min);
274 self
275 }
276
277 pub fn price_max(mut self, max: f64) -> Self {
279 self.request = self.request.query("price_max", max);
280 self
281 }
282
283 pub fn sort_by(mut self, field: impl Into<String>) -> Self {
285 self.request = self.request.query("sort_by", field.into());
286 self
287 }
288
289 pub fn sort_dir(mut self, dir: impl Into<String>) -> Self {
291 self.request = self.request.query("sort_dir", dir.into());
292 self
293 }
294
295 pub fn limit(mut self, limit: u32) -> Self {
297 self.request = self.request.query("limit", limit);
298 self
299 }
300
301 pub fn offset(mut self, offset: impl Into<String>) -> Self {
303 self.request = self.request.query("offset", offset.into());
304 self
305 }
306
307 pub async fn send(self) -> Result<RfqPaginatedResponse<RfqRequest>, ClobError> {
309 self.request.send().await
310 }
311}
312
313pub struct ListRfqQuotes {
315 request: Request<RfqPaginatedResponse<RfqQuote>>,
316}
317
318impl ListRfqQuotes {
319 pub fn quote_ids(mut self, ids: impl Into<Vec<String>>) -> Self {
321 self.request = self.request.query_many("quote_ids", ids.into());
322 self
323 }
324
325 pub fn request_ids(mut self, ids: impl Into<Vec<String>>) -> Self {
327 self.request = self.request.query_many("request_ids", ids.into());
328 self
329 }
330
331 pub fn state(mut self, state: impl Into<String>) -> Self {
333 self.request = self.request.query("state", state.into());
334 self
335 }
336
337 pub fn markets(mut self, markets: impl Into<Vec<String>>) -> Self {
339 self.request = self.request.query_many("markets", markets.into());
340 self
341 }
342
343 pub fn size_min(mut self, min: f64) -> Self {
345 self.request = self.request.query("size_min", min);
346 self
347 }
348
349 pub fn size_max(mut self, max: f64) -> Self {
351 self.request = self.request.query("size_max", max);
352 self
353 }
354
355 pub fn size_usdc_min(mut self, min: f64) -> Self {
357 self.request = self.request.query("size_usdc_min", min);
358 self
359 }
360
361 pub fn size_usdc_max(mut self, max: f64) -> Self {
363 self.request = self.request.query("size_usdc_max", max);
364 self
365 }
366
367 pub fn price_min(mut self, min: f64) -> Self {
369 self.request = self.request.query("price_min", min);
370 self
371 }
372
373 pub fn price_max(mut self, max: f64) -> Self {
375 self.request = self.request.query("price_max", max);
376 self
377 }
378
379 pub fn sort_by(mut self, field: impl Into<String>) -> Self {
381 self.request = self.request.query("sort_by", field.into());
382 self
383 }
384
385 pub fn sort_dir(mut self, dir: impl Into<String>) -> Self {
387 self.request = self.request.query("sort_dir", dir.into());
388 self
389 }
390
391 pub fn limit(mut self, limit: u32) -> Self {
393 self.request = self.request.query("limit", limit);
394 self
395 }
396
397 pub fn offset(mut self, offset: impl Into<String>) -> Self {
399 self.request = self.request.query("offset", offset.into());
400 self
401 }
402
403 pub async fn send(self) -> Result<RfqPaginatedResponse<RfqQuote>, ClobError> {
405 self.request.send().await
406 }
407}
408
409#[derive(Debug, Clone, Serialize)]
413#[serde(rename_all = "camelCase")]
414pub struct CreateRfqRequestParams {
415 pub asset_in: String,
416 pub asset_out: String,
417 pub amount_in: String,
418 pub amount_out: String,
419 pub user_type: u32,
420}
421
422#[derive(Debug, Clone, Serialize)]
424#[serde(rename_all = "camelCase")]
425pub struct CreateRfqQuoteParams {
426 pub request_id: String,
427 pub asset_in: String,
428 pub asset_out: String,
429 pub amount_in: String,
430 pub amount_out: String,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct RfqRequestResponse {
438 pub request_id: Option<String>,
439 pub error: Option<String>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct RfqQuoteResponse {
445 pub quote_id: Option<String>,
446 pub error: Option<String>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct RfqPaginatedResponse<T> {
452 pub data: Vec<T>,
453 pub next_cursor: Option<String>,
454 pub limit: Option<u32>,
455 pub count: Option<u32>,
456 pub total_count: Option<u32>,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct RfqRequest {
462 pub request_id: String,
463 pub user_address: String,
464 #[serde(default)]
465 pub proxy_address: Option<String>,
466 #[serde(default)]
467 pub token: Option<String>,
468 #[serde(default)]
469 pub complement: Option<String>,
470 #[serde(default)]
471 pub condition: Option<String>,
472 #[serde(default)]
473 pub side: Option<String>,
474 #[serde(default)]
475 pub size_in: Option<String>,
476 #[serde(default)]
477 pub size_out: Option<String>,
478 #[serde(default)]
479 pub price: Option<f64>,
480 #[serde(default)]
481 pub accepted_quote_id: Option<String>,
482 #[serde(default)]
483 pub state: Option<String>,
484 #[serde(default)]
485 pub expiry: Option<String>,
486 #[serde(default)]
487 pub created_at: Option<String>,
488 #[serde(default)]
489 pub updated_at: Option<String>,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct RfqQuote {
495 pub quote_id: String,
496 pub request_id: String,
497 pub user_address: String,
498 #[serde(default)]
499 pub proxy_address: Option<String>,
500 #[serde(default)]
501 pub complement: Option<String>,
502 #[serde(default)]
503 pub condition: Option<String>,
504 #[serde(default)]
505 pub token: Option<String>,
506 #[serde(default)]
507 pub side: Option<String>,
508 #[serde(default)]
509 pub size_in: Option<String>,
510 #[serde(default)]
511 pub size_out: Option<String>,
512 #[serde(default)]
513 pub price: Option<f64>,
514 #[serde(default)]
515 pub state: Option<String>,
516 #[serde(default)]
517 pub expiry: Option<String>,
518 #[serde(default)]
519 pub created_at: Option<String>,
520 #[serde(default)]
521 pub updated_at: Option<String>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct RfqConfig {
527 #[serde(flatten)]
528 pub data: serde_json::Value,
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn create_rfq_request_params_serializes() {
537 let params = CreateRfqRequestParams {
538 asset_in: "0xtoken1".into(),
539 asset_out: "0".into(),
540 amount_in: "100".into(),
541 amount_out: "50".into(),
542 user_type: 0,
543 };
544 let json = serde_json::to_value(¶ms).unwrap();
545 assert_eq!(json["assetIn"], "0xtoken1");
546 assert_eq!(json["assetOut"], "0");
547 assert_eq!(json["amountIn"], "100");
548 assert_eq!(json["amountOut"], "50");
549 assert_eq!(json["userType"], 0);
550 }
551
552 #[test]
553 fn create_rfq_quote_params_serializes() {
554 let params = CreateRfqQuoteParams {
555 request_id: "req-123".into(),
556 asset_in: "0xtoken1".into(),
557 asset_out: "0".into(),
558 amount_in: "100".into(),
559 amount_out: "50".into(),
560 };
561 let json = serde_json::to_value(¶ms).unwrap();
562 assert_eq!(json["requestId"], "req-123");
563 assert_eq!(json["assetIn"], "0xtoken1");
564 }
565
566 #[test]
567 fn rfq_request_response_deserializes() {
568 let json = r#"{"request_id": "req-abc", "error": null}"#;
569 let resp: RfqRequestResponse = serde_json::from_str(json).unwrap();
570 assert_eq!(resp.request_id.as_deref(), Some("req-abc"));
571 assert!(resp.error.is_none());
572 }
573
574 #[test]
575 fn rfq_request_response_with_error() {
576 let json = r#"{"request_id": null, "error": "invalid params"}"#;
577 let resp: RfqRequestResponse = serde_json::from_str(json).unwrap();
578 assert!(resp.request_id.is_none());
579 assert_eq!(resp.error.as_deref(), Some("invalid params"));
580 }
581
582 #[test]
583 fn rfq_quote_response_deserializes() {
584 let json = r#"{"quote_id": "quote-xyz", "error": null}"#;
585 let resp: RfqQuoteResponse = serde_json::from_str(json).unwrap();
586 assert_eq!(resp.quote_id.as_deref(), Some("quote-xyz"));
587 }
588
589 #[test]
590 fn rfq_request_deserializes() {
591 let json = r#"{
592 "request_id": "req-1",
593 "user_address": "0xuser",
594 "token": "0xtoken",
595 "side": "BUY",
596 "size_in": "100",
597 "size_out": "50",
598 "price": 0.5,
599 "state": "active",
600 "created_at": "2024-01-01T00:00:00Z"
601 }"#;
602 let req: RfqRequest = serde_json::from_str(json).unwrap();
603 assert_eq!(req.request_id, "req-1");
604 assert_eq!(req.user_address, "0xuser");
605 assert_eq!(req.side.as_deref(), Some("BUY"));
606 assert_eq!(req.price, Some(0.5));
607 assert_eq!(req.state.as_deref(), Some("active"));
608 }
609
610 #[test]
611 fn rfq_request_minimal_deserializes() {
612 let json = r#"{"request_id": "req-1", "user_address": "0xuser"}"#;
613 let req: RfqRequest = serde_json::from_str(json).unwrap();
614 assert_eq!(req.request_id, "req-1");
615 assert!(req.token.is_none());
616 assert!(req.side.is_none());
617 assert!(req.price.is_none());
618 }
619
620 #[test]
621 fn rfq_quote_deserializes() {
622 let json = r#"{
623 "quote_id": "q-1",
624 "request_id": "req-1",
625 "user_address": "0xquoter",
626 "token": "0xtoken",
627 "side": "SELL",
628 "price": 0.52,
629 "state": "active"
630 }"#;
631 let quote: RfqQuote = serde_json::from_str(json).unwrap();
632 assert_eq!(quote.quote_id, "q-1");
633 assert_eq!(quote.request_id, "req-1");
634 assert_eq!(quote.price, Some(0.52));
635 }
636
637 #[test]
638 fn rfq_paginated_response_deserializes() {
639 let json = r#"{
640 "data": [
641 {"request_id": "req-1", "user_address": "0xuser1"},
642 {"request_id": "req-2", "user_address": "0xuser2"}
643 ],
644 "next_cursor": "cursor-abc",
645 "limit": 10,
646 "count": 2,
647 "total_count": 50
648 }"#;
649 let resp: RfqPaginatedResponse<RfqRequest> = serde_json::from_str(json).unwrap();
650 assert_eq!(resp.data.len(), 2);
651 assert_eq!(resp.data[0].request_id, "req-1");
652 assert_eq!(resp.next_cursor.as_deref(), Some("cursor-abc"));
653 assert_eq!(resp.count, Some(2));
654 assert_eq!(resp.total_count, Some(50));
655 }
656
657 #[test]
658 fn rfq_paginated_response_empty() {
659 let json = r#"{"data": []}"#;
660 let resp: RfqPaginatedResponse<RfqQuote> = serde_json::from_str(json).unwrap();
661 assert!(resp.data.is_empty());
662 assert!(resp.next_cursor.is_none());
663 }
664
665 #[test]
666 fn rfq_config_deserializes() {
667 let json = r#"{"min_size": 10, "max_expiry": 3600}"#;
668 let config: RfqConfig = serde_json::from_str(json).unwrap();
669 assert_eq!(config.data["min_size"], 10);
670 }
671}