From b08cb26894de982b47bde6e9454a7ed7ba3f4174 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Mon, 30 May 2022 08:13:34 +0000 Subject: [PATCH] Merge pull request #355 from UniqueNetwork/feature/nft-children Structure children map --- --- 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; fn collection_tokens(&self) -> Vec; fn token_exists(&self, token: TokenId) -> bool; --- a/pallets/fungible/src/common.rs +++ b/pallets/fungible/src/common.rs @@ -298,6 +298,18 @@ fail!(>::FungibleDisallowsNesting) } + fn nest( + &self, + _under: TokenId, + _to_nest: (CollectionId, TokenId) + ) {} + + fn unnest( + &self, + _under: TokenId, + _to_nest: (CollectionId, TokenId) + ) {} + fn collection_tokens(&self) -> Vec { vec![TokenId::default()] } --- 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(>::CantDestroyNotEmptyCollection.into()); + } + // ========= PalletCommon::destroy_collection(collection.0, sender)?; @@ -155,6 +159,10 @@ Ok(()) } + fn collection_has_tokens(collection_id: CollectionId) -> bool { + >::get(collection_id) != 0 + } + pub fn burn( collection: &FungibleHandle, owner: &T::CrossAccountId, @@ -176,6 +184,11 @@ if balance == 0 { >::remove((collection.id, owner)); + >::unnest_if_nested( + owner, + collection.id, + TokenId::default() + ); } else { >::insert((collection.id, owner), balance); } @@ -229,25 +242,25 @@ None }; - if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) { - let handle = >::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, - )?; - } + >::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 { >::remove((collection.id, from)); + >::unnest_if_nested( + from, + collection.id, + TokenId::default() + ); } else { >::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 = >::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, - )?; - } + >::check_nesting( + sender.clone(), + to, + collection.id, + TokenId::default(), + nesting_budget, + )?; } // ========= @@ -325,7 +333,7 @@ >::insert(collection.id, total_supply); for (user, amount) in balances { >::insert((collection.id, &user), amount); - + >::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId::default()); >::deposit_log( ERC20Events::Transfer { from: H160::default(), --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -353,6 +353,22 @@ >::check_nesting(self, sender, from, under, budget) } + fn nest( + &self, + under: TokenId, + to_nest: (CollectionId, TokenId) + ) { + >::nest((self.id, under), to_nest); + } + + fn unnest( + &self, + under: TokenId, + to_unnest: (CollectionId, TokenId) + ) { + >::unnest((self.id, under), to_unnest); + } + fn account_tokens(&self, account: T::CrossAccountId) -> Vec { >::iter_prefix((self.id, account)) .map(|(id, _)| id) --- 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 = StorageNMap< + Key = ( + Key, + Key, + Key, + ), + Value = bool, + QueryKind = ValueQuery, + >; + + #[pallet::storage] pub type AccountBalance = StorageNMap< Key = ( Key, @@ -277,11 +292,16 @@ ) -> DispatchResult { let id = collection.id; + if Self::collection_has_tokens(id) { + return Err(>::CantDestroyNotEmptyCollection.into()); + } + // ========= PalletCommon::destroy_collection(collection.0, sender)?; >::remove_prefix((id,), None); + >::remove_prefix((id,), None); >::remove_prefix((id,), None); >::remove(id); >::remove(id); @@ -307,6 +327,10 @@ collection.check_allowlist(sender)?; } + if Self::token_has_children(collection.id, token) { + return Err(>::CantBurnNftWithChildren.into()); + } + let burnt = >::get(collection.id) .checked_add(1) .ok_or(ArithmeticError::Overflow)?; @@ -315,13 +339,20 @@ .checked_sub(1) .ok_or(ArithmeticError::Overflow)?; + // ========= + if balance == 0 { >::remove((collection.id, token_data.owner.clone())); } else { >::insert((collection.id, token_data.owner.clone()), balance); } - // ========= + >::unnest_if_nested( + &token_data.owner, + collection.id, + token + ); + >::remove((collection.id, &token_data.owner, token)); >::insert(collection.id, burnt); >::remove((collection.id, token)); @@ -553,20 +584,21 @@ None }; - if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) { - let handle = >::try_get(target.0)?; - let dispatch = T::CollectionDispatch::dispatch(handle); - let dispatch = dispatch.as_dyn(); + >::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, - )?; - } + // ========= - // ========= + >::unnest_if_nested( + from, + collection.id, + token + ); >::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 = >::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, - )?; - } + + >::check_nesting( + sender.clone(), + &data.owner, + collection.id, + token, + nesting_budget, + )?; } // ========= @@ -680,6 +709,8 @@ }, ); + >::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), + ) { + >::insert( + (under.0, under.1, (to_nest.0, to_nest.1)), + true + ); + } + + fn unnest( + under: (CollectionId, TokenId), + to_unnest: (CollectionId, TokenId), + ) { + >::remove( + (under.0, under.1, to_unnest) + ); + } + + fn collection_has_tokens(collection_id: CollectionId) -> bool { + >::iter_prefix((collection_id,)).next().is_some() + } + + fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool { + >::iter_prefix((collection_id, token_id)).next().is_some() + } + /// Delegated to `create_multiple_items` pub fn create_item( collection: &NonfungibleHandle, --- a/pallets/proxy-rmrk-core/src/misc.rs +++ b/pallets/proxy-rmrk-core/src/misc.rs @@ -26,6 +26,18 @@ } } +pub trait RmrkRebind { + fn rebind(&self) -> BoundedVec; +} + +impl RmrkRebind for BoundedVec where BoundedVec: TryFrom> { + fn rebind(&self) -> BoundedVec { + BoundedVec::::try_from( + self.clone().into_inner() + ).unwrap_or_default() + } +} + #[derive(Encode, Decode, PartialEq, Eq)] pub enum CollectionType { Regular, --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -313,6 +313,18 @@ fail!(>::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 { >::iter_prefix((self.id, account)) .map(|(id, _)| id) --- 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(>::CantDestroyNotEmptyCollection.into()); + } + // ========= PalletCommon::destroy_collection(collection.0, sender)?; @@ -226,6 +229,10 @@ Ok(()) } + fn collection_has_tokens(collection_id: CollectionId) -> bool { + >::iter_prefix((collection_id,)).next().is_some() + } + pub fn burn_token(collection: &RefungibleHandle, token_id: TokenId) -> DispatchResult { let burnt = >::get(collection.id) .checked_add(1) @@ -265,6 +272,7 @@ // ========= >::remove((collection.id, owner, token)); + >::unnest_if_nested(owner, collection.id, token); >::insert((collection.id, owner), account_balance); Self::burn_token(collection, token)?; >::deposit_event(CommonEvent::ItemDestroyed( @@ -292,6 +300,7 @@ if balance == 0 { >::remove((collection.id, owner, token)); + >::unnest_if_nested(owner, collection.id, token); >::remove((collection.id, token, owner)); >::insert((collection.id, owner), account_balance); } else { @@ -372,25 +381,25 @@ None }; - if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) { - let handle = >::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, - )?; - } - - // ========= + >::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 { >::remove((collection.id, token, from)); + >::unnest_if_nested( + from, + collection.id, + token + ); } else { >::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 = >::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, - )?; - } + >::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; } >::insert((collection.id, token_id, &user), amount); >::insert((collection.id, &user, TokenId(token_id)), true); + >::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId(token_id)); + // TODO: ERC20 transfer event >::deposit_event(CommonEvent::ItemCreated( collection.id, --- 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 { 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(>::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(>::TokenNotFound.into()), // Continue parent chain Parent::Token(_, _) => {} } @@ -182,4 +183,113 @@ Err(>::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, 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, TokenId) -> DispatchResult + ) -> DispatchResult { + let account = T::CrossTokenAddressMapping::address_to_token(account); + + if account.is_none() { + return Ok(()); + } + + let account = account.unwrap(); + + let handle = >::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) + } } --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -55,6 +55,8 @@ pub mod weights; use weights::WeightInfo; +const NESTING_BUDGET: u32 = 5; + decl_error! { /// Error for non-fungible-token module. pub enum Error for Module { @@ -569,7 +571,7 @@ #[transactional] pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let budget = budget::Value::new(2); + let budget = budget::Value::new(NESTING_BUDGET); dispatch_call::(collection_id, |d| d.create_item(sender, owner, data, &budget)) } @@ -597,7 +599,7 @@ pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec) -> DispatchResultWithPostInfo { ensure!(!items_data.is_empty(), Error::::EmptyArgument); let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let budget = budget::Value::new(2); + let budget = budget::Value::new(NESTING_BUDGET); dispatch_call::(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget)) } @@ -678,7 +680,7 @@ #[transactional] pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData) -> DispatchResultWithPostInfo { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let budget = budget::Value::new(2); + let budget = budget::Value::new(NESTING_BUDGET); dispatch_call::(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget)) } @@ -758,7 +760,7 @@ #[transactional] pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let budget = budget::Value::new(2); + let budget = budget::Value::new(NESTING_BUDGET); dispatch_call::(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget)) } @@ -790,7 +792,7 @@ #[transactional] pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let budget = budget::Value::new(2); + let budget = budget::Value::new(NESTING_BUDGET); dispatch_call::(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget)) } @@ -841,7 +843,7 @@ #[transactional] pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let budget = budget::Value::new(2); + let budget = budget::Value::new(NESTING_BUDGET); dispatch_call::(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget)) } --- 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, DispatchError> { - let budget = up_data_structs::budget::Value::new(5); + let budget = up_data_structs::budget::Value::new(10); Ok(Some(>::find_topmost_owner(collection, token, &budget)?)) } @@ -142,7 +142,7 @@ } fn collection_by_id(collection_id: RmrkCollectionId) -> Result>, 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, 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::::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::::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>, 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(), })) } --- 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/); -- gitstuff