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