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 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 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 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 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 pub async fn eject_trigger_order(&self) -> anyhow::Result<()> {
463 self.contract.methods().eject_trigger_order().call().await?;
464 Ok(())
465 }
466
467 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 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 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 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 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 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 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 None,
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 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 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 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, "e_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, "e_asset) + taker_balance_quote,
728 initial_balance as u128 + 1_000_000_000u128
729 );
730
731 let matches = result.decode_logs_with_type::<OrderMatchedEvent>().unwrap();
733 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(), "e_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(), "e_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}