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