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