Skip to main content

polyoxide_clob/api/
rfq.rs

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/// RFQ (Request for Quote) namespace for OTC-style trading
12#[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    // ── Create / Cancel ──────────────────────────────────────────
31
32    /// Create a new RFQ request
33    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    /// Cancel an RFQ request
49    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    /// Create a new RFQ quote
73    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    /// Cancel an RFQ quote
89    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    // ── Accept / Approve ─────────────────────────────────────────
113
114    /// Accept an RFQ request by submitting a signed order
115    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    /// Approve an RFQ quote by submitting a signed order
140    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    // ── Queries ──────────────────────────────────────────────────
165
166    /// List RFQ requests with optional filtering
167    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    /// List quotes as the requester
178    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    /// List quotes as the quoter
189    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    /// Get the best quote for an RFQ request
200    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    /// Get RFQ system configuration (no auth required)
211    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
221// ── Query builders ───────────────────────────────────────────────
222
223/// Request builder for listing RFQ requests
224pub struct ListRfqRequests {
225    request: Request<RfqPaginatedResponse<RfqRequest>>,
226}
227
228impl ListRfqRequests {
229    /// Filter by request IDs
230    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    /// Filter by state ("active" or "inactive")
236    pub fn state(mut self, state: impl Into<String>) -> Self {
237        self.request = self.request.query("state", state.into());
238        self
239    }
240
241    /// Filter by markets (condition IDs)
242    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    /// Minimum size filter
248    pub fn size_min(mut self, min: f64) -> Self {
249        self.request = self.request.query("size_min", min);
250        self
251    }
252
253    /// Maximum size filter
254    pub fn size_max(mut self, max: f64) -> Self {
255        self.request = self.request.query("size_max", max);
256        self
257    }
258
259    /// Minimum USDC size filter
260    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    /// Maximum USDC size filter
266    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    /// Minimum price filter
272    pub fn price_min(mut self, min: f64) -> Self {
273        self.request = self.request.query("price_min", min);
274        self
275    }
276
277    /// Maximum price filter
278    pub fn price_max(mut self, max: f64) -> Self {
279        self.request = self.request.query("price_max", max);
280        self
281    }
282
283    /// Sort by field ("price", "expiry", "size", "created")
284    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    /// Sort direction ("asc" or "desc")
290    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    /// Maximum number of results
296    pub fn limit(mut self, limit: u32) -> Self {
297        self.request = self.request.query("limit", limit);
298        self
299    }
300
301    /// Pagination offset cursor
302    pub fn offset(mut self, offset: impl Into<String>) -> Self {
303        self.request = self.request.query("offset", offset.into());
304        self
305    }
306
307    /// Execute the request
308    pub async fn send(self) -> Result<RfqPaginatedResponse<RfqRequest>, ClobError> {
309        self.request.send().await
310    }
311}
312
313/// Request builder for listing RFQ quotes
314pub struct ListRfqQuotes {
315    request: Request<RfqPaginatedResponse<RfqQuote>>,
316}
317
318impl ListRfqQuotes {
319    /// Filter by quote IDs
320    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    /// Filter by request IDs
326    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    /// Filter by state ("active" or "inactive")
332    pub fn state(mut self, state: impl Into<String>) -> Self {
333        self.request = self.request.query("state", state.into());
334        self
335    }
336
337    /// Filter by markets (condition IDs)
338    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    /// Minimum size filter
344    pub fn size_min(mut self, min: f64) -> Self {
345        self.request = self.request.query("size_min", min);
346        self
347    }
348
349    /// Maximum size filter
350    pub fn size_max(mut self, max: f64) -> Self {
351        self.request = self.request.query("size_max", max);
352        self
353    }
354
355    /// Minimum USDC size filter
356    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    /// Maximum USDC size filter
362    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    /// Minimum price filter
368    pub fn price_min(mut self, min: f64) -> Self {
369        self.request = self.request.query("price_min", min);
370        self
371    }
372
373    /// Maximum price filter
374    pub fn price_max(mut self, max: f64) -> Self {
375        self.request = self.request.query("price_max", max);
376        self
377    }
378
379    /// Sort by field ("price", "expiry", "created")
380    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    /// Sort direction ("asc" or "desc")
386    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    /// Maximum number of results
392    pub fn limit(mut self, limit: u32) -> Self {
393        self.request = self.request.query("limit", limit);
394        self
395    }
396
397    /// Pagination offset cursor
398    pub fn offset(mut self, offset: impl Into<String>) -> Self {
399        self.request = self.request.query("offset", offset.into());
400        self
401    }
402
403    /// Execute the request
404    pub async fn send(self) -> Result<RfqPaginatedResponse<RfqQuote>, ClobError> {
405        self.request.send().await
406    }
407}
408
409// ── Request types ────────────────────────────────────────────────
410
411/// Parameters for creating an RFQ request
412#[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/// Parameters for creating an RFQ quote
423#[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// ── Response types ───────────────────────────────────────────────
434
435/// Response from creating an RFQ request
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct RfqRequestResponse {
438    pub request_id: Option<String>,
439    pub error: Option<String>,
440}
441
442/// Response from creating an RFQ quote
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct RfqQuoteResponse {
445    pub quote_id: Option<String>,
446    pub error: Option<String>,
447}
448
449/// Paginated response wrapper for RFQ data endpoints
450#[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/// An RFQ request
460#[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/// An RFQ quote
493#[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/// RFQ system configuration
525#[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(&params).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(&params).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}