From 0c172129d62be7c7c48bd39ebb6990aec28858b1 Mon Sep 17 00:00:00 2001 From: usetech-llc Date: Sat, 20 Mar 2021 10:44:06 +0000 Subject: [PATCH] Merge pull request #130 from usetech-llc/NFTPAR-310_builders_review_107 Review changes --- --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -193,8 +193,7 @@ mint_mode: false, offchain_schema: vec![], schema_version: SchemaVersion::default(), - sponsor: get_account_id_from_seed::("Alice"), - sponsor_confirmed: true, + sponsorship: SponsorshipState::Confirmed(get_account_id_from_seed::("Alice")), const_on_chain_schema: vec![], variable_on_chain_schema: vec![], limits: CollectionLimits::default() --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -20,7 +20,7 @@ ensure, fail, parameter_types, traits::{ Currency, ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced, - Randomness, IsSubType, + Randomness, IsSubType, WithdrawReasons, }, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, @@ -28,6 +28,7 @@ WeightToFeePolynomial, DispatchClass, }, StorageValue, + transactional, }; use frame_system::{self as system, ensure_signed, ensure_root}; @@ -123,6 +124,42 @@ pub fraction: u128, } +#[derive(Encode, Decode, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum SponsorshipState { + /// The fees are applied to the transaction sender + Disabled, + Unconfirmed(AccountId), + /// Transactions are sponsored by specified account + Confirmed(AccountId), +} + +impl SponsorshipState { + fn sponsor(&self) -> Option<&AccountId> { + match self { + Self::Confirmed(sponsor) => Some(sponsor), + _ => None, + } + } + + fn pending_sponsor(&self) -> Option<&AccountId> { + match self { + Self::Unconfirmed(sponsor) | Self::Confirmed(sponsor) => Some(sponsor), + _ => None, + } + } + + fn confirmed(&self) -> bool { + matches!(self, Self::Confirmed(_)) + } +} + +impl Default for SponsorshipState { + fn default() -> Self { + Self::Disabled + } +} + #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct CollectionType { @@ -136,8 +173,7 @@ pub mint_mode: bool, pub offchain_schema: Vec, pub schema_version: SchemaVersion, - pub sponsor: AccountId, // Who pays fees. If set to default address, the fees are applied to the transaction sender - pub sponsor_confirmed: bool, // False if sponsor address has not yet confirmed sponsorship. True otherwise. + pub sponsorship: SponsorshipState, pub limits: CollectionLimits, // Collection private restrictions pub variable_on_chain_schema: Vec, // pub const_on_chain_schema: Vec, // @@ -387,7 +423,9 @@ /// Schema data size limit bound exceeded SchemaDataLimitExceeded, /// Maximum refungibility exceeded - WrongRefungiblePieces + WrongRefungiblePieces, + /// createRefungible should be called with one owner + BadCreateRefungibleCall, } } @@ -396,6 +434,10 @@ /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + type Currency: Currency; + type CollectionCreationPrice: Get<<::Currency as Currency>::Balance>; + type TreasuryAccountId: Get; } #[cfg(feature = "runtime-benchmarks")] @@ -403,54 +445,113 @@ // #endregion +// # Used definitions +// +// ## User control levels +// +// chain-controlled - key is uncontrolled by user +// i.e autoincrementing index +// can use non-cryptographic hash +// real - key is controlled by user +// but it is hard to generate enough colliding values, i.e owner of signed txs +// can use non-cryptographic hash +// controlled - key is completly controlled by users +// i.e maps with mutable keys +// should use cryptographic hash +// +// ## User control level downgrade reasons +// +// ?1 - chain-controlled -> controlled +// collections/tokens can be destroyed, resulting in massive holes +// ?2 - chain-controlled -> controlled +// same as ?1, but can be only added, resulting in easier exploitation +// ?3 - real -> controlled +// no confirmation required, so addresses can be easily generated decl_storage! { trait Store for Module as Nft { - // Private members - NextCollectionID: CollectionId; + //#region Private members + /// Id of next collection CreatedCollectionCount: u32; + /// Used for migrations ChainVersion: u64; - ItemListIndex: map hasher(identity) CollectionId => TokenId; + /// Id of last collection token + /// Collection id (controlled?1) + ItemListIndex: map hasher(blake2_128_concat) CollectionId => TokenId; + //#endregion - // Chain limits struct + //#region Chain limits struct pub ChainLimit get(fn chain_limit) config(): ChainLimits; + //#endregion - // Bound counters - CollectionCount: u32; + //#region Bound counters + /// Amount of collections destroyed, used for total amount tracking with + /// CreatedCollectionCount + DestroyedCollectionCount: u32; + /// Total amount of account owned tokens (NFTs + RFTs + unique fungibles) + /// Account id (real) pub AccountItemCount get(fn account_item_count): map hasher(twox_64_concat) T::AccountId => u32; + //#endregion - // Basic collections - pub Collection get(fn collection) config(): map hasher(identity) CollectionId => CollectionType; - pub AdminList get(fn admin_list_collection): map hasher(identity) CollectionId => Vec; - pub WhiteList get(fn white_list): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => bool; + //#region Basic collections + /// Collection info + /// Collection id (controlled?1) + pub Collection get(fn collection) config(): map hasher(blake2_128_concat) CollectionId => CollectionType; + /// List of collection admins + /// Collection id (controlled?2) + pub AdminList get(fn admin_list_collection): map hasher(blake2_128_concat) CollectionId => Vec; + /// Whitelisted collection users + /// Collection id (controlled?2), user id (controlled?3) + pub WhiteList get(fn white_list): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => bool; + //#endregion - /// Balance owner per collection map - pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128; + /// How many of collection items user have + /// Collection id (controlled?2), account id (real) + pub Balance get(fn balance_count): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => u128; - /// second parameter: item id + owner account id + spender account id - pub Allowances get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId, T::AccountId) => u128; + /// Amount of items which spender can transfer out of owners account (via transferFrom) + /// Collection id (controlled?2), (token id (controlled ?2) + owner account id (real) + spender account id (controlled?3)) + pub Allowances get(fn approved): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) (TokenId, T::AccountId, T::AccountId) => u128; - /// Item collections - pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType; - pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => FungibleItemType; - pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType; + //#region Item collections + /// Collection id (controlled?2), token id (controlled?1) + pub NftItemList get(fn nft_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => NftItemType; + /// Collection id (controlled?2), owner (controlled?2) + pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => FungibleItemType; + /// Collection id (controlled?2), token id (controlled?1) + pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => ReFungibleItemType; + //#endregion - /// Index list - pub AddressTokens get(fn address_tokens): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => Vec; + //#region Index list + /// Collection id (controlled?2), tokens owner (controlled?2) + pub AddressTokens get(fn address_tokens): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => Vec; + //#endregion - /// Tokens transfer baskets - 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; + //#region Tokens transfer rate limit baskets + /// (Collection id (controlled?2), who created (real)) + pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => T::BlockNumber; + /// Collection id (controlled?2), token id (controlled?2) + pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber; + /// Collection id (controlled?2), owning user (real) + pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber; + /// Collection id (controlled?2), token id (controlled?2) + pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber; + //#endregion - // Contract Sponsorship and Ownership + //#region Contract Sponsorship and Ownership + /// Contract address (real) pub ContractOwner get(fn contract_owner): map hasher(twox_64_concat) T::AccountId => T::AccountId; + /// Contract address (real) pub ContractSelfSponsoring get(fn contract_self_sponsoring): map hasher(twox_64_concat) T::AccountId => bool; + /// (Contract address(real), caller (real)) pub ContractSponsorBasket get(fn contract_sponsor_basket): map hasher(twox_64_concat) (T::AccountId, T::AccountId) => T::BlockNumber; + /// Contract address (real) pub ContractSponsoringRateLimit get(fn contract_sponsoring_rate_limit): map hasher(twox_64_concat) T::AccountId => T::BlockNumber; + /// Contract address (real) pub ContractWhiteListEnabled get(fn contract_white_list_enabled): map hasher(twox_64_concat) T::AccountId => bool; - pub ContractWhiteList get(fn contract_white_list): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::AccountId => bool; + /// Contract address (real) => Whitelisted user (controlled?3) + pub ContractWhiteList get(fn contract_white_list): double_map hasher(twox_64_concat) T::AccountId, hasher(blake2_128_concat) T::AccountId => bool; + //#endregion } add_extra_genesis { build(|config: &GenesisConfig| { @@ -534,14 +635,6 @@ type Error = Error; fn on_initialize(now: T::BlockNumber) -> Weight { - - if ChainVersion::get() < 2 - { - let value = NextCollectionID::get(); - CreatedCollectionCount::put(value); - ChainVersion::put(2); - } - 0 } @@ -562,6 +655,7 @@ /// * mode: [CollectionMode] collection type and type dependent data. // returns collection ID #[weight = ::WeightInfo::create_collection()] + #[transactional] pub fn create_collection(origin, collection_name: Vec, collection_description: Vec, @@ -571,6 +665,19 @@ // Anyone can create a collection let who = ensure_signed(origin)?; + // Take a (non-refundable) deposit of collection creation + let mut imbalance = <<::Currency as Currency>::PositiveImbalance>::zero(); + imbalance.subsume(<::Currency as Currency>::deposit_creating( + &T::TreasuryAccountId::get(), + T::CollectionCreationPrice::get(), + )); + ::Currency::settle( + &who, + imbalance, + WithdrawReasons::TRANSFER, + ExistenceRequirement::KeepAlive, + ).map_err(|_| Error::::NoPermission)?; + let decimal_points = match mode { CollectionMode::Fungible(points) => points, _ => 0 @@ -578,8 +685,11 @@ let chain_limit = ChainLimit::get(); + let created_count = CreatedCollectionCount::get(); + let destroyed_count = DestroyedCollectionCount::get(); + // bound Total number of collections - ensure!(CollectionCount::get() < chain_limit.collection_numbers_limit, Error::::TotalCollectionsLimitExceeded); + ensure!(created_count - destroyed_count < chain_limit.collection_numbers_limit, Error::::TotalCollectionsLimitExceeded); // check params ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::::CollectionDecimalPointLimitExceeded); @@ -588,17 +698,11 @@ ensure!(token_prefix.len() <= 16, Error::::CollectionTokenPrefixLimitExceeded); // Generate next collection ID - let next_id = CreatedCollectionCount::get() - .checked_add(1) - .ok_or(Error::::NumOverflow)?; - - // bound counter - let total = CollectionCount::get() + let next_id = created_count .checked_add(1) .ok_or(Error::::NumOverflow)?; CreatedCollectionCount::put(next_id); - CollectionCount::put(total); let limits = CollectionLimits { sponsored_data_size: chain_limit.custom_data_limit, @@ -617,8 +721,7 @@ token_prefix: token_prefix, offchain_schema: Vec::new(), schema_version: SchemaVersion::ImageURL, - sponsor: T::AccountId::default(), - sponsor_confirmed: false, + sponsorship: SponsorshipState::Disabled, variable_on_chain_schema: Vec::new(), const_on_chain_schema: Vec::new(), limits, @@ -643,6 +746,7 @@ /// /// * collection_id: collection to destroy. #[weight = ::WeightInfo::destroy_collection()] + #[transactional] pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -669,15 +773,9 @@ >::remove_prefix(collection_id); >::remove_prefix(collection_id); - if CollectionCount::get() > 0 - { - // bound couter - let total = CollectionCount::get() - .checked_sub(1) - .ok_or(Error::::NumOverflow)?; - - CollectionCount::put(total); - } + DestroyedCollectionCount::put(DestroyedCollectionCount::get() + .checked_add(1) + .ok_or(Error::::NumOverflow)?); Ok(()) } @@ -695,6 +793,7 @@ /// /// * address. #[weight = ::WeightInfo::add_to_white_list()] + #[transactional] pub fn add_to_white_list(origin, collection_id: CollectionId, address: T::AccountId) -> DispatchResult{ let sender = ensure_signed(origin)?; @@ -718,6 +817,7 @@ /// /// * address. #[weight = ::WeightInfo::remove_from_white_list()] + #[transactional] pub fn remove_from_white_list(origin, collection_id: CollectionId, address: T::AccountId) -> DispatchResult{ let sender = ensure_signed(origin)?; @@ -740,6 +840,7 @@ /// /// * mode: [AccessMode] #[weight = ::WeightInfo::set_public_access_mode()] + #[transactional] pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -766,6 +867,7 @@ /// /// * mint_permission: Boolean parameter. If True, allows minting to Anyone with conditions above. #[weight = ::WeightInfo::set_mint_permission()] + #[transactional] pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -790,6 +892,7 @@ /// /// * new_owner. #[weight = ::WeightInfo::change_collection_owner()] + #[transactional] pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -815,6 +918,7 @@ /// /// * new_admin_id: Address of new admin to add. #[weight = ::WeightInfo::add_collection_admin()] + #[transactional] pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::AccountId) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -849,6 +953,7 @@ /// /// * account_id: Address of admin to remove. #[weight = ::WeightInfo::remove_collection_admin()] + #[transactional] pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::AccountId) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -872,6 +977,7 @@ /// /// * new_sponsor. #[weight = ::WeightInfo::set_collection_sponsor()] + #[transactional] pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -880,8 +986,7 @@ let mut target_collection = >::get(collection_id); ensure!(sender == target_collection.owner, Error::::NoPermission); - target_collection.sponsor = new_sponsor; - target_collection.sponsor_confirmed = false; + target_collection.sponsorship = SponsorshipState::Unconfirmed(new_sponsor); >::insert(collection_id, target_collection); Ok(()) @@ -895,15 +1000,19 @@ /// /// * collection_id. #[weight = ::WeightInfo::confirm_sponsorship()] + #[transactional] pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult { let sender = ensure_signed(origin)?; ensure!(>::contains_key(collection_id), Error::::CollectionNotFound); let mut target_collection = >::get(collection_id); - ensure!(sender == target_collection.sponsor, Error::::ConfirmUnsetSponsorFail); + ensure!( + target_collection.sponsorship.pending_sponsor() == Some(&sender), + Error::::ConfirmUnsetSponsorFail + ); - target_collection.sponsor_confirmed = true; + target_collection.sponsorship = SponsorshipState::Confirmed(sender); >::insert(collection_id, target_collection); Ok(()) @@ -919,6 +1028,7 @@ /// /// * collection_id. #[weight = ::WeightInfo::remove_collection_sponsor()] + #[transactional] pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -927,8 +1037,7 @@ let mut target_collection = >::get(collection_id); ensure!(sender == target_collection.owner, Error::::NoPermission); - target_collection.sponsor = T::AccountId::default(); - target_collection.sponsor_confirmed = false; + target_collection.sponsorship = SponsorshipState::Disabled; >::insert(collection_id, target_collection); Ok(()) @@ -959,6 +1068,7 @@ // .saturating_add(RocksDbWeight::get().writes(8 as Weight))] #[weight = ::WeightInfo::create_item(data.len())] + #[transactional] pub fn create_item(origin, collection_id: CollectionId, owner: T::AccountId, data: CreateItemData) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -974,7 +1084,7 @@ Ok(()) } - /// This method creates multiple instances of NFT Collection created with CreateCollection method. + /// This method creates multiple items in a collection created with CreateCollection method. /// /// # Permissions /// @@ -995,6 +1105,7 @@ #[weight = ::WeightInfo::create_item(items_data.into_iter() .map(|data| { data.len() }) .sum())] + #[transactional] pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::AccountId, items_data: Vec) -> DispatchResult { ensure!(items_data.len() > 0, Error::::EmptyArgument); @@ -1029,6 +1140,7 @@ /// /// * item_id: ID of NFT to burn. #[weight = ::WeightInfo::burn_item()] + #[transactional] pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -1087,6 +1199,7 @@ /// * Fungible Mode: Must specify transferred amount /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1) #[weight = ::WeightInfo::transfer()] + #[transactional] pub fn transfer(origin, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult { let sender = ensure_signed(origin)?; Self::transfer_internal(sender, recipient, collection_id, item_id, value) @@ -1108,6 +1221,7 @@ /// /// * item_id: ID of the item. #[weight = ::WeightInfo::approve()] + #[transactional] pub fn approve(origin, spender: T::AccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -1171,6 +1285,7 @@ /// /// * value: Amount to transfer. #[weight = ::WeightInfo::transfer_from()] + #[transactional] pub fn transfer_from(origin, from: T::AccountId, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -1205,7 +1320,7 @@ } // Reduce approval by transferred amount or remove if remaining approval drops to 0 - if approval.checked_sub(value).unwrap_or(0) > 0 { + if approval.saturating_sub(value) > 0 { >::insert(collection_id, (item_id, &from, &sender), approval - value); } else { @@ -1251,6 +1366,7 @@ /// /// * schema: String representing the offchain data schema. #[weight = ::WeightInfo::set_variable_meta_data()] + #[transactional] pub fn set_variable_meta_data ( origin, collection_id: CollectionId, @@ -1296,6 +1412,7 @@ /// /// * schema: SchemaVersion: enum #[weight = ::WeightInfo::set_schema_version()] + #[transactional] pub fn set_schema_version( origin, collection_id: CollectionId, @@ -1323,6 +1440,7 @@ /// /// * schema: String representing the offchain data schema. #[weight = ::WeightInfo::set_offchain_schema()] + #[transactional] pub fn set_offchain_schema( origin, collection_id: CollectionId, @@ -1354,6 +1472,7 @@ /// /// * schema: String representing the const on-chain data schema. #[weight = ::WeightInfo::set_const_on_chain_schema()] + #[transactional] pub fn set_const_on_chain_schema ( origin, collection_id: CollectionId, @@ -1385,6 +1504,7 @@ /// /// * schema: String representing the variable on-chain data schema. #[weight = ::WeightInfo::set_const_on_chain_schema()] + #[transactional] pub fn set_variable_on_chain_schema ( origin, collection_id: CollectionId, @@ -1405,6 +1525,7 @@ // Sudo permissions function #[weight = ::WeightInfo::set_chain_limits()] + #[transactional] pub fn set_chain_limits( origin, limits: ChainLimits @@ -1429,6 +1550,7 @@ /// * enable flag /// #[weight = ::WeightInfo::enable_contract_sponsoring()] + #[transactional] pub fn enable_contract_sponsoring( origin, contract_address: T::AccountId, @@ -1464,6 +1586,7 @@ /// -`rate_limit`: Number of blocks to wait until the next sponsored transaction is allowed /// #[weight = ::WeightInfo::set_contract_sponsoring_rate_limit()] + #[transactional] pub fn set_contract_sponsoring_rate_limit( origin, contract_address: T::AccountId, @@ -1491,6 +1614,7 @@ /// /// - `enable`: . #[weight = ::WeightInfo::toggle_contract_white_list()] + #[transactional] pub fn toggle_contract_white_list( origin, contract_address: T::AccountId, @@ -1518,6 +1642,7 @@ /// /// -`account_address`: Address to add. #[weight = ::WeightInfo::add_to_contract_white_list()] + #[transactional] pub fn add_to_contract_white_list( origin, contract_address: T::AccountId, @@ -1545,6 +1670,7 @@ /// /// -`account_address`: Address to remove. #[weight = ::WeightInfo::remove_from_contract_white_list()] + #[transactional] pub fn remove_from_contract_white_list( origin, contract_address: T::AccountId, @@ -1561,6 +1687,7 @@ } #[weight = ::WeightInfo::set_collection_limits()] + #[transactional] pub fn set_collection_limits( origin, collection_id: u32, @@ -1726,9 +1853,6 @@ } }; - // call event - Self::deposit_event(RawEvent::ItemCreated(collection_id, ::get(collection_id), owner)); - Ok(()) } @@ -1752,6 +1876,7 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, (*owner).clone(), new_balance); + Self::deposit_event(RawEvent::ItemCreated(collection_id, 0, owner.clone())); Ok(()) } @@ -1761,8 +1886,14 @@ .ok_or(Error::::NumOverflow)?; let itemcopy = item.clone(); - let value = item.owner.first().unwrap().fraction; - let owner = item.owner.first().unwrap().owner.clone(); + ensure!( + item.owner.len() == 1, + Error::::BadCreateRefungibleCall, + ); + let item_owner = item.owner.first().expect("only one owner is defined"); + + let value = item_owner.fraction; + let owner = item_owner.owner.clone(); Self::add_token_index(collection_id, current_index, &owner)?; @@ -1775,6 +1906,7 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, owner.clone(), new_balance); + Self::deposit_event(RawEvent::ItemCreated(collection_id, current_index, owner)); Ok(()) } @@ -1795,6 +1927,7 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, item_owner.clone(), new_balance); + Self::deposit_event(RawEvent::ItemCreated(collection_id, current_index, item_owner)); Ok(()) } @@ -1811,9 +1944,8 @@ let rft_balance = token .owner .iter() - .filter(|&i| i.owner == *owner) - .next() - .unwrap(); + .find(|&i| i.owner == *owner) + .ok_or(Error::::TokenNotFound)?; Self::remove_token_index(collection_id, item_id, owner)?; // update balance @@ -1827,7 +1959,7 @@ .owner .iter() .position(|i| i.owner == *owner) - .unwrap(); + .expect("owned item is exists"); token.owner.remove(index); let owner_count = token.owner.len(); @@ -2054,7 +2186,7 @@ .iter() .filter(|i| i.owner == owner) .next() - .ok_or(Error::::NumOverflow)?; + .ok_or(Error::::TokenNotFound)?; let amount = item.fraction; ensure!(amount >= value, Error::::TokenValueTooLow); @@ -2065,10 +2197,10 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, item.owner.clone(), balance_old_owner); - let balancenew_owner = >::get(collection_id, new_owner.clone()) + let balance_new_owner = >::get(collection_id, new_owner.clone()) .checked_add(value) .ok_or(Error::::NumOverflow)?; - >::insert(collection_id, new_owner.clone(), balancenew_owner); + >::insert(collection_id, new_owner.clone(), balance_new_owner); let old_owner = item.owner.clone(); let new_owner_has_account = full_item.owner.iter().any(|i| i.owner == new_owner); @@ -2082,7 +2214,7 @@ .owner .iter_mut() .find(|i| i.owner == owner) - .unwrap() + .expect("old owner does present in refungible") .owner = new_owner.clone(); >::insert(collection_id, item_id, new_full_item); @@ -2094,7 +2226,7 @@ .owner .iter_mut() .find(|i| i.owner == owner) - .unwrap() + .expect("old owner does present in refungible") .fraction -= value; // separate amount @@ -2104,7 +2236,7 @@ .owner .iter_mut() .find(|i| i.owner == new_owner) - .unwrap() + .expect("new owner has account") .fraction += value; } else { // new owner do not have account @@ -2424,13 +2556,6 @@ > { let tip = self.0; - // Set fee based on call type. Creating collection costs 1 Unique. - // All other transactions have traditional fees so far - // let fee = match call.is_sub_type() { - // Some(Call::create_collection(..)) => >::from(1_000_000_000), - // _ => Self::traditional_fee(len, info, tip), // Flat fee model, use only for testing purposes - // // _ => >::from(100) - // }; let fee = Self::traditional_fee(len, info, tip); // Only mess with balances if fee is not zero. @@ -2447,7 +2572,9 @@ // sponsor timeout let block_number = >::block_number() as T::BlockNumber; - let limit = >::get(collection_id).limits.sponsor_transfer_timeout; + let collection = >::get(collection_id); + + let limit = collection.limits.sponsor_transfer_timeout; let mut sponsored = true; if >::contains_key((collection_id, &who)) { let last_tx_block = >::get((collection_id, &who)); @@ -2461,11 +2588,12 @@ } // check free create limit - if (>::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32)) && - (>::get(collection_id).sponsor_confirmed) && + if (collection.limits.sponsored_data_size >= (_properties.len() as u32)) && (sponsored) { - >::get(collection_id).sponsor + collection.sponsorship.sponsor() + .cloned() + .unwrap_or_default() } else { T::AccountId::default() } @@ -2473,7 +2601,7 @@ Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => { let mut sponsor_transfer = false; - if >::get(collection_id).sponsor_confirmed { + if >::get(collection_id).sponsorship.confirmed() { let collection_limits = >::get(collection_id).limits; let collection_mode = >::get(collection_id).mode; @@ -2560,7 +2688,9 @@ if !sponsor_transfer { T::AccountId::default() } else { - >::get(collection_id).sponsor + >::get(collection_id).sponsorship.sponsor() + .cloned() + .unwrap_or_default() } } --- a/pallets/nft/src/mock.rs +++ b/pallets/nft/src/mock.rs @@ -1,43 +1,44 @@ -// Creating mock runtime here - -use crate::{Module, Config}; - -use frame_support::{ - impl_outer_origin, parameter_types, - weights::{Weight, IdentityFee}, +use crate as pallet_template; +use sp_core::H256; +use frame_support::{ + parameter_types, + weights::IdentityFee, }; -use frame_system as system; -use system::limits::{BlockLength, BlockWeights}; -use transaction_payment::{self, CurrencyAdapter}; -use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, - Perbill, + traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + Perbill, }; -use pallet_contracts::WeightInfo; -pub use pallet_balances; +use pallet_transaction_payment::{ CurrencyAdapter}; +use frame_system as system; -impl_outer_origin! { - pub enum Origin for Test {} -} +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; -// For testing the pallet, we construct most of a mock runtime. This means -// first constructing a configuration type (`Test`) which `impl`s each of the -// configuration traits of pallets we want to use. -#[derive(Clone, Eq, PartialEq)] -pub struct Test; +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Module, Call, Config, Storage, Event}, + TemplateModule: pallet_template::{Module, Call, Storage}, + } +); + parameter_types! { - pub const BlockHashCount: u64 = 250; - pub TestBlockWeights: BlockWeights = BlockWeights::default(); - pub TestBlockLength: BlockLength = BlockLength::default(); - pub SS58Prefix: u8 = 0; + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; } impl system::Config for Test { type BaseCallFilter = (); + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); type Origin = Origin; - type Call = (); + type Call = Call; type Index = u64; type BlockNumber = u64; type Hash = H256; @@ -47,24 +48,20 @@ type Header = Header; type Event = (); type BlockHashCount = BlockHashCount; - type BlockWeights = TestBlockWeights; - type DbWeight = (); - type BlockLength = TestBlockLength; type Version = (); - type PalletInfo = (); + type PalletInfo = PalletInfo; type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); + type SystemWeightInfo = (); type SS58Prefix = SS58Prefix; - type SystemWeightInfo = (); } parameter_types! { pub const ExistentialDeposit: u64 = 1; pub const MaxLocks: u32 = 50; } - -type System = frame_system::Module; +//frame_system::Module; impl pallet_balances::Config for Test { type AccountStore = System; type Balance = u64; @@ -79,13 +76,12 @@ pub const TransactionByteFee: u64 = 1; } -impl transaction_payment::Config for Test { +impl pallet_transaction_payment::Config for Test { type OnChargeTransaction = CurrencyAdapter, ()>; type TransactionByteFee = TransactionByteFee; type WeightToFee = IdentityFee; type FeeMultiplierUpdate = (); } - parameter_types! { pub const MinimumPeriod: u64 = 1; @@ -110,12 +106,8 @@ pub const SignedClaimHandicap: u32 = 2; pub const MaxDepth: u32 = 32; pub const MaxValueSize: u32 = 16 * 1024; - pub DeletionWeightLimit: Weight = Perbill::from_percent(10) * - TestBlockWeights::get().max_block; - pub DeletionQueueDepth: u32 = ((DeletionWeightLimit::get() / ( - ::WeightInfo::on_initialize_per_queue_item(1) - - ::WeightInfo::on_initialize_per_queue_item(0) - )) / 5) as u32; + pub DeletionWeightLimit: u64 = u64::MAX;//Perbill::from_percent(10); + pub DeletionQueueDepth: u32 = 10; } impl pallet_contracts::Config for Test { @@ -136,23 +128,22 @@ type DeletionQueueDepth = DeletionQueueDepth; type MaxValueSize = MaxValueSize; type ChainExtension = (); + type MaxCodeSize = (); type WeightPrice = (); type WeightInfo = pallet_contracts::weights::SubstrateWeight; } -impl Config for Test { +parameter_types! { + pub const CollectionCreationPrice: u64 = 1_000_000_000_000; +} + +impl pallet_template::Config for Test { type Event = (); type WeightInfo = (); - + type CollectionCreationPrice = CollectionCreationPrice; } -pub type TemplateModule = Module; - -// This function basically just builds a genesis storage key/value store according to -// our desired mockup. +// Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - system::GenesisConfig::default() - .build_storage::() - .unwrap() - .into() -} + system::GenesisConfig::default().build_storage::().unwrap().into() +} \ No newline at end of file --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -24,7 +24,7 @@ create_runtime_str, generic, impl_opaque_keys, traits::{ Convert, ConvertInto, BlakeTwo256, Block as BlockT, IdentifyAccount, - IdentityLookup, NumberFor, Verify, + IdentityLookup, NumberFor, Verify, AccountIdConversion, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, MultiSignature, @@ -520,10 +520,18 @@ type WeightInfo = (); } +parameter_types! { + pub TreasuryAccountId: AccountId = TreasuryModuleId::get().into_account(); + pub const CollectionCreationPrice: Balance = 100 * UNIQUE; +} + /// Used for the module nft in `./nft.rs` impl pallet_nft::Config for Runtime { type Event = Event; type WeightInfo = nft_weights::WeightInfo; + type Currency = Balances; + type CollectionCreationPrice = CollectionCreationPrice; + type TreasuryAccountId = TreasuryAccountId; } construct_runtime!( --- a/runtime_types.json +++ b/runtime_types.json @@ -31,6 +31,13 @@ "ConstData": "Vec", "VariableData": "Vec" }, + "SponsorshipState": { + "_enum": { + "Disabled": null, + "Unconfirmed": "AccountId", + "Confirmed": "AccountId" + } + }, "CollectionType": { "Owner": "AccountId", "Mode": "CollectionMode", @@ -42,8 +49,7 @@ "MintMode": "bool", "OffchainSchema": "Vec", "SchemaVersion": "SchemaVersion", - "Sponsor": "AccountId", - "SponsorConfirmed": "bool", + "Sponsorship": "SponsorshipState", "Limits": "CollectionLimits", "VariableOnChainSchema": "Vec", "ConstOnChainSchema": "Vec" --- a/tests/package.json +++ b/tests/package.json @@ -25,7 +25,10 @@ "testSetSchemaVersion": "mocha --timeout 9999999 -r ts-node/register ./**/setSchemaVersion.test.ts", "testSetVariableMetaData": "mocha --timeout 9999999 -r ts-node/register ./**/setVariableMetaData.test.ts", "testSetCollectionLimits": "mocha --timeout 9999999 -r ts-node/register ./**/setCollectionLimits.test.ts", + "testSetCollectionSponsor": "mocha --timeout 9999999 -r ts-node/register ./**/setCollectionSponsor.test.ts", + "testConfirmSponsorship": "mocha --timeout 9999999 -r ts-node/register ./**/confirmSponsorship.test.ts", "testRemoveCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/removeCollectionAdmin.test.ts", + "testRemoveCollectionSponsor": "mocha --timeout 9999999 -r ts-node/register ./**/removeCollectionSponsor.test.ts", "testRemoveFromWhiteList": "mocha --timeout 9999999 -r ts-node/register ./**/removeFromWhiteList.test.ts", "testConnection": "mocha --timeout 9999999 -r ts-node/register ./**/connection.test.ts", "testCollection": "mocha --timeout 9999999 -r ts-node/register ./**/createCollection.test.ts", --- a/tests/src/createCollection.test.ts +++ b/tests/src/createCollection.test.ts @@ -1,59 +1,59 @@ -// -// This file is subject to the terms and conditions defined in -// file 'LICENSE', which is part of this source code package. -// - -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import { default as usingApi } from './substrate/substrate-api'; -import { 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: {type: 'NFT'}}); - }); - it('Create new NFT collection whith collection_name of maximum length (64 bytes)', async () => { - await createCollectionExpectSuccess({name: 'A'.repeat(64)}); - }); - it('Create new NFT collection whith collection_description of maximum length (256 bytes)', async () => { - await createCollectionExpectSuccess({description: 'A'.repeat(256)}); - }); - it('Create new NFT collection whith token_prefix of maximum length (16 bytes)', async () => { - await createCollectionExpectSuccess({tokenPrefix: 'A'.repeat(16)}); - }); - it('Create new Fungible collection', async () => { - await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - }); - it('Create new ReFungible collection', async () => { - await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - }); -}); - -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(), 10); - - 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(), 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), mode: {type: 'NFT'}}); - }); - it('(!negative test!) create new NFT collection whith incorrect data (collection_description)', async () => { - 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), mode: {type: 'NFT'}}); - }); -}); +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { default as usingApi } from './substrate/substrate-api'; +import { 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: {type: 'NFT'}}); + }); + it('Create new NFT collection whith collection_name of maximum length (64 bytes)', async () => { + await createCollectionExpectSuccess({name: 'A'.repeat(64)}); + }); + it('Create new NFT collection whith collection_description of maximum length (256 bytes)', async () => { + await createCollectionExpectSuccess({description: 'A'.repeat(256)}); + }); + it('Create new NFT collection whith token_prefix of maximum length (16 bytes)', async () => { + await createCollectionExpectSuccess({tokenPrefix: 'A'.repeat(16)}); + }); + it('Create new Fungible collection', async () => { + await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + }); + it('Create new ReFungible collection', async () => { + await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + }); +}); + +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.createdCollectionCount()).toString(), 10); + + 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.createdCollectionCount()).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), mode: {type: 'NFT'}}); + }); + it('(!negative test!) create new NFT collection whith incorrect data (collection_description)', async () => { + 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), mode: {type: 'NFT'}}); + }); +}); --- a/tests/src/creditFeesToTreasury.test.ts +++ b/tests/src/creditFeesToTreasury.test.ts @@ -25,6 +25,7 @@ const Treasury = "5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z"; const saneMinimumFee = 0.05; const saneMaximumFee = 0.5; +const createCollectionDeposit = 100; let alice: IKeyringPair; let bob: IKeyringPair; @@ -127,8 +128,8 @@ const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); const fee = aliceBalanceBefore.minus(aliceBalanceAfter); - expect(fee.dividedBy(1e15).toNumber()).to.be.lessThan(saneMaximumFee); - expect(fee.dividedBy(1e15).toNumber()).to.be.greaterThan(saneMinimumFee); + expect(fee.dividedBy(1e15).toNumber()).to.be.lessThan(saneMaximumFee + createCollectionDeposit); + expect(fee.dividedBy(1e15).toNumber()).to.be.greaterThan(saneMinimumFee + createCollectionDeposit); }); }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -377,8 +377,9 @@ // What to expect expect(result.success).to.be.true; - expect(collection.Sponsor.toString()).to.be.equal(sponsor.toString()); - expect(collection.SponsorConfirmed).to.be.false; + expect(collection.Sponsorship).to.deep.equal({ + Unconfirmed: sponsor.toString(), + }); }); } @@ -396,8 +397,7 @@ // What to expect expect(result.success).to.be.true; - expect(collection.Sponsor).to.be.equal(nullPublicKey); - expect(collection.SponsorConfirmed).to.be.false; + expect(collection.Sponsorship).to.be.deep.equal({ Disabled: null }); }); } @@ -435,8 +435,9 @@ // What to expect expect(result.success).to.be.true; - expect(collection.Sponsor).to.be.equal(sender.address); - expect(collection.SponsorConfirmed).to.be.true; + expect(collection.Sponsorship).to.be.deep.equal({ + Confirmed: sender.address, + }); }); } -- gitstuff