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
747        let trade_account_register_blob = TradeAccountRegistryManager::register_blob(
748            &deployer_wallet,
749            trade_account_oracle_id,
750            trade_account_proxy_blob.id,
751            &trade_account_registry_deploy_config,
752        )
753        .await?;
754
755        if trade_account_registry_blob_id
756            != ContractId::from(trade_account_register_blob.id)
757        {
758            tracing::info!(
759                "Upgrade TradeAccountRegistry blob from {:?} to {:?}",
760                trade_account_registry.contract_id,
761                ContractId::from(trade_account_register_blob.id)
762            );
763            trade_account_registry
764                .upgrade(
765                    trade_account_oracle_id,
766                    &TradeAccountRegistryDeployConfig::default(),
767                )
768                .await?;
769            trade_account_registry_blob_id = trade_account_register_blob.id.into();
770        }
771    }
772    Ok((trade_account_registry, trade_account_registry_blob_id))
773}
774
775async fn deploy_order_book_registry<W>(
776    deployer_wallet: W,
777    should_upgrade_bytecode: bool,
778    order_book_registry_id: Option<ContractId>,
779    salt: Salt,
780) -> anyhow::Result<(OrderBookRegistryManager<W>, ContractId)>
781where
782    W: Account + ViewOnlyAccount + Clone + 'static,
783{
784    let order_book_registry = match order_book_registry_id {
785        Some(registry_contract_id) => {
786            OrderBookRegistryManager::new(deployer_wallet.clone(), registry_contract_id)
787        }
788        None => {
789            OrderBookRegistryManager::deploy(
790                &deployer_wallet,
791                &OrderBookRegistryDeployConfig {
792                    salt,
793                    ..Default::default()
794                },
795            )
796            .await?
797        }
798    };
799    tracing::info!("OrderBookRegistry: {}", order_book_registry.contract_id);
800    let mut order_book_registry_blob_id = match order_book_registry
801        .registry_proxy
802        .methods()
803        .proxy_target()
804        .simulate(Execution::state_read_only())
805        .await?
806        .value
807    {
808        Some(blob_id) => blob_id,
809        None => {
810            tracing::info!("OrderBookRegistry proxy target not set, initializing...");
811            // Call initialize_proxy() to write INITIAL_OWNER and INITIAL_TARGET
812            // from configurables to storage (upgrade/set_proxy_target would fail
813            // because the owner is not yet in storage)
814            order_book_registry
815                .registry_proxy
816                .methods()
817                .initialize_proxy()
818                .call()
819                .await?;
820            order_book_registry
821                .registry
822                .methods()
823                .initialize()
824                .call()
825                .await?;
826            order_book_registry
827                .registry_proxy
828                .methods()
829                .proxy_target()
830                .simulate(Execution::state_read_only())
831                .await?
832                .value
833                .context(
834                    "OrderBookRegistry proxy target should be set after initialization",
835                )?
836        }
837    };
838
839    if should_upgrade_bytecode {
840        let order_book_register_deploy_config = OrderBookRegistryDeployConfig::default();
841        let order_book_register_blob = OrderBookRegistryManager::register_blob(
842            &deployer_wallet,
843            &order_book_register_deploy_config,
844        )
845        .await?;
846        if order_book_registry_blob_id != order_book_register_blob.id.into() {
847            tracing::info!(
848                "Upgrade OrderBookRegistry blob from {:?} to {:?}",
849                order_book_registry.contract_id,
850                ContractId::from(order_book_register_blob.id)
851            );
852            order_book_registry
853                .upgrade(&order_book_register_deploy_config)
854                .await?;
855            order_book_registry_blob_id = order_book_register_blob.id.into();
856        }
857    }
858
859    Ok((order_book_registry, order_book_registry_blob_id))
860}
861
862async fn deploy_order_books<W>(
863    deployer_wallet: W,
864    should_upgrade_bytecode: bool,
865    order_book_blacklist_id: Option<ContractId>,
866    order_book_whitelist_id: Option<ContractId>,
867    order_book_registry: OrderBookRegistryManager<W>,
868    order_book_configs: &mut [OrderBookConfig],
869    ownership_options: OwnershipTransferOptions,
870) -> anyhow::Result<Vec<OrderBookConfig>>
871where
872    W: Account + ViewOnlyAccount + Clone + 'static,
873{
874    let mut pairs: Vec<OrderBookConfig> = Vec::with_capacity(order_book_configs.len());
875
876    for order_book_config in order_book_configs.iter_mut() {
877        let pair = deploy_single_order_book(
878            &deployer_wallet,
879            should_upgrade_bytecode,
880            order_book_blacklist_id,
881            order_book_whitelist_id,
882            &order_book_registry,
883            order_book_config,
884            &ownership_options,
885        )
886        .await?;
887        pairs.push(pair);
888    }
889
890    Ok(pairs)
891}
892
893async fn deploy_single_order_book<W>(
894    deployer_wallet: &W,
895    should_upgrade_bytecode: bool,
896    order_book_blacklist_id: Option<ContractId>,
897    order_book_whitelist_id: Option<ContractId>,
898    order_book_registry: &OrderBookRegistryManager<W>,
899    order_book_config: &mut OrderBookConfig,
900    ownership_options: &OwnershipTransferOptions,
901) -> anyhow::Result<OrderBookConfig>
902where
903    W: Account + ViewOnlyAccount + Clone + 'static,
904{
905    let market_symbol = format!(
906        "{}/{}",
907        order_book_config.base.symbol, order_book_config.quote.symbol
908    );
909    let market_id = MarketIdAssets {
910        base_asset: order_book_config.base.asset,
911        quote_asset: order_book_config.quote.asset,
912    };
913    let order_book_configurables = build_order_book_configurables(
914        order_book_config,
915        order_book_blacklist_id,
916        order_book_whitelist_id,
917        deployer_wallet,
918    )?;
919
920    let order_book = load_or_deploy_order_book(
921        deployer_wallet,
922        order_book_registry,
923        &market_id,
924        &market_symbol,
925        &order_book_configurables,
926        order_book_config,
927    )
928    .await?;
929
930    tracing::info!(
931        "[{}] OrderBook: {}",
932        market_symbol,
933        order_book.contract.contract_id()
934    );
935
936    let order_book_blob_id = maybe_upgrade_order_book(
937        deployer_wallet,
938        should_upgrade_bytecode,
939        &order_book,
940        order_book_config,
941        order_book_configurables,
942        &market_symbol,
943    )
944    .await?;
945
946    transfer_order_book_ownership(&order_book, ownership_options, &market_symbol).await?;
947
948    order_book_config.contract_id = Some(order_book.contract.contract_id());
949    order_book_config.blob_id = order_book_blob_id.into();
950
951    Ok(order_book_config.clone())
952}
953
954fn build_order_book_configurables<W: ViewOnlyAccount>(
955    config: &OrderBookConfig,
956    order_book_blacklist_id: Option<ContractId>,
957    order_book_whitelist_id: Option<ContractId>,
958    deployer_wallet: &W,
959) -> anyhow::Result<OrderBookConfigurables> {
960    let price_precision = config
961        .quote
962        .decimals
963        .checked_sub(config.quote.max_precision)
964        .ok_or_else(|| {
965            anyhow::anyhow!(
966                "quote max_precision ({}) exceeds decimals ({})",
967                config.quote.max_precision,
968                config.quote.decimals
969            )
970        })?;
971    let quantity_precision = config
972        .base
973        .decimals
974        .checked_sub(config.base.max_precision)
975        .ok_or_else(|| {
976            anyhow::anyhow!(
977                "base max_precision ({}) exceeds decimals ({})",
978                config.base.max_precision,
979                config.base.decimals
980            )
981        })?;
982
983    Ok(OrderBookConfigurables::default()
984        .with_MIN_ORDER(config.min_order)?
985        .with_ALLOW_FRACTIONAL_PRICE(config.allow_fractional_price)?
986        .with_TAKER_FEE(config.taker_fee.into())?
987        .with_MAKER_FEE(config.maker_fee.into())?
988        .with_DUST(config.dust)?
989        .with_PRICE_WINDOW(config.price_window as u64)?
990        .with_BASE_DECIMALS(10u64.pow(config.base.decimals as u32))?
991        .with_QUOTE_DECIMALS(10u64.pow(config.quote.decimals as u32))?
992        .with_BASE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
993            config.base.symbol.clone(),
994        )?)?
995        .with_QUOTE_SYMBOL(SizedAsciiString::new_with_right_whitespace_padding(
996            config.quote.symbol.clone(),
997        )?)?
998        .with_PRICE_PRECISION(10u64.pow(price_precision as u32))?
999        .with_QUANTITY_PRECISION(10u64.pow(quantity_precision as u32))?
1000        .with_INITIAL_OWNER(o2_tools::order_book_deploy::State::Initialized(
1001            Identity::Address(ViewOnlyAccount::address(deployer_wallet)),
1002        ))?
1003        .with_WHITE_LIST_CONTRACT(order_book_whitelist_id)?
1004        .with_BLACK_LIST_CONTRACT(order_book_blacklist_id)?)
1005}
1006
1007async fn load_or_deploy_order_book<W>(
1008    deployer_wallet: &W,
1009    order_book_registry: &OrderBookRegistryManager<W>,
1010    market_id: &MarketIdAssets,
1011    market_symbol: &str,
1012    order_book_configurables: &OrderBookConfigurables,
1013    order_book_config: &OrderBookConfig,
1014) -> anyhow::Result<OrderBookManager<W>>
1015where
1016    W: Account + ViewOnlyAccount + Clone + 'static,
1017{
1018    let register_contract_id = order_book_registry
1019        .registry
1020        .methods()
1021        .get_order_book(to_registry_market_id(market_id))
1022        .simulate(Execution::state_read_only())
1023        .await?
1024        .value;
1025
1026    match register_contract_id {
1027        Some(contract_id) => {
1028            let order_book_deploy = OrderBookDeploy::new(
1029                deployer_wallet.clone(),
1030                contract_id,
1031                market_id.base_asset,
1032                market_id.quote_asset,
1033            );
1034            // Handle partially deployed contracts (e.g. previous deploy failed
1035            // after registering but before initializing the proxy)
1036            let proxy_target = order_book_deploy
1037                .order_book_proxy
1038                .methods()
1039                .proxy_target()
1040                .simulate(Execution::state_read_only())
1041                .await?
1042                .value;
1043            if proxy_target.is_none() {
1044                tracing::info!(
1045                    "[{}] Proxy target not set, initializing...",
1046                    market_symbol
1047                );
1048                order_book_deploy.initialize().await?;
1049            }
1050            Ok(OrderBookManager::new(
1051                deployer_wallet,
1052                10u64.pow(order_book_config.base.decimals as u32),
1053                10u64.pow(order_book_config.quote.decimals as u32),
1054                &order_book_deploy,
1055            ))
1056        }
1057        None => {
1058            let (order_book_deployment, initialization_required) =
1059                OrderBookDeploy::deploy_without_initialization(
1060                    deployer_wallet,
1061                    market_id.base_asset,
1062                    market_id.quote_asset,
1063                    &OrderBookDeployConfig {
1064                        order_book_configurables: order_book_configurables.clone(),
1065                        salt: Salt::from(*order_book_registry.contract_id),
1066                        ..Default::default()
1067                    },
1068                )
1069                .await?;
1070
1071            order_book_registry
1072                .register_order_book(
1073                    to_registry_market_id(market_id),
1074                    order_book_deployment.contract_id,
1075                )
1076                .await?;
1077
1078            if initialization_required {
1079                order_book_deployment.initialize().await?;
1080            }
1081            Ok(OrderBookManager::new(
1082                deployer_wallet,
1083                10u64.pow(order_book_config.base.decimals as u32),
1084                10u64.pow(order_book_config.quote.decimals as u32),
1085                &order_book_deployment,
1086            ))
1087        }
1088    }
1089}
1090
1091async fn maybe_upgrade_order_book<W>(
1092    deployer_wallet: &W,
1093    should_upgrade_bytecode: bool,
1094    order_book: &OrderBookManager<W>,
1095    order_book_config: &OrderBookConfig,
1096    order_book_configurables: OrderBookConfigurables,
1097    market_symbol: &str,
1098) -> anyhow::Result<ContractId>
1099where
1100    W: Account + ViewOnlyAccount + Clone + 'static,
1101{
1102    let mut order_book_blob_id = order_book
1103        .proxy
1104        .methods()
1105        .proxy_target()
1106        .simulate(Execution::state_read_only())
1107        .await?
1108        .value
1109        .context("Order book proxy target should be set after initialization")?;
1110
1111    if should_upgrade_bytecode {
1112        let order_book_deploy_config = OrderBookDeployConfig {
1113            order_book_configurables,
1114            ..Default::default()
1115        };
1116        let order_book_deploy = OrderBookDeploy::new(
1117            deployer_wallet.clone(),
1118            order_book.contract.contract_id(),
1119            order_book_config.base.asset,
1120            order_book_config.quote.asset,
1121        );
1122        let order_book_manager = OrderBookManager::new(
1123            deployer_wallet,
1124            10u64.pow(order_book_config.base.decimals as u32),
1125            10u64.pow(order_book_config.quote.decimals as u32),
1126            &order_book_deploy,
1127        );
1128        let order_book_blob = OrderBookDeploy::order_book_blob(
1129            deployer_wallet,
1130            order_book_config.base.asset,
1131            order_book_config.quote.asset,
1132            &order_book_deploy_config,
1133        )
1134        .await?;
1135
1136        if order_book_blob_id != order_book_blob.id.into() {
1137            tracing::info!(
1138                "[{}] Upgrade OrderBook blob from {:?} to {:?}",
1139                market_symbol,
1140                order_book_blob_id,
1141                ContractId::from(order_book_blob.id)
1142            );
1143            order_book_manager
1144                .upgrade(&order_book_deploy_config)
1145                .await?;
1146            tracing::info!(
1147                "[{}] Emit new configuration event for {}",
1148                market_symbol,
1149                order_book.contract.contract_id()
1150            );
1151            order_book_manager.emit_config().await?;
1152            order_book_blob_id = order_book_blob.id.into();
1153        }
1154    }
1155
1156    Ok(order_book_blob_id)
1157}
1158
1159async fn transfer_order_book_ownership<W>(
1160    order_book: &OrderBookManager<W>,
1161    ownership_options: &OwnershipTransferOptions,
1162    market_symbol: &str,
1163) -> anyhow::Result<()>
1164where
1165    W: Account + ViewOnlyAccount + Clone + 'static,
1166{
1167    if let Some(new_owner) = ownership_options.new_proxy_owner {
1168        let new_identity = Identity::Address(new_owner);
1169        tracing::info!(
1170            "[{}] Transferring OrderBook proxy ownership to {}",
1171            market_symbol,
1172            new_owner
1173        );
1174        order_book
1175            .proxy
1176            .methods()
1177            .set_owner(new_identity)
1178            .call()
1179            .await?;
1180    }
1181
1182    if let Some(new_owner) = ownership_options.new_contract_owner {
1183        let new_identity = Identity::Address(new_owner);
1184        tracing::info!(
1185            "[{}] Transferring OrderBook contract ownership to {}",
1186            market_symbol,
1187            new_owner
1188        );
1189        order_book
1190            .contract
1191            .methods()
1192            .transfer_ownership(new_identity)
1193            .call()
1194            .await?;
1195    }
1196
1197    Ok(())
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202    use super::*;
1203
1204    #[test]
1205    fn load_config_empty_path_returns_default() {
1206        let result: MarketsConfigPartial = load_config_from_file("").unwrap();
1207        assert!(result.pairs.is_empty());
1208    }
1209
1210    #[test]
1211    fn load_config_missing_file_errors() {
1212        let result: Result<MarketsConfigPartial, _> =
1213            load_config_from_file("nonexistent_file_12345.json");
1214        assert!(result.is_err());
1215    }
1216
1217    #[test]
1218    fn checked_sub_catches_overflow() {
1219        // Validates that our checked_sub pattern works correctly
1220        let decimals: u32 = 6;
1221        let max_precision: u32 = 8; // greater than decimals
1222
1223        let result = decimals.checked_sub(max_precision);
1224        assert!(
1225            result.is_none(),
1226            "should return None when max_precision > decimals"
1227        );
1228
1229        // Normal case
1230        let result = 9u32.checked_sub(6);
1231        assert_eq!(result, Some(3));
1232    }
1233
1234    #[test]
1235    fn markets_config_partial_default_has_empty_pairs() {
1236        let config = MarketsConfigPartial::default();
1237        assert!(config.pairs.is_empty());
1238    }
1239}