diff --git a/Cargo.lock b/Cargo.lock index d37babb5bbc0..8a7ddfdfd40b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10502,6 +10502,25 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-derivatives" +version = "1.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 14.0.0", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", +] + [[package]] name = "pallet-dev-mode" version = "10.0.0" @@ -14261,6 +14280,7 @@ dependencies = [ "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", + "pallet-derivatives", "pallet-dev-mode", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", diff --git a/Cargo.toml b/Cargo.toml index 7ae7c3bd1811..f549b4d39ebf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -227,6 +227,7 @@ members = [ "polkadot/xcm/docs", "polkadot/xcm/pallet-xcm", "polkadot/xcm/pallet-xcm-benchmarks", + "polkadot/xcm/pallet-derivatives", "polkadot/xcm/procedural", "polkadot/xcm/xcm-builder", "polkadot/xcm/xcm-executor", @@ -983,6 +984,7 @@ pallet-xcm = { path = "polkadot/xcm/pallet-xcm", default-features = false } pallet-xcm-benchmarks = { path = "polkadot/xcm/pallet-xcm-benchmarks", default-features = false } pallet-xcm-bridge-hub = { path = "bridges/modules/xcm-bridge-hub", default-features = false } pallet-xcm-bridge-hub-router = { path = "bridges/modules/xcm-bridge-hub-router", default-features = false } +pallet-derivatives = { path = "polkadot/xcm/pallet-derivatives", default-features = false } parachain-info = { path = "cumulus/parachains/pallets/parachain-info", default-features = false, package = "staging-parachain-info" } parachain-template-runtime = { path = "templates/parachain/runtime" } parachains-common = { path = "cumulus/parachains/common", default-features = false } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index a11dca4f6d7c..7689bc9214a5 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -49,17 +49,17 @@ use testnet_parachains_constants::rococo::snowbridge::{ }; use xcm::latest::prelude::*; use xcm_builder::{ - AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain, - AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, - DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal, DescribeFamily, - EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, - GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, LocalMint, - NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignPaidRemoteExporter, - SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + unique_instances::UniqueInstancesAdapter, AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, + AllowHrmpNotificationsFromRelayChain, AllowKnownQueryResponses, AllowSubscriptionsFrom, + AllowTopLevelPaidExecutionFrom, DenyReserveTransferToRelayChain, DenyThenTry, + DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, FrameTransactionalProcessor, + FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, + IsConcrete, LocalMint, MatchInClassInstances, NetworkExportTableItem, NoChecking, + ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, + SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, + SignedToAccountId32, SovereignPaidRemoteExporter, SovereignSignedViaLocation, StartsWith, + StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, + WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::XcmExecutor; @@ -149,19 +149,11 @@ pub type UniquesConvertedConcreteId = assets_common::UniquesConvertedConcreteId; /// Means for transacting unique assets. -pub type UniquesTransactor = NonFungiblesAdapter< - // Use this non-fungibles implementation: - Uniques, - // This adapter will handle any non-fungible asset from the uniques pallet. - UniquesConvertedConcreteId, - // Convert an XCM Location into a local account id: - LocationToAccountId, - // Our chain's account ID type (we can't get away without mentioning it explicitly): +pub type UniquesTransactor = UniqueInstancesAdapter< AccountId, - // Does not check teleports. - NoChecking, - // The account to use for tracking teleports. - CheckingAccount, + LocationToAccountId, + MatchInClassInstances, + pallet_uniques::asset_ops::Item, >; /// `AssetId`/`Balance` converter for `ForeignAssets`. diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index 5ecfce18b6da..96aaff0f121d 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -45,16 +45,17 @@ use polkadot_runtime_common::xcm_sender::ExponentialPrice; use sp_runtime::traits::{AccountIdConversion, ConvertInto}; use xcm::latest::prelude::*; use xcm_builder::{ - AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain, - AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, - DenyReserveTransferToRelayChain, DenyThenTry, DescribeFamily, DescribePalletTerminal, - EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, - GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, LocalMint, - NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, StartsWith, - StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, - WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, + unique_instances::UniqueInstancesAdapter, AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, + AllowHrmpNotificationsFromRelayChain, AllowKnownQueryResponses, AllowSubscriptionsFrom, + AllowTopLevelPaidExecutionFrom, DenyReserveTransferToRelayChain, DenyThenTry, DescribeFamily, + DescribePalletTerminal, EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, + FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, + LocalMint, MatchInClassInstances, NetworkExportTableItem, NoChecking, ParentAsSuperuser, + ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, + SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, + SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, + TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, + XcmFeeManagerFromComponents, }; use xcm_executor::XcmExecutor; @@ -141,19 +142,11 @@ pub type UniquesConvertedConcreteId = assets_common::UniquesConvertedConcreteId; /// Means for transacting unique assets. -pub type UniquesTransactor = NonFungiblesAdapter< - // Use this non-fungibles implementation: - Uniques, - // This adapter will handle any non-fungible asset from the uniques pallet. - UniquesConvertedConcreteId, - // Convert an XCM Location into a local account id: - LocationToAccountId, - // Our chain's account ID type (we can't get away without mentioning it explicitly): +pub type UniquesTransactor = UniqueInstancesAdapter< AccountId, - // Does not check teleports. - NoChecking, - // The account to use for tracking teleports. - CheckingAccount, + LocationToAccountId, + MatchInClassInstances, + pallet_uniques::asset_ops::Item, >; /// `AssetId`/`Balance` converter for `ForeignAssets`. diff --git a/polkadot/xcm/pallet-derivatives/Cargo.toml b/polkadot/xcm/pallet-derivatives/Cargo.toml new file mode 100644 index 000000000000..269498f4018c --- /dev/null +++ b/polkadot/xcm/pallet-derivatives/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "pallet-derivatives" +version = "1.0.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage = "https://substrate.io" +repository.workspace = true +description = "XCM derivatives pallet" +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false } +log = { workspace = true } +scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +xcm = { workspace = true } +xcm-executor = { workspace = true } +xcm-builder = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/polkadot/xcm/pallet-derivatives/README.md b/polkadot/xcm/pallet-derivatives/README.md new file mode 100644 index 000000000000..627482d2091a --- /dev/null +++ b/polkadot/xcm/pallet-derivatives/README.md @@ -0,0 +1 @@ +# derivatives diff --git a/polkadot/xcm/pallet-derivatives/src/benchmarking.rs b/polkadot/xcm/pallet-derivatives/src/benchmarking.rs new file mode 100644 index 000000000000..b272d44ce7b5 --- /dev/null +++ b/polkadot/xcm/pallet-derivatives/src/benchmarking.rs @@ -0,0 +1,48 @@ +use super::{Pallet as Derivatives, *}; +use frame_benchmarking::v2::*; + +pub struct Pallet, I: 'static = ()>(Derivatives); + +pub trait Config: super::Config { + fn original() -> OriginalOf; + + fn derivative_create_params() -> DerivativeCreateParamsOf; +} + +#[instance_benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn create_derivative() -> Result<(), BenchmarkError> { + let create_origin = >::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + let original = T::original(); + let params = T::derivative_create_params(); + + #[extrinsic_call] + _(create_origin as T::RuntimeOrigin, original, params); + + Ok(()) + } + + #[benchmark] + fn destroy_derivative() -> Result<(), BenchmarkError> { + let create_origin = >::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + let destroy_origin = >::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + let original = T::original(); + let params = T::derivative_create_params(); + + >::create_derivative(create_origin, original.clone(), params)?; + + #[extrinsic_call] + _(destroy_origin as T::RuntimeOrigin, original); + + Ok(()) + } +} diff --git a/polkadot/xcm/pallet-derivatives/src/lib.rs b/polkadot/xcm/pallet-derivatives/src/lib.rs new file mode 100644 index 000000000000..7893c6630a82 --- /dev/null +++ b/polkadot/xcm/pallet-derivatives/src/lib.rs @@ -0,0 +1,331 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![recursion_limit = "256"] +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + pallet_prelude::*, + traits::tokens::asset_ops::{ + common_strategies::JustDo, AssetDefinition, Create, CreateStrategy, Destroy, + DestroyStrategy, + }, +}; +use frame_system::{pallet_prelude::*, EnsureNever}; +use scale_info::TypeInfo; +use sp_runtime::DispatchResult; +use xcm_builder::unique_instances::{ + derivatives::{DerivativesRegistry, IterDerivativesRegistry}, + DerivativesExtra, +}; + +pub use pallet::*; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + +/// The log target of this pallet. +pub const LOG_TARGET: &'static str = "runtime::xcm::derivatives"; + +type OriginalOf = >::Original; +type DerivativeOf = >::Derivative; +type DerivativeExtraOf = >::DerivativeExtra; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + /// The in-code storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::config] + /// The module configuration trait. + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + type Original: Member + Parameter + MaxEncodedLen; + type Derivative: Member + Parameter + MaxEncodedLen; + + type DerivativeExtra: Member + Parameter + MaxEncodedLen; + + type ExtrinsicsConfig: ExtrinsicsConfig; + } + + #[pallet::storage] + #[pallet::getter(fn original_to_derivative)] + pub type OriginalToDerivative, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, OriginalOf, DerivativeOf, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn derivative_to_original)] + pub type DerivativeToOriginal, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, DerivativeOf, OriginalOf, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn derivative_extra)] + pub type DerivativeExtra, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, DerivativeOf, DerivativeExtraOf, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A derivative is registered. + DerivativeRegistered { original: OriginalOf, derivative: DerivativeOf }, + + /// A derivative is de-registered. + DerivativeDeregistered { original: OriginalOf, derivative: DerivativeOf }, + } + + #[pallet::error] + pub enum Error { + /// A derivative already exists. + DerivativeAlreadyExists, + + /// Failed to deregister a non-registered derivative. + NoDerivativeToDeregister, + + /// Failed to find a derivative. + DerivativeNotFound, + + /// Failed to get the derivative's extra data. + DerivativeExtraDataNotFound, + + /// Failed to get an original. + OriginalNotFound, + } + + #[pallet::call(weight(WeightInfoOf))] + impl, I: 'static> Pallet { + #[pallet::call_index(0)] + pub fn create_derivative( + origin: OriginFor, + original: OriginalOf, + derivative_create_params: DerivativeCreateParamsOf, + ) -> DispatchResult { + >::ensure_origin(origin)?; + + let derivative = >::create(derivative_create_params)?; + + Self::try_register_derivative(&original, &derivative) + } + + #[pallet::call_index(1)] + pub fn destroy_derivative( + origin: OriginFor, + original: OriginalOf, + ) -> DispatchResult { + >::ensure_origin(origin)?; + + let derivative = >::get(&original) + .ok_or(Error::::NoDerivativeToDeregister)?; + + >::destroy(&derivative, JustDo::default())?; + + Self::try_deregister_derivative_of(&original) + } + } +} + +impl, I: 'static> DerivativesRegistry, DerivativeOf> + for Pallet +{ + fn try_register_derivative( + original: &OriginalOf, + derivative: &DerivativeOf, + ) -> DispatchResult { + ensure!( + Self::original_to_derivative(original).is_none(), + Error::::DerivativeAlreadyExists, + ); + + >::insert(original, derivative); + >::insert(derivative, original); + + Self::deposit_event(Event::::DerivativeRegistered { + original: original.clone(), + derivative: derivative.clone(), + }); + + Ok(()) + } + + fn try_deregister_derivative_of(original: &OriginalOf) -> DispatchResult { + let derivative = >::take(&original) + .ok_or(Error::::NoDerivativeToDeregister)?; + + >::remove(&derivative); + >::remove(&derivative); + + Self::deposit_event(Event::::DerivativeDeregistered { + original: original.clone(), + derivative: derivative.clone(), + }); + + Ok(()) + } + + fn get_derivative(original: &OriginalOf) -> Option> { + >::get(original) + } + + fn get_original(derivative: &DerivativeOf) -> Option> { + >::get(derivative) + } +} + +impl, I: 'static> IterDerivativesRegistry, DerivativeOf> + for Pallet +{ + fn iter_originals() -> impl Iterator> { + >::iter_keys() + } + + fn iter_derivatives() -> impl Iterator> { + >::iter_values() + } + + fn iter() -> impl Iterator, DerivativeOf)> { + >::iter() + } +} + +impl, I: 'static> DerivativesExtra, DerivativeExtraOf> + for Pallet +{ + fn get_derivative_extra(derivative: &DerivativeOf) -> Option> { + >::get(derivative) + } + + fn set_derivative_extra( + derivative: &DerivativeOf, + extra: Option>, + ) -> DispatchResult { + ensure!( + >::contains_key(derivative), + Error::::DerivativeNotFound, + ); + + >::set(derivative, extra); + + Ok(()) + } +} + +pub trait WeightInfo { + fn create_derivative() -> Weight; + fn destroy_derivative() -> Weight; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn create_derivative() -> Weight { + Weight::from_parts(100_000_000, 0) + } + + fn destroy_derivative() -> Weight { + Weight::from_parts(100_000_000, 0) + } +} + +pub struct ProhibitiveWeightInfo; +impl WeightInfo for ProhibitiveWeightInfo { + fn create_derivative() -> Weight { + Weight::MAX + } + + fn destroy_derivative() -> Weight { + Weight::MAX + } +} + +pub trait ExtrinsicsConfig { + type CreateOrigin: EnsureOrigin; + type DestroyOrigin: EnsureOrigin; + + type DerivativeCreateParams: Parameter + CreateStrategy; + type DerivativeCreateOp: Create; + type DerivativeDestroyOp: AssetDefinition + Destroy; + + type WeightInfo: WeightInfo; +} + +impl ExtrinsicsConfig for () { + type CreateOrigin = EnsureNever<()>; + type DestroyOrigin = EnsureNever<()>; + + type DerivativeCreateParams = DerivativeEmptyParams; + type DerivativeCreateOp = DerivativeAlwaysErrOps; + type DerivativeDestroyOp = DerivativeAlwaysErrOps; + + type WeightInfo = ProhibitiveWeightInfo; +} + +#[derive(Encode, Decode, TypeInfo, RuntimeDebug, Clone, PartialEq, Eq)] +pub struct DerivativeEmptyParams(PhantomData); +impl CreateStrategy for DerivativeEmptyParams { + type Success = Derivative; +} + +pub struct DerivativeAlwaysErrOps(PhantomData); +impl AssetDefinition for DerivativeAlwaysErrOps { + type Id = Derivative; +} +impl Create for DerivativeAlwaysErrOps { + fn create(_strategy: S) -> Result { + Err(DispatchError::BadOrigin) + } +} +impl Destroy for DerivativeAlwaysErrOps { + fn destroy(_id: &Self::Id, _strategy: S) -> Result { + Err(DispatchError::BadOrigin) + } +} + +pub type ExtrinsicsConfigOf = >::ExtrinsicsConfig; +type RuntimeOriginOf = ::RuntimeOrigin; + +pub type CreateOriginOf = as ExtrinsicsConfig< + RuntimeOriginOf, + DerivativeOf, +>>::CreateOrigin; +pub type DestroyOriginOf = as ExtrinsicsConfig< + RuntimeOriginOf, + DerivativeOf, +>>::DestroyOrigin; +pub type DerivativeCreateParamsOf = as ExtrinsicsConfig< + RuntimeOriginOf, + DerivativeOf, +>>::DerivativeCreateParams; +pub type DerivativeCreateOpOf = as ExtrinsicsConfig< + RuntimeOriginOf, + DerivativeOf, +>>::DerivativeCreateOp; +pub type DerivativeDestroyOpOf = as ExtrinsicsConfig< + RuntimeOriginOf, + DerivativeOf, +>>::DerivativeDestroyOp; +pub type WeightInfoOf = as ExtrinsicsConfig< + RuntimeOriginOf, + DerivativeOf, +>>::WeightInfo; diff --git a/polkadot/xcm/xcm-builder/src/asset_conversion.rs b/polkadot/xcm/xcm-builder/src/asset_conversion.rs index 16ae05c20795..2082a96baa1a 100644 --- a/polkadot/xcm/xcm-builder/src/asset_conversion.rs +++ b/polkadot/xcm/xcm-builder/src/asset_conversion.rs @@ -20,7 +20,9 @@ use core::{marker::PhantomData, result}; use frame_support::traits::{Contains, Get}; use sp_runtime::traits::MaybeEquivalence; use xcm::latest::prelude::*; -use xcm_executor::traits::{Error as MatchError, MatchesFungibles, MatchesNonFungibles}; +use xcm_executor::traits::{ + Error as MatchError, MatchesFungibles, MatchesInstance, MatchesNonFungible, MatchesNonFungibles, +}; /// Converter struct implementing `AssetIdConversion` converting a numeric asset ID (must be /// `TryFrom/TryInto`) into a `GeneralIndex` junction, prefixed by some `Location` value. @@ -152,6 +154,26 @@ impl< } } +pub struct MatchInClassInstances(PhantomData); + +impl> + MatchesInstance<(ClassId, InstanceId)> for MatchInClassInstances +{ + fn matches_instance(a: &Asset) -> result::Result<(ClassId, InstanceId), MatchError> { + Matcher::matches_nonfungibles(a) + } +} + +pub struct MatchClasslessInstances(PhantomData); + +impl> MatchesInstance + for MatchClasslessInstances +{ + fn matches_instance(a: &Asset) -> result::Result { + Matcher::matches_nonfungible(a).ok_or(MatchError::AssetNotHandled) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/polkadot/xcm/xcm-builder/src/lib.rs b/polkadot/xcm/xcm-builder/src/lib.rs index bec3bdcb05a0..7d5d8d4102fc 100644 --- a/polkadot/xcm/xcm-builder/src/lib.rs +++ b/polkadot/xcm/xcm-builder/src/lib.rs @@ -32,7 +32,8 @@ mod asset_conversion; #[allow(deprecated)] pub use asset_conversion::ConvertedConcreteAssetId; pub use asset_conversion::{ - AsPrefixedGeneralIndex, ConvertedConcreteId, MatchedConvertedConcreteId, + AsPrefixedGeneralIndex, ConvertedConcreteId, MatchClasslessInstances, MatchInClassInstances, + MatchedConvertedConcreteId, }; mod asset_exchange; @@ -97,6 +98,8 @@ pub use matches_token::IsConcrete; mod matcher; pub use matcher::{CreateMatcher, MatchXcm, Matcher}; +pub mod unique_instances; + mod nonfungibles_adapter; pub use nonfungibles_adapter::{ NonFungiblesAdapter, NonFungiblesMutateAdapter, NonFungiblesTransferAdapter, diff --git a/polkadot/xcm/xcm-builder/src/unique_instances/adapter.rs b/polkadot/xcm/xcm-builder/src/unique_instances/adapter.rs new file mode 100644 index 000000000000..1033d9039723 --- /dev/null +++ b/polkadot/xcm/xcm-builder/src/unique_instances/adapter.rs @@ -0,0 +1,149 @@ +use core::marker::PhantomData; +use frame_support::traits::tokens::asset_ops::{ + common_strategies::{DeriveAndReportId, FromTo, IfOwnedBy, Owned, PredefinedId}, + AssetDefinition, Create, Destroy, Transfer, +}; +use xcm::latest::prelude::*; +use xcm_executor::traits::{ConvertLocation, Error as MatchError, MatchesInstance, TransactAsset}; + +use super::NonFungibleAsset; + +const LOG_TARGET: &str = "xcm::unique_instances"; + +/// The `UniqueInstancesAdapter` implements the [`TransactAsset`] for unique instances (NFT-like +/// entities), for which the `Matcher` can deduce the instance ID from the XCM [`AssetId`]. +/// +/// The adapter uses the following asset operations: +/// * [`Create`] with the [`Owned`] strategy, which uses the [`PredefinedId`] approach +/// to assign the instance ID deduced by the `Matcher`. +/// * [`Transfer`] with [`FromTo`] strategy +/// * [`Destroy`] with [`IfOwnedBy`] strategy +/// +/// This adapter assumes that the asset can be safely destroyed +/// without destroying any important data. +/// However, the "destroy" operation can be replaced by another operation. +/// For instance, one can use the [`StashOnDestroy`](super::ops::StashOnDestroy) type to stash the +/// instance instead of destroying it. See other similar types in the [`ops`](super::ops) module. +/// +/// Note on teleports: This adapter doesn't implement teleports since unique instances have +/// associated data that also should be teleported. Currently, neither XCM can transfer such data +/// nor does a standard approach exist in the ecosystem for this use case. +pub struct UniqueInstancesAdapter( + PhantomData<(AccountId, AccountIdConverter, Matcher, InstanceOps)>, +); + +impl TransactAsset + for UniqueInstancesAdapter +where + AccountIdConverter: ConvertLocation, + Matcher: MatchesInstance, + InstanceOps: AssetDefinition + + Create>> + + Transfer> + + Destroy>, +{ + fn deposit_asset(what: &Asset, who: &Location, context: Option<&XcmContext>) -> XcmResult { + log::trace!( + target: LOG_TARGET, + "UniqueInstancesAdapter::deposit_asset what: {:?}, who: {:?}, context: {:?}", + what, + who, + context, + ); + + let instance_id = Matcher::matches_instance(what)?; + let who = AccountIdConverter::convert_location(who) + .ok_or(MatchError::AccountIdConversionFailed)?; + + InstanceOps::create(Owned::new(who, PredefinedId::from(instance_id))) + .map(|_reported_id| ()) + .map_err(|e| XcmError::FailedToTransactAsset(e.into())) + } + + fn withdraw_asset( + what: &Asset, + who: &Location, + maybe_context: Option<&XcmContext>, + ) -> Result { + log::trace!( + target: LOG_TARGET, + "UniqueInstancesAdapter::withdraw_asset what: {:?}, who: {:?}, context: {:?}", + what, + who, + maybe_context, + ); + let instance_id = Matcher::matches_instance(what)?; + let who = AccountIdConverter::convert_location(who) + .ok_or(MatchError::AccountIdConversionFailed)?; + + InstanceOps::destroy(&instance_id, IfOwnedBy(who)) + .map_err(|e| XcmError::FailedToTransactAsset(e.into()))?; + + Ok(what.clone().into()) + } + + fn internal_transfer_asset( + what: &Asset, + from: &Location, + to: &Location, + context: &XcmContext, + ) -> Result { + log::trace!( + target: LOG_TARGET, + "UniqueInstancesAdapter::internal_transfer_asset what: {:?}, from: {:?}, to: {:?}, context: {:?}", + what, + from, + to, + context, + ); + + let instance_id = Matcher::matches_instance(what)?; + let from = AccountIdConverter::convert_location(from) + .ok_or(MatchError::AccountIdConversionFailed)?; + let to = AccountIdConverter::convert_location(to) + .ok_or(MatchError::AccountIdConversionFailed)?; + + InstanceOps::transfer(&instance_id, FromTo(from, to)) + .map_err(|e| XcmError::FailedToTransactAsset(e.into()))?; + + Ok(what.clone().into()) + } +} + +/// The `UniqueInstancesDepositAdapter` implements the [`TransactAsset`] to create unique instances +/// (NFT-like entities), for which no `Matcher` deduce the instance ID from the XCM +/// [`AssetId`]. Instead, this adapter requires the `InstanceCreateOp` to create an instance using +/// [`NonFungibleAsset`] as derive id parameters. +pub struct UniqueInstancesDepositAdapter( + PhantomData<(AccountId, AccountIdConverter, InstanceCreateOp)>, +); + +impl TransactAsset + for UniqueInstancesDepositAdapter +where + AccountIdConverter: ConvertLocation, + InstanceCreateOp: AssetDefinition + + Create>>, +{ + fn deposit_asset(what: &Asset, who: &Location, context: Option<&XcmContext>) -> XcmResult { + log::trace!( + target: LOG_TARGET, + "UniqueInstancesDepositAdapter::deposit_asset what: {:?}, who: {:?}, context: {:?}", + what, + who, + context, + ); + + let asset = match what.fun { + Fungibility::NonFungible(asset_instance) => (what.id.clone(), asset_instance), + _ => return Err(MatchError::AssetNotHandled.into()), + }; + + let who = AccountIdConverter::convert_location(who) + .ok_or(MatchError::AccountIdConversionFailed)?; + + InstanceCreateOp::create(Owned::new(who, DeriveAndReportId::from(asset))) + .map(|_reported_id| ()) + .map_err(|e| XcmError::FailedToTransactAsset(e.into())) + } +} diff --git a/polkadot/xcm/xcm-builder/src/unique_instances/derivatives.rs b/polkadot/xcm/xcm-builder/src/unique_instances/derivatives.rs new file mode 100644 index 000000000000..1ef03eb75714 --- /dev/null +++ b/polkadot/xcm/xcm-builder/src/unique_instances/derivatives.rs @@ -0,0 +1,94 @@ +//! Utilities for working with unique instances derivatives. + +use core::marker::PhantomData; +use frame_support::ensure; +use sp_runtime::DispatchResult; +use xcm::latest::prelude::*; +use xcm_executor::traits::{Error, MatchesInstance}; + +use super::NonFungibleAsset; + +/// A registry abstracts the mapping between an `Original` entity and a `Derivative` entity. +/// +/// The primary use cases of the registry are: +/// * a map between an `AssetId` and an chain-local asset ID. +/// For instance, it could be chain-local currency ID or an NFT collection ID. +/// * a map between a [`NonFungibleAsset`] and a derivative instance ID +/// to create a new derivative instance +pub trait DerivativesRegistry { + fn try_register_derivative(original: &Original, derivative: &Derivative) -> DispatchResult; + + fn try_deregister_derivative_of(original: &Original) -> DispatchResult; + + fn get_derivative(original: &Original) -> Option; + + fn get_original(derivative: &Derivative) -> Option; +} + +/// Iterator utilities for a derivatives registry. +pub trait IterDerivativesRegistry { + fn iter_originals() -> impl Iterator; + + fn iter_derivatives() -> impl Iterator; + + fn iter() -> impl Iterator; +} + +/// Derivatives extra data. +pub trait DerivativesExtra { + fn get_derivative_extra(derivative: &Derivative) -> Option; + + fn set_derivative_extra(derivative: &Derivative, extra: Option) -> DispatchResult; +} + +/// The `MatchDerivativeInstances` is an XCM Matcher +/// that uses a [`DerivativesRegistry`] to match the XCM identification of the original instance +/// to a derivative instance. +pub struct MatchDerivativeInstances(PhantomData); +impl, DerivativeId> + MatchesInstance for MatchDerivativeInstances +{ + fn matches_instance(asset: &Asset) -> Result { + match asset.fun { + Fungibility::NonFungible(asset_instance) => + Registry::get_derivative(&(asset.id.clone(), asset_instance)) + .ok_or(Error::AssetNotHandled), + Fungibility::Fungible(_) => Err(Error::AssetNotHandled), + } + } +} + +/// The `EnsureNotDerivativeInstance` is an XCM Matcher that +/// ensures that the instance returned by the inner `Matcher` isn't a derivative. +/// +/// The check is performed using the [`DerivativesRegistry`]. +/// +/// This Matcher is needed if derivative instances are created within the same NFT engine +/// as this chain's original instances, +/// i.e. if addressing a derivative instance using the local XCM identification is possible. +/// +/// For example, suppose this chain's original instances (for which this chain is the reserve +/// location) can be addressed like this `id: PalletInstance(111)/GeneralIndex(), fun: +/// NonFungible(Index())`. So, this chain is the reserve location for all +/// instances matching the above identification. +/// +/// However, if some of the instances within Pallet #111 could be derivatives as well, +/// we need to ensure that this chain won't act as the reserve location for these instances. +/// If we allow this, this chain could send a derivative as if it were the original NFT on this +/// chain. The other chain can't know that this instance isn't the original. +/// We must prevent that so this chain will act as an honest reserve location. +pub struct EnsureNotDerivativeInstance(PhantomData<(Registry, Matcher)>); +impl< + Registry: DerivativesRegistry, + Matcher: MatchesInstance, + DerivativeId, + > MatchesInstance for EnsureNotDerivativeInstance +{ + fn matches_instance(asset: &Asset) -> Result { + let instance_id = Matcher::matches_instance(asset)?; + + ensure!(Registry::get_original(&instance_id).is_none(), Error::AssetNotHandled); + + Ok(instance_id) + } +} diff --git a/polkadot/xcm/xcm-builder/src/unique_instances/mod.rs b/polkadot/xcm/xcm-builder/src/unique_instances/mod.rs new file mode 100644 index 000000000000..39e8c66bfb0f --- /dev/null +++ b/polkadot/xcm/xcm-builder/src/unique_instances/mod.rs @@ -0,0 +1,15 @@ +//! XCM utilities to work with NFT-like entities (unique instances). +//! The adapters and other utility types use the +//! [`asset_ops`](frame_support::traits::tokens::asset_ops) traits. + +use xcm::latest::prelude::*; + +pub mod adapter; +pub mod derivatives; +pub mod ops; + +pub use adapter::*; +pub use derivatives::*; +pub use ops::*; + +pub type NonFungibleAsset = (AssetId, AssetInstance); diff --git a/polkadot/xcm/xcm-builder/src/unique_instances/ops.rs b/polkadot/xcm/xcm-builder/src/unique_instances/ops.rs new file mode 100644 index 000000000000..f47b0899a609 --- /dev/null +++ b/polkadot/xcm/xcm-builder/src/unique_instances/ops.rs @@ -0,0 +1,153 @@ +//! Utilities for redefining and auto-implementing the unique instances operations. + +use core::marker::PhantomData; +use frame_support::traits::{ + tokens::asset_ops::{ + common_strategies::{FromTo, IfOwnedBy, IfRestorable, Owned, PredefinedId}, + AssetDefinition, Create, CreateStrategy, Destroy, DestroyStrategy, Restore, Stash, + Transfer, TransferStrategy, + }, + TypedGet, +}; +use sp_runtime::{DispatchError, DispatchResult}; + +/// The `UniqueInstancesOps` allows the creation of a new "NFT engine" capable of creating, +/// transferring, and destroying the unique instances by merging three distinct implementations. +/// +/// The resulting "NFT engine" can be used in the +/// [`UniqueInstancesAdapter`](super::UniqueInstancesAdapter). +pub struct UniqueInstancesOps( + PhantomData<(CreateOp, TransferOp, DestroyOp)>, +); +impl AssetDefinition + for UniqueInstancesOps +where + TransferOp: AssetDefinition, + DestroyOp: AssetDefinition, +{ + type Id = TransferOp::Id; +} +impl Create + for UniqueInstancesOps +where + Strategy: CreateStrategy, + CreateOp: Create, +{ + fn create(strategy: Strategy) -> Result { + CreateOp::create(strategy) + } +} +impl Transfer + for UniqueInstancesOps +where + Strategy: TransferStrategy, + TransferOp: Transfer, + DestroyOp: AssetDefinition, +{ + fn transfer(id: &Self::Id, strategy: Strategy) -> Result { + TransferOp::transfer(id, strategy) + } +} +impl Destroy + for UniqueInstancesOps +where + Strategy: DestroyStrategy, + TransferOp: AssetDefinition, + DestroyOp: AssetDefinition + Destroy, +{ + fn destroy(id: &Self::Id, strategy: Strategy) -> Result { + DestroyOp::destroy(id, strategy) + } +} + +/// The `SimpleStash` implements both the [`Stash`] and [`Restore`] operations +/// by utilizing the [`Transfer`] operation. +/// Stashing with the [`IfOwnedBy`] strategy is implemented as the transfer to the stash account +/// using the [`FromTo`] strategy. Restoring with the [`IfRestorable`] is implemented symmetrically +/// as the transfer from the stash account using the [`FromTo`] strategy. +pub struct SimpleStash(PhantomData<(StashAccount, InstanceOps)>); +impl AssetDefinition for SimpleStash +where + InstanceOps: AssetDefinition, +{ + type Id = InstanceOps::Id; +} +impl Stash> + for SimpleStash +where + StashAccount: TypedGet, + InstanceOps: Transfer>, +{ + fn stash( + id: &Self::Id, + IfOwnedBy(possible_owner): IfOwnedBy, + ) -> DispatchResult { + InstanceOps::transfer(id, FromTo(possible_owner, StashAccount::get())) + } +} +impl Restore> + for SimpleStash +where + StashAccount: TypedGet, + InstanceOps: Transfer>, +{ + fn restore( + id: &Self::Id, + IfRestorable(owner): IfRestorable, + ) -> DispatchResult { + InstanceOps::transfer(id, FromTo(StashAccount::get(), owner)) + } +} + +/// The `RestoreOnCreate` implements the [`Create`] operation by utilizing the [`Restore`] +/// operation. The creation is implemented using the [`Owned`] strategy with the [`PredefinedId`] ID +/// assignment. Such creation is modeled by the [`Restore`] operation using the [`IfRestorable`] +/// strategy. +/// +/// The implemented [`Create`] operation can be used in the +/// [`UniqueInstancesAdapter`](super::UniqueInstancesAdapter) via the [`UniqueInstancesOps`]. +pub struct RestoreOnCreate(PhantomData); +impl AssetDefinition for RestoreOnCreate +where + InstanceOps: AssetDefinition, +{ + type Id = InstanceOps::Id; +} +impl Create>> + for RestoreOnCreate +where + InstanceOps: Restore>, +{ + fn create( + strategy: Owned>, + ) -> Result { + let Owned { owner, id_assignment, .. } = strategy; + let instance_id = id_assignment.params; + + InstanceOps::restore(&instance_id, IfRestorable(owner))?; + + Ok(instance_id) + } +} + +/// The `StashOnDestroy` implements the [`Destroy`] operation by utilizing the [`Stash`] operation. +/// The destroy operation is implemented using the [`IfOwnedBy`] strategy +/// and modeled by the [`Stash`] operation using the same strategy. +/// +/// The implemented [`Destroy`] operation can be used in the +/// [`UniqueInstancesAdapter`](super::UniqueInstancesAdapter) via the [`UniqueInstancesOps`]. +pub struct StashOnDestroy(PhantomData); +impl AssetDefinition for StashOnDestroy +where + InstanceOps: AssetDefinition, +{ + type Id = InstanceOps::Id; +} +impl Destroy> for StashOnDestroy +where + InstanceOps: Stash>, +{ + fn destroy(id: &Self::Id, strategy: IfOwnedBy) -> DispatchResult { + InstanceOps::stash(id, strategy) + } +} diff --git a/polkadot/xcm/xcm-executor/src/traits/mod.rs b/polkadot/xcm/xcm-executor/src/traits/mod.rs index feb2922bcdff..5022d906b15b 100644 --- a/polkadot/xcm/xcm-executor/src/traits/mod.rs +++ b/polkadot/xcm/xcm-executor/src/traits/mod.rs @@ -35,7 +35,8 @@ mod filter_asset_location; pub use filter_asset_location::FilterAssetLocation; mod token_matching; pub use token_matching::{ - Error, MatchesFungible, MatchesFungibles, MatchesNonFungible, MatchesNonFungibles, + Error, MatchesFungible, MatchesFungibles, MatchesInstance, MatchesNonFungible, + MatchesNonFungibles, }; mod on_response; pub use on_response::{OnResponse, QueryHandler, QueryResponseStatus, VersionChangeNotifier}; @@ -60,9 +61,9 @@ pub mod prelude { pub use super::{ export_xcm, validate_export, AssetExchange, AssetLock, ClaimAssets, ConvertOrigin, DropAssets, Enact, Error, ExportXcm, FeeManager, FeeReason, LockError, MatchesFungible, - MatchesFungibles, MatchesNonFungible, MatchesNonFungibles, OnResponse, ProcessTransaction, - ShouldExecute, TransactAsset, VersionChangeNotifier, WeightBounds, WeightTrader, - WithOriginFilter, + MatchesFungibles, MatchesInstance, MatchesNonFungible, MatchesNonFungibles, OnResponse, + ProcessTransaction, ShouldExecute, TransactAsset, VersionChangeNotifier, WeightBounds, + WeightTrader, WithOriginFilter, }; #[allow(deprecated)] pub use super::{Identity, JustTry}; diff --git a/polkadot/xcm/xcm-executor/src/traits/token_matching.rs b/polkadot/xcm/xcm-executor/src/traits/token_matching.rs index aa44aee4f9de..721f1ebff3c1 100644 --- a/polkadot/xcm/xcm-executor/src/traits/token_matching.rs +++ b/polkadot/xcm/xcm-executor/src/traits/token_matching.rs @@ -101,7 +101,22 @@ impl MatchesNonFungibles for Tuple { for_tuples!( #( match Tuple::matches_nonfungibles(a) { o @ Ok(_) => return o, _ => () } )* ); - tracing::trace!(target: "xcm::matches_non_fungibles", asset = ?a, "did not match fungibles asset"); + tracing::trace!(target: "xcm::matches_non_fungibles", asset = ?a, "did not match non-fungibles asset"); + Err(Error::AssetNotHandled) + } +} + +pub trait MatchesInstance { + fn matches_instance(a: &Asset) -> result::Result; +} + +#[impl_trait_for_tuples::impl_for_tuples(30)] +impl MatchesInstance for Tuple { + fn matches_instance(a: &Asset) -> result::Result { + for_tuples!( #( + match Tuple::matches_instance(a) { o @ Ok(_) => return o, _ => () } + )* ); + tracing::trace!(target: "xcm::matches_instance", asset = ?a, "did not match an asset instance"); Err(Error::AssetNotHandled) } } diff --git a/substrate/frame/support/src/traits/tokens.rs b/substrate/frame/support/src/traits/tokens.rs index 138703cf1d13..68a3d55d6ccc 100644 --- a/substrate/frame/support/src/traits/tokens.rs +++ b/substrate/frame/support/src/traits/tokens.rs @@ -17,6 +17,7 @@ //! Traits for working with tokens and their associated datastructures. +pub mod asset_ops; pub mod currency; pub mod fungible; pub mod fungibles; diff --git a/substrate/frame/support/src/traits/tokens/asset_ops.rs b/substrate/frame/support/src/traits/tokens/asset_ops.rs new file mode 100644 index 000000000000..ad9593521176 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/asset_ops.rs @@ -0,0 +1,621 @@ +//! Abstract asset operations traits. +//! +//! The following operations are defined: +//! * [`InspectMetadata`] +//! * [`UpdateMetadata`] +//! * [`Create`] +//! * [`Transfer`] +//! * [`Destroy`] +//! * [`Stash`] +//! * [`Restore`] +//! +//! Also, all the operations above (except the `Create` operation) use +//! the [`AssetDefinition`] to retrieve the `Id` type of the asset. +//! +//! An asset operation can be implemented multiple times +//! using different strategies associated with this operation. +//! +//! A strategy defines the operation behavior, +//! may supply additional parameters, +//! and may define a return value type of the operation. + +use core::marker::PhantomData; +use sp_runtime::DispatchError; +use sp_std::vec::Vec; + +/// Trait for defining an asset. +/// The definition must provide the `Id` type to identify the asset. +pub trait AssetDefinition { + /// Type for identifying the asset. + type Id; +} + +/// Get the `Id` type of the asset definition. +pub type AssetIdOf = ::Id; + +/// A strategy for use in the [`InspectMetadata`] implementations. +/// +/// The common inspect strategies are: +/// * [`Bytes`](common_strategies::Bytes) +/// * [`Ownership`](common_strategies::Ownership) +/// * [`CanCreate`](common_strategies::CanCreate) +/// * [`CanTransfer`](common_strategies::CanTransfer) +/// * [`CanDestroy`](common_strategies::CanDestroy) +/// * [`CanUpdateMetadata`](common_strategies::CanUpdateMetadata) +pub trait MetadataInspectStrategy { + /// The type to return from the [`InspectMetadata::inspect_metadata`] function. + type Value; +} + +/// A trait representing the ability of a certain asset to **provide** its metadata +/// information. +/// +/// This trait can be implemented multiple times using different +/// [`inspect strategies`](MetadataInspectStrategy). +/// +/// An inspect strategy defines how the asset metadata is identified/retrieved +/// and what [`Value`](MetadataInspectStrategy::Value) type is returned. +pub trait InspectMetadata: AssetDefinition { + /// Inspect metadata information of the asset + /// using the given `id` and the inspect `strategy`. + /// + /// The ID type is retrieved from the [`AssetDefinition`]. + fn inspect_metadata( + id: &Self::Id, + strategy: Strategy, + ) -> Result; +} + +/// A strategy for use in the [`UpdateMetadata`] implementations. +/// +/// The common update strategies are: +/// * [`Bytes`](common_strategies::Bytes) +/// * [`CanCreate`](common_strategies::CanCreate) +/// * [`CanTransfer`](common_strategies::CanTransfer) +/// * [`CanDestroy`](common_strategies::CanDestroy) +/// * [`CanUpdateMetadata`](common_strategies::CanUpdateMetadata) +pub trait MetadataUpdateStrategy { + /// The type of metadata update to accept in the [`UpdateMetadata::update_metadata`] function. + type Update<'u>; + + /// This type represents a successful asset metadata update. + /// It will be in the [`Result`] type of the [`UpdateMetadata::update_metadata`] function. + type Success; +} + +/// A trait representing the ability of a certain asset to **update** its metadata information. +/// +/// This trait can be implemented multiple times using different +/// [`update strategies`](MetadataUpdateStrategy). +/// +/// An update strategy defines how the asset metadata is identified +/// and what [`Update`](MetadataUpdateStrategy::Update) type is used. +pub trait UpdateMetadata: AssetDefinition { + /// Update metadata information of the asset + /// using the given `id`, the update `strategy`, and the `update` value. + /// + /// The ID type is retrieved from the [`AssetDefinition`]. + fn update_metadata( + id: &Self::Id, + strategy: Strategy, + update: Strategy::Update<'_>, + ) -> Result; +} + +/// A strategy for use in the [`Create`] implementations. +/// +/// The common "create" strategies are: +/// * [`Owned`](common_strategies::Owned) +/// * [`Adminable`](common_strategies::Adminable) +pub trait CreateStrategy { + /// This type represents a successful asset creation. + /// It will be in the [`Result`] type of the [`Create::create`] function. + type Success; +} + +/// An ID assignment approach to use in the "create" strategies. +/// +/// The common ID assignments are: +/// * [`AutoId`](common_strategies::AutoId) +/// * [`PredefinedId`](common_strategies::PredefinedId) +/// * [`DeriveAndReportId`](common_strategies::DeriveAndReportId) +pub trait IdAssignment { + /// The reported ID type. + /// + /// Examples: + /// * [`AutoId`](common_strategies::AutoId) returns the ID of the newly created asset + /// * [`PredefinedId`](common_strategies::PredefinedId) accepts the ID to be assigned to the + /// newly created asset + /// * [`DeriveAndReportId`](common_strategies::DeriveAndReportId) returns the ID derived from + /// the input parameters + type ReportedId; +} + +/// A trait representing the ability of a certain asset to be created. +/// +/// This trait can be implemented multiple times using different +/// [`"create" strategies`](CreateStrategy). +/// +/// A create strategy defines all aspects of asset creation including how an asset ID is assigned. +pub trait Create { + /// Create a new asset using the provided `strategy`. + fn create(strategy: Strategy) -> Result; +} + +/// A strategy for use in the [`Transfer`] implementations. +/// +/// The common transfer strategies are: +/// * [`JustDo`](common_strategies::JustDo) +/// * [`FromTo`](common_strategies::FromTo) +pub trait TransferStrategy { + /// This type represents a successful asset transfer. + /// It will be in the [`Result`] type of the [`Transfer::transfer`] function. + type Success; +} + +/// A trait representing the ability of a certain asset to be transferred. +/// +/// This trait can be implemented multiple times using different +/// [`transfer strategies`](TransferStrategy). +/// +/// A transfer strategy defines transfer parameters. +pub trait Transfer: AssetDefinition { + /// Transfer the asset identified by the given `id` using the provided `strategy`. + /// + /// The ID type is retrieved from the [`AssetDefinition`]. + fn transfer(id: &Self::Id, strategy: Strategy) -> Result; +} + +/// A strategy for use in the [`Destroy`] implementations. +/// +/// The common destroy strategies are: +/// * [`JustDo`](common_strategies::JustDo) +/// * [`IfOwnedBy`](common_strategies::IfOwnedBy) +/// * [`WithWitness`](common_strategies::WithWitness) +/// * [`IfOwnedByWithWitness`](common_strategies::IfOwnedByWithWitness) +pub trait DestroyStrategy { + /// This type represents a successful asset destruction. + /// It will be in the [`Result`] type of the [`Destroy::destroy`] function. + type Success; +} + +/// A trait representing the ability of a certain asset to be destroyed. +/// +/// This trait can be implemented multiple times using different +/// [`destroy strategies`](DestroyStrategy). +/// +/// A destroy strategy defines destroy parameters and the result value type. +pub trait Destroy: AssetDefinition { + /// Destroy the asset identified by the given `id` using the provided `strategy`. + /// + /// The ID type is retrieved from the [`AssetDefinition`]. + fn destroy(id: &Self::Id, strategy: Strategy) -> Result; +} + +/// A strategy for use in the [`Stash`] implementations. +/// +/// The common stash strategies are: +/// * [`JustDo`](common_strategies::JustDo) +/// * [`IfOwnedBy`](common_strategies::IfOwnedBy) +pub trait StashStrategy { + /// This type represents a successful asset stashing. + /// It will be in the [`Result`] type of the [`Stash::stash`] function. + type Success; +} + +/// A trait representing the ability of a certain asset to be stashed. +/// +/// This trait can be implemented multiple times using different +/// [`stash strategies`](StashStrategy). +/// +/// A stash strategy defines stash parameters. +pub trait Stash: AssetDefinition { + /// Stash the asset identified by the given `id` using the provided `strategy`. + /// + /// The ID type is retrieved from the [`AssetDefinition`]. + fn stash(id: &Self::Id, strategy: Strategy) -> Result; +} + +/// A strategy for use in the [`Restore`] implementations. +/// The common restore strategies are: +/// * [`JustDo`](common_strategies::JustDo) +/// * [`IfRestorable`](common_strategies::IfRestorable) +pub trait RestoreStrategy { + /// This type represents a successful asset restoration. + /// It will be in the [`Result`] type of the [`Restore::restore`] function. + type Success; +} + +/// A trait representing the ability of a certain asset to be restored. +/// +/// This trait can be implemented multiple times using different +/// [`restore strategies`](RestoreStrategy). +/// +/// A restore strategy defines restore parameters. +pub trait Restore: AssetDefinition { + /// Restore the asset identified by the given `id` using the provided `strategy`. + /// + /// The ID type is retrieved from the [`AssetDefinition`]. + fn restore(id: &Self::Id, strategy: Strategy) -> Result; +} + +/// This modules contains the common asset ops strategies. +pub mod common_strategies { + use super::*; + use codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + use sp_runtime::RuntimeDebug; + + /// The `WithOrigin` is a strategy that accepts a runtime origin and the `Inner` strategy. + /// + /// It is meant to be used when the origin check should be performed + /// in addition to the `Inner` strategy. + /// + /// The `WithOrigin` implements any strategy that the `Inner` implements. + pub struct WithOrigin(pub RuntimeOrigin, pub Inner); + impl MetadataInspectStrategy + for WithOrigin + { + type Value = Inner::Value; + } + impl MetadataUpdateStrategy + for WithOrigin + { + type Update<'u> = Inner::Update<'u>; + type Success = Inner::Success; + } + impl CreateStrategy for WithOrigin { + type Success = Inner::Success; + } + impl TransferStrategy for WithOrigin { + type Success = Inner::Success; + } + impl DestroyStrategy for WithOrigin { + type Success = Inner::Success; + } + impl StashStrategy for WithOrigin { + type Success = Inner::Success; + } + impl RestoreStrategy for WithOrigin { + type Success = Inner::Success; + } + + /// The JustDo represents the simplest strategy, + /// which doesn't require additional checks to perform the operation. + /// + /// It can be used as the following strategies: + /// * [`transfer strategy`](TransferStrategy) + /// * [`destroy strategy`](DestroyStrategy) + /// * [`stash strategy`](StashStrategy) + /// * [`restore strategy`](RestoreStrategy) + /// + /// It accepts whatever parameters are set in its generic argument. + /// For instance, for an unchecked transfer, + /// this strategy may take a reference to a beneficiary account. + pub struct JustDo(pub Params); + impl Default for JustDo<()> { + fn default() -> Self { + Self(()) + } + } + impl TransferStrategy for JustDo { + type Success = (); + } + impl DestroyStrategy for JustDo { + type Success = (); + } + impl StashStrategy for JustDo { + type Success = (); + } + impl RestoreStrategy for JustDo { + type Success = (); + } + + /// The `Bytes` strategy represents raw metadata bytes. + /// It is both an [inspect](MetadataInspectStrategy) and [update](MetadataUpdateStrategy) + /// metadata strategy. + /// + /// * As the inspect strategy, it returns `Vec`. + /// * As the update strategy, it accepts `Option<&[u8]>`, where `None` means data removal. + /// + /// By default, the `Bytes` identifies a byte blob associated with the asset (the only one + /// blob). However, a user can define several flavors of this strategy by supplying the `Flavor` + /// type. The `Flavor` type can also contain additional data (like a byte key) to identify a + /// certain byte data. + pub struct Bytes(pub Flavor); + impl Default for Bytes<()> { + fn default() -> Self { + Self(()) + } + } + impl MetadataInspectStrategy for Bytes { + type Value = Vec; + } + impl MetadataUpdateStrategy for Bytes { + type Update<'u> = Option<&'u [u8]>; + type Success = (); + } + + /// The `Ownership` [inspect](MetadataInspectStrategy) metadata strategy allows getting the + /// owner of an asset. + pub struct Ownership(PhantomData); + impl Default for Ownership { + fn default() -> Self { + Self(PhantomData) + } + } + impl MetadataInspectStrategy for Ownership { + type Value = Owner; + } + + /// The `CanCreate` strategy represents the ability to create an asset. + /// It is both an [inspect](MetadataInspectStrategy) and [update](MetadataUpdateStrategy) + /// metadata strategy. + /// + /// * As the inspect strategy, it returns `bool`. + /// * As the update strategy is accepts `bool`. + /// + /// By default, this strategy means the ability to create an asset "in general". + /// However, a user can define several flavors of this strategy by supplying the `Flavor` type. + /// The `Flavor` type can add more details to the strategy. + /// For instance, "Can **a specific user** create an asset?". + pub struct CanCreate(pub Flavor); + impl Default for CanCreate<()> { + fn default() -> Self { + Self(()) + } + } + impl MetadataInspectStrategy for CanCreate { + type Value = bool; + } + impl MetadataUpdateStrategy for CanCreate { + type Update<'u> = bool; + type Success = (); + } + + /// The `CanTransfer` strategy represents the ability to transfer an asset. + /// It is both an [inspect](MetadataInspectStrategy) and [update](MetadataUpdateStrategy) + /// metadata strategy. + /// + /// * As the inspect strategy, it returns `bool`. + /// * As the update strategy is accepts `bool`. + /// + /// By default, this strategy means the ability to transfer an asset "in general". + /// However, a user can define several flavors of this strategy by supplying the `Flavor` type. + /// The `Flavor` type can add more details to the strategy. + /// For instance, "Can **a specific user** transfer an asset of **another user**?". + pub struct CanTransfer(pub Flavor); + impl Default for CanTransfer<()> { + fn default() -> Self { + Self(()) + } + } + impl MetadataInspectStrategy for CanTransfer { + type Value = bool; + } + impl MetadataUpdateStrategy for CanTransfer { + type Update<'u> = bool; + type Success = (); + } + + /// The `CanDestroy` strategy represents the ability to destroy an asset. + /// It is both an [inspect](MetadataInspectStrategy) and [update](MetadataUpdateStrategy) + /// metadata strategy. + /// + /// * As the inspect strategy, it returns `bool`. + /// * As the update strategy is accepts `bool`. + /// + /// By default, this strategy means the ability to destroy an asset "in general". + /// However, a user can define several flavors of this strategy by supplying the `Flavor` type. + /// The `Flavor` type can add more details to the strategy. + /// For instance, "Can **a specific user** destroy an asset of **another user**?". + pub struct CanDestroy(pub Flavor); + impl Default for CanDestroy<()> { + fn default() -> Self { + Self(()) + } + } + impl MetadataInspectStrategy for CanDestroy { + type Value = bool; + } + impl MetadataUpdateStrategy for CanDestroy { + type Update<'u> = bool; + type Success = (); + } + + /// The `CanUpdateMetadata` strategy represents the ability to update the metadata of an asset. + /// It is both an [inspect](MetadataInspectStrategy) and [update](MetadataUpdateStrategy) + /// metadata strategy. + /// + /// * As the inspect strategy, it returns `bool`. + /// * As the update strategy is accepts `bool`. + /// + /// By default, this strategy means the ability to update the metadata of an asset "in general". + /// However, a user can define several flavors of this strategy by supplying the `Flavor` type. + /// The `Flavor` type can add more details to the strategy. + /// For instance, "Can **a specific user** update the metadata of an asset **under a certain + /// key**?". + pub struct CanUpdateMetadata(pub Flavor); + impl Default for CanUpdateMetadata<()> { + fn default() -> Self { + Self(()) + } + } + impl MetadataInspectStrategy for CanUpdateMetadata { + type Value = bool; + } + impl MetadataUpdateStrategy for CanUpdateMetadata { + type Update<'u> = bool; + type Success = (); + } + + /// The `AutoId` is an ID assignment approach intended to be used in + /// [`"create" strategies`](CreateStrategy). + /// + /// It accepts the `Id` type of the asset. + /// The "create" strategy should report the value of type `ReportedId` upon successful asset + /// creation. + pub type AutoId = DeriveAndReportId<(), ReportedId>; + + /// The `PredefinedId` is an ID assignment approach intended to be used in + /// [`"create" strategies`](CreateStrategy). + /// + /// It accepts the `Id` that should be assigned to the newly created asset. + /// + /// The "create" strategy should report the `Id` value upon successful asset creation. + pub type PredefinedId = DeriveAndReportId; + + /// The `DeriveAndReportId` is an ID assignment approach intended to be used in + /// [`"create" strategies`](CreateStrategy). + /// + /// It accepts the `Params` and the `Id`. + /// The `ReportedId` value should be computed by the "create" strategy using the `Params` value. + /// + /// The "create" strategy should report the `ReportedId` value upon successful asset creation. + /// + /// An example of ID derivation is the creation of an NFT inside a collection using the + /// collection ID as `Params`. The `ReportedId` in this case is the full ID of the NFT. + #[derive(RuntimeDebug, PartialEq, Eq, Clone, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct DeriveAndReportId { + pub params: Params, + _phantom: PhantomData, + } + impl DeriveAndReportId<(), ReportedId> { + pub fn auto() -> AutoId { + Self { params: (), _phantom: PhantomData } + } + } + impl DeriveAndReportId { + pub fn from(params: Params) -> Self { + Self { params, _phantom: PhantomData } + } + } + impl IdAssignment for DeriveAndReportId { + type ReportedId = ReportedId; + } + + /// The `Owned` is a [`"create" strategy`](CreateStrategy). + /// + /// It accepts: + /// * The `owner` + /// * The [ID assignment](IdAssignment) approach + /// * The optional `config` + /// * The optional creation `witness` + /// + /// The [`Success`](CreateStrategy::Success) will contain + /// the [reported ID](IdAssignment::ReportedId) of the ID assignment approach. + #[derive(RuntimeDebug, PartialEq, Eq, Clone, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct Owned { + pub owner: Owner, + pub id_assignment: Assignment, + pub config: Config, + pub witness: Witness, + } + impl Owned { + pub fn new(owner: Owner, id_assignment: Assignment) -> Self { + Self { id_assignment, owner, config: (), witness: () } + } + } + impl Owned { + pub fn new_configured(owner: Owner, id_assignment: Assignment, config: Config) -> Self { + Self { id_assignment, owner, config, witness: () } + } + } + impl CreateStrategy + for Owned + { + type Success = Assignment::ReportedId; + } + + /// The `Adminable` is a [`"create" strategy`](CreateStrategy). + /// + /// It accepts: + /// * The `owner` + /// * The `admin` + /// * The [ID assignment](IdAssignment) approach + /// * The optional `config` + /// * The optional creation `witness` + /// + /// The [`Success`](CreateStrategy::Success) will contain + /// the [reported ID](IdAssignment::ReportedId) of the ID assignment approach. + #[derive(RuntimeDebug, PartialEq, Eq, Clone, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct Adminable { + pub owner: Account, + pub admin: Account, + pub id_assignment: Assignment, + pub config: Config, + pub witness: Witness, + } + impl Adminable { + pub fn new(id_assignment: Assignment, owner: Account, admin: Account) -> Self { + Self { id_assignment, owner, admin, config: (), witness: () } + } + } + impl Adminable { + pub fn new_configured( + owner: Account, + admin: Account, + id_assignment: Assignment, + config: Config, + ) -> Self { + Self { id_assignment, owner, admin, config, witness: () } + } + } + impl CreateStrategy + for Adminable + { + type Success = Assignment::ReportedId; + } + + /// The `FromTo` is a [`transfer strategy`](TransferStrategy). + /// + /// It accepts two parameters: `from` and `to` whom the asset should be transferred. + pub struct FromTo(pub Owner, pub Owner); + impl TransferStrategy for FromTo { + type Success = (); + } + + /// The `IfOwnedBy` is both a [`destroy strategy`](DestroyStrategy) + /// and a [`stash strategy`](StashStrategy). + /// + /// It accepts a possible owner of the asset. + /// If the provided entity owns the asset, the corresponding operation will be performed. + pub struct IfOwnedBy(pub Owner); + impl DestroyStrategy for IfOwnedBy { + type Success = (); + } + impl StashStrategy for IfOwnedBy { + type Success = (); + } + + /// The `IfRestorable` is a [`restore strategy`](RestoreStrategy). + /// + /// It accepts whatever parameters are set in its generic argument. + /// For instance, if an asset is restorable, + /// this strategy may reference a beneficiary account, + /// which should own the asset upon restoration. + pub struct IfRestorable(pub Params); + impl RestoreStrategy for IfRestorable { + type Success = (); + } + + /// The `WithWitness` is a [`destroy strategy`](DestroyStrategy). + /// + /// It accepts a `Witness` to destroy an asset. + /// It will also return a `Witness` value upon destruction. + pub struct WithWitness(pub Witness); + impl DestroyStrategy for WithWitness { + type Success = Witness; + } + + /// The `IfOwnedByWithWitness` is a [`destroy strategy`](DestroyStrategy). + /// + /// It is a combination of the [`IfOwnedBy`] and the [`WithWitness`] strategies. + pub struct IfOwnedByWithWitness { + pub owner: Owner, + pub witness: Witness, + } + impl DestroyStrategy for IfOwnedByWithWitness { + type Success = Witness; + } +} diff --git a/substrate/frame/uniques/src/asset_ops/collection.rs b/substrate/frame/uniques/src/asset_ops/collection.rs new file mode 100644 index 000000000000..e5bd2ca9765d --- /dev/null +++ b/substrate/frame/uniques/src/asset_ops/collection.rs @@ -0,0 +1,176 @@ +use core::marker::PhantomData; + +use crate::{asset_strategies::Attribute, Collection as CollectionStorage, *}; +use frame_support::{ + ensure, + traits::{ + tokens::asset_ops::{ + common_strategies::{ + Adminable, Bytes, IfOwnedByWithWitness, Ownership, PredefinedId, WithOrigin, + WithWitness, + }, + AssetDefinition, Create, Destroy, InspectMetadata, + }, + EnsureOrigin, Get, + }, + BoundedSlice, +}; +use frame_system::ensure_signed; +use sp_runtime::DispatchError; + +pub struct Collection(PhantomData); + +impl, I: 'static> AssetDefinition for Collection> { + type Id = T::CollectionId; +} + +impl, I: 'static> InspectMetadata> + for Collection> +{ + fn inspect_metadata( + collection: &Self::Id, + _ownership: Ownership, + ) -> Result { + CollectionStorage::::get(collection) + .map(|a| a.owner) + .ok_or(Error::::UnknownCollection.into()) + } +} + +impl, I: 'static> InspectMetadata for Collection> { + fn inspect_metadata(collection: &Self::Id, _bytes: Bytes) -> Result, DispatchError> { + CollectionMetadataOf::::get(collection) + .map(|m| m.data.into()) + .ok_or(Error::::NoMetadata.into()) + } +} + +impl<'a, T: Config, I: 'static> InspectMetadata>> + for Collection> +{ + fn inspect_metadata( + collection: &Self::Id, + strategy: Bytes, + ) -> Result, DispatchError> { + let Bytes(Attribute(attribute)) = strategy; + + let attribute = + BoundedSlice::try_from(attribute).map_err(|_| Error::::WrongAttribute)?; + crate::Attribute::::get((collection, Option::::None, attribute)) + .map(|a| a.0.into()) + .ok_or(Error::::AttributeNotFound.into()) + } +} + +impl, I: 'static> Create>> + for Collection> +{ + fn create( + strategy: Adminable>, + ) -> Result { + let Adminable { owner, admin, id_assignment, .. } = strategy; + let collection = id_assignment.params; + + >::do_create_collection( + collection.clone(), + owner.clone(), + admin.clone(), + T::CollectionDeposit::get(), + false, + Event::Created { collection: collection.clone(), creator: owner, owner: admin }, + )?; + + Ok(collection) + } +} + +impl, I: 'static> + Create>>> + for Collection> +{ + fn create( + strategy: WithOrigin< + T::RuntimeOrigin, + Adminable>, + >, + ) -> Result { + let WithOrigin(origin, creation) = strategy; + + let Adminable { owner, id_assignment, .. } = &creation; + let collection = &id_assignment.params; + + let maybe_check_signer = + T::ForceOrigin::try_origin(origin).map(|_| None).or_else(|origin| { + T::CreateOrigin::ensure_origin(origin, collection) + .map(Some) + .map_err(DispatchError::from) + })?; + + if let Some(signer) = maybe_check_signer { + ensure!(signer == *owner, Error::::NoPermission); + } + + Self::create(creation) + } +} + +impl, I: 'static> Destroy> for Collection> { + fn destroy( + collection: &Self::Id, + strategy: WithWitness, + ) -> Result { + let WithWitness(witness) = strategy; + + >::do_destroy_collection(collection.clone(), witness, None) + } +} + +impl, I: 'static> Destroy>> + for Collection> +{ + fn destroy( + collection: &Self::Id, + strategy: WithOrigin>, + ) -> Result { + let WithOrigin(origin, destroy) = strategy; + + T::ForceOrigin::ensure_origin(origin)?; + + Self::destroy(collection, destroy) + } +} + +impl, I: 'static> Destroy> + for Collection> +{ + fn destroy( + collection: &Self::Id, + strategy: IfOwnedByWithWitness, + ) -> Result { + let IfOwnedByWithWitness { owner, witness } = strategy; + + >::do_destroy_collection(collection.clone(), witness, Some(owner)) + } +} + +impl, I: 'static> + Destroy>> + for Collection> +{ + fn destroy( + collection: &Self::Id, + strategy: WithOrigin>, + ) -> Result { + let WithOrigin(origin, IfOwnedByWithWitness { owner, witness }) = strategy; + let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { + Ok(_) => None, + Err(origin) => Some(ensure_signed(origin)?), + }; + + if let Some(signer) = maybe_check_owner { + ensure!(signer == owner, Error::::NoPermission); + } + + >::do_destroy_collection(collection.clone(), witness, Some(owner)) + } +} diff --git a/substrate/frame/uniques/src/asset_ops/item.rs b/substrate/frame/uniques/src/asset_ops/item.rs new file mode 100644 index 000000000000..9384bd68c40b --- /dev/null +++ b/substrate/frame/uniques/src/asset_ops/item.rs @@ -0,0 +1,212 @@ +use core::marker::PhantomData; + +use crate::{asset_strategies::Attribute, Item as ItemStorage, *}; +use frame_support::{ + dispatch::DispatchResult, + ensure, + traits::tokens::asset_ops::{ + common_strategies::{ + Bytes, CanTransfer, FromTo, IfOwnedBy, JustDo, Owned, Ownership, PredefinedId, + WithOrigin, + }, + AssetDefinition, Create, Destroy, InspectMetadata, Transfer, + }, + BoundedSlice, +}; +use frame_system::ensure_signed; +use sp_runtime::DispatchError; + +pub struct Item(PhantomData); + +impl, I: 'static> AssetDefinition for Item> { + type Id = (T::CollectionId, T::ItemId); +} + +impl, I: 'static> InspectMetadata> for Item> { + fn inspect_metadata( + (collection, item): &Self::Id, + _ownership: Ownership, + ) -> Result { + ItemStorage::::get(collection, item) + .map(|a| a.owner) + .ok_or(Error::::UnknownItem.into()) + } +} + +impl, I: 'static> InspectMetadata for Item> { + fn inspect_metadata( + (collection, item): &Self::Id, + _bytes: Bytes, + ) -> Result, DispatchError> { + ItemMetadataOf::::get(collection, item) + .map(|m| m.data.into()) + .ok_or(Error::::NoMetadata.into()) + } +} + +impl<'a, T: Config, I: 'static> InspectMetadata>> for Item> { + fn inspect_metadata( + (collection, item): &Self::Id, + strategy: Bytes, + ) -> Result, DispatchError> { + let Bytes(Attribute(attribute)) = strategy; + + let attribute = + BoundedSlice::try_from(attribute).map_err(|_| Error::::WrongAttribute)?; + crate::Attribute::::get((collection, Some(item), attribute)) + .map(|a| a.0.into()) + .ok_or(Error::::AttributeNotFound.into()) + } +} + +impl, I: 'static> InspectMetadata for Item> { + fn inspect_metadata( + (collection, item): &Self::Id, + _can_transfer: CanTransfer, + ) -> Result { + match (Collection::::get(collection), ItemStorage::::get(collection, item)) { + (Some(cd), Some(id)) => Ok(!cd.is_frozen && !id.is_frozen), + _ => Err(Error::::UnknownItem.into()), + } + } +} + +impl, I: 'static> + Create>> for Item> +{ + fn create( + strategy: Owned>, + ) -> Result<(T::CollectionId, T::ItemId), DispatchError> { + let Owned { owner, id_assignment, .. } = strategy; + let (collection, item) = id_assignment.params; + + >::do_mint(collection.clone(), item, owner, |_| Ok(()))?; + + Ok((collection, item)) + } +} + +impl, I: 'static> + Create< + WithOrigin< + T::RuntimeOrigin, + Owned>, + >, + > for Item> +{ + fn create( + strategy: WithOrigin< + T::RuntimeOrigin, + Owned>, + >, + ) -> Result<(T::CollectionId, T::ItemId), DispatchError> { + let WithOrigin(origin, Owned { owner, id_assignment, .. }) = strategy; + let (collection, item) = id_assignment.params; + + let signer = ensure_signed(origin)?; + + >::do_mint(collection.clone(), item, owner, |collection_details| { + ensure!(collection_details.issuer == signer, Error::::NoPermission); + Ok(()) + })?; + + Ok((collection, item)) + } +} + +impl, I: 'static> Transfer> for Item> { + fn transfer((collection, item): &Self::Id, strategy: JustDo) -> DispatchResult { + let JustDo(dest) = strategy; + + >::do_transfer(collection.clone(), *item, dest, |_, _| Ok(())) + } +} + +impl, I: 'static> Transfer>> + for Item> +{ + fn transfer( + (collection, item): &Self::Id, + strategy: WithOrigin>, + ) -> DispatchResult { + let WithOrigin(origin, JustDo(dest)) = strategy; + + let signer = ensure_signed(origin)?; + + >::do_transfer( + collection.clone(), + *item, + dest.clone(), + |collection_details, details| { + if details.owner != signer && collection_details.admin != signer { + let approved = details.approved.take().map_or(false, |i| i == signer); + ensure!(approved, Error::::NoPermission); + } + Ok(()) + }, + ) + } +} + +impl, I: 'static> Transfer> for Item> { + fn transfer((collection, item): &Self::Id, strategy: FromTo) -> DispatchResult { + let FromTo(from, to) = strategy; + + >::do_transfer(collection.clone(), *item, to.clone(), |_, details| { + ensure!(details.owner == from, Error::::WrongOwner); + Ok(()) + }) + } +} + +impl, I: 'static> Destroy for Item> { + fn destroy((collection, item): &Self::Id, _strategy: JustDo) -> DispatchResult { + >::do_burn(collection.clone(), *item, |_, _| Ok(())) + } +} + +impl, I: 'static> Destroy> + for Item> +{ + fn destroy( + id @ (collection, item): &Self::Id, + strategy: WithOrigin, + ) -> DispatchResult { + let WithOrigin(origin, _just_do) = strategy; + let details = + ItemStorage::::get(collection, item).ok_or(Error::::UnknownCollection)?; + + Self::destroy(id, WithOrigin(origin, IfOwnedBy(details.owner))) + } +} + +impl, I: 'static> Destroy> for Item> { + fn destroy((collection, item): &Self::Id, strategy: IfOwnedBy) -> DispatchResult { + let IfOwnedBy(who) = strategy; + + >::do_burn(collection.clone(), *item, |_, d| { + ensure!(d.owner == who, Error::::NoPermission); + Ok(()) + }) + } +} + +impl, I: 'static> Destroy>> + for Item> +{ + fn destroy( + (collection, item): &Self::Id, + strategy: WithOrigin>, + ) -> DispatchResult { + let WithOrigin(origin, IfOwnedBy(who)) = strategy; + + let signer = ensure_signed(origin)?; + + >::do_burn(collection.clone(), *item, |collection_details, details| { + let is_permitted = collection_details.admin == signer || details.owner == signer; + ensure!(is_permitted, Error::::NoPermission); + ensure!(who == details.owner, Error::::WrongOwner); + Ok(()) + }) + } +} diff --git a/substrate/frame/uniques/src/asset_ops/mod.rs b/substrate/frame/uniques/src/asset_ops/mod.rs new file mode 100644 index 000000000000..e2105654505f --- /dev/null +++ b/substrate/frame/uniques/src/asset_ops/mod.rs @@ -0,0 +1,5 @@ +mod collection; +mod item; + +pub use collection::Collection; +pub use item::Item; diff --git a/substrate/frame/uniques/src/lib.rs b/substrate/frame/uniques/src/lib.rs index dc27c3356234..7de56f63676c 100644 --- a/substrate/frame/uniques/src/lib.rs +++ b/substrate/frame/uniques/src/lib.rs @@ -39,6 +39,7 @@ mod functions; mod impl_nonfungibles; mod types; +pub mod asset_ops; pub mod migration; pub mod weights; @@ -422,6 +423,14 @@ pub mod pallet { NotForSale, /// The provided bid is too low. BidTooLow, + /// No metadata is found. + NoMetadata, + /// Wrong metadata key/value bytes supplied. + WrongMetadata, + /// An attribute is not found. + AttributeNotFound, + /// Wrong attribute key/value bytes supplied. + WrongAttribute, } impl, I: 'static> Pallet { diff --git a/substrate/frame/uniques/src/types.rs b/substrate/frame/uniques/src/types.rs index a2e804f245f7..462bf20f6a30 100644 --- a/substrate/frame/uniques/src/types.rs +++ b/substrate/frame/uniques/src/types.rs @@ -131,3 +131,7 @@ pub struct ItemMetadata> { /// Whether the item metadata may be changed by a non Force origin. pub(super) is_frozen: bool, } + +pub mod asset_strategies { + pub struct Attribute<'a>(pub &'a [u8]); +} diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 8d85e26d8fe7..944e40bd30a3 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -162,6 +162,7 @@ std = [ "pallet-xcm-bridge-hub-router?/std", "pallet-xcm-bridge-hub?/std", "pallet-xcm?/std", + "pallet-derivatives?/std", "parachains-common?/std", "parachains-runtimes-test-utils?/std", "polkadot-core-primitives?/std", @@ -340,6 +341,7 @@ runtime-benchmarks = [ "pallet-xcm-bridge-hub-router?/runtime-benchmarks", "pallet-xcm-bridge-hub?/runtime-benchmarks", "pallet-xcm?/runtime-benchmarks", + "pallet-derivatives?/runtime-benchmarks", "parachains-common?/runtime-benchmarks", "polkadot-cli?/runtime-benchmarks", "polkadot-node-metrics?/runtime-benchmarks", @@ -471,6 +473,7 @@ try-runtime = [ "pallet-xcm-bridge-hub-router?/try-runtime", "pallet-xcm-bridge-hub?/try-runtime", "pallet-xcm?/try-runtime", + "pallet-derivatives?/try-runtime", "polkadot-cli?/try-runtime", "polkadot-runtime-common?/try-runtime", "polkadot-runtime-parachains?/try-runtime", @@ -697,6 +700,7 @@ runtime = [ "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", + "pallet-derivatives", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", @@ -1573,6 +1577,11 @@ path = "../bridges/modules/xcm-bridge-hub-router" default-features = false optional = true +[dependencies.pallet-derivatives] +path = "../polkadot/xcm/pallet-derivatives" +default-features = false +optional = true + [dependencies.parachains-common] path = "../cumulus/parachains/common" default-features = false diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index 58a5691961d9..6aa19cdcd957 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -753,6 +753,10 @@ pub use pallet_xcm_bridge_hub; #[cfg(feature = "pallet-xcm-bridge-hub-router")] pub use pallet_xcm_bridge_hub_router; +/// XCM derivatives pallet. +#[cfg(feature = "pallet-derivatives")] +pub use pallet_derivatives; + /// Logic which is common to all parachain runtimes. #[cfg(feature = "parachains-common")] pub use parachains_common;