From e92d9c33254d6f97865d94e7d197d15c9b02e921 Mon Sep 17 00:00:00 2001 From: William Freudenberger Date: Fri, 16 Sep 2022 16:28:05 +0200 Subject: [PATCH 01/46] feat: write delegator state migration --- pallets/parachain-staking/src/lib.rs | 3 +- pallets/parachain-staking/src/migration.rs | 135 +++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 pallets/parachain-staking/src/migration.rs diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index bc1607ee90..323632b006 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -119,6 +119,7 @@ pub mod benchmarking; pub mod default_weights; +pub mod migration; #[cfg(test)] pub(crate) mod mock; #[cfg(test)] @@ -171,7 +172,7 @@ pub mod pallet { pub(crate) const STAKING_ID: LockIdentifier = *b"kiltpstk"; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(7); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(8); /// Pallet for parachain staking. #[pallet::pallet] diff --git a/pallets/parachain-staking/src/migration.rs b/pallets/parachain-staking/src/migration.rs new file mode 100644 index 0000000000..b8e5c09e03 --- /dev/null +++ b/pallets/parachain-staking/src/migration.rs @@ -0,0 +1,135 @@ +// 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 + +#![cfg_attr(not(feature = "std"), no_std)] + +use crate::{ + set::OrderedSet, + types::{BalanceOf, Delegator, Stake}, +}; + +use super::*; +use core::marker::PhantomData; +use frame_support::{ + dispatch::GetStorageVersion, + pallet_prelude::ValueQuery, + parameter_types, storage_alias, + traits::{Get, OnRuntimeUpgrade}, + weights::Weight, + RuntimeDebug, +}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[cfg(feature = "try-runtime")] +use sp_runtime::SaturatedConversion; + +// Old types +#[derive(Encode, Decode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] +#[scale_info(skip_type_params(MaxCollatorsPerDelegator))] +#[codec(mel_bound(AccountId: MaxEncodedLen, Balance: MaxEncodedLen))] +pub struct DelegatorOld> { + pub delegations: OrderedSet, MaxCollatorsPerDelegator>, + pub total: Balance, +} +parameter_types! { + const MaxCollatorsPerDelegator: u32 = 1; +} + +/// Number of delegators post migration +#[storage_alias] +type CounterForDelegators = StorageValue, u32, ValueQuery>; + +pub struct StakingPayoutRefactor(PhantomData); +impl OnRuntimeUpgrade for StakingPayoutRefactor { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let onchain = Pallet::::on_chain_storage_version(); + + log::info!( + "Running migration with current storage version {:?} / onchain {:?}", + current, + onchain + ); + + if current == 8 && onchain == 7 { + let num_delegators = migrate_delegators::(); + T::DbWeight::get().reads_writes(num_delegators, num_delegators) + } else { + log::info!("StakingPayoutRefactor did not execute. This probably should be removed"); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<(), &'static str> { + use sp_runtime::traits::Zero; + + let current = Pallet::::current_storage_version(); + + assert_eq!( + current, 7, + "ParachainStaking StorageVersion is {:?} instead of 7", + current + ); + assert!( + CounterForDelegators::::get().is_zero(), + "CounterForDelegators already set." + ); + // store number of delegators before migration + CounterForDelegators::::put(DelegatorState::::iter_keys().count().saturated_into::()); + Ok(()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade() -> Result<(), &'static str> { + // new version must be set. + let onchain = Pallet::::on_chain_storage_version(); + + assert_eq!( + onchain, 8, + "ParachainStaking StorageVersion post-migration is not 8, but {:?} instead.", + onchain + ); + + let old_num_delegators: u32 = CounterForDelegators::::get(); + let new_num_delegators: u32 = DelegatorState::::iter_keys().count().saturated_into::(); + assert_eq!( + old_num_delegators, new_num_delegators, + "Number of delegators changed during migration! Before {:?} vs. now {:?}", + old_num_delegators, new_num_delegators + ); + Ok(()) + } +} + +fn migrate_delegators() -> Weight { + let mut counter = 0; + DelegatorState::::translate_values::< + Option, MaxCollatorsPerDelegator>>, + _, + >(|maybe_old| { + counter += 1; + maybe_old.map(|old| Delegator { + amount: old.total, + owner: old.delegations.get(0).map(|stake| stake.owner.clone()), + }) + }); + + counter +} From e703493c668c3eb2af734920ac05abca615de5c6 Mon Sep 17 00:00:00 2001 From: William Freudenberger Date: Mon, 19 Sep 2022 09:27:52 +0200 Subject: [PATCH 02/46] tests: comp api claim with claiming --- pallets/parachain-staking/src/tests.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index c708e16356..411fb0b74d 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -4295,14 +4295,28 @@ fn api_get_unclaimed_staking_rewards() { // Should only increment rewards of 3 roll_to(3, vec![None, None, Some(3)]); + let rewards_1 = StakePallet::get_unclaimed_staking_rewards(&1); + let rewards_2 = StakePallet::get_unclaimed_staking_rewards(&2); + let rewards_3 = StakePallet::get_unclaimed_staking_rewards(&3); assert_eq!( - 2 * StakePallet::get_unclaimed_staking_rewards(&1), - StakePallet::get_unclaimed_staking_rewards(&3), + 2 * rewards_1, + rewards_3, ); assert_eq!( - StakePallet::get_unclaimed_staking_rewards(&2), + rewards_2, inflation_config.delegator.reward_rate.per_block * stake * 2 ); + + // API and actual claiming should match + assert_ok!(StakePallet::increment_collator_rewards(Origin::signed(1))); + assert_ok!(StakePallet::claim_rewards(Origin::signed(1))); + assert_eq!(rewards_1, Balances::usable_balance(&1)); + assert_ok!(StakePallet::increment_delegator_rewards(Origin::signed(2))); + assert_ok!(StakePallet::claim_rewards(Origin::signed(2))); + assert_eq!(rewards_2, Balances::usable_balance(&2)); + assert_ok!(StakePallet::increment_collator_rewards(Origin::signed(3))); + assert_ok!(StakePallet::claim_rewards(Origin::signed(3))); + assert_eq!(rewards_3 + 98 * stake, Balances::usable_balance(&3)); }); } From 02eb40733520f5b4a5d18378e2bcbcfd3c79ff43 Mon Sep 17 00:00:00 2001 From: William Freudenberger Date: Mon, 19 Sep 2022 10:22:13 +0200 Subject: [PATCH 03/46] stlye: fmt --- pallets/parachain-staking/src/tests.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index 411fb0b74d..0b714dd2e7 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -4298,14 +4298,8 @@ fn api_get_unclaimed_staking_rewards() { let rewards_1 = StakePallet::get_unclaimed_staking_rewards(&1); let rewards_2 = StakePallet::get_unclaimed_staking_rewards(&2); let rewards_3 = StakePallet::get_unclaimed_staking_rewards(&3); - assert_eq!( - 2 * rewards_1, - rewards_3, - ); - assert_eq!( - rewards_2, - inflation_config.delegator.reward_rate.per_block * stake * 2 - ); + assert_eq!(2 * rewards_1, rewards_3,); + assert_eq!(rewards_2, inflation_config.delegator.reward_rate.per_block * stake * 2); // API and actual claiming should match assert_ok!(StakePallet::increment_collator_rewards(Origin::signed(1))); From a8f09806bcd56068895cf8639c416b68f7bc15e3 Mon Sep 17 00:00:00 2001 From: William Freudenberger Date: Mon, 19 Sep 2022 16:22:33 +0200 Subject: [PATCH 04/46] fix: migration after testing with try-runtime --- pallets/parachain-staking/src/migration.rs | 92 ++++++++++++++++------ runtimes/peregrine/src/lib.rs | 5 +- runtimes/spiritnet/src/lib.rs | 5 +- 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/pallets/parachain-staking/src/migration.rs b/pallets/parachain-staking/src/migration.rs index b8e5c09e03..b1528c7bc3 100644 --- a/pallets/parachain-staking/src/migration.rs +++ b/pallets/parachain-staking/src/migration.rs @@ -27,19 +27,20 @@ use super::*; use core::marker::PhantomData; use frame_support::{ dispatch::GetStorageVersion, - pallet_prelude::ValueQuery, + pallet_prelude::{StorageVersion, ValueQuery}, parameter_types, storage_alias, traits::{Get, OnRuntimeUpgrade}, weights::Weight, - RuntimeDebug, + Blake2_128Concat, RuntimeDebug, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; +use sp_runtime::traits::Zero; #[cfg(feature = "try-runtime")] use sp_runtime::SaturatedConversion; -// Old types +// Old delegator type needed for translating storage map #[derive(Encode, Decode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] #[scale_info(skip_type_params(MaxCollatorsPerDelegator))] #[codec(mel_bound(AccountId: MaxEncodedLen, Balance: MaxEncodedLen))] @@ -50,6 +51,14 @@ pub struct DelegatorOld = StorageMap< + Pallet, + Blake2_128Concat, + ::AccountId, + DelegatorOld<::AccountId, BalanceOf, MaxCollatorsPerDelegator>, +>; /// Number of delegators post migration #[storage_alias] @@ -62,37 +71,52 @@ impl OnRuntimeUpgrade for StakingPayoutRefactor { let onchain = Pallet::::on_chain_storage_version(); log::info!( - "Running migration with current storage version {:?} / onchain {:?}", + "💰 Running migration with current storage version {:?} / onchain {:?}", current, onchain ); if current == 8 && onchain == 7 { let num_delegators = migrate_delegators::(); + log::info!("💰 Migrated {:?} delegator states", num_delegators); + StorageVersion::new(8).put::>(); T::DbWeight::get().reads_writes(num_delegators, num_delegators) } else { - log::info!("StakingPayoutRefactor did not execute. This probably should be removed"); + log::info!("💰 StakingPayoutRefactor did not execute. This probably should be removed"); T::DbWeight::get().reads(1) } } #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result<(), &'static str> { - use sp_runtime::traits::Zero; - - let current = Pallet::::current_storage_version(); + let current = Pallet::::on_chain_storage_version(); assert_eq!( current, 7, - "ParachainStaking StorageVersion is {:?} instead of 7", + "ParachainStaking on-chain StorageVersion is {:?} instead of 7", current ); + + // sanity check each old entry + for delegator in DelegatorStateOld::::iter_values() { + assert!( + delegator.delegations.is_empty(), + "There exists a delegator without a collator in pre migration!" + ); + assert!( + !delegator.total.is_zero(), + "There exists a delegator without any self stake in pre migration!", + ) + } + assert!( CounterForDelegators::::get().is_zero(), "CounterForDelegators already set." ); - // store number of delegators before migration + // store number of delegators before migration to check against in post + // migration CounterForDelegators::::put(DelegatorState::::iter_keys().count().saturated_into::()); + Ok(()) } @@ -114,22 +138,44 @@ impl OnRuntimeUpgrade for StakingPayoutRefactor { "Number of delegators changed during migration! Before {:?} vs. now {:?}", old_num_delegators, new_num_delegators ); + + // sanity check each new entry + for delegator in DelegatorState::::iter_values() { + assert!( + delegator.owner.is_some(), + "There exists a delegator without a collator in post migration!" + ); + assert!( + !delegator.amount.is_zero(), + "There exists a delegator without any self stake in post migration!", + ) + } + + log::info!("💰 Post staking payout refactor upgrade checks match up."); Ok(()) } } +/// Translate all values from the DelegatorState StorageMap from old to new fn migrate_delegators() -> Weight { - let mut counter = 0; - DelegatorState::::translate_values::< - Option, MaxCollatorsPerDelegator>>, - _, - >(|maybe_old| { - counter += 1; - maybe_old.map(|old| Delegator { - amount: old.total, - owner: old.delegations.get(0).map(|stake| stake.owner.clone()), - }) - }); - - counter + let mut translations = 0; + DelegatorState::::translate_values( + |old: DelegatorOld, MaxCollatorsPerDelegator>| { + translations += 1; + + // Should never occur because of pre checks but let's be save + if old.total.is_zero() { + log::debug!("Translating delegator with 0 stake amount") + } + if old.delegations.get(0).is_none() { + log::debug!("Translating delegator without collator") + } + Some(Delegator { + amount: old.total, + // We only allowed one delegation per delegator, thus taking first element is sufficient + owner: old.delegations.get(0).map(|stake| stake.owner.clone()), + }) + }, + ); + translations } diff --git a/runtimes/peregrine/src/lib.rs b/runtimes/peregrine/src/lib.rs index e0081bff91..a55cf6111e 100644 --- a/runtimes/peregrine/src/lib.rs +++ b/runtimes/peregrine/src/lib.rs @@ -1036,7 +1036,10 @@ pub type Executive = frame_executive::Executive< // Executes pallet hooks in reverse order of definition in construct_runtime // If we want to switch to AllPalletsWithSystem, we need to reorder the staking pallets AllPalletsReversedWithSystemFirst, - EthereumMigration, + ( + EthereumMigration, + parachain_staking::migration::StakingPayoutRefactor, + ), >; impl_runtime_apis! { diff --git a/runtimes/spiritnet/src/lib.rs b/runtimes/spiritnet/src/lib.rs index b47da649a6..dfb30f4076 100644 --- a/runtimes/spiritnet/src/lib.rs +++ b/runtimes/spiritnet/src/lib.rs @@ -1028,7 +1028,10 @@ pub type Executive = frame_executive::Executive< // Executes pallet hooks in reverse order of definition in construct_runtime // If we want to switch to AllPalletsWithSystem, we need to reorder the staking pallets AllPalletsReversedWithSystemFirst, - EthereumMigration, + ( + EthereumMigration, + parachain_staking::migration::StakingPayoutRefactor, + ), >; impl_runtime_apis! { From 32908878551d842e47faab608cb98191d51a7d10 Mon Sep 17 00:00:00 2001 From: Antonio Date: Tue, 20 Sep 2022 15:21:16 +0200 Subject: [PATCH 05/46] feat: asset DIDs and public credentials (#378) * wip: skeleton in place * wip: trying to fight with rust-analyzer * wip: FFS * wip: tests for the new pallet * test: add unit tests for the new pallet * chore: fmt * fix: restore toolchain to nightly * core: clippy * wip: minor chores * test: add hcecks for deposit reservations * test: add InvalidInput test case * feat: working on parsing the right chain_id information * feat: add dotsama and solana chains * feat: generic chain support * feat: add utility functions for chain IDs * chore: clippy + fmt * fix: uncomment remaining test functions * wip: first version of new pallet * feat: new pallet for chain IDs * wip: fixing last issues * fix: chain ids now working fine * feat: asset id complete (untested) * most of unit tests for asset IDs in * test: unit tests passing * wip: almost complete * chore: small improvements * feat: default implementations of important traits * chore: few needed adjustments * test: add test for too long credentials * chore: mashnet-node-runtime compiling * chore: minor refinements * chore: remove asset transfer feature * chore: add test cases for smart contract addresses without leading * fix: dependencies in mashnet-node compilation * chore: re-organize imports * chore: last cleanups for chain asset DID stuff * fix: adjust asset DID definition in code * wip: whole project compiling * wip: benchmarking refactoring * chore: refactored the default implementation for AssetDid * wip: refactor almost completed * wip: first benchmark case compiling * wip: refactoring before review * wip: refactoring pt. 2 * wip: refactoring pt. 3 * chore: optimise eip155 chain reference * wip: optimizing storage * wip: tests failing * chore: completed * wip: switch to hex_literal * chore: refactor complete * chore: final chores for the asset DID crate * bench: setup for public-credentials pallet complete * wip: implementing the Display trait for the AssetDID type * test: unit tests for ChainID Display implementation working * test: unit tests completed for asset DID crate * feat: whole project compiling after implementing Display * chore: fmt * chore: update benchmarking script for new pallet * chore: update benchmark scripts and weights * chore: update weights * fix: correct signature verification weight * chore: address TODO * chore: update test script to use --all-targets and check for test targets as well * test: storage deposit test for runtimes * chore: address last TODOs * chore: fmt * chore: factor everything out into variables * fix: clippy issues * fix: checks for asset DID length bounds * chore: make failing clearer for decimal references * chore: factor out bounds checking function * fix: max line length limit * chore: factor out split function * chore: right syntax for generics * chore: typo * chore: factor out credential retrieval in public credentials pallet * feat: add public credentials runtime API definition * feat: add public credentials runtime API implementation for standalone * wip: only need to add the RPC to the node executable * wip: add serialization support * feat: compiling * fix: compiling with one generic added * fix: compiling with second argument * COMPILING * chore: qol improvements * feat: add key to return value of get_credentials * chore: remove unneded serde support * deps: remove unnecessary serde deps * wip: refactoring * almost there * feat: add runtime api implementation for parachain runtimes * chore: remove InputError type from public-credentials pallet * fix: change TryFrom requirement for SubjectId * chore: remove unused deps * chore: update repo links * chore: move errors in their own module * chore: comments * chore: split components into its own struct * chore: add link to const generics * chore: replace switch with if let * chore: replace matches! with contains * chore: make comment a doc comment * chore: revert Eq derive for DidDetails * chore: attestation namespace not needed * chore: re-add commented out pallets in bench script * chore: add some logging to the InvalidFormat error cases * chore: move serde import up again * chore: replace From for i32 with repr(i32) * chore: improve efficienct of Display * fix: changes to public credentials based on feedback (#392) * wip: changing stuff based on meeting * feat: generate credential ID on chain * remove dependency on attestation pallet * add revocation and unrevocation * add checks that the attestation pallet also performs * add ctype info in the storage * Add support for access control * Update benchmarks * Runtimes compiling * nodes compiling with new filtering feature * Remove authorization id from possible filters * fmt * update benchmarks and default weights (to be re-executed on bench machine) * change serialization logic for ctype filter * fixes and chores * refactor common function * change get_credential to only take a credential ID parameter * rename rpc endpoints for easier decoration * update RPC method names again * change encoding of filter to be camelCase to be consistent with PolkadotJS * Apply suggestions from review * Fix clone runtime for public credentials RPC * Remove unused deps * Remove most of the unrelated changes * Last chores * Remove non_exhaustive from all structs * Add comments to Cargo.toml * Switch ordering in Cargo.toml * Switch ordering in Cargo.toml * Cargo.toml fixes * Refactor benchmarking helper function * Update public credentials pallet description * Update comments in pallet * Update description of the reclaim_deposit extrinsic * More comments * Add case for invalid input * remove unused variable in benchmarks * Make comment a doc comment * fmt * Fix match case * Fix import * Fix single match case * Fix new line * Fix access control file * Fix more matches * Fix proxy type * Fmt * Fix repo links --- Cargo.lock | 90 +- Cargo.toml | 3 + README.md | 2 +- crates/assets/Cargo.toml | 36 + crates/assets/src/asset.rs | 947 ++++++++++++++++++ crates/assets/src/chain.rs | 900 +++++++++++++++++ crates/assets/src/errors.rs | 163 +++ crates/assets/src/lib.rs | 37 + crates/assets/src/v1.rs | 205 ++++ nodes/common/Cargo.toml | 18 + nodes/common/src/lib.rs | 21 + nodes/common/src/public_credentials.rs | 86 ++ nodes/parachain/Cargo.toml | 4 + nodes/parachain/src/rpc.rs | 43 +- nodes/parachain/src/service.rs | 22 +- nodes/standalone/Cargo.toml | 6 +- nodes/standalone/src/command.rs | 2 +- nodes/standalone/src/rpc.rs | 46 +- nodes/standalone/src/service.rs | 3 +- pallets/attestation/Cargo.toml | 2 +- pallets/attestation/src/lib.rs | 28 +- pallets/attestation/src/mock.rs | 9 +- pallets/ctype/Cargo.toml | 2 +- pallets/delegation/Cargo.toml | 6 +- pallets/delegation/src/access_control.rs | 101 +- pallets/did/Cargo.toml | 2 +- pallets/pallet-did-lookup/Cargo.toml | 2 +- pallets/pallet-inflation/Cargo.toml | 2 +- pallets/pallet-web3-names/Cargo.toml | 2 +- pallets/public-credentials/Cargo.toml | 64 ++ .../public-credentials/src/access_control.rs | 131 +++ .../public-credentials/src/benchmarking.rs | 186 ++++ pallets/public-credentials/src/credentials.rs | 61 ++ .../public-credentials/src/default_weights.rs | 147 +++ pallets/public-credentials/src/lib.rs | 548 ++++++++++ pallets/public-credentials/src/mock.rs | 449 +++++++++ pallets/public-credentials/src/tests.rs | 745 ++++++++++++++ rpc/did/Cargo.toml | 4 +- rpc/did/runtime-api/Cargo.toml | 2 +- rpc/public-credentials/Cargo.toml | 30 + rpc/public-credentials/runtime-api/Cargo.toml | 23 + rpc/public-credentials/runtime-api/src/lib.rs | 35 + rpc/public-credentials/src/lib.rs | 231 +++++ runtimes/clone/Cargo.toml | 8 +- runtimes/clone/src/lib.rs | 16 +- runtimes/common/Cargo.toml | 7 + runtimes/common/src/assets.rs | 103 ++ runtimes/common/src/authorization.rs | 114 ++- runtimes/common/src/constants.rs | 15 + runtimes/common/src/lib.rs | 1 + runtimes/peregrine/Cargo.toml | 7 + runtimes/peregrine/src/lib.rs | 42 + runtimes/peregrine/src/tests.rs | 27 +- runtimes/peregrine/src/weights/mod.rs | 1 + .../src/weights/public_credentials.rs | 93 ++ runtimes/spiritnet/Cargo.toml | 6 + runtimes/spiritnet/src/lib.rs | 42 + runtimes/spiritnet/src/tests.rs | 27 +- runtimes/spiritnet/src/weights/mod.rs | 1 + .../src/weights/public_credentials.rs | 93 ++ runtimes/standalone/Cargo.toml | 6 + runtimes/standalone/src/lib.rs | 40 + scripts/run_benches_for_pallets.sh | 1 + scripts/run_benches_for_runtime.sh | 1 + support/Cargo.toml | 8 +- support/src/deposit.rs | 24 +- support/src/lib.rs | 14 +- support/src/traits.rs | 7 + 68 files changed, 6038 insertions(+), 112 deletions(-) create mode 100644 crates/assets/Cargo.toml create mode 100644 crates/assets/src/asset.rs create mode 100644 crates/assets/src/chain.rs create mode 100644 crates/assets/src/errors.rs create mode 100644 crates/assets/src/lib.rs create mode 100644 crates/assets/src/v1.rs create mode 100644 nodes/common/Cargo.toml create mode 100644 nodes/common/src/lib.rs create mode 100644 nodes/common/src/public_credentials.rs create mode 100644 pallets/public-credentials/Cargo.toml create mode 100644 pallets/public-credentials/src/access_control.rs create mode 100644 pallets/public-credentials/src/benchmarking.rs create mode 100644 pallets/public-credentials/src/credentials.rs create mode 100644 pallets/public-credentials/src/default_weights.rs create mode 100644 pallets/public-credentials/src/lib.rs create mode 100644 pallets/public-credentials/src/mock.rs create mode 100644 pallets/public-credentials/src/tests.rs create mode 100644 rpc/public-credentials/Cargo.toml create mode 100644 rpc/public-credentials/runtime-api/Cargo.toml create mode 100644 rpc/public-credentials/runtime-api/src/lib.rs create mode 100644 rpc/public-credentials/src/lib.rs create mode 100644 runtimes/common/src/assets.rs create mode 100644 runtimes/peregrine/src/weights/public_credentials.rs create mode 100644 runtimes/spiritnet/src/weights/public_credentials.rs 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