Skip to main content

o2_deploy/
lib.rs

1//! Contract deployment logic for the Fuel O2 exchange.
2//!
3//! This crate extracts the core deploy workflow from the `api` package,
4//! making it reusable from both the API binary and the standalone `o2-deploy` CLI.
5
6use fuel_core_client::client::types::primitives::{
7    ContractId,
8    Salt,
9};
10use fuel_core_types::fuel_types::BlockHeight;
11use fuels::{
12    accounts::{
13        Account,
14        ViewOnlyAccount,
15    },
16    prelude::Execution,
17    types::{
18        Identity,
19        SizedAsciiString,
20    },
21};
22use o2_api_types::{
23    domain::book::{
24        AssetConfig,
25        MarketIdAssets,
26        OrderBookConfig,
27    },
28    parse::HexDisplayFromStr,
29};
30use o2_tools::{
31    order_book::OrderBookManager,
32    order_book_deploy::{
33        OrderBookBlacklist,
34        OrderBookConfigurables,
35        OrderBookDeploy,
36        OrderBookDeployConfig,
37        OrderBookWhitelist,
38    },
39    order_book_registry::{
40        OrderBookRegistryDeployConfig,
41        OrderBookRegistryManager,
42    },
43    trade_account_deploy::{
44        DeployConfig,
45        TradeAccountDeploy,
46        TradeAccountDeployConfig,
47    },
48    trade_account_registry::{
49        TradeAccountRegistryDeployConfig,
50        TradeAccountRegistryManager,
51    },
52};
53use serde_with::serde_as;
54use std::ops::{
55    Deref,
56    DerefMut,
57};
58
59fn to_registry_market_id(m: &MarketIdAssets) -> o2_tools::order_book_registry::MarketId {
60    o2_tools::order_book_registry::MarketId {
61        base_asset: m.base_asset,
62        quote_asset: m.quote_asset,
63    }
64}
65
66// ---------------------------------------------------------------------------
67// Types
68// ---------------------------------------------------------------------------
69
70#[serde_as]
71#[derive(Debug, serde::Serialize, Clone, Default)]
72pub struct MarketsConfigOutput {
73    pub starting_height: u32,
74    #[serde_as(as = "HexDisplayFromStr")]
75    pub trade_account_registry_id: ContractId,
76    #[serde_as(as = "HexDisplayFromStr")]
77    pub trade_account_registry_blob_id: ContractId,
78    #[serde_as(as = "HexDisplayFromStr")]
79    pub trade_account_oracle_id: ContractId,
80    #[serde_as(as = "HexDisplayFromStr")]
81    pub trade_account_root: ContractId,
82    #[serde_as(as = "HexDisplayFromStr")]
83    pub trade_account_proxy: ContractId,
84    #[serde_as(as = "HexDisplayFromStr")]
85    pub trade_account_blob_id: ContractId,
86    #[serde_as(as = "Option<HexDisplayFromStr>")]
87    pub order_book_whitelist_id: Option<ContractId>,
88    #[serde_as(as = "Option<HexDisplayFromStr>")]
89    pub order_book_blacklist_id: Option<ContractId>,
90    #[serde_as(as = "HexDisplayFromStr")]
91    pub order_book_registry_id: ContractId,
92    #[serde_as(as = "HexDisplayFromStr")]
93    pub order_book_registry_blob_id: ContractId,
94    #[serde_as(as = "Option<HexDisplayFromStr>")]
95    pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
96    pub pairs: Vec<OrderBookConfig>,
97}
98
99/// Intermediate type for deserializing order book configs with string-encoded numbers.
100#[serde_as]
101#[derive(Debug, Clone, serde::Deserialize)]
102struct OrderBookConfigDeHelper {
103    #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
104    blob_id: Option<ContractId>,
105    #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
106    contract_id: Option<ContractId>,
107    #[serde_as(as = "serde_with::DisplayFromStr")]
108    taker_fee: u64,
109    #[serde_as(as = "serde_with::DisplayFromStr")]
110    maker_fee: u64,
111    #[serde_as(as = "serde_with::DisplayFromStr")]
112    min_order: u64,
113    #[serde_as(as = "serde_with::DisplayFromStr")]
114    dust: u64,
115    price_window: u8,
116    base: AssetConfig,
117    quote: AssetConfig,
118}
119
120impl From<OrderBookConfigDeHelper> for OrderBookConfig {
121    fn from(h: OrderBookConfigDeHelper) -> Self {
122        let ids = MarketIdAssets {
123            base_asset: h.base.asset,
124            quote_asset: h.quote.asset,
125        };
126        let market_id = ids.market_id();
127        OrderBookConfig {
128            contract_id: h.contract_id,
129            blob_id: h.blob_id,
130            market_id,
131            taker_fee: h.taker_fee,
132            maker_fee: h.maker_fee,
133            min_order: h.min_order,
134            dust: h.dust,
135            price_window: h.price_window,
136            base: h.base,
137            quote: h.quote,
138        }
139    }
140}
141
142#[derive(Debug, Clone, Default, serde::Serialize)]
143pub struct MarketsConfigPartial {
144    pub starting_height: u32,
145    pub trade_account_registry_id: Option<ContractId>,
146    pub order_book_registry_id: Option<ContractId>,
147    pub trade_account_oracle_id: Option<ContractId>,
148    pub order_book_whitelist_id: Option<ContractId>,
149    pub order_book_blacklist_id: Option<ContractId>,
150    pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
151    pub pairs: Vec<OrderBookConfig>,
152}
153
154impl<'de> serde::Deserialize<'de> for MarketsConfigPartial {
155    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156    where
157        D: serde::Deserializer<'de>,
158    {
159        #[derive(serde::Deserialize, Default)]
160        struct Helper {
161            #[serde(default)]
162            starting_height: u32,
163            trade_account_registry_id: Option<ContractId>,
164            order_book_registry_id: Option<ContractId>,
165            trade_account_oracle_id: Option<ContractId>,
166            order_book_whitelist_id: Option<ContractId>,
167            order_book_blacklist_id: Option<ContractId>,
168            fast_bridge_asset_registry_proxy_id: Option<ContractId>,
169            #[serde(default)]
170            pairs: Vec<OrderBookConfigDeHelper>,
171        }
172        let h = Helper::deserialize(deserializer)?;
173        Ok(MarketsConfigPartial {
174            starting_height: h.starting_height,
175            trade_account_registry_id: h.trade_account_registry_id,
176            order_book_registry_id: h.order_book_registry_id,
177            trade_account_oracle_id: h.trade_account_oracle_id,
178            order_book_whitelist_id: h.order_book_whitelist_id,
179            order_book_blacklist_id: h.order_book_blacklist_id,
180            fast_bridge_asset_registry_proxy_id: h.fast_bridge_asset_registry_proxy_id,
181            pairs: h.pairs.into_iter().map(Into::into).collect(),
182        })
183    }
184}
185
186#[derive(Debug, Clone, Copy, Default)]
187pub struct OwnershipTransferOptions {
188    pub new_proxy_owner: Option<fuels::types::Address>,
189    pub new_contract_owner: Option<fuels::types::Address>,
190}
191
192/// Parameters for a deploy invocation.
193#[derive(Debug, Clone)]
194pub struct DeployParams {
195    pub deploy_config: MarketsConfigPartial,
196    pub output: Option<String>,
197    pub deploy_whitelist: bool,
198    pub deploy_blacklist: bool,
199    pub upgrade_bytecode: bool,
200    pub new_proxy_owner: Option<fuels::types::Address>,
201    pub new_contract_owner: Option<fuels::types::Address>,
202}
203
204// ---------------------------------------------------------------------------
205// Helpers
206// ---------------------------------------------------------------------------
207
208/// Load a JSON config file, returning `T::default()` when the path is empty.
209pub fn load_config_from_file<T>(config_path: &str) -> anyhow::Result<T>
210where
211    T: Default + serde::de::DeserializeOwned,
212{
213    if config_path.is_empty() {
214        return Ok(T::default());
215    }
216    let current_dir = std::env::current_dir()?;
217    let path = current_dir.join(config_path);
218    tracing::info!("Loading config from {}", path.display());
219    let file = std::fs::File::open(&path)?;
220    let config: T = serde_json::from_reader(file)?;
221    Ok(config)
222}
223
224// ---------------------------------------------------------------------------
225// Core deploy logic
226// ---------------------------------------------------------------------------
227
228/// Deploy (or upgrade) the full set of O2 contracts.
229///
230/// The wallet type `W` must implement `Account + Clone + Signer` (e.g.
231/// `fuels::prelude::WalletUnlocked` or the KMS-backed `O2Wallet` from the API).
232pub async fn deploy<W>(
233    wallet: W,
234    params: DeployParams,
235) -> anyhow::Result<MarketsConfigOutput>
236where
237    W: Account + ViewOnlyAccount + Clone + 'static,
238{
239    tracing::info!("Starting Fuel o2 Registries and Markets");
240    let mut markets_config_partial = params.deploy_config.clone();
241    let starting_height: BlockHeight = markets_config_partial.starting_height.into();
242    let trade_account_oracle_id = markets_config_partial.trade_account_oracle_id;
243    let order_book_registry_id = markets_config_partial.order_book_registry_id;
244    let trade_account_registry_id = markets_config_partial.trade_account_registry_id;
245    let fast_bridge_asset_registry_proxy_id =
246        markets_config_partial.fast_bridge_asset_registry_proxy_id;
247
248    let mut salt = Salt::zeroed();
249    salt.deref_mut()[..4].copy_from_slice(&starting_height.deref().to_be_bytes());
250
251    let (trade_account_oracle_deploy, trade_account_blob_id) =
252        deploy_trade_account_oracle(
253            wallet.clone(),
254            params.upgrade_bytecode,
255            trade_account_oracle_id,
256            salt,
257        )
258        .await?;
259    let (trade_account_registry, trade_account_registry_blob_id) =
260        deploy_trade_account_registry(
261            wallet.clone(),
262            params.upgrade_bytecode,
263            trade_account_oracle_deploy.clone(),
264            trade_account_registry_id,
265            salt,
266        )
267        .await?;
268    let order_book_blacklist_id = deploy_order_book_blacklist(
269        wallet.clone(),
270        params.deploy_blacklist,
271        markets_config_partial.order_book_blacklist_id,
272        salt,
273    )
274    .await?;
275    let order_book_whitelist_id = deploy_order_book_whitelist(
276        wallet.clone(),
277        params.deploy_whitelist,
278        markets_config_partial.order_book_whitelist_id,
279        salt,
280    )
281    .await?;
282    let (order_book_registry, order_book_registry_blob_id) = deploy_order_book_registry(
283        wallet.clone(),
284        params.upgrade_bytecode,
285        order_book_registry_id,
286        salt,
287    )
288    .await?;
289    let pairs = deploy_order_books(
290        wallet.clone(),
291        params.upgrade_bytecode,
292        order_book_blacklist_id,
293        order_book_whitelist_id,
294        order_book_registry.clone(),
295        &mut markets_config_partial.pairs,
296        OwnershipTransferOptions {
297            new_proxy_owner: params.new_proxy_owner,
298            new_contract_owner: params.new_contract_owner,
299        },
300    )
301    .await?;
302
303    let order_book_registry_id = order_book_registry.contract_id;
304    let trade_account_registry_id = trade_account_registry.contract_id;
305    let trade_account_oracle_id = trade_account_oracle_deploy.oracle_id;
306
307    let trade_account_proxy = trade_account_registry
308        .registry
309        .methods()
310        .default_bytecode()
311        .simulate(Execution::state_read_only())
312        .await?
313        .value
314        .expect("Trade account registry factory bytecode root should exist");
315    let trade_account_root = trade_account_registry
316        .registry
317        .methods()
318        .factory_bytecode_root()
319        .simulate(Execution::state_read_only())
320        .await?
321        .value
322        .expect("Trade account registry factory bytecode root should exist");
323
324    if let Some(new_proxy_owner) = params.new_proxy_owner {
325        let new_identity = Identity::Address(new_proxy_owner);
326        tracing::info!(
327            "Transferring OrderBookRegistry proxy ownership to {}",
328            new_proxy_owner
329        );
330        order_book_registry
331            .registry_proxy
332            .methods()
333            .set_owner(new_identity)
334            .call()
335            .await?;
336        tracing::info!(
337            "Transferring TradeAccountRegistry proxy ownership to {}",
338            new_proxy_owner
339        );
340        trade_account_registry
341            .registry_proxy
342            .methods()
343            .set_owner(new_identity)
344            .call()
345            .await?;
346    }
347
348    if let Some(new_contract_owner) = params.new_contract_owner {
349        let new_identity = Identity::Address(new_contract_owner);
350        tracing::info!(
351            "Transferring TradeAccountOracle ownership to {}",
352            new_contract_owner
353        );
354        trade_account_oracle_deploy
355            .oracle
356            .methods()
357            .transfer_ownership(new_identity)
358            .call()
359            .await?;
360        tracing::info!(
361            "Transferring TradeAccountRegistry ownership to {}",
362            new_contract_owner
363        );
364        trade_account_registry
365            .registry
366            .methods()
367            .transfer_ownership(new_identity)
368            .call()
369            .await?;
370        tracing::info!(
371            "Transferring OrderBookRegistry ownership to {}",
372            new_contract_owner
373        );
374        order_book_registry
375            .registry
376            .methods()
377            .transfer_ownership(new_identity)
378            .call()
379            .await?;
380        if let Some(blacklist_id) = order_book_blacklist_id {
381            tracing::info!(
382                "Transferring OrderBookBlacklist ownership to {}",
383                new_contract_owner
384            );
385            OrderBookBlacklist::new(blacklist_id, wallet.clone())
386                .methods()
387                .transfer_ownership(new_identity)
388                .call()
389                .await?;
390        }
391        if let Some(whitelist_id) = order_book_whitelist_id {
392            tracing::info!(
393                "Transferring OrderBookWhitelist ownership to {}",
394                new_contract_owner
395            );
396            OrderBookWhitelist::new(whitelist_id, wallet.clone())
397                .methods()
398                .transfer_ownership(new_identity)
399                .call()
400                .await?;
401        }
402    }
403
404    let deploy_result = MarketsConfigOutput {
405        starting_height: starting_height.into(),
406        trade_account_registry_id,
407        trade_account_registry_blob_id,
408        trade_account_proxy,
409        trade_account_blob_id,
410        trade_account_root: ContractId::from(trade_account_root.0),
411        trade_account_oracle_id,
412        order_book_whitelist_id,
413        order_book_blacklist_id,
414        order_book_registry_id,
415        order_book_registry_blob_id,
416        pairs,
417        fast_bridge_asset_registry_proxy_id,
418    };
419
420    if let Some(output_path) = params.output {
421        let json = serde_json::to_string_pretty(&deploy_result)?;
422        tracing::info!("Deploy result saved to {}", output_path);
423        std::fs::write(output_path, json)?;
424    }
425
426    Ok(deploy_result)
427}
428
429// ---------------------------------------------------------------------------
430// Internal deploy helpers
431// ---------------------------------------------------------------------------
432
433async fn deploy_order_book_blacklist<W>(
434    deployer_wallet: W,
435    deploy_blacklist: bool,
436    order_book_blacklist_id: Option<ContractId>,
437    salt: Salt,
438) -> anyhow::Result<Option<ContractId>>
439where
440    W: Account + ViewOnlyAccount + Clone + 'static,
441{
442    match order_book_blacklist_id {
443        Some(order_book_blacklist_id) => {
444            tracing::info!(
445                "Using existing OrderBookBlacklist: {}",
446                order_book_blacklist_id
447            );
448            Ok(Some(order_book_blacklist_id))
449        }
450        None => {
451            if !deploy_blacklist {
452                return Ok(None);
453            }
454            tracing::info!("Deploying OrderBookBlacklist");
455            let order_book_blacklist = OrderBookDeploy::deploy_order_book_blacklist(
456                &deployer_wallet,
457                &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
458                &OrderBookDeployConfig {
459                    salt,
460                    ..Default::default()
461                },
462            )
463            .await?;
464            tracing::info!("OrderBookBlacklist: {}", order_book_blacklist.contract_id());
465            Ok(Some(order_book_blacklist.contract_id()))
466        }
467    }
468}
469
470async fn deploy_order_book_whitelist<W>(
471    deployer_wallet: W,
472    deploy_whitelist: bool,
473    order_book_whitelist_id: Option<ContractId>,
474    salt: Salt,
475) -> anyhow::Result<Option<ContractId>>
476where
477    W: Account + ViewOnlyAccount + Clone + 'static,
478{
479    match (order_book_whitelist_id, deploy_whitelist) {
480        (Some(order_book_whitelist_id), false)
481        | (Some(order_book_whitelist_id), true) => {
482            tracing::info!(
483                "Using existing OrderBookWhitelist: {}",
484                order_book_whitelist_id
485            );
486            Ok(Some(order_book_whitelist_id))
487        }
488        (None, false) => Ok(None),
489        (None, true) => {
490            tracing::info!("Deploying OrderBookWhitelist");
491            let trade_account_whitelist = OrderBookDeploy::deploy_order_book_whitelist(
492                &deployer_wallet,
493                &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
494                &OrderBookDeployConfig {
495                    salt,
496                    ..Default::default()
497                },
498            )
499            .await?;
500            tracing::info!(
501                "OrderBookWhitelist: {}",
502                trade_account_whitelist.contract_id()
503            );
504            Ok(Some(trade_account_whitelist.contract_id()))
505        }
506    }
507}
508
509async fn deploy_trade_account_oracle<W>(
510    deployer_wallet: W,
511    should_upgrade_bytecode: bool,
512    trade_account_oracle_id: Option<ContractId>,
513    salt: Salt,
514) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
515where
516    W: Account + ViewOnlyAccount + Clone + 'static,
517{
518    let trade_account_oracle_deploy = match trade_account_oracle_id {
519        Some(oracle_id) => {
520            TradeAccountDeploy::from_oracle_id(&deployer_wallet, oracle_id).await?
521        }
522        None => {
523            TradeAccountDeploy::deploy(
524                &deployer_wallet,
525                &DeployConfig::Latest(TradeAccountDeployConfig {
526                    salt,
527                    ..Default::default()
528                }),
529            )
530            .await?
531        }
532    };
533    tracing::info!(
534        "TradeAccountOracle: {}",
535        trade_account_oracle_deploy.oracle_id
536    );
537    let mut trade_account_blob_id = trade_account_oracle_deploy
538        .oracle
539        .methods()
540        .get_trade_account_impl()
541        .simulate(Execution::state_read_only())
542        .await?
543        .value
544        .expect("Trade Account implementaion should exist");
545
546    if should_upgrade_bytecode {
547        let trade_account_blob =
548            TradeAccountDeploy::trade_account_blob(&deployer_wallet, &Default::default())
549                .await?;
550        if ContractId::from(trade_account_blob.id) != trade_account_blob_id {
551            tracing::info!(
552                "Update TradeAccountImpl on Oracle from {:?} to new blob {:?}",
553                trade_account_blob_id,
554                ContractId::from(trade_account_blob.id)
555            );
556            TradeAccountDeploy::deploy_trade_account_blob(
557                &deployer_wallet,
558                &DeployConfig::Latest(Default::default()),
559            )
560            .await?;
561            trade_account_oracle_deploy
562                .oracle
563                .methods()
564                .set_trade_account_impl(ContractId::from(trade_account_blob.id))
565                .call()
566                .await?;
567            trade_account_blob_id = ContractId::from(trade_account_blob.id);
568        }
569    }
570
571    Ok((trade_account_oracle_deploy, trade_account_blob_id))
572}
573
574async fn deploy_trade_account_registry<W>(
575    deployer_wallet: W,
576    should_upgrade_bytecode: bool,
577    trade_account_deploy: TradeAccountDeploy<W>,
578    trade_account_registry_id: Option<ContractId>,
579    salt: Salt,
580) -> anyhow::Result<(TradeAccountRegistryManager<W>, ContractId)>
581where
582    W: Account + ViewOnlyAccount + Clone + 'static,
583{
584    let trade_account_oracle_id = trade_account_deploy.oracle_id;
585    let trade_account_registry = match trade_account_registry_id {
586        Some(trade_account_registry_contract_id) => TradeAccountRegistryManager::new(
587            deployer_wallet.clone(),
588            trade_account_registry_contract_id,
589        ),
590        None => {
591            let trade_account_registry_deploy_config = TradeAccountRegistryDeployConfig {
592                salt,
593                ..Default::default()
594            };
595            TradeAccountRegistryManager::deploy(
596                &deployer_wallet,
597                trade_account_oracle_id,
598                &trade_account_registry_deploy_config,
599            )
600            .await?
601        }
602    };
603    tracing::info!(
604        "TradeAccountRegistry: {}",
605        trade_account_registry.contract_id
606    );
607    let mut trade_account_registry_blob_id = trade_account_registry
608        .registry_proxy
609        .methods()
610        .proxy_target()
611        .simulate(Execution::state_read_only())
612        .await?
613        .value
614        .expect("Current TradeAccountRegistry traget to be set");
615
616    if should_upgrade_bytecode {
617        let trade_account_registry_deploy_config =
618            TradeAccountRegistryDeployConfig::default();
619        let trade_account_proxy_blob = TradeAccountRegistryManager::register_proxy_blob(
620            &deployer_wallet,
621            &trade_account_registry_deploy_config,
622        )
623        .await?;
624
625        let trade_account_register_blob = TradeAccountRegistryManager::register_blob(
626            &deployer_wallet,
627            trade_account_oracle_id,
628            trade_account_proxy_blob.id,
629            &trade_account_registry_deploy_config,
630        )
631        .await?;
632
633        if trade_account_registry_blob_id
634            != ContractId::from(trade_account_register_blob.id)
635        {
636            tracing::info!(
637                "Upgrade TradeAccountRegistry blob from {:?} to {:?}",
638                trade_account_registry.contract_id,
639                ContractId::from(trade_account_register_blob.id)
640            );
641            trade_account_registry
642                .upgrade(
643                    trade_account_oracle_id,
644                    &TradeAccountRegistryDeployConfig::default(),
645                )
646                .await?;
647            trade_account_registry_blob_id = trade_account_register_blob.id.into();
648        }
649    }
650    Ok((trade_account_registry, trade_account_registry_blob_id))
651}
652
653async fn deploy_order_book_registry<W>(
654    deployer_wallet: W,
655    should_upgrade_bytecode: bool,
656    order_book_registry_id: Option<ContractId>,
657    salt: Salt,
658) -> anyhow::Result<(OrderBookRegistryManager<W>, ContractId)>
659where
660    W: Account + ViewOnlyAccount + Clone + 'static,
661{
662    let order_book_registry = match order_book_registry_id {
663        Some(registry_contract_id) => {
664            OrderBookRegistryManager::new(deployer_wallet.clone(), registry_contract_id)
665        }
666        None => {
667            OrderBookRegistryManager::deploy(
668                &deployer_wallet,
669                &OrderBookRegistryDeployConfig {
670                    salt,
671                    ..Default::default()
672                },
673            )
674            .await?
675        }
676    };
677    tracing::info!("OrderBookRegistry: {}", order_book_registry.contract_id);
678    let mut order_book_registry_blob_id = order_book_registry
679        .registry_proxy
680        .methods()
681        .proxy_target()
682        .simulate(Execution::state_read_only())
683        .await?
684        .value
685        .expect("Current OrderBookRegistry traget to be set");
686
687    if should_upgrade_bytecode {
688        let order_book_register_deploy_config = OrderBookRegistryDeployConfig::default();
689        let order_book_register_blob = OrderBookRegistryManager::register_blob(
690            &deployer_wallet,
691            &order_book_register_deploy_config,
692        )
693        .await?;
694        if order_book_registry_blob_id != order_book_register_blob.id.into() {
695            tracing::info!(
696                "Upgrade OrderBookRegistry blob from {:?} to {:?}",
697                order_book_registry.contract_id,
698                ContractId::from(order_book_register_blob.id)
699            );
700            order_book_registry
701                .upgrade(&order_book_register_deploy_config)
702                .await?;
703            order_book_registry_blob_id = order_book_register_blob.id.into();
704        }
705    }
706
707    Ok((order_book_registry, order_book_registry_blob_id))
708}
709
710async fn deploy_order_books<W>(
711    deployer_wallet: W,
712    should_upgrade_bytecode: bool,
713    order_book_blacklist_id: Option<ContractId>,
714    order_book_whitelist_id: Option<ContractId>,
715    order_book_registry: OrderBookRegistryManager<W>,
716    order_book_configs: &mut [OrderBookConfig],
717    ownership_options: OwnershipTransferOptions,
718) -> anyhow::Result<Vec<OrderBookConfig>>
719where
720    W: Account + ViewOnlyAccount + Clone + 'static,
721{
722    let mut pairs: Vec<OrderBookConfig> = Vec::with_capacity(order_book_configs.len());
723    let order_book_registry_id = order_book_registry.contract_id;
724
725    for order_book_config in order_book_configs.iter_mut() {
726        let market_symbol = format!(
727            "{}/{}",
728            order_book_config.base.symbol, order_book_config.quote.symbol
729        );
730        let market_id = MarketIdAssets {
731            base_asset: order_book_config.base.asset,
732            quote_asset: order_book_config.quote.asset,
733        };
734        let price_precision = order_book_config
735            .quote
736            .decimals
737            .checked_sub(order_book_config.quote.max_precision)
738            .ok_or_else(|| {
739                anyhow::anyhow!(
740                    "quote max_precision ({}) exceeds decimals ({})",
741                    order_book_config.quote.max_precision,
742                    order_book_config.quote.decimals
743                )
744            })?;
745        let quantity_precision = order_book_config
746            .base
747            .decimals
748            .checked_sub(order_book_config.base.max_precision)
749            .ok_or_else(|| {
750                anyhow::anyhow!(
751                    "base max_precision ({}) exceeds decimals ({})",
752                    order_book_config.base.max_precision,
753                    order_book_config.base.decimals
754                )
755            })?;
756
757        let order_book_configurables = OrderBookConfigurables::default()
758            .with_MIN_ORDER(order_book_config.min_order)?
759            .with_TAKER_FEE(order_book_config.taker_fee.into())?
760            .with_MAKER_FEE(order_book_config.maker_fee.into())?
761            .with_DUST(order_book_config.dust)?
762            .with_PRICE_WINDOW(order_book_config.price_window as u64)?
763            .with_BASE_DECIMALS(10u64.pow(order_book_config.base.decimals as u32))?
764            .with_QUOTE_DECIMALS(10u64.pow(order_book_config.quote.decimals as u32))?
765            .with_BASE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
766                order_book_config.base.symbol.clone(),
767            )?)?
768            .with_QUOTE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
769                order_book_config.quote.symbol.clone(),
770            )?)?
771            .with_PRICE_PRECISION(10u64.pow(price_precision as u32))?
772            .with_QUANTITY_PRECISION(10u64.pow(quantity_precision as u32))?
773            .with_INITIAL_OWNER(o2_tools::order_book_deploy::State::Initialized(
774                Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
775            ))?
776            .with_WHITE_LIST_CONTRACT(order_book_whitelist_id)?
777            .with_BLACK_LIST_CONTRACT(order_book_blacklist_id)?;
778
779        let register_contract_id = order_book_registry
780            .registry
781            .methods()
782            .get_order_book(to_registry_market_id(&market_id))
783            .simulate(Execution::state_read_only())
784            .await?
785            .value;
786
787        let order_book = match register_contract_id {
788            Some(contract_id) => OrderBookManager::new(
789                &deployer_wallet,
790                10u64.pow(order_book_config.base.decimals as u32),
791                10u64.pow(order_book_config.quote.decimals as u32),
792                &OrderBookDeploy::new(
793                    deployer_wallet.clone(),
794                    contract_id,
795                    market_id.base_asset,
796                    market_id.quote_asset,
797                ),
798            ),
799            None => {
800                let (order_book_deployment, initialization_required) =
801                    OrderBookDeploy::deploy_without_initialization(
802                        &deployer_wallet,
803                        market_id.base_asset,
804                        market_id.quote_asset,
805                        &OrderBookDeployConfig {
806                            order_book_configurables: order_book_configurables.clone(),
807                            salt: Salt::from(*order_book_registry_id),
808                            ..Default::default()
809                        },
810                    )
811                    .await?;
812
813                order_book_registry
814                    .register_order_book(
815                        to_registry_market_id(&market_id),
816                        order_book_deployment.contract_id,
817                    )
818                    .await?;
819
820                if initialization_required {
821                    order_book_deployment.initialize().await?;
822                }
823                OrderBookManager::new(
824                    &deployer_wallet,
825                    10u64.pow(order_book_config.base.decimals as u32),
826                    10u64.pow(order_book_config.quote.decimals as u32),
827                    &order_book_deployment,
828                )
829            }
830        };
831        tracing::info!(
832            "[{}] OrderBook: {}",
833            market_symbol,
834            order_book.contract.contract_id()
835        );
836
837        let mut order_book_blob_id = order_book
838            .proxy
839            .methods()
840            .proxy_target()
841            .simulate(Execution::state_read_only())
842            .await?
843            .value
844            .expect("Order book target should exist");
845
846        if should_upgrade_bytecode {
847            let order_book_deploy_config = OrderBookDeployConfig {
848                order_book_configurables,
849                ..Default::default()
850            };
851            let order_book_deploy = OrderBookDeploy::new(
852                deployer_wallet.clone(),
853                order_book.contract.contract_id(),
854                order_book_config.base.asset,
855                order_book_config.quote.asset,
856            );
857            let order_book_manager = OrderBookManager::new(
858                &deployer_wallet,
859                10u64.pow(order_book_config.base.decimals as u32),
860                10u64.pow(order_book_config.quote.decimals as u32),
861                &order_book_deploy,
862            );
863            let order_book_blob = OrderBookDeploy::order_book_blob(
864                &deployer_wallet,
865                order_book_config.base.asset,
866                order_book_config.quote.asset,
867                &order_book_deploy_config,
868            )
869            .await?;
870
871            if order_book_blob_id != order_book_blob.id.into() {
872                tracing::info!(
873                    "[{}] Upgrade OrderBook blob from {:?} to {:?}",
874                    market_symbol,
875                    order_book_blob_id,
876                    ContractId::from(order_book_blob.id)
877                );
878                order_book_manager
879                    .upgrade(&order_book_deploy_config)
880                    .await?;
881                tracing::info!(
882                    "[{}] Emit new configuration event for {}",
883                    market_symbol,
884                    order_book.contract.contract_id()
885                );
886                order_book_manager.emit_config().await?;
887                order_book_blob_id = order_book_blob.id.into();
888            }
889        }
890
891        if let Some(new_owner) = ownership_options.new_proxy_owner {
892            let new_identity = Identity::Address(new_owner);
893            tracing::info!(
894                "[{}] Transferring OrderBook proxy ownership to {}",
895                market_symbol,
896                new_owner
897            );
898            order_book
899                .proxy
900                .methods()
901                .set_owner(new_identity)
902                .call()
903                .await?;
904        }
905
906        if let Some(new_owner) = ownership_options.new_contract_owner {
907            let new_identity = Identity::Address(new_owner);
908            tracing::info!(
909                "[{}] Transferring OrderBook contract ownership to {}",
910                market_symbol,
911                new_owner
912            );
913            order_book
914                .contract
915                .methods()
916                .transfer_ownership(new_identity)
917                .call()
918                .await?;
919        }
920
921        order_book_config.contract_id = Some(order_book.contract.contract_id());
922        order_book_config.blob_id = order_book_blob_id.into();
923
924        pairs.push(order_book_config.clone());
925    }
926
927    Ok(pairs)
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933
934    #[test]
935    fn load_config_empty_path_returns_default() {
936        let result: MarketsConfigPartial = load_config_from_file("").unwrap();
937        assert!(result.pairs.is_empty());
938    }
939
940    #[test]
941    fn load_config_missing_file_errors() {
942        let result: Result<MarketsConfigPartial, _> =
943            load_config_from_file("nonexistent_file_12345.json");
944        assert!(result.is_err());
945    }
946
947    #[test]
948    fn checked_sub_catches_overflow() {
949        // Validates that our checked_sub pattern works correctly
950        let decimals: u32 = 6;
951        let max_precision: u32 = 8; // greater than decimals
952
953        let result = decimals.checked_sub(max_precision);
954        assert!(
955            result.is_none(),
956            "should return None when max_precision > decimals"
957        );
958
959        // Normal case
960        let result = 9u32.checked_sub(6);
961        assert_eq!(result, Some(3));
962    }
963
964    #[test]
965    fn markets_config_partial_default_has_empty_pairs() {
966        let config = MarketsConfigPartial::default();
967        assert!(config.pairs.is_empty());
968    }
969}