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