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