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