1use anyhow::Context;
7use fuel_core_client::client::types::primitives::{
8 ContractId,
9 Salt,
10};
11use fuel_core_types::fuel_types::BlockHeight;
12use fuels::{
13 accounts::{
14 Account,
15 ViewOnlyAccount,
16 },
17 prelude::Execution,
18 types::{
19 Identity,
20 SizedAsciiString,
21 },
22};
23use o2_api_types::{
24 domain::book::{
25 AssetConfig,
26 MarketIdAssets,
27 OrderBookConfig,
28 },
29 parse::HexDisplayFromStr,
30};
31use o2_tools::{
32 order_book::OrderBookManager,
33 order_book_deploy::{
34 OrderBookBlacklist,
35 OrderBookConfigurables,
36 OrderBookDeploy,
37 OrderBookDeployConfig,
38 OrderBookWhitelist,
39 },
40 order_book_registry::{
41 OrderBookRegistryDeployConfig,
42 OrderBookRegistryManager,
43 },
44 trade_account_deploy::{
45 DeployConfig,
46 TradeAccountDeploy,
47 TradeAccountDeployConfig,
48 TradingAccountOracle,
49 },
50 trade_account_registry::{
51 TradeAccountRegistryDeployConfig,
52 TradeAccountRegistryManager,
53 },
54};
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#[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#[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#[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
209pub 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
229pub 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 ¶ms,
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
367async 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
466async 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
546async 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 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 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 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 let decimals: u32 = 6;
1221 let max_precision: u32 = 8; let result = decimals.checked_sub(max_precision);
1224 assert!(
1225 result.is_none(),
1226 "should return None when max_precision > decimals"
1227 );
1228
1229 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}