From 9180c847b05adfe599d4ae2218c6291896ffad7c Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Mon, 22 Mar 2021 09:07:55 +0000 Subject: [PATCH] Merge branch 'develop' into feature/NFTPAR-196_sponsor_setVariableMetadata --- --- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -6,8 +6,8 @@ build: runs-on: ubuntu-latest steps: - - uses: avkviring/telegram-github-action@v0.0.8 + - uses: avkviring/telegram-github-action@v0.0.13 env: telegram_to: ${{ secrets.TELEGRAM_TO }} telegram_token: ${{ secrets.TELEGRAM_TOKEN }} - event: ${{ toJson(github.event) }} \ No newline at end of file + event: ${{ toJson(github.event) }} --- 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 @@ -21,7 +21,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}, @@ -29,6 +29,7 @@ WeightToFeePolynomial, DispatchClass, }, StorageValue, + transactional, }; use frame_system::{self as system, ensure_signed, ensure_root}; @@ -124,6 +125,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, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Collection { @@ -137,8 +174,7 @@ pub mint_mode: bool, pub offchain_schema: Vec, pub schema_version: SchemaVersion, - pub sponsor: T::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, // @@ -413,7 +449,9 @@ /// Schema data size limit bound exceeded SchemaDataLimitExceeded, /// Maximum refungibility exceeded - WrongRefungiblePieces + WrongRefungiblePieces, + /// createRefungible should be called with one owner + BadCreateRefungibleCall, } } @@ -422,6 +460,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")] @@ -429,57 +471,117 @@ // #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 CollectionById get(fn collection_id) config(): map hasher(identity) CollectionId => Option> = None; - 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 CollectionById get(fn collection_id) config(): map hasher(blake2_128_concat) CollectionId => Option> = None; + /// 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 /// Variable metadata sponsoring - pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => Option = None; - - // Contract Sponsorship and Ownership + /// Collection id (controlled?2), token id (controlled?2) + pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option = None; + + //#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| { @@ -563,14 +665,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 } @@ -591,6 +685,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, @@ -600,6 +695,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 @@ -607,8 +715,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); @@ -617,17 +728,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, @@ -646,8 +751,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, @@ -672,6 +776,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)?; @@ -699,16 +804,10 @@ >::remove_prefix(collection_id); - if CollectionCount::get() > 0 - { - // bound couter - let total = CollectionCount::get() - .checked_sub(1) - .ok_or(Error::::NumOverflow)?; + DestroyedCollectionCount::put(DestroyedCollectionCount::get() + .checked_add(1) + .ok_or(Error::::NumOverflow)?); - CollectionCount::put(total); - } - Ok(()) } @@ -725,6 +824,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)?; @@ -749,6 +849,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)?; @@ -772,6 +873,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)?; @@ -798,6 +900,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)?; @@ -822,6 +925,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)?; @@ -847,6 +951,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)?; @@ -882,6 +987,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)?; @@ -906,14 +1012,14 @@ /// /// * 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)?; let mut target_collection = Self::get_collection(collection_id)?; Self::check_owner_permissions(&target_collection, sender)?; - target_collection.sponsor = new_sponsor; - target_collection.sponsor_confirmed = false; + target_collection.sponsorship = SponsorshipState::Unconfirmed(new_sponsor); Self::save_collection(target_collection); Ok(()) @@ -927,14 +1033,18 @@ /// /// * collection_id. #[weight = ::WeightInfo::confirm_sponsorship()] + #[transactional] pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult { let sender = ensure_signed(origin)?; let mut target_collection = Self::get_collection(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); Self::save_collection(target_collection); Ok(()) @@ -950,6 +1060,7 @@ /// /// * collection_id. #[weight = ::WeightInfo::remove_collection_sponsor()] + #[transactional] pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -957,8 +1068,7 @@ let mut target_collection = Self::get_collection(collection_id)?; Self::check_owner_permissions(&target_collection, sender)?; - target_collection.sponsor = T::AccountId::default(); - target_collection.sponsor_confirmed = false; + target_collection.sponsorship = SponsorshipState::Disabled; Self::save_collection(target_collection); Ok(()) @@ -989,6 +1099,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)?; @@ -1002,7 +1113,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 /// @@ -1023,6 +1134,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); @@ -1056,6 +1168,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)?; @@ -1113,6 +1226,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)?; let collection = Self::get_collection(collection_id)?; @@ -1136,6 +1250,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)?; @@ -1200,6 +1315,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)?; @@ -1234,7 +1350,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 { @@ -1280,6 +1396,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, @@ -1324,6 +1441,7 @@ /// /// * schema: SchemaVersion: enum #[weight = ::WeightInfo::set_schema_version()] + #[transactional] pub fn set_schema_version( origin, collection_id: CollectionId, @@ -1351,6 +1469,7 @@ /// /// * schema: String representing the offchain data schema. #[weight = ::WeightInfo::set_offchain_schema()] + #[transactional] pub fn set_offchain_schema( origin, collection_id: CollectionId, @@ -1382,6 +1501,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, @@ -1413,6 +1533,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, @@ -1433,6 +1554,7 @@ // Sudo permissions function #[weight = ::WeightInfo::set_chain_limits()] + #[transactional] pub fn set_chain_limits( origin, limits: ChainLimits @@ -1457,6 +1579,7 @@ /// * enable flag /// #[weight = ::WeightInfo::enable_contract_sponsoring()] + #[transactional] pub fn enable_contract_sponsoring( origin, contract_address: T::AccountId, @@ -1492,6 +1615,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, @@ -1519,6 +1643,7 @@ /// /// - `enable`: . #[weight = ::WeightInfo::toggle_contract_white_list()] + #[transactional] pub fn toggle_contract_white_list( origin, contract_address: T::AccountId, @@ -1546,6 +1671,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, @@ -1573,6 +1699,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, @@ -1589,6 +1716,7 @@ } #[weight = ::WeightInfo::set_collection_limits()] + #[transactional] pub fn set_collection_limits( origin, collection_id: u32, @@ -1754,9 +1882,6 @@ Self::add_refungible_item(collection, item)?; } }; - - // call event - Self::deposit_event(RawEvent::ItemCreated(collection_id, ::get(collection_id), owner)); Ok(()) } @@ -1782,6 +1907,7 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, (*owner).clone(), new_balance); + Self::deposit_event(RawEvent::ItemCreated(collection_id, 0, owner.clone())); Ok(()) } @@ -1793,8 +1919,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)?; @@ -1807,6 +1939,7 @@ .ok_or(Error::::NumOverflow)?; >::insert(collection_id, owner.clone(), new_balance); + Self::deposit_event(RawEvent::ItemCreated(collection_id, current_index, owner)); Ok(()) } @@ -1829,6 +1962,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(()) } @@ -1847,9 +1981,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 @@ -1863,7 +1996,7 @@ .owner .iter() .position(|i| i.owner == *owner) - .unwrap(); + .expect("owned item is exists"); token.owner.remove(index); let owner_count = token.owner.len(); @@ -2100,7 +2233,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); @@ -2111,10 +2244,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); @@ -2128,7 +2261,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); @@ -2140,7 +2273,7 @@ .owner .iter_mut() .find(|i| i.owner == owner) - .unwrap() + .expect("old owner does present in refungible") .fraction -= value; // separate amount @@ -2150,7 +2283,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 @@ -2472,13 +2605,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. @@ -2496,7 +2622,10 @@ // sponsor timeout let block_number = >::block_number() as T::BlockNumber; + 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)); let limit_time = last_tx_block + limit.into(); @@ -2508,9 +2637,10 @@ // check free create limit if (collection.limits.sponsored_data_size >= (_properties.len() as u32)) && - (collection.sponsor_confirmed) + (sponsored) { - Some(collection.sponsor) + collection.sponsorship.sponsor() + .cloned() } else { None } @@ -2519,7 +2649,7 @@ let collection = >::get(collection_id)?; let mut sponsor_transfer = false; - if collection.sponsor_confirmed { + if collection.sponsorship.confirmed() { let collection_limits = collection.limits; let collection_mode = collection.mode; @@ -2606,7 +2736,8 @@ if !sponsor_transfer { None } else { - Some(collection.sponsor) + collection.sponsorship.sponsor() + .cloned() } } --- a/pallets/nft/src/mock.rs +++ b/pallets/nft/src/mock.rs @@ -133,9 +133,14 @@ type WeightInfo = pallet_contracts::weights::SubstrateWeight; } +parameter_types! { + pub const CollectionCreationPrice: u64 = 1_000_000_000_000; +} + impl pallet_template::Config for Test { type Event = (); type WeightInfo = (); + type CollectionCreationPrice = CollectionCreationPrice; } // Build genesis storage according to the mock runtime. --- 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" + } + }, "Collection": { "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 @@ -376,8 +376,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(), + }); }); } @@ -395,8 +396,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 }); }); } @@ -434,8 +434,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