diff --git a/CHANGELOG.md b/CHANGELOG.md index e53a155f7e..ef44c15496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ * [FIX][rust] `Client::prove_transaction_with` now checks that the `TransactionProver` returned a proof of the transaction it was asked to prove, rejecting a mismatch with the new `ClientError::MismatchedProvenTransaction` ([#2391](https://github.com/0xMiden/rust-sdk/pull/2391)). * [FIX][cli] `miden-client notes --show` now prints the note sender in the `Sender` row; it was printing the note tag there ([#2412](https://github.com/0xMiden/rust-sdk/pull/2412)). * [FIX][rust] `VerifyingRpcClient::get_account` now validates that the returned `AccountProof` belongs to the requested account ID, rejecting a mismatch with `RpcError::InvalidResponse` ([#2419](https://github.com/0xMiden/rust-sdk/pull/2419)). +* [FIX][rust] `Client::fetch_remote_token_metadata` now rejects a faucet whose token config reports more decimals than `FungibleFaucet::MAX_DECIMALS`, instead of caching the out-of-range value and rendering every balance for that faucet with it ([#2423](https://github.com/0xMiden/rust-sdk/pull/2423)). ## 0.16.0-alpha.1 (2026-07-17) diff --git a/crates/rust-client/src/account/mod.rs b/crates/rust-client/src/account/mod.rs index 12983bee7d..ed57a3977f 100644 --- a/crates/rust-client/src/account/mod.rs +++ b/crates/rust-client/src/account/mod.rs @@ -113,6 +113,24 @@ impl Deserializable for FaucetMetadata { } } +/// Decodes a fungible faucet token config slot value into display metadata. +/// +/// Returns `None` when the value does not describe a fungible faucet config the protocol would +/// accept: the symbol must decode as a [`TokenSymbol`], and the decimals must be within +/// [`FungibleFaucet::MAX_DECIMALS`], which is what [`FungibleFaucet`] enforces when the component +/// is built. +fn faucet_metadata_from_token_config(token_config: [Felt; 4]) -> Option { + let [_token_supply, _max_supply, decimals, symbol] = token_config; + + let symbol = TokenSymbol::try_from(symbol).ok()?; + let decimals = u8::try_from(decimals.as_canonical_u64()).ok()?; + if decimals > FungibleFaucet::MAX_DECIMALS { + return None; + } + + Some(FaucetMetadata { symbol: symbol.to_string(), decimals }) +} + mod account_reader; pub use account_reader::AccountReader; /// Raw access to `miden-standards` account modules for items not curated by `miden-client`. @@ -448,14 +466,7 @@ impl Client { return Ok(None); }; - let [_token_supply, _max_supply, decimals, symbol] = *slot_header.value(); - let Ok(symbol) = TokenSymbol::try_from(symbol) else { - return Ok(None); - }; - let Ok(decimals) = u8::try_from(decimals.as_canonical_u64()) else { - return Ok(None); - }; - Ok(Some(FaucetMetadata { symbol: symbol.to_string(), decimals })) + Ok(faucet_metadata_from_token_config(*slot_header.value())) } /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`]. If @@ -694,3 +705,57 @@ mod schema_commitment_tests { assert_ne!(commitment, EMPTY_WORD); } } + +#[cfg(test)] +mod faucet_metadata_tests { + use miden_protocol::Felt; + + use super::{FungibleFaucet, TokenSymbol, faucet_metadata_from_token_config}; + + /// Builds a token config slot value carrying the given decimals and the symbol "TST". + fn token_config(decimals: u32) -> [Felt; 4] { + [ + Felt::from(0u32), + Felt::from(0u32), + Felt::from(decimals), + TokenSymbol::new("TST").unwrap().as_element(), + ] + } + + #[test] + fn decodes_a_config_within_the_protocol_bounds() { + let metadata = faucet_metadata_from_token_config(token_config(8)).unwrap(); + + assert_eq!(metadata.symbol, "TST"); + assert_eq!(metadata.decimals, 8); + } + + #[test] + fn accepts_the_maximum_supported_decimals() { + let max = u32::from(FungibleFaucet::MAX_DECIMALS); + let metadata = faucet_metadata_from_token_config(token_config(max)).unwrap(); + + assert_eq!(metadata.decimals, FungibleFaucet::MAX_DECIMALS); + } + + #[test] + fn rejects_decimals_above_the_maximum() { + let above_max = u32::from(FungibleFaucet::MAX_DECIMALS) + 1; + + assert!(faucet_metadata_from_token_config(token_config(above_max)).is_none()); + assert!(faucet_metadata_from_token_config(token_config(200)).is_none()); + } + + #[test] + fn rejects_decimals_that_do_not_fit_a_u8() { + assert!(faucet_metadata_from_token_config(token_config(300)).is_none()); + } + + #[test] + fn rejects_a_symbol_that_is_not_a_token_symbol() { + let mut config = token_config(8); + config[3] = Felt::from(0u32); + + assert!(faucet_metadata_from_token_config(config).is_none()); + } +}