From 281e8172516b4c2a398b3a1bb4159aeac273ec63 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 22 Jul 2022 13:47:14 +0000 Subject: [PATCH] Merge pull request #439 from UniqueNetwork/doc/rmrk --- --- a/pallets/proxy-rmrk-core/src/benchmarking.rs +++ b/pallets/proxy-rmrk-core/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 sp_std::vec; use frame_benchmarking::{benchmarks, account}; --- a/pallets/proxy-rmrk-core/src/lib.rs +++ b/pallets/proxy-rmrk-core/src/lib.rs @@ -14,6 +14,135 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! # RMRK Core Proxy Pallet +//! +//! A pallet used as proxy for RMRK Core (). +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Pallet`] +//! +//! ## Overview +//! +//! The RMRK Core Proxy pallet mirrors the functionality of RMRK Core, +//! binding its externalities to Unique's own underlying structure. +//! It is purposed to mimic RMRK Core exactly, allowing seamless integrations +//! of solutions based on RMRK. +//! +//! RMRK Core itself contains essential functionality for RMRK's nested and +//! multi-resourced NFTs. +//! +//! *Note*, that while RMRK itself is subject to active development and restructuring, +//! the proxy may be caught temporarily out of date. +//! +//! ### What is RMRK? +//! +//! RMRK is a set of NFT standards which compose several "NFT 2.0 lego" primitives. +//! Putting these legos together allows a user to create NFT systems of arbitrary complexity. +//! +//! Meaning, RMRK NFTs are dynamic, able to nest into each other and form a hierarchy, +//! make use of specific changeable and partially shared metadata in the form of resources, +//! and more. +//! +//! Visit RMRK documentation and repositories to learn more: +//! - Docs: +//! - FAQ: +//! - Substrate code repository: +//! - RMRK specification repository: +//! +//! ## Terminology +//! +//! For more information on RMRK, see RMRK's own documentation. +//! +//! ### Intro to RMRK +//! +//! - **Resource:** Additional piece of metadata of an NFT usually serving to add +//! a piece of media on top of the root metadata (NFT's own), be it a different wing +//! on the root template bird or something entirely unrelated. +//! +//! - **Base:** A list of possible "components" - Parts, a combination of which can +//! be appended/equipped to/on an NFT. +//! +//! - **Part:** Something that, together with other Parts, can constitute an NFT. +//! Parts are defined in the Base to which they belong. Parts can be either +//! of the `slot` type or `fixed` type. Slots are intended for equippables. +//! Note that "part of something" and "Part of a Base" can be easily confused, +//! and so in this documentation these words are distinguished by the capital letter. +//! +//! - **Theme:** Named objects of variable => value pairs which get interpolated into +//! the Base's `themable` Parts. Themes can hold any value, but are often represented +//! in RMRK's examples as colors applied to visible Parts. +//! +//! ### Peculiarities in Unique +//! +//! - **Scoped properties:** Properties that are normally obscured from users. +//! Their purpose is to contain structured metadata that was not included in the Unique standard +//! for collections and tokens, meant to be operated on by proxies and other outliers. +//! Scoped property keys are prefixed with `some-scope:`, where `some-scope` is +//! an arbitrary keyword, like "rmrk". `:` is considered an unacceptable symbol in user-defined +//! properties, which, along with other safeguards, makes scoped ones impossible to tamper with. +//! +//! - **Auxiliary properties:** A slightly different structure of properties, +//! trading universality of use for more convenient storage, writes and access. +//! Meant to be inaccessible to end users. +//! +//! ## Proxy Implementation +//! +//! An external user is supposed to be able to utilize this proxy as they would +//! utilize RMRK, and get exactly the same results. Normally, Unique transactions +//! are off-limits to RMRK collections and tokens, and vice versa. However, +//! the information stored on chain can be freely interpreted by storage reads and Unique RPCs. +//! +//! ### ID Mapping +//! +//! RMRK's collections' IDs are counted independently of Unique's and start at 0. +//! Note that tokens' IDs still start at 1. +//! The collections themselves, as well as tokens, are stored as Unique collections, +//! and thus RMRK IDs are mapped to Unique IDs (but not vice versa). +//! +//! ### External/Internal Collection Insulation +//! +//! A Unique transaction cannot target collections purposed for RMRK, +//! and they are flagged as `external` to specify that. On the other hand, +//! due to the mapping, RMRK transactions and RPCs simply cannot reach Unique collections. +//! +//! ### Native Properties +//! +//! Many of RMRK's native parameters are stored as scoped properties of a collection +//! or an NFT on the chain. Scoped properties are prefixed with `rmrk:`, where `:` +//! is an unacceptable symbol in user-defined properties, which, along with other safeguards, +//! makes them impossible to tamper with. +//! +//! ### Collection and NFT Types, or Base, Parts and Themes Handling +//! +//! RMRK introduces the concept of a Base, which is a catalogue of Parts, +//! possible components of an NFT. Due to its similarity with the functionality +//! of a token collection, a Base is stored and handled as one, and the Base's Parts and Themes +//! are this collection's NFTs. See [`CollectionType`] and [`NftType`]. +//! +//! ## Interface +//! +//! ### Dispatchables +//! +//! - `create_collection` - Create a new collection of NFTs. +//! - `destroy_collection` - Destroy a collection. +//! - `change_collection_issuer` - Change the issuer of a collection. +//! Analogous to Unique's collection's [`owner`](up_data_structs::Collection). +//! - `lock_collection` - "Lock" the collection and prevent new token creation. **Cannot be undone.** +//! - `mint_nft` - Mint an NFT in a specified collection. +//! - `burn_nft` - Burn an NFT, destroying it and its nested tokens. +//! - `send` - Transfer an NFT from an account/NFT A to another account/NFT B. +//! - `accept_nft` - Accept an NFT sent from another account to self or an owned NFT. +//! - `reject_nft` - Reject an NFT sent from another account to self or owned NFT and **burn it**. +//! - `accept_resource` - Accept the addition of a newly created pending resource to an existing NFT. +//! - `accept_resource_removal` - Accept the removal of a removal-pending resource from an NFT. +//! - `set_property` - Add or edit a custom user property of a token or a collection. +//! - `set_priority` - Set a different order of resource priorities for an NFT. +//! - `add_basic_resource` - Create and set/propose a basic resource for an NFT. +//! - `add_composable_resource` - Create and set/propose a composable resource for an NFT. +//! - `add_slot_resource` - Create and set/propose a slot resource for an NFT. +//! - `remove_resource` - Remove and erase a resource from an NFT. + #![cfg_attr(not(feature = "std"), no_std)] use frame_support::{pallet_prelude::*, transactional, BoundedVec, dispatch::DispatchResult}; @@ -49,6 +178,7 @@ use RmrkProperty::*; +/// Maximum number of levels of depth in the token nesting tree. pub const NESTING_BUDGET: u32 = 5; type PendingTarget = (CollectionId, TokenId); @@ -66,14 +196,19 @@ pub trait Config: frame_system::Config + pallet_common::Config + pallet_nonfungible::Config + account::Config { + /// Overarching event type. type Event: From> + IsType<::Event>; + + /// The weight information of this pallet. type WeightInfo: WeightInfo; } + /// Latest yet-unused collection ID. #[pallet::storage] #[pallet::getter(fn collection_index)] pub type CollectionIndex = StorageValue<_, RmrkCollectionId, ValueQuery>; + /// Mapping from RMRK collection ID to Unique's. #[pallet::storage] pub type UniqueCollectionId = StorageMap<_, Twox64Concat, RmrkCollectionId, CollectionId, ValueQuery>; @@ -159,34 +294,66 @@ #[pallet::error] pub enum Error { - /* Unique-specific events */ + /* Unique proxy-specific events */ + /// Property of the type of RMRK collection could not be read successfully. CorruptedCollectionType, - NftTypeEncodeError, + // NftTypeEncodeError, + /// Too many symbols supplied as the property key. The maximum is [256](up_data_structs::MAX_PROPERTY_KEY_LENGTH). RmrkPropertyKeyIsTooLong, + /// Too many bytes supplied as the property value. The maximum is [32768](up_data_structs::MAX_PROPERTY_VALUE_LENGTH). RmrkPropertyValueIsTooLong, + /// Could not find a property by the supplied key. RmrkPropertyIsNotFound, + /// Something went wrong when decoding encoded data from the storage. + /// Perhaps, there was a wrong key supplied for the type, or the data was improperly stored. UnableToDecodeRmrkData, /* RMRK compatible events */ + /// Only destroying collections without tokens is allowed. CollectionNotEmpty, + /// Could not find an ID for a collection. It is likely there were too many collections created on the chain, causing an overflow. NoAvailableCollectionId, + /// Token does not exist, or there is no suitable ID for it, likely too many tokens were created in a collection, causing an overflow. NoAvailableNftId, + /// Collection does not exist, has a wrong type, or does not map to a Unique ID. CollectionUnknown, + /// No permission to perform action. NoPermission, + /// Token is marked as non-transferable, and thus cannot be transferred. NonTransferable, + /// Too many tokens created in the collection, no new ones are allowed. CollectionFullOrLocked, + /// No such resource found. ResourceDoesntExist, + /// If an NFT is sent to a descendant, that would form a nesting loop, an ouroboros. + /// Sending to self is redundant. CannotSendToDescendentOrSelf, + /// Not the target owner of the sent NFT. CannotAcceptNonOwnedNft, + /// Not the target owner of the sent NFT. CannotRejectNonOwnedNft, + /// NFT was not sent and is not pending. CannotRejectNonPendingNft, + /// Resource is not pending for the operation. ResourceNotPending, + /// Could not find an ID for the resource. It is likely there were too many resources created on an NFT, causing an overflow. NoAvailableResourceId, } #[pallet::call] impl Pallet { - /// Create a collection + // todo :refactor replace every collection_id with rmrk_collection_id (and nft_id) in arguments for uniformity? + + /// Create a new collection of NFTs. + /// + /// # Permissions: + /// * Anyone - will be assigned as the issuer of the collection. + /// + /// # Arguments: + /// - `metadata`: Metadata describing the collection, e.g. IPFS hash. Cannot be changed. + /// - `max`: Optional maximum number of tokens. + /// - `symbol`: UTF-8 string with token prefix, by which to represent the token in wallets and UIs. + /// Analogous to Unique's [`token_prefix`](up_data_structs::Collection). Cannot be changed. #[transactional] #[pallet::weight(>::create_collection())] pub fn create_collection( @@ -226,8 +393,8 @@ T::CrossAccountId::from_sub(sender.clone()), data, [ - Self::rmrk_property(Metadata, &metadata)?, - Self::rmrk_property(CollectionType, &misc::CollectionType::Regular)?, + Self::encode_rmrk_property(Metadata, &metadata)?, + Self::encode_rmrk_property(CollectionType, &misc::CollectionType::Regular)?, ] .into_iter(), )?; @@ -237,8 +404,8 @@ >::set_scoped_collection_property( unique_collection_id, - PropertyScope::Rmrk, - Self::rmrk_property(RmrkInternalCollectionId, &rmrk_collection_id)?, + RMRK_SCOPE, + Self::encode_rmrk_property(RmrkInternalCollectionId, &rmrk_collection_id)?, )?; >::mutate(|n| *n += 1); @@ -251,7 +418,15 @@ Ok(()) } - /// destroy collection + /// Destroy a collection. + /// + /// Only empty collections can be destroyed. If it has any tokens, they must be burned first. + /// + /// # Permissions: + /// * Collection issuer + /// + /// # Arguments: + /// - `collection_id`: RMRK ID of the collection to destroy. #[transactional] #[pallet::weight(>::destroy_collection())] pub fn destroy_collection( @@ -278,12 +453,14 @@ Ok(()) } - /// Change the issuer of a collection + /// Change the issuer of a collection. Analogous to Unique's collection's [`owner`](up_data_structs::Collection). + /// + /// # Permissions: + /// * Collection issuer /// - /// Parameters: - /// - `origin`: sender of the transaction - /// - `collection_id`: collection id of the nft to change issuer of - /// - `new_issuer`: Collection's new issuer + /// # Arguments: + /// - `collection_id`: RMRK collection ID to change the issuer of. + /// - `new_issuer`: Collection's new issuer. #[transactional] #[pallet::weight(>::change_collection_issuer())] pub fn change_collection_issuer( @@ -314,7 +491,13 @@ Ok(()) } - /// lock collection + /// "Lock" the collection and prevent new token creation. Cannot be undone. + /// + /// # Permissions: + /// * Collection issuer + /// + /// # Arguments: + /// - `collection_id`: RMRK ID of the collection to lock. #[transactional] #[pallet::weight(>::lock_collection())] pub fn lock_collection( @@ -346,16 +529,19 @@ Ok(()) } - /// Mints an NFT in the specified collection - /// Sets metadata and the royalty attribute + /// Mint an NFT in a specified collection. /// - /// Parameters: - /// - `collection_id`: The class of the asset to be minted. - /// - `nft_id`: The nft value of the asset to be minted. - /// - `recipient`: Receiver of the royalty - /// - `royalty`: Permillage reward from each trade for the Recipient - /// - `metadata`: Arbitrary data about an nft, e.g. IPFS hash - /// - `transferable`: Ability to transfer this NFT + /// # Permissions: + /// * Collection issuer + /// + /// # Arguments: + /// - `owner`: Owner account of the NFT. If set to None, defaults to the sender (collection issuer). + /// - `collection_id`: RMRK collection ID for the NFT to be minted within. Cannot be changed. + /// - `recipient`: Receiver account of the royalty. Has no effect if the `royalty_amount` is not set. Cannot be changed. + /// - `royalty_amount`: Optional permillage reward from each trade for the `recipient`. Cannot be changed. + /// - `metadata`: Arbitrary data about an NFT, e.g. IPFS hash. Cannot be changed. + /// - `transferable`: Can this NFT be transferred? Cannot be changed. + /// - `resources`: Resource data to be added to the NFT immediately after minting. #[transactional] #[pallet::weight(>::mint_nft(resources.as_ref().map(|r| r.len() as u32).unwrap_or(0)))] pub fn mint_nft( @@ -390,16 +576,16 @@ &cross_owner, &collection, [ - Self::rmrk_property(TokenType, &NftType::Regular)?, - Self::rmrk_property(Transferable, &transferable)?, - Self::rmrk_property(PendingNftAccept, &None::)?, - Self::rmrk_property(RoyaltyInfo, &royalty_info)?, - Self::rmrk_property(Metadata, &metadata)?, - Self::rmrk_property(Equipped, &false)?, - Self::rmrk_property(ResourcePriorities, &>::new())?, - Self::rmrk_property(NextResourceId, &(0 as RmrkResourceId))?, - Self::rmrk_property(PendingChildren, &PendingChildrenSet::new())?, - Self::rmrk_property(AssociatedBases, &BasesMap::new())?, + Self::encode_rmrk_property(TokenType, &NftType::Regular)?, + Self::encode_rmrk_property(Transferable, &transferable)?, + Self::encode_rmrk_property(PendingNftAccept, &None::)?, + Self::encode_rmrk_property(RoyaltyInfo, &royalty_info)?, + Self::encode_rmrk_property(Metadata, &metadata)?, + Self::encode_rmrk_property(Equipped, &false)?, + Self::encode_rmrk_property(ResourcePriorities, &>::new())?, + Self::encode_rmrk_property(NextResourceId, &(0 as RmrkResourceId))?, + Self::encode_rmrk_property(PendingChildren, &PendingChildrenSet::new())?, + Self::encode_rmrk_property(AssociatedBases, &BasesMap::new())?, ] .into_iter(), ) @@ -423,7 +609,22 @@ Ok(()) } - /// burn nft + /// Burn an NFT, destroying it and its nested tokens up to the specified limit. + /// If the burning budget is exceeded, the transaction is reverted. + /// + /// This is the way to burn a nested token as well. + /// + /// For more information, see [`burn_recursively`](pallet_nonfungible::pallet::Pallet::burn_recursively). + /// + /// # Permissions: + /// * Token owner + /// + /// # Arguments: + /// - `collection_id`: RMRK ID of the collection in which the NFT to burn belongs to. + /// - `nft_id`: ID of the NFT to be destroyed. + /// - `max_burns`: Maximum number of tokens to burn, assuming nesting. The transaction + /// is reverted if there are more tokens to burn in the nesting tree than this number. + /// This is primarily a mechanism of transaction weight control. #[transactional] #[pallet::weight(>::burn_nft(*max_burns))] pub fn burn_nft( @@ -458,13 +659,19 @@ Ok(()) } - /// Transfers a NFT from an Account or NFT A to another Account or NFT B + /// Transfer an NFT from an account/NFT A to another account/NFT B. + /// The token must be transferable. Nesting cannot occur deeper than the [`NESTING_BUDGET`]. /// - /// Parameters: - /// - `origin`: sender of the transaction - /// - `rmrk_collection_id`: collection id of the nft to be transferred - /// - `rmrk_nft_id`: nft id of the nft to be transferred - /// - `new_owner`: new owner of the nft which can be either an account or a NFT + /// If the target owner is an NFT owned by another account, then the NFT will enter + /// the pending state and will have to be accepted by the other account. + /// + /// # Permissions: + /// - Token owner + /// + /// # Arguments: + /// - `collection_id`: RMRK ID of the collection of the NFT to be transferred. + /// - `nft_id`: ID of the NFT to be transferred. + /// - `new_owner`: New owner of the nft which can be either an account or a NFT. #[transactional] #[pallet::weight(>::send())] pub fn send( @@ -535,8 +742,8 @@ >::set_scoped_token_property( collection.id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property::>( + RMRK_SCOPE, + Self::encode_rmrk_property::>( PendingNftAccept, &Some((target_collection_id, target_nft_id.into())), )?, @@ -578,14 +785,18 @@ Ok(()) } - /// Accepts an NFT sent from another account to self or owned NFT + /// Accept an NFT sent from another account to self or an owned NFT. + /// + /// The NFT in question must be pending, and, thus, be [sent](`Pallet::send`) first. /// - /// Parameters: - /// - `origin`: sender of the transaction - /// - `rmrk_collection_id`: collection id of the nft to be accepted - /// - `rmrk_nft_id`: nft id of the nft to be accepted - /// - `new_owner`: either origin's account ID or origin-owned NFT, whichever the NFT was - /// sent to + /// # Permissions: + /// - Token-owner-to-be + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID of the NFT to be accepted. + /// - `rmrk_nft_id`: ID of the NFT to be accepted. + /// - `new_owner`: Either the sender's account ID or a sender-owned NFT, + /// whichever the accepted NFT was sent to. #[transactional] #[pallet::weight(>::accept_nft())] pub fn accept_nft( @@ -650,8 +861,8 @@ >::set_scoped_token_property( collection.id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property(PendingNftAccept, &None::)?, + RMRK_SCOPE, + Self::encode_rmrk_property(PendingNftAccept, &None::)?, )?; } @@ -665,12 +876,17 @@ Ok(()) } - /// Rejects an NFT sent from another account to self or owned NFT + /// Reject an NFT sent from another account to self or owned NFT. + /// The NFT in question will not be sent back and burnt instead. + /// + /// The NFT in question must be pending, and, thus, be [sent](`Pallet::send`) first. + /// + /// # Permissions: + /// - Token-owner-to-be-not /// - /// Parameters: - /// - `origin`: sender of the transaction - /// - `rmrk_collection_id`: collection id of the nft to be accepted - /// - `rmrk_nft_id`: nft id of the nft to be accepted + /// # Arguments: + /// - `rmrk_collection_id`: RMRK ID of the NFT to be rejected. + /// - `rmrk_nft_id`: ID of the NFT to be rejected. #[transactional] #[pallet::weight(>::reject_nft())] pub fn reject_nft( @@ -724,7 +940,19 @@ Ok(()) } - /// accept the addition of a new resource to an existing NFT + /// Accept the addition of a newly created pending resource to an existing NFT. + /// + /// This transaction is needed when a resource is created and assigned to an NFT + /// by a non-owner, i.e. the collection issuer, with one of the + /// [`add_...` transactions](Pallet::add_basic_resource). + /// + /// # Permissions: + /// - Token owner + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID of the NFT. + /// - `rmrk_nft_id`: ID of the NFT with a pending resource to be accepted. + /// - `resource_id`: ID of the newly created pending resource. #[transactional] #[pallet::weight(>::accept_resource())] pub fn accept_resource( @@ -767,7 +995,18 @@ Ok(()) } - /// accept the removal of a resource of an existing NFT + /// Accept the removal of a removal-pending resource from an NFT. + /// + /// This transaction is needed when a non-owner, i.e. the collection issuer, + /// requests a [removal](`Pallet::remove_resource`) of a resource from an NFT. + /// + /// # Permissions: + /// - Token owner + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID of the NFT. + /// - `rmrk_nft_id`: ID of the NFT with a resource to be removed. + /// - `resource_id`: ID of the removal-pending resource. #[transactional] #[pallet::weight(>::accept_resource_removal())] pub fn accept_resource_removal( @@ -795,17 +1034,17 @@ ensure!(cross_sender == nft_owner, >::NoPermission); - let resource_id_key = Self::rmrk_property_key(ResourceId(resource_id))?; + let resource_id_key = Self::get_scoped_property_key(ResourceId(resource_id))?; let resource_info = >::token_aux_property(( collection_id, nft_id, - PropertyScope::Rmrk, + RMRK_SCOPE, resource_id_key.clone(), )) .ok_or(>::ResourceDoesntExist)?; - let resource_info: RmrkResourceInfo = Self::decode_property(&resource_info)?; + let resource_info: RmrkResourceInfo = Self::decode_property_value(&resource_info)?; ensure!( resource_info.pending_removal, @@ -815,7 +1054,7 @@ >::remove_token_aux_property( collection_id, nft_id, - PropertyScope::Rmrk, + RMRK_SCOPE, resource_id_key, ); @@ -833,7 +1072,22 @@ Ok(()) } - /// set a custom value on an NFT + /// Add or edit a custom user property, a key-value pair, describing the metadata + /// of a token or a collection, on either one of these. + /// + /// Note that in this proxy implementation many details regarding RMRK are stored + /// as scoped properties prefixed with "rmrk:", normally inaccessible + /// to external transactions and RPCs. + /// + /// # Permissions: + /// - Collection issuer - in case of collection property + /// - Token owner - in case of NFT property + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID. + /// - `maybe_nft_id`: Optional ID of the NFT. If left empty, then the property is set for the collection. + /// - `key`: Key of the custom property to be referenced by. + /// - `value`: Value of the custom property to be stored. #[transactional] #[pallet::weight(>::set_property())] pub fn set_property( @@ -863,8 +1117,8 @@ >::set_scoped_token_property( collection_id, token_id, - PropertyScope::Rmrk, - Self::rmrk_property(UserProperty(key.as_slice()), &value)?, + RMRK_SCOPE, + Self::encode_rmrk_property(UserProperty(key.as_slice()), &value)?, )?; } None => { @@ -877,8 +1131,8 @@ >::set_scoped_collection_property( collection_id, - PropertyScope::Rmrk, - Self::rmrk_property(UserProperty(key.as_slice()), &value)?, + RMRK_SCOPE, + Self::encode_rmrk_property(UserProperty(key.as_slice()), &value)?, )?; } } @@ -893,7 +1147,20 @@ Ok(()) } - /// set a different order of resource priority + /// Set a different order of resource priorities for an NFT. Priorities can be used, + /// for example, for order of rendering. + /// + /// Note that the priorities are not updated automatically, and are an empty vector + /// by default. There is no pre-set definition for the order to be particular, + /// it can be interpreted arbitrarily use-case by use-case. + /// + /// # Permissions: + /// - Token owner + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID of the NFT. + /// - `rmrk_nft_id`: ID of the NFT to rearrange resource priorities for. + /// - `priorities`: Ordered vector of resource IDs. #[transactional] #[pallet::weight(>::set_priority())] pub fn set_priority( @@ -920,8 +1187,8 @@ >::set_scoped_token_property( collection_id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property(ResourcePriorities, &priorities.into_inner())?, + RMRK_SCOPE, + Self::encode_rmrk_property(ResourcePriorities, &priorities.into_inner())?, )?; Self::deposit_event(Event::::PrioritySet { @@ -932,7 +1199,19 @@ Ok(()) } - /// Create basic resource + /// Create and set/propose a basic resource for an NFT. + /// + /// A basic resource is the simplest, lacking a Base and anything that comes with it. + /// See RMRK docs for more information and examples. + /// + /// # Permissions: + /// - Collection issuer - if not the token owner, adding the resource will warrant + /// the owner's [acceptance](Pallet::accept_resource). + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID of the NFT. + /// - `nft_id`: ID of the NFT to assign a resource to. + /// - `resource`: Data of the resource to be created. #[transactional] #[pallet::weight(>::add_basic_resource())] pub fn add_basic_resource( @@ -962,7 +1241,19 @@ Ok(()) } - /// Create composable resource + /// Create and set/propose a composable resource for an NFT. + /// + /// A composable resource links to a Base and has a subset of its Parts it is composed of. + /// See RMRK docs for more information and examples. + /// + /// # Permissions: + /// - Collection issuer - if not the token owner, adding the resource will warrant + /// the owner's [acceptance](Pallet::accept_resource). + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID of the NFT. + /// - `nft_id`: ID of the NFT to assign a resource to. + /// - `resource`: Data of the resource to be created. #[transactional] #[pallet::weight(>::add_composable_resource())] pub fn add_composable_resource( @@ -990,17 +1281,17 @@ >::try_mutate_token_aux_property( collection_id, nft_id.into(), - PropertyScope::Rmrk, - Self::rmrk_property_key(AssociatedBases)?, + RMRK_SCOPE, + Self::get_scoped_property_key(AssociatedBases)?, |value| -> DispatchResult { let mut bases: BasesMap = match value { - Some(value) => Self::decode_property(value)?, + Some(value) => Self::decode_property_value(value)?, None => BasesMap::new(), }; *bases.entry(base_id).or_insert(0) += 1; - *value = Some(Self::encode_property(&bases)?); + *value = Some(Self::encode_property_value(&bases)?); Ok(()) }, )?; @@ -1012,7 +1303,19 @@ Ok(()) } - /// Create slot resource + /// Create and set/propose a slot resource for an NFT. + /// + /// A slot resource links to a Base and a slot ID in it which it can fit into. + /// See RMRK docs for more information and examples. + /// + /// # Permissions: + /// - Collection issuer - if not the token owner, adding the resource will warrant + /// the owner's [acceptance](Pallet::accept_resource). + /// + /// # Arguments: + /// - `rmrk_collection_id`: RMRK collection ID of the NFT. + /// - `nft_id`: ID of the NFT to assign a resource to. + /// - `resource`: Data of the resource to be created. #[transactional] #[pallet::weight(>::add_slot_resource())] pub fn add_slot_resource( @@ -1042,7 +1345,18 @@ Ok(()) } - /// remove resource + /// Remove and erase a resource from an NFT. + /// + /// If the sender does not own the NFT, then it will be pending confirmation, + /// and will have to be [accepted](Pallet::accept_resource_removal) by the token owner. + /// + /// # Permissions + /// - Collection issuer + /// + /// # Arguments + /// - `collection_id`: RMRK ID of a collection to which the NFT making use of the resource belongs to. + /// - `nft_id`: ID of the NFT with a resource to be removed. + /// - `resource_id`: ID of the resource to be removed. #[transactional] #[pallet::weight(>::remove_resource())] pub fn remove_resource( @@ -1070,31 +1384,34 @@ } impl Pallet { - pub fn rmrk_property_key(rmrk_key: RmrkProperty) -> Result { + /// Transform one of possible RMRK keys into a byte key with a RMRK scope. + pub fn get_scoped_property_key(rmrk_key: RmrkProperty) -> Result { let key = rmrk_key.to_key::()?; - let scoped_key = PropertyScope::Rmrk + let scoped_key = RMRK_SCOPE .apply(key) .map_err(|_| >::RmrkPropertyKeyIsTooLong)?; Ok(scoped_key) } - // todo think about renaming these - pub fn rmrk_property( + /// Form a Unique property, transforming a RMRK key into bytes (without assigning the scope yet) + /// and encoding the value from an arbitrary type into bytes. + pub fn encode_rmrk_property( rmrk_key: RmrkProperty, value: &E, ) -> Result { let key = rmrk_key.to_key::()?; - let value = Self::encode_property(value)?; + let value = Self::encode_property_value(value)?; let property = Property { key, value }; Ok(property) } - pub fn encode_property>( + /// Encode property value from an arbitrary type into bytes for storage. + pub fn encode_property_value>( value: &E, ) -> Result, DispatchError> { let value = value @@ -1105,13 +1422,15 @@ Ok(value) } - pub fn decode_property>( + /// Decode property value from bytes into an arbitrary type. + pub fn decode_property_value>( vec: &BoundedBytes, ) -> Result { vec.decode() .map_err(|_| >::UnableToDecodeRmrkData.into()) } + /// Change the limit of a property value byte vector. pub fn rebind(vec: &BoundedVec) -> Result, DispatchError> where BoundedVec: TryFrom>, @@ -1120,6 +1439,9 @@ .map_err(|_| >::RmrkPropertyValueIsTooLong.into()) } + /// Initialize a new NFT collection with certain RMRK-scoped properties. + /// + /// See [`init_collection`](pallet_nonfungible::pallet::Pallet::init_collection) for more details. fn init_collection( sender: T::CrossAccountId, data: CreateCollectionData, @@ -1133,13 +1455,16 @@ >::set_scoped_collection_properties( collection_id?, - PropertyScope::Rmrk, + RMRK_SCOPE, properties, )?; collection_id } + /// Mint a new NFT with certain RMRK-scoped properties. Sender must be the collection owner. + /// + /// See [`create_item`](pallet_nonfungible::pallet::Pallet::create_item) for more details. pub fn create_nft( sender: &T::CrossAccountId, owner: &T::CrossAccountId, @@ -1157,16 +1482,14 @@ let nft_id = >::current_token_id(collection.id); - >::set_scoped_token_properties( - collection.id, - nft_id, - PropertyScope::Rmrk, - properties, - )?; + >::set_scoped_token_properties(collection.id, nft_id, RMRK_SCOPE, properties)?; Ok(nft_id) } + /// Burn an NFT, along with its nested children, limited by `max_burns`. The sender must be the token owner. + /// + /// See [`burn_recursively`](pallet_nonfungible::pallet::Pallet::burn_recursively) for more details. fn destroy_nft( sender: T::CrossAccountId, collection_id: CollectionId, @@ -1207,48 +1530,54 @@ ) } + /// Add a sent token pending acceptance to the target owning token as a property. fn insert_pending_child( target: (CollectionId, TokenId), child: (RmrkCollectionId, RmrkNftId), ) -> DispatchResult { - Self::mutate_pending_child(target, |pending_children| { + Self::mutate_pending_children(target, |pending_children| { pending_children.insert(child); }) } + /// Remove a sent token pending acceptance from the target token's properties. fn remove_pending_child( target: (CollectionId, TokenId), child: (RmrkCollectionId, RmrkNftId), ) -> DispatchResult { - Self::mutate_pending_child(target, |pending_children| { + Self::mutate_pending_children(target, |pending_children| { pending_children.remove(&child); }) } - fn mutate_pending_child( + /// Apply a mutation to the property of a token containing sent tokens + /// that are currently pending acceptance. + fn mutate_pending_children( (target_collection_id, target_nft_id): (CollectionId, TokenId), f: impl FnOnce(&mut PendingChildrenSet), ) -> DispatchResult { >::try_mutate_token_aux_property( target_collection_id, target_nft_id, - PropertyScope::Rmrk, - Self::rmrk_property_key(PendingChildren)?, + RMRK_SCOPE, + Self::get_scoped_property_key(PendingChildren)?, |pending_children| -> DispatchResult { let mut map = match pending_children { - Some(map) => Self::decode_property(map)?, + Some(map) => Self::decode_property_value(map)?, None => PendingChildrenSet::new(), }; f(&mut map); - *pending_children = Some(Self::encode_property(&map)?); + *pending_children = Some(Self::encode_property_value(&map)?); Ok(()) }, ) } + /// Get an iterator from a token's property containing tokens sent to it + /// that are currently pending acceptance. fn iterate_pending_children( collection_id: CollectionId, nft_id: TokenId, @@ -1256,18 +1585,22 @@ let property = >::token_aux_property(( collection_id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property_key(PendingChildren)?, + RMRK_SCOPE, + Self::get_scoped_property_key(PendingChildren)?, )); let pending_children = match property { - Some(map) => Self::decode_property(&map)?, + Some(map) => Self::decode_property_value(&map)?, None => PendingChildrenSet::new(), }; Ok(pending_children.into_iter()) } + /// Get incremented resource ID from within an NFT's properties and store the new latest ID. + /// Thus, the returned resource ID should be used. + /// + /// Resource IDs are unique only across an NFT. fn acquire_next_resource_id( collection_id: CollectionId, nft_id: TokenId, @@ -1282,13 +1615,15 @@ >::set_scoped_token_property( collection_id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property(NextResourceId, &next_id)?, + RMRK_SCOPE, + Self::encode_rmrk_property(NextResourceId, &next_id)?, )?; Ok(resource_id) } + /// Create and add a resource for a regular NFT, mark it as pending if the sender + /// is not the token owner. The sender must be the collection owner. fn resource_add( sender: T::AccountId, collection_id: CollectionId, @@ -1319,10 +1654,10 @@ >::try_mutate_token_aux_property( collection_id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property_key(ResourceId(id))?, + RMRK_SCOPE, + Self::get_scoped_property_key(ResourceId(id))?, |value| -> DispatchResult { - *value = Some(Self::encode_property(&resource_info)?); + *value = Some(Self::encode_property_value(&resource_info)?); Ok(()) }, @@ -1331,6 +1666,8 @@ Ok(id) } + /// Designate a resource for erasure from an NFT, and remove it if the sender is the token owner. + /// The sender must be the collection owner. fn resource_remove( sender: T::AccountId, collection_id: CollectionId, @@ -1341,18 +1678,17 @@ Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?; ensure!(collection.owner == sender, Error::::NoPermission); - let resource_id_key = Self::rmrk_property_key(ResourceId(resource_id))?; - let scope = PropertyScope::Rmrk; + let resource_id_key = Self::get_scoped_property_key(ResourceId(resource_id))?; let resource = >::token_aux_property(( collection_id, nft_id, - scope, + RMRK_SCOPE, resource_id_key.clone(), )) .ok_or(>::ResourceDoesntExist)?; - let resource_info: RmrkResourceInfo = Self::decode_property(&resource)?; + let resource_info: RmrkResourceInfo = Self::decode_property_value(&resource)?; let budget = up_data_structs::budget::Value::new(NESTING_BUDGET); let topmost_owner = @@ -1363,8 +1699,8 @@ >::remove_token_aux_property( collection_id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property_key(ResourceId(resource_id))?, + RMRK_SCOPE, + Self::get_scoped_property_key(ResourceId(resource_id))?, ); if let RmrkResourceTypes::Composable(resource) = resource_info.resource { @@ -1383,6 +1719,8 @@ Ok(()) } + /// Remove a Base ID from an NFT if they are associated. + /// The Base itself is deleted if the number of associated NFTs reaches 0. fn remove_associated_base_id( collection_id: CollectionId, nft_id: TokenId, @@ -1391,11 +1729,11 @@ >::try_mutate_token_aux_property( collection_id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property_key(AssociatedBases)?, + RMRK_SCOPE, + Self::get_scoped_property_key(AssociatedBases)?, |value| -> DispatchResult { let mut bases: BasesMap = match value { - Some(value) => Self::decode_property(value)?, + Some(value) => Self::decode_property_value(value)?, None => BasesMap::new(), }; @@ -1407,12 +1745,13 @@ } } - *value = Some(Self::encode_property(&bases)?); + *value = Some(Self::encode_property_value(&bases)?); Ok(()) }, ) } + /// Apply a mutation to a resource stored in the token properties of an NFT. fn try_mutate_resource_info( collection_id: CollectionId, nft_id: TokenId, @@ -1422,15 +1761,15 @@ >::try_mutate_token_aux_property( collection_id, nft_id, - PropertyScope::Rmrk, - Self::rmrk_property_key(ResourceId(resource_id))?, + RMRK_SCOPE, + Self::get_scoped_property_key(ResourceId(resource_id))?, |value| match value { Some(value) => { - let mut resource_info: RmrkResourceInfo = Self::decode_property(value)?; + let mut resource_info: RmrkResourceInfo = Self::decode_property_value(value)?; f(&mut resource_info)?; - *value = Self::encode_property(&resource_info)?; + *value = Self::encode_property_value(&resource_info)?; Ok(()) } @@ -1439,6 +1778,7 @@ ) } + /// Change the owner of an NFT collection, ensuring that the sender is the current owner. fn change_collection_owner( collection_id: CollectionId, collection_type: misc::CollectionType, @@ -1454,6 +1794,7 @@ collection.save() } + /// Ensure that an account is the collection owner/issuer, return an error if not. pub fn check_collection_owner( collection: &NonfungibleHandle, account: &T::CrossAccountId, @@ -1463,10 +1804,12 @@ .map_err(Self::map_unique_err_to_proxy) } + /// Get the latest yet-unused RMRK collection index from the storage. pub fn last_collection_idx() -> RmrkCollectionId { >::get() } + /// Get a mapping from a RMRK collection ID to its corresponding Unique collection ID. pub fn unique_collection_id( rmrk_collection_id: RmrkCollectionId, ) -> Result { @@ -1474,12 +1817,14 @@ .map_err(|_| >::CollectionUnknown.into()) } + /// Get a mapping from a Unique collection ID to its RMRK collection ID counterpart, if it exists. pub fn rmrk_collection_id( unique_collection_id: CollectionId, ) -> Result { Self::get_collection_property_decoded(unique_collection_id, RmrkInternalCollectionId) } + /// Fetch a Unique NFT collection. pub fn get_nft_collection( collection_id: CollectionId, ) -> Result, DispatchError> { @@ -1492,29 +1837,35 @@ } } + /// Check if an NFT collection with such an ID exists. pub fn collection_exists(collection_id: CollectionId) -> bool { >::try_get(collection_id).is_ok() } + /// Fetch and decode a RMRK-scoped collection property value in bytes. pub fn get_collection_property( collection_id: CollectionId, key: RmrkProperty, ) -> Result { let collection_property = >::collection_properties(collection_id) - .get(&Self::rmrk_property_key(key)?) + .get(&Self::get_scoped_property_key(key)?) .ok_or(>::CollectionUnknown)? .clone(); Ok(collection_property) } + /// Fetch a RMRK-scoped collection property and decode it from bytes into an appropriate type. pub fn get_collection_property_decoded( collection_id: CollectionId, key: RmrkProperty, ) -> Result { - Self::decode_property(&Self::get_collection_property(collection_id, key)?) + Self::decode_property_value(&Self::get_collection_property(collection_id, key)?) } + /// Get the type of a collection stored as a scoped property. + /// + /// RMRK Core proxy differentiates between regular collections as well as RMRK Bases as collections. pub fn get_collection_type( collection_id: CollectionId, ) -> Result { @@ -1527,6 +1878,8 @@ }) } + /// Ensure that the type of the collection equals the provided type, + /// otherwise return an error. pub fn ensure_collection_type( collection_id: CollectionId, collection_type: misc::CollectionType, @@ -1540,6 +1893,7 @@ Ok(()) } + /// Fetch an NFT collection, but make sure it has the appropriate type. pub fn get_typed_nft_collection( collection_id: CollectionId, collection_type: misc::CollectionType, @@ -1549,6 +1903,8 @@ Self::get_nft_collection(collection_id) } + /// Same as [`get_typed_nft_collection`](crate::pallet::Pallet::get_typed_nft_collection), + /// but also return the Unique collection ID. pub fn get_typed_nft_collection_mapped( rmrk_collection_id: RmrkCollectionId, collection_type: misc::CollectionType, @@ -1563,31 +1919,37 @@ Ok((collection, unique_collection_id)) } + /// Fetch and decode a RMRK-scoped NFT property value in bytes. pub fn get_nft_property( collection_id: CollectionId, nft_id: TokenId, key: RmrkProperty, ) -> Result { let nft_property = >::token_properties((collection_id, nft_id)) - .get(&Self::rmrk_property_key(key)?) + .get(&Self::get_scoped_property_key(key)?) .ok_or(>::RmrkPropertyIsNotFound)? .clone(); Ok(nft_property) } + /// Fetch a RMRK-scoped NFT property and decode it from bytes into an appropriate type. pub fn get_nft_property_decoded( collection_id: CollectionId, nft_id: TokenId, key: RmrkProperty, ) -> Result { - Self::decode_property(&Self::get_nft_property(collection_id, nft_id, key)?) + Self::decode_property_value(&Self::get_nft_property(collection_id, nft_id, key)?) } + /// Check that an NFT exists. pub fn nft_exists(collection_id: CollectionId, nft_id: TokenId) -> bool { >::contains_key((collection_id, nft_id)) } + /// Get the type of an NFT stored as a scoped property. + /// + /// RMRK Core proxy differentiates between regular NFTs, and RMRK Parts and Themes. pub fn get_nft_type( collection_id: CollectionId, token_id: TokenId, @@ -1596,6 +1958,7 @@ .map_err(|_| >::NoAvailableNftId.into()) } + /// Ensure that the type of the NFT equals the provided type, otherwise return an error. pub fn ensure_nft_type( collection_id: CollectionId, token_id: TokenId, @@ -1607,6 +1970,8 @@ Ok(()) } + /// Ensure that an account is the owner of the token, either directly + /// or at the top of the nesting hierarchy; return an error if it is not. pub fn ensure_nft_owner( collection_id: CollectionId, token_id: TokenId, @@ -1627,6 +1992,8 @@ Ok(()) } + /// Fetch non-scoped properties of a collection or a token that match the filter keys supplied, + /// or, if None are provided, return all non-scoped properties. pub fn filter_user_properties( collection_id: CollectionId, token_id: Option, @@ -1672,6 +2039,8 @@ }) } + /// Get all non-scoped properties from a collection or a token, and apply some transformation, + /// supplied by `mapper`, to each key-value pair. pub fn iterate_user_properties( collection_id: CollectionId, token_id: Option, @@ -1699,6 +2068,7 @@ Ok(properties) } + /// Match Unique errors to RMRK's own and return the RMRK error if a match is successful. fn map_unique_err_to_proxy(err: DispatchError) -> DispatchError { map_unique_err_to_proxy! { match err { --- a/pallets/proxy-rmrk-core/src/misc.rs +++ b/pallets/proxy-rmrk-core/src/misc.rs @@ -14,9 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! Miscellaneous helpers and utilities used by the proxy pallet. + use super::*; use codec::{Encode, Decode, Error}; +/// Match an error to a provided pattern matcher and get +/// the corresponding error of another type if a match is successful. #[macro_export] macro_rules! map_unique_err_to_proxy { (match $err:ident { $($unique_err_ty:ident :: $unique_err:ident => $proxy_err:ident),+ $(,)? }) => { @@ -30,8 +34,10 @@ }; } -// Utilize the RmrkCore pallet for access to Runtime errors. +/// Interface to decode some serialized bytes into an arbitrary type `T`, +/// preferably if these bytes were originally encoded from `T`. pub trait RmrkDecode { + /// Try to decode self into an arbitrary type `T`. fn decode(&self) -> Result; } @@ -43,8 +49,9 @@ } } -// Utilize the RmrkCore pallet for access to Runtime errors. +/// Interface to "rebind" - change the limit of a bounded byte vector. pub trait RmrkRebind { + /// Try to change the limit of a bounded byte vector. fn rebind(&self) -> Result, Error>; } @@ -58,12 +65,16 @@ } } +/// RMRK Base shares functionality with a regular collection, and is thus +/// stored as one, but they are used for different purposes and need to be differentiated. #[derive(Encode, Decode, PartialEq, Eq)] pub enum CollectionType { Regular, Base, } +/// RMRK Base, being stored as a collection, can have different kinds of tokens, +/// all except the `Regular` type, which is attributed to `Regular` collection. #[derive(Encode, Decode, PartialEq, Eq)] pub enum NftType { Regular, --- a/pallets/proxy-rmrk-core/src/property.rs +++ b/pallets/proxy-rmrk-core/src/property.rs @@ -14,13 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! Details of storing and handling RMRK properties. + use super::*; use up_data_structs::PropertyScope; use core::convert::AsRef; +/// Property prefix for storing resources. pub const RESOURCE_ID_PREFIX: &str = "rsid-"; +/// Property prefix for storing custom user-defined properties. pub const USER_PROPERTY_PREFIX: &str = "userprop-"; +/// Property scope for RMRK, used to signify that this property +/// was created and is used by RMRK. +pub const RMRK_SCOPE: PropertyScope = PropertyScope::Rmrk; +/// Predefined RMRK property keys for storage of RMRK data format on the Unique chain. pub enum RmrkProperty<'r> { Metadata, CollectionType, @@ -49,6 +57,7 @@ } impl<'r> RmrkProperty<'r> { + /// Convert a predefined RMRK property key enum into string bytes. pub fn to_key(self) -> Result> { fn get_bytes>(container: &T) -> &[u8] { container.as_ref() @@ -94,9 +103,10 @@ } } +/// Strip a property key of its prefix and RMRK scope. pub fn strip_key_prefix(key: &PropertyKey, prefix: &str) -> Option { let key_prefix = PropertyKey::try_from(prefix.as_bytes().to_vec()).ok()?; - let key_prefix = PropertyScope::Rmrk.apply(key_prefix).ok()?; + let key_prefix = RMRK_SCOPE.apply(key_prefix).ok()?; key.as_slice() .strip_prefix(key_prefix.as_slice())? @@ -105,6 +115,7 @@ .ok() } +/// Check that the key has the prefix. pub fn is_valid_key_prefix(key: &PropertyKey, prefix: &str) -> bool { strip_key_prefix(key, prefix).is_some() } --- a/pallets/proxy-rmrk-core/src/rpc.rs +++ b/pallets/proxy-rmrk-core/src/rpc.rs @@ -1,9 +1,29 @@ +// 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 . + +//! Realizations of RMRK RPCs (remote procedure calls) related to the Core pallet. + use super::*; +/// Get the latest created collection ID. pub fn last_collection_idx() -> Result { Ok(>::last_collection_idx()) } +/// Get collection info by ID. pub fn collection_by_id( collection_id: RmrkCollectionId, ) -> Result>, DispatchError> { @@ -29,6 +49,7 @@ })) } +/// Get NFT info by collection and NFT IDs. pub fn nft_by_id( collection_id: RmrkCollectionId, nft_by_id: RmrkNftId, @@ -83,6 +104,7 @@ })) } +/// Get tokens owned by an account in a collection. pub fn account_tokens( account_id: T::AccountId, collection_id: RmrkCollectionId, @@ -116,6 +138,7 @@ Ok(tokens) } +/// Get tokens nested in an NFT - its direct children (not the children's children). pub fn nft_children( collection_id: RmrkCollectionId, nft_id: RmrkNftId, @@ -152,6 +175,7 @@ ) } +/// Get collection properties, created by the user - not the proxy-specific properties. pub fn collection_properties( collection_id: RmrkCollectionId, filter_keys: Option>, @@ -174,6 +198,7 @@ Ok(properties) } +/// Get NFT properties, created by the user - not the proxy-specific properties. pub fn nft_properties( collection_id: RmrkCollectionId, nft_id: RmrkNftId, @@ -199,6 +224,7 @@ Ok(properties) } +/// Get full information on each resource of an NFT, including pending. pub fn nft_resources( collection_id: RmrkCollectionId, nft_id: RmrkNftId, @@ -226,7 +252,7 @@ return None; } - let resource_info: RmrkResourceInfo = >::decode_property(&value).ok()?; + let resource_info: RmrkResourceInfo = >::decode_property_value(&value).ok()?; Some(resource_info) }) @@ -235,6 +261,7 @@ Ok(resources) } +/// Get the priority of a resource in an NFT. pub fn nft_resource_priority( collection_id: RmrkCollectionId, nft_id: RmrkNftId, --- a/pallets/proxy-rmrk-equip/src/benchmarking.rs +++ b/pallets/proxy-rmrk-equip/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 sp_std::vec; use frame_benchmarking::{benchmarks, account}; --- a/pallets/proxy-rmrk-equip/src/lib.rs +++ b/pallets/proxy-rmrk-equip/src/lib.rs @@ -14,6 +14,123 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! # RMRK Core Proxy Pallet +//! +//! A pallet used as proxy for RMRK Core (). +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Pallet`] +//! +//! ## Overview +//! +//! The RMRK Equip Proxy pallet mirrors the functionality of RMRK Equip, +//! binding its externalities to Unique's own underlying structure. +//! It is purposed to mimic RMRK Equip exactly, allowing seamless integrations +//! of solutions based on RMRK. +//! +//! RMRK Equip itself contains functionality to equip NFTs, and work with Bases, +//! Parts, and Themes. See [Proxy Implementation](#proxy-implementation) for details. +//! +//! Equip Proxy is responsible for a more specific area of RMRK, and heavily relies on the Core. +//! For a more foundational description of proxy implementation, please refer to [`pallet_rmrk_core`]. +//! +//! *Note*, that while RMRK itself is subject to active development and restructuring, +//! the proxy may be caught temporarily out of date. +//! +//! ### What is RMRK? +//! +//! RMRK is a set of NFT standards which compose several "NFT 2.0 lego" primitives. +//! Putting these legos together allows a user to create NFT systems of arbitrary complexity. +//! +//! Meaning, RMRK NFTs are dynamic, able to nest into each other and form a hierarchy, +//! make use of specific changeable and partially shared metadata in the form of resources, +//! and more. +//! +//! Visit RMRK documentation and repositories to learn more: +//! - Docs: +//! - FAQ: +//! - Substrate code repository: +//! - RMRK spec repository: +//! +//! ## Terminology +//! +//! For more information on RMRK, see RMRK's own documentation. +//! +//! ### Intro to RMRK +//! +//! - **Resource:** Additional piece of metadata of an NFT usually serving to add +//! a piece of media on top of the root metadata (NFT's own), be it a different wing +//! on the root template bird or something entirely unrelated. +//! +//! - **Base:** A list of possible "components" - Parts, a combination of which can +//! be appended/equipped to/on an NFT. +//! +//! - **Part:** Something that, together with other Parts, can constitute an NFT. +//! Parts are defined in the Base to which they belong. Parts can be either +//! of the `slot` type or `fixed` type. Slots are intended for equippables. +//! Note that "part of something" and "Part of a Base" can be easily confused, +//! and so in this documentation these words are distinguished by the capital letter. +//! +//! - **Theme:** Named objects of variable => value pairs which get interpolated into +//! the Base's `themable` Parts. Themes can hold any value, but are often represented +//! in RMRK's examples as colors applied to visible Parts. +//! +//! ### Peculiarities in Unique +//! +//! - **Scoped properties:** Properties that are normally obscured from users. +//! Their purpose is to contain structured metadata that was not included in the Unique standard +//! for collections and tokens, meant to be operated on by proxies and other outliers. +//! Scoped property keys are prefixed with `some-scope:`, where `some-scope` is +//! an arbitrary keyword, like "rmrk". `:` is considered an unacceptable symbol in user-defined +//! properties, which, along with other safeguards, makes scoped ones impossible to tamper with. +//! +//! - **Auxiliary properties:** A slightly different structure of properties, +//! trading universality of use for more convenient storage, writes and access. +//! Meant to be inaccessible to end users. +//! +//! ## Proxy Implementation +//! +//! An external user is supposed to be able to utilize this proxy as they would +//! utilize RMRK, and get exactly the same results. Normally, Unique transactions +//! are off-limits to RMRK collections and tokens, and vice versa. However, +//! the information stored on chain can be freely interpreted by storage reads and Unique RPCs. +//! +//! ### ID Mapping +//! +//! RMRK's collections' IDs are counted independently of Unique's and start at 0. +//! Note that tokens' IDs still start at 1. +//! The collections themselves, as well as tokens, are stored as Unique collections, +//! and thus RMRK IDs are mapped to Unique IDs (but not vice versa). +//! +//! ### External/Internal Collection Insulation +//! +//! A Unique transaction cannot target collections purposed for RMRK, +//! and they are flagged as `external` to specify that. On the other hand, +//! due to the mapping, RMRK transactions and RPCs simply cannot reach Unique collections. +//! +//! ### Native Properties +//! +//! Many of RMRK's native parameters are stored as scoped properties of a collection +//! or an NFT on the chain. Scoped properties are prefixed with `rmrk:`, where `:` +//! is an unacceptable symbol in user-defined properties, which, along with other safeguards, +//! makes them impossible to tamper with. +//! +//! ### Collection and NFT Types, or Base, Parts and Themes Handling +//! +//! RMRK introduces the concept of a Base, which is a catalogue of Parts, +//! possible components of an NFT. Due to its similarity with the functionality +//! of a token collection, a Base is stored and handled as one, and the Base's Parts and Themes +//! are this collection's NFTs. See [`CollectionType`] and [`NftType`]. +//! +//! ## Interface +//! +//! ### Dispatchables +//! +//! - `create_base` - Create a new Base. +//! - `theme_add` - Add a Theme to a Base. +//! - `equippable` - Update the array of Collections allowed to be equipped to a Base's specified Slot Part. + #![cfg_attr(not(feature = "std"), no_std)] use frame_support::{pallet_prelude::*, transactional, BoundedVec, dispatch::DispatchResult}; @@ -45,15 +162,20 @@ #[pallet::config] pub trait Config: frame_system::Config + pallet_rmrk_core::Config { + /// Overarching event type. type Event: From> + IsType<::Event>; + + /// The weight information of this pallet. type WeightInfo: WeightInfo; } + /// Map of a Base ID and a Part ID to an NFT in the Base collection serving as the Part. #[pallet::storage] #[pallet::getter(fn internal_part_id)] pub type InernalPartId = StorageDoubleMap<_, Twox64Concat, CollectionId, Twox64Concat, RmrkPartId, TokenId>; + /// Checkmark that a Base has a Theme NFT named "default". #[pallet::storage] #[pallet::getter(fn base_has_default_theme)] pub type BaseHasDefaultTheme = @@ -78,26 +200,36 @@ #[pallet::error] pub enum Error { + /// No permission to perform action. PermissionError, + /// Could not find an ID for a Base collection. It is likely there were too many collections created on the chain, causing an overflow. NoAvailableBaseId, + /// Could not find a suitable ID for a Part, likely too many Part tokens were created in the Base, causing an overflow NoAvailablePartId, + /// Base collection linked to this ID does not exist. BaseDoesntExist, + /// No Theme named "default" is associated with the Base. NeedsDefaultThemeFirst, + /// Part linked to this ID does not exist. PartDoesntExist, + /// Cannot assign equippables to a fixed Part. NoEquippableOnFixedPart, } #[pallet::call] impl Pallet { - /// Creates a new Base. - /// Modeled after [base interaction](https://github.com/rmrk-team/rmrk-spec/blob/master/standards/rmrk2.0.0/interactions/base.md) + /// Create a new Base. /// - /// Parameters: - /// - origin: Caller, will be assigned as the issuer of the Base - /// - base_type: media type, e.g. "svg" - /// - symbol: arbitrary client-chosen symbol - /// - parts: array of Fixed and Slot parts composing the base, confined in length by - /// RmrkPartsLimit + /// Modeled after the [Base interaction](https://github.com/rmrk-team/rmrk-spec/blob/master/standards/rmrk2.0.0/interactions/base.md) + /// + /// # Permissions + /// - Anyone - will be assigned as the issuer of the Base. + /// + /// # Arguments: + /// - `base_type`: Arbitrary media type, e.g. "svg". + /// - `symbol`: Arbitrary client-chosen symbol. + /// - `parts`: Array of Fixed and Slot Parts composing the Base, + /// confined in length by [`RmrkPartsLimit`](up_data_structs::RmrkPartsLimit). #[transactional] #[pallet::weight(>::create_base(parts.len() as u32))] pub fn create_base( @@ -131,8 +263,11 @@ collection_id, PropertyScope::Rmrk, [ - >::rmrk_property(CollectionType, &misc::CollectionType::Base)?, - >::rmrk_property(BaseType, &base_type)?, + >::encode_rmrk_property( + CollectionType, + &misc::CollectionType::Base, + )?, + >::encode_rmrk_property(BaseType, &base_type)?, ] .into_iter(), )?; @@ -151,19 +286,21 @@ Ok(()) } - /// Adds a Theme to a Base. - /// Modeled after [themeadd interaction](https://github.com/rmrk-team/rmrk-spec/blob/master/standards/rmrk2.0.0/interactions/themeadd.md) - /// Themes are stored in the Themes storage + /// Add a Theme to a Base. /// A Theme named "default" is required prior to adding other Themes. /// - /// Parameters: - /// - origin: The caller of the function, must be issuer of the base - /// - base_id: The Base containing the Theme to be updated - /// - theme: The Theme to add to the Base. A Theme has a name and properties, which are an + /// Modeled after [Themeadd interaction](https://github.com/rmrk-team/rmrk-spec/blob/master/standards/rmrk2.0.0/interactions/themeadd.md). + /// + /// # Permissions: + /// - Base issuer + /// + /// # Arguments: + /// - `base_id`: Base ID containing the Theme to be updated. + /// - `theme`: Theme to add to the Base. A Theme has a name and properties, which are an /// array of [key, value, inherit]. - /// - key: arbitrary BoundedString, defined by client - /// - value: arbitrary BoundedString, defined by client - /// - inherit: optional bool + /// - `key`: Arbitrary BoundedString, defined by client. + /// - `value`: Arbitrary BoundedString, defined by client. + /// - `inherit`: Optional bool. #[transactional] #[pallet::weight(>::theme_add(theme.properties.len() as u32))] pub fn theme_add( @@ -191,9 +328,9 @@ owner, &collection, [ - >::rmrk_property(TokenType, &NftType::Theme)?, - >::rmrk_property(ThemeName, &theme.name)?, - >::rmrk_property(ThemeInherit, &theme.inherit)?, + >::encode_rmrk_property(TokenType, &NftType::Theme)?, + >::encode_rmrk_property(ThemeName, &theme.name)?, + >::encode_rmrk_property(ThemeInherit, &theme.inherit)?, ] .into_iter(), ) @@ -204,7 +341,7 @@ collection_id, token_id, PropertyScope::Rmrk, - >::rmrk_property( + >::encode_rmrk_property( UserProperty(property.key.as_slice()), &property.value, )?, @@ -214,6 +351,17 @@ Ok(()) } + /// Update the array of Collections allowed to be equipped to a Base's specified Slot Part. + /// + /// Modeled after [equippable interaction](https://github.com/rmrk-team/rmrk-spec/blob/master/standards/rmrk2.0.0/interactions/equippable.md). + /// + /// # Permissions: + /// - Base issuer + /// + /// # Arguments: + /// - `base_id`: Base containing the Slot Part to be updated. + /// - `part_id`: Slot Part whose Equippable List is being updated. + /// - `equippables`: List of equippables that will override the current Equippables list. #[transactional] #[pallet::weight(>::equippable())] pub fn equippable( @@ -253,7 +401,7 @@ base_collection_id, part_id, PropertyScope::Rmrk, - >::rmrk_property(EquippableList, &equippables)?, + >::encode_rmrk_property(EquippableList, &equippables)?, )?; } } @@ -266,6 +414,8 @@ } impl Pallet { + /// Create (or overwrite) a Part in a Base. + /// The Part and the Base are represented as an NFT and a Collection. fn create_part( sender: &T::CrossAccountId, collection: &NonfungibleHandle, @@ -298,7 +448,7 @@ collection.id, token_id, PropertyScope::Rmrk, - >::rmrk_property(ExternalPartId, &part_id)?, + >::encode_rmrk_property(ExternalPartId, &part_id)?, )?; token_id @@ -310,9 +460,9 @@ token_id, PropertyScope::Rmrk, [ - >::rmrk_property(TokenType, &nft_type)?, - >::rmrk_property(Src, &src)?, - >::rmrk_property(ZIndex, &z_index)?, + >::encode_rmrk_property(TokenType, &nft_type)?, + >::encode_rmrk_property(Src, &src)?, + >::encode_rmrk_property(ZIndex, &z_index)?, ] .into_iter(), )?; @@ -322,13 +472,15 @@ collection.id, token_id, PropertyScope::Rmrk, - >::rmrk_property(EquippableList, &part.equippable)?, + >::encode_rmrk_property(EquippableList, &part.equippable)?, )?; } Ok(()) } + /// Ensure that the collection under the Base ID is a Base collection, + /// and fetch it. fn get_base(base_id: CollectionId) -> Result, DispatchError> { let collection = >::get_typed_nft_collection(base_id, misc::CollectionType::Base) --- a/pallets/proxy-rmrk-equip/src/rpc.rs +++ b/pallets/proxy-rmrk-equip/src/rpc.rs @@ -1,7 +1,26 @@ +// 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 . + +//! Realizations of RMRK RPCs (remote procedure calls) related to the Equip pallet. + use super::*; use pallet_rmrk_core::{misc, property::*}; use sp_std::vec::Vec; +/// Get base info by its ID. pub fn base( base_id: RmrkBaseId, ) -> Result>, DispatchError> { @@ -22,6 +41,7 @@ })) } +/// Get all parts of a base. pub fn base_parts(base_id: RmrkBaseId) -> Result, DispatchError> { use pallet_common::CommonCollectionOperations; @@ -93,6 +113,7 @@ Ok(parts) } +/// Get the theme names belonging to a base. pub fn theme_names(base_id: RmrkBaseId) -> Result, DispatchError> { use pallet_common::CommonCollectionOperations; @@ -124,6 +145,7 @@ Ok(theme_names) } +/// Get theme info, including properties, optionally limited to the provided keys. pub fn theme( base_id: RmrkBaseId, theme_name: RmrkThemeName, --- a/primitives/rmrk-traits/src/resource.rs +++ b/primitives/rmrk-traits/src/resource.rs @@ -151,13 +151,13 @@ "#) )] pub struct ResourceInfo { - /// id is a 5-character string of reasonable uniqueness. - /// The combination of base ID and resource id should be unique across the entire RMRK - /// ecosystem which + /// ID is a unique identifier for a resource across all those of a single NFT. + /// The combination of a collection ID, an NFT ID, and the resource ID must be + /// unique across the entire RMRK ecosystem. //#[cfg_attr(feature = "std", serde(with = "serialize::vec"))] pub id: ResourceId, - /// Resource + /// Resource type and the accordingly structured data stored pub resource: ResourceTypes, /// If resource is sent to non-rootowned NFT, pending will be false and need to be accepted -- gitstuff