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 TradeAccountRegistryConfigurables,
52 TradeAccountRegistryDeployConfig,
53 TradeAccountRegistryManager,
54 },
55 trial_trade_account_deploy::{
56 DeployConfig as TrialDeployConfig,
57 TrialTradeAccountDeploy,
58 TrialTradeAccountDeployConfig,
59 TrialTradingAccountOracle,
60 },
61};
62use serde_with::serde_as;
63use std::ops::{
64 Deref,
65 DerefMut,
66};
67
68pub use o2_tools::prop_deploy::{
69 ExistingRegistry,
70 PropDeployConfig,
71 PropDeployment,
72};
73
74const ORDERBOOK_MAINTAINER_ROLE: u64 = 1;
79
80pub async fn deploy_prop_system<W>(
86 wallet: &W,
87 config: &PropDeployConfig,
88) -> anyhow::Result<PropDeployment<W>>
89where
90 W: Account + Clone,
91{
92 PropDeployment::deploy(wallet, config).await
93}
94
95fn to_registry_market_id(m: &MarketIdAssets) -> o2_tools::order_book_registry::MarketId {
96 o2_tools::order_book_registry::MarketId {
97 base_asset: m.base_asset,
98 quote_asset: m.quote_asset,
99 }
100}
101
102#[serde_as]
107#[derive(Debug, serde::Serialize, Clone, Default)]
108pub struct MarketsConfigOutput {
109 pub starting_height: u32,
110 #[serde_as(as = "HexDisplayFromStr")]
111 pub trade_account_registry_id: ContractId,
112 #[serde_as(as = "HexDisplayFromStr")]
113 pub trade_account_registry_blob_id: ContractId,
114 #[serde_as(as = "HexDisplayFromStr")]
115 pub trade_account_oracle_id: ContractId,
116 #[serde_as(as = "HexDisplayFromStr")]
117 pub trial_trade_account_oracle_id: ContractId,
118 #[serde_as(as = "HexDisplayFromStr")]
119 pub trade_account_root: ContractId,
120 #[serde_as(as = "HexDisplayFromStr")]
121 pub trade_account_proxy: ContractId,
122 #[serde_as(as = "HexDisplayFromStr")]
123 pub trade_account_blob_id: ContractId,
124 #[serde_as(as = "Option<HexDisplayFromStr>")]
125 pub order_book_whitelist_id: Option<ContractId>,
126 #[serde_as(as = "Option<HexDisplayFromStr>")]
127 pub order_book_blacklist_id: Option<ContractId>,
128 #[serde_as(as = "HexDisplayFromStr")]
129 pub order_book_registry_id: ContractId,
130 #[serde_as(as = "HexDisplayFromStr")]
131 pub order_book_registry_blob_id: ContractId,
132 #[serde_as(as = "Option<HexDisplayFromStr>")]
133 pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
134 #[serde_as(as = "Option<HexDisplayFromStr>")]
135 pub price_feed_id: Option<ContractId>,
136 #[serde_as(as = "Option<HexDisplayFromStr>")]
137 pub margin_pool_id: Option<ContractId>,
138 #[serde_as(as = "Option<HexDisplayFromStr>")]
139 pub margin_oracle_id: Option<ContractId>,
140 #[serde(skip_serializing_if = "Option::is_none")]
147 pub margin: Option<MarginConfig>,
148 pub pairs: Vec<OrderBookConfig>,
149}
150
151#[serde_as]
153#[derive(Debug, Clone, serde::Deserialize)]
154struct OrderBookConfigDeHelper {
155 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
156 blob_id: Option<ContractId>,
157 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
158 contract_id: Option<ContractId>,
159 #[serde_as(as = "serde_with::DisplayFromStr")]
160 taker_fee: u64,
161 #[serde_as(as = "serde_with::DisplayFromStr")]
162 maker_fee: u64,
163 #[serde_as(as = "serde_with::DisplayFromStr")]
164 min_order: u64,
165 #[serde_as(as = "serde_with::DisplayFromStr")]
166 dust: u64,
167 price_window: u8,
168 #[serde(default)]
169 allow_fractional_price: bool,
170 base: AssetConfig,
171 quote: AssetConfig,
172}
173
174impl From<OrderBookConfigDeHelper> for OrderBookConfig {
175 fn from(h: OrderBookConfigDeHelper) -> Self {
176 let ids = MarketIdAssets {
177 base_asset: h.base.asset,
178 quote_asset: h.quote.asset,
179 };
180 let market_id = ids.market_id();
181 OrderBookConfig {
182 contract_id: h.contract_id,
183 blob_id: h.blob_id,
184 market_id,
185 taker_fee: h.taker_fee,
186 maker_fee: h.maker_fee,
187 min_order: h.min_order,
188 dust: h.dust,
189 price_window: h.price_window,
190 allow_fractional_price: h.allow_fractional_price,
191 base: h.base,
192 quote: h.quote,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Default, serde::Serialize)]
198pub struct MarketsConfigPartial {
199 pub starting_height: u32,
200 pub trade_account_registry_id: Option<ContractId>,
201 pub order_book_registry_id: Option<ContractId>,
202 pub trade_account_oracle_id: Option<ContractId>,
203 pub trial_trade_account_oracle_id: Option<ContractId>,
204 pub order_book_whitelist_id: Option<ContractId>,
205 pub order_book_blacklist_id: Option<ContractId>,
206 pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
207 pub pairs: Vec<OrderBookConfig>,
208 pub margin: Option<MarginConfig>,
212}
213
214impl<'de> serde::Deserialize<'de> for MarketsConfigPartial {
215 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216 where
217 D: serde::Deserializer<'de>,
218 {
219 #[derive(serde::Deserialize, Default)]
220 struct Helper {
221 #[serde(default)]
222 starting_height: u32,
223 trade_account_registry_id: Option<ContractId>,
224 order_book_registry_id: Option<ContractId>,
225 trade_account_oracle_id: Option<ContractId>,
226 trial_trade_account_oracle_id: Option<ContractId>,
227 order_book_whitelist_id: Option<ContractId>,
228 order_book_blacklist_id: Option<ContractId>,
229 fast_bridge_asset_registry_proxy_id: Option<ContractId>,
230 #[serde(default)]
231 pairs: Vec<OrderBookConfigDeHelper>,
232 #[serde(default)]
233 margin: Option<MarginConfig>,
234 }
235 let h = Helper::deserialize(deserializer)?;
236 Ok(MarketsConfigPartial {
237 starting_height: h.starting_height,
238 trade_account_registry_id: h.trade_account_registry_id,
239 order_book_registry_id: h.order_book_registry_id,
240 trade_account_oracle_id: h.trade_account_oracle_id,
241 trial_trade_account_oracle_id: h.trial_trade_account_oracle_id,
242 order_book_whitelist_id: h.order_book_whitelist_id,
243 order_book_blacklist_id: h.order_book_blacklist_id,
244 fast_bridge_asset_registry_proxy_id: h.fast_bridge_asset_registry_proxy_id,
245 pairs: h.pairs.into_iter().map(Into::into).collect(),
246 margin: h.margin,
247 })
248 }
249}
250
251#[serde_as]
255#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
256pub struct MarginTierConfig {
257 pub tier_id: u64,
258 #[serde_as(as = "serde_with::DisplayFromStr")]
260 pub line: u64,
261 pub leverage: u64,
262 pub duration: u64,
263 pub maintenance_bps: u64,
264 pub open_buffer_bps: u64,
265 pub liq_price_factor: u64,
266 pub prolong_fee_bps: [u64; 4],
267 pub max_credit_line_bps: u64,
268 pub max_price_age: u64,
269 pub open_fee_bps: u64,
270 pub profit_share_bps: u64,
271 pub price_band_bps: u64,
272 pub markets: Vec<String>,
275}
276
277#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
281pub struct MarginInitialPrice {
282 pub asset: fuels::types::AssetId,
283 pub bid: String,
284 pub ask: String,
285 pub asset_decimals: u8,
286}
287
288#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
291pub struct MarginConfig {
292 #[serde(default)]
296 pub dry_run: bool,
297 pub price_feed_id: Option<ContractId>,
300 pub margin_pool_id: Option<ContractId>,
304 pub platform_payout: Option<String>,
307 #[serde(default)]
311 pub publishers: Vec<String>,
312 pub collateral_asset: Option<fuels::types::AssetId>,
315 pub collateral_decimals: Option<u8>,
318 pub max_tier_books: Option<u64>,
320 pub base_repay_fee_ppm: Option<u64>,
328 #[serde(default)]
331 pub initial_prices: Vec<MarginInitialPrice>,
332 #[serde(default)]
336 pub tiers: Vec<MarginTierConfig>,
337}
338
339#[derive(Debug, Clone, Default)]
340pub struct OwnershipTransferOptions {
341 pub new_proxy_owner: Option<fuels::types::Address>,
342 pub new_contract_owner: Option<fuels::types::Address>,
343 pub revoke_orderbook_maintainers: Vec<fuels::types::Address>,
347 pub new_orderbook_maintainers: Vec<fuels::types::Address>,
350}
351
352#[derive(Debug, Clone)]
354pub struct DeployParams {
355 pub deploy_config: MarketsConfigPartial,
356 pub output: Option<String>,
357 pub deploy_whitelist: bool,
358 pub deploy_blacklist: bool,
359 pub upgrade_bytecode: bool,
360 pub new_proxy_owner: Option<fuels::types::Address>,
361 pub new_contract_owner: Option<fuels::types::Address>,
362 pub trial_cosigner: Option<fuels::types::Address>,
368 pub trial_creator: Option<Identity>,
375 pub margin_tier_only: bool,
385 pub margin_cosigner: Option<fuels::types::Address>,
391 pub margin_liquidator: Option<Identity>,
396 pub revoke_orderbook_maintainers: Vec<fuels::types::Address>,
397 pub new_orderbook_maintainers: Vec<fuels::types::Address>,
398}
399
400pub fn load_config_from_file<T>(config_path: &str) -> anyhow::Result<T>
406where
407 T: Default + serde::de::DeserializeOwned,
408{
409 if config_path.is_empty() {
410 return Ok(T::default());
411 }
412 let current_dir = std::env::current_dir()?;
413 let path = current_dir.join(config_path);
414 tracing::info!("Loading config from {}", path.display());
415 let file = std::fs::File::open(&path)?;
416 let config: T = serde_json::from_reader(file)?;
417 Ok(config)
418}
419
420pub async fn deploy<W>(
429 wallet: W,
430 params: DeployParams,
431) -> anyhow::Result<MarketsConfigOutput>
432where
433 W: Account + ViewOnlyAccount + Clone + 'static,
434{
435 tracing::info!("Starting Fuel o2 Registries and Markets");
436 let mut markets_config_partial = params.deploy_config.clone();
437 let starting_height: BlockHeight = markets_config_partial.starting_height.into();
438 let trade_account_oracle_id = markets_config_partial.trade_account_oracle_id;
439 let trial_trade_account_oracle_id =
440 markets_config_partial.trial_trade_account_oracle_id;
441 let order_book_registry_id = markets_config_partial.order_book_registry_id;
442 let trade_account_registry_id = markets_config_partial.trade_account_registry_id;
443 let fast_bridge_asset_registry_proxy_id =
444 markets_config_partial.fast_bridge_asset_registry_proxy_id;
445
446 let mut salt = Salt::zeroed();
447 salt.deref_mut()[..4].copy_from_slice(&starting_height.deref().to_be_bytes());
448
449 let (trade_account_oracle_deploy, trade_account_blob_id) =
450 deploy_trade_account_oracle(
451 wallet.clone(),
452 params.upgrade_bytecode,
453 trade_account_oracle_id,
454 salt,
455 )
456 .await?;
457 let trial_trade_account_oracle_id = deploy_trial_trade_account_oracle(
461 wallet.clone(),
462 params.upgrade_bytecode,
463 trial_trade_account_oracle_id,
464 params.trial_cosigner,
465 salt,
466 )
467 .await?;
468 let (trade_account_registry, trade_account_registry_blob_id) =
469 deploy_trade_account_registry(
470 wallet.clone(),
471 params.upgrade_bytecode,
472 trade_account_oracle_deploy.clone(),
473 trial_trade_account_oracle_id,
474 trade_account_registry_id,
475 salt,
476 )
477 .await?;
478 let order_book_blacklist_id = deploy_order_book_blacklist(
479 wallet.clone(),
480 params.deploy_blacklist,
481 markets_config_partial.order_book_blacklist_id,
482 salt,
483 )
484 .await?;
485 let order_book_whitelist_id = deploy_order_book_whitelist(
486 wallet.clone(),
487 params.deploy_whitelist,
488 markets_config_partial.order_book_whitelist_id,
489 salt,
490 )
491 .await?;
492 let (order_book_registry, order_book_registry_blob_id) = deploy_order_book_registry(
493 wallet.clone(),
494 params.upgrade_bytecode,
495 order_book_registry_id,
496 salt,
497 )
498 .await?;
499 let pairs = deploy_order_books(
500 wallet.clone(),
501 params.upgrade_bytecode,
502 order_book_blacklist_id,
503 order_book_whitelist_id,
504 order_book_registry.clone(),
505 &mut markets_config_partial.pairs,
506 OwnershipTransferOptions {
507 new_proxy_owner: params.new_proxy_owner,
508 new_contract_owner: params.new_contract_owner,
509 revoke_orderbook_maintainers: params.revoke_orderbook_maintainers.clone(),
510 new_orderbook_maintainers: params.new_orderbook_maintainers.clone(),
511 },
512 )
513 .await?;
514
515 let order_book_registry_id = order_book_registry.contract_id;
516 let trade_account_registry_id = trade_account_registry.contract_id;
517 let trade_account_oracle_id = trade_account_oracle_deploy.oracle_id;
518
519 let trade_account_proxy = trade_account_registry
520 .registry
521 .methods()
522 .default_bytecode()
523 .simulate(Execution::state_read_only())
524 .await?
525 .value
526 .context(
527 "Trade account registry default bytecode should exist after initialization",
528 )?;
529 let trade_account_root = trade_account_registry
530 .registry
531 .methods()
532 .factory_bytecode_root()
533 .simulate(Execution::state_read_only())
534 .await?
535 .value
536 .context("Trade account registry factory bytecode root should exist after initialization")?;
537
538 let margin_ids = match &markets_config_partial.margin {
542 Some(margin) => Some(
543 deploy_margin(
544 &wallet,
545 margin,
546 salt,
547 params.margin_tier_only,
548 trade_account_registry_id,
549 trade_account_oracle_id,
550 trial_trade_account_oracle_id,
551 &pairs,
552 params.margin_cosigner,
553 params.margin_liquidator,
554 )
555 .await?,
556 ),
557 None => None,
558 };
559
560 if let Some(trial_creator) = params.trial_creator {
564 let current_creator = trade_account_registry
565 .get_trial_trade_account_creator()
566 .await?;
567 if current_creator != trial_creator {
568 tracing::info!("Setting trial trade account creator to {trial_creator:?}");
569 trade_account_registry
570 .set_trial_trade_account_creator(trial_creator)
571 .await?;
572 }
573 }
574
575 transfer_ownership(
576 &wallet,
577 ¶ms,
578 &order_book_registry,
579 &trade_account_registry,
580 &trade_account_oracle_deploy,
581 trial_trade_account_oracle_id,
582 order_book_blacklist_id,
583 order_book_whitelist_id,
584 )
585 .await?;
586
587 let deploy_result = MarketsConfigOutput {
588 starting_height: starting_height.into(),
589 trade_account_registry_id,
590 trade_account_registry_blob_id,
591 trade_account_proxy,
592 trade_account_blob_id,
593 trade_account_root: ContractId::from(trade_account_root.0),
594 trade_account_oracle_id,
595 trial_trade_account_oracle_id,
596 order_book_whitelist_id,
597 order_book_blacklist_id,
598 order_book_registry_id,
599 order_book_registry_blob_id,
600 pairs,
601 fast_bridge_asset_registry_proxy_id,
602 price_feed_id: margin_ids.map(|ids| ids.price_feed_id),
603 margin_pool_id: margin_ids.map(|ids| ids.margin_pool_id),
604 margin_oracle_id: margin_ids.and_then(|ids| ids.margin_oracle_id),
605 margin: markets_config_partial.margin.clone().map(|mut margin| {
612 if let (Some(ids), false) = (margin_ids, margin.dry_run) {
613 margin.price_feed_id = Some(ids.price_feed_id);
614 margin.margin_pool_id = Some(ids.margin_pool_id);
615 }
616 margin
617 }),
618 };
619
620 if let Some(output_path) = params.output {
621 let json = serde_json::to_string_pretty(&deploy_result)?;
622 tracing::info!("Deploy result saved to {}", output_path);
623 std::fs::write(output_path, json)?;
624 }
625
626 Ok(deploy_result)
627}
628
629#[derive(Debug, Clone, Copy)]
635pub struct MarginIds {
636 pub price_feed_id: ContractId,
637 pub margin_pool_id: ContractId,
638 pub margin_oracle_id: Option<ContractId>,
639}
640
641#[allow(clippy::too_many_arguments)]
649async fn deploy_margin<W>(
650 wallet: &W,
651 margin: &MarginConfig,
652 salt: fuels::types::Salt,
653 tier_only: bool,
654 trade_account_registry_id: ContractId,
655 trade_account_oracle_id: ContractId,
656 trial_trade_account_oracle_id: ContractId,
657 pairs: &[OrderBookConfig],
658 margin_cosigner: Option<fuels::types::Address>,
659 margin_liquidator: Option<Identity>,
660) -> anyhow::Result<MarginIds>
661where
662 W: Account + ViewOnlyAccount + Clone + 'static,
663{
664 use o2_tools::prop::{
665 PropAccountOracleContract,
666 PropMarginPoolContract,
667 PropPriceFeedMockContract,
668 };
669
670 let collateral_asset = match margin.collateral_asset {
671 Some(collateral_asset) => collateral_asset,
672 None => {
673 pairs
674 .first()
675 .context(
676 "margin.collateral_asset is not set and there are no pairs to \
677 default it from",
678 )?
679 .quote
680 .asset
681 }
682 };
683 let cosigner = margin_cosigner;
688 let platform_payout = margin
689 .platform_payout
690 .as_deref()
691 .map(parse_margin_identity)
692 .transpose()?;
693 let liquidator = margin_liquidator;
694
695 let mut prop_config = PropDeployConfig::new(collateral_asset);
696 prop_config.collateral_decimals = margin.collateral_decimals.context(
699 "margin.collateral_decimals is required - set it to the collateral \
700 asset's decimals in the deploy config",
701 )?;
702 prop_config.max_tier_books = margin.max_tier_books.unwrap_or(80);
703 prop_config.base_repay_fee_ppm = margin.base_repay_fee_ppm.unwrap_or(100);
704 prop_config.cosigner = cosigner;
705 prop_config.platform_payout = platform_payout;
706 prop_config.liquidator = liquidator;
707 prop_config.salt = salt;
708 prop_config.existing_price_feed = margin.price_feed_id;
709 prop_config.existing_pool = margin.margin_pool_id;
713 prop_config.existing_registry = Some(ExistingRegistry {
714 registry_id: trade_account_registry_id,
715 trade_account_oracle_id,
716 trial_trade_account_oracle_id,
717 });
718
719 anyhow::ensure!(
720 !(tier_only && margin.margin_pool_id.is_none()),
721 "--margin-tier-only was requested but the markets config names no \
722 `margin.margin_pool_id`: there is no pool to reconcile tiers against"
723 );
724
725 let (price_feed_id, margin_pool_id, margin_oracle_id, mutate) = if let (
727 true,
728 Some(margin_pool_id),
729 ) =
730 (tier_only, margin.margin_pool_id)
731 {
732 tracing::info!("Margin: tier-only mode against pool {margin_pool_id}");
733 let price_feed_id = match margin.price_feed_id {
734 Some(price_feed_id) => price_feed_id,
735 None => {
736 PropMarginPoolContract::new(margin_pool_id, wallet.clone())
737 .methods()
738 .price_feed()
739 .simulate(Execution::state_read_only())
740 .await
741 .context("read the pool's price feed")?
742 .value
743 }
744 };
745 let margin_oracle_id =
751 TradeAccountRegistryManager::new(wallet.clone(), trade_account_registry_id)
752 .registry
753 .methods()
754 .get_prop_oracle_id()
755 .simulate(Execution::state_read_only())
756 .await
757 .map(|result| result.value)
758 .ok()
759 .filter(|oracle_id| *oracle_id != ContractId::zeroed());
760 (
761 price_feed_id,
762 margin_pool_id,
763 margin_oracle_id,
764 !margin.dry_run,
765 )
766 } else if margin.dry_run {
767 let report = PropDeployment::verify(wallet, &prop_config).await?;
768 tracing::info!(
769 "Margin DRY-RUN: system {} (oracle {}, pool {}, feed {}, registry {})",
770 if report.up_to_date {
771 "up to date — a real run would send no system transaction"
772 } else {
773 "NOT up to date — see the [prop verify] lines above"
774 },
775 report.oracle_id,
776 report.pool_id,
777 report.price_feed_id,
778 report.registry_id,
779 );
780 (
781 report.price_feed_id,
782 report.pool_id,
783 Some(report.oracle_id),
784 false,
785 )
786 } else {
787 let deployment = deploy_prop_system(wallet, &prop_config).await?;
788 tracing::info!(
789 "Margin: oracle {}, pool {}, feed {} (shared registry {} upgraded in place)",
790 deployment.oracle_id,
791 deployment.pool_id,
792 deployment.price_feed_id,
793 deployment.registry_id,
794 );
795 (
796 deployment.price_feed_id,
797 deployment.pool_id,
798 Some(deployment.oracle_id),
799 true,
800 )
801 };
802
803 let pool_deployed = wallet
809 .try_provider()?
810 .contract_exists(&margin_pool_id)
811 .await?;
812 if pool_deployed {
813 let pool = PropMarginPoolContract::new(margin_pool_id, wallet.clone());
814 if let Some(platform_payout) = platform_payout {
815 let current = pool
816 .methods()
817 .platform_payout()
818 .simulate(Execution::state_read_only())
819 .await
820 .context("read the pool's platform payout")?
821 .value;
822 if current != platform_payout {
823 if mutate {
824 tracing::info!(
825 "Margin: platform payout {current:?} -> {platform_payout:?}"
826 );
827 pool.methods()
828 .set_platform_payout(platform_payout)
829 .call()
830 .await?;
831 } else {
832 tracing::info!(
833 "Margin DRY-RUN: would set platform payout \
834 {current:?} -> {platform_payout:?}"
835 );
836 }
837 }
838 }
839 if let Some(liquidator) = liquidator {
840 let current = pool
841 .methods()
842 .liquidator()
843 .simulate(Execution::state_read_only())
844 .await
845 .context("read the pool's liquidator")?
846 .value;
847 if current != liquidator {
848 if mutate {
849 tracing::info!("Margin: liquidator {current:?} -> {liquidator:?}");
850 pool.methods().set_liquidator(liquidator).call().await?;
851 } else {
852 tracing::info!(
853 "Margin DRY-RUN: would set liquidator \
854 {current:?} -> {liquidator:?}"
855 );
856 }
857 }
858 }
859 }
860
861 let margin_oracle_id = match margin_oracle_id {
864 Some(oracle_id) => Some(oracle_id),
865 None if cosigner.is_some() => {
866 let registry = o2_tools::trade_account_registry::TradeAccountRegistry::new(
867 trade_account_registry_id,
868 wallet.clone(),
869 );
870 let oracle_id = registry
871 .methods()
872 .get_prop_oracle_id()
873 .simulate(Execution::state_read_only())
874 .await
875 .context("read the registry's prop account oracle")?
876 .value;
877 (oracle_id != ContractId::zeroed()).then_some(oracle_id)
878 }
879 None => None,
880 };
881
882 if let (Some(cosigner), Some(oracle_id)) = (cosigner, margin_oracle_id)
887 && wallet.try_provider()?.contract_exists(&oracle_id).await?
888 {
889 let oracle = PropAccountOracleContract::new(oracle_id, wallet.clone());
890 let current = oracle
891 .methods()
892 .get_cosigner()
893 .simulate(Execution::state_read_only())
894 .await
895 .context("read the prop account oracle's cosigner")?
896 .value;
897 if current != Some(cosigner) {
898 if mutate {
899 tracing::info!("Margin: cosigner {current:?} -> {cosigner:?}");
900 oracle.methods().set_cosigner(cosigner).call().await?;
901 } else {
902 tracing::info!(
903 "Margin DRY-RUN: would set cosigner \
904 {current:?} -> {cosigner:?}"
905 );
906 }
907 }
908 }
909
910 let price_feed = PropPriceFeedMockContract::new(price_feed_id, wallet.clone());
912 if mutate {
913 for publisher in &margin.publishers {
914 let publisher = parse_margin_identity(publisher)?;
915 tracing::info!("Margin: adding price feed publisher {publisher:?}");
916 price_feed.methods().add_publisher(publisher).call().await?;
917 }
918 } else if !margin.publishers.is_empty() {
919 tracing::info!(
920 "Margin DRY-RUN: would add {} price feed publisher(s)",
921 margin.publishers.len()
922 );
923 }
924 let chain_now = wallet
928 .try_provider()?
929 .latest_block_time()
930 .await?
931 .context("the chain has no latest block time")?
932 .timestamp() as u64;
933 for initial_price in &margin.initial_prices {
934 let has_price = price_feed
935 .methods()
936 .has_price(initial_price.asset)
937 .simulate(Execution::state_read_only())
938 .await?
939 .value;
940 if has_price {
941 continue;
942 }
943 if !mutate {
944 tracing::info!(
945 "Margin DRY-RUN: would publish an initial price for {}",
946 initial_price.asset
947 );
948 continue;
949 }
950 let decimals = price_feed
951 .methods()
952 .get_asset_decimals(initial_price.asset)
953 .simulate(Execution::state_read_only())
954 .await?
955 .value;
956 if decimals.is_none() {
957 price_feed
958 .methods()
959 .set_asset_decimals(initial_price.asset, initial_price.asset_decimals)
960 .call()
961 .await?;
962 }
963 let bid: u128 = initial_price
964 .bid
965 .parse()
966 .map_err(|e| anyhow::anyhow!("invalid initial price bid: {e}"))?;
967 let ask: u128 = initial_price
968 .ask
969 .parse()
970 .map_err(|e| anyhow::anyhow!("invalid initial price ask: {e}"))?;
971 tracing::info!(
972 "Margin: publishing initial price for {}",
973 initial_price.asset
974 );
975 price_feed
976 .methods()
977 .publish_prices(vec![o2_tools::prop::PriceInput {
978 asset: initial_price.asset,
979 bid,
980 ask,
981 timestamp: chain_now,
982 }])
983 .call()
984 .await?;
985 }
986
987 let pool = PropMarginPoolContract::new(margin_pool_id, wallet.clone());
989 let pool_reachable = wallet
990 .try_provider()?
991 .contract_exists(&margin_pool_id)
992 .await?;
993 if pool_reachable {
994 reconcile_margin_tiers(&pool, price_feed_id, pairs, &margin.tiers, !mutate)
995 .await?;
996 } else if !margin.tiers.is_empty() {
997 tracing::info!(
998 "Margin DRY-RUN: pool not deployed yet — all {} tier(s) would publish \
999 their first version",
1000 margin.tiers.len()
1001 );
1002 }
1003
1004 Ok(MarginIds {
1005 price_feed_id,
1006 margin_pool_id,
1007 margin_oracle_id,
1008 })
1009}
1010
1011async fn reconcile_margin_tiers<W>(
1020 pool: &o2_tools::prop::PropMarginPoolContract<W>,
1021 price_feed_id: ContractId,
1022 pairs: &[OrderBookConfig],
1023 tiers: &[MarginTierConfig],
1024 dry_run: bool,
1025) -> anyhow::Result<()>
1026where
1027 W: Account + ViewOnlyAccount + Clone + 'static,
1028{
1029 for tier in tiers {
1030 let books = tier
1031 .markets
1032 .iter()
1033 .map(|market| resolve_tier_market(market, pairs))
1034 .collect::<anyhow::Result<Vec<_>>>()?;
1035 let params = tier_params(tier);
1036 let current_version = pool
1037 .methods()
1038 .current_tier_version(tier.tier_id)
1039 .simulate(Execution::state_read_only())
1040 .await?
1041 .value;
1042
1043 let mut contract_ids = vec![price_feed_id];
1044 contract_ids.extend(books.iter().copied());
1045
1046 let Some(version) = current_version else {
1047 if dry_run {
1048 tracing::info!(
1049 "Margin DRY-RUN: tier {} — would publish its FIRST version \
1050 ({} book(s))",
1051 tier.tier_id,
1052 books.len()
1053 );
1054 continue;
1055 }
1056 let version = pool
1057 .methods()
1058 .publish_tier_version(tier.tier_id, params, books)
1059 .with_contract_ids(&contract_ids)
1060 .call()
1061 .await?
1062 .value;
1063 tracing::info!("Margin: tier {} published version {version}", tier.tier_id);
1064 continue;
1065 };
1066
1067 let live = pool
1068 .methods()
1069 .get_tier(tier.tier_id, version)
1070 .simulate(Execution::state_read_only())
1071 .await?
1072 .value
1073 .with_context(|| {
1074 format!("tier {} version {version} vanished mid-read", tier.tier_id)
1075 })?;
1076 if live != params {
1077 if dry_run {
1078 tracing::info!(
1079 "Margin DRY-RUN: tier {} — params differ from live version \
1080 {version}; would publish a NEW version",
1081 tier.tier_id
1082 );
1083 continue;
1084 }
1085 let new_version = pool
1086 .methods()
1087 .publish_tier_version(tier.tier_id, params, books)
1088 .with_contract_ids(&contract_ids)
1089 .call()
1090 .await?
1091 .value;
1092 tracing::info!(
1093 "Margin: tier {} republished as version {new_version}",
1094 tier.tier_id
1095 );
1096 continue;
1097 }
1098
1099 let live_books = pool
1100 .methods()
1101 .tier_books(tier.tier_id, version)
1102 .simulate(Execution::state_read_only())
1103 .await?
1104 .value;
1105 let missing: Vec<ContractId> = books
1106 .iter()
1107 .copied()
1108 .filter(|book| !live_books.contains(book))
1109 .collect();
1110 let shrunk = live_books
1111 .iter()
1112 .filter(|book| !books.contains(book))
1113 .count();
1114 if shrunk > 0 {
1115 tracing::warn!(
1116 "Margin: tier {} declares {shrunk} fewer book(s) than live version \
1117 {version}; books are append-only — publish a new version to drop \
1118 markets",
1119 tier.tier_id
1120 );
1121 }
1122 if missing.is_empty() {
1123 tracing::info!(
1124 "Margin: tier {} version {version} matches the declared catalogue — \
1125 unchanged",
1126 tier.tier_id
1127 );
1128 continue;
1129 }
1130 if dry_run {
1131 tracing::info!(
1132 "Margin DRY-RUN: tier {} — would add {} book(s) to version {version}",
1133 tier.tier_id,
1134 missing.len()
1135 );
1136 continue;
1137 }
1138 let mut add_contract_ids = vec![price_feed_id];
1139 add_contract_ids.extend(missing.iter().copied());
1140 pool.methods()
1141 .add_books(tier.tier_id, missing.clone())
1142 .with_contract_ids(&add_contract_ids)
1143 .call()
1144 .await?;
1145 tracing::info!(
1146 "Margin: tier {} version {version} gained {} book(s)",
1147 tier.tier_id,
1148 missing.len()
1149 );
1150 }
1151 Ok(())
1152}
1153
1154fn tier_params(tier: &MarginTierConfig) -> o2_tools::prop::TierParams {
1155 o2_tools::prop::TierParams {
1156 line: tier.line,
1157 leverage: tier.leverage,
1158 duration: tier.duration,
1159 maintenance_bps: tier.maintenance_bps,
1160 open_buffer_bps: tier.open_buffer_bps,
1161 liq_price_factor: tier.liq_price_factor,
1162 prolong_fee_bps: tier.prolong_fee_bps,
1163 max_credit_line_bps: tier.max_credit_line_bps,
1164 max_price_age: tier.max_price_age,
1165 open_fee_bps: tier.open_fee_bps,
1166 profit_share_bps: tier.profit_share_bps,
1167 price_band_bps: tier.price_band_bps,
1168 }
1169}
1170
1171fn resolve_tier_market(
1174 market: &str,
1175 pairs: &[OrderBookConfig],
1176) -> anyhow::Result<ContractId> {
1177 let wanted = market.trim();
1178 let wanted_id = wanted
1179 .strip_prefix("0x")
1180 .unwrap_or(wanted)
1181 .to_ascii_lowercase();
1182 let pair = pairs
1183 .iter()
1184 .find(|pair| {
1185 let symbol = format!("{}/{}", pair.base.symbol, pair.quote.symbol);
1186 symbol.eq_ignore_ascii_case(wanted)
1187 || hex::encode(*pair.market_id) == wanted_id
1188 })
1189 .with_context(|| format!("margin tier references unknown market `{market}`"))?;
1190 pair.contract_id.with_context(|| {
1191 format!("margin tier market `{market}` has no deployed order book")
1192 })
1193}
1194
1195fn parse_margin_identity(s: &str) -> anyhow::Result<Identity> {
1197 if let Some(hex_part) = s.strip_prefix("address:") {
1198 Ok(Identity::Address(fuels::types::Address::new(
1199 parse_margin_bytes32(hex_part)?,
1200 )))
1201 } else if let Some(hex_part) = s.strip_prefix("contract:") {
1202 Ok(Identity::ContractId(ContractId::new(parse_margin_bytes32(
1203 hex_part,
1204 )?)))
1205 } else {
1206 anyhow::bail!("expected `address:0x..` or `contract:0x..`, got `{s}`")
1207 }
1208}
1209
1210fn parse_margin_bytes32(s: &str) -> anyhow::Result<[u8; 32]> {
1211 let raw = s.trim().strip_prefix("0x").unwrap_or(s.trim());
1212 let bytes = hex::decode(raw)
1213 .map_err(|e| anyhow::anyhow!("expected 32 hex bytes, got `{s}`: {e}"))?;
1214 bytes
1215 .try_into()
1216 .map_err(|_| anyhow::anyhow!("expected 32 hex bytes, got `{s}`"))
1217}
1218
1219#[allow(clippy::too_many_arguments)]
1224async fn transfer_ownership<W>(
1225 wallet: &W,
1226 params: &DeployParams,
1227 order_book_registry: &OrderBookRegistryManager<W>,
1228 trade_account_registry: &TradeAccountRegistryManager<W>,
1229 trade_account_oracle_deploy: &TradeAccountDeploy<W>,
1230 trial_trade_account_oracle_id: ContractId,
1231 order_book_blacklist_id: Option<ContractId>,
1232 order_book_whitelist_id: Option<ContractId>,
1233) -> anyhow::Result<()>
1234where
1235 W: Account + ViewOnlyAccount + Clone + 'static,
1236{
1237 if let Some(new_proxy_owner) = params.new_proxy_owner {
1238 let new_identity = Identity::Address(new_proxy_owner);
1239 tracing::info!(
1240 "Transferring OrderBookRegistry proxy ownership to {}",
1241 new_proxy_owner
1242 );
1243 order_book_registry
1244 .registry_proxy
1245 .methods()
1246 .set_owner(new_identity)
1247 .call()
1248 .await?;
1249 tracing::info!(
1250 "Transferring TradeAccountRegistry proxy ownership to {}",
1251 new_proxy_owner
1252 );
1253 trade_account_registry
1254 .registry_proxy
1255 .methods()
1256 .set_owner(new_identity)
1257 .call()
1258 .await?;
1259 }
1260
1261 if let Some(new_contract_owner) = params.new_contract_owner {
1262 let new_identity = Identity::Address(new_contract_owner);
1263 tracing::info!(
1264 "Transferring TradeAccountOracle ownership to {}",
1265 new_contract_owner
1266 );
1267 trade_account_oracle_deploy
1268 .oracle
1269 .methods()
1270 .transfer_ownership(new_identity)
1271 .call()
1272 .await?;
1273 tracing::info!(
1274 "Transferring TrialTradeAccountOracle ownership to {}",
1275 new_contract_owner
1276 );
1277 TrialTradingAccountOracle::new(trial_trade_account_oracle_id, wallet.clone())
1278 .methods()
1279 .transfer_ownership(new_identity)
1280 .call()
1281 .await?;
1282 tracing::info!(
1283 "Transferring TradeAccountRegistry ownership to {}",
1284 new_contract_owner
1285 );
1286 trade_account_registry
1287 .registry
1288 .methods()
1289 .transfer_ownership(new_identity)
1290 .call()
1291 .await?;
1292 tracing::info!(
1293 "Transferring OrderBookRegistry ownership to {}",
1294 new_contract_owner
1295 );
1296 order_book_registry
1297 .registry
1298 .methods()
1299 .transfer_ownership(new_identity)
1300 .call()
1301 .await?;
1302 if let Some(blacklist_id) = order_book_blacklist_id {
1303 tracing::info!(
1304 "Transferring OrderBookBlacklist ownership to {}",
1305 new_contract_owner
1306 );
1307 OrderBookBlacklist::new(blacklist_id, wallet.clone())
1308 .methods()
1309 .transfer_ownership(new_identity)
1310 .call()
1311 .await?;
1312 }
1313 if let Some(whitelist_id) = order_book_whitelist_id {
1314 tracing::info!(
1315 "Transferring OrderBookWhitelist ownership to {}",
1316 new_contract_owner
1317 );
1318 OrderBookWhitelist::new(whitelist_id, wallet.clone())
1319 .methods()
1320 .transfer_ownership(new_identity)
1321 .call()
1322 .await?;
1323 }
1324 }
1325
1326 Ok(())
1327}
1328
1329async fn deploy_order_book_blacklist<W>(
1334 deployer_wallet: W,
1335 deploy_blacklist: bool,
1336 order_book_blacklist_id: Option<ContractId>,
1337 salt: Salt,
1338) -> anyhow::Result<Option<ContractId>>
1339where
1340 W: Account + ViewOnlyAccount + Clone + 'static,
1341{
1342 match order_book_blacklist_id {
1343 Some(order_book_blacklist_id) => {
1344 tracing::info!(
1345 "Using existing OrderBookBlacklist: {}",
1346 order_book_blacklist_id
1347 );
1348 Ok(Some(order_book_blacklist_id))
1349 }
1350 None => {
1351 if !deploy_blacklist {
1352 return Ok(None);
1353 }
1354 tracing::info!("Deploying OrderBookBlacklist");
1355 let order_book_blacklist = OrderBookDeploy::deploy_order_book_blacklist(
1356 &deployer_wallet,
1357 &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
1358 &OrderBookDeployConfig {
1359 salt,
1360 ..Default::default()
1361 },
1362 )
1363 .await?;
1364 tracing::info!("OrderBookBlacklist: {}", order_book_blacklist.contract_id());
1365 Ok(Some(order_book_blacklist.contract_id()))
1366 }
1367 }
1368}
1369
1370async fn deploy_order_book_whitelist<W>(
1371 deployer_wallet: W,
1372 deploy_whitelist: bool,
1373 order_book_whitelist_id: Option<ContractId>,
1374 salt: Salt,
1375) -> anyhow::Result<Option<ContractId>>
1376where
1377 W: Account + ViewOnlyAccount + Clone + 'static,
1378{
1379 match (order_book_whitelist_id, deploy_whitelist) {
1380 (Some(order_book_whitelist_id), false)
1381 | (Some(order_book_whitelist_id), true) => {
1382 tracing::info!(
1383 "Using existing OrderBookWhitelist: {}",
1384 order_book_whitelist_id
1385 );
1386 Ok(Some(order_book_whitelist_id))
1387 }
1388 (None, false) => Ok(None),
1389 (None, true) => {
1390 tracing::info!("Deploying OrderBookWhitelist");
1391 let trade_account_whitelist = OrderBookDeploy::deploy_order_book_whitelist(
1392 &deployer_wallet,
1393 &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
1394 &OrderBookDeployConfig {
1395 salt,
1396 ..Default::default()
1397 },
1398 )
1399 .await?;
1400 tracing::info!(
1401 "OrderBookWhitelist: {}",
1402 trade_account_whitelist.contract_id()
1403 );
1404 Ok(Some(trade_account_whitelist.contract_id()))
1405 }
1406 }
1407}
1408
1409async fn load_or_recover_trade_account_oracle<W>(
1413 deployer_wallet: &W,
1414 oracle_id: ContractId,
1415) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
1416where
1417 W: Account + ViewOnlyAccount + Clone + 'static,
1418{
1419 let oracle = TradingAccountOracle::new(oracle_id, deployer_wallet.clone());
1420 let impl_id = oracle
1421 .methods()
1422 .get_trade_account_impl()
1423 .simulate(Execution::state_read_only())
1424 .await?
1425 .value;
1426
1427 let blob_id = match impl_id {
1428 Some(id) => id,
1429 None => {
1430 tracing::info!(
1431 "Trade account implementation not set on oracle {}, deploying...",
1432 oracle_id
1433 );
1434 let blob = TradeAccountDeploy::trade_account_blob(
1435 deployer_wallet,
1436 &Default::default(),
1437 )
1438 .await?;
1439 TradeAccountDeploy::deploy_trade_account_blob(
1440 deployer_wallet,
1441 &DeployConfig::Latest(Default::default()),
1442 )
1443 .await?;
1444 oracle
1445 .methods()
1446 .set_trade_account_impl(ContractId::from(blob.id))
1447 .call()
1448 .await?;
1449 ContractId::from(blob.id)
1450 }
1451 };
1452
1453 let deploy = TradeAccountDeploy {
1454 oracle,
1455 oracle_id,
1456 trade_account_blob_id: blob_id.into(),
1457 deployer_wallet: deployer_wallet.clone(),
1458 proxy: None,
1459 proxy_id: None,
1460 };
1461 Ok((deploy, blob_id))
1462}
1463
1464async fn deploy_trade_account_oracle<W>(
1465 deployer_wallet: W,
1466 should_upgrade_bytecode: bool,
1467 trade_account_oracle_id: Option<ContractId>,
1468 salt: Salt,
1469) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
1470where
1471 W: Account + ViewOnlyAccount + Clone + 'static,
1472{
1473 let (trade_account_oracle_deploy, mut trade_account_blob_id) =
1474 match trade_account_oracle_id {
1475 Some(oracle_id) => {
1476 load_or_recover_trade_account_oracle(&deployer_wallet, oracle_id).await?
1477 }
1478 None => {
1479 let deploy = TradeAccountDeploy::deploy(
1480 &deployer_wallet,
1481 &DeployConfig::Latest(TradeAccountDeployConfig {
1482 salt,
1483 ..Default::default()
1484 }),
1485 )
1486 .await?;
1487 let blob_id = deploy
1488 .oracle
1489 .methods()
1490 .get_trade_account_impl()
1491 .simulate(Execution::state_read_only())
1492 .await?
1493 .value
1494 .context("Trade account impl should exist after fresh deploy")?;
1495 (deploy, blob_id)
1496 }
1497 };
1498 tracing::info!(
1499 "TradeAccountOracle: {}",
1500 trade_account_oracle_deploy.oracle_id
1501 );
1502
1503 if should_upgrade_bytecode {
1504 let trade_account_blob =
1505 TradeAccountDeploy::trade_account_blob(&deployer_wallet, &Default::default())
1506 .await?;
1507 if ContractId::from(trade_account_blob.id) != trade_account_blob_id {
1508 tracing::info!(
1509 "Update TradeAccountImpl on Oracle from {:?} to new blob {:?}",
1510 trade_account_blob_id,
1511 ContractId::from(trade_account_blob.id)
1512 );
1513 TradeAccountDeploy::deploy_trade_account_blob(
1514 &deployer_wallet,
1515 &DeployConfig::Latest(Default::default()),
1516 )
1517 .await?;
1518 trade_account_oracle_deploy
1519 .oracle
1520 .methods()
1521 .set_trade_account_impl(ContractId::from(trade_account_blob.id))
1522 .call()
1523 .await?;
1524 trade_account_blob_id = ContractId::from(trade_account_blob.id);
1525 }
1526 }
1527
1528 Ok((trade_account_oracle_deploy, trade_account_blob_id))
1529}
1530
1531async fn deploy_trial_trade_account_oracle<W>(
1539 deployer_wallet: W,
1540 should_upgrade_bytecode: bool,
1541 trial_trade_account_oracle_id: Option<ContractId>,
1542 trial_cosigner: Option<fuels::types::Address>,
1543 salt: Salt,
1544) -> anyhow::Result<ContractId>
1545where
1546 W: Account + ViewOnlyAccount + Clone + 'static,
1547{
1548 let mut trial_deploy_config = TrialTradeAccountDeployConfig {
1549 salt,
1550 ..Default::default()
1551 };
1552 if let Some(cosigner) = trial_cosigner {
1553 trial_deploy_config = trial_deploy_config.with_cosigner(cosigner);
1554 }
1555 let deploy_config = TrialDeployConfig::Latest(trial_deploy_config);
1556
1557 let oracle_id = match trial_trade_account_oracle_id {
1558 None => {
1559 let trial_deploy =
1560 TrialTradeAccountDeploy::deploy(&deployer_wallet, &deploy_config).await?;
1561 tracing::info!(
1562 "TrialTradeAccountOracle: {} (implementation {:?}, cosigner {:?})",
1563 trial_deploy.oracle_id,
1564 trial_deploy.trial_trade_account_blob_id,
1565 trial_cosigner,
1566 );
1567 trial_deploy.oracle_id
1568 }
1569 Some(oracle_id) => {
1570 let current_trial_impl =
1571 TrialTradingAccountOracle::new(oracle_id, deployer_wallet.clone())
1572 .methods()
1573 .get_trial_account_impl()
1574 .simulate(Execution::state_read_only())
1575 .await?
1576 .value;
1577 if current_trial_impl.is_none()
1578 || should_upgrade_bytecode
1579 || trial_cosigner.is_some()
1580 {
1581 let trial_deploy = TrialTradeAccountDeploy::deploy_to_oracle(
1582 &deployer_wallet,
1583 oracle_id,
1584 &deploy_config,
1585 )
1586 .await?;
1587 tracing::info!(
1588 "Trial implementation {:?} deployed to oracle {} (cosigner {:?})",
1589 trial_deploy.trial_trade_account_blob_id,
1590 oracle_id,
1591 trial_cosigner,
1592 );
1593 }
1594 oracle_id
1595 }
1596 };
1597
1598 Ok(oracle_id)
1599}
1600
1601async fn deploy_trade_account_registry<W>(
1602 deployer_wallet: W,
1603 should_upgrade_bytecode: bool,
1604 trade_account_deploy: TradeAccountDeploy<W>,
1605 trial_trade_account_oracle_id: ContractId,
1606 trade_account_registry_id: Option<ContractId>,
1607 salt: Salt,
1608) -> anyhow::Result<(TradeAccountRegistryManager<W>, ContractId)>
1609where
1610 W: Account + ViewOnlyAccount + Clone + 'static,
1611{
1612 let trade_account_oracle_id = trade_account_deploy.oracle_id;
1613 let trade_account_registry = match trade_account_registry_id {
1614 Some(trade_account_registry_contract_id) => TradeAccountRegistryManager::new(
1615 deployer_wallet.clone(),
1616 trade_account_registry_contract_id,
1617 ),
1618 None => {
1619 let trade_account_registry_deploy_config = TradeAccountRegistryDeployConfig {
1620 salt,
1621 ..Default::default()
1622 };
1623 TradeAccountRegistryManager::deploy(
1624 &deployer_wallet,
1625 trade_account_oracle_id,
1626 trial_trade_account_oracle_id,
1627 &trade_account_registry_deploy_config,
1628 )
1629 .await?
1630 }
1631 };
1632 tracing::info!(
1633 "TradeAccountRegistry: {}",
1634 trade_account_registry.contract_id
1635 );
1636 let mut trade_account_registry_blob_id = match trade_account_registry
1637 .registry_proxy
1638 .methods()
1639 .proxy_target()
1640 .simulate(Execution::state_read_only())
1641 .await?
1642 .value
1643 {
1644 Some(blob_id) => blob_id,
1645 None => {
1646 tracing::info!("TradeAccountRegistry proxy target not set, initializing...");
1647 trade_account_registry
1651 .registry_proxy
1652 .methods()
1653 .initialize_proxy()
1654 .call()
1655 .await?;
1656 trade_account_registry
1657 .registry
1658 .methods()
1659 .initialize()
1660 .call()
1661 .await?;
1662 trade_account_registry
1663 .registry_proxy
1664 .methods()
1665 .proxy_target()
1666 .simulate(Execution::state_read_only())
1667 .await?
1668 .value
1669 .context("TradeAccountRegistry proxy target should be set after initialization")?
1670 }
1671 };
1672
1673 if should_upgrade_bytecode {
1674 let trade_account_registry_deploy_config = TradeAccountRegistryDeployConfig {
1678 registry_config: trade_account_registry
1679 .live_prop_config(TradeAccountRegistryConfigurables::default())
1680 .await?,
1681 ..Default::default()
1682 };
1683 let trade_account_proxy_blob = TradeAccountRegistryManager::register_proxy_blob(
1684 &deployer_wallet,
1685 &trade_account_registry_deploy_config,
1686 )
1687 .await?;
1688 let trial_trade_account_proxy_blob =
1689 TradeAccountRegistryManager::register_trial_proxy_blob(
1690 &deployer_wallet,
1691 &trade_account_registry_deploy_config,
1692 )
1693 .await?;
1694
1695 let trade_account_register_blob = TradeAccountRegistryManager::register_blob(
1696 &deployer_wallet,
1697 trade_account_oracle_id,
1698 trial_trade_account_oracle_id,
1699 trade_account_proxy_blob.id,
1700 trial_trade_account_proxy_blob.id,
1701 &trade_account_registry_deploy_config,
1702 )
1703 .await?;
1704
1705 if trade_account_registry_blob_id
1706 != ContractId::from(trade_account_register_blob.id)
1707 {
1708 tracing::info!(
1709 "Upgrade TradeAccountRegistry blob from {:?} to {:?}",
1710 trade_account_registry.contract_id,
1711 ContractId::from(trade_account_register_blob.id)
1712 );
1713 trade_account_registry
1717 .upgrade(
1718 trade_account_oracle_id,
1719 trial_trade_account_oracle_id,
1720 &trade_account_registry_deploy_config,
1721 )
1722 .await?;
1723 trade_account_registry_blob_id = trade_account_register_blob.id.into();
1724 }
1725 }
1726 Ok((trade_account_registry, trade_account_registry_blob_id))
1727}
1728
1729async fn deploy_order_book_registry<W>(
1730 deployer_wallet: W,
1731 should_upgrade_bytecode: bool,
1732 order_book_registry_id: Option<ContractId>,
1733 salt: Salt,
1734) -> anyhow::Result<(OrderBookRegistryManager<W>, ContractId)>
1735where
1736 W: Account + ViewOnlyAccount + Clone + 'static,
1737{
1738 let order_book_registry = match order_book_registry_id {
1739 Some(registry_contract_id) => {
1740 OrderBookRegistryManager::new(deployer_wallet.clone(), registry_contract_id)
1741 }
1742 None => {
1743 OrderBookRegistryManager::deploy(
1744 &deployer_wallet,
1745 &OrderBookRegistryDeployConfig {
1746 salt,
1747 ..Default::default()
1748 },
1749 )
1750 .await?
1751 }
1752 };
1753 tracing::info!("OrderBookRegistry: {}", order_book_registry.contract_id);
1754 let mut order_book_registry_blob_id = match order_book_registry
1755 .registry_proxy
1756 .methods()
1757 .proxy_target()
1758 .simulate(Execution::state_read_only())
1759 .await?
1760 .value
1761 {
1762 Some(blob_id) => blob_id,
1763 None => {
1764 tracing::info!("OrderBookRegistry proxy target not set, initializing...");
1765 order_book_registry
1769 .registry_proxy
1770 .methods()
1771 .initialize_proxy()
1772 .call()
1773 .await?;
1774 order_book_registry
1775 .registry
1776 .methods()
1777 .initialize()
1778 .call()
1779 .await?;
1780 order_book_registry
1781 .registry_proxy
1782 .methods()
1783 .proxy_target()
1784 .simulate(Execution::state_read_only())
1785 .await?
1786 .value
1787 .context(
1788 "OrderBookRegistry proxy target should be set after initialization",
1789 )?
1790 }
1791 };
1792
1793 if should_upgrade_bytecode {
1794 let order_book_register_deploy_config = OrderBookRegistryDeployConfig::default();
1795 let order_book_register_blob = OrderBookRegistryManager::register_blob(
1796 &deployer_wallet,
1797 &order_book_register_deploy_config,
1798 )
1799 .await?;
1800 if order_book_registry_blob_id != order_book_register_blob.id.into() {
1801 tracing::info!(
1802 "Upgrade OrderBookRegistry blob from {:?} to {:?}",
1803 order_book_registry.contract_id,
1804 ContractId::from(order_book_register_blob.id)
1805 );
1806 order_book_registry
1807 .upgrade(&order_book_register_deploy_config)
1808 .await?;
1809 order_book_registry_blob_id = order_book_register_blob.id.into();
1810 }
1811 }
1812
1813 Ok((order_book_registry, order_book_registry_blob_id))
1814}
1815
1816async fn deploy_order_books<W>(
1817 deployer_wallet: W,
1818 should_upgrade_bytecode: bool,
1819 order_book_blacklist_id: Option<ContractId>,
1820 order_book_whitelist_id: Option<ContractId>,
1821 order_book_registry: OrderBookRegistryManager<W>,
1822 order_book_configs: &mut [OrderBookConfig],
1823 ownership_options: OwnershipTransferOptions,
1824) -> anyhow::Result<Vec<OrderBookConfig>>
1825where
1826 W: Account + ViewOnlyAccount + Clone + 'static,
1827{
1828 let mut pairs: Vec<OrderBookConfig> = Vec::with_capacity(order_book_configs.len());
1829
1830 for order_book_config in order_book_configs.iter_mut() {
1831 let pair = deploy_single_order_book(
1832 &deployer_wallet,
1833 should_upgrade_bytecode,
1834 order_book_blacklist_id,
1835 order_book_whitelist_id,
1836 &order_book_registry,
1837 order_book_config,
1838 &ownership_options,
1839 )
1840 .await?;
1841 pairs.push(pair);
1842 }
1843
1844 Ok(pairs)
1845}
1846
1847async fn deploy_single_order_book<W>(
1848 deployer_wallet: &W,
1849 should_upgrade_bytecode: bool,
1850 order_book_blacklist_id: Option<ContractId>,
1851 order_book_whitelist_id: Option<ContractId>,
1852 order_book_registry: &OrderBookRegistryManager<W>,
1853 order_book_config: &mut OrderBookConfig,
1854 ownership_options: &OwnershipTransferOptions,
1855) -> anyhow::Result<OrderBookConfig>
1856where
1857 W: Account + ViewOnlyAccount + Clone + 'static,
1858{
1859 let market_symbol = format!(
1860 "{}/{}",
1861 order_book_config.base.symbol, order_book_config.quote.symbol
1862 );
1863 let market_id = MarketIdAssets {
1864 base_asset: order_book_config.base.asset,
1865 quote_asset: order_book_config.quote.asset,
1866 };
1867 let order_book_configurables = build_order_book_configurables(
1868 order_book_config,
1869 order_book_blacklist_id,
1870 order_book_whitelist_id,
1871 deployer_wallet,
1872 )?;
1873
1874 let order_book = load_or_deploy_order_book(
1875 deployer_wallet,
1876 order_book_registry,
1877 &market_id,
1878 &market_symbol,
1879 &order_book_configurables,
1880 order_book_config,
1881 )
1882 .await?;
1883
1884 tracing::info!(
1885 "[{}] OrderBook: {}",
1886 market_symbol,
1887 order_book.contract.contract_id()
1888 );
1889
1890 let order_book_blob_id = maybe_upgrade_order_book(
1891 deployer_wallet,
1892 should_upgrade_bytecode,
1893 &order_book,
1894 order_book_config,
1895 order_book_configurables,
1896 &market_symbol,
1897 )
1898 .await?;
1899
1900 set_order_book_maintainer(&order_book, ownership_options, &market_symbol).await?;
1904
1905 transfer_order_book_ownership(&order_book, ownership_options, &market_symbol).await?;
1906
1907 order_book_config.contract_id = Some(order_book.contract.contract_id());
1908 order_book_config.blob_id = order_book_blob_id.into();
1909
1910 Ok(order_book_config.clone())
1911}
1912
1913fn build_order_book_configurables<W: ViewOnlyAccount>(
1914 config: &OrderBookConfig,
1915 order_book_blacklist_id: Option<ContractId>,
1916 order_book_whitelist_id: Option<ContractId>,
1917 deployer_wallet: &W,
1918) -> anyhow::Result<OrderBookConfigurables> {
1919 let price_precision = config
1920 .quote
1921 .decimals
1922 .checked_sub(config.quote.max_precision)
1923 .ok_or_else(|| {
1924 anyhow::anyhow!(
1925 "quote max_precision ({}) exceeds decimals ({})",
1926 config.quote.max_precision,
1927 config.quote.decimals
1928 )
1929 })?;
1930 let quantity_precision = config
1931 .base
1932 .decimals
1933 .checked_sub(config.base.max_precision)
1934 .ok_or_else(|| {
1935 anyhow::anyhow!(
1936 "base max_precision ({}) exceeds decimals ({})",
1937 config.base.max_precision,
1938 config.base.decimals
1939 )
1940 })?;
1941
1942 Ok(OrderBookConfigurables::default()
1943 .with_MIN_ORDER(config.min_order)?
1944 .with_ALLOW_FRACTIONAL_PRICE(config.allow_fractional_price)?
1945 .with_TAKER_FEE(config.taker_fee.into())?
1946 .with_MAKER_FEE(config.maker_fee.into())?
1947 .with_DUST(config.dust)?
1948 .with_PRICE_WINDOW(config.price_window as u64)?
1949 .with_BASE_DECIMALS(10u64.pow(config.base.decimals as u32))?
1950 .with_QUOTE_DECIMALS(10u64.pow(config.quote.decimals as u32))?
1951 .with_BASE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
1952 config.base.symbol.clone(),
1953 )?)?
1954 .with_QUOTE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
1955 config.quote.symbol.clone(),
1956 )?)?
1957 .with_PRICE_PRECISION(10u64.pow(price_precision as u32))?
1958 .with_QUANTITY_PRECISION(10u64.pow(quantity_precision as u32))?
1959 .with_INITIAL_OWNER(o2_tools::order_book_deploy::State::Initialized(
1960 Identity::Address(ViewOnlyAccount::address(deployer_wallet)),
1961 ))?
1962 .with_WHITE_LIST_CONTRACT(order_book_whitelist_id)?
1963 .with_BLACK_LIST_CONTRACT(order_book_blacklist_id)?)
1964}
1965
1966async fn load_or_deploy_order_book<W>(
1967 deployer_wallet: &W,
1968 order_book_registry: &OrderBookRegistryManager<W>,
1969 market_id: &MarketIdAssets,
1970 market_symbol: &str,
1971 order_book_configurables: &OrderBookConfigurables,
1972 order_book_config: &OrderBookConfig,
1973) -> anyhow::Result<OrderBookManager<W>>
1974where
1975 W: Account + ViewOnlyAccount + Clone + 'static,
1976{
1977 let register_contract_id = order_book_registry
1978 .registry
1979 .methods()
1980 .get_order_book(to_registry_market_id(market_id))
1981 .simulate(Execution::state_read_only())
1982 .await?
1983 .value;
1984
1985 match register_contract_id {
1986 Some(contract_id) => {
1987 let order_book_deploy = OrderBookDeploy::new(
1988 deployer_wallet.clone(),
1989 contract_id,
1990 market_id.base_asset,
1991 market_id.quote_asset,
1992 );
1993 let proxy_target = order_book_deploy
1996 .order_book_proxy
1997 .methods()
1998 .proxy_target()
1999 .simulate(Execution::state_read_only())
2000 .await?
2001 .value;
2002 if proxy_target.is_none() {
2003 tracing::info!(
2004 "[{}] Proxy target not set, initializing...",
2005 market_symbol
2006 );
2007 order_book_deploy.initialize().await?;
2008 }
2009 Ok(OrderBookManager::new(
2010 deployer_wallet,
2011 10u64.pow(order_book_config.base.decimals as u32),
2012 10u64.pow(order_book_config.quote.decimals as u32),
2013 &order_book_deploy,
2014 ))
2015 }
2016 None => {
2017 let (order_book_deployment, initialization_required) =
2018 OrderBookDeploy::deploy_without_initialization(
2019 deployer_wallet,
2020 market_id.base_asset,
2021 market_id.quote_asset,
2022 &OrderBookDeployConfig {
2023 order_book_configurables: order_book_configurables.clone(),
2024 salt: Salt::from(*order_book_registry.contract_id),
2025 ..Default::default()
2026 },
2027 )
2028 .await?;
2029
2030 order_book_registry
2031 .register_order_book(
2032 to_registry_market_id(market_id),
2033 order_book_deployment.contract_id,
2034 )
2035 .await?;
2036
2037 if initialization_required {
2038 order_book_deployment.initialize().await?;
2039 }
2040 Ok(OrderBookManager::new(
2041 deployer_wallet,
2042 10u64.pow(order_book_config.base.decimals as u32),
2043 10u64.pow(order_book_config.quote.decimals as u32),
2044 &order_book_deployment,
2045 ))
2046 }
2047 }
2048}
2049
2050async fn maybe_upgrade_order_book<W>(
2051 deployer_wallet: &W,
2052 should_upgrade_bytecode: bool,
2053 order_book: &OrderBookManager<W>,
2054 order_book_config: &OrderBookConfig,
2055 order_book_configurables: OrderBookConfigurables,
2056 market_symbol: &str,
2057) -> anyhow::Result<ContractId>
2058where
2059 W: Account + ViewOnlyAccount + Clone + 'static,
2060{
2061 let mut order_book_blob_id = order_book
2062 .proxy
2063 .methods()
2064 .proxy_target()
2065 .simulate(Execution::state_read_only())
2066 .await?
2067 .value
2068 .context("Order book proxy target should be set after initialization")?;
2069
2070 if should_upgrade_bytecode {
2071 let order_book_deploy_config = OrderBookDeployConfig {
2072 order_book_configurables,
2073 ..Default::default()
2074 };
2075 let order_book_deploy = OrderBookDeploy::new(
2076 deployer_wallet.clone(),
2077 order_book.contract.contract_id(),
2078 order_book_config.base.asset,
2079 order_book_config.quote.asset,
2080 );
2081 let order_book_manager = OrderBookManager::new(
2082 deployer_wallet,
2083 10u64.pow(order_book_config.base.decimals as u32),
2084 10u64.pow(order_book_config.quote.decimals as u32),
2085 &order_book_deploy,
2086 );
2087 let order_book_blob = OrderBookDeploy::order_book_blob(
2088 deployer_wallet,
2089 order_book_config.base.asset,
2090 order_book_config.quote.asset,
2091 &order_book_deploy_config,
2092 )
2093 .await?;
2094
2095 if order_book_blob_id != order_book_blob.id.into() {
2096 tracing::info!(
2097 "[{}] Upgrade OrderBook blob from {:?} to {:?}",
2098 market_symbol,
2099 order_book_blob_id,
2100 ContractId::from(order_book_blob.id)
2101 );
2102 order_book_manager
2103 .upgrade(&order_book_deploy_config)
2104 .await?;
2105 tracing::info!(
2106 "[{}] Emit new configuration event for {}",
2107 market_symbol,
2108 order_book.contract.contract_id()
2109 );
2110 order_book_manager.emit_config().await?;
2111 order_book_blob_id = order_book_blob.id.into();
2112 }
2113 }
2114
2115 Ok(order_book_blob_id)
2116}
2117
2118async fn set_order_book_maintainer<W>(
2119 order_book: &OrderBookManager<W>,
2120 ownership_options: &OwnershipTransferOptions,
2121 market_symbol: &str,
2122) -> anyhow::Result<()>
2123where
2124 W: Account + ViewOnlyAccount + Clone + 'static,
2125{
2126 for account in &ownership_options.revoke_orderbook_maintainers {
2130 tracing::info!(
2131 "[{}] Revoking ORDERBOOK_MAINTAINER_ROLE from {}",
2132 market_symbol,
2133 account
2134 );
2135 order_book
2136 .contract
2137 .methods()
2138 .owner_revoke_role(ORDERBOOK_MAINTAINER_ROLE, Identity::Address(*account))
2139 .call()
2140 .await?;
2141 }
2142
2143 for account in &ownership_options.new_orderbook_maintainers {
2144 tracing::info!(
2145 "[{}] Granting ORDERBOOK_MAINTAINER_ROLE to {}",
2146 market_symbol,
2147 account
2148 );
2149 order_book
2150 .contract
2151 .methods()
2152 .owner_grant_role(ORDERBOOK_MAINTAINER_ROLE, Identity::Address(*account))
2153 .call()
2154 .await?;
2155 }
2156
2157 Ok(())
2158}
2159
2160async fn transfer_order_book_ownership<W>(
2161 order_book: &OrderBookManager<W>,
2162 ownership_options: &OwnershipTransferOptions,
2163 market_symbol: &str,
2164) -> anyhow::Result<()>
2165where
2166 W: Account + ViewOnlyAccount + Clone + 'static,
2167{
2168 if let Some(new_owner) = ownership_options.new_proxy_owner {
2169 let new_identity = Identity::Address(new_owner);
2170 tracing::info!(
2171 "[{}] Transferring OrderBook proxy ownership to {}",
2172 market_symbol,
2173 new_owner
2174 );
2175 order_book
2176 .proxy
2177 .methods()
2178 .set_owner(new_identity)
2179 .call()
2180 .await?;
2181 }
2182
2183 if let Some(new_owner) = ownership_options.new_contract_owner {
2184 let new_identity = Identity::Address(new_owner);
2185 tracing::info!(
2186 "[{}] Transferring OrderBook contract ownership to {}",
2187 market_symbol,
2188 new_owner
2189 );
2190 order_book
2191 .contract
2192 .methods()
2193 .transfer_ownership(new_identity)
2194 .call()
2195 .await?;
2196 }
2197
2198 Ok(())
2199}
2200
2201#[cfg(test)]
2202mod tests {
2203 use super::*;
2204
2205 #[test]
2206 fn load_config_empty_path_returns_default() {
2207 let result: MarketsConfigPartial = load_config_from_file("").unwrap();
2208 assert!(result.pairs.is_empty());
2209 }
2210
2211 #[test]
2212 fn load_config_missing_file_errors() {
2213 let result: Result<MarketsConfigPartial, _> =
2214 load_config_from_file("nonexistent_file_12345.json");
2215 assert!(result.is_err());
2216 }
2217
2218 #[test]
2219 fn checked_sub_catches_overflow() {
2220 let decimals: u32 = 6;
2222 let max_precision: u32 = 8; let result = decimals.checked_sub(max_precision);
2225 assert!(
2226 result.is_none(),
2227 "should return None when max_precision > decimals"
2228 );
2229
2230 let result = 9u32.checked_sub(6);
2232 assert_eq!(result, Some(3));
2233 }
2234
2235 #[test]
2236 fn markets_config_partial_default_has_empty_pairs() {
2237 let config = MarketsConfigPartial::default();
2238 assert!(config.pairs.is_empty());
2239 }
2240}