diff --git a/Cargo.lock b/Cargo.lock index 66ee61b879..1bd979862e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5369,6 +5369,7 @@ dependencies = [ "module-transaction-pause", "module-transaction-payment", "module-xcm-interface", + "module-xnft", "nutsfinance-stable-asset", "orml-auction", "orml-authority", @@ -7354,6 +7355,25 @@ dependencies = [ "staging-xcm-executor", ] +[[package]] +name = "module-xnft" +version = "0.1.0-dev" +dependencies = [ + "acala-primitives", + "cumulus-primitives-core", + "frame-support", + "frame-system", + "log", + "module-nft", + "orml-nft", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-executor", +] + [[package]] name = "multiaddr" version = "0.17.1" diff --git a/Cargo.toml b/Cargo.toml index e2995bb797..8a8e9bd2cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -210,6 +210,7 @@ module-incentives = { path = "modules/incentives", default-features = false } module-liquid-crowdloan = { path = "modules/liquid-crowdloan", default-features = false } module-loans = { path = "modules/loans", default-features = false } module-nft = { path = "modules/nft", default-features = false } +module-xnft = { path = "modules/xnft", default-features = false } module-nominees-election = { path = "modules/nominees-election", default-features = false } module-prices = { path = "modules/prices", default-features = false } module-relaychain = { path = "modules/relaychain", default-features = false } diff --git a/modules/nft/src/lib.rs b/modules/nft/src/lib.rs index 7d4519e84d..10ae107c37 100644 --- a/modules/nft/src/lib.rs +++ b/modules/nft/src/lib.rs @@ -388,7 +388,7 @@ pub mod module { impl Pallet { #[require_transactional] - fn do_transfer(from: &T::AccountId, to: &T::AccountId, token: (ClassIdOf, TokenIdOf)) -> DispatchResult { + pub fn do_transfer(from: &T::AccountId, to: &T::AccountId, token: (ClassIdOf, TokenIdOf)) -> DispatchResult { let class_info = orml_nft::Pallet::::classes(token.0).ok_or(Error::::ClassIdNotFound)?; let data = class_info.data; ensure!( diff --git a/modules/xnft/Cargo.toml b/modules/xnft/Cargo.toml new file mode 100644 index 0000000000..4fe0d3649c --- /dev/null +++ b/modules/xnft/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "module-xnft" +description = "XCM NFT PoC" +version = "0.1.0-dev" +authors = ["Unique Network Developers"] +edition = "2021" + +[dependencies] +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +cumulus-primitives-core = { workspace = true } + +xcm = { workspace = true } +xcm-executor = { workspace = true } + +primitives = { workspace = true } + +module-nft = { workspace = true } +orml-nft = { workspace = true } + +log = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "scale-info/std", + "sp-std/std", + "xcm-executor/std", + "xcm/std", + "module-nft/std", + "orml-nft/std", + "cumulus-primitives-core/std", +] +try-runtime = ["frame-support/try-runtime", "frame-system/try-runtime"] diff --git a/modules/xnft/src/impl_transactor.rs b/modules/xnft/src/impl_transactor.rs new file mode 100644 index 0000000000..37c7283185 --- /dev/null +++ b/modules/xnft/src/impl_transactor.rs @@ -0,0 +1,135 @@ +// This file is part of Acala. + +// Copyright (C) 2023 Unique Network. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{xcm_helpers::ClassLocality, *}; + +const LOG_TARGET: &str = "xcm::module_xnft::transactor"; + +impl TransactAsset for Pallet +where + TokenIdOf: TryFrom, + ClassIdOf: TryFrom, +{ + fn can_check_in( + _origin: &xcm::v3::MultiLocation, + _what: &MultiAsset, + _context: &xcm::v3::XcmContext, + ) -> xcm::v3::Result { + Err(xcm::v3::Error::Unimplemented) + } + + fn check_in(_origin: &xcm::v3::MultiLocation, _what: &MultiAsset, _context: &xcm::v3::XcmContext) {} + + fn can_check_out( + _dest: &xcm::v3::MultiLocation, + _what: &MultiAsset, + _context: &xcm::v3::XcmContext, + ) -> xcm::v3::Result { + Err(xcm::v3::Error::Unimplemented) + } + + fn check_out(_dest: &xcm::v3::MultiLocation, _what: &MultiAsset, _context: &xcm::v3::XcmContext) {} + + fn deposit_asset( + what: &MultiAsset, + who: &xcm::v3::MultiLocation, + context: Option<&xcm::v3::XcmContext>, + ) -> XcmResult { + log::trace!( + target: LOG_TARGET, + "deposit_asset what: {:?}, who: {:?}, context: {:?}", + what, + who, + context, + ); + + let Fungibility::NonFungible(asset_instance) = what.fun else { + return Err(XcmExecutorError::AssetNotHandled.into()); + }; + + let class_locality = Self::asset_to_collection(&what.id)?; + + let to = >::convert_location(who).ok_or(XcmExecutorError::AccountIdConversionFailed)?; + + match class_locality { + ClassLocality::Foreign(class_id) => Self::deposit_foreign_asset(&to, class_id, &asset_instance), + ClassLocality::Local(class_id) => Self::deposit_local_asset(&to, class_id, &asset_instance), + } + } + + fn withdraw_asset( + what: &MultiAsset, + who: &xcm::v3::MultiLocation, + maybe_context: Option<&xcm::v3::XcmContext>, + ) -> Result { + log::trace!( + target: LOG_TARGET, + "withdraw_asset what: {:?}, who: {:?}, maybe_context: {:?}", + what, + who, + maybe_context, + ); + + let Fungibility::NonFungible(asset_instance) = what.fun else { + return Err(XcmExecutorError::AssetNotHandled.into()); + }; + + let class_locality = Self::asset_to_collection(&what.id)?; + + let from = >::convert_location(who).ok_or(XcmExecutorError::AccountIdConversionFailed)?; + + let token = Self::asset_instance_to_token(class_locality, &asset_instance) + .ok_or(XcmExecutorError::InstanceConversionFailed)?; + + >::do_transfer(&from, &Self::account_id(), token) + .map(|_| what.clone().into()) + .map_err(|_| XcmError::FailedToTransactAsset("non-fungible item withdraw failed")) + } + + fn internal_transfer_asset( + asset: &MultiAsset, + from: &xcm::v3::MultiLocation, + to: &xcm::v3::MultiLocation, + context: &xcm::v3::XcmContext, + ) -> Result { + log::trace!( + target: LOG_TARGET, + "internal_transfer_asset: {:?}, from: {:?}, to: {:?}, context: {:?}", + asset, + from, + to, + context + ); + + let Fungibility::NonFungible(asset_instance) = asset.fun else { + return Err(XcmExecutorError::AssetNotHandled.into()); + }; + + let class_locality = Self::asset_to_collection(&asset.id)?; + + let from = >::convert_location(from).ok_or(XcmExecutorError::AccountIdConversionFailed)?; + let to = >::convert_location(to).ok_or(XcmExecutorError::AccountIdConversionFailed)?; + + let token = Self::asset_instance_to_token(class_locality, &asset_instance) + .ok_or(XcmExecutorError::InstanceConversionFailed)?; + + >::do_transfer(&from, &to, token) + .map(|_| asset.clone().into()) + .map_err(|_| XcmError::FailedToTransactAsset("non-fungible item internal transfer failed")) + } +} diff --git a/modules/xnft/src/lib.rs b/modules/xnft/src/lib.rs new file mode 100644 index 0000000000..c8a2105177 --- /dev/null +++ b/modules/xnft/src/lib.rs @@ -0,0 +1,169 @@ +// This file is part of Acala. + +// Copyright (C) 2023 Unique Network. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] + +use cumulus_primitives_core::ParaId; +use frame_support::{ensure, pallet_prelude::*, PalletId}; +use frame_system::pallet_prelude::*; +use module_nft::{ClassIdOf, TokenIdOf}; +use sp_runtime::{traits::AccountIdConversion, DispatchResult}; +use sp_std::boxed::Box; +use xcm::{ + v3::{ + AssetId, AssetInstance, Error as XcmError, Fungibility, InteriorMultiLocation, Junction::*, MultiAsset, + Result as XcmResult, + }, + VersionedAssetId, +}; +use xcm_executor::traits::{ConvertLocation, Error as XcmExecutorError, TransactAsset}; + +pub mod impl_transactor; +pub mod xcm_helpers; + +pub use pallet::*; + +pub type ConverterOf = ::LocationToAccountId; +pub type ModuleNftPallet = module_nft::Pallet; +pub type OrmlNftPallet = orml_nft::Pallet; + +#[frame_support::pallet] +pub mod pallet { + + use super::*; + use module_nft::WeightInfo as _; + use primitives::nft::{ClassProperty, Properties}; + + #[pallet::config] + pub trait Config: frame_system::Config + module_nft::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type PalletId: Get; + + type LocationToAccountId: ConvertLocation; + + type SelfParaId: Get; + + type NtfPalletLocation: Get; + + type RegisterOrigin: EnsureOrigin; + } + + /// Error for non-fungible-token module. + #[pallet::error] + pub enum Error { + /// The asset is already registered. + AssetAlreadyRegistered, + + /// The given asset ID could not be converted into the current XCM version. + BadAssetId, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + AssetRegistered { + asset_id: Box, + collection_id: ClassIdOf, + }, + } + + #[pallet::storage] + #[pallet::getter(fn foreign_asset_to_class)] + pub type ForeignAssetToClass = StorageMap<_, Twox64Concat, xcm::v3::AssetId, ClassIdOf, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn class_to_foreign_asset)] + pub type ClassToForeignAsset = StorageMap<_, Twox64Concat, ClassIdOf, xcm::v3::AssetId, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn asset_instance_to_item)] + pub type AssetInstanceToItem = StorageDoubleMap< + _, + Twox64Concat, + ClassIdOf, + Blake2_128Concat, + xcm::v3::AssetInstance, + TokenIdOf, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn item_to_asset_instance)] + pub type ItemToAssetInstance = StorageDoubleMap< + _, + Twox64Concat, + ClassIdOf, + Blake2_128Concat, + TokenIdOf, + xcm::v3::AssetInstance, + OptionQuery, + >; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(1_000_000, 0) + .saturating_add(>::create_class()) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(2)))] + pub fn register_asset(origin: OriginFor, versioned_foreign_asset: Box) -> DispatchResult { + T::RegisterOrigin::ensure_origin(origin)?; + + let foreign_asset: AssetId = versioned_foreign_asset + .as_ref() + .clone() + .try_into() + .map_err(|()| Error::::BadAssetId)?; + + ensure!( + !>::contains_key(foreign_asset), + >::AssetAlreadyRegistered, + ); + + let properties = + Properties(ClassProperty::Mintable | ClassProperty::Burnable | ClassProperty::Transferable); + let data = module_nft::ClassData { + deposit: Default::default(), + properties, + attributes: Default::default(), + }; + let collection_id = orml_nft::Pallet::::create_class(&Self::account_id(), Default::default(), data)?; + + >::insert(foreign_asset, collection_id); + >::insert(collection_id, foreign_asset); + + Self::deposit_event(Event::AssetRegistered { + asset_id: versioned_foreign_asset, + collection_id, + }); + + Ok(()) + } + } +} + +impl Pallet { + pub fn account_id() -> T::AccountId { + ::PalletId::get().into_account_truncating() + } +} diff --git a/modules/xnft/src/xcm_helpers.rs b/modules/xnft/src/xcm_helpers.rs new file mode 100644 index 0000000000..9d20412be1 --- /dev/null +++ b/modules/xnft/src/xcm_helpers.rs @@ -0,0 +1,118 @@ +// This file is part of Acala. + +// Copyright (C) 2023 Unique Network. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::*; +use module_nft::BalanceOf; +use primitives::nft::Attributes; +use sp_std::vec::Vec; +use xcm::v3::AssetId::Concrete; +use xcm_executor::traits::Error as MatchError; + +pub enum ClassLocality { + Local(ClassIdOf), + Foreign(ClassIdOf), +} + +impl Pallet +where + TokenIdOf: TryFrom, + ClassIdOf: TryFrom, +{ + pub fn asset_to_collection(asset: &AssetId) -> Result, MatchError> { + Self::foreign_asset_to_class(asset) + .map(ClassLocality::Foreign) + .or_else(|| Self::local_asset_to_class(asset).map(ClassLocality::Local)) + .ok_or(MatchError::AssetIdConversionFailed) + } + + fn local_asset_to_class(asset: &AssetId) -> Option> { + let Concrete(asset_location) = asset else { + return None; + }; + + let prefix = if asset_location.parents == 0 { + T::NtfPalletLocation::get() + } else if asset_location.parents == 1 { + T::NtfPalletLocation::get() + .pushed_front_with(Parachain(T::SelfParaId::get().into())) + .ok()? + } else { + return None; + }; + + match asset_location.interior.match_and_split(&prefix) { + Some(GeneralIndex(index)) => { + let class_id = (*index).try_into().ok()?; + Self::class_to_foreign_asset(class_id).is_none().then_some(class_id) + } + _ => None, + } + } + + pub fn deposit_foreign_asset(to: &T::AccountId, asset: ClassIdOf, asset_instance: &AssetInstance) -> XcmResult { + match Self::asset_instance_to_item(asset, asset_instance) { + Some(token_id) => >::do_transfer(&Self::account_id(), to, (asset, token_id)) + .map_err(|_| XcmError::FailedToTransactAsset("non-fungible foreign item deposit failed")), + None => { + let token_id = >::mint( + to, + asset, + Vec::new(), + module_nft::TokenData::> { + deposit: 0u32.into(), + attributes: Attributes::new(), + }, + ) + .map_err(|_| XcmError::FailedToTransactAsset("non-fungible new foreign item deposit failed"))?; + + >::insert(asset, asset_instance, token_id); + >::insert(asset, token_id, asset_instance); + + Ok(()) + } + } + } + + pub fn deposit_local_asset(to: &T::AccountId, asset: ClassIdOf, asset_instance: &AssetInstance) -> XcmResult { + let token_id = Self::convert_asset_instance(asset_instance)?; + >::do_transfer(&Self::account_id(), to, (asset, token_id)) + .map_err(|_| XcmError::FailedToTransactAsset("non-fungible local item deposit failed")) + } + + pub fn asset_instance_to_token( + class_locality: ClassLocality, + asset_instance: &AssetInstance, + ) -> Option<(ClassIdOf, TokenIdOf)> { + match class_locality { + ClassLocality::Foreign(class_id) => { + Self::asset_instance_to_item(class_id, asset_instance).map(|token_id| (class_id, token_id)) + } + ClassLocality::Local(class_id) => Self::convert_asset_instance(asset_instance) + .map(|token_id| (class_id, token_id)) + .ok(), + } + } + + fn convert_asset_instance(asset: &AssetInstance) -> Result, MatchError> { + let AssetInstance::Index(index) = asset else { + return Err(MatchError::InstanceConversionFailed); + }; + + (*index).try_into().map_err(|_| MatchError::InstanceConversionFailed) + } +} diff --git a/node/cli/build.rs b/node/cli/build.rs index 32104db6f5..3eabf6b52d 100644 --- a/node/cli/build.rs +++ b/node/cli/build.rs @@ -25,6 +25,7 @@ fn main() { "../../evm-tests", "../../ecosystem-modules/stable-asset", "../../launch", + "../../modules/xnft", "../../orml", "../../predeploy-contracts", "../../ts-tests", diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index b62e6e866c..52b1eb0c8d 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -351,6 +351,9 @@ pub type EnsureRootOrThreeFourthsTechnicalCommittee = EitherOfDiverse< pallet_collective::EnsureProportionAtLeast, >; +pub type EnsureRootOrOneTechnicalCommittee = + EitherOfDiverse, pallet_collective::EnsureMember>; + /// The type used to represent the kinds of proxying allowed. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub enum ProxyType { diff --git a/runtime/karura/Cargo.toml b/runtime/karura/Cargo.toml index 355bf47f74..da0f0671d4 100644 --- a/runtime/karura/Cargo.toml +++ b/runtime/karura/Cargo.toml @@ -122,6 +122,7 @@ module-support = { workspace = true } module-transaction-pause = { workspace = true } module-transaction-payment = { workspace = true } module-xcm-interface = { workspace = true } +module-xnft = { workspace = true } primitives = { workspace = true } runtime-common = { workspace = true } @@ -257,6 +258,7 @@ std = [ "module-transaction-pause/std", "module-transaction-payment/std", "module-xcm-interface/std", + "module-xnft/std", "primitives/std", "runtime-common/std", @@ -396,6 +398,7 @@ try-runtime = [ "module-transaction-pause/try-runtime", "module-transaction-payment/try-runtime", "module-xcm-interface/try-runtime", + "module-xnft/try-runtime", "primitives/try-runtime", diff --git a/runtime/karura/src/lib.rs b/runtime/karura/src/lib.rs index 200a7fa837..531c76e1fe 100644 --- a/runtime/karura/src/lib.rs +++ b/runtime/karura/src/lib.rs @@ -31,6 +31,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use parity_scale_codec::{Decode, DecodeLimit, Encode}; +use runtime_common::EnsureRootOrOneTechnicalCommittee; use scale_info::TypeInfo; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; @@ -174,6 +175,7 @@ parameter_types! { // Treasury reserve pub const TreasuryReservePalletId: PalletId = PalletId(*b"aca/reve"); pub const NftPalletId: PalletId = PalletId(*b"aca/aNFT"); + pub const XnftPalletId: PalletId = PalletId(*b"aca/xNFT"); // Vault all unrleased native token. pub UnreleasedNativeVaultAccountId: AccountId = PalletId(*b"aca/urls").into_account_truncating(); // This Pallet is only used to payment fee pool, it's not added to whitelist by design. @@ -1333,6 +1335,15 @@ impl orml_nft::Config for Runtime { type MaxTokenMetadata = ConstU32<1024>; } +impl module_xnft::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type PalletId = XnftPalletId; + type LocationToAccountId = xcm_config::LocationToAccountId; + type SelfParaId = ParachainInfo; + type NtfPalletLocation = xcm_config::NftPalletLocation; + type RegisterOrigin = EnsureRootOrOneTechnicalCommittee; +} + impl InstanceFilter for ProxyType { fn filter(&self, c: &RuntimeCall) -> bool { match self { @@ -1833,6 +1844,7 @@ construct_runtime!( Incentives: module_incentives = 120, NFT: module_nft = 121, AssetRegistry: module_asset_registry = 122, + XNFT: module_xnft = 123, // Smart contracts EVM: module_evm = 130, diff --git a/runtime/karura/src/xcm_config.rs b/runtime/karura/src/xcm_config.rs index 75d1b1ee96..3274fea58e 100644 --- a/runtime/karura/src/xcm_config.rs +++ b/runtime/karura/src/xcm_config.rs @@ -21,7 +21,7 @@ use super::{ AccountId, AllPalletsWithSystem, AssetIdMapping, AssetIdMaps, Balance, Balances, Convert, Currencies, CurrencyId, EvmAddressMapping, ExistentialDeposits, FixedRateOfAsset, GetNativeCurrencyId, KaruraTreasuryAccount, NativeTokenExistentialDeposit, ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, - RuntimeOrigin, UnknownTokens, XcmInterface, XcmpQueue, KAR, KUSD, LKSM, TAI, + RuntimeOrigin, UnknownTokens, XcmInterface, XcmpQueue, KAR, KUSD, LKSM, TAI, XNFT, }; use cumulus_primitives_core::ParaId; use frame_support::{ @@ -47,6 +47,7 @@ parameter_types! { pub const RelayNetwork: NetworkId = NetworkId::Kusama; pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get()), Parachain(ParachainInfo::parachain_id().into())); + pub NftPalletLocation: InteriorMultiLocation = X1(PalletInstance(121)); pub CheckingAccount: AccountId = PolkadotXcm::check_account(); } @@ -298,16 +299,19 @@ impl orml_xtokens::Config for Runtime { type ReserveProvider = AbsoluteReserveProvider; } -pub type LocalAssetTransactor = MultiCurrencyAdapter< - Currencies, - UnknownTokens, - IsNativeConcrete, - AccountId, - LocationToAccountId, - CurrencyId, - CurrencyIdConvert, - DepositToAlternative, ->; +pub type LocalAssetTransactor = ( + XNFT, + MultiCurrencyAdapter< + Currencies, + UnknownTokens, + IsNativeConcrete, + AccountId, + LocationToAccountId, + CurrencyId, + CurrencyIdConvert, + DepositToAlternative, + >, +); pub struct CurrencyIdConvert;