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