--- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -152,8 +152,7 @@ #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct FungibleItemType { - pub owner: AccountId, +pub struct FungibleItemType { pub value: u128, } @@ -165,13 +164,6 @@ pub variable_data: Vec, } -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct ApprovePermissions { - pub approved: AccountId, - pub amount: u128, -} - // #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] // #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] // pub struct VestingItem { @@ -182,13 +174,6 @@ // pub amount: u64, // pub vesting_date: Moment, // } - -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct BasketItem { - pub address: AccountId, - pub start_block: BlockNumber, -} #[derive(Encode, Decode, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] @@ -260,6 +245,7 @@ #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct CreateFungibleData { + pub value: u128, } #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] @@ -414,12 +400,12 @@ /// Balance owner per collection map pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128; - /// second parameter: item id + owner account id - pub ApprovedList get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId) => Vec>; + /// second parameter: item id + owner account id + spender account id + pub Allowances get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId, T::AccountId) => u128; /// Item collections pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType; - pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => FungibleItemType; + pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => FungibleItemType; pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType; /// Index list @@ -427,7 +413,7 @@ /// Tokens transfer baskets pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber; - pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => Vec>; + pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber; pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber; // Contract Sponsorship and Ownership @@ -447,8 +433,8 @@ >::init_nft_token(*_c, _i); } - for (_num, _c, _i) in &config.fungible_item_id { - >::init_fungible_token(*_c, _i); + for (collection_id, account_id, fungible_item) in &config.fungible_item_id { + >::init_fungible_token(*collection_id, account_id, fungible_item); } for (_num, _c, _i) in &config.refungible_item_id { @@ -619,7 +605,7 @@ Self::check_owner_permissions(collection_id, sender)?; >::remove_prefix(collection_id); - >::remove_prefix(collection_id); + >::remove_prefix(collection_id); >::remove_prefix(collection_id); ::remove(collection_id); >::remove(collection_id); @@ -1015,7 +1001,7 @@ /// /// * item_id: ID of NFT to burn. #[weight = T::WeightInfo::burn_item()] - pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId) -> DispatchResult { + pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult { let sender = ensure_signed(origin)?; Self::collection_exists(collection_id)?; @@ -1033,7 +1019,7 @@ match target_collection.mode { CollectionMode::NFT => Self::burn_nft_item(collection_id, item_id)?, - CollectionMode::Fungible(_) => Self::burn_fungible_item(collection_id, item_id)?, + CollectionMode::Fungible(_) => Self::burn_fungible_item(&sender, collection_id, value)?, CollectionMode::ReFungible(_) => Self::burn_refungible_item(collection_id, item_id, sender.clone())?, _ => () }; @@ -1089,7 +1075,7 @@ match target_collection.mode { CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?, - CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, sender.clone(), recipient)?, + CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &sender, &recipient)?, CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?, _ => () }; @@ -1113,7 +1099,7 @@ /// /// * item_id: ID of the item. #[weight = T::WeightInfo::approve()] - pub fn approve(origin, approved: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> DispatchResult { + pub fn approve(origin, spender: T::AccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -1125,28 +1111,15 @@ if target_collection.access == AccessMode::WhiteList { Self::check_white_list(collection_id, &sender)?; - Self::check_white_list(collection_id, &approved)?; + Self::check_white_list(collection_id, &spender)?; } - // amount param stub - let amount = 100000000; - - let list_exists = >::contains_key(collection_id, (item_id, sender.clone())); - if list_exists { - - let mut list = >::get(collection_id, (item_id, sender.clone())); - let item_contains = list.iter().any(|i| i.approved == approved); - - if !item_contains { - list.push(ApprovePermissions { approved: approved.clone(), amount: amount }); - >::insert(collection_id, (item_id, sender.clone()), list); - } - } else { - - let mut list = Vec::new(); - list.push(ApprovePermissions { approved: approved.clone(), amount: amount }); - >::insert(collection_id, (item_id, sender.clone()), list); + let allowance_exists = >::contains_key(collection_id, (item_id, &sender, &spender)); + let mut allowance: u128 = amount; + if allowance_exists { + allowance += >::get(collection_id, (item_id, &sender, &spender)); } + >::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance); Ok(()) } @@ -1176,15 +1149,12 @@ let sender = ensure_signed(origin)?; let mut appoved_transfer = false; - // Check approve - if >::contains_key(collection_id, (item_id, from.clone())) { - let list_itm = >::get(collection_id, (item_id, from.clone())); - let opt_item = list_itm.iter().find(|i| i.approved == sender.clone()); - if opt_item.is_some() - { - appoved_transfer = true; - ensure!(opt_item.unwrap().amount >= value, Error::::TokenValueNotEnough); - } + // Check approval + let mut approval: u128 = 0; + if >::contains_key(collection_id, (item_id, &from, &recipient)) { + approval = >::get(collection_id, (item_id, &from, &recipient)); + ensure!(approval >= value, Error::::TokenValueNotEnough); + appoved_transfer = true; } let target_collection = >::get(collection_id); @@ -1194,23 +1164,25 @@ // Transfer permissions check ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()), - Error::::NoPermission); + Error::::NoPermission); if target_collection.access == AccessMode::WhiteList { Self::check_white_list(collection_id, &sender)?; Self::check_white_list(collection_id, &recipient)?; } - // remove approve - let approve_list: Vec> = >::get(collection_id, (item_id, from.clone())) - .into_iter().filter(|i| i.approved != sender.clone()).collect(); - >::insert(collection_id, (item_id, from.clone()), approve_list); - + // Reduce approval by transferred amount or remove if remaining approval drops to 0 + if approval - value > 0 { + >::insert(collection_id, (item_id, &from, &recipient), approval - value); + } + else { + >::remove(collection_id, (item_id, &from, &recipient)); + } match target_collection.mode { CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, from, recipient)?, - CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, from.clone(), recipient)?, + CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &from, &recipient)?, CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, from.clone(), recipient)?, _ => () }; @@ -1574,13 +1546,8 @@ Self::add_nft_item(collection_id, item)?; }, - CreateItemData::Fungible(_) => { - let item = FungibleItemType { - owner, - value: (10 as u128).pow(collection.decimal_points as u32) - }; - - Self::add_fungible_item(collection_id, item)?; + CreateItemData::Fungible(data) => { + Self::add_fungible_item(collection_id, &owner, data.value)?; }, CreateItemData::ReFungible(data) => { let mut owner_list = Vec::new(); @@ -1603,27 +1570,25 @@ Ok(()) } - fn add_fungible_item(collection_id: CollectionId, item: FungibleItemType) -> DispatchResult { - let current_index = ::get(collection_id) - .checked_add(1) - .ok_or(Error::::NumOverflow)?; - let itemcopy = item.clone(); - let owner = item.owner.clone(); + fn add_fungible_item(collection_id: CollectionId, owner: &T::AccountId, value: u128) -> DispatchResult { - Self::add_token_index(collection_id, current_index, owner.clone())?; + // Does new owner already have an account? + let mut balance: u128 = 0; + if >::contains_key(collection_id, owner) { + balance = >::get(collection_id, owner).value; + } - ::insert(collection_id, current_index); - >::insert(collection_id, current_index, itemcopy); + // Mint + let item = FungibleItemType { + value: balance + value + }; + >::insert(collection_id, (*owner).clone(), item); - // Add current block - let v: Vec> = Vec::new(); - >::insert(collection_id, current_index, v); - // Update balance - let new_balance = >::get(collection_id, owner.clone()) - .checked_add(item.value) + let new_balance = >::get(collection_id, owner) + .checked_add(value) .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, owner.clone(), new_balance); + >::insert(collection_id, (*owner).clone(), new_balance); Ok(()) } @@ -1642,10 +1607,6 @@ ::insert(collection_id, current_index); >::insert(collection_id, current_index, itemcopy); - // Add current block - let block_number: T::BlockNumber = 0.into(); - >::insert(collection_id, current_index, block_number); - // Update balance let new_balance = >::get(collection_id, owner.clone()) .checked_add(value) @@ -1665,10 +1626,6 @@ ::insert(collection_id, current_index); >::insert(collection_id, current_index, item); - - // Add current block - let block_number: T::BlockNumber = 0.into(); - >::insert(collection_id, current_index, block_number); // Update balance let new_balance = >::get(collection_id, item_owner.clone()) @@ -1697,9 +1654,6 @@ .unwrap(); Self::remove_token_index(collection_id, item_id, owner.clone())?; - // remove approve list - >::remove(collection_id, (item_id, owner.clone())); - // update balance let new_balance = >::get(collection_id, item.owner.clone()) .checked_sub(item.fraction) @@ -1718,9 +1672,6 @@ ); let item = >::get(collection_id, item_id); Self::remove_token_index(collection_id, item_id, item.owner.clone())?; - - // remove approve list - >::remove(collection_id, (item_id, item.owner.clone())); // update balance let new_balance = >::get(collection_id, item.owner.clone()) @@ -1732,24 +1683,27 @@ Ok(()) } - fn burn_fungible_item(collection_id: CollectionId, item_id: TokenId) -> DispatchResult { + fn burn_fungible_item(owner: &T::AccountId, collection_id: CollectionId, value: u128) -> DispatchResult { ensure!( - >::contains_key(collection_id, item_id), + >::contains_key(collection_id, owner), Error::::TokenNotFound ); - let item = >::get(collection_id, item_id); - Self::remove_token_index(collection_id, item_id, item.owner.clone())?; - - // remove approve list - >::remove(collection_id, (item_id, item.owner.clone())); + let mut balance = >::get(collection_id, owner); + ensure!(balance.value >= value, Error::::TokenValueNotEnough); // update balance - let new_balance = >::get(collection_id, item.owner.clone()) - .checked_sub(item.value) + let new_balance = >::get(collection_id, owner) + .checked_sub(value) .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, item.owner.clone(), new_balance); + >::insert(collection_id, (*owner).clone(), new_balance); - >::remove(collection_id, item_id); + if balance.value - value > 0 { + balance.value -= value; + >::insert(collection_id, (*owner).clone(), balance); + } + else { + >::remove(collection_id, owner); + } Ok(()) } @@ -1810,7 +1764,7 @@ >::get(collection_id, item_id).owner == subject } CollectionMode::Fungible(_) => { - >::get(collection_id, item_id).owner == subject + >::contains_key(collection_id, &subject) } CollectionMode::ReFungible(_) => { >::get(collection_id, item_id) @@ -1833,85 +1787,31 @@ fn transfer_fungible( collection_id: CollectionId, - item_id: TokenId, value: u128, - owner: T::AccountId, - new_owner: T::AccountId, + owner: &T::AccountId, + recipient: &T::AccountId, ) -> DispatchResult { ensure!( - >::contains_key(collection_id, item_id), + >::contains_key(collection_id, owner), Error::::TokenNotFound ); - let full_item = >::get(collection_id, item_id); - let amount = full_item.value; + let mut balance = >::get(collection_id, owner); + ensure!(balance.value >= value, Error::::TokenValueTooLow); - ensure!(amount >= value, Error::::TokenValueTooLow); + // Send balance to recipient (updates balanceOf of recipient) + Self::add_fungible_item(collection_id, recipient, value)?; - // update balance - let balance_old_owner = >::get(collection_id, owner.clone()) - .checked_sub(value) - .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, owner.clone(), balance_old_owner); + // update balanceOf of sender + >::insert(collection_id, (*owner).clone(), balance.value - value); - let mut new_owner_account_id = 0; - let new_owner_items = >::get(collection_id, new_owner.clone()); - if new_owner_items.len() > 0 { - new_owner_account_id = new_owner_items[0]; + // Reduce or remove sender + if balance.value == value { + >::remove(collection_id, owner); } - - // transfer - if amount == value && new_owner_account_id == 0 { - // change owner - // new owner do not have account - let mut new_full_item = full_item.clone(); - new_full_item.owner = new_owner.clone(); - >::insert(collection_id, item_id, new_full_item); - - // update balance - let balancenew_owner = >::get(collection_id, new_owner.clone()) - .checked_add(value) - .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, new_owner.clone(), balancenew_owner); - - // update index collection - Self::move_token_index(collection_id, item_id, owner.clone(), new_owner.clone())?; - } else { - let mut new_full_item = full_item.clone(); - new_full_item.value -= value; - - // separate amount - if new_owner_account_id > 0 { - // new owner has account - let mut item = >::get(collection_id, new_owner_account_id); - item.value += value; - - // update balance - let balancenew_owner = >::get(collection_id, new_owner.clone()) - .checked_add(value) - .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, new_owner.clone(), balancenew_owner); - - >::insert(collection_id, new_owner_account_id, item); - } else { - // new owner do not have account - let item = FungibleItemType { - owner: new_owner.clone(), - value - }; - - Self::add_fungible_item(collection_id, item)?; - } - - if amount == value { - Self::remove_token_index(collection_id, item_id, full_item.owner.clone())?; - - // remove approve list - >::remove(collection_id, (item_id, full_item.owner.clone())); - >::remove(collection_id, item_id); - } - - >::insert(collection_id, item_id, new_full_item); + else { + balance.value -= value; + >::insert(collection_id, (*owner).clone(), balance); } Ok(()) @@ -2039,8 +1939,6 @@ // update index collection Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?; - // reset approved list - >::remove(collection_id, (item_id, old_owner)); Ok(()) } @@ -2052,7 +1950,6 @@ match mode { CollectionMode::NFT => ensure!(>::contains_key(collection_id, item_id), Error::::TokenNotFound), CollectionMode::ReFungible(_) => ensure!(>::contains_key(collection_id, item_id), Error::::TokenNotFound), - CollectionMode::Fungible(_) => ensure!(>::contains_key(collection_id, item_id), Error::::TokenNotFound), _ => () }; @@ -2131,21 +2028,20 @@ >::insert(collection_id, item_owner.clone(), new_balance); } - fn init_fungible_token(collection_id: CollectionId, item: &FungibleItemType) { + fn init_fungible_token(collection_id: CollectionId, owner: &T::AccountId, item: &FungibleItemType) { let current_index = ::get(collection_id) .checked_add(1) .unwrap(); - let owner = item.owner.clone(); - Self::add_token_index(collection_id, current_index, owner.clone()).unwrap(); + Self::add_token_index(collection_id, current_index, (*owner).clone()).unwrap(); ::insert(collection_id, current_index); // Update balance - let new_balance = >::get(collection_id, owner.clone()) + let new_balance = >::get(collection_id, owner) .checked_add(item.value) .unwrap(); - >::insert(collection_id, owner.clone(), new_balance); + >::insert(collection_id, (*owner).clone(), new_balance); } fn init_refungible_token(collection_id: CollectionId, item: &ReFungibleItemType) { @@ -2343,7 +2239,7 @@ T::AccountId::default() } } - Some(Call::transfer(new_owner, collection_id, item_id, _value)) => { + Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => { let mut sponsor_transfer = false; if >::get(collection_id).sponsor_confirmed { @@ -2352,6 +2248,7 @@ let collection_mode = >::get(collection_id).mode; // sponsor timeout + let block_number = >::block_number() as T::BlockNumber; sponsor_transfer = match collection_mode { CollectionMode::NFT => { @@ -2362,16 +2259,19 @@ ChainLimit::get().nft_sponsor_transfer_timeout }; - let basket = >::get(collection_id, item_id); - let block_number = >::block_number() as T::BlockNumber; - let limit_time = basket + limit.into(); - if block_number >= limit_time { - >::insert(collection_id, item_id, block_number); - true + let mut sponsored = true; + if >::contains_key(collection_id, item_id) { + let last_tx_block = >::get(collection_id, item_id); + let limit_time = last_tx_block + limit.into(); + if block_number <= limit_time { + sponsored = false; + } } - else { - false + if sponsored { + >::insert(collection_id, item_id, block_number); } + + sponsored } CollectionMode::Fungible(_) => { @@ -2382,26 +2282,20 @@ ChainLimit::get().fungible_sponsor_transfer_timeout }; - let mut basket = >::get(collection_id, item_id); let block_number = >::block_number() as T::BlockNumber; - if basket.iter().any(|i| i.address == new_owner.clone()) - { - let item = basket.iter_mut().find(|i| i.address == new_owner.clone()).unwrap().clone(); - let limit_time = item.start_block + limit.into(); - if block_number >= limit_time { - basket.retain(|x| x.address == item.address); - basket.push(BasketItem { start_block: block_number, address: new_owner.clone() }); - >::insert(collection_id, item_id, basket); - true - } - else { - false + let mut sponsored = true; + if >::contains_key(collection_id, who) { + let last_tx_block = >::get(collection_id, who); + let limit_time = last_tx_block + limit.into(); + if block_number <= limit_time { + sponsored = false; } } - else { - basket.push(BasketItem { start_block: block_number, address: new_owner.clone()}); - true + if sponsored { + >::insert(collection_id, who, block_number); } + + sponsored } CollectionMode::ReFungible(_) => { @@ -2412,15 +2306,19 @@ ChainLimit::get().refungible_sponsor_transfer_timeout }; - let basket = >::get(collection_id, item_id); - let block_number = >::block_number() as T::BlockNumber; - let limit_time = basket + limit.into(); - if block_number >= limit_time { + let mut sponsored = true; + if >::contains_key(collection_id, item_id) { + let last_tx_block = >::get(collection_id, item_id); + let limit_time = last_tx_block + limit.into(); + if block_number <= limit_time { + sponsored = false; + } + } + if sponsored { >::insert(collection_id, item_id, block_number); - true - } else { - false } + + sponsored } _ => { false --- a/runtime_types.json +++ b/runtime_types.json @@ -42,7 +42,6 @@ "Fraction": "u128" }, "FungibleItemType": { - "Owner": "AccountId", "Value": "u128" }, "NftItemType": { @@ -72,10 +71,6 @@ "VariableOnChainSchema": "Vec", "ConstOnChainSchema": "Vec" }, - "ApprovePermissions": { - "Approved": "AccountId", - "Amount": "u128" - }, "RawData": "Vec", "Address": "AccountId", "LookupSource": "AccountId", @@ -84,7 +79,9 @@ "const_data": "Vec", "variable_data": "Vec" }, - "CreateFungibleData": {}, + "CreateFungibleData": { + "value": "u128" + }, "CreateReFungibleData": { "const_data": "Vec", "variable_data": "Vec" @@ -104,10 +101,6 @@ }, "CollectionId": "u32", "TokenId": "u32", - "BasketItem": { - "Address": "AccountId", - "start_block": "BlockNumber" - }, "ChainLimits": { "collection_numbers_limit": "u32", "account_token_ownership_limit": "u32", --- a/tests/src/confirmSponsorship.test.ts +++ b/tests/src/confirmSponsorship.test.ts @@ -16,6 +16,7 @@ createItemExpectSuccess, findUnusedAddress, getGenericResult, + enableWhiteListExpectSuccess, } from "./util/helpers"; import { Keyring } from "@polkadot/api"; import { IKeyringPair } from "@polkadot/types/types"; @@ -72,7 +73,7 @@ // Mint token for unused address const itemId = await createItemExpectSuccess(collectionId, 'NFT', zeroBalance.address, '//Alice'); - // Transfer this token from unused address to Alice + // Transfer this tokens from unused address to Alice const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 0); const events = await submitTransactionAsync(zeroBalance, zeroToAlice); const result = getGenericResult(events); @@ -86,30 +87,198 @@ }); it('Fungible: Transfer fees are paid by the sponsor after confirmation', async () => { - expect(false).to.be.true; + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for unused address + const itemId = await createItemExpectSuccess(collectionId, 'Fungible', zeroBalance.address, '//Alice'); + + // Transfer this tokens from unused address to Alice + const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1); + const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result1 = getGenericResult(events1); + + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + expect(result1.success).to.be.true; + expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true; + }); }); it('ReFungible: Transfer fees are paid by the sponsor after confirmation', async () => { - expect(false).to.be.true; + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for unused address + const itemId = await createItemExpectSuccess(collectionId, 'ReFungible', zeroBalance.address, '//Alice'); + + // Transfer this tokens from unused address to Alice + const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1); + const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result1 = getGenericResult(events1); + + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + expect(result1.success).to.be.true; + expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true; + }); }); - it.skip('CreateItem fees are paid by the sponsor after confirmation', async () => { - // const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); - // await setCollectionSponsorExpectSuccess(collectionId, bob.address); - // await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); - expect(false).to.be.true; + it.only('CreateItem fees are paid by the sponsor after confirmation', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + // Enable collection white list + await enableWhiteListExpectSuccess(collectionId); + + // Enable public minting + + // Create Item + + + }); it('NFT: Sponsoring is rate limited', async () => { - expect(false).to.be.true; + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for alice + const itemId = await createItemExpectSuccess(collectionId, 'NFT', alice.address, '//Alice'); + + // Transfer this token from Alice to unused address and back + // Alice to Zero gets sponsored + const aliceToZero = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 0); + const events1 = await submitTransactionAsync(alice, aliceToZero); + const result1 = getGenericResult(events1); + + // Second transfer should fail + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + const zeroToAlice = api.tx.nft.transfer(alice.address, collectionId, itemId, 0); + const badTransaction = async function () { + console.log = function () {}; + console.error = function () {}; + await submitTransactionAsync(zeroBalance, zeroToAlice); + delete console.log; + delete console.error; + }; + await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees"); + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Try again after Zero gets some balance - now it should succeed + const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15); + await submitTransactionAsync(alice, balancetx); + const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result2 = getGenericResult(events2); + + expect(result1.success).to.be.true; + expect(result2.success).to.be.true; + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); }); it('Fungible: Sponsoring is rate limited', async () => { - expect(false).to.be.true; + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for unused address + const itemId = await createItemExpectSuccess(collectionId, 'Fungible', zeroBalance.address, '//Alice'); + + // Transfer this tokens in parts from unused address to Alice + const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1); + const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result1 = getGenericResult(events1); + + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + const badTransaction = async function () { + console.log = function () {}; + console.error = function () {}; + await submitTransactionAsync(zeroBalance, zeroToAlice); + delete console.log; + delete console.error; + }; + + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Try again after Zero gets some balance - now it should succeed + const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15); + await submitTransactionAsync(alice, balancetx); + const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result2 = getGenericResult(events2); + + expect(result1.success).to.be.true; + expect(result2.success).to.be.true; + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); }); it('ReFungible: Sponsoring is rate limited', async () => { - expect(false).to.be.true; + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for alice + const itemId = await createItemExpectSuccess(collectionId, 'ReFungible', alice.address, '//Alice'); + + // Transfer this token from Alice to unused address and back + // Alice to Zero gets sponsored + const aliceToZero = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1); + const events1 = await submitTransactionAsync(alice, aliceToZero); + const result1 = getGenericResult(events1); + + // Second transfer should fail + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + const zeroToAlice = api.tx.nft.transfer(alice.address, collectionId, itemId, 1); + const badTransaction = async function () { + console.log = function () {}; + console.error = function () {}; + await submitTransactionAsync(zeroBalance, zeroToAlice); + delete console.log; + delete console.error; + }; + await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees"); + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Try again after Zero gets some balance - now it should succeed + const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15); + await submitTransactionAsync(alice, balancetx); + const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result2 = getGenericResult(events2); + + expect(result1.success).to.be.true; + expect(result2.success).to.be.true; + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); }); }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -13,6 +13,8 @@ import { strToUTF16, utf16ToStr, hexToStr } from '../util/util'; import { IKeyringPair } from "@polkadot/types/types"; import { BigNumber } from 'bignumber.js'; +import { Struct, Enum } from '@polkadot/types/codec'; +import { u128 } from '@polkadot/types/primitive'; chai.use(chaiAsPromised); const expect = chai.expect; @@ -258,25 +260,73 @@ }); } +export interface CreateFungibleData extends Struct { + readonly value: u128; +}; + +export interface CreateReFungibleData extends Struct {}; +export interface CreateNftData extends Struct {}; + +export interface CreateItemData extends Enum { + NFT: CreateNftData, + Fungible: CreateFungibleData, + ReFungible: CreateReFungibleData +}; + export async function createItemExpectSuccess(collectionId: number, createMode: string, owner: string = '', senderSeed: string = '//Alice') { let newItemId: number = 0; await usingApi(async (api) => { const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString()); + const Aitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON(); + const AItemBalance = new BigNumber(Aitem.Value); const sender = privateKey(senderSeed); if (owner === '') owner = sender.address; - const tx = api.tx.nft.createItem(collectionId, owner, createMode); + + let tx; + if (createMode == 'Fungible') { + let createData = {fungible: {value: 10}}; + tx = api.tx.nft.createItem(collectionId, owner, createData); + } + else { + tx = api.tx.nft.createItem(collectionId, owner, createMode); + } const events = await submitTransactionAsync(sender, tx); const result = getCreateItemResult(events); - + const BItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString()); + const Bitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON(); + const BItemBalance = new BigNumber(Bitem.Value); // What to expect expect(result.success).to.be.true; - expect(BItemCount).to.be.equal(AItemCount+1); + if (createMode == 'Fungible') { + expect(BItemBalance.minus(AItemBalance).toNumber()).to.be.equal(10); + } + else { + expect(BItemCount).to.be.equal(AItemCount+1); + } expect(collectionId).to.be.equal(result.collectionId); expect(BItemCount).to.be.equal(result.itemId); newItemId = result.itemId; }); return newItemId; } + +export async function enableWhiteListExpectSuccess(collectionId: number, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + + // Run the transaction + const sender = privateKey(senderSeed); + const tx = api.tx.nft.setPublicAccessMode(collectionId, 'WhiteList'); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + // Get the collection + const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); + + // What to expect + expect(result.success).to.be.true; + expect(collection.Access).to.be.equal('WhiteList'); + }); +}