--- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -193,7 +193,7 @@ offchain_schema: vec![], schema_version: SchemaVersion::default(), sponsor: get_account_id_from_seed::("Alice"), - unconfirmed_sponsor: get_account_id_from_seed::("Alice"), + sponsor_confirmed: true, const_on_chain_schema: vec![], variable_on_chain_schema: vec![], limits: CollectionLimits::default() --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -136,7 +136,7 @@ pub offchain_schema: Vec, pub schema_version: SchemaVersion, pub sponsor: AccountId, // Who pays fees. If set to default address, the fees are applied to the transaction sender - pub unconfirmed_sponsor: AccountId, // Sponsor address that has not yet confirmed sponsorship + pub sponsor_confirmed: bool, // False if sponsor address has not yet confirmed sponsorship. True otherwise. pub limits: CollectionLimits, // Collection private restrictions pub variable_on_chain_schema: Vec, // pub const_on_chain_schema: Vec, // @@ -145,7 +145,6 @@ #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct NftItemType { - pub collection: CollectionId, pub owner: AccountId, pub const_data: Vec, pub variable_data: Vec, @@ -153,45 +152,28 @@ #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct FungibleItemType { - pub collection: CollectionId, - pub owner: AccountId, +pub struct FungibleItemType { pub value: u128, } #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct ReFungibleItemType { - pub collection: CollectionId, pub owner: Vec>, pub const_data: Vec, 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 { - pub sender: AccountId, - pub recipient: AccountId, - pub collection_id: CollectionId, - pub item_id: TokenId, - 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, Default, Debug, Clone, PartialEq)] +// #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +// pub struct VestingItem { +// pub sender: AccountId, +// pub recipient: AccountId, +// pub collection_id: CollectionId, +// pub item_id: TokenId, +// pub amount: u64, +// pub vesting_date: Moment, +// } #[derive(Encode, Decode, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] @@ -270,6 +252,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)] @@ -424,12 +407,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 @@ -437,7 +420,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 @@ -455,16 +438,16 @@ >::init_collection(_c); } - for (_num, _q, _i) in &config.nft_item_id { - >::init_nft_token(_i); + for (_num, _c, _i) in &config.nft_item_id { + >::init_nft_token(*_c, _i); } - for (_num, _q, _i) in &config.fungible_item_id { - >::init_fungible_token(_i); + for (collection_id, account_id, fungible_item) in &config.fungible_item_id { + >::init_fungible_token(*collection_id, account_id, fungible_item); } - for (_num, _q, _i) in &config.refungible_item_id { - >::init_refungible_token(_i); + for (_num, _c, _i) in &config.refungible_item_id { + >::init_refungible_token(*_c, _i); } }) } @@ -591,7 +574,7 @@ offchain_schema: Vec::new(), schema_version: SchemaVersion::ImageURL, sponsor: T::AccountId::default(), - unconfirmed_sponsor: T::AccountId::default(), + sponsor_confirmed: false, variable_on_chain_schema: Vec::new(), const_on_chain_schema: Vec::new(), limits: CollectionLimits::default(), @@ -622,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); @@ -850,7 +833,8 @@ let mut target_collection = >::get(collection_id); ensure!(sender == target_collection.owner, Error::::NoPermission); - target_collection.unconfirmed_sponsor = new_sponsor; + target_collection.sponsor = new_sponsor; + target_collection.sponsor_confirmed = false; >::insert(collection_id, target_collection); Ok(()) @@ -870,10 +854,9 @@ ensure!(>::contains_key(collection_id), Error::::CollectionNotFound); let mut target_collection = >::get(collection_id); - ensure!(sender == target_collection.unconfirmed_sponsor, Error::::ConfirmUnsetSponsorFail); + ensure!(sender == target_collection.sponsor, Error::::ConfirmUnsetSponsorFail); - target_collection.sponsor = target_collection.unconfirmed_sponsor; - target_collection.unconfirmed_sponsor = T::AccountId::default(); + target_collection.sponsor_confirmed = true; >::insert(collection_id, target_collection); Ok(()) @@ -898,6 +881,7 @@ ensure!(sender == target_collection.owner, Error::::NoPermission); target_collection.sponsor = T::AccountId::default(); + target_collection.sponsor_confirmed = false; >::insert(collection_id, target_collection); Ok(()) @@ -998,7 +982,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)?; @@ -1016,7 +1000,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())?, _ => () }; @@ -1072,7 +1056,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)?, _ => () }; @@ -1096,7 +1080,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)?; @@ -1108,28 +1092,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(()) } @@ -1159,15 +1130,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); @@ -1177,23 +1145,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)?, _ => () }; @@ -1627,22 +1597,15 @@ { CreateItemData::NFT(data) => { let item = NftItemType { - collection: collection_id, owner, const_data: data.const_data, variable_data: data.variable_data }; - Self::add_nft_item(item)?; + Self::add_nft_item(collection_id, item)?; }, - CreateItemData::Fungible(_) => { - let item = FungibleItemType { - collection: collection_id, - owner, - value: (10 as u128).pow(collection.decimal_points as u32) - }; - - Self::add_fungible_item(item)?; + CreateItemData::Fungible(data) => { + Self::add_fungible_item(collection_id, &owner, data.value)?; }, CreateItemData::ReFungible(data) => { let mut owner_list = Vec::new(); @@ -1650,13 +1613,12 @@ owner_list.push(Ownership {owner: owner.clone(), fraction: value}); let item = ReFungibleItemType { - collection: collection_id, owner: owner_list, const_data: data.const_data, variable_data: data.variable_data }; - Self::add_refungible_item(item)?; + Self::add_refungible_item(collection_id, item)?; } }; @@ -1666,33 +1628,31 @@ Ok(()) } - fn add_fungible_item(item: FungibleItemType) -> DispatchResult { - let current_index = ::get(item.collection) - .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(item.collection, 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(item.collection, current_index); - >::insert(item.collection, 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(item.collection, current_index, v); - // Update balance - let new_balance = >::get(item.collection, owner.clone()) - .checked_add(item.value) + let new_balance = >::get(collection_id, owner) + .checked_add(value) .ok_or(Error::::NumOverflow)?; - >::insert(item.collection, owner.clone(), new_balance); + >::insert(collection_id, (*owner).clone(), new_balance); Ok(()) } - fn add_refungible_item(item: ReFungibleItemType) -> DispatchResult { - let current_index = ::get(item.collection) + fn add_refungible_item(collection_id: CollectionId, item: ReFungibleItemType) -> DispatchResult { + let current_index = ::get(collection_id) .checked_add(1) .ok_or(Error::::NumOverflow)?; let itemcopy = item.clone(); @@ -1700,40 +1660,31 @@ let value = item.owner.first().unwrap().fraction; let owner = item.owner.first().unwrap().owner.clone(); - Self::add_token_index(item.collection, current_index, owner.clone())?; + Self::add_token_index(collection_id, current_index, owner.clone())?; - ::insert(item.collection, current_index); - >::insert(item.collection, current_index, itemcopy); - - // Add current block - let block_number: T::BlockNumber = 0.into(); - >::insert(item.collection, current_index, block_number); + ::insert(collection_id, current_index); + >::insert(collection_id, current_index, itemcopy); // Update balance - let new_balance = >::get(item.collection, owner.clone()) + let new_balance = >::get(collection_id, owner.clone()) .checked_add(value) .ok_or(Error::::NumOverflow)?; - >::insert(item.collection, owner.clone(), new_balance); + >::insert(collection_id, owner.clone(), new_balance); Ok(()) } - fn add_nft_item(item: NftItemType) -> DispatchResult { - let current_index = ::get(item.collection) + fn add_nft_item(collection_id: CollectionId, item: NftItemType) -> DispatchResult { + let current_index = ::get(collection_id) .checked_add(1) .ok_or(Error::::NumOverflow)?; let item_owner = item.owner.clone(); - let collection_id = item.collection.clone(); Self::add_token_index(collection_id, current_index, item.owner.clone())?; ::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()) .checked_add(1) @@ -1760,9 +1711,6 @@ .next() .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()) @@ -1783,9 +1731,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()) .checked_sub(1) @@ -1796,24 +1741,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(()) } @@ -1874,7 +1822,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) @@ -1895,86 +1843,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 balance_new_owner = >::get(collection_id, new_owner.clone()) - .checked_add(value) - .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, new_owner.clone(), balance_new_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 balance_new_owner = >::get(collection_id, new_owner.clone()) - .checked_add(value) - .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, new_owner.clone(), balance_new_owner); - - >::insert(collection_id, new_owner_account_id, item); - } else { - // new owner do not have account - let item = FungibleItemType { - collection: collection_id, - owner: new_owner.clone(), - value - }; - - Self::add_fungible_item(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(()) @@ -2009,10 +1902,10 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, item.owner.clone(), balance_old_owner); - let balance_new_owner = >::get(collection_id, new_owner.clone()) + let balancenew_owner = >::get(collection_id, new_owner.clone()) .checked_add(value) .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, new_owner.clone(), balance_new_owner); + >::insert(collection_id, new_owner.clone(), balancenew_owner); let old_owner = item.owner.clone(); let new_owner_has_account = full_item.owner.iter().any(|i| i.owner == new_owner); @@ -2089,10 +1982,10 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, item.owner.clone(), balance_old_owner); - let balance_new_owner = >::get(collection_id, new_owner.clone()) + let balancenew_owner = >::get(collection_id, new_owner.clone()) .checked_add(1) .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, new_owner.clone(), balance_new_owner); + >::insert(collection_id, new_owner.clone(), balancenew_owner); // change owner let old_owner = item.owner.clone(); @@ -2102,8 +1995,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(()) } @@ -2115,7 +2006,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), _ => () }; @@ -2177,13 +2067,12 @@ CreatedCollectionCount::put(next_id); } - fn init_nft_token(item: &NftItemType) { - let current_index = ::get(item.collection) + fn init_nft_token(collection_id: CollectionId, item: &NftItemType) { + let current_index = ::get(collection_id) .checked_add(1) .unwrap(); let item_owner = item.owner.clone(); - let collection_id = item.collection.clone(); Self::add_token_index(collection_id, current_index, item.owner.clone()).unwrap(); ::insert(collection_id, current_index); @@ -2195,40 +2084,39 @@ >::insert(collection_id, item_owner.clone(), new_balance); } - fn init_fungible_token(item: &FungibleItemType) { - let current_index = ::get(item.collection) + 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(item.collection, current_index, owner.clone()).unwrap(); + Self::add_token_index(collection_id, current_index, (*owner).clone()).unwrap(); - ::insert(item.collection, current_index); + ::insert(collection_id, current_index); // Update balance - let new_balance = >::get(item.collection, owner.clone()) + let new_balance = >::get(collection_id, owner) .checked_add(item.value) .unwrap(); - >::insert(item.collection, owner.clone(), new_balance); + >::insert(collection_id, (*owner).clone(), new_balance); } - fn init_refungible_token(item: &ReFungibleItemType) { - let current_index = ::get(item.collection) + fn init_refungible_token(collection_id: CollectionId, item: &ReFungibleItemType) { + let current_index = ::get(collection_id) .checked_add(1) .unwrap(); let value = item.owner.first().unwrap().fraction; let owner = item.owner.first().unwrap().owner.clone(); - Self::add_token_index(item.collection, current_index, owner.clone()).unwrap(); + Self::add_token_index(collection_id, current_index, owner.clone()).unwrap(); - ::insert(item.collection, current_index); + ::insert(collection_id, current_index); // Update balance - let new_balance = >::get(item.collection, owner.clone()) + let new_balance = >::get(collection_id, owner.clone()) .checked_add(value) .unwrap(); - >::insert(item.collection, owner.clone(), new_balance); + >::insert(collection_id, owner.clone(), new_balance); } fn add_token_index(collection_id: CollectionId, item_index: TokenId, owner: T::AccountId) -> DispatchResult { @@ -2399,99 +2287,111 @@ // }; let fee = Self::traditional_fee(len, info, tip); + // Only mess with balances if fee is not zero. + if fee.is_zero() { + return Ok((fee, None)); + } + // Determine who is paying transaction fee based on ecnomic model // Parse call to extract collection ID and access collection sponsor let mut sponsor: T::AccountId = match IsSubType::>::is_sub_type(call) { Some(Call::create_item(collection_id, _owner, _properties)) => { // check free create limit - if >::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32) + if (>::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32)) && + (>::get(collection_id).sponsor_confirmed) { >::get(collection_id).sponsor } else { T::AccountId::default() } } - Some(Call::transfer(_new_owner, collection_id, _item_id, _value)) => { + Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => { - let _collection_limits = >::get(collection_id).limits; - let _collection_mode = >::get(collection_id).mode; + let mut sponsor_transfer = false; + if >::get(collection_id).sponsor_confirmed { - // sponsor timeout - let sponsor_transfer = match _collection_mode { - CollectionMode::NFT => { + let collection_limits = >::get(collection_id).limits; + let collection_mode = >::get(collection_id).mode; + + // sponsor timeout + let block_number = >::block_number() as T::BlockNumber; + 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 + } else { + ChainLimit::get().nft_sponsor_transfer_timeout + }; + + 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); + } - // get correct limit - let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 { - _collection_limits.sponsor_transfer_timeout - } else { - 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 - } - else { - false + sponsored } - } - CollectionMode::Fungible(_) => { - - // get correct limit - let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 { - _collection_limits.sponsor_transfer_timeout - } else { - 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 + CollectionMode::Fungible(_) => { + + // get correct limit + let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { + collection_limits.sponsor_transfer_timeout + } else { + ChainLimit::get().fungible_sponsor_transfer_timeout + }; + + let block_number = >::block_number() as T::BlockNumber; + 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 { - false + if sponsored { + >::insert(collection_id, who, block_number); } + + sponsored } - else { - basket.push(BasketItem { start_block: block_number, address: _new_owner.clone()}); - true + CollectionMode::ReFungible(_) => { + + // get correct limit + let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { + collection_limits.sponsor_transfer_timeout + } else { + ChainLimit::get().refungible_sponsor_transfer_timeout + }; + + 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); + } + + sponsored } - } - CollectionMode::ReFungible(_) => { - - // get correct limit - let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 { - _collection_limits.sponsor_transfer_timeout - } else { - 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 { - >::insert(collection_id, _item_id, block_number); - true - } else { + _ => { false - } - } - _ => { - false - }, - }; + }, + }; + } if !sponsor_transfer { T::AccountId::default() @@ -2568,11 +2468,6 @@ let mut who_pays_fee: T::AccountId = sponsor.clone(); if sponsor == T::AccountId::default() { who_pays_fee = who.clone(); - } - - // Only mess with balances if fee is not zero. - if fee.is_zero() { - return Ok((fee, None)); } match ::Currency::withdraw( --- a/runtime_types.json +++ b/runtime_types.json @@ -42,18 +42,14 @@ "Fraction": "u128" }, "FungibleItemType": { - "Collection": "CollectionId", - "Owner": "AccountId", "Value": "u128" }, "NftItemType": { - "Collection": "CollectionId", "Owner": "AccountId", "ConstData": "Vec", "VariableData": "Vec" }, "ReFungibleItemType": { - "Collection": "CollectionId", "Owner": "Vec>", "ConstData": "Vec", "VariableData": "Vec" @@ -70,14 +66,10 @@ "OffchainSchema": "Vec", "SchemaVersion": "SchemaVersion", "Sponsor": "AccountId", - "UnconfirmedSponsor": "AccountId", + "SponsorConfirmed": "bool", "Limits": "CollectionLimits", "VariableOnChainSchema": "Vec", "ConstOnChainSchema": "Vec" - }, - "ApprovePermissions": { - "Approved": "AccountId", - "Amount": "u128" }, "RawData": "Vec", "Address": "AccountId", @@ -87,7 +79,9 @@ "const_data": "Vec", "variable_data": "Vec" }, - "CreateFungibleData": {}, + "CreateFungibleData": { + "value": "u128" + }, "CreateReFungibleData": { "const_data": "Vec", "variable_data": "Vec" @@ -107,10 +101,6 @@ }, "CollectionId": "u32", "TokenId": "u32", - "BasketItem": { - "Address": "AccountId", - "start_block": "BlockNumber" - }, "ChainLimits": { "collection_numbers_limit": "u32", "account_token_ownership_limit": "u32", --- /dev/null +++ b/tests/loadtester-src/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file --- /dev/null +++ b/tests/loadtester-src/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "loadtester" +version = "0.1.0" +authors = ["[your_name] <[your_email]>"] +edition = "2018" + +[dependencies] +ink_primitives = { version = "3.0.0-rc2", default-features = false } +ink_metadata = { version = "3.0.0-rc2", default-features = false, features = ["derive"], optional = true } +ink_env = { version = "3.0.0-rc2", default-features = false } +ink_storage = { version = "3.0.0-rc2", default-features = false } +ink_lang = { version = "3.0.0-rc2", default-features = false } +ink_prelude = { version = "3.0.0-rc2", default-features = false } + +scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] } +scale-info = { version = "0.4.1", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "loadtester" +path = "lib.rs" +crate-type = [ + # Used for normal contract Wasm blobs. + "cdylib", +] + +[features] +default = ["std"] +std = [ + "ink_metadata/std", + "ink_env/std", + "ink_storage/std", + "ink_primitives/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] --- /dev/null +++ b/tests/loadtester-src/lib.rs @@ -0,0 +1,53 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use ink_lang as ink; + +#[ink::contract] +mod loadtester { + use ink_storage::collections::Vec as InkVec; + + #[ink(storage)] + pub struct LoadTester { + vector: InkVec, + } + + impl LoadTester { + #[ink(constructor)] + pub fn new() -> Self { + Self { + vector: InkVec::new(), + } + } + + #[ink(message)] + pub fn bloat(&mut self, count: u64){ + for i in 1..count+1 { + self.vector.push(i); + } + } + + #[ink(message)] + pub fn get(&self) -> u128 { + let mut sum: u128 = 0; + for num in self.vector.iter() { + sum += *num as u128; + } + sum + } + } + + #[cfg(test)] + mod tests { + + use super::*; + + #[test] + fn it_works() { + let mut lt = LoadTester::new(); + lt.bloat(4); + assert_eq!(lt.get(), [1,2,3,4]); + lt.bloat(3); + assert_eq!(lt.get(), [1,2,3,4,1,2,3]); + } + } +} --- a/tests/package.json +++ b/tests/package.json @@ -16,7 +16,8 @@ "typescript": "^3.9.7" }, "scripts": { - "test": "mocha --timeout 9999999 -r ts-node/register ./**/*.test.ts" + "test": "mocha --timeout 9999999 -r ts-node/register ./**/*.test.ts", + "load": "mocha --timeout 9999999 -r ts-node/register ./**/*.load.ts" }, "author": "", "license": "Apache 2.0", --- /dev/null +++ b/tests/src/change-collection-owner.test.ts @@ -0,0 +1,59 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import privateKey from './substrate/privateKey'; +import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; +import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers"; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('Integration Test changeCollectionOwner(collection_id, new_owner):', () => { + it('Changing owner changes owner.', async () => { + await usingApi(async api => { + const collectionId = await createCollectionExpectSuccess(); + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const collection: any = (await api.query.nft.collection(collectionId)); + expect(collection.Owner.toString()).to.be.eq(alice.address); + + const changeOwnerTx = api.tx.nft.changeCollectionOwner(collectionId, bob.address); + await submitTransactionAsync(alice, changeOwnerTx); + + const collectionAfterOwnerChange: any = (await api.query.nft.collection(collectionId)); + expect(collectionAfterOwnerChange.Owner.toString()).to.be.eq(bob.address); + }); + }); +}); + +describe('Negative Integration Test changeCollectionOwner(collection_id, new_owner):', () => { + it(`Not owner can't change owner.`, async () => { + await usingApi(async api => { + const collectionId = await createCollectionExpectSuccess(); + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const changeOwnerTx = api.tx.nft.changeCollectionOwner(collectionId, bob.address); + await expect(submitTransactionExpectFailAsync(bob, changeOwnerTx)).to.be.rejected; + + const collectionAfterOwnerChange: any = (await api.query.nft.collection(collectionId)); + expect(collectionAfterOwnerChange.Owner.toString()).to.be.eq(alice.address); + + // Verifying that nothing bad happened (network is live, new collections can be created, etc.) + await createCollectionExpectSuccess(); + }); + }); + it(`Can't change owner of not existing collection.`, async () => { + await usingApi(async api => { + const collectionId = (1<<32) - 1; + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const changeOwnerTx = api.tx.nft.changeCollectionOwner(collectionId, bob.address); + await expect(submitTransactionExpectFailAsync(alice, changeOwnerTx)).to.be.rejected; + + // Verifying that nothing bad happened (network is live, new collections can be created, etc.) + await createCollectionExpectSuccess(); + }); + }); +}); --- /dev/null +++ b/tests/src/confirmSponsorship.test.ts @@ -0,0 +1,338 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; +import { + createCollectionExpectSuccess, + setCollectionSponsorExpectSuccess, + destroyCollectionExpectSuccess, + setCollectionSponsorExpectFailure, + confirmSponsorshipExpectSuccess, + confirmSponsorshipExpectFailure, + createItemExpectSuccess, + findUnusedAddress, + getGenericResult, + enableWhiteListExpectSuccess, + enablePublicMintingExpectSuccess, + addToWhiteListExpectSuccess, +} from "./util/helpers"; +import { Keyring } from "@polkadot/api"; +import { IKeyringPair } from "@polkadot/types/types"; +import type { AccountId } from '@polkadot/types/interfaces'; +import { BigNumber } from 'bignumber.js'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +let alice: IKeyringPair; +let bob: IKeyringPair; +let charlie: IKeyringPair; + +describe('integration test: ext. confirmSponsorship():', () => { + + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + alice = keyring.addFromUri(`//Alice`); + bob = keyring.addFromUri(`//Bob`); + charlie = keyring.addFromUri(`//Charlie`); + }); + }); + + it('Confirm collection sponsorship', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + }); + it('Add sponsor to a collection after the same sponsor was already added and confirmed', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + it('Add new sponsor to a collection after another sponsor was already added and confirmed', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + await setCollectionSponsorExpectSuccess(collectionId, charlie.address); + }); + + it('NFT: Transfer fees are paid by the sponsor after confirmation', async () => { + const collectionId = await createCollectionExpectSuccess(); + 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(alice, collectionId, 'NFT', zeroBalance.address); + + // 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); + + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + expect(result.success).to.be.true; + expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true; + }); + + }); + + it('Fungible: Transfer fees are paid by the sponsor after confirmation', async () => { + const collectionId = await createCollectionExpectSuccess({mode: '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(alice, collectionId, 'Fungible', zeroBalance.address); + + // 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 () => { + const collectionId = await createCollectionExpectSuccess({mode: '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(alice, collectionId, 'ReFungible', zeroBalance.address); + + // 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('CreateItem fees are paid by the sponsor after confirmation', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + // Enable collection white list + await enableWhiteListExpectSuccess(alice, collectionId); + + // Enable public minting + await enablePublicMintingExpectSuccess(alice, collectionId); + + // Create Item + 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); + + // Add zeroBalance address to white list + await addToWhiteListExpectSuccess(alice, collectionId, zeroBalance.address); + + // Mint token using unused address as signer + const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address); + + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true; + }); + }); + + it('NFT: Sponsoring is rate limited', async () => { + const collectionId = await createCollectionExpectSuccess(); + 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(alice, collectionId, 'NFT', alice.address); + + // 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 () { + await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); + }; + 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 () => { + const collectionId = await createCollectionExpectSuccess({mode: '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(alice, collectionId, 'Fungible', zeroBalance.address); + + // 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 () { + await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); + }; + + 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 () => { + const collectionId = await createCollectionExpectSuccess({mode: '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(alice, collectionId, 'ReFungible', alice.address); + + // 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 () { + await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); + }; + 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; + }); + }); + +}); + +describe('(!negative test!) integration test: ext. removeCollectionSponsor():', () => { + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + alice = keyring.addFromUri(`//Alice`); + bob = keyring.addFromUri(`//Bob`); + charlie = keyring.addFromUri(`//Charlie`); + }); + }); + + it('(!negative test!) Confirm sponsorship for a collection that never existed', async () => { + // Find the collection that never existed + const collectionId = 0; + await usingApi(async (api) => { + const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1; + }); + + await confirmSponsorshipExpectFailure(collectionId, '//Bob'); + }); + + it('(!negative test!) Confirm sponsorship using a non-sponsor address', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + + await usingApi(async (api) => { + const transfer = api.tx.balances.transfer(charlie.address, 1e15); + await submitTransactionAsync(alice, transfer); + }); + + await confirmSponsorshipExpectFailure(collectionId, '//Charlie'); + }); + + it('(!negative test!) Confirm sponsorship using owner address', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectFailure(collectionId, '//Alice'); + }); + + it('(!negative test!) Confirm sponsorship without sponsor being set with setCollectionSponsor', async () => { + const collectionId = await createCollectionExpectSuccess(); + await confirmSponsorshipExpectFailure(collectionId, '//Bob'); + }); + + it('(!negative test!) Confirm sponsorship in a collection that was destroyed', async () => { + const collectionId = await createCollectionExpectSuccess(); + await destroyCollectionExpectSuccess(collectionId); + await confirmSponsorshipExpectFailure(collectionId, '//Bob'); + }); +}); --- a/tests/src/connection.test.ts +++ b/tests/src/connection.test.ts @@ -21,6 +21,8 @@ }); it('Cannot connect to 255.255.255.255', async () => { + const log = console.log; + const error = console.error; console.log = function () {}; console.error = function () {}; @@ -31,7 +33,7 @@ }, { provider: neverConnectProvider }); })()).to.be.eventually.rejected; - delete console.log; - delete console.error; + console.log = log; + console.error = error; }); }); \ No newline at end of file --- a/tests/src/contracts.test.ts +++ b/tests/src/contracts.test.ts @@ -109,9 +109,6 @@ const bob = privateKey("//Bob"); const [contract, deployer] = await deployFlipper(api); - const consoleError = console.error; - console.error = (...data: any[]) => { - }; let expectedFlipValue = await getFlipValue(contract, deployer); @@ -166,7 +163,6 @@ const afterWhiteListDisabled = await getFlipValue(contract,deployer); expect(afterWhiteListDisabled).to.be.eq(expectedFlipValue, `Anyone can call contract with disabled whitelist.`); - console.error = consoleError; }); }); --- a/tests/src/createCollection.test.ts +++ b/tests/src/createCollection.test.ts @@ -6,37 +6,29 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { default as usingApi } from "./substrate/substrate-api"; -import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers"; +import { createCollectionExpectSuccess, createCollectionExpectFailure, CollectionMode } from "./util/helpers"; chai.use(chaiAsPromised); const expect = chai.expect; describe('integration test: ext. createCollection():', () => { it('Create new NFT collection', async () => { - await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: 'NFT'}); }); it('Create new NFT collection whith collection_name of maximum length (64 bytes)', async () => { - await createCollectionExpectSuccess( - 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCD', - '1', '1', 'NFT'); + await createCollectionExpectSuccess({name: 'A'.repeat(64)}); }); it('Create new NFT collection whith collection_description of maximum length (256 bytes)', async () => { - await createCollectionExpectSuccess( - 'A', - 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJabcdef', - '1', 'NFT'); + await createCollectionExpectSuccess({description: 'A'.repeat(256)}); }); it('Create new NFT collection whith token_prefix of maximum length (16 bytes)', async () => { - await createCollectionExpectSuccess( - '1', - '1', - 'ABCDEFGHIJABCDEF', 'NFT'); + await createCollectionExpectSuccess({tokenPrefix: 'A'.repeat(16)}); }); it('Create new Fungible collection', async () => { - await createCollectionExpectSuccess('1', '1', '1', 'Fungible'); + await createCollectionExpectSuccess({mode: 'Fungible'}); }); it('Create new ReFungible collection', async () => { - await createCollectionExpectSuccess('1', '1', '1', 'ReFungible'); + await createCollectionExpectSuccess({mode: 'ReFungible'}); }); }); @@ -46,7 +38,7 @@ const AcollectionCount = parseInt((await api.query.nft.collectionCount()).toString()); const badTransaction = async function () { - await createCollectionExpectSuccess('1', '1', '1', 'BadMode'); + await createCollectionExpectSuccess({mode: 'BadMode' as CollectionMode}); }; expect(badTransaction()).to.be.rejected; @@ -55,18 +47,12 @@ }); }); it('(!negative test!) create new NFT collection whith incorrect data (collection_name)', async () => { - await createCollectionExpectFailure( - 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDE', - '1', '1', 'NFT'); + await createCollectionExpectFailure({name: 'A'.repeat(65)}); }); it('(!negative test!) create new NFT collection whith incorrect data (collection_description)', async () => { - await createCollectionExpectFailure('1', - 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJabcdefg', - '1', 'NFT'); + await createCollectionExpectFailure({description: 'A'.repeat(257)}); }); it('(!negative test!) create new NFT collection whith incorrect data (token_prefix)', async () => { - await createCollectionExpectFailure('1', '1', - 'ABCDEFGHIJABCDEFG', - 'NFT'); + await createCollectionExpectFailure({tokenPrefix: 'A'.repeat(17)}); }); }); --- a/tests/src/createItem.test.ts +++ b/tests/src/createItem.test.ts @@ -1,27 +1,34 @@ -import { assert } from 'chai'; -import { alicesPublicKey } from './accounts'; -import privateKey from './substrate/privateKey'; import { default as usingApi } from './substrate/substrate-api'; -import waitNewBlocks from './substrate/wait-new-blocks'; +import { Keyring } from "@polkadot/api"; +import { IKeyringPair } from "@polkadot/types/types"; import { createCollectionExpectSuccess, createItemExpectSuccess } from './util/helpers'; +let alice: IKeyringPair; + describe('integration test: ext. createItem():', () => { + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + alice = keyring.addFromUri(`//Alice`); + }); + }); + it('Create new item in NFT collection', async () => { const createMode = 'NFT'; - const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode); - await createItemExpectSuccess(newCollectionID, createMode, '//Alice'); + const newCollectionID = await createCollectionExpectSuccess({mode: createMode}); + await createItemExpectSuccess(alice, newCollectionID, createMode); }); it('Create new item in Fungible collection', async () => { const createMode = 'Fungible'; - const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode); - await createItemExpectSuccess(newCollectionID, createMode, '//Alice'); + const newCollectionID = await createCollectionExpectSuccess({mode: createMode}); + await createItemExpectSuccess(alice, newCollectionID, createMode); }); it('Create new item in ReFungible collection', async () => { const createMode = 'ReFungible'; - const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode); - await createItemExpectSuccess(newCollectionID, createMode, '//Alice'); + const newCollectionID = await createCollectionExpectSuccess({mode: createMode}); + await createItemExpectSuccess(alice, newCollectionID, createMode); }); }); --- a/tests/src/createMultipleItems.test.ts +++ b/tests/src/createMultipleItems.test.ts @@ -11,7 +11,7 @@ const idCollection = 12; -describe('integration test: ext. createMultipleItems():', () => { +describe.skip('integration test: ext. createMultipleItems():', () => { it('Create two NFT tokens in active NFT collection', async () => { await usingApi(async (api) => { const AitemListIndex = await api.query.nft.itemListIndex(idCollection); --- a/tests/src/creditFeesToTreasury.test.ts +++ b/tests/src/creditFeesToTreasury.test.ts @@ -5,7 +5,7 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; +import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; import { alicesPublicKey, bobsPublicKey } from "./accounts"; import privateKey from "./substrate/privateKey"; import { BigNumber } from 'bignumber.js'; @@ -63,14 +63,13 @@ const bobBalanceBefore = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString()); const badTx = api.tx.balances.setBalance(alicesPublicKey, 0, 0); - const result = getGenericResult(await submitTransactionAsync(bobPrivateKey, badTx)); + await expect(submitTransactionExpectFailAsync(bobPrivateKey, badTx)).to.be.rejected; const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); const bobBalanceAfter = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString()); const fee = bobBalanceBefore.minus(bobBalanceAfter); const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); - expect(result.success).to.be.false; expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed()); }); }); @@ -80,7 +79,7 @@ const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await createCollectionExpectSuccess(); const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); @@ -96,7 +95,7 @@ const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await createCollectionExpectSuccess(); const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); --- a/tests/src/destroyCollection.test.ts +++ b/tests/src/destroyCollection.test.ts @@ -1,67 +1,24 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; -import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers"; +import { createCollectionExpectSuccess, createCollectionExpectFailure, destroyCollectionExpectSuccess, destroyCollectionExpectFailure } from "./util/helpers"; import type { AccountId, EventRecord } from '@polkadot/types/interfaces'; import privateKey from './substrate/privateKey'; -import { nullPublicKey } from './accounts'; chai.use(chaiAsPromised); const expect = chai.expect; - -function getDestroyResult(events: EventRecord[]): boolean { - let success: boolean = false; - events.forEach(({ phase, event: { data, method, section } }) => { - // console.log(` ${phase}: ${section}.${method}:: ${data}`); - if (method == 'ExtrinsicSuccess') { - success = true; - } - }); - return success; -} -async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') { - await usingApi(async (api) => { - // Run the DestroyCollection transaction - const alicePrivateKey = privateKey(senderSeed); - const tx = api.tx.nft.destroyCollection(collectionId); - const events = await submitTransactionAsync(alicePrivateKey, tx); - const result = getDestroyResult(events); - - // Get the collection - const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); - - // What to expect - expect(result).to.be.true; - expect(collection).to.be.not.null; - expect(collection.Owner).to.be.equal(nullPublicKey); - }); -} - -async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') { - await usingApi(async (api) => { - // Run the DestroyCollection transaction - const alicePrivateKey = privateKey(senderSeed); - const tx = api.tx.nft.destroyCollection(collectionId); - const events = await submitTransactionAsync(alicePrivateKey, tx); - const result = getDestroyResult(events); - - // What to expect - expect(result).to.be.false; - }); -} - describe('integration test: ext. destroyCollection():', () => { it('NFT collection can be destroyed', async () => { - const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + const collectionId = await createCollectionExpectSuccess(); await destroyCollectionExpectSuccess(collectionId); }); it('Fungible collection can be destroyed', async () => { - const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible'); + const collectionId = await createCollectionExpectSuccess({ mode: 'Fungible' }); await destroyCollectionExpectSuccess(collectionId); }); it('ReFungible collection can be destroyed', async () => { - const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible'); + const collectionId = await createCollectionExpectSuccess({ mode: 'ReFungible' }); await destroyCollectionExpectSuccess(collectionId); }); }); @@ -75,12 +32,12 @@ }); }); it('(!negative test!) Destroy a collection that has already been destroyed', async () => { - const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + const collectionId = await createCollectionExpectSuccess(); await destroyCollectionExpectSuccess(collectionId); await destroyCollectionExpectFailure(collectionId); }); it('(!negative test!) Destroy a collection using non-owner account', async () => { - const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + const collectionId = await createCollectionExpectSuccess(); await destroyCollectionExpectFailure(collectionId, '//Bob'); await destroyCollectionExpectSuccess(collectionId, '//Alice'); }); --- /dev/null +++ b/tests/src/load_test_sc/metadata.json @@ -0,0 +1,125 @@ +{ + "metadataVersion": "0.1.0", + "source": { + "hash": "0x168cc3cba9657ad3950fb506e568751f99b90fb097685107f6101675662a8303", + "language": "ink! 3.0.0-rc2", + "compiler": "rustc 1.49.0-nightly" + }, + "contract": { + "name": "loadtester", + "version": "0.1.0", + "authors": [ + "[your_name] <[your_email]>" + ] + }, + "spec": { + "constructors": [ + { + "args": [], + "docs": [], + "name": [ + "new" + ], + "selector": "0xd183512b" + } + ], + "docs": [], + "events": [], + "messages": [ + { + "args": [ + { + "name": "count", + "type": { + "displayName": [ + "u64" + ], + "type": 2 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "bloat" + ], + "payable": false, + "returnType": null, + "selector": "0x49891c2a" + }, + { + "args": [], + "docs": [], + "mutates": false, + "name": [ + "get" + ], + "payable": false, + "returnType": { + "displayName": [ + "u128" + ], + "type": 3 + }, + "selector": "0x1e5ca456" + } + ] + }, + "storage": { + "struct": { + "fields": [ + { + "layout": { + "struct": { + "fields": [ + { + "layout": { + "cell": { + "key": "0x0000000000000000000000000000000000000000000000000000000000000000", + "ty": 1 + } + }, + "name": "len" + }, + { + "layout": { + "array": { + "cellsPerElem": 1, + "layout": { + "cell": { + "key": "0x0000000001000000000000000000000000000000000000000000000000000000", + "ty": 2 + } + }, + "len": 4294967295, + "offset": "0x0100000000000000000000000000000000000000000000000000000000000000" + } + }, + "name": "elems" + } + ] + } + }, + "name": "vector" + } + ] + } + }, + "types": [ + { + "def": { + "primitive": "u32" + } + }, + { + "def": { + "primitive": "u64" + } + }, + { + "def": { + "primitive": "u128" + } + } + ] +} \ No newline at end of file --- /dev/null +++ b/tests/src/removeCollectionSponsor.test.ts @@ -0,0 +1,137 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; +import { + createCollectionExpectSuccess, + setCollectionSponsorExpectSuccess, + destroyCollectionExpectSuccess, + setCollectionSponsorExpectFailure, + confirmSponsorshipExpectSuccess, + confirmSponsorshipExpectFailure, + createItemExpectSuccess, + findUnusedAddress, + getGenericResult, + enableWhiteListExpectSuccess, + enablePublicMintingExpectSuccess, + addToWhiteListExpectSuccess, + removeCollectionSponsorExpectSuccess, + removeCollectionSponsorExpectFailure, +} from "./util/helpers"; +import { Keyring } from "@polkadot/api"; +import { IKeyringPair } from "@polkadot/types/types"; +import type { AccountId } from '@polkadot/types/interfaces'; +import { BigNumber } from 'bignumber.js'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +let alice: IKeyringPair; +let bob: IKeyringPair; +let charlie: IKeyringPair; + +describe('integration test: ext. removeCollectionSponsor():', () => { + + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + alice = keyring.addFromUri(`//Alice`); + bob = keyring.addFromUri(`//Bob`); + charlie = keyring.addFromUri(`//Charlie`); + }); + }); + + it('Remove NFT collection sponsor stops sponsorship', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + await removeCollectionSponsorExpectSuccess(collectionId); + + await usingApi(async (api) => { + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for unused address + const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', zeroBalance.address); + + // Transfer this tokens from unused address to Alice - 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 () { + await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); + }; + 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()); + + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); + }); + + it('Remove a sponsor after it was already removed', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + await removeCollectionSponsorExpectSuccess(collectionId); + await removeCollectionSponsorExpectSuccess(collectionId); + }); + + it('Remove sponsor in a collection that never had the sponsor set', async () => { + const collectionId = await createCollectionExpectSuccess(); + await removeCollectionSponsorExpectSuccess(collectionId); + }); + + it('Remove sponsor for a collection that had the sponsor set, but not confirmed', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await removeCollectionSponsorExpectSuccess(collectionId); + }); + +}); + +describe('(!negative test!) integration test: ext. removeCollectionSponsor():', () => { + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + alice = keyring.addFromUri(`//Alice`); + bob = keyring.addFromUri(`//Bob`); + charlie = keyring.addFromUri(`//Charlie`); + }); + }); + + it('(!negative test!) Remove sponsor for a collection that never existed', async () => { + // Find the collection that never existed + const collectionId = 0; + await usingApi(async (api) => { + const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1; + }); + + await removeCollectionSponsorExpectFailure(collectionId); + }); + + it('(!negative test!) Remove sponsor in a destroyed collection', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await destroyCollectionExpectSuccess(collectionId); + await removeCollectionSponsorExpectFailure(collectionId); + }); + + it('Set - remove - confirm: fails', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await removeCollectionSponsorExpectSuccess(collectionId); + await confirmSponsorshipExpectFailure(collectionId, '//Bob'); + }); + + it('Set - confirm - remove - confirm: Sponsor cannot come back', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + await removeCollectionSponsorExpectSuccess(collectionId); + await confirmSponsorshipExpectFailure(collectionId, '//Bob'); + }); + +}); --- /dev/null +++ b/tests/src/rpc.load.ts @@ -0,0 +1,145 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import { expect, assert } from "chai"; +import usingApi, { submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; +import { IKeyringPair } from "@polkadot/types/types"; +import { Abi, BlueprintPromise as Blueprint, CodePromise, ContractPromise as Contract } from "@polkadot/api-contract"; +import { ApiPromise, Keyring } from "@polkadot/api"; +import { ApiTypes, SubmittableExtrinsic } from "@polkadot/api/types"; +import { BigNumber } from 'bignumber.js'; +import { findUnusedAddress } from './util/helpers' +import fs from "fs"; +import privateKey from "./substrate/privateKey"; + +const value = 0; +const gasLimit = 500000n * 1000000n; +const endowment = `1000000000000000`; + + +function deployBlueprint(alice: IKeyringPair, code: CodePromise): Promise { + return new Promise(async (resolve, reject) => { + const unsub = await code + .createBlueprint() + .signAndSend(alice, (result) => { + if (result.status.isInBlock || result.status.isFinalized) { + // here we have an additional field in the result, containing the blueprint + resolve(result.blueprint); + unsub(); + } + }) + }); +} + +function deployContract(alice: IKeyringPair, blueprint: Blueprint) : Promise { + return new Promise(async (resolve, reject) => { + const unsub = await blueprint.tx + .new(endowment, gasLimit) + .signAndSend(alice, (result) => { + if (result.status.isInBlock || result.status.isFinalized) { + unsub(); + resolve(result); + } + }); + }); +} + +async function prepareDeployer(api: ApiPromise) { + // Find unused address + const deployer = await findUnusedAddress(api); + + // Transfer balance to it + const keyring = new Keyring({ type: 'sr25519' }); + const alice = keyring.addFromUri(`//Alice`); + let amount = new BigNumber(endowment); + amount = amount.plus(1e15); + const tx = api.tx.balances.transfer(deployer.address, amount.toFixed()); + await submitTransactionAsync(alice, tx); + + return deployer; +} + +async function deployLoadTester(api: ApiPromise): Promise<[Contract, IKeyringPair]> { + const metadata = JSON.parse(fs.readFileSync('./src/load_test_sc/metadata.json').toString('utf-8')); + const abi = new Abi(metadata); + + const deployer = await prepareDeployer(api); + + const wasm = fs.readFileSync('./src/load_test_sc/loadtester.wasm'); + + const code = new CodePromise(api, abi, wasm); + + const blueprint = await deployBlueprint(deployer, code); + const contract = (await deployContract(deployer, blueprint))['contract'] as Contract; + + return [contract, deployer]; +} + +async function getScData(contract: Contract, deployer: IKeyringPair) { + const result = await contract.query.get(deployer.address, value, gasLimit); + + if(!result.result.isSuccess) { + throw `Failed to get value`; + } + return result.result.asSuccess.data; +} + + +describe('RPC Tests', () => { + it('Simple RPC Load Test', async () => { + await usingApi(async api => { + let count = 0; + let hrTime = process.hrtime(); + let microsec1 = hrTime[0] * 1000000 + hrTime[1] / 1000; + let rate = 0; + const checkPoint = 1000; + while (true) { + await api.rpc.system.chain(); + count++; + process.stdout.write(`RPC reads: ${count} times at rate ${rate} r/s \r`); + + if (count % checkPoint == 0) { + hrTime = process.hrtime(); + let microsec2 = hrTime[0] * 1000000 + hrTime[1] / 1000; + rate = 1000000*checkPoint/(microsec2 - microsec1); + microsec1 = microsec2; + } + } + }); + }); + + it.only('Smart Contract RPC Load Test', async () => { + await usingApi(async api => { + + // Deploy smart contract + const [contract, deployer] = await deployLoadTester(api); + + // Fill smart contract up with data + const bob = privateKey("//Bob"); + const tx = contract.tx.bloat(value, gasLimit, 200); + await submitTransactionAsync(bob, tx); + + // Run load test + let count = 0; + let hrTime = process.hrtime(); + let microsec1 = hrTime[0] * 1000000 + hrTime[1] / 1000; + let rate = 0; + const checkPoint = 10; + while (true) { + await getScData(contract, deployer); + count++; + process.stdout.write(`SC reads: ${count} times at rate ${rate} r/s \r`); + + if (count % checkPoint == 0) { + hrTime = process.hrtime(); + let microsec2 = hrTime[0] * 1000000 + hrTime[1] / 1000; + rate = 1000000*checkPoint/(microsec2 - microsec1); + microsec1 = microsec2; + } + } + }); + }); + +}); --- /dev/null +++ b/tests/src/setCollectionSponsor.test.ts @@ -0,0 +1,82 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { default as usingApi } from "./substrate/substrate-api"; +import { createCollectionExpectSuccess, setCollectionSponsorExpectSuccess, destroyCollectionExpectSuccess, setCollectionSponsorExpectFailure } from "./util/helpers"; +import { Keyring } from "@polkadot/api"; +import { IKeyringPair } from "@polkadot/types/types"; +import type { AccountId } from '@polkadot/types/interfaces'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +let bob: IKeyringPair; + +describe('integration test: ext. setCollectionSponsor():', () => { + + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + bob = keyring.addFromUri(`//Bob`); + }); + }); + + it('Set NFT collection sponsor', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + it('Set Fungible collection sponsor', async () => { + const collectionId = await createCollectionExpectSuccess({ mode: 'Fungible' }); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + it('Set ReFungible collection sponsor', async () => { + const collectionId = await createCollectionExpectSuccess({ mode: 'ReFungible' }); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + + it('Set the same sponsor repeatedly', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + it('Replace collection sponsor', async () => { + const collectionId = await createCollectionExpectSuccess(); + + const keyring = new Keyring({ type: 'sr25519' }); + const charlie = keyring.addFromUri(`//Charlie`); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await setCollectionSponsorExpectSuccess(collectionId, charlie.address); + }); +}); + +describe('(!negative test!) integration test: ext. setCollectionSponsor():', () => { + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + bob = keyring.addFromUri(`//Bob`); + }); + }); + + it('(!negative test!) Add sponsor with a non-owner', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectFailure(collectionId, bob.address, '//Bob'); + }); + it('(!negative test!) Add sponsor to a collection that never existed', async () => { + // Find the collection that never existed + const collectionId = 0; + await usingApi(async (api) => { + const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1; + }); + + await setCollectionSponsorExpectFailure(collectionId, bob.address); + }); + it('(!negative test!) Add sponsor to a collection that was destroyed', async () => { + const collectionId = await createCollectionExpectSuccess(); + await destroyCollectionExpectSuccess(collectionId); + await setCollectionSponsorExpectFailure(collectionId, bob.address); + }); +}); --- a/tests/src/substrate/substrate-api.ts +++ b/tests/src/substrate/substrate-api.ts @@ -33,21 +33,42 @@ } } +enum TransactionStatus { + Success, + Fail, + NotReady +} + +function getTransactionStatus(events: EventRecord[], status: ExtrinsicStatus): TransactionStatus { + if (status.isReady) { + return TransactionStatus.NotReady; + } + if (status.isBroadcast) { + return TransactionStatus.NotReady; + } + if (status.isInBlock || status.isFinalized) { + if(events.filter(e => e.event.data.method === 'ExtrinsicFailed').length > 0) { + return TransactionStatus.Fail; + } + if(events.filter(e => e.event.data.method === 'ExtrinsicSuccess').length > 0) { + return TransactionStatus.Success; + } + } + + return TransactionStatus.Fail; +} + export function submitTransactionAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic): Promise { return new Promise(async function(resolve, reject) { try { await transaction.signAndSend(sender, ({ events = [], status }) => { - if (status.isReady) { - // nothing to do - // console.log(`Current tx status is Ready`); - } else if (status.isBroadcast) { - // nothing to do - // console.log(`Current tx status is Broadcast`); - } else if (status.isInBlock || status.isFinalized) { + const transactionStatus = getTransactionStatus(events, status); + + if (transactionStatus == TransactionStatus.Success) { resolve(events); - } else { + } else if (transactionStatus == TransactionStatus.Fail) { console.log(`Something went wrong with transaction. Status: ${status}`); - reject("Transaction failed"); + reject(events); } }); } catch (e) { @@ -58,19 +79,35 @@ } export function submitTransactionExpectFailAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic): Promise { - return new Promise(async function(resolve, reject) { + const consoleError = console.error; + const consoleLog = console.log; + console.error = () => {}; + console.log = () => {}; + + return new Promise(async function(res, rej) { + const resolve = (rec: EventRecord[]) => { + setTimeout(() => { + res(rec); + console.error = consoleError; + console.log = consoleLog; + + }); + }; + const reject = (errror: any) => { + setTimeout(() => { + rej(errror); + console.error = consoleError; + console.log = consoleLog; + }); + }; try { await transaction.signAndSend(sender, ({ events = [], status }) => { - if (status.isReady) { - // nothing to do - // console.log(`Current tx status is Ready`); - } else if (status.isBroadcast) { - // nothing to do - // console.log(`Current tx status is Broadcast`); - } else if (status.isInBlock || status.isFinalized) { + const transactionStatus = getTransactionStatus(events, status); + + if (transactionStatus == TransactionStatus.Success) { resolve(events); - } else { - reject("Transaction failed"); + } else if (transactionStatus == TransactionStatus.Fail) { + reject(events); } }); } catch (e) { --- a/tests/src/transfer.test.ts +++ b/tests/src/transfer.test.ts @@ -33,6 +33,8 @@ // Find unused address const pk = await findUnusedAddress(api); + const error = console.error; + const log = console.log; console.log = function () {}; console.error = function () {}; @@ -42,8 +44,8 @@ }; await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees"); - delete console.log; - delete console.error; + console.log = log; + console.error = error; }); }); }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -7,12 +7,14 @@ import chaiAsPromised from 'chai-as-promised'; import type { AccountId, EventRecord } from '@polkadot/types/interfaces'; import { ApiPromise, Keyring } from "@polkadot/api"; -import { default as usingApi, submitTransactionAsync } from "../substrate/substrate-api"; +import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "../substrate/substrate-api"; import privateKey from '../substrate/privateKey'; -import { alicesPublicKey } from "../accounts"; +import { alicesPublicKey, nullPublicKey } from "../accounts"; 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; @@ -26,6 +28,12 @@ collectionId: number }; +type CreateItemResult = { + success: boolean, + collectionId: number, + itemId: number +}; + export function getGenericResult(events: EventRecord[]): GenericResult { let result: GenericResult = { success: false @@ -57,8 +65,46 @@ return result; } -export async function createCollectionExpectSuccess(name: string, description: string, tokenPrefix: string, mode: string): Promise { +function getCreateItemResult(events: EventRecord[]): CreateItemResult { + let success = false; let collectionId: number = 0; + let itemId: number = 0; + events.forEach(({ phase, event: { data, method, section } }) => { + // console.log(` ${phase}: ${section}.${method}:: ${data}`); + if (method == 'ExtrinsicSuccess') { + success = true; + } else if ((section == 'nft') && (method == 'ItemCreated')) { + collectionId = parseInt(data[0].toString()); + itemId = parseInt(data[1].toString()); + } + }); + let result: CreateItemResult = { + success, + collectionId, + itemId + } + return result; +} + +export type CollectionMode = 'NFT' | 'Fungible' | 'ReFungible'; +export type CreateCollectionParams = { + mode: CollectionMode, + name: string, + description: string, + tokenPrefix: string +}; + +const defaultCreateCollectionParams: CreateCollectionParams = { + name: 'name', + description: 'description', + mode: 'NFT', + tokenPrefix: 'prefix' +} + +export async function createCollectionExpectSuccess(params: Partial = {}): Promise { + const {name, description, mode, tokenPrefix } = {...defaultCreateCollectionParams, ...params}; + + let collectionId: number = 0; await usingApi(async (api) => { // Get number of collections before the transaction const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString()); @@ -91,15 +137,17 @@ return collectionId; } -export async function createCollectionExpectFailure(name: string, description: string, tokenPrefix: string, mode: string) { +export async function createCollectionExpectFailure(params: Partial = {}) { + const {name, description, mode, tokenPrefix } = {...defaultCreateCollectionParams, ...params}; + await usingApi(async (api) => { // Get number of collections before the transaction const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString()); // Run the CreateCollection transaction const alicePrivateKey = privateKey('//Alice'); - const tx = api.tx.nft.createCollection(name, description, tokenPrefix, mode); - const events = await submitTransactionAsync(alicePrivateKey, tx); + const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), mode); + const events = await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; const result = getCreateCollectionResult(events); // Get number of collections after the transaction @@ -123,20 +171,231 @@ return unused; } -export async function createItemExpectSuccess(collectionId: number, createMode: string, senderSeed: string = '//Alice') { +function getDestroyResult(events: EventRecord[]): boolean { + let success: boolean = false; + events.forEach(({ phase, event: { data, method, section } }) => { + // console.log(` ${phase}: ${section}.${method}:: ${data}`); + if (method == 'ExtrinsicSuccess') { + success = true; + } + }); + return success; +} + +export async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + // Run the DestroyCollection transaction + const alicePrivateKey = privateKey(senderSeed); + const tx = api.tx.nft.destroyCollection(collectionId); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + }); +} + +export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + // Run the DestroyCollection transaction + const alicePrivateKey = privateKey(senderSeed); + const tx = api.tx.nft.destroyCollection(collectionId); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getDestroyResult(events); + + // Get the collection + const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); + + // What to expect + expect(result).to.be.true; + expect(collection).to.be.not.null; + expect(collection.Owner).to.be.equal(nullPublicKey); + }); +} + +export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string) { + await usingApi(async (api) => { + + // Run the transaction + const alicePrivateKey = privateKey('//Alice'); + const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor); + const events = await submitTransactionAsync(alicePrivateKey, 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.Sponsor.toString()).to.be.equal(sponsor.toString()); + expect(collection.SponsorConfirmed).to.be.false; + }); +} + +export async function removeCollectionSponsorExpectSuccess(collectionId: number) { await usingApi(async (api) => { - const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString()); + // Run the transaction + const alicePrivateKey = privateKey('//Alice'); + const tx = api.tx.nft.removeCollectionSponsor(collectionId); + const events = await submitTransactionAsync(alicePrivateKey, 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.Sponsor).to.be.equal(nullPublicKey); + expect(collection.SponsorConfirmed).to.be.false; + }); +} + +export async function removeCollectionSponsorExpectFailure(collectionId: number) { + await usingApi(async (api) => { + + // Run the transaction + const alicePrivateKey = privateKey('//Alice'); + const tx = api.tx.nft.removeCollectionSponsor(collectionId); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + }); +} + +export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + + // Run the transaction + const alicePrivateKey = privateKey(senderSeed); + const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor); + await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; + }); +} + +export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + + // Run the transaction const sender = privateKey(senderSeed); - const tx = api.tx.nft.createItem(collectionId, sender.address, createMode); + const tx = api.tx.nft.confirmSponsorship(collectionId); 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.Sponsor).to.be.equal(sender.address); + expect(collection.SponsorConfirmed).to.be.true; + }); +} + +export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + + // Run the transaction + const sender = privateKey(senderSeed); + const tx = api.tx.nft.confirmSponsorship(collectionId); + await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; + }); +} + +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(sender: IKeyringPair, collectionId: number, createMode: string, owner: string = '') { + 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); + + if (owner === '') owner = sender.address; + + 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; + 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(sender: IKeyringPair, collectionId: number) { + await usingApi(async (api) => { + + // Run the transaction + 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'); + }); +} + +export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) { + await usingApi(async (api) => { + + // Run the transaction + const tx = api.tx.nft.setMintPermission(collectionId, true); + 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(BItemCount).to.be.equal(AItemCount+1); + expect(collection.MintMode).to.be.equal(true); }); } + +export async function addToWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string) { + await usingApi(async (api) => { + + // Run the transaction + const tx = api.tx.nft.addToWhiteList(collectionId, address); + 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.MintMode).to.be.equal(true); + }); +} +