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    pub async fn emit_config(&self) -> anyhow::Result<()> {
312        self.contract
313            .methods()
314            .emit_orderbook_config()
315            .call()
316            .await?;
317        Ok(())
318    }
319
320    pub async fn upgrade(
321        &self,
322        deploy_config: &OrderBookDeployConfig,
323    ) -> anyhow::Result<()> {
324        let base_asset_id = self.contract.methods().get_base_asset().call().await?.value;
325        let quote_asset_id = self
326            .contract
327            .methods()
328            .get_quote_asset()
329            .simulate(Execution::state_read_only())
330            .await?
331            .value;
332        let order_book_blob_id = OrderBookDeploy::deploy_order_book_blob(
333            &self.gas_payer_wallet,
334            base_asset_id,
335            quote_asset_id,
336            deploy_config,
337        )
338        .await?;
339        self.proxy
340            .methods()
341            .set_proxy_target(ContractId::new(order_book_blob_id))
342            .call()
343            .await?;
344        Ok(())
345    }
346
347    pub async fn accumulated_fees(&self) -> anyhow::Result<(u64, u64)> {
348        let fees = self
349            .contract
350            .methods()
351            .current_fees()
352            .simulate(Execution::state_read_only())
353            .await?
354            .value;
355        Ok(fees)
356    }
357
358    pub async fn get_whitelist_id(&self) -> anyhow::Result<Option<ContractId>> {
359        let result = self
360            .contract
361            .methods()
362            .get_whitelist_id()
363            .simulate(Execution::state_read_only())
364            .await?;
365        Ok(result.value)
366    }
367
368    pub async fn get_blacklist_id(&self) -> anyhow::Result<Option<ContractId>> {
369        let result = self
370            .contract
371            .methods()
372            .get_blacklist_id()
373            .simulate(Execution::state_read_only())
374            .await?;
375        Ok(result.value)
376    }
377
378    pub fn base_asset(&self) -> AssetId {
379        self.config().base_asset
380    }
381
382    pub fn base_decimals(&self) -> u64 {
383        self.config().base_decimals
384    }
385
386    pub fn quote_asset(&self) -> AssetId {
387        self.config().quote_asset
388    }
389
390    pub fn quote_decimals(&self) -> u64 {
391        self.config().quote_decimals
392    }
393
394    pub fn config(&self) -> OrderbookConfig {
395        self.config
396    }
397
398    /// Creates a single trigger order. When `params.quantity` is
399    /// `TriggerQuantity::ParentOrder(spot_id)`, the trigger is attached to that resting spot
400    /// order. If the parent already has one child trigger, this trigger is registered as its
401    /// OCO sibling (one stop-loss, one take-profit). A parent may have at most two triggers.
402    ///
403    /// If `expected_parent_quantity` is `Some`, the call reverts unless the parent spot order's
404    /// remaining quantity exactly matches the provided value.
405    pub async fn create_trigger_order(
406        &self,
407        params: CreateTriggerOrderParams,
408        expected_parent_quantity: Option<u64>,
409    ) -> anyhow::Result<Bits256> {
410        let (asset_id, amount) = params.lock_amount(&self.config);
411        let result = self
412            .contract
413            .methods()
414            .create_trigger_order(
415                params.to_trigger_order_args(),
416                expected_parent_quantity,
417            )
418            .call_params(CallParameters::new(amount, asset_id, u64::MAX))?
419            .call()
420            .await?;
421        Ok(result.value)
422    }
423
424    /// Creates two OCO-linked trigger orders in a single call. Both params must use the same
425    /// `TriggerQuantity` variant; if both are `ParentOrder`, they must reference the same
426    /// spot order. `params_1` is canonical and its lock covers both orders.
427    ///
428    /// If `expected_parent_quantity` is `Some`, the call reverts unless the parent spot order's
429    /// remaining quantity exactly matches the provided value.
430    pub async fn create_trigger_orders(
431        &self,
432        params_1: CreateTriggerOrderParams,
433        params_2: CreateTriggerOrderParams,
434        expected_parent_quantity: Option<u64>,
435    ) -> anyhow::Result<(Bits256, Bits256)> {
436        let (asset_id, amount) = params_1.lock_amount(&self.config);
437        let result = self
438            .contract
439            .methods()
440            .create_trigger_orders(
441                params_1.to_trigger_order_args(),
442                params_2.to_trigger_order_args(),
443                expected_parent_quantity,
444            )
445            .call_params(CallParameters::new(amount, asset_id, u64::MAX))?
446            .call()
447            .await?;
448        Ok(result.value)
449    }
450
451    /// Executes the next trigger order at the head of the global queue.
452    pub async fn execute_trigger_order(&self) -> anyhow::Result<()> {
453        self.contract
454            .methods()
455            .execute_trigger_order()
456            .call()
457            .await?;
458        Ok(())
459    }
460
461    /// Admin: ejects the stuck trigger at the head of the queue.
462    pub async fn eject_trigger_order(&self) -> anyhow::Result<()> {
463        self.contract.methods().eject_trigger_order().call().await?;
464        Ok(())
465    }
466
467    /// Returns the trigger order with the given ID, or `None` if it doesn't exist.
468    pub async fn get_trigger_order(
469        &self,
470        order_id: Bits256,
471    ) -> anyhow::Result<Option<TriggerOrder>> {
472        let result = self
473            .contract
474            .methods()
475            .get_trigger_order(order_id)
476            .simulate(Execution::state_read_only())
477            .await?;
478        Ok(result.value)
479    }
480
481    /// Returns all trigger orders registered at the given trigger price.
482    pub async fn get_trigger_orders_at_price(
483        &self,
484        trigger_price: u64,
485    ) -> anyhow::Result<Vec<TriggerOrder>> {
486        let result = self
487            .contract
488            .methods()
489            .get_trigger_orders_at_price(trigger_price)
490            .simulate(Execution::state_read_only())
491            .await?;
492        Ok(result.value)
493    }
494
495    /// Returns the locked balance for a trader under a given trigger order ID.
496    pub async fn get_locked_balance_of(
497        &self,
498        trader: Identity,
499        order_id: Bits256,
500    ) -> anyhow::Result<u64> {
501        let result = self
502            .contract
503            .methods()
504            .get_locked_balance_of(trader, order_id)
505            .simulate(Execution::state_read_only())
506            .await?;
507        Ok(result.value)
508    }
509
510    /// Returns the sibling trigger order ID for an OCO pair, or `None` if no sibling exists.
511    pub async fn get_trigger_order_sibling(
512        &self,
513        order_id: Bits256,
514    ) -> anyhow::Result<Option<Bits256>> {
515        let result = self
516            .contract
517            .methods()
518            .get_trigger_order_sibling(order_id)
519            .simulate(Execution::state_read_only())
520            .await?;
521        Ok(result.value)
522    }
523
524    /// Returns the parent spot order ID for a trigger order, or `None` if not attached.
525    pub async fn get_trigger_order_parent(
526        &self,
527        order_id: Bits256,
528    ) -> anyhow::Result<Option<Bits256>> {
529        let result = self
530            .contract
531            .methods()
532            .get_trigger_order_parent(order_id)
533            .simulate(Execution::state_read_only())
534            .await?;
535        Ok(result.value)
536    }
537
538    /// Returns the trigger order at the head of the global execution queue, or `None` if empty.
539    pub async fn get_head_trigger_order(&self) -> anyhow::Result<Option<TriggerOrder>> {
540        let result = self
541            .contract
542            .methods()
543            .get_head_trigger_order()
544            .simulate(Execution::state_read_only())
545            .await?;
546        Ok(result.value)
547    }
548
549    pub async fn is_paused(&self) -> anyhow::Result<bool> {
550        let result = self
551            .contract
552            .methods()
553            .is_paused()
554            .simulate(Execution::state_read_only())
555            .await?;
556        Ok(result.value)
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use crate::{
563        helpers::get_asset_balance,
564        order_book_deploy::{
565            OrderBookDeployConfig,
566            OrderCreatedEvent,
567            OrderMatchedEvent,
568        },
569    };
570
571    use super::*;
572    use fuels::test_helpers::{
573        WalletsConfig,
574        launch_custom_provider_and_get_wallets,
575    };
576
577    #[tokio::test]
578    async fn test_order_book_manager() {
579        let base_asset = AssetId::from([1; 32]);
580        let quote_asset = AssetId::from([2; 32]);
581        let initial_balance = 1_000_000_000_000_000u64;
582
583        // Start fuel-core
584        let mut wallets = launch_custom_provider_and_get_wallets(
585            WalletsConfig::new_multiple_assets(
586                3,
587                vec![
588                    AssetConfig {
589                        id: AssetId::default(),
590                        num_coins: 1,
591                        coin_amount: initial_balance,
592                    },
593                    AssetConfig {
594                        id: quote_asset,
595                        num_coins: 1,
596                        coin_amount: initial_balance,
597                    },
598                    AssetConfig {
599                        id: base_asset,
600                        num_coins: 1,
601                        coin_amount: initial_balance,
602                    },
603                ],
604            ),
605            None,
606            Some(::fuels::test_helpers::ChainConfig::local_testnet()),
607        )
608        .await
609        .unwrap();
610        let deployer_wallet = wallets.pop().unwrap();
611        let maker_wallet = wallets.pop().unwrap();
612        let taker_wallet = wallets.pop().unwrap();
613
614        // Deploy OrderBook
615        let mut config = OrderBookDeployConfig::default();
616
617        config.order_book_configurables = config
618            .order_book_configurables
619            .with_MAKER_FEE(0.into())
620            .unwrap()
621            .with_TAKER_FEE(0.into())
622            .unwrap();
623
624        let deployment =
625            OrderBookDeploy::deploy(&deployer_wallet, base_asset, quote_asset, &config)
626                .await
627                .unwrap();
628
629        // Check if contract exists
630        let provider = deployer_wallet.try_provider().unwrap();
631        let contract_exists = provider
632            .contract_exists(&deployment.contract_id)
633            .await
634            .unwrap();
635        assert!(contract_exists, "OrderBook contract should exist");
636
637        // Verify configuration
638        assert_eq!(deployment.base_asset, base_asset);
639        assert_eq!(deployment.quote_asset, quote_asset);
640
641        let order_book_manager = OrderBookManager::new(
642            &deployer_wallet,
643            10u64.pow(9),
644            10u64.pow(9),
645            &deployment,
646        );
647        let maker_order_params = CreateOrderParams {
648            price: 1_000_000_000,
649            quantity: 2_000_000_000,
650            order_type: OrderType::Spot,
651            side: Side::Buy,
652            asset_id: quote_asset,
653        };
654        let maker_call_params =
655            order_book_manager.create_call_params(&maker_order_params, None);
656        let maker_order_book_instance = order_book_manager
657            .contract
658            .clone()
659            .with_account(maker_wallet.clone());
660        let result = maker_order_book_instance
661            .methods()
662            .create_order(maker_order_params.to_order_args())
663            .with_tx_policies(TxPolicies::default())
664            .call_params(CallParameters::new(
665                maker_call_params.coins,
666                maker_call_params.asset_id,
667                maker_call_params.gas,
668            ))
669            .unwrap()
670            .call()
671            .await
672            .unwrap();
673        let maker_order_created_events =
674            result.decode_logs_with_type::<OrderCreatedEvent>().unwrap();
675        let maker_order_created_event = maker_order_created_events.first().unwrap();
676        let taker_order_params = CreateOrderParams {
677            price: 1_000_000_000,
678            quantity: 1_000_000_000,
679            order_type: OrderType::Spot,
680            side: Side::Sell,
681            asset_id: base_asset,
682        };
683        let taker_call_params =
684            order_book_manager.create_call_params(&taker_order_params, None);
685        let result = order_book_manager
686            .contract
687            .clone()
688            .with_account(taker_wallet.clone())
689            .methods()
690            .create_order(taker_order_params.to_order_args())
691            .with_tx_policies(TxPolicies::default())
692            .call_params(CallParameters::new(
693                taker_call_params.coins,
694                taker_call_params.asset_id,
695                taker_call_params.gas,
696            ))
697            .unwrap()
698            .with_variable_output_policy(VariableOutputPolicy::Exactly(10))
699            .call()
700            .await
701            .unwrap();
702
703        let maker_balances = maker_wallet.get_balances().await.unwrap();
704        let taker_balances = taker_wallet.get_balances().await.unwrap();
705        let (maker_balance_base, maker_balance_quote) = order_book_manager
706            .balances_of(&Identity::Address(maker_wallet.address()))
707            .await
708            .unwrap();
709        let (taker_balance_base, taker_balance_quote) = order_book_manager
710            .balances_of(&Identity::Address(taker_wallet.address()))
711            .await
712            .unwrap();
713
714        assert_eq!(
715            get_asset_balance(&maker_balances, &base_asset) + maker_balance_base,
716            initial_balance as u128 + taker_order_params.quantity as u128
717        );
718        assert_eq!(
719            get_asset_balance(&maker_balances, &quote_asset) + maker_balance_quote,
720            initial_balance as u128 - maker_call_params.coins as u128
721        );
722        assert_eq!(
723            get_asset_balance(&taker_balances, &base_asset) + taker_balance_base,
724            initial_balance as u128 - taker_order_params.quantity as u128
725        );
726        assert_eq!(
727            get_asset_balance(&taker_balances, &quote_asset) + taker_balance_quote,
728            initial_balance as u128 + 1_000_000_000u128
729        );
730
731        // Log receipts
732        let matches = result.decode_logs_with_type::<OrderMatchedEvent>().unwrap();
733        // Only one match
734        assert_eq!(matches.len(), 1);
735        let match_event = matches.first().unwrap();
736        assert_eq!(match_event.price, maker_order_params.price);
737        assert_eq!(match_event.quantity, taker_order_params.quantity);
738
739        let _ = maker_order_book_instance
740            .methods()
741            .settle_balances(vec![
742                Identity::Address(maker_wallet.address()),
743                Identity::Address(taker_wallet.address()),
744            ])
745            .with_variable_output_policy(VariableOutputPolicy::Exactly(5))
746            .call()
747            .await
748            .unwrap();
749        let maker_quote_balance_before_cancel =
750            get_asset_balance(&maker_wallet.get_balances().await.unwrap(), &quote_asset);
751        let cancel_result = maker_order_book_instance
752            .methods()
753            .cancel_order(maker_order_created_event.order_id)
754            .with_tx_policies(TxPolicies::default())
755            .with_variable_output_policy(VariableOutputPolicy::Exactly(10))
756            .call()
757            .await
758            .unwrap();
759        let maker_quote_balance_after =
760            get_asset_balance(&maker_wallet.get_balances().await.unwrap(), &quote_asset);
761
762        assert!(cancel_result.value);
763        assert_eq!(
764            maker_quote_balance_after,
765            maker_quote_balance_before_cancel + 1_000_000_000u128
766        );
767    }
768}