clob_client_rust/
client.rs

1use crate::constants::{END_CURSOR, INITIAL_CURSOR};
2use crate::endpoints::*;
3use crate::errors::ClobError;
4// use crate::exchange_consts::ZERO_ADDRESS; // no longer needed; we resolve real exchange addresses dynamically
5use crate::http_helpers::{RequestOptions, get, post};
6use crate::order_builder::{BuilderConfig as ObBuilderConfig, OrderBuilder};
7use crate::signer_adapter::EthersSigner;
8use crate::types::OrderBookSummary;
9use crate::types::{ApiKeyCreds, ApiKeyRaw};
10use crate::types::{
11    Notification, OpenOrder, Order, OrderResponse, OrderType, Reward, SignedOrder, Trade,
12    UserMarketOrder, UserOrder,
13};
14// Removed unused alias import (Signer) after refactor; keep file clean
15use serde::Deserialize;
16use serde_json::Value;
17
18#[allow(dead_code)]
19#[derive(Deserialize)]
20struct OkResp {
21    ok: bool,
22}
23
24#[allow(dead_code)]
25#[derive(Deserialize)]
26struct SuccessResp {
27    success: bool,
28}
29// Helper enums to accept either a bare array or an object with a `data` field
30#[derive(Deserialize)]
31#[serde(untagged)]
32enum MaybeVec<T> {
33    Vec(Vec<T>),
34    Data { data: Vec<T> },
35}
36
37impl<T> MaybeVec<T> {
38    fn into_vec(self) -> Vec<T> {
39        match self {
40            MaybeVec::Vec(v) => v,
41            MaybeVec::Data { data } => data,
42        }
43    }
44}
45
46#[derive(Deserialize)]
47#[serde(untagged)]
48enum MaybeItem<T> {
49    Item(T),
50    Data { data: T },
51}
52
53impl<T> MaybeItem<T> {
54    fn into_item(self) -> T {
55        match self {
56            MaybeItem::Item(i) => i,
57            MaybeItem::Data { data } => data,
58        }
59    }
60}
61use std::collections::HashMap;
62use std::sync::Arc;
63
64/// TypeScript parity payload for cancellation: `{ orderID: string }`.
65/// 使用 serde rename 保持 JSON 键与 TS 保持一致。
66#[derive(Clone, Debug, serde::Serialize)]
67pub struct OrderPayloadParity {
68    #[serde(rename = "orderID")]
69    pub order_id: String,
70}
71
72impl OrderPayloadParity {
73    pub fn new(order_id: impl Into<String>) -> Self {
74        Self {
75            order_id: order_id.into(),
76        }
77    }
78}
79
80pub struct ClobClient {
81    pub host: String,
82    pub chain_id: i64,
83    pub signer: Option<Arc<EthersSigner>>,
84    pub creds: Option<ApiKeyCreds>,
85    pub use_server_time: bool,
86    pub tick_sizes: HashMap<String, String>,
87    pub neg_risk: HashMap<String, bool>,
88    pub fee_rates: HashMap<String, f64>,
89    // Optional Builder signer for builder-authenticated flows
90    pub builder_signer: Option<builder_signing_sdk_rs::BuilderSigner>,
91    // Optional default builder config for order creation
92    pub builder_config: Option<ObBuilderConfig>,
93}
94
95/// TypeScript `OrderMarketCancelParams` parity: `{ market?: string, asset_id?: string }`
96#[derive(Clone, Debug, serde::Serialize)]
97pub struct OrderMarketCancelParams {
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub market: Option<String>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub asset_id: Option<String>,
102}
103
104impl ClobClient {
105    // --- TypeScript parity alias section --------------------------------------------------
106    // These thin wrappers mirror the naming style of the original TypeScript client so that
107    // porting example code is mostly a mechanical rename (camelCase). They delegate to the
108    // existing snake_case Rust methods. Prefer using the snake_case versions in new Rust code.
109
110    /// TS parity alias for `get_markets` (camelCase). Prefer `get_markets` in Rust.
111    #[allow(non_snake_case)]
112    pub async fn getMarkets(
113        &self,
114        params: Option<std::collections::HashMap<String, String>>,
115    ) -> Result<Vec<crate::types::Market>, ClobError> {
116        self.get_markets(params).await
117    }
118
119    /// TS parity alias for `getMarket` -> `get_market`.
120    #[allow(non_snake_case)]
121    pub async fn getMarket(
122        &self,
123        market_id: &str,
124        params: Option<std::collections::HashMap<String, String>>,
125    ) -> Result<crate::types::MarketSummary, ClobError> {
126        self.get_market(market_id, params).await
127    }
128
129    #[allow(non_snake_case)]
130    pub async fn getOrderBook(&mut self, token_id: &str) -> Result<OrderBookSummary, ClobError> {
131        self.get_order_book(token_id).await
132    }
133
134    #[allow(non_snake_case)]
135    pub async fn getTickSize(&mut self, token_id: &str) -> Result<String, ClobError> {
136        self.get_tick_size(token_id).await
137    }
138
139    #[allow(non_snake_case)]
140    pub async fn getNegRisk(&mut self, token_id: &str) -> Result<bool, ClobError> {
141        self.get_neg_risk(token_id).await
142    }
143
144    #[allow(non_snake_case)]
145    pub async fn getFeeRate(&mut self, token_id: &str) -> Result<f64, ClobError> {
146        self.get_fee_rate(token_id).await
147    }
148
149    #[allow(non_snake_case)]
150    pub async fn getOpenOrders(
151        &self,
152        params: Option<std::collections::HashMap<String, String>>,
153    ) -> Result<Vec<SignedOrder>, ClobError> {
154        self.get_open_orders(params).await
155    }
156
157    #[allow(non_snake_case)]
158    pub async fn getOrder(&self, order_id: &str) -> Result<OpenOrder, ClobError> {
159        self.get_order(order_id).await
160    }
161
162    #[allow(non_snake_case)]
163    pub async fn postSignedOrder(
164        &self,
165        signed_order: &SignedOrder,
166    ) -> Result<OrderResponse, ClobError> {
167        self.post_signed_order(signed_order, OrderType::GTC, false)
168            .await
169    }
170
171    #[allow(non_snake_case)]
172    pub async fn postOrders(
173        &self,
174        orders: Vec<SignedOrder>,
175        defer_exec: bool,
176    ) -> Result<Vec<Order>, ClobError> {
177        self.post_orders(orders, defer_exec).await
178    }
179
180    #[allow(non_snake_case)]
181    pub async fn createOrder(
182        &mut self,
183        user_order: UserOrder,
184        options_tick: Option<&str>,
185    ) -> Result<SignedOrder, ClobError> {
186        self.create_order(user_order, options_tick).await
187    }
188
189    #[allow(non_snake_case)]
190    pub async fn createMarketOrder(
191        &mut self,
192        user_market_order: UserMarketOrder,
193        options_tick: Option<&str>,
194    ) -> Result<SignedOrder, ClobError> {
195        self.create_market_order(user_market_order, options_tick)
196            .await
197    }
198
199    #[allow(non_snake_case)]
200    pub async fn createAndPostOrder(
201        &mut self,
202        user_order: UserOrder,
203        options_tick: Option<&str>,
204    ) -> Result<OrderResponse, ClobError> {
205        self.create_and_post_order(user_order, options_tick, None)
206            .await
207    }
208
209    #[allow(non_snake_case)]
210    pub async fn createAndPostMarketOrder(
211        &mut self,
212        user_market_order: UserMarketOrder,
213        options_tick: Option<&str>,
214    ) -> Result<OrderResponse, ClobError> {
215        self.create_and_post_market_order(user_market_order, options_tick, None)
216            .await
217    }
218
219    /// TypeScript parity: `cancelOrder({ orderID })` 发送 DELETE /orders 携带 JSON body。
220    #[allow(non_snake_case)]
221    pub async fn cancelOrder(&self, payload: OrderPayloadParity) -> Result<Value, ClobError> {
222        self.cancel_order_payload_raw(payload).await
223    }
224
225    #[allow(non_snake_case)]
226    pub async fn cancelOrders(&self, order_ids: Vec<String>) -> Result<Vec<Order>, ClobError> {
227        self.cancel_orders(order_ids).await
228    }
229
230    #[allow(non_snake_case)]
231    pub async fn cancelAll(&self) -> Result<Vec<Order>, ClobError> {
232        self.cancel_all().await
233    }
234
235    #[allow(non_snake_case)]
236    pub async fn cancelMarketOrders(
237        &self,
238        payload: OrderMarketCancelParams,
239    ) -> Result<Vec<Order>, ClobError> {
240        self.cancel_market_orders(payload).await
241    }
242
243    #[allow(non_snake_case)]
244    pub async fn isOrderScoring(
245        &self,
246        params: Option<std::collections::HashMap<String, String>>,
247    ) -> Result<crate::types::OrderScoring, ClobError> {
248        self.is_order_scoring(params).await
249    }
250
251    #[allow(non_snake_case)]
252    pub async fn areOrdersScoring(
253        &self,
254        order_ids: Option<Vec<String>>,
255    ) -> Result<crate::types::OrdersScoring, ClobError> {
256        self.are_orders_scoring(order_ids).await
257    }
258
259    #[allow(non_snake_case)]
260    pub async fn getTrades(
261        &self,
262        params: Option<std::collections::HashMap<String, String>>,
263        only_first_page: bool,
264        next_cursor: Option<String>,
265    ) -> Result<Vec<Value>, ClobError> {
266        self.get_trades(params, only_first_page, next_cursor).await
267    }
268
269    #[allow(non_snake_case)]
270    pub async fn getTradesTyped(
271        &self,
272        params: Option<std::collections::HashMap<String, String>>,
273        only_first_page: bool,
274        next_cursor: Option<String>,
275    ) -> Result<Vec<Trade>, ClobError> {
276        self.get_trades_typed(params, only_first_page, next_cursor)
277            .await
278    }
279
280    #[allow(non_snake_case)]
281    pub async fn getNotifications(&self) -> Result<Vec<Notification>, ClobError> {
282        self.get_notifications().await
283    }
284
285    #[allow(non_snake_case)]
286    pub async fn dropNotifications(&self, ids: Option<&Vec<String>>) -> Result<(), ClobError> {
287        self.drop_notifications(ids).await
288    }
289
290    #[allow(non_snake_case)]
291    pub async fn getBalanceAllowance(
292        &self,
293        params: Option<std::collections::HashMap<String, String>>,
294    ) -> Result<crate::types::BalanceAllowanceResponse, ClobError> {
295        self.get_balance_allowance(params).await
296    }
297
298    #[allow(non_snake_case)]
299    pub async fn updateBalanceAllowance(
300        &self,
301        params: Option<std::collections::HashMap<String, String>>,
302    ) -> Result<(), ClobError> {
303        self.update_balance_allowance(params).await
304    }
305
306    #[allow(non_snake_case)]
307    pub async fn getTime(&self) -> Result<u64, ClobError> {
308        self.get_server_time().await
309    }
310
311    // API keys (L2)
312    #[allow(non_snake_case)]
313    pub async fn getApiKeys(&self) -> Result<Vec<crate::types::ApiKeyCreds>, ClobError> {
314        self.get_api_keys().await
315    }
316    #[allow(non_snake_case)]
317    pub async fn deleteApiKey(&self) -> Result<(), ClobError> {
318        self.delete_api_key().await
319    }
320    #[allow(non_snake_case)]
321    pub async fn getClosedOnlyMode(&self) -> Result<crate::types::BanStatus, ClobError> {
322        self.get_closed_only_mode().await
323    }
324
325    // L1-derived API keys
326    #[allow(non_snake_case)]
327    pub async fn createApiKey(&self, nonce: Option<u64>) -> Result<ApiKeyCreds, ClobError> {
328        self.create_api_key(nonce).await
329    }
330    #[allow(non_snake_case)]
331    pub async fn deriveApiKey(
332        &self,
333        params: Option<std::collections::HashMap<String, String>>,
334    ) -> Result<ApiKeyCreds, ClobError> {
335        self.derive_api_key(params).await
336    }
337
338    // Builder API keys
339    #[allow(non_snake_case)]
340    pub async fn createBuilderApiKey(&self) -> Result<ApiKeyCreds, ClobError> {
341        self.create_builder_api_key().await
342    }
343    #[allow(non_snake_case)]
344    pub async fn getBuilderApiKeys(&self) -> Result<Vec<ApiKeyCreds>, ClobError> {
345        self.get_builder_api_keys().await
346    }
347    #[allow(non_snake_case)]
348    pub async fn revokeBuilderApiKey(&self) -> Result<(), ClobError> {
349        self.revoke_builder_api_key().await
350    }
351
352    // Markets/prices aliases
353    #[allow(non_snake_case)]
354    pub async fn getSimplifiedMarkets(
355        &self,
356        params: Option<std::collections::HashMap<String, String>>,
357    ) -> Result<Vec<crate::types::Market>, ClobError> {
358        self.get_simplified_markets(params).await
359    }
360    #[allow(non_snake_case)]
361    pub async fn getSamplingMarkets(
362        &self,
363        params: Option<std::collections::HashMap<String, String>>,
364    ) -> Result<Vec<crate::types::Market>, ClobError> {
365        self.get_sampling_markets(params).await
366    }
367    #[allow(non_snake_case)]
368    pub async fn getSamplingSimplifiedMarkets(
369        &self,
370        params: Option<std::collections::HashMap<String, String>>,
371    ) -> Result<Vec<crate::types::Market>, ClobError> {
372        self.get_sampling_simplified_markets(params).await
373    }
374    #[allow(non_snake_case)]
375    pub async fn getOrderBooks(
376        &self,
377        params: Option<std::collections::HashMap<String, String>>,
378    ) -> Result<Vec<crate::types::OrderBookSummary>, ClobError> {
379        self.get_order_books(params).await
380    }
381    #[allow(non_snake_case)]
382    pub async fn getMidpoint(
383        &self,
384        params: Option<std::collections::HashMap<String, String>>,
385    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
386        self.get_midpoint(params).await
387    }
388    #[allow(non_snake_case)]
389    pub async fn getMidpoints(
390        &self,
391        params: Option<std::collections::HashMap<String, String>>,
392    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
393        self.get_midpoints(params).await
394    }
395    #[allow(non_snake_case)]
396    pub async fn getPrices(
397        &self,
398        params: Option<std::collections::HashMap<String, String>>,
399    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
400        self.get_prices(params).await
401    }
402    #[allow(non_snake_case)]
403    pub async fn getSpreads(
404        &self,
405        params: Option<std::collections::HashMap<String, String>>,
406    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
407        self.get_spreads(params).await
408    }
409    #[allow(non_snake_case)]
410    pub async fn getLastTradesPrices(
411        &self,
412        params: Option<std::collections::HashMap<String, String>>,
413    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
414        self.get_last_trades_prices(params).await
415    }
416    #[allow(non_snake_case)]
417    pub async fn getPricesHistory(
418        &self,
419        params: Option<std::collections::HashMap<String, String>>,
420    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
421        self.get_prices_history(params).await
422    }
423    #[allow(non_snake_case)]
424    pub async fn getMarketTradesEvents(
425        &self,
426        market_id: &str,
427        params: Option<std::collections::HashMap<String, String>>,
428    ) -> Result<Vec<crate::types::Trade>, ClobError> {
429        self.get_market_trades_events(market_id, params).await
430    }
431
432    // Builder trades
433    #[allow(non_snake_case)]
434    pub async fn getBuilderTrades(
435        &self,
436        params: Option<std::collections::HashMap<String, String>>,
437    ) -> Result<Vec<crate::types::BuilderTrade>, ClobError> {
438        self.get_builder_trades(params).await
439    }
440
441    // Rewards
442    #[allow(non_snake_case)]
443    pub async fn getEarningsForUserForDay(
444        &self,
445        params: Option<std::collections::HashMap<String, String>>,
446    ) -> Result<Vec<crate::types::Reward>, ClobError> {
447        self.get_earnings_for_user_for_day(params).await
448    }
449    #[allow(non_snake_case)]
450    pub async fn getEarningsForUserForDayTyped(
451        &self,
452        params: Option<std::collections::HashMap<String, String>>,
453    ) -> Result<Vec<crate::types::Reward>, ClobError> {
454        self.get_rewards_user_for_day_typed(params).await
455    }
456    #[allow(non_snake_case)]
457    pub async fn getTotalEarningsForUserForDay(
458        &self,
459        params: Option<std::collections::HashMap<String, String>>,
460    ) -> Result<Vec<crate::types::Reward>, ClobError> {
461        self.get_total_earnings_for_user_for_day(params).await
462    }
463    #[allow(non_snake_case)]
464    pub async fn getLiquidityRewardPercentages(
465        &self,
466        params: Option<std::collections::HashMap<String, String>>,
467    ) -> Result<std::collections::HashMap<String, f64>, ClobError> {
468        self.get_liquidity_reward_percentages(params).await
469    }
470    #[allow(non_snake_case)]
471    pub async fn getRewardsMarketsCurrent(
472        &self,
473        params: Option<std::collections::HashMap<String, String>>,
474    ) -> Result<Vec<crate::types::Reward>, ClobError> {
475        self.get_rewards_markets_current(params).await
476    }
477    #[allow(non_snake_case)]
478    pub async fn getRewardsEarningsPercentages(
479        &self,
480        params: Option<std::collections::HashMap<String, String>>,
481    ) -> Result<Vec<crate::types::Reward>, ClobError> {
482        self.get_rewards_earnings_percentages(params).await
483    }
484    // --------------------------------------------------------------------------------------
485    pub fn new(
486        host: &str,
487        chain_id: i64,
488        signer: Option<Arc<EthersSigner>>,
489        creds: Option<ApiKeyCreds>,
490        use_server_time: bool,
491    ) -> Self {
492        Self {
493            host: if let Some(stripped) = host.strip_suffix('/') {
494                stripped.to_string()
495            } else {
496                host.to_string()
497            },
498            chain_id,
499            signer,
500            creds,
501            use_server_time,
502            tick_sizes: HashMap::new(),
503            neg_risk: HashMap::new(),
504            fee_rates: HashMap::new(),
505            builder_signer: None,
506            builder_config: None,
507        }
508    }
509
510    /// Configure Builder API signer (builder auth). Secret must be base64 encoded.
511    pub fn with_builder_signer(
512        mut self,
513        key: String,
514        secret_b64: String,
515        passphrase: String,
516    ) -> Self {
517        let creds = builder_signing_sdk_rs::BuilderApiKeyCreds {
518            key,
519            secret: secret_b64,
520            passphrase,
521        };
522        self.builder_signer = Some(builder_signing_sdk_rs::BuilderSigner::new(creds));
523        self
524    }
525
526    /// Set a default builder config for order creation (tick size, neg risk, signature type, funder).
527    pub fn with_builder_config(mut self, cfg: ObBuilderConfig) -> Self {
528        self.builder_config = Some(cfg);
529        self
530    }
531
532    pub async fn get_order_book(&mut self, token_id: &str) -> Result<OrderBookSummary, ClobError> {
533        let endpoint = format!("{}{}", self.host, GET_ORDER_BOOK);
534        let mut params = std::collections::HashMap::new();
535        params.insert("token_id".to_string(), token_id.to_string());
536        let opts = RequestOptions {
537            headers: None,
538            data: None,
539            params: Some(params),
540        };
541        let val = get(&endpoint, Some(opts)).await?;
542        let obs: OrderBookSummary =
543            serde_json::from_value(val).map_err(|e| ClobError::Other(e.to_string()))?;
544        Ok(obs)
545    }
546
547    pub async fn get_tick_size(&mut self, token_id: &str) -> Result<String, ClobError> {
548        if let Some(v) = self.tick_sizes.get(token_id) {
549            return Ok(v.clone());
550        }
551        let endpoint = format!("{}{}", self.host, GET_TICK_SIZE);
552        let mut params = std::collections::HashMap::new();
553        params.insert("token_id".to_string(), token_id.to_string());
554        let opts = RequestOptions {
555            headers: None,
556            data: None,
557            params: Some(params),
558        };
559        let val = get(&endpoint, Some(opts)).await?;
560        let tick = val
561            .get("minimum_tick_size")
562            .and_then(|v| v.as_str())
563            .ok_or(ClobError::Other("invalid tick response".to_string()))?;
564        self.tick_sizes
565            .insert(token_id.to_string(), tick.to_string());
566        Ok(tick.to_string())
567    }
568
569    pub async fn get_neg_risk(&mut self, token_id: &str) -> Result<bool, ClobError> {
570        if let Some(v) = self.neg_risk.get(token_id) {
571            return Ok(*v);
572        }
573        let endpoint = format!("{}{}", self.host, GET_NEG_RISK);
574        let mut params = std::collections::HashMap::new();
575        params.insert("token_id".to_string(), token_id.to_string());
576        let opts = RequestOptions {
577            headers: None,
578            data: None,
579            params: Some(params),
580        };
581        let val = get(&endpoint, Some(opts)).await?;
582        let rr = val
583            .get("neg_risk")
584            .and_then(|v| v.as_bool())
585            .ok_or(ClobError::Other("invalid neg risk response".to_string()))?;
586        self.neg_risk.insert(token_id.to_string(), rr);
587        Ok(rr)
588    }
589
590    pub async fn get_fee_rate(&mut self, token_id: &str) -> Result<f64, ClobError> {
591        if let Some(v) = self.fee_rates.get(token_id) {
592            return Ok(*v);
593        }
594        let endpoint = format!("{}{}", self.host, GET_FEE_RATE);
595        let mut params = std::collections::HashMap::new();
596        params.insert("token_id".to_string(), token_id.to_string());
597        let opts = RequestOptions {
598            headers: None,
599            data: None,
600            params: Some(params),
601        };
602        let val = get(&endpoint, Some(opts)).await?;
603        let fee = val
604            .get("base_fee")
605            .and_then(|v| v.as_f64())
606            .ok_or(ClobError::Other("invalid fee response".to_string()))?;
607        self.fee_rates.insert(token_id.to_string(), fee);
608        Ok(fee)
609    }
610
611    /// Convenience method that accepts a typed `SignedOrder` and posts it to the
612    /// API. Delegates to `post_signed_order` which performs serialization,
613    /// header creation and response parsing. Uses default orderType=GTC.
614    pub async fn post_order(&self, signed_order: &SignedOrder) -> Result<OrderResponse, ClobError> {
615        // Delegate to the typed helper with default GTC order type
616        self.post_signed_order(signed_order, OrderType::GTC, false)
617            .await
618    }
619
620    pub async fn get_api_keys(&self) -> Result<Vec<crate::types::ApiKeyCreds>, ClobError> {
621        if self.creds.is_none() {
622            return Err(ClobError::Other("L2 creds required".to_string()));
623        }
624        let signer_arc = self
625            .signer
626            .as_ref()
627            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
628        let signer_ref: &EthersSigner = signer_arc.as_ref();
629        let endpoint = format!("{}{}", self.host, GET_API_KEYS);
630        let ts = if self.use_server_time {
631            Some(self.get_server_time().await?)
632        } else {
633            None
634        };
635        let headers = crate::headers::create_l2_headers(
636            signer_ref,
637            self.creds.as_ref().unwrap(),
638            "GET",
639            GET_API_KEYS,
640            None,
641            ts,
642        )
643        .await?;
644        let resp: crate::types::ApiKeysResponse = crate::http_helpers::get_typed(
645            &endpoint,
646            Some(RequestOptions::<Value> {
647                headers: Some(headers),
648                data: None,
649                params: None,
650            }),
651        )
652        .await?;
653        Ok(resp.api_keys)
654    }
655
656    pub async fn get_closed_only_mode(&self) -> Result<crate::types::BanStatus, ClobError> {
657        if self.creds.is_none() {
658            return Err(ClobError::Other("L2 creds required".to_string()));
659        }
660        let signer_arc = self
661            .signer
662            .as_ref()
663            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
664        let signer_ref: &EthersSigner = signer_arc.as_ref();
665        let endpoint = format!("{}{}", self.host, CLOSED_ONLY);
666        let ts = if self.use_server_time {
667            Some(self.get_server_time().await?)
668        } else {
669            None
670        };
671        let headers = crate::headers::create_l2_headers(
672            signer_ref,
673            self.creds.as_ref().unwrap(),
674            "GET",
675            CLOSED_ONLY,
676            None,
677            ts,
678        )
679        .await?;
680        let resp: crate::types::BanStatus = crate::http_helpers::get_typed(
681            &endpoint,
682            Some(RequestOptions::<Value> {
683                headers: Some(headers),
684                data: None,
685                params: None,
686            }),
687        )
688        .await?;
689        Ok(resp)
690    }
691
692    pub async fn delete_api_key(&self) -> Result<(), ClobError> {
693        if self.creds.is_none() {
694            return Err(ClobError::Other("L2 creds required".to_string()));
695        }
696        let signer_arc = self
697            .signer
698            .as_ref()
699            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
700        let signer_ref: &EthersSigner = signer_arc.as_ref();
701        let endpoint = format!("{}{}", self.host, DELETE_API_KEY);
702        let ts = if self.use_server_time {
703            Some(self.get_server_time().await?)
704        } else {
705            None
706        };
707        let headers = crate::headers::create_l2_headers(
708            signer_ref,
709            self.creds.as_ref().unwrap(),
710            "DELETE",
711            DELETE_API_KEY,
712            None,
713            ts,
714        )
715        .await?;
716        let _val: () = crate::http_helpers::del_typed::<(), Value>(
717            &endpoint,
718            Some(RequestOptions::<Value> {
719                headers: Some(headers),
720                data: None,
721                params: None,
722            }),
723        )
724        .await?;
725        Ok(())
726    }
727
728    pub async fn get_trades(
729        &self,
730        params: Option<std::collections::HashMap<String, String>>,
731        only_first_page: bool,
732        next_cursor: Option<String>,
733    ) -> Result<Vec<Value>, ClobError> {
734        if self.creds.is_none() {
735            return Err(ClobError::Other("L2 creds required".to_string()));
736        }
737        let signer_arc = self
738            .signer
739            .as_ref()
740            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
741        let signer_ref: &EthersSigner = signer_arc.as_ref();
742        let ts = if self.use_server_time {
743            Some(self.get_server_time().await?)
744        } else {
745            None
746        };
747        let headers = crate::headers::create_l2_headers(
748            signer_ref,
749            self.creds.as_ref().unwrap(),
750            "GET",
751            GET_TRADES,
752            None,
753            ts,
754        )
755        .await?;
756        let mut results: Vec<Value> = vec![];
757        let mut cursor = next_cursor.unwrap_or_else(|| INITIAL_CURSOR.to_string());
758        while cursor != END_CURSOR {
759            if only_first_page && cursor != INITIAL_CURSOR {
760                break;
761            }
762            let mut p = params.clone().unwrap_or_default();
763            p.insert("next_cursor".to_string(), cursor.clone());
764            let val = get(
765                &format!("{}{}", self.host, GET_TRADES),
766                Some(RequestOptions {
767                    headers: Some(headers.clone()),
768                    data: None,
769                    params: Some(p),
770                }),
771            )
772            .await?;
773            let data = val
774                .get("data")
775                .and_then(|v| v.as_array())
776                .cloned()
777                .unwrap_or_default();
778            for item in data {
779                results.push(item);
780            }
781            cursor = val
782                .get("next_cursor")
783                .and_then(|v| v.as_str())
784                .unwrap_or(END_CURSOR)
785                .to_string();
786        }
787        Ok(results)
788    }
789
790    /// Typed variant of get_trades that deserializes each trade into `Trade`.
791    pub async fn get_trades_typed(
792        &self,
793        params: Option<std::collections::HashMap<String, String>>,
794        only_first_page: bool,
795        next_cursor: Option<String>,
796    ) -> Result<Vec<Trade>, ClobError> {
797        let vals = self
798            .get_trades(params, only_first_page, next_cursor)
799            .await?;
800        let mut trades: Vec<Trade> = Vec::new();
801        for v in vals {
802            let t: Trade =
803                serde_json::from_value(v).map_err(|e| ClobError::Other(e.to_string()))?;
804            trades.push(t);
805        }
806        Ok(trades)
807    }
808
809    pub async fn get_notifications(&self) -> Result<Vec<Notification>, ClobError> {
810        if self.creds.is_none() {
811            return Err(ClobError::Other("L2 creds required".to_string()));
812        }
813        let signer_arc = self
814            .signer
815            .as_ref()
816            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
817        let signer_ref: &EthersSigner = signer_arc.as_ref();
818        let endpoint = format!("{}{}", self.host, GET_NOTIFICATIONS);
819        let mut params = std::collections::HashMap::new();
820        params.insert("signature_type".to_string(), "EOA".to_string());
821        let ts = if self.use_server_time {
822            Some(self.get_server_time().await?)
823        } else {
824            None
825        };
826        let headers = crate::headers::create_l2_headers(
827            signer_ref,
828            self.creds.as_ref().unwrap(),
829            "GET",
830            GET_NOTIFICATIONS,
831            None,
832            ts,
833        )
834        .await?;
835        let resp: MaybeVec<Notification> = crate::http_helpers::get_typed(
836            &endpoint,
837            Some(RequestOptions::<Value> {
838                headers: Some(headers),
839                data: None,
840                params: Some(params),
841            }),
842        )
843        .await?;
844        Ok(resp.into_vec())
845    }
846
847    /// Typed variant of get_notifications that deserializes notifications into `Notification`.
848    pub async fn get_notifications_typed(&self) -> Result<Vec<Notification>, ClobError> {
849        // Kept for compatibility: delegate to get_notifications
850        self.get_notifications().await
851    }
852
853    pub async fn drop_notifications(&self, ids: Option<&Vec<String>>) -> Result<(), ClobError> {
854        if self.creds.is_none() {
855            return Err(ClobError::Other("L2 creds required".to_string()));
856        }
857        let signer_arc = self
858            .signer
859            .as_ref()
860            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
861        let signer_ref: &EthersSigner = signer_arc.as_ref();
862        let endpoint = format!("{}{}", self.host, DROP_NOTIFICATIONS);
863        let params = crate::http_helpers::parse_drop_notification_params(ids);
864        let ts = if self.use_server_time {
865            Some(self.get_server_time().await?)
866        } else {
867            None
868        };
869        let headers = crate::headers::create_l2_headers(
870            signer_ref,
871            self.creds.as_ref().unwrap(),
872            "DELETE",
873            DROP_NOTIFICATIONS,
874            None,
875            ts,
876        )
877        .await?;
878        let _raw: SuccessResp = crate::http_helpers::del_typed(
879            &endpoint,
880            Some(RequestOptions::<Value> {
881                headers: Some(headers),
882                data: None,
883                params: Some(params),
884            }),
885        )
886        .await?;
887        Ok(())
888    }
889
890    pub async fn get_balance_allowance(
891        &self,
892        params: Option<std::collections::HashMap<String, String>>,
893    ) -> Result<crate::types::BalanceAllowanceResponse, ClobError> {
894        if self.creds.is_none() {
895            return Err(ClobError::Other("L2 creds required".to_string()));
896        }
897        let signer_arc = self
898            .signer
899            .as_ref()
900            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
901        let signer_ref: &EthersSigner = signer_arc.as_ref();
902        let ts = if self.use_server_time {
903            Some(self.get_server_time().await?)
904        } else {
905            None
906        };
907        let headers = crate::headers::create_l2_headers(
908            signer_ref,
909            self.creds.as_ref().unwrap(),
910            "GET",
911            GET_BALANCE_ALLOWANCE,
912            None,
913            ts,
914        )
915        .await?;
916        let resp: crate::types::BalanceAllowanceResponse = crate::http_helpers::get_typed(
917            &format!("{}{}", self.host, GET_BALANCE_ALLOWANCE),
918            Some(RequestOptions::<Value> {
919                headers: Some(headers),
920                data: None,
921                params,
922            }),
923        )
924        .await?;
925        Ok(resp)
926    }
927
928    pub async fn update_balance_allowance(
929        &self,
930        params: Option<std::collections::HashMap<String, String>>,
931    ) -> Result<(), ClobError> {
932        if self.creds.is_none() {
933            return Err(ClobError::Other("L2 creds required".to_string()));
934        }
935        let signer_arc = self
936            .signer
937            .as_ref()
938            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
939        let signer_ref: &EthersSigner = signer_arc.as_ref();
940        let ts = if self.use_server_time {
941            Some(self.get_server_time().await?)
942        } else {
943            None
944        };
945        let headers = crate::headers::create_l2_headers(
946            signer_ref,
947            self.creds.as_ref().unwrap(),
948            "GET",
949            UPDATE_BALANCE_ALLOWANCE,
950            None,
951            ts,
952        )
953        .await?;
954        // API returns no useful body for update; call and ignore body
955        let _resp: OkResp = crate::http_helpers::get_typed(
956            &format!("{}{}", self.host, UPDATE_BALANCE_ALLOWANCE),
957            Some(RequestOptions::<Value> {
958                headers: Some(headers),
959                data: None,
960                params,
961            }),
962        )
963        .await?;
964        Ok(())
965    }
966
967    /// Typed helper to fetch user rewards for a day (deserializes into `Reward`).
968    /// This endpoint may be public or require params depending on server; we accept optional query params.
969    pub async fn get_rewards_user_for_day_typed(
970        &self,
971        params: Option<std::collections::HashMap<String, String>>,
972    ) -> Result<Vec<Reward>, ClobError> {
973        let val = crate::http_helpers::get(
974            &format!("{}{}", self.host, GET_EARNINGS_FOR_USER_FOR_DAY),
975            Some(RequestOptions {
976                headers: None,
977                data: None,
978                params,
979            }),
980        )
981        .await?;
982
983        let arr = if let Some(a) = val.get("data").and_then(|v| v.as_array()) {
984            a.clone()
985        } else if val.is_array() {
986            val.as_array().cloned().unwrap_or_default()
987        } else {
988            Vec::new()
989        };
990        let mut out: Vec<Reward> = Vec::new();
991        for v in arr {
992            let r: Reward =
993                serde_json::from_value(v).map_err(|e| ClobError::Other(e.to_string()))?;
994            out.push(r);
995        }
996        Ok(out)
997    }
998
999    pub async fn create_order(
1000        &self,
1001        user_order: UserOrder,
1002        options_tick: Option<&str>,
1003    ) -> Result<SignedOrder, ClobError> {
1004        // L1 auth required
1005        self.can_l1_auth()?;
1006        let signer_arc = self
1007            .signer
1008            .as_ref()
1009            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1010        let signer_ref: &EthersSigner = signer_arc.as_ref();
1011        let ob = if let Some(cfg) = &self.builder_config {
1012            OrderBuilder::with_config(signer_ref, self.chain_id, cfg)
1013        } else {
1014            OrderBuilder::new(signer_ref, self.chain_id, None, None)
1015        };
1016        // 动态解析合约地址:根据 chainId 与 neg_risk(token) 选择标准或 negRisk 交易所
1017        let exchange_addr = self.resolve_exchange_address(&user_order.token_id);
1018        let signed = ob
1019            .build_order(&exchange_addr, &user_order, options_tick.unwrap_or("0.01"))
1020            .await?;
1021        Ok(signed)
1022    }
1023
1024    pub async fn create_market_order(
1025        &self,
1026        user_market_order: UserMarketOrder,
1027        options_tick: Option<&str>,
1028    ) -> Result<SignedOrder, ClobError> {
1029        self.can_l1_auth()?;
1030        let signer_arc = self
1031            .signer
1032            .as_ref()
1033            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1034        let signer_ref: &EthersSigner = signer_arc.as_ref();
1035        let ob = if let Some(cfg) = &self.builder_config {
1036            OrderBuilder::with_config(signer_ref, self.chain_id, cfg)
1037        } else {
1038            OrderBuilder::new(signer_ref, self.chain_id, None, None)
1039        };
1040        let exchange_addr = self.resolve_exchange_address(&user_market_order.token_id);
1041        let signed = ob
1042            .build_market_order(
1043                &exchange_addr,
1044                &user_market_order,
1045                options_tick.unwrap_or("0.01"),
1046            )
1047            .await?;
1048        Ok(signed)
1049    }
1050
1051    /// 依据链 ID 与 builder_config.neg_risk 选择 verifyingContract 地址。
1052    /// 注意:当前实现不主动查询 token neg_risk,只使用调用方传入的配置;如需自动检测可在上层先调用 get_neg_risk 并写入 builder_config。
1053    fn resolve_exchange_address(&self, _token_id: &str) -> String {
1054        let neg = self
1055            .builder_config
1056            .as_ref()
1057            .and_then(|c| c.neg_risk)
1058            .unwrap_or(false);
1059        match (self.chain_id, neg) {
1060            (137, false) => "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E".to_string(),
1061            (137, true) => "0xC5d563A36AE78145C45a50134d48A1215220f80a".to_string(),
1062            (80002, false) => "0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40".to_string(),
1063            (80002, true) => "0xC5d563A36AE78145C45a50134d48A1215220f80a".to_string(),
1064            (_other, _) => "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E".to_string(),
1065        }
1066    }
1067
1068    // removed unused builder-auth helpers; builder headers are injected inline where needed
1069
1070    pub async fn post_orders(
1071        &self,
1072        args: Vec<SignedOrder>,
1073        _defer_exec: bool,
1074    ) -> Result<Vec<Order>, ClobError> {
1075        // strong typed version returning parsed orders
1076        if self.creds.is_none() {
1077            return Err(ClobError::Other("L2 creds required".to_string()));
1078        }
1079        let signer_arc = self
1080            .signer
1081            .as_ref()
1082            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1083        let signer_ref: &EthersSigner = signer_arc.as_ref();
1084        let body_str = serde_json::to_string(&args).map_err(|e| ClobError::Other(e.to_string()))?;
1085        // 为了与 TypeScript SDK 保持完全一致的时间戳使用策略,这里在开启 use_server_time 时获取服务器时间用于 L2 HMAC
1086        let ts = if self.use_server_time {
1087            Some(self.get_server_time().await?)
1088        } else {
1089            None
1090        };
1091        let mut headers = crate::headers::create_l2_headers(
1092            signer_ref,
1093            self.creds.as_ref().unwrap(),
1094            "POST",
1095            POST_ORDERS,
1096            Some(&body_str),
1097            ts,
1098        )
1099        .await?;
1100        if let Some(b) = &self.builder_signer {
1101            let b_payload = b
1102                .create_builder_header_payload("POST", POST_ORDERS, Some(&body_str), None)
1103                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1104            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1105        }
1106        let endpoint = format!("{}{}", self.host, POST_ORDERS);
1107        let raw: MaybeVec<Order> = crate::http_helpers::post_typed(
1108            &endpoint,
1109            Some(RequestOptions {
1110                headers: Some(headers),
1111                data: Some(args),
1112                params: None,
1113            }),
1114        )
1115        .await?;
1116        Ok(raw.into_vec())
1117    }
1118
1119    /// Helper: post a single SignedOrder (typed) to the API. Wraps SignedOrder in NewOrder
1120    /// with orderType and performs L2-authenticated POST to POST_ORDER endpoint.
1121    pub async fn post_signed_order(
1122        &self,
1123        signed_order: &SignedOrder,
1124        order_type: OrderType,
1125        defer_exec: bool,
1126    ) -> Result<OrderResponse, ClobError> {
1127        // build headers and post, then parse into Order
1128        if self.creds.is_none() {
1129            return Err(ClobError::Other("L2 creds required".to_string()));
1130        }
1131        let signer_arc = self
1132            .signer
1133            .as_ref()
1134            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1135        let signer_ref: &EthersSigner = signer_arc.as_ref();
1136
1137        // IMPORTANT: Use API key as owner, NOT wallet address
1138        // This matches TypeScript SDK behavior: orderToJson(order, this.creds?.key || "", ...)
1139        let owner = self.creds.as_ref().unwrap().key.clone();
1140
1141        // Convert SignedOrder to NewOrder format
1142        let new_order =
1143            crate::utilities::order_to_json(signed_order, &owner, order_type, defer_exec);
1144
1145        let body_str =
1146            serde_json::to_string(&new_order).map_err(|e| ClobError::Other(e.to_string()))?;
1147        // 使用服务器时间保证与服务器 HMAC 计算节奏一致
1148        let ts = if self.use_server_time {
1149            Some(self.get_server_time().await?)
1150        } else {
1151            None
1152        };
1153        let mut headers = crate::headers::create_l2_headers(
1154            signer_ref,
1155            self.creds.as_ref().unwrap(),
1156            "POST",
1157            POST_ORDER,
1158            Some(&body_str),
1159            ts,
1160        )
1161        .await?;
1162        // Inject builder headers if builder auth configured
1163        if let Some(b) = &self.builder_signer {
1164            let b_payload = b
1165                .create_builder_header_payload("POST", POST_ORDER, Some(&body_str), None)
1166                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1167            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1168        }
1169        let endpoint = format!("{}{}", self.host, POST_ORDER);
1170        let opts = RequestOptions {
1171            headers: Some(headers),
1172            data: Some(new_order),
1173            params: None,
1174        };
1175        let res: MaybeItem<OrderResponse> =
1176            crate::http_helpers::post_typed(&endpoint, Some(opts)).await?;
1177        Ok(res.into_item())
1178    }
1179
1180    /// Typed variant of posting a signed order: posts the signed order and attempts to
1181    /// deserialize the response into an `OrderResponse` (or into the `data` field if present).
1182    pub async fn post_signed_order_typed(
1183        &self,
1184        signed_order: &SignedOrder,
1185        order_type: OrderType,
1186        defer_exec: bool,
1187    ) -> Result<OrderResponse, ClobError> {
1188        self.post_signed_order(signed_order, order_type, defer_exec)
1189            .await
1190    }
1191
1192    /// Convenience: create (build & sign) then immediately post a limit order.
1193    /// orderType defaults to GTC (Good Till Cancelled).
1194    pub async fn create_and_post_order(
1195        &mut self,
1196        user_order: UserOrder,
1197        options_tick: Option<&str>,
1198        order_type: Option<OrderType>,
1199    ) -> Result<OrderResponse, ClobError> {
1200        // 避免在创建阶段发起额外 HTTP: 优先使用调用方提供的 tick 或 builder_config 中的 tick,否则使用默认值
1201        let tick = if let Some(t) = options_tick {
1202            t.to_string()
1203        } else if let Some(cfg_tick) = self
1204            .builder_config
1205            .as_ref()
1206            .and_then(|c| c.tick_size.as_ref())
1207        {
1208            cfg_tick.clone()
1209        } else {
1210            "0.01".to_string()
1211        };
1212        let signed = self.create_order(user_order, Some(&tick)).await?;
1213        let order_type = order_type.unwrap_or(OrderType::GTC);
1214        self.post_signed_order(&signed, order_type, false).await
1215    }
1216
1217    /// Convenience: create (build & sign) then immediately post a market order.
1218    /// orderType defaults to FOK (Fill Or Kill).
1219    pub async fn create_and_post_market_order(
1220        &mut self,
1221        user_market_order: UserMarketOrder,
1222        options_tick: Option<&str>,
1223        order_type: Option<OrderType>,
1224    ) -> Result<OrderResponse, ClobError> {
1225        // 避免在创建阶段发起额外 HTTP: 同上
1226        let tick = if let Some(t) = options_tick {
1227            t.to_string()
1228        } else if let Some(cfg_tick) = self
1229            .builder_config
1230            .as_ref()
1231            .and_then(|c| c.tick_size.as_ref())
1232        {
1233            cfg_tick.clone()
1234        } else {
1235            "0.01".to_string()
1236        };
1237        let signed = self
1238            .create_market_order(user_market_order, Some(&tick))
1239            .await?;
1240        let order_type = order_type.unwrap_or(OrderType::FOK);
1241        self.post_signed_order(&signed, order_type, false).await
1242    }
1243
1244    // 已移除内部 tick size 解析逻辑(resolve_tick/get_tick_size_uncached)以避免隐式网络请求;保留显式 get_tick_size API。
1245    /// Helper: accept typed SignedOrder list and post them to POST_ORDERS endpoint.
1246    pub async fn post_orders_typed(
1247        &self,
1248        orders: Vec<SignedOrder>,
1249        _defer_exec: bool,
1250    ) -> Result<Value, ClobError> {
1251        if self.creds.is_none() {
1252            return Err(ClobError::Other("L2 creds required".to_string()));
1253        }
1254        let signer_arc = self
1255            .signer
1256            .as_ref()
1257            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1258        let signer_ref: &EthersSigner = signer_arc.as_ref();
1259        let body_str =
1260            serde_json::to_string(&orders).map_err(|e| ClobError::Other(e.to_string()))?;
1261        let ts = if self.use_server_time {
1262            Some(self.get_server_time().await?)
1263        } else {
1264            None
1265        };
1266        let headers = crate::headers::create_l2_headers(
1267            signer_ref,
1268            self.creds.as_ref().unwrap(),
1269            "POST",
1270            POST_ORDERS,
1271            Some(&body_str),
1272            ts,
1273        )
1274        .await?;
1275        let headers = if let Some(b) = &self.builder_signer {
1276            let b_payload = b
1277                .create_builder_header_payload("POST", POST_ORDERS, Some(&body_str), None)
1278                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1279            crate::headers::inject_builder_headers(headers, &b_payload)
1280        } else {
1281            headers
1282        };
1283        let endpoint = format!("{}{}", self.host, POST_ORDERS);
1284        let res = crate::http_helpers::post_typed::<Value, Vec<SignedOrder>>(
1285            &endpoint,
1286            Some(RequestOptions {
1287                headers: Some(headers),
1288                data: Some(orders),
1289                params: None,
1290            }),
1291        )
1292        .await?;
1293        Ok(res)
1294    }
1295
1296    /// Post typed orders and return parsed `Vec<Order>` from the response.
1297    /// This wraps the existing `post_orders_typed` which returns a raw JSON Value
1298    /// and attempts to parse either a top-level array or `data` field into `Vec<Order>`.
1299    pub async fn post_orders_typed_parsed(
1300        &self,
1301        orders: Vec<SignedOrder>,
1302        _defer_exec: bool,
1303    ) -> Result<Vec<Order>, ClobError> {
1304        // Build body and headers similarly to post_orders_typed, but use the typed http helper
1305        if self.creds.is_none() {
1306            return Err(ClobError::Other("L2 creds required".to_string()));
1307        }
1308        let signer_arc = self
1309            .signer
1310            .as_ref()
1311            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1312        let signer_ref: &EthersSigner = signer_arc.as_ref();
1313        let body_str =
1314            serde_json::to_string(&orders).map_err(|e| ClobError::Other(e.to_string()))?;
1315        let ts = if self.use_server_time {
1316            Some(self.get_server_time().await?)
1317        } else {
1318            None
1319        };
1320        let headers = crate::headers::create_l2_headers(
1321            signer_ref,
1322            self.creds.as_ref().unwrap(),
1323            "POST",
1324            POST_ORDERS,
1325            Some(&body_str),
1326            ts,
1327        )
1328        .await?;
1329        let headers = if let Some(b) = &self.builder_signer {
1330            let b_payload = b
1331                .create_builder_header_payload("POST", POST_ORDERS, Some(&body_str), None)
1332                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1333            crate::headers::inject_builder_headers(headers, &b_payload)
1334        } else {
1335            headers
1336        };
1337        let endpoint = format!("{}{}", self.host, POST_ORDERS);
1338        let raw: MaybeVec<Order> = crate::http_helpers::post_typed(
1339            &endpoint,
1340            Some(RequestOptions {
1341                headers: Some(headers),
1342                data: Some(orders),
1343                params: None,
1344            }),
1345        )
1346        .await?;
1347        Ok(raw.into_vec())
1348    }
1349
1350    pub async fn cancel_all(&self) -> Result<Vec<Order>, ClobError> {
1351        if self.creds.is_none() {
1352            return Err(ClobError::Other("L2 creds required".to_string()));
1353        }
1354        let signer_arc = self
1355            .signer
1356            .as_ref()
1357            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1358        let signer_ref: &EthersSigner = signer_arc.as_ref();
1359        let ts = if self.use_server_time {
1360            Some(self.get_server_time().await?)
1361        } else {
1362            None
1363        };
1364        let mut headers = crate::headers::create_l2_headers(
1365            signer_ref,
1366            self.creds.as_ref().unwrap(),
1367            "DELETE",
1368            CANCEL_ALL,
1369            None,
1370            ts,
1371        )
1372        .await?;
1373        if let Some(b) = &self.builder_signer {
1374            let b_payload = b
1375                .create_builder_header_payload("DELETE", CANCEL_ALL, None, None)
1376                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1377            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1378        }
1379        let endpoint = format!("{}{}", self.host, CANCEL_ALL);
1380        let raw: MaybeVec<Order> = crate::http_helpers::del_typed(
1381            &endpoint,
1382            Some(RequestOptions::<Value> {
1383                headers: Some(headers),
1384                data: None,
1385                params: None,
1386            }),
1387        )
1388        .await?;
1389        Ok(raw.into_vec())
1390    }
1391
1392    pub async fn cancel_market_orders(
1393        &self,
1394        payload: OrderMarketCancelParams,
1395    ) -> Result<Vec<Order>, ClobError> {
1396        if self.creds.is_none() {
1397            return Err(ClobError::Other("L2 creds required".to_string()));
1398        }
1399        let signer_arc = self
1400            .signer
1401            .as_ref()
1402            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1403        let signer_ref: &EthersSigner = signer_arc.as_ref();
1404        let body_str =
1405            serde_json::to_string(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
1406        let ts = if self.use_server_time {
1407            Some(self.get_server_time().await?)
1408        } else {
1409            None
1410        };
1411        let mut headers = crate::headers::create_l2_headers(
1412            signer_ref,
1413            self.creds.as_ref().unwrap(),
1414            "DELETE",
1415            CANCEL_MARKET_ORDERS,
1416            Some(&body_str),
1417            ts,
1418        )
1419        .await?;
1420        if let Some(b) = &self.builder_signer {
1421            let b_payload = b
1422                .create_builder_header_payload(
1423                    "DELETE",
1424                    CANCEL_MARKET_ORDERS,
1425                    Some(&body_str),
1426                    None,
1427                )
1428                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1429            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1430        }
1431        let endpoint = format!("{}{}", self.host, CANCEL_MARKET_ORDERS);
1432        // Serialize payload to generic Value for request data
1433        let body_val =
1434            serde_json::to_value(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
1435        let raw: MaybeVec<Order> = crate::http_helpers::del_typed(
1436            &endpoint,
1437            Some(RequestOptions::<Value> {
1438                headers: Some(headers),
1439                data: Some(body_val),
1440                params: None,
1441            }),
1442        )
1443        .await?;
1444        Ok(raw.into_vec())
1445    }
1446
1447    pub async fn is_order_scoring(
1448        &self,
1449        params: Option<std::collections::HashMap<String, String>>,
1450    ) -> Result<crate::types::OrderScoring, ClobError> {
1451        if self.creds.is_none() {
1452            return Err(ClobError::Other("L2 creds required".to_string()));
1453        }
1454        let signer_arc = self
1455            .signer
1456            .as_ref()
1457            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1458        let signer_ref: &EthersSigner = signer_arc.as_ref();
1459        let mut headers = crate::headers::create_l2_headers(
1460            signer_ref,
1461            self.creds.as_ref().unwrap(),
1462            "GET",
1463            IS_ORDER_SCORING,
1464            None,
1465            if self.use_server_time {
1466                Some(self.get_server_time().await?)
1467            } else {
1468                None
1469            },
1470        )
1471        .await?;
1472        if let Some(b) = &self.builder_signer {
1473            let b_payload = b
1474                .create_builder_header_payload("GET", IS_ORDER_SCORING, None, None)
1475                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1476            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1477        }
1478        let resp: crate::types::OrderScoring = crate::http_helpers::get_typed(
1479            &format!("{}{}", self.host, IS_ORDER_SCORING),
1480            Some(RequestOptions::<Value> {
1481                headers: Some(headers),
1482                data: None,
1483                params,
1484            }),
1485        )
1486        .await?;
1487        Ok(resp)
1488    }
1489
1490    pub async fn are_orders_scoring(
1491        &self,
1492        order_ids: Option<Vec<String>>,
1493    ) -> Result<crate::types::OrdersScoring, ClobError> {
1494        if self.creds.is_none() {
1495            return Err(ClobError::Other("L2 creds required".to_string()));
1496        }
1497        let signer_arc = self
1498            .signer
1499            .as_ref()
1500            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1501        let signer_ref: &EthersSigner = signer_arc.as_ref();
1502        let body_str =
1503            serde_json::to_string(&order_ids).map_err(|e| ClobError::Other(e.to_string()))?;
1504        let ts = if self.use_server_time {
1505            Some(self.get_server_time().await?)
1506        } else {
1507            None
1508        };
1509        let mut headers = crate::headers::create_l2_headers(
1510            signer_ref,
1511            self.creds.as_ref().unwrap(),
1512            "POST",
1513            ARE_ORDERS_SCORING,
1514            Some(&body_str),
1515            ts,
1516        )
1517        .await?;
1518        if let Some(b) = &self.builder_signer {
1519            let b_payload = b
1520                .create_builder_header_payload("POST", ARE_ORDERS_SCORING, Some(&body_str), None)
1521                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1522            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1523        }
1524        // Serialize optional ids to JSON Value (null or array)
1525        let body_json =
1526            serde_json::to_value(&order_ids).map_err(|e| ClobError::Other(e.to_string()))?;
1527        let resp: crate::types::OrdersScoring = crate::http_helpers::post_typed(
1528            &format!("{}{}", self.host, ARE_ORDERS_SCORING),
1529            Some(RequestOptions::<Value> {
1530                headers: Some(headers),
1531                data: Some(body_json),
1532                params: None,
1533            }),
1534        )
1535        .await?;
1536        Ok(resp)
1537    }
1538
1539    /// Parity version using JSON body `{ orderID }` instead of query param.
1540    pub async fn cancel_order_payload(
1541        &self,
1542        payload: OrderPayloadParity,
1543    ) -> Result<Order, ClobError> {
1544        if self.creds.is_none() {
1545            return Err(ClobError::Other("L2 creds required".to_string()));
1546        }
1547        let creds = self.creds.as_ref().unwrap();
1548        let signer_arc = self
1549            .signer
1550            .as_ref()
1551            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1552        let signer_ref: &EthersSigner = signer_arc.as_ref();
1553        let body_str =
1554            serde_json::to_string(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
1555        let ts = if self.use_server_time {
1556            Some(self.get_server_time().await?)
1557        } else {
1558            None
1559        };
1560        let mut headers = crate::headers::create_l2_headers(
1561            signer_ref,
1562            creds,
1563            "DELETE",
1564            CANCEL_ORDER,
1565            Some(&body_str),
1566            ts,
1567        )
1568        .await?;
1569        if let Some(b) = &self.builder_signer {
1570            let b_payload = b
1571                .create_builder_header_payload("DELETE", CANCEL_ORDER, Some(&body_str), None)
1572                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1573            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1574        }
1575        let endpoint = format!("{}{}", self.host, CANCEL_ORDER);
1576        // Serialize body as generic Value for RequestOptions
1577        let body_val =
1578            serde_json::to_value(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
1579        let opts: RequestOptions<Value> = RequestOptions::<Value> {
1580            headers: Some(headers),
1581            data: Some(body_val),
1582            params: None,
1583        };
1584        let raw: MaybeItem<Order> = crate::http_helpers::del_typed(&endpoint, Some(opts)).await?;
1585        Ok(raw.into_item())
1586    }
1587
1588    /// Raw JSON variant: return the API response as `serde_json::Value` (parity with TS `any`).
1589    pub async fn cancel_order_payload_raw(
1590        &self,
1591        payload: OrderPayloadParity,
1592    ) -> Result<Value, ClobError> {
1593        if self.creds.is_none() {
1594            return Err(ClobError::Other("L2 creds required".to_string()));
1595        }
1596        let creds = self.creds.as_ref().unwrap();
1597        let signer_arc = self
1598            .signer
1599            .as_ref()
1600            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1601        let signer_ref: &EthersSigner = signer_arc.as_ref();
1602        let body_str =
1603            serde_json::to_string(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
1604        let ts = if self.use_server_time {
1605            Some(self.get_server_time().await?)
1606        } else {
1607            None
1608        };
1609        let mut headers = crate::headers::create_l2_headers(
1610            signer_ref,
1611            creds,
1612            "DELETE",
1613            CANCEL_ORDER,
1614            Some(&body_str),
1615            ts,
1616        )
1617        .await?;
1618        if let Some(b) = &self.builder_signer {
1619            let b_payload = b
1620                .create_builder_header_payload("DELETE", CANCEL_ORDER, Some(&body_str), None)
1621                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1622            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1623        }
1624        let endpoint = format!("{}{}", self.host, CANCEL_ORDER);
1625        let body_val =
1626            serde_json::to_value(&payload).map_err(|e| ClobError::Other(e.to_string()))?;
1627        let opts: RequestOptions<Value> = RequestOptions::<Value> {
1628            headers: Some(headers),
1629            data: Some(body_val),
1630            params: None,
1631        };
1632        let raw: MaybeItem<Value> = crate::http_helpers::del_typed(&endpoint, Some(opts)).await?;
1633        Ok(raw.into_item())
1634    }
1635
1636    pub async fn cancel_orders(&self, order_ids: Vec<String>) -> Result<Vec<Order>, ClobError> {
1637        if self.creds.is_none() {
1638            return Err(ClobError::Other("L2 creds required".to_string()));
1639        }
1640        let creds = self.creds.as_ref().unwrap();
1641        let signer_arc = self
1642            .signer
1643            .as_ref()
1644            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1645        let signer_ref: &EthersSigner = signer_arc.as_ref();
1646        let body_str =
1647            serde_json::to_string(&order_ids).map_err(|e| ClobError::Other(e.to_string()))?;
1648        let ts = if self.use_server_time {
1649            Some(self.get_server_time().await?)
1650        } else {
1651            None
1652        };
1653        let mut headers = crate::headers::create_l2_headers(
1654            signer_ref,
1655            creds,
1656            "DELETE",
1657            CANCEL_ORDERS,
1658            Some(&body_str),
1659            ts,
1660        )
1661        .await?;
1662        if let Some(b) = &self.builder_signer {
1663            let b_payload = b
1664                .create_builder_header_payload("DELETE", CANCEL_ORDERS, Some(&body_str), None)
1665                .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
1666            headers = crate::headers::inject_builder_headers(headers, &b_payload);
1667        }
1668        let endpoint = format!("{}{}", self.host, CANCEL_ORDERS);
1669        let opts = RequestOptions {
1670            headers: Some(headers),
1671            data: Some(order_ids.clone()),
1672            params: None,
1673        };
1674        let raw: MaybeVec<Order> = crate::http_helpers::del_typed(&endpoint, Some(opts)).await?;
1675        Ok(raw.into_vec())
1676    }
1677
1678    pub async fn get_order(&self, order_id: &str) -> Result<OpenOrder, ClobError> {
1679        // 与 TypeScript SDK 保持一致:执行带 L2 鉴权的调用
1680        self.get_order_typed(order_id).await
1681    }
1682
1683    /// Typed variant: try to deserialize an order response into `OpenOrder`.
1684    pub async fn get_order_typed(&self, order_id: &str) -> Result<OpenOrder, ClobError> {
1685        // TS SDK 行为:必须 L2 鉴权(canL2Auth + createL2Headers),useServerTime 时使用服务器时间戳参与 HMAC
1686        if self.creds.is_none() {
1687            return Err(ClobError::Other("L2 creds required".to_string()));
1688        }
1689        let signer_arc = self
1690            .signer
1691            .as_ref()
1692            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1693        let signer_ref: &EthersSigner = signer_arc.as_ref();
1694        // requestPath 需要包含具体 /orders/{id},与 TS 保持完全一致
1695        let request_path = format!("{}{}", GET_ORDER, order_id);
1696        let endpoint = format!("{}{}", self.host, request_path);
1697        let ts = if self.use_server_time {
1698            Some(self.get_server_time().await?)
1699        } else {
1700            None
1701        };
1702        let headers = crate::headers::create_l2_headers(
1703            signer_ref,
1704            self.creds.as_ref().unwrap(),
1705            "GET",
1706            &request_path,
1707            None,
1708            ts,
1709        )
1710        .await?;
1711        let opts = RequestOptions {
1712            headers: Some(headers),
1713            data: None,
1714            params: None,
1715        };
1716        let val = get(&endpoint, Some(opts)).await?;
1717        // API may return object or { data: object }
1718        if val.is_object() && val.get("id").is_some() {
1719            let o: OpenOrder =
1720                serde_json::from_value(val).map_err(|e| ClobError::Other(e.to_string()))?;
1721            Ok(o)
1722        } else if let Some(d) = val.get("data") {
1723            let o: OpenOrder =
1724                serde_json::from_value(d.clone()).map_err(|e| ClobError::Other(e.to_string()))?;
1725            Ok(o)
1726        } else {
1727            Err(ClobError::Other(
1728                "unexpected order response shape".to_string(),
1729            ))
1730        }
1731    }
1732
1733    pub async fn get_open_orders(
1734        &self,
1735        params: Option<std::collections::HashMap<String, String>>,
1736    ) -> Result<Vec<SignedOrder>, ClobError> {
1737        // Delegate to typed variant
1738        self.get_open_orders_typed(params).await
1739    }
1740
1741    /// Typed variant: try to deserialize open orders `data` into Vec<SignedOrder>`.
1742    pub async fn get_open_orders_typed(
1743        &self,
1744        params: Option<std::collections::HashMap<String, String>>,
1745    ) -> Result<Vec<SignedOrder>, ClobError> {
1746        let endpoint = format!("{}{}", self.host, GET_OPEN_ORDERS);
1747        let opts = RequestOptions {
1748            headers: None,
1749            data: None,
1750            params,
1751        };
1752        let val = get(&endpoint, Some(opts)).await?;
1753        // API might return { data: [...] } or an array directly
1754        let arr = if val.is_array() {
1755            val
1756        } else if let Some(d) = val.get("data") {
1757            d.clone()
1758        } else {
1759            return Err(ClobError::Other(
1760                "unexpected open orders response shape".to_string(),
1761            ));
1762        };
1763        let orders: Vec<SignedOrder> =
1764            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
1765        Ok(orders)
1766    }
1767
1768    pub async fn get_markets(
1769        &self,
1770        params: Option<std::collections::HashMap<String, String>>,
1771    ) -> Result<Vec<crate::types::Market>, ClobError> {
1772        let endpoint = format!("{}{}", self.host, GET_MARKETS);
1773        let opts = RequestOptions {
1774            headers: None,
1775            data: None,
1776            params,
1777        };
1778        let val = get(&endpoint, Some(opts)).await?;
1779        // API may return an array or an object containing `data: [...]`
1780        let arr = if val.is_array() {
1781            val
1782        } else if let Some(d) = val.get("data") {
1783            d.clone()
1784        } else {
1785            return Err(ClobError::Other(
1786                "unexpected markets response shape".to_string(),
1787            ));
1788        };
1789        let markets: Vec<crate::types::Market> =
1790            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
1791        Ok(markets)
1792    }
1793
1794    pub async fn get_market(
1795        &self,
1796        market_id: &str,
1797        params: Option<std::collections::HashMap<String, String>>,
1798    ) -> Result<crate::types::MarketSummary, ClobError> {
1799        let endpoint = format!("{}{}{}", self.host, GET_MARKET, market_id);
1800        let opts = RequestOptions {
1801            headers: None,
1802            data: None,
1803            params,
1804        };
1805        let val = get(&endpoint, Some(opts)).await?;
1806        // Try direct deserialization into MarketSummary or unwrap `data` field
1807        if val.get("market").is_some() {
1808            let m: crate::types::MarketSummary =
1809                serde_json::from_value(val).map_err(|e| ClobError::Other(e.to_string()))?;
1810            Ok(m)
1811        } else if let Some(d) = val.get("data") {
1812            let m: crate::types::MarketSummary =
1813                serde_json::from_value(d.clone()).map_err(|e| ClobError::Other(e.to_string()))?;
1814            Ok(m)
1815        } else {
1816            Err(ClobError::Other(
1817                "unexpected market response shape".to_string(),
1818            ))
1819        }
1820    }
1821
1822    pub async fn get_simplified_markets(
1823        &self,
1824        params: Option<std::collections::HashMap<String, String>>,
1825    ) -> Result<Vec<crate::types::Market>, ClobError> {
1826        let endpoint = format!("{}{}", self.host, GET_SIMPLIFIED_MARKETS);
1827        let opts = RequestOptions {
1828            headers: None,
1829            data: None,
1830            params,
1831        };
1832        let val = get(&endpoint, Some(opts)).await?;
1833        let arr = if val.is_array() {
1834            val
1835        } else if let Some(d) = val.get("data") {
1836            d.clone()
1837        } else {
1838            return Err(ClobError::Other(
1839                "unexpected simplified markets response shape".to_string(),
1840            ));
1841        };
1842        let markets: Vec<crate::types::Market> =
1843            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
1844        Ok(markets)
1845    }
1846
1847    pub async fn get_sampling_markets(
1848        &self,
1849        params: Option<std::collections::HashMap<String, String>>,
1850    ) -> Result<Vec<crate::types::Market>, ClobError> {
1851        let endpoint = format!("{}{}", self.host, GET_SAMPLING_MARKETS);
1852        let opts = RequestOptions {
1853            headers: None,
1854            data: None,
1855            params,
1856        };
1857        let val = get(&endpoint, Some(opts)).await?;
1858        let arr = if val.is_array() {
1859            val
1860        } else if let Some(d) = val.get("data") {
1861            d.clone()
1862        } else {
1863            return Err(ClobError::Other(
1864                "unexpected sampling markets response shape".to_string(),
1865            ));
1866        };
1867        let markets: Vec<crate::types::Market> =
1868            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
1869        Ok(markets)
1870    }
1871
1872    pub async fn get_server_time(&self) -> Result<u64, ClobError> {
1873        let endpoint = format!("{}{}", self.host, TIME);
1874        let val = get(&endpoint, None).await?;
1875        // Expect number or object with `time`
1876        if val.is_number() {
1877            Ok(val
1878                .as_u64()
1879                .ok_or(ClobError::Other("invalid time value".to_string()))?)
1880        } else if val.get("time").is_some() {
1881            Ok(val
1882                .get("time")
1883                .and_then(|v| v.as_u64())
1884                .ok_or(ClobError::Other("invalid time value".to_string()))?)
1885        } else {
1886            Err(ClobError::Other(
1887                "unexpected server time response".to_string(),
1888            ))
1889        }
1890    }
1891
1892    fn can_l1_auth(&self) -> Result<(), ClobError> {
1893        if self.signer.is_none() {
1894            return Err(ClobError::Other("L1 auth required".to_string()));
1895        }
1896        Ok(())
1897    }
1898
1899    pub async fn create_api_key(&self, nonce: Option<u64>) -> Result<ApiKeyCreds, ClobError> {
1900        self.can_l1_auth()?;
1901        let signer_arc = self.signer.as_ref().unwrap();
1902        let signer_ref: &EthersSigner = signer_arc.as_ref();
1903        let ts = if self.use_server_time {
1904            Some(self.get_server_time().await?)
1905        } else {
1906            None
1907        };
1908        let headers =
1909            crate::headers::create_l1_headers(signer_ref, self.chain_id as i32, nonce, ts).await?;
1910        let endpoint = format!("{}{}", self.host, CREATE_API_KEY);
1911        let opts = RequestOptions {
1912            headers: Some(headers),
1913            data: None,
1914            params: None,
1915        };
1916        let val = post(&endpoint, Some(opts)).await?;
1917        // Deserialize into ApiKeyRaw then to ApiKeyCreds
1918        let api_raw: ApiKeyRaw =
1919            serde_json::from_value(val).map_err(|e| ClobError::Other(e.to_string()))?;
1920        let api_key = ApiKeyCreds {
1921            key: api_raw.api_key,
1922            secret: api_raw.secret,
1923            passphrase: api_raw.passphrase,
1924        };
1925        Ok(api_key)
1926    }
1927
1928    // Additional endpoints ported from TypeScript client
1929    pub async fn derive_api_key(
1930        &self,
1931        params: Option<std::collections::HashMap<String, String>>,
1932    ) -> Result<ApiKeyCreds, ClobError> {
1933        self.can_l1_auth()?;
1934        let signer_arc = self.signer.as_ref().unwrap();
1935        let signer_ref: &EthersSigner = signer_arc.as_ref();
1936        let ts = if self.use_server_time {
1937            Some(self.get_server_time().await?)
1938        } else {
1939            None
1940        };
1941        let headers =
1942            crate::headers::create_l1_headers(signer_ref, self.chain_id as i32, None, ts).await?;
1943        let endpoint = format!("{}{}", self.host, DERIVE_API_KEY);
1944        let opts = RequestOptions {
1945            headers: Some(headers),
1946            data: None,
1947            params,
1948        };
1949        let val = get(&endpoint, Some(opts)).await?;
1950        // Deserialize ApiKeyRaw then map to ApiKeyCreds
1951        let api_raw: ApiKeyRaw =
1952            serde_json::from_value(val).map_err(|e| ClobError::Other(e.to_string()))?;
1953        let api_key = ApiKeyCreds {
1954            key: api_raw.api_key,
1955            secret: api_raw.secret,
1956            passphrase: api_raw.passphrase,
1957        };
1958        Ok(api_key)
1959    }
1960
1961    pub async fn create_builder_api_key(&self) -> Result<crate::types::ApiKeyCreds, ClobError> {
1962        if self.creds.is_none() {
1963            return Err(ClobError::Other("L2 creds required".to_string()));
1964        }
1965        let signer_arc = self
1966            .signer
1967            .as_ref()
1968            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
1969        let signer_ref: &EthersSigner = signer_arc.as_ref();
1970        let ts = if self.use_server_time {
1971            Some(self.get_server_time().await?)
1972        } else {
1973            None
1974        };
1975        let headers = crate::headers::create_l2_headers(
1976            signer_ref,
1977            self.creds.as_ref().unwrap(),
1978            "POST",
1979            CREATE_BUILDER_API_KEY,
1980            None,
1981            ts,
1982        )
1983        .await?;
1984        let endpoint = format!("{}{}", self.host, CREATE_BUILDER_API_KEY);
1985        let resp: crate::types::ApiKeyCreds = crate::http_helpers::post_typed(
1986            &endpoint,
1987            Some(RequestOptions::<Value> {
1988                headers: Some(headers),
1989                data: None,
1990                params: None,
1991            }),
1992        )
1993        .await?;
1994        Ok(resp)
1995    }
1996
1997    pub async fn get_builder_api_keys(&self) -> Result<Vec<crate::types::ApiKeyCreds>, ClobError> {
1998        if self.creds.is_none() {
1999            return Err(ClobError::Other("L2 creds required".to_string()));
2000        }
2001        let signer_arc = self
2002            .signer
2003            .as_ref()
2004            .ok_or(ClobError::Other("L1 signer required".to_string()))?;
2005        let signer_ref: &EthersSigner = signer_arc.as_ref();
2006        let ts = if self.use_server_time {
2007            Some(self.get_server_time().await?)
2008        } else {
2009            None
2010        };
2011        let headers = crate::headers::create_l2_headers(
2012            signer_ref,
2013            self.creds.as_ref().unwrap(),
2014            "GET",
2015            GET_BUILDER_API_KEYS,
2016            None,
2017            ts,
2018        )
2019        .await?;
2020        let endpoint = format!("{}{}", self.host, GET_BUILDER_API_KEYS);
2021        let resp: Vec<crate::types::ApiKeyCreds> = crate::http_helpers::get_typed(
2022            &endpoint,
2023            Some(RequestOptions::<Value> {
2024                headers: Some(headers),
2025                data: None,
2026                params: None,
2027            }),
2028        )
2029        .await?;
2030        Ok(resp)
2031    }
2032
2033    pub async fn revoke_builder_api_key(&self) -> Result<(), ClobError> {
2034        if self.builder_signer.is_none() {
2035            return Err(ClobError::Other("Builder signer required".to_string()));
2036        }
2037        let b_signer = self.builder_signer.as_ref().unwrap();
2038        let headers_map = b_signer
2039            .create_builder_header_payload("DELETE", REVOKE_BUILDER_API_KEY, None, None)
2040            .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
2041
2042        let mut headers = crate::headers::Headers::new();
2043        for (k, v) in headers_map {
2044            headers.insert(k, v);
2045        }
2046
2047        let endpoint = format!("{}{}", self.host, REVOKE_BUILDER_API_KEY);
2048        let _val: () = crate::http_helpers::del_typed::<(), Value>(
2049            &endpoint,
2050            Some(RequestOptions::<Value> {
2051                headers: Some(headers),
2052                data: None,
2053                params: None,
2054            }),
2055        )
2056        .await?;
2057        Ok(())
2058    }
2059
2060    pub async fn get_sampling_simplified_markets(
2061        &self,
2062        params: Option<std::collections::HashMap<String, String>>,
2063    ) -> Result<Vec<crate::types::Market>, ClobError> {
2064        let endpoint = format!("{}{}", self.host, GET_SAMPLING_SIMPLIFIED_MARKETS);
2065        let opts = RequestOptions {
2066            headers: None,
2067            data: None,
2068            params,
2069        };
2070        let val = get(&endpoint, Some(opts)).await?;
2071        let arr = if val.is_object() && val.get("data").is_some() {
2072            val.get("data").cloned().unwrap_or_default()
2073        } else if val.is_array() {
2074            val
2075        } else {
2076            return Err(ClobError::Other(
2077                "unexpected sampling simplified markets response shape".to_string(),
2078            ));
2079        };
2080        let markets: Vec<crate::types::Market> =
2081            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2082        Ok(markets)
2083    }
2084
2085    pub async fn get_order_books(
2086        &self,
2087        params: Option<std::collections::HashMap<String, String>>,
2088    ) -> Result<Vec<crate::types::OrderBookSummary>, ClobError> {
2089        let endpoint = format!("{}{}", self.host, GET_ORDER_BOOKS);
2090        let opts = RequestOptions {
2091            headers: None,
2092            data: None,
2093            params,
2094        };
2095        let val = get(&endpoint, Some(opts)).await?;
2096        let arr = if val.is_array() {
2097            val
2098        } else if let Some(d) = val.get("data") {
2099            d.clone()
2100        } else {
2101            return Err(ClobError::Other(
2102                "unexpected order books response shape".to_string(),
2103            ));
2104        };
2105        let books: Vec<crate::types::OrderBookSummary> =
2106            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2107        Ok(books)
2108    }
2109
2110    pub async fn get_midpoint(
2111        &self,
2112        params: Option<std::collections::HashMap<String, String>>,
2113    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
2114        let endpoint = format!("{}{}", self.host, GET_MIDPOINT);
2115        let opts = RequestOptions {
2116            headers: None,
2117            data: None,
2118            params,
2119        };
2120        let val = get(&endpoint, Some(opts)).await?;
2121        let arr = if val.is_array() {
2122            val
2123        } else if let Some(d) = val.get("data") {
2124            d.clone()
2125        } else {
2126            return Err(ClobError::Other(
2127                "unexpected midpoint response shape".to_string(),
2128            ));
2129        };
2130        let prices: Vec<crate::types::MarketPrice> =
2131            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2132        Ok(prices)
2133    }
2134
2135    pub async fn get_midpoints(
2136        &self,
2137        params: Option<std::collections::HashMap<String, String>>,
2138    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
2139        let endpoint = format!("{}{}", self.host, GET_MIDPOINTS);
2140        let opts = RequestOptions {
2141            headers: None,
2142            data: None,
2143            params,
2144        };
2145        let val = get(&endpoint, Some(opts)).await?;
2146        let arr = if val.is_array() {
2147            val
2148        } else if let Some(d) = val.get("data") {
2149            d.clone()
2150        } else {
2151            return Err(ClobError::Other(
2152                "unexpected midpoints response shape".to_string(),
2153            ));
2154        };
2155        let prices: Vec<crate::types::MarketPrice> =
2156            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2157        Ok(prices)
2158    }
2159
2160    pub async fn get_prices(
2161        &self,
2162        params: Option<std::collections::HashMap<String, String>>,
2163    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
2164        let endpoint = format!("{}{}", self.host, GET_PRICES);
2165        let opts = RequestOptions {
2166            headers: None,
2167            data: None,
2168            params,
2169        };
2170        let val = get(&endpoint, Some(opts)).await?;
2171        let arr = if val.is_array() {
2172            val
2173        } else if let Some(d) = val.get("data") {
2174            d.clone()
2175        } else {
2176            return Err(ClobError::Other(
2177                "unexpected prices response shape".to_string(),
2178            ));
2179        };
2180        let prices: Vec<crate::types::MarketPrice> =
2181            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2182        Ok(prices)
2183    }
2184
2185    pub async fn get_spreads(
2186        &self,
2187        params: Option<std::collections::HashMap<String, String>>,
2188    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
2189        let endpoint = format!("{}{}", self.host, GET_SPREADS);
2190        let opts = RequestOptions {
2191            headers: None,
2192            data: None,
2193            params,
2194        };
2195        let val = get(&endpoint, Some(opts)).await?;
2196        let arr = if val.is_array() {
2197            val
2198        } else if let Some(d) = val.get("data") {
2199            d.clone()
2200        } else {
2201            return Err(ClobError::Other(
2202                "unexpected spreads response shape".to_string(),
2203            ));
2204        };
2205        let prices: Vec<crate::types::MarketPrice> =
2206            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2207        Ok(prices)
2208    }
2209
2210    pub async fn get_last_trades_prices(
2211        &self,
2212        params: Option<std::collections::HashMap<String, String>>,
2213    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
2214        let endpoint = format!("{}{}", self.host, GET_LAST_TRADES_PRICES);
2215        let opts = RequestOptions {
2216            headers: None,
2217            data: None,
2218            params,
2219        };
2220        let val = get(&endpoint, Some(opts)).await?;
2221        let arr = if val.is_array() {
2222            val
2223        } else if let Some(d) = val.get("data") {
2224            d.clone()
2225        } else {
2226            return Err(ClobError::Other(
2227                "unexpected last trades prices response shape".to_string(),
2228            ));
2229        };
2230        let prices: Vec<crate::types::MarketPrice> =
2231            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2232        Ok(prices)
2233    }
2234
2235    pub async fn get_prices_history(
2236        &self,
2237        params: Option<std::collections::HashMap<String, String>>,
2238    ) -> Result<Vec<crate::types::MarketPrice>, ClobError> {
2239        let endpoint = format!("{}{}", self.host, GET_PRICES_HISTORY);
2240        let opts = RequestOptions {
2241            headers: None,
2242            data: None,
2243            params,
2244        };
2245        let val = get(&endpoint, Some(opts)).await?;
2246        // Accept array or { data: [...] }
2247        let arr = if val.is_array() {
2248            val
2249        } else if let Some(d) = val.get("data") {
2250            d.clone()
2251        } else {
2252            return Err(ClobError::Other(
2253                "unexpected prices history response shape".to_string(),
2254            ));
2255        };
2256        let prices: Vec<crate::types::MarketPrice> =
2257            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2258        Ok(prices)
2259    }
2260
2261    pub async fn get_market_trades_events(
2262        &self,
2263        market_id: &str,
2264        params: Option<std::collections::HashMap<String, String>>,
2265    ) -> Result<Vec<crate::types::Trade>, ClobError> {
2266        let endpoint = format!("{}{}{}", self.host, GET_MARKET_TRADES_EVENTS, market_id);
2267        let opts = RequestOptions {
2268            headers: None,
2269            data: None,
2270            params,
2271        };
2272        let val = get(&endpoint, Some(opts)).await?;
2273        let arr = if val.is_array() {
2274            val
2275        } else if let Some(d) = val.get("data") {
2276            d.clone()
2277        } else {
2278            return Err(ClobError::Other(
2279                "unexpected market trades events response shape".to_string(),
2280            ));
2281        };
2282        let trades: Vec<crate::types::Trade> =
2283            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2284        Ok(trades)
2285    }
2286
2287    // Rewards endpoints
2288    pub async fn get_earnings_for_user_for_day(
2289        &self,
2290        params: Option<std::collections::HashMap<String, String>>,
2291    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2292        let endpoint = format!("{}{}", self.host, GET_EARNINGS_FOR_USER_FOR_DAY);
2293        let opts = RequestOptions {
2294            headers: None,
2295            data: None,
2296            params,
2297        };
2298        let val = get(&endpoint, Some(opts)).await?;
2299        let arr = if val.is_array() {
2300            val
2301        } else if let Some(d) = val.get("data") {
2302            d.clone()
2303        } else {
2304            return Err(ClobError::Other(
2305                "unexpected earnings response shape".to_string(),
2306            ));
2307        };
2308        let rewards: Vec<crate::types::Reward> =
2309            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2310        Ok(rewards)
2311    }
2312
2313    pub async fn get_total_earnings_for_user_for_day(
2314        &self,
2315        params: Option<std::collections::HashMap<String, String>>,
2316    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2317        let endpoint = format!("{}{}", self.host, GET_TOTAL_EARNINGS_FOR_USER_FOR_DAY);
2318        let opts = RequestOptions {
2319            headers: None,
2320            data: None,
2321            params,
2322        };
2323        let val = get(&endpoint, Some(opts)).await?;
2324        let arr = if val.is_array() {
2325            val
2326        } else if let Some(d) = val.get("data") {
2327            d.clone()
2328        } else {
2329            return Err(ClobError::Other(
2330                "unexpected total earnings response shape".to_string(),
2331            ));
2332        };
2333        let rewards: Vec<crate::types::Reward> =
2334            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2335        Ok(rewards)
2336    }
2337
2338    /// Typed wrapper for total earnings for user for day. Attempts to parse an array of Reward.
2339    pub async fn get_total_earnings_for_user_for_day_typed(
2340        &self,
2341        params: Option<std::collections::HashMap<String, String>>,
2342    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2343        let val = self.get_total_earnings_for_user_for_day(params).await?;
2344        Ok(val)
2345    }
2346
2347    pub async fn get_liquidity_reward_percentages(
2348        &self,
2349        params: Option<std::collections::HashMap<String, String>>,
2350    ) -> Result<std::collections::HashMap<String, f64>, ClobError> {
2351        let endpoint = format!("{}{}", self.host, GET_LIQUIDITY_REWARD_PERCENTAGES);
2352        let opts = RequestOptions {
2353            headers: None,
2354            data: None,
2355            params,
2356        };
2357        let val = get(&endpoint, Some(opts)).await?;
2358        // Accept object or { data: object }
2359        let obj = if val.is_object() {
2360            val
2361        } else if let Some(d) = val.get("data") {
2362            d.clone()
2363        } else {
2364            return Err(ClobError::Other(
2365                "unexpected liquidity percentages response shape".to_string(),
2366            ));
2367        };
2368        let map: std::collections::HashMap<String, f64> =
2369            serde_json::from_value(obj).map_err(|e| ClobError::Other(e.to_string()))?;
2370        Ok(map)
2371    }
2372
2373    /// Typed wrapper for liquidity reward percentages. Attempts to parse into a map of market -> percentage.
2374    pub async fn get_liquidity_reward_percentages_typed(
2375        &self,
2376        params: Option<std::collections::HashMap<String, String>>,
2377    ) -> Result<std::collections::HashMap<String, f64>, ClobError> {
2378        let val = self.get_liquidity_reward_percentages(params).await?;
2379        Ok(val)
2380    }
2381
2382    pub async fn get_rewards_markets_current(
2383        &self,
2384        params: Option<std::collections::HashMap<String, String>>,
2385    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2386        let endpoint = format!("{}{}", self.host, GET_REWARDS_MARKETS_CURRENT);
2387        let opts = RequestOptions {
2388            headers: None,
2389            data: None,
2390            params,
2391        };
2392        let val = get(&endpoint, Some(opts)).await?;
2393        let arr = if val.is_array() {
2394            val
2395        } else if let Some(d) = val.get("data") {
2396            d.clone()
2397        } else {
2398            return Err(ClobError::Other(
2399                "unexpected rewards markets response shape".to_string(),
2400            ));
2401        };
2402        let rewards: Vec<crate::types::Reward> =
2403            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2404        Ok(rewards)
2405    }
2406
2407    pub async fn get_rewards_markets(
2408        &self,
2409        market_id: &str,
2410        params: Option<std::collections::HashMap<String, String>>,
2411    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2412        let endpoint = format!("{}{}{}", self.host, GET_REWARDS_MARKETS, market_id);
2413        let opts = RequestOptions {
2414            headers: None,
2415            data: None,
2416            params,
2417        };
2418        let val = get(&endpoint, Some(opts)).await?;
2419        let arr = if val.is_array() {
2420            val
2421        } else if let Some(d) = val.get("data") {
2422            d.clone()
2423        } else {
2424            return Err(ClobError::Other(
2425                "unexpected rewards markets response shape".to_string(),
2426            ));
2427        };
2428        let rewards: Vec<crate::types::Reward> =
2429            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2430        Ok(rewards)
2431    }
2432
2433    /// Typed wrapper for get_rewards_markets (per-market rewards). Returns Vec<Reward>.
2434    pub async fn get_rewards_markets_typed(
2435        &self,
2436        market_id: &str,
2437        params: Option<std::collections::HashMap<String, String>>,
2438    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2439        let val = self.get_rewards_markets(market_id, params).await?;
2440        Ok(val)
2441    }
2442
2443    pub async fn get_rewards_earnings_percentages(
2444        &self,
2445        params: Option<std::collections::HashMap<String, String>>,
2446    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2447        let endpoint = format!("{}{}", self.host, GET_REWARDS_EARNINGS_PERCENTAGES);
2448        let opts = RequestOptions {
2449            headers: None,
2450            data: None,
2451            params,
2452        };
2453        let val = get(&endpoint, Some(opts)).await?;
2454        let arr = if val.is_array() {
2455            val
2456        } else if let Some(d) = val.get("data") {
2457            d.clone()
2458        } else {
2459            return Err(ClobError::Other(
2460                "unexpected rewards earnings percentages response shape".to_string(),
2461            ));
2462        };
2463        let rewards: Vec<crate::types::Reward> =
2464            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2465        Ok(rewards)
2466    }
2467
2468    /// Typed wrapper for rewards earnings percentages. Returns Vec<Reward> or object parsed into map.
2469    pub async fn get_rewards_earnings_percentages_typed(
2470        &self,
2471        params: Option<std::collections::HashMap<String, String>>,
2472    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2473        let val = self.get_rewards_earnings_percentages(params).await?;
2474        Ok(val)
2475    }
2476
2477    pub async fn get_builder_trades(
2478        &self,
2479        params: Option<std::collections::HashMap<String, String>>,
2480    ) -> Result<Vec<crate::types::BuilderTrade>, ClobError> {
2481        if self.builder_signer.is_none() {
2482            return Err(ClobError::Other("Builder signer required".to_string()));
2483        }
2484        let b_signer = self.builder_signer.as_ref().unwrap();
2485        let headers_map = b_signer
2486            .create_builder_header_payload("GET", GET_BUILDER_TRADES, None, None)
2487            .map_err(|e| ClobError::Other(format!("builder header error: {}", e)))?;
2488
2489        // Convert HashMap<String, String> to Headers
2490        let mut headers = crate::headers::Headers::new();
2491        for (k, v) in headers_map {
2492            headers.insert(k, v);
2493        }
2494
2495        let endpoint = format!("{}{}", self.host, GET_BUILDER_TRADES);
2496        let opts = RequestOptions {
2497            headers: Some(headers),
2498            data: None,
2499            params,
2500        };
2501        let val = get(&endpoint, Some(opts)).await?;
2502        let arr = if val.is_array() {
2503            val
2504        } else if let Some(d) = val.get("data") {
2505            d.clone()
2506        } else {
2507            return Err(ClobError::Other(
2508                "unexpected builder trades response shape".to_string(),
2509            ));
2510        };
2511        let trades: Vec<crate::types::BuilderTrade> =
2512            serde_json::from_value(arr).map_err(|e| ClobError::Other(e.to_string()))?;
2513        Ok(trades)
2514    }
2515
2516    /// Typed variant for builder trades (kept for compatibility)
2517    pub async fn get_builder_trades_typed(
2518        &self,
2519        params: Option<std::collections::HashMap<String, String>>,
2520    ) -> Result<Vec<crate::types::BuilderTrade>, ClobError> {
2521        self.get_builder_trades(params).await
2522    }
2523
2524    /// Typed wrapper for get_earnings_for_user_for_day -> Vec<Reward>
2525    pub async fn get_earnings_for_user_for_day_typed(
2526        &self,
2527        params: Option<std::collections::HashMap<String, String>>,
2528    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2529        let val = self.get_earnings_for_user_for_day(params).await?;
2530        Ok(val)
2531    }
2532
2533    /// Typed wrapper for current rewards markets
2534    pub async fn get_rewards_markets_current_typed(
2535        &self,
2536        params: Option<std::collections::HashMap<String, String>>,
2537    ) -> Result<Vec<crate::types::Reward>, ClobError> {
2538        let val = self.get_rewards_markets_current(params).await?;
2539        Ok(val)
2540    }
2541}