Skip to main content

clob_client_rust/
client.rs

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