--- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -306,9 +306,6 @@ /// Amount of collections destroyed, used for total amount tracking with /// CreatedCollectionCount DestroyedCollectionCount: u32; - /// Total amount of account owned tokens (NFTs + RFTs + unique fungibles) - /// Account id (real) - pub AccountItemCount get(fn account_item_count): map hasher(twox_64_concat) T::AccountId => u32; //#endregion //#region Basic collections @@ -1125,6 +1122,7 @@ let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let collection = Self::get_collection(collection_id)?; + Self::meta_update_check(&sender, &collection, item_id)?; Self::set_variable_meta_data_internal(&sender, &collection, item_id, data)?; @@ -1647,11 +1645,19 @@ collection.consume_sload()?; let account_items: u32 = >::get(collection_id, recipient.as_sub()).len() as u32; - ensure!( - collection.limits.account_token_ownership_limit > account_items, - Error::::AccountTokenLimitExceeded - ); + // zero limit means collection limit is disabled + // otherwise get lower value + let limit = if collection.limits.account_token_ownership_limit == 0 + || collection.limits.account_token_ownership_limit > ACCOUNT_TOKEN_OWNERSHIP_LIMIT + { + ACCOUNT_TOKEN_OWNERSHIP_LIMIT + } else { + collection.limits.account_token_ownership_limit + }; + + ensure!(limit > account_items, Error::::AccountTokenLimitExceeded); + // preliminary transfer check ensure!(collection.transfers_enabled, Error::::TransferNotAllowed); @@ -1674,12 +1680,23 @@ as u32) .checked_add(amount) .ok_or(Error::::AccountTokenLimitExceeded)?; + + // zero limit means collection limit is disabled + // otherwise get lower value + let account_token_limit = if collection.limits.account_token_ownership_limit == 0 + || collection.limits.account_token_ownership_limit > ACCOUNT_TOKEN_OWNERSHIP_LIMIT + { + ACCOUNT_TOKEN_OWNERSHIP_LIMIT + } else { + collection.limits.account_token_ownership_limit + }; + ensure!( collection.limits.token_limit >= total_items, Error::::CollectionTokenLimitExceeded ); ensure!( - collection.limits.account_token_ownership_limit >= account_items, + account_token_limit >= account_items, Error::::AccountTokenLimitExceeded ); @@ -2409,32 +2426,19 @@ item_index: TokenId, owner: &T::CrossAccountId, ) -> DispatchResult { - // add to account limit collection.consume_sload()?; - if >::contains_key(owner.as_sub()) { - // bound Owned tokens by a single address + let list_exists = >::contains_key(collection.id, owner.as_sub()); + if list_exists { collection.consume_sload()?; - let count = >::get(owner.as_sub()); + let mut list = >::get(collection.id, owner.as_sub()); + + // bound Owned tokens by a single address in collection + let account_items: u32 = list.len() as u32; ensure!( - count < ACCOUNT_TOKEN_OWNERSHIP_LIMIT, + account_items < ACCOUNT_TOKEN_OWNERSHIP_LIMIT, Error::::AddressOwnershipLimitExceeded ); - collection.consume_sstore()?; - >::insert( - owner.as_sub(), - count.checked_add(1).ok_or(Error::::NumOverflow)?, - ); - } else { - collection.consume_sstore()?; - >::insert(owner.as_sub(), 1); - } - - collection.consume_sload()?; - let list_exists = >::contains_key(collection.id, owner.as_sub()); - if list_exists { - collection.consume_sload()?; - let mut list = >::get(collection.id, owner.as_sub()); let item_contains = list.contains(&item_index.clone()); if !item_contains { @@ -2457,16 +2461,6 @@ item_index: TokenId, owner: &T::CrossAccountId, ) -> DispatchResult { - // update counter - collection.consume_sload()?; - collection.consume_sstore()?; - >::insert( - owner.as_sub(), - >::get(owner.as_sub()) - .checked_sub(1) - .ok_or(Error::::NumOverflow)?, - ); - collection.consume_sload()?; let list_exists = >::contains_key(collection.id, owner.as_sub()); if list_exists { --- a/pallets/nft/src/sponsorship.rs +++ b/pallets/nft/src/sponsorship.rs @@ -61,10 +61,15 @@ sponsor_transfer = match collection_mode { CollectionMode::NFT => { // get correct limit - let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { - collection_limits.sponsor_transfer_timeout + let limit: u32 = if collection_limits.sponsor_transfer_timeout != 0 { + if collection_limits.sponsor_transfer_timeout > NFT_SPONSOR_TRANSFER_TIMEOUT + { + collection_limits.sponsor_transfer_timeout + } else { + NFT_SPONSOR_TRANSFER_TIMEOUT + } } else { - NFT_SPONSOR_TRANSFER_TIMEOUT + 0 }; let mut sponsored = true; @@ -83,13 +88,18 @@ } CollectionMode::Fungible(_) => { // get correct limit - let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { - collection_limits.sponsor_transfer_timeout + let limit: u32 = if collection_limits.sponsor_transfer_timeout != 0 { + if collection_limits.sponsor_transfer_timeout + > FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT + { + collection_limits.sponsor_transfer_timeout + } else { + FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT + } } else { - FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT + 0 }; - let block_number = >::block_number() as T::BlockNumber; let mut sponsored = true; if FungibleTransferBasket::::contains_key(collection_id, who) { let last_tx_block = FungibleTransferBasket::::get(collection_id, who); @@ -106,10 +116,16 @@ } CollectionMode::ReFungible => { // get correct limit - let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { - collection_limits.sponsor_transfer_timeout + let limit: u32 = if collection_limits.sponsor_transfer_timeout != 0 { + if collection_limits.sponsor_transfer_timeout + > REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT + { + collection_limits.sponsor_transfer_timeout + } else { + REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT + } } else { - REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT + 0 }; let mut sponsored = true; --- a/pallets/nft/src/tests.rs +++ b/pallets/nft/src/tests.rs @@ -1789,7 +1789,7 @@ let data = default_nft_data(); assert_noop!( TemplateModule::create_item(origin1, 1, account(1), data.into()), - Error::::AddressOwnershipLimitExceeded + Error::::AccountTokenLimitExceeded ); }); } @@ -2016,11 +2016,11 @@ let data = default_nft_data(); create_test_item(1, &data.into()); - TemplateModule::set_meta_update_permission_flag( + assert_ok!(TemplateModule::set_meta_update_permission_flag( origin1.clone(), collection_id, MetaUpdatePermission::ItemOwner, - ); + )); let variable_data = b"ten chars.".to_vec(); assert_ok!(TemplateModule::set_variable_meta_data( @@ -2042,20 +2042,17 @@ #[test] fn set_variable_meta_data_on_nft_with_item_owner_permission_flag_neg() { new_test_ext().execute_with(|| { - // default_limits(); - - let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 2, 1); + let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 1, 1); let origin1 = Origin::signed(1); - let origin2 = Origin::signed(2); assert_ok!(TemplateModule::set_mint_permission( - origin2.clone(), + origin1.clone(), collection_id, true )); assert_ok!(TemplateModule::add_to_white_list( - origin2.clone(), + origin1.clone(), collection_id, account(1) )); @@ -2064,226 +2061,226 @@ create_test_item(1, &data.into()); assert_ok!(TemplateModule::set_meta_update_permission_flag( - origin2.clone(), + origin1.clone(), collection_id, MetaUpdatePermission::ItemOwner, )); - let variable_data = b"ten chars.++".to_vec(); + let variable_data = b"1234567890123".to_vec(); assert_noop!( TemplateModule::set_variable_meta_data( - origin2, + origin1, collection_id, 1, variable_data.clone() ), Error::::TokenVariableDataLimitExceeded ); + }) +} - #[test] - fn collection_transfer_flag_works() { - new_test_ext().execute_with(|| { - let origin1 = Origin::signed(1); +#[test] +fn collection_transfer_flag_works() { + new_test_ext().execute_with(|| { + let origin1 = Origin::signed(1); - let collection_id = create_test_collection(&CollectionMode::NFT, 1); - assert_ok!(TemplateModule::set_transfers_enabled_flag(origin1, 1, true)); + let collection_id = create_test_collection(&CollectionMode::NFT, 1); + assert_ok!(TemplateModule::set_transfers_enabled_flag(origin1, 1, true)); - let data = default_nft_data(); - create_test_item(collection_id, &data.into()); - assert_eq!(TemplateModule::balance_count(1, 1), 1); - assert_eq!(TemplateModule::address_tokens(1, 1), [1]); + let data = default_nft_data(); + create_test_item(collection_id, &data.into()); + assert_eq!(TemplateModule::balance_count(1, 1), 1); + assert_eq!(TemplateModule::address_tokens(1, 1), [1]); - let origin1 = Origin::signed(1); + let origin1 = Origin::signed(1); - // default scenario - assert_ok!(TemplateModule::transfer(origin1, account(2), 1, 1, 1000)); - assert_eq!(TemplateModule::nft_item_id(1, 1).unwrap().owner, account(2)); - assert_eq!(TemplateModule::balance_count(1, 1), 0); - assert_eq!(TemplateModule::balance_count(1, 2), 1); + // default scenario + assert_ok!(TemplateModule::transfer(origin1, account(2), 1, 1, 1000)); + assert_eq!(TemplateModule::nft_item_id(1, 1).unwrap().owner, account(2)); + assert_eq!(TemplateModule::balance_count(1, 1), 0); + assert_eq!(TemplateModule::balance_count(1, 2), 1); - assert_eq!(TemplateModule::address_tokens(1, 2), [1]); - }); - } + assert_eq!(TemplateModule::address_tokens(1, 2), [1]); + }); +} - #[test] - fn set_variable_meta_data_on_nft_with_admin_flag() { - new_test_ext().execute_with(|| { - // default_limits(); +#[test] +fn set_variable_meta_data_on_nft_with_admin_flag() { + new_test_ext().execute_with(|| { + // default_limits(); - let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 2, 1); + let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 2, 1); - let origin1 = Origin::signed(1); - let origin2 = Origin::signed(2); + let origin1 = Origin::signed(1); + let origin2 = Origin::signed(2); - assert_ok!(TemplateModule::set_mint_permission( - origin2.clone(), - collection_id, - true - )); - assert_ok!(TemplateModule::add_to_white_list( - origin2.clone(), - collection_id, - account(1) - )); + assert_ok!(TemplateModule::set_mint_permission( + origin2.clone(), + collection_id, + true + )); + assert_ok!(TemplateModule::add_to_white_list( + origin2.clone(), + collection_id, + account(1) + )); - assert_ok!(TemplateModule::add_collection_admin( - origin2.clone(), - collection_id, - account(1) - )); + assert_ok!(TemplateModule::add_collection_admin( + origin2.clone(), + collection_id, + account(1) + )); - let data = default_nft_data(); - create_test_item(1, &data.into()); + let data = default_nft_data(); + create_test_item(1, &data.into()); - assert_ok!(TemplateModule::set_meta_update_permission_flag( - origin2.clone(), - collection_id, - MetaUpdatePermission::Admin, - )); + assert_ok!(TemplateModule::set_meta_update_permission_flag( + origin2.clone(), + collection_id, + MetaUpdatePermission::Admin, + )); - let variable_data = b"test set_variable_meta_data method.".to_vec(); - assert_ok!(TemplateModule::set_variable_meta_data( - origin1, - collection_id, - 1, - variable_data.clone() - )); + let variable_data = b"test.".to_vec(); + assert_ok!(TemplateModule::set_variable_meta_data( + origin1, + collection_id, + 1, + variable_data.clone() + )); - assert_eq!( - TemplateModule::nft_item_id(collection_id, 1) - .unwrap() - .variable_data, - variable_data - ); - }); - } + assert_eq!( + TemplateModule::nft_item_id(collection_id, 1) + .unwrap() + .variable_data, + variable_data + ); + }); +} - #[test] - fn set_variable_meta_data_on_nft_with_admin_flag_neg() { - new_test_ext().execute_with(|| { - // default_limits(); +#[test] +fn set_variable_meta_data_on_nft_with_admin_flag_neg() { + new_test_ext().execute_with(|| { + // default_limits(); - let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 2, 1); + let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 2, 1); - let origin1 = Origin::signed(1); - let origin2 = Origin::signed(2); + let origin1 = Origin::signed(1); + let origin2 = Origin::signed(2); - assert_ok!(TemplateModule::set_mint_permission( - origin2.clone(), - collection_id, - true - )); - assert_ok!(TemplateModule::add_to_white_list( - origin2.clone(), - collection_id, - account(1) - )); + assert_ok!(TemplateModule::set_mint_permission( + origin2.clone(), + collection_id, + true + )); + assert_ok!(TemplateModule::add_to_white_list( + origin2.clone(), + collection_id, + account(1) + )); - let data = default_nft_data(); - create_test_item(1, &data.into()); + let data = default_nft_data(); + create_test_item(1, &data.into()); - assert_ok!(TemplateModule::set_meta_update_permission_flag( - origin2.clone(), - collection_id, - MetaUpdatePermission::Admin, - )); + assert_ok!(TemplateModule::set_meta_update_permission_flag( + origin2.clone(), + collection_id, + MetaUpdatePermission::Admin, + )); - let variable_data = b"test set_variable_meta_data method.".to_vec(); - assert_noop!( - TemplateModule::set_variable_meta_data( - origin1, - collection_id, - 1, - variable_data.clone() - ), - Error::::NoPermission - ); - }); - } + let variable_data = b"test.".to_vec(); + assert_noop!( + TemplateModule::set_variable_meta_data( + origin1, + collection_id, + 1, + variable_data.clone() + ), + Error::::NoPermission + ); + }); +} - #[test] - fn set_variable_meta_flag_after_freeze() { - new_test_ext().execute_with(|| { - // default_limits(); +#[test] +fn set_variable_meta_flag_after_freeze() { + new_test_ext().execute_with(|| { + // default_limits(); - let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 2, 1); + let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 2, 1); - let origin2 = Origin::signed(2); + let origin2 = Origin::signed(2); - assert_ok!(TemplateModule::set_meta_update_permission_flag( - origin2.clone(), - collection_id, - MetaUpdatePermission::None, - )); - assert_noop!( - TemplateModule::set_meta_update_permission_flag( - origin2.clone(), - collection_id, - MetaUpdatePermission::Admin - ), - Error::::MetadataFlagFrozen - ); - }); - } + assert_ok!(TemplateModule::set_meta_update_permission_flag( + origin2.clone(), + collection_id, + MetaUpdatePermission::None, + )); + assert_noop!( + TemplateModule::set_meta_update_permission_flag( + origin2.clone(), + collection_id, + MetaUpdatePermission::Admin + ), + Error::::MetadataFlagFrozen + ); + }); +} - #[test] - fn set_variable_meta_data_on_nft_with_none_flag_neg() { - new_test_ext().execute_with(|| { - // default_limits(); +#[test] +fn set_variable_meta_data_on_nft_with_none_flag_neg() { + new_test_ext().execute_with(|| { + // default_limits(); - let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 1, 1); - let origin1 = Origin::signed(1); + let collection_id = create_test_collection_for_owner(&CollectionMode::NFT, 1, 1); + let origin1 = Origin::signed(1); - let data = default_nft_data(); - create_test_item(1, &data.into()); + let data = default_nft_data(); + create_test_item(1, &data.into()); - assert_ok!(TemplateModule::set_meta_update_permission_flag( - origin1.clone(), - collection_id, - MetaUpdatePermission::None, - )); + assert_ok!(TemplateModule::set_meta_update_permission_flag( + origin1.clone(), + collection_id, + MetaUpdatePermission::None, + )); - let variable_data = b"test set_variable_meta_data method.".to_vec(); - assert_noop!( - TemplateModule::set_variable_meta_data( - origin1.clone(), - collection_id, - 1, - variable_data.clone() - ), - Error::::MetadataUpdateDenied - ); - }); - } + let variable_data = b"test.".to_vec(); + assert_noop!( + TemplateModule::set_variable_meta_data( + origin1.clone(), + collection_id, + 1, + variable_data.clone() + ), + Error::::MetadataUpdateDenied + ); + }); +} - #[test] - fn collection_transfer_flag_works_neg() { - new_test_ext().execute_with(|| { - let origin1 = Origin::signed(1); +#[test] +fn collection_transfer_flag_works_neg() { + new_test_ext().execute_with(|| { + let origin1 = Origin::signed(1); - let collection_id = create_test_collection(&CollectionMode::NFT, 1); - assert_ok!(TemplateModule::set_transfers_enabled_flag( - origin1, 1, false - )); + let collection_id = create_test_collection(&CollectionMode::NFT, 1); + assert_ok!(TemplateModule::set_transfers_enabled_flag( + origin1, 1, false + )); - let data = default_nft_data(); - create_test_item(collection_id, &data.into()); - assert_eq!(TemplateModule::balance_count(1, 1), 1); - assert_eq!(TemplateModule::address_tokens(1, 1), [1]); + let data = default_nft_data(); + create_test_item(collection_id, &data.into()); + assert_eq!(TemplateModule::balance_count(1, 1), 1); + assert_eq!(TemplateModule::address_tokens(1, 1), [1]); - let origin1 = Origin::signed(1); + let origin1 = Origin::signed(1); - // default scenario - assert_noop!( - TemplateModule::transfer(origin1, account(2), 1, 1, 1000), - Error::::TransferNotAllowed - ); - assert_eq!(TemplateModule::nft_item_id(1, 1).unwrap().owner, account(1)); - assert_eq!(TemplateModule::balance_count(1, 1), 1); - assert_eq!(TemplateModule::balance_count(1, 2), 0); + // default scenario + assert_noop!( + TemplateModule::transfer(origin1, account(2), 1, 1, 1000), + Error::::TransferNotAllowed + ); + assert_eq!(TemplateModule::nft_item_id(1, 1).unwrap().owner, account(1)); + assert_eq!(TemplateModule::balance_count(1, 1), 1); + assert_eq!(TemplateModule::balance_count(1, 2), 0); - assert_eq!(TemplateModule::address_tokens(1, 1), [1]); - }); - } + assert_eq!(TemplateModule::address_tokens(1, 1), [1]); }); } --- /dev/null +++ b/tests/src/limits.test.ts @@ -0,0 +1,378 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import { IKeyringPair } from '@polkadot/types/types'; +import privateKey from './substrate/privateKey'; +import usingApi from './substrate/substrate-api'; +import { + createCollectionExpectSuccess, + destroyCollectionExpectSuccess, + setCollectionLimitsExpectSuccess, + setCollectionSponsorExpectSuccess, + confirmSponsorshipExpectSuccess, + createItemExpectSuccess, + createItemExpectFailure, + transferExpectSuccess, + getFreeBalance, + waitNewBlocks, +} from './util/helpers'; +import { expect } from 'chai'; + +describe('Number of tokens per address (NFT)', () => { + let Alice: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + }); + }); + + it('Collection limits allow greater number than chain limits, chain limits are enforced', async () => { + + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 20 }); + for(let i = 0; i < 10; i++){ + await createItemExpectSuccess(Alice, collectionId, 'NFT'); + } + await createItemExpectFailure(Alice, collectionId, 'NFT'); + await destroyCollectionExpectSuccess(collectionId); + }); + + it('Collection limits allow lower number than chain limits, collection limits are enforced', async () => { + + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 1 }); + await createItemExpectSuccess(Alice, collectionId, 'NFT'); + await createItemExpectFailure(Alice, collectionId, 'NFT'); + await destroyCollectionExpectSuccess(collectionId); + }); +}); + +describe('Number of tokens per address (ReFungible)', () => { + let Alice: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + }); + }); + + it('Collection limits allow greater number than chain limits, chain limits are enforced', async () => { + const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }}); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 20 }); + for(let i = 0; i < 10; i++){ + await createItemExpectSuccess(Alice, collectionId, 'ReFungible'); + } + await createItemExpectFailure(Alice, collectionId, 'ReFungible'); + await destroyCollectionExpectSuccess(collectionId); + }); + + it('Collection limits allow lower number than chain limits, collection limits are enforced', async () => { + const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }}); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 1 }); + await createItemExpectSuccess(Alice, collectionId, 'ReFungible'); + await createItemExpectFailure(Alice, collectionId, 'ReFungible'); + await destroyCollectionExpectSuccess(collectionId); + }); +}); + +describe('Sponsor timeout (NFT)', () => { + let Alice: IKeyringPair; + let Bob: IKeyringPair; + let Charlie: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + Charlie = privateKey('//Charlie'); + }); + }); + + it('Collection limits have greater timeout value than chain limits, collection limits are enforced', async () => { + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 7 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 5, fail + await waitNewBlocks(5); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie); + const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore); + + // check setting SponsorTimeout = 7, success + await waitNewBlocks(2); // 5 + 2 + await transferExpectSuccess(collectionId, tokenId, Charlie, Bob); + const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore); + await destroyCollectionExpectSuccess(collectionId); + }); + + it('Collection limits have lower timeout value than chain limits, chain limits are enforced', async () => { + + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 1 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 1, fail + await waitNewBlocks(1); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie); + const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore); + + // check setting SponsorTimeout = 5, success + await waitNewBlocks(4); + await transferExpectSuccess(collectionId, tokenId, Charlie, Bob); + const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore); + await destroyCollectionExpectSuccess(collectionId); + }); +}); + +describe('Sponsor timeout (Fungible)', () => { + let Alice: IKeyringPair; + let Bob: IKeyringPair; + let Charlie: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + Charlie = privateKey('//Charlie'); + }); + }); + + it('Collection limits have greater timeout value than chain limits, collection limits are enforced', async () => { + const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 7 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 10, 'Fungible'); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 5, fail + await waitNewBlocks(5); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore); + + // check setting SponsorTimeout = 7, success + await waitNewBlocks(2); // 5 + 2 + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore); + + await destroyCollectionExpectSuccess(collectionId); + }); + + it('Collection limits have lower timeout value than chain limits, chain limits are enforced', async () => { + + const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 1 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 10, 'Fungible'); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 1, fail + await waitNewBlocks(1); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore); + + // check setting SponsorTimeout = 5, success + await waitNewBlocks(4); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore); + + await destroyCollectionExpectSuccess(collectionId); + }); +}); + +describe('Sponsor timeout (ReFungible)', () => { + let Alice: IKeyringPair; + let Bob: IKeyringPair; + let Charlie: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + Charlie = privateKey('//Charlie'); + }); + }); + + it('Collection limits have greater timeout value than chain limits, collection limits are enforced', async () => { + const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }}); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 7 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'ReFungible'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 100, 'ReFungible'); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 5, fail + await waitNewBlocks(5); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible'); + const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore); + + // check setting SponsorTimeout = 7, success + await waitNewBlocks(2); // 5 + 2 + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible'); + const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore); + await destroyCollectionExpectSuccess(collectionId); + }); + + it('Collection limits have lower timeout value than chain limits, chain limits are enforced', async () => { + + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 1 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 1, fail + await waitNewBlocks(1); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie); + const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore); + + // check setting SponsorTimeout = 5, success + await waitNewBlocks(4); + await transferExpectSuccess(collectionId, tokenId, Charlie, Bob); + const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore); + await destroyCollectionExpectSuccess(collectionId); + }); +}); + +describe('Collection zero limits (NFT)', () => { + let Alice: IKeyringPair; + let Bob: IKeyringPair; + let Charlie: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + Charlie = privateKey('//Charlie'); + }); + }); + + it('Limits have 0 in tokens per address field, the chain limits are applied', async () => { + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 0 }); + for(let i = 0; i < 10; i++){ + await createItemExpectSuccess(Alice, collectionId, 'NFT'); + } + await createItemExpectFailure(Alice, collectionId, 'NFT'); + }); + + it('Limits have 0 in sponsor timeout, no limits are applied', async () => { + + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 0 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 0, success with next block + await waitNewBlocks(1); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie); + const aliceBalanceAfterSponsoredTransaction1 = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction1).to.be.lessThan(aliceBalanceBefore); + }); +}); + +describe.only('Collection zero limits (Fungible)', () => { + let Alice: IKeyringPair; + let Bob: IKeyringPair; + let Charlie: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + Charlie = privateKey('//Charlie'); + }); + }); + + it('Limits have 0 in sponsor timeout, no limits are applied', async () => { + const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 0 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 10, 'Fungible'); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + + // check setting SponsorTimeout = 0, success with next block + await waitNewBlocks(1); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible'); + const aliceBalanceAfterSponsoredTransaction1 = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction1).to.be.lessThan(aliceBalanceBefore); + }); +}); + +describe.only('Collection zero limits (ReFungible)', () => { + let Alice: IKeyringPair; + let Bob: IKeyringPair; + let Charlie: IKeyringPair; + + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + Charlie = privateKey('//Charlie'); + }); + }); + + it('Limits have 0 in tokens per address field, the chain limits are applied', async () => { + const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }}); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 0 }); + for(let i = 0; i < 10; i++){ + await createItemExpectSuccess(Alice, collectionId, 'ReFungible'); + } + await createItemExpectFailure(Alice, collectionId, 'ReFungible'); + }); + + it('Limits have 0 in sponsor timeout, no limits are applied', async () => { + + const collectionId = await createCollectionExpectSuccess({ mode: { type: 'ReFungible' } }); + await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 0 }); + const tokenId = await createItemExpectSuccess(Alice, collectionId, 'ReFungible'); + await setCollectionSponsorExpectSuccess(collectionId, Alice.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Alice'); + await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 100, 'ReFungible'); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible'); + const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber(); + + // check setting SponsorTimeout = 0, success with next block + await waitNewBlocks(1); + await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible'); + const aliceBalanceAfterSponsoredTransaction1 = (await getFreeBalance(Alice)).toNumber(); + expect(aliceBalanceAfterSponsoredTransaction1).to.be.lessThan(aliceBalanceBefore); + }); +}); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -811,6 +811,17 @@ } export async function +getFreeBalance(account: IKeyringPair) : Promise +{ + let balance = new BigNumber(0) ; + await usingApi(async (api) => { + balance = new BigNumber((await api.query.system.account(account.address)).data.free.toString()); + }); + + return balance; +} + +export async function scheduleTransferExpectSuccess( collectionId: number, tokenId: number, @@ -884,8 +895,11 @@ if (type === 'ReFungible') { const nftItemData = (await api.query.nft.reFungibleItemList(collectionId, tokenId)).toJSON() as unknown as IReFungibleTokenDataType; - expect(nftItemData.Owner[0].Owner).to.be.deep.equal(to); - expect(nftItemData.Owner[0].Fraction.toString()).to.be.equal(value.toString()); + const expectedOwner = toSubstrateAddress(to); + const ownerIndex = nftItemData.Owner.findIndex(v => toSubstrateAddress(v.Owner as any as string) == expectedOwner); + expect(ownerIndex).to.not.equal(-1); + expect(nftItemData.Owner[ownerIndex].Owner).to.be.deep.equal(normalizeAccountId(to)); + expect(nftItemData.Owner[ownerIndex].Fraction).to.be.greaterThanOrEqual(value as number); } }); } @@ -1190,7 +1204,6 @@ export async function waitNewBlocks(blocksCount = 1): Promise { await usingApi(async (api) => { const promise = new Promise(async (resolve) => { - const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => { if (blocksCount > 0) { blocksCount--;