Skip to main content

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