1use anyhow::Context;
7use fuel_core_client::client::types::primitives::{
8 ContractId,
9 Salt,
10};
11use fuel_core_types::fuel_types::BlockHeight;
12use fuels::{
13 accounts::{
14 Account,
15 ViewOnlyAccount,
16 },
17 prelude::Execution,
18 types::{
19 Identity,
20 SizedAsciiString,
21 },
22};
23use o2_api_types::{
24 domain::book::{
25 AssetConfig,
26 MarketIdAssets,
27 OrderBookConfig,
28 },
29 parse::HexDisplayFromStr,
30};
31use o2_tools::{
32 order_book::OrderBookManager,
33 order_book_deploy::{
34 OrderBookBlacklist,
35 OrderBookConfigurables,
36 OrderBookDeploy,
37 OrderBookDeployConfig,
38 OrderBookWhitelist,
39 },
40 order_book_registry::{
41 OrderBookRegistryDeployConfig,
42 OrderBookRegistryManager,
43 },
44 trade_account_deploy::{
45 DeployConfig,
46 TradeAccountDeploy,
47 TradeAccountDeployConfig,
48 TradingAccountOracle,
49 },
50 trade_account_registry::{
51 TradeAccountRegistryDeployConfig,
52 TradeAccountRegistryManager,
53 },
54};
55use serde_with::serde_as;
56use std::ops::{
57 Deref,
58 DerefMut,
59};
60
61fn to_registry_market_id(m: &MarketIdAssets) -> o2_tools::order_book_registry::MarketId {
62 o2_tools::order_book_registry::MarketId {
63 base_asset: m.base_asset,
64 quote_asset: m.quote_asset,
65 }
66}
67
68#[serde_as]
73#[derive(Debug, serde::Serialize, Clone, Default)]
74pub struct MarketsConfigOutput {
75 pub starting_height: u32,
76 #[serde_as(as = "HexDisplayFromStr")]
77 pub trade_account_registry_id: ContractId,
78 #[serde_as(as = "HexDisplayFromStr")]
79 pub trade_account_registry_blob_id: ContractId,
80 #[serde_as(as = "HexDisplayFromStr")]
81 pub trade_account_oracle_id: ContractId,
82 #[serde_as(as = "HexDisplayFromStr")]
83 pub trade_account_root: ContractId,
84 #[serde_as(as = "HexDisplayFromStr")]
85 pub trade_account_proxy: ContractId,
86 #[serde_as(as = "HexDisplayFromStr")]
87 pub trade_account_blob_id: ContractId,
88 #[serde_as(as = "Option<HexDisplayFromStr>")]
89 pub order_book_whitelist_id: Option<ContractId>,
90 #[serde_as(as = "Option<HexDisplayFromStr>")]
91 pub order_book_blacklist_id: Option<ContractId>,
92 #[serde_as(as = "HexDisplayFromStr")]
93 pub order_book_registry_id: ContractId,
94 #[serde_as(as = "HexDisplayFromStr")]
95 pub order_book_registry_blob_id: ContractId,
96 #[serde_as(as = "Option<HexDisplayFromStr>")]
97 pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
98 pub pairs: Vec<OrderBookConfig>,
99}
100
101#[serde_as]
103#[derive(Debug, Clone, serde::Deserialize)]
104struct OrderBookConfigDeHelper {
105 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
106 blob_id: Option<ContractId>,
107 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
108 contract_id: Option<ContractId>,
109 #[serde_as(as = "serde_with::DisplayFromStr")]
110 taker_fee: u64,
111 #[serde_as(as = "serde_with::DisplayFromStr")]
112 maker_fee: u64,
113 #[serde_as(as = "serde_with::DisplayFromStr")]
114 min_order: u64,
115 #[serde_as(as = "serde_with::DisplayFromStr")]
116 dust: u64,
117 price_window: u8,
118 base: AssetConfig,
119 quote: AssetConfig,
120}
121
122impl From<OrderBookConfigDeHelper> for OrderBookConfig {
123 fn from(h: OrderBookConfigDeHelper) -> Self {
124 let ids = MarketIdAssets {
125 base_asset: h.base.asset,
126 quote_asset: h.quote.asset,
127 };
128 let market_id = ids.market_id();
129 OrderBookConfig {
130 contract_id: h.contract_id,
131 blob_id: h.blob_id,
132 market_id,
133 taker_fee: h.taker_fee,
134 maker_fee: h.maker_fee,
135 min_order: h.min_order,
136 dust: h.dust,
137 price_window: h.price_window,
138 base: h.base,
139 quote: h.quote,
140 }
141 }
142}
143
144#[derive(Debug, Clone, Default, serde::Serialize)]
145pub struct MarketsConfigPartial {
146 pub starting_height: u32,
147 pub trade_account_registry_id: Option<ContractId>,
148 pub order_book_registry_id: Option<ContractId>,
149 pub trade_account_oracle_id: Option<ContractId>,
150 pub order_book_whitelist_id: Option<ContractId>,
151 pub order_book_blacklist_id: Option<ContractId>,
152 pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
153 pub pairs: Vec<OrderBookConfig>,
154}
155
156impl<'de> serde::Deserialize<'de> for MarketsConfigPartial {
157 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158 where
159 D: serde::Deserializer<'de>,
160 {
161 #[derive(serde::Deserialize, Default)]
162 struct Helper {
163 #[serde(default)]
164 starting_height: u32,
165 trade_account_registry_id: Option<ContractId>,
166 order_book_registry_id: Option<ContractId>,
167 trade_account_oracle_id: Option<ContractId>,
168 order_book_whitelist_id: Option<ContractId>,
169 order_book_blacklist_id: Option<ContractId>,
170 fast_bridge_asset_registry_proxy_id: Option<ContractId>,
171 #[serde(default)]
172 pairs: Vec<OrderBookConfigDeHelper>,
173 }
174 let h = Helper::deserialize(deserializer)?;
175 Ok(MarketsConfigPartial {
176 starting_height: h.starting_height,
177 trade_account_registry_id: h.trade_account_registry_id,
178 order_book_registry_id: h.order_book_registry_id,
179 trade_account_oracle_id: h.trade_account_oracle_id,
180 order_book_whitelist_id: h.order_book_whitelist_id,
181 order_book_blacklist_id: h.order_book_blacklist_id,
182 fast_bridge_asset_registry_proxy_id: h.fast_bridge_asset_registry_proxy_id,
183 pairs: h.pairs.into_iter().map(Into::into).collect(),
184 })
185 }
186}
187
188#[derive(Debug, Clone, Copy, Default)]
189pub struct OwnershipTransferOptions {
190 pub new_proxy_owner: Option<fuels::types::Address>,
191 pub new_contract_owner: Option<fuels::types::Address>,
192}
193
194#[derive(Debug, Clone)]
196pub struct DeployParams {
197 pub deploy_config: MarketsConfigPartial,
198 pub output: Option<String>,
199 pub deploy_whitelist: bool,
200 pub deploy_blacklist: bool,
201 pub upgrade_bytecode: bool,
202 pub new_proxy_owner: Option<fuels::types::Address>,
203 pub new_contract_owner: Option<fuels::types::Address>,
204}
205
206pub fn load_config_from_file<T>(config_path: &str) -> anyhow::Result<T>
212where
213 T: Default + serde::de::DeserializeOwned,
214{
215 if config_path.is_empty() {
216 return Ok(T::default());
217 }
218 let current_dir = std::env::current_dir()?;
219 let path = current_dir.join(config_path);
220 tracing::info!("Loading config from {}", path.display());
221 let file = std::fs::File::open(&path)?;
222 let config: T = serde_json::from_reader(file)?;
223 Ok(config)
224}
225
226pub async fn deploy<W>(
235 wallet: W,
236 params: DeployParams,
237) -> anyhow::Result<MarketsConfigOutput>
238where
239 W: Account + ViewOnlyAccount + Clone + 'static,
240{
241 tracing::info!("Starting Fuel o2 Registries and Markets");
242 let mut markets_config_partial = params.deploy_config.clone();
243 let starting_height: BlockHeight = markets_config_partial.starting_height.into();
244 let trade_account_oracle_id = markets_config_partial.trade_account_oracle_id;
245 let order_book_registry_id = markets_config_partial.order_book_registry_id;
246 let trade_account_registry_id = markets_config_partial.trade_account_registry_id;
247 let fast_bridge_asset_registry_proxy_id =
248 markets_config_partial.fast_bridge_asset_registry_proxy_id;
249
250 let mut salt = Salt::zeroed();
251 salt.deref_mut()[..4].copy_from_slice(&starting_height.deref().to_be_bytes());
252
253 let (trade_account_oracle_deploy, trade_account_blob_id) =
254 deploy_trade_account_oracle(
255 wallet.clone(),
256 params.upgrade_bytecode,
257 trade_account_oracle_id,
258 salt,
259 )
260 .await?;
261 let (trade_account_registry, trade_account_registry_blob_id) =
262 deploy_trade_account_registry(
263 wallet.clone(),
264 params.upgrade_bytecode,
265 trade_account_oracle_deploy.clone(),
266 trade_account_registry_id,
267 salt,
268 )
269 .await?;
270 let order_book_blacklist_id = deploy_order_book_blacklist(
271 wallet.clone(),
272 params.deploy_blacklist,
273 markets_config_partial.order_book_blacklist_id,
274 salt,
275 )
276 .await?;
277 let order_book_whitelist_id = deploy_order_book_whitelist(
278 wallet.clone(),
279 params.deploy_whitelist,
280 markets_config_partial.order_book_whitelist_id,
281 salt,
282 )
283 .await?;
284 let (order_book_registry, order_book_registry_blob_id) = deploy_order_book_registry(
285 wallet.clone(),
286 params.upgrade_bytecode,
287 order_book_registry_id,
288 salt,
289 )
290 .await?;
291 let pairs = deploy_order_books(
292 wallet.clone(),
293 params.upgrade_bytecode,
294 order_book_blacklist_id,
295 order_book_whitelist_id,
296 order_book_registry.clone(),
297 &mut markets_config_partial.pairs,
298 OwnershipTransferOptions {
299 new_proxy_owner: params.new_proxy_owner,
300 new_contract_owner: params.new_contract_owner,
301 },
302 )
303 .await?;
304
305 let order_book_registry_id = order_book_registry.contract_id;
306 let trade_account_registry_id = trade_account_registry.contract_id;
307 let trade_account_oracle_id = trade_account_oracle_deploy.oracle_id;
308
309 let trade_account_proxy = trade_account_registry
310 .registry
311 .methods()
312 .default_bytecode()
313 .simulate(Execution::state_read_only())
314 .await?
315 .value
316 .context(
317 "Trade account registry default bytecode should exist after initialization",
318 )?;
319 let trade_account_root = trade_account_registry
320 .registry
321 .methods()
322 .factory_bytecode_root()
323 .simulate(Execution::state_read_only())
324 .await?
325 .value
326 .context("Trade account registry factory bytecode root should exist after initialization")?;
327
328 transfer_ownership(
329 &wallet,
330 ¶ms,
331 &order_book_registry,
332 &trade_account_registry,
333 &trade_account_oracle_deploy,
334 order_book_blacklist_id,
335 order_book_whitelist_id,
336 )
337 .await?;
338
339 let deploy_result = MarketsConfigOutput {
340 starting_height: starting_height.into(),
341 trade_account_registry_id,
342 trade_account_registry_blob_id,
343 trade_account_proxy,
344 trade_account_blob_id,
345 trade_account_root: ContractId::from(trade_account_root.0),
346 trade_account_oracle_id,
347 order_book_whitelist_id,
348 order_book_blacklist_id,
349 order_book_registry_id,
350 order_book_registry_blob_id,
351 pairs,
352 fast_bridge_asset_registry_proxy_id,
353 };
354
355 if let Some(output_path) = params.output {
356 let json = serde_json::to_string_pretty(&deploy_result)?;
357 tracing::info!("Deploy result saved to {}", output_path);
358 std::fs::write(output_path, json)?;
359 }
360
361 Ok(deploy_result)
362}
363
364async fn transfer_ownership<W>(
369 wallet: &W,
370 params: &DeployParams,
371 order_book_registry: &OrderBookRegistryManager<W>,
372 trade_account_registry: &TradeAccountRegistryManager<W>,
373 trade_account_oracle_deploy: &TradeAccountDeploy<W>,
374 order_book_blacklist_id: Option<ContractId>,
375 order_book_whitelist_id: Option<ContractId>,
376) -> anyhow::Result<()>
377where
378 W: Account + ViewOnlyAccount + Clone + 'static,
379{
380 if let Some(new_proxy_owner) = params.new_proxy_owner {
381 let new_identity = Identity::Address(new_proxy_owner);
382 tracing::info!(
383 "Transferring OrderBookRegistry proxy ownership to {}",
384 new_proxy_owner
385 );
386 order_book_registry
387 .registry_proxy
388 .methods()
389 .set_owner(new_identity)
390 .call()
391 .await?;
392 tracing::info!(
393 "Transferring TradeAccountRegistry proxy ownership to {}",
394 new_proxy_owner
395 );
396 trade_account_registry
397 .registry_proxy
398 .methods()
399 .set_owner(new_identity)
400 .call()
401 .await?;
402 }
403
404 if let Some(new_contract_owner) = params.new_contract_owner {
405 let new_identity = Identity::Address(new_contract_owner);
406 tracing::info!(
407 "Transferring TradeAccountOracle ownership to {}",
408 new_contract_owner
409 );
410 trade_account_oracle_deploy
411 .oracle
412 .methods()
413 .transfer_ownership(new_identity)
414 .call()
415 .await?;
416 tracing::info!(
417 "Transferring TradeAccountRegistry ownership to {}",
418 new_contract_owner
419 );
420 trade_account_registry
421 .registry
422 .methods()
423 .transfer_ownership(new_identity)
424 .call()
425 .await?;
426 tracing::info!(
427 "Transferring OrderBookRegistry ownership to {}",
428 new_contract_owner
429 );
430 order_book_registry
431 .registry
432 .methods()
433 .transfer_ownership(new_identity)
434 .call()
435 .await?;
436 if let Some(blacklist_id) = order_book_blacklist_id {
437 tracing::info!(
438 "Transferring OrderBookBlacklist ownership to {}",
439 new_contract_owner
440 );
441 OrderBookBlacklist::new(blacklist_id, wallet.clone())
442 .methods()
443 .transfer_ownership(new_identity)
444 .call()
445 .await?;
446 }
447 if let Some(whitelist_id) = order_book_whitelist_id {
448 tracing::info!(
449 "Transferring OrderBookWhitelist ownership to {}",
450 new_contract_owner
451 );
452 OrderBookWhitelist::new(whitelist_id, wallet.clone())
453 .methods()
454 .transfer_ownership(new_identity)
455 .call()
456 .await?;
457 }
458 }
459
460 Ok(())
461}
462
463async fn deploy_order_book_blacklist<W>(
468 deployer_wallet: W,
469 deploy_blacklist: bool,
470 order_book_blacklist_id: Option<ContractId>,
471 salt: Salt,
472) -> anyhow::Result<Option<ContractId>>
473where
474 W: Account + ViewOnlyAccount + Clone + 'static,
475{
476 match order_book_blacklist_id {
477 Some(order_book_blacklist_id) => {
478 tracing::info!(
479 "Using existing OrderBookBlacklist: {}",
480 order_book_blacklist_id
481 );
482 Ok(Some(order_book_blacklist_id))
483 }
484 None => {
485 if !deploy_blacklist {
486 return Ok(None);
487 }
488 tracing::info!("Deploying OrderBookBlacklist");
489 let order_book_blacklist = OrderBookDeploy::deploy_order_book_blacklist(
490 &deployer_wallet,
491 &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
492 &OrderBookDeployConfig {
493 salt,
494 ..Default::default()
495 },
496 )
497 .await?;
498 tracing::info!("OrderBookBlacklist: {}", order_book_blacklist.contract_id());
499 Ok(Some(order_book_blacklist.contract_id()))
500 }
501 }
502}
503
504async fn deploy_order_book_whitelist<W>(
505 deployer_wallet: W,
506 deploy_whitelist: bool,
507 order_book_whitelist_id: Option<ContractId>,
508 salt: Salt,
509) -> anyhow::Result<Option<ContractId>>
510where
511 W: Account + ViewOnlyAccount + Clone + 'static,
512{
513 match (order_book_whitelist_id, deploy_whitelist) {
514 (Some(order_book_whitelist_id), false)
515 | (Some(order_book_whitelist_id), true) => {
516 tracing::info!(
517 "Using existing OrderBookWhitelist: {}",
518 order_book_whitelist_id
519 );
520 Ok(Some(order_book_whitelist_id))
521 }
522 (None, false) => Ok(None),
523 (None, true) => {
524 tracing::info!("Deploying OrderBookWhitelist");
525 let trade_account_whitelist = OrderBookDeploy::deploy_order_book_whitelist(
526 &deployer_wallet,
527 &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
528 &OrderBookDeployConfig {
529 salt,
530 ..Default::default()
531 },
532 )
533 .await?;
534 tracing::info!(
535 "OrderBookWhitelist: {}",
536 trade_account_whitelist.contract_id()
537 );
538 Ok(Some(trade_account_whitelist.contract_id()))
539 }
540 }
541}
542
543async fn load_or_recover_trade_account_oracle<W>(
547 deployer_wallet: &W,
548 oracle_id: ContractId,
549) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
550where
551 W: Account + ViewOnlyAccount + Clone + 'static,
552{
553 let oracle = TradingAccountOracle::new(oracle_id, deployer_wallet.clone());
554 let impl_id = oracle
555 .methods()
556 .get_trade_account_impl()
557 .simulate(Execution::state_read_only())
558 .await?
559 .value;
560
561 let blob_id = match impl_id {
562 Some(id) => id,
563 None => {
564 tracing::info!(
565 "Trade account implementation not set on oracle {}, deploying...",
566 oracle_id
567 );
568 let blob = TradeAccountDeploy::trade_account_blob(
569 deployer_wallet,
570 &Default::default(),
571 )
572 .await?;
573 TradeAccountDeploy::deploy_trade_account_blob(
574 deployer_wallet,
575 &DeployConfig::Latest(Default::default()),
576 )
577 .await?;
578 oracle
579 .methods()
580 .set_trade_account_impl(ContractId::from(blob.id))
581 .call()
582 .await?;
583 ContractId::from(blob.id)
584 }
585 };
586
587 let deploy = TradeAccountDeploy {
588 oracle,
589 oracle_id,
590 trade_account_blob_id: blob_id.into(),
591 deployer_wallet: deployer_wallet.clone(),
592 proxy: None,
593 proxy_id: None,
594 };
595 Ok((deploy, blob_id))
596}
597
598async fn deploy_trade_account_oracle<W>(
599 deployer_wallet: W,
600 should_upgrade_bytecode: bool,
601 trade_account_oracle_id: Option<ContractId>,
602 salt: Salt,
603) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
604where
605 W: Account + ViewOnlyAccount + Clone + 'static,
606{
607 let (trade_account_oracle_deploy, mut trade_account_blob_id) =
608 match trade_account_oracle_id {
609 Some(oracle_id) => {
610 load_or_recover_trade_account_oracle(&deployer_wallet, oracle_id).await?
611 }
612 None => {
613 let deploy = TradeAccountDeploy::deploy(
614 &deployer_wallet,
615 &DeployConfig::Latest(TradeAccountDeployConfig {
616 salt,
617 ..Default::default()
618 }),
619 )
620 .await?;
621 let blob_id = deploy
622 .oracle
623 .methods()
624 .get_trade_account_impl()
625 .simulate(Execution::state_read_only())
626 .await?
627 .value
628 .context("Trade account impl should exist after fresh deploy")?;
629 (deploy, blob_id)
630 }
631 };
632 tracing::info!(
633 "TradeAccountOracle: {}",
634 trade_account_oracle_deploy.oracle_id
635 );
636
637 if should_upgrade_bytecode {
638 let trade_account_blob =
639 TradeAccountDeploy::trade_account_blob(&deployer_wallet, &Default::default())
640 .await?;
641 if ContractId::from(trade_account_blob.id) != trade_account_blob_id {
642 tracing::info!(
643 "Update TradeAccountImpl on Oracle from {:?} to new blob {:?}",
644 trade_account_blob_id,
645 ContractId::from(trade_account_blob.id)
646 );
647 TradeAccountDeploy::deploy_trade_account_blob(
648 &deployer_wallet,
649 &DeployConfig::Latest(Default::default()),
650 )
651 .await?;
652 trade_account_oracle_deploy
653 .oracle
654 .methods()
655 .set_trade_account_impl(ContractId::from(trade_account_blob.id))
656 .call()
657 .await?;
658 trade_account_blob_id = ContractId::from(trade_account_blob.id);
659 }
660 }
661
662 Ok((trade_account_oracle_deploy, trade_account_blob_id))
663}
664
665async fn deploy_trade_account_registry<W>(
666 deployer_wallet: W,
667 should_upgrade_bytecode: bool,
668 trade_account_deploy: TradeAccountDeploy<W>,
669 trade_account_registry_id: Option<ContractId>,
670 salt: Salt,
671) -> anyhow::Result<(TradeAccountRegistryManager<W>, ContractId)>
672where
673 W: Account + ViewOnlyAccount + Clone + 'static,
674{
675 let trade_account_oracle_id = trade_account_deploy.oracle_id;
676 let trade_account_registry = match trade_account_registry_id {
677 Some(trade_account_registry_contract_id) => TradeAccountRegistryManager::new(
678 deployer_wallet.clone(),
679 trade_account_registry_contract_id,
680 ),
681 None => {
682 let trade_account_registry_deploy_config = TradeAccountRegistryDeployConfig {
683 salt,
684 ..Default::default()
685 };
686 TradeAccountRegistryManager::deploy(
687 &deployer_wallet,
688 trade_account_oracle_id,
689 &trade_account_registry_deploy_config,
690 )
691 .await?
692 }
693 };
694 tracing::info!(
695 "TradeAccountRegistry: {}",
696 trade_account_registry.contract_id
697 );
698 let mut trade_account_registry_blob_id = match trade_account_registry
699 .registry_proxy
700 .methods()
701 .proxy_target()
702 .simulate(Execution::state_read_only())
703 .await?
704 .value
705 {
706 Some(blob_id) => blob_id,
707 None => {
708 tracing::info!("TradeAccountRegistry proxy target not set, initializing...");
709 trade_account_registry
713 .registry_proxy
714 .methods()
715 .initialize_proxy()
716 .call()
717 .await?;
718 trade_account_registry
719 .registry
720 .methods()
721 .initialize()
722 .call()
723 .await?;
724 trade_account_registry
725 .registry_proxy
726 .methods()
727 .proxy_target()
728 .simulate(Execution::state_read_only())
729 .await?
730 .value
731 .context("TradeAccountRegistry proxy target should be set after initialization")?
732 }
733 };
734
735 if should_upgrade_bytecode {
736 let trade_account_registry_deploy_config =
737 TradeAccountRegistryDeployConfig::default();
738 let trade_account_proxy_blob = TradeAccountRegistryManager::register_proxy_blob(
739 &deployer_wallet,
740 &trade_account_registry_deploy_config,
741 )
742 .await?;
743
744 let trade_account_register_blob = TradeAccountRegistryManager::register_blob(
745 &deployer_wallet,
746 trade_account_oracle_id,
747 trade_account_proxy_blob.id,
748 &trade_account_registry_deploy_config,
749 )
750 .await?;
751
752 if trade_account_registry_blob_id
753 != ContractId::from(trade_account_register_blob.id)
754 {
755 tracing::info!(
756 "Upgrade TradeAccountRegistry blob from {:?} to {:?}",
757 trade_account_registry.contract_id,
758 ContractId::from(trade_account_register_blob.id)
759 );
760 trade_account_registry
761 .upgrade(
762 trade_account_oracle_id,
763 &TradeAccountRegistryDeployConfig::default(),
764 )
765 .await?;
766 trade_account_registry_blob_id = trade_account_register_blob.id.into();
767 }
768 }
769 Ok((trade_account_registry, trade_account_registry_blob_id))
770}
771
772async fn deploy_order_book_registry<W>(
773 deployer_wallet: W,
774 should_upgrade_bytecode: bool,
775 order_book_registry_id: Option<ContractId>,
776 salt: Salt,
777) -> anyhow::Result<(OrderBookRegistryManager<W>, ContractId)>
778where
779 W: Account + ViewOnlyAccount + Clone + 'static,
780{
781 let order_book_registry = match order_book_registry_id {
782 Some(registry_contract_id) => {
783 OrderBookRegistryManager::new(deployer_wallet.clone(), registry_contract_id)
784 }
785 None => {
786 OrderBookRegistryManager::deploy(
787 &deployer_wallet,
788 &OrderBookRegistryDeployConfig {
789 salt,
790 ..Default::default()
791 },
792 )
793 .await?
794 }
795 };
796 tracing::info!("OrderBookRegistry: {}", order_book_registry.contract_id);
797 let mut order_book_registry_blob_id = match order_book_registry
798 .registry_proxy
799 .methods()
800 .proxy_target()
801 .simulate(Execution::state_read_only())
802 .await?
803 .value
804 {
805 Some(blob_id) => blob_id,
806 None => {
807 tracing::info!("OrderBookRegistry proxy target not set, initializing...");
808 order_book_registry
812 .registry_proxy
813 .methods()
814 .initialize_proxy()
815 .call()
816 .await?;
817 order_book_registry
818 .registry
819 .methods()
820 .initialize()
821 .call()
822 .await?;
823 order_book_registry
824 .registry_proxy
825 .methods()
826 .proxy_target()
827 .simulate(Execution::state_read_only())
828 .await?
829 .value
830 .context(
831 "OrderBookRegistry proxy target should be set after initialization",
832 )?
833 }
834 };
835
836 if should_upgrade_bytecode {
837 let order_book_register_deploy_config = OrderBookRegistryDeployConfig::default();
838 let order_book_register_blob = OrderBookRegistryManager::register_blob(
839 &deployer_wallet,
840 &order_book_register_deploy_config,
841 )
842 .await?;
843 if order_book_registry_blob_id != order_book_register_blob.id.into() {
844 tracing::info!(
845 "Upgrade OrderBookRegistry blob from {:?} to {:?}",
846 order_book_registry.contract_id,
847 ContractId::from(order_book_register_blob.id)
848 );
849 order_book_registry
850 .upgrade(&order_book_register_deploy_config)
851 .await?;
852 order_book_registry_blob_id = order_book_register_blob.id.into();
853 }
854 }
855
856 Ok((order_book_registry, order_book_registry_blob_id))
857}
858
859async fn deploy_order_books<W>(
860 deployer_wallet: W,
861 should_upgrade_bytecode: bool,
862 order_book_blacklist_id: Option<ContractId>,
863 order_book_whitelist_id: Option<ContractId>,
864 order_book_registry: OrderBookRegistryManager<W>,
865 order_book_configs: &mut [OrderBookConfig],
866 ownership_options: OwnershipTransferOptions,
867) -> anyhow::Result<Vec<OrderBookConfig>>
868where
869 W: Account + ViewOnlyAccount + Clone + 'static,
870{
871 let mut pairs: Vec<OrderBookConfig> = Vec::with_capacity(order_book_configs.len());
872
873 for order_book_config in order_book_configs.iter_mut() {
874 let pair = deploy_single_order_book(
875 &deployer_wallet,
876 should_upgrade_bytecode,
877 order_book_blacklist_id,
878 order_book_whitelist_id,
879 &order_book_registry,
880 order_book_config,
881 &ownership_options,
882 )
883 .await?;
884 pairs.push(pair);
885 }
886
887 Ok(pairs)
888}
889
890async fn deploy_single_order_book<W>(
891 deployer_wallet: &W,
892 should_upgrade_bytecode: bool,
893 order_book_blacklist_id: Option<ContractId>,
894 order_book_whitelist_id: Option<ContractId>,
895 order_book_registry: &OrderBookRegistryManager<W>,
896 order_book_config: &mut OrderBookConfig,
897 ownership_options: &OwnershipTransferOptions,
898) -> anyhow::Result<OrderBookConfig>
899where
900 W: Account + ViewOnlyAccount + Clone + 'static,
901{
902 let market_symbol = format!(
903 "{}/{}",
904 order_book_config.base.symbol, order_book_config.quote.symbol
905 );
906 let market_id = MarketIdAssets {
907 base_asset: order_book_config.base.asset,
908 quote_asset: order_book_config.quote.asset,
909 };
910 let order_book_configurables = build_order_book_configurables(
911 order_book_config,
912 order_book_blacklist_id,
913 order_book_whitelist_id,
914 deployer_wallet,
915 )?;
916
917 let order_book = load_or_deploy_order_book(
918 deployer_wallet,
919 order_book_registry,
920 &market_id,
921 &market_symbol,
922 &order_book_configurables,
923 order_book_config,
924 )
925 .await?;
926
927 tracing::info!(
928 "[{}] OrderBook: {}",
929 market_symbol,
930 order_book.contract.contract_id()
931 );
932
933 let order_book_blob_id = maybe_upgrade_order_book(
934 deployer_wallet,
935 should_upgrade_bytecode,
936 &order_book,
937 order_book_config,
938 order_book_configurables,
939 &market_symbol,
940 )
941 .await?;
942
943 transfer_order_book_ownership(&order_book, ownership_options, &market_symbol).await?;
944
945 order_book_config.contract_id = Some(order_book.contract.contract_id());
946 order_book_config.blob_id = order_book_blob_id.into();
947
948 Ok(order_book_config.clone())
949}
950
951fn build_order_book_configurables<W: ViewOnlyAccount>(
952 config: &OrderBookConfig,
953 order_book_blacklist_id: Option<ContractId>,
954 order_book_whitelist_id: Option<ContractId>,
955 deployer_wallet: &W,
956) -> anyhow::Result<OrderBookConfigurables> {
957 let price_precision = config
958 .quote
959 .decimals
960 .checked_sub(config.quote.max_precision)
961 .ok_or_else(|| {
962 anyhow::anyhow!(
963 "quote max_precision ({}) exceeds decimals ({})",
964 config.quote.max_precision,
965 config.quote.decimals
966 )
967 })?;
968 let quantity_precision = config
969 .base
970 .decimals
971 .checked_sub(config.base.max_precision)
972 .ok_or_else(|| {
973 anyhow::anyhow!(
974 "base max_precision ({}) exceeds decimals ({})",
975 config.base.max_precision,
976 config.base.decimals
977 )
978 })?;
979
980 Ok(OrderBookConfigurables::default()
981 .with_MIN_ORDER(config.min_order)?
982 .with_TAKER_FEE(config.taker_fee.into())?
983 .with_MAKER_FEE(config.maker_fee.into())?
984 .with_DUST(config.dust)?
985 .with_PRICE_WINDOW(config.price_window as u64)?
986 .with_BASE_DECIMALS(10u64.pow(config.base.decimals as u32))?
987 .with_QUOTE_DECIMALS(10u64.pow(config.quote.decimals as u32))?
988 .with_BASE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
989 config.base.symbol.clone(),
990 )?)?
991 .with_QUOTE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
992 config.quote.symbol.clone(),
993 )?)?
994 .with_PRICE_PRECISION(10u64.pow(price_precision as u32))?
995 .with_QUANTITY_PRECISION(10u64.pow(quantity_precision as u32))?
996 .with_INITIAL_OWNER(o2_tools::order_book_deploy::State::Initialized(
997 Identity::Address(ViewOnlyAccount::address(deployer_wallet)),
998 ))?
999 .with_WHITE_LIST_CONTRACT(order_book_whitelist_id)?
1000 .with_BLACK_LIST_CONTRACT(order_book_blacklist_id)?)
1001}
1002
1003async fn load_or_deploy_order_book<W>(
1004 deployer_wallet: &W,
1005 order_book_registry: &OrderBookRegistryManager<W>,
1006 market_id: &MarketIdAssets,
1007 market_symbol: &str,
1008 order_book_configurables: &OrderBookConfigurables,
1009 order_book_config: &OrderBookConfig,
1010) -> anyhow::Result<OrderBookManager<W>>
1011where
1012 W: Account + ViewOnlyAccount + Clone + 'static,
1013{
1014 let register_contract_id = order_book_registry
1015 .registry
1016 .methods()
1017 .get_order_book(to_registry_market_id(market_id))
1018 .simulate(Execution::state_read_only())
1019 .await?
1020 .value;
1021
1022 match register_contract_id {
1023 Some(contract_id) => {
1024 let order_book_deploy = OrderBookDeploy::new(
1025 deployer_wallet.clone(),
1026 contract_id,
1027 market_id.base_asset,
1028 market_id.quote_asset,
1029 );
1030 let proxy_target = order_book_deploy
1033 .order_book_proxy
1034 .methods()
1035 .proxy_target()
1036 .simulate(Execution::state_read_only())
1037 .await?
1038 .value;
1039 if proxy_target.is_none() {
1040 tracing::info!(
1041 "[{}] Proxy target not set, initializing...",
1042 market_symbol
1043 );
1044 order_book_deploy.initialize().await?;
1045 }
1046 Ok(OrderBookManager::new(
1047 deployer_wallet,
1048 10u64.pow(order_book_config.base.decimals as u32),
1049 10u64.pow(order_book_config.quote.decimals as u32),
1050 &order_book_deploy,
1051 ))
1052 }
1053 None => {
1054 let (order_book_deployment, initialization_required) =
1055 OrderBookDeploy::deploy_without_initialization(
1056 deployer_wallet,
1057 market_id.base_asset,
1058 market_id.quote_asset,
1059 &OrderBookDeployConfig {
1060 order_book_configurables: order_book_configurables.clone(),
1061 salt: Salt::from(*order_book_registry.contract_id),
1062 ..Default::default()
1063 },
1064 )
1065 .await?;
1066
1067 order_book_registry
1068 .register_order_book(
1069 to_registry_market_id(market_id),
1070 order_book_deployment.contract_id,
1071 )
1072 .await?;
1073
1074 if initialization_required {
1075 order_book_deployment.initialize().await?;
1076 }
1077 Ok(OrderBookManager::new(
1078 deployer_wallet,
1079 10u64.pow(order_book_config.base.decimals as u32),
1080 10u64.pow(order_book_config.quote.decimals as u32),
1081 &order_book_deployment,
1082 ))
1083 }
1084 }
1085}
1086
1087async fn maybe_upgrade_order_book<W>(
1088 deployer_wallet: &W,
1089 should_upgrade_bytecode: bool,
1090 order_book: &OrderBookManager<W>,
1091 order_book_config: &OrderBookConfig,
1092 order_book_configurables: OrderBookConfigurables,
1093 market_symbol: &str,
1094) -> anyhow::Result<ContractId>
1095where
1096 W: Account + ViewOnlyAccount + Clone + 'static,
1097{
1098 let mut order_book_blob_id = order_book
1099 .proxy
1100 .methods()
1101 .proxy_target()
1102 .simulate(Execution::state_read_only())
1103 .await?
1104 .value
1105 .context("Order book proxy target should be set after initialization")?;
1106
1107 if should_upgrade_bytecode {
1108 let order_book_deploy_config = OrderBookDeployConfig {
1109 order_book_configurables,
1110 ..Default::default()
1111 };
1112 let order_book_deploy = OrderBookDeploy::new(
1113 deployer_wallet.clone(),
1114 order_book.contract.contract_id(),
1115 order_book_config.base.asset,
1116 order_book_config.quote.asset,
1117 );
1118 let order_book_manager = OrderBookManager::new(
1119 deployer_wallet,
1120 10u64.pow(order_book_config.base.decimals as u32),
1121 10u64.pow(order_book_config.quote.decimals as u32),
1122 &order_book_deploy,
1123 );
1124 let order_book_blob = OrderBookDeploy::order_book_blob(
1125 deployer_wallet,
1126 order_book_config.base.asset,
1127 order_book_config.quote.asset,
1128 &order_book_deploy_config,
1129 )
1130 .await?;
1131
1132 if order_book_blob_id != order_book_blob.id.into() {
1133 tracing::info!(
1134 "[{}] Upgrade OrderBook blob from {:?} to {:?}",
1135 market_symbol,
1136 order_book_blob_id,
1137 ContractId::from(order_book_blob.id)
1138 );
1139 order_book_manager
1140 .upgrade(&order_book_deploy_config)
1141 .await?;
1142 tracing::info!(
1143 "[{}] Emit new configuration event for {}",
1144 market_symbol,
1145 order_book.contract.contract_id()
1146 );
1147 order_book_manager.emit_config().await?;
1148 order_book_blob_id = order_book_blob.id.into();
1149 }
1150 }
1151
1152 Ok(order_book_blob_id)
1153}
1154
1155async fn transfer_order_book_ownership<W>(
1156 order_book: &OrderBookManager<W>,
1157 ownership_options: &OwnershipTransferOptions,
1158 market_symbol: &str,
1159) -> anyhow::Result<()>
1160where
1161 W: Account + ViewOnlyAccount + Clone + 'static,
1162{
1163 if let Some(new_owner) = ownership_options.new_proxy_owner {
1164 let new_identity = Identity::Address(new_owner);
1165 tracing::info!(
1166 "[{}] Transferring OrderBook proxy ownership to {}",
1167 market_symbol,
1168 new_owner
1169 );
1170 order_book
1171 .proxy
1172 .methods()
1173 .set_owner(new_identity)
1174 .call()
1175 .await?;
1176 }
1177
1178 if let Some(new_owner) = ownership_options.new_contract_owner {
1179 let new_identity = Identity::Address(new_owner);
1180 tracing::info!(
1181 "[{}] Transferring OrderBook contract ownership to {}",
1182 market_symbol,
1183 new_owner
1184 );
1185 order_book
1186 .contract
1187 .methods()
1188 .transfer_ownership(new_identity)
1189 .call()
1190 .await?;
1191 }
1192
1193 Ok(())
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1201 fn load_config_empty_path_returns_default() {
1202 let result: MarketsConfigPartial = load_config_from_file("").unwrap();
1203 assert!(result.pairs.is_empty());
1204 }
1205
1206 #[test]
1207 fn load_config_missing_file_errors() {
1208 let result: Result<MarketsConfigPartial, _> =
1209 load_config_from_file("nonexistent_file_12345.json");
1210 assert!(result.is_err());
1211 }
1212
1213 #[test]
1214 fn checked_sub_catches_overflow() {
1215 let decimals: u32 = 6;
1217 let max_precision: u32 = 8; let result = decimals.checked_sub(max_precision);
1220 assert!(
1221 result.is_none(),
1222 "should return None when max_precision > decimals"
1223 );
1224
1225 let result = 9u32.checked_sub(6);
1227 assert_eq!(result, Some(3));
1228 }
1229
1230 #[test]
1231 fn markets_config_partial_default_has_empty_pairs() {
1232 let config = MarketsConfigPartial::default();
1233 assert!(config.pairs.is_empty());
1234 }
1235}