--- 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))] @@ -263,6 +245,7 @@ #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct CreateFungibleData { + pub value: u128, } #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] @@ -417,12 +400,12 @@ /// Balance owner per collection map pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128; - /// second parameter: item id + owner account id - pub ApprovedList get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId) => Vec>; + /// second parameter: item id + owner account id + spender account id + pub Allowances get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId, T::AccountId) => u128; /// Item collections pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType; - pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => FungibleItemType; + pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => FungibleItemType; pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType; /// Index list @@ -430,7 +413,7 @@ /// Tokens transfer baskets pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber; - pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => Vec>; + pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber; pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber; // Contract Sponsorship and Ownership @@ -448,16 +431,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); } }) } @@ -593,7 +576,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(), @@ -624,7 +607,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); @@ -852,7 +835,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(()) @@ -872,10 +856,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(()) @@ -900,6 +883,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(()) @@ -1000,7 +984,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)?; @@ -1018,7 +1002,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())?, _ => () }; @@ -1074,7 +1058,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)?, _ => () }; @@ -1098,7 +1082,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)?; @@ -1110,28 +1094,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(()) } @@ -1161,15 +1132,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); @@ -1179,23 +1147,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)?, _ => () }; @@ -1614,22 +1584,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(); @@ -1637,13 +1600,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)?; } }; @@ -1653,33 +1615,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(); @@ -1687,40 +1647,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) @@ -1747,9 +1698,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()) @@ -1770,9 +1718,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) @@ -1783,24 +1728,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(()) } @@ -1861,7 +1809,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) @@ -1882,86 +1830,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(()) @@ -1996,10 +1889,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); @@ -2076,10 +1969,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(); @@ -2089,8 +1982,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(()) } @@ -2102,7 +1993,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), _ => () }; @@ -2164,13 +2054,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); @@ -2182,40 +2071,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 { @@ -2386,99 +2274,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() @@ -2555,11 +2455,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/src/confirmSponsorship.test.ts @@ -0,0 +1,350 @@ +// +// 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 } 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.only('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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for unused address + const itemId = await createItemExpectSuccess(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('A', 'B', 'C', 'ReFungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for unused address + const itemId = await createItemExpectSuccess(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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for alice + const itemId = await createItemExpectSuccess(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 () { + console.log = function () {}; + console.error = function () {}; + await submitTransactionAsync(zeroBalance, zeroToAlice); + delete console.log; + delete console.error; + }; + await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees"); + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Try again after Zero gets some balance - now it should succeed + const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15); + await submitTransactionAsync(alice, balancetx); + const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result2 = getGenericResult(events2); + + expect(result1.success).to.be.true; + expect(result2.success).to.be.true; + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); + }); + + it('Fungible: Sponsoring is rate limited', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for unused address + const itemId = await createItemExpectSuccess(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 () { + console.log = function () {}; + console.error = function () {}; + await submitTransactionAsync(zeroBalance, zeroToAlice); + delete console.log; + delete console.error; + }; + + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Try again after Zero gets some balance - now it should succeed + const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15); + await submitTransactionAsync(alice, balancetx); + const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result2 = getGenericResult(events2); + + expect(result1.success).to.be.true; + expect(result2.success).to.be.true; + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); + }); + + it('ReFungible: Sponsoring is rate limited', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + // Find unused address + const zeroBalance = await findUnusedAddress(api); + + // Mint token for alice + const itemId = await createItemExpectSuccess(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 () { + console.log = function () {}; + console.error = function () {}; + await submitTransactionAsync(zeroBalance, zeroToAlice); + delete console.log; + delete console.error; + }; + await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees"); + const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + // Try again after Zero gets some balance - now it should succeed + const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15); + await submitTransactionAsync(alice, balancetx); + const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice); + const result2 = getGenericResult(events2); + + expect(result1.success).to.be.true; + expect(result2.success).to.be.true; + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); + }); + +}); + +describe.only('(!negative test!) integration test: ext. setCollectionSponsor():', () => { + 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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'NFT'); + await confirmSponsorshipExpectFailure(collectionId, '//Bob'); + }); + + it('(!negative test!) Confirm sponsorship in a collection that was destroyed', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await destroyCollectionExpectSuccess(collectionId); + await confirmSponsorshipExpectFailure(collectionId, '//Bob'); + }); +}); --- 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'); + 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'); + 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'); + 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/destroyCollection.test.ts +++ b/tests/src/destroyCollection.test.ts @@ -1,55 +1,12 @@ 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 () => { --- /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('A', 'B', 'C', 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + it('Set Fungible collection sponsor', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + it('Set ReFungible collection sponsor', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + + it('Set the same sponsor repeatedly', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + }); + it('Replace collection sponsor', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + + 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('A', 'B', 'C', 'NFT'); + 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('A', 'B', 'C', 'NFT'); + await destroyCollectionExpectSuccess(collectionId); + await setCollectionSponsorExpectFailure(collectionId, bob.address); + }); +}); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -9,10 +9,12 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { default as usingApi, submitTransactionAsync } 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,6 +65,27 @@ return result; } +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 async function createCollectionExpectSuccess(name: string, description: string, tokenPrefix: string, mode: string): Promise { let collectionId: number = 0; await usingApi(async (api) => { @@ -123,20 +152,214 @@ 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); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getDestroyResult(events); + + // What to expect + expect(result).to.be.false; + }); +} + +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) => { - const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString()); + // 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 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); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getGenericResult(events); + + // What to expect + expect(result.success).to.be.false; + }); +} + +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); + const events = await submitTransactionAsync(sender, tx); + const result = getGenericResult(events); + + // What to expect + expect(result.success).to.be.false; + }); +} + +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; - expect(BItemCount).to.be.equal(AItemCount+1); + if (createMode == 'Fungible') { + expect(BItemBalance.minus(AItemBalance).toNumber()).to.be.equal(10); + } + else { + expect(BItemCount).to.be.equal(AItemCount+1); + } + expect(collectionId).to.be.equal(result.collectionId); + expect(BItemCount).to.be.equal(result.itemId); + newItemId = result.itemId; + }); + return newItemId; +} + +export async function enableWhiteListExpectSuccess(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(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); + }); +} +