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::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
69pub 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
89pub 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
97pub fn side_to_contract(side: Side) -> ContractSide {
99 match side {
100 Side::Buy => ContractSide::Buy,
101 Side::Sell => ContractSide::Sell,
102 }
103}
104
105#[derive(Debug, Clone)]
108pub struct CreateOrderParams {
109 pub price: u64,
111 pub quantity: u64,
113 pub order_type: OrderType,
115 pub side: Side,
117 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 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#[derive(Debug, Clone)]
154pub struct CreateTriggerOrderParams {
155 pub trigger_price: u64,
157 pub order_type: TriggerOrderType,
159 pub quantity: TriggerQuantity,
161 pub side: Side,
163}
164
165impl CreateTriggerOrderParams {
166 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 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 pub base_asset: AssetId,
202 pub base_decimals: u64,
203 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, ¶ms.side);
234 let asset_id = self.get_order_side_asset(¶ms.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 pub proxy: OrderBookProxy<W>,
243 pub contract: OrderBook<W>,
245 pub gas_payer_wallet: W,
247 pub config: OrderbookConfig,
249}
250
251impl<W: Account + Clone> OrderBookManager<W> {
252 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 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 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 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 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 pub async fn eject_trigger_order(&self) -> anyhow::Result<()> {
479 self.contract.methods().eject_trigger_order().call().await?;
480 Ok(())
481 }
482
483 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 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 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 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 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 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 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 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 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 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, "e_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, "e_asset) + taker_balance_quote,
744 initial_balance as u128 + 1_000_000_000u128
745 );
746
747 let matches = result.decode_logs_with_type::<OrderMatchedEvent>().unwrap();
749 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(), "e_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(), "e_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}