--- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -1237,6 +1237,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/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; @@ -176,6 +176,11 @@ if balance == 0 { >::remove((collection.id, owner)); + >::unnest_if_nested( + owner, + collection.id, + TokenId::default() + ); } else { >::insert((collection.id, owner), balance); } @@ -228,22 +233,17 @@ } else { 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, - )?; - } - // ========= + >::try_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 { @@ -306,18 +306,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 +320,7 @@ >::insert(collection.id, total_supply); for (user, amount) in balances { >::insert((collection.id, &user), amount); - + >::nest_if_sent_to_token(&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}; @@ -77,6 +77,8 @@ NotNonfungibleDataUsedToMintFungibleCollectionToken, /// Used amount > 1 with NFT NonfungibleItemsHaveNoAmount, + /// Unable to burn NFT with children + CantBurnNftWithChildren, } #[pallet::config] @@ -128,7 +130,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, @@ -283,6 +298,7 @@ PalletCommon::destroy_collection(collection.0, sender)?; >::remove_prefix((id,), None); + >::remove_prefix((id,), None); >::remove_prefix((id,), None); >::remove(id); >::remove(id); @@ -308,6 +324,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)?; @@ -321,6 +341,11 @@ } else { >::insert((collection.id, token_data.owner.clone()), balance); } + + if let Some(owner) = T::CrossTokenAddressMapping::address_to_token(&token_data.owner) { + Self::unnest(owner, (collection.id, token)); + } + // ========= >::remove((collection.id, &token_data.owner, token)); @@ -554,18 +579,13 @@ 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, - )?; - } + >::try_nest_if_sent_to_token( + from.clone(), + to, + collection.id, + token, + nesting_budget + )?; // ========= @@ -654,17 +674,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, + )?; } // ========= @@ -681,6 +698,8 @@ }, ); + >::nest_if_sent_to_token(&data.owner, collection.id, TokenId(token)); + if let Err(e) = Self::set_token_properties( collection, sender, @@ -928,6 +947,29 @@ 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 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/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}; @@ -265,6 +264,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 +292,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 { @@ -371,22 +372,17 @@ } else { 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, - )?; - } // ========= + >::try_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 { @@ -488,18 +484,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 +511,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(&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}; @@ -174,7 +175,7 @@ v if v == 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_dispatched( + under, + |d, parent_id| d.check_nesting( + from, + (collection_id, token_id), + parent_id, + nesting_budget + ) + ) + } + + pub fn try_nest_if_sent_to_token( + from: T::CrossAccountId, + under: &T::CrossAccountId, + collection_id: CollectionId, + token_id: TokenId, + nesting_budget: &dyn Budget + ) -> DispatchResult { + Self::try_dispatched( + 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( + owner: &T::CrossAccountId, + collection_id: CollectionId, + token_id: TokenId + ) { + Self::dispatched( + 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::dispatched( + owner, + |d, parent_id| d.unnest( + parent_id, + (collection_id, token_id) + ) + ); + } + + fn dispatched( + account: &T::CrossAccountId, + action: impl FnOnce(&dyn CommonCollectionOperations, TokenId) + ) { + Self::try_dispatched( + account, + |d, id| { + action(d, id); + Ok(()) + } + ).unwrap(); + } + + fn try_dispatched( + 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) + } }