diff --git a/packages/icrc-ledger-agent/src/lib.rs b/packages/icrc-ledger-agent/src/lib.rs index 6fc8f817d189..37d3b9f86136 100644 --- a/packages/icrc-ledger-agent/src/lib.rs +++ b/packages/icrc-ledger-agent/src/lib.rs @@ -18,7 +18,8 @@ use icrc_ledger_types::icrc3::archive::{ArchivedRange, QueryBlockArchiveFn}; use icrc_ledger_types::icrc3::blocks::ICRC3DataCertificate; use icrc_ledger_types::icrc3::blocks::{GetBlocksRequest, GetBlocksResponse}; use icrc_ledger_types::{ - icrc::generic_metadata_value::MetadataValue as Value, icrc3::blocks::BlockRange, + icrc::generic_metadata_value::MetadataValue as Value, icrc::metadata_key::MetadataKey, + icrc3::blocks::BlockRange, }; #[derive(Debug)] @@ -126,15 +127,18 @@ impl Icrc1Agent { } /// Returns the list of metadata entries for this ledger - pub async fn metadata(&self, mode: CallMode) -> Result, Icrc1AgentError> { + pub async fn metadata( + &self, + mode: CallMode, + ) -> Result, Icrc1AgentError> { Ok(match mode { CallMode::Query => Decode!( &self.query("icrc1_metadata", &Encode!()?).await?, - Vec<(String, Value)> + Vec<(MetadataKey, Value)> )?, CallMode::Update => Decode!( &self.update("icrc1_metadata", &Encode!()?).await?, - Vec<(String, Value)> + Vec<(MetadataKey, Value)> )?, }) } diff --git a/packages/icrc-ledger-client/src/lib.rs b/packages/icrc-ledger-client/src/lib.rs index d9c498483944..42f1beaf1479 100644 --- a/packages/icrc-ledger-client/src/lib.rs +++ b/packages/icrc-ledger-client/src/lib.rs @@ -3,6 +3,7 @@ use candid::Principal; use candid::types::number::Nat; use candid::utils::{ArgumentDecoder, ArgumentEncoder}; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as Value; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferArg, TransferError}; use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; @@ -55,7 +56,7 @@ impl ICRC1Client { .map(untuple) } - pub async fn metadata(&self) -> Result, (i32, String)> { + pub async fn metadata(&self) -> Result, (i32, String)> { self.runtime .call(self.ledger_canister_id, "icrc1_metadata", ()) .await diff --git a/packages/icrc-ledger-types/src/icrc/generic_metadata_value.rs b/packages/icrc-ledger-types/src/icrc/generic_metadata_value.rs index bb5f4d169cde..b3a1844b314a 100644 --- a/packages/icrc-ledger-types/src/icrc/generic_metadata_value.rs +++ b/packages/icrc-ledger-types/src/icrc/generic_metadata_value.rs @@ -2,6 +2,8 @@ use candid::{CandidType, Deserialize, Int, Nat}; use serde::Serialize; use serde_bytes::ByteBuf; +pub use crate::icrc::metadata_key::{MetadataKey, MetadataKeyError}; + /// Variant type for the `icrc1_metadata` endpoint values. The corresponding metadata keys are /// arbitrary Unicode strings and must follow the pattern `:`, where `` /// is a string not containing colons. The namespace `icrc1` is reserved for keys defined in the @@ -17,9 +19,20 @@ pub enum MetadataValue { } impl MetadataValue { - /// Create a `(String, MetadataValue)` tuple for use in metadata maps. - pub fn entry(key: impl ToString, val: impl Into) -> (String, Self) { - (key.to_string(), val.into()) + /// Create a `(MetadataKey, MetadataValue)` tuple for use in metadata maps. + /// + /// The key must be a valid metadata key in the format `:`. + /// This is typically used with the predefined constants like `MetadataKey::ICRC1_NAME`. + /// + /// # Errors + /// + /// Returns an error if the key is not a valid metadata key format. + pub fn entry( + key: &str, + val: impl Into, + ) -> Result<(MetadataKey, Self), MetadataKeyError> { + let metadata_key = MetadataKey::parse(key)?; + Ok((metadata_key, val.into())) } } @@ -76,3 +89,21 @@ impl<'a> From<&'a [u8]> for MetadataValue { MetadataValue::Blob(ByteBuf::from(bytes.to_vec())) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_metadata_value_entry() { + let entry = MetadataValue::entry(MetadataKey::ICRC1_NAME, "My Token").unwrap(); + assert_eq!(entry.0.as_str(), "icrc1:name"); + assert_eq!(entry.1, MetadataValue::Text("My Token".to_string())); + } + + #[test] + fn test_metadata_value_entry_invalid_key() { + let result = MetadataValue::entry("invalid_no_colon", "value"); + assert!(result.is_err()); + } +} diff --git a/packages/icrc-ledger-types/src/icrc/metadata_key.rs b/packages/icrc-ledger-types/src/icrc/metadata_key.rs new file mode 100644 index 000000000000..3a556b61d47f --- /dev/null +++ b/packages/icrc-ledger-types/src/icrc/metadata_key.rs @@ -0,0 +1,314 @@ +//! Metadata key types for ICRC-1 ledger metadata. + +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// Error type for invalid metadata key format. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MetadataKeyError { + /// The key does not contain a colon separator. + MissingColon, + /// The namespace (part before the first colon) contains a colon. + ColonInNamespace, + /// The namespace is empty. + EmptyNamespace, + /// The key part (after the colon) is empty. + EmptyKey, +} + +impl fmt::Display for MetadataKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MetadataKeyError::MissingColon => { + write!(f, "metadata key must contain a colon separator") + } + MetadataKeyError::ColonInNamespace => { + write!(f, "namespace must not contain colons") + } + MetadataKeyError::EmptyNamespace => { + write!(f, "namespace must not be empty") + } + MetadataKeyError::EmptyKey => { + write!(f, "key part must not be empty") + } + } + } +} + +impl std::error::Error for MetadataKeyError {} + +/// A metadata key for ICRC-1 ledger metadata. +/// +/// Metadata keys should follow the pattern `:`, where `` is a string +/// not containing colons. The namespace `icrc1` is reserved for keys defined in the ICRC-1 standard. +/// +/// For more information, see the +/// [documentation of Metadata in the ICRC-1 standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#metadata). +/// +/// # Examples +/// +/// ``` +/// use icrc_ledger_types::icrc::metadata_key::MetadataKey; +/// +/// // Using parse (validates the format) +/// let key = MetadataKey::parse("icrc1:name").unwrap(); +/// assert_eq!(key.namespace(), Some("icrc1")); +/// assert_eq!(key.key(), Some("name")); +/// +/// // Using new (validates the format) +/// let key = MetadataKey::new("icrc1", "symbol").unwrap(); +/// assert_eq!(key.as_str(), "icrc1:symbol"); +/// ``` +#[derive( + CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub struct MetadataKey(String); + +impl MetadataKey { + /// The name of the token. + /// When present, should be the same as the result of the icrc1_name query call. + pub const ICRC1_NAME: &'static str = "icrc1:name"; + + /// The token currency code. + /// When present, should be the same as the result of the icrc1_symbol query call. + pub const ICRC1_SYMBOL: &'static str = "icrc1:symbol"; + + /// The number of decimals the token uses. For example, 8 means to divide the token amount by 10^8 to get its user representation. + /// When present, should be the same as the result of the icrc1_decimals query call. + pub const ICRC1_DECIMALS: &'static str = "icrc1:decimals"; + + /// The default transfer fee. + /// When present, should be the same as the result of the icrc1_fee query call. + pub const ICRC1_FEE: &'static str = "icrc1:fee"; + + /// The URL of the token logo. The value can contain the actual image if it's a Data URL. + pub const ICRC1_LOGO: &'static str = "icrc1:logo"; + + /// The maximum length of a memo in bytes. + pub const ICRC1_MAX_MEMO_LENGTH: &'static str = "icrc1:max_memo_length"; + + /// Whether allowance data is public or not. + pub const ICRC103_PUBLIC_ALLOWANCES: &'static str = "icrc103:public_allowances"; + + /// The maximum number of allowances the ledger will return in response to a query. + pub const ICRC103_MAX_TAKE_VALUE: &'static str = "icrc103:max_take_value"; + + /// The textual representation of the principal of the associated index canister. + pub const ICRC106_INDEX_PRINCIPAL: &'static str = "icrc106:index_principal"; + + /// Creates a new validated metadata key from namespace and key parts. + /// + /// # Errors + /// + /// Returns an error if: + /// - The namespace is empty + /// - The namespace contains a colon + /// - The key is empty + pub fn new(namespace: &str, key: &str) -> Result { + if namespace.is_empty() { + return Err(MetadataKeyError::EmptyNamespace); + } + if namespace.contains(':') { + return Err(MetadataKeyError::ColonInNamespace); + } + if key.is_empty() { + return Err(MetadataKeyError::EmptyKey); + } + Ok(Self(format!("{namespace}:{key}"))) + } + + /// Parses a metadata key from a string in the format `:`. + /// + /// # Errors + /// + /// Returns an error if the string does not follow the required format. + pub fn parse(s: &str) -> Result { + validate_key_format(s)?; + Ok(Self(s.to_string())) + } + + /// Creates a metadata key from a string without validation. + /// + /// # Warning + /// + /// This bypasses validation. Using `namespace()` or `key()` on an invalid key will panic. + /// This is intended for backwards compatibility with ledgers that may have stored + /// invalid metadata keys. + pub fn unchecked_from_string(s: impl Into) -> Self { + Self(s.into()) + } + + /// Returns the full key as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Returns `true` if this key follows the valid `:` format. + pub fn is_valid(&self) -> bool { + validate_key_format(&self.0).is_ok() + } + + /// Returns the namespace part of the key (before the colon). + /// + /// Returns `None` if the key does not contain a colon (i.e., was created with + /// `unchecked_from_string` with an invalid format). + pub fn namespace(&self) -> Option<&str> { + self.0.find(':').map(|pos| &self.0[..pos]) + } + + /// Returns the key part (after the colon). + /// + /// Returns `None` if the key does not contain a colon (i.e., was created with + /// `unchecked_from_string` with an invalid format). + pub fn key(&self) -> Option<&str> { + self.0.find(':').map(|pos| &self.0[pos + 1..]) + } +} + +fn validate_key_format(s: &str) -> Result<(), MetadataKeyError> { + let colon_pos = s.find(':').ok_or(MetadataKeyError::MissingColon)?; + let namespace = &s[..colon_pos]; + let key = &s[colon_pos + 1..]; + + if namespace.is_empty() { + return Err(MetadataKeyError::EmptyNamespace); + } + if key.is_empty() { + return Err(MetadataKeyError::EmptyKey); + } + Ok(()) +} + +impl fmt::Display for MetadataKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for MetadataKey { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl From for String { + fn from(key: MetadataKey) -> Self { + key.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_metadata_key_new() { + let key = MetadataKey::new("icrc1", "name").unwrap(); + assert_eq!(key.namespace(), Some("icrc1")); + assert_eq!(key.key(), Some("name")); + assert_eq!(key.as_str(), "icrc1:name"); + } + + #[test] + fn test_metadata_key_parse() { + let key = MetadataKey::parse("myapp:decimals").unwrap(); + assert_eq!(key.namespace(), Some("myapp")); + assert_eq!(key.key(), Some("decimals")); + } + + #[test] + fn test_metadata_key_with_colons_in_value() { + // Key part can contain colons + let key = MetadataKey::parse("myapp:some:complex:key").unwrap(); + assert_eq!(key.namespace(), Some("myapp")); + assert_eq!(key.key(), Some("some:complex:key")); + } + + #[test] + fn test_metadata_key_empty_namespace() { + assert_eq!( + MetadataKey::new("", "name"), + Err(MetadataKeyError::EmptyNamespace) + ); + assert_eq!( + MetadataKey::parse(":name"), + Err(MetadataKeyError::EmptyNamespace) + ); + } + + #[test] + fn test_metadata_key_empty_key() { + assert_eq!( + MetadataKey::new("icrc1", ""), + Err(MetadataKeyError::EmptyKey) + ); + assert_eq!( + MetadataKey::parse("icrc1:"), + Err(MetadataKeyError::EmptyKey) + ); + } + + #[test] + fn test_metadata_key_missing_colon() { + assert_eq!( + MetadataKey::parse("nonamespace"), + Err(MetadataKeyError::MissingColon) + ); + } + + #[test] + fn test_metadata_key_colon_in_namespace() { + assert_eq!( + MetadataKey::new("bad:namespace", "key"), + Err(MetadataKeyError::ColonInNamespace) + ); + } + + #[test] + fn test_metadata_key_display() { + let key = MetadataKey::new("icrc1", "symbol").unwrap(); + assert_eq!(format!("{}", key), "icrc1:symbol"); + } + + #[test] + fn test_metadata_key_into_string() { + let key = MetadataKey::new("icrc1", "decimals").unwrap(); + let s: String = key.into(); + assert_eq!(s, "icrc1:decimals"); + } + + #[test] + fn test_metadata_key_is_valid() { + assert!(MetadataKey::new("icrc1", "name").unwrap().is_valid()); + assert!(MetadataKey::parse("app:key").unwrap().is_valid()); + + let invalid = MetadataKey::unchecked_from_string("nocolon"); + assert!(!invalid.is_valid()); + } + + #[test] + fn test_metadata_key_unchecked_from_string() { + let key = MetadataKey::unchecked_from_string("icrc1:name"); + assert_eq!(key.namespace(), Some("icrc1")); + assert_eq!(key.key(), Some("name")); + assert!(key.is_valid()); + } + + #[test] + fn test_metadata_key_namespace_and_key_return_none_for_invalid() { + let invalid = MetadataKey::unchecked_from_string("nocolon"); + assert_eq!(invalid.namespace(), None); + assert_eq!(invalid.key(), None); + } + + #[test] + fn test_metadata_key_candid_roundtrip() { + use candid::{Decode, Encode}; + + let key = MetadataKey::parse("icrc1:name").unwrap(); + let encoded = Encode!(&key).unwrap(); + let decoded = Decode!(&encoded, MetadataKey).unwrap(); + assert_eq!(decoded, key); + } +} diff --git a/packages/icrc-ledger-types/src/icrc/mod.rs b/packages/icrc-ledger-types/src/icrc/mod.rs index 3ed31f228878..41f92466dcac 100644 --- a/packages/icrc-ledger-types/src/icrc/mod.rs +++ b/packages/icrc-ledger-types/src/icrc/mod.rs @@ -3,3 +3,4 @@ pub mod generic_metadata_value; pub mod generic_value; pub mod generic_value_predicate; +pub mod metadata_key; diff --git a/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs b/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs index 9a53bf66f02d..3fccca9a48d3 100644 --- a/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs +++ b/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs @@ -907,6 +907,7 @@ fn icrc1_ledger_init_arg( ) -> LedgerInitArgs { use ic_icrc1_ledger::FeatureFlags as LedgerFeatureFlags; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as LedgerMetadataValue; + use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::account::Account as LedgerAccount; const LEDGER_FEE_SUBACCOUNT: [u8; 32] = [ @@ -928,7 +929,7 @@ fn icrc1_ledger_init_arg( token_name: ledger_init_arg.token_name, token_symbol: ledger_init_arg.token_symbol, metadata: vec![( - "icrc1:logo".to_string(), + MetadataKey::ICRC1_LOGO.to_string(), LedgerMetadataValue::from(ledger_init_arg.token_logo), )], archive_options: icrc1_archive_options( diff --git a/rs/ethereum/ledger-suite-orchestrator/test_utils/src/flow.rs b/rs/ethereum/ledger-suite-orchestrator/test_utils/src/flow.rs index 9c0302a4e549..3bc0e064cbdf 100644 --- a/rs/ethereum/ledger-suite-orchestrator/test_utils/src/flow.rs +++ b/rs/ethereum/ledger-suite-orchestrator/test_utils/src/flow.rs @@ -1,7 +1,7 @@ use crate::universal_canister::UniversalCanister; use crate::{ - LedgerAccount, LedgerMetadataValue, LedgerSuiteOrchestrator, MINTER_PRINCIPAL, assert_reply, - ledger_wasm, out_of_band_upgrade, stop_canister, + LedgerAccount, LedgerMetadataKey, LedgerMetadataValue, LedgerSuiteOrchestrator, + MINTER_PRINCIPAL, assert_reply, ledger_wasm, out_of_band_upgrade, stop_canister, }; use candid::{Decode, Encode, Nat, Principal}; use ic_base_types::{CanisterId, PrincipalId}; @@ -453,4 +453,7 @@ assert_ledger!("icrc1_decimals", u8); assert_ledger!("icrc1_total_supply", Nat); assert_ledger!("icrc1_fee", Nat); assert_ledger!("icrc1_minting_account", Option); -assert_ledger!("icrc1_metadata", Vec<(String, LedgerMetadataValue)>); +assert_ledger!( + "icrc1_metadata", + Vec<(LedgerMetadataKey, LedgerMetadataValue)> +); diff --git a/rs/ethereum/ledger-suite-orchestrator/test_utils/src/lib.rs b/rs/ethereum/ledger-suite-orchestrator/test_utils/src/lib.rs index a055f6ead238..db4c3940df61 100644 --- a/rs/ethereum/ledger-suite-orchestrator/test_utils/src/lib.rs +++ b/rs/ethereum/ledger-suite-orchestrator/test_utils/src/lib.rs @@ -17,6 +17,7 @@ use ic_metrics_assert::{CanisterHttpQuery, MetricsAssert}; use ic_state_machine_tests::{StateMachine, StateMachineBuilder, UserError, WasmResult}; use ic_types::Cycles; pub use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as LedgerMetadataValue; +pub use icrc_ledger_types::icrc::metadata_key::MetadataKey as LedgerMetadataKey; pub use icrc_ledger_types::icrc1::account::Account as LedgerAccount; use std::sync::Arc; diff --git a/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs b/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs index 318a0f4f9209..e1b76f278c3d 100644 --- a/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs +++ b/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs @@ -15,6 +15,7 @@ use ic_ledger_suite_orchestrator_test_utils::{ }; use ic_state_machine_tests::ErrorCode; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as LedgerMetadataValue; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::account::Account as LedgerAccount; use std::sync::Arc; @@ -57,39 +58,39 @@ fn should_spawn_ledger_with_correct_init_args() { }) .assert_ledger_icrc1_metadata(vec![ ( - "icrc1:logo".to_string(), + MetadataKey::parse(MetadataKey::ICRC1_LOGO).unwrap(), LedgerMetadataValue::from(CKETH_TOKEN_LOGO), ), ( - "icrc1:decimals".to_string(), + MetadataKey::parse(MetadataKey::ICRC1_DECIMALS).unwrap(), LedgerMetadataValue::from(6_u64), ), ( - "icrc1:name".to_string(), + MetadataKey::parse(MetadataKey::ICRC1_NAME).unwrap(), LedgerMetadataValue::from("USD Coin"), ), ( - "icrc1:symbol".to_string(), + MetadataKey::parse(MetadataKey::ICRC1_SYMBOL).unwrap(), LedgerMetadataValue::from("USDC"), ), ( - "icrc1:fee".to_string(), + MetadataKey::parse(MetadataKey::ICRC1_FEE).unwrap(), LedgerMetadataValue::from(2_000_000_000_000_u64), ), ( - "icrc1:max_memo_length".to_string(), + MetadataKey::parse(MetadataKey::ICRC1_MAX_MEMO_LENGTH).unwrap(), LedgerMetadataValue::from(80_u64), ), ( - "icrc103:public_allowances".to_string(), + MetadataKey::parse(MetadataKey::ICRC103_PUBLIC_ALLOWANCES).unwrap(), LedgerMetadataValue::from("true"), ), ( - "icrc103:max_take_value".to_string(), + MetadataKey::parse(MetadataKey::ICRC103_MAX_TAKE_VALUE).unwrap(), LedgerMetadataValue::from(500u64), ), ( - "icrc106:index_principal".to_string(), + MetadataKey::parse(MetadataKey::ICRC106_INDEX_PRINCIPAL).unwrap(), LedgerMetadataValue::from("ryjl3-tyaaa-aaaaa-aaaba-cai"), ), ]); diff --git a/rs/ledger_suite/icp/ledger/src/main.rs b/rs/ledger_suite/icp/ledger/src/main.rs index a57291e9d79c..2cb6028d11f0 100644 --- a/rs/ledger_suite/icp/ledger/src/main.rs +++ b/rs/ledger_suite/icp/ledger/src/main.rs @@ -50,6 +50,7 @@ use icrc_ledger_types::icrc2::allowance::{Allowance, AllowanceArgs}; use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; use icrc_ledger_types::{ icrc::generic_metadata_value::MetadataValue as Value, + icrc::metadata_key::MetadataKey, icrc3::archive::QueryArchiveFn, icrc21::lib::{ build_icrc21_consent_info, build_icrc21_consent_info_for_icrc1_and_icrc2_endpoints, @@ -486,15 +487,24 @@ fn transfer_fee(_: TransferFeeArgs) -> TransferFee { } #[query] -fn icrc1_metadata() -> Vec<(String, Value)> { +fn icrc1_metadata() -> Vec<(MetadataKey, Value)> { vec![ - Value::entry("icrc1:decimals", DECIMAL_PLACES as u64), - Value::entry("icrc1:name", LEDGER.read().unwrap().token_name.to_string()), + Value::entry(MetadataKey::ICRC1_DECIMALS, DECIMAL_PLACES as u64).unwrap(), Value::entry( - "icrc1:symbol", + MetadataKey::ICRC1_NAME, + LEDGER.read().unwrap().token_name.to_string(), + ) + .unwrap(), + Value::entry( + MetadataKey::ICRC1_SYMBOL, LEDGER.read().unwrap().token_symbol.to_string(), - ), - Value::entry("icrc1:fee", LEDGER.read().unwrap().transfer_fee.get_e8s()), + ) + .unwrap(), + Value::entry( + MetadataKey::ICRC1_FEE, + LEDGER.read().unwrap().transfer_fee.get_e8s(), + ) + .unwrap(), ] } diff --git a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs index 20e533b292ef..257b29e29ade 100644 --- a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs +++ b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u256.rs @@ -12,6 +12,7 @@ use canbench_rs::{BenchResult, bench}; use candid::{Nat, Principal}; use ic_icrc1_ledger::{FeatureFlags, InitArgs, InitArgsBuilder}; use ic_ledger_canister_core::archive::ArchiveOptions; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::transfer::TransferArg; use icrc_ledger_types::icrc2::approve::ApproveArgs; use icrc_ledger_types::icrc3::blocks::GetBlocksRequest; @@ -138,7 +139,7 @@ fn cketh_ledger_init_args_with_archive() -> InitArgs { .with_token_symbol("ckETH") .with_token_name("ckETH") .with_feature_flags(FeatureFlags { icrc2: true }) - .with_metadata_entry("icrc1:logo", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQ2IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDE0NiAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDYiIGhlaWdodD0iMTQ2IiByeD0iNzMiIGZpbGw9IiMzQjAwQjkiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNi4zODM3IDc3LjIwNTJDMTguNDM0IDEwNS4yMDYgNDAuNzk0IDEyNy41NjYgNjguNzk0OSAxMjkuNjE2VjEzNS45NEMzNy4zMDg3IDEzMy44NjcgMTIuMTMzIDEwOC42OTEgMTAuMDYwNSA3Ny4yMDUySDE2LjM4MzdaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMTEwXzU4NikiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik02OC43NjQ2IDE2LjM1MzRDNDAuNzYzOCAxOC40MDM2IDE4LjQwMzcgNDAuNzYzNyAxNi4zNTM1IDY4Ljc2NDZMMTAuMDMwMyA2OC43NjQ2QzEyLjEwMjcgMzcuMjc4NCAzNy4yNzg1IDEyLjEwMjYgNjguNzY0NiAxMC4wMzAyTDY4Ljc2NDYgMTYuMzUzNFoiIGZpbGw9IiMyOUFCRTIiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMjkuNjE2IDY4LjczNDNDMTI3LjU2NiA0MC43MzM0IDEwNS4yMDYgMTguMzczMyA3Ny4yMDUxIDE2LjMyMzFMNzcuMjA1MSA5Ljk5OTk4QzEwOC42OTEgMTIuMDcyNCAxMzMuODY3IDM3LjI0ODEgMTM1LjkzOSA2OC43MzQzTDEyOS42MTYgNjguNzM0M1oiIGZpbGw9InVybCgjcGFpbnQxX2xpbmVhcl8xMTBfNTg2KSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTc3LjIzNTQgMTI5LjU4NkMxMDUuMjM2IDEyNy41MzYgMTI3LjU5NiAxMDUuMTc2IDEyOS42NDcgNzcuMTc0OUwxMzUuOTcgNzcuMTc0OUMxMzMuODk3IDEwOC42NjEgMTA4LjcyMiAxMzMuODM3IDc3LjIzNTQgMTM1LjkwOUw3Ny4yMzU0IDEyOS41ODZaIiBmaWxsPSIjMjlBQkUyIi8+CjxwYXRoIGQ9Ik03My4xOTA0IDMxVjYxLjY4MThMOTkuMTIzIDczLjI2OTZMNzMuMTkwNCAzMVoiIGZpbGw9IndoaXRlIiBmaWxsLW9wYWNpdHk9IjAuNiIvPgo8cGF0aCBkPSJNNzMuMTkwNCAzMUw0Ny4yNTQ0IDczLjI2OTZMNzMuMTkwNCA2MS42ODE4VjMxWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTczLjE5MDQgOTMuMTUyM1YxMTRMOTkuMTQwMyA3OC4wOTg0TDczLjE5MDQgOTMuMTUyM1oiIGZpbGw9IndoaXRlIiBmaWxsLW9wYWNpdHk9IjAuNiIvPgo8cGF0aCBkPSJNNzMuMTkwNCAxMTRWOTMuMTQ4OEw0Ny4yNTQ0IDc4LjA5ODRMNzMuMTkwNCAxMTRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzMuMTkwNCA4OC4zMjY5TDk5LjEyMyA3My4yNjk2TDczLjE5MDQgNjEuNjg4N1Y4OC4zMjY5WiIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC4yIi8+CjxwYXRoIGQ9Ik00Ny4yNTQ0IDczLjI2OTZMNzMuMTkwNCA4OC4zMjY5VjYxLjY4ODdMNDcuMjU0NCA3My4yNjk2WiIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC42Ii8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMTEwXzU4NiIgeDE9IjUzLjQ3MzYiIHkxPSIxMjIuNzkiIHgyPSIxNC4wMzYyIiB5Mj0iODkuNTc4NiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjEiIHN0b3AtY29sb3I9IiNFRDFFNzkiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTIyNzg1Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8xMTBfNTg2IiB4MT0iMTIwLjY1IiB5MT0iNTUuNjAyMSIgeDI9IjgxLjIxMyIgeTI9IjIyLjM5MTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjIxIiBzdG9wLWNvbG9yPSIjRjE1QTI0Ii8+CjxzdG9wIG9mZnNldD0iMC42ODQxIiBzdG9wLWNvbG9yPSIjRkJCMDNCIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==") + .with_metadata_entry(MetadataKey::ICRC1_LOGO, "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQ2IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDE0NiAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDYiIGhlaWdodD0iMTQ2IiByeD0iNzMiIGZpbGw9IiMzQjAwQjkiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNi4zODM3IDc3LjIwNTJDMTguNDM0IDEwNS4yMDYgNDAuNzk0IDEyNy41NjYgNjguNzk0OSAxMjkuNjE2VjEzNS45NEMzNy4zMDg3IDEzMy44NjcgMTIuMTMzIDEwOC42OTEgMTAuMDYwNSA3Ny4yMDUySDE2LjM4MzdaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMTEwXzU4NikiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik02OC43NjQ2IDE2LjM1MzRDNDAuNzYzOCAxOC40MDM2IDE4LjQwMzcgNDAuNzYzNyAxNi4zNTM1IDY4Ljc2NDZMMTAuMDMwMyA2OC43NjQ2QzEyLjEwMjcgMzcuMjc4NCAzNy4yNzg1IDEyLjEwMjYgNjguNzY0NiAxMC4wMzAyTDY4Ljc2NDYgMTYuMzUzNFoiIGZpbGw9IiMyOUFCRTIiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMjkuNjE2IDY4LjczNDNDMTI3LjU2NiA0MC43MzM0IDEwNS4yMDYgMTguMzczMyA3Ny4yMDUxIDE2LjMyMzFMNzcuMjA1MSA5Ljk5OTk4QzEwOC42OTEgMTIuMDcyNCAxMzMuODY3IDM3LjI0ODEgMTM1LjkzOSA2OC43MzQzTDEyOS42MTYgNjguNzM0M1oiIGZpbGw9InVybCgjcGFpbnQxX2xpbmVhcl8xMTBfNTg2KSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTc3LjIzNTQgMTI5LjU4NkMxMDUuMjM2IDEyNy41MzYgMTI3LjU5NiAxMDUuMTc2IDEyOS42NDcgNzcuMTc0OUwxMzUuOTcgNzcuMTc0OUMxMzMuODk3IDEwOC42NjEgMTA4LjcyMiAxMzMuODM3IDc3LjIzNTQgMTM1LjkwOUw3Ny4yMzU0IDEyOS41ODZaIiBmaWxsPSIjMjlBQkUyIi8+CjxwYXRoIGQ9Ik03My4xOTA0IDMxVjYxLjY4MThMOTkuMTIzIDczLjI2OTZMNzMuMTkwNCAzMVoiIGZpbGw9IndoaXRlIiBmaWxsLW9wYWNpdHk9IjAuNiIvPgo8cGF0aCBkPSJNNzMuMTkwNCAzMUw0Ny4yNTQ0IDczLjI2OTZMNzMuMTkwNCA2MS42ODE4VjMxWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTczLjE5MDQgOTMuMTUyM1YxMTRMOTkuMTQwMyA3OC4wOTg0TDczLjE5MDQgOTMuMTUyM1oiIGZpbGw9IndoaXRlIiBmaWxsLW9wYWNpdHk9IjAuNiIvPgo8cGF0aCBkPSJNNzMuMTkwNCAxMTRWOTMuMTQ4OEw0Ny4yNTQ0IDc4LjA5ODRMNzMuMTkwNCAxMTRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzMuMTkwNCA4OC4zMjY5TDk5LjEyMyA3My4yNjk2TDczLjE5MDQgNjEuNjg4N1Y4OC4zMjY5WiIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC4yIi8+CjxwYXRoIGQ9Ik00Ny4yNTQ0IDczLjI2OTZMNzMuMTkwNCA4OC4zMjY5VjYxLjY4ODdMNDcuMjU0NCA3My4yNjk2WiIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC42Ii8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMTEwXzU4NiIgeDE9IjUzLjQ3MzYiIHkxPSIxMjIuNzkiIHgyPSIxNC4wMzYyIiB5Mj0iODkuNTc4NiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjEiIHN0b3AtY29sb3I9IiNFRDFFNzkiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTIyNzg1Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8xMTBfNTg2IiB4MT0iMTIwLjY1IiB5MT0iNTUuNjAyMSIgeDI9IjgxLjIxMyIgeTI9IjIyLjM5MTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjIxIiBzdG9wLWNvbG9yPSIjRjE1QTI0Ii8+CjxzdG9wIG9mZnNldD0iMC42ODQxIiBzdG9wLWNvbG9yPSIjRkJCMDNCIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==") .with_archive_options(ArchiveOptions { trigger_threshold: 2_000, num_blocks_to_archive: 1_0000, diff --git a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs index 36a270321635..9fe3e42f8033 100644 --- a/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs +++ b/rs/ledger_suite/icrc1/ledger/src/benches/benches_u64.rs @@ -10,6 +10,7 @@ use canbench_rs::{BenchResult, bench}; use candid::{Nat, Principal}; use ic_icrc1_ledger::{FeatureFlags, InitArgs, InitArgsBuilder}; use ic_ledger_canister_core::archive::ArchiveOptions; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::transfer::TransferArg; use icrc_ledger_types::icrc2::approve::ApproveArgs; @@ -137,7 +138,7 @@ fn ckbtc_ledger_init_args_with_archive() -> InitArgs { .with_token_symbol("ckBTC") .with_token_name("ckBTC") .with_feature_flags(FeatureFlags { icrc2: true }) - .with_metadata_entry("icrc1:logo", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQ2IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDE0NiAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDYiIGhlaWdodD0iMTQ2IiByeD0iNzMiIGZpbGw9IiMzQjAwQjkiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNi4zODM3IDc3LjIwNTJDMTguNDM0IDEwNS4yMDYgNDAuNzk0IDEyNy41NjYgNjguNzk0OSAxMjkuNjE2VjEzNS45MzlDMzcuMzA4NyAxMzMuODY3IDEyLjEzMyAxMDguNjkxIDEwLjA2MDUgNzcuMjA1MkgxNi4zODM3WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzExMF81NzIpIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjguNzY0NiAxNi4zNTM0QzQwLjc2MzggMTguNDAzNiAxOC40MDM3IDQwLjc2MzcgMTYuMzUzNSA2OC43NjQ2TDEwLjAzMDMgNjguNzY0NkMxMi4xMDI3IDM3LjI3ODQgMzcuMjc4NSAxMi4xMDI2IDY4Ljc2NDYgMTAuMDMwMkw2OC43NjQ2IDE2LjM1MzRaIiBmaWxsPSIjMjlBQkUyIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTI5LjYxNiA2OC43MzQzQzEyNy41NjYgNDAuNzMzNSAxMDUuMjA2IDE4LjM3MzQgNzcuMjA1MSAxNi4zMjMyTDc3LjIwNTEgMTBDMTA4LjY5MSAxMi4wNzI0IDEzMy44NjcgMzcuMjQ4MiAxMzUuOTM5IDY4LjczNDNMMTI5LjYxNiA2OC43MzQzWiIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzExMF81NzIpIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNzcuMjM1NCAxMjkuNTg2QzEwNS4yMzYgMTI3LjUzNiAxMjcuNTk2IDEwNS4xNzYgMTI5LjY0NyA3Ny4xNzQ5TDEzNS45NyA3Ny4xNzQ5QzEzMy44OTcgMTA4LjY2MSAxMDguNzIyIDEzMy44MzcgNzcuMjM1NCAxMzUuOTA5TDc3LjIzNTQgMTI5LjU4NloiIGZpbGw9IiMyOUFCRTIiLz4KPHBhdGggZD0iTTk5LjgyMTcgNjQuNzI0NUMxMDEuMDE0IDU2Ljc1MzggOTQuOTQ0NyA1Mi40Njg5IDg2LjY0NTUgNDkuNjEwNEw4OS4zMzc2IDM4LjgxM0w4Mi43NjQ1IDM3LjE3NUw4MC4xNDM1IDQ3LjY4NzlDNzguNDE1NSA0Ny4yNTczIDc2LjY0MDYgNDYuODUxMSA3NC44NzcxIDQ2LjQ0ODdMNzcuNTE2OCAzNS44NjY1TDcwLjk0NzQgMzQuMjI4NUw2OC4yNTM0IDQ1LjAyMjJDNjYuODIzIDQ0LjY5NjUgNjUuNDE4OSA0NC4zNzQ2IDY0LjA1NiA0NC4wMzU3TDY0LjA2MzUgNDQuMDAyTDU0Ljk5ODUgNDEuNzM4OEw1My4yNDk5IDQ4Ljc1ODZDNTMuMjQ5OSA0OC43NTg2IDU4LjEyNjkgNDkuODc2MiA1OC4wMjM5IDQ5Ljk0NTRDNjAuNjg2MSA1MC42MSA2MS4xNjcyIDUyLjM3MTUgNjEuMDg2NyA1My43NjhDNTguNjI3IDYzLjYzNDUgNTYuMTcyMSA3My40Nzg4IDUzLjcxMDQgODMuMzQ2N0M1My4zODQ3IDg0LjE1NTQgNTIuNTU5MSA4NS4zNjg0IDUwLjY5ODIgODQuOTA3OUM1MC43NjM3IDg1LjAwMzQgNDUuOTIwNCA4My43MTU1IDQ1LjkyMDQgODMuNzE1NUw0Mi42NTcyIDkxLjIzODlMNTEuMjExMSA5My4zNzFDNTIuODAyNSA5My43Njk3IDU0LjM2MTkgOTQuMTg3MiA1NS44OTcxIDk0LjU4MDNMNTMuMTc2OSAxMDUuNTAxTDU5Ljc0MjYgMTA3LjEzOUw2Mi40MzY2IDk2LjMzNDNDNjQuMjMwMSA5Ni44MjEgNjUuOTcxMiA5Ny4yNzAzIDY3LjY3NDkgOTcuNjkzNEw2NC45OTAyIDEwOC40NDhMNzEuNTYzNCAxMTAuMDg2TDc0LjI4MzYgOTkuMTg1M0M4NS40OTIyIDEwMS4zMDYgOTMuOTIwNyAxMDAuNDUxIDk3LjQ2ODQgOTAuMzE0MUMxMDAuMzI3IDgyLjE1MjQgOTcuMzI2MSA3Ny40NDQ1IDkxLjQyODggNzQuMzc0NUM5NS43MjM2IDczLjM4NDIgOTguOTU4NiA3MC41NTk0IDk5LjgyMTcgNjQuNzI0NVpNODQuODAzMiA4NS43ODIxQzgyLjc3MiA5My45NDM4IDY5LjAyODQgODkuNTMxNiA2NC41NzI3IDg4LjQyNTNMNjguMTgyMiA3My45NTdDNzIuNjM4IDc1LjA2ODkgODYuOTI2MyA3Ny4yNzA0IDg0LjgwMzIgODUuNzgyMVpNODYuODM2NCA2NC42MDY2Qzg0Ljk4MyA3Mi4wMzA3IDczLjU0NDEgNjguMjU4OCA2OS44MzM1IDY3LjMzNEw3My4xMDYgNTQuMjExN0M3Ni44MTY2IDU1LjEzNjQgODguNzY2NiA1Ni44NjIzIDg2LjgzNjQgNjQuNjA2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMTEwXzU3MiIgeDE9IjUzLjQ3MzYiIHkxPSIxMjIuNzkiIHgyPSIxNC4wMzYyIiB5Mj0iODkuNTc4NiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjEiIHN0b3AtY29sb3I9IiNFRDFFNzkiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTIyNzg1Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8xMTBfNTcyIiB4MT0iMTIwLjY1IiB5MT0iNTUuNjAyMSIgeDI9IjgxLjIxMyIgeTI9IjIyLjM5MTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjIxIiBzdG9wLWNvbG9yPSIjRjE1QTI0Ii8+CjxzdG9wIG9mZnNldD0iMC42ODQxIiBzdG9wLWNvbG9yPSIjRkJCMDNCIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==") + .with_metadata_entry(MetadataKey::ICRC1_LOGO, "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQ2IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDE0NiAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDYiIGhlaWdodD0iMTQ2IiByeD0iNzMiIGZpbGw9IiMzQjAwQjkiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNi4zODM3IDc3LjIwNTJDMTguNDM0IDEwNS4yMDYgNDAuNzk0IDEyNy41NjYgNjguNzk0OSAxMjkuNjE2VjEzNS45MzlDMzcuMzA4NyAxMzMuODY3IDEyLjEzMyAxMDguNjkxIDEwLjA2MDUgNzcuMjA1MkgxNi4zODM3WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzExMF81NzIpIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjguNzY0NiAxNi4zNTM0QzQwLjc2MzggMTguNDAzNiAxOC40MDM3IDQwLjc2MzcgMTYuMzUzNSA2OC43NjQ2TDEwLjAzMDMgNjguNzY0NkMxMi4xMDI3IDM3LjI3ODQgMzcuMjc4NSAxMi4xMDI2IDY4Ljc2NDYgMTAuMDMwMkw2OC43NjQ2IDE2LjM1MzRaIiBmaWxsPSIjMjlBQkUyIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTI5LjYxNiA2OC43MzQzQzEyNy41NjYgNDAuNzMzNSAxMDUuMjA2IDE4LjM3MzQgNzcuMjA1MSAxNi4zMjMyTDc3LjIwNTEgMTBDMTA4LjY5MSAxMi4wNzI0IDEzMy44NjcgMzcuMjQ4MiAxMzUuOTM5IDY4LjczNDNMMTI5LjYxNiA2OC43MzQzWiIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzExMF81NzIpIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNzcuMjM1NCAxMjkuNTg2QzEwNS4yMzYgMTI3LjUzNiAxMjcuNTk2IDEwNS4xNzYgMTI5LjY0NyA3Ny4xNzQ5TDEzNS45NyA3Ny4xNzQ5QzEzMy44OTcgMTA4LjY2MSAxMDguNzIyIDEzMy44MzcgNzcuMjM1NCAxMzUuOTA5TDc3LjIzNTQgMTI5LjU4NloiIGZpbGw9IiMyOUFCRTIiLz4KPHBhdGggZD0iTTk5LjgyMTcgNjQuNzI0NUMxMDEuMDE0IDU2Ljc1MzggOTQuOTQ0NyA1Mi40Njg5IDg2LjY0NTUgNDkuNjEwNEw4OS4zMzc2IDM4LjgxM0w4Mi43NjQ1IDM3LjE3NUw4MC4xNDM1IDQ3LjY4NzlDNzguNDE1NSA0Ny4yNTczIDc2LjY0MDYgNDYuODUxMSA3NC44NzcxIDQ2LjQ0ODdMNzcuNTE2OCAzNS44NjY1TDcwLjk0NzQgMzQuMjI4NUw2OC4yNTM0IDQ1LjAyMjJDNjYuODIzIDQ0LjY5NjUgNjUuNDE4OSA0NC4zNzQ2IDY0LjA1NiA0NC4wMzU3TDY0LjA2MzUgNDQuMDAyTDU0Ljk5ODUgNDEuNzM4OEw1My4yNDk5IDQ4Ljc1ODZDNTMuMjQ5OSA0OC43NTg2IDU4LjEyNjkgNDkuODc2MiA1OC4wMjM5IDQ5Ljk0NTRDNjAuNjg2MSA1MC42MSA2MS4xNjcyIDUyLjM3MTUgNjEuMDg2NyA1My43NjhDNTguNjI3IDYzLjYzNDUgNTYuMTcyMSA3My40Nzg4IDUzLjcxMDQgODMuMzQ2N0M1My4zODQ3IDg0LjE1NTQgNTIuNTU5MSA4NS4zNjg0IDUwLjY5ODIgODQuOTA3OUM1MC43NjM3IDg1LjAwMzQgNDUuOTIwNCA4My43MTU1IDQ1LjkyMDQgODMuNzE1NUw0Mi42NTcyIDkxLjIzODlMNTEuMjExMSA5My4zNzFDNTIuODAyNSA5My43Njk3IDU0LjM2MTkgOTQuMTg3MiA1NS44OTcxIDk0LjU4MDNMNTMuMTc2OSAxMDUuNTAxTDU5Ljc0MjYgMTA3LjEzOUw2Mi40MzY2IDk2LjMzNDNDNjQuMjMwMSA5Ni44MjEgNjUuOTcxMiA5Ny4yNzAzIDY3LjY3NDkgOTcuNjkzNEw2NC45OTAyIDEwOC40NDhMNzEuNTYzNCAxMTAuMDg2TDc0LjI4MzYgOTkuMTg1M0M4NS40OTIyIDEwMS4zMDYgOTMuOTIwNyAxMDAuNDUxIDk3LjQ2ODQgOTAuMzE0MUMxMDAuMzI3IDgyLjE1MjQgOTcuMzI2MSA3Ny40NDQ1IDkxLjQyODggNzQuMzc0NUM5NS43MjM2IDczLjM4NDIgOTguOTU4NiA3MC41NTk0IDk5LjgyMTcgNjQuNzI0NVpNODQuODAzMiA4NS43ODIxQzgyLjc3MiA5My45NDM4IDY5LjAyODQgODkuNTMxNiA2NC41NzI3IDg4LjQyNTNMNjguMTgyMiA3My45NTdDNzIuNjM4IDc1LjA2ODkgODYuOTI2MyA3Ny4yNzA0IDg0LjgwMzIgODUuNzgyMVpNODYuODM2NCA2NC42MDY2Qzg0Ljk4MyA3Mi4wMzA3IDczLjU0NDEgNjguMjU4OCA2OS44MzM1IDY3LjMzNEw3My4xMDYgNTQuMjExN0M3Ni44MTY2IDU1LjEzNjQgODguNzY2NiA1Ni44NjIzIDg2LjgzNjQgNjQuNjA2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMTEwXzU3MiIgeDE9IjUzLjQ3MzYiIHkxPSIxMjIuNzkiIHgyPSIxNC4wMzYyIiB5Mj0iODkuNTc4NiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjEiIHN0b3AtY29sb3I9IiNFRDFFNzkiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTIyNzg1Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8xMTBfNTcyIiB4MT0iMTIwLjY1IiB5MT0iNTUuNjAyMSIgeDI9IjgxLjIxMyIgeTI9IjIyLjM5MTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjIxIiBzdG9wLWNvbG9yPSIjRjE1QTI0Ii8+CjxzdG9wIG9mZnNldD0iMC42ODQxIiBzdG9wLWNvbG9yPSIjRkJCMDNCIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==") .with_archive_options(ArchiveOptions { trigger_threshold: 2_000, num_blocks_to_archive: 1_0000, diff --git a/rs/ledger_suite/icrc1/ledger/src/lib.rs b/rs/ledger_suite/icrc1/ledger/src/lib.rs index 0d52629bc3cc..c3d1d34d9508 100644 --- a/rs/ledger_suite/icrc1/ledger/src/lib.rs +++ b/rs/ledger_suite/icrc1/ledger/src/lib.rs @@ -35,6 +35,7 @@ use ic_stable_structures::{DefaultMemoryImpl, StableBTreeMap}; use ic_stable_structures::{Storable, storable::Bound}; use icrc_ledger_types::{ icrc::generic_metadata_value::MetadataValue as Value, + icrc::metadata_key::MetadataKey, icrc3::archive::{ArchivedRange, QueryBlockArchiveFn, QueryTxArchiveFn}, }; use icrc_ledger_types::{ @@ -70,13 +71,6 @@ const MAX_TRANSACTIONS_TO_PURGE: usize = 100_000; #[allow(dead_code)] const MAX_U64_ENCODING_BYTES: usize = 10; const DEFAULT_MAX_MEMO_LENGTH: u16 = 32; -const METADATA_DECIMALS: &str = "icrc1:decimals"; -const METADATA_NAME: &str = "icrc1:name"; -const METADATA_SYMBOL: &str = "icrc1:symbol"; -const METADATA_FEE: &str = "icrc1:fee"; -const METADATA_MAX_MEMO_LENGTH: &str = "icrc1:max_memo_length"; -const METADATA_PUBLIC_ALLOWANCES: &str = "icrc103:public_allowances"; -const METADATA_MAX_TAKE_ALLOWANCES: &str = "icrc103:max_take_value"; const MAX_TAKE_ALLOWANCES: u64 = 500; #[cfg(not(feature = "u256-tokens"))] @@ -229,8 +223,10 @@ impl InitArgsBuilder { self } - pub fn with_metadata_entry(mut self, name: impl ToString, value: impl Into) -> Self { - self.0.metadata.push((name.to_string(), value.into())); + pub fn with_metadata_entry(mut self, key: &str, value: impl Into) -> Self { + // Validate the key format at build time + MetadataKey::parse(key).unwrap_or_else(|e| panic!("invalid metadata key '{key}': {e}")); + self.0.metadata.push((key.to_string(), value.into())); self } @@ -586,7 +582,7 @@ pub struct Ledger { token_symbol: String, token_name: String, - metadata: Vec<(String, StoredValue)>, + metadata: Vec<(MetadataKey, StoredValue)>, #[serde(default = "default_max_memo_length")] max_memo_length: u16, @@ -642,23 +638,48 @@ pub fn wasm_token_type() -> String { Tokens::TYPE.to_string() } -fn map_metadata_or_trap(arg_metadata: Vec<(String, Value)>) -> Vec<(String, StoredValue)> { +/// Validates and converts string metadata keys to checked keys. +/// +/// # Arguments +/// * `arg_metadata` - Metadata with string keys from init/upgrade args +/// * `require_valid` - If true, traps on invalid keys. If false, accepts any key for backwards compat. +fn map_metadata_or_trap( + arg_metadata: Vec<(String, Value)>, + require_valid: bool, + sink: impl Sink + Clone, +) -> Vec<(MetadataKey, StoredValue)> { const DISALLOWED_METADATA_FIELDS: [&str; 7] = [ - METADATA_DECIMALS, - METADATA_NAME, - METADATA_SYMBOL, - METADATA_FEE, - METADATA_MAX_MEMO_LENGTH, - METADATA_PUBLIC_ALLOWANCES, - METADATA_MAX_TAKE_ALLOWANCES, + MetadataKey::ICRC1_DECIMALS, + MetadataKey::ICRC1_NAME, + MetadataKey::ICRC1_SYMBOL, + MetadataKey::ICRC1_FEE, + MetadataKey::ICRC1_MAX_MEMO_LENGTH, + MetadataKey::ICRC103_PUBLIC_ALLOWANCES, + MetadataKey::ICRC103_MAX_TAKE_VALUE, ]; arg_metadata .into_iter() - .map(|(k, v)| { - if DISALLOWED_METADATA_FIELDS.contains(&k.as_str()) { - ic_cdk::trap(format!("Metadata field {k} is reserved and cannot be set")); + .map(|(key_str, v)| { + if DISALLOWED_METADATA_FIELDS.contains(&key_str.as_str()) { + ic_cdk::trap(format!( + "Metadata field {} is reserved and cannot be set", + key_str + )); } - (k, StoredValue::from(v)) + let metadata_key = MetadataKey::parse(&key_str).unwrap_or_else(|e| { + if require_valid { + ic_cdk::trap(format!("invalid metadata key '{}': {}", key_str, e)) + } else { + // For backwards compat with ledgers that have legacy invalid keys + log!( + sink, + "Warning: accepting invalid metadata key '{}' for backwards compatibility", + key_str + ); + MetadataKey::unchecked_from_string(key_str) + } + }); + (metadata_key, StoredValue::from(v)) }) .collect() } @@ -704,7 +725,7 @@ impl Ledger { token_symbol, token_name, decimals: decimals.unwrap_or_else(default_decimals), - metadata: map_metadata_or_trap(metadata), + metadata: map_metadata_or_trap(metadata, true, sink), // require_valid=true for init max_memo_length: max_memo_length.unwrap_or(DEFAULT_MAX_MEMO_LENGTH), feature_flags: feature_flags.unwrap_or_default(), maximum_number_of_accounts: 0, @@ -905,35 +926,44 @@ impl Ledger { MAX_TAKE_ALLOWANCES } - pub fn metadata(&self) -> Vec<(String, Value)> { - let mut records: Vec<(String, Value)> = self + pub fn metadata(&self) -> Vec<(MetadataKey, Value)> { + let mut records: Vec<(MetadataKey, Value)> = self .metadata .clone() .into_iter() .map(|(k, v)| (k, StoredValue::into(v))) .collect(); - records.push(Value::entry(METADATA_DECIMALS, self.decimals() as u64)); - records.push(Value::entry(METADATA_NAME, self.token_name())); - records.push(Value::entry(METADATA_SYMBOL, self.token_symbol())); - records.push(Value::entry(METADATA_FEE, Nat::from(self.transfer_fee()))); - records.push(Value::entry( - METADATA_MAX_MEMO_LENGTH, - self.max_memo_length() as u64, - )); - records.push(Value::entry(METADATA_PUBLIC_ALLOWANCES, "true")); - records.push(Value::entry( - METADATA_MAX_TAKE_ALLOWANCES, - Nat::from(self.max_take_allowances()), - )); + records.push(Value::entry(MetadataKey::ICRC1_DECIMALS, self.decimals() as u64).unwrap()); + records.push(Value::entry(MetadataKey::ICRC1_NAME, self.token_name()).unwrap()); + records.push(Value::entry(MetadataKey::ICRC1_SYMBOL, self.token_symbol()).unwrap()); + records.push(Value::entry(MetadataKey::ICRC1_FEE, Nat::from(self.transfer_fee())).unwrap()); + records.push( + Value::entry( + MetadataKey::ICRC1_MAX_MEMO_LENGTH, + self.max_memo_length() as u64, + ) + .unwrap(), + ); + records.push(Value::entry(MetadataKey::ICRC103_PUBLIC_ALLOWANCES, "true").unwrap()); + records.push( + Value::entry( + MetadataKey::ICRC103_MAX_TAKE_VALUE, + Nat::from(self.max_take_allowances()), + ) + .unwrap(), + ); // When adding new entries that cannot be set by the user // (e.g. because they are fixed or computed dynamically) // please also add them to `map_metadata_or_trap` to prevent // the entry being set using init or upgrade arguments. if let Some(index_principal) = self.index_principal() { - records.push(Value::entry( - "icrc106:index_principal", - index_principal.to_text(), - )); + records.push( + Value::entry( + MetadataKey::ICRC106_INDEX_PRINCIPAL, + index_principal.to_text(), + ) + .unwrap(), + ); } records } @@ -944,7 +974,11 @@ impl Ledger { pub fn upgrade(&mut self, sink: impl Sink + Clone, args: UpgradeArgs) { if let Some(upgrade_metadata_args) = args.metadata { - self.metadata = map_metadata_or_trap(upgrade_metadata_args); + // Only enforce strict validation if existing metadata has no invalid keys. + // This allows ledgers with legacy invalid keys to still be upgraded. + let existing_all_valid = self.metadata.iter().all(|(k, _)| k.is_valid()); + self.metadata = + map_metadata_or_trap(upgrade_metadata_args, existing_all_valid, sink.clone()); } if let Some(token_name) = args.token_name { self.token_name = token_name; diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index 2a7de1bfe37d..843332509753 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -47,6 +47,7 @@ use icrc_ledger_types::icrc103::get_allowances::{ use icrc_ledger_types::icrc106::errors::Icrc106Error; use icrc_ledger_types::{ icrc::generic_metadata_value::MetadataValue as Value, + icrc::metadata_key::MetadataKey, icrc3::{ archive::ArchiveInfo, blocks::GetBlocksRequest, @@ -644,7 +645,7 @@ fn icrc1_fee() -> Nat { } #[query] -fn icrc1_metadata() -> Vec<(String, Value)> { +fn icrc1_metadata() -> Vec<(MetadataKey, Value)> { Access::with_ledger(|ledger| ledger.metadata()) } diff --git a/rs/ledger_suite/icrc1/ledger/src/tests.rs b/rs/ledger_suite/icrc1/ledger/src/tests.rs index 28bd3d8469e4..0386a5b2ce83 100644 --- a/rs/ledger_suite/icrc1/ledger/src/tests.rs +++ b/rs/ledger_suite/icrc1/ledger/src/tests.rs @@ -1,4 +1,4 @@ -use crate::{InitArgs, Ledger, StorableAllowance}; +use crate::{InitArgs, Ledger, StorableAllowance, UpgradeArgs}; use ic_base_types::PrincipalId; use ic_canister_log::Sink; use ic_icrc1::{Operation, Transaction}; @@ -13,7 +13,6 @@ use ic_ledger_suite_state_machine_tests_constants::{ TEXT_META_VALUE, TOKEN_NAME, TOKEN_SYMBOL, }; use ic_stable_structures::Storable; -use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as Value; use icrc_ledger_types::icrc1::account::Account; use proptest::prelude::*; use proptest::strategy::Strategy; @@ -68,10 +67,10 @@ fn default_init_args() -> InitArgs { token_name: TOKEN_NAME.to_string(), token_symbol: TOKEN_SYMBOL.to_string(), metadata: vec![ - Value::entry(NAT_META_KEY, NAT_META_VALUE), - Value::entry(INT_META_KEY, INT_META_VALUE), - Value::entry(TEXT_META_KEY, TEXT_META_VALUE), - Value::entry(BLOB_META_KEY, BLOB_META_VALUE), + (NAT_META_KEY.to_string(), NAT_META_VALUE.into()), + (INT_META_KEY.to_string(), INT_META_VALUE.into()), + (TEXT_META_KEY.to_string(), TEXT_META_VALUE.into()), + (BLOB_META_KEY.to_string(), BLOB_META_VALUE.into()), ], archive_options: ArchiveOptions { trigger_threshold: ARCHIVE_TRIGGER_THRESHOLD as usize, @@ -726,3 +725,136 @@ fn test_mint_fee_error() { TxApplyError::BurnOrMintFee ); } + +mod metadata_validation_tests { + use super::*; + use icrc_ledger_types::icrc::metadata_key::MetadataKey; + + #[test] + fn test_init_with_valid_metadata_keys() { + let now = ts(1); + let init_args = InitArgs { + metadata: vec![ + ("namespace1:key1".to_string(), "value1".into()), + ("icrc1:logo".to_string(), "logo".into()), + ], + ..default_init_args() + }; + let ledger = Ledger::from_init_args(DummyLogger, init_args, now); + + // Verify both metadata entries are stored + assert_eq!(ledger.metadata.len(), 2); + + // Verify keys are properly parsed + let key1 = &ledger.metadata[0].0; + assert_eq!(key1.namespace(), Some("namespace1")); + assert_eq!(key1.key(), Some("key1")); + assert!(key1.is_valid()); + + let key2 = &ledger.metadata[1].0; + assert_eq!(key2.namespace(), Some("icrc1")); + assert_eq!(key2.key(), Some("logo")); + assert!(key2.is_valid()); + } + + #[test] + fn test_upgrade_with_valid_metadata_when_existing_all_valid() { + let now = ts(1); + + // Start with valid metadata + let init_args = InitArgs { + metadata: vec![("namespace:key".to_string(), "initial_value".into())], + ..default_init_args() + }; + let mut ledger = Ledger::from_init_args(DummyLogger, init_args, now); + + // Verify initial state has valid keys + assert!(ledger.metadata.iter().all(|(k, _)| k.is_valid())); + + // Upgrade with new valid metadata + let upgrade_args = UpgradeArgs { + metadata: Some(vec![("namespace:key".to_string(), "upgraded_value".into())]), + ..UpgradeArgs::default() + }; + ledger.upgrade(DummyLogger, upgrade_args); + + // Verify upgraded metadata + assert_eq!(ledger.metadata.len(), 1); + let key = &ledger.metadata[0].0; + let value = &ledger.metadata[0].1; + assert_eq!(key.namespace(), Some("namespace")); + assert_eq!(key.key(), Some("key")); + assert!(key.is_valid()); + match value { + crate::StoredValue::Text(s) => assert_eq!(s, "upgraded_value"), + other => panic!("Expected Text value, got {:?}", other), + } + } + + #[test] + fn test_upgrade_with_invalid_metadata_when_existing_has_invalid() { + let now = ts(1); + + // Create ledger with valid metadata first + let init_args = default_init_args(); + let mut ledger = Ledger::from_init_args(DummyLogger, init_args, now); + + // Manually inject an invalid key to simulate legacy state + ledger.metadata.push(( + MetadataKey::unchecked_from_string("invalid_no_colon"), + crate::StoredValue::Text("initial_value".to_string()), + )); + + // Verify we have an invalid key + assert!(!ledger.metadata.iter().all(|(k, _)| k.is_valid())); + + // Upgrade the invalid key metadata should succeed (backwards compat) + let upgrade_args = UpgradeArgs { + metadata: Some(vec![( + "invalid_no_colon".to_string(), + "upgraded_value".into(), + )]), + ..UpgradeArgs::default() + }; + ledger.upgrade(DummyLogger, upgrade_args); + + // Invalid key should be accepted + assert_eq!(ledger.metadata.len(), 1); + let key = &ledger.metadata[0].0; + let value = &ledger.metadata[0].1; + assert_eq!(key.as_str(), "invalid_no_colon"); + match value { + crate::StoredValue::Text(s) => assert_eq!(s, "upgraded_value"), + other => panic!("Expected Text value, got {:?}", other), + } + } + + #[test] + fn test_upgrade_preserves_metadata_when_not_specified() { + let now = ts(1); + + let init_args = InitArgs { + metadata: vec![("namespace:key".to_string(), "preserved_value".into())], + ..default_init_args() + }; + let mut ledger = Ledger::from_init_args(DummyLogger, init_args, now); + + // Upgrade without specifying metadata + let upgrade_args = UpgradeArgs { + metadata: None, + token_name: Some("New Name".to_string()), + ..UpgradeArgs::default() + }; + ledger.upgrade(DummyLogger, upgrade_args); + + // Verify metadata is preserved + assert_eq!(ledger.metadata.len(), 1); + let key = &ledger.metadata[0].0; + let value = &ledger.metadata[0].1; + assert_eq!(key.as_str(), "namespace:key"); + match value { + crate::StoredValue::Text(s) => assert_eq!(s, "preserved_value"), + other => panic!("Expected Text value, got {:?}", other), + } + } +} diff --git a/rs/ledger_suite/icrc1/ledger/tests/tests.rs b/rs/ledger_suite/icrc1/ledger/tests/tests.rs index 7394c86ccca1..e052ca904d56 100644 --- a/rs/ledger_suite/icrc1/ledger/tests/tests.rs +++ b/rs/ledger_suite/icrc1/ledger/tests/tests.rs @@ -208,10 +208,10 @@ fn encode_init_args(args: ic_ledger_suite_state_machine_tests::InitArgs) -> Ledg decimals: Some(DECIMAL_PLACES), token_symbol: TOKEN_SYMBOL.to_string(), metadata: vec![ - MetadataValue::entry(NAT_META_KEY, NAT_META_VALUE), - MetadataValue::entry(INT_META_KEY, INT_META_VALUE), - MetadataValue::entry(TEXT_META_KEY, TEXT_META_VALUE), - MetadataValue::entry(BLOB_META_KEY, BLOB_META_VALUE), + (NAT_META_KEY.to_string(), NAT_META_VALUE.into()), + (INT_META_KEY.to_string(), INT_META_VALUE.into()), + (TEXT_META_KEY.to_string(), TEXT_META_VALUE.into()), + (BLOB_META_KEY.to_string(), BLOB_META_VALUE.into()), ], archive_options: args.archive_options, max_memo_length: None, @@ -951,6 +951,22 @@ fn test_setting_forbidden_metadata_not_possible() { ); } +#[test] +fn test_init_with_invalid_metadata_keys_fails() { + ic_ledger_suite_state_machine_tests::metadata::test_init_with_invalid_metadata_keys_fails( + ledger_wasm(), + encode_init_args_with_provided_metadata, + ); +} + +#[test] +fn test_upgrade_with_invalid_metadata_keys_fails_when_existing_valid() { + ic_ledger_suite_state_machine_tests::metadata::test_upgrade_with_invalid_metadata_keys_fails_when_existing_valid( + ledger_wasm(), + encode_init_args_with_provided_metadata, + ); +} + #[test] fn test_cycles_for_archive_creation_no_overwrite_of_none_in_upgrade() { ic_ledger_suite_state_machine_tests::test_cycles_for_archive_creation_no_overwrite_of_none_in_upgrade( @@ -2170,10 +2186,10 @@ mod verify_written_blocks { decimals: Some(DECIMAL_PLACES), token_symbol: TOKEN_SYMBOL.to_string(), metadata: vec![ - MetadataValue::entry(NAT_META_KEY, NAT_META_VALUE), - MetadataValue::entry(INT_META_KEY, INT_META_VALUE), - MetadataValue::entry(TEXT_META_KEY, TEXT_META_VALUE), - MetadataValue::entry(BLOB_META_KEY, BLOB_META_VALUE), + (NAT_META_KEY.to_string(), NAT_META_VALUE.into()), + (INT_META_KEY.to_string(), INT_META_VALUE.into()), + (TEXT_META_KEY.to_string(), TEXT_META_VALUE.into()), + (BLOB_META_KEY.to_string(), BLOB_META_VALUE.into()), ], archive_options: ArchiveOptions { trigger_threshold: ARCHIVE_TRIGGER_THRESHOLD as usize, diff --git a/rs/ledger_suite/icrc1/test_utils/icrc3_test_ledger/src/main.rs b/rs/ledger_suite/icrc1/test_utils/icrc3_test_ledger/src/main.rs index 3a63541e1bcb..08785609ea37 100644 --- a/rs/ledger_suite/icrc1/test_utils/icrc3_test_ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/test_utils/icrc3_test_ledger/src/main.rs @@ -10,6 +10,7 @@ use ic_icrc3_test_ledger::{AddBlockResult, ArchiveBlocksArgs}; use ic_ledger_canister_core::range_utils; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; use icrc_ledger_types::icrc::generic_value::{ICRC3Value, Value}; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc3::archive::{ArchivedRange, QueryArchiveFn, QueryBlockArchiveFn}; use icrc_ledger_types::icrc3::blocks::{ArchivedBlocks, ICRC3DataCertificate}; use icrc_ledger_types::icrc3::blocks::{ @@ -341,12 +342,12 @@ fn get_blocks_for_request(blocks: &BlockStorage, request: GetBlocksRequest) -> B } #[query] -fn icrc1_metadata() -> Vec<(String, MetadataValue)> { +fn icrc1_metadata() -> Vec<(MetadataKey, MetadataValue)> { vec![ - MetadataValue::entry("icrc1:decimals", 0u64), - MetadataValue::entry("icrc1:name", ""), - MetadataValue::entry("icrc1:symbol", "XTST"), - MetadataValue::entry("icrc1:fee", 0u64), + MetadataValue::entry(MetadataKey::ICRC1_DECIMALS, 0u64).unwrap(), + MetadataValue::entry(MetadataKey::ICRC1_NAME, "").unwrap(), + MetadataValue::entry(MetadataKey::ICRC1_SYMBOL, "XTST").unwrap(), + MetadataValue::entry(MetadataKey::ICRC1_FEE, 0u64).unwrap(), ] } diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 100445935b35..a0a9629800d5 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -10,6 +10,7 @@ use ic_ledger_core::tokens::TokensType; use ic_ledger_hash_of::HashOf; use ic_secp256k1::PrivateKey as Secp256k1PrivateKey; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::account::{Account, Subaccount}; use icrc_ledger_types::icrc1::transfer::{Memo, TransferArg}; use icrc_ledger_types::icrc2::approve::ApproveArgs; @@ -1340,14 +1341,11 @@ pub fn symbol_strategy() -> impl Strategy { prop::string::string_regex("[A-Za-z0-9]{1,5}").expect("failed to make generator") } -pub fn metadata_strategy() -> impl Strategy> { +pub fn metadata_strategy() -> impl Strategy> { (symbol_strategy(), decimals_strategy()).prop_map(|(symbol, decimals)| { vec![ - ("icrc1:symbol".to_string(), MetadataValue::Text(symbol)), - ( - "icrc1:decimals".to_string(), - MetadataValue::Nat(candid::Nat::from(decimals)), - ), + MetadataValue::entry(MetadataKey::ICRC1_SYMBOL, symbol).unwrap(), + MetadataValue::entry(MetadataKey::ICRC1_DECIMALS, candid::Nat::from(decimals)).unwrap(), ] }) } diff --git a/rs/ledger_suite/test_utils/state_machine_helpers/src/lib.rs b/rs/ledger_suite/test_utils/state_machine_helpers/src/lib.rs index 0a760bbab2d2..45051abfd154 100644 --- a/rs/ledger_suite/test_utils/state_machine_helpers/src/lib.rs +++ b/rs/ledger_suite/test_utils/state_machine_helpers/src/lib.rs @@ -16,6 +16,7 @@ use ic_universal_canister::{call_args, wasm}; use icp_ledger::{AccountIdentifier, BinaryAccountBalanceArgs, IcpAllowanceArgs}; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as Value; use icrc_ledger_types::icrc::generic_value::ICRC3Value; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::transfer::{Memo, TransferArg, TransferError}; use icrc_ledger_types::icrc2::allowance::{Allowance, AllowanceArgs}; @@ -585,12 +586,12 @@ pub fn list_archives(env: &StateMachine, ledger: CanisterId) -> Vec .expect("failed to decode archives response") } -pub fn metadata(env: &StateMachine, ledger: CanisterId) -> BTreeMap { +pub fn metadata(env: &StateMachine, ledger: CanisterId) -> BTreeMap { Decode!( &env.query(ledger, "icrc1_metadata", Encode!().unwrap()) .expect("failed to query metadata") .bytes(), - Vec<(String, Value)> + Vec<(MetadataKey, Value)> ) .expect("failed to decode metadata response") .into_iter() diff --git a/rs/ledger_suite/tests/sm-tests/src/icrc_106.rs b/rs/ledger_suite/tests/sm-tests/src/icrc_106.rs index 07d5143638cd..ee502c32b1d0 100644 --- a/rs/ledger_suite/tests/sm-tests/src/icrc_106.rs +++ b/rs/ledger_suite/tests/sm-tests/src/icrc_106.rs @@ -175,7 +175,8 @@ fn assert_index_not_set( } assert_eq!( None, - metadata(env, ledger_canister_id).get("icrc106:index_principal") + metadata(env, ledger_canister_id) + .get(&MetadataKey::parse(MetadataKey::ICRC106_INDEX_PRINCIPAL).unwrap()) ); } @@ -192,7 +193,7 @@ fn assert_index_set( assert_eq!( &Value::Text(index_principal.to_text()), metadata(env, ledger_canister_id) - .get("icrc106:index_principal") + .get(&MetadataKey::parse(MetadataKey::ICRC106_INDEX_PRINCIPAL).unwrap()) .expect("should have index principal metadata") ); } diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index a0c5e3f8d44b..b03abdcdde25 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -43,6 +43,7 @@ use ic_universal_canister::UNIVERSAL_CANISTER_WASM; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as Value; use icrc_ledger_types::icrc::generic_value::ICRC3Value; use icrc_ledger_types::icrc::generic_value::Value as GenericValue; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc1::account::{Account, DEFAULT_SUBACCOUNT, Subaccount}; use icrc_ledger_types::icrc1::transfer::{Memo, TransferArg, TransferError}; use icrc_ledger_types::icrc2::allowance::AllowanceArgs; @@ -366,10 +367,10 @@ fn init_args(initial_balances: Vec<(Account, u64)>) -> InitArgs { token_symbol: TOKEN_SYMBOL.to_string(), decimals: Some(DECIMAL_PLACES), metadata: vec![ - Value::entry(NAT_META_KEY, NAT_META_VALUE), - Value::entry(INT_META_KEY, INT_META_VALUE), - Value::entry(TEXT_META_KEY, TEXT_META_VALUE), - Value::entry(BLOB_META_KEY, BLOB_META_VALUE), + (NAT_META_KEY.to_string(), NAT_META_VALUE.into()), + (INT_META_KEY.to_string(), INT_META_VALUE.into()), + (TEXT_META_KEY.to_string(), TEXT_META_VALUE.into()), + (BLOB_META_KEY.to_string(), BLOB_META_VALUE.into()), ], archive_options: ArchiveOptions { trigger_threshold: ARCHIVE_TRIGGER_THRESHOLD as usize, @@ -462,9 +463,10 @@ pub fn test_metadata_icp_ledger(ledger_wasm: Vec, encode_init_args: fn(In where T: CandidType, { - fn lookup<'a>(metadata: &'a BTreeMap, key: &str) -> &'a Value { + fn lookup<'a>(metadata: &'a BTreeMap, key: &str) -> &'a Value { + let key = MetadataKey::parse(key).unwrap(); metadata - .get(key) + .get(&key) .unwrap_or_else(|| panic!("no metadata key {key} in map {metadata:?}")) } @@ -493,13 +495,16 @@ where ); let metadata = metadata(&env, canister_id); - assert_eq!(lookup(&metadata, "icrc1:name"), &Value::from(TOKEN_NAME)); assert_eq!( - lookup(&metadata, "icrc1:symbol"), + lookup(&metadata, MetadataKey::ICRC1_NAME), + &Value::from(TOKEN_NAME) + ); + assert_eq!( + lookup(&metadata, MetadataKey::ICRC1_SYMBOL), &Value::from(TOKEN_SYMBOL) ); assert_eq!( - lookup(&metadata, "icrc1:decimals"), + lookup(&metadata, MetadataKey::ICRC1_DECIMALS), &Value::from(DECIMAL_PLACES as u64) ); @@ -514,9 +519,10 @@ pub fn test_metadata(ledger_wasm: Vec, encode_init_args: fn(InitArgs) -> where T: CandidType, { - fn lookup<'a>(metadata: &'a BTreeMap, key: &str) -> &'a Value { + fn lookup<'a>(metadata: &'a BTreeMap, key: &str) -> &'a Value { + let key = MetadataKey::parse(key).unwrap(); metadata - .get(key) + .get(&key) .unwrap_or_else(|| panic!("no metadata key {key} in map {metadata:?}")) } @@ -545,13 +551,16 @@ where ); let metadata = metadata(&env, canister_id); - assert_eq!(lookup(&metadata, "icrc1:name"), &Value::from(TOKEN_NAME)); assert_eq!( - lookup(&metadata, "icrc1:symbol"), + lookup(&metadata, MetadataKey::ICRC1_NAME), + &Value::from(TOKEN_NAME) + ); + assert_eq!( + lookup(&metadata, MetadataKey::ICRC1_SYMBOL), &Value::from(TOKEN_SYMBOL) ); assert_eq!( - lookup(&metadata, "icrc1:decimals"), + lookup(&metadata, MetadataKey::ICRC1_DECIMALS), &Value::from(DECIMAL_PLACES as u64) ); // Not all ICRC-1 implementations have the same metadata entries. Thus only certain basic fields are shared by all ICRC-1 implementations. @@ -1843,7 +1852,8 @@ where let (env, canister_id) = setup(ledger_wasm.clone(), encode_init_args, vec![]); let metadata_res = metadata(&env, canister_id); - let metadata_value = metadata_res.get(TEXT_META_KEY).unwrap(); + let text_meta_key = MetadataKey::parse(TEXT_META_KEY).unwrap(); + let metadata_value = metadata_res.get(&text_meta_key).unwrap(); assert_eq!(*metadata_value, Value::Text(TEXT_META_VALUE.to_string())); const OTHER_TOKEN_SYMBOL: &str = "NEWSYMBOL"; @@ -1866,7 +1876,7 @@ where let metadata_res_after_upgrade = metadata(&env, canister_id); assert_eq!( - *metadata_res_after_upgrade.get(TEXT_META_KEY).unwrap(), + *metadata_res_after_upgrade.get(&text_meta_key).unwrap(), Value::Text(TEXT_META_VALUE_2.to_string()) ); @@ -5199,11 +5209,11 @@ pub fn test_cycles_for_archive_creation_default_spawns_archive( pub mod metadata { use super::*; - const METADATA_DECIMALS: &str = "icrc1:decimals"; - const METADATA_NAME: &str = "icrc1:name"; - const METADATA_SYMBOL: &str = "icrc1:symbol"; - const METADATA_FEE: &str = "icrc1:fee"; - const METADATA_MAX_MEMO_LENGTH: &str = "icrc1:max_memo_length"; + const METADATA_DECIMALS: &str = MetadataKey::ICRC1_DECIMALS; + const METADATA_NAME: &str = MetadataKey::ICRC1_NAME; + const METADATA_SYMBOL: &str = MetadataKey::ICRC1_SYMBOL; + const METADATA_FEE: &str = MetadataKey::ICRC1_FEE; + const METADATA_MAX_MEMO_LENGTH: &str = MetadataKey::ICRC1_MAX_MEMO_LENGTH; const FORBIDDEN_METADATA: [&str; 5] = [ METADATA_DECIMALS, METADATA_NAME, @@ -5220,12 +5230,12 @@ pub mod metadata { { let env = StateMachine::new(); - let forbidden_metadata = vec![ - Value::entry(METADATA_DECIMALS, 8u64), - Value::entry(METADATA_NAME, "BogusName"), - Value::entry(METADATA_SYMBOL, "BN"), - Value::entry(METADATA_FEE, Nat::from(10_000u64)), - Value::entry(METADATA_MAX_MEMO_LENGTH, 8u64), + let forbidden_metadata: Vec<(String, Value)> = vec![ + (METADATA_DECIMALS.to_string(), 8u64.into()), + (METADATA_NAME.to_string(), "BogusName".into()), + (METADATA_SYMBOL.to_string(), "BN".into()), + (METADATA_FEE.to_string(), Nat::from(10_000u64).into()), + (METADATA_MAX_MEMO_LENGTH.to_string(), 8u64.into()), ]; let args = encode_init_args(InitArgs { @@ -5289,7 +5299,7 @@ pub mod metadata { // Verify that specifying any of the forbidden metadata in the init args is not possible. for forbidden_metadata in FORBIDDEN_METADATA.iter() { let args = encode_init_args(InitArgs { - metadata: vec![Value::entry(*forbidden_metadata, 8u64)], + metadata: vec![(forbidden_metadata.to_string(), 8u64.into())], ..init_args(vec![]) }); let args = Encode!(&args).unwrap(); @@ -5315,7 +5325,7 @@ pub mod metadata { // Verify that also upgrading does not accept the forbidden metadata for forbidden_metadata in FORBIDDEN_METADATA.iter() { let ledger_upgrade_arg = LedgerArgument::Upgrade(Some(UpgradeArgs { - metadata: Some(vec![Value::entry(*forbidden_metadata, 8u64)]), + metadata: Some(vec![(forbidden_metadata.to_string(), 8u64.into())]), ..UpgradeArgs::default() })); match env.upgrade_canister( @@ -5343,6 +5353,86 @@ pub mod metadata { ) .expect("should successfully upgrade the ledger"); } + + /// Invalid metadata keys (not following namespace:key format) are rejected during init. + pub fn test_init_with_invalid_metadata_keys_fails( + ledger_wasm: Vec, + encode_init_args: fn(InitArgs) -> T, + ) where + T: CandidType, + { + let env = StateMachine::new(); + + const INVALID_KEYS: [&str; 3] = [ + "invalid_no_colon", // missing colon separator + ":key_no_namespace", // empty namespace + "namespace:", // empty key + ]; + + for invalid_key in INVALID_KEYS.iter() { + let args = encode_init_args(InitArgs { + metadata: vec![(invalid_key.to_string(), "value".into())], + ..init_args(vec![]) + }); + let args = Encode!(&args).unwrap(); + match env.install_canister(ledger_wasm.clone(), args, None) { + Ok(_) => { + panic!( + "should not be able to install ledger with invalid metadata key '{}'", + invalid_key + ) + } + Err(err) => { + err.assert_contains(ErrorCode::CanisterCalledTrap, "invalid metadata key"); + } + } + } + } + + /// Invalid metadata keys are rejected during upgrade when existing metadata is all valid. + pub fn test_upgrade_with_invalid_metadata_keys_fails_when_existing_valid( + ledger_wasm: Vec, + encode_init_args: fn(InitArgs) -> T, + ) where + T: CandidType, + { + let env = StateMachine::new(); + + // Install ledger with valid metadata + let args = encode_init_args(InitArgs { + metadata: vec![("custom:valid_key".to_string(), "value".into())], + ..init_args(vec![]) + }); + let args = Encode!(&args).unwrap(); + let canister_id = env + .install_canister(ledger_wasm.clone(), args, None) + .expect("should successfully install ledger with valid metadata"); + + const INVALID_KEYS: [&str; 3] = ["invalid_no_colon", ":key_no_namespace", "namespace:"]; + + // Upgrading with invalid keys should fail when existing metadata is valid + for invalid_key in INVALID_KEYS.iter() { + let ledger_upgrade_arg = LedgerArgument::Upgrade(Some(UpgradeArgs { + metadata: Some(vec![(invalid_key.to_string(), "new_value".into())]), + ..UpgradeArgs::default() + })); + match env.upgrade_canister( + canister_id, + ledger_wasm.clone(), + Encode!(&ledger_upgrade_arg).unwrap(), + ) { + Ok(_) => { + panic!( + "should not be able to upgrade ledger with invalid metadata key '{}' when existing metadata is valid", + invalid_key + ) + } + Err(err) => { + err.assert_contains(ErrorCode::CanisterCalledTrap, "invalid metadata key"); + } + } + } + } } pub mod archiving { diff --git a/rs/nns/test_utils/src/state_test_helpers.rs b/rs/nns/test_utils/src/state_test_helpers.rs index d820da41addb..29a8a93d1f31 100644 --- a/rs/nns/test_utils/src/state_test_helpers.rs +++ b/rs/nns/test_utils/src/state_test_helpers.rs @@ -1857,10 +1857,11 @@ pub fn icrc1_token_symbol(machine: &StateMachine, ledger_id: CanisterId) -> Stri pub fn icrc1_token_logo(machine: &StateMachine, ledger_id: CanisterId) -> Option { let result = query(machine, ledger_id, "icrc1_metadata", Encode!(&()).unwrap()).unwrap(); use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; - Decode!(&result, Vec<(String, MetadataValue)>) + use icrc_ledger_types::icrc::metadata_key::MetadataKey; + Decode!(&result, Vec<(MetadataKey, MetadataValue)>) .unwrap() .into_iter() - .find(|(key, _)| key == "icrc1:logo") + .find(|(key, _)| key.as_str() == MetadataKey::ICRC1_LOGO) .map(|(_key, value)| match value { MetadataValue::Text(s) => s, m => panic!("Unexpected metadata value {m:?}"), diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 3648a7bb985f..ade8f2896e94 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -7,6 +7,7 @@ use ic_icrc1::blocks::encoded_block_to_generic_block; use ic_ledger_core::block::EncodedBlock; use ic_ledger_core::tokens::TokensType; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc3::blocks::GenericBlock; use icrc_ledger_types::{ icrc::generic_value::Value, @@ -543,11 +544,11 @@ pub struct MetadataEntry { } impl MetadataEntry { - pub fn from_metadata_value(key: &str, value: &MetadataValue) -> anyhow::Result { + pub fn from_metadata_value(key: &MetadataKey, value: &MetadataValue) -> anyhow::Result { let value = candid::encode_one(value)?; Ok(Self { - key: key.to_string(), + key: key.as_str().to_string(), value, }) } diff --git a/rs/rosetta-api/icrc1/src/lib.rs b/rs/rosetta-api/icrc1/src/lib.rs index 9559649a757d..3fa701c937d7 100644 --- a/rs/rosetta-api/icrc1/src/lib.rs +++ b/rs/rosetta-api/icrc1/src/lib.rs @@ -6,6 +6,7 @@ use common::storage::types::MetadataEntry; use ic_base_types::CanisterId; use icrc_ledger_agent::Icrc1Agent; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc3::archive::ArchiveInfo; use num_traits::ToPrimitive; use rosetta_core::objects::Currency; @@ -64,8 +65,8 @@ impl From for Currency { } impl Metadata { - const METADATA_DECIMALS_KEY: &'static str = "icrc1:decimals"; - const METADATA_SYMBOL_KEY: &'static str = "icrc1:symbol"; + const METADATA_DECIMALS_KEY: &'static str = MetadataKey::ICRC1_DECIMALS; + const METADATA_SYMBOL_KEY: &'static str = MetadataKey::ICRC1_SYMBOL; pub fn from_args(symbol: String, decimals: u8) -> Self { Self { symbol, decimals } diff --git a/rs/sns/governance/src/types.rs b/rs/sns/governance/src/types.rs index fa3094d4f76b..67f4b0758676 100644 --- a/rs/sns/governance/src/types.rs +++ b/rs/sns/governance/src/types.rs @@ -58,6 +58,7 @@ use ic_nervous_system_proto::pb::v1::{Duration as PbDuration, Percentage}; use ic_sns_governance_api::format_full_hash; use ic_sns_governance_proposal_criticality::{ProposalCriticality, VotingDurationParameters}; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use icrc_ledger_types::icrc::metadata_key::MetadataKey; use lazy_static::lazy_static; use std::{ collections::{BTreeMap, BTreeSet, HashSet}, @@ -1964,9 +1965,8 @@ impl From for LedgerUpgradeArgs { } = manage_ledger_parameters; let metadata = token_logo.map(|token_logo| { - let key = "icrc1:logo".to_string(); let value = MetadataValue::Text(token_logo); - vec![(key, value)] + vec![(MetadataKey::ICRC1_LOGO.to_string(), value)] }); LedgerUpgradeArgs { diff --git a/rs/sns/governance/src/types/tests.rs b/rs/sns/governance/src/types/tests.rs index 65950ca90990..46d3e803e3ff 100644 --- a/rs/sns/governance/src/types/tests.rs +++ b/rs/sns/governance/src/types/tests.rs @@ -1570,7 +1570,7 @@ fn test_from_manage_ledger_parameters_into_ledger_upgrade_args() { observed, LedgerUpgradeArgs { metadata: Some(vec![( - "icrc1:logo".to_string(), + MetadataKey::ICRC1_LOGO.to_string(), MetadataValue::Text("".to_string()) )]), token_name: Some("abc".to_string()), diff --git a/rs/sns/init/src/lib.rs b/rs/sns/init/src/lib.rs index a982e31fa6d7..a19e46a274ac 100644 --- a/rs/sns/init/src/lib.rs +++ b/rs/sns/init/src/lib.rs @@ -32,7 +32,10 @@ use ic_sns_swap::{ NeuronBasketConstructionParameters, NeuronsFundParticipationConstraints, }, }; -use icrc_ledger_types::{icrc::generic_metadata_value::MetadataValue, icrc1::account::Account}; +use icrc_ledger_types::{ + icrc::generic_metadata_value::MetadataValue, icrc::metadata_key::MetadataKey, + icrc1::account::Account, +}; use isocountry::CountryCode; use maplit::btreemap; use pb::v1::DappCanisters; @@ -77,7 +80,7 @@ pub const MIN_SNS_NEURONS_PER_BASKET: u64 = 2; /// Maximum allowed number of SNS neurons per neuron basket. pub const MAX_SNS_NEURONS_PER_BASKET: u64 = 10; -pub const ICRC1_TOKEN_LOGO_KEY: &str = "icrc1:logo"; +pub const ICRC1_TOKEN_LOGO_KEY: &str = MetadataKey::ICRC1_LOGO; enum MinDirectParticipationThresholdValidationError { // This value must be specified. @@ -603,7 +606,7 @@ impl SnsInitPayload { if let Some(token_logo) = &self.token_logo { payload_builder = payload_builder.with_metadata_entry( - ICRC1_TOKEN_LOGO_KEY.to_string(), + ICRC1_TOKEN_LOGO_KEY, MetadataValue::Text(token_logo.clone()), ); } diff --git a/rs/tests/driver/src/canister_api.rs b/rs/tests/driver/src/canister_api.rs index 49bc25ab77cc..cc2f4eab7309 100644 --- a/rs/tests/driver/src/canister_api.rs +++ b/rs/tests/driver/src/canister_api.rs @@ -33,6 +33,7 @@ use ic_sns_swap::pb::v1::{ use icp_ledger::Subaccount; use icrc_ledger_types::{ icrc::generic_metadata_value::MetadataValue as Value, + icrc::metadata_key::MetadataKey, icrc1::{ account::Account, transfer::{TransferArg, TransferError}, @@ -566,7 +567,7 @@ pub struct Icrc1MetadataRequest { icrc1_canister: Principal, } -pub type Icrc1MetadataResponse = Vec<(String, Value)>; +pub type Icrc1MetadataResponse = Vec<(MetadataKey, Value)>; impl Request for Icrc1MetadataRequest { fn mode(&self) -> CallMode { diff --git a/rs/tests/financial_integrations/icrc1_agent_test.rs b/rs/tests/financial_integrations/icrc1_agent_test.rs index b39ae9399bee..34b05f4fe032 100644 --- a/rs/tests/financial_integrations/icrc1_agent_test.rs +++ b/rs/tests/financial_integrations/icrc1_agent_test.rs @@ -15,7 +15,8 @@ use icrc_ledger_types::icrc1::transfer::TransferArg; use icrc_ledger_types::icrc2::approve::ApproveArgs; use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs; use icrc_ledger_types::{ - icrc::generic_metadata_value::MetadataValue as Value, icrc3::blocks::GetBlocksRequest, + icrc::generic_metadata_value::MetadataValue as Value, icrc::metadata_key::MetadataKey, + icrc3::blocks::GetBlocksRequest, }; use on_wire::IntoWire; use std::env; @@ -221,15 +222,16 @@ pub fn test(env: TestEnv) { // metadata let expected_metadata = vec![ Value::entry( - "icrc1:decimals", + MetadataKey::ICRC1_DECIMALS, ic_ledger_core::tokens::DECIMAL_PLACES as u64, - ), - Value::entry("icrc1:name", init_args.token_name), - Value::entry("icrc1:symbol", init_args.token_symbol), - Value::entry("icrc1:fee", init_args.transfer_fee.clone()), - Value::entry("icrc1:max_memo_length", 32u64), - Value::entry("icrc103:public_allowances", "true"), - Value::entry("icrc103:max_take_value", 500u64), + ) + .unwrap(), + Value::entry(MetadataKey::ICRC1_NAME, init_args.token_name).unwrap(), + Value::entry(MetadataKey::ICRC1_SYMBOL, init_args.token_symbol).unwrap(), + Value::entry(MetadataKey::ICRC1_FEE, init_args.transfer_fee.clone()).unwrap(), + Value::entry(MetadataKey::ICRC1_MAX_MEMO_LENGTH, 32u64).unwrap(), + Value::entry(MetadataKey::ICRC103_PUBLIC_ALLOWANCES, "true").unwrap(), + Value::entry(MetadataKey::ICRC103_MAX_TAKE_VALUE, 500u64).unwrap(), ]; assert_eq!( expected_metadata,