feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
[package]
|
||||
name = "pezpallet-derivatives"
|
||||
version = "1.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "Apache-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "FRAME derivatives pallet"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true }
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { workspace = true }
|
||||
pezframe-system = { workspace = true }
|
||||
log = { workspace = true }
|
||||
scale-info = { features = ["derive"], workspace = true }
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-io = { workspace = true }
|
||||
pezsp-runtime = { workspace = true }
|
||||
pezsp-std = { workspace = true }
|
||||
|
||||
xcm = { workspace = true }
|
||||
xcm-builder = { workspace = true }
|
||||
xcm-executor = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezpallet-balances = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"scale-info/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
"xcm-builder/std",
|
||||
"xcm-executor/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
"xcm-executor/runtime-benchmarks",
|
||||
"xcm/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,84 @@
|
||||
# derivatives
|
||||
|
||||
The purpose of the `pezpallet-derivatives` is to cover the following derivative asset support
|
||||
scenarios:
|
||||
|
||||
1. The `pezpallet-derivatives` can serve as an API for creating and destroying derivatives.
|
||||
2. It can store a mapping between the foreign original ID (e.g., XCM `AssetId` or `(AssetId, AssetInstance)`)
|
||||
and the local derivative ID.
|
||||
|
||||
The scenarios can be combined.
|
||||
|
||||
## Motivation
|
||||
|
||||
The motivation differs depending on the scenario in question.
|
||||
|
||||
### The first scenario
|
||||
|
||||
The `pezpallet-derivatives` can be helpful when another pallet, which hosts the derivative assets,
|
||||
doesn't provide a good enough way to create new assets in the context of them being derivatives.
|
||||
|
||||
For instance, the asset hosting pallet might have an asset class (NFT collection or fungible currency) creation extrinsic,
|
||||
but among its parameters, there could be things like some admin account, currency decimals, various permissions, etc.
|
||||
|
||||
When creating a regular (i.e., non-derivative) asset class via such an extrinsic,
|
||||
these parameters allow one to conveniently set all the needed data for the asset class.
|
||||
However, when creating a derivative asset class, we usually can't allow an arbitrary user to
|
||||
influence such parameters since they should be set per the original asset class owner's desires.
|
||||
|
||||
Thus, we can either require a privileged origin for derivative asset classes (such as Root or some collective)
|
||||
or we could provide an alternative API where the sensitive parameters are omitted (and set by the chain runtime automatically).
|
||||
|
||||
The first approach dominates in the ecosystem at the moment since:
|
||||
1. It is simple
|
||||
2. There was no pallet to make such an alternative API without rewriting individual
|
||||
asset-hosting pallets
|
||||
3. Only fungible derivatives were ever made (with rare exceptions like an NFT derivative
|
||||
collection on Karura).
|
||||
|
||||
The fungible derivatives are one of the reasons because they almost always have at least
|
||||
decimals and symbol information that should be correct, so only a privileged origin is
|
||||
acceptable to do the registration, since there is no way (at the time of writing) to communicate
|
||||
asset data between chains directly (this will be fixed when Fellowship RFC 125 will be implemented).
|
||||
|
||||
Derivative NFT collections and their tokens, on the other hand, just need to point to the
|
||||
originals. An NFT derivative is meant to participate in mechanisms unique to the given hosting
|
||||
chain, such as NFT fractionalization, nesting, etc., where only its ID is needed to do said interactions.
|
||||
|
||||
In the future, there could be interactions where NFT data is needed. These interactions will be
|
||||
able to leverage XCM Asset Metadata instructions from Fellowship RFC 125.
|
||||
However, even with the IDs only, there are use cases (as mentioned above), and more could be discovered.
|
||||
Requiring a privileged origin where no sensitive parameters are needed for registering derivative NFT collections
|
||||
is raising an unreasonable barrier for NFT interoperability between chains.
|
||||
So, providing an API for unprivileged derivative registration is a preferable choice in this case.
|
||||
|
||||
Moreover, the future data communication via XCM can benefit both fungible and non-fungible
|
||||
derivative collections registration.
|
||||
1. The `create_derivative` extrinsic of this pallet can be configured to initiate the
|
||||
registration process
|
||||
by sending the `ReportMetadata` instruction to the reserve chain. It can be configured such that
|
||||
this can be done by anyone.
|
||||
2. The reserve chain will decide whether to send the data or an error depending on its state.
|
||||
3. Our chain will handle the reserve chain's response and decide whether it is okay to register
|
||||
the given asset.
|
||||
|
||||
### The second scenario
|
||||
|
||||
Saving the mapping between the original ID and the derivative ID is needed when their types
|
||||
differ and the derivative ID value can't be deterministically deduced from the original ID.
|
||||
This situation can arise in the following cases:
|
||||
* The original ID type is incompatible with a derivative ID type.
|
||||
For example, let `pezpallet-nfts` instance host derivative NFT collections. We can't set the
|
||||
`CollectionId` (the derivative ID type) to XCM `AssetId` (the original ID type)
|
||||
because `pezpallet-nfts` requires `CollectionId` to be incrementable.
|
||||
* It is desired to have a continuous ID space for all objects, both derivative and local.
|
||||
For instance, one might want to reuse the existing pallet combinations (like `pezpallet-nfts`
|
||||
instance + `pezpallet-nfts-fractionalization` instance) without adding new pallet instances between
|
||||
the one hosting NFTs and many special logic pallets. In this case, the original ID type would be
|
||||
`(AssetId, AssetInstance)`, and the derivative ID type can be anything.
|
||||
|
||||
## Usage examples
|
||||
|
||||
The `src/mock/mod.rs` contains a mock runtime declaration that contains several instances of the pallet
|
||||
to test the scenarios mentioned above.
|
||||
This test configuration can be viewed as a usage example alongside the tests in the `src/tests.rs`.
|
||||
@@ -0,0 +1,61 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
use super::{Pallet as Derivatives, *};
|
||||
use pezframe_benchmarking::v2::*;
|
||||
|
||||
pub struct Pallet<T: Config<I>, I: 'static = ()>(Derivatives<T, I>);
|
||||
|
||||
pub trait Config<I: 'static = ()>: super::Config<I> {
|
||||
fn max_original() -> OriginalOf<Self, I>;
|
||||
}
|
||||
|
||||
#[instance_benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn create_derivative() -> Result<(), BenchmarkError> {
|
||||
let create_origin =
|
||||
T::CreateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
|
||||
let original = T::max_original();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(create_origin as T::RuntimeOrigin, original);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn destroy_derivative() -> Result<(), BenchmarkError> {
|
||||
let create_origin =
|
||||
T::CreateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
|
||||
let destroy_origin =
|
||||
T::DestroyOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
|
||||
let original = T::max_original();
|
||||
|
||||
<Derivatives<T, I>>::create_derivative(create_origin, original.clone())?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(destroy_origin as T::RuntimeOrigin, original);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
//! The purpose of the `pezpallet-derivatives` is to cover the following derivative asset support
|
||||
//! scenarios:
|
||||
//! 1. The `pezpallet-derivatives` can serve as an API for creating and destroying derivatives.
|
||||
//! 2. It can store a mapping between the foreign original ID (e.g., XCM `AssetId` or `(AssetId,
|
||||
//! AssetInstance)`) and the local derivative ID.
|
||||
//!
|
||||
//! The scenarios can be combined.
|
||||
//!
|
||||
//! ### Motivation
|
||||
//!
|
||||
//! The motivation differs depending on the scenario in question.
|
||||
//!
|
||||
//! #### The first scenario
|
||||
//!
|
||||
//! The `pezpallet-derivatives` can be helpful when another pallet, which hosts the derivative assets,
|
||||
//! doesn't provide a good enough way to create new assets in the context of them being derivatives.
|
||||
//!
|
||||
//! For instance, the asset hosting pallet might have an asset class (NFT collection or fungible
|
||||
//! currency) creation extrinsic, but among its parameters, there could be things like some admin
|
||||
//! account, currency decimals, various permissions, etc. When creating a regular (i.e.,
|
||||
//! non-derivative) asset class via such an extrinsic, these parameters allow one to conveniently
|
||||
//! set all the needed data for the asset class.
|
||||
//! However, when creating a derivative asset class, we usually can't allow an arbitrary user to
|
||||
//! influence such parameters since they should be set per the original asset class owner's desires.
|
||||
//! Thus, we can either require a privileged origin for derivative asset classes (such as Root or
|
||||
//! some collective) or we could provide an alternative API where the sensitive parameters are
|
||||
//! omitted (and set by the chain runtime automatically).
|
||||
//!
|
||||
//! The first approach dominates in the ecosystem at the moment since:
|
||||
//! 1. It is simple
|
||||
//! 2. There was no pallet to make such an alternative API without rewriting individual
|
||||
//! asset-hosting pallets
|
||||
//! 3. Only fungible derivatives were ever made (with rare exceptions like an NFT derivative
|
||||
//! collection on Karura).
|
||||
//!
|
||||
//! The fungible derivatives are one of the reasons because they almost always have at least
|
||||
//! decimals and symbol information that should be correct, so only a privileged origin is
|
||||
//! acceptable to do the registration, since there is no way (at the time of writing) to communicate
|
||||
//! asset data between chains directly (this will be fixed when Fellowship RFC 125 will be
|
||||
//! implemented).
|
||||
//!
|
||||
//! Derivative NFT collections and their tokens, on the other hand, just need to point to the
|
||||
//! originals. An NFT derivative is meant to participate in mechanisms unique to the given hosting
|
||||
//! chain, such as NFT fractionalization, nesting, etc., where only its ID is needed to do said
|
||||
//! interactions.
|
||||
//!
|
||||
//! In the future, there could be interactions where NFT data is needed. These interactions will be
|
||||
//! able to leverage XCM Asset Metadata instructions from Fellowship RFC 125. However, even with the
|
||||
//! IDs only, there are use cases (as mentioned above), and more could be discovered. Requiring a
|
||||
//! privileged origin where no sensitive parameters are needed for registering derivative NFT
|
||||
//! collections is raising an unreasonable barrier for NFT interoperability between chains. So,
|
||||
//! providing an API for unprivileged derivative registration is a preferable choice in this case.
|
||||
//!
|
||||
//! Moreover, the future data communication via XCM can benefit both fungible and non-fungible
|
||||
//! derivative collections registration.
|
||||
//! 1. The `create_derivative` extrinsic of this pallet can be configured to initiate the
|
||||
//! registration process
|
||||
//! by sending the `ReportMetadata` instruction to the reserve chain. It can be configured such that
|
||||
//! this can be done by anyone.
|
||||
//! 2. The reserve chain will decide whether to send the data or an error depending on its state.
|
||||
//! 3. Our chain will handle the reserve chain's response and decide whether it is okay to register
|
||||
//! the given asset.
|
||||
//!
|
||||
//! #### The second scenario
|
||||
//!
|
||||
//! Saving the mapping between the original ID and the derivative ID is needed when their types
|
||||
//! differ and the derivative ID value can't be deterministically deduced from the original ID.
|
||||
//!
|
||||
//! This situation can arise in the following cases:
|
||||
//! * The original ID type is incompatible with a derivative ID type.
|
||||
//! For example, let `pezpallet-nfts` instance host derivative NFT collections. We can't set the
|
||||
//! `CollectionId` (the derivative ID type) to XCM `AssetId` (the original ID type)
|
||||
//! because `pezpallet-nfts` requires `CollectionId` to be incrementable.
|
||||
//! * It is desired to have a continuous ID space for all objects, both derivative and local.
|
||||
//! For instance, one might want to reuse the existing pallet combinations (like `pezpallet-nfts`
|
||||
//! instance + `pezpallet-nfts-fractionalization` instance) without adding new pallet instances between
|
||||
//! the one hosting NFTs and many special logic pallets. In this case, the original ID type would be
|
||||
//! `(AssetId, AssetInstance)`, and the derivative ID type can be anything.
|
||||
|
||||
#![recursion_limit = "256"]
|
||||
// Ensure we're `no_std` when compiling for Wasm.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*,
|
||||
traits::tokens::asset_ops::{
|
||||
common_strategies::{DeriveAndReportId, NoParams},
|
||||
AssetDefinition, Create, Destroy,
|
||||
},
|
||||
};
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_runtime::DispatchResult;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub mod misc;
|
||||
|
||||
pub use misc::*;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub mod benchmarking;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// The log target of this pallet.
|
||||
pub const LOG_TARGET: &'static str = "runtime::xcm::derivatives";
|
||||
|
||||
/// A helper type representing the intention to store
|
||||
/// the mapping between the original and the given derivative.
|
||||
pub struct SaveMappingTo<Derivative>(pub Derivative);
|
||||
|
||||
type OriginalOf<T, I> = <T as Config<I>>::Original;
|
||||
type DerivativeOf<T, I> = <T as Config<I>>::Derivative;
|
||||
type DerivativeExtraOf<T, I> = <T as Config<I>>::DerivativeExtra;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config<I: 'static = ()>: pezframe_system::Config {
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// The type of an original
|
||||
type Original: Member + Parameter + MaxEncodedLen;
|
||||
|
||||
/// The type of a derivative
|
||||
type Derivative: Member + Parameter + MaxEncodedLen;
|
||||
|
||||
/// Optional derivative extra data
|
||||
type DerivativeExtra: Member + Parameter + MaxEncodedLen;
|
||||
|
||||
/// An Origin allowed to create a new derivative.
|
||||
type CreateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// Derivative creation operation.
|
||||
/// Used in the `create_derivative` extrinsic.
|
||||
///
|
||||
/// Can be configured to save the mapping between the original and the derivative
|
||||
/// if it returns `Some(SaveMappingTo(DERIVATIVE))`.
|
||||
///
|
||||
/// If the extrinsic isn't used, this type can be set to
|
||||
/// [DisabledOps](pezframe_support::traits::tokens::asset_ops::common_ops::DisabledOps).
|
||||
type CreateOp: Create<
|
||||
DeriveAndReportId<Self::Original, Option<SaveMappingTo<Self::Derivative>>>,
|
||||
>;
|
||||
|
||||
/// An Origin allowed to destroy a derivative.
|
||||
type DestroyOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// Derivative destruction operation.
|
||||
/// Used in the `destroy_derivative` extrinsic.
|
||||
///
|
||||
/// If the extrinsic isn't used, this type can be set to
|
||||
/// [DisabledOps](pezframe_support::traits::tokens::asset_ops::common_ops::DisabledOps).
|
||||
type DestroyOp: AssetDefinition<Id = Self::Original> + Destroy<NoParams>;
|
||||
}
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn original_to_derivative)]
|
||||
pub type OriginalToDerivative<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Blake2_128Concat, OriginalOf<T, I>, DerivativeOf<T, I>, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn derivative_to_original)]
|
||||
pub type DerivativeToOriginal<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Blake2_128Concat, DerivativeOf<T, I>, OriginalOf<T, I>, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn derivative_extra)]
|
||||
pub type DerivativeExtra<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Blake2_128Concat, DerivativeOf<T, I>, DerivativeExtraOf<T, I>, OptionQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {
|
||||
/// A derivative is created.
|
||||
DerivativeCreated { original: OriginalOf<T, I> },
|
||||
|
||||
/// A mapping between an original asset ID and a local derivative asset ID is created.
|
||||
DerivativeMappingCreated { original: OriginalOf<T, I>, derivative_id: DerivativeOf<T, I> },
|
||||
|
||||
/// A derivative is destroyed.
|
||||
DerivativeDestroyed { original: OriginalOf<T, I> },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T, I = ()> {
|
||||
/// 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,
|
||||
|
||||
/// Invalid asset to register as a derivative
|
||||
InvalidAsset,
|
||||
}
|
||||
|
||||
#[pallet::call(weight(T::WeightInfo))]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
#[pallet::call_index(0)]
|
||||
pub fn create_derivative(
|
||||
origin: OriginFor<T>,
|
||||
original: OriginalOf<T, I>,
|
||||
) -> DispatchResult {
|
||||
T::CreateOrigin::ensure_origin(origin)?;
|
||||
|
||||
let maybe_save_mapping =
|
||||
T::CreateOp::create(DeriveAndReportId::from(original.clone()))?;
|
||||
|
||||
if let Some(SaveMappingTo(derivative)) = maybe_save_mapping {
|
||||
Self::try_register_derivative(&original, &derivative)?;
|
||||
}
|
||||
|
||||
Self::deposit_event(Event::<T, I>::DerivativeCreated { original });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(1)]
|
||||
pub fn destroy_derivative(
|
||||
origin: OriginFor<T>,
|
||||
original: OriginalOf<T, I>,
|
||||
) -> DispatchResult {
|
||||
T::DestroyOrigin::ensure_origin(origin)?;
|
||||
|
||||
T::DestroyOp::destroy(&original, NoParams)?;
|
||||
|
||||
if Self::get_derivative(&original).is_ok() {
|
||||
Self::try_deregister_derivative_of(&original)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> DerivativesRegistry<OriginalOf<T, I>, DerivativeOf<T, I>>
|
||||
for Pallet<T, I>
|
||||
{
|
||||
fn try_register_derivative(
|
||||
original: &OriginalOf<T, I>,
|
||||
derivative: &DerivativeOf<T, I>,
|
||||
) -> DispatchResult {
|
||||
ensure!(
|
||||
Self::original_to_derivative(original).is_none(),
|
||||
Error::<T, I>::DerivativeAlreadyExists,
|
||||
);
|
||||
|
||||
<OriginalToDerivative<T, I>>::insert(original, derivative);
|
||||
<DerivativeToOriginal<T, I>>::insert(derivative, original);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::DerivativeCreated { original: original.clone() });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_deregister_derivative_of(original: &OriginalOf<T, I>) -> DispatchResult {
|
||||
let derivative = <OriginalToDerivative<T, I>>::take(&original)
|
||||
.ok_or(Error::<T, I>::NoDerivativeToDeregister)?;
|
||||
|
||||
<DerivativeToOriginal<T, I>>::remove(&derivative);
|
||||
<DerivativeExtra<T, I>>::remove(&derivative);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::DerivativeDestroyed { original: original.clone() });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_derivative(original: &OriginalOf<T, I>) -> Result<DerivativeOf<T, I>, DispatchError> {
|
||||
<OriginalToDerivative<T, I>>::get(original).ok_or(Error::<T, I>::DerivativeNotFound.into())
|
||||
}
|
||||
|
||||
fn get_original(derivative: &DerivativeOf<T, I>) -> Result<OriginalOf<T, I>, DispatchError> {
|
||||
<DerivativeToOriginal<T, I>>::get(derivative).ok_or(Error::<T, I>::OriginalNotFound.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> IterDerivativesRegistry<OriginalOf<T, I>, DerivativeOf<T, I>>
|
||||
for Pallet<T, I>
|
||||
{
|
||||
fn iter_originals() -> impl Iterator<Item = OriginalOf<T, I>> {
|
||||
<OriginalToDerivative<T, I>>::iter_keys()
|
||||
}
|
||||
|
||||
fn iter_derivatives() -> impl Iterator<Item = DerivativeOf<T, I>> {
|
||||
<OriginalToDerivative<T, I>>::iter_values()
|
||||
}
|
||||
|
||||
fn iter() -> impl Iterator<Item = (OriginalOf<T, I>, DerivativeOf<T, I>)> {
|
||||
<OriginalToDerivative<T, I>>::iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> DerivativesExtra<DerivativeOf<T, I>, DerivativeExtraOf<T, I>>
|
||||
for Pallet<T, I>
|
||||
{
|
||||
fn get_derivative_extra(derivative: &DerivativeOf<T, I>) -> Option<DerivativeExtraOf<T, I>> {
|
||||
<DerivativeExtra<T, I>>::get(derivative)
|
||||
}
|
||||
|
||||
fn set_derivative_extra(
|
||||
derivative: &DerivativeOf<T, I>,
|
||||
extra: Option<DerivativeExtraOf<T, I>>,
|
||||
) -> DispatchResult {
|
||||
ensure!(
|
||||
<DerivativeToOriginal<T, I>>::contains_key(derivative),
|
||||
Error::<T, I>::DerivativeNotFound,
|
||||
);
|
||||
|
||||
<DerivativeExtra<T, I>>::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)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `NoStoredMapping` adapter calls the `CreateOp` (which should take the `Original` value and
|
||||
/// return a `Derivative` one) and returns `None`, indicating that the mapping between the original
|
||||
/// and the derivative shouldn't be saved.
|
||||
///
|
||||
/// This adapter can be used when the types of the `Original` and the `Derivative` are the same,
|
||||
/// or they can be computed from one another.
|
||||
/// (in these cases, the pezpallet-derivatives is used as an a derivative-creation API only)
|
||||
pub struct NoStoredMapping<CreateOp>(PhantomData<CreateOp>);
|
||||
impl<CreateOp, Original, Derivative>
|
||||
Create<DeriveAndReportId<Original, Option<SaveMappingTo<Derivative>>>>
|
||||
for NoStoredMapping<CreateOp>
|
||||
where
|
||||
CreateOp: Create<DeriveAndReportId<Original, Derivative>>,
|
||||
{
|
||||
fn create(
|
||||
strategy: DeriveAndReportId<Original, Option<SaveMappingTo<Derivative>>>,
|
||||
) -> Result<Option<SaveMappingTo<Derivative>>, DispatchError> {
|
||||
CreateOp::create(DeriveAndReportId::from(strategy.params))?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `StoreMapping` adapter obtains a `Derivative` value by calling the `CreateOp`
|
||||
/// (which should take the `Original` value and return a `Derivative` one),
|
||||
/// and returns `Some(SaveMappingTo(DERIVATIVE_VALUE))`, indicating that the mapping should be
|
||||
/// saved.
|
||||
///
|
||||
/// This adapter can be used when the types of the `Original` and the `Derivative` differ
|
||||
/// and can't be computed from one another.
|
||||
pub struct StoreMapping<CreateOp>(PhantomData<CreateOp>);
|
||||
impl<CreateOp, Original, Derivative>
|
||||
Create<DeriveAndReportId<Original, Option<SaveMappingTo<Derivative>>>> for StoreMapping<CreateOp>
|
||||
where
|
||||
CreateOp: Create<DeriveAndReportId<Original, Derivative>>,
|
||||
{
|
||||
fn create(
|
||||
strategy: DeriveAndReportId<Original, Option<SaveMappingTo<Derivative>>>,
|
||||
) -> Result<Option<SaveMappingTo<Derivative>>, DispatchError> {
|
||||
let derivative = CreateOp::create(DeriveAndReportId::from(strategy.params))?;
|
||||
|
||||
Ok(Some(SaveMappingTo(derivative)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the `InvalidAsset` error from the given `pezpallet-derivatives` instance.
|
||||
pub struct InvalidAssetError<Pallet>(PhantomData<Pallet>);
|
||||
impl<T: Config<I>, I: 'static> TypedGet for InvalidAssetError<Pallet<T, I>> {
|
||||
type Type = Error<T, I>;
|
||||
|
||||
fn get() -> Self::Type {
|
||||
Error::<T, I>::InvalidAsset
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
//! Miscellaneous traits and types for working with unique instances derivatives.
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_support::{
|
||||
ensure, parameter_types,
|
||||
traits::{
|
||||
tokens::asset_ops::{
|
||||
common_strategies::{
|
||||
AutoId, ConfigValue, ConfigValueMarker, DeriveAndReportId, Owner, WithConfig,
|
||||
},
|
||||
Create,
|
||||
},
|
||||
Incrementable,
|
||||
},
|
||||
};
|
||||
use pezsp_runtime::{
|
||||
traits::{Convert, TypedGet},
|
||||
DispatchError, DispatchResult,
|
||||
};
|
||||
use xcm::latest::prelude::*;
|
||||
use xcm_builder::unique_instances::NonFungibleAsset;
|
||||
use xcm_executor::traits::{ConvertLocation, Error, MatchesInstance};
|
||||
|
||||
/// 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<Original, Derivative> {
|
||||
fn try_register_derivative(original: &Original, derivative: &Derivative) -> DispatchResult;
|
||||
|
||||
fn try_deregister_derivative_of(original: &Original) -> DispatchResult;
|
||||
|
||||
fn get_derivative(original: &Original) -> Result<Derivative, DispatchError>;
|
||||
|
||||
fn get_original(derivative: &Derivative) -> Result<Original, DispatchError>;
|
||||
}
|
||||
|
||||
/// The `OriginalToDerivativeConvert` uses the provided [DerivativesRegistry] to
|
||||
/// convert the `Original` value to the `Derivative` one.
|
||||
pub struct OriginalToDerivativeConvert<R>(PhantomData<R>);
|
||||
impl<Original, Derivative, R: DerivativesRegistry<Original, Derivative>>
|
||||
Convert<Original, Result<Derivative, DispatchError>> for OriginalToDerivativeConvert<R>
|
||||
{
|
||||
fn convert(a: Original) -> Result<Derivative, DispatchError> {
|
||||
R::get_derivative(&a)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `DerivativeToOriginalConvert` uses the provided [DerivativesRegistry] to
|
||||
/// convert the `Derivative` value to the `Original` one.
|
||||
pub struct DerivativeToOriginalConvert<R>(PhantomData<R>);
|
||||
impl<Original, Derivative, R: DerivativesRegistry<Original, Derivative>>
|
||||
Convert<Derivative, Result<Original, DispatchError>> for DerivativeToOriginalConvert<R>
|
||||
{
|
||||
fn convert(a: Derivative) -> Result<Original, DispatchError> {
|
||||
R::get_original(&a)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `RegisterDerivative` implements a creation operation with `DeriveAndReportId`,
|
||||
/// which takes the `Original` and derives the corresponding `Derivative`.
|
||||
///
|
||||
/// The mapping between them will be registered via the registry `R`.
|
||||
pub struct RegisterDerivative<R, CreateOp>(PhantomData<(R, CreateOp)>);
|
||||
impl<Original, Derivative, R, CreateOp> Create<DeriveAndReportId<Original, Derivative>>
|
||||
for RegisterDerivative<R, CreateOp>
|
||||
where
|
||||
Original: Clone,
|
||||
R: DerivativesRegistry<Original, Derivative>,
|
||||
CreateOp: Create<DeriveAndReportId<Original, Derivative>>,
|
||||
{
|
||||
fn create(
|
||||
id_assignment: DeriveAndReportId<Original, Derivative>,
|
||||
) -> Result<Derivative, DispatchError> {
|
||||
let original = id_assignment.params;
|
||||
let derivative = CreateOp::create(DeriveAndReportId::from(original.clone()))?;
|
||||
R::try_register_derivative(&original, &derivative)?;
|
||||
|
||||
Ok(derivative)
|
||||
}
|
||||
}
|
||||
impl<Original, Derivative, R, Config, CreateOp>
|
||||
Create<WithConfig<Config, DeriveAndReportId<Original, Derivative>>>
|
||||
for RegisterDerivative<R, CreateOp>
|
||||
where
|
||||
Original: Clone,
|
||||
R: DerivativesRegistry<Original, Derivative>,
|
||||
Config: ConfigValueMarker,
|
||||
CreateOp: Create<WithConfig<Config, DeriveAndReportId<Original, Derivative>>>,
|
||||
{
|
||||
fn create(
|
||||
strategy: WithConfig<Config, DeriveAndReportId<Original, Derivative>>,
|
||||
) -> Result<Derivative, DispatchError> {
|
||||
let WithConfig { config, extra: id_assignment } = strategy;
|
||||
let original = id_assignment.params;
|
||||
let derivative =
|
||||
CreateOp::create(WithConfig::new(config, DeriveAndReportId::from(original.clone())))?;
|
||||
R::try_register_derivative(&original, &derivative)?;
|
||||
|
||||
Ok(derivative)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator utilities for a derivatives registry.
|
||||
pub trait IterDerivativesRegistry<Original, Derivative> {
|
||||
fn iter_originals() -> impl Iterator<Item = Original>;
|
||||
|
||||
fn iter_derivatives() -> impl Iterator<Item = Derivative>;
|
||||
|
||||
fn iter() -> impl Iterator<Item = (Original, Derivative)>;
|
||||
}
|
||||
|
||||
/// Derivatives extra data.
|
||||
pub trait DerivativesExtra<Derivative, Extra> {
|
||||
fn get_derivative_extra(derivative: &Derivative) -> Option<Extra>;
|
||||
|
||||
fn set_derivative_extra(derivative: &Derivative, extra: Option<Extra>) -> DispatchResult;
|
||||
}
|
||||
|
||||
/// The `ConcatIncrementalExtra` implements a creation operation that takes a derivative.
|
||||
/// It takes the derivative's extra data and passes the tuple of the derivative and its extra data
|
||||
/// to the underlying `CreateOp` (i.e., concatenates the derivative and its extra).
|
||||
///
|
||||
/// The extra data gets incremented using the [Incrementable::increment] function, and the new extra
|
||||
/// value is set for the given derivative. The initial extra value is produced using the
|
||||
/// [Incrementable::initial_value] function.
|
||||
pub struct ConcatIncrementalExtra<Derivative, Extra, Registry, CreateOp>(
|
||||
PhantomData<(Derivative, Extra, Registry, CreateOp)>,
|
||||
);
|
||||
impl<Derivative, Extra, ReportedId, Registry, CreateOp>
|
||||
Create<DeriveAndReportId<Derivative, ReportedId>>
|
||||
for ConcatIncrementalExtra<Derivative, Extra, Registry, CreateOp>
|
||||
where
|
||||
Extra: Incrementable,
|
||||
Registry: DerivativesExtra<Derivative, Extra>,
|
||||
CreateOp: Create<DeriveAndReportId<(Derivative, Extra), ReportedId>>,
|
||||
{
|
||||
fn create(
|
||||
id_assignment: DeriveAndReportId<Derivative, ReportedId>,
|
||||
) -> Result<ReportedId, DispatchError> {
|
||||
let derivative = id_assignment.params;
|
||||
|
||||
let id = Registry::get_derivative_extra(&derivative).or(Extra::initial_value()).ok_or(
|
||||
DispatchError::Other(
|
||||
"ConcatIncrementalExtra: unable to initialize incremental derivative extra",
|
||||
),
|
||||
)?;
|
||||
let next_id = id
|
||||
.increment()
|
||||
.ok_or(DispatchError::Other("ConcatIncrementalExtra: failed to increment the id"))?;
|
||||
|
||||
Registry::set_derivative_extra(&derivative, Some(next_id))?;
|
||||
|
||||
CreateOp::create(DeriveAndReportId::from((derivative, id)))
|
||||
}
|
||||
}
|
||||
impl<Config, Derivative, Extra, ReportedId, Registry, CreateOp>
|
||||
Create<WithConfig<Config, DeriveAndReportId<Derivative, ReportedId>>>
|
||||
for ConcatIncrementalExtra<Derivative, Extra, Registry, CreateOp>
|
||||
where
|
||||
Config: ConfigValueMarker,
|
||||
Extra: Incrementable,
|
||||
Registry: DerivativesExtra<Derivative, Extra>,
|
||||
CreateOp: Create<WithConfig<Config, DeriveAndReportId<(Derivative, Extra), ReportedId>>>,
|
||||
{
|
||||
fn create(
|
||||
strategy: WithConfig<Config, DeriveAndReportId<Derivative, ReportedId>>,
|
||||
) -> Result<ReportedId, DispatchError> {
|
||||
let WithConfig { config, extra: id_assignment } = strategy;
|
||||
let derivative = id_assignment.params;
|
||||
|
||||
let id = Registry::get_derivative_extra(&derivative)
|
||||
.or(Extra::initial_value())
|
||||
.ok_or(DispatchError::Other("ConcatIncrementalExtra: no derivative extra is found"))?;
|
||||
let next_id = id
|
||||
.increment()
|
||||
.ok_or(DispatchError::Other("ConcatIncrementalExtra: failed to increment the id"))?;
|
||||
|
||||
Registry::set_derivative_extra(&derivative, Some(next_id))?;
|
||||
|
||||
CreateOp::create(WithConfig::new(config, DeriveAndReportId::from((derivative, id))))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Registry>(PhantomData<Registry>);
|
||||
impl<Registry: DerivativesRegistry<NonFungibleAsset, DerivativeId>, DerivativeId>
|
||||
MatchesInstance<DerivativeId> for MatchDerivativeInstances<Registry>
|
||||
{
|
||||
fn matches_instance(asset: &Asset) -> Result<DerivativeId, Error> {
|
||||
match asset.fun {
|
||||
Fungibility::NonFungible(asset_instance) =>
|
||||
Registry::get_derivative(&(asset.id.clone(), asset_instance))
|
||||
.map_err(|_| 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(<ClassId>), fun:
|
||||
/// NonFungible(Index(<InClassInstanceId>))`. 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<Registry, Matcher>(PhantomData<(Registry, Matcher)>);
|
||||
impl<
|
||||
Registry: DerivativesRegistry<NonFungibleAsset, DerivativeId>,
|
||||
Matcher: MatchesInstance<DerivativeId>,
|
||||
DerivativeId,
|
||||
> MatchesInstance<DerivativeId> for EnsureNotDerivativeInstance<Registry, Matcher>
|
||||
{
|
||||
fn matches_instance(asset: &Asset) -> Result<DerivativeId, Error> {
|
||||
let instance_id = Matcher::matches_instance(asset)?;
|
||||
|
||||
ensure!(Registry::get_original(&instance_id).is_err(), Error::AssetNotHandled);
|
||||
|
||||
Ok(instance_id)
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub OwnerConvertedLocationDefaultErr: DispatchError = DispatchError::Other("OwnerConvertedLocation: failed to convert the location");
|
||||
}
|
||||
|
||||
/// Converts a given `AssetId` to a `WithConfig` strategy with the owner account set to the asset's
|
||||
/// location converted to an account ID.
|
||||
pub struct OwnerConvertedLocation<CL, IdAssignment, Err = OwnerConvertedLocationDefaultErr>(
|
||||
PhantomData<(CL, IdAssignment, Err)>,
|
||||
);
|
||||
impl<AccountId, CL, Err, ReportedId>
|
||||
Convert<
|
||||
AssetId,
|
||||
Result<
|
||||
WithConfig<ConfigValue<Owner<AccountId>>, DeriveAndReportId<AssetId, ReportedId>>,
|
||||
DispatchError,
|
||||
>,
|
||||
> for OwnerConvertedLocation<CL, DeriveAndReportId<AssetId, ReportedId>, Err>
|
||||
where
|
||||
CL: ConvertLocation<AccountId>,
|
||||
Err: TypedGet,
|
||||
Err::Type: Into<DispatchError>,
|
||||
{
|
||||
fn convert(
|
||||
AssetId(location): AssetId,
|
||||
) -> Result<
|
||||
WithConfig<ConfigValue<Owner<AccountId>>, DeriveAndReportId<AssetId, ReportedId>>,
|
||||
DispatchError,
|
||||
> {
|
||||
CL::convert_location(&location)
|
||||
.map(|account| {
|
||||
WithConfig::new(ConfigValue(account), DeriveAndReportId::from(AssetId(location)))
|
||||
})
|
||||
.ok_or(Err::get().into())
|
||||
}
|
||||
}
|
||||
impl<AccountId, CL, Err, ReportedId>
|
||||
Convert<
|
||||
AssetId,
|
||||
Result<WithConfig<ConfigValue<Owner<AccountId>>, AutoId<ReportedId>>, DispatchError>,
|
||||
> for OwnerConvertedLocation<CL, AutoId<ReportedId>, Err>
|
||||
where
|
||||
CL: ConvertLocation<AccountId>,
|
||||
Err: TypedGet,
|
||||
Err::Type: Into<DispatchError>,
|
||||
{
|
||||
fn convert(
|
||||
AssetId(location): AssetId,
|
||||
) -> Result<WithConfig<ConfigValue<Owner<AccountId>>, AutoId<ReportedId>>, DispatchError> {
|
||||
CL::convert_location(&location)
|
||||
.map(|account| WithConfig::new(ConfigValue(account), AutoId::auto()))
|
||||
.ok_or(Err::get().into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub type AutoIdCollectionsInstance = unique_items::Instance2;
|
||||
pub type CollectionAutoId = u64;
|
||||
impl unique_items::Config<unique_items::Instance2> for Test {
|
||||
type ItemId = CollectionAutoId;
|
||||
}
|
||||
|
||||
// Below are the operations implementations:
|
||||
// * collection creation (with an automatically assigned ID)
|
||||
// * collection destruction
|
||||
|
||||
impl Create<WithConfig<ConfigValue<Owner<AccountId>>, AutoId<CollectionAutoId>>>
|
||||
for AutoIdCollections
|
||||
{
|
||||
fn create(
|
||||
strategy: WithConfig<ConfigValue<Owner<AccountId>>, AutoId<CollectionAutoId>>,
|
||||
) -> Result<CollectionAutoId, DispatchError> {
|
||||
let WithConfig { config: ConfigValue(owner), .. } = strategy;
|
||||
let id = unique_items::CurrentItemId::<Test, Instance2>::get().unwrap_or(0);
|
||||
|
||||
unique_items::ItemOwner::<Test, unique_items::Instance2>::insert(id, owner);
|
||||
unique_items::CurrentItemId::<Test, Instance2>::set(Some(id.saturating_add(1)));
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
impl AssetDefinition for AutoIdCollections {
|
||||
type Id = CollectionAutoId;
|
||||
}
|
||||
impl Destroy<NoParams> for AutoIdCollections {
|
||||
fn destroy(id: &Self::Id, _strategy: NoParams) -> DispatchResult {
|
||||
unique_items::ItemOwner::<Test, unique_items::Instance2>::remove(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub type PredefinedIdNftsInstance = unique_items::Instance3;
|
||||
pub type NftLocalId = u64;
|
||||
pub type NftFullId = (CollectionAutoId, NftLocalId);
|
||||
impl unique_items::Config<PredefinedIdNftsInstance> for Test {
|
||||
type ItemId = NftFullId;
|
||||
}
|
||||
|
||||
// Below are the operations implementations:
|
||||
// * NFT creation (with a predefined ID)
|
||||
// * NFT transfer from one account to another (checks if the `from` account is the NFT's current
|
||||
// owner)
|
||||
|
||||
impl Create<WithConfig<ConfigValue<Owner<AccountId>>, PredefinedId<NftFullId>>>
|
||||
for PredefinedIdNfts
|
||||
{
|
||||
fn create(
|
||||
strategy: WithConfig<ConfigValue<Owner<AccountId>>, PredefinedId<NftFullId>>,
|
||||
) -> Result<NftFullId, DispatchError> {
|
||||
let WithConfig { config: ConfigValue(owner), extra: id_assignment } = strategy;
|
||||
let id = id_assignment.params;
|
||||
|
||||
unique_items::ItemOwner::<Test, PredefinedIdNftsInstance>::try_mutate(
|
||||
id,
|
||||
|current_owner| {
|
||||
if current_owner.is_none() {
|
||||
*current_owner = Some(owner);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(unique_items::Error::<Test, PredefinedIdNftsInstance>::AlreadyExists)
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
impl AssetDefinition for PredefinedIdNfts {
|
||||
type Id = (CollectionAutoId, NftLocalId);
|
||||
}
|
||||
impl Update<ChangeOwnerFrom<AccountId>> for PredefinedIdNfts {
|
||||
fn update(
|
||||
id: &Self::Id,
|
||||
strategy: ChangeOwnerFrom<AccountId>,
|
||||
new_owner: &AccountId,
|
||||
) -> DispatchResult {
|
||||
let CheckState(check_owner, _) = strategy;
|
||||
|
||||
unique_items::ItemOwner::<Test, PredefinedIdNftsInstance>::try_mutate(id, |owner| {
|
||||
match owner {
|
||||
Some(current_owner) =>
|
||||
if *current_owner == check_owner {
|
||||
*owner = Some(*new_owner);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(unique_items::Error::<Test, PredefinedIdNftsInstance>::NoPermission
|
||||
.into())
|
||||
},
|
||||
None =>
|
||||
Err(unique_items::Error::<Test, PredefinedIdNftsInstance>::UnknownItem.into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
//! Test environment for `pezpallet-derivatives`.
|
||||
//!
|
||||
//! It contains a simple NFT-like `unique_items` pallet that emulate both NFT collections and their
|
||||
//! tokens (depending on the pallet instance). This test pallet is instatiated three times in the
|
||||
//! test environment to cover the usage scenarios of `pezpallet-derivatives` described in it's crate
|
||||
//! doc comment.
|
||||
//!
|
||||
//! * The first instance, called `PredefinedIdCollections`, emulates NFT collections that are
|
||||
//! created with a predefined ID.
|
||||
//! The ID is set to XCM `AssetId`, so a derivative collection can be created directly using the
|
||||
//! foreign collection's ID. This pallet instance illustrates and tests the `pezpallet-derivatives`
|
||||
//! usage scenario #1 (i.e., when no suitable way of directly creating a derivative collection is
|
||||
//! provided by the hosting pallet). The configuration of this instance can be found in the
|
||||
//! [predefined_id_collections] module. The corresponding `pezpallet-derivatives` instance is called
|
||||
//! `PredefinedIdDerivativeCollections`.
|
||||
//!
|
||||
//! * The second instance, called `AutoIdCollections`, emulates NFT collections that are created
|
||||
//! with an automatically assigned ID (e.g., an incremental one).
|
||||
//! The ID is set to `u64`, so a mapping between the foreign collection's ID and the derivative
|
||||
//! collection ID is needed. This pallet instance illustrates and tests the `pezpallet-derivatives`
|
||||
//! usage scenario #2 combined with scenario #1 (since we also test manual collection creation and
|
||||
//! destruction). The configuration of this instance can be found in the [auto_id_collections]
|
||||
//! module. The corresponding `pezpallet-derivatives` instance is called `AutoIdDerivativeCollections`.
|
||||
//!
|
||||
//! * The third instance, called `PredefinedIdNfts`, emulates non-fungible tokens within collections
|
||||
//! from the pallet's second instance.
|
||||
//! The full NFT ID is a tuple consisting of the collection ID and a token ID, both of which are of
|
||||
//! the `u64` type. Since a foreign NFT is identified by `(AssetId, AssetInstance)`, we need the
|
||||
//! mapping between it and the derivative NFT ID. This pallet instance illustrates and tests the
|
||||
//! `pezpallet-derivatives` usage scenario #2 without scenario #1 (the manual creation and destruction
|
||||
//! of derivative NFTs is forbidden). The configuration of this instance can be found in the
|
||||
//! [auto_id_nfts] module. The corresponding `pezpallet-derivatives` instance is called
|
||||
//! `DerivativeNfts`. Additionally, there is an example of asset transactors dealing with derivative
|
||||
//! NFTs implemented using `pezpallet-derivatives`, which includes original-to-derivative ID mapping.
|
||||
|
||||
use super::*;
|
||||
use crate as pezpallet_derivatives;
|
||||
|
||||
use pezframe_support::{
|
||||
construct_runtime, derive_impl, parameter_types,
|
||||
traits::{
|
||||
tokens::asset_ops::{common_ops::*, common_strategies::*, *},
|
||||
ContainsPair, Everything, Nothing, PalletInfoAccess,
|
||||
},
|
||||
};
|
||||
use pezframe_system::{EnsureNever, EnsureRoot, EnsureSigned};
|
||||
use pezsp_runtime::{traits::TryConvertInto, BuildStorage};
|
||||
use xcm::prelude::*;
|
||||
use xcm_builder::{
|
||||
unique_instances::{
|
||||
ExtractAssetId, NonFungibleAsset, UniqueInstancesAdapter, UniqueInstancesDepositAdapter,
|
||||
},
|
||||
AllowUnpaidExecutionFrom, AsPrefixedGeneralIndex, FixedWeightBounds, MatchInClassInstances,
|
||||
MatchedConvertedConcreteId, StartsWith,
|
||||
};
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
mod auto_id_collections;
|
||||
mod auto_id_nfts;
|
||||
mod predefined_id_collections;
|
||||
|
||||
pub use auto_id_collections::*;
|
||||
pub use auto_id_nfts::*;
|
||||
pub use predefined_id_collections::*;
|
||||
|
||||
type AccountId = u64;
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
type Balance = u64;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod unique_items {
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config<I: 'static = ()>: pezframe_system::Config {
|
||||
type ItemId: Member + Parameter + MaxEncodedLen + TypeInfo;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T, I = ()>(_);
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T, I = ()> {
|
||||
AlreadyExists,
|
||||
NoPermission,
|
||||
UnknownItem,
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {}
|
||||
|
||||
#[pallet::storage]
|
||||
pub type CurrentItemId<T: Config<I>, I: 'static = ()> = StorageValue<_, T::ItemId, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
pub type ItemOwner<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Blake2_128Concat, T::ItemId, T::AccountId, OptionQuery>;
|
||||
}
|
||||
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
|
||||
PredefinedIdCollections: unique_items::<Instance1>,
|
||||
PredefinedIdDerivativeCollections: pezpallet_derivatives::<Instance1>,
|
||||
|
||||
AutoIdCollections: unique_items::<Instance2>,
|
||||
AutoIdDerivativeCollections: pezpallet_derivatives::<Instance2>,
|
||||
|
||||
PredefinedIdNfts: unique_items::<Instance3>,
|
||||
DerivativeNfts: pezpallet_derivatives::<Instance3>,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Test {
|
||||
type AccountId = AccountId;
|
||||
type Block = Block;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
// XCM configuration parameters.
|
||||
parameter_types! {
|
||||
pub const DeliveryFees: u128 = 20; // Random value.
|
||||
pub const ExistentialDeposit: u128 = 1; // Random value.
|
||||
pub const BaseXcmWeight: Weight = Weight::from_parts(100, 10); // Random value.
|
||||
pub UniversalLocation: InteriorLocation = [GlobalConsensus(NetworkId::ByGenesis([0; 32])), Teyrchain(2000)].into();
|
||||
pub const HereLocation: Location = Location::here();
|
||||
pub const RelayLocation: Location = Location::parent();
|
||||
pub const MaxAssetsIntoHolding: u32 = 64;
|
||||
pub const AnyNetwork: Option<NetworkId> = None;
|
||||
pub LocalNftsPalletLocation: Location = PalletInstance(<PredefinedIdNfts as PalletInfoAccess>::index() as u8).into();
|
||||
pub StashAccountId: AccountId = u64::MAX;
|
||||
}
|
||||
|
||||
/// It is an `IsReserve` that returns true only if the given asset's location begins with a sibling
|
||||
/// teyrchain junction which equals the given origin.
|
||||
pub struct TrustAssetsFromSiblings;
|
||||
impl ContainsPair<Asset, Location> for TrustAssetsFromSiblings {
|
||||
fn contains(asset: &Asset, origin: &Location) -> bool {
|
||||
let AssetId(asset_location) = &asset.id;
|
||||
|
||||
match (asset_location.unpack(), origin.unpack()) {
|
||||
((1, [Teyrchain(asset_para_id), ..]), (1, [Teyrchain(origin_para_id)]))
|
||||
if asset_para_id == origin_para_id =>
|
||||
true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts locations that are only the `AccountIndex64` junction into local u64 accounts.
|
||||
pub struct AccountIndex64Aliases<Network, AccountId>(PhantomData<(Network, AccountId)>);
|
||||
impl<Network: Get<Option<NetworkId>>, AccountId: From<u64>> ConvertLocation<AccountId>
|
||||
for AccountIndex64Aliases<Network, AccountId>
|
||||
{
|
||||
fn convert_location(location: &Location) -> Option<AccountId> {
|
||||
let index = match location.unpack() {
|
||||
(0, [AccountIndex64 { index, network: None }]) => index,
|
||||
(0, [AccountIndex64 { index, network }]) if *network == Network::get() => index,
|
||||
_ => return None,
|
||||
};
|
||||
Some((*index).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom location converter to turn sibling chains into u64 accounts.
|
||||
pub struct SiblingChainToIndex64;
|
||||
impl ConvertLocation<AccountId> for SiblingChainToIndex64 {
|
||||
fn convert_location(location: &Location) -> Option<AccountId> {
|
||||
let index = match location.unpack() {
|
||||
(1, [Teyrchain(id)]) => id,
|
||||
_ => return None,
|
||||
};
|
||||
Some((*index).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// XCM executor's Location-to-AccountId converter.
|
||||
pub type LocationToAccountId = (AccountIndex64Aliases<AnyNetwork, u64>, SiblingChainToIndex64);
|
||||
|
||||
/// Converts an asset's location to the corresponding sibling teyrchain sovereign account.
|
||||
pub struct SiblingAssetToReserveLocationConvert;
|
||||
impl ConvertLocation<AccountId> for SiblingAssetToReserveLocationConvert {
|
||||
fn convert_location(location: &Location) -> Option<AccountId> {
|
||||
match location.unpack() {
|
||||
(1, [Teyrchain(para_id), ..]) =>
|
||||
LocationToAccountId::convert_location(&Location::new(1, Teyrchain(*para_id))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new derivative using the provided `IdAssignment` (can be a predefined ID or a derived
|
||||
/// one) and `CreateOp`. The `IdAssignment` must take XCM `AssetId` as a parameter.
|
||||
///
|
||||
/// The derivative will be created using the `WithConfig` strategy with the owner account set to the
|
||||
/// original asset's reserve location.
|
||||
///
|
||||
/// The `InvalidAssetErr` is the error that must be returned if getting the reserve location's
|
||||
/// sovereign account fails.
|
||||
pub type CreateDerivativeOwnedBySovAcc<IdAssignment, CreateOp, InvalidAssetErr> =
|
||||
DeriveStrategyThenCreate<
|
||||
// Derived strategy: assign the derivative owner.
|
||||
WithConfig<ConfigValue<Owner<AccountId>>, IdAssignment>,
|
||||
// Converts the provided XCM `AssetId` (original ID) to the reserve location's sovereign
|
||||
// account, and returns the specified strategy above.
|
||||
OwnerConvertedLocation<SiblingAssetToReserveLocationConvert, IdAssignment, InvalidAssetErr>,
|
||||
CreateOp,
|
||||
>;
|
||||
|
||||
/// The `pezpallet-derivatives` instance corresponding to the `PredefinedIdCollections` instance of the
|
||||
/// `unique_items` mock pallet.
|
||||
pub type PredefinedIdDerivativeCollectionsInstance = pezpallet_derivatives::Instance1;
|
||||
impl pezpallet_derivatives::Config<PredefinedIdDerivativeCollectionsInstance> for Test {
|
||||
type WeightInfo = pezpallet_derivatives::TestWeightInfo;
|
||||
|
||||
type Original = AssetId;
|
||||
type Derivative = AssetId;
|
||||
|
||||
type DerivativeExtra = ();
|
||||
|
||||
type CreateOrigin = EnsureSigned<AccountId>;
|
||||
|
||||
// `NoStoredMapping` tells the pallet not to store the mapping between the `Original` and the
|
||||
// `Derivative`
|
||||
type CreateOp = pezpallet_derivatives::NoStoredMapping<
|
||||
CreateDerivativeOwnedBySovAcc<
|
||||
PredefinedId<AssetId>,
|
||||
PredefinedIdCollections,
|
||||
pezpallet_derivatives::InvalidAssetError<PredefinedIdDerivativeCollections>,
|
||||
>,
|
||||
>;
|
||||
|
||||
type DestroyOrigin = EnsureRoot<AccountId>;
|
||||
type DestroyOp = PredefinedIdCollections;
|
||||
}
|
||||
|
||||
/// The `pezpallet-derivatives` instance corresponding to the `AutoIdCollections` instance of the
|
||||
/// `unique_items` mock pallet.
|
||||
pub type AutoIdDerivativeCollectionsInstance = pezpallet_derivatives::Instance2;
|
||||
impl pezpallet_derivatives::Config<AutoIdDerivativeCollectionsInstance> for Test {
|
||||
type WeightInfo = pezpallet_derivatives::TestWeightInfo;
|
||||
|
||||
type Original = AssetId;
|
||||
type Derivative = CollectionAutoId;
|
||||
|
||||
// The current in-collection derivative token ID to use when creating a new derivative NFT.
|
||||
type DerivativeExtra = NftLocalId;
|
||||
|
||||
type CreateOrigin = EnsureSigned<AccountId>;
|
||||
|
||||
// `StoreMapping` tells the pallet to store the mapping between the `Original` and the
|
||||
// `Derivative`
|
||||
type CreateOp = pezpallet_derivatives::StoreMapping<
|
||||
CreateDerivativeOwnedBySovAcc<
|
||||
AutoId<Self::Derivative>,
|
||||
AutoIdCollections,
|
||||
pezpallet_derivatives::InvalidAssetError<AutoIdDerivativeCollections>,
|
||||
>,
|
||||
>;
|
||||
|
||||
type DestroyOrigin = EnsureRoot<AccountId>;
|
||||
|
||||
// The `AutoIdCollections` uses `Derivative` as an ID type.
|
||||
// But the `destroy_derivative` extrinsic takes the ID parameter of type `Original`.
|
||||
//
|
||||
// We use `MapId` here to map the `Original` value to the corresponding `Derivative` one
|
||||
// using the `OriginalToDerivativeConvert`, which uses the stored mapping between them.
|
||||
type DestroyOp = MapId<
|
||||
Self::Original,
|
||||
Self::Derivative,
|
||||
OriginalToDerivativeConvert<AutoIdDerivativeCollections>,
|
||||
AutoIdCollections,
|
||||
>;
|
||||
}
|
||||
|
||||
/// The `pezpallet-derivatives` instance corresponding to the `PredefinedIdNfts` instance of the
|
||||
/// `unique_items` mock pallet.
|
||||
pub type DerivativeNftsInstance = pezpallet_derivatives::Instance3;
|
||||
impl pezpallet_derivatives::Config<DerivativeNftsInstance> for Test {
|
||||
type WeightInfo = pezpallet_derivatives::TestWeightInfo;
|
||||
|
||||
type Original = NonFungibleAsset;
|
||||
type Derivative = NftFullId;
|
||||
|
||||
type DerivativeExtra = ();
|
||||
|
||||
// The derivative NFTs can't be manually created.
|
||||
type CreateOrigin = EnsureNever<AccountId>;
|
||||
type CreateOp = DisabledOps<Self::Original>;
|
||||
|
||||
// The derivative NFTs can't be manually destroyed.
|
||||
type DestroyOrigin = EnsureNever<AccountId>;
|
||||
type DestroyOp = DisabledOps<Self::Original>;
|
||||
}
|
||||
|
||||
/// Matches NFTs within the `PredefinedIdNfts` pallet.
|
||||
/// These NFTs are considered "local" since they are minted on this chain.
|
||||
pub type LocalNftsMatcher = MatchInClassInstances<
|
||||
MatchedConvertedConcreteId<
|
||||
CollectionAutoId,
|
||||
NftLocalId,
|
||||
StartsWith<LocalNftsPalletLocation>,
|
||||
AsPrefixedGeneralIndex<LocalNftsPalletLocation, CollectionAutoId, TryConvertInto>,
|
||||
TryConvertInto,
|
||||
>,
|
||||
>;
|
||||
|
||||
/// This asset transactor deals with local NFTs that aren't derivatives.
|
||||
///
|
||||
/// The derivative NFTs are excluded to avoid acting like a reserve chain for assets that came from
|
||||
/// other chains.
|
||||
pub type LocalNftsTransactor = UniqueInstancesAdapter<
|
||||
AccountId,
|
||||
LocationToAccountId,
|
||||
// The `EnsureNotDerivativeInstance` uses the `DerivativeNfts` stored mapping
|
||||
// to prevent derivative NFTs from being matched.
|
||||
EnsureNotDerivativeInstance<DerivativeNfts, LocalNftsMatcher>,
|
||||
// The `StashAccountAssetOps` adds the `Stash` and `Restore` operations
|
||||
// to the `PredefinedIdNfts` by utilizing the NFT transfer to and from the `StashAccountId`
|
||||
// correspondingly.
|
||||
StashAccountAssetOps<StashAccountId, PredefinedIdNfts>,
|
||||
>;
|
||||
|
||||
/// This asset transactor deals with already registered derivative NFTs.
|
||||
pub type RegisteredDerivativeNftsTransactor = UniqueInstancesAdapter<
|
||||
AccountId,
|
||||
LocationToAccountId,
|
||||
// Matches derivative NFTs using the `DerivativeNfts` stored mapping.
|
||||
MatchDerivativeInstances<DerivativeNfts>,
|
||||
// The `StashAccountAssetOps` adds the `Stash` and `Restore` operations
|
||||
// to the `PredefinedIdNfts` utilizing the NFT transfer to and from the `StashAccountId`
|
||||
// correspondingly.
|
||||
StashAccountAssetOps<StashAccountId, PredefinedIdNfts>,
|
||||
>;
|
||||
|
||||
/// Takes `(AssetId, AssetInstance)` to create a new NFT while the underlying `CreateOp` accepts
|
||||
/// only the `AssetId`.
|
||||
///
|
||||
/// Extracts `AssetId` from the tuple and passes it to `CreateOp`.
|
||||
pub type CreateUsingXcmNft<CreateOp> = MapId<NonFungibleAsset, AssetId, ExtractAssetId, CreateOp>;
|
||||
|
||||
/// Takes `AssetId` to create a new NFT while the underlying `CreateOp` accepts `CollectionAutoId`.
|
||||
///
|
||||
/// Maps the provided `AssetId` to `CollectionAutoId` using the stored mapping in the
|
||||
/// `AutoIdDerivativeCollections`. Passes the mapped ID to `CreateOp`.
|
||||
pub type CreateUsingXcmAssetId<CreateOp> = MapId<
|
||||
AssetId,
|
||||
CollectionAutoId,
|
||||
OriginalToDerivativeConvert<AutoIdDerivativeCollections>,
|
||||
CreateOp,
|
||||
>;
|
||||
|
||||
/// * Takes `CollectionAutoId` (the NFT derivative collection ID)
|
||||
/// * Gets the associated extra data (current in-collection token ID) using the
|
||||
/// `AutoIdDerivativeCollections`,
|
||||
/// * Makes the `FullNftId` using the collection ID and current token ID,
|
||||
/// * Creates a derivative NFT using the `FullNftId`
|
||||
/// * Increments the token ID and sets it as the derivative collection's extra data.
|
||||
pub type DeriveNft = ConcatIncrementalExtra<
|
||||
CollectionAutoId,
|
||||
NftLocalId,
|
||||
AutoIdDerivativeCollections,
|
||||
PredefinedIdNfts,
|
||||
>;
|
||||
|
||||
/// Create a derivative NFT using `(AssetId, AssetInstance)`.
|
||||
pub type CreateDerivativeNft = CreateUsingXcmNft<CreateUsingXcmAssetId<DeriveNft>>;
|
||||
|
||||
/// This asset transactor registers a new derivative NFT.
|
||||
pub type DerivativeNftsRegistrar = UniqueInstancesDepositAdapter<
|
||||
AccountId,
|
||||
LocationToAccountId,
|
||||
NftFullId,
|
||||
// Creates a new NFT using the `CreateDerivativeNft`,
|
||||
// and stores the mapping between the XCM NFT ID and the derivative ID in the `DerivativeNfts`
|
||||
// registry.
|
||||
RegisterDerivative<DerivativeNfts, CreateDerivativeNft>,
|
||||
>;
|
||||
|
||||
pub type AssetTransactors =
|
||||
(LocalNftsTransactor, RegisteredDerivativeNftsTransactor, DerivativeNftsRegistrar);
|
||||
|
||||
pub struct XcmConfig;
|
||||
impl xcm_executor::Config for XcmConfig {
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type XcmSender = ();
|
||||
type XcmEventEmitter = ();
|
||||
type AssetTransactor = AssetTransactors;
|
||||
type OriginConverter = ();
|
||||
type IsReserve = TrustAssetsFromSiblings;
|
||||
type IsTeleporter = ();
|
||||
type UniversalLocation = UniversalLocation;
|
||||
type Barrier = AllowUnpaidExecutionFrom<Everything>;
|
||||
type Weigher = FixedWeightBounds<BaseXcmWeight, RuntimeCall, ConstU32<100>>;
|
||||
type Trader = ();
|
||||
type ResponseHandler = ();
|
||||
type AssetTrap = ();
|
||||
type AssetLocker = ();
|
||||
type AssetExchanger = ();
|
||||
type AssetClaims = ();
|
||||
type SubscriptionService = ();
|
||||
type PalletInstancesInfo = AllPalletsWithSystem;
|
||||
type MaxAssetsIntoHolding = MaxAssetsIntoHolding;
|
||||
type FeeManager = ();
|
||||
type MessageExporter = ();
|
||||
type UniversalAliases = ();
|
||||
type CallDispatcher = RuntimeCall;
|
||||
type SafeCallFilter = Nothing;
|
||||
type Aliasers = Nothing;
|
||||
type TransactionalProcessor = ();
|
||||
type HrmpNewChannelOpenRequestHandler = ();
|
||||
type HrmpChannelAcceptedHandler = ();
|
||||
type HrmpChannelClosingHandler = ();
|
||||
type XcmRecorder = ();
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
pezsp_io::TestExternalities::new(t)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub type PredefinedIdCollectionsInstance = unique_items::Instance1;
|
||||
impl unique_items::Config<PredefinedIdCollectionsInstance> for Test {
|
||||
type ItemId = AssetId;
|
||||
}
|
||||
|
||||
// Below are the operations implementations:
|
||||
// * collection creation (with a predefined ID)
|
||||
// * collection destruction
|
||||
|
||||
impl Create<WithConfig<ConfigValue<Owner<AccountId>>, PredefinedId<AssetId>>>
|
||||
for PredefinedIdCollections
|
||||
{
|
||||
fn create(
|
||||
strategy: WithConfig<ConfigValue<Owner<AccountId>>, PredefinedId<AssetId>>,
|
||||
) -> Result<AssetId, DispatchError> {
|
||||
let WithConfig { config: ConfigValue(owner), extra: id_assignment } = strategy;
|
||||
let id = id_assignment.params;
|
||||
|
||||
unique_items::ItemOwner::<Test, PredefinedIdCollectionsInstance>::try_mutate(
|
||||
id.clone(),
|
||||
|current_owner| {
|
||||
if current_owner.is_none() {
|
||||
*current_owner = Some(owner);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(unique_items::Error::<Test, PredefinedIdCollectionsInstance>::AlreadyExists)
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
impl AssetDefinition for PredefinedIdCollections {
|
||||
type Id = AssetId;
|
||||
}
|
||||
impl Destroy<NoParams> for PredefinedIdCollections {
|
||||
fn destroy(id: &Self::Id, _strategy: NoParams) -> DispatchResult {
|
||||
unique_items::ItemOwner::<Test, PredefinedIdCollectionsInstance>::remove(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
use super::*;
|
||||
use crate as pezpallet_derivatives;
|
||||
use pezframe_support::{assert_err, assert_ok, traits::tokens::asset_ops::common_strategies::*};
|
||||
use mock::*;
|
||||
|
||||
use xcm::prelude::*;
|
||||
use xcm_executor::XcmExecutor;
|
||||
|
||||
#[test]
|
||||
fn predefined_id_collection() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = AssetId(Location::new(1, [Teyrchain(1111), PalletInstance(42), GeneralIndex(1)]));
|
||||
|
||||
// An invalid origin is rejected.
|
||||
assert_err!(
|
||||
PredefinedIdDerivativeCollections::create_derivative(RuntimeOrigin::none(), id.clone()),
|
||||
DispatchError::BadOrigin,
|
||||
);
|
||||
|
||||
assert_ok!(PredefinedIdDerivativeCollections::create_derivative(
|
||||
RuntimeOrigin::signed(1),
|
||||
id.clone()
|
||||
));
|
||||
|
||||
// EnsureDerivativeCreateOrigin yielded a strategy to assign the item's owner to the
|
||||
// teyrchain's sovereign account.
|
||||
let owner =
|
||||
unique_items::ItemOwner::<Test, PredefinedIdCollectionsInstance>::get(&id).unwrap();
|
||||
|
||||
assert_eq!(owner, 1111);
|
||||
|
||||
// The inner errors are propagated
|
||||
assert_err!(
|
||||
PredefinedIdDerivativeCollections::create_derivative(
|
||||
RuntimeOrigin::signed(2),
|
||||
id.clone()
|
||||
),
|
||||
unique_items::Error::<Test, PredefinedIdCollectionsInstance>::AlreadyExists,
|
||||
);
|
||||
|
||||
// An invalid origin is rejected.
|
||||
assert_err!(
|
||||
PredefinedIdDerivativeCollections::destroy_derivative(
|
||||
RuntimeOrigin::signed(1),
|
||||
id.clone()
|
||||
),
|
||||
DispatchError::BadOrigin,
|
||||
);
|
||||
|
||||
assert_ok!(PredefinedIdDerivativeCollections::destroy_derivative(
|
||||
RuntimeOrigin::root(),
|
||||
id.clone()
|
||||
));
|
||||
assert!(
|
||||
unique_items::ItemOwner::<Test, PredefinedIdCollectionsInstance>::get(&id).is_none()
|
||||
);
|
||||
|
||||
// Only the assets that have the reserve location convertible to an account
|
||||
// cna be registered as derivatives
|
||||
let invalid_id = AssetId(Location::new(0, [PalletInstance(42), GeneralIndex(1)]));
|
||||
assert_err!(
|
||||
PredefinedIdDerivativeCollections::create_derivative(RuntimeOrigin::signed(3), invalid_id),
|
||||
pezpallet_derivatives::Error::<Test, PredefinedIdDerivativeCollectionsInstance>::InvalidAsset,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_id_collection() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id_a =
|
||||
AssetId(Location::new(1, [Teyrchain(2222), PalletInstance(42), GeneralIndex(1)]));
|
||||
let id_b =
|
||||
AssetId(Location::new(1, [Teyrchain(3333), PalletInstance(42), GeneralIndex(2)]));
|
||||
|
||||
// An invalid origin is rejected.
|
||||
assert_err!(
|
||||
AutoIdDerivativeCollections::create_derivative(RuntimeOrigin::none(), id_a.clone()),
|
||||
DispatchError::BadOrigin,
|
||||
);
|
||||
|
||||
assert_ok!(AutoIdDerivativeCollections::create_derivative(
|
||||
RuntimeOrigin::signed(3),
|
||||
id_a.clone()
|
||||
));
|
||||
|
||||
let derivative_id_a = AutoIdDerivativeCollections::get_derivative(&id_a).unwrap();
|
||||
|
||||
// EnsureDerivativeCreateOrigin yielded a strategy to assign the item's owner to the
|
||||
// teyrchain's sovereign account.
|
||||
let owner_a =
|
||||
unique_items::ItemOwner::<Test, AutoIdCollectionsInstance>::get(&derivative_id_a)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(owner_a, 2222);
|
||||
|
||||
// The stored mapping prevents derivative duplication
|
||||
assert_err!(
|
||||
AutoIdDerivativeCollections::create_derivative(RuntimeOrigin::signed(4), id_a.clone()),
|
||||
pezpallet_derivatives::Error::<Test, AutoIdCollectionsInstance>::DerivativeAlreadyExists,
|
||||
);
|
||||
|
||||
assert_ok!(AutoIdDerivativeCollections::create_derivative(
|
||||
RuntimeOrigin::signed(5),
|
||||
id_b.clone()
|
||||
));
|
||||
|
||||
let derivative_id_b = AutoIdDerivativeCollections::get_derivative(&id_b).unwrap();
|
||||
|
||||
assert_ne!(derivative_id_a, derivative_id_b);
|
||||
|
||||
// EnsureDerivativeCreateOrigin yielded a strategy to assign the item's owner to the
|
||||
// teyrchain's sovereign account.
|
||||
let owner_b =
|
||||
unique_items::ItemOwner::<Test, AutoIdCollectionsInstance>::get(&derivative_id_b)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(owner_b, 3333);
|
||||
|
||||
// The stored mapping prevents derivative duplication
|
||||
assert_err!(
|
||||
AutoIdDerivativeCollections::create_derivative(RuntimeOrigin::signed(6), id_b.clone()),
|
||||
pezpallet_derivatives::Error::<Test, AutoIdCollectionsInstance>::DerivativeAlreadyExists,
|
||||
);
|
||||
|
||||
// An invalid origin is rejected.
|
||||
assert_err!(
|
||||
AutoIdDerivativeCollections::destroy_derivative(RuntimeOrigin::signed(7), id_a.clone()),
|
||||
DispatchError::BadOrigin,
|
||||
);
|
||||
|
||||
assert_ok!(AutoIdDerivativeCollections::destroy_derivative(
|
||||
RuntimeOrigin::root(),
|
||||
id_a.clone()
|
||||
));
|
||||
assert!(unique_items::ItemOwner::<Test, AutoIdCollectionsInstance>::get(&derivative_id_a)
|
||||
.is_none());
|
||||
|
||||
assert_ok!(AutoIdDerivativeCollections::destroy_derivative(
|
||||
RuntimeOrigin::root(),
|
||||
id_b.clone()
|
||||
));
|
||||
assert!(unique_items::ItemOwner::<Test, AutoIdCollectionsInstance>::get(&derivative_id_b)
|
||||
.is_none());
|
||||
|
||||
// Only the assets that have the reserve location convertible to an account
|
||||
// cna be registered as derivatives
|
||||
let invalid_id = AssetId(Location::new(0, [PalletInstance(42), GeneralIndex(1)]));
|
||||
assert_err!(
|
||||
AutoIdDerivativeCollections::create_derivative(RuntimeOrigin::signed(8), invalid_id),
|
||||
pezpallet_derivatives::Error::<Test, AutoIdDerivativeCollectionsInstance>::InvalidAsset,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_nfts() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let collection_owner = 1;
|
||||
let nft_initial_owner = 2;
|
||||
let nft_beneficiary = 3;
|
||||
|
||||
// Create NFT collection
|
||||
let collection_id = AutoIdCollections::create(WithConfig::new(
|
||||
ConfigValue(collection_owner),
|
||||
AutoId::auto(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// Mint NFT within the collection
|
||||
let nft_local_id = 112;
|
||||
assert_ok!(PredefinedIdNfts::create(WithConfig::new(
|
||||
ConfigValue(nft_initial_owner),
|
||||
PredefinedId::from((collection_id, nft_local_id)),
|
||||
)));
|
||||
|
||||
// The NFT should be deposited to the correct account
|
||||
assert_eq!(
|
||||
unique_items::ItemOwner::<Test, PredefinedIdNftsInstance>::get(&(
|
||||
collection_id,
|
||||
nft_local_id
|
||||
)),
|
||||
Some(nft_initial_owner),
|
||||
);
|
||||
|
||||
let local_nfts_pallet_index = <PredefinedIdNfts as PalletInfoAccess>::index() as u8;
|
||||
let nft_asset: Asset = (
|
||||
(PalletInstance(local_nfts_pallet_index), GeneralIndex(collection_id.into())),
|
||||
Index(nft_local_id.into()),
|
||||
)
|
||||
.into();
|
||||
let nft_beneficiary_location = AccountIndex64 { index: nft_beneficiary, network: None };
|
||||
|
||||
let message = Xcm::builder_unpaid()
|
||||
.unpaid_execution(Unlimited, None)
|
||||
.withdraw_asset(nft_asset)
|
||||
.deposit_asset(AllCounted(1), nft_beneficiary_location)
|
||||
.build();
|
||||
|
||||
let origin: Location = AccountIndex64 { index: nft_initial_owner, network: None }.into();
|
||||
let mut hash = message.using_encoded(pezsp_io::hashing::blake2_256);
|
||||
|
||||
// Transfer the NFT from one account to another
|
||||
// This NFT is local, it should be handled by the LocalNftsTransactor
|
||||
let outcome = XcmExecutor::<XcmConfig>::prepare_and_execute(
|
||||
origin,
|
||||
message,
|
||||
&mut hash,
|
||||
Weight::MAX,
|
||||
Weight::zero(),
|
||||
);
|
||||
assert_ok!(outcome.ensure_complete());
|
||||
|
||||
// The NFT should be deposited to the correct beneficiary
|
||||
assert_eq!(
|
||||
unique_items::ItemOwner::<Test, PredefinedIdNftsInstance>::get(&(
|
||||
collection_id,
|
||||
nft_local_id
|
||||
)),
|
||||
Some(nft_beneficiary),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derivative_nfts() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let foreign_para_id = 2222;
|
||||
|
||||
// Create derivative NFT collection
|
||||
let foreign_collection_id = AssetId(Location::new(
|
||||
1,
|
||||
[Teyrchain(foreign_para_id), PalletInstance(42), GeneralIndex(1)],
|
||||
));
|
||||
let foreign_nft_id = Index(112);
|
||||
assert_ok!(AutoIdDerivativeCollections::create_derivative(
|
||||
RuntimeOrigin::signed(1),
|
||||
foreign_collection_id.clone(),
|
||||
));
|
||||
|
||||
let derivative_collection_id =
|
||||
AutoIdDerivativeCollections::get_derivative(&foreign_collection_id).unwrap();
|
||||
|
||||
// There is no derivative NFT yet
|
||||
assert!(DerivativeNfts::get_derivative(&(foreign_collection_id.clone(), foreign_nft_id))
|
||||
.is_err());
|
||||
|
||||
let nft_beneficiary = 1;
|
||||
|
||||
let nft_asset: Asset = (foreign_collection_id.clone(), foreign_nft_id).into();
|
||||
let nft_beneficiary_location = AccountIndex64 { index: nft_beneficiary, network: None };
|
||||
|
||||
let deposited_assets: Assets = nft_asset.clone().into();
|
||||
let message = Xcm::builder_unpaid()
|
||||
.unpaid_execution(Unlimited, None)
|
||||
.reserve_asset_deposited(deposited_assets)
|
||||
.deposit_asset(AllCounted(1), nft_beneficiary_location)
|
||||
.build();
|
||||
|
||||
let origin = Location::new(1, [Teyrchain(foreign_para_id)]);
|
||||
let mut hash = message.using_encoded(pezsp_io::hashing::blake2_256);
|
||||
|
||||
// Deposit a foreign NFT (i.e., create a derivative NFT)
|
||||
let outcome = XcmExecutor::<XcmConfig>::prepare_and_execute(
|
||||
origin,
|
||||
message,
|
||||
&mut hash,
|
||||
Weight::MAX,
|
||||
Weight::zero(),
|
||||
);
|
||||
assert_ok!(outcome.ensure_complete());
|
||||
|
||||
// The derivative NFT should exist now
|
||||
let derivative_full_nft_id =
|
||||
DerivativeNfts::get_derivative(&(foreign_collection_id.clone(), foreign_nft_id))
|
||||
.unwrap();
|
||||
|
||||
// The derivative NFT should be deposited to the correct beneficiary
|
||||
assert_eq!(
|
||||
unique_items::ItemOwner::<Test, PredefinedIdNftsInstance>::get(&derivative_full_nft_id),
|
||||
Some(nft_beneficiary),
|
||||
);
|
||||
// The derivative NFT is deposited within the correct collection
|
||||
assert_eq!(derivative_collection_id, derivative_full_nft_id.0);
|
||||
|
||||
let derivative_local_nft_id = derivative_full_nft_id.1;
|
||||
|
||||
let nft_owner = nft_beneficiary;
|
||||
let another_nft_beneficiary = nft_beneficiary + 1;
|
||||
|
||||
let local_nfts_pallet_index = <PredefinedIdNfts as PalletInfoAccess>::index() as u8;
|
||||
let nft_asset_as_local: Asset = (
|
||||
(
|
||||
PalletInstance(local_nfts_pallet_index),
|
||||
GeneralIndex(derivative_collection_id.into()),
|
||||
),
|
||||
Index(derivative_local_nft_id.into()),
|
||||
)
|
||||
.into();
|
||||
let another_nft_beneficiary_location =
|
||||
AccountIndex64 { index: another_nft_beneficiary, network: None };
|
||||
let message = Xcm::builder_unpaid()
|
||||
.unpaid_execution(Unlimited, None)
|
||||
.withdraw_asset(nft_asset_as_local)
|
||||
.deposit_asset(AllCounted(1), another_nft_beneficiary_location)
|
||||
.build();
|
||||
|
||||
let origin: Location = AccountIndex64 { index: nft_owner, network: None }.into();
|
||||
let mut hash = message.using_encoded(pezsp_io::hashing::blake2_256);
|
||||
|
||||
// Try to transfer the derivative NFT as if it were a local one
|
||||
// (this might lead the chain to act as a reserve location for NFTs which doesn't belong to
|
||||
// it).
|
||||
//
|
||||
// The LocalNftsTransactor should prevent this as it checks the NFT for being
|
||||
// non-derivative.
|
||||
let outcome = XcmExecutor::<XcmConfig>::prepare_and_execute(
|
||||
origin,
|
||||
message,
|
||||
&mut hash,
|
||||
Weight::MAX,
|
||||
Weight::zero(),
|
||||
);
|
||||
assert_err!(
|
||||
outcome.ensure_complete(),
|
||||
InstructionError { index: 1, error: XcmError::AssetNotFound }
|
||||
);
|
||||
|
||||
let message = Xcm::builder_unpaid()
|
||||
.unpaid_execution(Unlimited, None)
|
||||
.withdraw_asset(nft_asset)
|
||||
.deposit_asset(AllCounted(1), another_nft_beneficiary_location)
|
||||
.build();
|
||||
let origin: Location = AccountIndex64 { index: nft_owner, network: None }.into();
|
||||
let mut hash = message.using_encoded(pezsp_io::hashing::blake2_256);
|
||||
|
||||
// Transfer the derivative NFT from one account to another
|
||||
let outcome = XcmExecutor::<XcmConfig>::prepare_and_execute(
|
||||
origin,
|
||||
message,
|
||||
&mut hash,
|
||||
Weight::MAX,
|
||||
Weight::zero(),
|
||||
);
|
||||
assert_ok!(outcome.ensure_complete());
|
||||
|
||||
// The derivative NFT should be deposited to the correct beneficiary
|
||||
assert_eq!(
|
||||
unique_items::ItemOwner::<Test, PredefinedIdNftsInstance>::get(&derivative_full_nft_id),
|
||||
Some(another_nft_beneficiary),
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user