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
16use 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
28pub use o2_api_types::primitives::{
30 OrderType,
31 Side,
32};
33
34pub 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
50pub 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
73pub 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
97pub 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
105pub fn side_to_contract(side: Side) -> ContractSide {
107 match side {
108 Side::Buy => ContractSide::Buy,
109 Side::Sell => ContractSide::Sell,
110 }
111}
112
113#[derive(Debug, Clone)]
116pub struct CreateOrderParams {
117 pub price: u64,
119 pub quantity: u64,
121 pub order_type: OrderType,
123 pub side: Side,
125 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 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#[derive(Debug, Clone)]
162pub struct CreateTriggerOrderParams {
163 pub trigger_price: u64,
165 pub order_type: TriggerOrderType,
167 pub quantity: TriggerQuantity,
169 pub side: Side,
171}
172
173impl CreateTriggerOrderParams {
174 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 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 pub base_asset: AssetId,
210 pub base_decimals: u64,
211 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, ¶ms.side);
242 let asset_id = self.get_order_side_asset(¶ms.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 pub proxy: OrderBookProxy<W>,
251 pub contract: OrderBook<W>,
253 pub gas_payer_wallet: W,
255 pub config: OrderbookConfig,
257}
258
259impl<W: Account + Clone> OrderBookManager<W> {
260 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 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 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 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 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 pub async fn eject_trigger_order(&self) -> anyhow::Result<()> {
487 self.contract.methods().eject_trigger_order().call().await?;
488 Ok(())
489 }
490
491 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 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 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 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 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 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 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 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 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 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, "e_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, "e_asset) + taker_balance_quote,
763 initial_balance as u128 + 1_000_000_000u128
764 );
765
766 let matches = result.decode_logs_with_type::<OrderMatchedEvent>().unwrap();
768 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(), "e_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(), "e_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}