Skip to main content

miden_node_store/genesis/config/
mod.rs

1//! Describe a subset of the genesis manifest in easily human readable format
2
3use std::cmp::Ordering;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6
7use indexmap::IndexMap;
8use miden_node_tracing::debug;
9use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
10use miden_protocol::account::{Account, AccountBuilder, AccountFile, AccountId, AccountType};
11use miden_protocol::asset::{Asset, AssetAmount, FungibleAsset, TokenSymbol};
12use miden_protocol::block::{FeeParameters, ValidatorKeys};
13use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey as RpoSecretKey;
14use miden_protocol::errors::TokenSymbolError;
15use miden_protocol::{Felt, ONE};
16use miden_standards::account::auth::{Approver, AuthSingleSig, NetworkAccountNoteAllowlist};
17use miden_standards::account::faucets::{
18    FungibleFaucet,
19    TokenName,
20    create_native_fungible_faucet_for_genesis,
21};
22use miden_standards::account::fees::BasicConstantFeePolicy;
23use miden_standards::account::policies::{
24    BurnPolicy,
25    MintPolicy,
26    TokenPolicyManager,
27    TransferPolicy,
28};
29use miden_standards::account::wallets::create_basic_wallet;
30use miden_standards::note::{BurnNote, FeeSponsorshipNote, MintNote};
31use rand::distr::weighted::Weight;
32use rand::{RngExt, SeedableRng};
33use rand_chacha::ChaCha20Rng;
34use serde::{Deserialize, Serialize};
35
36use crate::{GenesisState, LOG_TARGET};
37
38mod errors;
39use self::errors::GenesisConfigError;
40
41#[cfg(test)]
42mod tests;
43
44const DEFAULT_NATIVE_FAUCET_SYMBOL: &str = "MIDEN";
45const DEFAULT_NATIVE_FAUCET_DECIMALS: u8 = 6;
46const DEFAULT_NATIVE_FAUCET_MAX_SUPPLY: u64 = 100_000_000_000_000_000;
47/// One thousand native tokens, used to bootstrap the operator's fee payments.
48const DEFAULT_FAUCET_OPERATOR_BALANCE: u64 = 1_000_000_000;
49
50/// Name of the account file written for the generated native faucet.
51pub const NATIVE_FAUCET_FILE_NAME: &str = "native_faucet.mac";
52
53/// Name of the account file written for the generated faucet operator.
54pub const FAUCET_OPERATOR_FILE_NAME: &str = "faucet_operator.mac";
55
56// GENESIS CONFIG
57// ================================================================================================
58
59/// An account loaded from a `.mac` file (path relative to genesis config directory).
60///
61/// Notice: Generic accounts are not validated (e.g. that their vault assets reference known
62/// faucets), leaving the responsibility of ensuring valid genesis state to the operator.
63#[derive(Debug, Clone, serde::Deserialize)]
64#[serde(deny_unknown_fields)]
65struct GenericAccountConfig {
66    path: PathBuf,
67}
68
69/// Specify a set of faucets and wallets with assets for easier test deployments.
70///
71/// Notice: Any faucet must be declared _before_ it's use in a wallet/regular account.
72#[derive(Debug, Clone, serde::Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct GenesisConfig {
75    version: u32,
76    timestamp: u32,
77    /// Override the native faucet with a custom faucet account.
78    ///
79    /// If unspecified, the native faucet is generated as a network account owned by a generated
80    /// faucet operator account, using:
81    ///
82    /// ```toml
83    /// symbol     = "MIDEN"
84    /// decimals   = 6
85    /// max_supply = 100_000_000_000_000_000
86    /// ```
87    ///
88    /// The generated operator is pre-funded with 1,000 MIDEN tokens so it can pay the fees required
89    /// to submit the first mint requests.
90    #[serde(default)]
91    native_faucet: Option<PathBuf>,
92    fee_parameters: FeeParameterConfig,
93    #[serde(default)]
94    wallet: Vec<WalletConfig>,
95    #[serde(default)]
96    fungible_faucet: Vec<FungibleFaucetConfig>,
97    #[serde(default)]
98    account: Vec<GenericAccountConfig>,
99    #[serde(skip)]
100    config_dir: PathBuf,
101}
102
103impl Default for GenesisConfig {
104    fn default() -> Self {
105        Self {
106            version: 1_u32,
107            timestamp: u32::try_from(
108                std::time::SystemTime::now()
109                    .duration_since(std::time::UNIX_EPOCH)
110                    .expect("Time does not go backwards")
111                    .as_secs(),
112            )
113            .expect("Timestamp should fit into u32"),
114            wallet: vec![],
115            native_faucet: None,
116            fee_parameters: FeeParameterConfig { verification_base_fee: 0 },
117            fungible_faucet: vec![],
118            account: vec![],
119            config_dir: PathBuf::from("."),
120        }
121    }
122}
123
124impl GenesisConfig {
125    /// Read the genesis config from a TOML file.
126    ///
127    /// The parent directory of `path` is used to resolve relative paths for account files
128    /// referenced in the configuration (e.g., `[[account]]` entries with `path` fields).
129    ///
130    /// Notice: It will generate the specified case during [`fn into_state`].
131    pub fn read_toml_file(path: &Path) -> Result<Self, GenesisConfigError> {
132        let toml_str = fs_err::read_to_string(path)
133            .map_err(|e| GenesisConfigError::ConfigFileRead(e, path.to_path_buf()))?;
134        let config_dir = path.parent().expect("config file path must have a parent directory");
135        Self::read_toml(&toml_str, config_dir)
136    }
137
138    /// Parse a genesis config from a TOML formatted string.
139    ///
140    /// The `config_dir` parameter is stored so that relative paths for account files
141    /// (e.g., `[[account]]` entries with `path` fields, or native faucet file references)
142    /// can be resolved later during [`Self::into_state`].
143    fn read_toml(toml_str: &str, config_dir: &Path) -> Result<Self, GenesisConfigError> {
144        let mut config: Self = toml::from_str(toml_str)?;
145        config.config_dir = config_dir.to_path_buf();
146        Ok(config)
147    }
148
149    /// Convert the in memory representation into the new genesis state
150    ///
151    /// The given `validator_keys` are the genesis validator set committed to by the genesis
152    /// header. The genesis block is not signed; the committed set is required to sign every block
153    /// after genesis.
154    ///
155    /// Also returns the set of secrets for the generated accounts.
156    #[expect(clippy::too_many_lines)]
157    pub fn into_state(
158        self,
159        validator_keys: ValidatorKeys,
160    ) -> Result<(GenesisState, AccountSecrets), GenesisConfigError> {
161        let GenesisConfig {
162            version,
163            timestamp,
164            native_faucet,
165            fee_parameters,
166            fungible_faucet: fungible_faucet_configs,
167            wallet: wallet_configs,
168            account: account_entries,
169            config_dir,
170        } = self;
171
172        // Load account files from disk
173        let file_loaded_accounts = account_entries
174            .into_iter()
175            .map(|acc| {
176                let full_path = config_dir.join(&acc.path);
177                let account_file = AccountFile::read(&full_path)
178                    .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
179                Ok(account_file.account)
180            })
181            .collect::<Result<Vec<_>, GenesisConfigError>>()?;
182
183        let mut wallet_accounts = Vec::<Account>::new();
184        // Every asset sitting in a wallet, has to reference a faucet for that asset
185        let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
186
187        // Collect the generated secret keys for the test, so one can interact with those
188        // accounts/sign transactions
189        let mut secrets = Vec::new();
190
191        // Handle native faucet: generate a network faucet and its operator, or load from file
192        let NativeFaucet {
193            account: native_faucet_account,
194            symbol,
195            operator,
196        } = NativeFaucetConfig(native_faucet).build_account(&config_dir)?;
197        let native_faucet_account_id = native_faucet_account.id();
198
199        // Track all genesis issuance, one entry per faucet account id. A generated faucet operator
200        // is pre-funded in `NativeFaucetConfig::build_account`, so account for that allocation
201        // before adding configured wallet balances below.
202        let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
203        if operator.is_some() {
204            faucet_issuance.insert(native_faucet_account_id, DEFAULT_FAUCET_OPERATOR_BALANCE);
205        }
206
207        let operator_account = match operator {
208            Some((operator, operator_secret)) => {
209                // The generated faucet is a network account and holds no key of its own, but
210                // consumers still need its state and id, so it is written out like any other
211                // generated account.
212                secrets.push((
213                    NATIVE_FAUCET_FILE_NAME.to_string(),
214                    native_faucet_account.id(),
215                    None,
216                ));
217                secrets.push((
218                    FAUCET_OPERATOR_FILE_NAME.to_string(),
219                    operator.id(),
220                    Some(operator_secret),
221                ));
222                Some(operator)
223            },
224            None => None,
225        };
226
227        faucet_accounts.insert(symbol.clone(), native_faucet_account);
228
229        // Setup additional fungible faucets from parameters
230        for fungible_faucet_config in fungible_faucet_configs {
231            let symbol = fungible_faucet_config.symbol.clone();
232            let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
233
234            if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
235                return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
236            }
237
238            secrets.push((
239                format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
240                faucet_account.id(),
241                Some(secret_key),
242            ));
243            // Do _not_ collect the account, only after we know all wallet assets we know the
244            // remaining supply in the faucets.
245        }
246
247        let fee_parameters =
248            FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee);
249
250        let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
251
252        // Setup all wallet accounts, which reference the faucet's for their provided assets.
253        for (index, WalletConfig { account_type, assets }) in wallet_configs.into_iter().enumerate()
254        {
255            debug!(
256                target: LOG_TARGET,
257                "Adding wallet account",
258                account.index = index,
259                account.assets.count = assets.len()
260            );
261
262            let mut rng = ChaCha20Rng::from_seed(rand::random());
263            let secret_key = RpoSecretKey::with_rng(&mut rng);
264            let auth =
265                Approver::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
266            let init_seed: [u8; 32] = rng.random();
267
268            let mut wallet_account = create_basic_wallet(init_seed, auth, account_type.into())?;
269
270            // Add fungible assets and track the faucet adjustments per faucet/asset.
271            let wallet_assets =
272                prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
273            for asset in wallet_assets {
274                wallet_account.vault_mut().add_asset(asset)?;
275            }
276
277            // Force the account nonce to 1.
278            //
279            // By convention, a nonce of zero indicates a freshly generated local account that has
280            // yet to be deployed. An account is deployed onchain along with its first
281            // transaction which results in a non-zero nonce onchain.
282            //
283            // The genesis block is special in that accounts are "deployed" without transactions and
284            // therefore we need bump the nonce manually to uphold this invariant.
285            wallet_account.set_nonce(ONE)?;
286
287            debug_assert_eq!(wallet_account.nonce(), ONE);
288
289            secrets.push((
290                format!("wallet_{index:0zero_padding_width$}.mac"),
291                wallet_account.id(),
292                Some(secret_key),
293            ));
294
295            wallet_accounts.push(wallet_account);
296        }
297
298        let mut all_accounts = Vec::<Account>::new();
299        // Apply all fungible faucet adjustments to the respective faucet
300        for (symbol, mut faucet_account) in faucet_accounts {
301            let faucet_id = faucet_account.id();
302            // If there is no account using the asset, we only bump the nonce to `ONE`.
303            let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
304
305            if total_issuance != 0 {
306                let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?;
307                let new_token_supply = AssetAmount::new(total_issuance)?;
308                let max_supply = current_faucet.max_supply().as_u64();
309                if max_supply < total_issuance {
310                    return Err(GenesisConfigError::MaxIssuanceExceeded {
311                        max_supply,
312                        symbol: symbol.clone(),
313                        total_issuance,
314                    });
315                }
316                let updated_faucet = current_faucet.with_token_supply(new_token_supply)?;
317                let slot = updated_faucet.token_config_slot_value();
318                faucet_account.storage_mut().set_item(slot.name(), slot.value())?;
319                debug!(
320                    target: LOG_TARGET,
321                    "Reducing faucet account issuance",
322                    account.id = faucet_id,
323                    asset.symbol = symbol.to_string(),
324                    asset.amount = total_issuance
325                );
326            } else {
327                debug!(
328                    target: LOG_TARGET,
329                    "No wallet references faucet asset",
330                    account.id = faucet_id,
331                    asset.symbol = symbol.to_string()
332                );
333            }
334
335            // Force the account nonce to 1, marking the faucet as deployed at genesis.
336            faucet_account.set_nonce(ONE)?;
337
338            debug_assert_eq!(faucet_account.nonce(), ONE);
339
340            // sanity check the total issuance against
341            let faucet = FungibleFaucet::try_from(faucet_account.storage())?;
342            let max_supply = faucet.max_supply().as_u64();
343            if max_supply < total_issuance {
344                return Err(GenesisConfigError::MaxIssuanceExceeded {
345                    max_supply,
346                    symbol,
347                    total_issuance,
348                });
349            }
350
351            all_accounts.push(faucet_account);
352        }
353        // Keep the operator after the faucets because its vault references the native faucet.
354        all_accounts.extend(operator_account);
355
356        // Ensure the faucets always precede the wallets referencing them
357        all_accounts.extend(wallet_accounts);
358
359        // Append file-loaded accounts as-is
360        all_accounts.extend(file_loaded_accounts);
361
362        Ok((
363            GenesisState {
364                fee_parameters,
365                accounts: all_accounts,
366                version,
367                timestamp,
368                validator_keys,
369            },
370            AccountSecrets { secrets },
371        ))
372    }
373}
374
375// FEE PARAMETER CONFIG
376// ================================================================================================
377
378/// Represents a the fee parameters using the given asset
379///
380/// A faucet providing the `symbol` token moste exist.
381#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
382#[serde(deny_unknown_fields)]
383pub struct FeeParameterConfig {
384    /// Verification base fee, in units of smallest denomination.
385    verification_base_fee: u32,
386}
387
388// NATIVE FAUCET CONFIG
389// ================================================================================================
390
391/// The native faucet resolved from the configuration.
392struct NativeFaucet {
393    account: Account,
394    symbol: TokenSymbolStr,
395    /// The operator account owning the faucet, with its signing key. Only present when the faucet
396    /// is generated; an imported faucet comes with its own keys.
397    operator: Option<(Account, RpoSecretKey)>,
398}
399
400/// Wraps an optional path to a pre-built faucet account file.
401///
402/// When no path is provided, a network faucet is generated together with the operator account
403/// owning it.
404struct NativeFaucetConfig(Option<PathBuf>);
405
406impl NativeFaucetConfig {
407    /// Build or load the native faucet account.
408    ///
409    /// For `None`, generates a network faucet plus the operator account owning it, and returns the
410    /// operator alongside its signing key.
411    ///
412    /// For `Some(path)`, loads the account from disk and validates it is a fungible faucet.
413    fn build_account(self, config_dir: &Path) -> Result<NativeFaucet, GenesisConfigError> {
414        match self.0 {
415            None => {
416                // The operator is built first, since the faucet it owns commits to its id.
417                let (mut operator, operator_secret) = build_faucet_operator()?;
418                let (account, symbol) = build_native_faucet(operator.id())?;
419                // Now that the faucet id is known, pre-fund the operator so it can pay fees for the
420                // first mint requests. Genesis issuance is updated in `into_state`.
421                let operator_asset =
422                    FungibleAsset::new(account.id(), DEFAULT_FAUCET_OPERATOR_BALANCE)?;
423                operator.vault_mut().add_asset(operator_asset.into())?;
424                Ok(NativeFaucet {
425                    account,
426                    symbol,
427                    operator: Some((operator, operator_secret)),
428                })
429            },
430            Some(path) => {
431                let full_path = config_dir.join(&path);
432                let account_file = AccountFile::read(&full_path)
433                    .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
434                let account = account_file.account;
435
436                let faucet = FungibleFaucet::try_from(&account).map_err(|_| {
437                    GenesisConfigError::NativeFaucetNotFungible { path: full_path.clone() }
438                })?;
439                let symbol = TokenSymbolStr::from(faucet.symbol().clone());
440                Ok(NativeFaucet { account, symbol, operator: None })
441            },
442        }
443    }
444}
445
446// FAUCET OPERATOR
447// ================================================================================================
448
449/// Builds the initially assetless faucet operator account and returns it with its signing key.
450///
451/// Its nonce is set to `1`, marking it as deployed at genesis. The operator is funded after the
452/// native faucet is built, because its asset id is not known before then.
453fn build_faucet_operator() -> Result<(Account, RpoSecretKey), GenesisConfigError> {
454    let mut rng = ChaCha20Rng::from_seed(rand::random());
455
456    let secret_key = RpoSecretKey::with_rng(&mut rng);
457    let auth = Approver::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
458    let init_seed: [u8; 32] = rng.random();
459    let mut operator = create_basic_wallet(init_seed, auth, AccountType::Public)?;
460    operator.set_nonce(ONE)?;
461
462    Ok((operator, secret_key))
463}
464
465// NATIVE FAUCET
466// ================================================================================================
467
468/// Builds the native faucet as a network account owned by `operator_id`.
469///
470/// The faucet is authenticated as a network account and therefore carries no signing key of its
471/// own; only the operator can mint from it.
472fn build_native_faucet(
473    operator_id: AccountId,
474) -> Result<(Account, TokenSymbolStr), GenesisConfigError> {
475    let mut rng = ChaCha20Rng::from_seed(rand::random());
476
477    let symbol = TokenSymbolStr::from_str(DEFAULT_NATIVE_FAUCET_SYMBOL)?;
478    let faucet_component = FungibleFaucet::builder()
479        .name(
480            TokenName::new(DEFAULT_NATIVE_FAUCET_SYMBOL)
481                .expect("token symbol fits within token name byte limit"),
482        )
483        .symbol(symbol.as_ref().clone())
484        .decimals(DEFAULT_NATIVE_FAUCET_DECIMALS)
485        .max_supply(AssetAmount::new(DEFAULT_NATIVE_FAUCET_MAX_SUPPLY)?)
486        .build()?;
487
488    let policies = TokenPolicyManager::builder()
489        .active_mint_policy(MintPolicy::owner_only())
490        .active_burn_policy(BurnPolicy::allow_all())
491        .active_send_policy(TransferPolicy::allow_all())
492        .active_receive_policy(TransferPolicy::allow_all())
493        .build();
494
495    let fee_policy = BasicConstantFeePolicy::new().with_fees([
496        (MintNote::script_root(), AssetAmount::ZERO),
497        (BurnNote::script_root(), AssetAmount::ZERO),
498    ]);
499
500    // The faucet charges fees in its own asset; the genesis constructor resolves the self-reference
501    // by patching the fee-asset slot after the account id is derived.
502    let faucet_seed: [u8; 32] = rng.random();
503    let faucet = create_native_fungible_faucet_for_genesis(
504        faucet_seed,
505        faucet_component,
506        operator_id,
507        policies,
508        fee_policy,
509    )?;
510
511    debug_assert_eq!(faucet.nonce(), ONE);
512
513    // The faucet's note allowlist must cover the fee sponsorship note, otherwise the network
514    // transaction builder will not work.
515    debug_assert!(
516        NetworkAccountNoteAllowlist::try_from(faucet.storage()).is_ok_and(|allowlist| {
517            allowlist.allowed_script_roots().contains(&FeeSponsorshipNote::script_root())
518        }),
519        "network faucet's note allowlist must cover the fee sponsorship note"
520    );
521
522    Ok((faucet, symbol))
523}
524
525// FUNGIBLE FAUCET CONFIG
526// ================================================================================================
527
528/// Represents a faucet with asset specific properties
529#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
530#[serde(deny_unknown_fields)]
531pub struct FungibleFaucetConfig {
532    symbol: TokenSymbolStr,
533    decimals: u8,
534    /// Max supply in full token units
535    ///
536    /// It will be converted internally to the smallest representable unit,
537    /// using based `10.powi(decimals)` as a multiplier.
538    max_supply: u64,
539    #[serde(default)]
540    account_type: AccountTypeConfig,
541}
542
543impl FungibleFaucetConfig {
544    /// Create a fungible faucet from a config entry
545    fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
546        let FungibleFaucetConfig {
547            symbol,
548            decimals,
549            max_supply,
550            account_type,
551        } = self;
552        let mut rng = ChaCha20Rng::from_seed(rand::random());
553        let secret_key = RpoSecretKey::with_rng(&mut rng);
554        let auth = AuthSingleSig::new(Approver::new(
555            secret_key.public_key().into(),
556            AuthScheme::Falcon512Poseidon2,
557        ));
558        let init_seed: [u8; 32] = rng.random();
559
560        let faucet = FungibleFaucet::builder()
561            .name(
562                TokenName::new(&symbol.to_string())
563                    .expect("token symbol fits within token name byte limit"),
564            )
565            .symbol(symbol.as_ref().clone())
566            .decimals(decimals)
567            .max_supply(AssetAmount::new(max_supply)?)
568            .build()?;
569
570        // It's similar to `fn create_basic_fungible_faucet`, but we need to cover more cases.
571        let faucet_account = AccountBuilder::new(init_seed)
572            .account_type(account_type.into())
573            .with_component(auth)
574            .with_component(faucet)
575            .with_components(
576                TokenPolicyManager::builder()
577                    .active_mint_policy(MintPolicy::allow_all())
578                    .active_burn_policy(BurnPolicy::allow_all())
579                    .build(),
580            )
581            .build()?;
582
583        debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
584
585        Ok((faucet_account, secret_key))
586    }
587}
588
589// WALLET CONFIG
590// ================================================================================================
591
592/// Represents a wallet, containing a set of assets
593#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
594#[serde(deny_unknown_fields)]
595pub struct WalletConfig {
596    #[serde(default)]
597    account_type: AccountTypeConfig,
598    assets: Vec<AssetEntry>,
599}
600
601#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
602struct AssetEntry {
603    symbol: TokenSymbolStr,
604    /// The amount of full token units the given asset is populated with
605    amount: u64,
606}
607
608// ACCOUNT TYPE CONFIG
609// ================================================================================================
610
611/// See the [full description](https://0xmiden.github.io/miden-protocol/account.html?highlight=Accoun#account-storage-mode)
612/// for details
613#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
614pub enum AccountTypeConfig {
615    /// A publicly stored account, lives on-chain.
616    #[serde(alias = "public")]
617    Public,
618    /// A private account, which must be known by interactors.
619    #[serde(alias = "private")]
620    #[default]
621    Private,
622}
623
624impl From<AccountTypeConfig> for AccountType {
625    fn from(value: AccountTypeConfig) -> AccountType {
626        match value {
627            AccountTypeConfig::Public => AccountType::Public,
628            AccountTypeConfig::Private => AccountType::Private,
629        }
630    }
631}
632
633// ACCOUNTS
634// ================================================================================================
635
636#[derive(Debug, Clone)]
637pub struct AccountFileWithName {
638    pub name: String,
639    pub account_file: AccountFile,
640}
641
642/// Secrets generated during the state generation
643#[derive(Debug, Clone)]
644pub struct AccountSecrets {
645    // name, account, private key of the account, if it has one
646    pub secrets: Vec<(String, AccountId, Option<RpoSecretKey>)>,
647}
648
649impl AccountSecrets {
650    /// Convert the internal tuple into an `AccountFile`
651    ///
652    /// If no name is present, a new one is generated based on the current time
653    /// and the index in
654    pub fn as_account_files(
655        &self,
656        genesis_state: &GenesisState,
657    ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
658        let account_lut = genesis_state
659            .accounts
660            .iter()
661            .map(|account| (account.id(), account.clone()))
662            .collect::<IndexMap<AccountId, Account>>();
663        self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
664            let account = account_lut
665                .get(&account_id)
666                .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
667            let auth_secret_keys =
668                secret_key.map(AuthSecretKey::Falcon512Poseidon2).into_iter().collect();
669            let account_file = AccountFile::new(account.clone(), auth_secret_keys);
670            Ok(AccountFileWithName { name, account_file })
671        })
672    }
673}
674
675// HELPERS
676// ================================================================================================
677
678/// Process wallet assets and return them as a fungible asset delta. Track the negative adjustments
679/// for the respective faucets.
680fn prepare_fungible_asset_update(
681    assets: impl IntoIterator<Item = AssetEntry>,
682    faucets: &IndexMap<TokenSymbolStr, Account>,
683    faucet_issuance: &mut IndexMap<AccountId, u64>,
684) -> Result<Vec<Asset>, GenesisConfigError> {
685    assets
686        .into_iter()
687        .map(|AssetEntry { amount, symbol }| {
688            let faucet_account = faucets.get(&symbol).ok_or_else(|| {
689                GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
690            })?;
691            let faucet_id = faucet_account.id();
692
693            let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
694            debug!(
695                target: LOG_TARGET,
696                "Updating faucet issuance",
697                account.id = faucet_id,
698                asset.symbol = symbol.to_string(),
699                asset.amount = amount
700            );
701            issuance
702                .checked_add_assign(&amount)
703                .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
704
705            Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount)?))
706        })
707        .collect()
708}
709
710/// Wrapper type used for configuration representation.
711///
712/// Required since `Felt` does not implement `Hash` or `Eq`, but both are useful and necessary for a
713/// coherent model construction.
714#[derive(Debug, Clone, PartialEq)]
715pub struct TokenSymbolStr {
716    /// The raw representation, used for `Hash` and `Eq`.
717    raw: String,
718    /// Maintain the duality with the actual implementation.
719    encoded: TokenSymbol,
720}
721
722impl AsRef<TokenSymbol> for TokenSymbolStr {
723    fn as_ref(&self) -> &TokenSymbol {
724        &self.encoded
725    }
726}
727
728impl std::fmt::Display for TokenSymbolStr {
729    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
730        f.write_str(&self.raw)
731    }
732}
733
734impl FromStr for TokenSymbolStr {
735    // note: we re-use the error type
736    type Err = TokenSymbolError;
737    fn from_str(s: &str) -> Result<Self, Self::Err> {
738        Ok(Self {
739            encoded: TokenSymbol::new(s)?,
740            raw: s.to_string(),
741        })
742    }
743}
744
745impl Eq for TokenSymbolStr {}
746
747impl From<TokenSymbolStr> for TokenSymbol {
748    fn from(value: TokenSymbolStr) -> Self {
749        value.encoded
750    }
751}
752
753impl From<TokenSymbol> for TokenSymbolStr {
754    fn from(symbol: TokenSymbol) -> Self {
755        let raw = symbol.to_string();
756        Self { raw, encoded: symbol }
757    }
758}
759
760impl Ord for TokenSymbolStr {
761    fn cmp(&self, other: &Self) -> Ordering {
762        self.raw.cmp(&other.raw)
763    }
764}
765
766impl PartialOrd for TokenSymbolStr {
767    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
768        Some(self.cmp(other))
769    }
770}
771
772impl std::hash::Hash for TokenSymbolStr {
773    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
774        self.raw.hash::<H>(state);
775    }
776}
777
778impl Serialize for TokenSymbolStr {
779    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
780    where
781        S: serde::Serializer,
782    {
783        serializer.serialize_str(&self.raw)
784    }
785}
786
787impl<'de> Deserialize<'de> for TokenSymbolStr {
788    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
789    where
790        D: serde::Deserializer<'de>,
791    {
792        deserializer.deserialize_str(TokenSymbolVisitor)
793    }
794}
795
796use serde::de::Visitor;
797
798struct TokenSymbolVisitor;
799
800impl Visitor<'_> for TokenSymbolVisitor {
801    type Value = TokenSymbolStr;
802
803    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
804        formatter.write_str("1 to 6 uppercase ascii letters")
805    }
806
807    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
808    where
809        E: serde::de::Error,
810    {
811        let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
812        let raw = v.to_string();
813        Ok(TokenSymbolStr { raw, encoded })
814    }
815}