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