difftreelog
Merge pull request #355 from UniqueNetwork/feature/nft-children
in: master
Structure children map
12 files changed
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -353,6 +353,8 @@
MustBeTokenOwner,
/// No permission to perform action
NoPermission,
+ /// Destroying only empty collections is allowed
+ CantDestroyNotEmptyCollection,
/// Collection is not in mint mode.
PublicMintingNotAllowed,
/// Address is not in allow list.
@@ -1268,6 +1270,18 @@
budget: &dyn Budget,
) -> DispatchResult;
+ fn nest(
+ &self,
+ under: TokenId,
+ to_nest: (CollectionId, TokenId)
+ );
+
+ fn unnest(
+ &self,
+ under: TokenId,
+ to_nest: (CollectionId, TokenId)
+ );
+
fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;
fn collection_tokens(&self) -> Vec<TokenId>;
fn token_exists(&self, token: TokenId) -> bool;
pallets/fungible/src/common.rsdiffbeforeafterboth--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -298,6 +298,18 @@
fail!(<Error<T>>::FungibleDisallowsNesting)
}
+ fn nest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
+ fn unnest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
fn collection_tokens(&self) -> Vec<TokenId> {
vec![TokenId::default()]
}
pallets/fungible/src/lib.rsdiffbeforeafterboth--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -25,8 +25,8 @@
budget::Budget,
};
use pallet_common::{
- Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
- dispatch::CollectionDispatch, eth::collection_id_to_address,
+ Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
+ eth::collection_id_to_address,
};
use pallet_evm::Pallet as PalletEvm;
use pallet_structure::Pallet as PalletStructure;
@@ -145,6 +145,10 @@
) -> DispatchResult {
let id = collection.id;
+ if Self::collection_has_tokens(id) {
+ return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+ }
+
// =========
PalletCommon::destroy_collection(collection.0, sender)?;
@@ -155,6 +159,10 @@
Ok(())
}
+ fn collection_has_tokens(collection_id: CollectionId) -> bool {
+ <TotalSupply<T>>::get(collection_id) != 0
+ }
+
pub fn burn(
collection: &FungibleHandle<T>,
owner: &T::CrossAccountId,
@@ -176,6 +184,11 @@
if balance == 0 {
<Balance<T>>::remove((collection.id, owner));
+ <PalletStructure<T>>::unnest_if_nested(
+ owner,
+ collection.id,
+ TokenId::default()
+ );
} else {
<Balance<T>>::insert((collection.id, owner), balance);
}
@@ -229,25 +242,25 @@
None
};
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
+ // =========
- dispatch.check_nesting(
- from.clone(),
- (collection.id, TokenId::default()),
- target.1,
- nesting_budget,
- )?;
- }
+ <PalletStructure<T>>::nest_if_sent_to_token(
+ from.clone(),
+ to,
+ collection.id,
+ TokenId::default(),
+ nesting_budget
+ )?;
- // =========
-
if let Some(balance_to) = balance_to {
// from != to
if balance_from == 0 {
<Balance<T>>::remove((collection.id, from));
+ <PalletStructure<T>>::unnest_if_nested(
+ from,
+ collection.id,
+ TokenId::default()
+ );
} else {
<Balance<T>>::insert((collection.id, from), balance_from);
}
@@ -306,18 +319,13 @@
}
for (to, _) in balances.iter() {
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
-
- dispatch.check_nesting(
- sender.clone(),
- (collection.id, TokenId::default()),
- target.1,
- nesting_budget,
- )?;
- }
+ <PalletStructure<T>>::check_nesting(
+ sender.clone(),
+ to,
+ collection.id,
+ TokenId::default(),
+ nesting_budget,
+ )?;
}
// =========
@@ -325,7 +333,7 @@
<TotalSupply<T>>::insert(collection.id, total_supply);
for (user, amount) in balances {
<Balance<T>>::insert((collection.id, &user), amount);
-
+ <PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId::default());
<PalletEvm<T>>::deposit_log(
ERC20Events::Transfer {
from: H160::default(),
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -353,6 +353,22 @@
<Pallet<T>>::check_nesting(self, sender, from, under, budget)
}
+ fn nest(
+ &self,
+ under: TokenId,
+ to_nest: (CollectionId, TokenId)
+ ) {
+ <Pallet<T>>::nest((self.id, under), to_nest);
+ }
+
+ fn unnest(
+ &self,
+ under: TokenId,
+ to_unnest: (CollectionId, TokenId)
+ ) {
+ <Pallet<T>>::unnest((self.id, under), to_unnest);
+ }
+
fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
<Owned<T>>::iter_prefix((self.id, account))
.map(|(id, _)| id)
pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -27,7 +27,7 @@
use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
use pallet_common::{
Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,
- dispatch::CollectionDispatch, eth::collection_id_to_address,
+ eth::collection_id_to_address,
};
use pallet_structure::Pallet as PalletStructure;
use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
@@ -76,6 +76,8 @@
NotNonfungibleDataUsedToMintFungibleCollectionToken,
/// Used amount > 1 with NFT
NonfungibleItemsHaveNoAmount,
+ /// Unable to burn NFT with children
+ CantBurnNftWithChildren,
}
#[pallet::config]
@@ -127,7 +129,20 @@
QueryKind = ValueQuery,
>;
+ /// Used to enumerate token's children
#[pallet::storage]
+ #[pallet::getter(fn token_children)]
+ pub type TokenChildren<T: Config> = StorageNMap<
+ Key = (
+ Key<Twox64Concat, CollectionId>,
+ Key<Twox64Concat, TokenId>,
+ Key<Twox64Concat, (CollectionId, TokenId)>,
+ ),
+ Value = bool,
+ QueryKind = ValueQuery,
+ >;
+
+ #[pallet::storage]
pub type AccountBalance<T: Config> = StorageNMap<
Key = (
Key<Twox64Concat, CollectionId>,
@@ -277,11 +292,16 @@
) -> DispatchResult {
let id = collection.id;
+ if Self::collection_has_tokens(id) {
+ return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+ }
+
// =========
PalletCommon::destroy_collection(collection.0, sender)?;
<TokenData<T>>::remove_prefix((id,), None);
+ <TokenChildren<T>>::remove_prefix((id,), None);
<Owned<T>>::remove_prefix((id,), None);
<TokensMinted<T>>::remove(id);
<TokensBurnt<T>>::remove(id);
@@ -307,6 +327,10 @@
collection.check_allowlist(sender)?;
}
+ if Self::token_has_children(collection.id, token) {
+ return Err(<Error<T>>::CantBurnNftWithChildren.into());
+ }
+
let burnt = <TokensBurnt<T>>::get(collection.id)
.checked_add(1)
.ok_or(ArithmeticError::Overflow)?;
@@ -315,13 +339,20 @@
.checked_sub(1)
.ok_or(ArithmeticError::Overflow)?;
+ // =========
+
if balance == 0 {
<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));
} else {
<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);
}
- // =========
+ <PalletStructure<T>>::unnest_if_nested(
+ &token_data.owner,
+ collection.id,
+ token
+ );
+
<Owned<T>>::remove((collection.id, &token_data.owner, token));
<TokensBurnt<T>>::insert(collection.id, burnt);
<TokenData<T>>::remove((collection.id, token));
@@ -553,20 +584,21 @@
None
};
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
+ <PalletStructure<T>>::nest_if_sent_to_token(
+ from.clone(),
+ to,
+ collection.id,
+ token,
+ nesting_budget
+ )?;
- dispatch.check_nesting(
- from.clone(),
- (collection.id, token),
- target.1,
- nesting_budget,
- )?;
- }
+ // =========
- // =========
+ <PalletStructure<T>>::unnest_if_nested(
+ from,
+ collection.id,
+ token
+ );
<TokenData<T>>::insert(
(collection.id, token),
@@ -653,17 +685,14 @@
for (i, data) in data.iter().enumerate() {
let token = TokenId(first_token + i as u32 + 1);
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
- dispatch.check_nesting(
- sender.clone(),
- (collection.id, token),
- target.1,
- nesting_budget,
- )?;
- }
+
+ <PalletStructure<T>>::check_nesting(
+ sender.clone(),
+ &data.owner,
+ collection.id,
+ token,
+ nesting_budget,
+ )?;
}
// =========
@@ -680,6 +709,8 @@
},
);
+ <PalletStructure<T>>::nest_if_sent_to_token_unchecked(&data.owner, collection.id, TokenId(token));
+
if let Err(e) = Self::set_token_properties(
collection,
sender,
@@ -927,6 +958,33 @@
Ok(())
}
+ fn nest(
+ under: (CollectionId, TokenId),
+ to_nest: (CollectionId, TokenId),
+ ) {
+ <TokenChildren<T>>::insert(
+ (under.0, under.1, (to_nest.0, to_nest.1)),
+ true
+ );
+ }
+
+ fn unnest(
+ under: (CollectionId, TokenId),
+ to_unnest: (CollectionId, TokenId),
+ ) {
+ <TokenChildren<T>>::remove(
+ (under.0, under.1, to_unnest)
+ );
+ }
+
+ fn collection_has_tokens(collection_id: CollectionId) -> bool {
+ <TokenData<T>>::iter_prefix((collection_id,)).next().is_some()
+ }
+
+ fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {
+ <TokenChildren<T>>::iter_prefix((collection_id, token_id)).next().is_some()
+ }
+
/// Delegated to `create_multiple_items`
pub fn create_item(
collection: &NonfungibleHandle<T>,
pallets/proxy-rmrk-core/src/misc.rsdiffbeforeafterboth--- a/pallets/proxy-rmrk-core/src/misc.rs
+++ b/pallets/proxy-rmrk-core/src/misc.rs
@@ -26,6 +26,18 @@
}
}
+pub trait RmrkRebind<T, S> {
+ fn rebind(&self) -> BoundedVec<u8, S>;
+}
+
+impl<T, S> RmrkRebind<T, S> for BoundedVec<u8, T> where BoundedVec<u8, S>: TryFrom<Vec<u8>> {
+ fn rebind(&self) -> BoundedVec<u8, S> {
+ BoundedVec::<u8, S>::try_from(
+ self.clone().into_inner()
+ ).unwrap_or_default()
+ }
+}
+
#[derive(Encode, Decode, PartialEq, Eq)]
pub enum CollectionType {
Regular,
pallets/refungible/src/common.rsdiffbeforeafterboth--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -313,6 +313,18 @@
fail!(<Error<T>>::RefungibleDisallowsNesting)
}
+ fn nest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
+ fn unnest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
<Owned<T>>::iter_prefix((self.id, account))
.map(|(id, _)| id)
pallets/refungible/src/lib.rsdiffbeforeafterboth--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -23,8 +23,7 @@
};
use pallet_evm::account::CrossAccountId;
use pallet_common::{
- Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
- dispatch::CollectionDispatch,
+ Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
};
use pallet_structure::Pallet as PalletStructure;
use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
@@ -211,6 +210,10 @@
) -> DispatchResult {
let id = collection.id;
+ if Self::collection_has_tokens(id) {
+ return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+ }
+
// =========
PalletCommon::destroy_collection(collection.0, sender)?;
@@ -226,6 +229,10 @@
Ok(())
}
+ fn collection_has_tokens(collection_id: CollectionId) -> bool {
+ <TokenData<T>>::iter_prefix((collection_id,)).next().is_some()
+ }
+
pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {
let burnt = <TokensBurnt<T>>::get(collection.id)
.checked_add(1)
@@ -265,6 +272,7 @@
// =========
<Owned<T>>::remove((collection.id, owner, token));
+ <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
<AccountBalance<T>>::insert((collection.id, owner), account_balance);
Self::burn_token(collection, token)?;
<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
@@ -292,6 +300,7 @@
if balance == 0 {
<Owned<T>>::remove((collection.id, owner, token));
+ <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
<Balance<T>>::remove((collection.id, token, owner));
<AccountBalance<T>>::insert((collection.id, owner), account_balance);
} else {
@@ -372,25 +381,25 @@
None
};
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
+ // =========
- dispatch.check_nesting(
- from.clone(),
- (collection.id, token),
- target.1,
- nesting_budget,
- )?;
- }
-
- // =========
+ <PalletStructure<T>>::nest_if_sent_to_token(
+ from.clone(),
+ to,
+ collection.id,
+ token,
+ nesting_budget
+ )?;
if let Some(balance_to) = balance_to {
// from != to
if balance_from == 0 {
<Balance<T>>::remove((collection.id, token, from));
+ <PalletStructure<T>>::unnest_if_nested(
+ from,
+ collection.id,
+ token
+ );
} else {
<Balance<T>>::insert((collection.id, token, from), balance_from);
}
@@ -488,18 +497,14 @@
for (i, token) in data.iter().enumerate() {
let token_id = TokenId(first_token_id + i as u32 + 1);
for (to, _) in token.users.iter() {
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
- dispatch.check_nesting(
- sender.clone(),
- (collection.id, token_id),
- target.1,
- nesting_budget,
- )?;
- }
+ <PalletStructure<T>>::check_nesting(
+ sender.clone(),
+ to,
+ collection.id,
+ token_id,
+ nesting_budget,
+ )?;
}
}
@@ -519,12 +524,15 @@
const_data: token.const_data,
},
);
+
for (user, amount) in token.users.into_iter() {
if amount == 0 {
continue;
}
<Balance<T>>::insert((collection.id, token_id, &user), amount);
<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);
+ <PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId(token_id));
+
// TODO: ERC20 transfer event
<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(
collection.id,
pallets/structure/src/lib.rsdiffbeforeafterboth--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -1,8 +1,9 @@
#![cfg_attr(not(feature = "std"), no_std)]
+use pallet_common::CommonCollectionOperations;
use sp_std::collections::btree_set::BTreeSet;
-use frame_support::dispatch::DispatchError;
+use frame_support::dispatch::{DispatchError, DispatchResult};
use frame_support::fail;
pub use pallet::*;
use pallet_common::{dispatch::CollectionDispatch, CollectionHandle};
@@ -155,8 +156,8 @@
budget: &dyn Budget,
) -> Result<bool, DispatchError> {
let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) {
- Some((collection, token)) => Parent::Token(collection, token),
- None => Parent::User(user),
+ Some((collection, token)) => Self::find_topmost_owner(collection, token, budget)?,
+ None => user,
};
// Tried to nest token in itself
@@ -171,10 +172,10 @@
return Err(<Error<T>>::OuroborosDetected.into())
}
// Found needed parent, token is indirecty owned
- v if v == target_parent => return Ok(true),
+ Parent::User(user) if user == target_parent => return Ok(true),
// Token is owned by other user
Parent::User(_) => return Ok(false),
- Parent::TokenNotFound => return Ok(false),
+ Parent::TokenNotFound => return Err(<Error<T>>::TokenNotFound.into()),
// Continue parent chain
Parent::Token(_, _) => {}
}
@@ -182,4 +183,113 @@
Err(<Error<T>>::DepthLimit.into())
}
+
+ pub fn check_nesting(
+ from: T::CrossAccountId,
+ under: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId,
+ nesting_budget: &dyn Budget
+ ) -> DispatchResult {
+ Self::try_exec_if_owner_is_valid_nft(
+ under,
+ |d, parent_id| d.check_nesting(
+ from,
+ (collection_id, token_id),
+ parent_id,
+ nesting_budget
+ )
+ )
+ }
+
+ pub fn nest_if_sent_to_token(
+ from: T::CrossAccountId,
+ under: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId,
+ nesting_budget: &dyn Budget
+ ) -> DispatchResult {
+ Self::try_exec_if_owner_is_valid_nft(
+ under,
+ |d, parent_id| {
+ d.check_nesting(
+ from,
+ (collection_id, token_id),
+ parent_id,
+ nesting_budget
+ )?;
+
+ d.nest(parent_id, (collection_id, token_id));
+
+ Ok(())
+ }
+ )
+ }
+
+ pub fn nest_if_sent_to_token_unchecked(
+ owner: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId
+ ) {
+ Self::exec_if_owner_is_valid_nft(
+ owner,
+ |d, parent_id| d.nest(
+ parent_id,
+ (collection_id, token_id)
+ )
+ );
+ }
+
+ pub fn unnest_if_nested(
+ owner: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId
+ ) {
+ Self::exec_if_owner_is_valid_nft(
+ owner,
+ |d, parent_id| d.unnest(
+ parent_id,
+ (collection_id, token_id)
+ )
+ );
+ }
+
+ fn exec_if_owner_is_valid_nft(
+ account: &T::CrossAccountId,
+ action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId)
+ ) {
+ Self::try_exec_if_owner_is_valid_nft(
+ account,
+ |d, id| {
+ action(d, id);
+ Ok(())
+ }
+ ).unwrap();
+ }
+
+ fn try_exec_if_owner_is_valid_nft(
+ account: &T::CrossAccountId,
+ action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult
+ ) -> DispatchResult {
+ let account = T::CrossTokenAddressMapping::address_to_token(account);
+
+ if account.is_none() {
+ return Ok(());
+ }
+
+ let account = account.unwrap();
+
+ let handle = <CollectionHandle<T>>::try_get(account.0);
+
+ if handle.is_err() {
+ return Ok(());
+ }
+
+ let handle = handle.unwrap();
+
+ let dispatch = T::CollectionDispatch::dispatch(handle);
+ let dispatch = dispatch.as_dyn();
+
+ action(dispatch, account.1)
+ }
}
pallets/unique/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![recursion_limit = "1024"]18#![cfg_attr(not(feature = "std"), no_std)]19#![allow(20 clippy::too_many_arguments,21 clippy::unnecessary_mut_passed,22 clippy::unused_unit23)]2425extern crate alloc;2627use frame_support::{28 decl_module, decl_storage, decl_error, decl_event,29 dispatch::DispatchResult,30 ensure,31 weights::{Weight},32 transactional,33 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},34 BoundedVec,35};36use scale_info::TypeInfo;37use frame_system::{self as system, ensure_signed};38use sp_runtime::{sp_std::prelude::Vec};39use up_data_structs::{40 MAX_COLLECTION_NAME_LENGTH,41 MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH, AccessMode, CreateItemData,42 CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId, SponsorshipState,43 CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,44 PropertyKeyPermission,45};46use pallet_evm::account::CrossAccountId;47use pallet_common::{48 CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_call,49 dispatch::CollectionDispatch,50};51pub mod eth;5253#[cfg(feature = "runtime-benchmarks")]54mod benchmarking;55pub mod weights;56use weights::WeightInfo;5758decl_error! {59 /// Error for non-fungible-token module.60 pub enum Error for Module<T: Config> {61 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.62 CollectionDecimalPointLimitExceeded,63 /// This address is not set as sponsor, use setCollectionSponsor first.64 ConfirmUnsetSponsorFail,65 /// Length of items properties must be greater than 0.66 EmptyArgument,67 }68}6970pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {71 type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;7273 /// Weight information for extrinsics in this pallet.74 type WeightInfo: WeightInfo;75 type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;76}7778decl_event! {79 pub enum Event<T>80 where81 <T as frame_system::Config>::AccountId,82 <T as pallet_evm::account::Config>::CrossAccountId,83 {84 /// Collection sponsor was removed85 ///86 /// # Arguments87 ///88 /// * collection_id: Globally unique collection identifier.89 CollectionSponsorRemoved(CollectionId),9091 /// Collection admin was added92 ///93 /// # Arguments94 ///95 /// * collection_id: Globally unique collection identifier.96 ///97 /// * admin: Admin address.98 CollectionAdminAdded(CollectionId, CrossAccountId),99100 /// Collection owned was change101 ///102 /// # Arguments103 ///104 /// * collection_id: Globally unique collection identifier.105 ///106 /// * owner: New owner address.107 CollectionOwnedChanged(CollectionId, AccountId),108109 /// Collection sponsor was set110 ///111 /// # Arguments112 ///113 /// * collection_id: Globally unique collection identifier.114 ///115 /// * owner: New sponsor address.116 CollectionSponsorSet(CollectionId, AccountId),117118 /// New sponsor was confirm119 ///120 /// # Arguments121 ///122 /// * collection_id: Globally unique collection identifier.123 ///124 /// * sponsor: New sponsor address.125 SponsorshipConfirmed(CollectionId, AccountId),126127 /// Collection admin was removed128 ///129 /// # Arguments130 ///131 /// * collection_id: Globally unique collection identifier.132 ///133 /// * admin: Admin address.134 CollectionAdminRemoved(CollectionId, CrossAccountId),135136 /// Address was remove from allow list137 ///138 /// # Arguments139 ///140 /// * collection_id: Globally unique collection identifier.141 ///142 /// * user: Address.143 AllowListAddressRemoved(CollectionId, CrossAccountId),144145 /// Address was add to allow list146 ///147 /// # Arguments148 ///149 /// * collection_id: Globally unique collection identifier.150 ///151 /// * user: Address.152 AllowListAddressAdded(CollectionId, CrossAccountId),153154 /// Collection limits was set155 ///156 /// # Arguments157 ///158 /// * collection_id: Globally unique collection identifier.159 CollectionLimitSet(CollectionId),160161 CollectionPermissionSet(CollectionId),162 }163}164165type SelfWeightOf<T> = <T as Config>::WeightInfo;166167// # Used definitions168//169// ## User control levels170//171// chain-controlled - key is uncontrolled by user172// i.e autoincrementing index173// can use non-cryptographic hash174// real - key is controlled by user175// but it is hard to generate enough colliding values, i.e owner of signed txs176// can use non-cryptographic hash177// controlled - key is completly controlled by users178// i.e maps with mutable keys179// should use cryptographic hash180//181// ## User control level downgrade reasons182//183// ?1 - chain-controlled -> controlled184// collections/tokens can be destroyed, resulting in massive holes185// ?2 - chain-controlled -> controlled186// same as ?1, but can be only added, resulting in easier exploitation187// ?3 - real -> controlled188// no confirmation required, so addresses can be easily generated189decl_storage! {190 trait Store for Module<T: Config> as Unique {191192 //#region Private members193 /// Used for migrations194 ChainVersion: u64;195 //#endregion196197 //#region Tokens transfer rate limit baskets198 /// (Collection id (controlled?2), who created (real))199 /// TODO: Off chain worker should remove from this map when collection gets removed200 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;201 /// Collection id (controlled?2), token id (controlled?2)202 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;203 /// Collection id (controlled?2), owning user (real)204 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;205 /// Collection id (controlled?2), token id (controlled?2)206 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;207 //#endregion208209 /// Variable metadata sponsoring210 /// Collection id (controlled?2), token id (controlled?2)211 #[deprecated]212 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;213 pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;214215 /// Approval sponsoring216 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;217 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;218 pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;219 }220}221222decl_module! {223 pub struct Module<T: Config> for enum Call224 where225 origin: T::Origin226 {227 type Error = Error<T>;228229 fn deposit_event() = default;230231 fn on_initialize(_now: T::BlockNumber) -> Weight {232 0233 }234235 fn on_runtime_upgrade() -> Weight {236 let limit = None;237238 <VariableMetaDataBasket<T>>::remove_all(limit);239240 0241 }242243 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner of the collection is set to the address that signed the transaction and can be changed later.244 ///245 /// # Permissions246 ///247 /// * Anyone.248 ///249 /// # Arguments250 ///251 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.252 ///253 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.254 ///255 /// * token_prefix: UTF-8 string with token prefix.256 ///257 /// * mode: [CollectionMode] collection type and type dependent data.258 // returns collection ID259 #[weight = <SelfWeightOf<T>>::create_collection()]260 #[transactional]261 #[deprecated]262 pub fn create_collection(origin,263 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,264 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,265 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,266 mode: CollectionMode) -> DispatchResult {267 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {268 name: collection_name,269 description: collection_description,270 token_prefix,271 mode,272 ..Default::default()273 };274 Self::create_collection_ex(origin, data)275 }276277 /// This method creates a collection278 ///279 /// Prefer it to deprecated [`created_collection`] method280 #[weight = <SelfWeightOf<T>>::create_collection()]281 #[transactional]282 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {283 let sender = ensure_signed(origin)?;284285 // =========286287 T::CollectionDispatch::create(sender, data)?;288289 Ok(())290 }291292 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.293 ///294 /// # Permissions295 ///296 /// * Collection Owner.297 ///298 /// # Arguments299 ///300 /// * collection_id: collection to destroy.301 #[weight = <SelfWeightOf<T>>::destroy_collection()]302 #[transactional]303 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {304 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);305 let collection = <CollectionHandle<T>>::try_get(collection_id)?;306307 // =========308309 T::CollectionDispatch::destroy(sender, collection)?;310311 <NftTransferBasket<T>>::remove_prefix(collection_id, None);312 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);313 <ReFungibleTransferBasket<T>>::remove_prefix((collection_id,), None);314315 <NftApproveBasket<T>>::remove_prefix(collection_id, None);316 <FungibleApproveBasket<T>>::remove_prefix(collection_id, None);317 <RefungibleApproveBasket<T>>::remove_prefix((collection_id,), None);318319 Ok(())320 }321322 /// Add an address to allow list.323 ///324 /// # Permissions325 ///326 /// * Collection Owner327 /// * Collection Admin328 ///329 /// # Arguments330 ///331 /// * collection_id.332 ///333 /// * address.334 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]335 #[transactional]336 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{337338 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);339 let collection = <CollectionHandle<T>>::try_get(collection_id)?;340341 <PalletCommon<T>>::toggle_allowlist(342 &collection,343 &sender,344 &address,345 true,346 )?;347348 Self::deposit_event(Event::<T>::AllowListAddressAdded(349 collection_id,350 address351 ));352353 Ok(())354 }355356 /// Remove an address from allow list.357 ///358 /// # Permissions359 ///360 /// * Collection Owner361 /// * Collection Admin362 ///363 /// # Arguments364 ///365 /// * collection_id.366 ///367 /// * address.368 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]369 #[transactional]370 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{371372 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);373 let collection = <CollectionHandle<T>>::try_get(collection_id)?;374375 <PalletCommon<T>>::toggle_allowlist(376 &collection,377 &sender,378 &address,379 false,380 )?;381382 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(383 collection_id,384 address385 ));386387 Ok(())388 }389390 /// Change the owner of the collection.391 ///392 /// # Permissions393 ///394 /// * Collection Owner.395 ///396 /// # Arguments397 ///398 /// * collection_id.399 ///400 /// * new_owner.401 #[weight = <SelfWeightOf<T>>::change_collection_owner()]402 #[transactional]403 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {404405 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);406407 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;408 target_collection.check_is_owner(&sender)?;409410 target_collection.owner = new_owner.clone();411 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(412 collection_id,413 new_owner414 ));415416 target_collection.save()417 }418419 /// Adds an admin of the Collection.420 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.421 ///422 /// # Permissions423 ///424 /// * Collection Owner.425 /// * Collection Admin.426 ///427 /// # Arguments428 ///429 /// * collection_id: ID of the Collection to add admin for.430 ///431 /// * new_admin_id: Address of new admin to add.432 #[weight = <SelfWeightOf<T>>::add_collection_admin()]433 #[transactional]434 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {435 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);436 let collection = <CollectionHandle<T>>::try_get(collection_id)?;437438 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(439 collection_id,440 new_admin_id.clone()441 ));442443 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)444 }445446 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.447 ///448 /// # Permissions449 ///450 /// * Collection Owner.451 /// * Collection Admin.452 ///453 /// # Arguments454 ///455 /// * collection_id: ID of the Collection to remove admin for.456 ///457 /// * account_id: Address of admin to remove.458 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]459 #[transactional]460 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {461 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);462 let collection = <CollectionHandle<T>>::try_get(collection_id)?;463464 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(465 collection_id,466 account_id.clone()467 ));468469 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)470 }471472 /// # Permissions473 ///474 /// * Collection Owner475 ///476 /// # Arguments477 ///478 /// * collection_id.479 ///480 /// * new_sponsor.481 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]482 #[transactional]483 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {484 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);485486 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;487 target_collection.check_is_owner(&sender)?;488489 target_collection.set_sponsor(new_sponsor.clone());490491 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(492 collection_id,493 new_sponsor494 ));495496 target_collection.save()497 }498499 /// # Permissions500 ///501 /// * Sponsor.502 ///503 /// # Arguments504 ///505 /// * collection_id.506 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]507 #[transactional]508 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {509 let sender = ensure_signed(origin)?;510511 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;512 ensure!(513 target_collection.confirm_sponsorship(&sender),514 Error::<T>::ConfirmUnsetSponsorFail515 );516517 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(518 collection_id,519 sender520 ));521522 target_collection.save()523 }524525 /// Switch back to pay-per-own-transaction model.526 ///527 /// # Permissions528 ///529 /// * Collection owner.530 ///531 /// # Arguments532 ///533 /// * collection_id.534 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]535 #[transactional]536 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {537 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);538539 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;540 target_collection.check_is_owner(&sender)?;541542 target_collection.sponsorship = SponsorshipState::Disabled;543544 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(545 collection_id546 ));547 target_collection.save()548 }549550 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.551 ///552 /// # Permissions553 ///554 /// * Collection Owner.555 /// * Collection Admin.556 /// * Anyone if557 /// * Allow List is enabled, and558 /// * Address is added to allow list, and559 /// * MintPermission is enabled (see SetMintPermission method)560 ///561 /// # Arguments562 ///563 /// * collection_id: ID of the collection.564 ///565 /// * owner: Address, initial owner of the NFT.566 ///567 /// * data: Token data to store on chain.568 #[weight = T::CommonWeightInfo::create_item()]569 #[transactional]570 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {571 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);572 let budget = budget::Value::new(2);573574 dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))575 }576577 /// This method creates multiple items in a collection created with CreateCollection method.578 ///579 /// # Permissions580 ///581 /// * Collection Owner.582 /// * Collection Admin.583 /// * Anyone if584 /// * Allow List is enabled, and585 /// * Address is added to allow list, and586 /// * MintPermission is enabled (see SetMintPermission method)587 ///588 /// # Arguments589 ///590 /// * collection_id: ID of the collection.591 ///592 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].593 ///594 /// * owner: Address, initial owner of the NFT.595 #[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]596 #[transactional]597 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {598 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);599 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);600 let budget = budget::Value::new(2);601602 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))603 }604605 #[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]606 #[transactional]607 pub fn set_collection_properties(608 origin,609 collection_id: CollectionId,610 properties: Vec<Property>611 ) -> DispatchResultWithPostInfo {612 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);613614 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);615616 dispatch_call::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))617 }618619 #[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]620 #[transactional]621 pub fn delete_collection_properties(622 origin,623 collection_id: CollectionId,624 property_keys: Vec<PropertyKey>,625 ) -> DispatchResultWithPostInfo {626 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);627628 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);629630 dispatch_call::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))631 }632633 #[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]634 #[transactional]635 pub fn set_token_properties(636 origin,637 collection_id: CollectionId,638 token_id: TokenId,639 properties: Vec<Property>640 ) -> DispatchResultWithPostInfo {641 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);642643 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);644645 dispatch_call::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties))646 }647648 #[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]649 #[transactional]650 pub fn delete_token_properties(651 origin,652 collection_id: CollectionId,653 token_id: TokenId,654 property_keys: Vec<PropertyKey>655 ) -> DispatchResultWithPostInfo {656 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);657658 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);659660 dispatch_call::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys))661 }662663 #[weight = T::CommonWeightInfo::set_property_permissions(property_permissions.len() as u32)]664 #[transactional]665 pub fn set_property_permissions(666 origin,667 collection_id: CollectionId,668 property_permissions: Vec<PropertyKeyPermission>,669 ) -> DispatchResultWithPostInfo {670 ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);671672 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);673674 dispatch_call::<T, _>(collection_id, |d| d.set_property_permissions(&sender, property_permissions))675 }676677 #[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]678 #[transactional]679 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {680 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);681 let budget = budget::Value::new(2);682683 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))684 }685686 // TODO! transaction weight687688 /// Set transfers_enabled value for particular collection689 ///690 /// # Permissions691 ///692 /// * Collection Owner.693 ///694 /// # Arguments695 ///696 /// * collection_id: ID of the collection.697 ///698 /// * value: New flag value.699 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]700 #[transactional]701 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {702 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);703 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;704 target_collection.check_is_owner(&sender)?;705706 // =========707708 target_collection.limits.transfers_enabled = Some(value);709 target_collection.save()710 }711712 /// Destroys a concrete instance of NFT.713 ///714 /// # Permissions715 ///716 /// * Collection Owner.717 /// * Collection Admin.718 /// * Current NFT Owner.719 ///720 /// # Arguments721 ///722 /// * collection_id: ID of the collection.723 ///724 /// * item_id: ID of NFT to burn.725 #[weight = T::CommonWeightInfo::burn_item()]726 #[transactional]727 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {728 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);729730 let post_info = dispatch_call::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;731 if value == 1 {732 <NftTransferBasket<T>>::remove(collection_id, item_id);733 <NftApproveBasket<T>>::remove(collection_id, item_id);734 }735 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?736 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());737 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));738 Ok(post_info)739 }740741 /// Destroys a concrete instance of NFT on behalf of the owner742 /// See also: [`approve`]743 ///744 /// # Permissions745 ///746 /// * Collection Owner.747 /// * Collection Admin.748 /// * Current NFT Owner.749 ///750 /// # Arguments751 ///752 /// * collection_id: ID of the collection.753 ///754 /// * item_id: ID of NFT to burn.755 ///756 /// * from: owner of item757 #[weight = T::CommonWeightInfo::burn_from()]758 #[transactional]759 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {760 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);761 let budget = budget::Value::new(2);762763 dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))764 }765766 /// Change ownership of the token.767 ///768 /// # Permissions769 ///770 /// * Collection Owner771 /// * Collection Admin772 /// * Current NFT owner773 ///774 /// # Arguments775 ///776 /// * recipient: Address of token recipient.777 ///778 /// * collection_id.779 ///780 /// * item_id: ID of the item781 /// * Non-Fungible Mode: Required.782 /// * Fungible Mode: Ignored.783 /// * Re-Fungible Mode: Required.784 ///785 /// * value: Amount to transfer.786 /// * Non-Fungible Mode: Ignored787 /// * Fungible Mode: Must specify transferred amount788 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)789 #[weight = T::CommonWeightInfo::transfer()]790 #[transactional]791 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {792 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);793 let budget = budget::Value::new(2);794795 dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))796 }797798 /// Set, change, or remove approved address to transfer the ownership of the NFT.799 ///800 /// # Permissions801 ///802 /// * Collection Owner803 /// * Collection Admin804 /// * Current NFT owner805 ///806 /// # Arguments807 ///808 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).809 ///810 /// * collection_id.811 ///812 /// * item_id: ID of the item.813 #[weight = T::CommonWeightInfo::approve()]814 #[transactional]815 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {816 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);817818 dispatch_call::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))819 }820821 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.822 ///823 /// # Permissions824 /// * Collection Owner825 /// * Collection Admin826 /// * Current NFT owner827 /// * Address approved by current NFT owner828 ///829 /// # Arguments830 ///831 /// * from: Address that owns token.832 ///833 /// * recipient: Address of token recipient.834 ///835 /// * collection_id.836 ///837 /// * item_id: ID of the item.838 ///839 /// * value: Amount to transfer.840 #[weight = T::CommonWeightInfo::transfer_from()]841 #[transactional]842 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {843 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);844 let budget = budget::Value::new(2);845846 dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))847 }848849 #[weight = <SelfWeightOf<T>>::set_collection_limits()]850 #[transactional]851 pub fn set_collection_limits(852 origin,853 collection_id: CollectionId,854 new_limit: CollectionLimits,855 ) -> DispatchResult {856 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);857 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;858 target_collection.check_is_owner(&sender)?;859 let old_limit = &target_collection.limits;860861 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;862863 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(864 collection_id865 ));866867 target_collection.save()868 }869870 #[weight = <SelfWeightOf<T>>::set_collection_limits()]871 #[transactional]872 pub fn set_collection_permissions(873 origin,874 collection_id: CollectionId,875 new_limit: CollectionPermissions,876 ) -> DispatchResult {877 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);878 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;879 target_collection.check_is_owner(&sender)?;880 let old_limit = &target_collection.permissions;881882 target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_limit)?;883884 <Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(885 collection_id886 ));887888 target_collection.save()889 }890 }891}runtime/common/src/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -25,7 +25,7 @@
dispatch_unique_runtime!(collection.token_owner(token))
}
fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
- let budget = up_data_structs::budget::Value::new(5);
+ let budget = up_data_structs::budget::Value::new(10);
Ok(Some(<pallet_structure::Pallet<Runtime>>::find_topmost_owner(collection, token, &budget)?))
}
@@ -142,7 +142,7 @@
}
fn collection_by_id(collection_id: RmrkCollectionId) -> Result<Option<RmrkCollectionInfo<AccountId>>, DispatchError> {
- use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode}};
+ use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind}};
let collection_id = CollectionId(collection_id);
let collection = match RmrkCore::get_typed_nft_collection(collection_id, CollectionType::Regular) {
@@ -156,7 +156,7 @@
issuer: collection.owner.clone(),
metadata: RmrkCore::get_collection_property(collection_id, RmrkProperty::Metadata)?.decode_or_default(),
max: collection.limits.token_limit,
- symbol: collection.token_prefix.decode_or_default(),
+ symbol: collection.token_prefix.rebind(),
nfts_count
}))
}
@@ -204,22 +204,21 @@
}
fn nft_children(collection_id: RmrkCollectionId, nft_id: RmrkNftId) -> Result<Vec<RmrkNftChild>, DispatchError> {
- use up_data_structs::mapping::TokenAddressMapping;
-
let collection_id = CollectionId(collection_id);
let nft_id = TokenId(nft_id);
if !RmrkCore::nft_exists(collection_id, nft_id) { return Ok(Vec::new()); }
- let cross_account_id = CrossAccountId::from_eth(
- EvmTokenAddressMapping::token_to_address(collection_id, nft_id)
- );
-
Ok(
- pallet_nonfungible::Owned::<Runtime>::iter_prefix((collection_id, cross_account_id))
- .map(|(child_id, _)| RmrkNftChild {
- collection_id: collection_id.0, // todo make sure they're always from this collection // spoiler: they're not
- nft_id: child_id.0,
- }).collect()
+ pallet_nonfungible::TokenChildren::<Runtime>::iter_prefix((collection_id, nft_id))
+ .filter_map(|(child_id, is_child)|
+ match is_child {
+ true => Some(RmrkNftChild {
+ collection_id: child_id.0.0,
+ nft_id: child_id.1.0,
+ }),
+ false => None,
+ }
+ ).collect()
)
}
@@ -332,7 +331,7 @@
fn base(base_id: RmrkBaseId) -> Result<Option<RmrkBaseInfo<AccountId>>, DispatchError> {
use pallet_proxy_rmrk_core::{
- RmrkProperty, misc::{CollectionType, RmrkDecode},
+ RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind},
};
let collection_id = CollectionId(base_id);
@@ -344,7 +343,7 @@
Ok(Some(RmrkBaseInfo {
issuer: collection.owner.clone(),
base_type: RmrkCore::get_collection_property(collection_id, RmrkProperty::BaseType)?.decode_or_default(),
- symbol: collection.token_prefix.decode_or_default(),
+ symbol: collection.token_prefix.rebind(),
}))
}
tests/src/nesting/nest.test.tsdiffbeforeafterboth--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -41,7 +41,7 @@
// Create a token to be nested
const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-
+
// Nest
await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
@@ -111,8 +111,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
))).to.not.be.rejected;
@@ -134,8 +134,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
))).to.not.be.rejected;
@@ -158,8 +158,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
))).to.not.be.rejected;
@@ -181,7 +181,7 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
+ collectionRFT,
targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
))).to.not.be.rejected;
@@ -207,17 +207,29 @@
await setCollectionPermissionsExceptSuccess(alice, collection, {nesting: 'Owner'});
const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+ const maxNestingLevel = 5;
+ let prevToken = targetToken;
+
// Create a nested-token matryoshka
- const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
- const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
- // The nesting depth is limited by 2
+ for (let i = 0; i < maxNestingLevel; i++) {
+ const nestedToken = await createItemExpectSuccess(
+ alice,
+ collection,
+ 'NFT',
+ {Ethereum: tokenIdToAddress(collection, prevToken)},
+ );
+
+ prevToken = nestedToken;
+ }
+
+ // The nesting depth is limited by `maxNestingLevel`
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, nestedToken2)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, prevToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/);
- expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
+ expect(await getTopmostTokenOwner(api, collection, prevToken)).to.be.deep.equal({Substrate: alice.address});
});
});
@@ -231,8 +243,8 @@
// Try to create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
@@ -259,8 +271,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -285,8 +297,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -307,8 +319,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
@@ -332,11 +344,11 @@
// Try to create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
-
+
// Create a token to be nested
const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
// Try to nest
@@ -366,8 +378,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -393,8 +405,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -417,8 +429,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
@@ -441,8 +453,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
@@ -477,8 +489,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -504,8 +516,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -528,8 +540,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);