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
23pub 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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(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("e).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 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 = "es.data[0];
726
727 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 = "es.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 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}