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