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 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};
55use serde_with::serde_as;
56use std::ops::{
57    Deref,
58    DerefMut,
59};
60
61fn to_registry_market_id(m: &MarketIdAssets) -> o2_tools::order_book_registry::MarketId {
62    o2_tools::order_book_registry::MarketId {
63        base_asset: m.base_asset,
64        quote_asset: m.quote_asset,
65    }
66}
67
68// ---------------------------------------------------------------------------
69// Types
70// ---------------------------------------------------------------------------
71
72#[serde_as]
73#[derive(Debug, serde::Serialize, Clone, Default)]
74pub struct MarketsConfigOutput {
75    pub starting_height: u32,
76    #[serde_as(as = "HexDisplayFromStr")]
77    pub trade_account_registry_id: ContractId,
78    #[serde_as(as = "HexDisplayFromStr")]
79    pub trade_account_registry_blob_id: ContractId,
80    #[serde_as(as = "HexDisplayFromStr")]
81    pub trade_account_oracle_id: ContractId,
82    #[serde_as(as = "HexDisplayFromStr")]
83    pub trade_account_root: ContractId,
84    #[serde_as(as = "HexDisplayFromStr")]
85    pub trade_account_proxy: ContractId,
86    #[serde_as(as = "HexDisplayFromStr")]
87    pub trade_account_blob_id: ContractId,
88    #[serde_as(as = "Option<HexDisplayFromStr>")]
89    pub order_book_whitelist_id: Option<ContractId>,
90    #[serde_as(as = "Option<HexDisplayFromStr>")]
91    pub order_book_blacklist_id: Option<ContractId>,
92    #[serde_as(as = "HexDisplayFromStr")]
93    pub order_book_registry_id: ContractId,
94    #[serde_as(as = "HexDisplayFromStr")]
95    pub order_book_registry_blob_id: ContractId,
96    #[serde_as(as = "Option<HexDisplayFromStr>")]
97    pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
98    pub pairs: Vec<OrderBookConfig>,
99}
100
101/// Intermediate type for deserializing order book configs with string-encoded numbers.
102#[serde_as]
103#[derive(Debug, Clone, serde::Deserialize)]
104struct OrderBookConfigDeHelper {
105    #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
106    blob_id: Option<ContractId>,
107    #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
108    contract_id: Option<ContractId>,
109    #[serde_as(as = "serde_with::DisplayFromStr")]
110    taker_fee: u64,
111    #[serde_as(as = "serde_with::DisplayFromStr")]
112    maker_fee: u64,
113    #[serde_as(as = "serde_with::DisplayFromStr")]
114    min_order: u64,
115    #[serde_as(as = "serde_with::DisplayFromStr")]
116    dust: u64,
117    price_window: u8,
118    base: AssetConfig,
119    quote: AssetConfig,
120}
121
122impl From<OrderBookConfigDeHelper> for OrderBookConfig {
123    fn from(h: OrderBookConfigDeHelper) -> Self {
124        let ids = MarketIdAssets {
125            base_asset: h.base.asset,
126            quote_asset: h.quote.asset,
127        };
128        let market_id = ids.market_id();
129        OrderBookConfig {
130            contract_id: h.contract_id,
131            blob_id: h.blob_id,
132            market_id,
133            taker_fee: h.taker_fee,
134            maker_fee: h.maker_fee,
135            min_order: h.min_order,
136            dust: h.dust,
137            price_window: h.price_window,
138            base: h.base,
139            quote: h.quote,
140        }
141    }
142}
143
144#[derive(Debug, Clone, Default, serde::Serialize)]
145pub struct MarketsConfigPartial {
146    pub starting_height: u32,
147    pub trade_account_registry_id: Option<ContractId>,
148    pub order_book_registry_id: Option<ContractId>,
149    pub trade_account_oracle_id: Option<ContractId>,
150    pub order_book_whitelist_id: Option<ContractId>,
151    pub order_book_blacklist_id: Option<ContractId>,
152    pub fast_bridge_asset_registry_proxy_id: Option<ContractId>,
153    pub pairs: Vec<OrderBookConfig>,
154}
155
156impl<'de> serde::Deserialize<'de> for MarketsConfigPartial {
157    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158    where
159        D: serde::Deserializer<'de>,
160    {
161        #[derive(serde::Deserialize, Default)]
162        struct Helper {
163            #[serde(default)]
164            starting_height: u32,
165            trade_account_registry_id: Option<ContractId>,
166            order_book_registry_id: Option<ContractId>,
167            trade_account_oracle_id: Option<ContractId>,
168            order_book_whitelist_id: Option<ContractId>,
169            order_book_blacklist_id: Option<ContractId>,
170            fast_bridge_asset_registry_proxy_id: Option<ContractId>,
171            #[serde(default)]
172            pairs: Vec<OrderBookConfigDeHelper>,
173        }
174        let h = Helper::deserialize(deserializer)?;
175        Ok(MarketsConfigPartial {
176            starting_height: h.starting_height,
177            trade_account_registry_id: h.trade_account_registry_id,
178            order_book_registry_id: h.order_book_registry_id,
179            trade_account_oracle_id: h.trade_account_oracle_id,
180            order_book_whitelist_id: h.order_book_whitelist_id,
181            order_book_blacklist_id: h.order_book_blacklist_id,
182            fast_bridge_asset_registry_proxy_id: h.fast_bridge_asset_registry_proxy_id,
183            pairs: h.pairs.into_iter().map(Into::into).collect(),
184        })
185    }
186}
187
188#[derive(Debug, Clone, Copy, Default)]
189pub struct OwnershipTransferOptions {
190    pub new_proxy_owner: Option<fuels::types::Address>,
191    pub new_contract_owner: Option<fuels::types::Address>,
192}
193
194/// Parameters for a deploy invocation.
195#[derive(Debug, Clone)]
196pub struct DeployParams {
197    pub deploy_config: MarketsConfigPartial,
198    pub output: Option<String>,
199    pub deploy_whitelist: bool,
200    pub deploy_blacklist: bool,
201    pub upgrade_bytecode: bool,
202    pub new_proxy_owner: Option<fuels::types::Address>,
203    pub new_contract_owner: Option<fuels::types::Address>,
204}
205
206// ---------------------------------------------------------------------------
207// Helpers
208// ---------------------------------------------------------------------------
209
210/// Load a JSON config file, returning `T::default()` when the path is empty.
211pub fn load_config_from_file<T>(config_path: &str) -> anyhow::Result<T>
212where
213    T: Default + serde::de::DeserializeOwned,
214{
215    if config_path.is_empty() {
216        return Ok(T::default());
217    }
218    let current_dir = std::env::current_dir()?;
219    let path = current_dir.join(config_path);
220    tracing::info!("Loading config from {}", path.display());
221    let file = std::fs::File::open(&path)?;
222    let config: T = serde_json::from_reader(file)?;
223    Ok(config)
224}
225
226// ---------------------------------------------------------------------------
227// Core deploy logic
228// ---------------------------------------------------------------------------
229
230/// Deploy (or upgrade) the full set of O2 contracts.
231///
232/// The wallet type `W` must implement `Account + Clone + Signer` (e.g.
233/// `fuels::prelude::WalletUnlocked` or the KMS-backed `O2Wallet` from the API).
234pub async fn deploy<W>(
235    wallet: W,
236    params: DeployParams,
237) -> anyhow::Result<MarketsConfigOutput>
238where
239    W: Account + ViewOnlyAccount + Clone + 'static,
240{
241    tracing::info!("Starting Fuel o2 Registries and Markets");
242    let mut markets_config_partial = params.deploy_config.clone();
243    let starting_height: BlockHeight = markets_config_partial.starting_height.into();
244    let trade_account_oracle_id = markets_config_partial.trade_account_oracle_id;
245    let order_book_registry_id = markets_config_partial.order_book_registry_id;
246    let trade_account_registry_id = markets_config_partial.trade_account_registry_id;
247    let fast_bridge_asset_registry_proxy_id =
248        markets_config_partial.fast_bridge_asset_registry_proxy_id;
249
250    let mut salt = Salt::zeroed();
251    salt.deref_mut()[..4].copy_from_slice(&starting_height.deref().to_be_bytes());
252
253    let (trade_account_oracle_deploy, trade_account_blob_id) =
254        deploy_trade_account_oracle(
255            wallet.clone(),
256            params.upgrade_bytecode,
257            trade_account_oracle_id,
258            salt,
259        )
260        .await?;
261    let (trade_account_registry, trade_account_registry_blob_id) =
262        deploy_trade_account_registry(
263            wallet.clone(),
264            params.upgrade_bytecode,
265            trade_account_oracle_deploy.clone(),
266            trade_account_registry_id,
267            salt,
268        )
269        .await?;
270    let order_book_blacklist_id = deploy_order_book_blacklist(
271        wallet.clone(),
272        params.deploy_blacklist,
273        markets_config_partial.order_book_blacklist_id,
274        salt,
275    )
276    .await?;
277    let order_book_whitelist_id = deploy_order_book_whitelist(
278        wallet.clone(),
279        params.deploy_whitelist,
280        markets_config_partial.order_book_whitelist_id,
281        salt,
282    )
283    .await?;
284    let (order_book_registry, order_book_registry_blob_id) = deploy_order_book_registry(
285        wallet.clone(),
286        params.upgrade_bytecode,
287        order_book_registry_id,
288        salt,
289    )
290    .await?;
291    let pairs = deploy_order_books(
292        wallet.clone(),
293        params.upgrade_bytecode,
294        order_book_blacklist_id,
295        order_book_whitelist_id,
296        order_book_registry.clone(),
297        &mut markets_config_partial.pairs,
298        OwnershipTransferOptions {
299            new_proxy_owner: params.new_proxy_owner,
300            new_contract_owner: params.new_contract_owner,
301        },
302    )
303    .await?;
304
305    let order_book_registry_id = order_book_registry.contract_id;
306    let trade_account_registry_id = trade_account_registry.contract_id;
307    let trade_account_oracle_id = trade_account_oracle_deploy.oracle_id;
308
309    let trade_account_proxy = trade_account_registry
310        .registry
311        .methods()
312        .default_bytecode()
313        .simulate(Execution::state_read_only())
314        .await?
315        .value
316        .context(
317            "Trade account registry default bytecode should exist after initialization",
318        )?;
319    let trade_account_root = trade_account_registry
320        .registry
321        .methods()
322        .factory_bytecode_root()
323        .simulate(Execution::state_read_only())
324        .await?
325        .value
326        .context("Trade account registry factory bytecode root should exist after initialization")?;
327
328    transfer_ownership(
329        &wallet,
330        &params,
331        &order_book_registry,
332        &trade_account_registry,
333        &trade_account_oracle_deploy,
334        order_book_blacklist_id,
335        order_book_whitelist_id,
336    )
337    .await?;
338
339    let deploy_result = MarketsConfigOutput {
340        starting_height: starting_height.into(),
341        trade_account_registry_id,
342        trade_account_registry_blob_id,
343        trade_account_proxy,
344        trade_account_blob_id,
345        trade_account_root: ContractId::from(trade_account_root.0),
346        trade_account_oracle_id,
347        order_book_whitelist_id,
348        order_book_blacklist_id,
349        order_book_registry_id,
350        order_book_registry_blob_id,
351        pairs,
352        fast_bridge_asset_registry_proxy_id,
353    };
354
355    if let Some(output_path) = params.output {
356        let json = serde_json::to_string_pretty(&deploy_result)?;
357        tracing::info!("Deploy result saved to {}", output_path);
358        std::fs::write(output_path, json)?;
359    }
360
361    Ok(deploy_result)
362}
363
364// ---------------------------------------------------------------------------
365// Ownership transfer
366// ---------------------------------------------------------------------------
367
368async fn transfer_ownership<W>(
369    wallet: &W,
370    params: &DeployParams,
371    order_book_registry: &OrderBookRegistryManager<W>,
372    trade_account_registry: &TradeAccountRegistryManager<W>,
373    trade_account_oracle_deploy: &TradeAccountDeploy<W>,
374    order_book_blacklist_id: Option<ContractId>,
375    order_book_whitelist_id: Option<ContractId>,
376) -> anyhow::Result<()>
377where
378    W: Account + ViewOnlyAccount + Clone + 'static,
379{
380    if let Some(new_proxy_owner) = params.new_proxy_owner {
381        let new_identity = Identity::Address(new_proxy_owner);
382        tracing::info!(
383            "Transferring OrderBookRegistry proxy ownership to {}",
384            new_proxy_owner
385        );
386        order_book_registry
387            .registry_proxy
388            .methods()
389            .set_owner(new_identity)
390            .call()
391            .await?;
392        tracing::info!(
393            "Transferring TradeAccountRegistry proxy ownership to {}",
394            new_proxy_owner
395        );
396        trade_account_registry
397            .registry_proxy
398            .methods()
399            .set_owner(new_identity)
400            .call()
401            .await?;
402    }
403
404    if let Some(new_contract_owner) = params.new_contract_owner {
405        let new_identity = Identity::Address(new_contract_owner);
406        tracing::info!(
407            "Transferring TradeAccountOracle ownership to {}",
408            new_contract_owner
409        );
410        trade_account_oracle_deploy
411            .oracle
412            .methods()
413            .transfer_ownership(new_identity)
414            .call()
415            .await?;
416        tracing::info!(
417            "Transferring TradeAccountRegistry ownership to {}",
418            new_contract_owner
419        );
420        trade_account_registry
421            .registry
422            .methods()
423            .transfer_ownership(new_identity)
424            .call()
425            .await?;
426        tracing::info!(
427            "Transferring OrderBookRegistry ownership to {}",
428            new_contract_owner
429        );
430        order_book_registry
431            .registry
432            .methods()
433            .transfer_ownership(new_identity)
434            .call()
435            .await?;
436        if let Some(blacklist_id) = order_book_blacklist_id {
437            tracing::info!(
438                "Transferring OrderBookBlacklist ownership to {}",
439                new_contract_owner
440            );
441            OrderBookBlacklist::new(blacklist_id, wallet.clone())
442                .methods()
443                .transfer_ownership(new_identity)
444                .call()
445                .await?;
446        }
447        if let Some(whitelist_id) = order_book_whitelist_id {
448            tracing::info!(
449                "Transferring OrderBookWhitelist ownership to {}",
450                new_contract_owner
451            );
452            OrderBookWhitelist::new(whitelist_id, wallet.clone())
453                .methods()
454                .transfer_ownership(new_identity)
455                .call()
456                .await?;
457        }
458    }
459
460    Ok(())
461}
462
463// ---------------------------------------------------------------------------
464// Internal deploy helpers
465// ---------------------------------------------------------------------------
466
467async fn deploy_order_book_blacklist<W>(
468    deployer_wallet: W,
469    deploy_blacklist: bool,
470    order_book_blacklist_id: Option<ContractId>,
471    salt: Salt,
472) -> anyhow::Result<Option<ContractId>>
473where
474    W: Account + ViewOnlyAccount + Clone + 'static,
475{
476    match order_book_blacklist_id {
477        Some(order_book_blacklist_id) => {
478            tracing::info!(
479                "Using existing OrderBookBlacklist: {}",
480                order_book_blacklist_id
481            );
482            Ok(Some(order_book_blacklist_id))
483        }
484        None => {
485            if !deploy_blacklist {
486                return Ok(None);
487            }
488            tracing::info!("Deploying OrderBookBlacklist");
489            let order_book_blacklist = OrderBookDeploy::deploy_order_book_blacklist(
490                &deployer_wallet,
491                &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
492                &OrderBookDeployConfig {
493                    salt,
494                    ..Default::default()
495                },
496            )
497            .await?;
498            tracing::info!("OrderBookBlacklist: {}", order_book_blacklist.contract_id());
499            Ok(Some(order_book_blacklist.contract_id()))
500        }
501    }
502}
503
504async fn deploy_order_book_whitelist<W>(
505    deployer_wallet: W,
506    deploy_whitelist: bool,
507    order_book_whitelist_id: Option<ContractId>,
508    salt: Salt,
509) -> anyhow::Result<Option<ContractId>>
510where
511    W: Account + ViewOnlyAccount + Clone + 'static,
512{
513    match (order_book_whitelist_id, deploy_whitelist) {
514        (Some(order_book_whitelist_id), false)
515        | (Some(order_book_whitelist_id), true) => {
516            tracing::info!(
517                "Using existing OrderBookWhitelist: {}",
518                order_book_whitelist_id
519            );
520            Ok(Some(order_book_whitelist_id))
521        }
522        (None, false) => Ok(None),
523        (None, true) => {
524            tracing::info!("Deploying OrderBookWhitelist");
525            let trade_account_whitelist = OrderBookDeploy::deploy_order_book_whitelist(
526                &deployer_wallet,
527                &Identity::Address(ViewOnlyAccount::address(&deployer_wallet)),
528                &OrderBookDeployConfig {
529                    salt,
530                    ..Default::default()
531                },
532            )
533            .await?;
534            tracing::info!(
535                "OrderBookWhitelist: {}",
536                trade_account_whitelist.contract_id()
537            );
538            Ok(Some(trade_account_whitelist.contract_id()))
539        }
540    }
541}
542
543/// Load an existing oracle and recover from partial deployment if needed.
544/// Unlike `TradeAccountDeploy::from_oracle_id`, this does not error when
545/// the trade account implementation is missing — it deploys and sets it.
546async fn load_or_recover_trade_account_oracle<W>(
547    deployer_wallet: &W,
548    oracle_id: ContractId,
549) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
550where
551    W: Account + ViewOnlyAccount + Clone + 'static,
552{
553    let oracle = TradingAccountOracle::new(oracle_id, deployer_wallet.clone());
554    let impl_id = oracle
555        .methods()
556        .get_trade_account_impl()
557        .simulate(Execution::state_read_only())
558        .await?
559        .value;
560
561    let blob_id = match impl_id {
562        Some(id) => id,
563        None => {
564            tracing::info!(
565                "Trade account implementation not set on oracle {}, deploying...",
566                oracle_id
567            );
568            let blob = TradeAccountDeploy::trade_account_blob(
569                deployer_wallet,
570                &Default::default(),
571            )
572            .await?;
573            TradeAccountDeploy::deploy_trade_account_blob(
574                deployer_wallet,
575                &DeployConfig::Latest(Default::default()),
576            )
577            .await?;
578            oracle
579                .methods()
580                .set_trade_account_impl(ContractId::from(blob.id))
581                .call()
582                .await?;
583            ContractId::from(blob.id)
584        }
585    };
586
587    let deploy = TradeAccountDeploy {
588        oracle,
589        oracle_id,
590        trade_account_blob_id: blob_id.into(),
591        deployer_wallet: deployer_wallet.clone(),
592        proxy: None,
593        proxy_id: None,
594    };
595    Ok((deploy, blob_id))
596}
597
598async fn deploy_trade_account_oracle<W>(
599    deployer_wallet: W,
600    should_upgrade_bytecode: bool,
601    trade_account_oracle_id: Option<ContractId>,
602    salt: Salt,
603) -> anyhow::Result<(TradeAccountDeploy<W>, ContractId)>
604where
605    W: Account + ViewOnlyAccount + Clone + 'static,
606{
607    let (trade_account_oracle_deploy, mut trade_account_blob_id) =
608        match trade_account_oracle_id {
609            Some(oracle_id) => {
610                load_or_recover_trade_account_oracle(&deployer_wallet, oracle_id).await?
611            }
612            None => {
613                let deploy = TradeAccountDeploy::deploy(
614                    &deployer_wallet,
615                    &DeployConfig::Latest(TradeAccountDeployConfig {
616                        salt,
617                        ..Default::default()
618                    }),
619                )
620                .await?;
621                let blob_id = deploy
622                    .oracle
623                    .methods()
624                    .get_trade_account_impl()
625                    .simulate(Execution::state_read_only())
626                    .await?
627                    .value
628                    .context("Trade account impl should exist after fresh deploy")?;
629                (deploy, blob_id)
630            }
631        };
632    tracing::info!(
633        "TradeAccountOracle: {}",
634        trade_account_oracle_deploy.oracle_id
635    );
636
637    if should_upgrade_bytecode {
638        let trade_account_blob =
639            TradeAccountDeploy::trade_account_blob(&deployer_wallet, &Default::default())
640                .await?;
641        if ContractId::from(trade_account_blob.id) != trade_account_blob_id {
642            tracing::info!(
643                "Update TradeAccountImpl on Oracle from {:?} to new blob {:?}",
644                trade_account_blob_id,
645                ContractId::from(trade_account_blob.id)
646            );
647            TradeAccountDeploy::deploy_trade_account_blob(
648                &deployer_wallet,
649                &DeployConfig::Latest(Default::default()),
650            )
651            .await?;
652            trade_account_oracle_deploy
653                .oracle
654                .methods()
655                .set_trade_account_impl(ContractId::from(trade_account_blob.id))
656                .call()
657                .await?;
658            trade_account_blob_id = ContractId::from(trade_account_blob.id);
659        }
660    }
661
662    Ok((trade_account_oracle_deploy, trade_account_blob_id))
663}
664
665async fn deploy_trade_account_registry<W>(
666    deployer_wallet: W,
667    should_upgrade_bytecode: bool,
668    trade_account_deploy: TradeAccountDeploy<W>,
669    trade_account_registry_id: Option<ContractId>,
670    salt: Salt,
671) -> anyhow::Result<(TradeAccountRegistryManager<W>, ContractId)>
672where
673    W: Account + ViewOnlyAccount + Clone + 'static,
674{
675    let trade_account_oracle_id = trade_account_deploy.oracle_id;
676    let trade_account_registry = match trade_account_registry_id {
677        Some(trade_account_registry_contract_id) => TradeAccountRegistryManager::new(
678            deployer_wallet.clone(),
679            trade_account_registry_contract_id,
680        ),
681        None => {
682            let trade_account_registry_deploy_config = TradeAccountRegistryDeployConfig {
683                salt,
684                ..Default::default()
685            };
686            TradeAccountRegistryManager::deploy(
687                &deployer_wallet,
688                trade_account_oracle_id,
689                &trade_account_registry_deploy_config,
690            )
691            .await?
692        }
693    };
694    tracing::info!(
695        "TradeAccountRegistry: {}",
696        trade_account_registry.contract_id
697    );
698    let mut trade_account_registry_blob_id = match trade_account_registry
699        .registry_proxy
700        .methods()
701        .proxy_target()
702        .simulate(Execution::state_read_only())
703        .await?
704        .value
705    {
706        Some(blob_id) => blob_id,
707        None => {
708            tracing::info!("TradeAccountRegistry proxy target not set, initializing...");
709            // Call initialize_proxy() to write INITIAL_OWNER and INITIAL_TARGET
710            // from configurables to storage (upgrade/set_proxy_target would fail
711            // because the owner is not yet in storage)
712            trade_account_registry
713                .registry_proxy
714                .methods()
715                .initialize_proxy()
716                .call()
717                .await?;
718            trade_account_registry
719                .registry
720                .methods()
721                .initialize()
722                .call()
723                .await?;
724            trade_account_registry
725                .registry_proxy
726                .methods()
727                .proxy_target()
728                .simulate(Execution::state_read_only())
729                .await?
730                .value
731                .context("TradeAccountRegistry proxy target should be set after initialization")?
732        }
733    };
734
735    if should_upgrade_bytecode {
736        let trade_account_registry_deploy_config =
737            TradeAccountRegistryDeployConfig::default();
738        let trade_account_proxy_blob = TradeAccountRegistryManager::register_proxy_blob(
739            &deployer_wallet,
740            &trade_account_registry_deploy_config,
741        )
742        .await?;
743
744        let trade_account_register_blob = TradeAccountRegistryManager::register_blob(
745            &deployer_wallet,
746            trade_account_oracle_id,
747            trade_account_proxy_blob.id,
748            &trade_account_registry_deploy_config,
749        )
750        .await?;
751
752        if trade_account_registry_blob_id
753            != ContractId::from(trade_account_register_blob.id)
754        {
755            tracing::info!(
756                "Upgrade TradeAccountRegistry blob from {:?} to {:?}",
757                trade_account_registry.contract_id,
758                ContractId::from(trade_account_register_blob.id)
759            );
760            trade_account_registry
761                .upgrade(
762                    trade_account_oracle_id,
763                    &TradeAccountRegistryDeployConfig::default(),
764                )
765                .await?;
766            trade_account_registry_blob_id = trade_account_register_blob.id.into();
767        }
768    }
769    Ok((trade_account_registry, trade_account_registry_blob_id))
770}
771
772async fn deploy_order_book_registry<W>(
773    deployer_wallet: W,
774    should_upgrade_bytecode: bool,
775    order_book_registry_id: Option<ContractId>,
776    salt: Salt,
777) -> anyhow::Result<(OrderBookRegistryManager<W>, ContractId)>
778where
779    W: Account + ViewOnlyAccount + Clone + 'static,
780{
781    let order_book_registry = match order_book_registry_id {
782        Some(registry_contract_id) => {
783            OrderBookRegistryManager::new(deployer_wallet.clone(), registry_contract_id)
784        }
785        None => {
786            OrderBookRegistryManager::deploy(
787                &deployer_wallet,
788                &OrderBookRegistryDeployConfig {
789                    salt,
790                    ..Default::default()
791                },
792            )
793            .await?
794        }
795    };
796    tracing::info!("OrderBookRegistry: {}", order_book_registry.contract_id);
797    let mut order_book_registry_blob_id = match order_book_registry
798        .registry_proxy
799        .methods()
800        .proxy_target()
801        .simulate(Execution::state_read_only())
802        .await?
803        .value
804    {
805        Some(blob_id) => blob_id,
806        None => {
807            tracing::info!("OrderBookRegistry proxy target not set, initializing...");
808            // Call initialize_proxy() to write INITIAL_OWNER and INITIAL_TARGET
809            // from configurables to storage (upgrade/set_proxy_target would fail
810            // because the owner is not yet in storage)
811            order_book_registry
812                .registry_proxy
813                .methods()
814                .initialize_proxy()
815                .call()
816                .await?;
817            order_book_registry
818                .registry
819                .methods()
820                .initialize()
821                .call()
822                .await?;
823            order_book_registry
824                .registry_proxy
825                .methods()
826                .proxy_target()
827                .simulate(Execution::state_read_only())
828                .await?
829                .value
830                .context(
831                    "OrderBookRegistry proxy target should be set after initialization",
832                )?
833        }
834    };
835
836    if should_upgrade_bytecode {
837        let order_book_register_deploy_config = OrderBookRegistryDeployConfig::default();
838        let order_book_register_blob = OrderBookRegistryManager::register_blob(
839            &deployer_wallet,
840            &order_book_register_deploy_config,
841        )
842        .await?;
843        if order_book_registry_blob_id != order_book_register_blob.id.into() {
844            tracing::info!(
845                "Upgrade OrderBookRegistry blob from {:?} to {:?}",
846                order_book_registry.contract_id,
847                ContractId::from(order_book_register_blob.id)
848            );
849            order_book_registry
850                .upgrade(&order_book_register_deploy_config)
851                .await?;
852            order_book_registry_blob_id = order_book_register_blob.id.into();
853        }
854    }
855
856    Ok((order_book_registry, order_book_registry_blob_id))
857}
858
859async fn deploy_order_books<W>(
860    deployer_wallet: W,
861    should_upgrade_bytecode: bool,
862    order_book_blacklist_id: Option<ContractId>,
863    order_book_whitelist_id: Option<ContractId>,
864    order_book_registry: OrderBookRegistryManager<W>,
865    order_book_configs: &mut [OrderBookConfig],
866    ownership_options: OwnershipTransferOptions,
867) -> anyhow::Result<Vec<OrderBookConfig>>
868where
869    W: Account + ViewOnlyAccount + Clone + 'static,
870{
871    let mut pairs: Vec<OrderBookConfig> = Vec::with_capacity(order_book_configs.len());
872
873    for order_book_config in order_book_configs.iter_mut() {
874        let pair = deploy_single_order_book(
875            &deployer_wallet,
876            should_upgrade_bytecode,
877            order_book_blacklist_id,
878            order_book_whitelist_id,
879            &order_book_registry,
880            order_book_config,
881            &ownership_options,
882        )
883        .await?;
884        pairs.push(pair);
885    }
886
887    Ok(pairs)
888}
889
890async fn deploy_single_order_book<W>(
891    deployer_wallet: &W,
892    should_upgrade_bytecode: bool,
893    order_book_blacklist_id: Option<ContractId>,
894    order_book_whitelist_id: Option<ContractId>,
895    order_book_registry: &OrderBookRegistryManager<W>,
896    order_book_config: &mut OrderBookConfig,
897    ownership_options: &OwnershipTransferOptions,
898) -> anyhow::Result<OrderBookConfig>
899where
900    W: Account + ViewOnlyAccount + Clone + 'static,
901{
902    let market_symbol = format!(
903        "{}/{}",
904        order_book_config.base.symbol, order_book_config.quote.symbol
905    );
906    let market_id = MarketIdAssets {
907        base_asset: order_book_config.base.asset,
908        quote_asset: order_book_config.quote.asset,
909    };
910    let order_book_configurables = build_order_book_configurables(
911        order_book_config,
912        order_book_blacklist_id,
913        order_book_whitelist_id,
914        deployer_wallet,
915    )?;
916
917    let order_book = load_or_deploy_order_book(
918        deployer_wallet,
919        order_book_registry,
920        &market_id,
921        &market_symbol,
922        &order_book_configurables,
923        order_book_config,
924    )
925    .await?;
926
927    tracing::info!(
928        "[{}] OrderBook: {}",
929        market_symbol,
930        order_book.contract.contract_id()
931    );
932
933    let order_book_blob_id = maybe_upgrade_order_book(
934        deployer_wallet,
935        should_upgrade_bytecode,
936        &order_book,
937        order_book_config,
938        order_book_configurables,
939        &market_symbol,
940    )
941    .await?;
942
943    transfer_order_book_ownership(&order_book, ownership_options, &market_symbol).await?;
944
945    order_book_config.contract_id = Some(order_book.contract.contract_id());
946    order_book_config.blob_id = order_book_blob_id.into();
947
948    Ok(order_book_config.clone())
949}
950
951fn build_order_book_configurables<W: ViewOnlyAccount>(
952    config: &OrderBookConfig,
953    order_book_blacklist_id: Option<ContractId>,
954    order_book_whitelist_id: Option<ContractId>,
955    deployer_wallet: &W,
956) -> anyhow::Result<OrderBookConfigurables> {
957    let price_precision = config
958        .quote
959        .decimals
960        .checked_sub(config.quote.max_precision)
961        .ok_or_else(|| {
962            anyhow::anyhow!(
963                "quote max_precision ({}) exceeds decimals ({})",
964                config.quote.max_precision,
965                config.quote.decimals
966            )
967        })?;
968    let quantity_precision = config
969        .base
970        .decimals
971        .checked_sub(config.base.max_precision)
972        .ok_or_else(|| {
973            anyhow::anyhow!(
974                "base max_precision ({}) exceeds decimals ({})",
975                config.base.max_precision,
976                config.base.decimals
977            )
978        })?;
979
980    Ok(OrderBookConfigurables::default()
981        .with_MIN_ORDER(config.min_order)?
982        .with_TAKER_FEE(config.taker_fee.into())?
983        .with_MAKER_FEE(config.maker_fee.into())?
984        .with_DUST(config.dust)?
985        .with_PRICE_WINDOW(config.price_window as u64)?
986        .with_BASE_DECIMALS(10u64.pow(config.base.decimals as u32))?
987        .with_QUOTE_DECIMALS(10u64.pow(config.quote.decimals as u32))?
988        .with_BASE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
989            config.base.symbol.clone(),
990        )?)?
991        .with_QUOTE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
992            config.quote.symbol.clone(),
993        )?)?
994        .with_PRICE_PRECISION(10u64.pow(price_precision as u32))?
995        .with_QUANTITY_PRECISION(10u64.pow(quantity_precision as u32))?
996        .with_INITIAL_OWNER(o2_tools::order_book_deploy::State::Initialized(
997            Identity::Address(ViewOnlyAccount::address(deployer_wallet)),
998        ))?
999        .with_WHITE_LIST_CONTRACT(order_book_whitelist_id)?
1000        .with_BLACK_LIST_CONTRACT(order_book_blacklist_id)?)
1001}
1002
1003async fn load_or_deploy_order_book<W>(
1004    deployer_wallet: &W,
1005    order_book_registry: &OrderBookRegistryManager<W>,
1006    market_id: &MarketIdAssets,
1007    market_symbol: &str,
1008    order_book_configurables: &OrderBookConfigurables,
1009    order_book_config: &OrderBookConfig,
1010) -> anyhow::Result<OrderBookManager<W>>
1011where
1012    W: Account + ViewOnlyAccount + Clone + 'static,
1013{
1014    let register_contract_id = order_book_registry
1015        .registry
1016        .methods()
1017        .get_order_book(to_registry_market_id(market_id))
1018        .simulate(Execution::state_read_only())
1019        .await?
1020        .value;
1021
1022    match register_contract_id {
1023        Some(contract_id) => {
1024            let order_book_deploy = OrderBookDeploy::new(
1025                deployer_wallet.clone(),
1026                contract_id,
1027                market_id.base_asset,
1028                market_id.quote_asset,
1029            );
1030            // Handle partially deployed contracts (e.g. previous deploy failed
1031            // after registering but before initializing the proxy)
1032            let proxy_target = order_book_deploy
1033                .order_book_proxy
1034                .methods()
1035                .proxy_target()
1036                .simulate(Execution::state_read_only())
1037                .await?
1038                .value;
1039            if proxy_target.is_none() {
1040                tracing::info!(
1041                    "[{}] Proxy target not set, initializing...",
1042                    market_symbol
1043                );
1044                order_book_deploy.initialize().await?;
1045            }
1046            Ok(OrderBookManager::new(
1047                deployer_wallet,
1048                10u64.pow(order_book_config.base.decimals as u32),
1049                10u64.pow(order_book_config.quote.decimals as u32),
1050                &order_book_deploy,
1051            ))
1052        }
1053        None => {
1054            let (order_book_deployment, initialization_required) =
1055                OrderBookDeploy::deploy_without_initialization(
1056                    deployer_wallet,
1057                    market_id.base_asset,
1058                    market_id.quote_asset,
1059                    &OrderBookDeployConfig {
1060                        order_book_configurables: order_book_configurables.clone(),
1061                        salt: Salt::from(*order_book_registry.contract_id),
1062                        ..Default::default()
1063                    },
1064                )
1065                .await?;
1066
1067            order_book_registry
1068                .register_order_book(
1069                    to_registry_market_id(market_id),
1070                    order_book_deployment.contract_id,
1071                )
1072                .await?;
1073
1074            if initialization_required {
1075                order_book_deployment.initialize().await?;
1076            }
1077            Ok(OrderBookManager::new(
1078                deployer_wallet,
1079                10u64.pow(order_book_config.base.decimals as u32),
1080                10u64.pow(order_book_config.quote.decimals as u32),
1081                &order_book_deployment,
1082            ))
1083        }
1084    }
1085}
1086
1087async fn maybe_upgrade_order_book<W>(
1088    deployer_wallet: &W,
1089    should_upgrade_bytecode: bool,
1090    order_book: &OrderBookManager<W>,
1091    order_book_config: &OrderBookConfig,
1092    order_book_configurables: OrderBookConfigurables,
1093    market_symbol: &str,
1094) -> anyhow::Result<ContractId>
1095where
1096    W: Account + ViewOnlyAccount + Clone + 'static,
1097{
1098    let mut order_book_blob_id = order_book
1099        .proxy
1100        .methods()
1101        .proxy_target()
1102        .simulate(Execution::state_read_only())
1103        .await?
1104        .value
1105        .context("Order book proxy target should be set after initialization")?;
1106
1107    if should_upgrade_bytecode {
1108        let order_book_deploy_config = OrderBookDeployConfig {
1109            order_book_configurables,
1110            ..Default::default()
1111        };
1112        let order_book_deploy = OrderBookDeploy::new(
1113            deployer_wallet.clone(),
1114            order_book.contract.contract_id(),
1115            order_book_config.base.asset,
1116            order_book_config.quote.asset,
1117        );
1118        let order_book_manager = OrderBookManager::new(
1119            deployer_wallet,
1120            10u64.pow(order_book_config.base.decimals as u32),
1121            10u64.pow(order_book_config.quote.decimals as u32),
1122            &order_book_deploy,
1123        );
1124        let order_book_blob = OrderBookDeploy::order_book_blob(
1125            deployer_wallet,
1126            order_book_config.base.asset,
1127            order_book_config.quote.asset,
1128            &order_book_deploy_config,
1129        )
1130        .await?;
1131
1132        if order_book_blob_id != order_book_blob.id.into() {
1133            tracing::info!(
1134                "[{}] Upgrade OrderBook blob from {:?} to {:?}",
1135                market_symbol,
1136                order_book_blob_id,
1137                ContractId::from(order_book_blob.id)
1138            );
1139            order_book_manager
1140                .upgrade(&order_book_deploy_config)
1141                .await?;
1142            tracing::info!(
1143                "[{}] Emit new configuration event for {}",
1144                market_symbol,
1145                order_book.contract.contract_id()
1146            );
1147            order_book_manager.emit_config().await?;
1148            order_book_blob_id = order_book_blob.id.into();
1149        }
1150    }
1151
1152    Ok(order_book_blob_id)
1153}
1154
1155async fn transfer_order_book_ownership<W>(
1156    order_book: &OrderBookManager<W>,
1157    ownership_options: &OwnershipTransferOptions,
1158    market_symbol: &str,
1159) -> anyhow::Result<()>
1160where
1161    W: Account + ViewOnlyAccount + Clone + 'static,
1162{
1163    if let Some(new_owner) = ownership_options.new_proxy_owner {
1164        let new_identity = Identity::Address(new_owner);
1165        tracing::info!(
1166            "[{}] Transferring OrderBook proxy ownership to {}",
1167            market_symbol,
1168            new_owner
1169        );
1170        order_book
1171            .proxy
1172            .methods()
1173            .set_owner(new_identity)
1174            .call()
1175            .await?;
1176    }
1177
1178    if let Some(new_owner) = ownership_options.new_contract_owner {
1179        let new_identity = Identity::Address(new_owner);
1180        tracing::info!(
1181            "[{}] Transferring OrderBook contract ownership to {}",
1182            market_symbol,
1183            new_owner
1184        );
1185        order_book
1186            .contract
1187            .methods()
1188            .transfer_ownership(new_identity)
1189            .call()
1190            .await?;
1191    }
1192
1193    Ok(())
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199
1200    #[test]
1201    fn load_config_empty_path_returns_default() {
1202        let result: MarketsConfigPartial = load_config_from_file("").unwrap();
1203        assert!(result.pairs.is_empty());
1204    }
1205
1206    #[test]
1207    fn load_config_missing_file_errors() {
1208        let result: Result<MarketsConfigPartial, _> =
1209            load_config_from_file("nonexistent_file_12345.json");
1210        assert!(result.is_err());
1211    }
1212
1213    #[test]
1214    fn checked_sub_catches_overflow() {
1215        // Validates that our checked_sub pattern works correctly
1216        let decimals: u32 = 6;
1217        let max_precision: u32 = 8; // greater than decimals
1218
1219        let result = decimals.checked_sub(max_precision);
1220        assert!(
1221            result.is_none(),
1222            "should return None when max_precision > decimals"
1223        );
1224
1225        // Normal case
1226        let result = 9u32.checked_sub(6);
1227        assert_eq!(result, Some(3));
1228    }
1229
1230    #[test]
1231    fn markets_config_partial_default_has_empty_pairs() {
1232        let config = MarketsConfigPartial::default();
1233        assert!(config.pairs.is_empty());
1234    }
1235}