Skip to main content

clob_client_rust/
rfq_client.rs

1use crate::endpoints::{
2    CANCEL_RFQ_QUOTE, CANCEL_RFQ_REQUEST, CREATE_RFQ_QUOTE, CREATE_RFQ_REQUEST, GET_RFQ_BEST_QUOTE,
3    GET_RFQ_QUOTER_QUOTES, GET_RFQ_REQUESTER_QUOTES, GET_RFQ_REQUESTS, RFQ_CONFIG,
4    RFQ_QUOTE_APPROVE, RFQ_REQUESTS_ACCEPT,
5};
6use crate::errors::ClobError;
7use crate::http_helpers::{QueryParams, RequestOptions};
8use crate::order_builder::{parse_units, rounding_config};
9use crate::types::{
10    AcceptQuoteParams, ApiKeyCreds, ApproveOrderParams, CancelRfqQuoteParams,
11    CancelRfqRequestParams, CreateRfqRequestParams, GetRfqBestQuoteParams, GetRfqQuotesParams,
12    GetRfqRequestsParams, RfqQuote, RfqQuoteResponse, RfqQuotesResponse,
13    RfqRequestOrderCreationPayload, RfqRequestResponse, RfqRequestsResponse, RfqUserOrder,
14    RfqUserQuote, Side, SignatureType, SignedOrder, UserOrder,
15};
16use crate::utilities::{round_down, round_normal};
17use rust_decimal::Decimal;
18use serde::Serialize;
19use serde_json::Value;
20
21const COLLATERAL_TOKEN_DECIMALS: u32 = 6;
22
23/// RFQ client (TypeScript parity).
24///
25/// 设计:
26/// - 通过 `ClobClient::rfq()` 生成一个绑定到同一个 client 的临时视图。
27/// - 需要签名与 API Key 的接口会检查 L2 鉴权可用性。
28/// - tick_size 依然遵循 Rust SDK 既有策略:默认 0.01;如果需要其他 tick 由调用方显式传入。
29pub struct RfqClient<'a> {
30    client: &'a crate::client::ClobClient,
31}
32
33impl<'a> RfqClient<'a> {
34    pub(crate) fn new(client: &'a crate::client::ClobClient) -> Self {
35        Self { client }
36    }
37
38    fn ensure_l2_auth(
39        &self,
40    ) -> Result<(&crate::signer_adapter::EthersSigner, &ApiKeyCreds), ClobError> {
41        let creds = self
42            .client
43            .creds
44            .as_ref()
45            .ok_or_else(|| ClobError::Other("L2 creds required".to_string()))?;
46        let signer_arc = self
47            .client
48            .signer
49            .as_ref()
50            .ok_or_else(|| ClobError::Other("L1 signer required".to_string()))?;
51        Ok((signer_arc.as_ref(), creds))
52    }
53
54    fn user_type(&self) -> u8 {
55        // TS 中 userType 来源于 order builder signatureType;Rust 侧取 builder_config.signature_type。
56        self.client
57            .builder_config
58            .as_ref()
59            .map(|c| u8::from(c.signature_type))
60            .unwrap_or(u8::from(SignatureType::EOA))
61    }
62
63    fn attach_geo_params(&self, params: Option<QueryParams>) -> Option<QueryParams> {
64        let tok = match self
65            .client
66            .geo_block_token
67            .as_ref()
68            .filter(|t| !t.trim().is_empty())
69        {
70            Some(t) => t.clone(),
71            None => return params,
72        };
73
74        let mut p = params.unwrap_or_default();
75        // TS 合并顺序:{ ...options.params, geo_block_token: this.geoBlockToken }
76        // => client token 覆盖 caller 的同名字段。
77        p.insert("geo_block_token".to_string(), tok);
78        Some(p)
79    }
80
81    fn tick_or_default(tick: Option<&str>) -> &str {
82        tick.unwrap_or("0.01")
83    }
84
85    /// Get the order creation payload from an RFQ quote based on match type.
86    ///
87    /// Reference: TypeScript SDK v5.1.3 getRequestOrderCreationPayload
88    ///
89    /// - COMPLEMENTARY: side = opposite of quote.side, token = quote.token, size = (BUY->size_out, SELL->size_in), price = quote.price
90    /// - MINT/MERGE: side = same as quote.side, token = quote.complement, size = (BUY->size_in, SELL->size_out), price = 1 - quote.price
91    fn get_request_order_creation_payload(rfq_quote: &RfqQuote) -> RfqRequestOrderCreationPayload {
92        let match_type = rfq_quote.match_type.as_deref().unwrap_or("COMPLEMENTARY");
93
94        match match_type {
95            "MINT" | "MERGE" => {
96                // For MINT/MERGE: same side, use complement token
97                let side = if rfq_quote.side.eq_ignore_ascii_case("BUY") {
98                    Side::BUY
99                } else {
100                    Side::SELL
101                };
102                let size = if rfq_quote.side.eq_ignore_ascii_case("BUY") {
103                    rfq_quote.size_in.clone()
104                } else {
105                    rfq_quote.size_out.clone()
106                };
107                // price = 1 - quote.price (quote.price is already f64)
108                let complement_price = 1.0 - rfq_quote.price;
109                let price = format!("{:.4}", complement_price);
110
111                RfqRequestOrderCreationPayload {
112                    token: rfq_quote.complement.clone(),
113                    side,
114                    size,
115                    price,
116                }
117            }
118            _ => {
119                // COMPLEMENTARY (default): opposite side, use quote token
120                let side = if rfq_quote.side.eq_ignore_ascii_case("BUY") {
121                    Side::SELL
122                } else {
123                    Side::BUY
124                };
125                let size = if rfq_quote.side.eq_ignore_ascii_case("BUY") {
126                    rfq_quote.size_out.clone()
127                } else {
128                    rfq_quote.size_in.clone()
129                };
130
131                RfqRequestOrderCreationPayload {
132                    token: rfq_quote.token.clone(),
133                    side,
134                    size,
135                    price: format!("{:.4}", rfq_quote.price),
136                }
137            }
138        }
139    }
140
141    fn round_config(tick: &str) -> crate::order_builder::RoundConfig {
142        // 如果调用方传入未知 tick,降级用 0.01 的配置(与 SDK 的“默认 tick”行为保持一致)。
143        rounding_config()
144            .get(tick)
145            .cloned()
146            .unwrap_or_else(|| rounding_config()["0.01"].clone())
147    }
148
149    fn to_fixed(num: Decimal, decimals: u32) -> String {
150        let rounded = num.round_dp(decimals);
151        format!("{}", rounded)
152    }
153
154    fn ok_string(val: Value) -> String {
155        if let Some(s) = val.as_str() {
156            return s.to_string();
157        }
158        if val.get("ok").and_then(|v| v.as_bool()).unwrap_or(false) {
159            return "OK".to_string();
160        }
161        val.to_string()
162    }
163
164    /// Build query string with repeated keys for array params (TS SDK v5.2.4 parity).
165    ///
166    /// Standard `QueryParams` (HashMap) collapses multiple values into a single
167    /// comma-separated string.  The new RFQ endpoints expect repeated keys like
168    /// `quoteIds=a&quoteIds=b`, so we manually build the query string.
169    fn build_rfq_quotes_query(rfq: &GetRfqQuotesParams) -> String {
170        let mut parts: Vec<String> = Vec::new();
171        if let Some(ids) = &rfq.quote_ids {
172            for id in ids {
173                parts.push(format!("quoteIds={id}"));
174            }
175        }
176        if let Some(v) = &rfq.state {
177            parts.push(format!("state={v}"));
178        }
179        if let Some(ids) = &rfq.markets {
180            for id in ids {
181                parts.push(format!("markets={id}"));
182            }
183        }
184        if let Some(ids) = &rfq.request_ids {
185            for id in ids {
186                parts.push(format!("requestIds={id}"));
187            }
188        }
189        if let Some(v) = rfq.size_min {
190            parts.push(format!("sizeMin={v}"));
191        }
192        if let Some(v) = rfq.size_max {
193            parts.push(format!("sizeMax={v}"));
194        }
195        if let Some(v) = rfq.size_usdc_min {
196            parts.push(format!("sizeUsdcMin={v}"));
197        }
198        if let Some(v) = rfq.size_usdc_max {
199            parts.push(format!("sizeUsdcMax={v}"));
200        }
201        if let Some(v) = rfq.price_min {
202            parts.push(format!("priceMin={v}"));
203        }
204        if let Some(v) = rfq.price_max {
205            parts.push(format!("priceMax={v}"));
206        }
207        if let Some(v) = &rfq.sort_by {
208            parts.push(format!("sortBy={v}"));
209        }
210        if let Some(v) = &rfq.sort_dir {
211            parts.push(format!("sortDir={v}"));
212        }
213        if let Some(v) = rfq.limit {
214            parts.push(format!("limit={v}"));
215        }
216        if let Some(v) = &rfq.offset {
217            parts.push(format!("offset={v}"));
218        }
219        parts.join("&")
220    }
221
222    fn parse_rfq_requests_params(&self, rfq: &GetRfqRequestsParams) -> QueryParams {
223        let mut params: QueryParams = QueryParams::new();
224        if let Some(v) = &rfq.request_ids {
225            params.insert("requestIds".to_string(), v.join(","));
226        }
227        if let Some(v) = &rfq.state {
228            params.insert("state".to_string(), v.clone());
229        }
230        if let Some(v) = &rfq.markets {
231            params.insert("markets".to_string(), v.join(","));
232        }
233        if let Some(v) = rfq.size_min {
234            params.insert("sizeMin".to_string(), v.to_string());
235        }
236        if let Some(v) = rfq.size_max {
237            params.insert("sizeMax".to_string(), v.to_string());
238        }
239        if let Some(v) = rfq.size_usdc_min {
240            params.insert("sizeUsdcMin".to_string(), v.to_string());
241        }
242        if let Some(v) = rfq.size_usdc_max {
243            params.insert("sizeUsdcMax".to_string(), v.to_string());
244        }
245        if let Some(v) = rfq.price_min {
246            params.insert("priceMin".to_string(), v.to_string());
247        }
248        if let Some(v) = rfq.price_max {
249            params.insert("priceMax".to_string(), v.to_string());
250        }
251        if let Some(v) = &rfq.sort_by {
252            params.insert("sortBy".to_string(), v.clone());
253        }
254        if let Some(v) = &rfq.sort_dir {
255            params.insert("sortDir".to_string(), v.clone());
256        }
257        if let Some(v) = rfq.limit {
258            params.insert("limit".to_string(), v.to_string());
259        }
260        if let Some(v) = &rfq.offset {
261            params.insert("offset".to_string(), v.clone());
262        }
263        params
264    }
265
266    pub async fn create_rfq_request(
267        &self,
268        user_order: RfqUserOrder,
269        tick_size: Option<&str>,
270    ) -> Result<RfqRequestResponse, ClobError> {
271        let (signer_ref, creds) = self.ensure_l2_auth()?;
272
273        let tick = Self::tick_or_default(tick_size);
274        let rc = Self::round_config(tick);
275
276        let rounded_price = round_normal(user_order.price, rc.price);
277        let rounded_size = round_down(user_order.size, rc.size);
278        let rounded_price_s = Self::to_fixed(rounded_price, rc.price);
279        let rounded_size_s = Self::to_fixed(rounded_size, rc.size);
280
281        let size_num = rounded_size_s
282            .parse::<Decimal>()
283            .map_err(|e| ClobError::Other(format!("invalid size: {}", e)))?;
284        let price_num = rounded_price_s
285            .parse::<Decimal>()
286            .map_err(|e| ClobError::Other(format!("invalid price: {}", e)))?;
287
288        let (asset_in, asset_out, amount_in, amount_out) = match user_order.side {
289            Side::BUY => {
290                // TS 逻辑保持一致
291                let amount_in = parse_units(&rounded_size_s, COLLATERAL_TOKEN_DECIMALS)?;
292                let amount_out = parse_units(
293                    &Self::to_fixed(size_num * price_num, rc.amount),
294                    COLLATERAL_TOKEN_DECIMALS,
295                )?;
296                (
297                    user_order.token_id.clone(),
298                    "0".to_string(),
299                    amount_in,
300                    amount_out,
301                )
302            }
303            Side::SELL => {
304                let amount_in = parse_units(
305                    &Self::to_fixed(size_num * price_num, rc.amount),
306                    COLLATERAL_TOKEN_DECIMALS,
307                )?;
308                let amount_out = parse_units(&rounded_size_s, COLLATERAL_TOKEN_DECIMALS)?;
309                (
310                    "0".to_string(),
311                    user_order.token_id.clone(),
312                    amount_in,
313                    amount_out,
314                )
315            }
316        };
317
318        let payload = CreateRfqRequestParams {
319            asset_in,
320            asset_out,
321            amount_in,
322            amount_out,
323            user_type: self.user_type(),
324        };
325        let body_str =
326            serde_json::to_string(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
327
328        let ts = if self.client.use_server_time {
329            Some(self.client.get_server_time().await?)
330        } else {
331            None
332        };
333        let headers = crate::headers::create_l2_headers(
334            signer_ref,
335            creds,
336            "POST",
337            CREATE_RFQ_REQUEST,
338            Some(&body_str),
339            ts,
340        )
341        .await?;
342
343        let endpoint = format!("{}{}", self.client.host, CREATE_RFQ_REQUEST);
344        let params = self.attach_geo_params(None);
345        let resp: RfqRequestResponse = crate::http_helpers::post_typed(
346            &self.client.http_client,
347            &endpoint,
348            Some(RequestOptions {
349                headers: Some(headers),
350                data: Some(payload),
351                params,
352            }),
353        )
354        .await?;
355        Ok(resp)
356    }
357
358    pub async fn cancel_rfq_request(
359        &self,
360        req: CancelRfqRequestParams,
361    ) -> Result<String, ClobError> {
362        let (signer_ref, creds) = self.ensure_l2_auth()?;
363
364        let body_str = serde_json::to_string(&req).map_err(|e| ClobError::Other(e.to_string()))?;
365        let ts = if self.client.use_server_time {
366            Some(self.client.get_server_time().await?)
367        } else {
368            None
369        };
370        let headers = crate::headers::create_l2_headers(
371            signer_ref,
372            creds,
373            "DELETE",
374            CANCEL_RFQ_REQUEST,
375            Some(&body_str),
376            ts,
377        )
378        .await?;
379
380        let endpoint = format!("{}{}", self.client.host, CANCEL_RFQ_REQUEST);
381        let params = self.attach_geo_params(None);
382        let val = crate::http_helpers::del(
383            &self.client.http_client,
384            &endpoint,
385            Some(RequestOptions {
386                headers: Some(headers),
387                data: Some(serde_json::to_value(req).map_err(|e| ClobError::Other(e.to_string()))?),
388                params,
389            }),
390        )
391        .await?;
392        Ok(Self::ok_string(val))
393    }
394
395    pub async fn get_rfq_requests(
396        &self,
397        params: Option<GetRfqRequestsParams>,
398    ) -> Result<RfqRequestsResponse, ClobError> {
399        let (signer_ref, creds) = self.ensure_l2_auth()?;
400
401        let ts = if self.client.use_server_time {
402            Some(self.client.get_server_time().await?)
403        } else {
404            None
405        };
406        let headers =
407            crate::headers::create_l2_headers(signer_ref, creds, "GET", GET_RFQ_REQUESTS, None, ts)
408                .await?;
409
410        let endpoint = format!("{}{}", self.client.host, GET_RFQ_REQUESTS);
411        let query = params.map(|p| self.parse_rfq_requests_params(&p));
412        let query = self.attach_geo_params(query);
413        let resp: RfqRequestsResponse = crate::http_helpers::get_typed(
414            &self.client.http_client,
415            &endpoint,
416            Some(RequestOptions::<Value> {
417                headers: Some(headers),
418                data: None,
419                params: query,
420            }),
421        )
422        .await?;
423        Ok(resp)
424    }
425
426    pub async fn create_rfq_quote(
427        &self,
428        user_quote: RfqUserQuote,
429        tick_size: Option<&str>,
430    ) -> Result<RfqQuoteResponse, ClobError> {
431        let (signer_ref, creds) = self.ensure_l2_auth()?;
432
433        let tick = Self::tick_or_default(tick_size);
434        let rc = Self::round_config(tick);
435
436        let rounded_price = round_normal(user_quote.price, rc.price);
437        let rounded_size = round_down(user_quote.size, rc.size);
438        let rounded_price_s = Self::to_fixed(rounded_price, rc.price);
439        let rounded_size_s = Self::to_fixed(rounded_size, rc.size);
440
441        let size_num = rounded_size_s
442            .parse::<Decimal>()
443            .map_err(|e| ClobError::Other(format!("invalid size: {}", e)))?;
444        let price_num = rounded_price_s
445            .parse::<Decimal>()
446            .map_err(|e| ClobError::Other(format!("invalid price: {}", e)))?;
447
448        let (asset_in, asset_out, amount_in, amount_out) = match user_quote.side {
449            Side::SELL => {
450                let amount_in = parse_units(
451                    &Self::to_fixed(size_num * price_num, rc.amount),
452                    COLLATERAL_TOKEN_DECIMALS,
453                )?;
454                let amount_out = parse_units(&rounded_size_s, COLLATERAL_TOKEN_DECIMALS)?;
455                (
456                    "0".to_string(),
457                    user_quote.token_id.clone(),
458                    amount_in,
459                    amount_out,
460                )
461            }
462            Side::BUY => {
463                let amount_in = parse_units(&rounded_size_s, COLLATERAL_TOKEN_DECIMALS)?;
464                let amount_out = parse_units(
465                    &Self::to_fixed(size_num * price_num, rc.amount),
466                    COLLATERAL_TOKEN_DECIMALS,
467                )?;
468                (
469                    user_quote.token_id.clone(),
470                    "0".to_string(),
471                    amount_in,
472                    amount_out,
473                )
474            }
475        };
476
477        // TS: CreateRfqQuoteParams + userType
478        #[derive(Serialize)]
479        #[serde(rename_all = "camelCase")]
480        struct CreateRfqQuoteWithUserType {
481            request_id: String,
482            asset_in: String,
483            asset_out: String,
484            amount_in: String,
485            amount_out: String,
486            user_type: u8,
487        }
488
489        let payload = CreateRfqQuoteWithUserType {
490            request_id: user_quote.request_id.clone(),
491            asset_in,
492            asset_out,
493            amount_in,
494            amount_out,
495            user_type: self.user_type(),
496        };
497
498        let body_str =
499            serde_json::to_string(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
500        let ts = if self.client.use_server_time {
501            Some(self.client.get_server_time().await?)
502        } else {
503            None
504        };
505
506        let headers = crate::headers::create_l2_headers(
507            signer_ref,
508            creds,
509            "POST",
510            CREATE_RFQ_QUOTE,
511            Some(&body_str),
512            ts,
513        )
514        .await?;
515
516        let endpoint = format!("{}{}", self.client.host, CREATE_RFQ_QUOTE);
517        let params = self.attach_geo_params(None);
518        let resp: RfqQuoteResponse = crate::http_helpers::post_typed(
519            &self.client.http_client,
520            &endpoint,
521            Some(RequestOptions {
522                headers: Some(headers),
523                data: Some(payload),
524                params,
525            }),
526        )
527        .await?;
528        Ok(resp)
529    }
530
531    /// Internal implementation: fetch RFQ quotes from a parameterised endpoint.
532    async fn get_rfq_quotes_internal(
533        &self,
534        endpoint_path: &str,
535        params: Option<GetRfqQuotesParams>,
536    ) -> Result<RfqQuotesResponse, ClobError> {
537        let (signer_ref, creds) = self.ensure_l2_auth()?;
538
539        let ts = if self.client.use_server_time {
540            Some(self.client.get_server_time().await?)
541        } else {
542            None
543        };
544        let headers =
545            crate::headers::create_l2_headers(signer_ref, creds, "GET", endpoint_path, None, ts)
546                .await?;
547
548        // Build URL with repeated-key query string for array params
549        let mut url = format!("{}{}", self.client.host, endpoint_path);
550        if let Some(ref p) = params {
551            let qs = Self::build_rfq_quotes_query(p);
552            if !qs.is_empty() {
553                url = format!("{url}?{qs}");
554            }
555        }
556
557        let geo_params = self.attach_geo_params(None);
558        let resp: RfqQuotesResponse = crate::http_helpers::get_typed(
559            &self.client.http_client,
560            &url,
561            Some(RequestOptions::<Value> {
562                headers: Some(headers),
563                data: None,
564                params: geo_params,
565            }),
566        )
567        .await?;
568        Ok(resp)
569    }
570
571    /// Fetch RFQ quotes as a **requester** (new endpoint, TS SDK v5.2.4).
572    pub async fn get_rfq_requester_quotes(
573        &self,
574        params: Option<GetRfqQuotesParams>,
575    ) -> Result<RfqQuotesResponse, ClobError> {
576        self.get_rfq_quotes_internal(GET_RFQ_REQUESTER_QUOTES, params)
577            .await
578    }
579
580    /// Fetch RFQ quotes as a **quoter** (new endpoint, TS SDK v5.2.4).
581    pub async fn get_rfq_quoter_quotes(
582        &self,
583        params: Option<GetRfqQuotesParams>,
584    ) -> Result<RfqQuotesResponse, ClobError> {
585        self.get_rfq_quotes_internal(GET_RFQ_QUOTER_QUOTES, params)
586            .await
587    }
588
589    /// Deprecated: use [`get_rfq_requester_quotes`] or [`get_rfq_quoter_quotes`].
590    #[deprecated(note = "use get_rfq_requester_quotes or get_rfq_quoter_quotes")]
591    pub async fn get_rfq_quotes(
592        &self,
593        params: Option<GetRfqQuotesParams>,
594    ) -> Result<RfqQuotesResponse, ClobError> {
595        self.get_rfq_requester_quotes(params).await
596    }
597
598    pub async fn get_rfq_best_quote(
599        &self,
600        params: Option<GetRfqBestQuoteParams>,
601    ) -> Result<RfqQuote, ClobError> {
602        let (signer_ref, creds) = self.ensure_l2_auth()?;
603
604        let ts = if self.client.use_server_time {
605            Some(self.client.get_server_time().await?)
606        } else {
607            None
608        };
609        let headers = crate::headers::create_l2_headers(
610            signer_ref,
611            creds,
612            "GET",
613            GET_RFQ_BEST_QUOTE,
614            None,
615            ts,
616        )
617        .await?;
618
619        let endpoint = format!("{}{}", self.client.host, GET_RFQ_BEST_QUOTE);
620        let mut query: QueryParams = QueryParams::new();
621        if let Some(p) = params
622            && let Some(id) = p.request_id
623        {
624            query.insert("requestId".to_string(), id);
625        }
626        let query = self.attach_geo_params(if query.is_empty() { None } else { Some(query) });
627        let resp: RfqQuote = crate::http_helpers::get_typed(
628            &self.client.http_client,
629            &endpoint,
630            Some(RequestOptions::<Value> {
631                headers: Some(headers),
632                data: None,
633                params: query,
634            }),
635        )
636        .await?;
637        Ok(resp)
638    }
639
640    pub async fn cancel_rfq_quote(&self, quote: CancelRfqQuoteParams) -> Result<String, ClobError> {
641        let (signer_ref, creds) = self.ensure_l2_auth()?;
642
643        let body_str =
644            serde_json::to_string(&quote).map_err(|e| ClobError::Other(e.to_string()))?;
645        let ts = if self.client.use_server_time {
646            Some(self.client.get_server_time().await?)
647        } else {
648            None
649        };
650        let headers = crate::headers::create_l2_headers(
651            signer_ref,
652            creds,
653            "DELETE",
654            CANCEL_RFQ_QUOTE,
655            Some(&body_str),
656            ts,
657        )
658        .await?;
659
660        let endpoint = format!("{}{}", self.client.host, CANCEL_RFQ_QUOTE);
661        let params = self.attach_geo_params(None);
662        let val = crate::http_helpers::del(
663            &self.client.http_client,
664            &endpoint,
665            Some(RequestOptions {
666                headers: Some(headers),
667                data: Some(
668                    serde_json::to_value(quote).map_err(|e| ClobError::Other(e.to_string()))?,
669                ),
670                params,
671            }),
672        )
673        .await?;
674        Ok(Self::ok_string(val))
675    }
676
677    pub async fn rfq_config(&self) -> Result<Value, ClobError> {
678        let (signer_ref, creds) = self.ensure_l2_auth()?;
679
680        let ts = if self.client.use_server_time {
681            Some(self.client.get_server_time().await?)
682        } else {
683            None
684        };
685        let headers =
686            crate::headers::create_l2_headers(signer_ref, creds, "GET", RFQ_CONFIG, None, ts)
687                .await?;
688
689        let endpoint = format!("{}{}", self.client.host, RFQ_CONFIG);
690        let params = self.attach_geo_params(None);
691        let val = crate::http_helpers::get(
692            &self.client.http_client,
693            &endpoint,
694            Some(RequestOptions::<Value> {
695                headers: Some(headers),
696                data: None,
697                params,
698            }),
699        )
700        .await?;
701        Ok(val)
702    }
703
704    pub async fn accept_rfq_quote(
705        &self,
706        payload: AcceptQuoteParams,
707        fee_rate_bps: Decimal,
708        tick_size: Option<&str>,
709    ) -> Result<String, ClobError> {
710        // 先拷贝 owner(API key)
711        let owner_key = {
712            let (_signer_ref, creds) = self.ensure_l2_auth()?;
713            creds.key.clone()
714        };
715
716        let quotes = self
717            .get_rfq_requester_quotes(Some(GetRfqQuotesParams {
718                quote_ids: Some(vec![payload.quote_id.clone()]),
719                ..Default::default()
720            }))
721            .await?;
722        if quotes.data.is_empty() {
723            return Err(ClobError::Other("RFQ quote not found".to_string()));
724        }
725        let rfq_quote = &quotes.data[0];
726
727        // Use match_type aware order creation payload (v5.1.3)
728        let order_payload = Self::get_request_order_creation_payload(rfq_quote);
729
730        let size = order_payload
731            .size
732            .parse::<Decimal>()
733            .map_err(|e| ClobError::Other(format!("invalid quote size: {}", e)))?;
734
735        let price = order_payload
736            .price
737            .parse::<Decimal>()
738            .map_err(|e| ClobError::Other(format!("invalid quote price: {}", e)))?;
739
740        let tick = Self::tick_or_default(tick_size);
741        let order = self
742            .client
743            .create_order(
744                UserOrder {
745                    token_id: order_payload.token,
746                    price,
747                    size,
748                    side: order_payload.side,
749                    fee_rate_bps,
750                    nonce: None,
751                    expiration: Some(payload.expiration),
752                    taker: None,
753                },
754                Some(tick),
755            )
756            .await?;
757
758        let accept_payload =
759            RfqOrderActionPayload::try_new(payload.request_id, payload.quote_id, owner_key, order)?;
760
761        self.post_order_action(RFQ_REQUESTS_ACCEPT, accept_payload)
762            .await
763    }
764
765    pub async fn approve_rfq_order(
766        &self,
767        payload: ApproveOrderParams,
768        fee_rate_bps: Decimal,
769        tick_size: Option<&str>,
770    ) -> Result<String, ClobError> {
771        let owner_key = {
772            let (_signer_ref, creds) = self.ensure_l2_auth()?;
773            creds.key.clone()
774        };
775
776        let quotes = self
777            .get_rfq_quoter_quotes(Some(GetRfqQuotesParams {
778                quote_ids: Some(vec![payload.quote_id.clone()]),
779                ..Default::default()
780            }))
781            .await?;
782        if quotes.data.is_empty() {
783            return Err(ClobError::Other("RFQ quote not found".to_string()));
784        }
785        let rfq_quote = &quotes.data[0];
786
787        let (side, size_str) = if rfq_quote.side.eq_ignore_ascii_case("BUY") {
788            (Side::BUY, rfq_quote.size_in.as_str())
789        } else {
790            (Side::SELL, rfq_quote.size_out.as_str())
791        };
792
793        let size = size_str
794            .parse::<Decimal>()
795            .map_err(|e| ClobError::Other(format!("invalid quote size: {}", e)))?;
796
797        let tick = Self::tick_or_default(tick_size);
798        let order = self
799            .client
800            .create_order(
801                UserOrder {
802                    token_id: rfq_quote.token.clone(),
803                    price: rfq_quote
804                        .price
805                        .to_string()
806                        .parse::<Decimal>()
807                        .map_err(|e| ClobError::Other(format!("invalid rfq quote price: {}", e)))?,
808                    size,
809                    side,
810                    fee_rate_bps,
811                    nonce: None,
812                    expiration: Some(payload.expiration),
813                    taker: None,
814                },
815                Some(tick),
816            )
817            .await?;
818
819        let approve_payload =
820            RfqOrderActionPayload::try_new(payload.request_id, payload.quote_id, owner_key, order)?;
821
822        self.post_order_action(RFQ_QUOTE_APPROVE, approve_payload)
823            .await
824    }
825
826    async fn post_order_action(
827        &self,
828        request_path: &str,
829        payload: RfqOrderActionPayload,
830    ) -> Result<String, ClobError> {
831        let (signer_ref, creds) = self.ensure_l2_auth()?;
832
833        let body_str =
834            serde_json::to_string(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
835        let ts = if self.client.use_server_time {
836            Some(self.client.get_server_time().await?)
837        } else {
838            None
839        };
840
841        let headers = crate::headers::create_l2_headers(
842            signer_ref,
843            creds,
844            "POST",
845            request_path,
846            Some(&body_str),
847            ts,
848        )
849        .await?;
850
851        let endpoint = format!("{}{}", self.client.host, request_path);
852        let params = self.attach_geo_params(None);
853        let val = crate::http_helpers::post(
854            &self.client.http_client,
855            &endpoint,
856            Some(RequestOptions {
857                headers: Some(headers),
858                data: Some(
859                    serde_json::to_value(payload).map_err(|e| ClobError::Other(e.to_string()))?,
860                ),
861                params,
862            }),
863        )
864        .await?;
865        Ok(Self::ok_string(val))
866    }
867}
868
869#[derive(Debug, Clone, Serialize)]
870#[serde(rename_all = "camelCase")]
871struct RfqSignedOrderPayload {
872    pub salt: i64,
873    pub maker: String,
874    pub signer: String,
875    pub taker: String,
876    pub token_id: String,
877    pub maker_amount: String,
878    pub taker_amount: String,
879    pub expiration: i64,
880    pub nonce: String,
881    pub fee_rate_bps: String,
882    pub side: Side,
883    pub signature_type: SignatureType,
884    pub signature: String,
885}
886
887impl RfqSignedOrderPayload {
888    fn try_from_signed_order(order: SignedOrder) -> Result<Self, ClobError> {
889        let salt = order
890            .salt
891            .parse::<i64>()
892            .map_err(|e| ClobError::Other(format!("invalid salt: {}", e)))?;
893        let expiration = order
894            .expiration
895            .parse::<i64>()
896            .map_err(|e| ClobError::Other(format!("invalid expiration: {}", e)))?;
897
898        Ok(Self {
899            salt,
900            maker: order.maker,
901            signer: order.signer,
902            taker: order.taker,
903            token_id: order.token_id,
904            maker_amount: order.maker_amount,
905            taker_amount: order.taker_amount,
906            expiration,
907            nonce: order.nonce,
908            fee_rate_bps: order.fee_rate_bps,
909            side: order.side,
910            signature_type: order.signature_type,
911            signature: order.signature,
912        })
913    }
914}
915
916#[derive(Debug, Clone, Serialize)]
917#[serde(rename_all = "camelCase")]
918struct RfqOrderActionPayload {
919    pub request_id: String,
920    pub quote_id: String,
921    pub owner: String,
922    #[serde(flatten)]
923    pub order: RfqSignedOrderPayload,
924}
925
926impl RfqOrderActionPayload {
927    fn try_new(
928        request_id: String,
929        quote_id: String,
930        owner: String,
931        order: SignedOrder,
932    ) -> Result<Self, ClobError> {
933        Ok(Self {
934            request_id,
935            quote_id,
936            owner,
937            order: RfqSignedOrderPayload::try_from_signed_order(order)?,
938        })
939    }
940}
941
942#[cfg(test)]
943mod tests {
944    use super::*;
945
946    #[test]
947    fn rfq_action_payload_serializes_flattened_numeric_fields() {
948        let signed = SignedOrder {
949            salt: "123".to_string(),
950            maker: "0xmaker".to_string(),
951            signer: "0xsigner".to_string(),
952            taker: "0xtaker".to_string(),
953            token_id: "42".to_string(),
954            maker_amount: "100".to_string(),
955            taker_amount: "200".to_string(),
956            expiration: "999".to_string(),
957            nonce: "0".to_string(),
958            fee_rate_bps: "0".to_string(),
959            side: Side::BUY,
960            signature_type: SignatureType::EOA,
961            signature: "0xsig".to_string(),
962        };
963
964        let payload = RfqOrderActionPayload::try_new(
965            "req".to_string(),
966            "quote".to_string(),
967            "owner".to_string(),
968            signed,
969        )
970        .unwrap_or_else(|e| panic!("payload should build: {}", e));
971
972        let v = serde_json::to_value(payload).unwrap_or_else(|e| panic!("serialize: {}", e));
973        assert_eq!(v.get("requestId").and_then(|x| x.as_str()), Some("req"));
974        assert_eq!(v.get("quoteId").and_then(|x| x.as_str()), Some("quote"));
975        assert_eq!(v.get("owner").and_then(|x| x.as_str()), Some("owner"));
976        // Flattened order fields exist at top-level
977        assert_eq!(v.get("tokenId").and_then(|x| x.as_str()), Some("42"));
978        assert_eq!(v.get("salt").and_then(|x| x.as_i64()), Some(123));
979        assert_eq!(v.get("expiration").and_then(|x| x.as_i64()), Some(999));
980    }
981}