Skip to main content

nautilus_hyperliquid/http/
query.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use serde::Serialize;
17
18use crate::{
19    common::enums::{HyperliquidBarInterval, HyperliquidInfoRequestType},
20    http::models::{
21        HyperliquidExecBuilderFee, HyperliquidExecCancelByCloidRequest, HyperliquidExecGrouping,
22        HyperliquidExecModifyOrderRequest, HyperliquidExecPlaceOrderRequest,
23    },
24};
25
26/// Exchange action types for Hyperliquid.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub enum ExchangeActionType {
30    /// Place orders
31    Order,
32    /// Cancel orders by order ID
33    Cancel,
34    /// Cancel orders by client order ID
35    CancelByCloid,
36    /// Modify an existing order
37    Modify,
38    /// Update leverage for an asset
39    UpdateLeverage,
40    /// Update isolated margin for an asset
41    UpdateIsolatedMargin,
42}
43
44impl AsRef<str> for ExchangeActionType {
45    fn as_ref(&self) -> &str {
46        match self {
47            Self::Order => "order",
48            Self::Cancel => "cancel",
49            Self::CancelByCloid => "cancelByCloid",
50            Self::Modify => "modify",
51            Self::UpdateLeverage => "updateLeverage",
52            Self::UpdateIsolatedMargin => "updateIsolatedMargin",
53        }
54    }
55}
56
57/// Parameters for placing orders.
58#[derive(Debug, Clone, Serialize)]
59pub struct OrderParams {
60    pub orders: Vec<HyperliquidExecPlaceOrderRequest>,
61    pub grouping: HyperliquidExecGrouping,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub builder: Option<HyperliquidExecBuilderFee>,
64}
65
66/// Parameters for canceling orders.
67#[derive(Debug, Clone, Serialize)]
68pub struct CancelParams {
69    pub cancels: Vec<HyperliquidExecCancelByCloidRequest>,
70    #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
71    pub fast: Option<bool>,
72}
73
74/// Parameters for modifying an order.
75#[derive(Debug, Clone, Serialize)]
76pub struct ModifyParams {
77    #[serde(flatten)]
78    pub request: HyperliquidExecModifyOrderRequest,
79}
80
81/// Parameters for updating leverage.
82#[derive(Debug, Clone, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct UpdateLeverageParams {
85    pub asset: u32,
86    pub is_cross: bool,
87    pub leverage: u32,
88}
89
90/// Parameters for updating isolated margin.
91#[derive(Debug, Clone, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct UpdateIsolatedMarginParams {
94    pub asset: u32,
95    pub is_buy: bool,
96    pub ntli: i64,
97}
98
99/// Parameters for L2 book request.
100#[derive(Debug, Clone, Serialize)]
101pub struct L2BookParams {
102    pub coin: String,
103}
104
105/// Parameters for recent trades request.
106#[derive(Debug, Clone, Serialize)]
107pub struct RecentTradesParams {
108    pub coin: String,
109}
110
111/// Parameters for user fills request.
112#[derive(Debug, Clone, Serialize)]
113pub struct UserFillsParams {
114    pub user: String,
115}
116
117/// Parameters for order status request.
118#[derive(Debug, Clone, Serialize)]
119pub struct OrderStatusParams {
120    pub user: String,
121    pub oid: u64,
122}
123
124/// Parameters for open orders request.
125#[derive(Debug, Clone, Serialize)]
126pub struct OpenOrdersParams {
127    pub user: String,
128}
129
130/// Parameters for clearinghouse state request.
131#[derive(Debug, Clone, Serialize)]
132pub struct ClearinghouseStateParams {
133    pub user: String,
134}
135
136/// Parameters for spot clearinghouse state request.
137#[derive(Debug, Clone, Serialize)]
138pub struct SpotClearinghouseStateParams {
139    pub user: String,
140}
141
142/// Parameters for candle snapshot request.
143#[derive(Debug, Clone, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct CandleSnapshotReq {
146    pub coin: String,
147    pub interval: HyperliquidBarInterval,
148    pub start_time: u64,
149    pub end_time: u64,
150}
151
152/// Wrapper for candle snapshot parameters.
153#[derive(Debug, Clone, Serialize)]
154pub struct CandleSnapshotParams {
155    pub req: CandleSnapshotReq,
156}
157
158/// Parameters for funding history request.
159#[derive(Debug, Clone, Serialize)]
160#[serde(rename_all = "camelCase")]
161pub struct FundingHistoryParams {
162    pub coin: String,
163    pub start_time: u64,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub end_time: Option<u64>,
166}
167
168/// Info request parameters.
169#[derive(Debug, Clone, Serialize)]
170#[serde(untagged)]
171pub enum InfoRequestParams {
172    L2Book(L2BookParams),
173    RecentTrades(RecentTradesParams),
174    UserFills(UserFillsParams),
175    OrderStatus(OrderStatusParams),
176    OpenOrders(OpenOrdersParams),
177    ClearinghouseState(ClearinghouseStateParams),
178    SpotClearinghouseState(SpotClearinghouseStateParams),
179    CandleSnapshot(CandleSnapshotParams),
180    FundingHistory(FundingHistoryParams),
181    None,
182}
183
184/// Represents an info request wrapper for `POST /info`.
185#[derive(Debug, Clone, Serialize)]
186pub struct InfoRequest {
187    #[serde(rename = "type")]
188    pub request_type: HyperliquidInfoRequestType,
189    #[serde(flatten)]
190    pub params: InfoRequestParams,
191}
192
193impl InfoRequest {
194    /// Creates a request to get metadata about available markets.
195    pub fn meta() -> Self {
196        Self {
197            request_type: HyperliquidInfoRequestType::Meta,
198            params: InfoRequestParams::None,
199        }
200    }
201
202    /// Creates a request to get metadata for all perp dexes (standard + HIP-3).
203    pub fn all_perp_metas() -> Self {
204        Self {
205            request_type: HyperliquidInfoRequestType::AllPerpMetas,
206            params: InfoRequestParams::None,
207        }
208    }
209
210    /// Creates a request to get the list of perp dexes.
211    pub fn perp_dexs() -> Self {
212        Self {
213            request_type: HyperliquidInfoRequestType::PerpDexs,
214            params: InfoRequestParams::None,
215        }
216    }
217
218    /// Creates a request to get spot metadata (tokens and pairs).
219    pub fn spot_meta() -> Self {
220        Self {
221            request_type: HyperliquidInfoRequestType::SpotMeta,
222            params: InfoRequestParams::None,
223        }
224    }
225
226    /// Creates a request to get metadata with asset contexts (for price precision).
227    pub fn meta_and_asset_ctxs() -> Self {
228        Self {
229            request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
230            params: InfoRequestParams::None,
231        }
232    }
233
234    /// Creates a request to get spot metadata with asset contexts.
235    pub fn spot_meta_and_asset_ctxs() -> Self {
236        Self {
237            request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
238            params: InfoRequestParams::None,
239        }
240    }
241
242    /// Creates a request to get outcome metadata.
243    pub fn outcome_meta() -> Self {
244        Self {
245            request_type: HyperliquidInfoRequestType::OutcomeMeta,
246            params: InfoRequestParams::None,
247        }
248    }
249
250    /// Creates a request to get L2 order book for a coin.
251    pub fn l2_book(coin: &str) -> Self {
252        Self {
253            request_type: HyperliquidInfoRequestType::L2Book,
254            params: InfoRequestParams::L2Book(L2BookParams {
255                coin: coin.to_string(),
256            }),
257        }
258    }
259
260    /// Creates a request to get recent public trades for a coin.
261    pub fn recent_trades(coin: &str) -> Self {
262        Self {
263            request_type: HyperliquidInfoRequestType::RecentTrades,
264            params: InfoRequestParams::RecentTrades(RecentTradesParams {
265                coin: coin.to_string(),
266            }),
267        }
268    }
269
270    /// Creates a request to get user fills.
271    pub fn user_fills(user: &str) -> Self {
272        Self {
273            request_type: HyperliquidInfoRequestType::UserFills,
274            params: InfoRequestParams::UserFills(UserFillsParams {
275                user: user.to_string(),
276            }),
277        }
278    }
279
280    /// Creates a request to get order status for a user.
281    pub fn order_status(user: &str, oid: u64) -> Self {
282        Self {
283            request_type: HyperliquidInfoRequestType::OrderStatus,
284            params: InfoRequestParams::OrderStatus(OrderStatusParams {
285                user: user.to_string(),
286                oid,
287            }),
288        }
289    }
290
291    /// Creates a request to get all open orders for a user.
292    pub fn open_orders(user: &str) -> Self {
293        Self {
294            request_type: HyperliquidInfoRequestType::OpenOrders,
295            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
296                user: user.to_string(),
297            }),
298        }
299    }
300
301    /// Creates a request to get frontend open orders (includes more detail).
302    pub fn frontend_open_orders(user: &str) -> Self {
303        Self {
304            request_type: HyperliquidInfoRequestType::FrontendOpenOrders,
305            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
306                user: user.to_string(),
307            }),
308        }
309    }
310
311    /// Creates a request to get historical orders for a user.
312    pub fn historical_orders(user: &str) -> Self {
313        Self {
314            request_type: HyperliquidInfoRequestType::HistoricalOrders,
315            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
316                user: user.to_string(),
317            }),
318        }
319    }
320
321    /// Creates a request to get user state (balances, positions, margin).
322    pub fn clearinghouse_state(user: &str) -> Self {
323        Self {
324            request_type: HyperliquidInfoRequestType::ClearinghouseState,
325            params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
326                user: user.to_string(),
327            }),
328        }
329    }
330
331    /// Creates a request to get spot clearinghouse state (per-token spot balances).
332    pub fn spot_clearinghouse_state(user: &str) -> Self {
333        Self {
334            request_type: HyperliquidInfoRequestType::SpotClearinghouseState,
335            params: InfoRequestParams::SpotClearinghouseState(SpotClearinghouseStateParams {
336                user: user.to_string(),
337            }),
338        }
339    }
340
341    /// Creates a request to get user fee schedule and effective rates.
342    pub fn user_fees(user: &str) -> Self {
343        Self {
344            request_type: HyperliquidInfoRequestType::UserFees,
345            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
346                user: user.to_string(),
347            }),
348        }
349    }
350
351    /// Creates a request to get candle/bar data.
352    pub fn candle_snapshot(
353        coin: &str,
354        interval: HyperliquidBarInterval,
355        start_time: u64,
356        end_time: u64,
357    ) -> Self {
358        Self {
359            request_type: HyperliquidInfoRequestType::CandleSnapshot,
360            params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
361                req: CandleSnapshotReq {
362                    coin: coin.to_string(),
363                    interval,
364                    start_time,
365                    end_time,
366                },
367            }),
368        }
369    }
370
371    /// Creates a request to get funding rate history for a coin.
372    pub fn funding_history(coin: &str, start_time: u64, end_time: Option<u64>) -> Self {
373        Self {
374            request_type: HyperliquidInfoRequestType::FundingHistory,
375            params: InfoRequestParams::FundingHistory(FundingHistoryParams {
376                coin: coin.to_string(),
377                start_time,
378                end_time,
379            }),
380        }
381    }
382}
383
384/// Exchange action parameters.
385#[derive(Debug, Clone, Serialize)]
386#[serde(untagged)]
387pub enum ExchangeActionParams {
388    Order(OrderParams),
389    Cancel(CancelParams),
390    Modify(ModifyParams),
391    UpdateLeverage(UpdateLeverageParams),
392    UpdateIsolatedMargin(UpdateIsolatedMarginParams),
393}
394
395/// Represents an exchange action wrapper for `POST /exchange`.
396#[derive(Debug, Clone, Serialize)]
397pub struct ExchangeAction {
398    #[serde(rename = "type", serialize_with = "serialize_action_type")]
399    pub action_type: ExchangeActionType,
400    #[serde(flatten)]
401    pub params: ExchangeActionParams,
402}
403
404fn serialize_action_type<S>(
405    action_type: &ExchangeActionType,
406    serializer: S,
407) -> Result<S::Ok, S::Error>
408where
409    S: serde::Serializer,
410{
411    serializer.serialize_str(action_type.as_ref())
412}
413
414impl ExchangeAction {
415    /// Creates an action to place orders with builder attribution.
416    pub fn order(
417        orders: Vec<HyperliquidExecPlaceOrderRequest>,
418        builder: Option<HyperliquidExecBuilderFee>,
419    ) -> Self {
420        Self {
421            action_type: ExchangeActionType::Order,
422            params: ExchangeActionParams::Order(OrderParams {
423                orders,
424                grouping: HyperliquidExecGrouping::Na,
425                builder,
426            }),
427        }
428    }
429
430    /// Creates an action to cancel orders.
431    pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
432        Self {
433            action_type: ExchangeActionType::Cancel,
434            params: ExchangeActionParams::Cancel(CancelParams {
435                cancels,
436                fast: None,
437            }),
438        }
439    }
440
441    /// Creates an action to cancel orders by client order ID.
442    pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
443        Self {
444            action_type: ExchangeActionType::CancelByCloid,
445            params: ExchangeActionParams::Cancel(CancelParams {
446                cancels,
447                fast: None,
448            }),
449        }
450    }
451
452    /// Creates an action to modify an order.
453    pub fn modify(request: HyperliquidExecModifyOrderRequest) -> Self {
454        Self {
455            action_type: ExchangeActionType::Modify,
456            params: ExchangeActionParams::Modify(ModifyParams { request }),
457        }
458    }
459
460    /// Creates an action to update leverage for an asset.
461    pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
462        Self {
463            action_type: ExchangeActionType::UpdateLeverage,
464            params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
465                asset,
466                is_cross,
467                leverage,
468            }),
469        }
470    }
471
472    /// Creates an action to update isolated margin for an asset.
473    pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
474        Self {
475            action_type: ExchangeActionType::UpdateIsolatedMargin,
476            params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
477                asset,
478                is_buy,
479                ntli,
480            }),
481        }
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use rstest::rstest;
488    use rust_decimal::Decimal;
489
490    use super::*;
491    use crate::http::models::{
492        Cloid, HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams,
493        HyperliquidExecModifyOrderRequest, HyperliquidExecOrderKind,
494        HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
495    };
496
497    #[rstest]
498    fn test_info_request_meta() {
499        let req = InfoRequest::meta();
500
501        assert_eq!(req.request_type, HyperliquidInfoRequestType::Meta);
502        assert!(matches!(req.params, InfoRequestParams::None));
503    }
504
505    #[rstest]
506    fn test_info_request_all_perp_metas() {
507        let req = InfoRequest::all_perp_metas();
508
509        assert_eq!(req.request_type, HyperliquidInfoRequestType::AllPerpMetas);
510        let json = serde_json::to_string(&req).unwrap();
511        assert!(json.contains(r#""type":"allPerpMetas""#));
512    }
513
514    #[rstest]
515    fn test_info_request_outcome_meta() {
516        let req = InfoRequest::outcome_meta();
517
518        assert_eq!(req.request_type, HyperliquidInfoRequestType::OutcomeMeta);
519        assert!(matches!(req.params, InfoRequestParams::None));
520        let json = serde_json::to_string(&req).unwrap();
521        assert_eq!(json, r#"{"type":"outcomeMeta"}"#);
522    }
523
524    #[rstest]
525    fn test_info_request_l2_book() {
526        let req = InfoRequest::l2_book("BTC");
527
528        assert_eq!(req.request_type, HyperliquidInfoRequestType::L2Book);
529        let json = serde_json::to_string(&req).unwrap();
530        assert!(json.contains("\"coin\":\"BTC\""));
531    }
532
533    #[rstest]
534    fn test_info_request_recent_trades() {
535        let req = InfoRequest::recent_trades("BTC");
536
537        assert_eq!(req.request_type, HyperliquidInfoRequestType::RecentTrades);
538        let json = serde_json::to_string(&req).unwrap();
539        assert_eq!(json, r#"{"type":"recentTrades","coin":"BTC"}"#);
540    }
541
542    #[rstest]
543    fn test_info_request_spot_clearinghouse_state() {
544        let req = InfoRequest::spot_clearinghouse_state("0xabc");
545
546        assert_eq!(
547            req.request_type,
548            HyperliquidInfoRequestType::SpotClearinghouseState
549        );
550        let json = serde_json::to_string(&req).unwrap();
551        assert!(json.contains(r#""type":"spotClearinghouseState""#));
552        assert!(json.contains(r#""user":"0xabc""#));
553    }
554
555    #[rstest]
556    fn test_info_request_funding_history_with_end_time() {
557        let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, Some(1_700_003_600_000));
558
559        assert_eq!(req.request_type, HyperliquidInfoRequestType::FundingHistory);
560        let json = serde_json::to_string(&req).unwrap();
561        assert!(json.contains(r#""type":"fundingHistory""#));
562        assert!(json.contains(r#""coin":"BTC""#));
563        assert!(json.contains(r#""startTime":1700000000000"#));
564        assert!(json.contains(r#""endTime":1700003600000"#));
565    }
566
567    #[rstest]
568    fn test_info_request_funding_history_omits_end_time_when_none() {
569        // Hyperliquid defaults `endTime` to current time when absent; the
570        // serializer must omit the field rather than emit `null`.
571        let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, None);
572        let json = serde_json::to_string(&req).unwrap();
573        assert!(json.contains(r#""startTime":1700000000000"#));
574        assert!(
575            !json.contains("endTime"),
576            "endTime must be omitted when None; json={json}",
577        );
578    }
579
580    #[rstest]
581    fn test_exchange_action_order() {
582        let order = HyperliquidExecPlaceOrderRequest {
583            asset: 0,
584            is_buy: true,
585            price: Decimal::new(50000, 0),
586            size: Decimal::new(1, 0),
587            reduce_only: false,
588            kind: HyperliquidExecOrderKind::Limit {
589                limit: HyperliquidExecLimitParams {
590                    tif: HyperliquidExecTif::Gtc,
591                },
592            },
593            cloid: None,
594        };
595
596        let action = ExchangeAction::order(vec![order], None);
597
598        assert_eq!(action.action_type, ExchangeActionType::Order);
599        let json = serde_json::to_string(&action).unwrap();
600        assert!(json.contains("\"orders\""));
601    }
602
603    #[rstest]
604    fn test_exchange_action_cancel() {
605        let cancel = HyperliquidExecCancelByCloidRequest {
606            asset: 0,
607            cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
608        };
609
610        let action = ExchangeAction::cancel(vec![cancel]);
611
612        assert_eq!(action.action_type, ExchangeActionType::Cancel);
613    }
614
615    #[rstest]
616    fn test_exchange_action_serialization() {
617        let order = HyperliquidExecPlaceOrderRequest {
618            asset: 0,
619            is_buy: true,
620            price: Decimal::new(50000, 0),
621            size: Decimal::new(1, 0),
622            reduce_only: false,
623            kind: HyperliquidExecOrderKind::Limit {
624                limit: HyperliquidExecLimitParams {
625                    tif: HyperliquidExecTif::Gtc,
626                },
627            },
628            cloid: None,
629        };
630
631        let action = ExchangeAction::order(vec![order], None);
632
633        let json = serde_json::to_string(&action).unwrap();
634        // Verify that action_type is serialized as "type" with the correct string value
635        assert!(json.contains(r#""type":"order""#));
636        assert!(json.contains(r#""orders""#));
637        assert!(json.contains(r#""grouping":"na""#));
638    }
639
640    #[rstest]
641    fn test_exchange_action_type_as_ref() {
642        assert_eq!(ExchangeActionType::Order.as_ref(), "order");
643        assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
644        assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
645        assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
646        assert_eq!(
647            ExchangeActionType::UpdateLeverage.as_ref(),
648            "updateLeverage"
649        );
650        assert_eq!(
651            ExchangeActionType::UpdateIsolatedMargin.as_ref(),
652            "updateIsolatedMargin"
653        );
654    }
655
656    #[rstest]
657    fn test_update_leverage_serialization() {
658        let action = ExchangeAction::update_leverage(1, true, 10);
659        let json = serde_json::to_string(&action).unwrap();
660
661        assert!(json.contains(r#""type":"updateLeverage""#));
662        assert!(json.contains(r#""asset":1"#));
663        assert!(json.contains(r#""isCross":true"#));
664        assert!(json.contains(r#""leverage":10"#));
665    }
666
667    #[rstest]
668    fn test_update_isolated_margin_serialization() {
669        let action = ExchangeAction::update_isolated_margin(2, false, 1000);
670        let json = serde_json::to_string(&action).unwrap();
671
672        assert!(json.contains(r#""type":"updateIsolatedMargin""#));
673        assert!(json.contains(r#""asset":2"#));
674        assert!(json.contains(r#""isBuy":false"#));
675        assert!(json.contains(r#""ntli":1000"#));
676    }
677
678    #[rstest]
679    fn test_cancel_by_cloid_serialization() {
680        let cancel_request = HyperliquidExecCancelByCloidRequest {
681            asset: 0,
682            cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
683        };
684        let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
685        let json = serde_json::to_string(&action).unwrap();
686
687        assert!(json.contains(r#""type":"cancelByCloid""#));
688        assert!(json.contains(r#""cancels""#));
689    }
690
691    #[rstest]
692    fn test_modify_serialization() {
693        let modify_request = HyperliquidExecModifyOrderRequest {
694            oid: 12345.into(),
695            order: HyperliquidExecPlaceOrderRequest {
696                asset: 0,
697                is_buy: true,
698                price: Decimal::new(51000, 0),
699                size: Decimal::new(2, 0),
700                reduce_only: false,
701                kind: HyperliquidExecOrderKind::Limit {
702                    limit: HyperliquidExecLimitParams {
703                        tif: HyperliquidExecTif::Gtc,
704                    },
705                },
706                cloid: None,
707            },
708        };
709        let action = ExchangeAction::modify(modify_request);
710        let json = serde_json::to_string(&action).unwrap();
711
712        assert!(json.contains(r#""type":"modify""#));
713        assert!(json.contains(r#""oid":12345"#));
714        assert!(json.contains(r#""order""#));
715    }
716}