diff --git a/Cargo.dev.toml b/Cargo.dev.toml index bffa0dd42..ff41946a4 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -16,6 +16,7 @@ members = [ "rewards", "nft", "xcm", + "xnft", "xtokens", "xcm-support", "unknown-tokens", diff --git a/xcm-support/Cargo.toml b/xcm-support/Cargo.toml index 3151c8994..33f254ffe 100644 --- a/xcm-support/Cargo.toml +++ b/xcm-support/Cargo.toml @@ -9,16 +9,22 @@ edition = "2021" [dependencies] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } +scale-info = { version = "2.9.0", default-features = false, features = [ + "derive", +] } -frame-support = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false , branch = "release-polkadot-v1.1.0" } -sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false , branch = "release-polkadot-v1.1.0" } -sp-std = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false , branch = "release-polkadot-v1.1.0" } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" } +sp-std = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" } -xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", default-features = false , branch = "release-polkadot-v1.1.0" } -xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", default-features = false , branch = "release-polkadot-v1.1.0" } +xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" } +xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" } +xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" } orml-traits = { path = "../traits", version = "0.4.1-dev", default-features = false } +log = { version = "0.4.17", default-features = false } + [features] default = ["std"] std = [ @@ -29,4 +35,5 @@ std = [ "sp-std/std", "xcm-executor/std", "xcm/std", + "xcm-builder/std", ] diff --git a/xcm-support/src/lib.rs b/xcm-support/src/lib.rs index 1b3ed2e69..07d2caa22 100644 --- a/xcm-support/src/lib.rs +++ b/xcm-support/src/lib.rs @@ -22,9 +22,8 @@ use xcm_executor::traits::MatchesFungible; use orml_traits::{location::Reserve, GetByKey}; pub use currency_adapter::{DepositToAlternative, MultiCurrencyAdapter, OnDepositFail}; - mod currency_adapter; - +pub mod parity_adapters; mod tests; /// A `MatchesFungible` implementation. It matches concrete fungible assets diff --git a/xcm-support/src/parity_adapters.rs b/xcm-support/src/parity_adapters.rs new file mode 100644 index 000000000..750a78dc4 --- /dev/null +++ b/xcm-support/src/parity_adapters.rs @@ -0,0 +1,416 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Adapters to work with [`frame_support::traits::tokens::nonfungibles_v2`] +//! through XCM. +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + ensure, + traits::{tokens::nonfungibles_v2, Get}, +}; +use sp_runtime::scale_info::TypeInfo; +use sp_std::{marker::PhantomData, prelude::*, result}; +use xcm::latest::prelude::*; +use xcm_builder::{AssetChecking, MintLocation}; +use xcm_executor::traits::{ConvertLocation, Error as MatchError, MatchesNonFungibles, TransactAsset}; + +const LOG_TARGET: &str = "xcm::nonfungibles_v2_adapter"; +/// Adapter for transferring non-fungible tokens (NFTs) using +/// [`nonfungibles_v2`]. +/// +/// This adapter facilitates the transfer of NFTs between different locations. +pub struct NonFungiblesV2TransferAdapter( + PhantomData<(Assets, Matcher, AccountIdConverter, AccountId)>, +); +impl< + Assets: nonfungibles_v2::Transfer, + Matcher: MatchesNonFungibles, + AccountIdConverter: ConvertLocation, + AccountId: Clone, // can't get away without it since `nonfungibles_v2` is generic over it. + > TransactAsset for NonFungiblesV2TransferAdapter +{ + fn transfer_asset( + what: &MultiAsset, + from: &MultiLocation, + to: &MultiLocation, + context: &XcmContext, + ) -> result::Result { + log::trace!( + target: LOG_TARGET, + "transfer_asset what: {:?}, from: {:?}, to: {:?}, context: {:?}", + what, + from, + to, + context, + ); + // Check we handle this asset. + let (class, instance) = Matcher::matches_nonfungibles(what)?; + let destination = AccountIdConverter::convert_location(to).ok_or(MatchError::AccountIdConversionFailed)?; + Assets::transfer(&class, &instance, &destination).map_err(|e| XcmError::FailedToTransactAsset(e.into()))?; + Ok(what.clone().into()) + } +} + +/// Adapter for mutating non-fungible tokens (NFTs) using [`nonfungibles_v2`]. +/// +/// This adapter provides functions to withdraw, deposit, check in and check out +/// non fungibles. +pub struct NonFungiblesV2MutateAdapter< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, +>( + PhantomData<( + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + )>, +) +where + ItemConfig: Default; + +impl< + Assets: nonfungibles_v2::Mutate, + Matcher: MatchesNonFungibles, + AccountIdConverter: ConvertLocation, + AccountId: Clone + Eq, // can't get away without it since `nonfungibles_v2` is generic over it. + CheckAsset: AssetChecking, + CheckingAccount: Get>, + ItemConfig: Default, + > NonFungiblesV2MutateAdapter +{ + fn can_accrue_checked(class: Assets::CollectionId, instance: Assets::ItemId) -> XcmResult { + ensure!(Assets::owner(&class, &instance).is_none(), XcmError::NotDepositable); + Ok(()) + } + fn can_reduce_checked(class: Assets::CollectionId, instance: Assets::ItemId) -> XcmResult { + if let Some(checking_account) = CheckingAccount::get() { + // This is an asset whose teleports we track. + let owner = Assets::owner(&class, &instance); + ensure!(owner == Some(checking_account), XcmError::NotWithdrawable); + ensure!(Assets::can_transfer(&class, &instance), XcmError::NotWithdrawable); + } + Ok(()) + } + fn accrue_checked(class: Assets::CollectionId, instance: Assets::ItemId) { + if let Some(checking_account) = CheckingAccount::get() { + let ok = Assets::mint_into(&class, &instance, &checking_account, &ItemConfig::default(), true).is_ok(); + debug_assert!(ok, "`mint_into` cannot generally fail; qed"); + } + } + fn reduce_checked(class: Assets::CollectionId, instance: Assets::ItemId) { + let ok = Assets::burn(&class, &instance, None).is_ok(); + debug_assert!(ok, "`can_check_in` must have returned `true` immediately prior; qed"); + } +} + +impl< + Assets: nonfungibles_v2::Mutate, + Matcher: MatchesNonFungibles, + AccountIdConverter: ConvertLocation, + AccountId: Clone + Eq, // can't get away without it since `nonfungibles_v2` is generic over it. + CheckAsset: AssetChecking, + CheckingAccount: Get>, + ItemConfig: Default, + > TransactAsset + for NonFungiblesV2MutateAdapter +{ + fn can_check_in(_origin: &MultiLocation, what: &MultiAsset, context: &XcmContext) -> XcmResult { + log::trace!( + target: LOG_TARGET, + "can_check_in origin: {:?}, what: {:?}, context: {:?}", + _origin, + what, + context, + ); + // Check we handle this asset. + let (class, instance) = Matcher::matches_nonfungibles(what)?; + match CheckAsset::asset_checking(&class) { + // We track this asset's teleports to ensure no more come in than have gone out. + Some(MintLocation::Local) => Self::can_reduce_checked(class, instance), + // We track this asset's teleports to ensure no more go out than have come in. + Some(MintLocation::NonLocal) => Self::can_accrue_checked(class, instance), + _ => Ok(()), + } + } + + fn check_in(_origin: &MultiLocation, what: &MultiAsset, context: &XcmContext) { + log::trace!( + target: LOG_TARGET, + "check_in origin: {:?}, what: {:?}, context: {:?}", + _origin, + what, + context, + ); + if let Ok((class, instance)) = Matcher::matches_nonfungibles(what) { + match CheckAsset::asset_checking(&class) { + // We track this asset's teleports to ensure no more come in than have gone out. + Some(MintLocation::Local) => Self::reduce_checked(class, instance), + // We track this asset's teleports to ensure no more go out than have come in. + Some(MintLocation::NonLocal) => Self::accrue_checked(class, instance), + _ => (), + } + } + } + + fn can_check_out(_dest: &MultiLocation, what: &MultiAsset, context: &XcmContext) -> XcmResult { + log::trace!( + target: LOG_TARGET, + "can_check_out dest: {:?}, what: {:?}, context: {:?}", + _dest, + what, + context, + ); + // Check we handle this asset. + let (class, instance) = Matcher::matches_nonfungibles(what)?; + match CheckAsset::asset_checking(&class) { + // We track this asset's teleports to ensure no more come in than have gone out. + Some(MintLocation::Local) => Self::can_accrue_checked(class, instance), + // We track this asset's teleports to ensure no more go out than have come in. + Some(MintLocation::NonLocal) => Self::can_reduce_checked(class, instance), + _ => Ok(()), + } + } + + fn check_out(_dest: &MultiLocation, what: &MultiAsset, context: &XcmContext) { + log::trace!( + target: LOG_TARGET, + "check_out dest: {:?}, what: {:?}, context: {:?}", + _dest, + what, + context, + ); + if let Ok((class, instance)) = Matcher::matches_nonfungibles(what) { + match CheckAsset::asset_checking(&class) { + // We track this asset's teleports to ensure no more come in than have gone out. + Some(MintLocation::Local) => Self::accrue_checked(class, instance), + // We track this asset's teleports to ensure no more go out than have come in. + Some(MintLocation::NonLocal) => Self::reduce_checked(class, instance), + _ => (), + } + } + } + + fn deposit_asset(what: &MultiAsset, who: &MultiLocation, context: &XcmContext) -> XcmResult { + log::trace!( + target: LOG_TARGET, + "deposit_asset what: {:?}, who: {:?}, context: {:?}", + what, + who, + context, + ); + // Check we handle this asset. + let (class, instance) = Matcher::matches_nonfungibles(what)?; + let who = AccountIdConverter::convert_location(who).ok_or(MatchError::AccountIdConversionFailed)?; + + Assets::mint_into(&class, &instance, &who, &ItemConfig::default(), true) + .map_err(|e| XcmError::FailedToTransactAsset(e.into())) + } + + fn withdraw_asset( + what: &MultiAsset, + who: &MultiLocation, + maybe_context: Option<&XcmContext>, + ) -> result::Result { + log::trace!( + target: LOG_TARGET, + "withdraw_asset what: {:?}, who: {:?}, maybe_context: {:?}", + what, + who, + maybe_context, + ); + // Check we handle this asset. + let who = AccountIdConverter::convert_location(who).ok_or(MatchError::AccountIdConversionFailed)?; + let (class, instance) = Matcher::matches_nonfungibles(what)?; + Assets::burn(&class, &instance, Some(&who)).map_err(|e| XcmError::FailedToTransactAsset(e.into()))?; + Ok(what.clone().into()) + } +} + +/// Adapter for handling non-fungible tokens (NFTs) using [`nonfungibles_v2`]. +/// +/// This adapter combines the functionalities of both the +/// [`NonFungiblesV2TransferAdapter`] and [`NonFungiblesV2MutateAdapter`] +/// adapters, allowing handling NFTs in various scenarios. +/// For detailed information on the functions, refer to [`TransactAsset`]. +pub struct NonFungiblesV2Adapter< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, +>( + PhantomData<( + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + )>, +) +where + ItemConfig: Default; +impl< + Assets: nonfungibles_v2::Mutate + nonfungibles_v2::Transfer, + Matcher: MatchesNonFungibles, + AccountIdConverter: ConvertLocation, + AccountId: Clone + Eq, // can't get away without it since `nonfungibles_v2` is generic over it. + CheckAsset: AssetChecking, + CheckingAccount: Get>, + ItemConfig: Default, + > TransactAsset + for NonFungiblesV2Adapter +{ + fn can_check_in(origin: &MultiLocation, what: &MultiAsset, context: &XcmContext) -> XcmResult { + NonFungiblesV2MutateAdapter::< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + >::can_check_in(origin, what, context) + } + + fn check_in(origin: &MultiLocation, what: &MultiAsset, context: &XcmContext) { + NonFungiblesV2MutateAdapter::< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + >::check_in(origin, what, context) + } + + fn can_check_out(dest: &MultiLocation, what: &MultiAsset, context: &XcmContext) -> XcmResult { + NonFungiblesV2MutateAdapter::< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + >::can_check_out(dest, what, context) + } + + fn check_out(dest: &MultiLocation, what: &MultiAsset, context: &XcmContext) { + NonFungiblesV2MutateAdapter::< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + >::check_out(dest, what, context) + } + + fn deposit_asset(what: &MultiAsset, who: &MultiLocation, context: &XcmContext) -> XcmResult { + NonFungiblesV2MutateAdapter::< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + >::deposit_asset(what, who, context) + } + + fn withdraw_asset( + what: &MultiAsset, + who: &MultiLocation, + maybe_context: Option<&XcmContext>, + ) -> result::Result { + NonFungiblesV2MutateAdapter::< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + >::withdraw_asset(what, who, maybe_context) + } + + fn transfer_asset( + what: &MultiAsset, + from: &MultiLocation, + to: &MultiLocation, + context: &XcmContext, + ) -> result::Result { + NonFungiblesV2TransferAdapter::::transfer_asset( + what, from, to, context, + ) + } +} + +#[derive(Copy, Clone, Decode, Encode, Eq, PartialEq, Ord, PartialOrd, Debug, TypeInfo, MaxEncodedLen)] +/// Represents a collection ID based on a MultiLocation. +/// +/// This structure provides a way to map a MultiLocation to a collection ID, +/// which is useful for describing collections that do not follow an incremental +/// pattern. +pub struct MultiLocationCollectionId(MultiLocation); +impl MultiLocationCollectionId { + /// Consume `self` and return the inner MultiLocation. + pub fn into_inner(self) -> MultiLocation { + self.0 + } + + /// Return a reference to the inner MultiLocation. + pub fn inner(&self) -> &MultiLocation { + &self.0 + } +} + +// impl Incrementable for MultiLocationCollectionId { +// fn increment(&self) -> Option { +// None +// } + +// fn initial_value() -> Option { +// None +// } +// } + +impl From for MultiLocationCollectionId { + fn from(value: MultiLocation) -> Self { + MultiLocationCollectionId(value) + } +} + +impl From for MultiLocation { + fn from(value: MultiLocationCollectionId) -> MultiLocation { + value.into_inner() + } +} diff --git a/xnft/Cargo.toml b/xnft/Cargo.toml new file mode 100644 index 000000000..067b89ada --- /dev/null +++ b/xnft/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "orml-xnft" +description = "POC" +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/xnft" +license = "Apache-2.0" +version = "0.4.1-dev" +authors = ["Unique Developers"] +edition = "2021" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "max-encoded-len", +] } +scale-info = { version = "2.9.0", default-features = false, features = [ + "derive", +] } +serde = { version = "1.0.136", optional = true } + +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } + +xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } + +[dev-dependencies] +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } +sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } + +[features] +default = ["std"] +std = [ + "serde", + + "codec/std", + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "scale-info/std", + "sp-std/std", + "xcm-executor/std", + "xcm/std", +] +try-runtime = ["frame-support/try-runtime", "frame-system/try-runtime"] diff --git a/xnft/README.md b/xnft/README.md new file mode 100644 index 000000000..3f4ee815a --- /dev/null +++ b/xnft/README.md @@ -0,0 +1,4 @@ +# XCM non-fungible-token module + +### Overview + diff --git a/xnft/src/impl_matches.rs b/xnft/src/impl_matches.rs new file mode 100644 index 000000000..a41863572 --- /dev/null +++ b/xnft/src/impl_matches.rs @@ -0,0 +1,37 @@ +use crate::*; +use frame_support::traits::Incrementable; +use xcm::v3::Fungibility; +use xcm_executor::traits::{Error as MatchError, MatchesNonFungibles}; + +impl MatchesNonFungibles, ItemIdOf> for Pallet +where + ItemIdOf: MaxEncodedLen + Incrementable, + CollectionIdOf: MaxEncodedLen, +{ + fn matches_nonfungibles( + foreign_asset: &MultiAsset, + ) -> core::result::Result<(CollectionIdOf, ItemIdOf), MatchError> { + let Fungibility::NonFungible(asset_instance) = foreign_asset.fun else { + return Err(MatchError::AssetNotHandled); + }; + let asset = Self::assets(foreign_asset.id).ok_or(MatchError::AssetNotHandled)?; + let item = Self::items(&asset, asset_instance).unwrap_or(Self::get_next_item_of(&asset)?); + Ok((asset, item)) + } +} + +impl Pallet +where + ItemIdOf: MaxEncodedLen + Incrementable, + CollectionIdOf: MaxEncodedLen, +{ + pub fn get_next_item_of(collection_id: &CollectionIdOf) -> Result, MatchError> { + let item = >::get(collection_id) + .unwrap_or(>::initial_value().ok_or(MatchError::InstanceConversionFailed)?); + >::set( + collection_id, + Some(item.increment().ok_or(MatchError::InstanceConversionFailed)?), + ); + Ok(item) + } +} diff --git a/xnft/src/impl_nonfungibles.rs b/xnft/src/impl_nonfungibles.rs new file mode 100644 index 000000000..0a1ae6571 --- /dev/null +++ b/xnft/src/impl_nonfungibles.rs @@ -0,0 +1,214 @@ +use crate::*; + +impl Inspect for Pallet +where + ItemIdOf: MaxEncodedLen, + CollectionIdOf: MaxEncodedLen, +{ + type ItemId = ItemIdOf; + + type CollectionId = CollectionIdOf; + + fn owner(collection: &Self::CollectionId, item: &Self::ItemId) -> Option { + >::owner(collection, item) + } + + fn collection_owner(collection: &Self::CollectionId) -> Option { + >::collection_owner(collection) + } + + fn attribute(collection: &Self::CollectionId, item: &Self::ItemId, key: &[u8]) -> Option> { + >::attribute(collection, item, key) + } + + fn custom_attribute( + account: &T::AccountId, + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &[u8], + ) -> Option> { + >::custom_attribute(account, collection, item, key) + } + + fn system_attribute(collection: &Self::CollectionId, item: &Self::ItemId, key: &[u8]) -> Option> { + >::system_attribute(collection, item, key) + } + + fn typed_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + ) -> Option { + >::typed_attribute(collection, item, key) + } + + fn typed_custom_attribute( + account: &T::AccountId, + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + ) -> Option { + >::typed_custom_attribute(account, collection, item, key) + } + + fn typed_system_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + ) -> Option { + >::typed_system_attribute(collection, item, key) + } + + fn collection_attribute(collection: &Self::CollectionId, key: &[u8]) -> Option> { + >::collection_attribute(collection, key) + } + + fn typed_collection_attribute(collection: &Self::CollectionId, key: &K) -> Option { + >::typed_collection_attribute(collection, key) + } + + fn can_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> bool { + >::can_transfer(collection, item) + } +} + +impl Transfer for Pallet +where + ItemIdOf: MaxEncodedLen, + CollectionIdOf: MaxEncodedLen, + // ItemId: MaxEncodedLen + Parameter, + // CollectionId: MaxEncodedLen + Parameter, +{ + fn disable_transfer( + collection: &Self::CollectionId, + item: &Self::ItemId, + ) -> frame_support::pallet_prelude::DispatchResult { + >::disable_transfer(collection, item) + } + + fn enable_transfer( + collection: &Self::CollectionId, + item: &Self::ItemId, + ) -> frame_support::pallet_prelude::DispatchResult { + >::enable_transfer(collection, item) + } + + fn transfer( + collection: &Self::CollectionId, + item: &Self::ItemId, + destination: &T::AccountId, + ) -> frame_support::pallet_prelude::DispatchResult { + >::transfer(collection, item, destination) + } +} + +impl Mutate for Pallet +where + ItemIdOf: MaxEncodedLen, + CollectionIdOf: MaxEncodedLen, +{ + fn mint_into( + collection: &Self::CollectionId, + item: &Self::ItemId, + who: &T::AccountId, + config: &T::ItemConfig, + deposit_collection_owner: bool, + ) -> frame_support::pallet_prelude::DispatchResult { + >::mint_into(collection, item, who, config, deposit_collection_owner) + } + + fn burn( + collection: &Self::CollectionId, + item: &Self::ItemId, + maybe_check_owner: Option<&T::AccountId>, + ) -> frame_support::pallet_prelude::DispatchResult { + >::burn(collection, item, maybe_check_owner) + } + + fn set_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &[u8], + value: &[u8], + ) -> frame_support::pallet_prelude::DispatchResult { + >::set_attribute(collection, item, key, value) + } + + fn set_typed_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + value: &V, + ) -> frame_support::pallet_prelude::DispatchResult { + >::set_typed_attribute(collection, item, key, value) + } + + fn set_collection_attribute( + collection: &Self::CollectionId, + key: &[u8], + value: &[u8], + ) -> frame_support::pallet_prelude::DispatchResult { + >::set_collection_attribute(collection, key, value) + } + + fn set_typed_collection_attribute( + collection: &Self::CollectionId, + key: &K, + value: &V, + ) -> frame_support::pallet_prelude::DispatchResult { + >::set_typed_collection_attribute(collection, key, value) + } + + fn clear_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &[u8], + ) -> frame_support::pallet_prelude::DispatchResult { + >::clear_attribute(collection, item, key) + } + + fn clear_typed_attribute( + collection: &Self::CollectionId, + item: &Self::ItemId, + key: &K, + ) -> frame_support::pallet_prelude::DispatchResult { + >::clear_typed_attribute(collection, item, key) + } + + fn clear_collection_attribute( + collection: &Self::CollectionId, + key: &[u8], + ) -> frame_support::pallet_prelude::DispatchResult { + >::clear_collection_attribute(collection, key) + } + + fn clear_typed_collection_attribute( + collection: &Self::CollectionId, + key: &K, + ) -> frame_support::pallet_prelude::DispatchResult { + >::clear_typed_collection_attribute(collection, key) + } +} + +impl Create for Pallet +where + ItemIdOf: MaxEncodedLen, + CollectionIdOf: MaxEncodedLen, +{ + fn create_collection( + who: &T::AccountId, + admin: &T::AccountId, + config: &T::CollectionConfig, + ) -> Result { + >::create_collection(who, admin, config) + } + + fn create_collection_with_id( + collection: Self::CollectionId, + who: &T::AccountId, + admin: &T::AccountId, + config: &T::CollectionConfig, + ) -> Result<(), DispatchError> { + >::create_collection_with_id(collection, who, admin, config) + } +} diff --git a/xnft/src/lib.rs b/xnft/src/lib.rs new file mode 100644 index 000000000..e43820ad1 --- /dev/null +++ b/xnft/src/lib.rs @@ -0,0 +1,114 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + ensure, + pallet_prelude::*, + traits::tokens::nonfungibles_v2::{Create, Inspect, Mutate, Transfer}, +}; +use frame_system::pallet_prelude::*; +use frame_system::Config as SystemConfig; +use scale_info::TypeInfo; +use sp_runtime::{traits::AccountIdConversion, DispatchError, DispatchResult, RuntimeDebug}; +use sp_std::{boxed::Box, vec::Vec}; +use xcm::v3::{AssetId, AssetInstance, MultiAsset}; + +pub mod impl_matches; +pub mod impl_nonfungibles; +pub mod types; +pub use pallet::*; +pub(crate) use types::*; + +#[frame_support::pallet] +pub mod pallet { + use xcm::v3::Fungibility; + + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config + where + ItemIdOf: MaxEncodedLen, + CollectionIdOf: MaxEncodedLen, + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type NftExecutor: Create + + Mutate + + Transfer; + + type ItemConfig: Default; + + type CollectionConfig: Default; + } + + /// Error for non-fungible-token module. + #[pallet::error] + pub enum Error { + AssetAlreadyRegistered, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event + where + ItemIdOf: MaxEncodedLen, + CollectionIdOf: MaxEncodedLen, + { + RegisteredAsset { + asset_id: AssetId, + collection_id: CollectionIdOf, + }, + } + + #[pallet::storage] + #[pallet::getter(fn assets)] + pub type AssetsMapping = StorageMap<_, Twox64Concat, AssetId, CollectionIdOf, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn items)] + pub type ItemsMapping = + StorageDoubleMap<_, Twox64Concat, CollectionIdOf, Twox64Concat, AssetInstance, ItemIdOf, OptionQuery>; + + #[pallet::storage] + pub type NextItemId = StorageMap<_, Twox64Concat, CollectionIdOf, ItemIdOf, OptionQuery>; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::call] + impl Pallet + where + ItemIdOf: MaxEncodedLen + Default, + CollectionIdOf: MaxEncodedLen, + { + #[pallet::call_index(0)] + #[pallet::weight(0)] + pub fn register_asset(origin: OriginFor, foreign_asset: Box) -> DispatchResult { + ensure_signed(origin)?; + ensure!( + !>::contains_key(foreign_asset.as_ref()), + >::AssetAlreadyRegistered, + ); + let collection_id = + >::create_collection(&Self::account_id(), &Self::account_id(), &Default::default())?; + >::insert(foreign_asset.as_ref(), collection_id.clone()); + Self::deposit_event(Event::RegisteredAsset { + asset_id: *foreign_asset, + collection_id, + }); + Ok(()) + } + } +} + +impl Pallet +where + ItemIdOf: MaxEncodedLen + Default, + CollectionIdOf: MaxEncodedLen, +{ + pub fn account_id() -> T::AccountId { + frame_support::PalletId(*b"poc_xnft").into_account_truncating() + } +} diff --git a/xnft/src/types.rs b/xnft/src/types.rs new file mode 100644 index 000000000..6cc2dc98f --- /dev/null +++ b/xnft/src/types.rs @@ -0,0 +1,7 @@ +use crate::*; + +pub type ItemIdOf = <::NftExecutor as Inspect<::AccountId>>::ItemId; + +pub type CollectionIdOf = <::NftExecutor as Inspect<::AccountId>>::CollectionId; + +pub type ExecutorOf = ::NftExecutor; diff --git a/xtokens/src/lib.rs b/xtokens/src/lib.rs index fe479f27a..4d535717d 100644 --- a/xtokens/src/lib.rs +++ b/xtokens/src/lib.rs @@ -526,20 +526,34 @@ pub mod module { let asset_len = assets.len(); for i in 0..asset_len { let asset = assets.get(i).ok_or(Error::::AssetIndexNonExistent)?; - ensure!( - matches!(asset.fun, Fungibility::Fungible(x) if !x.is_zero()), - Error::::InvalidAsset - ); - // `assets` includes fee, the reserve location is decided by non fee asset - if (fee != *asset && non_fee_reserve.is_none()) || asset_len == 1 { - non_fee_reserve = T::ReserveProvider::reserve(asset); - } - // make sure all non fee assets share the same reserve - if non_fee_reserve.is_some() { + + if fee == *asset { + // Fee payment can only be made by using fungibles ensure!( - non_fee_reserve == T::ReserveProvider::reserve(asset), - Error::::DistinctReserveForAssetAndFee + matches!(asset.fun, Fungibility::Fungible(x) if !x.is_zero()), + Error::::InvalidAsset ); + } else { + match asset.fun { + Fungibility::Fungible(x) => ensure!(!x.is_zero(), Error::::InvalidAsset), + Fungibility::NonFungible(AssetInstance::Undefined) => { + return Err(Error::::InvalidAsset.into()) + } + _ => {} + } + + // `assets` includes fee, the reserve location is decided by non fee asset + if non_fee_reserve.is_none() || asset_len == 1 { + non_fee_reserve = T::ReserveProvider::reserve(asset); + } + + // make sure all non fee assets share the same reserve + if non_fee_reserve.is_some() { + ensure!( + non_fee_reserve == T::ReserveProvider::reserve(asset), + Error::::DistinctReserveForAssetAndFee + ); + } } }