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