-
Notifications
You must be signed in to change notification settings - Fork 379
feat(icrc-ledger-types): add MetadataKey type #8216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mducroux
merged 43 commits into
master
from
mducroux/DEFI-1010-add-metadata-keys-icrc-ledger-types
Jan 22, 2026
Merged
Changes from 19 commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
03154a5
feat(icrc-ledger-types): add MetadataKey type
mducroux 08378c6
refactor: replace hardcoded metadata keys with const
mducroux c3edc99
refactor: icrc1_metadata returns MetadataKey instead of String
mducroux db89279
Automatically fixing code for linting and formatting issues
2d82842
fix build
mducroux cb406ef
add ICRC-106 key
mducroux 7a78d5b
Merge branch 'mducroux/DEFI-1010-add-metadata-keys-icrc-ledger-types'…
mducroux e82af09
use explicit MetadataKey in hashmaps
mducroux a04776e
fix build
mducroux 314af8b
Automatically fixing code for linting and formatting issues
6bb1d01
fix build
mducroux eda1eec
Update mod.rs
mducroux 78ce22f
fix build
mducroux b3ba885
fix build
mducroux 70d47d3
Automatically fixing code for linting and formatting issues
1d3c0b2
clippy
mducroux d552e5c
remove unused imports
mducroux b93e6ea
Merge branch 'master' into mducroux/DEFI-1010-add-metadata-keys-icrc-…
mducroux 26261b0
improve doc
mducroux 0b7dfe5
use type-state pattern to store checked/unchecked metadata keys
mducroux fbfe50e
revert: remove MetadataKey<Unchecked> and use string in init and upgr…
mducroux 02334de
Automatically fixing code for linting and formatting issues
4b391f0
use candid encode in tests
mducroux 18a7317
revert changes to legacy UpgradeArgs/InitArgs
mducroux 1fb91e8
fix build
mducroux 238a135
add setting metadata key tests during init/upgrade icrc1 ledger
mducroux 4ce4c5b
Automatically fixing code for linting and formatting issues
f3d9799
Update lib.rs
mducroux 3c22c9c
Automatically fixing code for linting and formatting issues
e1d5fe7
make entry err instead of panic
mducroux 3b98373
Automatically fixing code for linting and formatting issues
b37f351
make key and namespace return an Option
mducroux 03e86a0
Update Cargo.toml
mducroux 3415b80
Update metadata_key.rs
mducroux 3239802
remove unused dep
mducroux 4461bf7
Automatically updated Cargo*.lock
2d5f324
remove reference to ISO-4217
mducroux 771238f
inline CONST metadata values
mducroux af7d83e
Automatically fixing code for linting and formatting issues
b837851
add logs when accepting invalid metadata key during upgrades
mducroux 0e8a0c9
Automatically fixing code for linting and formatting issues
10284c7
Update lib.rs
mducroux 59bffda
Automatically fixing code for linting and formatting issues
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,268 @@ | ||
| //! Metadata key types for ICRC-1 ledger metadata. | ||
|
|
||
| use candid::{CandidType, Deserialize}; | ||
| use serde::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 validated metadata key following the ICRC-1 standard format `<namespace>:<key>`. | ||
| /// | ||
| /// Metadata keys are arbitrary Unicode strings that must follow the pattern `<namespace>:<key>`, | ||
| /// where `<namespace>` 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; | ||
| /// | ||
| /// // Valid keys | ||
| /// let key = MetadataKey::new("icrc1", "name").unwrap(); | ||
| /// assert_eq!(key.namespace(), "icrc1"); | ||
| /// assert_eq!(key.key(), "name"); | ||
| /// assert_eq!(key.as_str(), "icrc1:name"); | ||
| /// | ||
| /// // Parse from string | ||
| /// let key = MetadataKey::parse("myapp:decimals").unwrap(); | ||
| /// assert_eq!(key.namespace(), "myapp"); | ||
| /// assert_eq!(key.key(), "decimals"); | ||
| /// ``` | ||
| #[derive( | ||
| CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, | ||
| )] | ||
| pub struct MetadataKey(String); | ||
|
|
||
| impl MetadataKey { | ||
| // ==================== ICRC-1 Standard Keys ==================== | ||
|
|
||
| /// 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 (see ISO-4217). | ||
mducroux marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// 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 108 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"; | ||
|
|
||
| // ==================== ICRC-103 Keys ==================== | ||
|
|
||
| /// 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"; | ||
|
|
||
| // ==================== ICRC-106 Keys ==================== | ||
|
|
||
| /// The textual representation of the principal of the associated index canister. | ||
| pub const ICRC106_INDEX_PRINCIPAL: &'static str = "icrc106:index_principal"; | ||
|
|
||
| /// Creates a new 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<Self, MetadataKeyError> { | ||
| 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 `<namespace>:<key>`. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns an error if the string does not follow the required format. | ||
| pub fn parse(s: &str) -> Result<Self, 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(Self(s.to_string())) | ||
| } | ||
mducroux marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// Returns the namespace part of the key. | ||
| pub fn namespace(&self) -> &str { | ||
| self.0 | ||
| .find(':') | ||
| .map(|pos| &self.0[..pos]) | ||
| .expect("BUG: MetadataKey should always contain a colon") | ||
| } | ||
|
|
||
| /// Returns the key part (after the namespace). | ||
| pub fn key(&self) -> &str { | ||
| self.0 | ||
| .find(':') | ||
| .map(|pos| &self.0[pos + 1..]) | ||
| .expect("BUG: MetadataKey should always contain a colon") | ||
| } | ||
mducroux marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// Returns the full key as a string slice. | ||
| pub fn as_str(&self) -> &str { | ||
| &self.0 | ||
| } | ||
| } | ||
|
|
||
| impl fmt::Display for MetadataKey { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| write!(f, "{}", self.0) | ||
| } | ||
| } | ||
|
|
||
| impl AsRef<str> for MetadataKey { | ||
| fn as_ref(&self) -> &str { | ||
| &self.0 | ||
| } | ||
| } | ||
|
|
||
| impl From<MetadataKey> 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(), "icrc1"); | ||
| assert_eq!(key.key(), "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(), "myapp"); | ||
| assert_eq!(key.key(), "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(), "myapp"); | ||
| assert_eq!(key.key(), "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"); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ | |
| pub mod generic_metadata_value; | ||
| pub mod generic_value; | ||
| pub mod generic_value_predicate; | ||
| pub mod metadata_key; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.