diff --git a/Cargo.lock b/Cargo.lock index 0e0e47f955ad0..2dd8bc64b5dcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20945,6 +20945,7 @@ dependencies = [ "log", "pallet-balances", "pallet-message-queue", + "pallet-nfts", "pallet-uniques", "pallet-xcm", "parity-scale-codec", diff --git a/polkadot/xcm/xcm-builder/src/lib.rs b/polkadot/xcm/xcm-builder/src/lib.rs index e3f9104096384..31581caff226c 100644 --- a/polkadot/xcm/xcm-builder/src/lib.rs +++ b/polkadot/xcm/xcm-builder/src/lib.rs @@ -78,6 +78,12 @@ pub use nonfungibles_adapter::{ NonFungiblesAdapter, NonFungiblesMutateAdapter, NonFungiblesTransferAdapter, }; +mod nonfungibles_v2_adapter; +pub use nonfungibles_v2_adapter::{ + MultiLocationCollectionId, NonFungiblesV2Adapter, NonFungiblesV2MutateAdapter, + NonFungiblesV2TransferAdapter, +}; + mod weight; pub use weight::{ FixedRateOfFungible, FixedWeightBounds, TakeRevenue, UsingComponents, WeightInfoBounds, diff --git a/polkadot/xcm/xcm-builder/src/nonfungibles_adapter.rs b/polkadot/xcm/xcm-builder/src/nonfungibles_adapter.rs index 6cf5980df0e9e..fa2747dea1cb2 100644 --- a/polkadot/xcm/xcm-builder/src/nonfungibles_adapter.rs +++ b/polkadot/xcm/xcm-builder/src/nonfungibles_adapter.rs @@ -14,7 +14,7 @@ // 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::fungibles` through XCM. +//! Adapters to work with `frame_support::traits::tokens::nonfungibles` through XCM. use crate::{AssetChecking, MintLocation}; use frame_support::{ diff --git a/polkadot/xcm/xcm-builder/src/nonfungibles_v2_adapter.rs b/polkadot/xcm/xcm-builder/src/nonfungibles_v2_adapter.rs new file mode 100644 index 0000000000000..66b61f8be6f0f --- /dev/null +++ b/polkadot/xcm/xcm-builder/src/nonfungibles_v2_adapter.rs @@ -0,0 +1,452 @@ +// 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 crate::{AssetChecking, MintLocation}; +use frame_support::{ + ensure, + traits::{tokens::nonfungibles_v2, Get, Incrementable}, +}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_std::{marker::PhantomData, prelude::*, result}; +use xcm::latest::prelude::*; +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< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + > +{ + 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< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + > +{ + 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< + Assets, + Matcher, + AccountIdConverter, + AccountId, + CheckAsset, + CheckingAccount, + ItemConfig, + > +{ + 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/polkadot/xcm/xcm-simulator/example/Cargo.toml b/polkadot/xcm/xcm-simulator/example/Cargo.toml index e29c1c0d1f2ce..52f4aef1ced22 100644 --- a/polkadot/xcm/xcm-simulator/example/Cargo.toml +++ b/polkadot/xcm/xcm-simulator/example/Cargo.toml @@ -16,6 +16,7 @@ frame-support = { path = "../../../../substrate/frame/support" } pallet-balances = { path = "../../../../substrate/frame/balances" } pallet-message-queue = { path = "../../../../substrate/frame/message-queue" } pallet-uniques = { path = "../../../../substrate/frame/uniques" } +pallet-nfts = { path = "../../../../substrate/frame/nfts" } sp-std = { path = "../../../../substrate/primitives/std" } sp-core = { path = "../../../../substrate/primitives/core" } sp-runtime = { path = "../../../../substrate/primitives/runtime" } diff --git a/polkadot/xcm/xcm-simulator/example/src/lib.rs b/polkadot/xcm/xcm-simulator/example/src/lib.rs index 85b8ad1c5cb7b..fa0efffff94ec 100644 --- a/polkadot/xcm/xcm-simulator/example/src/lib.rs +++ b/polkadot/xcm/xcm-simulator/example/src/lib.rs @@ -24,6 +24,7 @@ use xcm_executor::traits::ConvertLocation; use xcm_simulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain, TestExt}; pub const ALICE: sp_runtime::AccountId32 = sp_runtime::AccountId32::new([0u8; 32]); +pub const BOB: sp_runtime::AccountId32 = sp_runtime::AccountId32::new([0u8; 32]); pub const INITIAL_BALANCE: u128 = 1_000_000_000; decl_test_parachain! { @@ -68,27 +69,27 @@ decl_test_network! { pub fn parent_account_id() -> parachain::AccountId { let location = (Parent,); - parachain::LocationToAccountId::convert_location(&location.into()).unwrap() + parachain::SovereignAccountOf::convert_location(&location.into()).unwrap() } pub fn child_account_id(para: u32) -> relay_chain::AccountId { let location = (Parachain(para),); - relay_chain::LocationToAccountId::convert_location(&location.into()).unwrap() + relay_chain::SovereignAccountOf::convert_location(&location.into()).unwrap() } pub fn child_account_account_id(para: u32, who: sp_runtime::AccountId32) -> relay_chain::AccountId { let location = (Parachain(para), AccountId32 { network: None, id: who.into() }); - relay_chain::LocationToAccountId::convert_location(&location.into()).unwrap() + relay_chain::SovereignAccountOf::convert_location(&location.into()).unwrap() } pub fn sibling_account_account_id(para: u32, who: sp_runtime::AccountId32) -> parachain::AccountId { let location = (Parent, Parachain(para), AccountId32 { network: None, id: who.into() }); - parachain::LocationToAccountId::convert_location(&location.into()).unwrap() + parachain::SovereignAccountOf::convert_location(&location.into()).unwrap() } pub fn parent_account_account_id(who: sp_runtime::AccountId32) -> parachain::AccountId { let location = (Parent, AccountId32 { network: None, id: who.into() }); - parachain::LocationToAccountId::convert_location(&location.into()).unwrap() + parachain::SovereignAccountOf::convert_location(&location.into()).unwrap() } pub fn para_ext(para_id: u32) -> sp_io::TestExternalities { @@ -140,11 +141,15 @@ pub type ParachainPalletXcm = pallet_xcm::Pallet; #[cfg(test)] mod tests { + use crate::parachain::Balance; + use super::*; use codec::Encode; use frame_support::{assert_ok, weights::Weight}; + use pallet_nfts::{CollectionConfig, CollectionSettings, MintSettings}; use xcm::latest::QueryResponseInfo; + use xcm_builder::MultiLocationCollectionId; use xcm_simulator::TestExt; // Helper function for forming buy execution message @@ -152,6 +157,15 @@ mod tests { BuyExecution { fees: fees.into(), weight_limit: Unlimited } } + // helper function for CollectionConfig + fn get_collection_config() -> CollectionConfig { + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: None, + mint_settings: MintSettings::default(), + } + } + #[test] fn remote_account_ids_work() { child_account_account_id(1, ALICE); @@ -364,7 +378,7 @@ mod tests { #[test] fn teleport_nft() { MockNet::reset(); - + let collection_id = MultiLocation { parents: 1, interior: X1(GeneralIndex(1)) }.into(); Relay::execute_with(|| { // Mint the NFT (1, 69) and give it to our "parachain#1 alias". assert_ok!(relay_chain::Uniques::mint( @@ -381,17 +395,18 @@ mod tests { ); }); ParaA::execute_with(|| { - assert_ok!(parachain::ForeignUniques::force_create( - parachain::RuntimeOrigin::root(), - (Parent, GeneralIndex(1)).into(), + use pallet_nfts::Event; + + assert_ok!(parachain::ForeignNfts::do_create_collection( + collection_id, + ALICE, ALICE, - false, + get_collection_config(), + 1000u128, + Event::ForceCreated { collection: collection_id, owner: ALICE }, )); - assert_eq!( - parachain::ForeignUniques::owner((Parent, GeneralIndex(1)).into(), 69u32.into()), - None, - ); - assert_eq!(parachain::Balances::reserved_balance(&ALICE), 0); + + assert_eq!(parachain::Balances::reserved_balance(&ALICE), 1000); // IRL Alice would probably just execute this locally on the Relay-chain, but we can't // easily do that here since we only send between chains. @@ -411,11 +426,8 @@ mod tests { assert_ok!(ParachainPalletXcm::send_xcm(alice, Parent, message)); }); ParaA::execute_with(|| { - assert_eq!( - parachain::ForeignUniques::owner((Parent, GeneralIndex(1)).into(), 69u32.into()), - Some(ALICE), - ); - assert_eq!(parachain::Balances::reserved_balance(&ALICE), 1000); + assert_eq!(parachain::ForeignNfts::owner(collection_id, 69u32.into()), Some(ALICE),); + assert_eq!(parachain::Balances::reserved_balance(&ALICE), 2000); }); Relay::execute_with(|| { assert_eq!(relay_chain::Uniques::owner(1, 69), None); @@ -429,9 +441,8 @@ mod tests { /// Asserts that the parachain accounts are updated as expected. #[test] fn reserve_asset_transfer_nft() { - sp_tracing::init_for_tests(); MockNet::reset(); - + let collection_id = MultiLocation { parents: 1, interior: X1(GeneralIndex(2)) }.into(); Relay::execute_with(|| { assert_ok!(relay_chain::Uniques::force_create( relay_chain::RuntimeOrigin::root(), @@ -451,17 +462,18 @@ mod tests { ); }); ParaA::execute_with(|| { - assert_ok!(parachain::ForeignUniques::force_create( - parachain::RuntimeOrigin::root(), - (Parent, GeneralIndex(2)).into(), + use pallet_nfts::Event; + + assert_ok!(parachain::ForeignNfts::do_create_collection( + collection_id, + ALICE, ALICE, - false, + get_collection_config(), + 1000u128, + Event::ForceCreated { collection: collection_id, owner: ALICE }, )); - assert_eq!( - parachain::ForeignUniques::owner((Parent, GeneralIndex(2)).into(), 69u32.into()), - None, - ); - assert_eq!(parachain::Balances::reserved_balance(&ALICE), 0); + assert_eq!(parachain::ForeignNfts::owner(collection_id, 69u32.into()), None,); + assert_eq!(parachain::Balances::reserved_balance(&ALICE), 1000); let message = Xcm(vec![ WithdrawAsset((GeneralIndex(2), 69u32).into()), @@ -479,12 +491,8 @@ mod tests { assert_ok!(ParachainPalletXcm::send_xcm(alice, Parent, message)); }); ParaA::execute_with(|| { - log::debug!(target: "xcm-exceutor", "Hello"); - assert_eq!( - parachain::ForeignUniques::owner((Parent, GeneralIndex(2)).into(), 69u32.into()), - Some(ALICE), - ); - assert_eq!(parachain::Balances::reserved_balance(&ALICE), 1000); + assert_eq!(parachain::ForeignNfts::owner(collection_id, 69u32.into()), Some(ALICE),); + assert_eq!(parachain::Balances::reserved_balance(&ALICE), 2000); }); Relay::execute_with(|| { @@ -492,70 +500,116 @@ mod tests { }); } + /// Scenario: - /// The relay-chain creates an asset class on a parachain and then Alice transfers her NFT into - /// that parachain's sovereign account, who then mints a trustless-backed-derivative locally. - /// - /// Asserts that the parachain accounts are updated as expected. + /// Alice on Parachain A transfers an NFT into a parachain B's sovereign account. + /// Parachain B then mints a derivative NFT locally. Alice then transfers the derivative + /// NFT to Bob on Parachain B. Bob then withdraws the derivative NFT on Parachain B and + /// transfers the NFT to himself on Parachain A. The derivative is burned on Parachain B + /// and the original is transfered to Bob on Parachain A. #[test] - fn reserve_asset_class_create_and_reserve_transfer() { + fn two_way_reserve_asset_transfer_nft() { + use pallet_nfts::Event; MockNet::reset(); - - Relay::execute_with(|| { - assert_ok!(relay_chain::Uniques::force_create( - relay_chain::RuntimeOrigin::root(), - 2, + let collection_id = MultiLocation { parents: 0, interior: X1(GeneralIndex(2)) }.into(); + ParaA::execute_with(|| { + assert_ok!(parachain::ForeignNfts::do_create_collection( + collection_id, ALICE, - false + ALICE, + get_collection_config(), + 1000, + Event::ForceCreated { collection: collection_id, owner: ALICE }, )); - assert_ok!(relay_chain::Uniques::mint( - relay_chain::RuntimeOrigin::signed(ALICE), - 2, - 69, - child_account_account_id(1, ALICE) + + assert_ok!(parachain::ForeignNfts::mint( + parachain::RuntimeOrigin::signed(ALICE), + collection_id, + Index(69), + sibling_account_account_id(2, ALICE), + None )); + assert_eq!( - relay_chain::Uniques::owner(2, 69), - Some(child_account_account_id(1, ALICE)) + parachain::ForeignNfts::owner(collection_id, Index(69)), + Some(sibling_account_account_id(2, ALICE)) ); - - let message = Xcm(vec![Transact { - origin_kind: OriginKind::Xcm, - require_weight_at_most: Weight::from_parts(1_000_000_000, 1024 * 1024), - call: parachain::RuntimeCall::from( - pallet_uniques::Call::::create { - collection: (Parent, 2u64).into(), - admin: parent_account_id(), - }, - ) - .encode() - .into(), - }]); - // Send creation. - assert_ok!(RelayChainPalletXcm::send_xcm(Here, Parachain(1), message)); }); - ParaA::execute_with(|| { - // Then transfer + + let foreign_collection_id = + MultiLocation { parents: 1, interior: X2(Parachain(1), GeneralIndex(2)) }.into(); + + ParaB::execute_with(|| { + assert_ok!(parachain::ForeignNfts::do_create_collection( + foreign_collection_id, + ALICE, + ALICE, + get_collection_config(), + 1000, + Event::ForceCreated { collection: foreign_collection_id, owner: ALICE }, + )); + let message = Xcm(vec![ WithdrawAsset((GeneralIndex(2), 69u32).into()), DepositReserveAsset { assets: AllCounted(1).into(), - dest: Parachain(1).into(), + dest: (Parent, Parachain(2)).into(), xcm: Xcm(vec![DepositAsset { assets: AllCounted(1).into(), beneficiary: (AccountId32 { id: ALICE.into(), network: None },).into(), }]), }, ]); + // Send transfer + let alice = AccountId32 { id: ALICE.into(), network: None }; - assert_ok!(ParachainPalletXcm::send_xcm(alice, Parent, message)); + assert_ok!(ParachainPalletXcm::send_xcm(alice, (Parent, Parachain(1)), message)); }); - ParaA::execute_with(|| { - assert_eq!(parachain::Balances::reserved_balance(&parent_account_id()), 1000); + + ParaB::execute_with(|| { assert_eq!( - parachain::ForeignUniques::collection_owner((Parent, 2u64).into()), - Some(parent_account_id()) + parachain::ForeignNfts::owner(foreign_collection_id, Index(69)), + Some(ALICE) ); + + let message = Xcm(vec![TransferAsset { + assets: (foreign_collection_id, 69u32).into(), + beneficiary: (AccountId32 { id: BOB.into(), network: None },).into(), + }]); + + assert_ok!(ParachainPalletXcm::execute( + parachain::RuntimeOrigin::signed(ALICE), + Box::new(VersionedXcm::V3(message)), + Weight::MAX + )); + + assert_eq!(parachain::ForeignNfts::owner(foreign_collection_id, Index(69)), Some(BOB)); + }); + + ParaB::execute_with(|| { + let message: Xcm<_> = Xcm(vec![ + WithdrawAsset((foreign_collection_id, 69u32).into()), + InitiateReserveWithdraw { + assets: AllCounted(1).into(), + reserve: (Parent, Parachain(1)).into(), + xcm: Xcm(vec![DepositAsset { + assets: AllCounted(1).into(), + beneficiary: (AccountId32 { id: BOB.into(), network: None },).into(), + }]), + }, + ]); + + assert_ok!(ParachainPalletXcm::execute( + parachain::RuntimeOrigin::signed(BOB), + Box::new(VersionedXcm::V3(message)), + Weight::MAX + )); + + assert_eq!(parachain::ForeignNfts::owner(foreign_collection_id, Index(69)), None); + }); + + ParaA::execute_with(|| { + assert_eq!(parachain::ForeignNfts::owner(collection_id, Index(69)), Some(BOB)); }); } diff --git a/polkadot/xcm/xcm-simulator/example/src/parachain.rs b/polkadot/xcm/xcm-simulator/example/src/parachain.rs index 9c4cbe8fcb2d8..95f6210158efd 100644 --- a/polkadot/xcm/xcm-simulator/example/src/parachain.rs +++ b/polkadot/xcm/xcm-simulator/example/src/parachain.rs @@ -25,10 +25,10 @@ use frame_support::{ }; use frame_system::EnsureRoot; -use sp_core::{ConstU32, H256}; +use sp_core::{ConstU32, ConstU64, H256}; use sp_runtime::{ - traits::{Get, Hash, IdentityLookup}, - AccountId32, + traits::{Get, Hash, IdentityLookup, Verify}, + AccountId32, MultiSignature, }; use sp_std::prelude::*; @@ -41,7 +41,7 @@ use xcm::{latest::prelude::*, VersionedXcm}; use xcm_builder::{ Account32Hash, AccountId32Aliases, AllowUnpaidExecutionFrom, ConvertedConcreteId, CurrencyAdapter as XcmCurrencyAdapter, EnsureXcmOrigin, FixedRateOfFungible, FixedWeightBounds, - IsConcrete, NativeAsset, NoChecking, NonFungiblesAdapter, ParentIsPreset, + IsConcrete, MultiLocationCollectionId, NativeAsset, NoChecking, NonFungiblesV2Adapter, ParentIsPreset, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, }; @@ -50,10 +50,13 @@ use xcm_executor::{ Config, XcmExecutor, }; +use pallet_nfts::{ItemConfig, PalletFeatures}; + pub type SovereignAccountOf = ( SiblingParachainConvertsVia, AccountId32Aliases, ParentIsPreset, + Account32Hash<(), AccountId>, ); pub type AccountId = AccountId32; @@ -111,59 +114,26 @@ impl pallet_balances::Config for Runtime { type MaxFreezes = ConstU32<0>; } -#[cfg(feature = "runtime-benchmarks")] -pub struct UniquesHelper; -#[cfg(feature = "runtime-benchmarks")] -impl pallet_uniques::BenchmarkHelper for UniquesHelper { - fn collection(i: u16) -> MultiLocation { - GeneralIndex(i as u128).into() - } - fn item(i: u16) -> AssetInstance { - AssetInstance::Index(i as u128) - } -} - -impl pallet_uniques::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type CollectionId = MultiLocation; - type ItemId = AssetInstance; - type Currency = Balances; - type CreateOrigin = ForeignCreators; - type ForceOrigin = frame_system::EnsureRoot; - type CollectionDeposit = frame_support::traits::ConstU128<1_000>; - type ItemDeposit = frame_support::traits::ConstU128<1_000>; - type MetadataDepositBase = frame_support::traits::ConstU128<1_000>; - type AttributeDepositBase = frame_support::traits::ConstU128<1_000>; - type DepositPerByte = frame_support::traits::ConstU128<1>; - type StringLimit = ConstU32<64>; - type KeyLimit = ConstU32<64>; - type ValueLimit = ConstU32<128>; - type Locker = (); - type WeightInfo = (); - #[cfg(feature = "runtime-benchmarks")] - type Helper = UniquesHelper; -} - // `EnsureOriginWithArg` impl for `CreateOrigin` which allows only XCM origins // which are locations containing the class location. -pub struct ForeignCreators; -impl EnsureOriginWithArg for ForeignCreators { +pub struct ForeignNftCreators; +impl EnsureOriginWithArg for ForeignNftCreators { type Success = AccountId; fn try_origin( o: RuntimeOrigin, - a: &MultiLocation, + a: &MultiLocationCollectionId, ) -> sp_std::result::Result { let origin_location = pallet_xcm::EnsureXcm::::try_origin(o.clone())?; - if !a.starts_with(&origin_location) { + if !a.inner().starts_with(&origin_location) { return Err(o) } SovereignAccountOf::convert_location(&origin_location).ok_or(o) } #[cfg(feature = "runtime-benchmarks")] - fn try_successful_origin(a: &MultiLocation) -> Result { - Ok(pallet_xcm::Origin::Xcm(a.clone()).into()) + fn try_successful_origin(a: &MultiLocationCollectionId) -> Result { + Ok(pallet_xcm::Origin::Xcm(a.clone().into()).into()) } } @@ -178,15 +148,9 @@ parameter_types! { pub UniversalLocation: InteriorMultiLocation = Parachain(MsgQueue::parachain_id().into()).into(); } -pub type LocationToAccountId = ( - ParentIsPreset, - SiblingParachainConvertsVia, - AccountId32Aliases, - Account32Hash<(), AccountId>, -); pub type XcmOriginToCallOrigin = ( - SovereignSignedViaLocation, + SovereignSignedViaLocation, SignedAccountId32AsNative, XcmPassthrough, ); @@ -200,14 +164,15 @@ parameter_types! { } pub type LocalAssetTransactor = ( - XcmCurrencyAdapter, LocationToAccountId, AccountId, ()>, - NonFungiblesAdapter< - ForeignUniques, - ConvertedConcreteId, + XcmCurrencyAdapter, SovereignAccountOf, AccountId, ()>, + NonFungiblesV2Adapter< + ForeignNfts, + ConvertedConcreteId, SovereignAccountOf, AccountId, NoChecking, (), + ItemConfig, >, ); @@ -437,7 +402,7 @@ impl pallet_xcm::Config for Runtime { type Currency = Balances; type CurrencyMatcher = (); type TrustedLockers = TrustedLockers; - type SovereignAccountOf = LocationToAccountId; + type SovereignAccountOf = SovereignAccountOf; type MaxLockers = ConstU32<8>; type MaxRemoteLockConsumers = ConstU32<0>; type RemoteLockConsumerIdentifier = (); @@ -447,6 +412,42 @@ impl pallet_xcm::Config for Runtime { type AdminOrigin = EnsureRoot; } +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + +pub type Signature = MultiSignature; +pub type AccountPublic = ::Signer; + +impl pallet_nfts::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type CollectionId = MultiLocationCollectionId; + type ItemId = AssetInstance; + type Currency = Balances; + type CreateOrigin = ForeignNftCreators; + type ForceOrigin = frame_system::EnsureRoot; + type CollectionDeposit = frame_support::traits::ConstU128<1_000>; + type ItemDeposit = frame_support::traits::ConstU128<1_000>; + type MetadataDepositBase = frame_support::traits::ConstU128<1_000>; + type AttributeDepositBase = frame_support::traits::ConstU128<1_000>; + type DepositPerByte = frame_support::traits::ConstU128<1>; + type StringLimit = ConstU32<64>; + type KeyLimit = ConstU32<64>; + type ValueLimit = ConstU32<128>; + type ApprovalsLimit = ConstU32<10>; + type ItemAttributesApprovalsLimit = ConstU32<2>; + type MaxTips = ConstU32<10>; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxAttributesPerCall = ConstU32<2>; + type Features = Features; + type OffchainSignature = Signature; + type OffchainPublic = AccountPublic; + type Locker = (); + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); +} + type Block = frame_system::mocking::MockBlock; construct_runtime!( @@ -456,6 +457,6 @@ construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, MsgQueue: mock_msg_queue::{Pallet, Storage, Event}, PolkadotXcm: pallet_xcm::{Pallet, Call, Event, Origin}, - ForeignUniques: pallet_uniques::{Pallet, Call, Storage, Event}, + ForeignNfts: pallet_nfts::{Pallet, Call, Storage, Event}, } ); diff --git a/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs b/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs index 6f0e92dc91b9a..8e19ae322866f 100644 --- a/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs +++ b/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs @@ -132,18 +132,18 @@ parameter_types! { pub UnitWeightCost: u64 = 1_000; } -pub type LocationToAccountId = ( +pub type SovereignAccountOf = ( ChildParachainConvertsVia, AccountId32Aliases, Account32Hash<(), AccountId>, ); pub type LocalAssetTransactor = ( - XcmCurrencyAdapter, LocationToAccountId, AccountId, ()>, + XcmCurrencyAdapter, SovereignAccountOf, AccountId, ()>, NonFungiblesAdapter< Uniques, ConvertedConcreteId, JustTry>, - LocationToAccountId, + SovereignAccountOf, AccountId, NoChecking, (), @@ -151,7 +151,7 @@ pub type LocalAssetTransactor = ( ); type LocalOriginConverter = ( - SovereignSignedViaLocation, + SovereignSignedViaLocation, ChildParachainAsNative, SignedAccountId32AsNative, ChildSystemParachainAsSuperuser, @@ -222,7 +222,7 @@ impl pallet_xcm::Config for Runtime { type Currency = Balances; type CurrencyMatcher = IsConcrete; type TrustedLockers = (); - type SovereignAccountOf = LocationToAccountId; + type SovereignAccountOf = SovereignAccountOf; type MaxLockers = ConstU32<8>; type MaxRemoteLockConsumers = ConstU32<0>; type RemoteLockConsumerIdentifier = ();