From d9abff92da75268f309d783fe075f138b7e96f88 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Tue, 26 Jan 2021 09:12:16 +0000 Subject: [PATCH] Merge branch 'develop' into feature/NFTPAR-288 --- --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -323,6 +323,8 @@ CollectionNotFound, /// Item not exists. TokenNotFound, + /// Admin not found + AdminNotFound, /// Arithmetic calculation overflow. NumOverflow, /// Account already has admin role. @@ -426,7 +428,7 @@ pub AddressTokens get(fn address_tokens): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => Vec; /// Tokens transfer baskets - pub CreateItemBasket get(fn create_item_basket): map hasher(identity) CollectionId => T::BlockNumber; + pub CreateItemBasket get(fn create_item_basket): map hasher(twox_64_concat) (CollectionId, T::AccountId) => T::BlockNumber; 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(twox_64_concat) T::AccountId => T::BlockNumber; pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber; @@ -812,13 +814,11 @@ let sender = ensure_signed(origin)?; Self::check_owner_or_admin_permissions(collection_id, sender)?; + ensure!(>::contains_key(collection_id), Error::::AdminNotFound); - if >::contains_key(collection_id) - { - let mut admin_arr = >::get(collection_id); - admin_arr.retain(|i| *i != account_id); - >::insert(collection_id, admin_arr); - } + let mut admin_arr = >::get(collection_id); + admin_arr.retain(|i| *i != account_id); + >::insert(collection_id, admin_arr); Ok(()) } @@ -1009,7 +1009,7 @@ { CollectionMode::NFT => Self::burn_nft_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())?, + CollectionMode::ReFungible(_) => Self::burn_refungible_item(collection_id, item_id, &sender)?, _ => () }; @@ -1092,6 +1092,9 @@ let sender = ensure_signed(origin)?; + Self::collection_exists(collection_id)?; + Self::token_exists(collection_id, item_id, &sender)?; + // Transfer permissions check let target_collection = >::get(collection_id); ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) || @@ -1161,7 +1164,7 @@ } // Reduce approval by transferred amount or remove if remaining approval drops to 0 - if approval - value > 0 { + if approval.checked_sub(value).unwrap_or(0) > 0 { >::insert(collection_id, (item_id, &from, &recipient), approval - value); } else { @@ -1216,7 +1219,8 @@ let sender = ensure_signed(origin)?; Self::collection_exists(collection_id)?; - + Self::token_exists(collection_id, item_id, &sender)?; + ensure!(ChainLimit::get().custom_data_limit >= data.len() as u32, Error::::TokenVariableDataLimitExceeded); // Modify permissions check @@ -1224,8 +1228,6 @@ ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) || Self::is_owner_or_admin_permissions(collection_id, sender.clone()), Error::::NoPermission); - - Self::item_exists(collection_id, item_id, &target_collection.mode)?; match target_collection.mode { @@ -1320,7 +1322,7 @@ Self::check_owner_or_admin_permissions(collection_id, sender.clone())?; // check schema limit - ensure!(schema.len() as u32 > ChainLimit::get().const_on_chain_schema_limit, ""); + ensure!(schema.len() as u32 <= ChainLimit::get().const_on_chain_schema_limit, ""); let mut target_collection = >::get(collection_id); target_collection.const_on_chain_schema = schema; @@ -1351,7 +1353,7 @@ Self::check_owner_or_admin_permissions(collection_id, sender.clone())?; // check schema limit - ensure!(schema.len() as u32 > ChainLimit::get().variable_on_chain_schema_limit, ""); + ensure!(schema.len() as u32 <= ChainLimit::get().variable_on_chain_schema_limit, ""); let mut target_collection = >::get(collection_id); target_collection.variable_on_chain_schema = schema; @@ -1677,13 +1679,13 @@ let value = item.owner.first().unwrap().fraction; let owner = item.owner.first().unwrap().owner.clone(); - Self::add_token_index(collection_id, current_index, owner.clone())?; + Self::add_token_index(collection_id, current_index, &owner)?; ::insert(collection_id, current_index); >::insert(collection_id, current_index, itemcopy); // Update balance - let new_balance = >::get(collection_id, owner.clone()) + let new_balance = >::get(collection_id, &owner) .checked_add(value) .ok_or(Error::::NumOverflow)?; >::insert(collection_id, owner.clone(), new_balance); @@ -1697,7 +1699,7 @@ .ok_or(Error::::NumOverflow)?; let item_owner = item.owner.clone(); - Self::add_token_index(collection_id, current_index, item.owner.clone())?; + Self::add_token_index(collection_id, current_index, &item.owner)?; ::insert(collection_id, current_index); >::insert(collection_id, current_index, item); @@ -1714,29 +1716,44 @@ fn burn_refungible_item( collection_id: CollectionId, item_id: TokenId, - owner: T::AccountId, + owner: &T::AccountId, ) -> DispatchResult { ensure!( >::contains_key(collection_id, item_id), Error::::TokenNotFound ); - let collection = >::get(collection_id, item_id); - let item = collection + let mut token = >::get(collection_id, item_id); + let rft_balance = token .owner .iter() - .filter(|&i| i.owner == owner) + .filter(|&i| i.owner == *owner) .next() .unwrap(); - Self::remove_token_index(collection_id, item_id, owner.clone())?; + Self::remove_token_index(collection_id, item_id, owner)?; // update balance - let new_balance = >::get(collection_id, item.owner.clone()) - .checked_sub(item.fraction) + let new_balance = >::get(collection_id, rft_balance.owner.clone()) + .checked_sub(rft_balance.fraction) .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, item.owner.clone(), new_balance); + >::insert(collection_id, rft_balance.owner.clone(), new_balance); - >::remove(collection_id, item_id); + // Re-create owners list with sender removed + let index = token + .owner + .iter() + .position(|i| i.owner == *owner) + .unwrap(); + token.owner.remove(index); + let owner_count = token.owner.len(); + // Burn the token completely if this was the last (only) owner + if owner_count == 0 { + >::remove(collection_id, item_id); + } + else { + >::insert(collection_id, item_id, token); + } + Ok(()) } @@ -1746,10 +1763,10 @@ Error::::TokenNotFound ); let item = >::get(collection_id, item_id); - Self::remove_token_index(collection_id, item_id, item.owner.clone())?; + Self::remove_token_index(collection_id, item_id, &item.owner)?; // update balance - let new_balance = >::get(collection_id, item.owner.clone()) + let new_balance = >::get(collection_id, &item.owner) .checked_sub(1) .ok_or(Error::::NumOverflow)?; >::insert(collection_id, item.owner.clone(), new_balance); @@ -1858,16 +1875,33 @@ Ok(()) } + /// Check if token exists. In case of Fungible, check if there is an entry for + /// the owner in fungible balances double map + fn token_exists( + collection_id: CollectionId, + item_id: TokenId, + owner: &T::AccountId + ) -> DispatchResult { + let target_collection = >::get(collection_id); + let exists = match target_collection.mode + { + CollectionMode::NFT => >::contains_key(collection_id, item_id), + CollectionMode::Fungible(_) => >::contains_key(collection_id, owner), + CollectionMode::ReFungible(_) => >::contains_key(collection_id, item_id), + _ => false + }; + + ensure!(exists == true, Error::::TokenNotFound); + Ok(()) + } + fn transfer_fungible( collection_id: CollectionId, value: u128, owner: &T::AccountId, recipient: &T::AccountId, ) -> DispatchResult { - ensure!( - >::contains_key(collection_id, owner), - Error::::TokenNotFound - ); + Self::token_exists(collection_id, 0, owner)?; let mut balance = >::get(collection_id, owner); ensure!(balance.value >= value, Error::::TokenValueTooLow); @@ -1897,10 +1931,7 @@ owner: T::AccountId, new_owner: T::AccountId, ) -> DispatchResult { - ensure!( - >::contains_key(collection_id, item_id), - Error::::TokenNotFound - ); + Self::token_exists(collection_id, item_id, &owner)?; let full_item = >::get(collection_id, item_id); let item = full_item @@ -1941,7 +1972,7 @@ >::insert(collection_id, item_id, new_full_item); // update index collection - Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?; + Self::move_token_index(collection_id, item_id, &old_owner, &new_owner)?; } else { let mut new_full_item = full_item.clone(); new_full_item @@ -1966,7 +1997,7 @@ owner: new_owner.clone(), fraction: value, }); - Self::add_token_index(collection_id, item_id, new_owner.clone())?; + Self::add_token_index(collection_id, item_id, &new_owner)?; } >::insert(collection_id, item_id, new_full_item); @@ -1981,10 +2012,7 @@ sender: T::AccountId, new_owner: T::AccountId, ) -> DispatchResult { - ensure!( - >::contains_key(collection_id, item_id), - Error::::TokenNotFound - ); + Self::token_exists(collection_id, item_id, &sender)?; let mut item = >::get(collection_id, item_id); @@ -2010,25 +2038,11 @@ >::insert(collection_id, item_id, item); // update index collection - Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?; + Self::move_token_index(collection_id, item_id, &old_owner, &new_owner)?; Ok(()) } - fn item_exists( - collection_id: CollectionId, - item_id: TokenId, - mode: &CollectionMode - ) -> DispatchResult { - match mode { - CollectionMode::NFT => ensure!(>::contains_key(collection_id, item_id), Error::::TokenNotFound), - CollectionMode::ReFungible(_) => ensure!(>::contains_key(collection_id, item_id), Error::::TokenNotFound), - _ => () - }; - - Ok(()) - } - fn set_re_fungible_variable_data( collection_id: CollectionId, item_id: TokenId, @@ -2090,12 +2104,12 @@ .unwrap(); let item_owner = item.owner.clone(); - Self::add_token_index(collection_id, current_index, item.owner.clone()).unwrap(); + Self::add_token_index(collection_id, current_index, &item.owner).unwrap(); ::insert(collection_id, current_index); // Update balance - let new_balance = >::get(collection_id, item_owner.clone()) + let new_balance = >::get(collection_id, &item_owner) .checked_add(1) .unwrap(); >::insert(collection_id, item_owner.clone(), new_balance); @@ -2106,7 +2120,7 @@ .checked_add(1) .unwrap(); - Self::add_token_index(collection_id, current_index, (*owner).clone()).unwrap(); + Self::add_token_index(collection_id, current_index, owner).unwrap(); ::insert(collection_id, current_index); @@ -2125,24 +2139,24 @@ let value = item.owner.first().unwrap().fraction; let owner = item.owner.first().unwrap().owner.clone(); - Self::add_token_index(collection_id, current_index, owner.clone()).unwrap(); + Self::add_token_index(collection_id, current_index, &owner).unwrap(); ::insert(collection_id, current_index); // Update balance - let new_balance = >::get(collection_id, owner.clone()) + let new_balance = >::get(collection_id, &owner) .checked_add(value) .unwrap(); >::insert(collection_id, owner.clone(), new_balance); } - fn add_token_index(collection_id: CollectionId, item_index: TokenId, owner: T::AccountId) -> DispatchResult { + fn add_token_index(collection_id: CollectionId, item_index: TokenId, owner: &T::AccountId) -> DispatchResult { // add to account limit - if >::contains_key(owner.clone()) { + if >::contains_key(owner) { // bound Owned tokens by a single address - let count = >::get(owner.clone()); + let count = >::get(owner); ensure!(count < ChainLimit::get().account_token_ownership_limit, Error::::AddressOwnershipLimitExceeded); >::insert(owner.clone(), count @@ -2153,9 +2167,9 @@ >::insert(owner.clone(), 1); } - let list_exists = >::contains_key(collection_id, owner.clone()); + let list_exists = >::contains_key(collection_id, owner); if list_exists { - let mut list = >::get(collection_id, owner.clone()); + let mut list = >::get(collection_id, owner); let item_contains = list.contains(&item_index.clone()); if !item_contains { @@ -2166,8 +2180,7 @@ } else { let mut itm = Vec::new(); itm.push(item_index.clone()); - >::insert(collection_id, owner, itm); - + >::insert(collection_id, owner.clone(), itm); } Ok(()) @@ -2176,24 +2189,24 @@ fn remove_token_index( collection_id: CollectionId, item_index: TokenId, - owner: T::AccountId, + owner: &T::AccountId, ) -> DispatchResult { // update counter >::insert(owner.clone(), - >::get(owner.clone()) + >::get(owner) .checked_sub(1) .ok_or(Error::::NumOverflow)?); - let list_exists = >::contains_key(collection_id, owner.clone()); + let list_exists = >::contains_key(collection_id, owner); if list_exists { - let mut list = >::get(collection_id, owner.clone()); + let mut list = >::get(collection_id, owner); let item_contains = list.contains(&item_index.clone()); if item_contains { list.retain(|&item| item != item_index); - >::insert(collection_id, owner, list); + >::insert(collection_id, owner.clone(), list); } } @@ -2203,8 +2216,8 @@ fn move_token_index( collection_id: CollectionId, item_index: TokenId, - old_owner: T::AccountId, - new_owner: T::AccountId, + old_owner: &T::AccountId, + new_owner: &T::AccountId, ) -> DispatchResult { Self::remove_token_index(collection_id, item_index, old_owner)?; Self::add_token_index(collection_id, item_index, new_owner)?; @@ -2319,15 +2332,15 @@ let limit = >::get(collection_id).limits.sponsor_transfer_timeout; let mut sponsored = true; - if >::contains_key(collection_id) { - let last_tx_block = >::get(collection_id); + 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; } } if sponsored { - >::insert(collection_id, block_number); + >::insert((collection_id, who.clone()), block_number); } // check free create limit --- a/pallets/nft/src/tests.rs +++ b/pallets/nft/src/tests.rs @@ -1,7 +1,7 @@ // Tests to be written here use super::*; use crate::mock::*; -use crate::{AccessMode, ApprovePermissions, CollectionMode, +use crate::{AccessMode, CollectionMode, Ownership, ChainLimits, CreateItemData, CreateNftData, CreateFungibleData, CreateReFungibleData, CollectionId, TokenId, MAX_DECIMAL_POINTS}; use frame_support::{assert_noop, assert_ok}; @@ -12,14 +12,17 @@ } fn default_limits() { - assert_ok!(TemplateModule::set_chain_limits(RawOrigin::Root.into(), ChainLimits { + assert_ok!(TemplateModule::set_chain_limits(RawOrigin::Root.into(), ChainLimits { collection_numbers_limit: default_collection_numbers_limit(), account_token_ownership_limit: 10, collections_admins_limit: 5, custom_data_limit: 2048, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); } @@ -28,7 +31,7 @@ } fn default_fungible_data () -> CreateFungibleData { - CreateFungibleData { } + CreateFungibleData { value: 5 } } fn default_re_fungible_data () -> CreateReFungibleData { @@ -238,35 +241,35 @@ let data = default_fungible_data(); create_test_item(collection_id, &data.into()); - assert_eq!(TemplateModule::fungible_item_id(collection_id, 1).owner, 1); + assert_eq!(TemplateModule::fungible_item_id(collection_id, 1).value, 5); }); } -#[test] -fn create_multiple_fungible_items() { - new_test_ext().execute_with(|| { - default_limits(); +//#[test] +// fn create_multiple_fungible_items() { +// new_test_ext().execute_with(|| { +// default_limits(); - create_test_collection(&CollectionMode::Fungible(3), 1); +// create_test_collection(&CollectionMode::Fungible(3), 1); - let origin1 = Origin::signed(1); +// let origin1 = Origin::signed(1); - let items_data = vec![default_fungible_data(), default_fungible_data(), default_fungible_data()]; +// let items_data = vec![default_fungible_data(), default_fungible_data(), default_fungible_data()]; - assert_ok!(TemplateModule::create_multiple_items( - origin1.clone(), - 1, - 1, - items_data.clone().into_iter().map(|d| { d.into() }).collect() - )); +// assert_ok!(TemplateModule::create_multiple_items( +// origin1.clone(), +// 1, +// 1, +// items_data.clone().into_iter().map(|d| { d.into() }).collect() +// )); - for (index, _) in items_data.iter().enumerate() { - assert_eq!(TemplateModule::fungible_item_id(1, (index + 1) as TokenId).owner, 1); - } - assert_eq!(TemplateModule::balance_count(1, 1), 3000); - assert_eq!(TemplateModule::address_tokens(1, 1), [1, 2, 3]); - }); -} +// for (index, _) in items_data.iter().enumerate() { +// assert_eq!(TemplateModule::fungible_item_id(1, (index + 1) as TokenId).value, 5); +// } +// assert_eq!(TemplateModule::balance_count(1, 1), 3000); +// assert_eq!(TemplateModule::address_tokens(1, 1), [1, 2, 3]); +// }); +// } #[test] fn transfer_fungible_item() { @@ -281,36 +284,26 @@ let data = default_fungible_data(); create_test_item(collection_id, &data.into()); - assert_eq!(TemplateModule::fungible_item_id(1, 1).owner, 1); - assert_eq!(TemplateModule::balance_count(1, 1), 1000); - assert_eq!(TemplateModule::address_tokens(1, 1), [1]); + assert_eq!(TemplateModule::fungible_item_id(1, 1).value, 5); + assert_eq!(TemplateModule::balance_count(1, 1), 5); // change owner scenario - assert_ok!(TemplateModule::transfer(origin1.clone(), 2, 1, 1, 1000)); - assert_eq!(TemplateModule::fungible_item_id(1, 1).owner, 2); - assert_eq!(TemplateModule::fungible_item_id(1, 1).value, 1000); + assert_ok!(TemplateModule::transfer(origin1.clone(), 2, 1, 1, 5)); + assert_eq!(TemplateModule::fungible_item_id(1, 1).value, 0); assert_eq!(TemplateModule::balance_count(1, 1), 0); - assert_eq!(TemplateModule::balance_count(1, 2), 1000); - // assert_eq!(TemplateModule::address_tokens(1, 1), []); - assert_eq!(TemplateModule::address_tokens(1, 2), [1]); + assert_eq!(TemplateModule::balance_count(1, 2), 5); // split item scenario - assert_ok!(TemplateModule::transfer(origin2.clone(), 3, 1, 1, 500)); - assert_eq!(TemplateModule::fungible_item_id(1, 1).owner, 2); - assert_eq!(TemplateModule::fungible_item_id(1, 2).owner, 3); - assert_eq!(TemplateModule::balance_count(1, 2), 500); - assert_eq!(TemplateModule::balance_count(1, 3), 500); - assert_eq!(TemplateModule::address_tokens(1, 2), [1]); - assert_eq!(TemplateModule::address_tokens(1, 3), [2]); + assert_ok!(TemplateModule::transfer(origin2.clone(), 3, 1, 1, 3)); + assert_eq!(TemplateModule::balance_count(1, 2), 2); + assert_eq!(TemplateModule::balance_count(1, 3), 3); // split item and new owner has account scenario - assert_ok!(TemplateModule::transfer(origin2.clone(), 3, 1, 1, 200)); - assert_eq!(TemplateModule::fungible_item_id(1, 1).value, 300); - assert_eq!(TemplateModule::fungible_item_id(1, 2).value, 700); - assert_eq!(TemplateModule::balance_count(1, 2), 300); - assert_eq!(TemplateModule::balance_count(1, 3), 700); - assert_eq!(TemplateModule::address_tokens(1, 2), [1]); - assert_eq!(TemplateModule::address_tokens(1, 3), [2]); + assert_ok!(TemplateModule::transfer(origin2.clone(), 3, 1, 1, 1)); + assert_eq!(TemplateModule::fungible_item_id(1, 2).value, 1); + assert_eq!(TemplateModule::fungible_item_id(1, 3).value, 4); + assert_eq!(TemplateModule::balance_count(1, 2), 1); + assert_eq!(TemplateModule::balance_count(1, 3), 4); }); } @@ -451,14 +444,11 @@ 1), Error::::NoPermission); // do approve - assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); + assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1, 5)); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 5); assert_eq!( - TemplateModule::approved(1, (1, 1))[0], - ApprovePermissions { - approved: 2, - amount: 100000000 - } + TemplateModule::approved(1, (1, 1, 2)), + 5 ); assert_ok!(TemplateModule::transfer_from( @@ -469,7 +459,7 @@ 1, 1 )); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 0); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 4); }); } @@ -505,17 +495,10 @@ assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), 1, 3)); // do approve - assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); - assert_ok!(TemplateModule::approve(origin1.clone(), 3, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 2); - assert_eq!( - TemplateModule::approved(1, (1, 1))[0], - ApprovePermissions { - approved: 2, - amount: 100000000 - } - ); + assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1, 5)); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 5); + assert_ok!(TemplateModule::approve(origin1.clone(), 3, 1, 1, 5)); + assert_eq!(TemplateModule::approved(1, (1, 1, 3)), 5); assert_ok!(TemplateModule::transfer_from( origin2.clone(), @@ -525,7 +508,7 @@ 1, 1 )); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 0); + assert_eq!(TemplateModule::approved(1, (1, 1, 3)), 4); }); } @@ -560,17 +543,10 @@ assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), 1, 3)); // do approve - assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); - assert_ok!(TemplateModule::approve(origin1.clone(), 3, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 2); - assert_eq!( - TemplateModule::approved(1, (1, 1))[0], - ApprovePermissions { - approved: 2, - amount: 100000000 - } - ); + assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1, 5)); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 5); + assert_ok!(TemplateModule::approve(origin1.clone(), 3, 1, 1, 1000)); + assert_eq!(TemplateModule::approved(1, (1, 1, 3)), 1000); assert_ok!(TemplateModule::transfer_from( origin2.clone(), @@ -585,13 +561,9 @@ assert_eq!(TemplateModule::address_tokens(1, 1), [1]); assert_eq!(TemplateModule::address_tokens(1, 3), [1]); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); assert_eq!( - TemplateModule::approved(1, (1, 1))[0], - ApprovePermissions { - approved: 3, - amount: 100000000 - } + TemplateModule::approved(1, (1, 1, 3)), + 900 ); }); } @@ -609,8 +581,7 @@ let origin1 = Origin::signed(1); let origin2 = Origin::signed(2); - assert_eq!(TemplateModule::balance_count(1, 1), 1000); - assert_eq!(TemplateModule::address_tokens(1, 1), [1]); + assert_eq!(TemplateModule::balance_count(1, 1), 5); assert_ok!(TemplateModule::set_mint_permission( origin1.clone(), @@ -627,16 +598,13 @@ assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), 1, 3)); // do approve - assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); - assert_ok!(TemplateModule::approve(origin1.clone(), 3, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 2); + assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1, 5)); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 5); + assert_ok!(TemplateModule::approve(origin1.clone(), 3, 1, 1, 5)); + assert_eq!(TemplateModule::approved(1, (1, 1, 3)), 5); assert_eq!( - TemplateModule::approved(1, (1, 1))[0], - ApprovePermissions { - approved: 2, - amount: 100000000 - } + TemplateModule::approved(1, (1, 1, 2)), + 5 ); assert_ok!(TemplateModule::transfer_from( @@ -645,37 +613,23 @@ 3, 1, 1, - 100 + 4 )); - assert_eq!(TemplateModule::balance_count(1, 1), 900); - assert_eq!(TemplateModule::balance_count(1, 3), 100); - assert_eq!(TemplateModule::address_tokens(1, 1), [1]); - assert_eq!(TemplateModule::address_tokens(1, 3), [2]); + assert_eq!(TemplateModule::balance_count(1, 1), 1); + assert_eq!(TemplateModule::balance_count(1, 3), 4); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); - assert_eq!( - TemplateModule::approved(1, (1, 1))[0], - ApprovePermissions { - approved: 3, - amount: 100000000 - } - ); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 5); + assert_eq!(TemplateModule::approved(1, (1, 1, 3)), 1); - assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1)); - assert_ok!(TemplateModule::transfer_from( + assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1, 5)); + assert_noop!(TemplateModule::transfer_from( origin2.clone(), 1, 3, 1, 1, - 900 - )); - assert_eq!(TemplateModule::balance_count(1, 1), 0); - assert_eq!(TemplateModule::balance_count(1, 3), 1000); - // assert_eq!(TemplateModule::address_tokens(1, 1), []); - assert_eq!(TemplateModule::address_tokens(1, 3), [2]); - - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 0); + 4 + ), Error::::TokenValueNotEnough); }); } @@ -725,9 +679,9 @@ assert_eq!(TemplateModule::balance_count(1, 1), 1); // burn item - assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1)); + assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1, 5)); assert_noop!( - TemplateModule::burn_item(origin1.clone(), 1, 1), + TemplateModule::burn_item(origin1.clone(), 1, 1, 5), Error::::TokenNotFound ); @@ -749,12 +703,12 @@ create_test_item(collection_id, &data.into()); // check balance (collection with id = 1, user id = 1) - assert_eq!(TemplateModule::balance_count(1, 1), 1000); + assert_eq!(TemplateModule::balance_count(1, 1), 5); // burn item - assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1)); + assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1, 5)); assert_noop!( - TemplateModule::burn_item(origin1.clone(), 1, 1), + TemplateModule::burn_item(origin1.clone(), 1, 1, 5), Error::::TokenNotFound ); @@ -791,9 +745,9 @@ assert_eq!(TemplateModule::balance_count(1, 1), 1000); // burn item - assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1)); + assert_ok!(TemplateModule::burn_item(origin1.clone(), 1, 1, 1000)); assert_noop!( - TemplateModule::burn_item(origin1.clone(), 1, 1), + TemplateModule::burn_item(origin1.clone(), 1, 1, 1000), Error::::TokenNotFound ); @@ -875,10 +829,10 @@ // check balance (collection with id = 1, user id = 1) assert_eq!(TemplateModule::balance_count(nft_collection_id, 1), 1); - assert_eq!(TemplateModule::balance_count(fungible_collection_id, 1), 1000); + assert_eq!(TemplateModule::balance_count(fungible_collection_id, 1), 5); assert_eq!(TemplateModule::balance_count(re_fungible_collection_id, 1), 1000); assert_eq!(TemplateModule::nft_item_id(nft_collection_id, 1).owner, 1); - assert_eq!(TemplateModule::fungible_item_id(fungible_collection_id, 1).owner, 1); + assert_eq!(TemplateModule::fungible_item_id(fungible_collection_id, 1).value, 5); assert_eq!(TemplateModule::refungible_item_id(re_fungible_collection_id, 1).owner[0].owner, 1); }); } @@ -896,8 +850,8 @@ let origin1 = Origin::signed(1); // approve - assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1))[0].approved, 2); + assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1, 1)); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 1); }); } @@ -914,8 +868,8 @@ create_test_item(collection_id, &data.into()); // approve - assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1))[0].approved, 2); + assert_ok!(TemplateModule::approve(origin1.clone(), 2, 1, 1, 1)); + assert_eq!(TemplateModule::approved(1, (1, 1, 2)), 1); assert_ok!(TemplateModule::set_mint_permission( origin1.clone(), @@ -1199,8 +1153,8 @@ assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), 1, 2)); // do approve - assert_ok!(TemplateModule::approve(origin1.clone(), 1, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); + assert_ok!(TemplateModule::approve(origin1.clone(), 1, 1, 1, 1)); + assert_eq!(TemplateModule::approved(1, (1, 1, 1)), 1); assert_ok!(TemplateModule::remove_from_white_list( origin1.clone(), @@ -1263,8 +1217,8 @@ assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), collection_id, 2)); // do approve - assert_ok!(TemplateModule::approve(origin1.clone(), 1, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); + assert_ok!(TemplateModule::approve(origin1.clone(), 1, 1, 1, 1)); + assert_eq!(TemplateModule::approved(1, (1, 1, 1)), 1); assert_ok!(TemplateModule::remove_from_white_list( origin1.clone(), @@ -1298,13 +1252,13 @@ AccessMode::WhiteList )); assert_noop!( - TemplateModule::burn_item(origin1.clone(), 1, 1), + TemplateModule::burn_item(origin1.clone(), 1, 1, 5), Error::::AddresNotInWhiteList ); }); } -// If Public Access mode is set to WhiteList, oken transfers can’t be Approved by a non-whitelisted address (see Approve method). +// If Public Access mode is set to WhiteList, token transfers can’t be Approved by a non-whitelisted address (see Approve method). #[test] fn white_list_test_6() { new_test_ext().execute_with(|| { @@ -1313,6 +1267,10 @@ let collection_id = create_test_collection(&CollectionMode::NFT, 1); let origin1 = Origin::signed(1); + + let data = default_nft_data(); + create_test_item(collection_id, &data.into()); + assert_ok!(TemplateModule::set_public_access_mode( origin1.clone(), collection_id, @@ -1321,7 +1279,7 @@ // do approve assert_noop!( - TemplateModule::approve(origin1.clone(), 1, 1, 1), + TemplateModule::approve(origin1.clone(), 1, 1, 1, 5), Error::::AddresNotInWhiteList ); }); @@ -1374,8 +1332,8 @@ assert_ok!(TemplateModule::add_to_white_list(origin1.clone(), collection_id, 2)); // do approve - assert_ok!(TemplateModule::approve(origin1.clone(), 1, 1, 1)); - assert_eq!(TemplateModule::approved(1, (1, 1)).len(), 1); + assert_ok!(TemplateModule::approve(origin1.clone(), 1, 1, 1, 5)); + assert_eq!(TemplateModule::approved(1, (1, 1, 1)), 5); assert_ok!(TemplateModule::transfer_from( origin1.clone(), @@ -1687,7 +1645,10 @@ custom_data_limit: 2048, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1716,7 +1677,10 @@ custom_data_limit: 2048, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1739,7 +1703,10 @@ custom_data_limit: 2048, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1762,7 +1729,10 @@ custom_data_limit: 2, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1793,7 +1763,10 @@ custom_data_limit: 2, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1824,7 +1797,10 @@ custom_data_limit: 2, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1855,7 +1831,10 @@ custom_data_limit: 2, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1972,7 +1951,10 @@ custom_data_limit: 10, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); let collection_id = create_test_collection(&CollectionMode::NFT, 1); @@ -1997,7 +1979,10 @@ custom_data_limit: 10, nft_sponsor_transfer_timeout: 15, fungible_sponsor_transfer_timeout: 15, - refungible_sponsor_transfer_timeout: 15, + refungible_sponsor_transfer_timeout: 15, + const_on_chain_schema_limit: 1024, + offchain_schema_limit: 1024, + variable_on_chain_schema_limit: 1024, })); --- a/tests/package.json +++ b/tests/package.json @@ -18,10 +18,19 @@ "scripts": { "test": "mocha --timeout 9999999 -r ts-node/register ./**/*.test.ts", "load": "mocha --timeout 9999999 -r ts-node/register ./**/*.load.ts", + "testAddCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/addCollectionAdmin.test.ts", "testSetSchemaVersion": "mocha --timeout 9999999 -r ts-node/register ./**/setSchemaVersion.test.ts", "testSetCollectionLimits": "mocha --timeout 9999999 -r ts-node/register ./**/setCollectionLimits.test.ts", + "testRemoveCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/removeCollectionAdmin.test.ts", "testConnection": "mocha --timeout 9999999 -r ts-node/register ./**/connection.test.ts", - "testCollection": "mocha --timeout 9999999 -r ts-node/register ./**/createCollection.test.ts" + "testCollection": "mocha --timeout 9999999 -r ts-node/register ./**/createCollection.test.ts", + "testApprove": "mocha --timeout 9999999 -r ts-node/register ./**/approve.test.ts", + "testTransferFrom": "mocha --timeout 9999999 -r ts-node/register ./**/transferFrom.test.ts", + "testCreateCollection": "mocha --timeout 9999999 -r ts-node/register ./**/createCollection.test.ts", + "testToggleContractWhiteList": "mocha --timeout 9999999 -r ts-node/register ./**/toggleContractWhiteList.test.ts", + "testAddToContractWhiteList": "mocha --timeout 9999999 -r ts-node/register ./**/addToContractWhiteList.test.ts", + "testTransfer": "mocha --timeout 9999999 -r ts-node/register ./**/transfer.test.ts", + "testBurnItem": "mocha --timeout 9999999 -r ts-node/register ./**/burnItem.test.ts" }, "author": "", "license": "Apache 2.0", --- /dev/null +++ b/tests/src/addCollectionAdmin.test.ts @@ -0,0 +1,130 @@ +import { ApiPromise } from '@polkadot/api'; +import BN from 'bn.js'; +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, destroyCollectionExpectSuccess} from './util/helpers'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => { + it('Add collection admin.', 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 changeAdminTx = api.tx.nft.addCollectionAdmin(collectionId, bob.address); + await submitTransactionAsync(alice, changeAdminTx); + + const adminListAfterAddAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListAfterAddAdmin).to.be.contains(bob.address); + }); + }); + + it('Add admin using added collection admin.', async () => { + await usingApi(async (api) => { + const collectionId = await createCollectionExpectSuccess(); + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const Charlie = privateKey('//CHARLIE'); + + const collection: any = (await api.query.nft.collection(collectionId)); + expect(collection.Owner.toString()).to.be.eq(Alice.address); + + const changeAdminTx = api.tx.nft.addCollectionAdmin(collectionId, Bob.address); + await submitTransactionAsync(Alice, changeAdminTx); + + const adminListAfterAddAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListAfterAddAdmin).to.be.contains(Bob.address); + + const changeAdminTxCharlie = api.tx.nft.addCollectionAdmin(collectionId, Charlie.address); + await submitTransactionAsync(Bob, changeAdminTxCharlie); + const adminListAfterAddNewAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListAfterAddNewAdmin).to.be.contains(Bob.address); + expect(adminListAfterAddNewAdmin).to.be.contains(Charlie.address); + }); + }); +}); + +describe('Negative Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => { + it("Not owner can't add collection admin.", async () => { + await usingApi(async (api) => { + const collectionId = await createCollectionExpectSuccess(); + const alice = privateKey('//Alice'); + const nonOwner = privateKey('//Bob_stash'); + + const changeAdminTx = api.tx.nft.addCollectionAdmin(collectionId, alice.address); + await expect(submitTransactionExpectFailAsync(nonOwner, changeAdminTx)).to.be.rejected; + + const adminListAfterAddAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListAfterAddAdmin).not.to.be.contains(alice.address); + + // Verifying that nothing bad happened (network is live, new collections can be created, etc.) + await createCollectionExpectSuccess(); + }); + }); + it("Can't add collection admin of not existing collection.", async () => { + await usingApi(async (api) => { + // tslint:disable-next-line: no-bitwise + const collectionId = (1 << 32) - 1; + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const changeOwnerTx = api.tx.nft.addCollectionAdmin(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(); + }); + }); + + it("Can't add an admin to a destroyed collection.", async () => { + await usingApi(async (api) => { + const collectionId = await createCollectionExpectSuccess(); + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + await destroyCollectionExpectSuccess(collectionId); + const changeOwnerTx = api.tx.nft.addCollectionAdmin(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(); + }); + }); + + it('Add an admin to a collection that has reached the maximum number of admins limit', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const accounts = [ + 'GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz', + 'FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP', + 'JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5', + 'Fr4NzY1udSFFLzb2R3qxVQkwz9cZraWkyfH4h3mVVk7BK7P', + 'DfnTB4z7eUvYRqcGtTpFsLC69o6tvBSC1pEv8vWPZFtCkaK', + 'HnMAUz7r2G8G3hB27SYNyit5aJmh2a5P4eMdDtACtMFDbam', + 'DE14BzQ1bDXWPKeLoAqdLAm1GpyAWaWF1knF74cEZeomTBM', + ]; + const collectionId = await createCollectionExpectSuccess(); + + const chainLimit = await api.query.nft.chainLimit() as unknown as { collections_admins_limit: BN }; + const chainLimitNumber = chainLimit.collections_admins_limit.toNumber(); + expect(chainLimitNumber).to.be.equal(5); + + for (let i = 0; i < chainLimitNumber; i++) { + const changeAdminTx = api.tx.nft.addCollectionAdmin(collectionId, accounts[i]); + await submitTransactionAsync(Alice, changeAdminTx); + const adminListAfterAddAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListAfterAddAdmin).to.be.contains(accounts[i]); + } + + const tx = api.tx.nft.addCollectionAdmin(collectionId, accounts[chainLimitNumber]); + await expect(submitTransactionExpectFailAsync(Alice, tx)).to.be.rejected; + }); + }); +}); --- /dev/null +++ b/tests/src/addToContractWhiteList.test.ts @@ -0,0 +1,91 @@ +import chai from "chai"; +import chaiAsPromised from 'chai-as-promised'; +import usingApi, { submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; +import privateKey from "./substrate/privateKey"; +import { + deployFlipper, + getFlipValue +} from "./util/contracthelpers"; +import { + getGenericResult +} from "./util/helpers" + +chai.use(chaiAsPromised); +const expect = chai.expect; + +const value = 0; +const gasLimit = 3000n * 1000000n; + +describe('Integration Test addToContractWhiteList', () => { + + it(`Add an address to a contract white list`, async () => { + await usingApi(async api => { + const bob = privateKey("//Bob"); + const [contract, deployer] = await deployFlipper(api); + + const whiteListedBefore = (await api.query.nft.contractWhiteList(contract.address, bob.address)).toJSON(); + const addTx = api.tx.nft.addToContractWhiteList(contract.address, bob.address); + const addEvents = await submitTransactionAsync(deployer, addTx); + const whiteListedAfter = (await api.query.nft.contractWhiteList(contract.address, bob.address)).toJSON(); + + expect(getGenericResult(addEvents).success).to.be.true; + expect(whiteListedBefore).to.be.false; + expect(whiteListedAfter).to.be.true; + }); + }); + + it(`Adding same address to white list repeatedly should not produce errors`, async () => { + await usingApi(async api => { + const bob = privateKey("//Bob"); + const [contract, deployer] = await deployFlipper(api); + + const whiteListedBefore = (await api.query.nft.contractWhiteList(contract.address, bob.address)).toJSON(); + const addTx = api.tx.nft.addToContractWhiteList(contract.address, bob.address); + const addEvents = await submitTransactionAsync(deployer, addTx); + const whiteListedAfter = (await api.query.nft.contractWhiteList(contract.address, bob.address)).toJSON(); + const addAgainEvents = await submitTransactionAsync(deployer, addTx); + const whiteListedAgainAfter = (await api.query.nft.contractWhiteList(contract.address, bob.address)).toJSON(); + + expect(getGenericResult(addEvents).success).to.be.true; + expect(whiteListedBefore).to.be.false; + expect(whiteListedAfter).to.be.true; + expect(getGenericResult(addAgainEvents).success).to.be.true; + expect(whiteListedAgainAfter).to.be.true; + }); + }); +}); + +describe('Negative Integration Test addToContractWhiteList', () => { + + it(`Add an address to a white list of a non-contract`, async () => { + await usingApi(async api => { + const alice = privateKey("//Bob"); + const bob = privateKey("//Bob"); + const charlieGuineaPig = privateKey("//Charlie"); + + const whiteListedBefore = (await api.query.nft.contractWhiteList(charlieGuineaPig.address, bob.address)).toJSON(); + const addTx = api.tx.nft.addToContractWhiteList(charlieGuineaPig.address, bob.address); + await expect(submitTransactionExpectFailAsync(alice, addTx)).to.be.rejected; + const whiteListedAfter = (await api.query.nft.contractWhiteList(charlieGuineaPig.address, bob.address)).toJSON(); + + expect(whiteListedBefore).to.be.false; + expect(whiteListedAfter).to.be.false; + }); + }); + + it(`Add to a contract white list using a non-owner address`, async () => { + await usingApi(async api => { + const bob = privateKey("//Bob"); + const [contract, deployer] = await deployFlipper(api); + + const whiteListedBefore = (await api.query.nft.contractWhiteList(contract.address, bob.address)).toJSON(); + const addTx = api.tx.nft.addToContractWhiteList(contract.address, bob.address); + await expect(submitTransactionExpectFailAsync(bob, addTx)).to.be.rejected; + const whiteListedAfter = (await api.query.nft.contractWhiteList(contract.address, bob.address)).toJSON(); + + expect(whiteListedBefore).to.be.false; + expect(whiteListedAfter).to.be.false; + }); + }); + +}); --- /dev/null +++ b/tests/src/addToWhiteList.test.ts @@ -0,0 +1,81 @@ +import { IKeyringPair } from '@polkadot/types/types'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import privateKey from './substrate/privateKey'; +import usingApi, { submitTransactionExpectFailAsync } from './substrate/substrate-api'; +import { + addToWhiteListExpectSuccess, + createCollectionExpectSuccess, + createItemExpectSuccess, + destroyCollectionExpectSuccess, + enablePublicMintingExpectSuccess, + enableWhiteListExpectSuccess, +} from './util/helpers'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +let Alice: IKeyringPair; +let Bob: IKeyringPair; + +describe('Integration Test ext. addToWhiteList()', () => { + + before(async () => { + await usingApi(async (api) => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + }); + }); + + it('Execute the extrinsic with parameters: Collection ID and address to add to the white list', async () => { + const collectionId = await createCollectionExpectSuccess(); + await addToWhiteListExpectSuccess(Alice, collectionId, Bob.address); + }); + + it('Whitelisted minting: list restrictions', async () => { + const collectionId = await createCollectionExpectSuccess(); + await addToWhiteListExpectSuccess(Alice, collectionId, Bob.address); + await enableWhiteListExpectSuccess(Alice, collectionId); + await enablePublicMintingExpectSuccess(Alice, collectionId); + await createItemExpectSuccess(Bob, collectionId, 'NFT', Bob.address); + }); +}); + +describe('Negative Integration Test ext. addToWhiteList()', () => { + + it('White list an address in the collection that does not exist', async () => { + await usingApi(async (api) => { + // tslint:disable-next-line: no-bitwise + const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1; + const Bob = privateKey('//Bob'); + + const tx = api.tx.nft.addToWhiteList(collectionId, Bob.address); + await expect(submitTransactionExpectFailAsync(Alice, tx)).to.be.rejected; + }); + }); + + it('White list an address in the collection that was destroyed', async () => { + await usingApi(async (api) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + // tslint:disable-next-line: no-bitwise + const collectionId = await createCollectionExpectSuccess(); + await destroyCollectionExpectSuccess(collectionId); + const tx = api.tx.nft.addToWhiteList(collectionId, Bob.address); + await expect(submitTransactionExpectFailAsync(Alice, tx)).to.be.rejected; + }); + }); + + it('White list an address in the collection that does not have white list access enabled', async () => { + await usingApi(async (api) => { + const Alice = privateKey('//Alice'); + const Ferdie = privateKey('//Ferdie'); + const collectionId = await createCollectionExpectSuccess(); + await enableWhiteListExpectSuccess(Alice, collectionId); + await enablePublicMintingExpectSuccess(Alice, collectionId); + const tx = api.tx.nft.createItem(collectionId, Ferdie.address, 'NFT'); + await expect(submitTransactionExpectFailAsync(Ferdie, tx)).to.be.rejected; + }); + }); + +}); --- /dev/null +++ b/tests/src/approve.test.ts @@ -0,0 +1,140 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// +import { ApiPromise } from '@polkadot/api'; +import BN from 'bn.js'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import privateKey from './substrate/privateKey'; +import { default as usingApi } from './substrate/substrate-api'; +import { + approveExpectFail, + approveExpectSuccess, + createCollectionExpectSuccess, + createItemExpectSuccess, + destroyCollectionExpectSuccess, +} from './util/helpers'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('Integration Test approve(spender, collection_id, item_id, amount):', () => { + it('Execute the extrinsic and check approvedList', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const nftCollectionId = await createCollectionExpectSuccess(); + // nft + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob); + }); + }); + + it('Remove approval by using 0 amount', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const nftCollectionId = await createCollectionExpectSuccess(); + // nft + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 0); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 0); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 1); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 0); + }); + }); +}); + +describe('Negative Integration Test approve(spender, collection_id, item_id, amount):', () => { + it('Approve for a collection that does not exist', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + // nft + const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob); + // fungible + const fungibleCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await approveExpectFail(fungibleCollectionCount + 1, 1, Alice, Bob); + // reFungible + const reFungibleCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await approveExpectFail(reFungibleCollectionCount + 1, 1, Alice, Bob); + }); + }); + + it('Approve for a collection that was destroyed', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + await destroyCollectionExpectSuccess(nftCollectionId); + await approveExpectFail(nftCollectionId, 1, Alice, Bob); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await destroyCollectionExpectSuccess(fungibleCollectionId); + await approveExpectFail(fungibleCollectionId, 1, Alice, Bob); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + await destroyCollectionExpectSuccess(reFungibleCollectionId); + await approveExpectFail(reFungibleCollectionId, 1, Alice, Bob); + }); + }); + + it('Approve transfer of a token that does not exist', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + await approveExpectFail(nftCollectionId, 2, Alice, Bob); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await approveExpectFail(fungibleCollectionId, 2, Alice, Bob); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + await approveExpectFail(reFungibleCollectionId, 2, Alice, Bob); + }); + }); + + it('Approve using the address that does not own the approved token', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const nftCollectionId = await createCollectionExpectSuccess(); + // nft + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectFail(fungibleCollectionId, newFungibleTokenId, Bob, Alice); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice); + }); + }); +}); --- /dev/null +++ b/tests/src/burnItem.test.ts @@ -0,0 +1,237 @@ +import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from './substrate/substrate-api'; +import { Keyring } from "@polkadot/api"; +import { IKeyringPair } from "@polkadot/types/types"; +import { + createCollectionExpectSuccess, + createItemExpectSuccess, + getGenericResult, + destroyCollectionExpectSuccess +} from './util/helpers'; +import { nullPublicKey } from "./accounts"; + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised); +const expect = chai.expect; + +let alice: IKeyringPair; +let bob: IKeyringPair; + +describe('integration test: ext. burnItem():', () => { + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + alice = keyring.addFromUri(`//Alice`); + bob = keyring.addFromUri(`//Bob`); + }); + }); + + it('Burn item in NFT collection', async () => { + const createMode = 'NFT'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode}}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); + + await usingApi(async (api) => { + const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const events = await submitTransactionAsync(alice, tx); + const result = getGenericResult(events); + // Get the item + const item: any = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON(); + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + // tslint:disable-next-line:no-unused-expression + expect(item).to.be.not.null; + expect(item.Owner).to.be.equal(nullPublicKey); + }); + + }); + it('Burn item in Fungible collection', async () => { + const createMode = 'Fungible'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0 }}); + await createItemExpectSuccess(alice, collectionId, createMode); // Helper creates 10 fungible tokens + const tokenId = 0; // ignored + + await usingApi(async (api) => { + // Destroy 1 of 10 + const tx = api.tx.nft.burnItem(collectionId, tokenId, 1); + const events = await submitTransactionAsync(alice, tx); + const result = getGenericResult(events); + + // Get alice balance + const balance: any = (await api.query.nft.fungibleItemList(collectionId, alice.address)).toJSON(); + + // What to expect + expect(result.success).to.be.true; + expect(balance).to.be.not.null; + expect(balance.Value).to.be.equal(9); + }); + + }); + it('Burn item in ReFungible collection', async () => { + const createMode = 'ReFungible'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 2 }}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); + + await usingApi(async (api) => { + const tx = api.tx.nft.burnItem(collectionId, tokenId, 1); + const events = await submitTransactionAsync(alice, tx); + const result = getGenericResult(events); + + // Get alice balance + const balance: any = (await api.query.nft.reFungibleItemList(collectionId, tokenId)).toJSON(); + + // What to expect + expect(result.success).to.be.true; + expect(balance).to.be.not.null; + expect(balance.Owner.length).to.be.equal(0); + }); + + }); + + it('Burn owned portion of item in ReFungible collection', async () => { + const createMode = 'ReFungible'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 2 }}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); + + await usingApi(async (api) => { + // Transfer 1/100 of the token to Bob + const transfertx = api.tx.nft.transfer(bob.address, collectionId, tokenId, 1); + const events1 = await submitTransactionAsync(alice, transfertx); + const result1 = getGenericResult(events1); + + // Get balances + const balanceBefore: any = (await api.query.nft.reFungibleItemList(collectionId, tokenId)).toJSON(); + + // Bob burns his portion + const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const events2 = await submitTransactionAsync(bob, tx); + const result2 = getGenericResult(events2); + + // Get balances + const balance: any = (await api.query.nft.reFungibleItemList(collectionId, tokenId)).toJSON(); + // console.log(balance); + + // What to expect before burning + expect(result1.success).to.be.true; + expect(balanceBefore).to.be.not.null; + expect(balanceBefore.Owner.length).to.be.equal(2); + expect(balanceBefore.Owner[0].Owner).to.be.equal(alice.address); + expect(balanceBefore.Owner[0].Fraction).to.be.equal(99); + expect(balanceBefore.Owner[1].Owner).to.be.equal(bob.address); + expect(balanceBefore.Owner[1].Fraction).to.be.equal(1); + + // What to expect after burning + expect(result2.success).to.be.true; + expect(balance).to.be.not.null; + expect(balance.Owner.length).to.be.equal(1); + expect(balance.Owner[0].Fraction).to.be.equal(99); + expect(balance.Owner[0].Owner).to.be.equal(alice.address); + }); + + }); + +}); + +describe('Negative integration test: ext. burnItem():', () => { + before(async () => { + await usingApi(async (api) => { + const keyring = new Keyring({ type: 'sr25519' }); + alice = keyring.addFromUri(`//Alice`); + bob = keyring.addFromUri(`//Bob`); + }); + }); + + it('Burn a token in a destroyed collection', async () => { + const createMode = 'NFT'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode }}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); + await destroyCollectionExpectSuccess(collectionId); + + await usingApi(async (api) => { + const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const badTransaction = async function () { + await submitTransactionExpectFailAsync(alice, tx); + }; + await expect(badTransaction()).to.be.rejected; + }); + + }); + + it('Burn a token that was never created', async () => { + const createMode = 'NFT'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode }}); + const tokenId = 10; + + await usingApi(async (api) => { + const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const badTransaction = async function () { + await submitTransactionExpectFailAsync(alice, tx); + }; + await expect(badTransaction()).to.be.rejected; + }); + + }); + + it('Burn a token using the address that does not own it', async () => { + const createMode = 'NFT'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode }}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); + + await usingApi(async (api) => { + const tx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const badTransaction = async function () { + await submitTransactionExpectFailAsync(bob, tx); + }; + await expect(badTransaction()).to.be.rejected; + }); + + }); + + it('Transfer a burned a token', async () => { + const createMode = 'NFT'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode }}); + const tokenId = await createItemExpectSuccess(alice, collectionId, createMode); + + await usingApi(async (api) => { + + const burntx = api.tx.nft.burnItem(collectionId, tokenId, 0); + const events1 = await submitTransactionAsync(alice, burntx); + const result1 = getGenericResult(events1); + expect(result1.success).to.be.true; + + const tx = api.tx.nft.transfer(bob.address, collectionId, tokenId, 0); + const badTransaction = async function () { + await submitTransactionExpectFailAsync(alice, tx); + }; + await expect(badTransaction()).to.be.rejected; + }); + + }); + + it('Burn more than owned in Fungible collection', async () => { + const createMode = 'Fungible'; + const collectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0 }}); + // Helper creates 10 fungible tokens + await createItemExpectSuccess(alice, collectionId, createMode); + const tokenId = 0; // ignored + + await usingApi(async (api) => { + // Destroy 11 of 10 + const tx = api.tx.nft.burnItem(collectionId, tokenId, 11); + const badTransaction = async function () { + await submitTransactionExpectFailAsync(alice, tx); + }; + await expect(badTransaction()).to.be.rejected; + + // Get alice balance + const balance: any = (await api.query.nft.fungibleItemList(collectionId, alice.address)).toJSON(); + + // What to expect + expect(balance).to.be.not.null; + expect(balance.Value).to.be.equal(10); + }); + + }); + +}); --- a/tests/src/confirmSponsorship.test.ts +++ b/tests/src/confirmSponsorship.test.ts @@ -89,7 +89,7 @@ }); it('Fungible: Transfer fees are paid by the sponsor after confirmation', async () => { - const collectionId = await createCollectionExpectSuccess({mode: 'Fungible'}); + const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0 }}); await setCollectionSponsorExpectSuccess(collectionId, bob.address); await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); @@ -115,7 +115,7 @@ }); it('ReFungible: Transfer fees are paid by the sponsor after confirmation', async () => { - const collectionId = await createCollectionExpectSuccess({mode: 'ReFungible'}); + const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0 }}); await setCollectionSponsorExpectSuccess(collectionId, bob.address); await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); @@ -170,7 +170,7 @@ }); }); - it('NFT: Sponsoring is rate limited', async () => { + it('NFT: Sponsoring of transfers is rate limited', async () => { const collectionId = await createCollectionExpectSuccess(); await setCollectionSponsorExpectSuccess(collectionId, bob.address); await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); @@ -210,7 +210,7 @@ }); it('Fungible: Sponsoring is rate limited', async () => { - const collectionId = await createCollectionExpectSuccess({mode: 'Fungible'}); + const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0 }}); await setCollectionSponsorExpectSuccess(collectionId, bob.address); await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); @@ -247,7 +247,7 @@ }); it('ReFungible: Sponsoring is rate limited', async () => { - const collectionId = await createCollectionExpectSuccess({mode: 'ReFungible'}); + const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0 }}); await setCollectionSponsorExpectSuccess(collectionId, bob.address); await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); @@ -285,6 +285,51 @@ }); }); + it('NFT: Sponsoring of createItem is rate limited', 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); + + await usingApi(async (api) => { + // 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 - gets sponsored + await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address); + + // Second mint should fail + const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString()); + + const consoleError = console.error; + const consoleLog = console.log; + console.error = () => {}; + console.log = () => {}; + const badTransaction = async function () { + await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address); + }; + await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees"); + console.error = consoleError; + console.log = consoleLog; + 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); + await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address); + + expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true; + }); + }); + }); describe('(!negative test!) integration test: ext. removeCollectionSponsor():', () => { --- a/tests/src/contracts.test.ts +++ b/tests/src/contracts.test.ts @@ -2,95 +2,20 @@ import chaiAsPromised from 'chai-as-promised'; import usingApi, { submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; import fs from "fs"; -import { Abi, BlueprintPromise as Blueprint, CodePromise, ContractPromise as Contract } from "@polkadot/api-contract"; -import { IKeyringPair } from "@polkadot/types/types"; -import { ApiPromise, Keyring } from "@polkadot/api"; -import { ApiTypes, SubmittableExtrinsic } from "@polkadot/api/types"; +import { Abi, ContractPromise as Contract } from "@polkadot/api-contract"; import privateKey from "./substrate/privateKey"; +import { + deployFlipper, + getFlipValue +} from "./util/contracthelpers"; chai.use(chaiAsPromised); const expect = chai.expect; -import { BigNumber } from 'bignumber.js'; -import { findUnusedAddress } from './util/helpers'; const value = 0; const gasLimit = 3000n * 1000000n; -const endowment = `1000000000000000`; const marketContractAddress = '5CYN9j3YvRkqxewoxeSvRbhAym4465C57uMmX5j4yz99L5H6'; - -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 endowment = 1000000000000000n; - const initValue = true; - - const unsub = await blueprint.tx - .new(endowment, gasLimit, initValue) - .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; -} - -export async function deployFlipper(api: ApiPromise): Promise<[Contract, IKeyringPair]> { - const metadata = JSON.parse(fs.readFileSync('./src/flipper/metadata.json').toString('utf-8')); - const abi = new Abi(metadata); - - const deployer = await prepareDeployer(api); - - const wasm = fs.readFileSync('./src/flipper/flipper.wasm'); - - const code = new CodePromise(api, abi, wasm); - - const blueprint = await deployBlueprint(deployer, code); - const contract = (await deployContract(deployer, blueprint))['contract'] as Contract; - - const initialGetResponse = await getFlipValue(contract, deployer); - expect(initialGetResponse).to.be.true; - - return [contract, deployer]; -} - -async function getFlipValue(contract: Contract, deployer: IKeyringPair) { - const result = await contract.query.get(deployer.address, value, gasLimit); - - if(!result.result.isSuccess) { - throw `Failed to get flipper value`; - } - return (result.result.asSuccess.data[0] == 0x00) ? false : true; -} - describe('Contracts', () => { it(`Can deploy smart contract Flipper, instantiate it and call it's get and flip messages.`, async () => { await usingApi(async api => { @@ -103,68 +28,6 @@ const afterFlipGetResponse = await getFlipValue(contract, deployer); expect(afterFlipGetResponse).not.to.be.eq(initialGetResponse, 'Flipping should change value.'); - }); - }); - - it(`Whitelisted account can call contract.`, async () => { - await usingApi(async api => { - const bob = privateKey("//Bob"); - - const [contract, deployer] = await deployFlipper(api); - - let expectedFlipValue = await getFlipValue(contract, deployer); - - const flip = contract.exec('flip', value, gasLimit); - await submitTransactionAsync(bob, flip); - expectedFlipValue = !expectedFlipValue; - const afterFlip = await getFlipValue(contract,deployer); - expect(afterFlip).to.be.eq(expectedFlipValue, `Anyone can call new contract.`); - - const deployerCanFlip = async () => { - expectedFlipValue = !expectedFlipValue; - const deployerFlip = contract.exec('flip', value, gasLimit); - await submitTransactionAsync(deployer, deployerFlip); - const aliceFlip1Response = await getFlipValue(contract, deployer); - expect(aliceFlip1Response).to.be.eq(expectedFlipValue, `Deployer always can flip.`); - }; - await deployerCanFlip(); - - const enableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, true); - const enableResult = await submitTransactionAsync(deployer, enableWhiteListTx); - const flipWithEnabledWhiteList = contract.exec('flip', value, gasLimit); - await expect(submitTransactionExpectFailAsync(bob, flipWithEnabledWhiteList)).to.be.rejected; - const flipValueAfterEnableWhiteList = await getFlipValue(contract, deployer); - expect(flipValueAfterEnableWhiteList).to.be.eq(expectedFlipValue, `Enabling whitelist doesn't make it possible to call contract for everyone.`); - - await deployerCanFlip(); - - const addBobToWhiteListTx = api.tx.nft.addToContractWhiteList(contract.address, bob.address); - const addBobResult = await submitTransactionAsync(deployer, addBobToWhiteListTx); - const flipWithWhitelistedBob = contract.exec('flip', value, gasLimit); - await submitTransactionAsync(bob, flipWithWhitelistedBob); - expectedFlipValue = !expectedFlipValue; - const flipAfterWhiteListed = await getFlipValue(contract,deployer); - expect(flipAfterWhiteListed).to.be.eq(expectedFlipValue, `Bob was whitelisted, now he can flip.`); - - await deployerCanFlip(); - - const removeBobFromWhiteListTx = api.tx.nft.removeFromContractWhiteList(contract.address, bob.address); - const removeBobResult = await submitTransactionAsync(deployer, removeBobFromWhiteListTx); - const bobRemoved = contract.exec('flip', value, gasLimit); - await expect(submitTransactionExpectFailAsync(bob, bobRemoved)).to.be.rejected; - const afterBobRemoved = await getFlipValue(contract, deployer); - expect(afterBobRemoved).to.be.eq(expectedFlipValue, `Bob can't call contract, now when he is removeed from white list.`); - - await deployerCanFlip(); - - const disableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, false); - const disableWhiteListResult = await submitTransactionAsync(deployer, disableWhiteListTx); - const whiteListDisabledFlip = contract.exec('flip', value, gasLimit); - await submitTransactionAsync(bob, whiteListDisabledFlip); - expectedFlipValue = !expectedFlipValue; - const afterWhiteListDisabled = await getFlipValue(contract,deployer); - expect(afterWhiteListDisabled).to.be.eq(expectedFlipValue, `Anyone can call contract with disabled whitelist.`); - }); }); --- a/tests/src/createCollection.test.ts +++ b/tests/src/createCollection.test.ts @@ -5,15 +5,15 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { default as usingApi } from "./substrate/substrate-api"; -import { createCollectionExpectSuccess, createCollectionExpectFailure, CollectionMode } from "./util/helpers"; +import { default as usingApi } from './substrate/substrate-api'; +import { createCollectionExpectFailure, createCollectionExpectSuccess } from './util/helpers'; chai.use(chaiAsPromised); const expect = chai.expect; describe('integration test: ext. createCollection():', () => { it('Create new NFT collection', async () => { - await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: 'NFT'}); + await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}}); }); it('Create new NFT collection whith collection_name of maximum length (64 bytes)', async () => { await createCollectionExpectSuccess({name: 'A'.repeat(64)}); @@ -25,34 +25,35 @@ await createCollectionExpectSuccess({tokenPrefix: 'A'.repeat(16)}); }); it('Create new Fungible collection', async () => { - await createCollectionExpectSuccess({mode: 'Fungible'}); + await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); }); it('Create new ReFungible collection', async () => { - await createCollectionExpectSuccess({mode: 'ReFungible'}); + await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); }); }); describe('(!negative test!) integration test: ext. createCollection():', () => { it('(!negative test!) create new NFT collection whith incorrect data (mode)', async () => { await usingApi(async (api) => { - const AcollectionCount = parseInt((await api.query.nft.collectionCount()).toString()); + const AcollectionCount = parseInt((await api.query.nft.collectionCount()).toString(), 10); - const badTransaction = async function () { - await createCollectionExpectSuccess({mode: 'BadMode' as CollectionMode}); + const badTransaction = async () => { + await createCollectionExpectSuccess({mode: {type: 'Invalid'}}); }; + // tslint:disable-next-line:no-unused-expression expect(badTransaction()).to.be.rejected; - const BcollectionCount = parseInt((await api.query.nft.collectionCount()).toString()); + const BcollectionCount = parseInt((await api.query.nft.collectionCount()).toString(), 10); expect(BcollectionCount).to.be.equal(AcollectionCount, 'Error: Incorrect collection created.'); }); }); it('(!negative test!) create new NFT collection whith incorrect data (collection_name)', async () => { - await createCollectionExpectFailure({name: 'A'.repeat(65)}); + await createCollectionExpectFailure({ name: 'A'.repeat(65), mode: {type: 'NFT'}}); }); it('(!negative test!) create new NFT collection whith incorrect data (collection_description)', async () => { - await createCollectionExpectFailure({description: 'A'.repeat(257)}); + await createCollectionExpectFailure({ description: 'A'.repeat(257), mode: { type: 'NFT' }}); }); it('(!negative test!) create new NFT collection whith incorrect data (token_prefix)', async () => { - await createCollectionExpectFailure({tokenPrefix: 'A'.repeat(17)}); + await createCollectionExpectFailure({tokenPrefix: 'A'.repeat(17), mode: {type: 'NFT'}}); }); }); --- a/tests/src/createItem.test.ts +++ b/tests/src/createItem.test.ts @@ -18,17 +18,17 @@ it('Create new item in NFT collection', async () => { const createMode = 'NFT'; - const newCollectionID = await createCollectionExpectSuccess({mode: createMode}); + const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode}}); await createItemExpectSuccess(alice, newCollectionID, createMode); }); it('Create new item in Fungible collection', async () => { const createMode = 'Fungible'; - const newCollectionID = await createCollectionExpectSuccess({mode: createMode}); + const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}}); await createItemExpectSuccess(alice, newCollectionID, createMode); }); it('Create new item in ReFungible collection', async () => { const createMode = 'ReFungible'; - const newCollectionID = await createCollectionExpectSuccess({mode: createMode}); + const newCollectionID = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}}); await createItemExpectSuccess(alice, newCollectionID, createMode); }); }); --- a/tests/src/destroyCollection.test.ts +++ b/tests/src/destroyCollection.test.ts @@ -14,11 +14,11 @@ await destroyCollectionExpectSuccess(collectionId); }); it('Fungible collection can be destroyed', async () => { - const collectionId = await createCollectionExpectSuccess({ mode: 'Fungible' }); + const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); await destroyCollectionExpectSuccess(collectionId); }); it('ReFungible collection can be destroyed', async () => { - const collectionId = await createCollectionExpectSuccess({ mode: 'ReFungible' }); + const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); await destroyCollectionExpectSuccess(collectionId); }); }); --- /dev/null +++ b/tests/src/removeCollectionAdmin.test.ts @@ -0,0 +1,82 @@ +import { ApiPromise } from '@polkadot/api'; +import BN from 'bn.js'; +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, destroyCollectionExpectSuccess} from './util/helpers'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('Integration Test removeCollectionAdmin(collection_id, account_id):', () => { + it('Remove collection admin.', async () => { + await usingApi(async (api: ApiPromise) => { + 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); + // first - add collection admin Bob + const addAdminTx = api.tx.nft.addCollectionAdmin(collectionId, Bob.address); + await submitTransactionAsync(Alice, addAdminTx); + + const adminListAfterAddAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListAfterAddAdmin).to.be.contains(Bob.address); + + // then remove bob from admins of collection + const removeAdminTx = api.tx.nft.removeCollectionAdmin(collectionId, Bob.address); + await submitTransactionAsync(Alice, removeAdminTx); + + const adminListAfterRemoveAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListAfterRemoveAdmin).not.to.be.contains(Bob.address); + }); + }); +}); + +describe('Negative Integration Test removeCollectionAdmin(collection_id, account_id):', () => { + it('Can\'t remove collection admin from not existing collection', async () => { + await usingApi(async (api: ApiPromise) => { + // tslint:disable-next-line: no-bitwise + const collectionId = (1 << 32) - 1; + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const changeOwnerTx = api.tx.nft.removeCollectionAdmin(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(); + }); + }); + + it('Can\'t remove collection admin from deleted collection', async () => { + await usingApi(async (api: ApiPromise) => { + // tslint:disable-next-line: no-bitwise + const collectionId = await createCollectionExpectSuccess(); + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + + await destroyCollectionExpectSuccess(collectionId); + + const changeOwnerTx = api.tx.nft.removeCollectionAdmin(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(); + }); + }); + + it('Remove admin from collection that has no admins', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const collectionId = await createCollectionExpectSuccess(); + + const adminListBeforeAddAdmin: any = (await api.query.nft.adminList(collectionId)); + expect(adminListBeforeAddAdmin).to.have.lengthOf(0); + + const tx = api.tx.nft.removeCollectionAdmin(collectionId, Alice.address); + await expect(submitTransactionExpectFailAsync(Alice, tx)).to.be.rejected; + }); + }); +}); --- a/tests/src/setCollectionLimits.test.ts +++ b/tests/src/setCollectionLimits.test.ts @@ -32,7 +32,7 @@ }); it('choose or create collection for testing', async () => { await usingApi(async () => { - collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: 'NFT'}); + collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}}); }); }); }); --- a/tests/src/setCollectionSponsor.test.ts +++ b/tests/src/setCollectionSponsor.test.ts @@ -30,11 +30,11 @@ await setCollectionSponsorExpectSuccess(collectionId, bob.address); }); it('Set Fungible collection sponsor', async () => { - const collectionId = await createCollectionExpectSuccess({ mode: 'Fungible' }); + const collectionId = await createCollectionExpectSuccess({ mode: {type: 'Fungible', decimalPoints: 0} }); await setCollectionSponsorExpectSuccess(collectionId, bob.address); }); it('Set ReFungible collection sponsor', async () => { - const collectionId = await createCollectionExpectSuccess({ mode: 'ReFungible' }); + const collectionId = await createCollectionExpectSuccess({ mode: {type: 'ReFungible', decimalPoints: 0} }); await setCollectionSponsorExpectSuccess(collectionId, bob.address); }); --- /dev/null +++ b/tests/src/setPublicAccessMode.test.ts @@ -0,0 +1,89 @@ +// https://unique-network.readthedocs.io/en/latest/jsapi.html#setschemaversion +import { ApiPromise, Keyring } from '@polkadot/api'; +import { IKeyringPair } from '@polkadot/types/types'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import privateKey from './substrate/privateKey'; +import usingApi, {submitTransactionExpectFailAsync} from './substrate/substrate-api'; +import { + addToWhiteListExpectSuccess, + createCollectionExpectSuccess, + createItemExpectSuccess, + destroyCollectionExpectSuccess, + enablePublicMintingExpectSuccess, + enableWhiteListExpectSuccess, +} from './util/helpers'; +import { utf16ToStr } from './util/util'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +let Alice: IKeyringPair; +let Bob: IKeyringPair; + +describe('Integration Test setPublicAccessMode(): ', () => { + before(async () => { + await usingApi(async (api) => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + }); + }); + + it('Run extrinsic with collection id parameters, set the whitelist mode for the collection', async () => { + await usingApi(async (api: ApiPromise) => { + const collectionId: number = await createCollectionExpectSuccess(); + await enableWhiteListExpectSuccess(Alice, collectionId); + await enablePublicMintingExpectSuccess(Alice, collectionId); + await addToWhiteListExpectSuccess(Alice, collectionId, Bob.address); + await createItemExpectSuccess(Bob, collectionId, 'NFT', Bob.address); + }); + }); + + it('Whitelisted collection limits', async () => { + await usingApi(async (api: ApiPromise) => { + const collectionId = await createCollectionExpectSuccess(); + await enableWhiteListExpectSuccess(Alice, collectionId); + await enablePublicMintingExpectSuccess(Alice, collectionId); + const tx = api.tx.nft.createItem(collectionId, Bob.address, 'NFT'); + await expect(submitTransactionExpectFailAsync(Bob, tx)).to.be.rejected; + }); + }); +}); + +describe('Negative Integration Test ext. setPublicAccessMode(): ', () => { + it('Set a non-existent collection', async () => { + await usingApi(async (api: ApiPromise) => { + // tslint:disable-next-line: radix + const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1; + const tx = api.tx.nft.setPublicAccessMode(collectionId, 'WhiteList'); + await expect(submitTransactionExpectFailAsync(Alice, tx)).to.be.rejected; + }); + }); + + it('Set the collection that has been deleted', async () => { + await usingApi(async (api: ApiPromise) => { + // tslint:disable-next-line: no-bitwise + const collectionId = await createCollectionExpectSuccess(); + await destroyCollectionExpectSuccess(collectionId); + const tx = api.tx.nft.setPublicAccessMode(collectionId, 'WhiteList'); + await expect(submitTransactionExpectFailAsync(Alice, tx)).to.be.rejected; + }); + }); + + it('Re-set the list mode already set in quantity', async () => { + await usingApi(async (api: ApiPromise) => { + const collectionId: number = await createCollectionExpectSuccess(); + await enableWhiteListExpectSuccess(Alice, collectionId); + await enableWhiteListExpectSuccess(Alice, collectionId); + }); + }); + + it('Execute method not on behalf of the collection owner', async () => { + await usingApi(async (api: ApiPromise) => { + // tslint:disable-next-line: no-bitwise + const collectionId = await createCollectionExpectSuccess(); + const tx = api.tx.nft.setPublicAccessMode(collectionId, 'WhiteList'); + await expect(submitTransactionExpectFailAsync(Bob, tx)).to.be.rejected; + }); + }); +}); --- a/tests/src/setSchemaVersion.test.ts +++ b/tests/src/setSchemaVersion.test.ts @@ -35,7 +35,7 @@ }); it('choose or create collection for testing', async () => { await usingApi(async () => { - collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: 'NFT'}); + collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}}); }); }); }); @@ -93,12 +93,6 @@ const nonExistedCollectionId = collectionCount + 1; tx = api.tx.nft.setSchemaVersion(nonExistedCollectionId, 'ImageURL'); await expect(submitTransactionExpectFailAsync(alice, tx)).to.be.rejected; - /*try { - await submitTransactionAsync(alice, tx); - } catch (e) { - // tslint:disable-next-line:no-unused-expression - expect(e).to.be.exist; - }*/ }); }); @@ -119,13 +113,6 @@ await destroyCollectionExpectSuccess(collectionIdForTesting); tx = api.tx.nft.setSchemaVersion(collectionIdForTesting, 'ImageURL'); await expect(submitTransactionExpectFailAsync(alice, tx)).to.be.rejected; - /*try { - tx = api.tx.nft.setSchemaVersion(collectionIdForTesting, 'ImageURL'); - await submitTransactionAsync(alice, tx); - } catch (e) { - // tslint:disable-next-line:no-unused-expression - expect(e).to.be.exist; - }*/ }); }); }); --- a/tests/src/substrate/get-balance.ts +++ b/tests/src/substrate/get-balance.ts @@ -3,12 +3,12 @@ // file 'LICENSE', which is part of this source code package. // -import { ApiPromise } from "@polkadot/api"; -import promisifySubstrate from "./promisify-substrate"; -import {AccountInfo} from "@polkadot/types/interfaces/system"; +import { ApiPromise } from '@polkadot/api'; +import {AccountInfo} from '@polkadot/types/interfaces/system'; +import promisifySubstrate from './promisify-substrate'; -export default async function getBalance(api: ApiPromise, accounts: string[]): Promise { - const balance = promisifySubstrate(api, (accounts: string[]) => api.query.system.account.multi(accounts)); +export default async function getBalance(api: ApiPromise, accounts: string[]): Promise> { + const balance = promisifySubstrate(api, (acc: string[]) => api.query.system.account.multi(acc)); const responce = await balance(accounts) as unknown as AccountInfo[]; - return responce.map(r => r.data.free.toBigInt().valueOf()); + return responce.map((r) => r.data.free.toBigInt().valueOf()); } --- a/tests/src/substrate/substrate-api.ts +++ b/tests/src/substrate/substrate-api.ts @@ -58,21 +58,22 @@ return TransactionStatus.Fail; } -export function submitTransactionAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic): Promise { - return new Promise(async function(resolve, reject) { +export function +submitTransactionAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic): Promise { + return new Promise(async (resolve, reject) => { try { await transaction.signAndSend(sender, ({ events = [], status }) => { const transactionStatus = getTransactionStatus(events, status); - if (transactionStatus == TransactionStatus.Success) { + if (transactionStatus === TransactionStatus.Success) { resolve(events); - } else if (transactionStatus == TransactionStatus.Fail) { + } else if (transactionStatus === TransactionStatus.Fail) { console.log(`Something went wrong with transaction. Status: ${status}`); reject(events); } }); } catch (e) { - console.log("Error: ", e); + console.log('Error: ', e); reject(e); } }); @@ -90,7 +91,6 @@ res(rec); console.error = consoleError; console.log = consoleLog; - }); }; const reject = (errror: any) => { @@ -104,6 +104,8 @@ await transaction.signAndSend(sender, ({ events = [], status }) => { const transactionStatus = getTransactionStatus(events, status); + console.log('transactionStatus', transactionStatus, 'events', events); + if (transactionStatus == TransactionStatus.Success) { resolve(events); } else if (transactionStatus == TransactionStatus.Fail) { @@ -114,4 +116,4 @@ reject(e); } }); -} \ No newline at end of file +} --- /dev/null +++ b/tests/src/toggleContractWhiteList.test.ts @@ -0,0 +1,151 @@ +import chai from "chai"; +import chaiAsPromised from 'chai-as-promised'; +import usingApi, { submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api"; +import privateKey from "./substrate/privateKey"; +import { + deployFlipper, + getFlipValue +} from "./util/contracthelpers"; +import { + getGenericResult +} from "./util/helpers" + +chai.use(chaiAsPromised); +const expect = chai.expect; + +const value = 0; +const gasLimit = 3000n * 1000000n; + +describe('Integration Test toggleContractWhiteList', () => { + + it(`Enable white list contract mode`, async () => { + await usingApi(async api => { + const [contract, deployer] = await deployFlipper(api); + + const enabledBefore = (await api.query.nft.contractWhiteListEnabled(contract.address)).toJSON(); + const enableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, true); + const enableEvents = await submitTransactionAsync(deployer, enableWhiteListTx); + const enabled = (await api.query.nft.contractWhiteListEnabled(contract.address)).toJSON(); + + expect(getGenericResult(enableEvents).success).to.be.true; + expect(enabledBefore).to.be.false; + expect(enabled).to.be.true; + }); + }); + + it(`Only whitelisted account can call contract`, async () => { + await usingApi(async api => { + const bob = privateKey("//Bob"); + + const [contract, deployer] = await deployFlipper(api); + + let flipValueBefore = await getFlipValue(contract, deployer); + const flip = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(bob, flip); + const flipValueAfter = await getFlipValue(contract,deployer); + expect(flipValueAfter).to.be.eq(!flipValueBefore, `Anyone can call new contract.`); + + const deployerCanFlip = async () => { + let flipValueBefore = await getFlipValue(contract, deployer); + const deployerFlip = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(deployer, deployerFlip); + const aliceFlip1Response = await getFlipValue(contract, deployer); + expect(aliceFlip1Response).to.be.eq(!flipValueBefore, `Deployer always can flip.`); + }; + await deployerCanFlip(); + + flipValueBefore = await getFlipValue(contract, deployer); + const enableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, true); + const enableResult = await submitTransactionAsync(deployer, enableWhiteListTx); + const flipWithEnabledWhiteList = contract.exec('flip', value, gasLimit); + await expect(submitTransactionExpectFailAsync(bob, flipWithEnabledWhiteList)).to.be.rejected; + const flipValueAfterEnableWhiteList = await getFlipValue(contract, deployer); + expect(flipValueAfterEnableWhiteList).to.be.eq(flipValueBefore, `Enabling whitelist doesn't make it possible to call contract for everyone.`); + + await deployerCanFlip(); + + flipValueBefore = await getFlipValue(contract, deployer); + const addBobToWhiteListTx = api.tx.nft.addToContractWhiteList(contract.address, bob.address); + const addBobResult = await submitTransactionAsync(deployer, addBobToWhiteListTx); + const flipWithWhitelistedBob = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(bob, flipWithWhitelistedBob); + const flipAfterWhiteListed = await getFlipValue(contract,deployer); + expect(flipAfterWhiteListed).to.be.eq(!flipValueBefore, `Bob was whitelisted, now he can flip.`); + + await deployerCanFlip(); + + flipValueBefore = await getFlipValue(contract, deployer); + const removeBobFromWhiteListTx = api.tx.nft.removeFromContractWhiteList(contract.address, bob.address); + const removeBobResult = await submitTransactionAsync(deployer, removeBobFromWhiteListTx); + const bobRemoved = contract.exec('flip', value, gasLimit); + await expect(submitTransactionExpectFailAsync(bob, bobRemoved)).to.be.rejected; + const afterBobRemoved = await getFlipValue(contract, deployer); + expect(afterBobRemoved).to.be.eq(flipValueBefore, `Bob can't call contract, now when he is removeed from white list.`); + + await deployerCanFlip(); + + flipValueBefore = await getFlipValue(contract, deployer); + const disableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, false); + const disableWhiteListResult = await submitTransactionAsync(deployer, disableWhiteListTx); + const whiteListDisabledFlip = contract.exec('flip', value, gasLimit); + await submitTransactionAsync(bob, whiteListDisabledFlip); + const afterWhiteListDisabled = await getFlipValue(contract,deployer); + expect(afterWhiteListDisabled).to.be.eq(!flipValueBefore, `Anyone can call contract with disabled whitelist.`); + + }); + }); + + it(`Enabling white list repeatedly should not produce errors`, async () => { + await usingApi(async api => { + const [contract, deployer] = await deployFlipper(api); + + const enabledBefore = (await api.query.nft.contractWhiteListEnabled(contract.address)).toJSON(); + const enableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, true); + const enableEvents = await submitTransactionAsync(deployer, enableWhiteListTx); + const enabled = (await api.query.nft.contractWhiteListEnabled(contract.address)).toJSON(); + const enableAgainEvents = await submitTransactionAsync(deployer, enableWhiteListTx); + const enabledAgain = (await api.query.nft.contractWhiteListEnabled(contract.address)).toJSON(); + + expect(getGenericResult(enableEvents).success).to.be.true; + expect(enabledBefore).to.be.false; + expect(enabled).to.be.true; + expect(getGenericResult(enableAgainEvents).success).to.be.true; + expect(enabledAgain).to.be.true; + }); + }); + +}); + +describe('Negative Integration Test toggleContractWhiteList', () => { + + it(`Enable white list for a non-contract`, async () => { + await usingApi(async api => { + const alice = privateKey("//Alice"); + const bobGuineaPig = privateKey("//Bob"); + + const enabledBefore = (await api.query.nft.contractWhiteListEnabled(bobGuineaPig.address)).toJSON(); + const enableWhiteListTx = api.tx.nft.toggleContractWhiteList(bobGuineaPig.address, true); + await expect(submitTransactionExpectFailAsync(alice, enableWhiteListTx)).to.be.rejected; + const enabled = (await api.query.nft.contractWhiteListEnabled(bobGuineaPig.address)).toJSON(); + + expect(enabledBefore).to.be.false; + expect(enabled).to.be.false; + }); + }); + + it(`Enable white list using a non-owner address`, async () => { + await usingApi(async api => { + const bob = privateKey("//Bob"); + const [contract, deployer] = await deployFlipper(api); + + const enabledBefore = (await api.query.nft.contractWhiteListEnabled(contract.address)).toJSON(); + const enableWhiteListTx = api.tx.nft.toggleContractWhiteList(contract.address, true); + await expect(submitTransactionExpectFailAsync(bob, enableWhiteListTx)).to.be.rejected; + const enabled = (await api.query.nft.contractWhiteListEnabled(contract.address)).toJSON(); + + expect(enabledBefore).to.be.false; + expect(enabled).to.be.false; + }); + }); + +}); --- a/tests/src/transfer.test.ts +++ b/tests/src/transfer.test.ts @@ -3,49 +3,174 @@ // file 'LICENSE', which is part of this source code package. // -import { expect, assert } from "chai"; -import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; -import { alicesPublicKey, bobsPublicKey, ferdiesPublicKey } from "./accounts"; -import privateKey from "./substrate/privateKey"; -import getBalance from "./substrate/get-balance"; -import { BigNumber } from 'bignumber.js'; -import { findUnusedAddress } from './util/helpers' +import { ApiPromise } from '@polkadot/api'; +import { IKeyringPair } from '@polkadot/types/types'; +import { expect } from 'chai'; +import { alicesPublicKey, bobsPublicKey } from './accounts'; +import getBalance from './substrate/get-balance'; +import privateKey from './substrate/privateKey'; +import { default as usingApi, submitTransactionAsync } from './substrate/substrate-api'; +import { + burnItemExpectSuccess, createCollectionExpectSuccess, createItemExpectSuccess, + destroyCollectionExpectSuccess, + findUnusedAddress, + getCreateCollectionResult, + getCreateItemResult, + transferExpectFail, + transferExpectSuccess, +} from './util/helpers'; -describe('Transfer', () => { - it('Balance transfers', async () => { - await usingApi(async api => { +let Alice: IKeyringPair; +let Bob: IKeyringPair; +let Charlie: IKeyringPair; + +describe('Integration Test Transfer(recipient, collection_id, item_id, value)', () => { + it('Balance transfers and check balance', async () => { + await usingApi(async (api: ApiPromise) => { const [alicesBalanceBefore, bobsBalanceBefore] = await getBalance(api, [alicesPublicKey, bobsPublicKey]); const alicePrivateKey = privateKey('//Alice'); - + const transfer = api.tx.balances.transfer(bobsPublicKey, 1n); - const result = await submitTransactionAsync(alicePrivateKey, transfer); + const events = await submitTransactionAsync(alicePrivateKey, transfer); + const result = getCreateItemResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; const [alicesBalanceAfter, bobsBalanceAfter] = await getBalance(api, [alicesPublicKey, bobsPublicKey]); + // tslint:disable-next-line:no-unused-expression expect(alicesBalanceAfter < alicesBalanceBefore).to.be.true; + // tslint:disable-next-line:no-unused-expression expect(bobsBalanceAfter > bobsBalanceBefore).to.be.true; }); }); it('Inability to pay fees error message is correct', async () => { - await usingApi(async api => { + await usingApi(async (api) => { // Find unused address const pk = await findUnusedAddress(api); - const error = console.error; - const log = console.log; - console.log = function () {}; - console.error = function () {}; - const badTransfer = api.tx.balances.transfer(bobsPublicKey, 1n); - const badTransaction = async function () { - const result = await submitTransactionAsync(pk, badTransfer); + // const events = await submitTransactionAsync(pk, badTransfer); + const badTransaction = async () => { + const events = await submitTransactionAsync(pk, badTransfer); + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; }; - await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees"); + expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees , e.g. account balance too low'); + }); + }); - console.log = log; - console.error = error; + it('Create collection, balance transfers and check balance', async () => { + await usingApi(async (api) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT'); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await transferExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1, 'Fungible'); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await transferExpectSuccess(reFungibleCollectionId, + newReFungibleTokenId, Alice, Bob, 1, 'ReFungible'); }); }); }); + +describe('Negative Integration Test Transfer(recipient, collection_id, item_id, value)', () => { + before(async () => { + await usingApi(async (api: ApiPromise) => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + Charlie = privateKey('//Charlie'); + }); + }); + it('Transfer with not existed collection_id', async () => { + await usingApi(async (api) => { + // nft + const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await transferExpectFail(nftCollectionCount + 1, 1, Alice, Bob, 1); + // fungible + const fungibleCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await transferExpectFail(fungibleCollectionCount + 1, 1, Alice, Bob, 1); + // reFungible + const reFungibleCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await transferExpectFail(reFungibleCollectionCount + 1, 1, Alice, Bob, 1); + }); + }); + it('Transfer with deleted collection_id', async () => { + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await destroyCollectionExpectSuccess(nftCollectionId); + await transferExpectFail(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT'); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await destroyCollectionExpectSuccess(fungibleCollectionId); + await transferExpectFail(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1, 'Fungible'); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await destroyCollectionExpectSuccess(reFungibleCollectionId); + await transferExpectFail(reFungibleCollectionId, + newReFungibleTokenId, Alice, Bob, 1, 'ReFungible'); + }); + it('Transfer with not existed item_id', async () => { + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + await transferExpectFail(nftCollectionId, 2, Alice, Bob, 1, 'NFT'); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await transferExpectFail(fungibleCollectionId, 2, Alice, Bob, 1, 'Fungible'); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + await transferExpectFail(reFungibleCollectionId, + 2, Alice, Bob, 1, 'ReFungible'); + }); + it('Transfer with deleted item_id', async () => { + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await burnItemExpectSuccess(Alice, nftCollectionId, newNftTokenId, 1); + await transferExpectFail(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT'); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await burnItemExpectSuccess(Alice, fungibleCollectionId, newFungibleTokenId, 10); + await transferExpectFail(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1, 'Fungible'); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 1); + await transferExpectFail(reFungibleCollectionId, + newReFungibleTokenId, Alice, Bob, 1, 'ReFungible'); + }); + it('Transfer with recipient that is not owner', async () => { + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await transferExpectFail(nftCollectionId, newNftTokenId, Charlie, Bob, 1, 'NFT'); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await transferExpectFail(fungibleCollectionId, newFungibleTokenId, Charlie, Bob, 1, 'Fungible'); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await transferExpectFail(reFungibleCollectionId, + newReFungibleTokenId, Charlie, Bob, 1, 'ReFungible'); + }); +}); --- /dev/null +++ b/tests/src/transferFrom.test.ts @@ -0,0 +1,188 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// +import { ApiPromise } from '@polkadot/api'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import privateKey from './substrate/privateKey'; +import { default as usingApi } from './substrate/substrate-api'; +import { + approveExpectFail, + approveExpectSuccess, + createCollectionExpectSuccess, + createItemExpectSuccess, + destroyCollectionExpectSuccess, + transferFromExpectFail, + transferFromExpectSuccess, +} from './util/helpers'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => { + it('Execute the extrinsic and check nftItemList - owner of token', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const Charlie = privateKey('//CHARLIE'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob); + + await transferFromExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice, Charlie, 1, 'NFT'); + + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob); + await transferFromExpectSuccess(fungibleCollectionId, newFungibleTokenId, Bob, Alice, Charlie, 1, 'Fungible'); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob); + await transferFromExpectSuccess(reFungibleCollectionId, + newReFungibleTokenId, Bob, Alice, Charlie, 1, 'ReFungible'); + }); + }); +}); + +describe('Negative Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => { + it('transferFrom for a collection that does not exist', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const Charlie = privateKey('//CHARLIE'); + // nft + const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob); + + await transferFromExpectFail(nftCollectionCount + 1, 1, Bob, Alice, Charlie, 1); + + // fungible + const fungibleCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await approveExpectFail(fungibleCollectionCount + 1, 1, Alice, Bob); + + await transferFromExpectFail(fungibleCollectionCount + 1, 1, Bob, Alice, Charlie, 1); + // reFungible + const reFungibleCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; + await approveExpectFail(reFungibleCollectionCount + 1, 1, Alice, Bob); + + await transferFromExpectFail(reFungibleCollectionCount + 1, 1, Bob, Alice, Charlie, 1); + }); + }); + + /* it('transferFrom for a collection that was destroyed', async () => { + await usingApi(async (api: ApiPromise) => { + this test copies approve negative test + }); + }); */ + + /* it('transferFrom a token that does not exist', async () => { + await usingApi(async (api: ApiPromise) => { + this test copies approve negative test + }); + }); */ + + /* it('transferFrom a token that was deleted', async () => { + await usingApi(async (api: ApiPromise) => { + this test copies approve negative test + }); + }); */ + + it('transferFrom for not approved address', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const Charlie = privateKey('//CHARLIE'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + + await transferFromExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, Charlie, 1); + + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await transferFromExpectFail(fungibleCollectionId, newFungibleTokenId, Bob, Alice, Charlie, 1); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await transferFromExpectFail(reFungibleCollectionId, + newReFungibleTokenId, Bob, Alice, Charlie, 1); + }); + }); + + it('transferFrom incorrect token count', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const Charlie = privateKey('//CHARLIE'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob); + + await transferFromExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, Charlie, 2); + + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob); + await transferFromExpectFail(fungibleCollectionId, newFungibleTokenId, Bob, Alice, Charlie, 2); + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob); + await transferFromExpectFail(reFungibleCollectionId, + newReFungibleTokenId, Bob, Alice, Charlie, 2); + }); + }); + + it('execute transferFrom from account that is not owner of collection', async () => { + await usingApi(async (api: ApiPromise) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + const Charlie = privateKey('//CHARLIE'); + const Dave = privateKey('//DAVE'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + try { + await approveExpectFail(nftCollectionId, newNftTokenId, Dave, Bob); + await transferFromExpectFail(nftCollectionId, newNftTokenId, Dave, Alice, Charlie, 1); + } catch (e) { + // tslint:disable-next-line:no-unused-expression + expect(e).to.be.exist; + } + + // await transferFromExpectFail(nftCollectionId, newNftTokenId, Dave, Alice, Charlie, 1); + + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + try { + await approveExpectFail(fungibleCollectionId, newFungibleTokenId, Dave, Bob); + await transferFromExpectFail(fungibleCollectionId, newFungibleTokenId, Dave, Alice, Charlie, 1); + } catch (e) { + // tslint:disable-next-line:no-unused-expression + expect(e).to.be.exist; + } + // reFungible + const reFungibleCollectionId = await + createCollectionExpectSuccess({mode: {type: 'ReFungible', decimalPoints: 0}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + try { + await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Dave, Bob); + await transferFromExpectFail(reFungibleCollectionId, newReFungibleTokenId, Dave, Alice, Charlie, 1); + } catch (e) { + // tslint:disable-next-line:no-unused-expression + expect(e).to.be.exist; + } + }); + }); +}); --- /dev/null +++ b/tests/src/util/contracthelpers.ts @@ -0,0 +1,94 @@ +// +// 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 { submitTransactionAsync } from "../substrate/substrate-api"; +import fs from "fs"; +import { Abi, BlueprintPromise as Blueprint, CodePromise, ContractPromise as Contract } from "@polkadot/api-contract"; +import { IKeyringPair } from "@polkadot/types/types"; +import { ApiPromise, Keyring } from "@polkadot/api"; + +chai.use(chaiAsPromised); +const expect = chai.expect; +import { BigNumber } from 'bignumber.js'; +import { findUnusedAddress } from '../util/helpers'; + +const value = 0; +const gasLimit = 3000n * 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 endowment = 1000000000000000n; + const initValue = true; + + const unsub = await blueprint.tx + .new(endowment, gasLimit, initValue) + .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; +} + +export async function deployFlipper(api: ApiPromise): Promise<[Contract, IKeyringPair]> { + const metadata = JSON.parse(fs.readFileSync('./src/flipper/metadata.json').toString('utf-8')); + const abi = new Abi(metadata); + + const deployer = await prepareDeployer(api); + + const wasm = fs.readFileSync('./src/flipper/flipper.wasm'); + + const code = new CodePromise(api, abi, wasm); + + const blueprint = await deployBlueprint(deployer, code); + const contract = (await deployContract(deployer, blueprint))['contract'] as Contract; + + const initialGetResponse = await getFlipValue(contract, deployer); + expect(initialGetResponse).to.be.true; + + return [contract, deployer]; +} + +export async function getFlipValue(contract: Contract, deployer: IKeyringPair) { + const result = await contract.query.get(deployer.address, value, gasLimit); + + if(!result.result.isSuccess) { + throw `Failed to get flipper value`; + } + return (result.result.asSuccess.data[0] == 0x00) ? false : true; +} --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -3,20 +3,20 @@ // file 'LICENSE', which is part of this source code package. // +import { ApiPromise, Keyring } from '@polkadot/api'; +import { Enum, Struct } from '@polkadot/types/codec'; +import type { AccountId, EventRecord } from '@polkadot/types/interfaces'; +import { u128 } from '@polkadot/types/primitive'; +import { IKeyringPair } from '@polkadot/types/types'; +import { BigNumber } from 'bignumber.js'; +import BN from 'bn.js'; import chai from 'chai'; 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, submitTransactionExpectFailAsync } from "../substrate/substrate-api"; +import { alicesPublicKey, nullPublicKey } from '../accounts'; import privateKey from '../substrate/privateKey'; -import { alicesPublicKey, nullPublicKey } from "../accounts"; -import { strToUTF16, utf16ToStr, hexToStr } from './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'; +import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from '../substrate/substrate-api'; import { ICollectionInterface } from '../types'; -import BN from "bn.js"; +import { hexToStr, strToUTF16, utf16ToStr } from './util'; chai.use(chaiAsPromised); const expect = chai.expect; @@ -25,24 +25,45 @@ success: boolean, }; -type CreateCollectionResult = { - success: boolean, - collectionId: number -}; +interface CreateCollectionResult { + success: boolean; + collectionId: number; +} + +interface CreateItemResult { + success: boolean; + collectionId: number; + itemId: number; +} + +interface IReFungibleOwner { + Fraction: BN; + Owner: number[]; +} + +interface ITokenDataType { + Owner: number[]; + ConstData: number[]; + VariableData: number[]; +} + +interface IFungibleTokenDataType { + Value: BN; +} -type CreateItemResult = { - success: boolean, - collectionId: number, - itemId: number -}; +interface IReFungibleTokenDataType { + Owner: IReFungibleOwner[]; + ConstData: number[]; + VariableData: number[]; +} export function getGenericResult(events: EventRecord[]): GenericResult { - let result: GenericResult = { - success: false - } + const result: GenericResult = { + success: false, + }; events.forEach(({ phase, event: { data, method, section } }) => { // console.log(` ${phase}: ${section}.${method}:: ${data}`); - if (method == 'ExtrinsicSuccess') { + if (method === 'ExtrinsicSuccess') { result.success = true; } }); @@ -60,10 +81,10 @@ collectionId = parseInt(data[0].toString()); } }); - let result: CreateCollectionResult = { + const result: CreateCollectionResult = { success, - collectionId - } + collectionId, + }; return result; } @@ -80,27 +101,60 @@ itemId = parseInt(data[1].toString()); } }); - let result: CreateItemResult = { + const result: CreateItemResult = { success, collectionId, - itemId - } + itemId, + }; return result; } -export type CollectionMode = 'NFT' | 'Fungible' | 'ReFungible'; +interface Invalid { + type: 'Invalid'; +} + +interface Nft { + type: 'NFT'; +} + +interface Fungible { + type: 'Fungible'; + decimalPoints: number; +} + +interface ReFungible { + type: 'ReFungible'; + decimalPoints: number; +} + +interface Nft { + type: 'NFT' +} + +interface Fungible { + type: 'Fungible', + decimalPoints: number +} + +interface ReFungible { + type: 'ReFungible', + decimalPoints: number +} + +type CollectionMode = Nft | Fungible | ReFungible | Invalid; + export type CreateCollectionParams = { mode: CollectionMode, name: string, description: string, - tokenPrefix: string + tokenPrefix: string, }; const defaultCreateCollectionParams: CreateCollectionParams = { + description: 'description', + mode: { type: 'NFT' }, name: 'name', - description: 'description', - mode: 'NFT', - tokenPrefix: 'prefix' + tokenPrefix: 'prefix', } export async function createCollectionExpectSuccess(params: Partial = {}): Promise { @@ -109,25 +163,39 @@ let collectionId: number = 0; await usingApi(async (api) => { // Get number of collections before the transaction - const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString()); + const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString(), 10); // Run the CreateCollection transaction const alicePrivateKey = privateKey('//Alice'); - const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), mode); + + let modeprm = {}; + if (mode.type === 'NFT') { + modeprm = {nft: null}; + } else if (mode.type === 'Fungible') { + modeprm = {fungible: mode.decimalPoints}; + } else if (mode.type === 'ReFungible') { + modeprm = {refungible: mode.decimalPoints}; + } else if (mode.type === 'Invalid') { + modeprm = {invalid: null}; + } + + const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), modeprm); const events = await submitTransactionAsync(alicePrivateKey, tx); const result = getCreateCollectionResult(events); // Get number of collections after the transaction - const BcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString()); + const BcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString(), 10); - // Get the collection + // Get the collection const collection: any = (await api.query.nft.collection(result.collectionId)).toJSON(); // What to expect + // tslint:disable-next-line:no-unused-expression expect(result.success).to.be.true; expect(result.collectionId).to.be.equal(BcollectionCount); + // tslint:disable-next-line:no-unused-expression expect(collection).to.be.not.null; - expect(BcollectionCount).to.be.equal(AcollectionCount+1, 'Error: NFT collection NOT created.'); + expect(BcollectionCount).to.be.equal(AcollectionCount + 1, 'Error: NFT collection NOT created.'); expect(collection.Owner).to.be.equal(alicesPublicKey); expect(utf16ToStr(collection.Name)).to.be.equal(name); expect(utf16ToStr(collection.Description)).to.be.equal(description); @@ -138,17 +206,28 @@ return collectionId; } - + export async function createCollectionExpectFailure(params: Partial = {}) { const {name, description, mode, tokenPrefix } = {...defaultCreateCollectionParams, ...params}; + let modeprm = {}; + if (mode.type === 'NFT') { + modeprm = {nft: null}; + } else if (mode.type === 'Fungible') { + modeprm = {fungible: mode.decimalPoints}; + } else if (mode.type === 'ReFungible') { + modeprm = {refungible: mode.decimalPoints}; + } else if (mode.type === 'Invalid') { + modeprm = {invalid: null}; + } + 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(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), mode); + const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), modeprm); const events = await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; const result = getCreateCollectionResult(events); @@ -156,11 +235,12 @@ const BcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString()); // What to expect + // tslint:disable-next-line:no-unused-expression expect(result.success).to.be.false; expect(BcollectionCount).to.be.equal(AcollectionCount, 'Error: Collection with incorrect data created.'); }); } - + export async function findUnusedAddress(api: ApiPromise): Promise { let bal = new BigNumber(0); let unused; @@ -170,7 +250,7 @@ unused = keyring.addFromUri(`//${randomSeed}`); bal = new BigNumber((await api.query.system.account(unused.address)).data.free.toString()); } while (bal.toFixed() != '0'); - return unused; + return unused; } function getDestroyResult(events: EventRecord[]): boolean { @@ -201,7 +281,7 @@ const events = await submitTransactionAsync(alicePrivateKey, tx); const result = getDestroyResult(events); - // Get the collection + // Get the collection const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); // What to expect @@ -220,7 +300,7 @@ const events = await submitTransactionAsync(alicePrivateKey, tx); const result = getGenericResult(events); - // Get the collection + // Get the collection const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); // What to expect @@ -239,7 +319,7 @@ const events = await submitTransactionAsync(alicePrivateKey, tx); const result = getGenericResult(events); - // Get the collection + // Get the collection const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); // What to expect @@ -278,7 +358,7 @@ const events = await submitTransactionAsync(sender, tx); const result = getGenericResult(events); - // Get the collection + // Get the collection const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); // What to expect @@ -300,48 +380,200 @@ export interface CreateFungibleData extends Struct { readonly value: u128; -}; +} -export interface CreateReFungibleData extends Struct {}; -export interface CreateNftData extends Struct {}; +export interface CreateReFungibleData extends Struct {} +export interface CreateNftData extends Struct {} export interface CreateItemData extends Enum { - NFT: CreateNftData, - Fungible: CreateFungibleData, - ReFungible: CreateReFungibleData -}; + NFT: CreateNftData; + Fungible: CreateFungibleData; + ReFungible: CreateReFungibleData; +} + +export async function burnItemExpectSuccess(owner: IKeyringPair, collectionId: number, tokenId: number, value = 0) { + await usingApi(async (api) => { + const tx = api.tx.nft.burnItem(collectionId, tokenId, value); + const events = await submitTransactionAsync(owner, tx); + const result = getGenericResult(events); + // Get the item + const item: any = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON(); + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + // tslint:disable-next-line:no-unused-expression + expect(item).to.be.not.null; + expect(item.Owner).to.be.equal(nullPublicKey); + }); +} + +export async function +approveExpectSuccess(collectionId: number, + tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number = 1) { + await usingApi(async (api: ApiPromise) => { + const allowanceBefore = + await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved.address]) as unknown as BN; + const approveNftTx = await api.tx.nft.approve(approved.address, collectionId, tokenId, amount); + const events = await submitTransactionAsync(owner, approveNftTx); + const result = getCreateItemResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + const allowanceAfter = + await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved.address]) as unknown as BN; + expect(allowanceAfter.toNumber() - allowanceBefore.toNumber()).to.be.equal(amount); + }); +} + +export async function +transferFromExpectSuccess(collectionId: number, + tokenId: number, + accountApproved: IKeyringPair, + accountFrom: IKeyringPair, + accountTo: IKeyringPair, + value: number = 1, + type: string = 'NFT') { + await usingApi(async (api: ApiPromise) => { + let balanceBefore = new BN(0); + if (type === 'Fungible') { + balanceBefore = await api.query.nft.balance(collectionId, accountTo.address) as unknown as BN; + } + const transferFromTx = await api.tx.nft.transferFrom( + accountFrom.address, accountTo.address, collectionId, tokenId, value); + const events = await submitTransactionAsync(accountFrom, transferFromTx); + const result = getCreateItemResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + if (type === 'NFT') { + const nftItemData = await api.query.nft.nftItemList(collectionId, tokenId) as unknown as ITokenDataType; + expect(nftItemData.Owner.toString()).to.be.equal(accountTo.address); + } + if (type === 'Fungible') { + const balanceAfter = await api.query.nft.balance(collectionId, accountTo.address) as unknown as BN; + expect(balanceAfter.sub(balanceBefore).toNumber()).to.be.equal(value); + } + if (type === 'ReFungible') { + const nftItemData = + await api.query.nft.reFungibleItemList(collectionId, tokenId) as unknown as IReFungibleTokenDataType; + expect(nftItemData.Owner[0].Owner.toString()).to.be.equal(accountTo.address); + expect(nftItemData.Owner[0].Fraction.toNumber()).to.be.equal(value); + } + }); +} + +export async function +transferFromExpectFail(collectionId: number, + tokenId: number, + accountApproved: IKeyringPair, + accountFrom: IKeyringPair, + accountTo: IKeyringPair, + value: number = 1) { + await usingApi(async (api: ApiPromise) => { + const transferFromTx = await api.tx.nft.transferFrom( + accountFrom.address, accountTo.address, collectionId, tokenId, value); + const events = await expect(submitTransactionExpectFailAsync(accountApproved, transferFromTx)).to.be.rejected; + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} + +export async function +transferExpectSuccess(collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair, + value: number = 1, + type: string = 'NFT') { + await usingApi(async (api: ApiPromise) => { + let balanceBefore = new BN(0); + if (type === 'Fungible') { + balanceBefore = await api.query.nft.balance(collectionId, recipient.address) as unknown as BN; + } + const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value); + const events = await submitTransactionAsync(sender, transferTx); + const result = getCreateItemResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.true; + if (type === 'NFT') { + const nftItemData = await api.query.nft.nftItemList(collectionId, tokenId) as unknown as ITokenDataType; + expect(nftItemData.Owner.toString()).to.be.equal(recipient.address); + } + if (type === 'Fungible') { + const balanceAfter = await api.query.nft.balance(collectionId, recipient.address) as unknown as BN; + expect(balanceAfter.sub(balanceBefore).toNumber()).to.be.equal(value); + } + if (type === 'ReFungible') { + const nftItemData = + await api.query.nft.reFungibleItemList(collectionId, tokenId) as unknown as IReFungibleTokenDataType; + expect(nftItemData.Owner[0].Owner.toString()).to.be.equal(recipient.address); + expect(nftItemData.Owner[0].Fraction.toNumber()).to.be.equal(value); + } + }); +} + +export async function +transferExpectFail(collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair, + value: number = 1, + type: string = 'NFT') { + await usingApi(async (api: ApiPromise) => { + const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value); + const events = await expect(submitTransactionExpectFailAsync(sender, transferTx)).to.be.rejected; + if (events && Array.isArray(events)) { + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + } + }); +} + +export async function +approveExpectFail(collectionId: number, + tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number = 1) { + await usingApi(async (api: ApiPromise) => { + const approveNftTx = await api.tx.nft.approve(approved.address, collectionId, tokenId, amount); + const events = await expect(submitTransactionExpectFailAsync(owner, approveNftTx)).to.be.rejected; + const result = getCreateCollectionResult(events); + // tslint:disable-next-line:no-unused-expression + expect(result.success).to.be.false; + }); +} -export async function createItemExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, owner: string = '') { +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 AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString(), 10); + const Aitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON(); const AItemBalance = new BigNumber(Aitem.Value); - if (owner === '') owner = sender.address; + if (owner === '') { + owner = sender.address; + } let tx; - if (createMode == 'Fungible') { - let createData = {fungible: {value: 10}}; + if (createMode === 'Fungible') { + const createData = {fungible: {value: 10}}; tx = api.tx.nft.createItem(collectionId, owner, createData); - } - else { + } 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 BItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString(), 10); + const Bitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON(); const BItemBalance = new BigNumber(Bitem.Value); // What to expect + // tslint:disable-next-line:no-unused-expression expect(result.success).to.be.true; - if (createMode == 'Fungible') { + if (createMode === 'Fungible') { expect(BItemBalance.minus(AItemBalance).toNumber()).to.be.equal(10); - } - else { - expect(BItemCount).to.be.equal(AItemCount+1); + } else { + expect(BItemCount).to.be.equal(AItemCount + 1); } expect(collectionId).to.be.equal(result.collectionId); expect(BItemCount).to.be.equal(result.itemId); @@ -358,10 +590,11 @@ const events = await submitTransactionAsync(sender, tx); const result = getGenericResult(events); - // Get the collection + // Get the collection const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); // What to expect + // tslint:disable-next-line:no-unused-expression expect(result.success).to.be.true; expect(collection.Access).to.be.equal('WhiteList'); }); @@ -375,7 +608,7 @@ const events = await submitTransactionAsync(sender, tx); const result = getGenericResult(events); - // Get the collection + // Get the collection const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); // What to expect @@ -387,17 +620,22 @@ export async function addToWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string) { await usingApi(async (api) => { + const whiteListedBefore = (await api.query.nft.whiteList(collectionId, address)).toJSON(); + // 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(); + const whiteListedAfter = (await api.query.nft.whiteList(collectionId, address)).toJSON(); // What to expect + // tslint:disable-next-line:no-unused-expression expect(result.success).to.be.true; - expect(collection.MintMode).to.be.equal(true); + // tslint:disable-next-line: no-unused-expression + expect(whiteListedBefore).to.be.false; + // tslint:disable-next-line: no-unused-expression + expect(whiteListedAfter).to.be.true; }); } -- gitstuff