From 1bd93c2627cff4b9f4dee2205ab44afcd0662a8c Mon Sep 17 00:00:00 2001 From: Igor Kozyrev Date: Wed, 27 Oct 2021 20:03:04 +0000 Subject: [PATCH] manual merge of develop --- --- a/pallets/nft/src/eth/erc.rs +++ b/pallets/nft/src/eth/erc.rs @@ -194,7 +194,8 @@ let caller = T::CrossAccountId::from_eth(caller); let token_id = token_id.try_into().map_err(|_| "amount overflow")?; - >::burn_item_internal(&caller, &self, token_id, 1).map_err(|_| "burn error")?; + >::burn_item_internal(&caller, &self, token_id, 1, true) + .map_err(|_| "burn error")?; Ok(()) } } --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -992,11 +992,39 @@ let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let target_collection = Self::get_collection(collection_id)?; - Self::burn_item_internal(&sender, &target_collection, item_id, value)?; + Self::burn_item_internal(&sender, &target_collection, item_id, value, true)?; target_collection.submit_logs() } + /// Destroys a concrete instance of NFT on behalf of the owner + /// See also: [`approve`] + /// + /// # Permissions + /// + /// * Collection Owner. + /// * Collection Admin. + /// * Current NFT Owner. + /// + /// # Arguments + /// + /// * collection_id: ID of the collection. + /// + /// * item_id: ID of NFT to burn. + /// + /// * from: owner of item + #[weight = >::burn_item()] + #[transactional] + pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResult { + + let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); + let target_collection = Self::get_collection(collection_id)?; + + Self::burn_from_internal(&sender, &target_collection, &from, item_id, value)?; + + target_collection.submit_logs() + } + /// Change ownership of the token. /// /// # Permissions @@ -1593,13 +1621,60 @@ } pub fn burn_item_internal( + owner: &T::CrossAccountId, + collection: &CollectionHandle, + item_id: TokenId, + value: u128, + allow_escalation: bool, + ) -> DispatchResult { + ensure!( + Self::is_item_owner(owner, collection, item_id)? + || (allow_escalation + && collection.limits.owner_can_transfer + && Self::is_owner_or_admin_permissions(collection, owner)?), + Error::::NoPermission + ); + + if collection.access == AccessMode::WhiteList { + Self::check_white_list(collection, owner)?; + } + + match collection.mode { + CollectionMode::NFT => match value { + 1 => Self::burn_nft_item(collection, item_id)?, + 0 => (), + _ => fail!(>::TokenValueTooLow), + }, + CollectionMode::Fungible(_) => Self::burn_fungible_item(collection, owner, value)?, + CollectionMode::ReFungible => { + Self::burn_refungible_item(collection, item_id, owner, value)? + } + _ => (), + }; + + Ok(()) + } + + pub fn burn_from_internal( sender: &T::CrossAccountId, collection: &CollectionHandle, + from: &T::CrossAccountId, item_id: TokenId, - value: u128, + amount: u128, ) -> DispatchResult { + if sender == from { + // Transfer by `from`, because it is either equal to sender, or derived from him + return Self::burn_item_internal(from, collection, item_id, amount, true); + } + + // Check approval + collection.consume_sload()?; + let approval: u128 = + >::get(collection.id, (item_id, from.as_sub(), sender.as_sub())); + + // Transfer permissions check ensure!( - Self::is_item_owner(sender, collection, item_id)? + approval >= amount || (collection.limits.owner_can_transfer && Self::is_owner_or_admin_permissions(collection, sender)?), Error::::NoPermission @@ -1609,11 +1684,29 @@ Self::check_white_list(collection, sender)?; } - match collection.mode { - CollectionMode::NFT => Self::burn_nft_item(collection, item_id)?, - CollectionMode::Fungible(_) => Self::burn_fungible_item(sender, collection, value)?, - CollectionMode::ReFungible => Self::burn_refungible_item(collection, item_id, sender)?, - }; + // Reduce approval by transferred amount or remove if remaining approval drops to 0 + let allowance = approval.saturating_sub(amount); + collection.consume_sstore()?; + if allowance > 0 { + >::insert( + collection.id, + (item_id, from.as_sub(), sender.as_sub()), + allowance, + ); + } else { + >::remove(collection.id, (item_id, from.as_sub(), sender.as_sub())); + } + + // Escalation is disallowed here, because we need to be sure that passed owner is real + Self::burn_item_internal(from, collection, item_id, amount, false)?; + + if matches!(collection.mode, CollectionMode::Fungible(_)) { + collection.log(ERC20Events::Approval { + owner: *from.as_eth(), + spender: *sender.as_eth(), + value: allowance.into(), + })?; + } Ok(()) } @@ -1884,31 +1977,42 @@ collection: &CollectionHandle, item_id: TokenId, owner: &T::CrossAccountId, + value: u128, ) -> DispatchResult { let collection_id = collection.id; let mut token = >::get(collection_id, item_id) .ok_or(Error::::TokenNotFound)?; - let rft_balance = token + let mut rft_balance = token .owner .iter() .find(|&i| i.owner == *owner) - .ok_or(Error::::TokenNotFound)?; + .ok_or(Error::::TokenNotFound)? + .clone(); Self::remove_token_index(collection, item_id, owner)?; // update balance let new_balance = >::get(collection_id, rft_balance.owner.as_sub()) - .checked_sub(rft_balance.fraction) + .checked_sub(value) .ok_or(Error::::NumOverflow)?; >::insert(collection_id, rft_balance.owner.as_sub(), new_balance); - // Re-create owners list with sender removed + rft_balance.fraction = (rft_balance.fraction) + .checked_sub(value) + .ok_or(Error::::NumOverflow)?; + let index = token .owner .iter() .position(|i| i.owner == *owner) .expect("owned item is exists"); - token.owner.remove(index); + if rft_balance.fraction == 0 { + // Re-create owners list with sender removed + token.owner.remove(index); + } else { + token.owner[index] = rft_balance; + } + let owner_count = token.owner.len(); // Burn the token completely if this was the last (only) owner @@ -1947,8 +2051,8 @@ } fn burn_fungible_item( + collection: &CollectionHandle, owner: &T::CrossAccountId, - collection: &CollectionHandle, value: u128, ) -> DispatchResult { let collection_id = collection.id; --- a/pallets/nft/src/tests.rs +++ b/pallets/nft/src/tests.rs @@ -707,9 +707,15 @@ assert_eq!(TemplateModule::balance_count(1, 1), 1); // burn item - assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1, 5)); + assert_ok!(TemplateModule::burn_item( + origin1.clone(), + 1, + 1, + account(1), + 5 + )); assert_noop!( - TemplateModule::burn_item(origin1, 1, 1, 5), + TemplateModule::burn_item(origin1, 1, 1, account(1), 5), Error::::TokenNotFound ); @@ -736,9 +742,15 @@ assert_eq!(TemplateModule::balance_count(1, 1), 5); // burn item - assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1, 5)); + assert_ok!(TemplateModule::burn_item( + origin1.clone(), + 1, + 1, + account(1), + 5 + )); assert_noop!( - TemplateModule::burn_item(origin1, 1, 1, 5), + TemplateModule::burn_item(origin1, 1, 1, account(1), 5), Error::::TokenValueNotEnough ); @@ -781,9 +793,15 @@ assert_eq!(TemplateModule::balance_count(1, 1), 1023); // burn item - assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1, 1023)); + assert_ok!(TemplateModule::burn_item( + origin1.clone(), + 1, + 1, + account(1), + 1023 + )); assert_noop!( - TemplateModule::burn_item(origin1, 1, 1, 1023), + TemplateModule::burn_item(origin1, 1, 1, account(1), 1023), Error::::TokenNotFound ); @@ -1378,7 +1396,7 @@ AccessMode::WhiteList )); assert_noop!( - TemplateModule::burn_item(origin1, 1, 1, 5), + TemplateModule::burn_item(origin1.clone(), 1, 1, account(1), 5), Error::::AddresNotInWhiteList ); }); --- a/tests/.vscode/settings.json +++ b/tests/.vscode/settings.json @@ -1,3 +1,5 @@ { - "mocha.enabled": true -} \ No newline at end of file + "mocha.enabled": true, + "mochaExplorer.files": "**/*.test.ts", + "mochaExplorer.require": "ts-node/register" +} --- a/tests/package.json +++ b/tests/package.json @@ -20,6 +20,10 @@ "tslint": "^6.1.3", "typescript": "^4.4.4" }, + "mocha": { + "timeout": 9999999, + "require": "ts-node/register" + }, "scripts": { "lint": "eslint --ext .ts,.js src/", "fix": "eslint --ext .ts,.js src/ --fix", --- a/tests/src/burnItem.test.ts +++ b/tests/src/burnItem.test.ts @@ -38,7 +38,7 @@ const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); await usingApi(async (api) => { - const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const tx = api.tx.nft.burnItem(collectionId, tokenId, 1); const events = await submitTransactionAsync(alice, tx); const result = getGenericResult(events); // Get the item @@ -79,7 +79,7 @@ const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); await usingApi(async (api) => { - const tx = api.tx.nft.burnItem(collectionId, tokenId, 1); + const tx = api.tx.nft.burnItem(collectionId, tokenId, 100); const events = await submitTransactionAsync(alice, tx); const result = getGenericResult(events); @@ -107,7 +107,7 @@ const balanceBefore: any = (await api.query.nft.reFungibleItemList(collectionId, tokenId)).toJSON(); // Bob burns his portion - const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const tx = api.tx.nft.burnItem(collectionId, tokenId, 1); const events2 = await submitTransactionAsync(bob, tx); const result2 = getGenericResult(events2); @@ -152,7 +152,7 @@ await addCollectionAdminExpectSuccess(alice, collectionId, bob); await usingApi(async (api) => { - const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const tx = api.tx.nft.burnItem(collectionId, tokenId, 1); const events = await submitTransactionAsync(bob, tx); const result = getGenericResult(events); // Get the item @@ -164,6 +164,48 @@ expect(item).to.be.null; }); }); + + + it('Burn item in Fungible collection', async () => { + const createMode = 'Fungible'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0 }}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); // Helper creates 10 fungible tokens + await addCollectionAdminExpectSuccess(alice, collectionId, bob); + + await usingApi(async (api) => { + // Destroy 1 of 10 + const tx = api.tx.nft.burnFrom(collectionId, normalizeAccountId(alice.address), tokenId, 1); + const events = await submitTransactionAsync(bob, tx); + const result = getGenericResult(events); + + // Get alice balance + const balance: any = (await api.query.nft.fungibleItemList(collectionId, alice.address)).toJSON(); + + // What to expect + expect(result.success).to.be.true; + expect(balance).to.be.not.null; + expect(balance.Value).to.be.equal(9); + }); + }); + + it('Burn item in ReFungible collection', async () => { + const createMode = 'ReFungible'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode }}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); + await addCollectionAdminExpectSuccess(alice, collectionId, bob); + + await usingApi(async (api) => { + const tx = api.tx.nft.burnFrom(collectionId, normalizeAccountId(alice.address), tokenId, 100); + const events = await submitTransactionAsync(bob, tx); + const result = getGenericResult(events); + // Get alice balance + const balance: any = (await api.query.nft.reFungibleItemList(collectionId, tokenId)).toJSON(); + + // What to expect + expect(result.success).to.be.true; + expect(balance).to.be.null; + }); + }); }); describe('Negative integration test: ext. burnItem():', () => { @@ -197,8 +239,8 @@ const tokenId = 10; await usingApi(async (api) => { - const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); - const badTransaction = async function () { + const tx = api.tx.nft.burnItem(collectionId, tokenId, 1); + const badTransaction = async function () { await submitTransactionExpectFailAsync(alice, tx); }; await expect(badTransaction()).to.be.rejected; @@ -228,7 +270,7 @@ await usingApi(async (api) => { - const burntx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const burntx = api.tx.nft.burnItem(collectionId, tokenId, 1); const events1 = await submitTransactionAsync(alice, burntx); const result1 = getGenericResult(events1); expect(result1.success).to.be.true; --- a/tests/src/transfer.test.ts +++ b/tests/src/transfer.test.ts @@ -199,7 +199,7 @@ const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); - await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 1); + await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 100); await transferExpectFailure( reFungibleCollectionId, newReFungibleTokenId, --- a/tests/src/transferFrom.test.ts +++ b/tests/src/transferFrom.test.ts @@ -265,7 +265,7 @@ await usingApi(async () => { const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); - await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 1); + await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 100); await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob); await transferFromExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice, Charlie, 1); @@ -297,7 +297,7 @@ const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob); - await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 1); + await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 100); await transferFromExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice, Charlie, 1); }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -700,10 +700,10 @@ ReFungible: CreateReFungibleData; }; -export async function burnItemExpectSuccess(owner: IKeyringPair, collectionId: number, tokenId: number, value = 0) { +export async function burnItemExpectSuccess(sender: IKeyringPair, collectionId: number, tokenId: number, value = 1) { await usingApi(async (api) => { const tx = api.tx.nft.burnItem(collectionId, tokenId, value); - const events = await submitTransactionAsync(owner, tx); + const events = await submitTransactionAsync(sender, tx); const result = getGenericResult(events); // Get the item const item: any = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON(); -- gitstuff