1use alloy::{
2 genesis::{ChainConfig, Genesis, GenesisAccount},
3 primitives::{Address, U256, address},
4 signers::{local::MnemonicBuilder, utils::secret_key_to_address},
5};
6use alloy_primitives::Bytes;
7use commonware_codec::Encode as _;
8use commonware_cryptography::ed25519::PublicKey;
9use eyre::{WrapErr as _, eyre};
10use indicatif::{ParallelProgressIterator, ProgressIterator};
11use rayon::prelude::*;
12use reth_evm::{
13 EvmEnv, EvmFactory,
14 revm::{
15 database::{CacheDB, EmptyDB},
16 inspector::JournalExt,
17 },
18};
19use std::{
20 collections::BTreeMap,
21 net::SocketAddr,
22 path::{Path, PathBuf},
23};
24use tempo_chainspec::{hardfork::TempoHardfork, spec::TEMPO_BASE_FEE};
25use tempo_commonware_node_config::{Peers, PublicPolynomial, SigningKey, SigningShare};
26use tempo_contracts::{
27 ARACHNID_CREATE2_FACTORY_ADDRESS, CREATEX_ADDRESS, DEFAULT_7702_DELEGATE_ADDRESS,
28 MULTICALL_ADDRESS, PERMIT2_ADDRESS, SAFE_DEPLOYER_ADDRESS,
29 contracts::{ARACHNID_CREATE2_FACTORY_BYTECODE, CREATEX_POST_ALLEGRO_MODERATO_BYTECODE},
30 precompiles::{ITIP20Factory, IValidatorConfig},
31};
32use tempo_dkg_onchain_artifacts::PublicOutcome;
33use tempo_evm::evm::{TempoEvm, TempoEvmFactory};
34use tempo_precompiles::{
35 PATH_USD_ADDRESS,
36 nonce::NonceManager,
37 stablecoin_exchange::StablecoinExchange,
38 storage::{ContractStorage, StorageCtx},
39 tip_fee_manager::{IFeeManager, TipFeeManager},
40 tip20::{ISSUER_ROLE, ITIP20, TIP20Token, address_to_token_id_unchecked},
41 tip20_factory::TIP20Factory,
42 tip20_rewards_registry::TIP20RewardsRegistry,
43 tip403_registry::TIP403Registry,
44 validator_config::ValidatorConfig,
45};
46
47#[derive(Debug, clap::Args)]
49pub(crate) struct GenesisArgs {
50 #[arg(short, long, default_value = "50000")]
52 accounts: u32,
53
54 #[arg(
56 short,
57 long,
58 default_value = "test test test test test test test test test test test junk"
59 )]
60 mnemonic: String,
61
62 #[arg(long, default_value = "0xD3C21BCECCEDA1000000")]
64 balance: U256,
65
66 #[arg(long, default_value = "0x0000000000000000000000000000000000000000")]
68 coinbase: Address,
69
70 #[arg(long, short, default_value = "1337")]
72 chain_id: u64,
73
74 #[arg(long, default_value_t = TEMPO_BASE_FEE.into())]
76 base_fee_per_gas: u128,
77
78 #[arg(long, default_value_t = 17000000000000)]
80 gas_limit: u64,
81
82 #[arg(long, default_value_t = 0)]
84 adagio_time: u64,
85
86 #[arg(long, default_value_t = 0)]
88 pub moderato_time: u64,
89
90 #[arg(long)]
92 pub allegretto_time: Option<u64>,
93
94 #[arg(long)]
96 pub allegro_moderato_time: Option<u64>,
97
98 #[arg(long, default_value_t = 302_400)]
100 epoch_length: u64,
101
102 #[arg(
104 long,
105 value_name = "<ip>:<port>",
106 value_delimiter = ',',
107 required_if_eq("allegretto_time", "0")
108 )]
109 validators: Vec<SocketAddr>,
110
111 #[arg(long)]
114 no_validators_in_genesis: bool,
115
116 #[arg(long)]
119 no_dkg_in_genesis: bool,
120
121 #[arg(long)]
124 pub(crate) seed: Option<u64>,
125}
126
127#[derive(Clone, Debug)]
128pub(crate) struct ConsensusConfig {
129 pub(crate) public_polynomial: PublicPolynomial,
130 pub(crate) peers: Peers,
131 pub(crate) validators: Vec<Validator>,
132}
133impl ConsensusConfig {
134 pub(crate) fn to_genesis_dkg_outcome(&self) -> PublicOutcome {
135 PublicOutcome {
136 epoch: 0,
137 participants: self.peers.public_keys().clone(),
138 public: self.public_polynomial.clone().into_inner(),
139 }
140 }
141}
142
143#[derive(Clone, Debug)]
144pub(crate) struct Validator {
145 pub(crate) addr: SocketAddr,
146 pub(crate) signing_key: SigningKey,
147 pub(crate) signing_share: SigningShare,
148}
149
150impl Validator {
151 pub(crate) fn public_key(&self) -> PublicKey {
152 self.signing_key.public_key()
153 }
154
155 pub(crate) fn dst_dir(&self, path: impl AsRef<Path>) -> PathBuf {
156 path.as_ref().join(self.addr.to_string())
157 }
158 pub(crate) fn dst_signing_key(&self, path: impl AsRef<Path>) -> PathBuf {
159 self.dst_dir(path).join("signing.key")
160 }
161
162 pub(crate) fn dst_signing_share(&self, path: impl AsRef<Path>) -> PathBuf {
163 self.dst_dir(path).join("signing.share")
164 }
165}
166
167impl GenesisArgs {
168 pub(crate) async fn generate_genesis(self) -> eyre::Result<(Genesis, Option<ConsensusConfig>)> {
173 println!("Generating {:?} accounts", self.accounts);
174
175 let addresses: Vec<Address> = (0..self.accounts)
176 .into_par_iter()
177 .progress()
178 .map(|worker_id| -> eyre::Result<Address> {
179 let signer = MnemonicBuilder::from_phrase_nth(&self.mnemonic, worker_id);
180 let address = secret_key_to_address(signer.credential());
181 Ok(address)
182 })
183 .collect::<eyre::Result<Vec<Address>>>()?;
184
185 let admin = addresses[0];
189 let mut evm = setup_tempo_evm();
190
191 println!("Initializing registry");
192 initialize_registry(&mut evm)?;
193
194 println!("Initializing TIP20Factory");
196 initialize_tip20_factory(&mut evm)?;
197
198 println!("Creating PathUSD through factory");
200 create_path_usd_token(admin, &addresses, &mut evm)?;
201
202 println!("Initializing TIP20 tokens");
203 let (_, alpha_token_address) = create_and_mint_token(
204 "AlphaUSD",
205 "AlphaUSD",
206 "USD",
207 PATH_USD_ADDRESS,
208 admin,
209 &addresses,
210 U256::from(u64::MAX),
211 &mut evm,
212 )?;
213
214 let (_, beta_token_address) = create_and_mint_token(
215 "BetaUSD",
216 "BetaUSD",
217 "USD",
218 PATH_USD_ADDRESS,
219 admin,
220 &addresses,
221 U256::from(u64::MAX),
222 &mut evm,
223 )?;
224
225 let (_, theta_token_address) = create_and_mint_token(
226 "ThetaUSD",
227 "ThetaUSD",
228 "USD",
229 PATH_USD_ADDRESS,
230 admin,
231 &addresses,
232 U256::from(u64::MAX),
233 &mut evm,
234 )?;
235
236 println!("Initializing TIP20RewardsRegistry");
237 initialize_tip20_rewards_registry(&mut evm)?;
238
239 println!(
240 "generating consensus config for validators: {:?}",
241 self.validators
242 );
243 let consensus_config = generate_consensus_config(&self.validators, self.seed);
244
245 println!("Initializing validator config");
246 initialize_validator_config(
247 admin,
248 &mut evm,
249 &consensus_config,
250 &addresses[1..],
252 self.no_validators_in_genesis,
253 )?;
254
255 println!("Initializing fee manager");
256 initialize_fee_manager(
257 alpha_token_address,
258 addresses.clone(),
259 vec![self.coinbase],
261 &mut evm,
262 );
263
264 println!("Initializing stablecoin exchange");
265 initialize_stablecoin_exchange(&mut evm)?;
266
267 println!("Initializing nonce manager");
268 initialize_nonce_manager(&mut evm)?;
269
270 println!("Minting pairwise FeeAMM liquidity");
271 mint_pairwise_liquidity(
272 alpha_token_address,
273 vec![PATH_USD_ADDRESS, beta_token_address, theta_token_address],
274 U256::from(10u64.pow(10)),
275 admin,
276 &mut evm,
277 );
278
279 println!("Saving EVM state to allocation");
281 let evm_state = evm.ctx_mut().journaled_state.evm_state();
282 let mut genesis_alloc: BTreeMap<Address, GenesisAccount> = evm_state
283 .iter()
284 .progress()
285 .map(|(address, account)| {
286 let storage = if !account.storage.is_empty() {
287 Some(
288 account
289 .storage
290 .iter()
291 .map(|(key, val)| ((*key).into(), val.present_value.into()))
292 .collect(),
293 )
294 } else {
295 None
296 };
297 let genesis_account = GenesisAccount {
298 nonce: Some(account.info.nonce),
299 code: account.info.code.as_ref().map(|c| c.original_bytes()),
300 storage,
301 ..Default::default()
302 };
303 (*address, genesis_account)
304 })
305 .collect();
306
307 genesis_alloc.insert(
308 MULTICALL_ADDRESS,
309 GenesisAccount {
310 code: Some(tempo_contracts::Multicall::DEPLOYED_BYTECODE.clone()),
311 nonce: Some(1),
312 ..Default::default()
313 },
314 );
315
316 genesis_alloc.insert(
317 DEFAULT_7702_DELEGATE_ADDRESS,
318 GenesisAccount {
319 code: Some(tempo_contracts::IthacaAccount::DEPLOYED_BYTECODE.clone()),
320 nonce: Some(1),
321 ..Default::default()
322 },
323 );
324
325 genesis_alloc.insert(
326 CREATEX_ADDRESS,
327 GenesisAccount {
328 code: Some(CREATEX_POST_ALLEGRO_MODERATO_BYTECODE),
329 nonce: Some(1),
330 ..Default::default()
331 },
332 );
333
334 genesis_alloc.insert(
335 SAFE_DEPLOYER_ADDRESS,
336 GenesisAccount {
337 code: Some(tempo_contracts::SafeDeployer::DEPLOYED_BYTECODE.clone()),
338 nonce: Some(1),
339 ..Default::default()
340 },
341 );
342
343 genesis_alloc.insert(
344 PERMIT2_ADDRESS,
345 GenesisAccount {
346 code: Some(tempo_contracts::Permit2::DEPLOYED_BYTECODE.clone()),
347 nonce: Some(1),
348 ..Default::default()
349 },
350 );
351
352 genesis_alloc.insert(
353 ARACHNID_CREATE2_FACTORY_ADDRESS,
354 GenesisAccount {
355 code: Some(ARACHNID_CREATE2_FACTORY_BYTECODE),
356 nonce: Some(1),
357 ..Default::default()
358 },
359 );
360
361 let mut chain_config = ChainConfig {
362 chain_id: self.chain_id,
363 homestead_block: Some(0),
364 eip150_block: Some(0),
365 eip155_block: Some(0),
366 eip158_block: Some(0),
367 byzantium_block: Some(0),
368 constantinople_block: Some(0),
369 petersburg_block: Some(0),
370 istanbul_block: Some(0),
371 berlin_block: Some(0),
372 london_block: Some(0),
373 merge_netsplit_block: Some(0),
374 shanghai_time: Some(0),
375 cancun_time: Some(0),
376 prague_time: Some(0),
377 osaka_time: Some(0),
378 terminal_total_difficulty: Some(U256::from(0)),
379 terminal_total_difficulty_passed: true,
380 deposit_contract_address: Some(address!("0x00000000219ab540356cBB839Cbe05303d7705Fa")),
381 ..Default::default()
382 };
383
384 chain_config.extra_fields.insert(
386 "adagioTime".to_string(),
387 serde_json::json!(self.adagio_time),
388 );
389 chain_config.extra_fields.insert(
390 "moderatoTime".to_string(),
391 serde_json::json!(self.moderato_time),
392 );
393 if let Some(allegretto_time) = self.allegretto_time {
394 chain_config.extra_fields.insert(
395 "allegrettoTime".to_string(),
396 serde_json::json!(allegretto_time),
397 );
398 }
399 if let Some(allegro_moderato_time) = self.allegro_moderato_time {
400 chain_config.extra_fields.insert(
401 "allegroModeratoTime".to_string(),
402 serde_json::json!(allegro_moderato_time),
403 );
404 }
405
406 chain_config
407 .extra_fields
408 .insert_value("epochLength".to_string(), self.epoch_length)?;
409 let mut extra_data = Bytes::from_static(b"tempo-genesis");
410
411 if let Some(consensus_config) = &consensus_config {
412 chain_config
413 .extra_fields
414 .insert_value("validators".to_string(), consensus_config.peers.clone())?;
415 chain_config.extra_fields.insert_value(
416 "publicPolynomial".to_string(),
417 consensus_config.public_polynomial.clone(),
418 )?;
419
420 if self.no_dkg_in_genesis {
421 println!("no-initial-dkg-in-genesis passed; not writing to header extra_data");
422 } else {
423 extra_data = consensus_config
424 .to_genesis_dkg_outcome()
425 .encode()
426 .freeze()
427 .to_vec()
428 .into();
429 }
430 }
431
432 let mut genesis = Genesis::default()
433 .with_gas_limit(self.gas_limit)
434 .with_base_fee(Some(self.base_fee_per_gas))
435 .with_nonce(0x42)
436 .with_extra_data(extra_data)
437 .with_coinbase(self.coinbase);
438
439 genesis.alloc = genesis_alloc;
440 genesis.config = chain_config;
441
442 Ok((genesis, consensus_config))
443 }
444}
445
446fn setup_tempo_evm() -> TempoEvm<CacheDB<EmptyDB>> {
447 let db = CacheDB::default();
448 let mut env = EvmEnv::default().with_timestamp(U256::ZERO);
450 env.cfg_env = env.cfg_env.with_spec(TempoHardfork::Allegretto);
453 let factory = TempoEvmFactory::default();
454 factory.create_evm(db, env)
455}
456
457fn initialize_tip20_factory(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
459 let ctx = evm.ctx_mut();
460 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
461 TIP20Factory::new().initialize()
462 })?;
463 Ok(())
464}
465
466fn create_path_usd_token(
469 admin: Address,
470 recipients: &[Address],
471 evm: &mut TempoEvm<CacheDB<EmptyDB>>,
472) -> eyre::Result<()> {
473 let ctx = evm.ctx_mut();
474 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
475 let token_address = TIP20Factory::new()
477 .create_token(
478 admin,
479 ITIP20Factory::createTokenCall {
480 name: "pathUSD".into(),
481 symbol: "pathUSD".into(),
482 currency: "USD".into(),
483 quoteToken: Address::ZERO, admin,
485 },
486 )
487 .expect("Could not create PathUSD token");
488
489 assert_eq!(
491 token_address, PATH_USD_ADDRESS,
492 "PathUSD should be created at token_id=0 address"
493 );
494
495 let mut token = TIP20Token::new(0);
496 token.grant_role_internal(admin, *ISSUER_ROLE)?;
497
498 for recipient in recipients.iter().progress() {
500 token
501 .mint(
502 admin,
503 ITIP20::mintCall {
504 to: *recipient,
505 amount: U256::from(u64::MAX),
506 },
507 )
508 .expect("Could not mint pathUSD");
509 }
510
511 Ok(())
512 })
513}
514
515#[expect(clippy::too_many_arguments)]
517fn create_and_mint_token(
518 symbol: &str,
519 name: &str,
520 currency: &str,
521 quote_token: Address,
522 admin: Address,
523 recipients: &[Address],
524 mint_amount: U256,
525 evm: &mut TempoEvm<CacheDB<EmptyDB>>,
526) -> eyre::Result<(u64, Address)> {
527 let ctx = evm.ctx_mut();
528 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
529 let mut factory = TIP20Factory::new();
530 assert!(
531 factory
532 .is_initialized()
533 .expect("Could not check factory initialization"),
534 "TIP20Factory must be initialized before creating tokens"
535 );
536 let token_address = factory
537 .create_token(
538 admin,
539 ITIP20Factory::createTokenCall {
540 name: name.into(),
541 symbol: symbol.into(),
542 currency: currency.into(),
543 quoteToken: quote_token,
544 admin,
545 },
546 )
547 .expect("Could not create token");
548
549 let token_id = address_to_token_id_unchecked(token_address);
550
551 let mut token = TIP20Token::new(token_id);
552 token.grant_role_internal(admin, *ISSUER_ROLE)?;
553
554 let result = token.set_supply_cap(
555 admin,
556 ITIP20::setSupplyCapCall {
557 newSupplyCap: U256::from(u128::MAX),
558 },
559 );
560 assert!(result.is_ok());
561
562 token
563 .mint(
564 admin,
565 ITIP20::mintCall {
566 to: admin,
567 amount: mint_amount,
568 },
569 )
570 .expect("Token minting failed");
571
572 for address in recipients.iter().progress() {
573 token
574 .mint(
575 admin,
576 ITIP20::mintCall {
577 to: *address,
578 amount: U256::from(u64::MAX),
579 },
580 )
581 .expect("Could not mint fee token");
582 }
583
584 Ok((token_id, token.address()))
585 })
586}
587
588fn initialize_tip20_rewards_registry(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
589 let ctx = evm.ctx_mut();
590 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
591 TIP20RewardsRegistry::new().initialize()
592 })?;
593
594 Ok(())
595}
596
597fn initialize_fee_manager(
598 default_fee_address: Address,
599 initial_accounts: Vec<Address>,
600 validators: Vec<Address>,
601 evm: &mut TempoEvm<CacheDB<EmptyDB>>,
602) {
603 let ctx = evm.ctx_mut();
605 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
606 let mut fee_manager = TipFeeManager::new();
607 fee_manager
608 .initialize()
609 .expect("Could not init fee manager");
610 for address in initial_accounts.iter().progress() {
611 fee_manager
612 .set_user_token(
613 *address,
614 IFeeManager::setUserTokenCall {
615 token: default_fee_address,
616 },
617 )
618 .expect("Could not set fee token");
619 }
620
621 for validator in validators {
623 fee_manager
624 .set_validator_token(
625 validator,
626 IFeeManager::setValidatorTokenCall {
627 token: PATH_USD_ADDRESS,
628 },
629 Address::random(),
631 )
632 .expect("Could not set validator fee token");
633 }
634 });
635}
636
637fn initialize_registry(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
639 let ctx = evm.ctx_mut();
640 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
641 TIP403Registry::new().initialize()
642 })?;
643
644 Ok(())
645}
646
647fn initialize_stablecoin_exchange(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
648 let ctx = evm.ctx_mut();
649 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
650 StablecoinExchange::new().initialize()
651 })?;
652
653 Ok(())
654}
655
656fn initialize_nonce_manager(evm: &mut TempoEvm<CacheDB<EmptyDB>>) -> eyre::Result<()> {
657 let ctx = evm.ctx_mut();
658 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
659 NonceManager::new().initialize()
660 })?;
661
662 Ok(())
663}
664
665fn initialize_validator_config(
670 admin: Address,
671 evm: &mut TempoEvm<CacheDB<EmptyDB>>,
672 consensus_config: &Option<ConsensusConfig>,
673 addresses: &[Address],
674 no_validators_in_genesis: bool,
675) -> eyre::Result<()> {
676 let ctx = evm.ctx_mut();
677 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
678 let mut validator_config = ValidatorConfig::new();
679 validator_config
680 .initialize(admin)
681 .wrap_err("failed to initialize validator config contract")?;
682
683 if no_validators_in_genesis {
684 println!("no-validators-genesis passed; not writing validators to genesis block");
685 return Ok(());
686 }
687
688 if let Some(consensus_config) = consensus_config.clone() {
689 println!(
690 "writing {} validators into contract",
691 consensus_config.validators.len()
692 );
693 for (i, validator) in consensus_config.validators.iter().enumerate() {
694 #[expect(non_snake_case, reason = "field of a snakeCase smart contract call")]
695 let newValidatorAddress = *addresses.get(i).ok_or_else(|| {
696 eyre!(
697 "need `{}` addresses for all validators, but only `{}` were generated",
698 consensus_config.validators.len(),
699 addresses.len()
700 )
701 })?;
702 let public_key = validator.public_key();
703 let addr = validator.addr;
704 validator_config
705 .add_validator(
706 admin,
707 IValidatorConfig::addValidatorCall {
708 newValidatorAddress,
709 publicKey: public_key.encode().freeze().as_ref().try_into().unwrap(),
710 active: true,
711 inboundAddress: addr.to_string(),
712 outboundAddress: addr.to_string(),
713 },
714 )
715 .wrap_err(
716 "failed to execute smart contract call to add validator to evm state",
717 )?;
718 println!(
719 "added validator\
720 \n\tpublic key: {public_key}\
721 \n\tonchain address: {newValidatorAddress}\
722 \n\tnet address: {addr}"
723 );
724 }
725 } else {
726 println!("no consensus config passed; no validators to write to contract");
727 }
728
729 Ok(())
730 })
731}
732
733fn generate_consensus_config(
735 validators: &[SocketAddr],
736 seed: Option<u64>,
737) -> Option<ConsensusConfig> {
738 use commonware_cryptography::{PrivateKeyExt as _, Signer as _, ed25519::PrivateKey};
739 use rand::SeedableRng as _;
740
741 if validators.is_empty() {
742 println!("no validator socket addresses provided; not generating consensus config");
743 return None;
744 }
745
746 let mut rng = rand::rngs::StdRng::seed_from_u64(seed.unwrap_or_else(rand::random::<u64>));
747 let mut signers = (0..validators.len())
748 .map(|_| PrivateKey::from_rng(&mut rng))
749 .collect::<Vec<_>>();
750
751 let threshold = commonware_utils::quorum(validators.len() as u32);
753 let (polynomial, shares) = commonware_cryptography::bls12381::dkg::ops::generate_shares::<
754 _,
755 commonware_cryptography::bls12381::primitives::variant::MinSig,
756 >(&mut rng, None, validators.len() as u32, threshold);
757
758 signers.sort_by_key(|signer| signer.public_key());
759 let peers = validators
760 .iter()
761 .zip(signers.iter())
762 .map(|(addr, private_key)| (private_key.public_key(), *addr))
763 .collect::<commonware_utils::set::OrderedAssociated<_, _>>();
764
765 let mut validators = vec![];
766 for (addr, (signer, share)) in peers.values().iter().zip(signers.into_iter().zip(shares)) {
767 validators.push(Validator {
768 addr: *addr,
769 signing_key: SigningKey::from(signer),
770 signing_share: SigningShare::from(share),
771 });
772 }
773
774 Some(ConsensusConfig {
775 peers: peers.into(),
776 public_polynomial: polynomial.into(),
777 validators,
778 })
779}
780
781fn mint_pairwise_liquidity(
782 a_token: Address,
783 b_tokens: Vec<Address>,
784 amount: U256,
785 admin: Address,
786 evm: &mut TempoEvm<CacheDB<EmptyDB>>,
787) {
788 let ctx = evm.ctx_mut();
789 StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, || {
790 let mut fee_manager = TipFeeManager::new();
791
792 for b_token_address in b_tokens {
793 fee_manager
794 .mint(admin, a_token, b_token_address, amount, amount, admin)
795 .expect("Could not mint A -> B Liquidity pool");
796 }
797 });
798}