diff --git a/pallets/rmrk-core/src/functions.rs b/pallets/rmrk-core/src/functions.rs new file mode 100644 index 00000000..6f884611 --- /dev/null +++ b/pallets/rmrk-core/src/functions.rs @@ -0,0 +1,29 @@ +use super::*; + +impl Pallet { + pub fn is_x_descendent_of_y( + child_collection_id: T::CollectionId, + child_nft_id: T::NftId, + parent_collection_id: T::CollectionId, + parent_nft_id: T::NftId, + ) -> bool { + let mut found_child = false; + if let Some(children) = Children::::get(parent_collection_id, parent_nft_id) { + for child in children { + if child == (child_collection_id, child_nft_id) { + return true; + } else { + if Pallet::::is_x_descendent_of_y( + child_collection_id, + child_nft_id, + child.0, + child.1, + ) { + found_child = true; + } + } + } + } + found_child + } +} diff --git a/pallets/rmrk-core/src/lib.rs b/pallets/rmrk-core/src/lib.rs index ef65d16c..99da03cf 100644 --- a/pallets/rmrk-core/src/lib.rs +++ b/pallets/rmrk-core/src/lib.rs @@ -20,6 +20,8 @@ use sp_std::{convert::TryInto, vec::Vec}; use types::{AccountIdOrCollectionNftTuple, ClassInfo, InstanceInfo}; +mod functions; + #[cfg(test)] mod mock; @@ -107,6 +109,18 @@ pub mod pallet { InstanceInfoOf, >; + #[pallet::storage] + #[pallet::getter(fn children)] + /// Stores nft info + pub type Children = StorageDoubleMap< + _, + Twox64Concat, + T::CollectionId, + Twox64Concat, + T::NftId, + Vec<(T::CollectionId, T::NftId)>, + >; + #[pallet::storage] #[pallet::getter(fn resources)] /// Stores resource info @@ -163,6 +177,7 @@ pub mod pallet { NoPermission, NoWitness, CollectionNotEmpty, + CannotSendToDescendent, } #[pallet::call] @@ -270,7 +285,6 @@ pub mod pallet { } /// burn nft - /// TODO: If an NFT that contains other NFTs is being burnt, the owned NFTs are also burned. #[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1))] #[transactional] pub fn burn_nft( @@ -278,11 +292,16 @@ pub mod pallet { collection_id: T::CollectionId, nft_id: T::NftId, ) -> DispatchResult { - let sender = ensure_signed(origin)?; + let sender = ensure_signed(origin.clone())?; pallet_uniques::Pallet::::do_burn(collection_id.into(), nft_id.into(), |_, _| { Ok(()) })?; NFTs::::remove(collection_id, nft_id); + if let Some(kids) = Children::::take(collection_id, nft_id) { + for child in kids { + Pallet::::burn_nft(origin.clone(), child.0, child.1)?; + } + } Self::deposit_event(Event::NFTBurned(sender, nft_id)); Ok(()) } @@ -339,15 +358,48 @@ pub mod pallet { match new_owner.clone() { AccountIdOrCollectionNftTuple::AccountId(account_id) => { + // Remove previous parental relationship + if let AccountIdOrCollectionNftTuple::CollectionAndNftTuple(cid, nid) = + sending_nft.owner + { + if let Some(mut kids) = Children::::take(cid, nid) { + kids.retain(|&kid| kid != (collection_id, nft_id)); + Children::::insert(cid, nid, kids); + } + } sending_nft.rootowner = account_id.clone(); - }, + } AccountIdOrCollectionNftTuple::CollectionAndNftTuple(cid, nid) => { let recipient_nft = NFTs::::get(cid, nid).ok_or(Error::::NoAvailableNftId)?; + // Check if sending NFT is already a child of recipient NFT + ensure!( + !Pallet::::is_x_descendent_of_y(cid, nid, collection_id, nft_id), + Error::::CannotSendToDescendent + ); + + // Remove parent if exists: first we only care if the owner is a non-AccountId) + if let AccountIdOrCollectionNftTuple::CollectionAndNftTuple(cid, nid) = + sending_nft.owner + { + // second we only care if the parent has children (it should) + if let Some(mut kids) = Children::::take(cid, nid) { + // third we only "retain" the other children + kids.retain(|&kid| kid != (collection_id, nft_id)); + Children::::insert(cid, nid, kids); + } + } if sending_nft.rootowner != recipient_nft.rootowner { sending_nft.rootowner = recipient_nft.rootowner } - }, + match Children::::take(cid, nid) { + None => Children::::insert(cid, nid, vec![(collection_id, nft_id)]), + Some(mut kids) => { + kids.push((collection_id, nft_id)); + Children::::insert(cid, nid, kids); + } + } + } }; sending_nft.owner = new_owner.clone(); @@ -383,7 +435,7 @@ pub mod pallet { // collection.issuer = dest.clone(); // Collections::::insert(collection_id, collection); // Ok(()) - // })?; + // })?; // Check that sender is current issuer let mut collection = diff --git a/pallets/rmrk-core/src/tests.rs b/pallets/rmrk-core/src/tests.rs index 26087149..3abf1dff 100644 --- a/pallets/rmrk-core/src/tests.rs +++ b/pallets/rmrk-core/src/tests.rs @@ -171,6 +171,137 @@ fn send_nft_to_minted_nft_works() { }); } +#[test] +fn send_two_nfts_to_same_nft_creates_two_children() { + ExtBuilder::default().build().execute_with(|| { + let collection_metadata = stv("testing"); + let nft_metadata = stv("testing"); + assert_ok!(RMRKCore::create_collection(Origin::signed(ALICE), collection_metadata)); + // Alice mints NFT (0, 0) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + 0, + Some(ALICE), + Some(0), + Some(nft_metadata.clone()) + )); + // Alice mints NFT (0, 1) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + 0, + Some(ALICE), + Some(0), + Some(nft_metadata.clone()) + )); + // Alice mints NFT (0, 2) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + 0, + Some(ALICE), + Some(0), + Some(nft_metadata) + )); + + // Alice sends NFT (0, 1) to NFT (0, 0) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 1, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0), + )); + // Alice sends NFT (0, 2) to NFT (0, 0) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 2, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0), + )); + // Children for NFT (0, 0) contains (0, 1) and (0, 2) + assert_eq!(RMRKCore::children(0, 0).unwrap(), vec![(0, 1), (0, 2)]); + }); +} + +#[test] +fn send_nft_removes_existing_parent() { + ExtBuilder::default().build().execute_with(|| { + let collection_metadata = stv("testing"); + let nft_metadata = stv("testing"); + assert_ok!(RMRKCore::create_collection(Origin::signed(ALICE), collection_metadata)); + // Alice mints NFT (0, 0) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + 0, + Some(ALICE), + Some(0), + Some(nft_metadata.clone()) + )); + // Alice mints NFT (0, 1) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + 0, + Some(ALICE), + Some(0), + Some(nft_metadata.clone()) + )); + // Alice mints NFT (0, 2) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + 0, + Some(ALICE), + Some(0), + Some(nft_metadata.clone()) + )); + // Alice mints NFT (0, 3) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + 0, + Some(ALICE), + Some(0), + Some(nft_metadata) + )); + + // Alice sends NFT (0, 1) to NFT (0, 0) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 1, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0), + )); + // Alice sends NFT (0, 2) to NFT (0, 0) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 2, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0), + )); + + // NFT (0, 0) is parent of NFT (0, 1) + assert_eq!(RMRKCore::children(0, 0).unwrap(), vec![(0, 1), (0, 2)]); + + // Alice sends NFT (0, 1) to NFT (0, 2) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 1, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 2), + )); + + // NFT (0, 0) is not parent of NFT (0, 1) + assert_eq!(RMRKCore::children(0, 0).unwrap(), vec![(0, 2)]); + }); +} + +// #[test] +// TODO fn cannot send to its own descendent? this should be easy enough to check +// TODO fn cannot send to its own grandparent? this seems difficult to check without implementing a new Parent storage struct + #[test] fn change_issuer_works() { ExtBuilder::default().build().execute_with(|| { @@ -199,6 +330,137 @@ fn burn_nft_works() { }); } +#[test] +fn burn_nft_with_great_grandchildren_works() { + ExtBuilder::default().build().execute_with(|| { + let metadata = stv("testing"); + assert_ok!(RMRKCore::create_collection(Origin::signed(ALICE), metadata.clone())); + // Alice mints (0, 0) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + COLLECTION_ID_0, + Some(ALICE), + Some(0), + Some(metadata.clone()) + )); + // Alice mints (0, 1) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + COLLECTION_ID_0, + Some(ALICE), + Some(0), + Some(metadata.clone()) + )); + // Alice mints (0, 2) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + COLLECTION_ID_0, + Some(ALICE), + Some(0), + Some(metadata.clone()) + )); + // Alice mints (0, 3) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + COLLECTION_ID_0, + Some(ALICE), + Some(0), + Some(metadata.clone()) + )); + // Alice sends NFT (0, 1) to NFT (0, 0) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 1, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0), + )); + // Alice sends NFT (0, 2) to NFT (0, 1) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 2, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 1), + )); + // Alice sends NFT (0, 3) to NFT (0, 2) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 3, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 2), + )); + // Child is alive + assert_eq!(RMRKCore::nfts(COLLECTION_ID_0, 3).is_some(), true); + // Burn great-grandfather + assert_ok!(RMRKCore::burn_nft(Origin::signed(ALICE), COLLECTION_ID_0, NFT_ID_0)); + // Child is dead + assert_eq!(RMRKCore::nfts(COLLECTION_ID_0, 3), None); + }); +} + +#[test] +fn send_to_grandchild_fails() { + ExtBuilder::default().build().execute_with(|| { + let metadata = stv("testing"); + assert_ok!(RMRKCore::create_collection(Origin::signed(ALICE), metadata.clone())); + // Alice mints (0, 0) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + COLLECTION_ID_0, + Some(ALICE), + Some(0), + Some(metadata.clone()) + )); + // Alice mints (0, 1) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + COLLECTION_ID_0, + Some(ALICE), + Some(0), + Some(metadata.clone()) + )); + // Alice mints (0, 2) + assert_ok!(RMRKCore::mint_nft( + Origin::signed(ALICE), + ALICE, + COLLECTION_ID_0, + Some(ALICE), + Some(0), + Some(metadata.clone()) + )); + // Alice sends NFT (0, 1) to NFT (0, 0) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 1, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0), + )); + // Alice sends NFT (0, 2) to NFT (0, 1) + assert_ok!(RMRKCore::send( + Origin::signed(ALICE), + 0, + 2, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 1), + )); + + // Alice sends (0, 0) to (0, 2) + assert_noop!( + RMRKCore::send( + Origin::signed(ALICE), + 0, + 0, + AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 2), + ), + Error::::CannotSendToDescendent + ); + }); +} + #[test] fn destroy_collection_works() { ExtBuilder::default().build().execute_with(|| {