1use fuel_core_client::client::types::primitives::{
7 ContractId,
8 Salt,
9};
10use fuel_core_types::fuel_types::BlockHeight;
11use fuels::{
12 accounts::{
13 Account,
14 ViewOnlyAccount,
15 },
16 prelude::Execution,
17 types::{
18 Identity,
19 SizedAsciiString,
20 },
21};
22use o2_api_types::{
23 domain::book::{
24 AssetConfig,
25 MarketIdAssets,
26 OrderBookConfig,
27 },
28 parse::HexDisplayFromStr,
29};
30use o2_tools::{
31 order_book::OrderBookManager,
32 order_book_deploy::{
33 OrderBookBlacklist,
34 OrderBookConfigurables,
35 OrderBookDeploy,
36 OrderBookDeployConfig,
37 OrderBookWhitelist,
38 },
39 order_book_registry::{
40 OrderBookRegistryDeployConfig,
41 OrderBookRegistryManager,
42 },
43 trade_account_deploy::{
44 DeployConfig,
45 TradeAccountDeploy,
46 TradeAccountDeployConfig,
47 },
48 trade_account_registry::{
49 TradeAccountRegistryDeployConfig,
50 TradeAccountRegistryManager,
51 },
52};
53use serde_with::serde_as;
54use std::ops::{
55 Deref,
56 DerefMut,
57};
58
59fn to_registry_market_id(m: &MarketIdAssets) -> o2_tools::order_book_registry::MarketId {
60 o2_tools::order_book_registry::MarketId {
61 base_asset: m.base_asset,
62 quote_asset: m.quote_asset,
63 }
64}
65
66#[serde_as]
71#[derive(Debug, serde::Serialize, Clone, Default)]
72pub struct MarketsConfigOutput {
73 pub starting_height: u32,
74 #[serde_as(as = "HexDisplayFromStr")]
75 pub trade_account_registry_id: ContractId,
76 #[serde_as(as = "HexDisplayFromStr")]
77 pub trade_account_registry_blob_id: ContractId,
78 #[serde_as(as = "HexDisplayFromStr")]
79 pub trade_account_oracle_id: ContractId,
80 #[serde_as(as = "HexDisplayFromStr")]
81 pub trade_account_root: ContractId,
82 #[serde_as(as = "HexDisplayFromStr")]
83 pub trade_account_proxy: ContractId,
84 #[serde_as(as = "HexDisplayFromStr")]
85 pub trade_account_blob_id: ContractId,
86 #[serde_as(as = "Option<HexDisplayFromStr>")]
87 pub order_book_whitelist_id: Option<ContractId>,
88 #[serde_as(as = "Option<HexDisplayFromStr>")]
89 pub order_book_blacklist_id: Option<ContractId>,
90 #[serde_as(as = "HexDisplayFromStr")]
91 pub order_book_registry_id: ContractId,
92 #[serde_as(as = "HexDisplayFromStr")]
93 pub order_book_registry_blob_id: ContractId,
94 #[serde_as(as = "Option<HexDisplayFromStr>")]
95 pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
96 pub pairs: Vec<OrderBookConfig>,
97}
98
99#[serde_as]
101#[derive(Debug, Clone, serde::Deserialize)]
102struct OrderBookConfigDeHelper {
103 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
104 blob_id: Option<ContractId>,
105 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
106 contract_id: Option<ContractId>,
107 #[serde_as(as = "serde_with::DisplayFromStr")]
108 taker_fee: u64,
109 #[serde_as(as = "serde_with::DisplayFromStr")]
110 maker_fee: u64,
111 #[serde_as(as = "serde_with::DisplayFromStr")]
112 min_order: u64,
113 #[serde_as(as = "serde_with::DisplayFromStr")]
114 dust: u64,
115 price_window: u8,
116 base: AssetConfig,
117 quote: AssetConfig,
118}
119
120impl From<OrderBookConfigDeHelper> for OrderBookConfig {
121 fn from(h: OrderBookConfigDeHelper) -> Self {
122 let ids = MarketIdAssets {
123 base_asset: h.base.asset,
124 quote_asset: h.quote.asset,
125 };
126 let market_id = ids.market_id();
127 OrderBookConfig {
128 contract_id: h.contract_id,
129 blob_id: h.blob_id,
130 market_id,
131 taker_fee: h.taker_fee,
132 maker_fee: h.maker_fee,
133 min_order: h.min_order,
134 dust: h.dust,
135 price_window: h.price_window,
136 base: h.base,
137 quote: h.quote,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Default, serde::Serialize)]
143pub struct MarketsConfigPartial {
144 pub starting_height: u32,
145 pub trade_account_registry_id: Option<ContractId>,
146 pub order_book_registry_id: Option<ContractId>,
147 pub trade_account_oracle_id: Option<ContractId>,
148 pub order_book_whitelist_id: Option<ContractId>,
149 pub order_book_blacklist_id: Option<ContractId>,
150 pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
151 pub pairs: Vec<OrderBookConfig>,
152}
153
154impl<'de> serde::Deserialize<'de> for MarketsConfigPartial {
155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156 where
157 D: serde::Deserializer<'de>,
158 {
159 #[derive(serde::Deserialize, Default)]
160 struct Helper {
161 #[serde(default)]
162 starting_height: u32,
163 trade_account_registry_id: Option<ContractId>,
164 order_book_registry_id: Option<ContractId>,
165 trade_account_oracle_id: Option<ContractId>,
166 order_book_whitelist_id: Option<ContractId>,
167 order_book_blacklist_id: Option<ContractId>,
168 fast_bridge_asset_registry_proxy_id: Option<ContractId>,
169 #[serde(default)]
170 pairs: Vec<OrderBookConfigDeHelper>,
171 }
172 let h = Helper::deserialize(deserializer)?;
173 Ok(MarketsConfigPartial {
174 starting_height: h.starting_height,
175 trade_account_registry_id: h.trade_account_registry_id,
176 order_book_registry_id: h.order_book_registry_id,
177 trade_account_oracle_id: h.trade_account_oracle_id,
178 order_book_whitelist_id: h.order_book_whitelist_id,
179 order_book_blacklist_id: h.order_book_blacklist_id,
180 fast_bridge_asset_registry_proxy_id: h.fast_bridge_asset_registry_proxy_id,
181 pairs: h.pairs.into_iter().map(Into::into).collect(),
182 })
183 }
184}
185
186#[derive(Debug, Clone, Copy, Default)]
187pub struct OwnershipTransferOptions {
188 pub new_proxy_owner: Option<fuels::types::Address>,
189 pub new_contract_owner: Option<fuels::types::Address>,
190}
191
192#[derive(Debug, Clone)]
194pub struct DeployParams {
195 pub deploy_config: MarketsConfigPartial,
196 pub output: Option<String>,
197 pub deploy_whitelist: bool,
198 pub deploy_blacklist: bool,
199 pub upgrade_bytecode: bool,
200 pub new_proxy_owner: Option<fuels::types::Address>,
201 pub new_contract_owner: Option<fuels::types::Address>,
202}
203
204pub fn load_config_from_file<T>(config_path: &str) -> anyhow::Result<T>
210where
211 T: Default + serde::de::DeserializeOwned,
212{
213 if config_path.is_empty() {
214 return Ok(T::default());
215 }
216 let current_dir = std::env::current_dir()?;
217 let path = current_dir.join(config_path);
218 tracing::info!("Loading config from {}", path.display());
219 let file = std::fs::File::open(&path)?;
220 let config: T = serde_json::from_reader(file)?;
221 Ok(config)
222}
223
224pub async fn deploy<W>(
233 wallet: W,
234 params: DeployParams,
235) -> anyhow::Result<MarketsConfigOutput>
236where
237 W: Account + ViewOnlyAccount + Clone + 'static,
238{
239 tracing::info!("Starting Fuel o2 Registries and Markets");
240 let mut markets_config_partial = params.deploy_config.clone();
241 let starting_height: BlockHeight = markets_config_partial.starting_height.into();
242 let trade_account_oracle_id = markets_config_partial.trade_account_oracle_id;
243 let order_book_registry_id = markets_config_partial.order_book_registry_id;
244 let trade_account_registry_id = markets_config_partial.trade_account_registry_id;
245 let fast_bridge_asset_registry_proxy_id =
246 markets_config_partial.fast_bridge_asset_registry_proxy_id;
247
248 let mut salt = Salt::zeroed();
249 salt.deref_mut()[..4].copy_from_slice(&starting_height.deref().to_be_bytes());
250
251 let (trade_account_oracle_deploy, trade_account_blob_id) =
252 deploy_trade_account_oracle(
253 wallet.clone(),
254 params.upgrade_bytecode,
255 trade_account_oracle_id,
256 salt,
257 )
258 .await?;
259 let (trade_account_registry, trade_account_registry_blob_id) =
260 deploy_trade_account_registry(
261 wallet.clone(),
262 params.upgrade_bytecode,
263 trade_account_oracle_deploy.clone(),
264 trade_account_registry_id,
265 salt,
266 )
267 .await?;
268 let order_book_blacklist_id = deploy_order_book_blacklist(
269 wallet.clone(),
270 params.deploy_blacklist,
271 markets_config_partial.order_book_blacklist_id,
272 salt,
273 )
274 .await?;
275 let order_book_whitelist_id = deploy_order_book_whitelist(
276 wallet.clone(),
277 params.deploy_whitelist,
278 markets_config_partial.order_book_whitelist_id,
279 salt,
280 )
281 .await?;
282 let (order_book_registry, order_book_registry_blob_id) = deploy_order_book_registry(
283 wallet.clone(),
284 params.upgrade_bytecode,
285 order_book_registry_id,
286 salt,
287 )
288 .await?;
289 let pairs = deploy_order_books(
290 wallet.clone(),
291 params.upgrade_bytecode,
292 order_book_blacklist_id,
293 order_book_whitelist_id,
294 order_book_registry.clone(),
295 &mut markets_config_partial.pairs,
296 OwnershipTransferOptions {
297 new_proxy_owner: params.new_proxy_owner,
298 new_contract_owner: params.new_contract_owner,
299 },
300 )
301 .await?;
302
303 let order_book_registry_id = order_book_registry.contract_id;
304 let trade_account_registry_id = trade_account_registry.contract_id;
305 let trade_account_oracle_id = trade_account_oracle_deploy.oracle_id;
306
307 let trade_account_proxy = trade_account_registry
308 .registry
309 .methods()
310 .default_bytecode()
311 .simulate(Execution::state_read_only())
312 .await?
313 .value
314 .expect("Trade account registry factory bytecode root should exist");
315 let trade_account_root = trade_account_registry
316 .registry
317 .methods()
318 .factory_bytecode_root()
319 .simulate(Execution::state_read_only())
320 .await?
321 .value
322 .expect("Trade account registry factory bytecode root should exist");
323
324 if let Some(new_proxy_owner) = params.new_proxy_owner {
325 let new_identity = Identity::Address(new_proxy_owner);
326 tracing::info!(
327 "Transferring OrderBookRegistry proxy ownership to {}",
328 new_proxy_owner
329 );
330 order_book_registry
331 .registry_proxy
332 .methods()
333 .set_owner(new_identity)
334 .call()
335 .await?;
336 tracing::info!(
337 "Transferring TradeAccountRegistry proxy ownership to {}",
338 new_proxy_owner
339 );
340 trade_account_registry
341 .registry_proxy
342 .methods()
343 .set_owner(new_identity)
344 .call()
345 .await?;
346 }
347
348 if let Some(new_contract_owner) = params.new_contract_owner {
349 let new_identity = Identity::Address(new_contract_owner);
350 tracing::info!(
351 "Transferring TradeAccountOracle ownership to {}",
352 new_contract_owner
353 );
354 trade_account_oracle_deploy
355 .oracle
356 .methods()
357 .transfer_ownership(new_identity)
358 .call()
359 .await?;
360 tracing::info!(
361 "Transferring TradeAccountRegistry ownership to {}",
362 new_contract_owner
363 );
364 trade_account_registry
365 .registry
366 .methods()
367 .transfer_ownership(new_identity)
368 .call()
369 .await?;
370 tracing::info!(
371 "Transferring OrderBookRegistry ownership to {}",
372 new_contract_owner
373 );
374 order_book_registry
375 .registry
376 .methods()
377 .transfer_ownership(new_identity)
378 .call()
379 .await?;
380 if let Some(blacklist_id) = order_book_blacklist_id {
381 tracing::info!(
382 "Transferring OrderBookBlacklist ownership to {}",
383 new_contract_owner
384 );
385 OrderBookBlacklist::new(blacklist_id, wallet.clone())
386 .methods()
387 .transfer_ownership(new_identity)
388 .call()
389 .await?;
390 }
391 if let Some(whitelist_id) = order_book_whitelist_id {
392 tracing::info!(
393 "Transferring OrderBookWhitelist ownership to {}",
394 new_contract_owner
395 );
396 OrderBookWhitelist::new(whitelist_id, wallet.clone())
397 .methods()
398 .transfer_ownership(new_identity)
399 .call()
400 .await?;
401 }
402 }
403
404 let deploy_result = MarketsConfigOutput {
405 starting_height: starting_height.into(),
406 trade_account_registry_id,
407 trade_account_registry_blob_id,
408 trade_account_proxy,
409 trade_account_blob_id,
410 trade_account_root: ContractId::from(trade_account_root.0),
411 trade_account_oracle_id,
412 order_book_whitelist_id,
413 order_book_blacklist_id,
414 order_book_registry_id,
415 order_book_registry_blob_id,
416 pairs,
417 fast_bridge_asset_registry_proxy_id,
418 };
419
420 if let Some(output_path) = params.output {
421 let json = serde_json::to_string_pretty(&deploy_result)?;
422 tracing::info!("Deploy result saved to {}", output_path);
423 std::fs::write(output_path, json)?;
424 }
425
426 Ok(deploy_result)
427}
428
429async fn deploy_order_book_blacklist<W>(
434 deployer_wallet: W,
435 deploy_blacklist: bool,
436 order_book_blacklist_id: Option<ContractId>,
437 salt: Salt,
438) -> anyhow::Result<Option<ContractId>>
439where
440 W: Account + ViewOnlyAccount + Clone + 'static,
441{
442 match order_book_blacklist_id {
443 Some(order_book_blacklist_id) => {
444 tracing::info!(
445 "Using existing OrderBookBlacklist: {}",
446 order_book_blacklist_id
447 );
448 Ok(Some(order_book_blacklist_id))
449 }
450 None => {
451 if !deploy_blacklist {
452 return Ok(None);
453 }
454 tracing::info!("Deploying OrderBookBlacklist");
455 let order_book_blacklist = OrderBookDeploy::deploy_order_book_blacklist(
456 &deployer_wallet,
457 &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
458 &OrderBookDeployConfig {
459 salt,
460 ..Default::default()
461 },
462 )
463 .await?;
464 tracing::info!("OrderBookBlacklist: {}", order_book_blacklist.contract_id());
465 Ok(Some(order_book_blacklist.contract_id()))
466 }
467 }
468}
469
470async fn deploy_order_book_whitelist<W>(
471 deployer_wallet: W,
472 deploy_whitelist: bool,
473 order_book_whitelist_id: Option<ContractId>,
474 salt: Salt,
475) -> anyhow::Result<Option<ContractId>>
476where
477 W: Account + ViewOnlyAccount + Clone + 'static,
478{
479 match (order_book_whitelist_id, deploy_whitelist) {
480 (Some(order_book_whitelist_id), false)
481 | (Some(order_book_whitelist_id), true) => {
482 tracing::info!(
483 "Using existing OrderBookWhitelist: {}",
484 order_book_whitelist_id
485 );
486 Ok(Some(order_book_whitelist_id))
487 }
488 (None, false) => Ok(None),
489 (None, true) => {
490 tracing::info!("Deploying OrderBookWhitelist");
491 let trade_account_whitelist = OrderBookDeploy::deploy_order_book_whitelist(
492 &deployer_wallet,
493 &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
494 &OrderBookDeployConfig {
495 salt,
496 ..Default::default()
497 },
498 )
499 .await?;
500 tracing::info!(
501 "OrderBookWhitelist: {}",
502 trade_account_whitelist.contract_id()
503 );
504 Ok(Some(trade_account_whitelist.contract_id()))
505 }
506 }
507}
508
509async fn deploy_trade_account_oracle<W>(
510 deployer_wallet: W,
511 should_upgrade_bytecode: bool,
512 trade_account_oracle_id: Option<ContractId>,
513 salt: Salt,
514) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
515where
516 W: Account + ViewOnlyAccount + Clone + 'static,
517{
518 let trade_account_oracle_deploy = match trade_account_oracle_id {
519 Some(oracle_id) => {
520 TradeAccountDeploy::from_oracle_id(&deployer_wallet, oracle_id).await?
521 }
522 None => {
523 TradeAccountDeploy::deploy(
524 &deployer_wallet,
525 &DeployConfig::Latest(TradeAccountDeployConfig {
526 salt,
527 ..Default::default()
528 }),
529 )
530 .await?
531 }
532 };
533 tracing::info!(
534 "TradeAccountOracle: {}",
535 trade_account_oracle_deploy.oracle_id
536 );
537 let mut trade_account_blob_id = trade_account_oracle_deploy
538 .oracle
539 .methods()
540 .get_trade_account_impl()
541 .simulate(Execution::state_read_only())
542 .await?
543 .value
544 .expect("Trade Account implementaion should exist");
545
546 if should_upgrade_bytecode {
547 let trade_account_blob =
548 TradeAccountDeploy::trade_account_blob(&deployer_wallet, &Default::default())
549 .await?;
550 if ContractId::from(trade_account_blob.id) != trade_account_blob_id {
551 tracing::info!(
552 "Update TradeAccountImpl on Oracle from {:?} to new blob {:?}",
553 trade_account_blob_id,
554 ContractId::from(trade_account_blob.id)
555 );
556 TradeAccountDeploy::deploy_trade_account_blob(
557 &deployer_wallet,
558 &DeployConfig::Latest(Default::default()),
559 )
560 .await?;
561 trade_account_oracle_deploy
562 .oracle
563 .methods()
564 .set_trade_account_impl(ContractId::from(trade_account_blob.id))
565 .call()
566 .await?;
567 trade_account_blob_id = ContractId::from(trade_account_blob.id);
568 }
569 }
570
571 Ok((trade_account_oracle_deploy, trade_account_blob_id))
572}
573
574async fn deploy_trade_account_registry<W>(
575 deployer_wallet: W,
576 should_upgrade_bytecode: bool,
577 trade_account_deploy: TradeAccountDeploy<W>,
578 trade_account_registry_id: Option<ContractId>,
579 salt: Salt,
580) -> anyhow::Result<(TradeAccountRegistryManager<W>, ContractId)>
581where
582 W: Account + ViewOnlyAccount + Clone + 'static,
583{
584 let trade_account_oracle_id = trade_account_deploy.oracle_id;
585 let trade_account_registry = match trade_account_registry_id {
586 Some(trade_account_registry_contract_id) => TradeAccountRegistryManager::new(
587 deployer_wallet.clone(),
588 trade_account_registry_contract_id,
589 ),
590 None => {
591 let trade_account_registry_deploy_config = TradeAccountRegistryDeployConfig {
592 salt,
593 ..Default::default()
594 };
595 TradeAccountRegistryManager::deploy(
596 &deployer_wallet,
597 trade_account_oracle_id,
598 &trade_account_registry_deploy_config,
599 )
600 .await?
601 }
602 };
603 tracing::info!(
604 "TradeAccountRegistry: {}",
605 trade_account_registry.contract_id
606 );
607 let mut trade_account_registry_blob_id = trade_account_registry
608 .registry_proxy
609 .methods()
610 .proxy_target()
611 .simulate(Execution::state_read_only())
612 .await?
613 .value
614 .expect("Current TradeAccountRegistry traget to be set");
615
616 if should_upgrade_bytecode {
617 let trade_account_registry_deploy_config =
618 TradeAccountRegistryDeployConfig::default();
619 let trade_account_proxy_blob = TradeAccountRegistryManager::register_proxy_blob(
620 &deployer_wallet,
621 &trade_account_registry_deploy_config,
622 )
623 .await?;
624
625 let trade_account_register_blob = TradeAccountRegistryManager::register_blob(
626 &deployer_wallet,
627 trade_account_oracle_id,
628 trade_account_proxy_blob.id,
629 &trade_account_registry_deploy_config,
630 )
631 .await?;
632
633 if trade_account_registry_blob_id
634 != ContractId::from(trade_account_register_blob.id)
635 {
636 tracing::info!(
637 "Upgrade TradeAccountRegistry blob from {:?} to {:?}",
638 trade_account_registry.contract_id,
639 ContractId::from(trade_account_register_blob.id)
640 );
641 trade_account_registry
642 .upgrade(
643 trade_account_oracle_id,
644 &TradeAccountRegistryDeployConfig::default(),
645 )
646 .await?;
647 trade_account_registry_blob_id = trade_account_register_blob.id.into();
648 }
649 }
650 Ok((trade_account_registry, trade_account_registry_blob_id))
651}
652
653async fn deploy_order_book_registry<W>(
654 deployer_wallet: W,
655 should_upgrade_bytecode: bool,
656 order_book_registry_id: Option<ContractId>,
657 salt: Salt,
658) -> anyhow::Result<(OrderBookRegistryManager<W>, ContractId)>
659where
660 W: Account + ViewOnlyAccount + Clone + 'static,
661{
662 let order_book_registry = match order_book_registry_id {
663 Some(registry_contract_id) => {
664 OrderBookRegistryManager::new(deployer_wallet.clone(), registry_contract_id)
665 }
666 None => {
667 OrderBookRegistryManager::deploy(
668 &deployer_wallet,
669 &OrderBookRegistryDeployConfig {
670 salt,
671 ..Default::default()
672 },
673 )
674 .await?
675 }
676 };
677 tracing::info!("OrderBookRegistry: {}", order_book_registry.contract_id);
678 let mut order_book_registry_blob_id = order_book_registry
679 .registry_proxy
680 .methods()
681 .proxy_target()
682 .simulate(Execution::state_read_only())
683 .await?
684 .value
685 .expect("Current OrderBookRegistry traget to be set");
686
687 if should_upgrade_bytecode {
688 let order_book_register_deploy_config = OrderBookRegistryDeployConfig::default();
689 let order_book_register_blob = OrderBookRegistryManager::register_blob(
690 &deployer_wallet,
691 &order_book_register_deploy_config,
692 )
693 .await?;
694 if order_book_registry_blob_id != order_book_register_blob.id.into() {
695 tracing::info!(
696 "Upgrade OrderBookRegistry blob from {:?} to {:?}",
697 order_book_registry.contract_id,
698 ContractId::from(order_book_register_blob.id)
699 );
700 order_book_registry
701 .upgrade(&order_book_register_deploy_config)
702 .await?;
703 order_book_registry_blob_id = order_book_register_blob.id.into();
704 }
705 }
706
707 Ok((order_book_registry, order_book_registry_blob_id))
708}
709
710async fn deploy_order_books<W>(
711 deployer_wallet: W,
712 should_upgrade_bytecode: bool,
713 order_book_blacklist_id: Option<ContractId>,
714 order_book_whitelist_id: Option<ContractId>,
715 order_book_registry: OrderBookRegistryManager<W>,
716 order_book_configs: &mut [OrderBookConfig],
717 ownership_options: OwnershipTransferOptions,
718) -> anyhow::Result<Vec<OrderBookConfig>>
719where
720 W: Account + ViewOnlyAccount + Clone + 'static,
721{
722 let mut pairs: Vec<OrderBookConfig> = Vec::with_capacity(order_book_configs.len());
723 let order_book_registry_id = order_book_registry.contract_id;
724
725 for order_book_config in order_book_configs.iter_mut() {
726 let market_symbol = format!(
727 "{}/{}",
728 order_book_config.base.symbol, order_book_config.quote.symbol
729 );
730 let market_id = MarketIdAssets {
731 base_asset: order_book_config.base.asset,
732 quote_asset: order_book_config.quote.asset,
733 };
734 let price_precision = order_book_config
735 .quote
736 .decimals
737 .checked_sub(order_book_config.quote.max_precision)
738 .ok_or_else(|| {
739 anyhow::anyhow!(
740 "quote max_precision ({}) exceeds decimals ({})",
741 order_book_config.quote.max_precision,
742 order_book_config.quote.decimals
743 )
744 })?;
745 let quantity_precision = order_book_config
746 .base
747 .decimals
748 .checked_sub(order_book_config.base.max_precision)
749 .ok_or_else(|| {
750 anyhow::anyhow!(
751 "base max_precision ({}) exceeds decimals ({})",
752 order_book_config.base.max_precision,
753 order_book_config.base.decimals
754 )
755 })?;
756
757 let order_book_configurables = OrderBookConfigurables::default()
758 .with_MIN_ORDER(order_book_config.min_order)?
759 .with_TAKER_FEE(order_book_config.taker_fee.into())?
760 .with_MAKER_FEE(order_book_config.maker_fee.into())?
761 .with_DUST(order_book_config.dust)?
762 .with_PRICE_WINDOW(order_book_config.price_window as u64)?
763 .with_BASE_DECIMALS(10u64.pow(order_book_config.base.decimals as u32))?
764 .with_QUOTE_DECIMALS(10u64.pow(order_book_config.quote.decimals as u32))?
765 .with_BASE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
766 order_book_config.base.symbol.clone(),
767 )?)?
768 .with_QUOTE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
769 order_book_config.quote.symbol.clone(),
770 )?)?
771 .with_PRICE_PRECISION(10u64.pow(price_precision as u32))?
772 .with_QUANTITY_PRECISION(10u64.pow(quantity_precision as u32))?
773 .with_INITIAL_OWNER(o2_tools::order_book_deploy::State::Initialized(
774 Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
775 ))?
776 .with_WHITE_LIST_CONTRACT(order_book_whitelist_id)?
777 .with_BLACK_LIST_CONTRACT(order_book_blacklist_id)?;
778
779 let register_contract_id = order_book_registry
780 .registry
781 .methods()
782 .get_order_book(to_registry_market_id(&market_id))
783 .simulate(Execution::state_read_only())
784 .await?
785 .value;
786
787 let order_book = match register_contract_id {
788 Some(contract_id) => OrderBookManager::new(
789 &deployer_wallet,
790 10u64.pow(order_book_config.base.decimals as u32),
791 10u64.pow(order_book_config.quote.decimals as u32),
792 &OrderBookDeploy::new(
793 deployer_wallet.clone(),
794 contract_id,
795 market_id.base_asset,
796 market_id.quote_asset,
797 ),
798 ),
799 None => {
800 let (order_book_deployment, initialization_required) =
801 OrderBookDeploy::deploy_without_initialization(
802 &deployer_wallet,
803 market_id.base_asset,
804 market_id.quote_asset,
805 &OrderBookDeployConfig {
806 order_book_configurables: order_book_configurables.clone(),
807 salt: Salt::from(*order_book_registry_id),
808 ..Default::default()
809 },
810 )
811 .await?;
812
813 order_book_registry
814 .register_order_book(
815 to_registry_market_id(&market_id),
816 order_book_deployment.contract_id,
817 )
818 .await?;
819
820 if initialization_required {
821 order_book_deployment.initialize().await?;
822 }
823 OrderBookManager::new(
824 &deployer_wallet,
825 10u64.pow(order_book_config.base.decimals as u32),
826 10u64.pow(order_book_config.quote.decimals as u32),
827 &order_book_deployment,
828 )
829 }
830 };
831 tracing::info!(
832 "[{}] OrderBook: {}",
833 market_symbol,
834 order_book.contract.contract_id()
835 );
836
837 let mut order_book_blob_id = order_book
838 .proxy
839 .methods()
840 .proxy_target()
841 .simulate(Execution::state_read_only())
842 .await?
843 .value
844 .expect("Order book target should exist");
845
846 if should_upgrade_bytecode {
847 let order_book_deploy_config = OrderBookDeployConfig {
848 order_book_configurables,
849 ..Default::default()
850 };
851 let order_book_deploy = OrderBookDeploy::new(
852 deployer_wallet.clone(),
853 order_book.contract.contract_id(),
854 order_book_config.base.asset,
855 order_book_config.quote.asset,
856 );
857 let order_book_manager = OrderBookManager::new(
858 &deployer_wallet,
859 10u64.pow(order_book_config.base.decimals as u32),
860 10u64.pow(order_book_config.quote.decimals as u32),
861 &order_book_deploy,
862 );
863 let order_book_blob = OrderBookDeploy::order_book_blob(
864 &deployer_wallet,
865 order_book_config.base.asset,
866 order_book_config.quote.asset,
867 &order_book_deploy_config,
868 )
869 .await?;
870
871 if order_book_blob_id != order_book_blob.id.into() {
872 tracing::info!(
873 "[{}] Upgrade OrderBook blob from {:?} to {:?}",
874 market_symbol,
875 order_book_blob_id,
876 ContractId::from(order_book_blob.id)
877 );
878 order_book_manager
879 .upgrade(&order_book_deploy_config)
880 .await?;
881 tracing::info!(
882 "[{}] Emit new configuration event for {}",
883 market_symbol,
884 order_book.contract.contract_id()
885 );
886 order_book_manager.emit_config().await?;
887 order_book_blob_id = order_book_blob.id.into();
888 }
889 }
890
891 if let Some(new_owner) = ownership_options.new_proxy_owner {
892 let new_identity = Identity::Address(new_owner);
893 tracing::info!(
894 "[{}] Transferring OrderBook proxy ownership to {}",
895 market_symbol,
896 new_owner
897 );
898 order_book
899 .proxy
900 .methods()
901 .set_owner(new_identity)
902 .call()
903 .await?;
904 }
905
906 if let Some(new_owner) = ownership_options.new_contract_owner {
907 let new_identity = Identity::Address(new_owner);
908 tracing::info!(
909 "[{}] Transferring OrderBook contract ownership to {}",
910 market_symbol,
911 new_owner
912 );
913 order_book
914 .contract
915 .methods()
916 .transfer_ownership(new_identity)
917 .call()
918 .await?;
919 }
920
921 order_book_config.contract_id = Some(order_book.contract.contract_id());
922 order_book_config.blob_id = order_book_blob_id.into();
923
924 pairs.push(order_book_config.clone());
925 }
926
927 Ok(pairs)
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933
934 #[test]
935 fn load_config_empty_path_returns_default() {
936 let result: MarketsConfigPartial = load_config_from_file("").unwrap();
937 assert!(result.pairs.is_empty());
938 }
939
940 #[test]
941 fn load_config_missing_file_errors() {
942 let result: Result<MarketsConfigPartial, _> =
943 load_config_from_file("nonexistent_file_12345.json");
944 assert!(result.is_err());
945 }
946
947 #[test]
948 fn checked_sub_catches_overflow() {
949 let decimals: u32 = 6;
951 let max_precision: u32 = 8; let result = decimals.checked_sub(max_precision);
954 assert!(
955 result.is_none(),
956 "should return None when max_precision > decimals"
957 );
958
959 let result = 9u32.checked_sub(6);
961 assert_eq!(result, Some(3));
962 }
963
964 #[test]
965 fn markets_config_partial_default_has_empty_pairs() {
966 let config = MarketsConfigPartial::default();
967 assert!(config.pairs.is_empty());
968 }
969}