Skip to main content

o2_tools/
order_book.rs

1use fuels::{
2    prelude::*,
3    types::{
4        AssetId,
5        Bits256,
6        Identity,
7    },
8};
9
10use crate::order_book_deploy::{
11    OrderBookDeploy,
12    OrderBookDeployConfig,
13    OrderBookProxy,
14};
15
16// Re-export types from the contract ABI
17use crate::{
18    order_book_deploy,
19    order_book_deploy::{
20        OrderArgs,
21        OrderBook,
22        Side as ContractSide,
23        Time,
24    },
25    trade_account_deploy::CallParams,
26};
27
28// Re-export domain types from api-types
29pub use o2_api_types::primitives::{
30    OrderType,
31    Side,
32};
33
34// Re-export trigger order types from contract ABI so callers don't need to import order_book_deploy
35pub use crate::order_book_deploy::{
36    EjectionReason,
37    OrderHaltedEvent,
38    TriggerOrder,
39    TriggerOrderArgs,
40    TriggerOrderCancelError,
41    TriggerOrderCreatedEvent,
42    TriggerOrderCreationError,
43    TriggerOrderEjectedEvent,
44    TriggerOrderType,
45    TriggerQuantity,
46};
47
48pub const DEFAULT_METHOD_GAS: u64 = 1_000_000;
49
50/// Convert a contract OrderType into the shared OrderType.
51pub fn order_type_from_contract(order_type: order_book_deploy::OrderType) -> OrderType {
52    match order_type {
53        order_book_deploy::OrderType::Spot => OrderType::Spot,
54        order_book_deploy::OrderType::Limit((price, timestamp)) => {
55            OrderType::Limit(price, timestamp.unix as u128)
56        }
57        order_book_deploy::OrderType::FillOrKill => OrderType::FillOrKill,
58        order_book_deploy::OrderType::PostOnly => OrderType::PostOnly,
59        order_book_deploy::OrderType::Market => OrderType::Market,
60        order_book_deploy::OrderType::BoundedMarket((max_price, min_price)) => {
61            OrderType::BoundedMarket {
62                max_price,
63                min_price,
64            }
65        }
66    }
67}
68
69/// Convert a shared OrderType into the contract OrderType.
70pub fn order_type_to_contract(order_type: OrderType) -> order_book_deploy::OrderType {
71    match order_type {
72        OrderType::Spot => order_book_deploy::OrderType::Spot,
73        OrderType::Limit(price, timestamp) => order_book_deploy::OrderType::Limit((
74            price,
75            Time {
76                unix: timestamp as u64,
77            },
78        )),
79        OrderType::FillOrKill => order_book_deploy::OrderType::FillOrKill,
80        OrderType::PostOnly => order_book_deploy::OrderType::PostOnly,
81        OrderType::Market => order_book_deploy::OrderType::Market,
82        OrderType::BoundedMarket {
83            max_price,
84            min_price,
85        } => order_book_deploy::OrderType::BoundedMarket((max_price, min_price)),
86    }
87}
88
89/// Convert a contract Side into the shared Side.
90pub fn side_from_contract(order_side: ContractSide) -> Side {
91    match order_side {
92        ContractSide::Buy => Side::Buy,
93        ContractSide::Sell => Side::Sell,
94    }
95}
96
97/// Convert a shared Side into the contract Side.
98pub fn side_to_contract(side: Side) -> ContractSide {
99    match side {
100        Side::Buy => ContractSide::Buy,
101        Side::Sell => ContractSide::Sell,
102    }
103}
104
105/// Parameters for creating a new order in the order book.
106/// Contains all information needed to place a buy or sell order.
107#[derive(Debug, Clone)]
108pub struct CreateOrderParams {
109    /// The price per unit (in quote asset)
110    pub price: u64,
111    /// The quantity to buy/sell (in base asset)
112    pub quantity: u64,
113    /// The type of order (Limit or Spot)
114    pub order_type: OrderType,
115    /// The side of the order (Buy or Sell)
116    pub side: Side,
117    /// The asset ID of the asset being traded
118    pub asset_id: AssetId,
119}
120
121impl CreateOrderParams {
122    pub fn random(
123        side: Side,
124        asset_id: AssetId,
125        min_price: u64,
126        max_price: u64,
127        min_quantity: u64,
128        max_quantity: u64,
129    ) -> Self {
130        let price = (rand::random::<u64>() + min_price) % max_price;
131        let quantity = (rand::random::<u64>() + min_quantity) % max_quantity;
132        let order_type = OrderType::Spot;
133        Self {
134            price,
135            quantity,
136            order_type,
137            side,
138            asset_id,
139        }
140    }
141
142    /// Converts to the contract's OrderArgs format.
143    pub fn to_order_args(&self) -> OrderArgs {
144        OrderArgs {
145            price: self.price,
146            quantity: self.quantity,
147            order_type: order_type_to_contract(self.order_type),
148        }
149    }
150}
151
152/// Parameters for creating a trigger (stop-loss / take-profit) order.
153#[derive(Debug, Clone)]
154pub struct CreateTriggerOrderParams {
155    /// The price that, when crossed, activates the order.
156    pub trigger_price: u64,
157    /// What kind of order to place once the trigger fires.
158    pub order_type: TriggerOrderType,
159    /// Explicit quantity lock, or derive from a linked parent spot order.
160    pub quantity: TriggerQuantity,
161    /// Buy or sell.
162    pub side: Side,
163}
164
165impl CreateTriggerOrderParams {
166    /// Converts to the contract's `TriggerOrderArgs` format.
167    pub fn to_trigger_order_args(&self) -> TriggerOrderArgs {
168        TriggerOrderArgs {
169            quantity: self.quantity.clone(),
170            order_type: self.order_type.clone(),
171            trigger_price: self.trigger_price,
172        }
173    }
174
175    /// Returns the `(asset_id, amount)` that must be forwarded when calling
176    /// `create_trigger_order` / `create_trigger_orders`.
177    ///
178    /// For `TriggerQuantity::ParentOrder` the required lock is zero — the
179    /// parent spot order's escrow covers it.  For `TriggerQuantity::Quantity`
180    /// the lock mirrors a regular order at the effective limit price.
181    pub fn lock_amount(&self, config: &OrderbookConfig) -> (AssetId, u64) {
182        let asset = config.get_order_side_asset(&self.side);
183        let amount = match &self.quantity {
184            TriggerQuantity::ParentOrder(_) => 0,
185            TriggerQuantity::Quantity(qty) => {
186                let limit_price = match &self.order_type {
187                    TriggerOrderType::Market => self.trigger_price,
188                    TriggerOrderType::MarketBounded((max, _)) => *max,
189                    TriggerOrderType::Spot(price) => *price,
190                };
191                config.get_order_side_amount(*qty, limit_price, &self.side)
192            }
193        };
194        (asset, amount)
195    }
196}
197
198#[derive(Clone, Copy)]
199pub struct OrderbookConfig {
200    /// Base asset ID for the trading pair
201    pub base_asset: AssetId,
202    pub base_decimals: u64,
203    /// Quote asset ID for the trading pair
204    pub quote_asset: AssetId,
205    pub quote_decimals: u64,
206}
207
208impl OrderbookConfig {
209    pub fn get_order_side_asset(&self, order_side: &Side) -> AssetId {
210        if order_side == &Side::Buy {
211            self.quote_asset
212        } else {
213            self.base_asset
214        }
215    }
216
217    pub fn get_order_side_amount(&self, quantity: u64, price: u64, side: &Side) -> u64 {
218        if side == &Side::Buy {
219            ((quantity as u128 * price as u128) / self.base_decimals as u128)
220                .try_into()
221                .unwrap()
222        } else {
223            quantity
224        }
225    }
226
227    pub fn create_call_params(
228        &self,
229        params: &CreateOrderParams,
230        gas: Option<u64>,
231    ) -> CallParams {
232        let amount =
233            self.get_order_side_amount(params.quantity, params.price, &params.side);
234        let asset_id = self.get_order_side_asset(&params.side);
235        CallParams::new(amount, asset_id, gas.unwrap_or(u64::MAX))
236    }
237}
238
239#[derive(Clone)]
240pub struct OrderBookManager<W: Account + Clone> {
241    /// The OrderBook contract instance
242    pub proxy: OrderBookProxy<W>,
243    /// The OrderBook contract instance
244    pub contract: OrderBook<W>,
245    /// The wallet used for creating transactions
246    pub gas_payer_wallet: W,
247    /// Confguration
248    pub config: OrderbookConfig,
249}
250
251impl<W: Account + Clone> OrderBookManager<W> {
252    /// Creates a new OrderBookManager instance.
253    ///
254    /// # Arguments
255    /// * `contract_id` - The deployed OrderBook contract ID
256    /// * `wallet` - The wallet to use for transactions
257    /// * `base_asset` - The base asset ID for the trading pair
258    /// * `quote_asset` - The quote asset ID for the trading pair
259    pub fn new(
260        gas_payer_wallet: &W,
261        base_decimals: u64,
262        quote_decimals: u64,
263        order_book_deploy: &OrderBookDeploy<W>,
264    ) -> Self {
265        let config = OrderbookConfig {
266            base_asset: order_book_deploy.base_asset,
267            base_decimals,
268            quote_asset: order_book_deploy.quote_asset,
269            quote_decimals,
270        };
271        Self {
272            proxy: order_book_deploy
273                .order_book_proxy
274                .clone()
275                .with_account(gas_payer_wallet.clone()),
276            contract: order_book_deploy
277                .order_book
278                .clone()
279                .with_account(gas_payer_wallet.clone()),
280            config,
281            gas_payer_wallet: gas_payer_wallet.clone(),
282        }
283    }
284
285    pub fn create_call_params(
286        &self,
287        params: &CreateOrderParams,
288        gas: Option<u64>,
289    ) -> CallParams {
290        self.config().create_call_params(params, gas)
291    }
292
293    pub fn get_order_side_asset(&self, order_side: &Side) -> AssetId {
294        self.config().get_order_side_asset(order_side)
295    }
296
297    pub fn get_order_side_amount(&self, quantity: u64, price: u64, side: &Side) -> u64 {
298        self.config().get_order_side_amount(quantity, price, side)
299    }
300
301    pub async fn balances_of(&self, identity: &Identity) -> anyhow::Result<(u128, u128)> {
302        let result = self
303            .contract
304            .methods()
305            .get_settled_balance_of(*identity)
306            .simulate(Execution::state_read_only())
307            .await?;
308        Ok((result.value.0 as u128, result.value.1 as u128))
309    }
310
311    /// Returns the total balance of a trader: every asset the trader owns in the
312    /// contract, including the settled balance, locked balances, and balances escrowed
313    /// in open orders. The tuple is ordered `(BASE_ASSET, QUOTE_ASSET)`.
314    pub async fn total_balances_of(
315        &self,
316        identity: &Identity,
317    ) -> anyhow::Result<(u128, u128)> {
318        let result = self
319            .contract
320            .methods()
321            .get_total_balance_of(*identity)
322            .simulate(Execution::state_read_only())
323            .await?;
324        Ok((result.value.0 as u128, result.value.1 as u128))
325    }
326
327    pub async fn emit_config(&self) -> anyhow::Result<()> {
328        self.contract
329            .methods()
330            .emit_orderbook_config()
331            .call()
332            .await?;
333        Ok(())
334    }
335
336    pub async fn upgrade(
337        &self,
338        deploy_config: &OrderBookDeployConfig,
339    ) -> anyhow::Result<()> {
340        let base_asset_id = self.contract.methods().get_base_asset().call().await?.value;
341        let quote_asset_id = self
342            .contract
343            .methods()
344            .get_quote_asset()
345            .simulate(Execution::state_read_only())
346            .await?
347            .value;
348        let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
349            &self.gas_payer_wallet,
350            base_asset_id,
351            quote_asset_id,
352            deploy_config,
353        )
354        .await?;
355        self.proxy
356            .methods()
357            .set_proxy_target(ContractId::new(order_book_blob_id))
358            .call()
359            .await?;
360        Ok(())
361    }
362
363    pub async fn accumulated_fees(&self) -> anyhow::Result<(u64, u64)> {
364        let fees = self
365            .contract
366            .methods()
367            .current_fees()
368            .simulate(Execution::state_read_only())
369            .await?
370            .value;
371        Ok(fees)
372    }
373
374    pub async fn get_whitelist_id(&self) -> anyhow::Result<Option<ContractId>> {
375        let result = self
376            .contract
377            .methods()
378            .get_whitelist_id()
379            .simulate(Execution::state_read_only())
380            .await?;
381        Ok(result.value)
382    }
383
384    pub async fn get_blacklist_id(&self) -> anyhow::Result<Option<ContractId>> {
385        let result = self
386            .contract
387            .methods()
388            .get_blacklist_id()
389            .simulate(Execution::state_read_only())
390            .await?;
391        Ok(result.value)
392    }
393
394    pub fn base_asset(&self) -> AssetId {
395        self.config().base_asset
396    }
397
398    pub fn base_decimals(&self) -> u64 {
399        self.config().base_decimals
400    }
401
402    pub fn quote_asset(&self) -> AssetId {
403        self.config().quote_asset
404    }
405
406    pub fn quote_decimals(&self) -> u64 {
407        self.config().quote_decimals
408    }
409
410    pub fn config(&self) -> OrderbookConfig {
411        self.config
412    }
413
414    /// Creates a single trigger order. When `params.quantity` is
415    /// `TriggerQuantity::ParentOrder(spot_id)`, the trigger is attached to that resting spot
416    /// order. If the parent already has one child trigger, this trigger is registered as its
417    /// OCO sibling (one stop-loss, one take-profit). A parent may have at most two triggers.
418    ///
419    /// If `expected_parent_quantity` is `Some`, the call reverts unless the parent spot order's
420    /// remaining quantity exactly matches the provided value.
421    pub async fn create_trigger_order(
422        &self,
423        params: CreateTriggerOrderParams,
424        expected_parent_quantity: Option<u64>,
425    ) -> anyhow::Result<Bits256> {
426        let (asset_id, amount) = params.lock_amount(&self.config);
427        let result = self
428            .contract
429            .methods()
430            .create_trigger_order(
431                params.to_trigger_order_args(),
432                expected_parent_quantity,
433            )
434            .call_params(CallParameters::new(amount, asset_id, u64::MAX))?
435            .call()
436            .await?;
437        Ok(result.value)
438    }
439
440    /// Creates two OCO-linked trigger orders in a single call. Both params must use the same
441    /// `TriggerQuantity` variant; if both are `ParentOrder`, they must reference the same
442    /// spot order. `params_1` is canonical and its lock covers both orders.
443    ///
444    /// If `expected_parent_quantity` is `Some`, the call reverts unless the parent spot order's
445    /// remaining quantity exactly matches the provided value.
446    pub async fn create_trigger_orders(
447        &self,
448        params_1: CreateTriggerOrderParams,
449        params_2: CreateTriggerOrderParams,
450        expected_parent_quantity: Option<u64>,
451    ) -> anyhow::Result<(Bits256, Bits256)> {
452        let (asset_id, amount) = params_1.lock_amount(&self.config);
453        let result = self
454            .contract
455            .methods()
456            .create_trigger_orders(
457                params_1.to_trigger_order_args(),
458                params_2.to_trigger_order_args(),
459                expected_parent_quantity,
460            )
461            .call_params(CallParameters::new(amount, asset_id, u64::MAX))?
462            .call()
463            .await?;
464        Ok(result.value)
465    }
466
467    /// Executes the next trigger order at the head of the global queue.
468    pub async fn execute_trigger_order(&self) -> anyhow::Result<()> {
469        self.contract
470            .methods()
471            .execute_trigger_order()
472            .call()
473            .await?;
474        Ok(())
475    }
476
477    /// Admin: ejects the stuck trigger at the head of the queue.
478    pub async fn eject_trigger_order(&self) -> anyhow::Result<()> {
479        self.contract.methods().eject_trigger_order().call().await?;
480        Ok(())
481    }
482
483    /// Returns the trigger order with the given ID, or `None` if it doesn't exist.
484    pub async fn get_trigger_order(
485        &self,
486        order_id: Bits256,
487    ) -> anyhow::Result<Option<TriggerOrder>> {
488        let result = self
489            .contract
490            .methods()
491            .get_trigger_order(order_id)
492            .simulate(Execution::state_read_only())
493            .await?;
494        Ok(result.value)
495    }
496
497    /// Returns all trigger orders registered at the given trigger price.
498    pub async fn get_trigger_orders_at_price(
499        &self,
500        trigger_price: u64,
501    ) -> anyhow::Result<Vec<TriggerOrder>> {
502        let result = self
503            .contract
504            .methods()
505            .get_trigger_orders_at_price(trigger_price)
506            .simulate(Execution::state_read_only())
507            .await?;
508        Ok(result.value)
509    }
510
511    /// Returns the locked balance for a trader under a given trigger order ID.
512    pub async fn get_locked_balance_of(
513        &self,
514        trader: Identity,
515        order_id: Bits256,
516    ) -> anyhow::Result<u64> {
517        let result = self
518            .contract
519            .methods()
520            .get_locked_balance_of(trader, order_id)
521            .simulate(Execution::state_read_only())
522            .await?;
523        Ok(result.value)
524    }
525
526    /// Returns the sibling trigger order ID for an OCO pair, or `None` if no sibling exists.
527    pub async fn get_trigger_order_sibling(
528        &self,
529        order_id: Bits256,
530    ) -> anyhow::Result<Option<Bits256>> {
531        let result = self
532            .contract
533            .methods()
534            .get_trigger_order_sibling(order_id)
535            .simulate(Execution::state_read_only())
536            .await?;
537        Ok(result.value)
538    }
539
540    /// Returns the parent spot order ID for a trigger order, or `None` if not attached.
541    pub async fn get_trigger_order_parent(
542        &self,
543        order_id: Bits256,
544    ) -> anyhow::Result<Option<Bits256>> {
545        let result = self
546            .contract
547            .methods()
548            .get_trigger_order_parent(order_id)
549            .simulate(Execution::state_read_only())
550            .await?;
551        Ok(result.value)
552    }
553
554    /// Returns the trigger order at the head of the global execution queue, or `None` if empty.
555    pub async fn get_head_trigger_order(&self) -> anyhow::Result<Option<TriggerOrder>> {
556        let result = self
557            .contract
558            .methods()
559            .get_head_trigger_order()
560            .simulate(Execution::state_read_only())
561            .await?;
562        Ok(result.value)
563    }
564
565    pub async fn is_paused(&self) -> anyhow::Result<bool> {
566        let result = self
567            .contract
568            .methods()
569            .is_paused()
570            .simulate(Execution::state_read_only())
571            .await?;
572        Ok(result.value)
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use crate::{
579        helpers::get_asset_balance,
580        order_book_deploy::{
581            OrderBookDeployConfig,
582            OrderCreatedEvent,
583            OrderMatchedEvent,
584        },
585    };
586
587    use super::*;
588    use fuels::test_helpers::{
589        WalletsConfig,
590        launch_custom_provider_and_get_wallets,
591    };
592
593    #[tokio::test]
594    async fn test_order_book_manager() {
595        let base_asset = AssetId::from([1; 32]);
596        let quote_asset = AssetId::from([2; 32]);
597        let initial_balance = 1_000_000_000_000_000u64;
598
599        // Start fuel-core
600        let mut wallets = launch_custom_provider_and_get_wallets(
601            WalletsConfig::new_multiple_assets(
602                3,
603                vec![
604                    AssetConfig {
605                        id: AssetId::default(),
606                        num_coins: 1,
607                        coin_amount: initial_balance,
608                    },
609                    AssetConfig {
610                        id: quote_asset,
611                        num_coins: 1,
612                        coin_amount: initial_balance,
613                    },
614                    AssetConfig {
615                        id: base_asset,
616                        num_coins: 1,
617                        coin_amount: initial_balance,
618                    },
619                ],
620            ),
621            None,
622            Some(::fuels::test_helpers::ChainConfig::local_testnet()),
623        )
624        .await
625        .unwrap();
626        let deployer_wallet = wallets.pop().unwrap();
627        let maker_wallet = wallets.pop().unwrap();
628        let taker_wallet = wallets.pop().unwrap();
629
630        // Deploy OrderBook
631        let mut config = OrderBookDeployConfig::default();
632
633        config.order_book_configurables = config
634            .order_book_configurables
635            .with_MAKER_FEE(0.into())
636            .unwrap()
637            .with_TAKER_FEE(0.into())
638            .unwrap();
639
640        let deployment =
641            OrderBookDeploy::deploy(&deployer_wallet, base_asset, quote_asset, &config)
642                .await
643                .unwrap();
644
645        // Check if contract exists
646        let provider = deployer_wallet.try_provider().unwrap();
647        let contract_exists = provider
648            .contract_exists(&deployment.contract_id)
649            .await
650            .unwrap();
651        assert!(contract_exists, "OrderBook contract should exist");
652
653        // Verify configuration
654        assert_eq!(deployment.base_asset, base_asset);
655        assert_eq!(deployment.quote_asset, quote_asset);
656
657        let order_book_manager = OrderBookManager::new(
658            &deployer_wallet,
659            10u64.pow(9),
660            10u64.pow(9),
661            &deployment,
662        );
663        let maker_order_params = CreateOrderParams {
664            price: 1_000_000_000,
665            quantity: 2_000_000_000,
666            order_type: OrderType::Spot,
667            side: Side::Buy,
668            asset_id: quote_asset,
669        };
670        let maker_call_params =
671            order_book_manager.create_call_params(&maker_order_params, None);
672        let maker_order_book_instance = order_book_manager
673            .contract
674            .clone()
675            .with_account(maker_wallet.clone());
676        let result = maker_order_book_instance
677            .methods()
678            .create_order(maker_order_params.to_order_args())
679            .with_tx_policies(TxPolicies::default())
680            .call_params(CallParameters::new(
681                maker_call_params.coins,
682                maker_call_params.asset_id,
683                maker_call_params.gas,
684            ))
685            .unwrap()
686            .call()
687            .await
688            .unwrap();
689        let maker_order_created_events =
690            result.decode_logs_with_type::<OrderCreatedEvent>().unwrap();
691        let maker_order_created_event = maker_order_created_events.first().unwrap();
692        let taker_order_params = CreateOrderParams {
693            price: 1_000_000_000,
694            quantity: 1_000_000_000,
695            order_type: OrderType::Spot,
696            side: Side::Sell,
697            asset_id: base_asset,
698        };
699        let taker_call_params =
700            order_book_manager.create_call_params(&taker_order_params, None);
701        let result = order_book_manager
702            .contract
703            .clone()
704            .with_account(taker_wallet.clone())
705            .methods()
706            .create_order(taker_order_params.to_order_args())
707            .with_tx_policies(TxPolicies::default())
708            .call_params(CallParameters::new(
709                taker_call_params.coins,
710                taker_call_params.asset_id,
711                taker_call_params.gas,
712            ))
713            .unwrap()
714            .with_variable_output_policy(VariableOutputPolicy::Exactly(10))
715            .call()
716            .await
717            .unwrap();
718
719        let maker_balances = maker_wallet.get_balances().await.unwrap();
720        let taker_balances = taker_wallet.get_balances().await.unwrap();
721        let (maker_balance_base, maker_balance_quote) = order_book_manager
722            .balances_of(&Identity::Address(maker_wallet.address()))
723            .await
724            .unwrap();
725        let (taker_balance_base, taker_balance_quote) = order_book_manager
726            .balances_of(&Identity::Address(taker_wallet.address()))
727            .await
728            .unwrap();
729
730        assert_eq!(
731            get_asset_balance(&maker_balances, &base_asset) + maker_balance_base,
732            initial_balance as u128 + taker_order_params.quantity as u128
733        );
734        assert_eq!(
735            get_asset_balance(&maker_balances, &quote_asset) + maker_balance_quote,
736            initial_balance as u128 - maker_call_params.coins as u128
737        );
738        assert_eq!(
739            get_asset_balance(&taker_balances, &base_asset) + taker_balance_base,
740            initial_balance as u128 - taker_order_params.quantity as u128
741        );
742        assert_eq!(
743            get_asset_balance(&taker_balances, &quote_asset) + taker_balance_quote,
744            initial_balance as u128 + 1_000_000_000u128
745        );
746
747        // Log receipts
748        let matches = result.decode_logs_with_type::<OrderMatchedEvent>().unwrap();
749        // Only one match
750        assert_eq!(matches.len(), 1);
751        let match_event = matches.first().unwrap();
752        assert_eq!(match_event.price, maker_order_params.price);
753        assert_eq!(match_event.quantity, taker_order_params.quantity);
754
755        let _ = maker_order_book_instance
756            .methods()
757            .settle_balances(vec![
758                Identity::Address(maker_wallet.address()),
759                Identity::Address(taker_wallet.address()),
760            ])
761            .with_variable_output_policy(VariableOutputPolicy::Exactly(5))
762            .call()
763            .await
764            .unwrap();
765        let maker_quote_balance_before_cancel =
766            get_asset_balance(&maker_wallet.get_balances().await.unwrap(), &quote_asset);
767        let cancel_result = maker_order_book_instance
768            .methods()
769            .cancel_order(maker_order_created_event.order_id)
770            .with_tx_policies(TxPolicies::default())
771            .with_variable_output_policy(VariableOutputPolicy::Exactly(10))
772            .call()
773            .await
774            .unwrap();
775        let maker_quote_balance_after =
776            get_asset_balance(&maker_wallet.get_balances().await.unwrap(), &quote_asset);
777
778        assert!(cancel_result.value);
779        assert_eq!(
780            maker_quote_balance_after,
781            maker_quote_balance_before_cancel + 1_000_000_000u128
782        );
783    }
784}