--- a/Cargo.lock +++ b/Cargo.lock @@ -5731,7 +5731,7 @@ [[package]] name = "pallet-common" -version = "0.1.0" +version = "0.1.2" dependencies = [ "ethereum", "evm-coder", @@ -6320,7 +6320,7 @@ [[package]] name = "pallet-refungible" -version = "0.1.0" +version = "0.1.1" dependencies = [ "frame-benchmarking", "frame-support", @@ -12733,7 +12733,7 @@ [[package]] name = "up-data-structs" -version = "0.1.0" +version = "0.1.1" dependencies = [ "derivative", "frame-support", --- /dev/null +++ b/pallets/common/CHANGELOG.MD @@ -0,0 +1,10 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.1.2] - 2022-07-20 + +### Fixed + +- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid + mutability modifiers, causing invalid stub/abi generation. --- a/pallets/common/Cargo.toml +++ b/pallets/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-common" -version = "0.1.0" +version = "0.1.2" license = "GPLv3" edition = "2021" --- a/pallets/common/src/dispatch.rs +++ b/pallets/common/src/dispatch.rs @@ -1,3 +1,5 @@ +//! Module with interfaces for dispatching collections. + use frame_support::{ dispatch::{ DispatchResultWithPostInfo, PostDispatchInfo, Weight, DispatchErrorWithPostInfo, @@ -20,7 +22,10 @@ // submit_logs is measured as part of collection pallets } -/// Helper function to implement substrate calls for common collection methods +/// Helper function to implement substrate calls for common collection methods. +/// +/// * `collection` - The collection on which to call the method. +/// * `call` - The function in which to call the corresponding method from [`CommonCollectionOperations`]. pub fn dispatch_tx< T: Config, C: FnOnce(&dyn CommonCollectionOperations) -> DispatchResultWithPostInfo, @@ -64,15 +69,31 @@ result } +/// Interface for working with different collections through the dispatcher. pub trait CollectionDispatch { + /// Create a collection. The collection will be created according to the value of [`data.mode`](CreateCollectionData::mode). + /// + /// * `sender` - The user who will become the owner of the collection. + /// * `data` - Description of the created collection. fn create( sender: T::CrossAccountId, data: CreateCollectionData, ) -> DispatchResult; + + /// Delete the collection. + /// + /// * `sender` - The owner of the collection. + /// * `handle` - Collection handle. fn destroy(sender: T::CrossAccountId, handle: CollectionHandle) -> DispatchResult; + /// Get a specialized collection from the handle. + /// + /// * `handle` - Collection handle. fn dispatch(handle: CollectionHandle) -> Self; + + /// Get the collection handle for the corresponding implementation. fn into_inner(self) -> CollectionHandle; + /// Get the implementation of [`CommonCollectionOperations`]. fn as_dyn(&self) -> &dyn CommonCollectionOperations; } --- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! This module contains the implementation of pallet methods for evm. + use evm_coder::{ solidity_interface, solidity, ToLog, types::*, @@ -29,29 +31,40 @@ use crate::{Pallet, CollectionHandle, Config, CollectionProperties}; +/// Events for ethereum collection helper. #[derive(ToLog)] pub enum CollectionHelpersEvents { + /// The collection has been created. CollectionCreated { + /// Collection owner. #[indexed] owner: address, + + /// Collection ID. #[indexed] collection_id: address, }, } /// Does not always represent a full collection, for RFT it is either -/// collection (Implementing ERC721), or specific collection token (Implementing ERC20) +/// collection (Implementing ERC721), or specific collection token (Implementing ERC20). pub trait CommonEvmHandler { const CODE: &'static [u8]; + /// Call precompiled handle. fn call(self, handle: &mut impl PrecompileHandle) -> Option; } +/// @title A contract that allows you to work with collections. #[solidity_interface(name = "Collection")] impl CollectionHandle where T::AccountId: From<[u8; 32]>, { + /// Set collection property. + /// + /// @param key Property key. + /// @param value Propery value. fn set_collection_property( &mut self, caller: caller, @@ -68,6 +81,9 @@ .map_err(dispatch_to_evm::) } + /// Delete collection property. + /// + /// @param key Property key. fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> { let caller = T::CrossAccountId::from_eth(caller); let key = >::from(key) @@ -77,7 +93,12 @@ >::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::) } - /// Throws error if key not found + /// Get collection property. + /// + /// @dev Throws error if key not found. + /// + /// @param key Property key. + /// @return bytes The property corresponding to the key. fn collection_property(&self, key: string) -> Result { let key = >::from(key) .try_into() @@ -89,6 +110,11 @@ Ok(prop.to_vec()) } + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result { check_is_owner_or_admin(caller, self)?; @@ -98,6 +124,9 @@ save(self) } + /// Collection sponsorship confirmation. + /// + /// @dev After setting the sponsor for the collection, it must be confirmed with this function. fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result { let caller = T::CrossAccountId::from_eth(caller); if !self @@ -109,6 +138,16 @@ save(self) } + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "accountTokenOwnershipLimit", + /// "sponsoredDataSize", + /// "sponsoredDataRateLimit", + /// "tokenLimit", + /// "sponsorTransferTimeout", + /// "sponsorApproveTimeout" + /// @param value Value of the limit. #[solidity(rename_selector = "setCollectionLimit")] fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result { check_is_owner_or_admin(caller, self)?; @@ -145,6 +184,13 @@ save(self) } + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "ownerCanTransfer", + /// "ownerCanDestroy", + /// "transfersEnabled" + /// @param value Value of the limit. #[solidity(rename_selector = "setCollectionLimit")] fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result { check_is_owner_or_admin(caller, self)?; @@ -172,11 +218,18 @@ save(self) } + /// Get contract address. fn contract_address(&self, _caller: caller) -> Result
{ Ok(crate::eth::collection_id_to_address(self.id)) } - fn add_collection_admin_substrate(&self, caller: caller, new_admin: uint256) -> Result { + /// Add collection admin by substrate address. + /// @param new_admin Substrate administrator address. + fn add_collection_admin_substrate( + &mut self, + caller: caller, + new_admin: uint256, + ) -> Result { let caller = T::CrossAccountId::from_eth(caller); let mut new_admin_arr: [u8; 32] = Default::default(); new_admin.to_big_endian(&mut new_admin_arr); @@ -186,35 +239,44 @@ Ok(()) } + /// Remove collection admin by substrate address. + /// @param admin Substrate administrator address. fn remove_collection_admin_substrate( - &self, + &mut self, caller: caller, - new_admin: uint256, + admin: uint256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); - let mut new_admin_arr: [u8; 32] = Default::default(); - new_admin.to_big_endian(&mut new_admin_arr); - let account_id = T::AccountId::from(new_admin_arr); - let new_admin = T::CrossAccountId::from_sub(account_id); - >::toggle_admin(self, &caller, &new_admin, false) - .map_err(dispatch_to_evm::)?; + let mut admin_arr: [u8; 32] = Default::default(); + admin.to_big_endian(&mut admin_arr); + let account_id = T::AccountId::from(admin_arr); + let admin = T::CrossAccountId::from_sub(account_id); + >::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::)?; Ok(()) } - fn add_collection_admin(&self, caller: caller, new_admin: address) -> Result { + /// Add collection admin. + /// @param new_admin Address of the added administrator. + fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result { let caller = T::CrossAccountId::from_eth(caller); let new_admin = T::CrossAccountId::from_eth(new_admin); >::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::)?; Ok(()) } - fn remove_collection_admin(&self, caller: caller, admin: address) -> Result { + /// Remove collection admin. + /// + /// @param new_admin Address of the removed administrator. + fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result { let caller = T::CrossAccountId::from_eth(caller); let admin = T::CrossAccountId::from_eth(admin); >::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::)?; Ok(()) } + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' #[solidity(rename_selector = "setCollectionNesting")] fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result { check_is_owner_or_admin(caller, self)?; @@ -235,6 +297,10 @@ save(self) } + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + /// @param collections Addresses of collections that will be available for nesting. #[solidity(rename_selector = "setCollectionNesting")] fn set_nesting( &mut self, @@ -280,6 +346,10 @@ save(self) } + /// Set the collection access method. + /// @param mode Access mode + /// 0 for Normal + /// 1 for AllowList fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result { check_is_owner_or_admin(caller, self)?; let permissions = CollectionPermissions { @@ -300,20 +370,29 @@ save(self) } - fn add_to_collection_allow_list(&self, caller: caller, user: address) -> Result { + /// Add the user to the allowed list. + /// + /// @param user Address of a trusted user. + fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result { let caller = T::CrossAccountId::from_eth(caller); let user = T::CrossAccountId::from_eth(user); >::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::)?; Ok(()) } - fn remove_from_collection_allow_list(&self, caller: caller, user: address) -> Result { + /// Remove the user from the allowed list. + /// + /// @param user Address of a removed user. + fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result { let caller = T::CrossAccountId::from_eth(caller); let user = T::CrossAccountId::from_eth(user); >::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::)?; Ok(()) } + /// Switch permission for minting. + /// + /// @param mode Enable if "true". fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result { check_is_owner_or_admin(caller, self)?; let permissions = CollectionPermissions { @@ -351,6 +430,7 @@ Ok(()) } +/// Get the "tokenURI" key as [PropertyKey](up_data_structs::PropertyKey). pub fn token_uri_key() -> up_data_structs::PropertyKey { b"tokenURI" .to_vec() --- a/pallets/common/src/eth.rs +++ b/pallets/common/src/eth.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! The module contains a number of functions for converting and checking ethereum identifiers. + use up_data_structs::CollectionId; use sp_core::H160; @@ -23,6 +25,7 @@ 0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e, ]; +/// Maps the ethereum address of the collection in substrate. pub fn map_eth_to_id(eth: &H160) -> Option { if eth[0..16] != ETH_COLLECTION_PREFIX { return None; @@ -31,6 +34,8 @@ id_bytes.copy_from_slice(ð[16..20]); Some(CollectionId(u32::from_be_bytes(id_bytes))) } + +/// Maps the substrate collection id in ethereum. pub fn collection_id_to_address(id: CollectionId) -> H160 { let mut out = [0; 20]; out[0..16].copy_from_slice(Ð_COLLECTION_PREFIX); @@ -38,6 +43,7 @@ H160(out) } +/// Check if the ethereum address is a collection. pub fn is_collection(address: &H160) -> bool { address[0..16] == ETH_COLLECTION_PREFIX } --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -14,8 +14,43 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -#![cfg_attr(not(feature = "std"), no_std)] +//! # Common pallet +//! +//! The Common pallet provides functionality for handling collections. +//! +//! ## Overview +//! +//! The Common pallet provides an interface for common collection operations for different collection types +//! (see [CommonCollectionOperations]), as well as a generic dispatcher for these, see [dispatch] module. +//! It also provides this functionality to EVM, see [erc] and [eth] modules. +//! +//! The Common pallet provides functions for: +//! +//! - Setting and approving collection sponsor. +//! - Get\set\delete allow list. +//! - Get\set\delete collection properties. +//! - Get\set\delete collection property permissions. +//! - Get\set\delete token property permissions. +//! - Get\set\delete collection administrators. +//! - Checking access permissions. +//! +//! ### Terminology +//! **Collection sponsor** - For the collection, you can set a sponsor, at whose expense it will +//! be possible to mint tokens. +//! +//! **Allow list** - List of users who have the right to minting tokens. +//! +//! **Collection properties** - Collection properties are simply key-value stores where various +//! metadata can be placed. +//! +//! **Permissions on token properties** - For each property in the token can be set permission +//! to change, see [`PropertyPermission`]. +//! +//! **Collection administrator** - For a collection, you can set administrators who have the right +//! to most actions on the collection. +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; use core::ops::{Deref, DerefMut}; @@ -90,14 +125,24 @@ pub mod eth; pub mod weights; +/// Weight info. pub type SelfWeightOf = ::WeightInfo; +/// Collection handle contains information about collection data and id. +/// Also provides functionality to count consumed gas. +/// +/// CollectionHandle is used as a generic wrapper for collections of all types. +/// It allows to perform common operations and queries on any collection type, +/// both completely general for all, as well as their respective implementations of [`CommonCollectionOperations`]. #[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"] pub struct CollectionHandle { + /// Collection id pub id: CollectionId, collection: Collection, + /// Substrate recorder for counting consumed gas pub recorder: SubstrateRecorder, } + impl WithRecorder for CollectionHandle { fn recorder(&self) -> &SubstrateRecorder { &self.recorder @@ -106,7 +151,9 @@ self.recorder } } + impl CollectionHandle { + /// Same as [CollectionHandle::new] but with an explicit gas limit. pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option { >::get(id).map(|collection| Self { id, @@ -115,6 +162,7 @@ }) } + /// Same as [CollectionHandle::new] but with an existed [`SubstrateRecorder`]. pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder) -> Option { >::get(id).map(|collection| Self { id, @@ -123,14 +171,18 @@ }) } + /// Retrives collection data from storage and creates collection handle with default parameters. + /// If collection not found return `None` pub fn new(id: CollectionId) -> Option { Self::new_with_gas_limit(id, u64::MAX) } + /// Same as [`CollectionHandle::new`] but if collection not found [CollectionNotFound](Error::CollectionNotFound) returned. pub fn try_get(id: CollectionId) -> Result { Ok(Self::new(id).ok_or(>::CollectionNotFound)?) } + /// Consume gas for reading. pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> { self.recorder .consume_gas(T::GasWeightMapping::weight_to_gas( @@ -140,6 +192,7 @@ )) } + /// Consume gas for writing. pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> { self.recorder .consume_gas(T::GasWeightMapping::weight_to_gas( @@ -148,16 +201,27 @@ .saturating_mul(writes), )) } + + /// Save collection to storage. pub fn save(self) -> DispatchResult { >::insert(self.id, self.collection); Ok(()) } + /// Set collection sponsor. + /// + /// Unique collections allows sponsoring for certain actions. + /// This method allows you to set the sponsor of the collection. + /// In order for sponsorship to become active, it must be confirmed through [`Self::confirm_sponsorship`]. pub fn set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult { self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor); Ok(()) } + /// Confirm sponsorship + /// + /// In order for the sponsorship to become active, the user set as the sponsor must confirm their participation. + /// Before confirming sponsorship, the user must be specified as the sponsor of the collection via [`Self::set_sponsor`]. pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> Result { if self.collection.sponsorship.pending_sponsor() != Some(sender) { return Ok(false); @@ -168,7 +232,7 @@ } /// Checks that the collection was created with, and must be operated upon through **Unique API**. - /// Now check only the `external_collection` flag and if it's **true**, then return `CollectionIsExternal` error. + /// Now check only the `external_collection` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error. pub fn check_is_internal(&self) -> DispatchResult { if self.external_collection { return Err(>::CollectionIsExternal)?; @@ -178,7 +242,7 @@ } /// Checks that the collection was created with, and must be operated upon through an **assimilated API**. - /// Now check only the `external_collection` flag and if it's **false**, then return `CollectionIsInternal` error. + /// Now check only the `external_collection` flag and if it's **false**, then return [`Error::CollectionIsInternal`] error. pub fn check_is_external(&self) -> DispatchResult { if !self.external_collection { return Err(>::CollectionIsInternal)?; @@ -203,23 +267,34 @@ } impl CollectionHandle { - pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult { - ensure!(*subject.as_sub() == self.owner, >::NoPermission); + /// Checks if the `user` is the owner of the collection. + pub fn check_is_owner(&self, user: &T::CrossAccountId) -> DispatchResult { + ensure!(*user.as_sub() == self.owner, >::NoPermission); Ok(()) } - pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool { - *subject.as_sub() == self.owner || >::get((self.id, subject)) + + /// Returns **true** if the `user` is the owner or administrator of the collection. + pub fn is_owner_or_admin(&self, user: &T::CrossAccountId) -> bool { + *user.as_sub() == self.owner || >::get((self.id, user)) } - pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult { - ensure!(self.is_owner_or_admin(subject), >::NoPermission); + + /// Checks if the `user` is the owner or administrator of the collection. + pub fn check_is_owner_or_admin(&self, user: &T::CrossAccountId) -> DispatchResult { + ensure!(self.is_owner_or_admin(user), >::NoPermission); Ok(()) } + + /// Return **true** if `user` was not allowed to have tokens, and he can ignore such restrictions. pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool { self.limits.owner_can_transfer() && self.is_owner_or_admin(user) } + + /// Return **true** if `user` does not have enough token parts, and he can ignore such restrictions. pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool { self.limits.owner_can_transfer() && self.is_owner_or_admin(user) } + + /// Checks if the user is in the allow list. If not [Error::AddressNotInAllowlist] returns. pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult { ensure!( >::get((self.id, user)), @@ -249,21 +324,34 @@ + TypeInfo + account::Config { + /// Weight information for functions of this pallet. type WeightInfo: WeightInfo; + + /// Events compatible with [`frame_system::Config::Event`]. type Event: IsType<::Event> + From>; + /// Handler of accounts and payment. type Currency: Currency; + /// Set price to create a collection. #[pallet::constant] type CollectionCreationPrice: Get< <::Currency as Currency>::Balance, >; + + /// Dispatcher of operations on collections. type CollectionDispatch: CollectionDispatch; + /// Account which holds the chain's treasury. type TreasuryAccountId: Get; + + /// Address under which the CollectionHelper contract would be available. type ContractAddress: Get; + /// Mapper for token addresses to Ethereum addresses. type EvmTokenAddressMapping: TokenAddressMapping; + + /// Mapper for token addresses to [`CrossAccountId`]. type CrossTokenAddressMapping: TokenAddressMapping; } @@ -276,6 +364,7 @@ #[pallet::extra_constants] impl Pallet { + /// Maximum admins per collection. pub fn collection_admins_limit() -> u32 { COLLECTION_ADMINS_LIMIT } @@ -285,94 +374,116 @@ #[pallet::generate_deposit(pub fn deposit_event)] pub enum Event { /// New collection was created - /// - /// # Arguments - /// - /// * collection_id: Globally unique identifier of newly created collection. - /// - /// * mode: [CollectionMode] converted into u8. - /// - /// * account_id: Collection owner. - CollectionCreated(CollectionId, u8, T::AccountId), + CollectionCreated( + /// Globally unique identifier of newly created collection. + CollectionId, + /// [`CollectionMode`] converted into _u8_. + u8, + /// Collection owner. + T::AccountId, + ), /// New collection was destroyed - /// - /// # Arguments - /// - /// * collection_id: Globally unique identifier of collection. - CollectionDestroyed(CollectionId), + CollectionDestroyed( + /// Globally unique identifier of collection. + CollectionId, + ), /// New item was created. - /// - /// # Arguments - /// - /// * collection_id: Id of the collection where item was created. - /// - /// * item_id: Id of an item. Unique within the collection. - /// - /// * recipient: Owner of newly created item - /// - /// * amount: Always 1 for NFT - ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128), + ItemCreated( + /// Id of the collection where item was created. + CollectionId, + /// Id of an item. Unique within the collection. + TokenId, + /// Owner of newly created item + T::CrossAccountId, + /// Always 1 for NFT + u128, + ), /// Collection item was burned. - /// - /// # Arguments - /// - /// * collection_id. - /// - /// * item_id: Identifier of burned NFT. - /// - /// * owner: which user has destroyed its tokens - /// - /// * amount: Always 1 for NFT - ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128), + ItemDestroyed( + /// Id of the collection where item was destroyed. + CollectionId, + /// Identifier of burned NFT. + TokenId, + /// Which user has destroyed its tokens. + T::CrossAccountId, + /// Amount of token pieces destroed. Always 1 for NFT. + u128, + ), /// Item was transferred - /// - /// * collection_id: Id of collection to which item is belong - /// - /// * item_id: Id of an item - /// - /// * sender: Original owner of item - /// - /// * recipient: New owner of item - /// - /// * amount: Always 1 for NFT Transfer( + /// Id of collection to which item is belong. CollectionId, + /// Id of an item. TokenId, + /// Original owner of item. T::CrossAccountId, + /// New owner of item. T::CrossAccountId, + /// Amount of token pieces transfered. Always 1 for NFT. u128, ), - /// * collection_id - /// - /// * item_id - /// - /// * sender - /// - /// * spender - /// - /// * amount + /// Amount pieces of token owned by `sender` was approved for `spender`. Approved( + /// Id of collection to which item is belong. CollectionId, + /// Id of an item. TokenId, + /// Original owner of item. T::CrossAccountId, + /// Id for which the approval was granted. T::CrossAccountId, + /// Amount of token pieces transfered. Always 1 for NFT. u128, ), - CollectionPropertySet(CollectionId, PropertyKey), + /// The colletion property has been set. + CollectionPropertySet( + /// Id of collection to which property has been set. + CollectionId, + /// The property that was set. + PropertyKey, + ), - CollectionPropertyDeleted(CollectionId, PropertyKey), + /// The property has been deleted. + CollectionPropertyDeleted( + /// Id of collection to which property has been deleted. + CollectionId, + /// The property that was deleted. + PropertyKey, + ), - TokenPropertySet(CollectionId, TokenId, PropertyKey), + /// The token property has been set. + TokenPropertySet( + /// Identifier of the collection whose token has the property set. + CollectionId, + /// The token for which the property was set. + TokenId, + /// The property that was set. + PropertyKey, + ), - TokenPropertyDeleted(CollectionId, TokenId, PropertyKey), + /// The token property has been deleted. + TokenPropertyDeleted( + /// Identifier of the collection whose token has the property deleted. + CollectionId, + /// The token for which the property was deleted. + TokenId, + /// The property that was deleted. + PropertyKey, + ), - PropertyPermissionSet(CollectionId, PropertyKey), + /// The colletion property permission has been set. + PropertyPermissionSet( + /// Id of collection to which property permission has been set. + CollectionId, + /// The property permission that was set. + PropertyKey, + ), } #[pallet::error] @@ -460,13 +571,16 @@ CollectionIsInternal, } + /// Storage of the count of created collections. #[pallet::storage] pub type CreatedCollectionCount = StorageValue; + + /// Storage of the count of deleted collections. #[pallet::storage] pub type DestroyedCollectionCount = StorageValue; - /// Collection info + /// Storage of collection info. #[pallet::storage] pub type CollectionById = StorageMap< Hasher = Blake2_128Concat, @@ -475,7 +589,7 @@ QueryKind = OptionQuery, >; - /// Collection properties + /// Storage of collection properties. #[pallet::storage] #[pallet::getter(fn collection_properties)] pub type CollectionProperties = StorageMap< @@ -486,6 +600,7 @@ OnEmpty = up_data_structs::CollectionProperties, >; + /// Storage of collection properties permissions. #[pallet::storage] #[pallet::getter(fn property_permissions)] pub type CollectionPropertyPermissions = StorageMap< @@ -495,6 +610,7 @@ QueryKind = ValueQuery, >; + /// Storage of collection admins count. #[pallet::storage] pub type AdminAmount = StorageMap< Hasher = Blake2_128Concat, @@ -525,7 +641,7 @@ QueryKind = ValueQuery, >; - /// Not used by code, exists only to provide some types to metadata + /// Not used by code, exists only to provide some types to metadata. #[pallet::storage] pub type DummyStorageValue = StorageValue< Value = ( @@ -619,7 +735,9 @@ } impl Pallet { - /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens + /// Enshure that receiver address is correct. + /// + /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens. pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult { ensure!( &T::CrossAccountId::from_eth(H160([0; 20])) != receiver, @@ -627,19 +745,27 @@ ); Ok(()) } + + /// Get a vector of collection admins. pub fn adminlist(collection: CollectionId) -> Vec { >::iter_prefix((collection,)) .map(|(a, _)| a) .collect() } + + /// Get a vector of users allowed to mint tokens. pub fn allowlist(collection: CollectionId) -> Vec { >::iter_prefix((collection,)) .map(|(a, _)| a) .collect() } + + /// Is `user` allowed to mint token in `collection`. pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool { >::get((collection, user)) } + + /// Get statistics of collections. pub fn collection_stats() -> CollectionStats { let created = >::get(); let destroyed = >::get(); @@ -650,6 +776,7 @@ } } + /// Get the effective limits for the collection. pub fn effective_collection_limits(collection: CollectionId) -> Option { let collection = >::get(collection); if collection.is_none() { @@ -683,6 +810,7 @@ Some(effective_limits) } + /// Returns information about the `collection` adapted for rpc. pub fn rpc_collection(collection: CollectionId) -> Option> { let Collection { name, @@ -758,6 +886,11 @@ } impl Pallet { + /// Create new collection. + /// + /// * `owner` - The owner of the collection. + /// * `data` - Description of the created collection. + /// * `is_external` - Marks that collection managet by not "Unique network". pub fn init_collection( owner: T::CrossAccountId, data: CreateCollectionData, @@ -833,7 +966,7 @@ ), ); ::Currency::settle( - &owner.as_sub(), + owner.as_sub(), imbalance, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive, @@ -858,6 +991,10 @@ Ok(id) } + /// Destroy collection. + /// + /// * `collection` - Collection handler. + /// * `sender` - The owner or administrator of the collection. pub fn destroy_collection( collection: CollectionHandle, sender: &T::CrossAccountId, @@ -886,6 +1023,11 @@ Ok(()) } + /// Set collection property. + /// + /// * `collection` - Collection handler. + /// * `sender` - The owner or administrator of the collection. + /// * `property` - The property to set. pub fn set_collection_property( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -904,6 +1046,11 @@ Ok(()) } + /// Set scouped collection property. + /// + /// * `collection_id` - ID of the collection for which the property is being set. + /// * `scope` - Property scope. + /// * `property` - The property to set. pub fn set_scoped_collection_property( collection_id: CollectionId, scope: PropertyScope, @@ -917,6 +1064,11 @@ Ok(()) } + /// Set scouped collection properties. + /// + /// * `collection_id` - ID of the collection for which the properties is being set. + /// * `scope` - Property scope. + /// * `properties` - The properties to set. pub fn set_scoped_collection_properties( collection_id: CollectionId, scope: PropertyScope, @@ -930,6 +1082,11 @@ Ok(()) } + /// Set collection properties. + /// + /// * `collection` - Collection handler. + /// * `sender` - The owner or administrator of the collection. + /// * `properties` - The properties to set. #[transactional] pub fn set_collection_properties( collection: &CollectionHandle, @@ -943,6 +1100,11 @@ Ok(()) } + /// Delete collection property. + /// + /// * `collection` - Collection handler. + /// * `sender` - The owner or administrator of the collection. + /// * `property` - The property to delete. pub fn delete_collection_property( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -963,6 +1125,11 @@ Ok(()) } + /// Delete collection properties. + /// + /// * `collection` - Collection handler. + /// * `sender` - The owner or administrator of the collection. + /// * `properties` - The properties to delete. #[transactional] pub fn delete_collection_properties( collection: &CollectionHandle, @@ -976,7 +1143,12 @@ Ok(()) } - // For migrations + /// Set collection propetry permission without any checks. + /// + /// Used for migrations. + /// + /// * `collection` - Collection handler. + /// * `property_permissions` - Property permissions. pub fn set_property_permission_unchecked( collection: CollectionId, property_permission: PropertyKeyPermission, @@ -988,6 +1160,11 @@ Ok(()) } + /// Set collection property permission. + /// + /// * `collection` - Collection handler. + /// * `sender` - The owner or administrator of the collection. + /// * `property_permission` - Property permission. pub fn set_property_permission( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -1018,6 +1195,11 @@ Ok(()) } + /// Set token property permission. + /// + /// * `collection` - Collection handler. + /// * `sender` - The owner or administrator of the collection. + /// * `property_permissions` - Property permissions. #[transactional] pub fn set_token_property_permissions( collection: &CollectionHandle, @@ -1031,6 +1213,7 @@ Ok(()) } + /// Get collection property. pub fn get_collection_property( collection_id: CollectionId, key: &PropertyKey, @@ -1038,6 +1221,7 @@ Self::collection_properties(collection_id).get(key).cloned() } + /// Convert byte vector to property key vector. pub fn bytes_keys_to_property_keys( keys: Vec>, ) -> Result, DispatchError> { @@ -1049,6 +1233,7 @@ .collect::, DispatchError>>() } + /// Get properties according to given keys. pub fn filter_collection_properties( collection_id: CollectionId, keys: Option>, @@ -1076,6 +1261,7 @@ Ok(properties) } + /// Get property permissions according to given keys. pub fn filter_property_permissions( collection_id: CollectionId, keys: Option>, @@ -1105,6 +1291,7 @@ Ok(key_permissions) } + /// Toggle `user` participation in the `collection`'s allow list. pub fn toggle_allowlist( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -1124,6 +1311,7 @@ Ok(()) } + /// Toggle `user` participation in the `collection`'s admin list. pub fn toggle_admin( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -1159,6 +1347,7 @@ Ok(()) } + /// Merge set fields from `new_limit` to `old_limit`. pub fn clamp_limits( mode: CollectionMode, old_limit: &CollectionLimits, @@ -1204,20 +1393,22 @@ Ok(new_limit) } + /// Merge set fields from `new_permission` to `old_permission`. pub fn clamp_permissions( _mode: CollectionMode, - old_limit: &CollectionPermissions, - mut new_limit: CollectionPermissions, + old_permission: &CollectionPermissions, + mut new_permission: CollectionPermissions, ) -> Result { - limit_default_clone!(old_limit, new_limit, + limit_default_clone!(old_permission, new_permission, access => {}, mint_mode => {}, nesting => { /* todo check for permissive, if only it gets out of benchmarks */ }, ); - Ok(new_limit) + Ok(new_permission) } } +/// Indicates unsupported methods by returning [Error::UnsupportedOperation]. #[macro_export] macro_rules! unsupported { () => { @@ -1225,32 +1416,72 @@ }; } -/// Worst cases +/// Return weights for various worst-case operations. pub trait CommonWeightInfo { + /// Weight of item creation. fn create_item() -> Weight; + + /// Weight of items creation. fn create_multiple_items(amount: &[CreateItemData]) -> Weight; + + /// Weight of items creation. fn create_multiple_items_ex(cost: &CreateItemExData) -> Weight; + + /// The weight of the burning item. fn burn_item() -> Weight; + + /// Property setting weight. + /// + /// * `amount`- The number of properties to set. fn set_collection_properties(amount: u32) -> Weight; + + /// Collection property deletion weight. + /// + /// * `amount`- The number of properties to set. fn delete_collection_properties(amount: u32) -> Weight; + + /// Token property setting weight. + /// + /// * `amount`- The number of properties to set. fn set_token_properties(amount: u32) -> Weight; + + /// Token property deletion weight. + /// + /// * `amount`- The number of properties to delete. fn delete_token_properties(amount: u32) -> Weight; + + /// Token property permissions set weight. + /// + /// * `amount`- The number of property permissions to set. fn set_token_property_permissions(amount: u32) -> Weight; + + /// Transfer price of the token or its parts. fn transfer() -> Weight; + + /// The price of setting the permission of the operation from another user. fn approve() -> Weight; + + /// Transfer price from another user. fn transfer_from() -> Weight; + + /// The price of burning a token from another user. fn burn_from() -> Weight; /// Differs from burn_item in case of Fungible and Refungible, as it should burn - /// whole users's balance + /// whole users's balance. /// - /// This method shouldn't be used directly, as it doesn't count breadth price, use `burn_recursively` instead + /// This method shouldn't be used directly, as it doesn't count breadth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead fn burn_recursively_self_raw() -> Weight; - /// Cost of iterating over `amount` children while burning, without counting child burning itself + + /// Cost of iterating over `amount` children while burning, without counting child burning itself. /// - /// This method shouldn't be used directly, as it doesn't count depth price, use `burn_recursively` instead + /// This method shouldn't be used directly, as it doesn't count depth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead fn burn_recursively_breadth_raw(amount: u32) -> Weight; + /// The price of recursive burning a token. + /// + /// `max_selfs` - The maximum burning weight of the token itself. + /// `max_breadth` - The maximum number of nested tokens to burn. fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight { Self::burn_recursively_self_raw() .saturating_mul(max_selfs.max(1) as u64) @@ -1258,11 +1489,23 @@ } } +/// Weight info extension trait for refungible pallet. pub trait RefungibleExtensionsWeightInfo { + /// Weight of token repartition. fn repartition() -> Weight; } +/// Common collection operations. +/// +/// It wraps methods in Fungible, Nonfungible and Refungible pallets +/// and adds weight info. pub trait CommonCollectionOperations { + /// Create token. + /// + /// * `sender` - The user who mint the token and pays for the transaction. + /// * `to` - The user who will own the token. + /// * `data` - Token data. + /// * `nesting_budget` - A budget that can be spent on nesting tokens. fn create_item( &self, sender: T::CrossAccountId, @@ -1270,6 +1513,13 @@ data: CreateItemData, nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Create multiple tokens. + /// + /// * `sender` - The user who mint the token and pays for the transaction. + /// * `to` - The user who will own the token. + /// * `data` - Token data. + /// * `nesting_budget` - A budget that can be spent on nesting tokens. fn create_multiple_items( &self, sender: T::CrossAccountId, @@ -1277,18 +1527,38 @@ data: Vec, nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Create multiple tokens. + /// + /// * `sender` - The user who mint the token and pays for the transaction. + /// * `to` - The user who will own the token. + /// * `data` - Token data. + /// * `nesting_budget` - A budget that can be spent on nesting tokens. fn create_multiple_items_ex( &self, sender: T::CrossAccountId, data: CreateItemExData, nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Burn token. + /// + /// * `sender` - The user who owns the token. + /// * `token` - Token id that will burned. + /// * `amount` - The number of parts of the token that will be burned. fn burn_item( &self, sender: T::CrossAccountId, token: TokenId, amount: u128, ) -> DispatchResultWithPostInfo; + + /// Burn token and all nested tokens recursievly. + /// + /// * `sender` - The user who owns the token. + /// * `token` - Token id that will burned. + /// * `self_budget` - The budget that can be spent on burning tokens. + /// * `breadth_budget` - The budget that can be spent on burning nested tokens. fn burn_item_recursively( &self, sender: T::CrossAccountId, @@ -1296,43 +1566,95 @@ self_budget: &dyn Budget, breadth_budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Set collection properties. + /// + /// * `sender` - Must be either the owner of the collection or its admin. + /// * `properties` - Properties to be set. fn set_collection_properties( &self, sender: T::CrossAccountId, properties: Vec, ) -> DispatchResultWithPostInfo; + + /// Delete collection properties. + /// + /// * `sender` - Must be either the owner of the collection or its admin. + /// * `properties` - The properties to be removed. fn delete_collection_properties( &self, sender: &T::CrossAccountId, property_keys: Vec, ) -> DispatchResultWithPostInfo; + + /// Set token properties. + /// + /// The appropriate [`PropertyPermission`] for the token property + /// must be set with [`Self::set_token_property_permissions`]. + /// + /// * `sender` - Must be either the owner of the token or its admin. + /// * `token_id` - The token for which the properties are being set. + /// * `properties` - Properties to be set. + /// * `budget` - Budget for setting properties. fn set_token_properties( &self, sender: T::CrossAccountId, token_id: TokenId, - property: Vec, - nesting_budget: &dyn Budget, + properties: Vec, + budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Remove token properties. + /// + /// The appropriate [`PropertyPermission`] for the token property + /// must be set with [`Self::set_token_property_permissions`]. + /// + /// * `sender` - Must be either the owner of the token or its admin. + /// * `token_id` - The token for which the properties are being remove. + /// * `property_keys` - Keys to remove corresponding properties. + /// * `budget` - Budget for removing properties. fn delete_token_properties( &self, sender: T::CrossAccountId, token_id: TokenId, property_keys: Vec, - nesting_budget: &dyn Budget, + budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Set token property permissions. + /// + /// * `sender` - Must be either the owner of the token or its admin. + /// * `token_id` - The token for which the properties are being set. + /// * `properties` - Properties to be set. + /// * `budget` - Budget for setting properties. fn set_token_property_permissions( &self, sender: &T::CrossAccountId, property_permissions: Vec, ) -> DispatchResultWithPostInfo; + + /// Transfer amount of token pieces. + /// + /// * `sender` - Donor user. + /// * `to` - Recepient user. + /// * `token` - The token of which parts are being sent. + /// * `amount` - The number of parts of the token that will be transferred. + /// * `budget` - The maximum budget that can be spent on the transfer. fn transfer( &self, sender: T::CrossAccountId, to: T::CrossAccountId, token: TokenId, amount: u128, - nesting_budget: &dyn Budget, + budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Grant access to another account to transfer parts of the token owned by the calling user via [Self::transfer_from]. + /// + /// * `sender` - The user who grants access to the token. + /// * `spender` - The user to whom the rights are granted. + /// * `token` - The token to which access is granted. + /// * `amount` - The amount of pieces that another user can dispose of. fn approve( &self, sender: T::CrossAccountId, @@ -1340,6 +1662,17 @@ token: TokenId, amount: u128, ) -> DispatchResultWithPostInfo; + + /// Send parts of a token owned by another user. + /// + /// Before calling this method, you must grant rights to the calling user via [`Self::approve`]. + /// + /// * `sender` - The user who must have access to the token (see [`Self::approve`]). + /// * `from` - The user who owns the token. + /// * `to` - Recepient user. + /// * `token` - The token of which parts are being sent. + /// * `amount` - The number of parts of the token that will be transferred. + /// * `budget` - The maximum budget that can be spent on the transfer. fn transfer_from( &self, sender: T::CrossAccountId, @@ -1347,67 +1680,140 @@ to: T::CrossAccountId, token: TokenId, amount: u128, - nesting_budget: &dyn Budget, + budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + + /// Burn parts of a token owned by another user. + /// + /// Before calling this method, you must grant rights to the calling user via [`Self::approve`]. + /// + /// * `sender` - The user who must have access to the token (see [`Self::approve`]). + /// * `from` - The user who owns the token. + /// * `token` - The token of which parts are being sent. + /// * `amount` - The number of parts of the token that will be transferred. + /// * `budget` - The maximum budget that can be spent on the burn. fn burn_from( &self, sender: T::CrossAccountId, from: T::CrossAccountId, token: TokenId, amount: u128, - nesting_budget: &dyn Budget, + budget: &dyn Budget, ) -> DispatchResultWithPostInfo; + /// Check permission to nest token. + /// + /// * `sender` - The user who initiated the check. + /// * `from` - The token that is checked for embedding. + /// * `under` - Token under which to check. + /// * `budget` - The maximum budget that can be spent on the check. fn check_nesting( &self, sender: T::CrossAccountId, from: (CollectionId, TokenId), under: TokenId, - nesting_budget: &dyn Budget, + budget: &dyn Budget, ) -> DispatchResult; + /// Nest one token into another. + /// + /// * `under` - Token holder. + /// * `to_nest` - Nested token. fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId)); + /// Unnest token. + /// + /// * `under` - Token holder. + /// * `to_nest` - Token to unnest. fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId)); + /// Get all user tokens. + /// + /// * `account` - Account for which you need to get tokens. fn account_tokens(&self, account: T::CrossAccountId) -> Vec; + + /// Get all the tokens in the collection. fn collection_tokens(&self) -> Vec; + + /// Check if the token exists. + /// + /// * `token` - Id token to check. fn token_exists(&self, token: TokenId) -> bool; + + /// Get the id of the last minted token. fn last_token_id(&self) -> TokenId; + /// Get the owner of the token. + /// + /// * `token` - The token for which you need to find out the owner. fn token_owner(&self, token: TokenId) -> Option; + + /// Get the value of the token property by key. + /// + /// * `token` - Token with the property to get. + /// * `key` - Property name. fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option; - fn token_properties(&self, token_id: TokenId, keys: Option>) -> Vec; + + /// Get a set of token properties by key vector. + /// + /// * `token` - Token with the property to get. + /// * `keys` - Vector of property keys. If this parameter is [None](sp_std::result::Result), + /// then all properties are returned. + fn token_properties(&self, token: TokenId, keys: Option>) -> Vec; + /// Amount of unique collection tokens fn total_supply(&self) -> u32; - /// Amount of different tokens account has (Applicable to nonfungible/refungible) + + /// Amount of different tokens account has. + /// + /// * `account` - The account for which need to get the balance. fn account_balance(&self, account: T::CrossAccountId) -> u32; - /// Amount of specific token account have (Applicable to fungible/refungible) + + /// Amount of specific token account have. fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128; + /// Amount of token pieces fn total_pieces(&self, token: TokenId) -> Option; + + /// Get the number of parts of the token that a trusted user can manage. + /// + /// * `sender` - Trusted user. + /// * `spender` - Owner of the token. + /// * `token` - The token for which to get the value. fn allowance( &self, sender: T::CrossAccountId, spender: T::CrossAccountId, token: TokenId, ) -> u128; + + /// Get extension for RFT collection. fn refungible_extensions(&self) -> Option<&dyn RefungibleExtensions>; } +/// Extension for RFT collection. pub trait RefungibleExtensions where T: Config, { + /// Change the number of parts of the token. + /// + /// When the value changes down, this function is equivalent to burning parts of the token. + /// + /// * `sender` - The user calling the repartition operation. Must be the owner of the token. + /// * `token` - The token for which you want to change the number of parts. + /// * `amount` - The new value of the parts of the token. fn repartition( &self, - owner: &T::CrossAccountId, + sender: &T::CrossAccountId, token: TokenId, amount: u128, ) -> DispatchResultWithPostInfo; } -// Flexible enough for implementing CommonCollectionOperations +/// Merge [`DispatchResult`] with [`Weight`] into [`DispatchResultWithPostInfo`]. +/// +/// Used for [`CommonCollectionOperations`] implementations and flexible enough to do so. pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo { let post_info = PostDispatchInfo { actual_weight: Some(weight), --- a/pallets/fungible/src/common.rs +++ b/pallets/fungible/src/common.rs @@ -105,6 +105,8 @@ } } +/// Implementation of `CommonCollectionOperations` for `FungibleHandle`. It wraps FungibleHandle Pallete +/// methods and adds weight info. impl CommonCollectionOperations for FungibleHandle { fn create_item( &self, --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! ERC-20 standart support implementation. + use core::char::{REPLACEMENT_CHARACTER, decode_utf16}; use core::convert::TryInto; use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*, weight}; --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -14,6 +14,68 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! # Fungible Pallet +//! +//! The Fungible pallet provides functionality for dealing with fungible assets. +//! +//! - [`CreateItemData`] +//! - [`Config`] +//! - [`FungibleHandle`] +//! - [`Pallet`] +//! - [`TotalSupply`] +//! - [`Balance`] +//! - [`Allowance`] +//! - [`Error`] +//! +//! ## Fungible tokens +//! +//! Fungible tokens or assets are divisible and non-unique. For instance, +//! fiat currencies like the dollar are fungible: A $1 bill +//! in New York City has the same value as a $1 bill in Miami. +//! A fungible token can also be a cryptocurrency like Bitcoin: 1 BTC is worth 1 BTC, +//! no matter where it is issued. Thus, the fungibility refers to a specific currency’s +//! ability to maintain one standard value. As well, it needs to have uniform acceptance. +//! This means that a currency’s history should not be able to affect its value, +//! and this is due to the fact that each piece that is a part of the currency is equal +//! in value when compared to every other piece of that exact same currency. +//! In the world of cryptocurrencies, this is essentially a coin or a token +//! that can be replaced by another identical coin or token, and they are +//! both mutually interchangeable. A popular implementation of fungible tokens is +//! the ERC-20 token standard. +//! +//! ### ERC-20 +//! +//! The [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) (Ethereum Request for Comments 20), proposed by Fabian Vogelsteller in November 2015, +//! is a Token Standard that implements an API for tokens within Smart Contracts. +//! +//! Example functionalities ERC-20 provides: +//! +//! * transfer tokens from one account to another +//! * get the current token balance of an account +//! * get the total supply of the token available on the network +//! * approve whether an amount of token from an account can be spent by a third-party account +//! +//! ## Overview +//! +//! The module provides functionality for asset management of fungible asset, supports ERC-20 standart, includes: +//! +//! * Asset Issuance +//! * Asset Transferal +//! * Asset Destruction +//! * Delegated Asset Transfers +//! +//! **NOTE:** The created fungible asset always has `token_id` = 0. +//! So `tokenA` and `tokenB` will have different `collection_id`. +//! +//! ### Implementations +//! +//! The Fungible pallet provides implementations for the following traits. +//! +//! - [`WithRecorder`](pallet_evm_coder_substrate::WithRecorder): Trait for EVM support +//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing with collections +//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight +//! - [`CommonEvmHandler`](pallet_common::erc::CommonEvmHandler): Function for handling EVM runtime calls + #![cfg_attr(not(feature = "std"), no_std)] use core::ops::Deref; @@ -57,13 +119,14 @@ pub enum Error { /// Not Fungible item data used to mint in Fungible collection. NotFungibleDataUsedToMintFungibleCollectionToken, - /// Not default id passed as TokenId argument + /// Not default id passed as TokenId argument. + /// The default value of TokenId for Fungible collection is 0. FungibleItemsHaveNoId, - /// Tried to set data for fungible item + /// Tried to set data for fungible item. FungibleItemsDontHaveData, - /// Fungible token does not support nested + /// Fungible token does not support nesting. FungibleDisallowsNesting, - /// Setting item properties is not allowed + /// Setting item properties is not allowed. SettingPropertiesNotAllowed, } @@ -78,10 +141,12 @@ #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); + /// Total amount of fungible tokens inside a collection. #[pallet::storage] pub type TotalSupply = StorageMap; + /// Amount of tokens owned by an account inside a collection. #[pallet::storage] pub type Balance = StorageNMap< Key = ( @@ -92,6 +157,7 @@ QueryKind = ValueQuery, >; + /// Storage for delegated assets. #[pallet::storage] pub type Allowance = StorageNMap< Key = ( @@ -104,14 +170,23 @@ >; } +/// Wrapper around untyped collection handle, asserting inner collection is of fungible type. +/// Required for interaction with Fungible collections, type safety and implementation [`solidity_interface`][`evm_coder::solidity_interface`]. + pub struct FungibleHandle(pallet_common::CollectionHandle); + +/// Implementation of methods required for dispatching during runtime. impl FungibleHandle { + /// Casts [`CollectionHandle`][`pallet_common::CollectionHandle`] into [`FungibleHandle`]. pub fn cast(inner: pallet_common::CollectionHandle) -> Self { Self(inner) } + + /// Casts [`FungibleHandle`] into [`CollectionHandle`][`pallet_common::CollectionHandle`]. pub fn into_inner(self) -> pallet_common::CollectionHandle { self.0 } + /// Returns a mutable reference to the internal [`CollectionHandle`][`pallet_common::CollectionHandle`]. pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle { &mut self.0 } @@ -132,13 +207,17 @@ } } +/// Pallet implementation for fungible assets impl Pallet { + /// Initializes the collection. Returns [CollectionId] on success, [DispatchError] otherwise. pub fn init_collection( owner: T::CrossAccountId, data: CreateCollectionData, ) -> Result { >::init_collection(owner, data, false) } + + /// Destroys a collection. pub fn destroy_collection( collection: FungibleHandle, sender: &T::CrossAccountId, @@ -159,10 +238,14 @@ Ok(()) } + ///Checks if collection has tokens. Return `true` if it has. fn collection_has_tokens(collection_id: CollectionId) -> bool { >::get(collection_id) != 0 } + /// Burns the specified amount of the token. If the token balance + /// or total supply is less than the given value, + /// it will return [DispatchError]. pub fn burn( collection: &FungibleHandle, owner: &T::CrossAccountId, @@ -207,6 +290,13 @@ Ok(()) } + /// Transfers the specified amount of tokens. Will check that + /// the transfer is allowed for the token. + /// + /// - `from`: Owner of tokens to transfer. + /// - `to`: Recepient of transfered tokens. + /// - `amount`: Amount of tokens to transfer. + /// - `collection`: Collection that contains the token pub fn transfer( collection: &FungibleHandle, from: &T::CrossAccountId, @@ -277,6 +367,8 @@ Ok(()) } + /// Minting tokens for multiple IDs. + /// See [`create_item`][`Pallet::create_item`] for more details. pub fn create_multiple_items( collection: &FungibleHandle, sender: &T::CrossAccountId, @@ -378,6 +470,12 @@ )); } + /// Set allowance for the spender to `transfer` or `burn` owner's tokens. + /// + /// - `collection`: Collection that contains the token + /// - `owner`: Owner of tokens that sets the allowance. + /// - `spender`: Recipient of the allowance rights. + /// - `amount`: Amount of tokens the spender is allowed to `transfer` or `burn`. pub fn set_allowance( collection: &FungibleHandle, owner: &T::CrossAccountId, @@ -402,6 +500,13 @@ Ok(()) } + /// Checks if a non-owner has (enough) allowance from the owner to perform operations on the tokens. + /// Returns the expected remaining allowance - it should be set manually if the transaction proceeds. + /// + /// - `collection`: Collection that contains the token. + /// - `spender`: CrossAccountId who has the allowance rights. + /// - `from`: The owner of the tokens who sets the allowance. + /// - `amount`: Amount of tokens by which the allowance sholud be reduced. fn check_allowed( collection: &FungibleHandle, spender: &T::CrossAccountId, @@ -441,6 +546,11 @@ Ok(allowance) } + /// Transfer fungible tokens from one account to another. + /// Same as the [`transfer`][`Pallet::transfer`] but spender doesn't needs to be an owner of the token pieces. + /// The owner should set allowance for the spender to transfer pieces. + /// See [`set_allowance`][`Pallet::set_allowance`] for more details. + pub fn transfer_from( collection: &FungibleHandle, spender: &T::CrossAccountId, @@ -460,6 +570,11 @@ Ok(()) } + /// Burn fungible tokens from the account. + /// + /// Same as the [`burn`][`Pallet::burn`] but spender doesn't need to be an owner of the tokens. The `from` should + /// set allowance for the spender to burn tokens. + /// See [`set_allowance`][`Pallet::set_allowance`] for more details. pub fn burn_from( collection: &FungibleHandle, spender: &T::CrossAccountId, @@ -478,7 +593,13 @@ Ok(()) } - /// Delegated to `create_multiple_items` + /// Creates fungible token. + /// + /// The sender should be the owner/admin of the collection or collection should be configured + /// to allow public minting. + /// + /// - `data`: Contains user who will become the owners of the tokens and amount + /// of tokens he will receive. pub fn create_item( collection: &FungibleHandle, sender: &T::CrossAccountId, --- a/pallets/fungible/src/stubs/UniqueFungible.sol +++ b/pallets/fungible/src/stubs/UniqueFungible.sol @@ -43,6 +43,233 @@ } } +// Selector: 7d9262e6 +contract Collection is Dummy, ERC165 { + // Set collection property. + // + // @param key Property key. + // @param value Propery value. + // + // Selector: setCollectionProperty(string,bytes) 2f073f66 + function setCollectionProperty(string memory key, bytes memory value) + public + { + require(false, stub_error); + key; + value; + dummy = 0; + } + + // Delete collection property. + // + // @param key Property key. + // + // Selector: deleteCollectionProperty(string) 7b7debce + function deleteCollectionProperty(string memory key) public { + require(false, stub_error); + key; + dummy = 0; + } + + // Get collection property. + // + // @dev Throws error if key not found. + // + // @param key Property key. + // @return bytes The property corresponding to the key. + // + // Selector: collectionProperty(string) cf24fd6d + function collectionProperty(string memory key) + public + view + returns (bytes memory) + { + require(false, stub_error); + key; + dummy; + return hex""; + } + + // Set the sponsor of the collection. + // + // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + // + // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + // + // Selector: setCollectionSponsor(address) 7623402e + function setCollectionSponsor(address sponsor) public { + require(false, stub_error); + sponsor; + dummy = 0; + } + + // Collection sponsorship confirmation. + // + // @dev After setting the sponsor for the collection, it must be confirmed with this function. + // + // Selector: confirmCollectionSponsorship() 3c50e97a + function confirmCollectionSponsorship() public { + require(false, stub_error); + dummy = 0; + } + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "accountTokenOwnershipLimit", + // "sponsoredDataSize", + // "sponsoredDataRateLimit", + // "tokenLimit", + // "sponsorTransferTimeout", + // "sponsorApproveTimeout" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,uint32) 6a3841db + function setCollectionLimit(string memory limit, uint32 value) public { + require(false, stub_error); + limit; + value; + dummy = 0; + } + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "ownerCanTransfer", + // "ownerCanDestroy", + // "transfersEnabled" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,bool) 993b7fba + function setCollectionLimit(string memory limit, bool value) public { + require(false, stub_error); + limit; + value; + dummy = 0; + } + + // Get contract address. + // + // Selector: contractAddress() f6b4dfb4 + function contractAddress() public view returns (address) { + require(false, stub_error); + dummy; + return 0x0000000000000000000000000000000000000000; + } + + // Add collection admin by substrate address. + // @param new_admin Substrate administrator address. + // + // Selector: addCollectionAdminSubstrate(uint256) 5730062b + function addCollectionAdminSubstrate(uint256 newAdmin) public { + require(false, stub_error); + newAdmin; + dummy = 0; + } + + // Remove collection admin by substrate address. + // @param admin Substrate administrator address. + // + // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + function removeCollectionAdminSubstrate(uint256 admin) public { + require(false, stub_error); + admin; + dummy = 0; + } + + // Add collection admin. + // @param new_admin Address of the added administrator. + // + // Selector: addCollectionAdmin(address) 92e462c7 + function addCollectionAdmin(address newAdmin) public { + require(false, stub_error); + newAdmin; + dummy = 0; + } + + // Remove collection admin. + // + // @param new_admin Address of the removed administrator. + // + // Selector: removeCollectionAdmin(address) fafd7b42 + function removeCollectionAdmin(address admin) public { + require(false, stub_error); + admin; + dummy = 0; + } + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + // + // Selector: setCollectionNesting(bool) 112d4586 + function setCollectionNesting(bool enable) public { + require(false, stub_error); + enable; + dummy = 0; + } + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + // @param collections Addresses of collections that will be available for nesting. + // + // Selector: setCollectionNesting(bool,address[]) 64872396 + function setCollectionNesting(bool enable, address[] memory collections) + public + { + require(false, stub_error); + enable; + collections; + dummy = 0; + } + + // Set the collection access method. + // @param mode Access mode + // 0 for Normal + // 1 for AllowList + // + // Selector: setCollectionAccess(uint8) 41835d4c + function setCollectionAccess(uint8 mode) public { + require(false, stub_error); + mode; + dummy = 0; + } + + // Add the user to the allowed list. + // + // @param user Address of a trusted user. + // + // Selector: addToCollectionAllowList(address) 67844fe6 + function addToCollectionAllowList(address user) public { + require(false, stub_error); + user; + dummy = 0; + } + + // Remove the user from the allowed list. + // + // @param user Address of a removed user. + // + // Selector: removeFromCollectionAllowList(address) 85c51acb + function removeFromCollectionAllowList(address user) public { + require(false, stub_error); + user; + dummy = 0; + } + + // Switch permission for minting. + // + // @param mode Enable if "true". + // + // Selector: setCollectionMintMode(bool) 00018e84 + function setCollectionMintMode(bool mode) public { + require(false, stub_error); + mode; + dummy = 0; + } +} + // Selector: 942e8b22 contract ERC20 is Dummy, ERC165, ERC20Events { // Selector: name() 06fdde03 @@ -124,76 +351,6 @@ spender; dummy; return 0; - } -} - -// Selector: c894dc35 -contract Collection is Dummy, ERC165 { - // Selector: setCollectionProperty(string,bytes) 2f073f66 - function setCollectionProperty(string memory key, bytes memory value) - public - { - require(false, stub_error); - key; - value; - dummy = 0; - } - - // Selector: deleteCollectionProperty(string) 7b7debce - function deleteCollectionProperty(string memory key) public { - require(false, stub_error); - key; - dummy = 0; - } - - // Throws error if key not found - // - // Selector: collectionProperty(string) cf24fd6d - function collectionProperty(string memory key) - public - view - returns (bytes memory) - { - require(false, stub_error); - key; - dummy; - return hex""; - } - - // Selector: ethSetSponsor(address) 8f9af356 - function ethSetSponsor(address sponsor) public { - require(false, stub_error); - sponsor; - dummy = 0; - } - - // Selector: ethConfirmSponsorship() a8580d1a - function ethConfirmSponsorship() public { - require(false, stub_error); - dummy = 0; - } - - // Selector: setLimit(string,uint32) 68db30ca - function setLimit(string memory limit, uint32 value) public { - require(false, stub_error); - limit; - value; - dummy = 0; - } - - // Selector: setLimit(string,bool) ea67e4c2 - function setLimit(string memory limit, bool value) public { - require(false, stub_error); - limit; - value; - dummy = 0; - } - - // Selector: contractAddress() f6b4dfb4 - function contractAddress() public view returns (address) { - require(false, stub_error); - dummy; - return 0x0000000000000000000000000000000000000000; } } --- a/pallets/inflation/src/lib.rs +++ b/pallets/inflation/src/lib.rs @@ -14,6 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! # Inflation +//! +//! The inflation pallet is designed to increase the number of tokens at certain intervals. +//! With each iteration, increases the `total_issuance` value for the native token. +//! Executing an `on_initialize` hook at the beginning of each block, causing inflation to begin. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! * `start_inflation` - This method sets the inflation start date. Can be only called once. +//! Inflation start block can be backdated and will catch up. The method will create Treasury +//! account if it does not exist and perform the first inflation deposit. + // #![recursion_limit = "1024"] #![cfg_attr(not(feature = "std"), no_std)] --- a/pallets/nonfungible/src/benchmarking.rs +++ b/pallets/nonfungible/src/benchmarking.rs @@ -183,7 +183,7 @@ value: property_value(), }).collect::>(); let item = create_max_item(&collection, &owner, owner.clone())?; - }: {>::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?} + }: {>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?} delete_token_properties { let b in 0..MAX_PROPERTIES_PER_ITEM; @@ -205,7 +205,7 @@ value: property_value(), }).collect::>(); let item = create_max_item(&collection, &owner, owner.clone())?; - >::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?; + >::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?; let to_delete = (0..b).map(|k| property_key(k as usize)).collect::>(); - }: {>::delete_token_properties(&collection, &owner, item, to_delete, &Unlimited)?} + }: {>::delete_token_properties(&collection, &owner, item, to_delete.into_iter(), &Unlimited)?} } --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -133,6 +133,8 @@ } } +/// Implementation of `CommonCollectionOperations` for `NonfungibleHandle`. It wraps Nonfungible Pallete +/// methods and adds weight info. impl CommonCollectionOperations for NonfungibleHandle { fn create_item( &self, --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -14,6 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! # Nonfungible Pallet EVM API +//! +//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Nonfungible Pallet. +//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods. + extern crate alloc; use core::{ char::{REPLACEMENT_CHARACTER, decode_utf16}, @@ -40,8 +45,15 @@ SelfWeightOf, weights::WeightInfo, TokenProperties, }; +/// @title A contract that allows to set and delete token properties and change token property permissions. #[solidity_interface(name = "TokenProperties")] impl NonfungibleHandle { + /// @notice Set permissions for token property. + /// @dev Throws error if `msg.sender` is not admin or owner of the collection. + /// @param key Property key. + /// @param is_mutable Permission to mutate property. + /// @param collection_admin Permission to mutate property by collection admin if property is mutable. + /// @param token_owner Permission to mutate property by token owner if property is mutable. fn set_token_property_permission( &mut self, caller: caller, @@ -68,6 +80,11 @@ .map_err(dispatch_to_evm::) } + /// @notice Set token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @param value Property value. fn set_property( &mut self, caller: caller, @@ -96,6 +113,10 @@ .map_err(dispatch_to_evm::) } + /// @notice Delete token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> { let caller = T::CrossAccountId::from_eth(caller); let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?; @@ -111,7 +132,11 @@ .map_err(dispatch_to_evm::) } - /// Throws error if key not found + /// @notice Get token property value. + /// @dev Throws error if key not found + /// @param tokenId ID of the token. + /// @param key Property key. + /// @return Property value bytes fn property(&self, token_id: uint256, key: string) -> Result { let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?; let key = >::from(key) @@ -127,6 +152,11 @@ #[derive(ToLog)] pub enum ERC721Events { + /// @dev This emits when ownership of any NFT changes by any mechanism. + /// This event emits when NFTs are created (`from` == 0) and destroyed + /// (`to` == 0). Exception: during contract creation, any number of NFTs + /// may be created and assigned without emitting Transfer. At the time of + /// any transfer, the approved address for that NFT (if any) is reset to none. Transfer { #[indexed] from: address, @@ -135,6 +165,10 @@ #[indexed] token_id: uint256, }, + /// @dev This emits when the approved address for an NFT is changed or + /// reaffirmed. The zero address indicates there is no approved address. + /// When a Transfer event emits, this also indicates that the approved + /// address for that NFT (if any) is reset to none. Approval { #[indexed] owner: address, @@ -143,6 +177,8 @@ #[indexed] token_id: uint256, }, + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. #[allow(dead_code)] ApprovalForAll { #[indexed] @@ -159,19 +195,27 @@ MintingFinished {}, } +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 #[solidity_interface(name = "ERC721Metadata")] impl NonfungibleHandle { + /// @notice A descriptive name for a collection of NFTs in this contract fn name(&self) -> Result { Ok(decode_utf16(self.name.iter().copied()) .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) .collect::()) } + /// @notice An abbreviated name for NFTs in this contract fn symbol(&self) -> Result { Ok(string::from_utf8_lossy(&self.token_prefix).into()) } - /// Returns token's const_metadata + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC + /// 3986. The URI may point to a JSON file that conforms to the "ERC721 + /// Metadata JSON Schema". + /// @return token's const_metadata #[solidity(rename_selector = "tokenURI")] fn token_uri(&self, token_id: uint256) -> Result { let key = token_uri_key(); @@ -192,32 +236,53 @@ } } +/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 #[solidity_interface(name = "ERC721Enumerable")] impl NonfungibleHandle { + /// @notice Enumerate valid NFTs + /// @param index A counter less than `totalSupply()` + /// @return The token identifier for the `index`th NFT, + /// (sort order not specified) fn token_by_index(&self, index: uint256) -> Result { Ok(index) } - /// Not implemented + /// @dev Not implemented fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result { // TODO: Not implemetable Err("not implemented".into()) } + /// @notice Count NFTs tracked by this contract + /// @return A count of valid NFTs tracked by this contract, where each one of + /// them has an assigned and queryable owner not equal to the zero address fn total_supply(&self) -> Result { self.consume_store_reads(1)?; Ok(>::total_supply(self).into()) } } +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md #[solidity_interface(name = "ERC721", events(ERC721Events))] impl NonfungibleHandle { + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An address for whom to query the balance + /// @return The number of NFTs owned by `owner`, possibly zero fn balance_of(&self, owner: address) -> Result { self.consume_store_reads(1)?; let owner = T::CrossAccountId::from_eth(owner); let balance = >::get((self.id, owner)); Ok(balance.into()) } + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param tokenId The identifier for an NFT + /// @return The address of the owner of the NFT fn owner_of(&self, token_id: uint256) -> Result
{ self.consume_store_reads(1)?; let token: TokenId = token_id.try_into()?; @@ -226,7 +291,7 @@ .owner .as_eth()) } - /// Not implemented + /// @dev Not implemented fn safe_transfer_from_with_data( &mut self, _from: address, @@ -238,7 +303,7 @@ // TODO: Not implemetable Err("not implemented".into()) } - /// Not implemented + /// @dev Not implemented fn safe_transfer_from( &mut self, _from: address, @@ -250,6 +315,16 @@ Err("not implemented".into()) } + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @param _value Not used for an NFT #[weight(>::transfer_from())] fn transfer_from( &mut self, @@ -272,6 +347,12 @@ Ok(()) } + /// @notice Set or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param approved The new approved NFT controller + /// @param tokenId The NFT to approve #[weight(>::approve())] fn approve( &mut self, @@ -289,7 +370,7 @@ Ok(()) } - /// Not implemented + /// @dev Not implemented fn set_approval_for_all( &mut self, _caller: caller, @@ -300,21 +381,26 @@ Err("not implemented".into()) } - /// Not implemented + /// @dev Not implemented fn get_approved(&self, _token_id: uint256) -> Result
{ // TODO: Not implemetable Err("not implemented".into()) } - /// Not implemented + /// @dev Not implemented fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result
{ // TODO: Not implemetable Err("not implemented".into()) } } +/// @title ERC721 Token that can be irreversibly burned (destroyed). #[solidity_interface(name = "ERC721Burnable")] impl NonfungibleHandle { + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param tokenId The NFT to approve #[weight(>::burn_item())] fn burn(&mut self, caller: caller, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -325,14 +411,18 @@ } } +/// @title ERC721 minting logic. #[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))] impl NonfungibleHandle { fn minting_finished(&self) -> Result { Ok(false) } - /// `token_id` should be obtained with `next_token_id` method, - /// unlike standard, you can't specify it manually + /// @notice Function to mint token. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted NFT #[weight(>::create_item())] fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -364,8 +454,12 @@ Ok(true) } - /// `token_id` should be obtained with `next_token_id` method, - /// unlike standard, you can't specify it manually + /// @notice Function to mint token with the given tokenUri. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted NFT + /// @param tokenUri Token URI that would be stored in the NFT properties #[solidity(rename_selector = "mintWithTokenURI")] #[weight(>::create_item())] fn mint_with_token_uri( @@ -420,7 +514,7 @@ Ok(true) } - /// Not implemented + /// @dev Not implemented fn finish_minting(&mut self, _caller: caller) -> Result { Err("not implementable".into()) } @@ -449,8 +543,15 @@ false } +/// @title Unique extensions for ERC721. #[solidity_interface(name = "ERC721UniqueExtensions")] impl NonfungibleHandle { + /// @notice Transfer ownership of an NFT + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @param _value Not used for an NFT #[weight(>::transfer())] fn transfer( &mut self, @@ -470,6 +571,13 @@ Ok(()) } + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param tokenId The NFT to transfer + /// @param _value Not used for an NFT #[weight(>::burn_from())] fn burn_from( &mut self, @@ -490,6 +598,7 @@ Ok(()) } + /// @notice Returns next free NFT ID. fn next_token_id(&self) -> Result { self.consume_store_reads(1)?; Ok(>::get(self.id) @@ -498,6 +607,11 @@ .into()) } + /// @notice Function to mint multiple tokens. + /// @dev `tokenIds` should be an array of consecutive numbers and first number + /// should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokenIds IDs of the minted NFTs #[weight(>::create_multiple_items(token_ids.len() as u32))] fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -529,6 +643,11 @@ Ok(true) } + /// @notice Function to mint multiple tokens with the given tokenUris. + /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive + /// numbers and first number should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokens array of pairs of token ID and token URI for minted tokens #[solidity(rename_selector = "mintBulkWithTokenURI")] #[weight(>::create_multiple_items(tokens.len() as u32))] fn mint_bulk_with_token_uri( --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -14,6 +14,80 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! # Nonfungible Pallet +//! +//! The Nonfungible pallet provides functionality for handling nonfungible collections and tokens. +//! +//! - [`Config`] +//! - [`NonfungibleHandle`] +//! - [`Pallet`] +//! - [`CommonWeights`] +//! +//! ## Overview +//! +//! The Nonfungible pallet provides functions for: +//! +//! - NFT collection creation and removal +//! - Minting and burning of NFT tokens +//! - Retrieving account balances +//! - Transfering NFT tokens +//! - Setting and checking allowance for NFT tokens +//! - Setting properties and permissions for NFT collections and tokens +//! - Nesting and unnesting tokens +//! +//! ### Terminology +//! +//! - **NFT token:** Non fungible token. +//! +//! - **NFT Collection:** A collection of NFT tokens. All NFT tokens are part of a collection. +//! Each collection can define it's own properties, properties for it's tokens and set of permissions. +//! +//! - **Balance:** Number of NFT tokens owned by an account +//! +//! - **Allowance:** NFT tokens owned by one account that another account is allowed to make operations on +//! +//! - **Burning:** The process of “deleting” a token from a collection and from +//! an account balance of the owner. +//! +//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting +//! owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in +//! it's child token i.e. parent-child relationship graph shouldn't have cycles. +//! +//! - **Properties:** Key-Values pairs. Token properties are attached to a token. Collection properties are +//! attached to a collection. Set of permissions could be defined for each property. +//! +//! ### Implementations +//! +//! The Nonfungible pallet provides implementations for the following traits. If these traits provide +//! the functionality that you need, then you can avoid coupling with the Nonfungible pallet. +//! +//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight +//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing +//! with collections +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! - `init_collection` - Create NFT collection. NFT collection can be configured to allow or deny access for +//! some accounts. +//! - `destroy_collection` - Destroy exising NFT collection. There should be no tokens in the collection. +//! - `burn` - Burn NFT token owned by account. +//! - `transfer` - Transfer NFT token. Transfers should be enabled for NFT collection. +//! Nests the NFT token if it is sent to another token. +//! - `create_item` - Mint NFT token in collection. Sender should have permission to mint tokens. +//! - `set_allowance` - Set allowance for another account. +//! - `set_token_property` - Set token property value. +//! - `delete_token_property` - Remove property from the token. +//! - `set_collection_properties` - Set collection properties. +//! - `delete_collection_properties` - Remove properties from the collection. +//! - `set_property_permission` - Set collection property permission. +//! - `set_token_property_permissions` - Set token property permissions. +//! +//! ## Assumptions +//! +//! * To perform operations on tokens sender should be in collection's allow list if collection access mode is `AllowList`. + #![cfg_attr(not(feature = "std"), no_std)] use erc::ERC721Events; @@ -102,13 +176,17 @@ #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); + /// Amount of tokens minted for collection. #[pallet::storage] pub type TokensMinted = StorageMap; + + /// Amount of burnt tokens for collection. #[pallet::storage] pub type TokensBurnt = StorageMap; + /// Custom data serialized to bytes for token. #[pallet::storage] pub type TokenData = StorageNMap< Key = (Key, Key), @@ -116,6 +194,7 @@ QueryKind = OptionQuery, >; + /// Key-Value map stored for token. #[pallet::storage] #[pallet::getter(fn token_properties)] pub type TokenProperties = StorageNMap< @@ -125,6 +204,8 @@ OnEmpty = up_data_structs::TokenProperties, >; + /// Custom data that is serialized to bytes and attached to a token property. + /// Currently used to store RMRK data. #[pallet::storage] #[pallet::getter(fn token_aux_property)] pub type TokenAuxProperties = StorageNMap< @@ -138,7 +219,7 @@ QueryKind = OptionQuery, >; - /// Used to enumerate tokens owned by account + /// Used to enumerate tokens owned by account. #[pallet::storage] pub type Owned = StorageNMap< Key = ( @@ -150,7 +231,7 @@ QueryKind = ValueQuery, >; - /// Used to enumerate token's children + /// Used to enumerate token's children. #[pallet::storage] #[pallet::getter(fn token_children)] pub type TokenChildren = StorageNMap< @@ -163,6 +244,7 @@ QueryKind = ValueQuery, >; + /// Amount of tokens owned by account. #[pallet::storage] pub type AccountBalance = StorageNMap< Key = ( @@ -173,6 +255,7 @@ QueryKind = ValueQuery, >; + /// Allowance set by an owner for a spender for a token. #[pallet::storage] pub type Allowance = StorageNMap< Key = (Key, Key), @@ -273,13 +356,21 @@ } impl Pallet { + /// Get number of NFT tokens in collection. pub fn total_supply(collection: &NonfungibleHandle) -> u32 { >::get(collection.id) - >::get(collection.id) } + + /// Check that NFT token exists. + /// + /// - `token`: Token ID. pub fn token_exists(collection: &NonfungibleHandle, token: TokenId) -> bool { >::contains_key((collection.id, token)) } + /// Set the token property with the scope. + /// + /// - `property`: Contains key-value pair. pub fn set_scoped_token_property( collection_id: CollectionId, token_id: TokenId, @@ -294,6 +385,7 @@ Ok(()) } + /// Batch operation to set multiple properties with the same scope. pub fn set_scoped_token_properties( collection_id: CollectionId, token_id: TokenId, @@ -308,6 +400,9 @@ Ok(()) } + /// Add or edit auxiliary data for the property. + /// + /// - `f`: function that adds or edits auxiliary data. pub fn try_mutate_token_aux_property( collection_id: CollectionId, token_id: TokenId, @@ -318,6 +413,7 @@ >::try_mutate((collection_id, token_id, scope, key), f) } + /// Remove auxiliary data for the property. pub fn remove_token_aux_property( collection_id: CollectionId, token_id: TokenId, @@ -327,6 +423,9 @@ >::remove((collection_id, token_id, scope, key)); } + /// Get all auxiliary data in a given scope. + /// + /// Returns iterator over Property Key - Data pairs. pub fn iterate_token_aux_properties( collection_id: CollectionId, token_id: TokenId, @@ -335,6 +434,7 @@ >::iter_prefix((collection_id, token_id, scope)) } + /// Get ID of the last minted token pub fn current_token_id(collection_id: CollectionId) -> TokenId { TokenId(>::get(collection_id)) } @@ -342,6 +442,11 @@ // unchecked calls skips any permission checks impl Pallet { + /// Create NFT collection + /// + /// `init_collection` will take non-refundable deposit for collection creation. + /// + /// - `data`: Contains settings for collection limits and permissions. pub fn init_collection( owner: T::CrossAccountId, data: CreateCollectionData, @@ -349,6 +454,11 @@ ) -> Result { >::init_collection(owner, data, is_external) } + + /// Destroy NFT collection + /// + /// `destroy_collection` will throw error if collection contains any tokens. + /// Only owner can destroy collection. pub fn destroy_collection( collection: NonfungibleHandle, sender: &T::CrossAccountId, @@ -373,6 +483,15 @@ Ok(()) } + /// Burn NFT token + /// + /// `burn` removes `token` from the `collection`, from it's owner and from the parent token + /// if the token is nested. + /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens. + /// Also removes all corresponding properties and auxiliary properties. + /// + /// - `token`: Token that should be burned + /// - `collection`: Collection that contains the token pub fn burn( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -442,6 +561,12 @@ Ok(()) } + /// Same as [`burn`] but burns all the tokens that are nested in the token first + /// + /// - `self_budget`: Limit for searching children in depth. + /// - `breadth_budget`: Limit of breadth of searching children. + /// + /// [`burn`]: struct.Pallet.html#method.burn #[transactional] pub fn burn_recursively( collection: &NonfungibleHandle, @@ -481,6 +606,14 @@ }) } + /// Batch operation to add, edit or remove properties for the token + /// + /// All affected properties should have mutable permission and sender should have + /// permission to edit those properties. + /// + /// - `nesting_budget`: Limit for searching parents in depth to check ownership. + /// - `is_token_create`: Indicates that method is called during token initialization. + /// Allows to bypass ownership check. #[transactional] fn modify_token_properties( collection: &NonfungibleHandle, @@ -574,6 +707,11 @@ Ok(()) } + /// Batch operation to add or edit properties for the token + /// + /// Same as [`modify_token_properties`] but doesn't allow to remove properties + /// + /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties pub fn set_token_properties( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -592,6 +730,11 @@ ) } + /// Add or edit single property for the token + /// + /// Calls [`set_token_properties`] internally + /// + /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties pub fn set_token_property( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -611,6 +754,11 @@ ) } + /// Batch operation to remove properties from the token + /// + /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties + /// + /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties pub fn delete_token_properties( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -630,6 +778,11 @@ ) } + /// Remove single property from the token + /// + /// Calls [`delete_token_properties`] internally + /// + /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties pub fn delete_token_property( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -646,6 +799,7 @@ ) } + /// Add or edit properties for the collection pub fn set_collection_properties( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -654,6 +808,7 @@ >::set_collection_properties(collection, sender, properties) } + /// Remove properties from the collection pub fn delete_collection_properties( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -662,6 +817,9 @@ >::delete_collection_properties(collection, sender, property_keys) } + /// Set property permissions for the token. + /// + /// Sender should be the owner or admin of token's collection. pub fn set_token_property_permissions( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -670,6 +828,9 @@ >::set_token_property_permissions(collection, sender, property_permissions) } + /// Set property permissions for the collection. + /// + /// Sender should be the owner or admin of the collection. pub fn set_property_permission( collection: &CollectionHandle, sender: &T::CrossAccountId, @@ -678,6 +839,15 @@ >::set_property_permission(collection, sender, permission) } + /// Transfer NFT token from one account to another. + /// + /// `from` account stops being the owner and `to` account becomes the owner of the token. + /// If `to` is token than `to` becomes owner of the token and the token become nested. + /// Unnests token from previous parent if it was nested before. + /// Removes allowance for the token if there was any. + /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit. + /// + /// - `nesting_budget`: Limit for token nesting depth pub fn transfer( collection: &NonfungibleHandle, from: &T::CrossAccountId, @@ -769,6 +939,16 @@ Ok(()) } + /// Batch operation to mint multiple NFT tokens. + /// + /// The sender should be the owner/admin of the collection or collection should be configured + /// to allow public minting. + /// Throws if amount of tokens reached it's limit for the collection or if caller reached + /// token ownership limit. + /// + /// - `data`: Contains list of token properties and users who will become the owners of the + /// corresponging tokens. + /// - `nesting_budget`: Limit for token nesting depth pub fn create_multiple_items( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -953,6 +1133,9 @@ } } + /// Set allowance for the spender to `transfer` or `burn` sender's token. + /// + /// - `token`: Token the spender is allowed to `transfer` or `burn`. pub fn set_allowance( collection: &NonfungibleHandle, sender: &T::CrossAccountId, @@ -985,6 +1168,7 @@ Ok(()) } + /// Checks allowance for the spender to use the token. fn check_allowed( collection: &NonfungibleHandle, spender: &T::CrossAccountId, @@ -1027,6 +1211,12 @@ Ok(()) } + /// Transfer NFT token from one account to another. + /// + /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token. + /// The owner should set allowance for the spender to transfer token. + /// + /// [`transfer`]: struct.Pallet.html#method.transfer pub fn transfer_from( collection: &NonfungibleHandle, spender: &T::CrossAccountId, @@ -1043,6 +1233,12 @@ Self::transfer(collection, from, to, token, nesting_budget) } + /// Burn NFT token for `from` account. + /// + /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should + /// set allowance for the spender to burn token. + /// + /// [`burn`]: struct.Pallet.html#method.burn pub fn burn_from( collection: &NonfungibleHandle, spender: &T::CrossAccountId, @@ -1057,6 +1253,8 @@ Self::burn(collection, from, token) } + /// Check that `from` token could be nested in `under` token. + /// pub fn check_nesting( handle: &NonfungibleHandle, sender: T::CrossAccountId, @@ -1126,7 +1324,11 @@ .collect() } - /// Delegated to `create_multiple_items` + /// Mint single NFT token. + /// + /// Delegated to [`create_multiple_items`] + /// + /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items pub fn create_item( collection: &NonfungibleHandle, sender: &T::CrossAccountId, --- a/pallets/nonfungible/src/stubs/UniqueNFT.sol +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -53,6 +53,13 @@ // Selector: 41369377 contract TokenProperties is Dummy, ERC165 { + // @notice Set permissions for token property. + // @dev Throws error if `msg.sender` is not admin or owner of the collection. + // @param key Property key. + // @param is_mutable Permission to mutate property. + // @param collection_admin Permission to mutate property by collection admin if property is mutable. + // @param token_owner Permission to mutate property by token owner if property is mutable. + // // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa function setTokenPropertyPermission( string memory key, @@ -68,6 +75,12 @@ dummy = 0; } + // @notice Set token property value. + // @dev Throws error if `msg.sender` has no permission to edit the property. + // @param tokenId ID of the token. + // @param key Property key. + // @param value Property value. + // // Selector: setProperty(uint256,string,bytes) 1752d67b function setProperty( uint256 tokenId, @@ -81,6 +94,11 @@ dummy = 0; } + // @notice Delete token property value. + // @dev Throws error if `msg.sender` has no permission to edit the property. + // @param tokenId ID of the token. + // @param key Property key. + // // Selector: deleteProperty(uint256,string) 066111d1 function deleteProperty(uint256 tokenId, string memory key) public { require(false, stub_error); @@ -89,7 +107,11 @@ dummy = 0; } - // Throws error if key not found + // @notice Get token property value. + // @dev Throws error if key not found + // @param tokenId ID of the token. + // @param key Property key. + // @return Property value bytes // // Selector: property(uint256,string) 7228c327 function property(uint256 tokenId, string memory key) @@ -107,6 +129,11 @@ // Selector: 42966c68 contract ERC721Burnable is Dummy, ERC165 { + // @notice Burns a specific ERC721 token. + // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + // operator of the current owner. + // @param tokenId The NFT to approve + // // Selector: burn(uint256) 42966c68 function burn(uint256 tokenId) public { require(false, stub_error); @@ -117,6 +144,12 @@ // Selector: 58800161 contract ERC721 is Dummy, ERC165, ERC721Events { + // @notice Count all NFTs assigned to an owner + // @dev NFTs assigned to the zero address are considered invalid, and this + // function throws for queries about the zero address. + // @param owner An address for whom to query the balance + // @return The number of NFTs owned by `owner`, possibly zero + // // Selector: balanceOf(address) 70a08231 function balanceOf(address owner) public view returns (uint256) { require(false, stub_error); @@ -125,6 +158,12 @@ return 0; } + // @notice Find the owner of an NFT + // @dev NFTs assigned to zero address are considered invalid, and queries + // about them do throw. + // @param tokenId The identifier for an NFT + // @return The address of the owner of the NFT + // // Selector: ownerOf(uint256) 6352211e function ownerOf(uint256 tokenId) public view returns (address) { require(false, stub_error); @@ -133,7 +172,7 @@ return 0x0000000000000000000000000000000000000000; } - // Not implemented + // @dev Not implemented // // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672 function safeTransferFromWithData( @@ -150,7 +189,7 @@ dummy = 0; } - // Not implemented + // @dev Not implemented // // Selector: safeTransferFrom(address,address,uint256) 42842e0e function safeTransferFrom( @@ -165,6 +204,17 @@ dummy = 0; } + // @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + // TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE + // THEY MAY BE PERMANENTLY LOST + // @dev Throws unless `msg.sender` is the current owner or an authorized + // operator for this NFT. Throws if `from` is not the current owner. Throws + // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + // @param from The current owner of the NFT + // @param to The new owner + // @param tokenId The NFT to transfer + // @param _value Not used for an NFT + // // Selector: transferFrom(address,address,uint256) 23b872dd function transferFrom( address from, @@ -178,6 +228,13 @@ dummy = 0; } + // @notice Set or reaffirm the approved address for an NFT + // @dev The zero address indicates there is no approved address. + // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + // operator of the current owner. + // @param approved The new approved NFT controller + // @param tokenId The NFT to approve + // // Selector: approve(address,uint256) 095ea7b3 function approve(address approved, uint256 tokenId) public { require(false, stub_error); @@ -186,7 +243,7 @@ dummy = 0; } - // Not implemented + // @dev Not implemented // // Selector: setApprovalForAll(address,bool) a22cb465 function setApprovalForAll(address operator, bool approved) public { @@ -196,7 +253,7 @@ dummy = 0; } - // Not implemented + // @dev Not implemented // // Selector: getApproved(uint256) 081812fc function getApproved(uint256 tokenId) public view returns (address) { @@ -206,7 +263,7 @@ return 0x0000000000000000000000000000000000000000; } - // Not implemented + // @dev Not implemented // // Selector: isApprovedForAll(address,address) e985e9c5 function isApprovedForAll(address owner, address operator) @@ -224,6 +281,8 @@ // Selector: 5b5e139f contract ERC721Metadata is Dummy, ERC165 { + // @notice A descriptive name for a collection of NFTs in this contract + // // Selector: name() 06fdde03 function name() public view returns (string memory) { require(false, stub_error); @@ -231,6 +290,8 @@ return ""; } + // @notice An abbreviated name for NFTs in this contract + // // Selector: symbol() 95d89b41 function symbol() public view returns (string memory) { require(false, stub_error); @@ -238,7 +299,11 @@ return ""; } - // Returns token's const_metadata + // @notice A distinct Uniform Resource Identifier (URI) for a given asset. + // @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC + // 3986. The URI may point to a JSON file that conforms to the "ERC721 + // Metadata JSON Schema". + // @return token's const_metadata // // Selector: tokenURI(uint256) c87b56dd function tokenURI(uint256 tokenId) public view returns (string memory) { @@ -258,8 +323,11 @@ return false; } - // `token_id` should be obtained with `next_token_id` method, - // unlike standard, you can't specify it manually + // @notice Function to mint token. + // @dev `tokenId` should be obtained with `nextTokenId` method, + // unlike standard, you can't specify it manually + // @param to The new owner + // @param tokenId ID of the minted NFT // // Selector: mint(address,uint256) 40c10f19 function mint(address to, uint256 tokenId) public returns (bool) { @@ -270,8 +338,12 @@ return false; } - // `token_id` should be obtained with `next_token_id` method, - // unlike standard, you can't specify it manually + // @notice Function to mint token with the given tokenUri. + // @dev `tokenId` should be obtained with `nextTokenId` method, + // unlike standard, you can't specify it manually + // @param to The new owner + // @param tokenId ID of the minted NFT + // @param tokenUri Token URI that would be stored in the NFT properties // // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f function mintWithTokenURI( @@ -287,7 +359,7 @@ return false; } - // Not implemented + // @dev Not implemented // // Selector: finishMinting() 7d64bcb4 function finishMinting() public returns (bool) { @@ -299,6 +371,11 @@ // Selector: 780e9d63 contract ERC721Enumerable is Dummy, ERC165 { + // @notice Enumerate valid NFTs + // @param index A counter less than `totalSupply()` + // @return The token identifier for the `index`th NFT, + // (sort order not specified) + // // Selector: tokenByIndex(uint256) 4f6ccce7 function tokenByIndex(uint256 index) public view returns (uint256) { require(false, stub_error); @@ -307,7 +384,7 @@ return 0; } - // Not implemented + // @dev Not implemented // // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59 function tokenOfOwnerByIndex(address owner, uint256 index) @@ -322,6 +399,10 @@ return 0; } + // @notice Count NFTs tracked by this contract + // @return A count of valid NFTs tracked by this contract, where each one of + // them has an assigned and queryable owner not equal to the zero address + // // Selector: totalSupply() 18160ddd function totalSupply() public view returns (uint256) { require(false, stub_error); @@ -332,6 +413,11 @@ // Selector: 7d9262e6 contract Collection is Dummy, ERC165 { + // Set collection property. + // + // @param key Property key. + // @param value Propery value. + // // Selector: setCollectionProperty(string,bytes) 2f073f66 function setCollectionProperty(string memory key, bytes memory value) public @@ -342,6 +428,10 @@ dummy = 0; } + // Delete collection property. + // + // @param key Property key. + // // Selector: deleteCollectionProperty(string) 7b7debce function deleteCollectionProperty(string memory key) public { require(false, stub_error); @@ -349,8 +439,13 @@ dummy = 0; } - // Throws error if key not found + // Get collection property. + // + // @dev Throws error if key not found. // + // @param key Property key. + // @return bytes The property corresponding to the key. + // // Selector: collectionProperty(string) cf24fd6d function collectionProperty(string memory key) public @@ -363,6 +458,12 @@ return hex""; } + // Set the sponsor of the collection. + // + // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + // + // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + // // Selector: setCollectionSponsor(address) 7623402e function setCollectionSponsor(address sponsor) public { require(false, stub_error); @@ -370,12 +471,27 @@ dummy = 0; } + // Collection sponsorship confirmation. + // + // @dev After setting the sponsor for the collection, it must be confirmed with this function. + // // Selector: confirmCollectionSponsorship() 3c50e97a function confirmCollectionSponsorship() public { require(false, stub_error); dummy = 0; } + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "accountTokenOwnershipLimit", + // "sponsoredDataSize", + // "sponsoredDataRateLimit", + // "tokenLimit", + // "sponsorTransferTimeout", + // "sponsorApproveTimeout" + // @param value Value of the limit. + // // Selector: setCollectionLimit(string,uint32) 6a3841db function setCollectionLimit(string memory limit, uint32 value) public { require(false, stub_error); @@ -384,6 +500,14 @@ dummy = 0; } + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "ownerCanTransfer", + // "ownerCanDestroy", + // "transfersEnabled" + // @param value Value of the limit. + // // Selector: setCollectionLimit(string,bool) 993b7fba function setCollectionLimit(string memory limit, bool value) public { require(false, stub_error); @@ -392,6 +516,8 @@ dummy = 0; } + // Get contract address. + // // Selector: contractAddress() f6b4dfb4 function contractAddress() public view returns (address) { require(false, stub_error); @@ -399,34 +525,51 @@ return 0x0000000000000000000000000000000000000000; } + // Add collection admin by substrate address. + // @param new_admin Substrate administrator address. + // // Selector: addCollectionAdminSubstrate(uint256) 5730062b - function addCollectionAdminSubstrate(uint256 newAdmin) public view { + function addCollectionAdminSubstrate(uint256 newAdmin) public { require(false, stub_error); newAdmin; - dummy; + dummy = 0; } + // Remove collection admin by substrate address. + // @param admin Substrate administrator address. + // // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 - function removeCollectionAdminSubstrate(uint256 newAdmin) public view { + function removeCollectionAdminSubstrate(uint256 admin) public { require(false, stub_error); - newAdmin; - dummy; + admin; + dummy = 0; } + // Add collection admin. + // @param new_admin Address of the added administrator. + // // Selector: addCollectionAdmin(address) 92e462c7 - function addCollectionAdmin(address newAdmin) public view { + function addCollectionAdmin(address newAdmin) public { require(false, stub_error); newAdmin; - dummy; + dummy = 0; } + // Remove collection admin. + // + // @param new_admin Address of the removed administrator. + // // Selector: removeCollectionAdmin(address) fafd7b42 - function removeCollectionAdmin(address admin) public view { + function removeCollectionAdmin(address admin) public { require(false, stub_error); admin; - dummy; + dummy = 0; } + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + // // Selector: setCollectionNesting(bool) 112d4586 function setCollectionNesting(bool enable) public { require(false, stub_error); @@ -434,6 +577,11 @@ dummy = 0; } + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + // @param collections Addresses of collections that will be available for nesting. + // // Selector: setCollectionNesting(bool,address[]) 64872396 function setCollectionNesting(bool enable, address[] memory collections) public @@ -444,6 +592,11 @@ dummy = 0; } + // Set the collection access method. + // @param mode Access mode + // 0 for Normal + // 1 for AllowList + // // Selector: setCollectionAccess(uint8) 41835d4c function setCollectionAccess(uint8 mode) public { require(false, stub_error); @@ -451,20 +604,32 @@ dummy = 0; } + // Add the user to the allowed list. + // + // @param user Address of a trusted user. + // // Selector: addToCollectionAllowList(address) 67844fe6 - function addToCollectionAllowList(address user) public view { + function addToCollectionAllowList(address user) public { require(false, stub_error); user; - dummy; + dummy = 0; } + // Remove the user from the allowed list. + // + // @param user Address of a removed user. + // // Selector: removeFromCollectionAllowList(address) 85c51acb - function removeFromCollectionAllowList(address user) public view { + function removeFromCollectionAllowList(address user) public { require(false, stub_error); user; - dummy; + dummy = 0; } + // Switch permission for minting. + // + // @param mode Enable if "true". + // // Selector: setCollectionMintMode(bool) 00018e84 function setCollectionMintMode(bool mode) public { require(false, stub_error); @@ -475,6 +640,13 @@ // Selector: d74d154f contract ERC721UniqueExtensions is Dummy, ERC165 { + // @notice Transfer ownership of an NFT + // @dev Throws unless `msg.sender` is the current owner. Throws if `to` + // is the zero address. Throws if `tokenId` is not a valid NFT. + // @param to The new owner + // @param tokenId The NFT to transfer + // @param _value Not used for an NFT + // // Selector: transfer(address,uint256) a9059cbb function transfer(address to, uint256 tokenId) public { require(false, stub_error); @@ -483,6 +655,14 @@ dummy = 0; } + // @notice Burns a specific ERC721 token. + // @dev Throws unless `msg.sender` is the current owner or an authorized + // operator for this NFT. Throws if `from` is not the current owner. Throws + // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + // @param from The current owner of the NFT + // @param tokenId The NFT to transfer + // @param _value Not used for an NFT + // // Selector: burnFrom(address,uint256) 79cc6790 function burnFrom(address from, uint256 tokenId) public { require(false, stub_error); @@ -491,6 +671,8 @@ dummy = 0; } + // @notice Returns next free NFT ID. + // // Selector: nextTokenId() 75794a3c function nextTokenId() public view returns (uint256) { require(false, stub_error); @@ -498,6 +680,12 @@ return 0; } + // @notice Function to mint multiple tokens. + // @dev `tokenIds` should be an array of consecutive numbers and first number + // should be obtained with `nextTokenId` method + // @param to The new owner + // @param tokenIds IDs of the minted NFTs + // // Selector: mintBulk(address,uint256[]) 44a9945e function mintBulk(address to, uint256[] memory tokenIds) public @@ -510,6 +698,12 @@ return false; } + // @notice Function to mint multiple tokens with the given tokenUris. + // @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive + // numbers and first number should be obtained with `nextTokenId` method + // @param to The new owner + // @param tokens array of pairs of token ID and token URI for minted tokens + // // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006 function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) public --- a/pallets/nonfungible/src/weights.rs +++ b/pallets/nonfungible/src/weights.rs @@ -3,7 +3,7 @@ //! Autogenerated weights for pallet_nonfungible //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-06-15, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-07-20, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 // Executed Command: @@ -56,7 +56,7 @@ // Storage: Nonfungible TokenData (r:0 w:1) // Storage: Nonfungible Owned (r:0 w:1) fn create_item() -> Weight { - (24_135_000 as Weight) + (20_328_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -65,9 +65,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (21_952_000 as Weight) - // Standard Error: 5_000 - .saturating_add((4_727_000 as Weight).saturating_mul(b as Weight)) + (10_134_000 as Weight) + // Standard Error: 3_000 + .saturating_add((4_927_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(b as Weight))) @@ -77,9 +77,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items_ex(b: u32, ) -> Weight { - (10_432_000 as Weight) - // Standard Error: 6_000 - .saturating_add((7_383_000 as Weight).saturating_mul(b as Weight)) + (5_710_000 as Weight) + // Standard Error: 4_000 + .saturating_add((7_578_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(1 as Weight)) @@ -93,7 +93,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_item() -> Weight { - (29_798_000 as Weight) + (28_433_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -105,7 +105,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_recursively_self_raw() -> Weight { - (37_955_000 as Weight) + (34_435_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -119,8 +119,8 @@ // Storage: Common CollectionById (r:1 w:0) fn burn_recursively_breadth_plus_self_plus_self_per_each_raw(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 1_349_000 - .saturating_add((275_145_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_539_000 + .saturating_add((304_456_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(7 as Weight)) .saturating_add(T::DbWeight::get().reads((4 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(6 as Weight)) @@ -131,14 +131,14 @@ // Storage: Nonfungible Allowance (r:1 w:0) // Storage: Nonfungible Owned (r:0 w:2) fn transfer() -> Weight { - (27_867_000 as Weight) + (24_376_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible Allowance (r:1 w:1) fn approve() -> Weight { - (18_824_000 as Weight) + (15_890_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } @@ -147,7 +147,7 @@ // Storage: Nonfungible AccountBalance (r:2 w:2) // Storage: Nonfungible Owned (r:0 w:2) fn transfer_from() -> Weight { - (32_879_000 as Weight) + (28_634_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } @@ -159,7 +159,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (37_061_000 as Weight) + (32_201_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } @@ -167,28 +167,26 @@ fn set_token_property_permissions(b: u32, ) -> Weight { (0 as Weight) // Standard Error: 57_000 - .saturating_add((15_149_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((15_232_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_278_000 - .saturating_add((409_613_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) + // Standard Error: 1_648_000 + .saturating_add((288_654_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_234_000 - .saturating_add((408_185_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) + // Standard Error: 1_632_000 + .saturating_add((289_190_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } } @@ -200,7 +198,7 @@ // Storage: Nonfungible TokenData (r:0 w:1) // Storage: Nonfungible Owned (r:0 w:1) fn create_item() -> Weight { - (24_135_000 as Weight) + (20_328_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -209,9 +207,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (21_952_000 as Weight) - // Standard Error: 5_000 - .saturating_add((4_727_000 as Weight).saturating_mul(b as Weight)) + (10_134_000 as Weight) + // Standard Error: 3_000 + .saturating_add((4_927_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(b as Weight))) @@ -221,9 +219,9 @@ // Storage: Nonfungible TokenData (r:0 w:4) // Storage: Nonfungible Owned (r:0 w:4) fn create_multiple_items_ex(b: u32, ) -> Weight { - (10_432_000 as Weight) - // Standard Error: 6_000 - .saturating_add((7_383_000 as Weight).saturating_mul(b as Weight)) + (5_710_000 as Weight) + // Standard Error: 4_000 + .saturating_add((7_578_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) @@ -237,7 +235,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_item() -> Weight { - (29_798_000 as Weight) + (28_433_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -249,7 +247,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_recursively_self_raw() -> Weight { - (37_955_000 as Weight) + (34_435_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -263,8 +261,8 @@ // Storage: Common CollectionById (r:1 w:0) fn burn_recursively_breadth_plus_self_plus_self_per_each_raw(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 1_349_000 - .saturating_add((275_145_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_539_000 + .saturating_add((304_456_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(7 as Weight)) .saturating_add(RocksDbWeight::get().reads((4 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) @@ -275,14 +273,14 @@ // Storage: Nonfungible Allowance (r:1 w:0) // Storage: Nonfungible Owned (r:0 w:2) fn transfer() -> Weight { - (27_867_000 as Weight) + (24_376_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible Allowance (r:1 w:1) fn approve() -> Weight { - (18_824_000 as Weight) + (15_890_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } @@ -291,7 +289,7 @@ // Storage: Nonfungible AccountBalance (r:2 w:2) // Storage: Nonfungible Owned (r:0 w:2) fn transfer_from() -> Weight { - (32_879_000 as Weight) + (28_634_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } @@ -303,7 +301,7 @@ // Storage: Nonfungible Owned (r:0 w:1) // Storage: Nonfungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (37_061_000 as Weight) + (32_201_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } @@ -311,28 +309,26 @@ fn set_token_property_permissions(b: u32, ) -> Weight { (0 as Weight) // Standard Error: 57_000 - .saturating_add((15_149_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((15_232_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_278_000 - .saturating_add((409_613_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + // Standard Error: 1_648_000 + .saturating_add((288_654_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenData (r:1 w:0) // Storage: Nonfungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 2_234_000 - .saturating_add((408_185_000 as Weight).saturating_mul(b as Weight)) - .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + // Standard Error: 1_632_000 + .saturating_add((289_190_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } } --- a/pallets/refungible/Cargo.toml +++ b/pallets/refungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-refungible" -version = "0.1.0" +version = "0.1.1" license = "GPLv3" edition = "2021" --- /dev/null +++ b/pallets/refungible/Changelog.md @@ -0,0 +1,3 @@ +### 0.1.1 +--- +* Added support for properties for RFT collections and tokens. --- a/pallets/refungible/src/benchmarking.rs +++ b/pallets/refungible/src/benchmarking.rs @@ -18,9 +18,12 @@ use crate::{Pallet, Config, RefungibleHandle}; use sp_std::prelude::*; -use pallet_common::benchmarking::{create_collection_raw, create_data}; +use pallet_common::benchmarking::{create_collection_raw, property_key, property_value, create_data}; use frame_benchmarking::{benchmarks, account}; -use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, CUSTOM_DATA_LIMIT, budget::Unlimited}; +use up_data_structs::{ + CollectionMode, MAX_ITEMS_PER_BATCH, MAX_PROPERTIES_PER_ITEM, CUSTOM_DATA_LIMIT, + budget::Unlimited, +}; use pallet_common::bench_init; use core::convert::TryInto; use core::iter::IntoIterator; @@ -38,6 +41,7 @@ .collect::>() .try_into() .unwrap(), + properties: Default::default(), } } fn create_max_item( @@ -204,6 +208,68 @@ >::set_allowance(&collection, &sender, &burner, item, 200)?; }: {>::burn_from(&collection, &burner, &sender, item, 200, &Unlimited)?} + set_token_property_permissions { + let b in 0..MAX_PROPERTIES_PER_ITEM; + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let perms = (0..b).map(|k| PropertyKeyPermission { + key: property_key(k as usize), + permission: PropertyPermission { + mutable: false, + collection_admin: false, + token_owner: false, + }, + }).collect::>(); + }: {>::set_token_property_permissions(&collection, &owner, perms)?} + + set_token_properties { + let b in 0..MAX_PROPERTIES_PER_ITEM; + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let perms = (0..b).map(|k| PropertyKeyPermission { + key: property_key(k as usize), + permission: PropertyPermission { + mutable: false, + collection_admin: true, + token_owner: true, + }, + }).collect::>(); + >::set_token_property_permissions(&collection, &owner, perms)?; + let props = (0..b).map(|k| Property { + key: property_key(k as usize), + value: property_value(), + }).collect::>(); + let item = create_max_item(&collection, &owner, [(owner.clone(), 200)])?; + }: {>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?} + + delete_token_properties { + let b in 0..MAX_PROPERTIES_PER_ITEM; + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let perms = (0..b).map(|k| PropertyKeyPermission { + key: property_key(k as usize), + permission: PropertyPermission { + mutable: true, + collection_admin: true, + token_owner: true, + }, + }).collect::>(); + >::set_token_property_permissions(&collection, &owner, perms)?; + let props = (0..b).map(|k| Property { + key: property_key(k as usize), + value: property_value(), + }).collect::>(); + let item = create_max_item(&collection, &owner, [(owner.clone(), 200)])?; + >::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?; + let to_delete = (0..b).map(|k| property_key(k as usize)).collect::>(); + }: {>::delete_token_properties(&collection, &owner, item, to_delete.into_iter(), &Unlimited)?} + repartition_item { bench_init!{ owner: sub; collection: collection(owner); --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -20,9 +20,12 @@ use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight, traits::Get}; use up_data_structs::{ CollectionId, TokenId, CreateItemExData, CreateRefungibleExData, budget::Budget, Property, - PropertyKey, PropertyValue, PropertyKeyPermission, CreateItemData, + PropertyKey, PropertyValue, PropertyKeyPermission, CreateItemData, CollectionPropertiesVec, }; -use pallet_common::{CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight}; +use pallet_common::{ + CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight, + weights::WeightInfo as _, +}; use pallet_structure::Error as StructureError; use sp_runtime::{DispatchError}; use sp_std::{vec::Vec, vec}; @@ -41,6 +44,14 @@ }; } +fn properties_weight(properties: &CollectionPropertiesVec) -> u64 { + if properties.len() > 0 { + >::set_token_properties(properties.len() as u32) + } else { + 0 + } +} + pub struct CommonWeights(PhantomData); impl CommonWeightInfo for CommonWeights { fn create_item() -> Weight { @@ -48,16 +59,31 @@ } fn create_multiple_items(data: &[CreateItemData]) -> Weight { - >::create_multiple_items(data.len() as u32) + >::create_multiple_items(data.len() as u32).saturating_add( + data.iter() + .map(|data| match data { + CreateItemData::ReFungible(rft_data) => { + properties_weight::(&rft_data.properties) + } + _ => 0, + }) + .fold(0, |a, b| a.saturating_add(b)), + ) } fn create_multiple_items_ex(call: &CreateItemExData) -> Weight { match call { CreateItemExData::RefungibleMultipleOwners(i) => { >::create_multiple_items_ex_multiple_owners(i.users.len() as u32) + .saturating_add(properties_weight::(&i.properties)) } CreateItemExData::RefungibleMultipleItems(i) => { >::create_multiple_items_ex_multiple_items(i.len() as u32) + .saturating_add( + i.iter() + .map(|d| properties_weight::(&d.properties)) + .fold(0, |a, b| a.saturating_add(b)), + ) } _ => 0, } @@ -67,29 +93,24 @@ max_weight_of!(burn_item_partial(), burn_item_fully()) } - fn set_collection_properties(_amount: u32) -> Weight { - // Error - 0 + fn set_collection_properties(amount: u32) -> Weight { + >::set_collection_properties(amount) } - fn delete_collection_properties(_amount: u32) -> Weight { - // Error - 0 + fn delete_collection_properties(amount: u32) -> Weight { + >::delete_collection_properties(amount) } - fn set_token_properties(_amount: u32) -> Weight { - // Error - 0 + fn set_token_properties(amount: u32) -> Weight { + >::set_token_properties(amount) } - fn delete_token_properties(_amount: u32) -> Weight { - // Error - 0 + fn delete_token_properties(amount: u32) -> Weight { + >::delete_token_properties(amount) } - fn set_token_property_permissions(_amount: u32) -> Weight { - // Error - 0 + fn set_token_property_permissions(amount: u32) -> Weight { + >::set_token_property_permissions(amount) } fn transfer() -> Weight { @@ -140,11 +161,14 @@ out.insert(to.clone(), data.pieces); out.try_into().expect("limit > 0") }, + properties: data.properties, }), _ => fail!(>::NotRefungibleDataUsedToMintFungibleCollectionToken), } } +/// Implementation of `CommonCollectionOperations` for `RefungibleHandle`. It wraps Refungible Pallete +/// methods and adds weight info. impl CommonCollectionOperations for RefungibleHandle { fn create_item( &self, @@ -295,46 +319,85 @@ fn set_collection_properties( &self, - _sender: T::CrossAccountId, - _property: Vec, + sender: T::CrossAccountId, + properties: Vec, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::set_collection_properties(properties.len() as u32); + + with_weight( + >::set_collection_properties(self, &sender, properties), + weight, + ) } fn delete_collection_properties( &self, - _sender: &T::CrossAccountId, - _property_keys: Vec, + sender: &T::CrossAccountId, + property_keys: Vec, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::delete_collection_properties(property_keys.len() as u32); + + with_weight( + >::delete_collection_properties(self, sender, property_keys), + weight, + ) } fn set_token_properties( &self, - _sender: T::CrossAccountId, - _token_id: TokenId, - _property: Vec, - _nesting_budget: &dyn Budget, + sender: T::CrossAccountId, + token_id: TokenId, + properties: Vec, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::set_token_properties(properties.len() as u32); + + with_weight( + >::set_token_properties( + self, + &sender, + token_id, + properties.into_iter(), + false, + nesting_budget, + ), + weight, + ) } fn set_token_property_permissions( &self, - _sender: &T::CrossAccountId, - _property_permissions: Vec, + sender: &T::CrossAccountId, + property_permissions: Vec, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = + >::set_token_property_permissions(property_permissions.len() as u32); + + with_weight( + >::set_token_property_permissions(self, sender, property_permissions), + weight, + ) } fn delete_token_properties( &self, - _sender: T::CrossAccountId, - _token_id: TokenId, - _property_keys: Vec, - _nesting_budget: &dyn Budget, + sender: T::CrossAccountId, + token_id: TokenId, + property_keys: Vec, + nesting_budget: &dyn Budget, ) -> DispatchResultWithPostInfo { - fail!(>::SettingPropertiesNotAllowed) + let weight = >::delete_token_properties(property_keys.len() as u32); + + with_weight( + >::delete_token_properties( + self, + &sender, + token_id, + property_keys.into_iter(), + nesting_budget, + ), + weight, + ) } fn check_nesting( @@ -371,8 +434,8 @@ TokenId(>::get(self.id)) } - fn token_owner(&self, _token: TokenId) -> Option { - None + fn token_owner(&self, token: TokenId) -> Option { + >::token_owner(self.id, token) } fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option { --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -14,17 +14,93 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! # Refungible Pallet +//! +//! The Refungible pallet provides functionality for handling refungible collections and tokens. +//! +//! - [`Config`] +//! - [`RefungibleHandle`] +//! - [`Pallet`] +//! - [`CommonWeights`] +//! +//! ## Overview +//! +//! The Refungible pallet provides functions for: +//! +//! - RFT collection creation and removal +//! - Minting and burning of RFT tokens +//! - Partition and repartition of RFT tokens +//! - Retrieving number of pieces of RFT token +//! - Retrieving account balances +//! - Transfering RFT token pieces +//! - Burning RFT token pieces +//! - Setting and checking allowance for RFT tokens +//! +//! ### Terminology +//! +//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all +//! of the RFT token pieces than it owns the RFT token and can repartition it. +//! +//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection. +//! Each collection has its own settings and set of permissions. +//! +//! - **RFT token piece:** A fungible part of an RFT token. +//! +//! - **Balance:** RFT token pieces owned by an account +//! +//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to +//! transfer from the balance of another account +//! +//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from +//! an account balance. +//! +//! ### Implementations +//! +//! The Refungible pallet provides implementations for the following traits. If these traits provide +//! the functionality that you need, then you can avoid coupling with the Refungible pallet. +//! +//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight +//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing +//! with collections +//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible +//! collection +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for +//! some accounts. +//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection. +//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left. +//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection. +//! Nests the RFT token if RFT token pieces are sent to another token. +//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens. +//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account. +//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces. +//! +//! ## Assumptions +//! +//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`. +//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`. +//! * Sender should be in collection's allow list to perform operations on tokens. + #![cfg_attr(not(feature = "std"), no_std)] -use frame_support::{ensure, BoundedVec}; +use frame_support::{ensure, fail, BoundedVec, transactional, storage::with_transaction}; use up_data_structs::{ AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId, CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget, + Property, PropertyScope, TrySetProperty, PropertyKey, PropertyValue, PropertyPermission, + PropertyKeyPermission, }; use pallet_evm::account::CrossAccountId; -use pallet_common::{Error as CommonError, Event as CommonEvent, Pallet as PalletCommon}; +use pallet_common::{ + Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, + CommonCollectionOperations as _, +}; use pallet_structure::Pallet as PalletStructure; -use sp_runtime::{ArithmeticError, DispatchError, DispatchResult}; +use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome}; use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap}; use core::ops::Deref; use codec::{Encode, Decode, MaxEncodedLen}; @@ -86,13 +162,17 @@ #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); + /// Amount of tokens minted for collection #[pallet::storage] pub type TokensMinted = StorageMap; + + /// Amount of burnt tokens for collection #[pallet::storage] pub type TokensBurnt = StorageMap; + /// Custom data serialized to bytes for token #[pallet::storage] pub type TokenData = StorageNMap< Key = (Key, Key), @@ -101,6 +181,16 @@ >; #[pallet::storage] + #[pallet::getter(fn token_properties)] + pub type TokenProperties = StorageNMap< + Key = (Key, Key), + Value = up_data_structs::Properties, + QueryKind = ValueQuery, + OnEmpty = up_data_structs::TokenProperties, + >; + + /// Total amount of pieces for token + #[pallet::storage] pub type TotalSupply = StorageNMap< Key = (Key, Key), Value = u128, @@ -119,6 +209,7 @@ QueryKind = ValueQuery, >; + /// Amount of tokens owned by account #[pallet::storage] pub type AccountBalance = StorageNMap< Key = ( @@ -130,6 +221,7 @@ QueryKind = ValueQuery, >; + /// Amount of token pieces owned by account #[pallet::storage] pub type Balance = StorageNMap< Key = ( @@ -142,6 +234,7 @@ QueryKind = ValueQuery, >; + /// Allowance set by an owner for a spender for a token #[pallet::storage] pub type Allowance = StorageNMap< Key = ( @@ -188,22 +281,65 @@ } impl Pallet { + /// Get number of RFT tokens in collection pub fn total_supply(collection: &RefungibleHandle) -> u32 { >::get(collection.id) - >::get(collection.id) } + + /// Check that RFT token exists + /// + /// - `token`: Token ID. pub fn token_exists(collection: &RefungibleHandle, token: TokenId) -> bool { >::contains_key((collection.id, token)) } + + pub fn set_scoped_token_property( + collection_id: CollectionId, + token_id: TokenId, + scope: PropertyScope, + property: Property, + ) -> DispatchResult { + TokenProperties::::try_mutate((collection_id, token_id), |properties| { + properties.try_scoped_set(scope, property.key, property.value) + }) + .map_err(>::from)?; + + Ok(()) + } + + pub fn set_scoped_token_properties( + collection_id: CollectionId, + token_id: TokenId, + scope: PropertyScope, + properties: impl Iterator, + ) -> DispatchResult { + TokenProperties::::try_mutate((collection_id, token_id), |stored_properties| { + stored_properties.try_scoped_set_from_iter(scope, properties) + }) + .map_err(>::from)?; + + Ok(()) + } } // unchecked calls skips any permission checks impl Pallet { + /// Create RFT collection + /// + /// `init_collection` will take non-refundable deposit for collection creation. + /// + /// - `data`: Contains settings for collection limits and permissions. pub fn init_collection( owner: T::CrossAccountId, data: CreateCollectionData, ) -> Result { >::init_collection(owner, data, false) } + + /// Destroy RFT collection + /// + /// `destroy_collection` will throw error if collection contains any tokens. + /// Only owner can destroy collection. pub fn destroy_collection( collection: RefungibleHandle, sender: &T::CrossAccountId, @@ -235,13 +371,17 @@ .is_some() } - pub fn burn_token(collection: &RefungibleHandle, token_id: TokenId) -> DispatchResult { + pub fn burn_token_unchecked( + collection: &RefungibleHandle, + token_id: TokenId, + ) -> DispatchResult { let burnt = >::get(collection.id) .checked_add(1) .ok_or(ArithmeticError::Overflow)?; >::insert(collection.id, burnt); >::remove((collection.id, token_id)); + >::remove((collection.id, token_id)); >::remove((collection.id, token_id)); >::remove_prefix((collection.id, token_id), None); >::remove_prefix((collection.id, token_id), None); @@ -249,6 +389,17 @@ Ok(()) } + /// Burn RFT token pieces + /// + /// `burn` will decrease total amount of token pieces and amount owned by sender. + /// `burn` can be called even if there are multiple owners of the RFT token. + /// If sender wouldn't have any pieces left after `burn` than she will stop being + /// one of the owners of the token. If there is no account that owns any pieces of + /// the token than token will be burned too. + /// + /// - `amount`: Amount of token pieces to burn. + /// - `token`: Token who's pieces should be burned + /// - `collection`: Collection that contains the token pub fn burn( collection: &RefungibleHandle, owner: &T::CrossAccountId, @@ -276,7 +427,7 @@ >::remove((collection.id, owner, token)); >::unnest_if_nested(owner, collection.id, token); >::insert((collection.id, owner), account_balance); - Self::burn_token(collection, token)?; + Self::burn_token_unchecked(collection, token)?; >::deposit_event(CommonEvent::ItemDestroyed( collection.id, token, @@ -319,6 +470,179 @@ Ok(()) } + #[transactional] + fn modify_token_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + properties: impl Iterator)>, + is_token_create: bool, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + let is_collection_admin = || collection.is_owner_or_admin(sender); + let is_token_owner = || -> Result { + let balance = collection.balance(sender.clone(), token_id); + let total_pieces: u128 = + Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX); + if balance != total_pieces { + return Ok(false); + } + + let is_bundle_owner = >::check_indirectly_owned( + sender.clone(), + collection.id, + token_id, + None, + nesting_budget, + )?; + + Ok(is_bundle_owner) + }; + + for (key, value) in properties { + let permission = >::property_permissions(collection.id) + .get(&key) + .cloned() + .unwrap_or_else(PropertyPermission::none); + + let is_property_exists = TokenProperties::::get((collection.id, token_id)) + .get(&key) + .is_some(); + + match permission { + PropertyPermission { mutable: false, .. } if is_property_exists => { + return Err(>::NoPermission.into()); + } + + PropertyPermission { + collection_admin, + token_owner, + .. + } => { + //TODO: investigate threats during public minting. + let is_token_create = + is_token_create && (collection_admin || token_owner) && value.is_some(); + if !(is_token_create + || (collection_admin && is_collection_admin()) + || (token_owner && is_token_owner()?)) + { + fail!(>::NoPermission); + } + } + } + + match value { + Some(value) => { + >::try_mutate((collection.id, token_id), |properties| { + properties.try_set(key.clone(), value) + }) + .map_err(>::from)?; + + >::deposit_event(CommonEvent::TokenPropertySet( + collection.id, + token_id, + key, + )); + } + None => { + >::try_mutate((collection.id, token_id), |properties| { + properties.remove(&key) + }) + .map_err(>::from)?; + + >::deposit_event(CommonEvent::TokenPropertyDeleted( + collection.id, + token_id, + key, + )); + } + } + } + + Ok(()) + } + + pub fn set_token_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + properties: impl Iterator, + is_token_create: bool, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + Self::modify_token_properties( + collection, + sender, + token_id, + properties.map(|p| (p.key, Some(p.value))), + is_token_create, + nesting_budget, + ) + } + + pub fn set_token_property( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + property: Property, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + let is_token_create = false; + + Self::set_token_properties( + collection, + sender, + token_id, + [property].into_iter(), + is_token_create, + nesting_budget, + ) + } + + pub fn delete_token_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + property_keys: impl Iterator, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + let is_token_create = false; + + Self::modify_token_properties( + collection, + sender, + token_id, + property_keys.into_iter().map(|key| (key, None)), + is_token_create, + nesting_budget, + ) + } + + pub fn delete_token_property( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + property_key: PropertyKey, + nesting_budget: &dyn Budget, + ) -> DispatchResult { + Self::delete_token_properties( + collection, + sender, + token_id, + [property_key].into_iter(), + nesting_budget, + ) + } + + /// Transfer RFT token pieces from one account to another. + /// + /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token. + /// + /// - `from`: Owner of token pieces to transfer. + /// - `to`: Recepient of transfered token pieces. + /// - `amount`: Amount of token pieces to transfer. + /// - `token`: Token whos pieces should be transfered + /// - `collection`: Collection that contains the token pub fn transfer( collection: &RefungibleHandle, from: &T::CrossAccountId, @@ -423,6 +747,11 @@ Ok(()) } + /// Batched operation to create multiple RFT tokens. + /// + /// Same as `create_item` but creates multiple tokens. + /// + /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens. pub fn create_multiple_items( collection: &RefungibleHandle, sender: &T::CrossAccountId, @@ -507,32 +836,58 @@ // ========= + with_transaction(|| { + for (i, data) in data.iter().enumerate() { + let token_id = first_token_id + i as u32 + 1; + >::insert((collection.id, token_id), totals[i]); + + >::insert( + (collection.id, token_id), + ItemData { + const_data: data.const_data.clone(), + }, + ); + + for (user, amount) in data.users.iter() { + if *amount == 0 { + continue; + } + >::insert((collection.id, token_id, &user), amount); + >::insert((collection.id, &user, TokenId(token_id)), true); + >::nest_if_sent_to_token_unchecked( + user, + collection.id, + TokenId(token_id), + ); + } + + if let Err(e) = Self::set_token_properties( + collection, + sender, + TokenId(token_id), + data.properties.clone().into_iter(), + true, + nesting_budget, + ) { + return TransactionOutcome::Rollback(Err(e)); + } + } + TransactionOutcome::Commit(Ok(())) + })?; + >::insert(collection.id, tokens_minted); + for (account, balance) in balances { >::insert((collection.id, account), balance); } + for (i, token) in data.into_iter().enumerate() { let token_id = first_token_id + i as u32 + 1; - >::insert((collection.id, token_id), totals[i]); - >::insert( - (collection.id, token_id), - ItemData { - const_data: token.const_data, - }, - ); - for (user, amount) in token.users.into_iter() { if amount == 0 { continue; } - >::insert((collection.id, token_id, &user), amount); - >::insert((collection.id, &user, TokenId(token_id)), true); - >::nest_if_sent_to_token_unchecked( - &user, - collection.id, - TokenId(token_id), - ); // TODO: ERC20 transfer event >::deposit_event(CommonEvent::ItemCreated( @@ -568,6 +923,9 @@ )) } + /// Set allowance for the spender to `transfer` or `burn` sender's token pieces. + /// + /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn. pub fn set_allowance( collection: &RefungibleHandle, sender: &T::CrossAccountId, @@ -636,6 +994,12 @@ Ok(allowance) } + /// Transfer RFT token pieces from one account to another. + /// + /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces. + /// The owner should set allowance for the spender to transfer pieces. + /// + /// [`transfer`]: struct.Pallet.html#method.transfer pub fn transfer_from( collection: &RefungibleHandle, spender: &T::CrossAccountId, @@ -657,6 +1021,12 @@ Ok(()) } + /// Burn RFT token pieces from the account. + /// + /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should + /// set allowance for the spender to burn pieces + /// + /// [`burn`]: struct.Pallet.html#method.burn pub fn burn_from( collection: &RefungibleHandle, spender: &T::CrossAccountId, @@ -677,7 +1047,13 @@ Ok(()) } - /// Delegated to `create_multiple_items` + /// Create RFT token. + /// + /// The sender should be the owner/admin of the collection or collection should be configured + /// to allow public minting. + /// + /// - `data`: Contains list of users who will become the owners of the token pieces and amount + /// of token pieces they will receive. pub fn create_item( collection: &RefungibleHandle, sender: &T::CrossAccountId, @@ -687,6 +1063,13 @@ Self::create_multiple_items(collection, sender, vec![data], nesting_budget) } + /// Repartition RFT token. + /// + /// `repartition` will set token balance of the sender and total amount of token pieces. + /// Sender should own all of the token pieces. `repartition' could be done even if some + /// token pieces were burned before. + /// + /// - `amount`: Total amount of token pieces that the token will have after `repartition`. pub fn repartition( collection: &RefungibleHandle, owner: &T::CrossAccountId, @@ -699,10 +1082,10 @@ ); ensure!(amount > 0, >::TokenValueTooLow); // Ensure user owns all pieces - let total_supply = >::get((collection.id, token)); + let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX); let balance = >::get((collection.id, token, owner)); ensure!( - total_supply == balance, + total_pieces == balance, >::RepartitionWhileNotOwningAllPieces ); @@ -711,7 +1094,44 @@ Ok(()) } + fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option { + let mut owner = None; + let mut count = 0; + for key in Balance::::iter_key_prefix((collection_id, token_id)) { + count += 1; + if count > 1 { + return None; + } + owner = Some(key); + } + owner + } + fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option { >::try_get((collection_id, token_id)).ok() } + + pub fn set_collection_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + properties: Vec, + ) -> DispatchResult { + >::set_collection_properties(collection, sender, properties) + } + + pub fn delete_collection_properties( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + property_keys: Vec, + ) -> DispatchResult { + >::delete_collection_properties(collection, sender, property_keys) + } + + pub fn set_token_property_permissions( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + property_permissions: Vec, + ) -> DispatchResult { + >::set_token_property_permissions(collection, sender, property_permissions) + } } --- a/pallets/refungible/src/weights.rs +++ b/pallets/refungible/src/weights.rs @@ -3,7 +3,7 @@ //! Autogenerated weights for pallet_refungible //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-06-27, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-07-20, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 // Executed Command: @@ -49,6 +49,9 @@ fn transfer_from_removing() -> Weight; fn transfer_from_creating_removing() -> Weight; fn burn_from() -> Weight; + fn set_token_property_permissions(b: u32, ) -> Weight; + fn set_token_properties(b: u32, ) -> Weight; + fn delete_token_properties(b: u32, ) -> Weight; fn repartition_item() -> Weight; } @@ -62,7 +65,7 @@ // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) fn create_item() -> Weight { - (17_553_000 as Weight) + (21_310_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } @@ -73,9 +76,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (10_654_000 as Weight) - // Standard Error: 1_000 - .saturating_add((5_114_000 as Weight).saturating_mul(b as Weight)) + (9_552_000 as Weight) + // Standard Error: 2_000 + .saturating_add((7_056_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) .saturating_add(T::DbWeight::get().writes((4 as Weight).saturating_mul(b as Weight))) @@ -87,9 +90,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_items(b: u32, ) -> Weight { - (3_587_000 as Weight) + (4_857_000 as Weight) // Standard Error: 2_000 - .saturating_add((7_931_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((9_838_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(1 as Weight)) @@ -102,9 +105,9 @@ // Storage: Refungible Balance (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_owners(b: u32, ) -> Weight { - (1_980_000 as Weight) + (11_335_000 as Weight) // Standard Error: 2_000 - .saturating_add((6_305_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((6_784_000 as Weight).saturating_mul(b as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(T::DbWeight::get().writes(3 as Weight)) @@ -115,7 +118,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn burn_item_partial() -> Weight { - (21_010_000 as Weight) + (21_239_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -125,14 +128,15 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_item_fully() -> Weight { - (28_413_000 as Weight) + (29_426_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(7 as Weight)) } // Storage: Refungible Balance (r:2 w:2) fn transfer_normal() -> Weight { - (17_513_000 as Weight) + (17_743_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } @@ -140,7 +144,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_creating() -> Weight { - (20_469_000 as Weight) + (20_699_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -148,7 +152,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_removing() -> Weight { - (22_472_000 as Weight) + (22_833_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -156,21 +160,21 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_creating_removing() -> Weight { - (24_866_000 as Weight) + (24_936_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } // Storage: Refungible Balance (r:1 w:0) // Storage: Refungible Allowance (r:0 w:1) fn approve() -> Weight { - (13_475_000 as Weight) + (13_446_000 as Weight) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Refungible Allowance (r:1 w:1) // Storage: Refungible Balance (r:2 w:2) fn transfer_from_normal() -> Weight { - (24_707_000 as Weight) + (24_777_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } @@ -179,7 +183,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_creating() -> Weight { - (27_812_000 as Weight) + (28_483_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -188,7 +192,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_removing() -> Weight { - (29_966_000 as Weight) + (29_896_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(5 as Weight)) } @@ -197,7 +201,7 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_from_creating_removing() -> Weight { - (31_660_000 as Weight) + (32_070_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(7 as Weight)) } @@ -208,15 +212,42 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (36_248_000 as Weight) + (36_789_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) - .saturating_add(T::DbWeight::get().writes(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:1) + fn set_token_property_permissions(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 62_000 + .saturating_add((15_803_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn set_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_668_000 + .saturating_add((302_308_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn delete_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_619_000 + .saturating_add((294_574_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Refungible TotalSupply (r:1 w:1) // Storage: Refungible Balance (r:1 w:1) fn repartition_item() -> Weight { - (8_226_000 as Weight) + (8_325_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } @@ -231,7 +262,7 @@ // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) fn create_item() -> Weight { - (17_553_000 as Weight) + (21_310_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } @@ -242,9 +273,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items(b: u32, ) -> Weight { - (10_654_000 as Weight) - // Standard Error: 1_000 - .saturating_add((5_114_000 as Weight).saturating_mul(b as Weight)) + (9_552_000 as Weight) + // Standard Error: 2_000 + .saturating_add((7_056_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) .saturating_add(RocksDbWeight::get().writes((4 as Weight).saturating_mul(b as Weight))) @@ -256,9 +287,9 @@ // Storage: Refungible TokenData (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_items(b: u32, ) -> Weight { - (3_587_000 as Weight) + (4_857_000 as Weight) // Standard Error: 2_000 - .saturating_add((7_931_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((9_838_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) @@ -271,9 +302,9 @@ // Storage: Refungible Balance (r:0 w:4) // Storage: Refungible Owned (r:0 w:4) fn create_multiple_items_ex_multiple_owners(b: u32, ) -> Weight { - (1_980_000 as Weight) + (11_335_000 as Weight) // Standard Error: 2_000 - .saturating_add((6_305_000 as Weight).saturating_mul(b as Weight)) + .saturating_add((6_784_000 as Weight).saturating_mul(b as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight))) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) @@ -284,7 +315,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn burn_item_partial() -> Weight { - (21_010_000 as Weight) + (21_239_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -294,14 +325,15 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_item_fully() -> Weight { - (28_413_000 as Weight) + (29_426_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(7 as Weight)) } // Storage: Refungible Balance (r:2 w:2) fn transfer_normal() -> Weight { - (17_513_000 as Weight) + (17_743_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } @@ -309,7 +341,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_creating() -> Weight { - (20_469_000 as Weight) + (20_699_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -317,7 +349,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_removing() -> Weight { - (22_472_000 as Weight) + (22_833_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -325,21 +357,21 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_creating_removing() -> Weight { - (24_866_000 as Weight) + (24_936_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } // Storage: Refungible Balance (r:1 w:0) // Storage: Refungible Allowance (r:0 w:1) fn approve() -> Weight { - (13_475_000 as Weight) + (13_446_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } // Storage: Refungible Allowance (r:1 w:1) // Storage: Refungible Balance (r:2 w:2) fn transfer_from_normal() -> Weight { - (24_707_000 as Weight) + (24_777_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } @@ -348,7 +380,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_creating() -> Weight { - (27_812_000 as Weight) + (28_483_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -357,7 +389,7 @@ // Storage: Refungible AccountBalance (r:1 w:1) // Storage: Refungible Owned (r:0 w:1) fn transfer_from_removing() -> Weight { - (29_966_000 as Weight) + (29_896_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } @@ -366,7 +398,7 @@ // Storage: Refungible AccountBalance (r:2 w:2) // Storage: Refungible Owned (r:0 w:2) fn transfer_from_creating_removing() -> Weight { - (31_660_000 as Weight) + (32_070_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(7 as Weight)) } @@ -377,15 +409,42 @@ // Storage: Refungible TokensBurnt (r:1 w:1) // Storage: Refungible TokenData (r:0 w:1) // Storage: Refungible Owned (r:0 w:1) + // Storage: Refungible TokenProperties (r:0 w:1) fn burn_from() -> Weight { - (36_248_000 as Weight) + (36_789_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) - .saturating_add(RocksDbWeight::get().writes(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(8 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:1) + fn set_token_property_permissions(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 62_000 + .saturating_add((15_803_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn set_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_668_000 + .saturating_add((302_308_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } + // Storage: Common CollectionPropertyPermissions (r:1 w:0) + // Storage: Refungible TokenProperties (r:1 w:1) + fn delete_token_properties(b: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 1_619_000 + .saturating_add((294_574_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } // Storage: Refungible TotalSupply (r:1 w:1) // Storage: Refungible Balance (r:1 w:1) fn repartition_item() -> Weight { - (8_226_000 as Weight) + (8_325_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } --- a/pallets/scheduler/src/lib.rs +++ b/pallets/scheduler/src/lib.rs @@ -152,7 +152,6 @@ pub use preimage_provider::PreimageProviderAndMaybeRecipient; -/// Weight templates for calculating actual fees pub(crate) trait MarginalWeightInfo: WeightInfo { fn item(periodic: bool, named: bool, resolved: Option) -> Weight { match (periodic, named, resolved) { @@ -259,7 +258,7 @@ /// If `Some` then the number of blocks to postpone execution for when the item is delayed. type NoPreimagePostponement: Get>; - /// Sponsoring function. In this version sposorship is disabled + /// Sponsoring function. // type SponsorshipHandler: SponsorshipHandler::Call>; /// The helper type used for custom transaction fee logic. @@ -268,7 +267,6 @@ /// A Scheduler-Runtime interface for finer payment handling. pub trait DispatchCall { - /// Lock balance required for transaction payment fn reserve_balance( id: ScheduledId, sponsor: ::AccountId, @@ -292,7 +290,6 @@ TransactionValidityError, >; - /// Cancel schedule reservation and unlock balance fn cancel_reserve( id: ScheduledId, sponsor: ::AccountId, @@ -436,7 +433,6 @@ continue; } - // Sender is the account who signed transaction let sender = ensure_signed( <::Origin as From>::from(s.origin.clone()) .into(), @@ -500,6 +496,7 @@ } /// Weight should be 0, because transaction already paid 0 + //total_weight } } --- a/pallets/structure/src/benchmarking.rs +++ b/pallets/structure/src/benchmarking.rs @@ -1,3 +1,19 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + use super::*; use frame_benchmarking::{benchmarks, account}; --- a/pallets/structure/src/lib.rs +++ b/pallets/structure/src/lib.rs @@ -1,3 +1,56 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +//! # Structure Pallet +//! +//! The Structure pallet provides functionality for handling tokens nesting an unnesting. +//! +//! - [`Config`] +//! - [`Pallet`] +//! +//! ## Overview +//! +//! The Structure pallet provides functions for: +//! +//! - Searching for token parents, children and owners. Actual implementation of searching for +//! parent/child is done by pallets corresponding to token's collection type. +//! - Nesting and unnesting tokens. Actual implementation of nesting is done by pallets corresponding +//! to token's collection type. +//! +//! ### Terminology +//! +//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting +//! owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in +//! it's child token i.e. parent-child relationship graph shouldn't have +//! +//! - **Parent:** Token that current token is nested in. +//! +//! - **Owner:** Account that owns the token and all nested tokens. +//! +//! ## Interface +//! +//! ### Available Functions +//! +//! - `find_parent` - Find parent of the token. It could be an account or another token. +//! - `parent_chain` - Find chain of parents of the token. +//! - `find_topmost_owner` - Find account or token in the end of the chain of parents. +//! - `check_nesting` - Check if the token could be nested in the other token +//! - `nest_if_sent_to_token` - Nest the token in the other token +//! - `unnest_if_nested` - Unnest the token from the other token + #![cfg_attr(not(feature = "std"), no_std)] use pallet_common::CommonCollectionOperations; @@ -82,6 +135,12 @@ } impl Pallet { + /// Find account owning the `token` or a token that the `token` is nested in. + /// + /// Returns the enum that have three variants: + /// - [`User`](crate::Parent::User): Contains account. + /// - [`Token`](crate::Parent::Token): Contains token id and collection id. + /// - [`TokenNotFound`](crate::Parent::TokenNotFound): Indicates that parent was not found pub fn find_parent( collection: CollectionId, token: TokenId, @@ -103,6 +162,11 @@ }) } + /// Get the chain of parents of a token in the nesting hierarchy + /// + /// Returns an iterator of addresses of the owning tokens and the owning account, + /// starting from the immediate parent token, ending with the account. + /// Returns error if cycle is detected. pub fn parent_chain( mut collection: CollectionId, mut token: TokenId, @@ -133,6 +197,8 @@ /// Try to dereference address, until finding top level owner /// /// May return token address if parent token not yet exists + /// + /// - `budget`: Limit for searching parents in depth. pub fn find_topmost_owner( collection: CollectionId, token: TokenId, @@ -149,6 +215,10 @@ }) } + /// Find the topmost parent and check that assigning `for_nest` token as a child for + /// `token` wouldn't create a cycle. + /// + /// - `budget`: Limit for searching parents in depth. pub fn get_checked_topmost_owner( collection: CollectionId, token: TokenId, @@ -177,6 +247,10 @@ Err(>::DepthLimit.into()) } + /// Burn token and all of it's nested tokens + /// + /// - `self_budget`: Limit for searching children in depth. + /// - `breadth_budget`: Limit of breadth of searching children. pub fn burn_item_recursively( from: T::CrossAccountId, collection: CollectionId, @@ -190,7 +264,13 @@ dispatch.burn_item_recursively(from.clone(), token, self_budget, breadth_budget) } - /// Check if token indirectly owned by specified user + /// Check if `token` indirectly owned by `user` + /// + /// Returns `true` if `user` is `token`'s owner. Or If token is provided as `user` then + /// check that `user` and `token` have same owner. + /// Checks that assigning `for_nest` token as a child for `token` wouldn't create a cycle. + /// + /// - `budget`: Limit for searching parents in depth. pub fn check_indirectly_owned( user: T::CrossAccountId, collection: CollectionId, @@ -207,6 +287,12 @@ .map(|indirect_owner| indirect_owner == target_parent) } + /// Checks that `under` is valid token and that `token_id` could be nested under it + /// and that `from` is `under`'s owner + /// + /// Returns OK if `under` is not a token + /// + /// - `nesting_budget`: Limit for searching parents in depth. pub fn check_nesting( from: T::CrossAccountId, under: &T::CrossAccountId, @@ -219,6 +305,11 @@ }) } + /// Nests `token_id` under `under` token + /// + /// Returns OK if `under` is not a token. Checks that nesting is possible. + /// + /// - `nesting_budget`: Limit for searching parents in depth. pub fn nest_if_sent_to_token( from: T::CrossAccountId, under: &T::CrossAccountId, @@ -235,6 +326,9 @@ }) } + /// Nests `token_id` under `owner` token + /// + /// Caller should check that nesting wouldn't cause recursion in nesting pub fn nest_if_sent_to_token_unchecked( owner: &T::CrossAccountId, collection_id: CollectionId, @@ -245,6 +339,7 @@ }); } + /// Unnests `token_id` from `owner`. pub fn unnest_if_nested( owner: &T::CrossAccountId, collection_id: CollectionId, --- a/primitives/data-structs/Cargo.toml +++ b/primitives/data-structs/Cargo.toml @@ -6,7 +6,7 @@ license = 'GPLv3' homepage = "https://unique.network" repository = 'https://github.com/UniqueNetwork/unique-chain' -version = '0.1.0' +version = '0.1.1' [dependencies] scale-info = { version = "2.0.1", default-features = false, features = [ --- /dev/null +++ b/primitives/data-structs/Changelog.md @@ -0,0 +1,3 @@ +### 0.1.1 +--- +* Added fields with properties to `CreateReFungibleData` and `CreateRefungibleExData`. \ No newline at end of file --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -534,7 +534,12 @@ #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] #[derivative(Debug(format_with = "bounded::vec_debug"))] pub const_data: BoundedVec, + pub pieces: u128, + + #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] + #[derivative(Debug(format_with = "bounded::vec_debug"))] + pub properties: CollectionPropertiesVec, } #[derive(Encode, Decode, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)] @@ -568,6 +573,8 @@ pub const_data: BoundedVec, #[derivative(Debug(format_with = "bounded::map_debug"))] pub users: BoundedBTreeMap>, + #[derivative(Debug(format_with = "bounded::vec_debug"))] + pub properties: CollectionPropertiesVec, } #[derive(Encode, Decode, MaxEncodedLen, PartialEq, Clone, TypeInfo, Derivative)] --- a/runtime/opal/src/lib.rs +++ b/runtime/opal/src/lib.rs @@ -707,7 +707,6 @@ pub type Barrier = ( TakeWeightCredit, AllowTopLevelPaidExecutionFrom, - AllowUnpaidExecutionFrom, // ^^^ Parent & its unit plurality gets free execution ); --- a/runtime/quartz/src/lib.rs +++ b/runtime/quartz/src/lib.rs @@ -706,7 +706,6 @@ pub type Barrier = ( TakeWeightCredit, AllowTopLevelPaidExecutionFrom, - AllowUnpaidExecutionFrom, // ^^^ Parent & its unit plurality gets free execution ); --- a/runtime/unique/src/lib.rs +++ b/runtime/unique/src/lib.rs @@ -705,7 +705,6 @@ pub type Barrier = ( TakeWeightCredit, AllowTopLevelPaidExecutionFrom, - AllowUnpaidExecutionFrom, // ^^^ Parent & its unit plurality gets free execution ); --- a/tests/src/createMultipleItems.test.ts +++ b/tests/src/createMultipleItems.test.ts @@ -376,14 +376,26 @@ // ReFungible const collectionIdReFungible = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - const argsReFungible = [ - {ReFungible: ['1'.repeat(2049), 10]}, - {ReFungible: ['2'.repeat(2049), 10]}, - {ReFungible: ['3'.repeat(2049), 10]}, - ]; - const createMultipleItemsTxFungible = api.tx.unique - .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible); - await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected; + { + const argsReFungible = [ + {ReFungible: ['1'.repeat(2049), 10, []]}, + {ReFungible: ['2'.repeat(2049), 10, []]}, + {ReFungible: ['3'.repeat(2049), 10, []]}, + ]; + const createMultipleItemsTxFungible = api.tx.unique + .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible); + await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected; + } + { + const argsReFungible = [ + {ReFungible: {properties: [{key: 'key', value: 'A'.repeat(32769)}]}}, + {ReFungible: {properties: [{key: 'key', value: 'B'.repeat(32769)}]}}, + {ReFungible: {properties: [{key: 'key', value: 'C'.repeat(32769)}]}}, + ]; + const createMultipleItemsTxFungible = api.tx.unique + .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible); + await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected; + } }); }); --- a/tests/src/eth/api/UniqueFungible.sol +++ b/tests/src/eth/api/UniqueFungible.sol @@ -28,6 +28,154 @@ function burnFrom(address from, uint256 amount) external returns (bool); } +// Selector: 7d9262e6 +interface Collection is Dummy, ERC165 { + // Set collection property. + // + // @param key Property key. + // @param value Propery value. + // + // Selector: setCollectionProperty(string,bytes) 2f073f66 + function setCollectionProperty(string memory key, bytes memory value) + external; + + // Delete collection property. + // + // @param key Property key. + // + // Selector: deleteCollectionProperty(string) 7b7debce + function deleteCollectionProperty(string memory key) external; + + // Get collection property. + // + // @dev Throws error if key not found. + // + // @param key Property key. + // @return bytes The property corresponding to the key. + // + // Selector: collectionProperty(string) cf24fd6d + function collectionProperty(string memory key) + external + view + returns (bytes memory); + + // Set the sponsor of the collection. + // + // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + // + // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + // + // Selector: setCollectionSponsor(address) 7623402e + function setCollectionSponsor(address sponsor) external; + + // Collection sponsorship confirmation. + // + // @dev After setting the sponsor for the collection, it must be confirmed with this function. + // + // Selector: confirmCollectionSponsorship() 3c50e97a + function confirmCollectionSponsorship() external; + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "accountTokenOwnershipLimit", + // "sponsoredDataSize", + // "sponsoredDataRateLimit", + // "tokenLimit", + // "sponsorTransferTimeout", + // "sponsorApproveTimeout" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,uint32) 6a3841db + function setCollectionLimit(string memory limit, uint32 value) external; + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "ownerCanTransfer", + // "ownerCanDestroy", + // "transfersEnabled" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,bool) 993b7fba + function setCollectionLimit(string memory limit, bool value) external; + + // Get contract address. + // + // Selector: contractAddress() f6b4dfb4 + function contractAddress() external view returns (address); + + // Add collection admin by substrate address. + // @param new_admin Substrate administrator address. + // + // Selector: addCollectionAdminSubstrate(uint256) 5730062b + function addCollectionAdminSubstrate(uint256 newAdmin) external; + + // Remove collection admin by substrate address. + // @param admin Substrate administrator address. + // + // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + function removeCollectionAdminSubstrate(uint256 admin) external; + + // Add collection admin. + // @param new_admin Address of the added administrator. + // + // Selector: addCollectionAdmin(address) 92e462c7 + function addCollectionAdmin(address newAdmin) external; + + // Remove collection admin. + // + // @param new_admin Address of the removed administrator. + // + // Selector: removeCollectionAdmin(address) fafd7b42 + function removeCollectionAdmin(address admin) external; + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + // + // Selector: setCollectionNesting(bool) 112d4586 + function setCollectionNesting(bool enable) external; + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + // @param collections Addresses of collections that will be available for nesting. + // + // Selector: setCollectionNesting(bool,address[]) 64872396 + function setCollectionNesting(bool enable, address[] memory collections) + external; + + // Set the collection access method. + // @param mode Access mode + // 0 for Normal + // 1 for AllowList + // + // Selector: setCollectionAccess(uint8) 41835d4c + function setCollectionAccess(uint8 mode) external; + + // Add the user to the allowed list. + // + // @param user Address of a trusted user. + // + // Selector: addToCollectionAllowList(address) 67844fe6 + function addToCollectionAllowList(address user) external; + + // Remove the user from the allowed list. + // + // @param user Address of a removed user. + // + // Selector: removeFromCollectionAllowList(address) 85c51acb + function removeFromCollectionAllowList(address user) external; + + // Switch permission for minting. + // + // @param mode Enable if "true". + // + // Selector: setCollectionMintMode(bool) 00018e84 + function setCollectionMintMode(bool mode) external; +} + // Selector: 942e8b22 interface ERC20 is Dummy, ERC165, ERC20Events { // Selector: name() 06fdde03 @@ -63,39 +211,6 @@ external view returns (uint256); -} - -// Selector: c894dc35 -interface Collection is Dummy, ERC165 { - // Selector: setCollectionProperty(string,bytes) 2f073f66 - function setCollectionProperty(string memory key, bytes memory value) - external; - - // Selector: deleteCollectionProperty(string) 7b7debce - function deleteCollectionProperty(string memory key) external; - - // Throws error if key not found - // - // Selector: collectionProperty(string) cf24fd6d - function collectionProperty(string memory key) - external - view - returns (bytes memory); - - // Selector: ethSetSponsor(address) 8f9af356 - function ethSetSponsor(address sponsor) external; - - // Selector: ethConfirmSponsorship() a8580d1a - function ethConfirmSponsorship() external; - - // Selector: setLimit(string,uint32) 68db30ca - function setLimit(string memory limit, uint32 value) external; - - // Selector: setLimit(string,bool) ea67e4c2 - function setLimit(string memory limit, bool value) external; - - // Selector: contractAddress() f6b4dfb4 - function contractAddress() external view returns (address); } interface UniqueFungible is --- a/tests/src/eth/api/UniqueNFT.sol +++ b/tests/src/eth/api/UniqueNFT.sol @@ -44,6 +44,13 @@ // Selector: 41369377 interface TokenProperties is Dummy, ERC165 { + // @notice Set permissions for token property. + // @dev Throws error if `msg.sender` is not admin or owner of the collection. + // @param key Property key. + // @param is_mutable Permission to mutate property. + // @param collection_admin Permission to mutate property by collection admin if property is mutable. + // @param token_owner Permission to mutate property by token owner if property is mutable. + // // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa function setTokenPropertyPermission( string memory key, @@ -52,6 +59,12 @@ bool tokenOwner ) external; + // @notice Set token property value. + // @dev Throws error if `msg.sender` has no permission to edit the property. + // @param tokenId ID of the token. + // @param key Property key. + // @param value Property value. + // // Selector: setProperty(uint256,string,bytes) 1752d67b function setProperty( uint256 tokenId, @@ -59,10 +72,19 @@ bytes memory value ) external; + // @notice Delete token property value. + // @dev Throws error if `msg.sender` has no permission to edit the property. + // @param tokenId ID of the token. + // @param key Property key. + // // Selector: deleteProperty(uint256,string) 066111d1 function deleteProperty(uint256 tokenId, string memory key) external; - // Throws error if key not found + // @notice Get token property value. + // @dev Throws error if key not found + // @param tokenId ID of the token. + // @param key Property key. + // @return Property value bytes // // Selector: property(uint256,string) 7228c327 function property(uint256 tokenId, string memory key) @@ -73,19 +95,36 @@ // Selector: 42966c68 interface ERC721Burnable is Dummy, ERC165 { + // @notice Burns a specific ERC721 token. + // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + // operator of the current owner. + // @param tokenId The NFT to approve + // // Selector: burn(uint256) 42966c68 function burn(uint256 tokenId) external; } // Selector: 58800161 interface ERC721 is Dummy, ERC165, ERC721Events { + // @notice Count all NFTs assigned to an owner + // @dev NFTs assigned to the zero address are considered invalid, and this + // function throws for queries about the zero address. + // @param owner An address for whom to query the balance + // @return The number of NFTs owned by `owner`, possibly zero + // // Selector: balanceOf(address) 70a08231 function balanceOf(address owner) external view returns (uint256); + // @notice Find the owner of an NFT + // @dev NFTs assigned to zero address are considered invalid, and queries + // about them do throw. + // @param tokenId The identifier for an NFT + // @return The address of the owner of the NFT + // // Selector: ownerOf(uint256) 6352211e function ownerOf(uint256 tokenId) external view returns (address); - // Not implemented + // @dev Not implemented // // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672 function safeTransferFromWithData( @@ -95,7 +134,7 @@ bytes memory data ) external; - // Not implemented + // @dev Not implemented // // Selector: safeTransferFrom(address,address,uint256) 42842e0e function safeTransferFrom( @@ -104,6 +143,17 @@ uint256 tokenId ) external; + // @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + // TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE + // THEY MAY BE PERMANENTLY LOST + // @dev Throws unless `msg.sender` is the current owner or an authorized + // operator for this NFT. Throws if `from` is not the current owner. Throws + // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + // @param from The current owner of the NFT + // @param to The new owner + // @param tokenId The NFT to transfer + // @param _value Not used for an NFT + // // Selector: transferFrom(address,address,uint256) 23b872dd function transferFrom( address from, @@ -111,20 +161,27 @@ uint256 tokenId ) external; + // @notice Set or reaffirm the approved address for an NFT + // @dev The zero address indicates there is no approved address. + // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + // operator of the current owner. + // @param approved The new approved NFT controller + // @param tokenId The NFT to approve + // // Selector: approve(address,uint256) 095ea7b3 function approve(address approved, uint256 tokenId) external; - // Not implemented + // @dev Not implemented // // Selector: setApprovalForAll(address,bool) a22cb465 function setApprovalForAll(address operator, bool approved) external; - // Not implemented + // @dev Not implemented // // Selector: getApproved(uint256) 081812fc function getApproved(uint256 tokenId) external view returns (address); - // Not implemented + // @dev Not implemented // // Selector: isApprovedForAll(address,address) e985e9c5 function isApprovedForAll(address owner, address operator) @@ -135,13 +192,21 @@ // Selector: 5b5e139f interface ERC721Metadata is Dummy, ERC165 { + // @notice A descriptive name for a collection of NFTs in this contract + // // Selector: name() 06fdde03 function name() external view returns (string memory); + // @notice An abbreviated name for NFTs in this contract + // // Selector: symbol() 95d89b41 function symbol() external view returns (string memory); - // Returns token's const_metadata + // @notice A distinct Uniform Resource Identifier (URI) for a given asset. + // @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC + // 3986. The URI may point to a JSON file that conforms to the "ERC721 + // Metadata JSON Schema". + // @return token's const_metadata // // Selector: tokenURI(uint256) c87b56dd function tokenURI(uint256 tokenId) external view returns (string memory); @@ -152,14 +217,21 @@ // Selector: mintingFinished() 05d2035b function mintingFinished() external view returns (bool); - // `token_id` should be obtained with `next_token_id` method, - // unlike standard, you can't specify it manually + // @notice Function to mint token. + // @dev `tokenId` should be obtained with `nextTokenId` method, + // unlike standard, you can't specify it manually + // @param to The new owner + // @param tokenId ID of the minted NFT // // Selector: mint(address,uint256) 40c10f19 function mint(address to, uint256 tokenId) external returns (bool); - // `token_id` should be obtained with `next_token_id` method, - // unlike standard, you can't specify it manually + // @notice Function to mint token with the given tokenUri. + // @dev `tokenId` should be obtained with `nextTokenId` method, + // unlike standard, you can't specify it manually + // @param to The new owner + // @param tokenId ID of the minted NFT + // @param tokenUri Token URI that would be stored in the NFT properties // // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f function mintWithTokenURI( @@ -168,7 +240,7 @@ string memory tokenUri ) external returns (bool); - // Not implemented + // @dev Not implemented // // Selector: finishMinting() 7d64bcb4 function finishMinting() external returns (bool); @@ -176,10 +248,15 @@ // Selector: 780e9d63 interface ERC721Enumerable is Dummy, ERC165 { + // @notice Enumerate valid NFTs + // @param index A counter less than `totalSupply()` + // @return The token identifier for the `index`th NFT, + // (sort order not specified) + // // Selector: tokenByIndex(uint256) 4f6ccce7 function tokenByIndex(uint256 index) external view returns (uint256); - // Not implemented + // @dev Not implemented // // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59 function tokenOfOwnerByIndex(address owner, uint256 index) @@ -187,20 +264,38 @@ view returns (uint256); + // @notice Count NFTs tracked by this contract + // @return A count of valid NFTs tracked by this contract, where each one of + // them has an assigned and queryable owner not equal to the zero address + // // Selector: totalSupply() 18160ddd function totalSupply() external view returns (uint256); } // Selector: 7d9262e6 interface Collection is Dummy, ERC165 { + // Set collection property. + // + // @param key Property key. + // @param value Propery value. + // // Selector: setCollectionProperty(string,bytes) 2f073f66 function setCollectionProperty(string memory key, bytes memory value) external; + // Delete collection property. + // + // @param key Property key. + // // Selector: deleteCollectionProperty(string) 7b7debce function deleteCollectionProperty(string memory key) external; - // Throws error if key not found + // Get collection property. + // + // @dev Throws error if key not found. + // + // @param key Property key. + // @return bytes The property corresponding to the key. // // Selector: collectionProperty(string) cf24fd6d function collectionProperty(string memory key) @@ -208,69 +303,168 @@ view returns (bytes memory); + // Set the sponsor of the collection. + // + // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + // + // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + // // Selector: setCollectionSponsor(address) 7623402e function setCollectionSponsor(address sponsor) external; + // Collection sponsorship confirmation. + // + // @dev After setting the sponsor for the collection, it must be confirmed with this function. + // // Selector: confirmCollectionSponsorship() 3c50e97a function confirmCollectionSponsorship() external; + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "accountTokenOwnershipLimit", + // "sponsoredDataSize", + // "sponsoredDataRateLimit", + // "tokenLimit", + // "sponsorTransferTimeout", + // "sponsorApproveTimeout" + // @param value Value of the limit. + // // Selector: setCollectionLimit(string,uint32) 6a3841db function setCollectionLimit(string memory limit, uint32 value) external; + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "ownerCanTransfer", + // "ownerCanDestroy", + // "transfersEnabled" + // @param value Value of the limit. + // // Selector: setCollectionLimit(string,bool) 993b7fba function setCollectionLimit(string memory limit, bool value) external; + // Get contract address. + // // Selector: contractAddress() f6b4dfb4 function contractAddress() external view returns (address); + // Add collection admin by substrate address. + // @param new_admin Substrate administrator address. + // // Selector: addCollectionAdminSubstrate(uint256) 5730062b - function addCollectionAdminSubstrate(uint256 newAdmin) external view; + function addCollectionAdminSubstrate(uint256 newAdmin) external; + // Remove collection admin by substrate address. + // @param admin Substrate administrator address. + // // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 - function removeCollectionAdminSubstrate(uint256 newAdmin) external view; + function removeCollectionAdminSubstrate(uint256 admin) external; + // Add collection admin. + // @param new_admin Address of the added administrator. + // // Selector: addCollectionAdmin(address) 92e462c7 - function addCollectionAdmin(address newAdmin) external view; + function addCollectionAdmin(address newAdmin) external; + // Remove collection admin. + // + // @param new_admin Address of the removed administrator. + // // Selector: removeCollectionAdmin(address) fafd7b42 - function removeCollectionAdmin(address admin) external view; + function removeCollectionAdmin(address admin) external; + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + // // Selector: setCollectionNesting(bool) 112d4586 function setCollectionNesting(bool enable) external; + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + // @param collections Addresses of collections that will be available for nesting. + // // Selector: setCollectionNesting(bool,address[]) 64872396 function setCollectionNesting(bool enable, address[] memory collections) external; + // Set the collection access method. + // @param mode Access mode + // 0 for Normal + // 1 for AllowList + // // Selector: setCollectionAccess(uint8) 41835d4c function setCollectionAccess(uint8 mode) external; + // Add the user to the allowed list. + // + // @param user Address of a trusted user. + // // Selector: addToCollectionAllowList(address) 67844fe6 - function addToCollectionAllowList(address user) external view; + function addToCollectionAllowList(address user) external; + // Remove the user from the allowed list. + // + // @param user Address of a removed user. + // // Selector: removeFromCollectionAllowList(address) 85c51acb - function removeFromCollectionAllowList(address user) external view; + function removeFromCollectionAllowList(address user) external; + // Switch permission for minting. + // + // @param mode Enable if "true". + // // Selector: setCollectionMintMode(bool) 00018e84 function setCollectionMintMode(bool mode) external; } // Selector: d74d154f interface ERC721UniqueExtensions is Dummy, ERC165 { + // @notice Transfer ownership of an NFT + // @dev Throws unless `msg.sender` is the current owner. Throws if `to` + // is the zero address. Throws if `tokenId` is not a valid NFT. + // @param to The new owner + // @param tokenId The NFT to transfer + // @param _value Not used for an NFT + // // Selector: transfer(address,uint256) a9059cbb function transfer(address to, uint256 tokenId) external; + // @notice Burns a specific ERC721 token. + // @dev Throws unless `msg.sender` is the current owner or an authorized + // operator for this NFT. Throws if `from` is not the current owner. Throws + // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + // @param from The current owner of the NFT + // @param tokenId The NFT to transfer + // @param _value Not used for an NFT + // // Selector: burnFrom(address,uint256) 79cc6790 function burnFrom(address from, uint256 tokenId) external; + // @notice Returns next free NFT ID. + // // Selector: nextTokenId() 75794a3c function nextTokenId() external view returns (uint256); + // @notice Function to mint multiple tokens. + // @dev `tokenIds` should be an array of consecutive numbers and first number + // should be obtained with `nextTokenId` method + // @param to The new owner + // @param tokenIds IDs of the minted NFTs + // // Selector: mintBulk(address,uint256[]) 44a9945e function mintBulk(address to, uint256[] memory tokenIds) external returns (bool); + // @notice Function to mint multiple tokens with the given tokenUris. + // @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive + // numbers and first number should be obtained with `nextTokenId` method + // @param to The new owner + // @param tokens array of pairs of token ID and token URI for minted tokens + // // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006 function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) external --- a/tests/src/eth/fungibleAbi.json +++ b/tests/src/eth/fungibleAbi.json @@ -51,6 +51,33 @@ }, { "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "addCollectionAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "newAdmin", "type": "uint256" } + ], + "name": "addCollectionAdminSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "addToCollectionAllowList", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" } ], @@ -97,6 +124,13 @@ }, { "inputs": [], + "name": "confirmCollectionSponsorship", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], "name": "contractAddress", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", @@ -118,43 +152,51 @@ }, { "inputs": [], - "name": "ethConfirmSponsorship", - "outputs": [], - "stateMutability": "nonpayable", + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", "type": "function" }, { "inputs": [ - { "internalType": "address", "name": "sponsor", "type": "address" } + { "internalType": "address", "name": "admin", "type": "address" } ], - "name": "ethSetSponsor", + "name": "removeCollectionAdmin", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { - "inputs": [], - "name": "name", - "outputs": [{ "internalType": "string", "name": "", "type": "string" }], - "stateMutability": "view", + "inputs": [ + { "internalType": "uint256", "name": "admin", "type": "uint256" } + ], + "name": "removeCollectionAdminSubstrate", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ - { "internalType": "string", "name": "key", "type": "string" }, - { "internalType": "bytes", "name": "value", "type": "bytes" } + { "internalType": "address", "name": "user", "type": "address" } ], - "name": "setCollectionProperty", + "name": "removeFromCollectionAllowList", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { + "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }], + "name": "setCollectionAccess", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "string", "name": "limit", "type": "string" }, { "internalType": "uint32", "name": "value", "type": "uint32" } ], - "name": "setLimit", + "name": "setCollectionLimit", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -164,7 +206,54 @@ { "internalType": "string", "name": "limit", "type": "string" }, { "internalType": "bool", "name": "value", "type": "bool" } ], - "name": "setLimit", + "name": "setCollectionLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }], + "name": "setCollectionMintMode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }], + "name": "setCollectionNesting", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bool", "name": "enable", "type": "bool" }, + { + "internalType": "address[]", + "name": "collections", + "type": "address[]" + } + ], + "name": "setCollectionNesting", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "string", "name": "key", "type": "string" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "name": "setCollectionProperty", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sponsor", "type": "address" } + ], + "name": "setCollectionSponsor", "outputs": [], "stateMutability": "nonpayable", "type": "function" --- a/tests/src/eth/nonFungibleAbi.json +++ b/tests/src/eth/nonFungibleAbi.json @@ -86,7 +86,7 @@ ], "name": "addCollectionAdmin", "outputs": [], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { @@ -95,7 +95,7 @@ ], "name": "addCollectionAdminSubstrate", "outputs": [], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { @@ -104,7 +104,7 @@ ], "name": "addToCollectionAllowList", "outputs": [], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { @@ -304,16 +304,16 @@ ], "name": "removeCollectionAdmin", "outputs": [], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ - { "internalType": "uint256", "name": "newAdmin", "type": "uint256" } + { "internalType": "uint256", "name": "admin", "type": "uint256" } ], "name": "removeCollectionAdminSubstrate", "outputs": [], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { @@ -322,7 +322,7 @@ ], "name": "removeFromCollectionAllowList", "outputs": [], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { --- a/tests/src/interfaces/augment-api-errors.ts +++ b/tests/src/interfaces/augment-api-errors.ts @@ -277,15 +277,16 @@ }; fungible: { /** - * Fungible token does not support nested + * Fungible token does not support nesting. **/ FungibleDisallowsNesting: AugmentedError; /** - * Tried to set data for fungible item + * Tried to set data for fungible item. **/ FungibleItemsDontHaveData: AugmentedError; /** - * Not default id passed as TokenId argument + * Not default id passed as TokenId argument. + * The default value of TokenId for Fungible collection is 0. **/ FungibleItemsHaveNoId: AugmentedError; /** @@ -293,7 +294,7 @@ **/ NotFungibleDataUsedToMintFungibleCollectionToken: AugmentedError; /** - * Setting item properties is not allowed + * Setting item properties is not allowed. **/ SettingPropertiesNotAllowed: AugmentedError; /** --- a/tests/src/interfaces/augment-api-query.ts +++ b/tests/src/interfaces/augment-api-query.ts @@ -187,8 +187,17 @@ [key: string]: QueryableStorageEntry; }; fungible: { + /** + * Storage for delegated assets. + **/ allowance: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; + /** + * Amount of tokens owned by an account inside a collection. + **/ balance: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; + /** + * Total amount of fungible tokens inside a collection. + **/ totalSupply: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; /** * Generic query @@ -400,16 +409,35 @@ [key: string]: QueryableStorageEntry; }; refungible: { + /** + * Amount of tokens owned by account + **/ accountBalance: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; + /** + * Allowance set by an owner for a spender for a token + **/ allowance: AugmentedQuery Observable, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; + /** + * Amount of token pieces owned by account + **/ balance: AugmentedQuery Observable, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; /** * Used to enumerate tokens owned by account **/ owned: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32]> & QueryableStorageEntry; + /** + * Custom data serialized to bytes for token + **/ tokenData: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; + tokenProperties: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; tokensBurnt: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; + /** + * Amount of tokens minted for collection + **/ tokensMinted: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; + /** + * Total amount of pieces for token + **/ totalSupply: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; /** * Generic query --- a/tests/src/interfaces/default/types.ts +++ b/tests/src/interfaces/default/types.ts @@ -2461,12 +2461,14 @@ export interface UpDataStructsCreateReFungibleData extends Struct { readonly constData: Bytes; readonly pieces: u128; + readonly properties: Vec; } /** @name UpDataStructsCreateRefungibleExData */ export interface UpDataStructsCreateRefungibleExData extends Struct { readonly constData: Bytes; readonly users: BTreeMap; + readonly properties: Vec; } /** @name UpDataStructsNestingPermissions */ --- a/tests/src/interfaces/lookup.ts +++ b/tests/src/interfaces/lookup.ts @@ -1502,7 +1502,8 @@ **/ UpDataStructsCreateReFungibleData: { constData: 'Bytes', - pieces: 'u128' + pieces: 'u128', + properties: 'Vec' }, /** * Lookup193: up_data_structs::CreateItemExData> @@ -1527,7 +1528,8 @@ **/ UpDataStructsCreateRefungibleExData: { constData: 'Bytes', - users: 'BTreeMap' + users: 'BTreeMap', + properties: 'Vec' }, /** * Lookup204: pallet_unique_scheduler::pallet::Call --- a/tests/src/interfaces/types-lookup.ts +++ b/tests/src/interfaces/types-lookup.ts @@ -1629,6 +1629,7 @@ export interface UpDataStructsCreateReFungibleData extends Struct { readonly constData: Bytes; readonly pieces: u128; + readonly properties: Vec; } /** @name UpDataStructsCreateItemExData (193) */ @@ -1654,6 +1655,7 @@ export interface UpDataStructsCreateRefungibleExData extends Struct { readonly constData: Bytes; readonly users: BTreeMap; + readonly properties: Vec; } /** @name PalletUniqueSchedulerCall (204) */ --- a/tests/src/nesting/properties.test.ts +++ b/tests/src/nesting/properties.test.ts @@ -2,6 +2,7 @@ import usingApi, {executeTransaction} from '../substrate/substrate-api'; import { addCollectionAdminExpectSuccess, + CollectionMode, createCollectionExpectSuccess, setCollectionPermissionsExpectSuccess, createItemExpectSuccess, @@ -23,9 +24,9 @@ }); }); - it('Makes sure collectionById supplies required fields', async () => { + async function testMakeSureSuppliesRequired(mode: CollectionMode) { await usingApi(async api => { - const collectionId = await createCollectionExpectSuccess(); + const collectionId = await createCollectionExpectSuccess({mode: mode}); const collectionOption = await api.rpc.unique.collectionById(collectionId); expect(collectionOption.isSome).to.be.true; @@ -57,7 +58,15 @@ expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties); }); + } + + it('Makes sure collectionById supplies required fields for NFT', async () => { + await testMakeSureSuppliesRequired({type: 'NFT'}); }); + + it('Makes sure collectionById supplies required fields for ReFungible', async () => { + await testMakeSureSuppliesRequired({type: 'ReFungible'}); + }); }); // ---------- COLLECTION PROPERTIES @@ -79,9 +88,10 @@ }); }); - it('Sets properties for a collection', async () => { + + async function testSetsPropertiesForCollection(mode: string) { await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'})); + const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); const {collectionId} = getCreateCollectionResult(events); // As owner @@ -106,48 +116,54 @@ {key: 'black_hole', value: ''}, ]); }); + } + it('Sets properties for a NFT collection', async () => { + await testSetsPropertiesForCollection('NFT'); }); + it('Sets properties for a ReFungible collection', async () => { + await testSetsPropertiesForCollection('ReFungible'); + }); - it('Check valid names for collection properties keys', async () => { + async function testCheckValidNames(mode: string) { await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'})); + const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); const {collectionId} = getCreateCollectionResult(events); - + // alpha symbols await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), )).to.not.be.rejected; - + // numeric symbols await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), )).to.not.be.rejected; - + // underscore symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), )).to.not.be.rejected; - + // dash symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), )).to.not.be.rejected; - + // underscore symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), )).to.not.be.rejected; - + const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs']; const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman(); expect(properties).to.be.deep.equal([ @@ -158,54 +174,72 @@ {key: 'build.rs', value: ''}, ]); }); + } + it('Check valid names for NFT collection properties keys', async () => { + await testCheckValidNames('NFT'); + }); + it('Check valid names for ReFungible collection properties keys', async () => { + await testCheckValidNames('ReFungible'); }); - it('Changes properties of a collection', async () => { + async function testChangesProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), )).to.not.be.rejected; - + // Mutate the properties await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; - + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ {key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}, ]); }); + } + it('Changes properties of a NFT collection', async () => { + await testChangesProperties({type: 'NFT'}); + }); + it('Changes properties of a ReFungible collection', async () => { + await testChangesProperties({type: 'ReFungible'}); }); - it('Deletes properties of a collection', async () => { + async function testDeleteProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.deleteCollectionProperties(collection, ['electron']), )).to.not.be.rejected; - + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ {key: 'black_hole', value: 'LIGO'}, ]); - }); + }); + } + it('Deletes properties of a NFT collection', async () => { + await testDeleteProperties({type: 'NFT'}); + }); + it('Deletes properties of a ReFungible collection', async () => { + await testDeleteProperties({type: 'ReFungible'}); }); }); @@ -217,10 +251,10 @@ }); }); - it('Fails to set properties in a collection if not its onwer/administrator', async () => { + async function testFailsSetPropertiesIfNotOwnerOrAdmin(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, bob, @@ -230,14 +264,20 @@ const properties = (await api.query.common.collectionProperties(collection)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.equal(0); - }); + }); + } + it('Fails to set properties in a NFT collection if not its onwer/administrator', async () => { + await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'NFT'}); }); + it('Fails to set properties in a ReFungible collection if not its onwer/administrator', async () => { + await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'ReFungible'}); + }); - it('Fails to set properties that exceed the limits', async () => { + async function testFailsSetPropertiesThatExeedLimits(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); + const collection = await createCollectionExpectSuccess({mode: mode}); const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; - + // Mute the general tx parsing error, too many bytes to process { console.error = () => {}; @@ -247,10 +287,10 @@ api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), )).to.be.rejected; } - + let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON(); expect(properties).to.be.empty; - + await expect(executeTransaction( api, alice, @@ -259,16 +299,22 @@ {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, ]), )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - + properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON(); expect(properties).to.be.empty; - }); + }); + } + it('Fails to set properties that exceed the limits (NFT)', async () => { + await testFailsSetPropertiesThatExeedLimits({type: 'NFT'}); + }); + it('Fails to set properties that exceed the limits (ReFungible)', async () => { + await testFailsSetPropertiesThatExeedLimits({type: 'ReFungible'}); }); - it('Fails to set more properties than it is allowed', async () => { + async function testFailsSetMorePropertiesThanAllowed(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const propertiesToBeSet = []; for (let i = 0; i < 65; i++) { propertiesToBeSet.push({ @@ -276,29 +322,35 @@ value: Math.random() > 0.5 ? 'high' : 'low', }); } - + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), )).to.be.rejectedWith(/common\.PropertyLimitReached/); - + const properties = (await api.query.common.collectionProperties(collection)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.equal(0); - }); + }); + } + it('Fails to set more properties than it is allowed (NFT)', async () => { + await testFailsSetMorePropertiesThanAllowed({type: 'NFT'}); }); - - it('Fails to set properties with invalid names', async () => { + it('Fails to set more properties than it is allowed (ReFungible)', async () => { + await testFailsSetMorePropertiesThanAllowed({type: 'ReFungible'}); + }); + + async function testFailsSetPropertiesWithInvalidNames(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const invalidProperties = [ [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}], [{key: 'Mr/Sandman', value: 'Bring me a gene'}], [{key: 'déjà vu', value: 'hmm...'}], ]; - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -306,13 +358,13 @@ api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } - + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - + await expect(executeTransaction( api, alice, @@ -320,14 +372,14 @@ {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, ]), ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat(''); - + const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman(); expect(properties).to.be.deep.equal([ {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, ]); - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -336,7 +388,13 @@ ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } }); + } + it('Fails to set properties with invalid names (NFT)', async () => { + await testFailsSetPropertiesWithInvalidNames({type: 'NFT'}); }); + it('Fails to set properties with invalid names (ReFungible)', async () => { + await testFailsSetPropertiesWithInvalidNames({type: 'ReFungible'}); + }); }); // ---------- ACCESS RIGHTS @@ -357,54 +415,66 @@ }); }); - it('Sets access rights to properties of a collection', async () => { + async function testSetsAccessRightsToProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), )).to.not.be.rejected; - + await addCollectionAdminExpectSuccess(alice, collection, bob.address); - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), )).to.not.be.rejected; - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}}, {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}}, ]); - }); + }); + } + it('Sets access rights to properties of a collection (NFT)', async () => { + await testSetsAccessRightsToProperties({type: 'NFT'}); }); + it('Sets access rights to properties of a collection (ReFungible)', async () => { + await testSetsAccessRightsToProperties({type: 'ReFungible'}); + }); - it('Changes access rights to properties of a collection', async () => { + async function testChangesAccessRightsToProperty(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), )).to.not.be.rejected; - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, ]); }); + } + it('Changes access rights to properties of a NFT collection', async () => { + await testChangesAccessRightsToProperty({type: 'NFT'}); }); + it('Changes access rights to properties of a ReFungible collection', async () => { + await testChangesAccessRightsToProperty({type: 'ReFungible'}); + }); }); describe('Negative Integration Test: Access Rights to Token Properties', () => { @@ -415,25 +485,31 @@ }); }); - it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => { + async function testPreventsFromSettingAccessRightsNotAdminOrOwner(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, bob, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), )).to.be.rejectedWith(/common\.NoPermission/); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON(); expect(propertyRights).to.be.empty; }); + } + it('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async () => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'NFT'}); }); + it('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', async () => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'ReFungible'}); + }); - it('Prevents from adding too many possible properties', async () => { + async function testPreventFromAddingTooManyPossibleProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const constitution = []; for (let i = 0; i < 65; i++) { constitution.push({ @@ -441,51 +517,63 @@ permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {}, }); } - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, constitution), )).to.be.rejectedWith(/common\.PropertyLimitReached/); - + const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON(); expect(propertyRights).to.be.empty; - }); + }); + } + it('Prevents from adding too many possible properties (NFT)', async () => { + await testPreventFromAddingTooManyPossibleProperties({type: 'NFT'}); }); + it('Prevents from adding too many possible properties (ReFungible)', async () => { + await testPreventFromAddingTooManyPossibleProperties({type: 'ReFungible'}); + }); - it('Prevents access rights to be modified if constant', async () => { + async function testPreventAccessRightsModifiedIfConstant(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), )).to.be.rejectedWith(/common\.NoPermission/); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.deep.equal([ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, ]); - }); + }); + } + it('Prevents access rights to be modified if constant (NFT)', async () => { + await testPreventAccessRightsModifiedIfConstant({type: 'NFT'}); + }); + it('Prevents access rights to be modified if constant (ReFungible)', async () => { + await testPreventAccessRightsModifiedIfConstant({type: 'ReFungible'}); }); - it('Prevents adding properties with invalid names', async () => { + async function testPreventsAddingPropertiesWithInvalidNames(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const invalidProperties = [ [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}], [{key: 'G#4', permission: {tokenOwner: true}}], [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}], ]; - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -493,13 +581,13 @@ api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - + const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string await expect(executeTransaction( api, @@ -508,30 +596,33 @@ {key: correctKey, permission: {collectionAdmin: true}}, ]), ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat(''); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}, ]); }); + } + it('Prevents adding properties with invalid names (NFT)', async () => { + await testPreventsAddingPropertiesWithInvalidNames({type: 'NFT'}); + }); + it('Prevents adding properties with invalid names (ReFungible)', async () => { + await testPreventsAddingPropertiesWithInvalidNames({type: 'ReFungible'}); }); }); // ---------- TOKEN PROPERTIES describe('Integration Test: Token Properties', () => { - let collection: number; - let token: number; - let nestedToken: number; let permissions: {permission: any, signers: IKeyringPair[]}[]; before(async () => { await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - charlie = privateKeyWrapper('//Charlie'); + alice = privateKeyWrapper('//Alice'); // collection owner + bob = privateKeyWrapper('//Bob'); // collection admin + charlie = privateKeyWrapper('//Charlie'); // token owner permissions = [ {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]}, @@ -543,36 +634,34 @@ ]; }); }); - - beforeEach(async () => { - await usingApi(async () => { - collection = await createCollectionExpectSuccess(); - await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); - - token = await createItemExpectSuccess(alice, collection, 'NFT'); - nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); - - await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie); - }); - }); - it('Reads yet empty properties of a token', async () => { + async function testReadsYetEmptyProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.be.equal(0); - + const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties; expect(tokenData).to.be.empty; }); + } + it('Reads yet empty properties of a token (NFT)', async () => { + await testReadsYetEmptyProperties({type: 'NFT'}); }); + it('Reads yet empty properties of a token (ReFungible)', async () => { + await testReadsYetEmptyProperties({type: 'ReFungible'}); + }); - it('Assigns properties to a token according to permissions', async () => { + async function testAssignPropertiesAccordingToPermissions(mode: CollectionMode, pieces: number) { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -603,10 +692,21 @@ expect(tokensData[i].value).to.be.equal('Serotonin increase'); } }); + } + it('Assigns properties to a token according to permissions (NFT)', async () => { + await testAssignPropertiesAccordingToPermissions({type: 'NFT'}, 1); + }); + it('Assigns properties to a token according to permissions (ReFungible)', async () => { + await testAssignPropertiesAccordingToPermissions({type: 'ReFungible'}, 100); }); - it('Changes properties of a token according to permissions', async () => { + async function testChangesPropertiesAccordingPermission(mode: CollectionMode, pieces: number) { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -615,29 +715,29 @@ for (const signer of permission.signers) { const key = i + '_' + signer.address; propertyKeys.push(key); - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected; } - + i++; } - + const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[]; const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[]; for (let i = 0; i < properties.length; i++) { @@ -645,32 +745,43 @@ expect(tokensData[i].value).to.be.equal('Serotonin stable'); } }); + } + it('Changes properties of a token according to permissions (NFT)', async () => { + await testChangesPropertiesAccordingPermission({type: 'NFT'}, 1); }); + it('Changes properties of a token according to permissions (ReFungible)', async () => { + await testChangesPropertiesAccordingPermission({type: 'ReFungible'}, 100); + }); - it('Deletes properties of a token according to permissions', async () => { + async function testDeletePropertiesAccordingPermission(mode: CollectionMode, pieces: number) { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: mode}); + const token = await createItemExpectSuccess(alice, collection, mode.type); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); + const propertyKeys: string[] = []; let i = 0; - + for (const permission of permissions) { if (!permission.permission.mutable) continue; for (const signer of permission.signers) { const key = i + '_' + signer.address; propertyKeys.push(key); - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, @@ -680,17 +791,30 @@ i++; } - + const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[]; expect(properties).to.be.empty; const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[]; expect(tokensData).to.be.empty; expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0); }); + } + it('Deletes properties of a token according to permissions (NFT)', async () => { + await testDeletePropertiesAccordingPermission({type: 'NFT'}, 1); }); + it('Deletes properties of a token according to permissions (ReFungible)', async () => { + await testDeletePropertiesAccordingPermission({type: 'ReFungible'}, 100); + }); it('Assigns properties to a nested token according to permissions', async () => { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); + await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); + const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -725,6 +849,13 @@ it('Changes properties of a nested token according to permissions', async () => { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); + await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); + const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie); + const propertyKeys: string[] = []; let i = 0; for (const permission of permissions) { @@ -767,6 +898,13 @@ it('Deletes properties of a nested token according to permissions', async () => { await usingApi(async api => { + const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); + await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}}); + const token = await createItemExpectSuccess(alice, collection, 'NFT'); + const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)}); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + await transferExpectSuccess(collection, token, alice, charlie); + const propertyKeys: string[] = []; let i = 0; @@ -832,11 +970,11 @@ }); }); - beforeEach(async () => { - collection = await createCollectionExpectSuccess(); - token = await createItemExpectSuccess(alice, collection, 'NFT'); + async function prepare(mode: CollectionMode, pieces: number) { + collection = await createCollectionExpectSuccess({mode: mode}); + token = await createItemExpectSuccess(alice, collection, mode.type); await addCollectionAdminExpectSuccess(alice, collection, bob.address); - await transferExpectSuccess(collection, token, alice, charlie); + await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type); await usingApi(async api => { let i = 0; @@ -848,71 +986,89 @@ alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; - + await expect(executeTransaction( api, signer, api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; - + i++; } - + originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number; - }); - }); + }); + } - it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => { + async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); + await usingApi(async api => { let i = -1; for (const forbiddance of constitution) { i++; if (!forbiddance.permission.mutable) continue; - + await expect(executeTransaction( api, forbiddance.sinner, api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); - + await expect(executeTransaction( api, forbiddance.sinner, api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); } - + const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); }); + } + it('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async () => { + await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'NFT'}, 1); }); + it('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', async () => { + await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'ReFungible'}, 100); + }); - it('Forbids changing/deleting properties of a token if the property is permanent (immutable)', async () => { + async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); + await usingApi(async api => { let i = -1; for (const permission of constitution) { i++; if (permission.permission.mutable) continue; - + await expect(executeTransaction( api, permission.signers[0], api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); - + await expect(executeTransaction( api, permission.signers[0], api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); } - + const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); - }); + }); + } + it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async () => { + await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'NFT'}, 1); }); + it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', async () => { + await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'ReFungible'}, 100); + }); + + async function testForbidsAddingPropertiesIfPropertyNotDeclared(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); - it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => { await usingApi(async api => { await expect(executeTransaction( api, @@ -925,20 +1081,28 @@ alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), ), 'on setting a new non-permitted property').to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/); - + expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty; const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); }); + } + it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async () => { + await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'NFT'}, 1); + }); + it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', async () => { + await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'ReFungible'}, 100); }); - it('Forbids adding too many properties to a token', async () => { + async function testForbidsAddingTooManyProperties(mode: CollectionMode, pieces: number) { + await prepare(mode, pieces); + await usingApi(async api => { await expect(executeTransaction( api, @@ -948,7 +1112,7 @@ {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}}, ]), ), 'on setting a new non-permitted property').to.not.be.rejected; - + // Mute the general tx parsing error { console.error = () => {}; @@ -958,7 +1122,7 @@ api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), )).to.be.rejected; } - + await expect(executeTransaction( api, alice, @@ -967,10 +1131,107 @@ {key: 'young_years', value: 'neverending'.repeat(1490)}, ]), )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - + expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young_years'])).toJSON()).to.be.empty; const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(propertiesMap.consumedSpace).to.be.equal(originalSpace); }); + } + it('Forbids adding too many properties to a token (NFT)', async () => { + await testForbidsAddingTooManyProperties({type: 'NFT'}, 1); + }); + it('Forbids adding too many properties to a token (ReFungible)', async () => { + await testForbidsAddingTooManyProperties({type: 'ReFungible'}, 100); + }); +}); + +describe('ReFungible token properties permissions tests', () => { + let collection: number; + let token: number; + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + charlie = privateKeyWrapper('//Charlie'); + }); + }); + + beforeEach(async () => { + await usingApi(async api => { + collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + token = await createItemExpectSuccess(alice, collection, 'ReFungible'); + await addCollectionAdminExpectSuccess(alice, collection, bob.address); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), + )).to.not.be.rejected; + }); + }); + + it('Forbids add token property with tokenOwher==true but signer have\'t all pieces', async () => { + await usingApi(async api => { + await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'word'}, + ]), + )).to.be.rejectedWith(/common\.NoPermission/); + }); + }); + + it('Forbids mutate token property with tokenOwher==true but signer have\'t all pieces', async () => { + await usingApi(async api => { + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), + )).to.not.be.rejected; + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'word'}, + ]), + )).to.be.not.rejected; + + await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'bad word'}, + ]), + )).to.be.rejectedWith(/common\.NoPermission/); + }); + }); + + it('Forbids delete token property with tokenOwher==true but signer have\'t all pieces', async () => { + await usingApi(async api => { + await expect(executeTransaction( + api, + alice, + api.tx.unique.setTokenProperties(collection, token, [ + {key: 'key', value: 'word'}, + ]), + )).to.be.not.rejected; + + await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible'); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.deleteTokenProperties(collection, token, [ + 'key', + ]), + )).to.be.rejectedWith(/common\.NoPermission/); + }); }); }); --- a/tests/src/refungible.test.ts +++ b/tests/src/refungible.test.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {default as usingApi} from './substrate/substrate-api'; +import {default as usingApi, executeTransaction} from './substrate/substrate-api'; import {IKeyringPair} from '@polkadot/types/types'; import { createCollectionExpectSuccess, @@ -30,6 +30,8 @@ transfer, burnItem, repartitionRFT, + createCollectionWithPropsExpectSuccess, + getDetailedCollectionInfo, } from './util/helpers'; import chai from 'chai'; @@ -187,3 +189,26 @@ }); }); }); + +describe('Test Refungible properties:', () => { + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + }); + }); + + it('Сreate new collection with properties', async () => { + await usingApi(async api => { + const properties = [{key: 'key1', value: 'val1'}]; + const propertyPermissions = [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}]; + const collectionId = await createCollectionWithPropsExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'ReFungible'}, + properties: properties, + propPerm: propertyPermissions, + }); + const collection = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collection.properties.toHuman()).to.be.deep.equal(properties); + expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); + }); + }); +}); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -284,7 +284,7 @@ type: 'ReFungible'; } -type CollectionMode = Nft | Fungible | ReFungible; +export type CollectionMode = Nft | Fungible | ReFungible; export type Property = { key: any, @@ -1414,7 +1414,7 @@ tx = api.tx.unique.createItem(collectionId, to, createData as any); } - const events = await submitTransactionAsync(sender, tx); + const events = await executeTransaction(api, sender, tx); const result = getCreateItemResult(events); const itemCountAfter = await getLastTokenId(api, collectionId); --- a/tests/src/xcmTransfer.test.ts +++ b/tests/src/xcmTransfer.test.ts @@ -31,6 +31,7 @@ const UNIQUE_CHAIN = 1000; const KARURA_CHAIN = 2000; const KARURA_PORT = '9946'; +const TRANSFER_AMOUNT = 2000000000000000000000000n; describe('Integration test: Exchanging QTZ with Karura', () => { let alice: IKeyringPair; @@ -113,7 +114,7 @@ }, }, fun: { - Fungible: 5000000000, + Fungible: TRANSFER_AMOUNT, }, }, ], @@ -148,19 +149,17 @@ await usingApi(async (api) => { const destination = { - V0: { - X3: [ - 'Parent', - { - Parachain: UNIQUE_CHAIN, - }, - { - AccountId32: { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: UNIQUE_CHAIN}, + {AccountId32: { network: 'Any', id: alice.addressRaw, - }, - }, - ], + }}, + ], + }, }, }; @@ -168,7 +167,7 @@ ForeignAsset: 0, }; - const amount = 5000000000; + const amount = TRANSFER_AMOUNT; const destWeight = 50000000; const tx = api.tx.xTokens.transfer(id, amount, destination, destWeight);