Skip to main content

deribit_http/endpoints/
private.rs

1//! Private endpoints for authenticated API calls
2
3use crate::DeribitHttpClient;
4use crate::constants::endpoints::*;
5use crate::error::HttpError;
6use crate::model::account::Subaccount;
7use crate::model::api_key::{ApiKeyInfo, CreateApiKeyRequest, EditApiKeyRequest};
8use crate::model::position::Position;
9use crate::model::request::mass_quote::MassQuoteRequest;
10use crate::model::request::order::AdvancedOrderType;
11use crate::model::request::order::OrderRequest;
12use crate::model::request::position::MovePositionTrade;
13use crate::model::request::trade::TradesRequest;
14use crate::model::response::api_response::ApiResponse;
15use crate::model::response::deposit::DepositsResponse;
16use crate::model::response::margin::{MarginsResponse, OrderMargin};
17use crate::model::response::mass_quote::MassQuoteResponse;
18use crate::model::response::mmp::{MmpConfig, MmpStatus, SetMmpConfigRequest};
19use crate::model::response::order::LinkedOrderType;
20use crate::model::response::order::{OrderInfoResponse, OrderResponse};
21use crate::model::response::other::{
22    AccountSummariesResponse, AccountSummaryResponse, SettlementsResponse, TransactionLogResponse,
23    TransferResultResponse,
24};
25use crate::model::response::position::MovePositionResult;
26use crate::model::response::subaccount::SubaccountDetails;
27use crate::model::response::transfer::{InternalTransfer, TransfersResponse};
28use crate::model::response::trigger::TriggerOrderHistoryResponse;
29use crate::model::response::withdrawal::WithdrawalsResponse;
30use crate::model::trigger::TriggerFillCondition;
31use crate::model::{
32    TransactionLogRequest, UserTradeResponseByOrder, UserTradeWithPaginationResponse,
33};
34use crate::prelude::Trigger;
35
36fn trigger_str(t: &Trigger) -> &'static str {
37    match t {
38        Trigger::IndexPrice => "index_price",
39        Trigger::MarkPrice => "mark_price",
40        Trigger::LastPrice => "last_price",
41    }
42}
43
44fn linked_order_str(t: &LinkedOrderType) -> &'static str {
45    match t {
46        LinkedOrderType::OneTriggersOther => "one_triggers_other",
47        LinkedOrderType::OneCancelsOther => "one_cancels_other",
48        LinkedOrderType::OneTriggersOneCancelsOther => "one_triggers_one_cancels_other",
49    }
50}
51
52fn trigger_fill_str(t: &TriggerFillCondition) -> &'static str {
53    match t {
54        TriggerFillCondition::FirstHit => "first_hit",
55        TriggerFillCondition::CompleteFill => "complete_fill",
56        TriggerFillCondition::Incremental => "incremental",
57    }
58}
59
60fn advanced_str(a: &AdvancedOrderType) -> &'static str {
61    match a {
62        AdvancedOrderType::Usd => "usd",
63        AdvancedOrderType::Implv => "implv",
64    }
65}
66
67/// Append every settable OrderRequest field to a query-param vector.
68/// Skips `order_id` and `instrument_name`; the caller owns those.
69fn append_order_params(
70    out: &mut Vec<(String, String)>,
71    req: &crate::model::request::order::OrderRequest,
72) {
73    if let Some(a) = req.amount {
74        out.push(("amount".into(), a.to_string()));
75    }
76    if let Some(c) = req.contracts {
77        out.push(("contracts".into(), c.to_string()));
78    }
79    if let Some(t) = req.type_ {
80        out.push(("type".into(), t.as_str().into()));
81    }
82    if let Some(label) = req.label.as_ref() {
83        out.push(("label".into(), label.clone()));
84    }
85    if let Some(p) = req.price {
86        out.push(("price".into(), p.to_string()));
87    }
88    if let Some(tif) = req.time_in_force {
89        out.push(("time_in_force".into(), tif.as_str().into()));
90    }
91    if let Some(d) = req.display_amount {
92        out.push(("display_amount".into(), d.to_string()));
93    }
94    if req.post_only == Some(true) {
95        out.push(("post_only".into(), "true".into()));
96    }
97    if req.reject_post_only == Some(true) {
98        out.push(("reject_post_only".into(), "true".into()));
99    }
100    if req.reduce_only == Some(true) {
101        out.push(("reduce_only".into(), "true".into()));
102    }
103    if let Some(tp) = req.trigger_price {
104        out.push(("trigger_price".into(), tp.to_string()));
105    }
106    if let Some(to) = req.trigger_offset {
107        out.push(("trigger_offset".into(), to.to_string()));
108    }
109    if let Some(t) = req.trigger.as_ref() {
110        out.push(("trigger".into(), trigger_str(t).into()));
111    }
112    if let Some(a) = req.advanced.as_ref() {
113        out.push(("advanced".into(), advanced_str(a).into()));
114    }
115    if req.mmp == Some(true) {
116        out.push(("mmp".into(), "true".into()));
117    }
118    if let Some(vu) = req.valid_until {
119        out.push(("valid_until".into(), vu.to_string()));
120    }
121    if let Some(l) = req.linked_order_type.as_ref() {
122        out.push(("linked_order_type".into(), linked_order_str(l).into()));
123    }
124    if let Some(tfc) = req.trigger_fill_condition.as_ref() {
125        out.push((
126            "trigger_fill_condition".into(),
127            trigger_fill_str(tfc).into(),
128        ));
129    }
130    if let Some(cfg) = req.otoco_config.as_ref()
131        && !cfg.is_empty()
132        && let Ok(json) = serde_json::to_string(cfg)
133    {
134        out.push(("otoco_config".into(), json));
135    }
136}
137
138/// Private endpoints implementation
139impl DeribitHttpClient {
140    /// Get subaccounts
141    ///
142    /// Retrieves the list of subaccounts associated with the main account.
143    ///
144    /// # Arguments
145    ///
146    /// * `with_portfolio` - Include portfolio information (optional)
147    ///
148    /// # Examples
149    ///
150    /// ```rust
151    /// use deribit_http::DeribitHttpClient;
152    ///
153    /// let client = DeribitHttpClient::new();
154    /// // let subaccounts = client.get_subaccounts(Some(true)).await?;
155    /// // tracing::info!("Found {} subaccounts", subaccounts.len());
156    /// ```
157    pub async fn get_subaccounts(
158        &self,
159        with_portfolio: Option<bool>,
160    ) -> Result<Vec<Subaccount>, HttpError> {
161        let mut query_params = Vec::new();
162
163        if let Some(with_portfolio) = with_portfolio {
164            query_params.push(("with_portfolio".to_string(), with_portfolio.to_string()));
165        }
166
167        let query_string = if query_params.is_empty() {
168            String::new()
169        } else {
170            "?".to_string()
171                + &query_params
172                    .iter()
173                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
174                    .collect::<Vec<_>>()
175                    .join("&")
176        };
177
178        let url = format!("{}{}{}", self.base_url(), GET_SUBACCOUNTS, query_string);
179
180        let response = self.make_authenticated_request(&url).await?;
181
182        if !response.status().is_success() {
183            let error_text = response
184                .text()
185                .await
186                .unwrap_or_else(|_| "Unknown error".to_string());
187            return Err(HttpError::RequestFailed(format!(
188                "Get subaccounts failed: {}",
189                error_text
190            )));
191        }
192
193        // Debug: Get raw response text first
194        let response_text = response.text().await.map_err(|e| {
195            HttpError::InvalidResponse(format!("Failed to read response text: {}", e))
196        })?;
197
198        tracing::debug!("Raw API response: {}", response_text);
199
200        let api_response: ApiResponse<Vec<Subaccount>> = serde_json::from_str(&response_text)
201            .map_err(|e| {
202                HttpError::InvalidResponse(format!(
203                    "Failed to parse JSON: {} - Raw response: {}",
204                    e, response_text
205                ))
206            })?;
207
208        if let Some(error) = api_response.error {
209            return Err(HttpError::RequestFailed(format!(
210                "API error: {} - {}",
211                error.code, error.message
212            )));
213        }
214
215        api_response.result.ok_or_else(|| {
216            HttpError::InvalidResponse("No subaccounts data in response".to_string())
217        })
218    }
219
220    /// Get subaccounts details with positions
221    ///
222    /// Retrieves position details for all subaccounts for a specific currency.
223    /// Returns positions aggregated across all subaccounts, including size,
224    /// average entry price, mark price, and P&L information.
225    ///
226    /// # Arguments
227    ///
228    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
229    /// * `with_open_orders` - Include open orders for each subaccount (optional)
230    ///
231    /// # Examples
232    ///
233    /// ```rust
234    /// use deribit_http::DeribitHttpClient;
235    ///
236    /// let client = DeribitHttpClient::new();
237    /// // let details = client.get_subaccounts_details("BTC", Some(true)).await?;
238    /// ```
239    pub async fn get_subaccounts_details(
240        &self,
241        currency: &str,
242        with_open_orders: Option<bool>,
243    ) -> Result<Vec<SubaccountDetails>, HttpError> {
244        let mut query = format!("?currency={}", urlencoding::encode(currency));
245        if let Some(with_open_orders) = with_open_orders {
246            query.push_str(&format!("&with_open_orders={}", with_open_orders));
247        }
248        self.private_get(GET_SUBACCOUNTS_DETAILS, &query).await
249    }
250
251    /// Create a new subaccount
252    ///
253    /// Creates a new subaccount under the main account.
254    ///
255    /// # Returns
256    ///
257    /// Returns the newly created `Subaccount` with its details.
258    ///
259    /// # Errors
260    ///
261    /// Returns `HttpError` if the request fails or if the user is not a main account.
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use deribit_http::DeribitHttpClient;
267    ///
268    /// let client = DeribitHttpClient::new();
269    /// // let subaccount = client.create_subaccount().await?;
270    /// // tracing::info!("Created subaccount with ID: {}", subaccount.id);
271    /// ```
272    pub async fn create_subaccount(&self) -> Result<Subaccount, HttpError> {
273        self.private_get(CREATE_SUBACCOUNT, "").await
274    }
275
276    /// Remove an empty subaccount
277    ///
278    /// Removes a subaccount that has no open positions or pending orders.
279    ///
280    /// # Arguments
281    ///
282    /// * `subaccount_id` - The ID of the subaccount to remove
283    ///
284    /// # Returns
285    ///
286    /// Returns `"ok"` on success.
287    ///
288    /// # Errors
289    ///
290    /// Returns `HttpError` if the request fails or if the subaccount is not empty.
291    ///
292    /// # Examples
293    ///
294    /// ```rust
295    /// use deribit_http::DeribitHttpClient;
296    ///
297    /// let client = DeribitHttpClient::new();
298    /// // let result = client.remove_subaccount(123).await?;
299    /// // assert_eq!(result, "ok");
300    /// ```
301    pub async fn remove_subaccount(&self, subaccount_id: u64) -> Result<String, HttpError> {
302        let query = format!("?subaccount_id={}", subaccount_id);
303        self.private_get(REMOVE_SUBACCOUNT, &query).await
304    }
305
306    /// Change the name of a subaccount
307    ///
308    /// Updates the username for a subaccount.
309    ///
310    /// # Arguments
311    ///
312    /// * `sid` - The subaccount ID
313    /// * `name` - The new username for the subaccount
314    ///
315    /// # Returns
316    ///
317    /// Returns `"ok"` on success.
318    ///
319    /// # Errors
320    ///
321    /// Returns `HttpError` if the request fails.
322    ///
323    /// # Examples
324    ///
325    /// ```rust
326    /// use deribit_http::DeribitHttpClient;
327    ///
328    /// let client = DeribitHttpClient::new();
329    /// // let result = client.change_subaccount_name(123, "new_name").await?;
330    /// // assert_eq!(result, "ok");
331    /// ```
332    pub async fn change_subaccount_name(&self, sid: u64, name: &str) -> Result<String, HttpError> {
333        let query = format!("?sid={}&name={}", sid, urlencoding::encode(name));
334        self.private_get(CHANGE_SUBACCOUNT_NAME, &query).await
335    }
336
337    /// Enable or disable login for a subaccount
338    ///
339    /// Toggles whether a subaccount can log in. If login is disabled and a session
340    /// for the subaccount exists, that session will be terminated.
341    ///
342    /// # Arguments
343    ///
344    /// * `sid` - The subaccount ID
345    /// * `state` - Either `"enable"` or `"disable"`
346    ///
347    /// # Returns
348    ///
349    /// Returns `"ok"` on success.
350    ///
351    /// # Errors
352    ///
353    /// Returns `HttpError` if the request fails.
354    ///
355    /// # Examples
356    ///
357    /// ```rust
358    /// use deribit_http::DeribitHttpClient;
359    ///
360    /// let client = DeribitHttpClient::new();
361    /// // let result = client.toggle_subaccount_login(123, "enable").await?;
362    /// // assert_eq!(result, "ok");
363    /// ```
364    pub async fn toggle_subaccount_login(
365        &self,
366        sid: u64,
367        state: &str,
368    ) -> Result<String, HttpError> {
369        let query = format!("?sid={}&state={}", sid, urlencoding::encode(state));
370        self.private_get(TOGGLE_SUBACCOUNT_LOGIN, &query).await
371    }
372
373    /// Set email address for a subaccount
374    ///
375    /// Assigns an email address to a subaccount. The user will receive an email
376    /// with a confirmation link.
377    ///
378    /// # Arguments
379    ///
380    /// * `sid` - The subaccount ID
381    /// * `email` - The email address to assign
382    ///
383    /// # Returns
384    ///
385    /// Returns `"ok"` on success.
386    ///
387    /// # Errors
388    ///
389    /// Returns `HttpError` if the request fails.
390    ///
391    /// # Examples
392    ///
393    /// ```rust
394    /// use deribit_http::DeribitHttpClient;
395    ///
396    /// let client = DeribitHttpClient::new();
397    /// // let result = client.set_email_for_subaccount(123, "user@example.com").await?;
398    /// // assert_eq!(result, "ok");
399    /// ```
400    pub async fn set_email_for_subaccount(
401        &self,
402        sid: u64,
403        email: &str,
404    ) -> Result<String, HttpError> {
405        let query = format!("?sid={}&email={}", sid, urlencoding::encode(email));
406        self.private_get(SET_EMAIL_FOR_SUBACCOUNT, &query).await
407    }
408
409    /// Enable or disable notifications for a subaccount
410    ///
411    /// Toggles whether the main account receives notifications from a subaccount.
412    ///
413    /// # Arguments
414    ///
415    /// * `sid` - The subaccount ID
416    /// * `state` - `true` to enable notifications, `false` to disable
417    ///
418    /// # Returns
419    ///
420    /// Returns `"ok"` on success.
421    ///
422    /// # Errors
423    ///
424    /// Returns `HttpError` if the request fails.
425    ///
426    /// # Examples
427    ///
428    /// ```rust
429    /// use deribit_http::DeribitHttpClient;
430    ///
431    /// let client = DeribitHttpClient::new();
432    /// // let result = client.toggle_notifications_from_subaccount(123, true).await?;
433    /// // assert_eq!(result, "ok");
434    /// ```
435    pub async fn toggle_notifications_from_subaccount(
436        &self,
437        sid: u64,
438        state: bool,
439    ) -> Result<String, HttpError> {
440        let query = format!("?sid={}&state={}", sid, state);
441        self.private_get(TOGGLE_NOTIFICATIONS_FROM_SUBACCOUNT, &query)
442            .await
443    }
444
445    /// Get transaction log
446    ///
447    /// Retrieves transaction log entries for the account.
448    ///
449    /// # Arguments
450    ///
451    /// * `request` - A `TransactionLogRequest` struct containing:
452    ///   * `currency` - Currency symbol (BTC, ETH, etc.)
453    ///   * `start_timestamp` - Start timestamp in milliseconds (optional)
454    ///   * `end_timestamp` - End timestamp in milliseconds (optional)
455    ///   * `count` - Number of requested items (optional, default 10)
456    ///   * `continuation` - Continuation token for pagination (optional)
457    ///
458    /// # Examples
459    ///
460    /// ```rust
461    /// use deribit_http::DeribitHttpClient;
462    /// use crate::model::TransactionLogRequest;
463    ///
464    /// let client = DeribitHttpClient::new();
465    /// // let request = TransactionLogRequest { currency: "BTC".into(), ..Default::default() };
466    /// // let log = client.get_transaction_log(request).await?;
467    /// ```
468    pub async fn get_transaction_log(
469        &self,
470        request: TransactionLogRequest,
471    ) -> Result<TransactionLogResponse, HttpError> {
472        let mut query_params = vec![
473            ("currency", request.currency.to_string()),
474            ("start_timestamp", request.start_timestamp.to_string()),
475            ("end_timestamp", request.end_timestamp.to_string()),
476        ];
477        if let Some(query) = request.query {
478            query_params.push(("query", query));
479        }
480        if let Some(count) = request.count {
481            query_params.push(("count", count.to_string()));
482        }
483        if let Some(subaccount_id) = request.subaccount_id {
484            query_params.push(("subaccount_id", subaccount_id.to_string()));
485        }
486        if let Some(continuation) = request.continuation {
487            query_params.push(("continuation", continuation.to_string()));
488        }
489        let query = format!(
490            "?{}",
491            query_params
492                .iter()
493                .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
494                .collect::<Vec<_>>()
495                .join("&")
496        );
497        self.private_get(GET_TRANSACTION_LOG, &query).await
498    }
499
500    /// Get deposits
501    ///
502    /// Retrieves the latest user deposits.
503    ///
504    /// # Arguments
505    ///
506    /// * `currency` - Currency symbol (BTC, ETH, etc.)
507    /// * `count` - Number of requested items (optional, default 10)
508    /// * `offset` - Offset for pagination (optional, default 0)
509    ///
510    /// # Examples
511    ///
512    /// ```rust
513    /// use deribit_http::DeribitHttpClient;
514    ///
515    /// let client = DeribitHttpClient::new();
516    /// // let deposits = client.get_deposits("BTC", Some(20), Some(0)).await?;
517    /// // tracing::info!("Found {} deposits", deposits.data.len());
518    /// ```
519    pub async fn get_deposits(
520        &self,
521        currency: &str,
522        count: Option<u32>,
523        offset: Option<u32>,
524    ) -> Result<DepositsResponse, HttpError> {
525        let mut query = format!("?currency={}", urlencoding::encode(currency));
526        if let Some(count) = count {
527            query.push_str(&format!("&count={}", count));
528        }
529        if let Some(offset) = offset {
530            query.push_str(&format!("&offset={}", offset));
531        }
532        self.private_get(GET_DEPOSITS, &query).await
533    }
534
535    /// Get withdrawals
536    ///
537    /// Retrieves the latest user withdrawals.
538    ///
539    /// # Arguments
540    ///
541    /// * `currency` - Currency symbol (BTC, ETH, etc.)
542    /// * `count` - Number of requested items (optional, default 10)
543    /// * `offset` - Offset for pagination (optional, default 0)
544    ///
545    /// # Examples
546    ///
547    /// ```rust
548    /// use deribit_http::DeribitHttpClient;
549    ///
550    /// let client = DeribitHttpClient::new();
551    /// // let withdrawals = client.get_withdrawals("BTC", Some(20), Some(0)).await?;
552    /// // tracing::info!("Found {} withdrawals", withdrawals.data.len());
553    /// ```
554    pub async fn get_withdrawals(
555        &self,
556        currency: &str,
557        count: Option<u32>,
558        offset: Option<u32>,
559    ) -> Result<WithdrawalsResponse, HttpError> {
560        let mut query = format!("?currency={}", urlencoding::encode(currency));
561        if let Some(count) = count {
562            query.push_str(&format!("&count={}", count));
563        }
564        if let Some(offset) = offset {
565            query.push_str(&format!("&offset={}", offset));
566        }
567        self.private_get(GET_WITHDRAWALS, &query).await
568    }
569
570    /// Submit transfer to subaccount
571    ///
572    /// Transfers funds to a subaccount.
573    ///
574    /// # Arguments
575    ///
576    /// * `currency` - Currency symbol (BTC, ETH, etc.)
577    /// * `amount` - Amount of funds to be transferred
578    /// * `destination` - ID of destination subaccount
579    ///
580    /// # Examples
581    ///
582    /// ```rust
583    /// use deribit_http::DeribitHttpClient;
584    ///
585    /// let client = DeribitHttpClient::new();
586    /// // let transfer = client.submit_transfer_to_subaccount("BTC", 0.001, 123).await?;
587    /// // tracing::info!("Transfer ID: {}", transfer.id);
588    /// ```
589    pub async fn submit_transfer_to_subaccount(
590        &self,
591        currency: &str,
592        amount: f64,
593        destination: u64,
594    ) -> Result<TransferResultResponse, HttpError> {
595        let query = format!(
596            "?currency={}&amount={}&destination={}",
597            urlencoding::encode(currency),
598            amount,
599            destination
600        );
601        self.private_get(SUBMIT_TRANSFER_TO_SUBACCOUNT, &query)
602            .await
603    }
604
605    /// Submit transfer to user
606    ///
607    /// Transfers funds to another user.
608    ///
609    /// # Arguments
610    ///
611    /// * `currency` - Currency symbol (BTC, ETH, etc.)
612    /// * `amount` - Amount of funds to be transferred
613    /// * `destination` - Destination wallet address from address book
614    ///
615    /// # Examples
616    ///
617    /// ```rust
618    /// use deribit_http::DeribitHttpClient;
619    ///
620    /// let client = DeribitHttpClient::new();
621    /// // let transfer = client.submit_transfer_to_user("ETH", 0.1, "0x1234...").await?;
622    /// // tracing::info!("Transfer ID: {}", transfer.id);
623    /// ```
624    pub async fn submit_transfer_to_user(
625        &self,
626        currency: &str,
627        amount: f64,
628        destination: &str,
629    ) -> Result<TransferResultResponse, HttpError> {
630        let query = format!(
631            "?currency={}&amount={}&destination={}",
632            urlencoding::encode(currency),
633            amount,
634            urlencoding::encode(destination)
635        );
636        self.private_get(SUBMIT_TRANSFER_TO_USER, &query).await
637    }
638
639    /// Get transfers list
640    ///
641    /// Retrieves the user's internal transfers (between subaccounts or to other users).
642    ///
643    /// # Arguments
644    ///
645    /// * `currency` - Currency symbol (BTC, ETH, etc.)
646    /// * `count` - Number of transfers to retrieve (1-1000, default 10)
647    /// * `offset` - Offset for pagination (default 0)
648    ///
649    /// # Returns
650    ///
651    /// Returns a `TransfersResponse` containing the total count and list of transfers.
652    ///
653    /// # Errors
654    ///
655    /// Returns `HttpError` if the request fails or the response is invalid.
656    ///
657    /// # Examples
658    ///
659    /// ```rust
660    /// use deribit_http::DeribitHttpClient;
661    ///
662    /// let client = DeribitHttpClient::new();
663    /// // let transfers = client.get_transfers("BTC", Some(10), None).await?;
664    /// // tracing::info!("Found {} transfers", transfers.count);
665    /// ```
666    pub async fn get_transfers(
667        &self,
668        currency: &str,
669        count: Option<u32>,
670        offset: Option<u32>,
671    ) -> Result<TransfersResponse, HttpError> {
672        let mut query = format!("?currency={}", urlencoding::encode(currency));
673        if let Some(c) = count {
674            query.push_str(&format!("&count={}", c));
675        }
676        if let Some(o) = offset {
677            query.push_str(&format!("&offset={}", o));
678        }
679        self.private_get(GET_TRANSFERS, &query).await
680    }
681
682    /// Cancel a transfer by ID
683    ///
684    /// Cancels a pending internal transfer.
685    ///
686    /// # Arguments
687    ///
688    /// * `currency` - Currency symbol (BTC, ETH, etc.)
689    /// * `id` - Transfer ID to cancel
690    ///
691    /// # Returns
692    ///
693    /// Returns the cancelled `InternalTransfer`.
694    ///
695    /// # Errors
696    ///
697    /// Returns `HttpError` if the transfer cannot be cancelled or does not exist.
698    ///
699    /// # Examples
700    ///
701    /// ```rust
702    /// use deribit_http::DeribitHttpClient;
703    ///
704    /// let client = DeribitHttpClient::new();
705    /// // let transfer = client.cancel_transfer_by_id("BTC", 123).await?;
706    /// // tracing::info!("Cancelled transfer: {:?}", transfer.state);
707    /// ```
708    pub async fn cancel_transfer_by_id(
709        &self,
710        currency: &str,
711        id: i64,
712    ) -> Result<InternalTransfer, HttpError> {
713        let query = format!("?currency={}&id={}", urlencoding::encode(currency), id);
714        self.private_get(CANCEL_TRANSFER_BY_ID, &query).await
715    }
716
717    /// Submit transfer between subaccounts
718    ///
719    /// Transfers funds between two subaccounts or between a subaccount and the main account.
720    ///
721    /// # Arguments
722    ///
723    /// * `currency` - Currency symbol (BTC, ETH, etc.)
724    /// * `amount` - Amount of funds to transfer
725    /// * `destination` - Destination subaccount ID
726    /// * `source` - Source subaccount ID (optional, defaults to requesting account)
727    ///
728    /// # Returns
729    ///
730    /// Returns the created `InternalTransfer`.
731    ///
732    /// # Errors
733    ///
734    /// Returns `HttpError` if the transfer fails or validation fails.
735    ///
736    /// # Examples
737    ///
738    /// ```rust
739    /// use deribit_http::DeribitHttpClient;
740    ///
741    /// let client = DeribitHttpClient::new();
742    /// // let transfer = client.submit_transfer_between_subaccounts("ETH", 1.5, 20, Some(10)).await?;
743    /// // tracing::info!("Transfer ID: {}", transfer.id);
744    /// ```
745    pub async fn submit_transfer_between_subaccounts(
746        &self,
747        currency: &str,
748        amount: f64,
749        destination: i64,
750        source: Option<i64>,
751    ) -> Result<InternalTransfer, HttpError> {
752        let mut query = format!(
753            "?currency={}&amount={}&destination={}",
754            urlencoding::encode(currency),
755            amount,
756            destination
757        );
758        if let Some(s) = source {
759            query.push_str(&format!("&source={}", s));
760        }
761        self.private_get(SUBMIT_TRANSFER_BETWEEN_SUBACCOUNTS, &query)
762            .await
763    }
764
765    /// Place a buy order
766    ///
767    /// Places a buy order for the specified instrument.
768    ///
769    /// # Arguments
770    ///
771    /// * `request` - The buy order request parameters
772    ///
773    /// # Errors
774    ///
775    /// Returns `HttpError::RequestFailed` if neither `amount` nor `contracts` is set,
776    /// if the upstream API rejects the order, or on network / decoding failure.
777    pub async fn buy_order(&self, request: OrderRequest) -> Result<OrderResponse, HttpError> {
778        self.submit_order(request, BUY, "Buy order").await
779    }
780
781    /// Place a sell order
782    ///
783    /// Places a sell order for the specified instrument.
784    ///
785    /// # Arguments
786    ///
787    /// * `request` - The sell order request parameters
788    ///
789    /// # Errors
790    ///
791    /// Returns `HttpError::RequestFailed` if neither `amount` nor `contracts` is set,
792    /// if the upstream API rejects the order, or on network / decoding failure.
793    pub async fn sell_order(&self, request: OrderRequest) -> Result<OrderResponse, HttpError> {
794        self.submit_order(request, SELL, "Sell order").await
795    }
796
797    async fn submit_order(
798        &self,
799        request: OrderRequest,
800        endpoint: &str,
801        op_name: &str,
802    ) -> Result<OrderResponse, HttpError> {
803        if request.amount.is_none() && request.contracts.is_none() {
804            return Err(HttpError::RequestFailed(format!(
805                "{}: either `amount` or `contracts` must be set",
806                op_name
807            )));
808        }
809
810        let mut query_params: Vec<(String, String)> = Vec::with_capacity(16);
811        query_params.push((
812            "instrument_name".to_string(),
813            request.instrument_name.clone(),
814        ));
815        append_order_params(&mut query_params, &request);
816
817        let query_string = query_params
818            .iter()
819            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
820            .collect::<Vec<_>>()
821            .join("&");
822
823        let url = format!("{}{}?{}", self.base_url(), endpoint, query_string);
824
825        let response = self.make_authenticated_request(&url).await?;
826
827        if !response.status().is_success() {
828            let error_text = response
829                .text()
830                .await
831                .unwrap_or_else(|_| "Unknown error".to_string());
832            return Err(HttpError::RequestFailed(format!(
833                "{} failed: {}",
834                op_name, error_text
835            )));
836        }
837
838        let response_text = response
839            .text()
840            .await
841            .map_err(|e| HttpError::NetworkError(e.to_string()))?;
842
843        tracing::debug!("Raw API response: {}", response_text);
844
845        let api_response: ApiResponse<OrderResponse> = serde_json::from_str(&response_text)
846            .map_err(|e| {
847                HttpError::InvalidResponse(format!(
848                    "Failed to parse JSON: {} - Raw response: {}",
849                    e, response_text
850                ))
851            })?;
852
853        if let Some(error) = api_response.error {
854            return Err(HttpError::RequestFailed(format!(
855                "API error: {} - {}",
856                error.code, error.message
857            )));
858        }
859
860        api_response
861            .result
862            .ok_or_else(|| HttpError::InvalidResponse("No order data in response".to_string()))
863    }
864
865    /// Cancel an order
866    ///
867    /// Cancels an order by its ID.
868    ///
869    /// # Arguments
870    ///
871    /// * `order_id` - The order ID to cancel
872    ///
873    pub async fn cancel_order(&self, order_id: &str) -> Result<OrderInfoResponse, HttpError> {
874        let query = format!("?order_id={}", urlencoding::encode(order_id));
875        self.private_get(CANCEL, &query).await
876    }
877
878    /// Cancel all orders
879    ///
880    /// Cancels all orders for the account.
881    ///
882    /// # Returns
883    ///
884    /// Returns the number of cancelled orders.
885    pub async fn cancel_all(&self) -> Result<u32, HttpError> {
886        self.private_get(CANCEL_ALL, "").await
887    }
888
889    /// Cancel all orders by currency
890    ///
891    /// Cancels all orders for the specified currency.
892    ///
893    /// # Arguments
894    ///
895    /// * `currency` - Currency to cancel orders for (BTC, ETH, USDC, etc.)
896    ///
897    /// # Returns
898    ///
899    /// Returns the number of cancelled orders.
900    pub async fn cancel_all_by_currency(&self, currency: &str) -> Result<u32, HttpError> {
901        let query = format!("?currency={}", urlencoding::encode(currency));
902        self.private_get(CANCEL_ALL_BY_CURRENCY, &query).await
903    }
904
905    /// Cancel all orders by currency pair
906    ///
907    /// Cancels all orders for the specified currency pair.
908    ///
909    /// # Arguments
910    ///
911    /// * `currency_pair` - Currency pair to cancel orders for (e.g., "BTC_USD")
912    ///
913    /// # Returns
914    ///
915    /// Returns the number of cancelled orders.
916    pub async fn cancel_all_by_currency_pair(&self, currency_pair: &str) -> Result<u32, HttpError> {
917        let query = format!("?currency_pair={}", urlencoding::encode(currency_pair));
918        self.private_get(CANCEL_ALL_BY_CURRENCY_PAIR, &query).await
919    }
920
921    /// Cancel all orders by instrument
922    ///
923    /// Cancels all orders for the specified instrument.
924    ///
925    /// # Arguments
926    ///
927    /// * `instrument_name` - Instrument name to cancel orders for (e.g., "BTC-PERPETUAL")
928    ///
929    /// # Returns
930    ///
931    /// Returns the number of cancelled orders.
932    pub async fn cancel_all_by_instrument(&self, instrument_name: &str) -> Result<u32, HttpError> {
933        let query = format!("?instrument_name={}", urlencoding::encode(instrument_name));
934        self.private_get(CANCEL_ALL_BY_INSTRUMENT, &query).await
935    }
936
937    /// Cancel all orders by kind or type
938    ///
939    /// Cancels all orders for the specified kind or type.
940    ///
941    /// # Arguments
942    ///
943    /// * `kind` - Kind of orders to cancel (future, option, spot, etc.) - optional
944    /// * `order_type` - Type of orders to cancel (limit, market, etc.) - optional
945    ///
946    /// # Returns
947    ///
948    /// Returns the number of cancelled orders.
949    pub async fn cancel_all_by_kind_or_type(
950        &self,
951        kind: Option<&str>,
952        order_type: Option<&str>,
953    ) -> Result<u32, HttpError> {
954        let mut query_params = Vec::new();
955        if let Some(kind) = kind {
956            query_params.push(format!("kind={}", urlencoding::encode(kind)));
957        }
958        if let Some(order_type) = order_type {
959            query_params.push(format!("type={}", urlencoding::encode(order_type)));
960        }
961        let query = if query_params.is_empty() {
962            String::new()
963        } else {
964            format!("?{}", query_params.join("&"))
965        };
966        self.private_get(CANCEL_ALL_BY_KIND_OR_TYPE, &query).await
967    }
968
969    /// Cancel orders by label
970    ///
971    /// Cancels all orders with the specified label.
972    ///
973    /// # Arguments
974    ///
975    /// * `label` - Label of orders to cancel
976    ///
977    /// # Returns
978    ///
979    /// Returns the number of cancelled orders.
980    pub async fn cancel_by_label(&self, label: &str) -> Result<u32, HttpError> {
981        let query = format!("?label={}", urlencoding::encode(label));
982        self.private_get(CANCEL_BY_LABEL, &query).await
983    }
984
985    /// Get account summary
986    ///
987    /// Retrieves account summary information including balance, margin, and other account details.
988    ///
989    /// # Arguments
990    ///
991    /// * `currency` - Currency to get summary for (BTC, ETH, USDC, etc.)
992    /// * `extended` - Whether to include extended information
993    ///
994    pub async fn get_account_summary(
995        &self,
996        currency: &str,
997        extended: Option<bool>,
998    ) -> Result<AccountSummaryResponse, HttpError> {
999        let mut query = format!("?currency={}", urlencoding::encode(currency));
1000        if let Some(extended) = extended {
1001            query.push_str(&format!("&extended={}", extended));
1002        }
1003        self.private_get(GET_ACCOUNT_SUMMARY, &query).await
1004    }
1005
1006    /// Get account summaries for all currencies
1007    ///
1008    /// Retrieves a per-currency list of account summaries for the authenticated user.
1009    /// Each summary includes balance, equity, available funds, and margin information
1010    /// for each currency. Unlike `get_account_summary`, this returns data for all
1011    /// currencies at once.
1012    ///
1013    /// # Arguments
1014    ///
1015    /// * `subaccount_id` - Retrieve summaries for a specific subaccount (optional)
1016    /// * `extended` - Include additional account details (id, username, email, type) (optional)
1017    ///
1018    /// # Examples
1019    ///
1020    /// ```rust
1021    /// use deribit_http::DeribitHttpClient;
1022    ///
1023    /// let client = DeribitHttpClient::new();
1024    /// // let summaries = client.get_account_summaries(None, Some(true)).await?;
1025    /// ```
1026    pub async fn get_account_summaries(
1027        &self,
1028        subaccount_id: Option<i64>,
1029        extended: Option<bool>,
1030    ) -> Result<AccountSummariesResponse, HttpError> {
1031        let mut params = Vec::new();
1032        if let Some(subaccount_id) = subaccount_id {
1033            params.push(format!("subaccount_id={}", subaccount_id));
1034        }
1035        if let Some(extended) = extended {
1036            params.push(format!("extended={}", extended));
1037        }
1038        let query = if params.is_empty() {
1039            String::new()
1040        } else {
1041            format!("?{}", params.join("&"))
1042        };
1043        self.private_get(GET_ACCOUNT_SUMMARIES, &query).await
1044    }
1045
1046    /// Get positions
1047    ///
1048    /// Retrieves user positions for the specified currency and kind.
1049    ///
1050    /// # Arguments
1051    ///
1052    /// * `currency` - Currency filter (BTC, ETH, USDC, etc.) - optional
1053    /// * `kind` - Kind filter (future, option, spot, etc.) - optional
1054    /// * `subaccount_id` - Subaccount ID - optional
1055    ///
1056    /// # Examples
1057    ///
1058    /// ```rust
1059    /// use deribit_http::DeribitHttpClient;
1060    ///
1061    /// let client = DeribitHttpClient::new();
1062    /// // let positions = client.get_positions(Some("BTC"), Some("future"), None).await?;
1063    /// // println!("Found {} positions", positions.len());
1064    /// ```
1065    pub async fn get_positions(
1066        &self,
1067        currency: Option<&str>,
1068        kind: Option<&str>,
1069        subaccount_id: Option<i32>,
1070    ) -> Result<Vec<Position>, HttpError> {
1071        let mut params = Vec::new();
1072        if let Some(currency) = currency {
1073            params.push(format!("currency={}", urlencoding::encode(currency)));
1074        }
1075        if let Some(kind) = kind {
1076            params.push(format!("kind={}", urlencoding::encode(kind)));
1077        }
1078        if let Some(subaccount_id) = subaccount_id {
1079            params.push(format!("subaccount_id={}", subaccount_id));
1080        }
1081        let query = if params.is_empty() {
1082            String::new()
1083        } else {
1084            format!("?{}", params.join("&"))
1085        };
1086        self.private_get(GET_POSITIONS, &query).await
1087    }
1088
1089    /// Get position for a specific instrument
1090    ///
1091    /// Retrieves the current position for the specified instrument.
1092    ///
1093    /// # Arguments
1094    ///
1095    /// * `instrument_name` - The name of the instrument to get position for
1096    ///
1097    /// # Returns
1098    ///
1099    /// Returns a vector of positions for the specified instrument
1100    ///
1101    pub async fn get_position(&self, instrument_name: &str) -> Result<Vec<Position>, HttpError> {
1102        let query = format!("?instrument_name={}", urlencoding::encode(instrument_name));
1103        self.private_get(GET_POSITION, &query).await
1104    }
1105
1106    /// Edit an order
1107    ///
1108    /// Edits an existing order.
1109    ///
1110    /// # Arguments
1111    ///
1112    /// * `request` - The edit order request parameters
1113    ///
1114    pub async fn edit_order(&self, request: OrderRequest) -> Result<OrderResponse, HttpError> {
1115        let order_id = request.order_id.clone().ok_or_else(|| {
1116            HttpError::RequestFailed("order_id is required for edit_order".to_string())
1117        })?;
1118        let mut query_params: Vec<(String, String)> = vec![("order_id".into(), order_id)];
1119        append_order_params(&mut query_params, &request);
1120
1121        let query_string = query_params
1122            .iter()
1123            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
1124            .collect::<Vec<_>>()
1125            .join("&");
1126
1127        let url = format!("{}{}?{}", self.base_url(), EDIT, query_string);
1128
1129        let response = self.make_authenticated_request(&url).await?;
1130
1131        if !response.status().is_success() {
1132            let error_text = response
1133                .text()
1134                .await
1135                .unwrap_or_else(|_| "Unknown error".to_string());
1136            return Err(HttpError::RequestFailed(format!(
1137                "Edit order failed: {}",
1138                error_text
1139            )));
1140        }
1141
1142        let api_response: ApiResponse<OrderResponse> = response
1143            .json()
1144            .await
1145            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
1146
1147        if let Some(error) = api_response.error {
1148            return Err(HttpError::RequestFailed(format!(
1149                "API error: {} - {}",
1150                error.code, error.message
1151            )));
1152        }
1153
1154        api_response
1155            .result
1156            .ok_or_else(|| HttpError::InvalidResponse("No order data in response".to_string()))
1157    }
1158
1159    /// Edit an order by label
1160    ///
1161    /// Modifies an order identified by its label. This method works only when there
1162    /// is exactly one open order with the specified label.
1163    ///
1164    /// # Arguments
1165    ///
1166    /// * `request` - The edit order request parameters (must include label and instrument_name)
1167    ///
1168    /// # Examples
1169    ///
1170    /// ```rust
1171    /// use deribit_http::DeribitHttpClient;
1172    /// use deribit_http::model::request::order::OrderRequest;
1173    ///
1174    /// let client = DeribitHttpClient::new();
1175    /// // let request = OrderRequest {
1176    /// //     label: Some("my_order_label".to_string()),
1177    /// //     instrument_name: "BTC-PERPETUAL".to_string(),
1178    /// //     amount: Some(150.0),
1179    /// //     price: Some(50111.0),
1180    /// //     ..Default::default()
1181    /// // };
1182    /// // let result = client.edit_order_by_label(request).await?;
1183    /// ```
1184    pub async fn edit_order_by_label(
1185        &self,
1186        request: OrderRequest,
1187    ) -> Result<OrderResponse, HttpError> {
1188        let label = request.label.ok_or_else(|| {
1189            HttpError::RequestFailed("label is required for edit_order_by_label".to_string())
1190        })?;
1191
1192        let mut query_params = vec![
1193            ("label".to_string(), label),
1194            ("instrument_name".to_string(), request.instrument_name),
1195        ];
1196
1197        if let Some(amount) = request.amount {
1198            query_params.push(("amount".to_string(), amount.to_string()));
1199        }
1200
1201        if let Some(contracts) = request.contracts {
1202            query_params.push(("contracts".to_string(), contracts.to_string()));
1203        }
1204
1205        if let Some(price) = request.price {
1206            query_params.push(("price".to_string(), price.to_string()));
1207        }
1208
1209        if let Some(post_only) = request.post_only
1210            && post_only
1211        {
1212            query_params.push(("post_only".to_string(), "true".to_string()));
1213        }
1214
1215        if let Some(reduce_only) = request.reduce_only
1216            && reduce_only
1217        {
1218            query_params.push(("reduce_only".to_string(), "true".to_string()));
1219        }
1220
1221        if let Some(reject_post_only) = request.reject_post_only
1222            && reject_post_only
1223        {
1224            query_params.push(("reject_post_only".to_string(), "true".to_string()));
1225        }
1226
1227        if let Some(advanced) = request.advanced {
1228            let advanced_str = match advanced {
1229                crate::model::request::order::AdvancedOrderType::Usd => "usd",
1230                crate::model::request::order::AdvancedOrderType::Implv => "implv",
1231            };
1232            query_params.push(("advanced".to_string(), advanced_str.to_string()));
1233        }
1234
1235        if let Some(trigger_price) = request.trigger_price {
1236            query_params.push(("trigger_price".to_string(), trigger_price.to_string()));
1237        }
1238
1239        if let Some(mmp) = request.mmp
1240            && mmp
1241        {
1242            query_params.push(("mmp".to_string(), "true".to_string()));
1243        }
1244
1245        if let Some(valid_until) = request.valid_until {
1246            query_params.push(("valid_until".to_string(), valid_until.to_string()));
1247        }
1248
1249        let query_string = query_params
1250            .iter()
1251            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
1252            .collect::<Vec<_>>()
1253            .join("&");
1254
1255        let url = format!("{}{}?{}", self.base_url(), EDIT_BY_LABEL, query_string);
1256
1257        let response = self.make_authenticated_request(&url).await?;
1258
1259        if !response.status().is_success() {
1260            let error_text = response
1261                .text()
1262                .await
1263                .unwrap_or_else(|_| "Unknown error".to_string());
1264            return Err(HttpError::RequestFailed(format!(
1265                "Edit order by label failed: {}",
1266                error_text
1267            )));
1268        }
1269
1270        let api_response: ApiResponse<OrderResponse> = response
1271            .json()
1272            .await
1273            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
1274
1275        if let Some(error) = api_response.error {
1276            return Err(HttpError::RequestFailed(format!(
1277                "API error: {} - {}",
1278                error.code, error.message
1279            )));
1280        }
1281
1282        api_response
1283            .result
1284            .ok_or_else(|| HttpError::InvalidResponse("No order data in response".to_string()))
1285    }
1286
1287    /// Close an existing position
1288    ///
1289    /// Places a reduce-only order to close an existing position. The order will
1290    /// automatically be set to reduce-only to ensure it only closes the position.
1291    ///
1292    /// # Arguments
1293    ///
1294    /// * `instrument_name` - Instrument identifier (e.g., "BTC-PERPETUAL")
1295    /// * `order_type` - Order type: "market" or "limit"
1296    /// * `price` - Optional price for limit orders (required if order_type is "limit")
1297    ///
1298    /// # Examples
1299    ///
1300    /// ```rust
1301    /// use deribit_http::DeribitHttpClient;
1302    ///
1303    /// let client = DeribitHttpClient::new();
1304    /// // Close position with market order
1305    /// // let result = client.close_position("BTC-PERPETUAL", "market", None).await?;
1306    /// // Close position with limit order
1307    /// // let result = client.close_position("ETH-PERPETUAL", "limit", Some(2500.0)).await?;
1308    /// ```
1309    pub async fn close_position(
1310        &self,
1311        instrument_name: &str,
1312        order_type: &str,
1313        price: Option<f64>,
1314    ) -> Result<OrderResponse, HttpError> {
1315        let mut query = format!(
1316            "?instrument_name={}&type={}",
1317            urlencoding::encode(instrument_name),
1318            urlencoding::encode(order_type)
1319        );
1320        if let Some(price) = price {
1321            query.push_str(&format!("&price={}", price));
1322        }
1323        self.private_get(CLOSE_POSITION, &query).await
1324    }
1325
1326    /// Get margin requirements
1327    ///
1328    /// Calculates margin requirements for a hypothetical order on a given instrument.
1329    /// Returns initial margin and maintenance margin for the specified instrument,
1330    /// quantity, and price.
1331    ///
1332    /// # Arguments
1333    ///
1334    /// * `instrument_name` - Instrument identifier (e.g., "BTC-PERPETUAL")
1335    /// * `amount` - Order size (USD for perpetual/inverse, base currency for options/linear)
1336    /// * `price` - Order price
1337    ///
1338    /// # Examples
1339    ///
1340    /// ```rust
1341    /// use deribit_http::DeribitHttpClient;
1342    ///
1343    /// let client = DeribitHttpClient::new();
1344    /// // let margins = client.get_margins("BTC-PERPETUAL", 10000.0, 50000.0).await?;
1345    /// // println!("Buy margin: {}, Sell margin: {}", margins.buy, margins.sell);
1346    /// ```
1347    pub async fn get_margins(
1348        &self,
1349        instrument_name: &str,
1350        amount: f64,
1351        price: f64,
1352    ) -> Result<MarginsResponse, HttpError> {
1353        let query = format!(
1354            "?instrument_name={}&amount={}&price={}",
1355            urlencoding::encode(instrument_name),
1356            amount,
1357            price
1358        );
1359        self.private_get(GET_MARGINS, &query).await
1360    }
1361
1362    /// Get order margin by IDs
1363    ///
1364    /// Retrieves the initial margin requirements for one or more orders identified
1365    /// by their order IDs. Initial margin is the amount of funds required to open
1366    /// a position with these orders.
1367    ///
1368    /// # Arguments
1369    ///
1370    /// * `ids` - Array of order IDs (e.g., ["ETH-349280", "ETH-349279"])
1371    ///
1372    /// # Examples
1373    ///
1374    /// ```rust
1375    /// use deribit_http::DeribitHttpClient;
1376    ///
1377    /// let client = DeribitHttpClient::new();
1378    /// // let margins = client.get_order_margin_by_ids(&["ETH-349280", "ETH-349279"]).await?;
1379    /// ```
1380    pub async fn get_order_margin_by_ids(
1381        &self,
1382        ids: &[&str],
1383    ) -> Result<Vec<OrderMargin>, HttpError> {
1384        if ids.is_empty() {
1385            return Err(HttpError::RequestFailed(
1386                "ids array cannot be empty".to_string(),
1387            ));
1388        }
1389
1390        // Format IDs as JSON array for the query parameter
1391        let ids_json = serde_json::to_string(ids)
1392            .map_err(|e| HttpError::InvalidResponse(format!("Failed to serialize ids: {}", e)))?;
1393
1394        let query_string = format!("ids={}", urlencoding::encode(&ids_json));
1395        let url = format!(
1396            "{}{}?{}",
1397            self.base_url(),
1398            GET_ORDER_MARGIN_BY_IDS,
1399            query_string
1400        );
1401
1402        let response = self.make_authenticated_request(&url).await?;
1403
1404        if !response.status().is_success() {
1405            let error_text = response
1406                .text()
1407                .await
1408                .unwrap_or_else(|_| "Unknown error".to_string());
1409            return Err(HttpError::RequestFailed(format!(
1410                "Get order margin by IDs failed: {}",
1411                error_text
1412            )));
1413        }
1414
1415        let api_response: ApiResponse<Vec<OrderMargin>> = response
1416            .json()
1417            .await
1418            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
1419
1420        if let Some(error) = api_response.error {
1421            return Err(HttpError::RequestFailed(format!(
1422                "API error: {} - {}",
1423                error.code, error.message
1424            )));
1425        }
1426
1427        api_response.result.ok_or_else(|| {
1428            HttpError::InvalidResponse("No order margin data in response".to_string())
1429        })
1430    }
1431
1432    /// Get order state by label
1433    ///
1434    /// Retrieves the state of recent orders that have a specific label.
1435    /// Results are filtered by currency and label. The response includes
1436    /// order details such as status, filled amount, remaining amount, and
1437    /// other order properties for all orders with the specified label.
1438    ///
1439    /// # Arguments
1440    ///
1441    /// * `currency` - Currency symbol (e.g., "BTC", "ETH", "USDC")
1442    /// * `label` - User-defined label (max 64 characters)
1443    ///
1444    /// # Examples
1445    ///
1446    /// ```rust
1447    /// use deribit_http::DeribitHttpClient;
1448    ///
1449    /// let client = DeribitHttpClient::new();
1450    /// // let orders = client.get_order_state_by_label("ETH", "myLabel").await?;
1451    /// ```
1452    pub async fn get_order_state_by_label(
1453        &self,
1454        currency: &str,
1455        label: &str,
1456    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
1457        let query = format!(
1458            "?currency={}&label={}",
1459            urlencoding::encode(currency),
1460            urlencoding::encode(label)
1461        );
1462        self.private_get(GET_ORDER_STATE_BY_LABEL, &query).await
1463    }
1464
1465    /// Get settlement history by currency
1466    ///
1467    /// Retrieves settlement, delivery, and bankruptcy events that have affected
1468    /// your account for a specific currency. Settlements occur when futures or
1469    /// options contracts expire and are settled at the delivery price.
1470    ///
1471    /// # Arguments
1472    ///
1473    /// * `currency` - Currency symbol (e.g., "BTC", "ETH", "USDC")
1474    /// * `settlement_type` - Settlement type: "settlement", "delivery", or "bankruptcy" (optional)
1475    /// * `count` - Number of items (default 20, max 1000) (optional)
1476    /// * `continuation` - Pagination token (optional)
1477    /// * `search_start_timestamp` - Latest timestamp to return results from in ms (optional)
1478    ///
1479    /// # Examples
1480    ///
1481    /// ```rust
1482    /// use deribit_http::DeribitHttpClient;
1483    ///
1484    /// let client = DeribitHttpClient::new();
1485    /// // let history = client.get_settlement_history_by_currency("BTC", None, None, None, None).await?;
1486    /// ```
1487    pub async fn get_settlement_history_by_currency(
1488        &self,
1489        currency: &str,
1490        settlement_type: Option<&str>,
1491        count: Option<u32>,
1492        continuation: Option<&str>,
1493        search_start_timestamp: Option<u64>,
1494    ) -> Result<SettlementsResponse, HttpError> {
1495        let mut query = format!("?currency={}", urlencoding::encode(currency));
1496        if let Some(settlement_type) = settlement_type {
1497            query.push_str(&format!("&type={}", urlencoding::encode(settlement_type)));
1498        }
1499        if let Some(count) = count {
1500            query.push_str(&format!("&count={}", count));
1501        }
1502        if let Some(continuation) = continuation {
1503            query.push_str(&format!(
1504                "&continuation={}",
1505                urlencoding::encode(continuation)
1506            ));
1507        }
1508        if let Some(search_start_timestamp) = search_start_timestamp {
1509            query.push_str(&format!(
1510                "&search_start_timestamp={}",
1511                search_start_timestamp
1512            ));
1513        }
1514        self.private_get(GET_SETTLEMENT_HISTORY_BY_CURRENCY, &query)
1515            .await
1516    }
1517
1518    /// Get settlement history by instrument
1519    ///
1520    /// Retrieves settlement, delivery, and bankruptcy events for a specific
1521    /// instrument that have affected your account. Settlements occur when futures
1522    /// or options contracts expire and are settled at the delivery price.
1523    ///
1524    /// # Arguments
1525    ///
1526    /// * `instrument_name` - Instrument identifier (e.g., "BTC-PERPETUAL")
1527    /// * `settlement_type` - Settlement type: "settlement", "delivery", or "bankruptcy" (optional)
1528    /// * `count` - Number of items (default 20, max 1000) (optional)
1529    /// * `continuation` - Pagination token (optional)
1530    /// * `search_start_timestamp` - Latest timestamp to return results from in ms (optional)
1531    ///
1532    /// # Examples
1533    ///
1534    /// ```rust
1535    /// use deribit_http::DeribitHttpClient;
1536    ///
1537    /// let client = DeribitHttpClient::new();
1538    /// // let history = client.get_settlement_history_by_instrument("BTC-PERPETUAL", None, None, None, None).await?;
1539    /// ```
1540    pub async fn get_settlement_history_by_instrument(
1541        &self,
1542        instrument_name: &str,
1543        settlement_type: Option<&str>,
1544        count: Option<u32>,
1545        continuation: Option<&str>,
1546        search_start_timestamp: Option<u64>,
1547    ) -> Result<SettlementsResponse, HttpError> {
1548        let mut query = format!("?instrument_name={}", urlencoding::encode(instrument_name));
1549        if let Some(settlement_type) = settlement_type {
1550            query.push_str(&format!("&type={}", urlencoding::encode(settlement_type)));
1551        }
1552        if let Some(count) = count {
1553            query.push_str(&format!("&count={}", count));
1554        }
1555        if let Some(continuation) = continuation {
1556            query.push_str(&format!(
1557                "&continuation={}",
1558                urlencoding::encode(continuation)
1559            ));
1560        }
1561        if let Some(search_start_timestamp) = search_start_timestamp {
1562            query.push_str(&format!(
1563                "&search_start_timestamp={}",
1564                search_start_timestamp
1565            ));
1566        }
1567        self.private_get(GET_SETTLEMENT_HISTORY_BY_INSTRUMENT, &query)
1568            .await
1569    }
1570
1571    /// Get trigger order history
1572    ///
1573    /// Retrieves a detailed log of all trigger orders (stop orders, take-profit orders, etc.)
1574    /// for the authenticated account. The log includes trigger order creation, activation,
1575    /// execution, and cancellation events.
1576    ///
1577    /// # Arguments
1578    ///
1579    /// * `currency` - Currency symbol (e.g., "BTC", "ETH", "USDC")
1580    /// * `instrument_name` - Filter by specific instrument (optional)
1581    /// * `count` - Number of items (default 20, max 1000) (optional)
1582    /// * `continuation` - Pagination token (optional)
1583    ///
1584    /// # Examples
1585    ///
1586    /// ```rust
1587    /// use deribit_http::DeribitHttpClient;
1588    ///
1589    /// let client = DeribitHttpClient::new();
1590    /// // let history = client.get_trigger_order_history("BTC", None, None, None).await?;
1591    /// ```
1592    pub async fn get_trigger_order_history(
1593        &self,
1594        currency: &str,
1595        instrument_name: Option<&str>,
1596        count: Option<u32>,
1597        continuation: Option<&str>,
1598    ) -> Result<TriggerOrderHistoryResponse, HttpError> {
1599        let mut query = format!("?currency={}", urlencoding::encode(currency));
1600        if let Some(instrument_name) = instrument_name {
1601            query.push_str(&format!(
1602                "&instrument_name={}",
1603                urlencoding::encode(instrument_name)
1604            ));
1605        }
1606        if let Some(count) = count {
1607            query.push_str(&format!("&count={}", count));
1608        }
1609        if let Some(continuation) = continuation {
1610            query.push_str(&format!(
1611                "&continuation={}",
1612                urlencoding::encode(continuation)
1613            ));
1614        }
1615        self.private_get(GET_TRIGGER_ORDER_HISTORY, &query).await
1616    }
1617
1618    /// Move positions between subaccounts
1619    ///
1620    /// Moves positions from a source subaccount to a target subaccount. This operation
1621    /// transfers open positions between subaccounts, which is useful for rebalancing
1622    /// or reorganizing trading activities.
1623    ///
1624    /// **Rate Limits**: 6 requests/minute, 100 move_position uses per week (168 hours)
1625    ///
1626    /// **Important**: In rare cases, the request may return an internal_server_error.
1627    /// This does not necessarily mean the operation failed entirely. Part or all of
1628    /// the position transfer might have still been processed successfully.
1629    ///
1630    /// # Arguments
1631    ///
1632    /// * `currency` - Currency symbol (e.g., "BTC", "ETH", "USDC")
1633    /// * `source_uid` - Source subaccount ID
1634    /// * `target_uid` - Target subaccount ID
1635    /// * `trades` - List of position trades to move
1636    ///
1637    /// # Examples
1638    ///
1639    /// ```rust
1640    /// use deribit_http::DeribitHttpClient;
1641    /// use deribit_http::model::request::position::MovePositionTrade;
1642    ///
1643    /// let client = DeribitHttpClient::new();
1644    /// let trades = vec![
1645    ///     MovePositionTrade::with_price("BTC-PERPETUAL", 110.0, 35800.0),
1646    /// ];
1647    /// // let results = client.move_positions("BTC", 3, 23, &trades).await?;
1648    /// ```
1649    pub async fn move_positions(
1650        &self,
1651        currency: &str,
1652        source_uid: i64,
1653        target_uid: i64,
1654        trades: &[MovePositionTrade],
1655    ) -> Result<Vec<MovePositionResult>, HttpError> {
1656        let mut url = format!(
1657            "{}{}?currency={}&source_uid={}&target_uid={}",
1658            self.base_url(),
1659            MOVE_POSITIONS,
1660            urlencoding::encode(currency),
1661            source_uid,
1662            target_uid
1663        );
1664
1665        // Build trades array as JSON
1666        let trades_json = serde_json::to_string(trades).map_err(|e| {
1667            HttpError::InvalidResponse(format!("Failed to serialize trades: {}", e))
1668        })?;
1669        url.push_str(&format!("&trades={}", urlencoding::encode(&trades_json)));
1670
1671        let response = self.make_authenticated_request(&url).await?;
1672
1673        if !response.status().is_success() {
1674            let error_text = response
1675                .text()
1676                .await
1677                .unwrap_or_else(|_| "Unknown error".to_string());
1678            return Err(HttpError::RequestFailed(format!(
1679                "Move positions failed: {}",
1680                error_text
1681            )));
1682        }
1683
1684        let api_response: ApiResponse<Vec<MovePositionResult>> = response
1685            .json()
1686            .await
1687            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
1688
1689        if let Some(error) = api_response.error {
1690            return Err(HttpError::RequestFailed(format!(
1691                "API error: {} - {}",
1692                error.code, error.message
1693            )));
1694        }
1695
1696        api_response.result.ok_or_else(|| {
1697            HttpError::InvalidResponse("No move positions data in response".to_string())
1698        })
1699    }
1700
1701    /// Get MMP configuration
1702    ///
1703    /// Retrieves Market Maker Protection (MMP) configuration for an index.
1704    /// If index_name is not provided, returns all MMP configurations.
1705    ///
1706    /// # Arguments
1707    ///
1708    /// * `index_name` - Index identifier (e.g., "btc_usd", "eth_usd"), optional
1709    /// * `mmp_group` - MMP group name for Mass Quotes, optional
1710    /// * `block_rfq` - If true, retrieve MMP config for Block RFQ, optional
1711    ///
1712    /// # Examples
1713    ///
1714    /// ```rust
1715    /// use deribit_http::DeribitHttpClient;
1716    ///
1717    /// let client = DeribitHttpClient::new();
1718    /// // let configs = client.get_mmp_config(Some("btc_usd"), None, None).await?;
1719    /// ```
1720    pub async fn get_mmp_config(
1721        &self,
1722        index_name: Option<&str>,
1723        mmp_group: Option<&str>,
1724        block_rfq: Option<bool>,
1725    ) -> Result<Vec<MmpConfig>, HttpError> {
1726        let mut params = Vec::new();
1727        if let Some(index) = index_name {
1728            params.push(format!("index_name={}", urlencoding::encode(index)));
1729        }
1730        if let Some(group) = mmp_group {
1731            params.push(format!("mmp_group={}", urlencoding::encode(group)));
1732        }
1733        if let Some(rfq) = block_rfq
1734            && rfq
1735        {
1736            params.push("block_rfq=true".to_string());
1737        }
1738        let query = if params.is_empty() {
1739            String::new()
1740        } else {
1741            format!("?{}", params.join("&"))
1742        };
1743        self.private_get(GET_MMP_CONFIG, &query).await
1744    }
1745
1746    /// Get MMP status
1747    ///
1748    /// Retrieves Market Maker Protection (MMP) status for a triggered index or MMP group.
1749    /// If index_name is not provided, returns all triggered MMP statuses.
1750    ///
1751    /// # Arguments
1752    ///
1753    /// * `index_name` - Index identifier (e.g., "btc_usd", "eth_usd"), optional
1754    /// * `mmp_group` - MMP group name for Mass Quotes, optional
1755    /// * `block_rfq` - If true, retrieve MMP status for Block RFQ, optional
1756    ///
1757    /// # Examples
1758    ///
1759    /// ```rust
1760    /// use deribit_http::DeribitHttpClient;
1761    ///
1762    /// let client = DeribitHttpClient::new();
1763    /// // let statuses = client.get_mmp_status(Some("btc_usd"), None, None).await?;
1764    /// ```
1765    pub async fn get_mmp_status(
1766        &self,
1767        index_name: Option<&str>,
1768        mmp_group: Option<&str>,
1769        block_rfq: Option<bool>,
1770    ) -> Result<Vec<MmpStatus>, HttpError> {
1771        let mut params = Vec::new();
1772        if let Some(index) = index_name {
1773            params.push(format!("index_name={}", urlencoding::encode(index)));
1774        }
1775        if let Some(group) = mmp_group {
1776            params.push(format!("mmp_group={}", urlencoding::encode(group)));
1777        }
1778        if let Some(rfq) = block_rfq
1779            && rfq
1780        {
1781            params.push("block_rfq=true".to_string());
1782        }
1783        let query = if params.is_empty() {
1784            String::new()
1785        } else {
1786            format!("?{}", params.join("&"))
1787        };
1788        self.private_get(GET_MMP_STATUS, &query).await
1789    }
1790
1791    /// Set MMP configuration
1792    ///
1793    /// Configures Market Maker Protection (MMP) for a specific index.
1794    /// Set interval to 0 to remove MMP configuration.
1795    ///
1796    /// # Arguments
1797    ///
1798    /// * `request` - The MMP configuration request
1799    ///
1800    /// # Examples
1801    ///
1802    /// ```rust
1803    /// use deribit_http::DeribitHttpClient;
1804    /// use deribit_http::model::response::mmp::SetMmpConfigRequest;
1805    ///
1806    /// let client = DeribitHttpClient::new();
1807    /// // let request = SetMmpConfigRequest {
1808    /// //     index_name: "btc_usd".to_string(),
1809    /// //     interval: 60,
1810    /// //     frozen_time: 0,
1811    /// //     quantity_limit: Some(3.0),
1812    /// //     max_quote_quantity: Some(2.5),
1813    /// //     ..Default::default()
1814    /// // };
1815    /// // let config = client.set_mmp_config(request).await?;
1816    /// ```
1817    pub async fn set_mmp_config(
1818        &self,
1819        request: SetMmpConfigRequest,
1820    ) -> Result<MmpConfig, HttpError> {
1821        let mut query_params = vec![
1822            ("index_name".to_string(), request.index_name),
1823            ("interval".to_string(), request.interval.to_string()),
1824            ("frozen_time".to_string(), request.frozen_time.to_string()),
1825        ];
1826
1827        if let Some(quantity_limit) = request.quantity_limit {
1828            query_params.push(("quantity_limit".to_string(), quantity_limit.to_string()));
1829        }
1830
1831        if let Some(delta_limit) = request.delta_limit {
1832            query_params.push(("delta_limit".to_string(), delta_limit.to_string()));
1833        }
1834
1835        if let Some(vega_limit) = request.vega_limit {
1836            query_params.push(("vega_limit".to_string(), vega_limit.to_string()));
1837        }
1838
1839        if let Some(max_quote_quantity) = request.max_quote_quantity {
1840            query_params.push((
1841                "max_quote_quantity".to_string(),
1842                max_quote_quantity.to_string(),
1843            ));
1844        }
1845
1846        if let Some(mmp_group) = request.mmp_group {
1847            query_params.push(("mmp_group".to_string(), mmp_group));
1848        }
1849
1850        if let Some(block_rfq) = request.block_rfq
1851            && block_rfq
1852        {
1853            query_params.push(("block_rfq".to_string(), "true".to_string()));
1854        }
1855
1856        let query_string = query_params
1857            .iter()
1858            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
1859            .collect::<Vec<_>>()
1860            .join("&");
1861
1862        let url = format!("{}{}?{}", self.base_url(), SET_MMP_CONFIG, query_string);
1863
1864        let response = self.make_authenticated_request(&url).await?;
1865
1866        if !response.status().is_success() {
1867            let error_text = response
1868                .text()
1869                .await
1870                .unwrap_or_else(|_| "Unknown error".to_string());
1871            return Err(HttpError::RequestFailed(format!(
1872                "Set MMP config failed: {}",
1873                error_text
1874            )));
1875        }
1876
1877        let api_response: ApiResponse<MmpConfig> = response
1878            .json()
1879            .await
1880            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
1881
1882        if let Some(error) = api_response.error {
1883            return Err(HttpError::RequestFailed(format!(
1884                "API error: {} - {}",
1885                error.code, error.message
1886            )));
1887        }
1888
1889        api_response
1890            .result
1891            .ok_or_else(|| HttpError::InvalidResponse("No MMP config data in response".to_string()))
1892    }
1893
1894    /// Reset MMP limits
1895    ///
1896    /// Resets Market Maker Protection (MMP) limits for the specified currency pair or MMP group.
1897    /// If MMP protection has been triggered and quoting is frozen, this allows manual resume.
1898    ///
1899    /// # Arguments
1900    ///
1901    /// * `index_name` - Currency pair (e.g., "btc_usd", "eth_usd")
1902    /// * `mmp_group` - MMP group name for Mass Quotes, optional
1903    /// * `block_rfq` - If true, reset MMP for Block RFQ, optional
1904    ///
1905    /// # Examples
1906    ///
1907    /// ```rust
1908    /// use deribit_http::DeribitHttpClient;
1909    ///
1910    /// let client = DeribitHttpClient::new();
1911    /// // let result = client.reset_mmp("btc_usd", None, None).await?;
1912    /// ```
1913    pub async fn reset_mmp(
1914        &self,
1915        index_name: &str,
1916        mmp_group: Option<&str>,
1917        block_rfq: Option<bool>,
1918    ) -> Result<String, HttpError> {
1919        let mut query = format!("?index_name={}", urlencoding::encode(index_name));
1920        if let Some(group) = mmp_group {
1921            query.push_str(&format!("&mmp_group={}", urlencoding::encode(group)));
1922        }
1923        if let Some(rfq) = block_rfq
1924            && rfq
1925        {
1926            query.push_str("&block_rfq=true");
1927        }
1928        self.private_get(RESET_MMP, &query).await
1929    }
1930
1931    /// Mass quote
1932    ///
1933    /// Places multiple quotes at once.
1934    ///
1935    /// # Arguments
1936    ///
1937    /// * `quotes` - Vector of mass quote requests
1938    ///
1939    pub async fn mass_quote(
1940        &self,
1941        _quotes: MassQuoteRequest,
1942    ) -> Result<MassQuoteResponse, HttpError> {
1943        Err(HttpError::ConfigError(
1944            "Mass quote endpoint is only available via WebSocket connections. \
1945             According to Deribit's technical specifications, private/mass_quote requires \
1946             WebSocket for real-time quote management, MMP group integration, and \
1947             Cancel-on-Disconnect functionality. Please use the deribit-websocket client \
1948             for mass quote operations."
1949                .to_string(),
1950        ))
1951    }
1952
1953    /// Get user trades by instrument
1954    ///
1955    /// Retrieves user trades for a specific instrument.
1956    ///
1957    /// # Arguments
1958    ///
1959    /// * `instrument_name` - Instrument name
1960    /// * `start_seq` - Start sequence number (optional)
1961    /// * `end_seq` - End sequence number (optional)
1962    /// * `count` - Number of requested items (optional)
1963    /// * `include_old` - Include old trades (optional)
1964    /// * `sorting` - Direction of results sorting (optional)
1965    ///
1966    pub async fn get_user_trades_by_instrument(
1967        &self,
1968        instrument_name: &str,
1969        start_seq: Option<u64>,
1970        end_seq: Option<u64>,
1971        count: Option<u32>,
1972        include_old: Option<bool>,
1973        sorting: Option<&str>,
1974    ) -> Result<UserTradeWithPaginationResponse, HttpError> {
1975        let mut query_params = vec![("instrument_name".to_string(), instrument_name.to_string())];
1976
1977        if let Some(start_seq) = start_seq {
1978            query_params.push(("start_seq".to_string(), start_seq.to_string()));
1979        }
1980
1981        if let Some(end_seq) = end_seq {
1982            query_params.push(("end_seq".to_string(), end_seq.to_string()));
1983        }
1984
1985        if let Some(count) = count {
1986            query_params.push(("count".to_string(), count.to_string()));
1987        }
1988
1989        if let Some(include_old) = include_old {
1990            query_params.push(("include_old".to_string(), include_old.to_string()));
1991        }
1992
1993        if let Some(sorting) = sorting {
1994            query_params.push(("sorting".to_string(), sorting.to_string()));
1995        }
1996
1997        let query_string = query_params
1998            .iter()
1999            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
2000            .collect::<Vec<_>>()
2001            .join("&");
2002
2003        let url = format!(
2004            "{}{}?{}",
2005            self.base_url(),
2006            GET_USER_TRADES_BY_INSTRUMENT,
2007            query_string
2008        );
2009
2010        let response = self.make_authenticated_request(&url).await?;
2011
2012        if !response.status().is_success() {
2013            let error_text = response
2014                .text()
2015                .await
2016                .unwrap_or_else(|_| "Unknown error".to_string());
2017            return Err(HttpError::RequestFailed(format!(
2018                "Get user trades by instrument failed: {}",
2019                error_text
2020            )));
2021        }
2022
2023        // Debug: Log the raw response text before trying to parse it
2024        let response_text = response.text().await.map_err(|e| {
2025            HttpError::InvalidResponse(format!("Failed to read response text: {}", e))
2026        })?;
2027
2028        tracing::debug!(
2029            "Raw API response for get_user_trades_by_instrument: {}",
2030            response_text
2031        );
2032
2033        // Try to parse as JSON
2034        let api_response: ApiResponse<UserTradeWithPaginationResponse> =
2035            serde_json::from_str(&response_text).map_err(|e| {
2036                HttpError::InvalidResponse(format!(
2037                    "error decoding response body: {} - Raw response: {}",
2038                    e, response_text
2039                ))
2040            })?;
2041
2042        if let Some(error) = api_response.error {
2043            return Err(HttpError::RequestFailed(format!(
2044                "API error: {} - {}",
2045                error.code, error.message
2046            )));
2047        }
2048
2049        api_response.result.ok_or_else(|| {
2050            HttpError::InvalidResponse("No user trades data in response".to_string())
2051        })
2052    }
2053
2054    /// Cancel quotes
2055    ///
2056    /// Cancels all mass quotes.
2057    ///
2058    /// # Arguments
2059    ///
2060    /// * `cancel_type` - Type of cancellation ("all", "by_currency", "by_instrument", etc.)
2061    ///
2062    pub async fn cancel_quotes(&self, cancel_type: Option<&str>) -> Result<u32, HttpError> {
2063        let query = format!(
2064            "?cancel_type={}",
2065            urlencoding::encode(cancel_type.unwrap_or("all"))
2066        );
2067        self.private_get(CANCEL_QUOTES, &query).await
2068    }
2069
2070    /// Get open orders
2071    ///
2072    /// Retrieves list of user's open orders across many currencies.
2073    ///
2074    /// # Arguments
2075    ///
2076    /// * `kind` - Instrument kind filter (optional)
2077    /// * `order_type` - Order type filter (optional)
2078    ///
2079    pub async fn get_open_orders(
2080        &self,
2081        kind: Option<&str>,
2082        order_type: Option<&str>,
2083    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
2084        let mut params = Vec::new();
2085        if let Some(kind) = kind {
2086            params.push(format!("kind={}", urlencoding::encode(kind)));
2087        }
2088        if let Some(order_type) = order_type {
2089            params.push(format!("type={}", urlencoding::encode(order_type)));
2090        }
2091        let query = if params.is_empty() {
2092            String::new()
2093        } else {
2094            format!("?{}", params.join("&"))
2095        };
2096        self.private_get(GET_OPEN_ORDERS, &query).await
2097    }
2098
2099    /// Get open orders by label
2100    ///
2101    /// Retrieves open orders filtered by a specific label.
2102    ///
2103    /// # Arguments
2104    ///
2105    /// * `label` - The label to filter orders by
2106    /// * `currency` - The currency symbol (BTC, ETH, etc.)
2107    ///
2108    pub async fn get_open_orders_by_label(
2109        &self,
2110        label: &str,
2111        currency: &str,
2112    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
2113        let query = format!(
2114            "?label={}&currency={}",
2115            urlencoding::encode(label),
2116            urlencoding::encode(currency)
2117        );
2118        self.private_get(GET_OPEN_ORDERS_BY_LABEL, &query).await
2119    }
2120
2121    /// Get order state
2122    ///
2123    /// Retrieves the state of a specific order.
2124    ///
2125    /// # Arguments
2126    ///
2127    /// * `order_id` - The order ID
2128    ///
2129    pub async fn get_order_state(&self, order_id: &str) -> Result<OrderInfoResponse, HttpError> {
2130        let query = format!("?order_id={}", urlencoding::encode(order_id));
2131        self.private_get(GET_ORDER_STATE, &query).await
2132    }
2133
2134    /// Get open orders by currency
2135    ///
2136    /// Retrieves open orders for a specific currency.
2137    ///
2138    /// # Arguments
2139    ///
2140    /// * `currency` - The currency symbol (BTC, ETH, etc.)
2141    /// * `kind` - Instrument kind filter (optional)
2142    /// * `order_type` - Order type filter (optional)
2143    ///
2144    pub async fn get_open_orders_by_currency(
2145        &self,
2146        currency: &str,
2147        kind: Option<&str>,
2148        order_type: Option<&str>,
2149    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
2150        let mut query = format!("?currency={}", urlencoding::encode(currency));
2151        if let Some(kind) = kind {
2152            query.push_str(&format!("&kind={}", urlencoding::encode(kind)));
2153        }
2154        if let Some(order_type) = order_type {
2155            query.push_str(&format!("&type={}", urlencoding::encode(order_type)));
2156        }
2157        self.private_get(GET_OPEN_ORDERS_BY_CURRENCY, &query).await
2158    }
2159
2160    /// Get open orders by instrument
2161    ///
2162    /// Retrieves open orders for a specific instrument.
2163    ///
2164    /// # Arguments
2165    ///
2166    /// * `instrument_name` - The instrument name
2167    /// * `order_type` - Order type filter (optional)
2168    ///
2169    pub async fn get_open_orders_by_instrument(
2170        &self,
2171        instrument_name: &str,
2172        order_type: Option<&str>,
2173    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
2174        let mut query = format!("?instrument_name={}", urlencoding::encode(instrument_name));
2175        if let Some(order_type) = order_type {
2176            query.push_str(&format!("&type={}", urlencoding::encode(order_type)));
2177        }
2178        self.private_get(GET_OPEN_ORDERS_BY_INSTRUMENT, &query)
2179            .await
2180    }
2181
2182    /// Get order history
2183    ///
2184    /// Retrieves history of orders that have been partially or fully filled.
2185    ///
2186    /// # Arguments
2187    ///
2188    /// * `currency` - Currency symbol (BTC, ETH, etc.)
2189    /// * `kind` - Instrument kind filter (optional)
2190    /// * `count` - Number of requested items (optional, default 20)
2191    /// * `offset` - Offset for pagination (optional)
2192    ///
2193    pub async fn get_order_history(
2194        &self,
2195        currency: &str,
2196        kind: Option<&str>,
2197        count: Option<u32>,
2198        offset: Option<u32>,
2199    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
2200        let mut query = format!("?currency={}", urlencoding::encode(currency));
2201        if let Some(kind) = kind {
2202            query.push_str(&format!("&kind={}", urlencoding::encode(kind)));
2203        }
2204        if let Some(count) = count {
2205            query.push_str(&format!("&count={}", count));
2206        }
2207        if let Some(offset) = offset {
2208            query.push_str(&format!("&offset={}", offset));
2209        }
2210        self.private_get(GET_ORDER_HISTORY_BY_CURRENCY, &query)
2211            .await
2212    }
2213
2214    /// Get order history by currency
2215    ///
2216    /// Retrieves order history for a specific currency.
2217    ///
2218    /// # Arguments
2219    ///
2220    /// * `currency` - Currency symbol (BTC, ETH, etc.)
2221    /// * `kind` - Instrument kind filter (optional)
2222    /// * `count` - Number of requested items (optional)
2223    /// * `offset` - Offset for pagination (optional)
2224    ///
2225    pub async fn get_order_history_by_currency(
2226        &self,
2227        currency: &str,
2228        kind: Option<&str>,
2229        count: Option<u32>,
2230        offset: Option<u32>,
2231    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
2232        // This is an alias to the existing get_order_history method
2233        self.get_order_history(currency, kind, count, offset).await
2234    }
2235
2236    /// Get order history by instrument
2237    ///
2238    /// Retrieves order history for a specific instrument.
2239    ///
2240    /// # Arguments
2241    ///
2242    /// * `instrument_name` - The instrument name
2243    /// * `count` - Number of requested items (optional)
2244    /// * `offset` - Offset for pagination (optional)
2245    ///
2246    pub async fn get_order_history_by_instrument(
2247        &self,
2248        instrument_name: &str,
2249        count: Option<u32>,
2250        offset: Option<u32>,
2251    ) -> Result<Vec<OrderInfoResponse>, HttpError> {
2252        let mut query = format!("?instrument_name={}", urlencoding::encode(instrument_name));
2253        if let Some(count) = count {
2254            query.push_str(&format!("&count={}", count));
2255        }
2256        if let Some(offset) = offset {
2257            query.push_str(&format!("&offset={}", offset));
2258        }
2259        self.private_get(GET_ORDER_HISTORY_BY_INSTRUMENT, &query)
2260            .await
2261    }
2262
2263    /// Get user trades by currency
2264    ///
2265    /// Retrieves user trades filtered by currency.
2266    ///
2267    /// # Arguments
2268    ///
2269    /// * `request` - A `TradesRequest` struct containing:
2270    ///   * `currency` - Currency symbol (BTC, ETH, etc.)
2271    ///   * `kind` - Instrument kind filter (optional)
2272    ///   * `start_id` - The ID of the first trade to be returned (optional)
2273    ///   * `end_id` - The ID of the last trade to be returned (optional)
2274    ///   * `count` - Number of requested items (optional, default 10, max 1000)
2275    ///   * `start_timestamp` - The earliest timestamp to return results from (optional)
2276    ///   * `end_timestamp` - The most recent timestamp to return results from (optional)
2277    ///   * `sorting` - Direction of results sorting (optional)
2278    ///   * `historical` - If true, retrieves historical records that persist indefinitely.
2279    ///     If false (default), retrieves recent records available for 24 hours.
2280    ///   * `subaccount_id` - The user id for the subaccount (optional)
2281    ///
2282    #[allow(clippy::too_many_arguments)]
2283    pub async fn get_user_trades_by_currency(
2284        &self,
2285        request: TradesRequest,
2286    ) -> Result<UserTradeWithPaginationResponse, HttpError> {
2287        let mut query_params = vec![("currency".to_string(), request.currency.to_string())];
2288
2289        if let Some(kind) = request.kind {
2290            query_params.push(("kind".to_string(), kind.to_string()));
2291        }
2292
2293        if let Some(start_id) = request.start_id {
2294            query_params.push(("start_id".to_string(), start_id));
2295        }
2296
2297        if let Some(end_id) = request.end_id {
2298            query_params.push(("end_id".to_string(), end_id));
2299        }
2300
2301        if let Some(count) = request.count {
2302            query_params.push(("count".to_string(), count.to_string()));
2303        }
2304
2305        if let Some(start_timestamp) = request.start_timestamp {
2306            query_params.push(("start_timestamp".to_string(), start_timestamp.to_string()));
2307        }
2308
2309        if let Some(end_timestamp) = request.end_timestamp {
2310            query_params.push(("end_timestamp".to_string(), end_timestamp.to_string()));
2311        }
2312
2313        if let Some(sorting) = request.sorting {
2314            query_params.push(("sorting".to_string(), sorting.to_string()));
2315        }
2316
2317        if let Some(historical) = request.historical {
2318            query_params.push(("historical".to_string(), historical.to_string()));
2319        }
2320
2321        if let Some(subaccount_id) = request.subaccount_id {
2322            query_params.push(("subaccount_id".to_string(), subaccount_id.to_string()));
2323        }
2324
2325        let query_string = query_params
2326            .iter()
2327            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
2328            .collect::<Vec<_>>()
2329            .join("&");
2330
2331        let url = format!(
2332            "{}{}?{}",
2333            self.base_url(),
2334            GET_USER_TRADES_BY_CURRENCY,
2335            query_string
2336        );
2337
2338        let response = self.make_authenticated_request(&url).await?;
2339
2340        if !response.status().is_success() {
2341            let error_text = response
2342                .text()
2343                .await
2344                .unwrap_or_else(|_| "Unknown error".to_string());
2345            return Err(HttpError::RequestFailed(format!(
2346                "Get user trades by currency failed: {}",
2347                error_text
2348            )));
2349        }
2350
2351        // Debug: Log the raw response text before trying to parse it
2352        let response_text = response.text().await.map_err(|e| {
2353            HttpError::InvalidResponse(format!("Failed to read response text: {}", e))
2354        })?;
2355
2356        tracing::debug!(
2357            "Raw API response for get_user_trades_by_order: {}",
2358            response_text
2359        );
2360
2361        // Try to parse as JSON
2362        let api_response: ApiResponse<UserTradeWithPaginationResponse> =
2363            serde_json::from_str(&response_text).map_err(|e| {
2364                HttpError::InvalidResponse(format!(
2365                    "error decoding response body: {} - Raw response: {}",
2366                    e, response_text
2367                ))
2368            })?;
2369
2370        if let Some(error) = api_response.error {
2371            return Err(HttpError::RequestFailed(format!(
2372                "API error: {} - {}",
2373                error.code, error.message
2374            )));
2375        }
2376
2377        api_response.result.ok_or_else(|| {
2378            HttpError::InvalidResponse("No user trades data in response".to_string())
2379        })
2380    }
2381
2382    /// Get user trades by currency and time
2383    ///
2384    /// Retrieves user trades filtered by currency within a time range.
2385    ///
2386    /// # Arguments
2387    ///
2388    /// * `request` - A `TradesRequest` struct containing:
2389    ///   * `currency` - Currency symbol (BTC, ETH, etc.)
2390    ///   * `kind` - Instrument kind filter (optional)
2391    ///   * `start_id` - The ID of the first trade to be returned (optional)
2392    ///   * `end_id` - The ID of the last trade to be returned (optional)
2393    ///   * `count` - Number of requested items (optional, default 10, max 1000)
2394    ///   * `start_timestamp` - The earliest timestamp to return results from (optional)
2395    ///   * `end_timestamp` - The most recent timestamp to return results from (optional)
2396    ///   * `sorting` - Direction of results sorting (optional)
2397    ///   * `historical` - If true, retrieves historical records that persist indefinitely.
2398    ///     If false (default), retrieves recent records available for 24 hours.
2399    ///   * `subaccount_id` - The user id for the subaccount (optional)
2400    ///
2401    #[allow(clippy::too_many_arguments)]
2402    pub async fn get_user_trades_by_currency_and_time(
2403        &self,
2404        request: TradesRequest,
2405    ) -> Result<UserTradeWithPaginationResponse, HttpError> {
2406        let mut query_params = vec![("currency".to_string(), request.currency.to_string())];
2407
2408        if let Some(kind) = request.kind {
2409            query_params.push(("kind".to_string(), kind.to_string()));
2410        }
2411
2412        if let Some(start_id) = request.start_id {
2413            query_params.push(("start_id".to_string(), start_id));
2414        }
2415
2416        if let Some(end_id) = request.end_id {
2417            query_params.push(("end_id".to_string(), end_id));
2418        }
2419
2420        if let Some(count) = request.count {
2421            query_params.push(("count".to_string(), count.to_string()));
2422        }
2423
2424        if let Some(start_timestamp) = request.start_timestamp {
2425            query_params.push(("start_timestamp".to_string(), start_timestamp.to_string()));
2426        }
2427
2428        if let Some(end_timestamp) = request.end_timestamp {
2429            query_params.push(("end_timestamp".to_string(), end_timestamp.to_string()));
2430        }
2431
2432        if let Some(sorting) = request.sorting {
2433            query_params.push(("sorting".to_string(), sorting.to_string()));
2434        }
2435
2436        if let Some(historical) = request.historical {
2437            query_params.push(("historical".to_string(), historical.to_string()));
2438        }
2439
2440        if let Some(subaccount_id) = request.subaccount_id {
2441            query_params.push(("subaccount_id".to_string(), subaccount_id.to_string()));
2442        }
2443
2444        let query_string = query_params
2445            .iter()
2446            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
2447            .collect::<Vec<_>>()
2448            .join("&");
2449
2450        let url = format!(
2451            "{}{}?{}",
2452            self.base_url(),
2453            GET_USER_TRADES_BY_CURRENCY_AND_TIME,
2454            query_string
2455        );
2456
2457        let response = self.make_authenticated_request(&url).await?;
2458
2459        if !response.status().is_success() {
2460            let error_text = response
2461                .text()
2462                .await
2463                .unwrap_or_else(|_| "Unknown error".to_string());
2464            return Err(HttpError::RequestFailed(format!(
2465                "Get user trades by currency and time failed: {}",
2466                error_text
2467            )));
2468        }
2469
2470        // Debug: Log the raw response text before trying to parse it
2471        let response_text = response.text().await.map_err(|e| {
2472            HttpError::InvalidResponse(format!("Failed to read response text: {}", e))
2473        })?;
2474
2475        tracing::debug!(
2476            "Raw API response for get_user_trades_by_order: {}",
2477            response_text
2478        );
2479
2480        // Try to parse as JSON
2481        let api_response: ApiResponse<UserTradeWithPaginationResponse> =
2482            serde_json::from_str(&response_text).map_err(|e| {
2483                HttpError::InvalidResponse(format!(
2484                    "error decoding response body: {} - Raw response: {}",
2485                    e, response_text
2486                ))
2487            })?;
2488
2489        if let Some(error) = api_response.error {
2490            return Err(HttpError::RequestFailed(format!(
2491                "API error: {} - {}",
2492                error.code, error.message
2493            )));
2494        }
2495
2496        api_response.result.ok_or_else(|| {
2497            HttpError::InvalidResponse("No user trades data in response".to_string())
2498        })
2499    }
2500
2501    /// Get user trades by instrument and time
2502    ///
2503    /// Retrieves user trades for a specific instrument within a time range.
2504    ///
2505    /// # Arguments
2506    ///
2507    /// * `instrument_name` - Instrument name
2508    /// * `start_timestamp` - Start timestamp in milliseconds
2509    /// * `end_timestamp` - End timestamp in milliseconds
2510    /// * `count` - Number of requested items (optional, default 10)
2511    /// * `include_old` - Include trades older than 7 days (optional)
2512    /// * `sorting` - Direction of results sorting (optional)
2513    ///
2514    pub async fn get_user_trades_by_instrument_and_time(
2515        &self,
2516        instrument_name: &str,
2517        start_timestamp: u64,
2518        end_timestamp: u64,
2519        count: Option<u32>,
2520        include_old: Option<bool>,
2521        sorting: Option<&str>,
2522    ) -> Result<UserTradeWithPaginationResponse, HttpError> {
2523        let mut query_params = vec![
2524            ("instrument_name".to_string(), instrument_name.to_string()),
2525            ("start_timestamp".to_string(), start_timestamp.to_string()),
2526            ("end_timestamp".to_string(), end_timestamp.to_string()),
2527        ];
2528
2529        if let Some(count) = count {
2530            query_params.push(("count".to_string(), count.to_string()));
2531        }
2532
2533        if let Some(include_old) = include_old {
2534            query_params.push(("include_old".to_string(), include_old.to_string()));
2535        }
2536
2537        if let Some(sorting) = sorting {
2538            query_params.push(("sorting".to_string(), sorting.to_string()));
2539        }
2540
2541        let query_string = query_params
2542            .iter()
2543            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
2544            .collect::<Vec<_>>()
2545            .join("&");
2546
2547        let url = format!(
2548            "{}{}?{}",
2549            self.base_url(),
2550            GET_USER_TRADES_BY_INSTRUMENT_AND_TIME,
2551            query_string
2552        );
2553
2554        let response = self.make_authenticated_request(&url).await?;
2555
2556        if !response.status().is_success() {
2557            let error_text = response
2558                .text()
2559                .await
2560                .unwrap_or_else(|_| "Unknown error".to_string());
2561            return Err(HttpError::RequestFailed(format!(
2562                "Get user trades by instrument and time failed: {}",
2563                error_text
2564            )));
2565        }
2566
2567        // Debug: Log the raw response text before trying to parse it
2568        let response_text = response.text().await.map_err(|e| {
2569            HttpError::InvalidResponse(format!("Failed to read response text: {}", e))
2570        })?;
2571
2572        tracing::debug!(
2573            "Raw API response for get_user_trades_by_instrument_and_time: {}",
2574            response_text
2575        );
2576
2577        // Try to parse as JSON
2578        let api_response: ApiResponse<UserTradeWithPaginationResponse> =
2579            serde_json::from_str(&response_text).map_err(|e| {
2580                HttpError::InvalidResponse(format!(
2581                    "error decoding response body: {} - Raw response: {}",
2582                    e, response_text
2583                ))
2584            })?;
2585
2586        if let Some(error) = api_response.error {
2587            return Err(HttpError::RequestFailed(format!(
2588                "API error: {} - {}",
2589                error.code, error.message
2590            )));
2591        }
2592
2593        api_response.result.ok_or_else(|| {
2594            HttpError::InvalidResponse("No user trades data in response".to_string())
2595        })
2596    }
2597
2598    /// Get user trades by order
2599    ///
2600    /// Retrieves user trades for a specific order.
2601    ///
2602    /// # Arguments
2603    ///
2604    /// * `order_id` - Order ID
2605    /// * `sorting` - Direction of results sorting (optional)
2606    ///
2607    pub async fn get_user_trades_by_order(
2608        &self,
2609        order_id: &str,
2610        sorting: Option<&str>,
2611        historical: bool,
2612    ) -> Result<Vec<UserTradeResponseByOrder>, HttpError> {
2613        let mut query = format!("?order_id={}", urlencoding::encode(order_id));
2614        if let Some(sorting) = sorting {
2615            query.push_str(&format!("&sorting={}", urlencoding::encode(sorting)));
2616        }
2617        if historical {
2618            query.push_str("&historical=true");
2619        }
2620        self.private_get(GET_USER_TRADES_BY_ORDER, &query).await
2621    }
2622
2623    // ==================== API Key Management ====================
2624
2625    /// Create a new API key
2626    ///
2627    /// Creates a new API key with the specified scope and optional settings.
2628    ///
2629    /// # Arguments
2630    ///
2631    /// * `request` - The create API key request parameters
2632    ///
2633    /// # Returns
2634    ///
2635    /// Returns the newly created API key information including client_id and client_secret.
2636    ///
2637    /// # Errors
2638    ///
2639    /// Returns `HttpError` if the request fails or authentication is invalid.
2640    ///
2641    /// # Example
2642    ///
2643    /// ```rust,no_run
2644    /// use deribit_http::{DeribitHttpClient, model::CreateApiKeyRequest};
2645    ///
2646    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2647    /// let client = DeribitHttpClient::new();
2648    /// let request = CreateApiKeyRequest {
2649    ///     max_scope: "account:read trade:read_write".to_string(),
2650    ///     name: Some("my_trading_key".to_string()),
2651    ///     ..Default::default()
2652    /// };
2653    /// let api_key = client.create_api_key(request).await?;
2654    /// println!("Created API key: {}", api_key.client_id);
2655    /// # Ok(())
2656    /// # }
2657    /// ```
2658    pub async fn create_api_key(
2659        &self,
2660        request: CreateApiKeyRequest,
2661    ) -> Result<ApiKeyInfo, HttpError> {
2662        let mut query_params = vec![("max_scope".to_string(), request.max_scope)];
2663
2664        if let Some(name) = request.name {
2665            query_params.push(("name".to_string(), name));
2666        }
2667
2668        if let Some(public_key) = request.public_key {
2669            query_params.push(("public_key".to_string(), public_key));
2670        }
2671
2672        if let Some(enabled_features) = request.enabled_features {
2673            for feature in enabled_features {
2674                query_params.push(("enabled_features".to_string(), feature));
2675            }
2676        }
2677
2678        let query_string = query_params
2679            .iter()
2680            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
2681            .collect::<Vec<_>>()
2682            .join("&");
2683
2684        let url = format!("{}{}?{}", self.base_url(), CREATE_API_KEY, query_string);
2685
2686        let response = self.make_authenticated_request(&url).await?;
2687
2688        if !response.status().is_success() {
2689            let error_text = response
2690                .text()
2691                .await
2692                .unwrap_or_else(|_| "Unknown error".to_string());
2693            return Err(HttpError::RequestFailed(format!(
2694                "Create API key failed: {}",
2695                error_text
2696            )));
2697        }
2698
2699        let api_response: ApiResponse<ApiKeyInfo> = response
2700            .json()
2701            .await
2702            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
2703
2704        if let Some(error) = api_response.error {
2705            return Err(HttpError::RequestFailed(format!(
2706                "API error: {} - {}",
2707                error.code, error.message
2708            )));
2709        }
2710
2711        api_response
2712            .result
2713            .ok_or_else(|| HttpError::InvalidResponse("No API key data in response".to_string()))
2714    }
2715
2716    /// Edit an existing API key
2717    ///
2718    /// Modifies an existing API key's scope, name, or other settings.
2719    ///
2720    /// # Arguments
2721    ///
2722    /// * `request` - The edit API key request parameters
2723    ///
2724    /// # Returns
2725    ///
2726    /// Returns the updated API key information.
2727    ///
2728    /// # Errors
2729    ///
2730    /// Returns `HttpError` if the request fails or the API key is not found.
2731    pub async fn edit_api_key(&self, request: EditApiKeyRequest) -> Result<ApiKeyInfo, HttpError> {
2732        let mut query_params = vec![
2733            ("id".to_string(), request.id.to_string()),
2734            ("max_scope".to_string(), request.max_scope),
2735        ];
2736
2737        if let Some(name) = request.name {
2738            query_params.push(("name".to_string(), name));
2739        }
2740
2741        if let Some(enabled) = request.enabled {
2742            query_params.push(("enabled".to_string(), enabled.to_string()));
2743        }
2744
2745        if let Some(enabled_features) = request.enabled_features {
2746            for feature in enabled_features {
2747                query_params.push(("enabled_features".to_string(), feature));
2748            }
2749        }
2750
2751        if let Some(ip_whitelist) = request.ip_whitelist {
2752            for ip in ip_whitelist {
2753                query_params.push(("ip_whitelist".to_string(), ip));
2754            }
2755        }
2756
2757        let query_string = query_params
2758            .iter()
2759            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
2760            .collect::<Vec<_>>()
2761            .join("&");
2762
2763        let url = format!("{}{}?{}", self.base_url(), EDIT_API_KEY, query_string);
2764
2765        let response = self.make_authenticated_request(&url).await?;
2766
2767        if !response.status().is_success() {
2768            let error_text = response
2769                .text()
2770                .await
2771                .unwrap_or_else(|_| "Unknown error".to_string());
2772            return Err(HttpError::RequestFailed(format!(
2773                "Edit API key failed: {}",
2774                error_text
2775            )));
2776        }
2777
2778        let api_response: ApiResponse<ApiKeyInfo> = response
2779            .json()
2780            .await
2781            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
2782
2783        if let Some(error) = api_response.error {
2784            return Err(HttpError::RequestFailed(format!(
2785                "API error: {} - {}",
2786                error.code, error.message
2787            )));
2788        }
2789
2790        api_response
2791            .result
2792            .ok_or_else(|| HttpError::InvalidResponse("No API key data in response".to_string()))
2793    }
2794
2795    /// Disable an API key
2796    ///
2797    /// Disables the API key with the specified ID. The key cannot be used
2798    /// for authentication until it is re-enabled.
2799    ///
2800    /// # Arguments
2801    ///
2802    /// * `id` - The ID of the API key to disable
2803    ///
2804    /// # Returns
2805    ///
2806    /// Returns the updated API key information with `enabled` set to `false`.
2807    ///
2808    /// # Errors
2809    ///
2810    /// Returns `HttpError` if the request fails or the API key is not found.
2811    pub async fn disable_api_key(&self, id: u64) -> Result<ApiKeyInfo, HttpError> {
2812        let query = format!("?id={}", id);
2813        self.private_get(DISABLE_API_KEY, &query).await
2814    }
2815
2816    /// Enable an API key
2817    ///
2818    /// Enables a previously disabled API key with the specified ID.
2819    ///
2820    /// # Arguments
2821    ///
2822    /// * `id` - The ID of the API key to enable
2823    ///
2824    /// # Returns
2825    ///
2826    /// Returns the updated API key information with `enabled` set to `true`.
2827    ///
2828    /// # Errors
2829    ///
2830    /// Returns `HttpError` if the request fails or the API key is not found.
2831    pub async fn enable_api_key(&self, id: u64) -> Result<ApiKeyInfo, HttpError> {
2832        let query = format!("?id={}", id);
2833        self.private_get(ENABLE_API_KEY, &query).await
2834    }
2835
2836    /// List all API keys
2837    ///
2838    /// Retrieves a list of all API keys associated with the account.
2839    ///
2840    /// # Returns
2841    ///
2842    /// Returns a vector of API key information.
2843    ///
2844    /// # Errors
2845    ///
2846    /// Returns `HttpError` if the request fails or authentication is invalid.
2847    ///
2848    /// # Example
2849    ///
2850    /// ```rust,no_run
2851    /// use deribit_http::DeribitHttpClient;
2852    ///
2853    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2854    /// let client = DeribitHttpClient::new();
2855    /// let api_keys = client.list_api_keys().await?;
2856    /// for key in api_keys {
2857    ///     println!("Key ID: {}, Name: {}, Enabled: {}", key.id, key.name, key.enabled);
2858    /// }
2859    /// # Ok(())
2860    /// # }
2861    /// ```
2862    pub async fn list_api_keys(&self) -> Result<Vec<ApiKeyInfo>, HttpError> {
2863        self.private_get(LIST_API_KEYS, "").await
2864    }
2865
2866    /// Remove an API key
2867    ///
2868    /// Permanently removes the API key with the specified ID.
2869    ///
2870    /// # Arguments
2871    ///
2872    /// * `id` - The ID of the API key to remove
2873    ///
2874    /// # Returns
2875    ///
2876    /// Returns `"ok"` on success.
2877    ///
2878    /// # Errors
2879    ///
2880    /// Returns `HttpError` if the request fails or the API key is not found.
2881    pub async fn remove_api_key(&self, id: u64) -> Result<String, HttpError> {
2882        let query = format!("?id={}", id);
2883        self.private_get(REMOVE_API_KEY, &query).await
2884    }
2885
2886    /// Reset an API key secret
2887    ///
2888    /// Generates a new client_secret for the API key with the specified ID.
2889    /// The old secret will no longer be valid.
2890    ///
2891    /// # Arguments
2892    ///
2893    /// * `id` - The ID of the API key to reset
2894    ///
2895    /// # Returns
2896    ///
2897    /// Returns the updated API key information with the new client_secret.
2898    ///
2899    /// # Errors
2900    ///
2901    /// Returns `HttpError` if the request fails or the API key is not found.
2902    pub async fn reset_api_key(&self, id: u64) -> Result<ApiKeyInfo, HttpError> {
2903        let query = format!("?id={}", id);
2904        self.private_get(RESET_API_KEY, &query).await
2905    }
2906
2907    /// Change API key name
2908    ///
2909    /// Changes the name of the API key with the specified ID.
2910    ///
2911    /// # Arguments
2912    ///
2913    /// * `id` - The ID of the API key
2914    /// * `name` - The new name (only letters, numbers and underscores; max 16 characters)
2915    ///
2916    /// # Returns
2917    ///
2918    /// Returns the updated API key information.
2919    ///
2920    /// # Errors
2921    ///
2922    /// Returns `HttpError` if the request fails or the API key is not found.
2923    pub async fn change_api_key_name(&self, id: u64, name: &str) -> Result<ApiKeyInfo, HttpError> {
2924        let query = format!("?id={}&name={}", id, urlencoding::encode(name));
2925        self.private_get(CHANGE_API_KEY_NAME, &query).await
2926    }
2927
2928    /// Change API key scope
2929    ///
2930    /// Changes the maximum scope of the API key with the specified ID.
2931    ///
2932    /// # Arguments
2933    ///
2934    /// * `id` - The ID of the API key
2935    /// * `max_scope` - The new maximum scope (e.g., "account:read trade:read_write")
2936    ///
2937    /// # Returns
2938    ///
2939    /// Returns the updated API key information.
2940    ///
2941    /// # Errors
2942    ///
2943    /// Returns `HttpError` if the request fails or the API key is not found.
2944    pub async fn change_scope_in_api_key(
2945        &self,
2946        id: u64,
2947        max_scope: &str,
2948    ) -> Result<ApiKeyInfo, HttpError> {
2949        let query = format!("?id={}&max_scope={}", id, urlencoding::encode(max_scope));
2950        self.private_get(CHANGE_SCOPE_IN_API_KEY, &query).await
2951    }
2952
2953    // ========================================================================
2954    // Address Beneficiary Endpoints
2955    // ========================================================================
2956
2957    /// Save address beneficiary information.
2958    ///
2959    /// Saves beneficiary information for a cryptocurrency address,
2960    /// required for travel rule compliance.
2961    ///
2962    /// # Arguments
2963    ///
2964    /// * `request` - The beneficiary information to save
2965    ///
2966    /// # Errors
2967    ///
2968    /// Returns `HttpError` if the request fails or the response is invalid.
2969    ///
2970    /// # Examples
2971    ///
2972    /// ```rust,no_run
2973    /// use deribit_http::DeribitHttpClient;
2974    /// use deribit_http::model::SaveAddressBeneficiaryRequest;
2975    ///
2976    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2977    /// let client = DeribitHttpClient::new();
2978    /// let request = SaveAddressBeneficiaryRequest {
2979    ///     currency: "BTC".to_string(),
2980    ///     address: "bc1qtest".to_string(),
2981    ///     agreed: true,
2982    ///     personal: false,
2983    ///     unhosted: false,
2984    ///     beneficiary_vasp_name: "Test VASP".to_string(),
2985    ///     beneficiary_vasp_did: "did:test:123".to_string(),
2986    ///     beneficiary_address: "Test Address".to_string(),
2987    ///     ..Default::default()
2988    /// };
2989    /// // let beneficiary = client.save_address_beneficiary(&request).await?;
2990    /// # Ok(())
2991    /// # }
2992    /// ```
2993    pub async fn save_address_beneficiary(
2994        &self,
2995        request: &crate::model::SaveAddressBeneficiaryRequest,
2996    ) -> Result<crate::model::AddressBeneficiary, HttpError> {
2997        let mut params = vec![
2998            format!("currency={}", urlencoding::encode(&request.currency)),
2999            format!("address={}", urlencoding::encode(&request.address)),
3000            format!("agreed={}", request.agreed),
3001            format!("personal={}", request.personal),
3002            format!("unhosted={}", request.unhosted),
3003            format!(
3004                "beneficiary_vasp_name={}",
3005                urlencoding::encode(&request.beneficiary_vasp_name)
3006            ),
3007            format!(
3008                "beneficiary_vasp_did={}",
3009                urlencoding::encode(&request.beneficiary_vasp_did)
3010            ),
3011            format!(
3012                "beneficiary_address={}",
3013                urlencoding::encode(&request.beneficiary_address)
3014            ),
3015        ];
3016
3017        if let Some(ref tag) = request.tag {
3018            params.push(format!("tag={}", urlencoding::encode(tag)));
3019        }
3020        if let Some(ref website) = request.beneficiary_vasp_website {
3021            params.push(format!(
3022                "beneficiary_vasp_website={}",
3023                urlencoding::encode(website)
3024            ));
3025        }
3026        if let Some(ref first_name) = request.beneficiary_first_name {
3027            params.push(format!(
3028                "beneficiary_first_name={}",
3029                urlencoding::encode(first_name)
3030            ));
3031        }
3032        if let Some(ref last_name) = request.beneficiary_last_name {
3033            params.push(format!(
3034                "beneficiary_last_name={}",
3035                urlencoding::encode(last_name)
3036            ));
3037        }
3038        if let Some(ref company_name) = request.beneficiary_company_name {
3039            params.push(format!(
3040                "beneficiary_company_name={}",
3041                urlencoding::encode(company_name)
3042            ));
3043        }
3044
3045        let url = format!(
3046            "{}{}?{}",
3047            self.base_url(),
3048            SAVE_ADDRESS_BENEFICIARY,
3049            params.join("&")
3050        );
3051
3052        let response = self.make_authenticated_request(&url).await?;
3053
3054        if !response.status().is_success() {
3055            let error_text = response
3056                .text()
3057                .await
3058                .unwrap_or_else(|_| "Unknown error".to_string());
3059            return Err(HttpError::RequestFailed(format!(
3060                "Save address beneficiary failed: {}",
3061                error_text
3062            )));
3063        }
3064
3065        let api_response: ApiResponse<crate::model::AddressBeneficiary> = response
3066            .json()
3067            .await
3068            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3069
3070        if let Some(error) = api_response.error {
3071            return Err(HttpError::RequestFailed(format!(
3072                "API error: {} - {}",
3073                error.code, error.message
3074            )));
3075        }
3076
3077        api_response.result.ok_or_else(|| {
3078            HttpError::InvalidResponse("No beneficiary data in response".to_string())
3079        })
3080    }
3081
3082    /// Delete address beneficiary information.
3083    ///
3084    /// Removes beneficiary information for the specified address.
3085    ///
3086    /// # Arguments
3087    ///
3088    /// * `currency` - Currency symbol (e.g., "BTC", "ETH")
3089    /// * `address` - The cryptocurrency address
3090    /// * `tag` - Optional tag for XRP addresses
3091    ///
3092    /// # Errors
3093    ///
3094    /// Returns `HttpError` if the request fails or the response is invalid.
3095    ///
3096    /// # Examples
3097    ///
3098    /// ```rust,no_run
3099    /// use deribit_http::DeribitHttpClient;
3100    ///
3101    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3102    /// let client = DeribitHttpClient::new();
3103    /// // let result = client.delete_address_beneficiary("BTC", "bc1qtest", None).await?;
3104    /// # Ok(())
3105    /// # }
3106    /// ```
3107    pub async fn delete_address_beneficiary(
3108        &self,
3109        currency: &str,
3110        address: &str,
3111        tag: Option<&str>,
3112    ) -> Result<String, HttpError> {
3113        let mut query = format!(
3114            "?currency={}&address={}",
3115            urlencoding::encode(currency),
3116            urlencoding::encode(address)
3117        );
3118        if let Some(t) = tag {
3119            query.push_str(&format!("&tag={}", urlencoding::encode(t)));
3120        }
3121        self.private_get(DELETE_ADDRESS_BENEFICIARY, &query).await
3122    }
3123
3124    /// Get address beneficiary information.
3125    ///
3126    /// Retrieves beneficiary information for the specified address.
3127    ///
3128    /// # Arguments
3129    ///
3130    /// * `currency` - Currency symbol (e.g., "BTC", "ETH")
3131    /// * `address` - The cryptocurrency address
3132    /// * `tag` - Optional tag for XRP addresses
3133    ///
3134    /// # Errors
3135    ///
3136    /// Returns `HttpError` if the request fails or the response is invalid.
3137    ///
3138    /// # Examples
3139    ///
3140    /// ```rust,no_run
3141    /// use deribit_http::DeribitHttpClient;
3142    ///
3143    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3144    /// let client = DeribitHttpClient::new();
3145    /// // let beneficiary = client.get_address_beneficiary("BTC", "bc1qtest", None).await?;
3146    /// # Ok(())
3147    /// # }
3148    /// ```
3149    pub async fn get_address_beneficiary(
3150        &self,
3151        currency: &str,
3152        address: &str,
3153        tag: Option<&str>,
3154    ) -> Result<crate::model::AddressBeneficiary, HttpError> {
3155        let mut query = format!(
3156            "?currency={}&address={}",
3157            urlencoding::encode(currency),
3158            urlencoding::encode(address)
3159        );
3160        if let Some(t) = tag {
3161            query.push_str(&format!("&tag={}", urlencoding::encode(t)));
3162        }
3163        self.private_get(GET_ADDRESS_BENEFICIARY, &query).await
3164    }
3165
3166    /// List address beneficiaries with filtering and pagination.
3167    ///
3168    /// Returns a paginated list of address beneficiaries with optional filters.
3169    ///
3170    /// # Arguments
3171    ///
3172    /// * `request` - Optional filtering and pagination parameters
3173    ///
3174    /// # Errors
3175    ///
3176    /// Returns `HttpError` if the request fails or the response is invalid.
3177    ///
3178    /// # Examples
3179    ///
3180    /// ```rust,no_run
3181    /// use deribit_http::DeribitHttpClient;
3182    /// use deribit_http::model::ListAddressBeneficiariesRequest;
3183    ///
3184    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3185    /// let client = DeribitHttpClient::new();
3186    /// let request = ListAddressBeneficiariesRequest {
3187    ///     currency: Some("BTC".to_string()),
3188    ///     limit: Some(10),
3189    ///     ..Default::default()
3190    /// };
3191    /// // let response = client.list_address_beneficiaries(Some(&request)).await?;
3192    /// # Ok(())
3193    /// # }
3194    /// ```
3195    pub async fn list_address_beneficiaries(
3196        &self,
3197        request: Option<&crate::model::ListAddressBeneficiariesRequest>,
3198    ) -> Result<crate::model::ListAddressBeneficiariesResponse, HttpError> {
3199        let mut params: Vec<String> = Vec::new();
3200
3201        if let Some(req) = request {
3202            if let Some(ref currency) = req.currency {
3203                params.push(format!("currency={}", urlencoding::encode(currency)));
3204            }
3205            if let Some(ref address) = req.address {
3206                params.push(format!("address={}", urlencoding::encode(address)));
3207            }
3208            if let Some(ref tag) = req.tag {
3209                params.push(format!("tag={}", urlencoding::encode(tag)));
3210            }
3211            if let Some(created_before) = req.created_before {
3212                params.push(format!("created_before={}", created_before));
3213            }
3214            if let Some(created_after) = req.created_after {
3215                params.push(format!("created_after={}", created_after));
3216            }
3217            if let Some(updated_before) = req.updated_before {
3218                params.push(format!("updated_before={}", updated_before));
3219            }
3220            if let Some(updated_after) = req.updated_after {
3221                params.push(format!("updated_after={}", updated_after));
3222            }
3223            if let Some(personal) = req.personal {
3224                params.push(format!("personal={}", personal));
3225            }
3226            if let Some(unhosted) = req.unhosted {
3227                params.push(format!("unhosted={}", unhosted));
3228            }
3229            if let Some(ref vasp_name) = req.beneficiary_vasp_name {
3230                params.push(format!(
3231                    "beneficiary_vasp_name={}",
3232                    urlencoding::encode(vasp_name)
3233                ));
3234            }
3235            if let Some(ref vasp_did) = req.beneficiary_vasp_did {
3236                params.push(format!(
3237                    "beneficiary_vasp_did={}",
3238                    urlencoding::encode(vasp_did)
3239                ));
3240            }
3241            if let Some(ref vasp_website) = req.beneficiary_vasp_website {
3242                params.push(format!(
3243                    "beneficiary_vasp_website={}",
3244                    urlencoding::encode(vasp_website)
3245                ));
3246            }
3247            if let Some(limit) = req.limit {
3248                params.push(format!("limit={}", limit));
3249            }
3250            if let Some(ref continuation) = req.continuation {
3251                params.push(format!(
3252                    "continuation={}",
3253                    urlencoding::encode(continuation)
3254                ));
3255            }
3256        }
3257
3258        let url = if params.is_empty() {
3259            format!("{}{}", self.base_url(), LIST_ADDRESS_BENEFICIARIES)
3260        } else {
3261            format!(
3262                "{}{}?{}",
3263                self.base_url(),
3264                LIST_ADDRESS_BENEFICIARIES,
3265                params.join("&")
3266            )
3267        };
3268
3269        let response = self.make_authenticated_request(&url).await?;
3270
3271        if !response.status().is_success() {
3272            let error_text = response
3273                .text()
3274                .await
3275                .unwrap_or_else(|_| "Unknown error".to_string());
3276            return Err(HttpError::RequestFailed(format!(
3277                "List address beneficiaries failed: {}",
3278                error_text
3279            )));
3280        }
3281
3282        let api_response: ApiResponse<crate::model::ListAddressBeneficiariesResponse> = response
3283            .json()
3284            .await
3285            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3286
3287        if let Some(error) = api_response.error {
3288            return Err(HttpError::RequestFailed(format!(
3289                "API error: {} - {}",
3290                error.code, error.message
3291            )));
3292        }
3293
3294        api_response.result.ok_or_else(|| {
3295            HttpError::InvalidResponse("No beneficiaries data in response".to_string())
3296        })
3297    }
3298
3299    /// Set clearance originator for a deposit.
3300    ///
3301    /// Sets the originator information for a deposit transaction,
3302    /// required for travel rule compliance.
3303    ///
3304    /// # Arguments
3305    ///
3306    /// * `deposit_id` - Identifier of the deposit
3307    /// * `originator` - Information about the originator
3308    ///
3309    /// # Errors
3310    ///
3311    /// Returns `HttpError` if the request fails or the response is invalid.
3312    ///
3313    /// # Examples
3314    ///
3315    /// ```rust,no_run
3316    /// use deribit_http::DeribitHttpClient;
3317    /// use deribit_http::model::{DepositId, Originator};
3318    ///
3319    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3320    /// let client = DeribitHttpClient::new();
3321    /// let deposit_id = DepositId {
3322    ///     currency: "BTC".to_string(),
3323    ///     user_id: 123,
3324    ///     address: "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz".to_string(),
3325    ///     tx_hash: "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda".to_string(),
3326    /// };
3327    /// let originator = Originator {
3328    ///     is_personal: false,
3329    ///     company_name: Some("Company Name".to_string()),
3330    ///     first_name: Some("First".to_string()),
3331    ///     last_name: Some("Last".to_string()),
3332    ///     address: "NL, Amsterdam, Street, 1".to_string(),
3333    /// };
3334    /// // let result = client.set_clearance_originator(&deposit_id, &originator).await?;
3335    /// # Ok(())
3336    /// # }
3337    /// ```
3338    pub async fn set_clearance_originator(
3339        &self,
3340        deposit_id: &crate::model::DepositId,
3341        originator: &crate::model::Originator,
3342    ) -> Result<crate::model::ClearanceDepositResult, HttpError> {
3343        let deposit_id_json = serde_json::to_string(deposit_id).map_err(|e| {
3344            HttpError::InvalidResponse(format!("Failed to serialize deposit_id: {}", e))
3345        })?;
3346        let originator_json = serde_json::to_string(originator).map_err(|e| {
3347            HttpError::InvalidResponse(format!("Failed to serialize originator: {}", e))
3348        })?;
3349
3350        let url = format!(
3351            "{}{}?deposit_id={}&originator={}",
3352            self.base_url(),
3353            SET_CLEARANCE_ORIGINATOR,
3354            urlencoding::encode(&deposit_id_json),
3355            urlencoding::encode(&originator_json)
3356        );
3357
3358        let response = self.make_authenticated_request(&url).await?;
3359
3360        if !response.status().is_success() {
3361            let error_text = response
3362                .text()
3363                .await
3364                .unwrap_or_else(|_| "Unknown error".to_string());
3365            return Err(HttpError::RequestFailed(format!(
3366                "Set clearance originator failed: {}",
3367                error_text
3368            )));
3369        }
3370
3371        let api_response: ApiResponse<crate::model::ClearanceDepositResult> = response
3372            .json()
3373            .await
3374            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3375
3376        if let Some(error) = api_response.error {
3377            return Err(HttpError::RequestFailed(format!(
3378                "API error: {} - {}",
3379                error.code, error.message
3380            )));
3381        }
3382
3383        api_response
3384            .result
3385            .ok_or_else(|| HttpError::InvalidResponse("No deposit result in response".to_string()))
3386    }
3387
3388    /// Get account access log
3389    ///
3390    /// Retrieves the account access history showing login attempts and API access.
3391    ///
3392    /// # Arguments
3393    ///
3394    /// * `count` - Number of entries to retrieve (optional, default 10)
3395    /// * `offset` - Offset for pagination (optional, default 0)
3396    ///
3397    pub async fn get_access_log(
3398        &self,
3399        count: Option<u32>,
3400        offset: Option<u32>,
3401    ) -> Result<crate::model::AccessLogResponse, HttpError> {
3402        let mut params = Vec::new();
3403        if let Some(count) = count {
3404            params.push(format!("count={}", count));
3405        }
3406        if let Some(offset) = offset {
3407            params.push(format!("offset={}", offset));
3408        }
3409        let query = if params.is_empty() {
3410            String::new()
3411        } else {
3412            format!("?{}", params.join("&"))
3413        };
3414        self.private_get(crate::constants::endpoints::GET_ACCESS_LOG, &query)
3415            .await
3416    }
3417
3418    /// Get user account locks
3419    ///
3420    /// Retrieves information about any locks on the user's account.
3421    ///
3422    pub async fn get_user_locks(&self) -> Result<Vec<crate::model::UserLock>, HttpError> {
3423        self.private_get(crate::constants::endpoints::GET_USER_LOCKS, "")
3424            .await
3425    }
3426
3427    /// List custody accounts
3428    ///
3429    /// Retrieves the list of custody accounts for the specified currency.
3430    ///
3431    /// # Arguments
3432    ///
3433    /// * `currency` - Currency symbol (BTC, ETH, etc.)
3434    ///
3435    pub async fn list_custody_accounts(
3436        &self,
3437        currency: &str,
3438    ) -> Result<Vec<crate::model::CustodyAccount>, HttpError> {
3439        let query = format!("?currency={}", urlencoding::encode(currency));
3440        self.private_get(crate::constants::endpoints::LIST_CUSTODY_ACCOUNTS, &query)
3441            .await
3442    }
3443
3444    /// Simulate portfolio margin
3445    ///
3446    /// Simulates portfolio margin for hypothetical positions.
3447    ///
3448    /// # Arguments
3449    ///
3450    /// * `request` - Simulation request parameters
3451    ///
3452    pub async fn simulate_portfolio(
3453        &self,
3454        request: crate::model::SimulatePortfolioRequest,
3455    ) -> Result<crate::model::SimulatePortfolioResponse, HttpError> {
3456        let mut query_params = vec![format!(
3457            "currency={}",
3458            urlencoding::encode(&request.currency)
3459        )];
3460
3461        if let Some(add_positions) = request.add_positions {
3462            query_params.push(format!("add_positions={}", add_positions));
3463        }
3464
3465        if let Some(ref positions) = request.simulated_positions {
3466            let positions_json = serde_json::to_string(positions)
3467                .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3468            query_params.push(format!(
3469                "simulated_positions={}",
3470                urlencoding::encode(&positions_json)
3471            ));
3472        }
3473
3474        let url = format!(
3475            "{}{}?{}",
3476            self.base_url(),
3477            crate::constants::endpoints::SIMULATE_PORTFOLIO,
3478            query_params.join("&")
3479        );
3480
3481        let response = self.make_authenticated_request(&url).await?;
3482
3483        if !response.status().is_success() {
3484            let error_text = response
3485                .text()
3486                .await
3487                .unwrap_or_else(|_| "Unknown error".to_string());
3488            return Err(HttpError::RequestFailed(format!(
3489                "Simulate portfolio failed: {}",
3490                error_text
3491            )));
3492        }
3493
3494        let api_response: ApiResponse<crate::model::SimulatePortfolioResponse> = response
3495            .json()
3496            .await
3497            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3498
3499        if let Some(error) = api_response.error {
3500            return Err(HttpError::RequestFailed(format!(
3501                "API error: {} - {}",
3502                error.code, error.message
3503            )));
3504        }
3505
3506        api_response.result.ok_or_else(|| {
3507            HttpError::InvalidResponse("No portfolio simulation data in response".to_string())
3508        })
3509    }
3510
3511    /// PME margin simulation
3512    ///
3513    /// Simulates Portfolio Margin Engine (PME) margin for the specified currency.
3514    ///
3515    /// # Arguments
3516    ///
3517    /// * `currency` - Currency symbol (BTC, ETH, etc.)
3518    ///
3519    pub async fn pme_simulate(
3520        &self,
3521        currency: &str,
3522    ) -> Result<crate::model::PmeSimulateResponse, HttpError> {
3523        let query = format!("?currency={}", urlencoding::encode(currency));
3524        self.private_get(crate::constants::endpoints::PME_SIMULATE, &query)
3525            .await
3526    }
3527
3528    /// Change margin model
3529    ///
3530    /// Changes the margin model for the account or a specific user.
3531    ///
3532    /// # Arguments
3533    ///
3534    /// * `margin_model` - The new margin model to set
3535    /// * `user_id` - Optional user ID (for main account operating on subaccounts)
3536    /// * `dry_run` - Optional flag to simulate the change without applying it
3537    ///
3538    pub async fn change_margin_model(
3539        &self,
3540        margin_model: crate::model::MarginModel,
3541        user_id: Option<u64>,
3542        dry_run: Option<bool>,
3543    ) -> Result<crate::model::ChangeMarginModelResponse, HttpError> {
3544        let mut query_params = vec![format!("margin_model={}", margin_model.as_str())];
3545
3546        if let Some(user_id) = user_id {
3547            query_params.push(format!("user_id={}", user_id));
3548        }
3549
3550        if let Some(dry_run) = dry_run {
3551            query_params.push(format!("dry_run={}", dry_run));
3552        }
3553
3554        let url = format!(
3555            "{}{}?{}",
3556            self.base_url(),
3557            crate::constants::endpoints::CHANGE_MARGIN_MODEL,
3558            query_params.join("&")
3559        );
3560
3561        let response = self.make_authenticated_request(&url).await?;
3562
3563        if !response.status().is_success() {
3564            let error_text = response
3565                .text()
3566                .await
3567                .unwrap_or_else(|_| "Unknown error".to_string());
3568            return Err(HttpError::RequestFailed(format!(
3569                "Change margin model failed: {}",
3570                error_text
3571            )));
3572        }
3573
3574        let api_response: ApiResponse<crate::model::ChangeMarginModelResponse> = response
3575            .json()
3576            .await
3577            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3578
3579        if let Some(error) = api_response.error {
3580            return Err(HttpError::RequestFailed(format!(
3581                "API error: {} - {}",
3582                error.code, error.message
3583            )));
3584        }
3585
3586        api_response.result.ok_or_else(|| {
3587            HttpError::InvalidResponse("No margin model change data in response".to_string())
3588        })
3589    }
3590
3591    /// Set self-trading configuration
3592    ///
3593    /// Configures self-trading prevention settings for the account.
3594    ///
3595    /// # Arguments
3596    ///
3597    /// * `mode` - Self-trading prevention mode
3598    /// * `extended_to_subaccounts` - Whether to extend the config to subaccounts
3599    /// * `block_rfq_self_match_prevention` - Optional RFQ self-match prevention setting
3600    ///
3601    pub async fn set_self_trading_config(
3602        &self,
3603        mode: crate::model::SelfTradingMode,
3604        extended_to_subaccounts: bool,
3605        block_rfq_self_match_prevention: Option<bool>,
3606    ) -> Result<bool, HttpError> {
3607        let mut query_params = vec![
3608            format!("mode={}", mode.as_str()),
3609            format!("extended_to_subaccounts={}", extended_to_subaccounts),
3610        ];
3611
3612        if let Some(block_rfq) = block_rfq_self_match_prevention {
3613            query_params.push(format!("block_rfq_self_match_prevention={}", block_rfq));
3614        }
3615
3616        let url = format!(
3617            "{}{}?{}",
3618            self.base_url(),
3619            crate::constants::endpoints::SET_SELF_TRADING_CONFIG,
3620            query_params.join("&")
3621        );
3622
3623        let response = self.make_authenticated_request(&url).await?;
3624
3625        if !response.status().is_success() {
3626            let error_text = response
3627                .text()
3628                .await
3629                .unwrap_or_else(|_| "Unknown error".to_string());
3630            return Err(HttpError::RequestFailed(format!(
3631                "Set self trading config failed: {}",
3632                error_text
3633            )));
3634        }
3635
3636        let api_response: ApiResponse<String> = response
3637            .json()
3638            .await
3639            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3640
3641        if let Some(error) = api_response.error {
3642            return Err(HttpError::RequestFailed(format!(
3643                "API error: {} - {}",
3644                error.code, error.message
3645            )));
3646        }
3647
3648        Ok(api_response.result.map(|s| s == "ok").unwrap_or(true))
3649    }
3650
3651    /// Set disabled trading products
3652    ///
3653    /// Disables specific trading products for a user.
3654    ///
3655    /// # Arguments
3656    ///
3657    /// * `trading_products` - List of trading products to disable
3658    /// * `user_id` - User ID to apply the setting to
3659    ///
3660    pub async fn set_disabled_trading_products(
3661        &self,
3662        trading_products: &[crate::model::TradingProduct],
3663        user_id: u64,
3664    ) -> Result<bool, HttpError> {
3665        let products: Vec<&str> = trading_products.iter().map(|p| p.as_str()).collect();
3666        let products_json = serde_json::to_string(&products)
3667            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3668
3669        let url = format!(
3670            "{}{}?trading_products={}&user_id={}",
3671            self.base_url(),
3672            crate::constants::endpoints::SET_DISABLED_TRADING_PRODUCTS,
3673            urlencoding::encode(&products_json),
3674            user_id
3675        );
3676
3677        let response = self.make_authenticated_request(&url).await?;
3678
3679        if !response.status().is_success() {
3680            let error_text = response
3681                .text()
3682                .await
3683                .unwrap_or_else(|_| "Unknown error".to_string());
3684            return Err(HttpError::RequestFailed(format!(
3685                "Set disabled trading products failed: {}",
3686                error_text
3687            )));
3688        }
3689
3690        let api_response: ApiResponse<String> = response
3691            .json()
3692            .await
3693            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3694
3695        if let Some(error) = api_response.error {
3696            return Err(HttpError::RequestFailed(format!(
3697                "API error: {} - {}",
3698                error.code, error.message
3699            )));
3700        }
3701
3702        Ok(api_response.result.map(|s| s == "ok").unwrap_or(true))
3703    }
3704
3705    /// Get new (unread) announcements
3706    ///
3707    /// Retrieves announcements that have not been marked as read.
3708    ///
3709    pub async fn get_new_announcements(
3710        &self,
3711    ) -> Result<Vec<crate::model::Announcement>, HttpError> {
3712        self.private_get(crate::constants::endpoints::GET_NEW_ANNOUNCEMENTS, "")
3713            .await
3714    }
3715
3716    /// Mark announcement as read
3717    ///
3718    /// Marks a specific announcement as read so it won't appear in new announcements.
3719    ///
3720    /// # Arguments
3721    ///
3722    /// * `announcement_id` - ID of the announcement to mark as read
3723    ///
3724    pub async fn set_announcement_as_read(&self, announcement_id: u64) -> Result<bool, HttpError> {
3725        let query = format!("?announcement_id={}", announcement_id);
3726        let result: String = self
3727            .private_get(
3728                crate::constants::endpoints::SET_ANNOUNCEMENT_AS_READ,
3729                &query,
3730            )
3731            .await?;
3732        Ok(result == "ok")
3733    }
3734
3735    /// Enable affiliate program
3736    ///
3737    /// Enables the affiliate program for the user's account.
3738    ///
3739    pub async fn enable_affiliate_program(&self) -> Result<bool, HttpError> {
3740        let result: String = self
3741            .private_get(crate::constants::endpoints::ENABLE_AFFILIATE_PROGRAM, "")
3742            .await?;
3743        Ok(result == "ok")
3744    }
3745
3746    /// Get affiliate program information
3747    ///
3748    /// Retrieves information about the user's affiliate program status.
3749    ///
3750    pub async fn get_affiliate_program_info(
3751        &self,
3752    ) -> Result<crate::model::AffiliateProgramInfo, HttpError> {
3753        self.private_get(crate::constants::endpoints::GET_AFFILIATE_PROGRAM_INFO, "")
3754            .await
3755    }
3756
3757    /// Set email language preference
3758    ///
3759    /// Sets the preferred language for email communications.
3760    ///
3761    /// # Arguments
3762    ///
3763    /// * `language` - The language to set for emails
3764    ///
3765    pub async fn set_email_language(
3766        &self,
3767        language: crate::model::EmailLanguage,
3768    ) -> Result<bool, HttpError> {
3769        let query = format!("?language={}", language.as_str());
3770        let result: String = self
3771            .private_get(crate::constants::endpoints::SET_EMAIL_LANGUAGE, &query)
3772            .await?;
3773        Ok(result == "ok")
3774    }
3775
3776    /// Get email language preference
3777    ///
3778    /// Retrieves the current email language preference.
3779    ///
3780    pub async fn get_email_language(&self) -> Result<String, HttpError> {
3781        self.private_get(crate::constants::endpoints::GET_EMAIL_LANGUAGE, "")
3782            .await
3783    }
3784
3785    // ========================================================================
3786    // Wallet Endpoints
3787    // ========================================================================
3788
3789    /// Create a withdrawal request
3790    ///
3791    /// Creates a new withdrawal request for the specified currency and amount.
3792    /// The destination address must be in the address book.
3793    ///
3794    /// # Arguments
3795    ///
3796    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
3797    /// * `address` - Withdrawal address (must be in address book)
3798    /// * `amount` - Amount to withdraw
3799    /// * `priority` - Optional withdrawal priority level
3800    ///
3801    /// # Returns
3802    ///
3803    /// Returns the withdrawal details including ID, state, and fee.
3804    ///
3805    /// # Errors
3806    ///
3807    /// Returns `HttpError` if the request fails or the address is not in the address book.
3808    pub async fn withdraw(
3809        &self,
3810        currency: &str,
3811        address: &str,
3812        amount: f64,
3813        priority: Option<crate::model::wallet::WithdrawalPriorityLevel>,
3814    ) -> Result<crate::model::Withdrawal, HttpError> {
3815        let mut query_params = vec![
3816            ("currency".to_string(), currency.to_string()),
3817            ("address".to_string(), address.to_string()),
3818            ("amount".to_string(), amount.to_string()),
3819        ];
3820
3821        if let Some(p) = priority {
3822            query_params.push(("priority".to_string(), p.as_str().to_string()));
3823        }
3824
3825        let query_string = query_params
3826            .iter()
3827            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
3828            .collect::<Vec<_>>()
3829            .join("&");
3830
3831        let url = format!("{}{}?{}", self.base_url(), WITHDRAW, query_string);
3832
3833        let response = self.make_authenticated_request(&url).await?;
3834
3835        if !response.status().is_success() {
3836            let error_text = response
3837                .text()
3838                .await
3839                .unwrap_or_else(|_| "Unknown error".to_string());
3840            return Err(HttpError::RequestFailed(format!(
3841                "Withdraw failed: {}",
3842                error_text
3843            )));
3844        }
3845
3846        let api_response: ApiResponse<crate::model::Withdrawal> = response
3847            .json()
3848            .await
3849            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
3850
3851        if let Some(error) = api_response.error {
3852            return Err(HttpError::RequestFailed(format!(
3853                "API error: {} - {}",
3854                error.code, error.message
3855            )));
3856        }
3857
3858        api_response
3859            .result
3860            .ok_or_else(|| HttpError::InvalidResponse("No withdrawal data in response".to_string()))
3861    }
3862
3863    /// Cancel a pending withdrawal
3864    ///
3865    /// Cancels a withdrawal request that has not yet been processed.
3866    ///
3867    /// # Arguments
3868    ///
3869    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
3870    /// * `id` - Withdrawal ID to cancel
3871    ///
3872    /// # Returns
3873    ///
3874    /// Returns the cancelled withdrawal details.
3875    ///
3876    /// # Errors
3877    ///
3878    /// Returns `HttpError` if the withdrawal cannot be cancelled or does not exist.
3879    pub async fn cancel_withdrawal(
3880        &self,
3881        currency: &str,
3882        id: u64,
3883    ) -> Result<crate::model::Withdrawal, HttpError> {
3884        let query = format!("?currency={}&id={}", urlencoding::encode(currency), id);
3885        self.private_get(CANCEL_WITHDRAWAL, &query).await
3886    }
3887
3888    /// Create a new deposit address
3889    ///
3890    /// Generates a new deposit address for the specified currency.
3891    ///
3892    /// # Arguments
3893    ///
3894    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
3895    ///
3896    /// # Returns
3897    ///
3898    /// Returns the newly created deposit address.
3899    ///
3900    /// # Errors
3901    ///
3902    /// Returns `HttpError` if address creation fails.
3903    pub async fn create_deposit_address(
3904        &self,
3905        currency: &str,
3906    ) -> Result<crate::model::wallet::DepositAddress, HttpError> {
3907        let query = format!("?currency={}", urlencoding::encode(currency));
3908        self.private_get(CREATE_DEPOSIT_ADDRESS, &query).await
3909    }
3910
3911    /// Get the current deposit address
3912    ///
3913    /// Retrieves the current deposit address for the specified currency.
3914    ///
3915    /// # Arguments
3916    ///
3917    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
3918    ///
3919    /// # Returns
3920    ///
3921    /// Returns the current deposit address.
3922    ///
3923    /// # Errors
3924    ///
3925    /// Returns `HttpError` if no address exists or the request fails.
3926    pub async fn get_current_deposit_address(
3927        &self,
3928        currency: &str,
3929    ) -> Result<crate::model::wallet::DepositAddress, HttpError> {
3930        let query = format!("?currency={}", urlencoding::encode(currency));
3931        self.private_get(GET_CURRENT_DEPOSIT_ADDRESS, &query).await
3932    }
3933
3934    /// Add an address to the address book
3935    ///
3936    /// Adds a new address entry to the address book.
3937    ///
3938    /// # Arguments
3939    ///
3940    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
3941    /// * `address_type` - Type of address book entry
3942    /// * `address` - The cryptocurrency address
3943    /// * `label` - Optional label for the address
3944    /// * `tag` - Optional tag for XRP addresses
3945    ///
3946    /// # Returns
3947    ///
3948    /// Returns the created address book entry.
3949    ///
3950    /// # Errors
3951    ///
3952    /// Returns `HttpError` if the address is invalid or already exists.
3953    pub async fn add_to_address_book(
3954        &self,
3955        currency: &str,
3956        address_type: crate::model::wallet::AddressBookType,
3957        address: &str,
3958        label: Option<&str>,
3959        tag: Option<&str>,
3960    ) -> Result<crate::model::wallet::AddressBookEntry, HttpError> {
3961        let mut query_params = vec![
3962            ("currency".to_string(), currency.to_string()),
3963            ("type".to_string(), address_type.as_str().to_string()),
3964            ("address".to_string(), address.to_string()),
3965        ];
3966
3967        if let Some(l) = label {
3968            query_params.push(("label".to_string(), l.to_string()));
3969        }
3970
3971        if let Some(t) = tag {
3972            query_params.push(("tag".to_string(), t.to_string()));
3973        }
3974
3975        let query_string = query_params
3976            .iter()
3977            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
3978            .collect::<Vec<_>>()
3979            .join("&");
3980
3981        let url = format!(
3982            "{}{}?{}",
3983            self.base_url(),
3984            ADD_TO_ADDRESS_BOOK,
3985            query_string
3986        );
3987
3988        let response = self.make_authenticated_request(&url).await?;
3989
3990        if !response.status().is_success() {
3991            let error_text = response
3992                .text()
3993                .await
3994                .unwrap_or_else(|_| "Unknown error".to_string());
3995            return Err(HttpError::RequestFailed(format!(
3996                "Add to address book failed: {}",
3997                error_text
3998            )));
3999        }
4000
4001        let api_response: ApiResponse<crate::model::wallet::AddressBookEntry> = response
4002            .json()
4003            .await
4004            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4005
4006        if let Some(error) = api_response.error {
4007            return Err(HttpError::RequestFailed(format!(
4008                "API error: {} - {}",
4009                error.code, error.message
4010            )));
4011        }
4012
4013        api_response.result.ok_or_else(|| {
4014            HttpError::InvalidResponse("No address book entry in response".to_string())
4015        })
4016    }
4017
4018    /// Remove an address from the address book
4019    ///
4020    /// Removes an address entry from the address book.
4021    ///
4022    /// # Arguments
4023    ///
4024    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
4025    /// * `address_type` - Type of address book entry
4026    /// * `address` - The cryptocurrency address to remove
4027    ///
4028    /// # Returns
4029    ///
4030    /// Returns `true` if the address was successfully removed.
4031    ///
4032    /// # Errors
4033    ///
4034    /// Returns `HttpError` if the address does not exist or cannot be removed.
4035    pub async fn remove_from_address_book(
4036        &self,
4037        currency: &str,
4038        address_type: crate::model::wallet::AddressBookType,
4039        address: &str,
4040    ) -> Result<bool, HttpError> {
4041        let query = format!(
4042            "?currency={}&type={}&address={}",
4043            urlencoding::encode(currency),
4044            urlencoding::encode(address_type.as_str()),
4045            urlencoding::encode(address)
4046        );
4047        let result: String = self.private_get(REMOVE_FROM_ADDRESS_BOOK, &query).await?;
4048        Ok(result == "ok")
4049    }
4050
4051    /// Update an address in the address book
4052    ///
4053    /// Updates beneficiary information for an existing address book entry.
4054    /// This is used for travel rule compliance.
4055    ///
4056    /// # Arguments
4057    ///
4058    /// * `request` - The update request containing all required fields
4059    ///
4060    /// # Returns
4061    ///
4062    /// Returns `true` if the address was successfully updated.
4063    ///
4064    /// # Errors
4065    ///
4066    /// Returns `HttpError` if the address does not exist or validation fails.
4067    pub async fn update_in_address_book(
4068        &self,
4069        request: &crate::model::request::wallet::UpdateInAddressBookRequest,
4070    ) -> Result<bool, HttpError> {
4071        let mut query_params = vec![
4072            ("currency".to_string(), request.currency.clone()),
4073            (
4074                "type".to_string(),
4075                request.address_type.as_str().to_string(),
4076            ),
4077            ("address".to_string(), request.address.clone()),
4078            ("label".to_string(), request.label.clone()),
4079            ("agreed".to_string(), request.agreed.to_string()),
4080            ("personal".to_string(), request.personal.to_string()),
4081            (
4082                "beneficiary_vasp_name".to_string(),
4083                request.beneficiary_vasp_name.clone(),
4084            ),
4085            (
4086                "beneficiary_vasp_did".to_string(),
4087                request.beneficiary_vasp_did.clone(),
4088            ),
4089            (
4090                "beneficiary_address".to_string(),
4091                request.beneficiary_address.clone(),
4092            ),
4093        ];
4094
4095        if let Some(ref website) = request.beneficiary_vasp_website {
4096            query_params.push(("beneficiary_vasp_website".to_string(), website.clone()));
4097        }
4098
4099        if let Some(ref first_name) = request.beneficiary_first_name {
4100            query_params.push(("beneficiary_first_name".to_string(), first_name.clone()));
4101        }
4102
4103        if let Some(ref last_name) = request.beneficiary_last_name {
4104            query_params.push(("beneficiary_last_name".to_string(), last_name.clone()));
4105        }
4106
4107        if let Some(ref company_name) = request.beneficiary_company_name {
4108            query_params.push(("beneficiary_company_name".to_string(), company_name.clone()));
4109        }
4110
4111        if let Some(ref tag) = request.tag {
4112            query_params.push(("tag".to_string(), tag.clone()));
4113        }
4114
4115        let query_string = query_params
4116            .iter()
4117            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
4118            .collect::<Vec<_>>()
4119            .join("&");
4120
4121        let url = format!(
4122            "{}{}?{}",
4123            self.base_url(),
4124            UPDATE_IN_ADDRESS_BOOK,
4125            query_string
4126        );
4127
4128        let response = self.make_authenticated_request(&url).await?;
4129
4130        if !response.status().is_success() {
4131            let error_text = response
4132                .text()
4133                .await
4134                .unwrap_or_else(|_| "Unknown error".to_string());
4135            return Err(HttpError::RequestFailed(format!(
4136                "Update in address book failed: {}",
4137                error_text
4138            )));
4139        }
4140
4141        let api_response: ApiResponse<String> = response
4142            .json()
4143            .await
4144            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4145
4146        if let Some(error) = api_response.error {
4147            return Err(HttpError::RequestFailed(format!(
4148                "API error: {} - {}",
4149                error.code, error.message
4150            )));
4151        }
4152
4153        Ok(api_response.result.map(|s| s == "ok").unwrap_or(true))
4154    }
4155
4156    /// Get addresses from the address book
4157    ///
4158    /// Retrieves address book entries for a specific currency and type.
4159    ///
4160    /// # Arguments
4161    ///
4162    /// * `currency` - Currency symbol (BTC, ETH, USDC, etc.)
4163    /// * `address_type` - Type of address book entries to retrieve
4164    ///
4165    /// # Returns
4166    ///
4167    /// Returns a list of address book entries.
4168    ///
4169    /// # Errors
4170    ///
4171    /// Returns `HttpError` if the request fails.
4172    pub async fn get_address_book(
4173        &self,
4174        currency: &str,
4175        address_type: crate::model::wallet::AddressBookType,
4176    ) -> Result<Vec<crate::model::wallet::AddressBookEntry>, HttpError> {
4177        let query = format!(
4178            "?currency={}&type={}",
4179            urlencoding::encode(currency),
4180            urlencoding::encode(address_type.as_str())
4181        );
4182        self.private_get(GET_ADDRESS_BOOK, &query).await
4183    }
4184
4185    // ========================================================================
4186    // Block Trade Endpoints
4187    // ========================================================================
4188
4189    /// Approve a pending block trade
4190    ///
4191    /// Used to approve a pending block trade. The `nonce` and `timestamp` identify
4192    /// the block trade while `role` should be opposite to the trading counterparty.
4193    ///
4194    /// To use block trade approval, the API key must have `enabled_features: block_trade_approval`.
4195    ///
4196    /// # Arguments
4197    ///
4198    /// * `timestamp` - Timestamp shared with other party, in milliseconds since UNIX epoch
4199    /// * `nonce` - Nonce shared with other party
4200    /// * `role` - Role in the trade (maker or taker)
4201    ///
4202    /// # Returns
4203    ///
4204    /// Returns `true` if the block trade was successfully approved.
4205    ///
4206    /// # Errors
4207    ///
4208    /// Returns `HttpError` if the request fails or the block trade cannot be approved.
4209    pub async fn approve_block_trade(
4210        &self,
4211        timestamp: u64,
4212        nonce: &str,
4213        role: crate::model::block_trade::BlockTradeRole,
4214    ) -> Result<bool, HttpError> {
4215        let query = format!(
4216            "?timestamp={}&nonce={}&role={}",
4217            timestamp,
4218            urlencoding::encode(nonce),
4219            urlencoding::encode(&role.to_string())
4220        );
4221        let result: String = self.private_get(APPROVE_BLOCK_TRADE, &query).await?;
4222        Ok(result == "ok")
4223    }
4224
4225    /// Execute a block trade
4226    ///
4227    /// Creates and executes a block trade with the counterparty signature.
4228    /// Both parties must agree on the same timestamp, nonce, and trades fields.
4229    /// The server ensures that roles are different between the two parties.
4230    ///
4231    /// # Arguments
4232    ///
4233    /// * `request` - The execute block trade request containing all required parameters
4234    ///
4235    /// # Returns
4236    ///
4237    /// Returns the block trade result with trade details.
4238    ///
4239    /// # Errors
4240    ///
4241    /// Returns `HttpError` if the request fails or the block trade cannot be executed.
4242    pub async fn execute_block_trade(
4243        &self,
4244        request: &crate::model::block_trade::ExecuteBlockTradeRequest,
4245    ) -> Result<crate::model::block_trade::BlockTradeResult, HttpError> {
4246        let trades_json = serde_json::to_string(&request.trades).map_err(|e| {
4247            HttpError::InvalidResponse(format!("Failed to serialize trades: {}", e))
4248        })?;
4249
4250        let query_params = [
4251            ("timestamp".to_string(), request.timestamp.to_string()),
4252            ("nonce".to_string(), request.nonce.clone()),
4253            ("role".to_string(), request.role.to_string()),
4254            ("trades".to_string(), trades_json),
4255            (
4256                "counterparty_signature".to_string(),
4257                request.counterparty_signature.clone(),
4258            ),
4259        ];
4260
4261        let query_string = query_params
4262            .iter()
4263            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
4264            .collect::<Vec<_>>()
4265            .join("&");
4266
4267        let url = format!(
4268            "{}{}?{}",
4269            self.base_url(),
4270            EXECUTE_BLOCK_TRADE,
4271            query_string
4272        );
4273
4274        let response = self.make_authenticated_request(&url).await?;
4275
4276        if !response.status().is_success() {
4277            let error_text = response
4278                .text()
4279                .await
4280                .unwrap_or_else(|_| "Unknown error".to_string());
4281            return Err(HttpError::RequestFailed(format!(
4282                "Execute block trade failed: {}",
4283                error_text
4284            )));
4285        }
4286
4287        let api_response: ApiResponse<crate::model::block_trade::BlockTradeResult> = response
4288            .json()
4289            .await
4290            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4291
4292        if let Some(error) = api_response.error {
4293            return Err(HttpError::RequestFailed(format!(
4294                "API error: {} - {}",
4295                error.code, error.message
4296            )));
4297        }
4298
4299        api_response.result.ok_or_else(|| {
4300            HttpError::InvalidResponse("No block trade result in response".to_string())
4301        })
4302    }
4303
4304    /// Get a specific block trade by ID
4305    ///
4306    /// Returns information about a user's block trade.
4307    ///
4308    /// # Arguments
4309    ///
4310    /// * `id` - Block trade ID
4311    ///
4312    /// # Returns
4313    ///
4314    /// Returns the block trade details.
4315    ///
4316    /// # Errors
4317    ///
4318    /// Returns `HttpError` if the request fails or the block trade is not found.
4319    pub async fn get_block_trade(
4320        &self,
4321        id: &str,
4322    ) -> Result<crate::model::block_trade::BlockTrade, HttpError> {
4323        let query = format!("?id={}", urlencoding::encode(id));
4324        self.private_get(GET_BLOCK_TRADE, &query).await
4325    }
4326
4327    /// Get pending block trade requests
4328    ///
4329    /// Provides a list of pending block trade approvals.
4330    /// The `timestamp` and `nonce` received in response can be used to
4331    /// approve or reject the pending block trade.
4332    ///
4333    /// # Arguments
4334    ///
4335    /// * `broker_code` - Optional broker code to filter by
4336    ///
4337    /// # Returns
4338    ///
4339    /// Returns a list of pending block trade requests.
4340    ///
4341    /// # Errors
4342    ///
4343    /// Returns `HttpError` if the request fails.
4344    pub async fn get_block_trade_requests(
4345        &self,
4346        broker_code: Option<&str>,
4347    ) -> Result<Vec<crate::model::block_trade::BlockTradeRequest>, HttpError> {
4348        let mut query_params = Vec::new();
4349
4350        if let Some(code) = broker_code {
4351            query_params.push(("broker_code".to_string(), code.to_string()));
4352        }
4353
4354        let query_string = if query_params.is_empty() {
4355            String::new()
4356        } else {
4357            format!(
4358                "?{}",
4359                query_params
4360                    .iter()
4361                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
4362                    .collect::<Vec<_>>()
4363                    .join("&")
4364            )
4365        };
4366
4367        let url = format!(
4368            "{}{}{}",
4369            self.base_url(),
4370            GET_BLOCK_TRADE_REQUESTS,
4371            query_string
4372        );
4373
4374        let response = self.make_authenticated_request(&url).await?;
4375
4376        if !response.status().is_success() {
4377            let error_text = response
4378                .text()
4379                .await
4380                .unwrap_or_else(|_| "Unknown error".to_string());
4381            return Err(HttpError::RequestFailed(format!(
4382                "Get block trade requests failed: {}",
4383                error_text
4384            )));
4385        }
4386
4387        let api_response: ApiResponse<Vec<crate::model::block_trade::BlockTradeRequest>> = response
4388            .json()
4389            .await
4390            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4391
4392        if let Some(error) = api_response.error {
4393            return Err(HttpError::RequestFailed(format!(
4394                "API error: {} - {}",
4395                error.code, error.message
4396            )));
4397        }
4398
4399        Ok(api_response.result.unwrap_or_default())
4400    }
4401
4402    /// Get block trades with optional filters
4403    ///
4404    /// Returns a list of block trades for the user.
4405    ///
4406    /// # Arguments
4407    ///
4408    /// * `request` - Request parameters with optional filters
4409    ///
4410    /// # Returns
4411    ///
4412    /// Returns a list of block trades.
4413    ///
4414    /// # Errors
4415    ///
4416    /// Returns `HttpError` if the request fails.
4417    pub async fn get_block_trades(
4418        &self,
4419        request: &crate::model::block_trade::GetBlockTradesRequest,
4420    ) -> Result<Vec<crate::model::block_trade::BlockTrade>, HttpError> {
4421        let mut query_params = Vec::new();
4422
4423        if let Some(ref currency) = request.currency {
4424            query_params.push(("currency".to_string(), currency.clone()));
4425        }
4426        if let Some(count) = request.count {
4427            query_params.push(("count".to_string(), count.to_string()));
4428        }
4429        if let Some(ref continuation) = request.continuation {
4430            query_params.push(("continuation".to_string(), continuation.clone()));
4431        }
4432        if let Some(start_ts) = request.start_timestamp {
4433            query_params.push(("start_timestamp".to_string(), start_ts.to_string()));
4434        }
4435        if let Some(end_ts) = request.end_timestamp {
4436            query_params.push(("end_timestamp".to_string(), end_ts.to_string()));
4437        }
4438
4439        let query_string = if query_params.is_empty() {
4440            String::new()
4441        } else {
4442            format!(
4443                "?{}",
4444                query_params
4445                    .iter()
4446                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
4447                    .collect::<Vec<_>>()
4448                    .join("&")
4449            )
4450        };
4451
4452        let url = format!("{}{}{}", self.base_url(), GET_BLOCK_TRADES, query_string);
4453
4454        let response = self.make_authenticated_request(&url).await?;
4455
4456        if !response.status().is_success() {
4457            let error_text = response
4458                .text()
4459                .await
4460                .unwrap_or_else(|_| "Unknown error".to_string());
4461            return Err(HttpError::RequestFailed(format!(
4462                "Get block trades failed: {}",
4463                error_text
4464            )));
4465        }
4466
4467        let api_response: ApiResponse<Vec<crate::model::block_trade::BlockTrade>> = response
4468            .json()
4469            .await
4470            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4471
4472        if let Some(error) = api_response.error {
4473            return Err(HttpError::RequestFailed(format!(
4474                "API error: {} - {}",
4475                error.code, error.message
4476            )));
4477        }
4478
4479        Ok(api_response.result.unwrap_or_default())
4480    }
4481
4482    /// Get broker trade requests
4483    ///
4484    /// Provides a list of pending broker trade requests.
4485    ///
4486    /// # Returns
4487    ///
4488    /// Returns a list of broker trade requests.
4489    ///
4490    /// # Errors
4491    ///
4492    /// Returns `HttpError` if the request fails.
4493    pub async fn get_broker_trade_requests(
4494        &self,
4495    ) -> Result<Vec<crate::model::block_trade::BlockTradeRequest>, HttpError> {
4496        self.private_get(GET_BROKER_TRADE_REQUESTS, "").await
4497    }
4498
4499    /// Get broker trades with optional filters
4500    ///
4501    /// Returns a list of broker trades.
4502    ///
4503    /// # Arguments
4504    ///
4505    /// * `request` - Request parameters with optional filters
4506    ///
4507    /// # Returns
4508    ///
4509    /// Returns a list of broker trades.
4510    ///
4511    /// # Errors
4512    ///
4513    /// Returns `HttpError` if the request fails.
4514    pub async fn get_broker_trades(
4515        &self,
4516        request: &crate::model::block_trade::GetBlockTradesRequest,
4517    ) -> Result<Vec<crate::model::block_trade::BlockTrade>, HttpError> {
4518        let mut query_params = Vec::new();
4519
4520        if let Some(ref currency) = request.currency {
4521            query_params.push(("currency".to_string(), currency.clone()));
4522        }
4523        if let Some(count) = request.count {
4524            query_params.push(("count".to_string(), count.to_string()));
4525        }
4526        if let Some(ref continuation) = request.continuation {
4527            query_params.push(("continuation".to_string(), continuation.clone()));
4528        }
4529        if let Some(start_ts) = request.start_timestamp {
4530            query_params.push(("start_timestamp".to_string(), start_ts.to_string()));
4531        }
4532        if let Some(end_ts) = request.end_timestamp {
4533            query_params.push(("end_timestamp".to_string(), end_ts.to_string()));
4534        }
4535
4536        let query_string = if query_params.is_empty() {
4537            String::new()
4538        } else {
4539            format!(
4540                "?{}",
4541                query_params
4542                    .iter()
4543                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
4544                    .collect::<Vec<_>>()
4545                    .join("&")
4546            )
4547        };
4548
4549        let url = format!("{}{}{}", self.base_url(), GET_BROKER_TRADES, query_string);
4550
4551        let response = self.make_authenticated_request(&url).await?;
4552
4553        if !response.status().is_success() {
4554            let error_text = response
4555                .text()
4556                .await
4557                .unwrap_or_else(|_| "Unknown error".to_string());
4558            return Err(HttpError::RequestFailed(format!(
4559                "Get broker trades failed: {}",
4560                error_text
4561            )));
4562        }
4563
4564        let api_response: ApiResponse<Vec<crate::model::block_trade::BlockTrade>> = response
4565            .json()
4566            .await
4567            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4568
4569        if let Some(error) = api_response.error {
4570            return Err(HttpError::RequestFailed(format!(
4571                "API error: {} - {}",
4572                error.code, error.message
4573            )));
4574        }
4575
4576        Ok(api_response.result.unwrap_or_default())
4577    }
4578
4579    /// Invalidate a block trade signature
4580    ///
4581    /// Invalidates a previously generated block trade signature.
4582    ///
4583    /// # Arguments
4584    ///
4585    /// * `signature` - The signature to invalidate
4586    ///
4587    /// # Returns
4588    ///
4589    /// Returns `true` if the signature was successfully invalidated.
4590    ///
4591    /// # Errors
4592    ///
4593    /// Returns `HttpError` if the request fails.
4594    pub async fn invalidate_block_trade_signature(
4595        &self,
4596        signature: &str,
4597    ) -> Result<bool, HttpError> {
4598        let query = format!("?signature={}", urlencoding::encode(signature));
4599        let result: String = self
4600            .private_get(INVALIDATE_BLOCK_TRADE_SIGNATURE, &query)
4601            .await?;
4602        Ok(result == "ok")
4603    }
4604
4605    /// Reject a pending block trade
4606    ///
4607    /// Used to reject a pending block trade.
4608    ///
4609    /// # Arguments
4610    ///
4611    /// * `timestamp` - Timestamp shared with other party, in milliseconds since UNIX epoch
4612    /// * `nonce` - Nonce shared with other party
4613    /// * `role` - Role in the trade (maker or taker)
4614    ///
4615    /// # Returns
4616    ///
4617    /// Returns `true` if the block trade was successfully rejected.
4618    ///
4619    /// # Errors
4620    ///
4621    /// Returns `HttpError` if the request fails.
4622    pub async fn reject_block_trade(
4623        &self,
4624        timestamp: u64,
4625        nonce: &str,
4626        role: crate::model::block_trade::BlockTradeRole,
4627    ) -> Result<bool, HttpError> {
4628        let query = format!(
4629            "?timestamp={}&nonce={}&role={}",
4630            timestamp,
4631            urlencoding::encode(nonce),
4632            urlencoding::encode(&role.to_string())
4633        );
4634        let result: String = self.private_get(REJECT_BLOCK_TRADE, &query).await?;
4635        Ok(result == "ok")
4636    }
4637
4638    /// Simulate a block trade
4639    ///
4640    /// Checks if a block trade can be executed.
4641    ///
4642    /// # Arguments
4643    ///
4644    /// * `request` - The simulation request containing trades and optional role
4645    ///
4646    /// # Returns
4647    ///
4648    /// Returns `true` if the block trade can be executed, `false` otherwise.
4649    ///
4650    /// # Errors
4651    ///
4652    /// Returns `HttpError` if the request fails.
4653    pub async fn simulate_block_trade(
4654        &self,
4655        request: &crate::model::block_trade::SimulateBlockTradeRequest,
4656    ) -> Result<bool, HttpError> {
4657        let trades_json = serde_json::to_string(&request.trades).map_err(|e| {
4658            HttpError::InvalidResponse(format!("Failed to serialize trades: {}", e))
4659        })?;
4660
4661        let mut query_params = vec![("trades".to_string(), trades_json)];
4662
4663        if let Some(ref role) = request.role {
4664            query_params.push(("role".to_string(), role.to_string()));
4665        }
4666
4667        let query_string = query_params
4668            .iter()
4669            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
4670            .collect::<Vec<_>>()
4671            .join("&");
4672
4673        let url = format!(
4674            "{}{}?{}",
4675            self.base_url(),
4676            SIMULATE_BLOCK_TRADE,
4677            query_string
4678        );
4679
4680        let response = self.make_authenticated_request(&url).await?;
4681
4682        if !response.status().is_success() {
4683            let error_text = response
4684                .text()
4685                .await
4686                .unwrap_or_else(|_| "Unknown error".to_string());
4687            return Err(HttpError::RequestFailed(format!(
4688                "Simulate block trade failed: {}",
4689                error_text
4690            )));
4691        }
4692
4693        let api_response: ApiResponse<bool> = response
4694            .json()
4695            .await
4696            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4697
4698        if let Some(error) = api_response.error {
4699            return Err(HttpError::RequestFailed(format!(
4700                "API error: {} - {}",
4701                error.code, error.message
4702            )));
4703        }
4704
4705        Ok(api_response.result.unwrap_or(false))
4706    }
4707
4708    /// Verify and create a block trade signature
4709    ///
4710    /// Verifies the block trade parameters and creates a signature.
4711    /// The signature is valid for 5 minutes around the given timestamp.
4712    ///
4713    /// # Arguments
4714    ///
4715    /// * `request` - The verify block trade request containing all required parameters
4716    ///
4717    /// # Returns
4718    ///
4719    /// Returns the block trade signature.
4720    ///
4721    /// # Errors
4722    ///
4723    /// Returns `HttpError` if the request fails or verification fails.
4724    pub async fn verify_block_trade(
4725        &self,
4726        request: &crate::model::block_trade::VerifyBlockTradeRequest,
4727    ) -> Result<crate::model::block_trade::BlockTradeSignature, HttpError> {
4728        let trades_json = serde_json::to_string(&request.trades).map_err(|e| {
4729            HttpError::InvalidResponse(format!("Failed to serialize trades: {}", e))
4730        })?;
4731
4732        let query_params = [
4733            ("timestamp".to_string(), request.timestamp.to_string()),
4734            ("nonce".to_string(), request.nonce.clone()),
4735            ("role".to_string(), request.role.to_string()),
4736            ("trades".to_string(), trades_json),
4737        ];
4738
4739        let query_string = query_params
4740            .iter()
4741            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
4742            .collect::<Vec<_>>()
4743            .join("&");
4744
4745        let url = format!("{}{}?{}", self.base_url(), VERIFY_BLOCK_TRADE, query_string);
4746
4747        let response = self.make_authenticated_request(&url).await?;
4748
4749        if !response.status().is_success() {
4750            let error_text = response
4751                .text()
4752                .await
4753                .unwrap_or_else(|_| "Unknown error".to_string());
4754            return Err(HttpError::RequestFailed(format!(
4755                "Verify block trade failed: {}",
4756                error_text
4757            )));
4758        }
4759
4760        let api_response: ApiResponse<crate::model::block_trade::BlockTradeSignature> = response
4761            .json()
4762            .await
4763            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4764
4765        if let Some(error) = api_response.error {
4766            return Err(HttpError::RequestFailed(format!(
4767                "API error: {} - {}",
4768                error.code, error.message
4769            )));
4770        }
4771
4772        api_response
4773            .result
4774            .ok_or_else(|| HttpError::InvalidResponse("No signature in response".to_string()))
4775    }
4776
4777    // ========================================================================
4778    // Combo Books Endpoints
4779    // ========================================================================
4780
4781    /// Create a combo book
4782    ///
4783    /// Verifies and creates a combo book or returns an existing combo
4784    /// matching the given trades.
4785    ///
4786    /// # Arguments
4787    ///
4788    /// * `trades` - List of trades used to create a combo
4789    ///
4790    /// # Errors
4791    ///
4792    /// Returns `HttpError` if the request fails or the response is invalid.
4793    ///
4794    /// # Examples
4795    ///
4796    /// ```rust,no_run
4797    /// use deribit_http::DeribitHttpClient;
4798    /// use deribit_http::model::ComboTrade;
4799    ///
4800    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
4801    /// let client = DeribitHttpClient::new();
4802    /// let trades = vec![
4803    ///     ComboTrade::buy("BTC-29APR22-37500-C", Some(1.0)),
4804    ///     ComboTrade::sell("BTC-29APR22-37500-P", Some(1.0)),
4805    /// ];
4806    /// // let combo = client.create_combo(&trades).await?;
4807    /// # Ok(())
4808    /// # }
4809    /// ```
4810    pub async fn create_combo(
4811        &self,
4812        trades: &[crate::model::ComboTrade],
4813    ) -> Result<crate::model::Combo, HttpError> {
4814        let trades_json = serde_json::to_string(trades).map_err(|e| {
4815            HttpError::InvalidResponse(format!("Failed to serialize trades: {}", e))
4816        })?;
4817
4818        let url = format!(
4819            "{}{}?trades={}",
4820            self.base_url(),
4821            CREATE_COMBO,
4822            urlencoding::encode(&trades_json)
4823        );
4824
4825        let response = self.make_authenticated_request(&url).await?;
4826
4827        if !response.status().is_success() {
4828            let error_text = response
4829                .text()
4830                .await
4831                .unwrap_or_else(|_| "Unknown error".to_string());
4832            return Err(HttpError::RequestFailed(format!(
4833                "Create combo failed: {}",
4834                error_text
4835            )));
4836        }
4837
4838        let api_response: ApiResponse<crate::model::Combo> = response
4839            .json()
4840            .await
4841            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4842
4843        if let Some(error) = api_response.error {
4844            return Err(HttpError::RequestFailed(format!(
4845                "API error: {} - {}",
4846                error.code, error.message
4847            )));
4848        }
4849
4850        api_response
4851            .result
4852            .ok_or_else(|| HttpError::InvalidResponse("No combo data in response".to_string()))
4853    }
4854
4855    /// Get leg prices for a combo structure
4856    ///
4857    /// Returns individual leg prices for a given combo structure based on
4858    /// an aggregated price of the strategy and the mark prices of the
4859    /// individual legs.
4860    ///
4861    /// # Arguments
4862    ///
4863    /// * `legs` - List of legs for which the prices will be calculated
4864    /// * `price` - Price for the whole leg structure
4865    ///
4866    /// # Errors
4867    ///
4868    /// Returns `HttpError` if the request fails or the response is invalid.
4869    ///
4870    /// # Examples
4871    ///
4872    /// ```rust,no_run
4873    /// use deribit_http::DeribitHttpClient;
4874    /// use deribit_http::model::LegInput;
4875    ///
4876    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
4877    /// let client = DeribitHttpClient::new();
4878    /// let legs = vec![
4879    ///     LegInput::buy("BTC-1NOV24-67000-C", 2.0),
4880    ///     LegInput::sell("BTC-1NOV24-66000-C", 2.0),
4881    /// ];
4882    /// // let prices = client.get_leg_prices(&legs, 0.6).await?;
4883    /// # Ok(())
4884    /// # }
4885    /// ```
4886    pub async fn get_leg_prices(
4887        &self,
4888        legs: &[crate::model::LegInput],
4889        price: f64,
4890    ) -> Result<crate::model::LegPricesResponse, HttpError> {
4891        let legs_json = serde_json::to_string(legs)
4892            .map_err(|e| HttpError::InvalidResponse(format!("Failed to serialize legs: {}", e)))?;
4893
4894        let url = format!(
4895            "{}{}?legs={}&price={}",
4896            self.base_url(),
4897            GET_LEG_PRICES,
4898            urlencoding::encode(&legs_json),
4899            price
4900        );
4901
4902        let response = self.make_authenticated_request(&url).await?;
4903
4904        if !response.status().is_success() {
4905            let error_text = response
4906                .text()
4907                .await
4908                .unwrap_or_else(|_| "Unknown error".to_string());
4909            return Err(HttpError::RequestFailed(format!(
4910                "Get leg prices failed: {}",
4911                error_text
4912            )));
4913        }
4914
4915        let api_response: ApiResponse<crate::model::LegPricesResponse> = response
4916            .json()
4917            .await
4918            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
4919
4920        if let Some(error) = api_response.error {
4921            return Err(HttpError::RequestFailed(format!(
4922                "API error: {} - {}",
4923                error.code, error.message
4924            )));
4925        }
4926
4927        api_response
4928            .result
4929            .ok_or_else(|| HttpError::InvalidResponse("No leg prices in response".to_string()))
4930    }
4931
4932    // ========================================================================
4933    // Block RFQ endpoints
4934    // ========================================================================
4935
4936    /// Creates a new Block RFQ (taker method).
4937    ///
4938    /// # Arguments
4939    ///
4940    /// * `legs` - List of legs for the Block RFQ
4941    /// * `hedge` - Optional hedge leg
4942    /// * `label` - Optional user-defined label (max 64 chars)
4943    /// * `makers` - Optional list of targeted makers
4944    /// * `non_anonymous` - Whether the RFQ is non-anonymous (default true)
4945    /// * `trade_allocations` - Optional pre-allocation for splitting amounts
4946    ///
4947    /// # Errors
4948    ///
4949    /// Returns `HttpError` if the request fails.
4950    pub async fn create_block_rfq(
4951        &self,
4952        legs: &[crate::model::response::BlockRfqLeg],
4953        hedge: Option<&crate::model::response::BlockRfqHedge>,
4954        label: Option<&str>,
4955        makers: Option<&[&str]>,
4956        non_anonymous: Option<bool>,
4957        trade_allocations: Option<&[crate::model::response::BlockRfqTradeAllocation]>,
4958    ) -> Result<crate::model::response::BlockRfq, HttpError> {
4959        let legs_json = serde_json::to_string(legs)
4960            .map_err(|e| HttpError::InvalidResponse(format!("Failed to serialize legs: {}", e)))?;
4961
4962        let mut query_params = vec![format!("legs={}", urlencoding::encode(&legs_json))];
4963
4964        if let Some(h) = hedge {
4965            let hedge_json = serde_json::to_string(h).map_err(|e| {
4966                HttpError::InvalidResponse(format!("Failed to serialize hedge: {}", e))
4967            })?;
4968            query_params.push(format!("hedge={}", urlencoding::encode(&hedge_json)));
4969        }
4970
4971        if let Some(l) = label {
4972            query_params.push(format!("label={}", urlencoding::encode(l)));
4973        }
4974
4975        if let Some(m) = makers {
4976            let makers_json = serde_json::to_string(m).map_err(|e| {
4977                HttpError::InvalidResponse(format!("Failed to serialize makers: {}", e))
4978            })?;
4979            query_params.push(format!("makers={}", urlencoding::encode(&makers_json)));
4980        }
4981
4982        if let Some(na) = non_anonymous {
4983            query_params.push(format!("non_anonymous={}", na));
4984        }
4985
4986        if let Some(ta) = trade_allocations {
4987            let ta_json = serde_json::to_string(ta).map_err(|e| {
4988                HttpError::InvalidResponse(format!("Failed to serialize trade_allocations: {}", e))
4989            })?;
4990            query_params.push(format!(
4991                "trade_allocations={}",
4992                urlencoding::encode(&ta_json)
4993            ));
4994        }
4995
4996        let url = format!(
4997            "{}{}?{}",
4998            self.base_url(),
4999            crate::constants::endpoints::CREATE_BLOCK_RFQ,
5000            query_params.join("&")
5001        );
5002
5003        let response = self.make_authenticated_request(&url).await?;
5004
5005        if !response.status().is_success() {
5006            let error_text = response
5007                .text()
5008                .await
5009                .unwrap_or_else(|_| "Unknown error".to_string());
5010            return Err(HttpError::RequestFailed(format!(
5011                "Create Block RFQ failed: {}",
5012                error_text
5013            )));
5014        }
5015
5016        let api_response: ApiResponse<crate::model::response::BlockRfq> = response
5017            .json()
5018            .await
5019            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
5020
5021        if let Some(error) = api_response.error {
5022            return Err(HttpError::RequestFailed(format!(
5023                "API error: {} - {}",
5024                error.code, error.message
5025            )));
5026        }
5027
5028        api_response
5029            .result
5030            .ok_or_else(|| HttpError::InvalidResponse("No Block RFQ in response".to_string()))
5031    }
5032
5033    /// Cancels a Block RFQ (taker method).
5034    ///
5035    /// # Arguments
5036    ///
5037    /// * `block_rfq_id` - ID of the Block RFQ to cancel
5038    ///
5039    /// # Errors
5040    ///
5041    /// Returns `HttpError` if the request fails.
5042    pub async fn cancel_block_rfq(
5043        &self,
5044        block_rfq_id: i64,
5045    ) -> Result<crate::model::response::BlockRfq, HttpError> {
5046        let query = format!("?block_rfq_id={}", block_rfq_id);
5047        self.private_get(crate::constants::endpoints::CANCEL_BLOCK_RFQ, &query)
5048            .await
5049    }
5050
5051    /// Accepts a Block RFQ quote (taker method).
5052    ///
5053    /// # Arguments
5054    ///
5055    /// * `block_rfq_id` - ID of the Block RFQ
5056    /// * `legs` - Legs to trade
5057    /// * `price` - Maximum acceptable price
5058    /// * `direction` - Direction from taker perspective
5059    /// * `amount` - Order amount
5060    /// * `time_in_force` - Time in force (fill_or_kill or good_til_cancelled)
5061    /// * `hedge` - Optional hedge leg
5062    ///
5063    /// # Errors
5064    ///
5065    /// Returns `HttpError` if the request fails.
5066    #[allow(clippy::too_many_arguments)]
5067    pub async fn accept_block_rfq(
5068        &self,
5069        block_rfq_id: i64,
5070        legs: &[crate::model::response::BlockRfqLeg],
5071        price: f64,
5072        direction: crate::model::types::Direction,
5073        amount: f64,
5074        time_in_force: Option<crate::model::response::BlockRfqTimeInForce>,
5075        hedge: Option<&crate::model::response::BlockRfqHedge>,
5076    ) -> Result<crate::model::response::AcceptBlockRfqResponse, HttpError> {
5077        let legs_json = serde_json::to_string(legs)
5078            .map_err(|e| HttpError::InvalidResponse(format!("Failed to serialize legs: {}", e)))?;
5079
5080        let direction_str = match direction {
5081            crate::model::types::Direction::Buy => "buy",
5082            crate::model::types::Direction::Sell => "sell",
5083            crate::model::types::Direction::Unknown => "buy",
5084        };
5085
5086        let mut query_params = vec![
5087            format!("block_rfq_id={}", block_rfq_id),
5088            format!("legs={}", urlencoding::encode(&legs_json)),
5089            format!("price={}", price),
5090            format!("direction={}", direction_str),
5091            format!("amount={}", amount),
5092        ];
5093
5094        if let Some(tif) = time_in_force {
5095            let tif_str = match tif {
5096                crate::model::response::BlockRfqTimeInForce::FillOrKill => "fill_or_kill",
5097                crate::model::response::BlockRfqTimeInForce::GoodTilCancelled => {
5098                    "good_til_cancelled"
5099                }
5100            };
5101            query_params.push(format!("time_in_force={}", tif_str));
5102        }
5103
5104        if let Some(h) = hedge {
5105            let hedge_json = serde_json::to_string(h).map_err(|e| {
5106                HttpError::InvalidResponse(format!("Failed to serialize hedge: {}", e))
5107            })?;
5108            query_params.push(format!("hedge={}", urlencoding::encode(&hedge_json)));
5109        }
5110
5111        let url = format!(
5112            "{}{}?{}",
5113            self.base_url(),
5114            crate::constants::endpoints::ACCEPT_BLOCK_RFQ,
5115            query_params.join("&")
5116        );
5117
5118        let response = self.make_authenticated_request(&url).await?;
5119
5120        if !response.status().is_success() {
5121            let error_text = response
5122                .text()
5123                .await
5124                .unwrap_or_else(|_| "Unknown error".to_string());
5125            return Err(HttpError::RequestFailed(format!(
5126                "Accept Block RFQ failed: {}",
5127                error_text
5128            )));
5129        }
5130
5131        let api_response: ApiResponse<crate::model::response::AcceptBlockRfqResponse> = response
5132            .json()
5133            .await
5134            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
5135
5136        if let Some(error) = api_response.error {
5137            return Err(HttpError::RequestFailed(format!(
5138                "API error: {} - {}",
5139                error.code, error.message
5140            )));
5141        }
5142
5143        api_response
5144            .result
5145            .ok_or_else(|| HttpError::InvalidResponse("No accept Block RFQ response".to_string()))
5146    }
5147
5148    /// Retrieves Block RFQs.
5149    ///
5150    /// # Arguments
5151    ///
5152    /// * `count` - Optional number of RFQs to return (max 1000)
5153    /// * `state` - Optional state filter
5154    /// * `role` - Optional role filter
5155    /// * `continuation` - Optional continuation token for pagination
5156    /// * `block_rfq_id` - Optional specific Block RFQ ID
5157    /// * `currency` - Optional currency filter
5158    ///
5159    /// # Errors
5160    ///
5161    /// Returns `HttpError` if the request fails.
5162    pub async fn get_block_rfqs(
5163        &self,
5164        count: Option<u32>,
5165        state: Option<crate::model::response::BlockRfqState>,
5166        role: Option<crate::model::response::BlockRfqRole>,
5167        continuation: Option<&str>,
5168        block_rfq_id: Option<i64>,
5169        currency: Option<&str>,
5170    ) -> Result<crate::model::response::BlockRfqsResponse, HttpError> {
5171        let mut query_params: Vec<String> = Vec::new();
5172
5173        if let Some(c) = count {
5174            query_params.push(format!("count={}", c));
5175        }
5176
5177        if let Some(s) = state {
5178            let state_str = match s {
5179                crate::model::response::BlockRfqState::Open => "open",
5180                crate::model::response::BlockRfqState::Filled => "filled",
5181                crate::model::response::BlockRfqState::Traded => "traded",
5182                crate::model::response::BlockRfqState::Cancelled => "cancelled",
5183                crate::model::response::BlockRfqState::Expired => "expired",
5184                crate::model::response::BlockRfqState::Closed => "closed",
5185                crate::model::response::BlockRfqState::Created => "created",
5186            };
5187            query_params.push(format!("state={}", state_str));
5188        }
5189
5190        if let Some(r) = role {
5191            let role_str = match r {
5192                crate::model::response::BlockRfqRole::Taker => "taker",
5193                crate::model::response::BlockRfqRole::Maker => "maker",
5194                crate::model::response::BlockRfqRole::Any => "any",
5195            };
5196            query_params.push(format!("role={}", role_str));
5197        }
5198
5199        if let Some(cont) = continuation {
5200            query_params.push(format!("continuation={}", urlencoding::encode(cont)));
5201        }
5202
5203        if let Some(id) = block_rfq_id {
5204            query_params.push(format!("block_rfq_id={}", id));
5205        }
5206
5207        if let Some(curr) = currency {
5208            query_params.push(format!("currency={}", curr));
5209        }
5210
5211        let url = if query_params.is_empty() {
5212            format!(
5213                "{}{}",
5214                self.base_url(),
5215                crate::constants::endpoints::GET_BLOCK_RFQS
5216            )
5217        } else {
5218            format!(
5219                "{}{}?{}",
5220                self.base_url(),
5221                crate::constants::endpoints::GET_BLOCK_RFQS,
5222                query_params.join("&")
5223            )
5224        };
5225
5226        let response = self.make_authenticated_request(&url).await?;
5227
5228        if !response.status().is_success() {
5229            let error_text = response
5230                .text()
5231                .await
5232                .unwrap_or_else(|_| "Unknown error".to_string());
5233            return Err(HttpError::RequestFailed(format!(
5234                "Get Block RFQs failed: {}",
5235                error_text
5236            )));
5237        }
5238
5239        let api_response: ApiResponse<crate::model::response::BlockRfqsResponse> = response
5240            .json()
5241            .await
5242            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
5243
5244        if let Some(error) = api_response.error {
5245            return Err(HttpError::RequestFailed(format!(
5246                "API error: {} - {}",
5247                error.code, error.message
5248            )));
5249        }
5250
5251        api_response
5252            .result
5253            .ok_or_else(|| HttpError::InvalidResponse("No Block RFQs in response".to_string()))
5254    }
5255
5256    /// Retrieves open quotes for Block RFQs (maker method).
5257    ///
5258    /// # Arguments
5259    ///
5260    /// * `block_rfq_id` - Optional Block RFQ ID filter
5261    /// * `label` - Optional label filter
5262    /// * `block_rfq_quote_id` - Optional specific quote ID
5263    ///
5264    /// # Errors
5265    ///
5266    /// Returns `HttpError` if the request fails.
5267    pub async fn get_block_rfq_quotes(
5268        &self,
5269        block_rfq_id: Option<i64>,
5270        label: Option<&str>,
5271        block_rfq_quote_id: Option<i64>,
5272    ) -> Result<Vec<crate::model::response::BlockRfqQuote>, HttpError> {
5273        let mut query_params: Vec<String> = Vec::new();
5274
5275        if let Some(id) = block_rfq_id {
5276            query_params.push(format!("block_rfq_id={}", id));
5277        }
5278
5279        if let Some(l) = label {
5280            query_params.push(format!("label={}", urlencoding::encode(l)));
5281        }
5282
5283        if let Some(qid) = block_rfq_quote_id {
5284            query_params.push(format!("block_rfq_quote_id={}", qid));
5285        }
5286
5287        let url = if query_params.is_empty() {
5288            format!(
5289                "{}{}",
5290                self.base_url(),
5291                crate::constants::endpoints::GET_BLOCK_RFQ_QUOTES
5292            )
5293        } else {
5294            format!(
5295                "{}{}?{}",
5296                self.base_url(),
5297                crate::constants::endpoints::GET_BLOCK_RFQ_QUOTES,
5298                query_params.join("&")
5299            )
5300        };
5301
5302        let response = self.make_authenticated_request(&url).await?;
5303
5304        if !response.status().is_success() {
5305            let error_text = response
5306                .text()
5307                .await
5308                .unwrap_or_else(|_| "Unknown error".to_string());
5309            return Err(HttpError::RequestFailed(format!(
5310                "Get Block RFQ quotes failed: {}",
5311                error_text
5312            )));
5313        }
5314
5315        let api_response: ApiResponse<Vec<crate::model::response::BlockRfqQuote>> = response
5316            .json()
5317            .await
5318            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
5319
5320        if let Some(error) = api_response.error {
5321            return Err(HttpError::RequestFailed(format!(
5322                "API error: {} - {}",
5323                error.code, error.message
5324            )));
5325        }
5326
5327        api_response.result.ok_or_else(|| {
5328            HttpError::InvalidResponse("No Block RFQ quotes in response".to_string())
5329        })
5330    }
5331
5332    /// Adds a quote to a Block RFQ (maker method).
5333    ///
5334    /// # Arguments
5335    ///
5336    /// * `block_rfq_id` - ID of the Block RFQ
5337    /// * `amount` - Quote amount
5338    /// * `direction` - Direction from maker perspective
5339    /// * `legs` - Legs with prices
5340    /// * `label` - Optional user-defined label
5341    /// * `hedge` - Optional hedge leg
5342    /// * `execution_instruction` - Optional execution instruction
5343    /// * `expires_at` - Optional expiration timestamp in milliseconds
5344    ///
5345    /// # Errors
5346    ///
5347    /// Returns `HttpError` if the request fails.
5348    #[allow(clippy::too_many_arguments)]
5349    pub async fn add_block_rfq_quote(
5350        &self,
5351        block_rfq_id: i64,
5352        amount: f64,
5353        direction: crate::model::types::Direction,
5354        legs: &[crate::model::response::BlockRfqLeg],
5355        label: Option<&str>,
5356        hedge: Option<&crate::model::response::BlockRfqHedge>,
5357        execution_instruction: Option<crate::model::response::ExecutionInstruction>,
5358        expires_at: Option<i64>,
5359    ) -> Result<crate::model::response::BlockRfqQuote, HttpError> {
5360        let legs_json = serde_json::to_string(legs)
5361            .map_err(|e| HttpError::InvalidResponse(format!("Failed to serialize legs: {}", e)))?;
5362
5363        let direction_str = match direction {
5364            crate::model::types::Direction::Buy => "buy",
5365            crate::model::types::Direction::Sell => "sell",
5366            crate::model::types::Direction::Unknown => "buy",
5367        };
5368
5369        let mut query_params = vec![
5370            format!("block_rfq_id={}", block_rfq_id),
5371            format!("amount={}", amount),
5372            format!("direction={}", direction_str),
5373            format!("legs={}", urlencoding::encode(&legs_json)),
5374        ];
5375
5376        if let Some(l) = label {
5377            query_params.push(format!("label={}", urlencoding::encode(l)));
5378        }
5379
5380        if let Some(h) = hedge {
5381            let hedge_json = serde_json::to_string(h).map_err(|e| {
5382                HttpError::InvalidResponse(format!("Failed to serialize hedge: {}", e))
5383            })?;
5384            query_params.push(format!("hedge={}", urlencoding::encode(&hedge_json)));
5385        }
5386
5387        if let Some(ei) = execution_instruction {
5388            let ei_str = match ei {
5389                crate::model::response::ExecutionInstruction::AllOrNone => "all_or_none",
5390                crate::model::response::ExecutionInstruction::AnyPartOf => "any_part_of",
5391            };
5392            query_params.push(format!("execution_instruction={}", ei_str));
5393        }
5394
5395        if let Some(exp) = expires_at {
5396            query_params.push(format!("expires_at={}", exp));
5397        }
5398
5399        let url = format!(
5400            "{}{}?{}",
5401            self.base_url(),
5402            crate::constants::endpoints::ADD_BLOCK_RFQ_QUOTE,
5403            query_params.join("&")
5404        );
5405
5406        let response = self.make_authenticated_request(&url).await?;
5407
5408        if !response.status().is_success() {
5409            let error_text = response
5410                .text()
5411                .await
5412                .unwrap_or_else(|_| "Unknown error".to_string());
5413            return Err(HttpError::RequestFailed(format!(
5414                "Add Block RFQ quote failed: {}",
5415                error_text
5416            )));
5417        }
5418
5419        let api_response: ApiResponse<crate::model::response::BlockRfqQuote> = response
5420            .json()
5421            .await
5422            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
5423
5424        if let Some(error) = api_response.error {
5425            return Err(HttpError::RequestFailed(format!(
5426                "API error: {} - {}",
5427                error.code, error.message
5428            )));
5429        }
5430
5431        api_response
5432            .result
5433            .ok_or_else(|| HttpError::InvalidResponse("No Block RFQ quote in response".to_string()))
5434    }
5435
5436    /// Edits a Block RFQ quote (maker method).
5437    ///
5438    /// # Arguments
5439    ///
5440    /// * `block_rfq_quote_id` - Optional quote ID to edit
5441    /// * `block_rfq_id` - Optional Block RFQ ID (used with label)
5442    /// * `label` - Optional label (used with block_rfq_id)
5443    /// * `amount` - Optional new amount
5444    /// * `legs` - Optional new legs
5445    /// * `hedge` - Optional new hedge
5446    /// * `execution_instruction` - Optional new execution instruction
5447    /// * `expires_at` - Optional new expiration timestamp
5448    ///
5449    /// # Errors
5450    ///
5451    /// Returns `HttpError` if the request fails.
5452    #[allow(clippy::too_many_arguments)]
5453    pub async fn edit_block_rfq_quote(
5454        &self,
5455        block_rfq_quote_id: Option<i64>,
5456        block_rfq_id: Option<i64>,
5457        label: Option<&str>,
5458        amount: Option<f64>,
5459        legs: Option<&[crate::model::response::BlockRfqLeg]>,
5460        hedge: Option<&crate::model::response::BlockRfqHedge>,
5461        execution_instruction: Option<crate::model::response::ExecutionInstruction>,
5462        expires_at: Option<i64>,
5463    ) -> Result<crate::model::response::BlockRfqQuote, HttpError> {
5464        let mut query_params: Vec<String> = Vec::new();
5465
5466        if let Some(qid) = block_rfq_quote_id {
5467            query_params.push(format!("block_rfq_quote_id={}", qid));
5468        }
5469
5470        if let Some(id) = block_rfq_id {
5471            query_params.push(format!("block_rfq_id={}", id));
5472        }
5473
5474        if let Some(l) = label {
5475            query_params.push(format!("label={}", urlencoding::encode(l)));
5476        }
5477
5478        if let Some(a) = amount {
5479            query_params.push(format!("amount={}", a));
5480        }
5481
5482        if let Some(l) = legs {
5483            let legs_json = serde_json::to_string(l).map_err(|e| {
5484                HttpError::InvalidResponse(format!("Failed to serialize legs: {}", e))
5485            })?;
5486            query_params.push(format!("legs={}", urlencoding::encode(&legs_json)));
5487        }
5488
5489        if let Some(h) = hedge {
5490            let hedge_json = serde_json::to_string(h).map_err(|e| {
5491                HttpError::InvalidResponse(format!("Failed to serialize hedge: {}", e))
5492            })?;
5493            query_params.push(format!("hedge={}", urlencoding::encode(&hedge_json)));
5494        }
5495
5496        if let Some(ei) = execution_instruction {
5497            let ei_str = match ei {
5498                crate::model::response::ExecutionInstruction::AllOrNone => "all_or_none",
5499                crate::model::response::ExecutionInstruction::AnyPartOf => "any_part_of",
5500            };
5501            query_params.push(format!("execution_instruction={}", ei_str));
5502        }
5503
5504        if let Some(exp) = expires_at {
5505            query_params.push(format!("expires_at={}", exp));
5506        }
5507
5508        let url = format!(
5509            "{}{}?{}",
5510            self.base_url(),
5511            crate::constants::endpoints::EDIT_BLOCK_RFQ_QUOTE,
5512            query_params.join("&")
5513        );
5514
5515        let response = self.make_authenticated_request(&url).await?;
5516
5517        if !response.status().is_success() {
5518            let error_text = response
5519                .text()
5520                .await
5521                .unwrap_or_else(|_| "Unknown error".to_string());
5522            return Err(HttpError::RequestFailed(format!(
5523                "Edit Block RFQ quote failed: {}",
5524                error_text
5525            )));
5526        }
5527
5528        let api_response: ApiResponse<crate::model::response::BlockRfqQuote> = response
5529            .json()
5530            .await
5531            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
5532
5533        if let Some(error) = api_response.error {
5534            return Err(HttpError::RequestFailed(format!(
5535                "API error: {} - {}",
5536                error.code, error.message
5537            )));
5538        }
5539
5540        api_response
5541            .result
5542            .ok_or_else(|| HttpError::InvalidResponse("No Block RFQ quote in response".to_string()))
5543    }
5544
5545    /// Cancels a single Block RFQ quote (maker method).
5546    ///
5547    /// # Arguments
5548    ///
5549    /// * `block_rfq_quote_id` - Optional quote ID to cancel
5550    /// * `block_rfq_id` - Optional Block RFQ ID (used with label)
5551    /// * `label` - Optional label (used with block_rfq_id)
5552    ///
5553    /// # Errors
5554    ///
5555    /// Returns `HttpError` if the request fails.
5556    pub async fn cancel_block_rfq_quote(
5557        &self,
5558        block_rfq_quote_id: Option<i64>,
5559        block_rfq_id: Option<i64>,
5560        label: Option<&str>,
5561    ) -> Result<crate::model::response::BlockRfqQuote, HttpError> {
5562        let mut query_params: Vec<String> = Vec::new();
5563
5564        if let Some(qid) = block_rfq_quote_id {
5565            query_params.push(format!("block_rfq_quote_id={}", qid));
5566        }
5567
5568        if let Some(id) = block_rfq_id {
5569            query_params.push(format!("block_rfq_id={}", id));
5570        }
5571
5572        if let Some(l) = label {
5573            query_params.push(format!("label={}", urlencoding::encode(l)));
5574        }
5575
5576        let url = format!(
5577            "{}{}?{}",
5578            self.base_url(),
5579            crate::constants::endpoints::CANCEL_BLOCK_RFQ_QUOTE,
5580            query_params.join("&")
5581        );
5582
5583        let response = self.make_authenticated_request(&url).await?;
5584
5585        if !response.status().is_success() {
5586            let error_text = response
5587                .text()
5588                .await
5589                .unwrap_or_else(|_| "Unknown error".to_string());
5590            return Err(HttpError::RequestFailed(format!(
5591                "Cancel Block RFQ quote failed: {}",
5592                error_text
5593            )));
5594        }
5595
5596        let api_response: ApiResponse<crate::model::response::BlockRfqQuote> = response
5597            .json()
5598            .await
5599            .map_err(|e| HttpError::InvalidResponse(e.to_string()))?;
5600
5601        if let Some(error) = api_response.error {
5602            return Err(HttpError::RequestFailed(format!(
5603                "API error: {} - {}",
5604                error.code, error.message
5605            )));
5606        }
5607
5608        api_response
5609            .result
5610            .ok_or_else(|| HttpError::InvalidResponse("No Block RFQ quote in response".to_string()))
5611    }
5612
5613    /// Cancels all Block RFQ quotes (maker method).
5614    ///
5615    /// # Errors
5616    ///
5617    /// Returns `HttpError` if the request fails.
5618    pub async fn cancel_all_block_rfq_quotes(
5619        &self,
5620    ) -> Result<Vec<crate::model::response::BlockRfqQuote>, HttpError> {
5621        self.private_get(crate::constants::endpoints::CANCEL_ALL_BLOCK_RFQ_QUOTES, "")
5622            .await
5623    }
5624}