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