Skip to main content

deribit_websocket/message/
request.rs

1//! WebSocket request message handling
2
3use serde::ser::Error as _;
4
5use crate::error::WebSocketError;
6use crate::model::{quote::*, trading::*, ws_types::JsonRpcRequest};
7
8/// Build a [`WebSocketError::Serialization`] carrying `msg` as the underlying
9/// `serde_json::Error`. Used to surface non-finite-float rejections the same
10/// way a real JSON serialization failure would surface.
11#[cold]
12#[inline(never)]
13fn serialization_error(msg: impl std::fmt::Display) -> WebSocketError {
14    WebSocketError::Serialization(serde_json::Error::custom(msg))
15}
16
17/// Reject `NaN` and `+/- Infinity`. `serde_json` silently maps these to
18/// `null` when serializing — which would otherwise corrupt outgoing requests.
19#[inline]
20fn check_finite(field: &'static str, value: f64) -> Result<(), WebSocketError> {
21    if value.is_finite() {
22        Ok(())
23    } else {
24        Err(serialization_error(format_args!(
25            "field `{field}` must be finite, got {value}"
26        )))
27    }
28}
29
30/// Same as [`check_finite`] for optional values. `None` is always accepted.
31#[inline]
32fn check_finite_opt(field: &'static str, value: Option<f64>) -> Result<(), WebSocketError> {
33    match value {
34        Some(v) => check_finite(field, v),
35        None => Ok(()),
36    }
37}
38
39/// Request builder for WebSocket messages
40#[derive(Debug, Clone)]
41pub struct RequestBuilder {
42    id_counter: u64,
43}
44
45impl Default for RequestBuilder {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl RequestBuilder {
52    /// Create a new request builder
53    pub fn new() -> Self {
54        Self { id_counter: 1 }
55    }
56
57    /// Build a JSON-RPC request
58    pub fn build_request(
59        &mut self,
60        method: &str,
61        params: Option<serde_json::Value>,
62    ) -> JsonRpcRequest {
63        let id = self.id_counter;
64        self.id_counter += 1;
65
66        JsonRpcRequest {
67            jsonrpc: "2.0".to_string(),
68            id: serde_json::Value::Number(serde_json::Number::from(id)),
69            method: method.to_string(),
70            params,
71        }
72    }
73
74    /// Build authentication request
75    pub fn build_auth_request(&mut self, client_id: &str, client_secret: &str) -> JsonRpcRequest {
76        let params = serde_json::json!({
77            "grant_type": "client_credentials",
78            "client_id": client_id,
79            "client_secret": client_secret
80        });
81
82        self.build_request("public/auth", Some(params))
83    }
84
85    /// Build subscription request
86    pub fn build_subscribe_request(&mut self, channels: Vec<String>) -> JsonRpcRequest {
87        let params = serde_json::json!({
88            "channels": channels
89        });
90
91        self.build_request("public/subscribe", Some(params))
92    }
93
94    /// Build unsubscription request
95    pub fn build_unsubscribe_request(&mut self, channels: Vec<String>) -> JsonRpcRequest {
96        let params = serde_json::json!({
97            "channels": channels
98        });
99
100        self.build_request("public/unsubscribe", Some(params))
101    }
102
103    /// Build public unsubscribe_all request
104    ///
105    /// Unsubscribes from all public channels. Takes no parameters.
106    ///
107    /// # Returns
108    ///
109    /// A JSON-RPC request for unsubscribing from all public channels
110    pub fn build_public_unsubscribe_all_request(&mut self) -> JsonRpcRequest {
111        self.build_request(
112            crate::constants::methods::PUBLIC_UNSUBSCRIBE_ALL,
113            Some(serde_json::json!({})),
114        )
115    }
116
117    /// Build private unsubscribe_all request
118    ///
119    /// Unsubscribes from all private channels. Takes no parameters.
120    /// Requires authentication.
121    ///
122    /// # Returns
123    ///
124    /// A JSON-RPC request for unsubscribing from all private channels
125    pub fn build_private_unsubscribe_all_request(&mut self) -> JsonRpcRequest {
126        self.build_request(
127            crate::constants::methods::PRIVATE_UNSUBSCRIBE_ALL,
128            Some(serde_json::json!({})),
129        )
130    }
131
132    /// Build test request
133    pub fn build_test_request(&mut self) -> JsonRpcRequest {
134        self.build_request(crate::constants::methods::PUBLIC_TEST, None)
135    }
136
137    /// Build set_heartbeat request
138    ///
139    /// Enables heartbeat with specified interval. The server will send a heartbeat
140    /// message every `interval` seconds, and expects a response within the same interval.
141    ///
142    /// # Arguments
143    ///
144    /// * `interval` - Heartbeat interval in seconds (10-3600)
145    ///
146    /// # Returns
147    ///
148    /// A JSON-RPC request for setting the heartbeat interval
149    pub fn build_set_heartbeat_request(&mut self, interval: u64) -> JsonRpcRequest {
150        let params = serde_json::json!({
151            "interval": interval
152        });
153        self.build_request(
154            crate::constants::methods::PUBLIC_SET_HEARTBEAT,
155            Some(params),
156        )
157    }
158
159    /// Build disable_heartbeat request
160    ///
161    /// Disables heartbeat messages. The server will stop sending heartbeat messages
162    /// and test_request notifications.
163    ///
164    /// # Returns
165    ///
166    /// A JSON-RPC request for disabling heartbeats
167    pub fn build_disable_heartbeat_request(&mut self) -> JsonRpcRequest {
168        self.build_request(
169            crate::constants::methods::PUBLIC_DISABLE_HEARTBEAT,
170            Some(serde_json::json!({})),
171        )
172    }
173
174    /// Build hello request
175    ///
176    /// Sends client identification to the server. This is used for client tracking
177    /// and debugging purposes.
178    ///
179    /// # Arguments
180    ///
181    /// * `client_name` - Name of the client application
182    /// * `client_version` - Version of the client application
183    ///
184    /// # Returns
185    ///
186    /// A JSON-RPC request for client identification
187    pub fn build_hello_request(
188        &mut self,
189        client_name: &str,
190        client_version: &str,
191    ) -> JsonRpcRequest {
192        let params = serde_json::json!({
193            "client_name": client_name,
194            "client_version": client_version
195        });
196        self.build_request(crate::constants::methods::PUBLIC_HELLO, Some(params))
197    }
198
199    /// Build get time request
200    pub fn build_get_time_request(&mut self) -> JsonRpcRequest {
201        self.build_request("public/get_time", None)
202    }
203
204    /// Build enable_cancel_on_disconnect request
205    ///
206    /// Enables automatic cancellation of all open orders when the WebSocket connection
207    /// is lost. This is a safety feature to prevent unintended order execution when
208    /// the client loses connectivity.
209    ///
210    /// # Returns
211    ///
212    /// A JSON-RPC request for enabling cancel-on-disconnect
213    pub fn build_enable_cancel_on_disconnect_request(&mut self) -> JsonRpcRequest {
214        self.build_request(
215            crate::constants::methods::PRIVATE_ENABLE_CANCEL_ON_DISCONNECT,
216            Some(serde_json::json!({})),
217        )
218    }
219
220    /// Build disable_cancel_on_disconnect request
221    ///
222    /// Disables automatic cancellation of orders on disconnect. Orders will remain
223    /// active even if the WebSocket connection is lost.
224    ///
225    /// # Returns
226    ///
227    /// A JSON-RPC request for disabling cancel-on-disconnect
228    pub fn build_disable_cancel_on_disconnect_request(&mut self) -> JsonRpcRequest {
229        self.build_request(
230            crate::constants::methods::PRIVATE_DISABLE_CANCEL_ON_DISCONNECT,
231            Some(serde_json::json!({})),
232        )
233    }
234
235    /// Build get_cancel_on_disconnect request
236    ///
237    /// Retrieves the current cancel-on-disconnect status for the session.
238    ///
239    /// # Returns
240    ///
241    /// A JSON-RPC request for getting the cancel-on-disconnect status
242    pub fn build_get_cancel_on_disconnect_request(&mut self) -> JsonRpcRequest {
243        self.build_request(
244            crate::constants::methods::PRIVATE_GET_CANCEL_ON_DISCONNECT,
245            Some(serde_json::json!({})),
246        )
247    }
248
249    /// Build mass quote request
250    ///
251    /// # Errors
252    ///
253    /// Returns [`WebSocketError::Serialization`] if the request contains values
254    /// that cannot be represented in JSON (for example `NaN` or `Infinity` in
255    /// any `f64` field such as `price` or `amount`).
256    pub fn build_mass_quote_request(
257        &mut self,
258        request: MassQuoteRequest,
259    ) -> Result<JsonRpcRequest, WebSocketError> {
260        for quote in &request.quotes {
261            check_finite("quotes[].amount", quote.amount)?;
262            check_finite("quotes[].price", quote.price)?;
263        }
264        let params = serde_json::to_value(request)?;
265        Ok(self.build_request("private/mass_quote", Some(params)))
266    }
267
268    /// Build cancel quotes request
269    ///
270    /// # Errors
271    ///
272    /// Returns [`WebSocketError::Serialization`] if the request contains values
273    /// that cannot be represented in JSON (for example `NaN` or `Infinity` in
274    /// the `delta_range` tuple).
275    pub fn build_cancel_quotes_request(
276        &mut self,
277        request: CancelQuotesRequest,
278    ) -> Result<JsonRpcRequest, WebSocketError> {
279        if let Some((min, max)) = request.delta_range {
280            check_finite("delta_range.min", min)?;
281            check_finite("delta_range.max", max)?;
282        }
283        let params = serde_json::to_value(request)?;
284        Ok(self.build_request("private/cancel_quotes", Some(params)))
285    }
286
287    /// Build set MMP config request
288    ///
289    /// # Errors
290    ///
291    /// Returns [`WebSocketError::Serialization`] if `config.quantity_limit` or
292    /// `config.delta_limit` is `NaN` or `Infinity`. `MmpGroupConfig::new`
293    /// enforces magnitude invariants but NaN comparisons always return false
294    /// and silently bypass them, which is why the finite check is repeated
295    /// here.
296    pub fn build_set_mmp_config_request(
297        &mut self,
298        config: MmpGroupConfig,
299    ) -> Result<JsonRpcRequest, WebSocketError> {
300        check_finite("quantity_limit", config.quantity_limit)?;
301        check_finite("delta_limit", config.delta_limit)?;
302
303        let mut params = serde_json::json!({
304            "mmp_group": config.mmp_group,
305            "quantity_limit": config.quantity_limit,
306            "delta_limit": config.delta_limit,
307            "interval": config.interval,
308            "frozen_time": config.frozen_time
309        });
310
311        // If interval is 0, this disables the group
312        if config.interval == 0 {
313            params["interval"] = serde_json::Value::Number(serde_json::Number::from(0));
314        }
315
316        Ok(self.build_request("private/set_mmp_config", Some(params)))
317    }
318
319    /// Build get MMP config request
320    pub fn build_get_mmp_config_request(&mut self, mmp_group: Option<String>) -> JsonRpcRequest {
321        let params = if let Some(group) = mmp_group {
322            serde_json::json!({
323                "mmp_group": group
324            })
325        } else {
326            serde_json::json!({})
327        };
328
329        self.build_request("private/get_mmp_config", Some(params))
330    }
331
332    /// Build reset MMP request
333    pub fn build_reset_mmp_request(&mut self, mmp_group: Option<String>) -> JsonRpcRequest {
334        let params = if let Some(group) = mmp_group {
335            serde_json::json!({
336                "mmp_group": group
337            })
338        } else {
339            serde_json::json!({})
340        };
341
342        self.build_request("private/reset_mmp", Some(params))
343    }
344
345    /// Build get open orders request
346    pub fn build_get_open_orders_request(
347        &mut self,
348        currency: Option<String>,
349        kind: Option<String>,
350        type_filter: Option<String>,
351    ) -> JsonRpcRequest {
352        let mut params = serde_json::json!({});
353
354        if let Some(currency) = currency {
355            params["currency"] = serde_json::Value::String(currency);
356        }
357        if let Some(kind) = kind {
358            params["kind"] = serde_json::Value::String(kind);
359        }
360        if let Some(type_filter) = type_filter {
361            params["type"] = serde_json::Value::String(type_filter);
362        }
363
364        self.build_request("private/get_open_orders", Some(params))
365    }
366
367    /// Build buy order request
368    ///
369    /// # Errors
370    ///
371    /// Returns [`WebSocketError::Serialization`] if `request` contains values
372    /// that cannot be represented in JSON (for example `NaN` or `Infinity` in
373    /// `price`, `amount`, `max_show` or `trigger_price`).
374    pub fn build_buy_request(
375        &mut self,
376        request: &OrderRequest,
377    ) -> Result<JsonRpcRequest, WebSocketError> {
378        check_finite("amount", request.amount)?;
379        check_finite_opt("price", request.price)?;
380        check_finite_opt("max_show", request.max_show)?;
381        check_finite_opt("trigger_price", request.trigger_price)?;
382        let params = serde_json::to_value(request)?;
383        Ok(self.build_request("private/buy", Some(params)))
384    }
385
386    /// Build sell order request
387    ///
388    /// # Errors
389    ///
390    /// Returns [`WebSocketError::Serialization`] if `request` contains values
391    /// that cannot be represented in JSON (for example `NaN` or `Infinity` in
392    /// `price`, `amount`, `max_show` or `trigger_price`).
393    pub fn build_sell_request(
394        &mut self,
395        request: &OrderRequest,
396    ) -> Result<JsonRpcRequest, WebSocketError> {
397        check_finite("amount", request.amount)?;
398        check_finite_opt("price", request.price)?;
399        check_finite_opt("max_show", request.max_show)?;
400        check_finite_opt("trigger_price", request.trigger_price)?;
401        let params = serde_json::to_value(request)?;
402        Ok(self.build_request("private/sell", Some(params)))
403    }
404
405    /// Build cancel order request
406    pub fn build_cancel_request(&mut self, order_id: &str) -> JsonRpcRequest {
407        let params = serde_json::json!({
408            "order_id": order_id
409        });
410
411        self.build_request("private/cancel", Some(params))
412    }
413
414    /// Build cancel all orders request
415    pub fn build_cancel_all_request(&mut self) -> JsonRpcRequest {
416        self.build_request("private/cancel_all", Some(serde_json::json!({})))
417    }
418
419    /// Build cancel all orders by currency request
420    pub fn build_cancel_all_by_currency_request(&mut self, currency: &str) -> JsonRpcRequest {
421        let params = serde_json::json!({
422            "currency": currency
423        });
424
425        self.build_request("private/cancel_all_by_currency", Some(params))
426    }
427
428    /// Build cancel all orders by instrument request
429    pub fn build_cancel_all_by_instrument_request(
430        &mut self,
431        instrument_name: &str,
432    ) -> JsonRpcRequest {
433        let params = serde_json::json!({
434            "instrument_name": instrument_name
435        });
436
437        self.build_request("private/cancel_all_by_instrument", Some(params))
438    }
439
440    /// Build edit order request
441    ///
442    /// # Errors
443    ///
444    /// Returns [`WebSocketError::Serialization`] if `request` contains values
445    /// that cannot be represented in JSON (for example `NaN` or `Infinity` in
446    /// `price`, `amount` or `trigger_price`).
447    pub fn build_edit_request(
448        &mut self,
449        request: &EditOrderRequest,
450    ) -> Result<JsonRpcRequest, WebSocketError> {
451        check_finite("amount", request.amount)?;
452        check_finite_opt("price", request.price)?;
453        check_finite_opt("trigger_price", request.trigger_price)?;
454        let params = serde_json::to_value(request)?;
455        Ok(self.build_request("private/edit", Some(params)))
456    }
457
458    // Account methods
459
460    /// Build a get_positions request
461    ///
462    /// # Arguments
463    ///
464    /// * `currency` - Currency filter (BTC, ETH, USDC, etc.) - optional
465    /// * `kind` - Kind filter (future, option, spot, etc.) - optional
466    ///
467    /// # Returns
468    ///
469    /// A JSON-RPC request for getting positions
470    pub fn build_get_positions_request(
471        &mut self,
472        currency: Option<&str>,
473        kind: Option<&str>,
474    ) -> JsonRpcRequest {
475        let mut params = serde_json::Map::new();
476
477        if let Some(currency) = currency {
478            params.insert(
479                "currency".to_string(),
480                serde_json::Value::String(currency.to_string()),
481            );
482        }
483
484        if let Some(kind) = kind {
485            params.insert(
486                "kind".to_string(),
487                serde_json::Value::String(kind.to_string()),
488            );
489        }
490
491        if params.is_empty() {
492            self.build_request(crate::constants::methods::PRIVATE_GET_POSITIONS, None)
493        } else {
494            self.build_request(
495                crate::constants::methods::PRIVATE_GET_POSITIONS,
496                Some(serde_json::Value::Object(params)),
497            )
498        }
499    }
500
501    /// Build a get_account_summary request
502    ///
503    /// # Arguments
504    ///
505    /// * `currency` - Currency to get summary for (BTC, ETH, USDC, etc.)
506    /// * `extended` - Whether to include extended information
507    ///
508    /// # Returns
509    ///
510    /// A JSON-RPC request for getting account summary
511    pub fn build_get_account_summary_request(
512        &mut self,
513        currency: &str,
514        extended: Option<bool>,
515    ) -> JsonRpcRequest {
516        let mut params = serde_json::Map::new();
517        params.insert(
518            "currency".to_string(),
519            serde_json::Value::String(currency.to_string()),
520        );
521
522        if let Some(extended) = extended {
523            params.insert("extended".to_string(), serde_json::Value::Bool(extended));
524        }
525
526        self.build_request(
527            crate::constants::methods::PRIVATE_GET_ACCOUNT_SUMMARY,
528            Some(serde_json::Value::Object(params)),
529        )
530    }
531
532    /// Build a get_order_state request
533    ///
534    /// # Arguments
535    ///
536    /// * `order_id` - The order ID to get state for
537    ///
538    /// # Returns
539    ///
540    /// A JSON-RPC request for getting order state
541    pub fn build_get_order_state_request(&mut self, order_id: &str) -> JsonRpcRequest {
542        let params = serde_json::json!({
543            "order_id": order_id
544        });
545
546        self.build_request(
547            crate::constants::methods::PRIVATE_GET_ORDER_STATE,
548            Some(params),
549        )
550    }
551
552    /// Build a get_order_history_by_currency request
553    ///
554    /// # Arguments
555    ///
556    /// * `currency` - Currency to get order history for
557    /// * `kind` - Kind filter (future, option, spot, etc.) - optional
558    /// * `count` - Number of items to return - optional
559    ///
560    /// # Returns
561    ///
562    /// A JSON-RPC request for getting order history
563    pub fn build_get_order_history_by_currency_request(
564        &mut self,
565        currency: &str,
566        kind: Option<&str>,
567        count: Option<u32>,
568    ) -> JsonRpcRequest {
569        let mut params = serde_json::Map::new();
570        params.insert(
571            "currency".to_string(),
572            serde_json::Value::String(currency.to_string()),
573        );
574
575        if let Some(kind) = kind {
576            params.insert(
577                "kind".to_string(),
578                serde_json::Value::String(kind.to_string()),
579            );
580        }
581
582        if let Some(count) = count {
583            params.insert(
584                "count".to_string(),
585                serde_json::Value::Number(serde_json::Number::from(count)),
586            );
587        }
588
589        self.build_request(
590            crate::constants::methods::PRIVATE_GET_ORDER_HISTORY_BY_CURRENCY,
591            Some(serde_json::Value::Object(params)),
592        )
593    }
594
595    // Position management methods
596
597    /// Build a close_position request
598    ///
599    /// # Arguments
600    ///
601    /// * `instrument_name` - The instrument to close position for
602    /// * `order_type` - Order type: "limit" or "market"
603    /// * `price` - Price for limit orders (required if order_type is "limit")
604    ///
605    /// # Returns
606    ///
607    /// A JSON-RPC request for closing a position
608    ///
609    /// # Errors
610    ///
611    /// Returns [`WebSocketError::Serialization`] if `price` is `NaN` or
612    /// `Infinity`, which cannot be represented in JSON.
613    pub fn build_close_position_request(
614        &mut self,
615        instrument_name: &str,
616        order_type: &str,
617        price: Option<f64>,
618    ) -> Result<JsonRpcRequest, WebSocketError> {
619        let mut params = serde_json::Map::new();
620        params.insert(
621            "instrument_name".to_string(),
622            serde_json::Value::String(instrument_name.to_string()),
623        );
624        params.insert(
625            "type".to_string(),
626            serde_json::Value::String(order_type.to_string()),
627        );
628
629        if let Some(price) = price {
630            check_finite("price", price)?;
631            params.insert("price".to_string(), serde_json::to_value(price)?);
632        }
633
634        Ok(self.build_request(
635            crate::constants::methods::PRIVATE_CLOSE_POSITION,
636            Some(serde_json::Value::Object(params)),
637        ))
638    }
639
640    /// Build a move_positions request
641    ///
642    /// # Arguments
643    ///
644    /// * `currency` - Currency for the positions (BTC, ETH, etc.)
645    /// * `source_uid` - Source subaccount ID
646    /// * `target_uid` - Target subaccount ID
647    /// * `trades` - List of positions to move
648    ///
649    /// # Returns
650    ///
651    /// A JSON-RPC request for moving positions between subaccounts
652    ///
653    /// # Errors
654    ///
655    /// Returns [`WebSocketError::Serialization`] if any `amount` or `price`
656    /// value in `trades` is `NaN` or `Infinity`, which cannot be represented
657    /// in JSON.
658    pub fn build_move_positions_request(
659        &mut self,
660        currency: &str,
661        source_uid: u64,
662        target_uid: u64,
663        trades: &[crate::model::MovePositionTrade],
664    ) -> Result<JsonRpcRequest, WebSocketError> {
665        for trade in trades {
666            check_finite("trades[_].amount", trade.amount)?;
667            check_finite_opt("trades[_].price", trade.price)?;
668        }
669        let trades_json = serde_json::to_value(trades)?;
670
671        let params = serde_json::json!({
672            "currency": currency,
673            "source_uid": source_uid,
674            "target_uid": target_uid,
675            "trades": trades_json
676        });
677
678        Ok(self.build_request(
679            crate::constants::methods::PRIVATE_MOVE_POSITIONS,
680            Some(params),
681        ))
682    }
683}