diff --git a/Cargo.lock b/Cargo.lock index 13e2925ab2..6b1cb397e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,6 +991,8 @@ dependencies = [ "parachain-info", "parity-scale-codec", "polkadot-parachain", + "public-credentials", + "public-credentials-runtime-api", "runtime-common", "scale-info", "serde", @@ -1929,6 +1931,7 @@ dependencies = [ "log", "pallet-balances", "parity-scale-codec", + "public-credentials", "scale-info", "serde", "sp-core", @@ -1998,7 +2001,7 @@ dependencies = [ [[package]] name = "did-rpc" -version = "1.6.2" +version = "1.7.2" dependencies = [ "did-rpc-runtime-api", "jsonrpsee", @@ -2011,7 +2014,7 @@ dependencies = [ [[package]] name = "did-rpc-runtime-api" -version = "1.6.2" +version = "1.7.2" dependencies = [ "did", "kilt-support", @@ -3593,6 +3596,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" +[[package]] +name = "kilt-asset-dids" +version = "1.7.2" +dependencies = [ + "base58", + "frame-support", + "hex", + "hex-literal", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-std", +] + [[package]] name = "kilt-parachain" version = "1.7.2" @@ -3618,6 +3636,7 @@ dependencies = [ "hex-literal", "jsonrpsee", "log", + "node-common", "pallet-did-lookup", "pallet-transaction-payment-rpc", "parity-scale-codec", @@ -3627,6 +3646,8 @@ dependencies = [ "polkadot-parachain", "polkadot-primitives", "polkadot-service", + "public-credentials", + "public-credentials-rpc", "runtime-common", "sc-basic-authorship", "sc-chain-spec", @@ -4598,9 +4619,12 @@ dependencies = [ "jsonrpsee", "log", "mashnet-node-runtime", + "node-common", "pallet-did-lookup", "pallet-transaction-payment", "pallet-transaction-payment-rpc", + "public-credentials", + "public-credentials-rpc", "runtime-common", "sc-basic-authorship", "sc-cli", @@ -4673,6 +4697,8 @@ dependencies = [ "pallet-utility", "pallet-web3-names", "parity-scale-codec", + "public-credentials", + "public-credentials-runtime-api", "runtime-common", "scale-info", "serde", @@ -5047,6 +5073,16 @@ dependencies = [ "libc", ] +[[package]] +name = "node-common" +version = "1.7.2" +dependencies = [ + "kilt-support", + "public-credentials", + "public-credentials-rpc", + "serde", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -6511,6 +6547,8 @@ dependencies = [ "parachain-staking", "parity-scale-codec", "polkadot-parachain", + "public-credentials", + "public-credentials-runtime-api", "runtime-common", "scale-info", "serde", @@ -7990,6 +8028,49 @@ dependencies = [ "cc", ] +[[package]] +name = "public-credentials" +version = "1.7.2" +dependencies = [ + "base58", + "ctype", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex", + "kilt-support", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "public-credentials-rpc" +version = "1.7.2" +dependencies = [ + "jsonrpsee", + "parity-scale-codec", + "public-credentials-runtime-api", + "sp-api", + "sp-blockchain", + "sp-runtime", +] + +[[package]] +name = "public-credentials-runtime-api" +version = "1.7.2" +dependencies = [ + "parity-scale-codec", + "sp-api", + "sp-std", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -8440,6 +8521,8 @@ dependencies = [ "attestation", "frame-support", "frame-system", + "kilt-asset-dids", + "kilt-support", "log", "pallet-authorship", "pallet-balances", @@ -8448,6 +8531,7 @@ dependencies = [ "parachain-staking", "parity-scale-codec", "polkadot-parachain", + "public-credentials", "scale-info", "serde", "smallvec", @@ -10822,6 +10906,8 @@ dependencies = [ "parachain-staking", "parity-scale-codec", "polkadot-parachain", + "public-credentials", + "public-credentials-runtime-api", "runtime-common", "scale-info", "serde", diff --git a/Cargo.toml b/Cargo.toml index 040d093c24..a91307b735 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ members = [ "pallets/*", "rpc/did", "rpc/did/runtime-api", + "rpc/public-credentials", + "rpc/public-credentials/runtime-api", "runtimes/*", "support", + "crates/*", ] diff --git a/README.md b/README.md index 6b15195d69..d52b685d45 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# KILT-node · [![tests](https://gitlab.com/kiltprotocol/mashnet-node/badges/develop/pipeline.svg)](https://gitlab.com/kiltprotocol/mashnet-node/-/commits/develop) +# KILT-node · [![tests](https://gitlab.com/kiltprotocol/kilt-node/badges/develop/pipeline.svg)](https://gitlab.com/kiltprotocol/kilt-node/-/commits/develop)

diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml new file mode 100644 index 0000000000..8a94802e8f --- /dev/null +++ b/crates/assets/Cargo.toml @@ -0,0 +1,36 @@ +[package] +authors = ["KILT "] +description = "Asset DIDs and related structs, suitable for no_std environments." +edition = "2021" +name = "kilt-asset-dids" +repository = "https://github.com/KILTprotocol/kilt-node" +version = "1.7.2" + +[dependencies] +# External dependencies +base58 = {version = "0.2.0", default-features = false} +hex = {version = "0.4.3", default-features = false} +hex-literal = {version = "0.3.4", default-features = false} +log = {version = "0.4.17", default-features = false} + +# Parity dependencies +codec = {package = "parity-scale-codec", version = "3.1.2", default-features = false, features = ["derive"]} +scale-info = {version = "2.1.1", default-features = false, features = ["derive"]} + +# Substrate dependencies +frame-support = {branch = "polkadot-v0.9.28", default-features = false, git = "https://github.com/paritytech/substrate"} +sp-core = {branch = "polkadot-v0.9.28", default-features = false, git = "https://github.com/paritytech/substrate"} +sp-std = {branch = "polkadot-v0.9.28", default-features = false, git = "https://github.com/paritytech/substrate"} + +[features] +default = ["std"] + +std = [ + "codec/std", + "hex/std", + "log/std", + "scale-info/std", + "frame-support/std", + "sp-core/std", + "sp-std/std", +] diff --git a/crates/assets/src/asset.rs b/crates/assets/src/asset.rs new file mode 100644 index 0000000000..413adb10b9 --- /dev/null +++ b/crates/assets/src/asset.rs @@ -0,0 +1,947 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2022 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +// Exported types. This will always only re-export the latest version by +// default. +pub use v1::*; + +pub mod v1 { + use crate::errors::asset::{Error, IdentifierError, NamespaceError, ReferenceError}; + + use codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + + use core::{format_args, str}; + + use frame_support::{sp_runtime::RuntimeDebug, traits::ConstU32, BoundedVec}; + use sp_core::U256; + use sp_std::{fmt::Display, vec::Vec}; + + /// The minimum length, including separator symbols, an asset ID can have + /// according to the minimum values defined by the CAIP-19 definition. + pub const MINIMUM_ASSET_ID_LENGTH: usize = MINIMUM_NAMESPACE_LENGTH + 1 + MINIMUM_REFERENCE_LENGTH; + /// The maximum length, including separator symbols, an asset ID can have + /// according to the minimum values defined by the CAIP-19 definition. + pub const MAXIMUM_ASSET_ID_LENGTH: usize = + MAXIMUM_NAMESPACE_LENGTH + 1 + MAXIMUM_REFERENCE_LENGTH + 1 + MAXIMUM_IDENTIFIER_LENGTH; + + /// The minimum length of a valid asset ID namespace. + pub const MINIMUM_NAMESPACE_LENGTH: usize = 3; + /// The maximum length of a valid asset ID namespace. + pub const MAXIMUM_NAMESPACE_LENGTH: usize = 8; + const MAXIMUM_NAMESPACE_LENGTH_U32: u32 = MAXIMUM_NAMESPACE_LENGTH as u32; + /// The minimum length of a valid asset ID reference. + pub const MINIMUM_REFERENCE_LENGTH: usize = 1; + /// The maximum length of a valid asset ID reference. + pub const MAXIMUM_REFERENCE_LENGTH: usize = 64; + const MAXIMUM_REFERENCE_LENGTH_U32: u32 = MAXIMUM_REFERENCE_LENGTH as u32; + /// The minimum length of a valid asset ID identifier. + pub const MINIMUM_IDENTIFIER_LENGTH: usize = 1; + /// The maximum length of a valid asset ID reference. + pub const MAXIMUM_IDENTIFIER_LENGTH: usize = 78; + const MAXIMUM_IDENTIFIER_LENGTH_U32: u32 = MAXIMUM_IDENTIFIER_LENGTH as u32; + + /// Separator between asset namespace and asset reference. + const NAMESPACE_REFERENCE_SEPARATOR: u8 = b':'; + /// Separator between asset reference and asset identifier. + const REFERENCE_IDENTIFIER_SEPARATOR: u8 = b':'; + + /// Namespace for Slip44 assets. + pub const SLIP44_NAMESPACE: &[u8] = b"slip44"; + /// Namespace for Erc20 assets. + pub const ERC20_NAMESPACE: &[u8] = b"erc20"; + /// Namespace for Erc721 assets. + pub const ERC721_NAMESPACE: &[u8] = b"erc721"; + /// Namespace for Erc1155 assets. + pub const ERC1155_NAMESPACE: &[u8] = b"erc1155"; + + // TODO: Add link to the Asset DID spec once merged. + + /// The Asset ID component as specified in the Asset DID specification. + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub enum AssetId { + // A SLIP44 asset reference. + Slip44(Slip44Reference), + // An ERC20 asset reference. + Erc20(EvmSmartContractFungibleReference), + // An ERC721 asset reference. + Erc721(EvmSmartContractNonFungibleReference), + // An ERC1155 asset reference. + Erc1155(EvmSmartContractNonFungibleReference), + // A generic asset. + Generic(GenericAssetId), + } + + impl From for AssetId { + fn from(reference: Slip44Reference) -> Self { + Self::Slip44(reference) + } + } + + impl From for AssetId { + fn from(reference: EvmSmartContractFungibleReference) -> Self { + Self::Erc20(reference) + } + } + + impl AssetId { + /// Try to parse an `AssetId` instance from the provided UTF8-encoded + /// input. + pub fn from_utf8_encoded(input: I) -> Result + where + I: AsRef<[u8]> + Into>, + { + let input = input.as_ref(); + let input_length = input.len(); + if !(MINIMUM_ASSET_ID_LENGTH..=MAXIMUM_ASSET_ID_LENGTH).contains(&input_length) { + log::trace!( + "Length of provided input {} is not included in the inclusive range [{},{}]", + input_length, + MINIMUM_ASSET_ID_LENGTH, + MAXIMUM_ASSET_ID_LENGTH + ); + return Err(Error::InvalidFormat); + } + + let AssetComponents { + namespace, + reference, + identifier, + } = split_components(input); + + match (namespace, reference, identifier) { + // "slip44:" assets -> https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-20.md + (Some(SLIP44_NAMESPACE), _, Some(_)) => { + log::trace!("Slip44 namespace does not accept an asset identifier."); + Err(Error::InvalidFormat) + } + (Some(SLIP44_NAMESPACE), Some(slip44_reference), None) => { + Slip44Reference::from_utf8_encoded(slip44_reference).map(Self::Slip44) + } + // "erc20:" assets -> https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-21.md + (Some(ERC20_NAMESPACE), _, Some(_)) => { + log::trace!("Erc20 namespace does not accept an asset identifier."); + Err(Error::InvalidFormat) + } + (Some(ERC20_NAMESPACE), Some(erc20_reference), None) => { + EvmSmartContractFungibleReference::from_utf8_encoded(erc20_reference).map(Self::Erc20) + } + // "erc721:" assets -> https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-22.md + (Some(ERC721_NAMESPACE), Some(erc721_reference), identifier) => { + let reference = EvmSmartContractFungibleReference::from_utf8_encoded(erc721_reference)?; + let identifier = identifier.map_or(Ok(None), |id| { + EvmSmartContractNonFungibleIdentifier::from_utf8_encoded(id).map(Some) + })?; + Ok(Self::Erc721(EvmSmartContractNonFungibleReference( + reference, identifier, + ))) + } + // "erc1155:" assets-> https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-29.md + (Some(ERC1155_NAMESPACE), Some(erc1155_reference), identifier) => { + let reference = EvmSmartContractFungibleReference::from_utf8_encoded(erc1155_reference)?; + let identifier = identifier.map_or(Ok(None), |id| { + EvmSmartContractNonFungibleIdentifier::from_utf8_encoded(id).map(Some) + })?; + Ok(Self::Erc1155(EvmSmartContractNonFungibleReference( + reference, identifier, + ))) + } + // Generic yet valid asset IDs + _ => GenericAssetId::from_utf8_encoded(input).map(Self::Generic), + } + } + } + + impl Display for AssetId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Slip44(reference) => { + write!( + f, + "{}", + str::from_utf8(SLIP44_NAMESPACE) + .expect("Conversion of Slip44 namespace to string should never fail.") + )?; + write!(f, "{}", char::from(NAMESPACE_REFERENCE_SEPARATOR))?; + reference.fmt(f)?; + } + Self::Erc20(reference) => { + write!( + f, + "{}", + str::from_utf8(ERC20_NAMESPACE) + .expect("Conversion of Erc20 namespace to string should never fail.") + )?; + write!(f, "{}", char::from(NAMESPACE_REFERENCE_SEPARATOR))?; + reference.fmt(f)?; + } + Self::Erc721(EvmSmartContractNonFungibleReference(reference, identifier)) => { + write!( + f, + "{}", + str::from_utf8(ERC721_NAMESPACE) + .expect("Conversion of Erc721 namespace to string should never fail.") + )?; + write!(f, "{}", char::from(NAMESPACE_REFERENCE_SEPARATOR))?; + reference.fmt(f)?; + if let Some(id) = identifier { + write!(f, "{}", char::from(REFERENCE_IDENTIFIER_SEPARATOR))?; + id.fmt(f)?; + } + } + Self::Erc1155(EvmSmartContractNonFungibleReference(reference, identifier)) => { + write!( + f, + "{}", + str::from_utf8(ERC1155_NAMESPACE) + .expect("Conversion of Erc1155 namespace to string should never fail.") + )?; + write!(f, "{}", char::from(NAMESPACE_REFERENCE_SEPARATOR))?; + reference.fmt(f)?; + if let Some(id) = identifier { + write!(f, "{}", char::from(REFERENCE_IDENTIFIER_SEPARATOR))?; + id.fmt(f)?; + } + } + Self::Generic(GenericAssetId { + namespace, + reference, + id, + }) => { + namespace.fmt(f)?; + write!(f, "{}", char::from(NAMESPACE_REFERENCE_SEPARATOR))?; + reference.fmt(f)?; + if let Some(identifier) = id { + write!(f, "{}", char::from(REFERENCE_IDENTIFIER_SEPARATOR))?; + identifier.fmt(f)?; + } + } + } + Ok(()) + } + } + + const fn check_namespace_length_bounds(namespace: &[u8]) -> Result<(), NamespaceError> { + let namespace_length = namespace.len(); + if namespace_length < MINIMUM_NAMESPACE_LENGTH { + Err(NamespaceError::TooShort) + } else if namespace_length > MAXIMUM_NAMESPACE_LENGTH { + Err(NamespaceError::TooLong) + } else { + Ok(()) + } + } + + const fn check_reference_length_bounds(reference: &[u8]) -> Result<(), ReferenceError> { + let reference_length = reference.len(); + if reference_length < MINIMUM_REFERENCE_LENGTH { + Err(ReferenceError::TooShort) + } else if reference_length > MAXIMUM_REFERENCE_LENGTH { + Err(ReferenceError::TooLong) + } else { + Ok(()) + } + } + + const fn check_identifier_length_bounds(identifier: &[u8]) -> Result<(), IdentifierError> { + let identifier_length = identifier.len(); + if identifier_length < MINIMUM_IDENTIFIER_LENGTH { + Err(IdentifierError::TooShort) + } else if identifier_length > MAXIMUM_IDENTIFIER_LENGTH { + Err(IdentifierError::TooLong) + } else { + Ok(()) + } + } + + /// Split the given input into its components, i.e., namespace, reference, + /// and identifier, if the proper separators are found. + fn split_components(input: &[u8]) -> AssetComponents { + let mut split = input.splitn(2, |c| *c == NAMESPACE_REFERENCE_SEPARATOR); + let (namespace, reference) = (split.next(), split.next()); + + // Split the remaining reference to extract the identifier, if present + let (reference, identifier) = if let Some(r) = reference { + let mut split = r.splitn(2, |c| *c == REFERENCE_IDENTIFIER_SEPARATOR); + // Split the reference further, if present + (split.next(), split.next()) + } else { + // Return the old reference, which is None if we are at this point + (reference, None) + }; + + AssetComponents { + namespace, + reference, + identifier, + } + } + + struct AssetComponents<'a> { + namespace: Option<&'a [u8]>, + reference: Option<&'a [u8]>, + identifier: Option<&'a [u8]>, + } + + /// A Slip44 asset reference. + /// It is a modification of the [CAIP-20 spec](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-20.md) + /// according to the rules defined in the Asset DID method specification. + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct Slip44Reference(pub(crate) U256); + + impl Slip44Reference { + /// Parse a UTF8-encoded decimal Slip44 asset reference, failing if the + /// input string is not valid. + pub(crate) fn from_utf8_encoded(input: I) -> Result + where + I: AsRef<[u8]> + Into>, + { + let input = input.as_ref(); + check_reference_length_bounds(input)?; + + let decoded = str::from_utf8(input).map_err(|_| { + log::trace!("Provided input is not a valid UTF8 string as expected by a Slip44 reference."); + ReferenceError::InvalidFormat + })?; + let parsed = U256::from_dec_str(decoded).map_err(|_| { + log::trace!("Provided input is not a valid u256 value as expected by a Slip44 reference."); + ReferenceError::InvalidFormat + })?; + // Unchecked since we already checked for maximum length and hence maximum value + Ok(Self(parsed)) + } + } + + impl TryFrom for Slip44Reference { + type Error = Error; + + fn try_from(value: U256) -> Result { + // Max value for 64-digit decimal values (used for Slip44 references so far). + // TODO: This could be enforced at compilation time once constraints on generics + // will be available. + // https://rust-lang.github.io/rfcs/2000-const-generics.html + if value + <= U256::from_str_radix("9999999999999999999999999999999999999999999999999999999999999999", 10) + .expect("Casting the maximum value for a Slip44 reference into a U256 should never fail.") + { + Ok(Self(value)) + } else { + Err(ReferenceError::TooLong.into()) + } + } + } + + impl From for Slip44Reference { + fn from(value: u128) -> Self { + Self(value.into()) + } + } + + // Getters + impl Slip44Reference { + pub fn inner(&self) -> &U256 { + &self.0 + } + } + + impl Display for Slip44Reference { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } + } + + /// An asset reference that is identifiable only by an EVM smart contract + /// (e.g., a fungible token). It is a modification of the [CAIP-21 spec](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-21.md) + /// according to the rules defined in the Asset DID method specification. + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct EvmSmartContractFungibleReference(pub(crate) [u8; 20]); + + impl EvmSmartContractFungibleReference { + /// Parse a UTF8-encoded smart contract HEX address (including the `0x` + /// prefix), failing if the input string is not valid. + pub(crate) fn from_utf8_encoded(input: I) -> Result + where + I: AsRef<[u8]> + Into>, + { + let input = input.as_ref(); + // If the prefix is "0x" => parse the address + if let [b'0', b'x', contract_address @ ..] = input { + check_reference_length_bounds(contract_address)?; + + let decoded = hex::decode(contract_address).map_err(|_| { + log::trace!("Provided input is not a valid hex value as expected by a smart contract reference."); + ReferenceError::InvalidFormat + })?; + let inner: [u8; 20] = decoded.try_into().map_err(|_| { + log::trace!("Provided input is not 20 bytes long as expected by a smart contract reference."); + ReferenceError::InvalidFormat + })?; + Ok(Self(inner)) + // Otherwise fail + } else { + log::trace!("Provided input does not have the `0x` prefix as expected by a smart contract reference."); + Err(ReferenceError::InvalidFormat.into()) + } + } + } + + // Getters + impl EvmSmartContractFungibleReference { + pub fn inner(&self) -> &[u8] { + &self.0 + } + } + + impl Display for EvmSmartContractFungibleReference { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "0x{}", hex::encode(self.0)) + } + } + + /// An asset reference that is identifiable by an EVM smart contract and an + /// optional identifier (e.g., an NFT collection or instance thereof). It is + /// a modification of the [CAIP-22 spec](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-22.md) and + /// [CAIP-29 spec](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-29.md) + /// according to the rules defined in the Asset DID method specification. + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct EvmSmartContractNonFungibleReference( + pub(crate) EvmSmartContractFungibleReference, + pub(crate) Option, + ); + + // Getters + impl EvmSmartContractNonFungibleReference { + pub fn smart_contract(&self) -> &EvmSmartContractFungibleReference { + &self.0 + } + + pub fn identifier(&self) -> &Option { + &self.1 + } + } + + /// An asset identifier for an EVM smart contract collection (e.g., an NFT + /// instance). + /// Since the identifier can be up to 78 characters long of an unknown + /// alphabet, this type simply contains the UTF-8 encoding of such an + /// identifier without applying any special parsing/decoding logic. + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct EvmSmartContractNonFungibleIdentifier( + pub(crate) BoundedVec>, + ); + + impl EvmSmartContractNonFungibleIdentifier { + /// Parse a UTF8-encoded smart contract asset identifier, failing if the + /// input string is not valid. + pub(crate) fn from_utf8_encoded(input: I) -> Result + where + I: AsRef<[u8]> + Into>, + { + let input = input.as_ref(); + check_identifier_length_bounds(input)?; + + input.iter().try_for_each(|c| { + if !(b'0'..=b'9').contains(c) { + log::trace!("Provided input has some invalid values as expected by a smart contract-based asset identifier."); + Err(IdentifierError::InvalidFormat) + } else { + Ok(()) + } + })?; + + Ok(Self( + Vec::::from(input) + .try_into() + .map_err(|_| IdentifierError::InvalidFormat)?, + )) + } + } + + // Getters + impl EvmSmartContractNonFungibleIdentifier { + pub fn inner(&self) -> &[u8] { + &self.0 + } + } + + impl Display for EvmSmartContractNonFungibleIdentifier { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // We checked when the type is created that all characters are valid digits. + write!( + f, + "{}", + str::from_utf8(&self.0) + .expect("Conversion of EvmSmartContractNonFungibleIdentifier to string should never fail.") + ) + } + } + + /// A generic asset ID compliant with the [CAIP-19 spec](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-19.md) that cannot be boxed in any of the supported variants. + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct GenericAssetId { + pub(crate) namespace: GenericAssetNamespace, + pub(crate) reference: GenericAssetReference, + pub(crate) id: Option, + } + + impl GenericAssetId { + /// Parse a generic UTF8-encoded asset ID, failing if the input does not + /// respect the CAIP-19 requirements. + pub(crate) fn from_utf8_encoded(input: I) -> Result + where + I: AsRef<[u8]> + Into>, + { + let AssetComponents { + namespace, + reference, + identifier, + } = split_components(input.as_ref()); + + match (namespace, reference, identifier) { + (Some(namespace), Some(reference), identifier) => Ok(Self { + namespace: GenericAssetNamespace::from_utf8_encoded(namespace)?, + reference: GenericAssetReference::from_utf8_encoded(reference)?, + // Transform Option to Result