difftreelog
Merge pull request #848 from UniqueNetwork/fix/find-parent
in: master
15 files changed
pallets/common/src/erc.rsdiffbeforeafterboth--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -28,8 +28,8 @@
use sp_std::{vec, vec::Vec};
use sp_core::U256;
use up_data_structs::{
- AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,
- SponsoringRateLimit, SponsorshipState,
+ CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property, SponsoringRateLimit,
+ SponsorshipState,
};
use crate::{
pallets/common/src/eth.rsdiffbeforeafterboth--- a/pallets/common/src/eth.rs
+++ b/pallets/common/src/eth.rs
@@ -80,10 +80,7 @@
if cross_account_id.is_canonical_substrate() {
Self::from_sub::<T>(cross_account_id.as_sub())
} else {
- Self {
- eth: *cross_account_id.as_eth(),
- sub: Default::default(),
- }
+ Self::from_eth(*cross_account_id.as_eth())
}
}
/// Creates [`CrossAddress`] from Substrate account.
@@ -97,6 +94,13 @@
sub: U256::from_big_endian(account_id.as_ref()),
}
}
+ /// Creates [`CrossAddress`] from Ethereum account.
+ pub fn from_eth(address: Address) -> Self {
+ Self {
+ eth: address,
+ sub: Default::default(),
+ }
+ }
/// Converts [`CrossAddress`] to `CrossAccountId`.
pub fn into_sub_cross_account<T>(&self) -> evm_coder::execution::Result<T::CrossAccountId>
where
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -100,6 +100,7 @@
PropertyValue,
PropertyPermission,
PropertiesError,
+ TokenOwnerError,
PropertyKeyPermission,
TokenData,
TrySetProperty,
@@ -2134,7 +2135,7 @@
/// 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<T::CrossAccountId>;
+ fn token_owner(&self, token: TokenId) -> Result<T::CrossAccountId, TokenOwnerError>;
/// Returns 10 tokens owners in no particular order.
///
pallets/fungible/src/common.rsdiffbeforeafterboth--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -17,7 +17,9 @@
use core::marker::PhantomData;
use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight, traits::Get};
-use up_data_structs::{TokenId, CollectionId, CreateItemExData, budget::Budget, CreateItemData};
+use up_data_structs::{
+ TokenId, CollectionId, CreateItemExData, budget::Budget, CreateItemData, TokenOwnerError,
+};
use pallet_common::{
CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight,
weights::WeightInfo as _,
@@ -404,8 +406,8 @@
TokenId::default()
}
- fn token_owner(&self, _token: TokenId) -> Option<T::CrossAccountId> {
- None
+ fn token_owner(&self, _token: TokenId) -> Result<T::CrossAccountId, TokenOwnerError> {
+ Err(TokenOwnerError::MultipleOwners)
}
/// Returns 10 tokens owners in no particular order.
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -19,7 +19,7 @@
use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight};
use up_data_structs::{
TokenId, CreateItemExData, CollectionId, budget::Budget, Property, PropertyKey,
- PropertyKeyPermission, PropertyValue,
+ PropertyKeyPermission, PropertyValue, TokenOwnerError,
};
use pallet_common::{
CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight,
@@ -460,13 +460,15 @@
TokenId(<TokensMinted<T>>::get(self.id))
}
- fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId> {
- <TokenData<T>>::get((self.id, token)).map(|t| t.owner)
+ fn token_owner(&self, token: TokenId) -> Result<T::CrossAccountId, TokenOwnerError> {
+ <TokenData<T>>::get((self.id, token))
+ .map(|t| t.owner)
+ .ok_or(TokenOwnerError::NotFound)
}
/// Returns token owners.
fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId> {
- self.token_owner(token).map_or_else(|| vec![], |t| vec![t])
+ self.token_owner(token).map_or_else(|_| vec![], |t| vec![t])
}
fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue> {
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -728,7 +728,7 @@
fn cross_owner_of(&self, token_id: U256) -> Result<eth::CrossAddress> {
Self::token_owner(&self, token_id.try_into()?)
.map(|o| eth::CrossAddress::from_sub_cross_account::<T>(&o))
- .ok_or(Error::Revert("key too large".into()))
+ .map_err(|_| Error::Revert("token not found".into()))
}
/// Returns the token properties.
pallets/proxy-rmrk-core/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # RMRK Core Proxy Pallet18//!19//! A pallet used as proxy for RMRK Core (<https://rmrk-team.github.io/rmrk-substrate/#/pallets/rmrk-core>).20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The RMRK Core Proxy pallet mirrors the functionality of RMRK Core,28//! binding its externalities to Unique's own underlying structure.29//! It is purposed to mimic RMRK Core exactly, allowing seamless integrations30//! of solutions based on RMRK.31//!32//! RMRK Core itself contains essential functionality for RMRK's nested and33//! multi-resourced NFTs.34//!35//! *Note*, that while RMRK itself is subject to active development and restructuring,36//! the proxy may be caught temporarily out of date.37//!38//! ### What is RMRK?39//!40//! RMRK is a set of NFT standards which compose several "NFT 2.0 lego" primitives.41//! Putting these legos together allows a user to create NFT systems of arbitrary complexity.42//!43//! Meaning, RMRK NFTs are dynamic, able to nest into each other and form a hierarchy,44//! make use of specific changeable and partially shared metadata in the form of resources,45//! and more.46//!47//! Visit RMRK documentation and repositories to learn more:48//! - Docs: <https://docs.rmrk.app/getting-started/>49//! - FAQ: <https://coda.io/@rmrk/faq>50//! - Substrate code repository: <https://github.com/rmrk-team/rmrk-substrate>51//! - RMRK specification repository: <https://github.com/rmrk-team/rmrk-spec>52//!53//! ## Terminology54//!55//! For more information on RMRK, see RMRK's own documentation.56//!57//! ### Intro to RMRK58//!59//! - **Resource:** Additional piece of metadata of an NFT usually serving to add60//! a piece of media on top of the root metadata (NFT's own), be it a different wing61//! on the root template bird or something entirely unrelated.62//!63//! - **Base:** A list of possible "components" - Parts, a combination of which can64//! be appended/equipped to/on an NFT.65//!66//! - **Part:** Something that, together with other Parts, can constitute an NFT.67//! Parts are defined in the Base to which they belong. Parts can be either68//! of the `slot` type or `fixed` type. Slots are intended for equippables.69//! Note that "part of something" and "Part of a Base" can be easily confused,70//! and so in this documentation these words are distinguished by the capital letter.71//!72//! - **Theme:** Named objects of variable => value pairs which get interpolated into73//! the Base's `themable` Parts. Themes can hold any value, but are often represented74//! in RMRK's examples as colors applied to visible Parts.75//!76//! ### Peculiarities in Unique77//!78//! - **Scoped properties:** Properties that are normally obscured from users.79//! Their purpose is to contain structured metadata that was not included in the Unique standard80//! for collections and tokens, meant to be operated on by proxies and other outliers.81//! Scoped property keys are prefixed with `some-scope:`, where `some-scope` is82//! an arbitrary keyword, like "rmrk". `:` is considered an unacceptable symbol in user-defined83//! properties, which, along with other safeguards, makes scoped ones impossible to tamper with.84//!85//! - **Auxiliary properties:** A slightly different structure of properties,86//! trading universality of use for more convenient storage, writes and access.87//! Meant to be inaccessible to end users.88//!89//! ## Proxy Implementation90//!91//! An external user is supposed to be able to utilize this proxy as they would92//! utilize RMRK, and get exactly the same results. Normally, Unique transactions93//! are off-limits to RMRK collections and tokens, and vice versa. However,94//! the information stored on chain can be freely interpreted by storage reads and Unique RPCs.95//!96//! ### ID Mapping97//!98//! RMRK's collections' IDs are counted independently of Unique's and start at 0.99//! Note that tokens' IDs still start at 1.100//! The collections themselves, as well as tokens, are stored as Unique collections,101//! and thus RMRK IDs are mapped to Unique IDs (but not vice versa).102//!103//! ### External/Internal Collection Insulation104//!105//! A Unique transaction cannot target collections purposed for RMRK,106//! and they are flagged as `external` to specify that. On the other hand,107//! due to the mapping, RMRK transactions and RPCs simply cannot reach Unique collections.108//!109//! ### Native Properties110//!111//! Many of RMRK's native parameters are stored as scoped properties of a collection112//! or an NFT on the chain. Scoped properties are prefixed with `rmrk:`, where `:`113//! is an unacceptable symbol in user-defined properties, which, along with other safeguards,114//! makes them impossible to tamper with.115//!116//! ### Collection and NFT Types, or Base, Parts and Themes Handling117//!118//! RMRK introduces the concept of a Base, which is a catalogue of Parts,119//! possible components of an NFT. Due to its similarity with the functionality120//! of a token collection, a Base is stored and handled as one, and the Base's Parts and Themes121//! are this collection's NFTs. See [`CollectionType`] and [`NftType`].122//!123//! ## Interface124//!125//! ### Dispatchables126//!127//! - `create_collection` - Create a new collection of NFTs.128//! - `destroy_collection` - Destroy a collection.129//! - `change_collection_issuer` - Change the issuer of a collection.130//! Analogous to Unique's collection's [`owner`](up_data_structs::Collection).131//! - `lock_collection` - "Lock" the collection and prevent new token creation. **Cannot be undone.**132//! - `mint_nft` - Mint an NFT in a specified collection.133//! - `burn_nft` - Burn an NFT, destroying it and its nested tokens.134//! - `send` - Transfer an NFT from an account/NFT A to another account/NFT B.135//! - `accept_nft` - Accept an NFT sent from another account to self or an owned NFT.136//! - `reject_nft` - Reject an NFT sent from another account to self or owned NFT and **burn it**.137//! - `accept_resource` - Accept the addition of a newly created pending resource to an existing NFT.138//! - `accept_resource_removal` - Accept the removal of a removal-pending resource from an NFT.139//! - `set_property` - Add or edit a custom user property of a token or a collection.140//! - `set_priority` - Set a different order of resource priorities for an NFT.141//! - `add_basic_resource` - Create and set/propose a basic resource for an NFT.142//! - `add_composable_resource` - Create and set/propose a composable resource for an NFT.143//! - `add_slot_resource` - Create and set/propose a slot resource for an NFT.144//! - `remove_resource` - Remove and erase a resource from an NFT.145146#![cfg_attr(not(feature = "std"), no_std)]147148use frame_support::{pallet_prelude::*, BoundedVec, dispatch::DispatchResult};149use frame_system::{pallet_prelude::*, ensure_signed};150use sp_runtime::{DispatchError, Permill, traits::StaticLookup};151use sp_std::{152 vec::Vec,153 collections::{btree_set::BTreeSet, btree_map::BTreeMap},154};155use up_data_structs::{*, mapping::TokenAddressMapping};156use pallet_common::{157 Pallet as PalletCommon, Error as CommonError, CollectionHandle, CommonCollectionOperations,158};159use pallet_nonfungible::{Pallet as PalletNft, NonfungibleHandle, TokenData};160use pallet_structure::{Pallet as PalletStructure, Error as StructureError};161use pallet_evm::account::CrossAccountId;162use core::convert::AsRef;163164pub use pallet::*;165166#[cfg(feature = "runtime-benchmarks")]167pub mod benchmarking;168pub mod misc;169pub mod property;170pub mod rpc;171pub mod weights;172173pub type SelfWeightOf<T> = <T as Config>::WeightInfo;174175use weights::WeightInfo;176use misc::*;177pub use property::*;178179use RmrkProperty::*;180181/// A maximum number of levels of depth in the token nesting tree.182pub const NESTING_BUDGET: u32 = 5;183184type PendingTarget = (CollectionId, TokenId);185type PendingChild = (RmrkCollectionId, RmrkNftId);186type PendingChildrenSet = BTreeSet<PendingChild>;187188type BasesMap = BTreeMap<RmrkBaseId, u32>;189190#[frame_support::pallet]191pub mod pallet {192 use super::*;193194 #[pallet::config]195 pub trait Config:196 frame_system::Config197 + pallet_common::Config198 + pallet_nonfungible::Config199 + pallet_evm::Config200 {201 /// Overarching event type.202 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;203204 /// The weight information of this pallet.205 type WeightInfo: WeightInfo;206 }207208 /// Latest yet-unused collection ID.209 #[pallet::storage]210 #[pallet::getter(fn collection_index)]211 pub type CollectionIndex<T: Config> = StorageValue<_, RmrkCollectionId, ValueQuery>;212213 /// Mapping from RMRK collection ID to Unique's.214 #[pallet::storage]215 pub type UniqueCollectionId<T: Config> =216 StorageMap<_, Twox64Concat, RmrkCollectionId, CollectionId, ValueQuery>;217218 #[pallet::pallet]219 #[pallet::generate_store(pub(super) trait Store)]220 pub struct Pallet<T>(_);221222 #[pallet::event]223 #[pallet::generate_deposit(pub(super) fn deposit_event)]224 pub enum Event<T: Config> {225 CollectionCreated {226 issuer: T::AccountId,227 collection_id: RmrkCollectionId,228 },229 CollectionDestroyed {230 issuer: T::AccountId,231 collection_id: RmrkCollectionId,232 },233 IssuerChanged {234 old_issuer: T::AccountId,235 new_issuer: T::AccountId,236 collection_id: RmrkCollectionId,237 },238 CollectionLocked {239 issuer: T::AccountId,240 collection_id: RmrkCollectionId,241 },242 NftMinted {243 owner: T::AccountId,244 collection_id: RmrkCollectionId,245 nft_id: RmrkNftId,246 },247 NFTBurned {248 owner: T::AccountId,249 nft_id: RmrkNftId,250 },251 NFTSent {252 sender: T::AccountId,253 recipient: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,254 collection_id: RmrkCollectionId,255 nft_id: RmrkNftId,256 approval_required: bool,257 },258 NFTAccepted {259 sender: T::AccountId,260 recipient: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,261 collection_id: RmrkCollectionId,262 nft_id: RmrkNftId,263 },264 NFTRejected {265 sender: T::AccountId,266 collection_id: RmrkCollectionId,267 nft_id: RmrkNftId,268 },269 PropertySet {270 collection_id: RmrkCollectionId,271 maybe_nft_id: Option<RmrkNftId>,272 key: RmrkKeyString,273 value: RmrkValueString,274 },275 ResourceAdded {276 nft_id: RmrkNftId,277 resource_id: RmrkResourceId,278 },279 ResourceRemoval {280 nft_id: RmrkNftId,281 resource_id: RmrkResourceId,282 },283 ResourceAccepted {284 nft_id: RmrkNftId,285 resource_id: RmrkResourceId,286 },287 ResourceRemovalAccepted {288 nft_id: RmrkNftId,289 resource_id: RmrkResourceId,290 },291 PrioritySet {292 collection_id: RmrkCollectionId,293 nft_id: RmrkNftId,294 },295 }296297 #[pallet::error]298 pub enum Error<T> {299 /* Unique proxy-specific events */300 /// Property of the type of RMRK collection could not be read successfully.301 CorruptedCollectionType,302 // NftTypeEncodeError,303 /// Too many symbols supplied as the property key. The maximum is [256](up_data_structs::MAX_PROPERTY_KEY_LENGTH).304 RmrkPropertyKeyIsTooLong,305 /// Too many bytes supplied as the property value. The maximum is [32768](up_data_structs::MAX_PROPERTY_VALUE_LENGTH).306 RmrkPropertyValueIsTooLong,307 /// Could not find a property by the supplied key.308 RmrkPropertyIsNotFound,309 /// Something went wrong when decoding encoded data from the storage.310 /// Perhaps, there was a wrong key supplied for the type, or the data was improperly stored.311 UnableToDecodeRmrkData,312313 /* RMRK compatible events */314 /// Only destroying collections without tokens is allowed.315 CollectionNotEmpty,316 /// Could not find an ID for a collection. It is likely there were too many collections created on the chain, causing an overflow.317 NoAvailableCollectionId,318 /// Token does not exist, or there is no suitable ID for it, likely too many tokens were created in a collection, causing an overflow.319 NoAvailableNftId,320 /// Collection does not exist, has a wrong type, or does not map to a Unique ID.321 CollectionUnknown,322 /// No permission to perform action.323 NoPermission,324 /// Token is marked as non-transferable, and thus cannot be transferred.325 NonTransferable,326 /// Too many tokens created in the collection, no new ones are allowed.327 CollectionFullOrLocked,328 /// No such resource found.329 ResourceDoesntExist,330 /// If an NFT is sent to a descendant, that would form a nesting loop, an ouroboros.331 /// Sending to self is redundant.332 CannotSendToDescendentOrSelf,333 /// Not the target owner of the sent NFT.334 CannotAcceptNonOwnedNft,335 /// Not the target owner of the sent NFT.336 CannotRejectNonOwnedNft,337 /// NFT was not sent and is not pending.338 CannotRejectNonPendingNft,339 /// Resource is not pending for the operation.340 ResourceNotPending,341 /// Could not find an ID for the resource. It is likely there were too many resources created on an NFT, causing an overflow.342 NoAvailableResourceId,343 }344345 #[pallet::call]346 impl<T: Config> Pallet<T> {347 // todo :refactor replace every collection_id with rmrk_collection_id (and nft_id) in arguments for uniformity?348349 /// Create a new collection of NFTs.350 ///351 /// # Permissions:352 /// * Anyone - will be assigned as the issuer of the collection.353 ///354 /// # Arguments:355 /// - `origin`: sender of the transaction356 /// - `metadata`: Metadata describing the collection, e.g. IPFS hash. Cannot be changed.357 /// - `max`: Optional maximum number of tokens.358 /// - `symbol`: UTF-8 string with token prefix, by which to represent the token in wallets and UIs.359 /// Analogous to Unique's [`token_prefix`](up_data_structs::Collection). Cannot be changed.360 #[pallet::call_index(0)]361 #[pallet::weight(<SelfWeightOf<T>>::create_collection())]362 pub fn create_collection(363 origin: OriginFor<T>,364 metadata: RmrkString,365 max: Option<u32>,366 symbol: RmrkCollectionSymbol,367 ) -> DispatchResult {368 let sender = ensure_signed(origin)?;369370 let limits = CollectionLimits {371 owner_can_transfer: Some(false),372 token_limit: max,373 ..Default::default()374 };375376 let data = CreateCollectionData {377 limits: Some(limits),378 token_prefix: symbol379 .into_inner()380 .try_into()381 .map_err(|_| <CommonError<T>>::CollectionTokenPrefixLimitExceeded)?,382 permissions: Some(CollectionPermissions {383 nesting: Some(NestingPermissions {384 token_owner: true,385 collection_admin: false,386 restricted: None,387 #[cfg(feature = "runtime-benchmarks")]388 permissive: false,389 }),390 ..Default::default()391 }),392 ..Default::default()393 };394395 let unique_collection_id = Self::init_collection(396 T::CrossAccountId::from_sub(sender.clone()),397 data,398 [399 Self::encode_rmrk_property(Metadata, &metadata)?,400 Self::encode_rmrk_property(CollectionType, &misc::CollectionType::Regular)?,401 ]402 .into_iter(),403 )?;404 let rmrk_collection_id = <CollectionIndex<T>>::get();405406 <UniqueCollectionId<T>>::insert(rmrk_collection_id, unique_collection_id);407408 <PalletCommon<T>>::set_scoped_collection_property(409 unique_collection_id,410 RMRK_SCOPE,411 Self::encode_rmrk_property(RmrkInternalCollectionId, &rmrk_collection_id)?,412 )?;413414 <CollectionIndex<T>>::mutate(|n| *n += 1);415416 Self::deposit_event(Event::CollectionCreated {417 issuer: sender,418 collection_id: rmrk_collection_id,419 });420421 Ok(())422 }423424 /// Destroy a collection.425 ///426 /// Only empty collections can be destroyed. If it has any tokens, they must be burned first.427 ///428 /// # Permissions:429 /// * Collection issuer430 ///431 /// # Arguments:432 /// - `origin`: sender of the transaction433 /// - `collection_id`: RMRK ID of the collection to destroy.434 #[pallet::call_index(1)]435 #[pallet::weight(<SelfWeightOf<T>>::destroy_collection())]436 pub fn destroy_collection(437 origin: OriginFor<T>,438 collection_id: RmrkCollectionId,439 ) -> DispatchResult {440 let sender = ensure_signed(origin)?;441 let cross_sender = T::CrossAccountId::from_sub(sender.clone());442443 let collection = Self::get_typed_nft_collection(444 Self::unique_collection_id(collection_id)?,445 misc::CollectionType::Regular,446 )?;447 collection.check_is_external()?;448449 <PalletNft<T>>::destroy_collection(collection, &cross_sender)450 .map_err(Self::map_unique_err_to_proxy)?;451452 Self::deposit_event(Event::CollectionDestroyed {453 issuer: sender,454 collection_id,455 });456457 Ok(())458 }459460 /// Change the issuer of a collection. Analogous to Unique's collection's [`owner`](up_data_structs::Collection).461 ///462 /// # Permissions:463 /// * Collection issuer464 ///465 /// # Arguments:466 /// - `origin`: sender of the transaction467 /// - `collection_id`: RMRK collection ID to change the issuer of.468 /// - `new_issuer`: Collection's new issuer.469 #[pallet::call_index(2)]470 #[pallet::weight(<SelfWeightOf<T>>::change_collection_issuer())]471 pub fn change_collection_issuer(472 origin: OriginFor<T>,473 collection_id: RmrkCollectionId,474 new_issuer: <T::Lookup as StaticLookup>::Source,475 ) -> DispatchResult {476 let sender = ensure_signed(origin)?;477478 let collection = Self::get_nft_collection(Self::unique_collection_id(collection_id)?)?;479 collection.check_is_external()?;480481 let new_issuer = T::Lookup::lookup(new_issuer)?;482483 Self::change_collection_owner(484 Self::unique_collection_id(collection_id)?,485 misc::CollectionType::Regular,486 sender.clone(),487 new_issuer.clone(),488 )?;489490 Self::deposit_event(Event::IssuerChanged {491 old_issuer: sender,492 new_issuer,493 collection_id,494 });495496 Ok(())497 }498499 /// "Lock" the collection and prevent new token creation. Cannot be undone.500 ///501 /// # Permissions:502 /// * Collection issuer503 ///504 /// # Arguments:505 /// - `origin`: sender of the transaction506 /// - `collection_id`: RMRK ID of the collection to lock.507 #[pallet::call_index(3)]508 #[pallet::weight(<SelfWeightOf<T>>::lock_collection())]509 pub fn lock_collection(510 origin: OriginFor<T>,511 collection_id: RmrkCollectionId,512 ) -> DispatchResult {513 let sender = ensure_signed(origin)?;514 let cross_sender = T::CrossAccountId::from_sub(sender.clone());515516 let collection = Self::get_typed_nft_collection(517 Self::unique_collection_id(collection_id)?,518 misc::CollectionType::Regular,519 )?;520 collection.check_is_external()?;521522 Self::check_collection_owner(&collection, &cross_sender)?;523524 let token_count = collection.total_supply();525526 let mut collection = collection.into_inner();527 collection.limits.token_limit = Some(token_count);528 collection.save()?;529530 Self::deposit_event(Event::CollectionLocked {531 issuer: sender,532 collection_id,533 });534535 Ok(())536 }537538 /// Mint an NFT in a specified collection.539 ///540 /// # Permissions:541 /// * Collection issuer542 ///543 /// # Arguments:544 /// - `origin`: sender of the transaction545 /// - `owner`: Owner account of the NFT. If set to None, defaults to the sender (collection issuer).546 /// - `collection_id`: RMRK collection ID for the NFT to be minted within. Cannot be changed.547 /// - `recipient`: Receiver account of the royalty. Has no effect if the `royalty_amount` is not set. Cannot be changed.548 /// - `royalty_amount`: Optional permillage reward from each trade for the `recipient`. Cannot be changed.549 /// - `metadata`: Arbitrary data about an NFT, e.g. IPFS hash. Cannot be changed.550 /// - `transferable`: Can this NFT be transferred? Cannot be changed.551 /// - `resources`: Resource data to be added to the NFT immediately after minting.552 #[pallet::call_index(4)]553 #[pallet::weight(<SelfWeightOf<T>>::mint_nft(resources.as_ref().map(|r| r.len() as u32).unwrap_or(0)))]554 pub fn mint_nft(555 origin: OriginFor<T>,556 owner: Option<T::AccountId>,557 collection_id: RmrkCollectionId,558 recipient: Option<T::AccountId>,559 royalty_amount: Option<Permill>,560 metadata: RmrkString,561 transferable: bool,562 resources: Option<BoundedVec<RmrkResourceTypes, MaxResourcesOnMint>>,563 ) -> DispatchResult {564 let sender = ensure_signed(origin)?;565 let cross_sender = T::CrossAccountId::from_sub(sender.clone());566567 let owner = owner.unwrap_or(sender.clone());568 let cross_owner = T::CrossAccountId::from_sub(owner.clone());569570 let collection = Self::get_typed_nft_collection(571 Self::unique_collection_id(collection_id)?,572 misc::CollectionType::Regular,573 )?;574 collection.check_is_external()?;575576 let royalty_info = royalty_amount.map(|amount| rmrk_traits::RoyaltyInfo {577 recipient: recipient.unwrap_or_else(|| owner.clone()),578 amount,579 });580581 let nft_id = Self::create_nft(582 &cross_sender,583 &cross_owner,584 &collection,585 [586 Self::encode_rmrk_property(TokenType, &NftType::Regular)?,587 Self::encode_rmrk_property(Transferable, &transferable)?,588 Self::encode_rmrk_property(PendingNftAccept, &None::<PendingTarget>)?,589 Self::encode_rmrk_property(RoyaltyInfo, &royalty_info)?,590 Self::encode_rmrk_property(Metadata, &metadata)?,591 Self::encode_rmrk_property(Equipped, &false)?,592 Self::encode_rmrk_property(ResourcePriorities, &<Vec<u8>>::new())?,593 Self::encode_rmrk_property(NextResourceId, &(0 as RmrkResourceId))?,594 Self::encode_rmrk_property(PendingChildren, &PendingChildrenSet::new())?,595 Self::encode_rmrk_property(AssociatedBases, &BasesMap::new())?,596 ]597 .into_iter(),598 )599 .map_err(|err| match err {600 DispatchError::Arithmetic(_) => <Error<T>>::NoAvailableNftId.into(),601 err => Self::map_unique_err_to_proxy(err),602 })?;603604 if let Some(resources) = resources {605 for resource in resources {606 Self::resource_add(sender.clone(), collection.id, nft_id, resource)?;607 }608 }609610 Self::deposit_event(Event::NftMinted {611 owner,612 collection_id,613 nft_id: nft_id.0,614 });615616 Ok(())617 }618619 /// Burn an NFT, destroying it and its nested tokens up to the specified limit.620 /// If the burning budget is exceeded, the transaction is reverted.621 ///622 /// This is the way to burn a nested token as well.623 ///624 /// For more information, see [`burn_recursively`](pallet_nonfungible::pallet::Pallet::burn_recursively).625 ///626 /// # Permissions:627 /// * Token owner628 ///629 /// # Arguments:630 /// - `origin`: sender of the transaction631 /// - `collection_id`: RMRK ID of the collection in which the NFT to burn belongs to.632 /// - `nft_id`: ID of the NFT to be destroyed.633 /// - `max_burns`: Maximum number of tokens to burn, assuming nesting. The transaction634 /// is reverted if there are more tokens to burn in the nesting tree than this number.635 /// This is primarily a mechanism of transaction weight control.636 #[pallet::call_index(5)]637 #[pallet::weight(<SelfWeightOf<T>>::burn_nft(*max_burns))]638 pub fn burn_nft(639 origin: OriginFor<T>,640 collection_id: RmrkCollectionId,641 nft_id: RmrkNftId,642 max_burns: u32,643 ) -> DispatchResult {644 let sender = ensure_signed(origin)?;645 let cross_sender = T::CrossAccountId::from_sub(sender.clone());646647 let collection = Self::get_typed_nft_collection(648 Self::unique_collection_id(collection_id)?,649 misc::CollectionType::Regular,650 )?;651 collection.check_is_external()?;652653 Self::destroy_nft(654 cross_sender,655 Self::unique_collection_id(collection_id)?,656 nft_id.into(),657 max_burns,658 <Error<T>>::NoPermission,659 )660 .map_err(|err| Self::map_unique_err_to_proxy(err.error))?;661662 Self::deposit_event(Event::NFTBurned {663 owner: sender,664 nft_id,665 });666667 Ok(())668 }669670 /// Transfer an NFT from an account/NFT A to another account/NFT B.671 /// The token must be transferable. Nesting cannot occur deeper than the [`NESTING_BUDGET`].672 ///673 /// If the target owner is an NFT owned by another account, then the NFT will enter674 /// the pending state and will have to be accepted by the other account.675 ///676 /// # Permissions:677 /// - Token owner678 ///679 /// # Arguments:680 /// - `origin`: sender of the transaction681 /// - `rmrk_collection_id`: RMRK ID of the collection of the NFT to be transferred.682 /// - `rmrk_nft_id`: ID of the NFT to be transferred.683 /// - `new_owner`: New owner of the nft which can be either an account or a NFT.684 #[pallet::call_index(6)]685 #[pallet::weight(<SelfWeightOf<T>>::send())]686 pub fn send(687 origin: OriginFor<T>,688 rmrk_collection_id: RmrkCollectionId,689 rmrk_nft_id: RmrkNftId,690 new_owner: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,691 ) -> DispatchResult {692 let sender = ensure_signed(origin.clone())?;693 let cross_sender = T::CrossAccountId::from_sub(sender.clone());694695 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;696 let nft_id = rmrk_nft_id.into();697698 let collection =699 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;700 collection.check_is_external()?;701702 let token_data =703 <TokenData<T>>::get((collection_id, nft_id)).ok_or(<Error<T>>::NoAvailableNftId)?;704705 let from = token_data.owner;706707 ensure!(708 Self::get_nft_property_decoded(collection_id, nft_id, RmrkProperty::Transferable)?,709 <Error<T>>::NonTransferable710 );711712 ensure!(713 Self::get_nft_property_decoded::<Option<PendingTarget>>(714 collection_id,715 nft_id,716 RmrkProperty::PendingNftAccept717 )?718 .is_none(),719 <Error<T>>::NoPermission720 );721722 let target_owner;723 let approval_required;724725 match new_owner {726 RmrkAccountIdOrCollectionNftTuple::AccountId(ref account_id) => {727 target_owner = T::CrossAccountId::from_sub(account_id.clone());728 approval_required = false;729 }730 RmrkAccountIdOrCollectionNftTuple::CollectionAndNftTuple(731 target_collection_id,732 target_nft_id,733 ) => {734 let target_collection_id = Self::unique_collection_id(target_collection_id)?;735736 let target_nft_budget = budget::Value::new(NESTING_BUDGET);737738 let target_nft_owner = <PalletStructure<T>>::get_checked_topmost_owner(739 target_collection_id,740 target_nft_id.into(),741 Some((collection_id, nft_id)),742 &target_nft_budget,743 )744 .map_err(Self::map_unique_err_to_proxy)?;745746 approval_required = cross_sender != target_nft_owner;747748 if approval_required {749 target_owner = target_nft_owner;750751 <PalletNft<T>>::set_scoped_token_property(752 collection.id,753 nft_id,754 RMRK_SCOPE,755 Self::encode_rmrk_property::<Option<PendingTarget>>(756 PendingNftAccept,757 &Some((target_collection_id, target_nft_id.into())),758 )?,759 )?;760761 Self::insert_pending_child(762 (target_collection_id, target_nft_id.into()),763 (rmrk_collection_id, rmrk_nft_id),764 )?;765 } else {766 target_owner = T::CrossTokenAddressMapping::token_to_address(767 target_collection_id,768 target_nft_id.into(),769 );770 }771 }772 }773774 let src_nft_budget = budget::Value::new(NESTING_BUDGET);775776 <PalletNft<T>>::transfer_from(777 &collection,778 &cross_sender,779 &from,780 &target_owner,781 nft_id,782 &src_nft_budget,783 )784 .map_err(Self::map_unique_err_to_proxy)?;785786 Self::deposit_event(Event::NFTSent {787 sender,788 recipient: new_owner,789 collection_id: rmrk_collection_id,790 nft_id: rmrk_nft_id,791 approval_required,792 });793794 Ok(())795 }796797 /// Accept an NFT sent from another account to self or an owned NFT.798 ///799 /// The NFT in question must be pending, and, thus, be [sent](`Pallet::send`) first.800 ///801 /// # Permissions:802 /// - Token-owner-to-be803 ///804 /// # Arguments:805 /// - `origin`: sender of the transaction806 /// - `rmrk_collection_id`: RMRK collection ID of the NFT to be accepted.807 /// - `rmrk_nft_id`: ID of the NFT to be accepted.808 /// - `new_owner`: Either the sender's account ID or a sender-owned NFT,809 /// whichever the accepted NFT was sent to.810 #[pallet::call_index(7)]811 #[pallet::weight(<SelfWeightOf<T>>::accept_nft())]812 pub fn accept_nft(813 origin: OriginFor<T>,814 rmrk_collection_id: RmrkCollectionId,815 rmrk_nft_id: RmrkNftId,816 new_owner: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,817 ) -> DispatchResult {818 let sender = ensure_signed(origin.clone())?;819 let cross_sender = T::CrossAccountId::from_sub(sender.clone());820821 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;822 let nft_id = rmrk_nft_id.into();823824 let collection =825 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;826 collection.check_is_external()?;827828 let new_cross_owner = match new_owner {829 RmrkAccountIdOrCollectionNftTuple::AccountId(ref account_id) => {830 T::CrossAccountId::from_sub(account_id.clone())831 }832 RmrkAccountIdOrCollectionNftTuple::CollectionAndNftTuple(833 target_collection_id,834 target_nft_id,835 ) => {836 let target_collection_id = Self::unique_collection_id(target_collection_id)?;837838 T::CrossTokenAddressMapping::token_to_address(839 target_collection_id,840 TokenId(target_nft_id),841 )842 }843 };844845 let budget = budget::Value::new(NESTING_BUDGET);846847 <PalletNft<T>>::transfer(848 &collection,849 &cross_sender,850 &new_cross_owner,851 nft_id,852 &budget,853 )854 .map_err(|err| {855 if err == <CommonError<T>>::UserIsNotAllowedToNest.into() {856 <Error<T>>::CannotAcceptNonOwnedNft.into()857 } else {858 Self::map_unique_err_to_proxy(err)859 }860 })?;861862 let pending_target = Self::get_nft_property_decoded::<Option<PendingTarget>>(863 collection_id,864 nft_id,865 RmrkProperty::PendingNftAccept,866 )?;867868 if let Some(pending_target) = pending_target {869 Self::remove_pending_child(pending_target, (rmrk_collection_id, rmrk_nft_id))?;870871 <PalletNft<T>>::set_scoped_token_property(872 collection.id,873 nft_id,874 RMRK_SCOPE,875 Self::encode_rmrk_property(PendingNftAccept, &None::<PendingTarget>)?,876 )?;877 }878879 Self::deposit_event(Event::NFTAccepted {880 sender,881 recipient: new_owner,882 collection_id: rmrk_collection_id,883 nft_id: rmrk_nft_id,884 });885886 Ok(())887 }888889 /// Reject an NFT sent from another account to self or owned NFT.890 /// The NFT in question will not be sent back and burnt instead.891 ///892 /// The NFT in question must be pending, and, thus, be [sent](`Pallet::send`) first.893 ///894 /// # Permissions:895 /// - Token-owner-to-be-not896 ///897 /// # Arguments:898 /// - `origin`: sender of the transaction899 /// - `rmrk_collection_id`: RMRK ID of the NFT to be rejected.900 /// - `rmrk_nft_id`: ID of the NFT to be rejected.901 #[pallet::call_index(8)]902 #[pallet::weight(<SelfWeightOf<T>>::reject_nft())]903 pub fn reject_nft(904 origin: OriginFor<T>,905 rmrk_collection_id: RmrkCollectionId,906 rmrk_nft_id: RmrkNftId,907 ) -> DispatchResult {908 let sender = ensure_signed(origin)?;909 let cross_sender = T::CrossAccountId::from_sub(sender.clone());910911 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;912 let nft_id = rmrk_nft_id.into();913914 let collection =915 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;916 collection.check_is_external()?;917918 ensure!(919 <TokenData<T>>::get((collection_id, nft_id)).is_some(),920 <Error<T>>::NoAvailableNftId921 );922923 let pending_target = Self::get_nft_property_decoded::<Option<PendingTarget>>(924 collection_id,925 nft_id,926 RmrkProperty::PendingNftAccept,927 )?;928929 match pending_target {930 Some(pending_target) => {931 Self::remove_pending_child(pending_target, (rmrk_collection_id, rmrk_nft_id))?932 }933 None => return Err(<Error<T>>::CannotRejectNonPendingNft.into()),934 }935936 Self::destroy_nft(937 cross_sender,938 collection_id,939 nft_id,940 NESTING_BUDGET,941 <Error<T>>::CannotRejectNonOwnedNft,942 )943 .map_err(|err| Self::map_unique_err_to_proxy(err.error))?;944945 Self::deposit_event(Event::NFTRejected {946 sender,947 collection_id: rmrk_collection_id,948 nft_id: rmrk_nft_id,949 });950951 Ok(())952 }953954 /// Accept the addition of a newly created pending resource to an existing NFT.955 ///956 /// This transaction is needed when a resource is created and assigned to an NFT957 /// by a non-owner, i.e. the collection issuer, with one of the958 /// [`add_...` transactions](Pallet::add_basic_resource).959 ///960 /// # Permissions:961 /// - Token owner962 ///963 /// # Arguments:964 /// - `origin`: sender of the transaction965 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.966 /// - `rmrk_nft_id`: ID of the NFT with a pending resource to be accepted.967 /// - `resource_id`: ID of the newly created pending resource.968 /// accept the addition of a new resource to an existing NFT969 #[pallet::call_index(9)]970 #[pallet::weight(<SelfWeightOf<T>>::accept_resource())]971 pub fn accept_resource(972 origin: OriginFor<T>,973 rmrk_collection_id: RmrkCollectionId,974 rmrk_nft_id: RmrkNftId,975 resource_id: RmrkResourceId,976 ) -> DispatchResult {977 let sender = ensure_signed(origin)?;978 let cross_sender = T::CrossAccountId::from_sub(sender);979980 let collection_id = Self::unique_collection_id(rmrk_collection_id)981 .map_err(|_| <Error<T>>::ResourceDoesntExist)?;982 let collection =983 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;984 collection.check_is_external()?;985986 let nft_id = rmrk_nft_id.into();987988 let budget = budget::Value::new(NESTING_BUDGET);989990 let nft_owner =991 <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)992 .map_err(|_| <Error<T>>::ResourceDoesntExist)?;993994 Self::try_mutate_resource_info(collection_id, nft_id, resource_id, |res| {995 ensure!(res.pending, <Error<T>>::ResourceNotPending);996 ensure!(cross_sender == nft_owner, <Error<T>>::NoPermission);997998 res.pending = false;9991000 Ok(())1001 })?;10021003 Self::deposit_event(Event::<T>::ResourceAccepted {1004 nft_id: rmrk_nft_id,1005 resource_id,1006 });10071008 Ok(())1009 }10101011 /// Accept the removal of a removal-pending resource from an NFT.1012 ///1013 /// This transaction is needed when a non-owner, i.e. the collection issuer,1014 /// requests a [removal](`Pallet::remove_resource`) of a resource from an NFT.1015 ///1016 /// # Permissions:1017 /// - Token owner1018 ///1019 /// # Arguments:1020 /// - `origin`: sender of the transaction1021 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1022 /// - `rmrk_nft_id`: ID of the NFT with a resource to be removed.1023 /// - `resource_id`: ID of the removal-pending resource.1024 #[pallet::call_index(10)]1025 #[pallet::weight(<SelfWeightOf<T>>::accept_resource_removal())]1026 pub fn accept_resource_removal(1027 origin: OriginFor<T>,1028 rmrk_collection_id: RmrkCollectionId,1029 rmrk_nft_id: RmrkNftId,1030 resource_id: RmrkResourceId,1031 ) -> DispatchResult {1032 let sender = ensure_signed(origin)?;1033 let cross_sender = T::CrossAccountId::from_sub(sender);10341035 let collection_id = Self::unique_collection_id(rmrk_collection_id)1036 .map_err(|_| <Error<T>>::ResourceDoesntExist)?;1037 let collection =1038 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1039 collection.check_is_external()?;10401041 let nft_id = rmrk_nft_id.into();10421043 let budget = budget::Value::new(NESTING_BUDGET);10441045 let nft_owner =1046 <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)1047 .map_err(|_| <Error<T>>::ResourceDoesntExist)?;10481049 ensure!(cross_sender == nft_owner, <Error<T>>::NoPermission);10501051 let resource_id_key = Self::get_scoped_property_key(ResourceId(resource_id))?;10521053 let resource_info = <PalletNft<T>>::token_aux_property((1054 collection_id,1055 nft_id,1056 RMRK_SCOPE,1057 resource_id_key.clone(),1058 ))1059 .ok_or(<Error<T>>::ResourceDoesntExist)?;10601061 let resource_info: RmrkResourceInfo = Self::decode_property_value(&resource_info)?;10621063 ensure!(1064 resource_info.pending_removal,1065 <Error<T>>::ResourceNotPending1066 );10671068 <PalletNft<T>>::remove_token_aux_property(1069 collection_id,1070 nft_id,1071 RMRK_SCOPE,1072 resource_id_key,1073 );10741075 if let RmrkResourceTypes::Composable(resource) = resource_info.resource {1076 let base_id = resource.base;10771078 Self::remove_associated_base_id(collection_id, nft_id, base_id)?;1079 }10801081 Self::deposit_event(Event::<T>::ResourceRemovalAccepted {1082 nft_id: rmrk_nft_id,1083 resource_id,1084 });10851086 Ok(())1087 }10881089 /// Add or edit a custom user property, a key-value pair, describing the metadata1090 /// of a token or a collection, on either one of these.1091 ///1092 /// Note that in this proxy implementation many details regarding RMRK are stored1093 /// as scoped properties prefixed with "rmrk:", normally inaccessible1094 /// to external transactions and RPCs.1095 ///1096 /// # Permissions:1097 /// - Collection issuer - in case of collection property1098 /// - Token owner - in case of NFT property1099 ///1100 /// # Arguments:1101 /// - `origin`: sender of the transaction1102 /// - `rmrk_collection_id`: RMRK collection ID.1103 /// - `maybe_nft_id`: Optional ID of the NFT. If left empty, then the property is set for the collection.1104 /// - `key`: Key of the custom property to be referenced by.1105 /// - `value`: Value of the custom property to be stored.1106 #[pallet::call_index(11)]1107 #[pallet::weight(<SelfWeightOf<T>>::set_property())]1108 pub fn set_property(1109 origin: OriginFor<T>,1110 #[pallet::compact] rmrk_collection_id: RmrkCollectionId,1111 maybe_nft_id: Option<RmrkNftId>,1112 key: RmrkKeyString,1113 value: RmrkValueString,1114 ) -> DispatchResult {1115 let sender = ensure_signed(origin)?;1116 let sender = T::CrossAccountId::from_sub(sender);11171118 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1119 let collection =1120 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1121 collection.check_is_external()?;11221123 let budget = budget::Value::new(NESTING_BUDGET);11241125 match maybe_nft_id {1126 Some(nft_id) => {1127 let token_id: TokenId = nft_id.into();11281129 Self::ensure_nft_type(collection_id, token_id, NftType::Regular)?;1130 Self::ensure_nft_owner(collection_id, token_id, &sender, &budget)?;11311132 <PalletNft<T>>::set_scoped_token_property(1133 collection_id,1134 token_id,1135 RMRK_SCOPE,1136 Self::encode_rmrk_property(UserProperty(key.as_slice()), &value)?,1137 )?;1138 }1139 None => {1140 let collection = Self::get_typed_nft_collection(1141 collection_id,1142 misc::CollectionType::Regular,1143 )?;11441145 Self::check_collection_owner(&collection, &sender)?;11461147 <PalletCommon<T>>::set_scoped_collection_property(1148 collection_id,1149 RMRK_SCOPE,1150 Self::encode_rmrk_property(UserProperty(key.as_slice()), &value)?,1151 )?;1152 }1153 }11541155 Self::deposit_event(Event::PropertySet {1156 collection_id: rmrk_collection_id,1157 maybe_nft_id,1158 key,1159 value,1160 });11611162 Ok(())1163 }11641165 /// Set a different order of resource priorities for an NFT. Priorities can be used,1166 /// for example, for order of rendering.1167 ///1168 /// Note that the priorities are not updated automatically, and are an empty vector1169 /// by default. There is no pre-set definition for the order to be particular,1170 /// it can be interpreted arbitrarily use-case by use-case.1171 ///1172 /// # Permissions:1173 /// - Token owner1174 ///1175 /// # Arguments:1176 /// - `origin`: sender of the transaction1177 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1178 /// - `rmrk_nft_id`: ID of the NFT to rearrange resource priorities for.1179 /// - `priorities`: Ordered vector of resource IDs.1180 #[pallet::call_index(12)]1181 #[pallet::weight(<SelfWeightOf<T>>::set_priority())]1182 pub fn set_priority(1183 origin: OriginFor<T>,1184 rmrk_collection_id: RmrkCollectionId,1185 rmrk_nft_id: RmrkNftId,1186 priorities: BoundedVec<RmrkResourceId, RmrkMaxPriorities>,1187 ) -> DispatchResult {1188 let sender = ensure_signed(origin)?;1189 let sender = T::CrossAccountId::from_sub(sender);11901191 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1192 let nft_id = rmrk_nft_id.into();11931194 let collection =1195 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1196 collection.check_is_external()?;11971198 let budget = budget::Value::new(NESTING_BUDGET);11991200 Self::ensure_nft_type(collection_id, nft_id, NftType::Regular)?;1201 Self::ensure_nft_owner(collection_id, nft_id, &sender, &budget)?;12021203 <PalletNft<T>>::set_scoped_token_property(1204 collection_id,1205 nft_id,1206 RMRK_SCOPE,1207 Self::encode_rmrk_property(ResourcePriorities, &priorities.into_inner())?,1208 )?;12091210 Self::deposit_event(Event::<T>::PrioritySet {1211 collection_id: rmrk_collection_id,1212 nft_id: rmrk_nft_id,1213 });12141215 Ok(())1216 }12171218 /// Create and set/propose a basic resource for an NFT.1219 ///1220 /// A basic resource is the simplest, lacking a Base and anything that comes with it.1221 /// See RMRK docs for more information and examples.1222 ///1223 /// # Permissions:1224 /// - Collection issuer - if not the token owner, adding the resource will warrant1225 /// the owner's [acceptance](Pallet::accept_resource).1226 ///1227 /// # Arguments:1228 /// - `origin`: sender of the transaction1229 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1230 /// - `nft_id`: ID of the NFT to assign a resource to.1231 /// - `resource`: Data of the resource to be created.1232 #[pallet::call_index(13)]1233 #[pallet::weight(<SelfWeightOf<T>>::add_basic_resource())]1234 pub fn add_basic_resource(1235 origin: OriginFor<T>,1236 rmrk_collection_id: RmrkCollectionId,1237 nft_id: RmrkNftId,1238 resource: RmrkBasicResource,1239 ) -> DispatchResult {1240 let sender = ensure_signed(origin.clone())?;12411242 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1243 let collection =1244 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1245 collection.check_is_external()?;12461247 let resource_id = Self::resource_add(1248 sender,1249 collection_id,1250 nft_id.into(),1251 RmrkResourceTypes::Basic(resource),1252 )?;12531254 Self::deposit_event(Event::ResourceAdded {1255 nft_id,1256 resource_id,1257 });1258 Ok(())1259 }12601261 /// Create and set/propose a composable resource for an NFT.1262 ///1263 /// A composable resource links to a Base and has a subset of its Parts it is composed of.1264 /// See RMRK docs for more information and examples.1265 ///1266 /// # Permissions:1267 /// - Collection issuer - if not the token owner, adding the resource will warrant1268 /// the owner's [acceptance](Pallet::accept_resource).1269 ///1270 /// # Arguments:1271 /// - `origin`: sender of the transaction1272 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1273 /// - `nft_id`: ID of the NFT to assign a resource to.1274 /// - `resource`: Data of the resource to be created.1275 #[pallet::call_index(14)]1276 #[pallet::weight(<SelfWeightOf<T>>::add_composable_resource())]1277 pub fn add_composable_resource(1278 origin: OriginFor<T>,1279 rmrk_collection_id: RmrkCollectionId,1280 nft_id: RmrkNftId,1281 resource: RmrkComposableResource,1282 ) -> DispatchResult {1283 let sender = ensure_signed(origin.clone())?;12841285 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1286 let collection =1287 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1288 collection.check_is_external()?;12891290 let base_id = resource.base;12911292 let resource_id = Self::resource_add(1293 sender,1294 collection_id,1295 nft_id.into(),1296 RmrkResourceTypes::Composable(resource),1297 )?;12981299 <PalletNft<T>>::try_mutate_token_aux_property(1300 collection_id,1301 nft_id.into(),1302 RMRK_SCOPE,1303 Self::get_scoped_property_key(AssociatedBases)?,1304 |value| -> DispatchResult {1305 let mut bases: BasesMap = match value {1306 Some(value) => Self::decode_property_value(value)?,1307 None => BasesMap::new(),1308 };13091310 *bases.entry(base_id).or_insert(0) += 1;13111312 *value = Some(Self::encode_property_value(&bases)?);1313 Ok(())1314 },1315 )?;13161317 Self::deposit_event(Event::ResourceAdded {1318 nft_id,1319 resource_id,1320 });1321 Ok(())1322 }13231324 /// Create and set/propose a slot resource for an NFT.1325 ///1326 /// A slot resource links to a Base and a slot ID in it which it can fit into.1327 /// See RMRK docs for more information and examples.1328 ///1329 /// # Permissions:1330 /// - Collection issuer - if not the token owner, adding the resource will warrant1331 /// the owner's [acceptance](Pallet::accept_resource).1332 ///1333 /// # Arguments:1334 /// - `origin`: sender of the transaction1335 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1336 /// - `nft_id`: ID of the NFT to assign a resource to.1337 /// - `resource`: Data of the resource to be created.1338 #[pallet::call_index(15)]1339 #[pallet::weight(<SelfWeightOf<T>>::add_slot_resource())]1340 pub fn add_slot_resource(1341 origin: OriginFor<T>,1342 rmrk_collection_id: RmrkCollectionId,1343 nft_id: RmrkNftId,1344 resource: RmrkSlotResource,1345 ) -> DispatchResult {1346 let sender = ensure_signed(origin.clone())?;13471348 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1349 let collection =1350 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1351 collection.check_is_external()?;13521353 let resource_id = Self::resource_add(1354 sender,1355 collection_id,1356 nft_id.into(),1357 RmrkResourceTypes::Slot(resource),1358 )?;13591360 Self::deposit_event(Event::ResourceAdded {1361 nft_id,1362 resource_id,1363 });1364 Ok(())1365 }13661367 /// Remove and erase a resource from an NFT.1368 ///1369 /// If the sender does not own the NFT, then it will be pending confirmation,1370 /// and will have to be [accepted](Pallet::accept_resource_removal) by the token owner.1371 ///1372 /// # Permissions1373 /// - Collection issuer1374 ///1375 /// # Arguments1376 /// - `origin`: sender of the transaction1377 /// - `rmrk_collection_id`: RMRK ID of a collection to which the NFT making use of the resource belongs to.1378 /// - `nft_id`: ID of the NFT with a resource to be removed.1379 /// - `resource_id`: ID of the resource to be removed.1380 #[pallet::call_index(16)]1381 #[pallet::weight(<SelfWeightOf<T>>::remove_resource())]1382 pub fn remove_resource(1383 origin: OriginFor<T>,1384 rmrk_collection_id: RmrkCollectionId,1385 nft_id: RmrkNftId,1386 resource_id: RmrkResourceId,1387 ) -> DispatchResult {1388 let sender = ensure_signed(origin.clone())?;13891390 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1391 let collection =1392 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1393 collection.check_is_external()?;13941395 Self::resource_remove(sender, collection_id, nft_id.into(), resource_id)?;13961397 Self::deposit_event(Event::ResourceRemoval {1398 nft_id,1399 resource_id,1400 });1401 Ok(())1402 }1403 }1404}14051406impl<T: Config> Pallet<T> {1407 /// Transform one of possible RMRK keys into a byte key with a RMRK scope.1408 pub fn get_scoped_property_key(rmrk_key: RmrkProperty) -> Result<PropertyKey, DispatchError> {1409 let key = rmrk_key.to_key::<T>()?;14101411 let scoped_key = RMRK_SCOPE1412 .apply(key)1413 .map_err(|_| <Error<T>>::RmrkPropertyKeyIsTooLong)?;14141415 Ok(scoped_key)1416 }14171418 /// Form a Unique property, transforming a RMRK key into bytes (without assigning the scope yet)1419 /// and encoding the value from an arbitrary type into bytes.1420 pub fn encode_rmrk_property<E: Encode>(1421 rmrk_key: RmrkProperty,1422 value: &E,1423 ) -> Result<Property, DispatchError> {1424 let key = rmrk_key.to_key::<T>()?;14251426 let value = Self::encode_property_value(value)?;14271428 let property = Property { key, value };14291430 Ok(property)1431 }14321433 /// Encode property value from an arbitrary type into bytes for storage.1434 pub fn encode_property_value<E: Encode, S: Get<u32>>(1435 value: &E,1436 ) -> Result<BoundedBytes<S>, DispatchError> {1437 let value = value1438 .encode()1439 .try_into()1440 .map_err(|_| <Error<T>>::RmrkPropertyValueIsTooLong)?;14411442 Ok(value)1443 }14441445 /// Decode property value from bytes into an arbitrary type.1446 pub fn decode_property_value<D: Decode, S: Get<u32>>(1447 vec: &BoundedBytes<S>,1448 ) -> Result<D, DispatchError> {1449 vec.decode()1450 .map_err(|_| <Error<T>>::UnableToDecodeRmrkData.into())1451 }14521453 /// Change the limit of a property value byte vector.1454 pub fn rebind<L, S>(vec: &BoundedVec<u8, L>) -> Result<BoundedVec<u8, S>, DispatchError>1455 where1456 BoundedVec<u8, S>: TryFrom<Vec<u8>>,1457 {1458 vec.rebind()1459 .map_err(|_| <Error<T>>::RmrkPropertyValueIsTooLong.into())1460 }14611462 /// Initialize a new NFT collection with certain RMRK-scoped properties.1463 ///1464 /// See [`init_collection`](pallet_nonfungible::pallet::Pallet::init_collection) for more details.1465 fn init_collection(1466 sender: T::CrossAccountId,1467 data: CreateCollectionData<T::AccountId>,1468 properties: impl Iterator<Item = Property>,1469 ) -> Result<CollectionId, DispatchError> {1470 let collection_id = <PalletNft<T>>::init_collection(1471 sender.clone(),1472 sender,1473 data,1474 up_data_structs::CollectionFlags {1475 external: true,1476 ..Default::default()1477 },1478 );14791480 if let Err(DispatchError::Arithmetic(_)) = &collection_id {1481 return Err(<Error<T>>::NoAvailableCollectionId.into());1482 }14831484 <PalletCommon<T>>::set_scoped_collection_properties(1485 collection_id?,1486 RMRK_SCOPE,1487 properties,1488 )?;14891490 collection_id1491 }14921493 /// Mint a new NFT with certain RMRK-scoped properties. Sender must be the collection owner.1494 ///1495 /// See [`create_item`](pallet_nonfungible::pallet::Pallet::create_item) for more details.1496 pub fn create_nft(1497 sender: &T::CrossAccountId,1498 owner: &T::CrossAccountId,1499 collection: &NonfungibleHandle<T>,1500 properties: impl Iterator<Item = Property>,1501 ) -> Result<TokenId, DispatchError> {1502 let data = CreateNftExData {1503 properties: BoundedVec::default(),1504 owner: owner.clone(),1505 };15061507 let budget = budget::Value::new(NESTING_BUDGET);15081509 <PalletNft<T>>::create_item(collection, sender, data, &budget)?;15101511 let nft_id = <PalletNft<T>>::current_token_id(collection.id);15121513 <PalletNft<T>>::set_scoped_token_properties(collection.id, nft_id, RMRK_SCOPE, properties)?;15141515 Ok(nft_id)1516 }15171518 /// Burn an NFT, along with its nested children, limited by `max_burns`. The sender must be the token owner.1519 ///1520 /// See [`burn_recursively`](pallet_nonfungible::pallet::Pallet::burn_recursively) for more details.1521 fn destroy_nft(1522 sender: T::CrossAccountId,1523 collection_id: CollectionId,1524 token_id: TokenId,1525 max_burns: u32,1526 error_if_not_owned: Error<T>,1527 ) -> DispatchResultWithPostInfo {1528 let collection =1529 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;15301531 let token_data =1532 <TokenData<T>>::get((collection_id, token_id)).ok_or(<Error<T>>::NoAvailableNftId)?;15331534 let from = token_data.owner;15351536 let owner_check_budget = budget::Value::new(NESTING_BUDGET);15371538 ensure!(1539 <PalletStructure<T>>::check_indirectly_owned(1540 sender.clone(),1541 collection_id,1542 token_id,1543 None,1544 &owner_check_budget1545 )?,1546 error_if_not_owned,1547 );15481549 let burns_budget = budget::Value::new(max_burns);1550 let breadth_budget = budget::Value::new(max_burns);15511552 <PalletNft<T>>::burn_recursively(1553 &collection,1554 &from,1555 token_id,1556 &burns_budget,1557 &breadth_budget,1558 )1559 }15601561 /// Add a sent token pending acceptance to the target owning token as a property.1562 fn insert_pending_child(1563 target: (CollectionId, TokenId),1564 child: (RmrkCollectionId, RmrkNftId),1565 ) -> DispatchResult {1566 Self::mutate_pending_children(target, |pending_children| {1567 pending_children.insert(child);1568 })1569 }15701571 /// Remove a sent token pending acceptance from the target token's properties.1572 fn remove_pending_child(1573 target: (CollectionId, TokenId),1574 child: (RmrkCollectionId, RmrkNftId),1575 ) -> DispatchResult {1576 Self::mutate_pending_children(target, |pending_children| {1577 pending_children.remove(&child);1578 })1579 }15801581 /// Apply a mutation to the property of a token containing sent tokens1582 /// that are currently pending acceptance.1583 fn mutate_pending_children(1584 (target_collection_id, target_nft_id): (CollectionId, TokenId),1585 f: impl FnOnce(&mut PendingChildrenSet),1586 ) -> DispatchResult {1587 <PalletNft<T>>::try_mutate_token_aux_property(1588 target_collection_id,1589 target_nft_id,1590 RMRK_SCOPE,1591 Self::get_scoped_property_key(PendingChildren)?,1592 |pending_children| -> DispatchResult {1593 let mut map = match pending_children {1594 Some(map) => Self::decode_property_value(map)?,1595 None => PendingChildrenSet::new(),1596 };15971598 f(&mut map);15991600 *pending_children = Some(Self::encode_property_value(&map)?);16011602 Ok(())1603 },1604 )1605 }16061607 /// Get an iterator from a token's property containing tokens sent to it1608 /// that are currently pending acceptance.1609 fn iterate_pending_children(1610 collection_id: CollectionId,1611 nft_id: TokenId,1612 ) -> Result<impl Iterator<Item = PendingChild>, DispatchError> {1613 let property = <PalletNft<T>>::token_aux_property((1614 collection_id,1615 nft_id,1616 RMRK_SCOPE,1617 Self::get_scoped_property_key(PendingChildren)?,1618 ));16191620 let pending_children = match property {1621 Some(map) => Self::decode_property_value(&map)?,1622 None => PendingChildrenSet::new(),1623 };16241625 Ok(pending_children.into_iter())1626 }16271628 /// Get incremented resource ID from within an NFT's properties and store the new latest ID.1629 /// Thus, the returned resource ID should be used.1630 ///1631 /// Resource IDs are unique only across an NFT.1632 fn acquire_next_resource_id(1633 collection_id: CollectionId,1634 nft_id: TokenId,1635 ) -> Result<RmrkResourceId, DispatchError> {1636 let resource_id: RmrkResourceId =1637 Self::get_nft_property_decoded(collection_id, nft_id, NextResourceId)?;16381639 let next_id = resource_id1640 .checked_add(1)1641 .ok_or(<Error<T>>::NoAvailableResourceId)?;16421643 <PalletNft<T>>::set_scoped_token_property(1644 collection_id,1645 nft_id,1646 RMRK_SCOPE,1647 Self::encode_rmrk_property(NextResourceId, &next_id)?,1648 )?;16491650 Ok(resource_id)1651 }16521653 /// Create and add a resource for a regular NFT, mark it as pending if the sender1654 /// is not the token owner. The sender must be the collection owner.1655 fn resource_add(1656 sender: T::AccountId,1657 collection_id: CollectionId,1658 nft_id: TokenId,1659 resource: RmrkResourceTypes,1660 ) -> Result<RmrkResourceId, DispatchError> {1661 let collection =1662 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1663 ensure!(collection.owner == sender, Error::<T>::NoPermission);16641665 let sender = T::CrossAccountId::from_sub(sender);1666 let budget = budget::Value::new(NESTING_BUDGET);16671668 let nft_owner = <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)1669 .map_err(Self::map_unique_err_to_proxy)?;16701671 let pending = sender != nft_owner;16721673 let id = Self::acquire_next_resource_id(collection_id, nft_id)?;16741675 let resource_info = RmrkResourceInfo {1676 id,1677 resource,1678 pending,1679 pending_removal: false,1680 };16811682 <PalletNft<T>>::try_mutate_token_aux_property(1683 collection_id,1684 nft_id,1685 RMRK_SCOPE,1686 Self::get_scoped_property_key(ResourceId(id))?,1687 |value| -> DispatchResult {1688 *value = Some(Self::encode_property_value(&resource_info)?);16891690 Ok(())1691 },1692 )?;16931694 Ok(id)1695 }16961697 /// Designate a resource for erasure from an NFT, and remove it if the sender is the token owner.1698 /// The sender must be the collection owner.1699 fn resource_remove(1700 sender: T::AccountId,1701 collection_id: CollectionId,1702 nft_id: TokenId,1703 resource_id: RmrkResourceId,1704 ) -> DispatchResult {1705 let collection =1706 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1707 ensure!(collection.owner == sender, Error::<T>::NoPermission);17081709 let resource_id_key = Self::get_scoped_property_key(ResourceId(resource_id))?;17101711 let resource = <PalletNft<T>>::token_aux_property((1712 collection_id,1713 nft_id,1714 RMRK_SCOPE,1715 resource_id_key.clone(),1716 ))1717 .ok_or(<Error<T>>::ResourceDoesntExist)?;17181719 let resource_info: RmrkResourceInfo = Self::decode_property_value(&resource)?;17201721 let budget = up_data_structs::budget::Value::new(NESTING_BUDGET);1722 let topmost_owner =1723 <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)?;17241725 let sender = T::CrossAccountId::from_sub(sender);1726 if topmost_owner == sender {1727 <PalletNft<T>>::remove_token_aux_property(1728 collection_id,1729 nft_id,1730 RMRK_SCOPE,1731 Self::get_scoped_property_key(ResourceId(resource_id))?,1732 );17331734 if let RmrkResourceTypes::Composable(resource) = resource_info.resource {1735 let base_id = resource.base;17361737 Self::remove_associated_base_id(collection_id, nft_id, base_id)?;1738 }1739 } else {1740 Self::try_mutate_resource_info(collection_id, nft_id, resource_id, |res| {1741 res.pending_removal = true;17421743 Ok(())1744 })?;1745 }17461747 Ok(())1748 }17491750 /// Remove a Base ID from an NFT if they are associated.1751 /// The Base itself is deleted if the number of associated NFTs reaches 0.1752 fn remove_associated_base_id(1753 collection_id: CollectionId,1754 nft_id: TokenId,1755 base_id: RmrkBaseId,1756 ) -> DispatchResult {1757 <PalletNft<T>>::try_mutate_token_aux_property(1758 collection_id,1759 nft_id,1760 RMRK_SCOPE,1761 Self::get_scoped_property_key(AssociatedBases)?,1762 |value| -> DispatchResult {1763 let mut bases: BasesMap = match value {1764 Some(value) => Self::decode_property_value(value)?,1765 None => BasesMap::new(),1766 };17671768 let remaining = bases.get(&base_id);17691770 if let Some(remaining) = remaining {1771 if let Some(0) | None = remaining.checked_sub(1) {1772 bases.remove(&base_id);1773 }1774 }17751776 *value = Some(Self::encode_property_value(&bases)?);1777 Ok(())1778 },1779 )1780 }17811782 /// Apply a mutation to a resource stored in the token properties of an NFT.1783 fn try_mutate_resource_info(1784 collection_id: CollectionId,1785 nft_id: TokenId,1786 resource_id: RmrkResourceId,1787 f: impl FnOnce(&mut RmrkResourceInfo) -> DispatchResult,1788 ) -> DispatchResult {1789 <PalletNft<T>>::try_mutate_token_aux_property(1790 collection_id,1791 nft_id,1792 RMRK_SCOPE,1793 Self::get_scoped_property_key(ResourceId(resource_id))?,1794 |value| match value {1795 Some(value) => {1796 let mut resource_info: RmrkResourceInfo = Self::decode_property_value(value)?;17971798 f(&mut resource_info)?;17991800 *value = Self::encode_property_value(&resource_info)?;18011802 Ok(())1803 }1804 None => Err(<Error<T>>::ResourceDoesntExist.into()),1805 },1806 )1807 }18081809 /// Change the owner of an NFT collection, ensuring that the sender is the current owner.1810 fn change_collection_owner(1811 collection_id: CollectionId,1812 collection_type: misc::CollectionType,1813 sender: T::AccountId,1814 new_owner: T::AccountId,1815 ) -> DispatchResult {1816 let collection = Self::get_typed_nft_collection(collection_id, collection_type)?;1817 Self::check_collection_owner(&collection, &T::CrossAccountId::from_sub(sender))?;18181819 let mut collection = collection.into_inner();18201821 collection.owner = new_owner;1822 collection.save()1823 }18241825 /// Ensure that an account is the collection owner/issuer, return an error if not.1826 pub fn check_collection_owner(1827 collection: &NonfungibleHandle<T>,1828 account: &T::CrossAccountId,1829 ) -> DispatchResult {1830 collection1831 .check_is_owner(account)1832 .map_err(Self::map_unique_err_to_proxy)1833 }18341835 /// Get the latest yet-unused RMRK collection index from the storage.1836 pub fn last_collection_idx() -> RmrkCollectionId {1837 <CollectionIndex<T>>::get()1838 }18391840 /// Get a mapping from a RMRK collection ID to its corresponding Unique collection ID.1841 pub fn unique_collection_id(1842 rmrk_collection_id: RmrkCollectionId,1843 ) -> Result<CollectionId, DispatchError> {1844 <UniqueCollectionId<T>>::try_get(rmrk_collection_id)1845 .map_err(|_| <Error<T>>::CollectionUnknown.into())1846 }18471848 /// Get a mapping from a Unique collection ID to its RMRK collection ID counterpart, if it exists.1849 pub fn rmrk_collection_id(1850 unique_collection_id: CollectionId,1851 ) -> Result<RmrkCollectionId, DispatchError> {1852 Self::get_collection_property_decoded(unique_collection_id, RmrkInternalCollectionId)1853 }18541855 /// Fetch a Unique NFT collection.1856 pub fn get_nft_collection(1857 collection_id: CollectionId,1858 ) -> Result<NonfungibleHandle<T>, DispatchError> {1859 let collection = <CollectionHandle<T>>::try_get(collection_id)1860 .map_err(|_| <Error<T>>::CollectionUnknown)?;18611862 match collection.mode {1863 CollectionMode::NFT => Ok(NonfungibleHandle::cast(collection)),1864 _ => Err(<Error<T>>::CollectionUnknown.into()),1865 }1866 }18671868 /// Check if an NFT collection with such an ID exists.1869 pub fn collection_exists(collection_id: CollectionId) -> bool {1870 <CollectionHandle<T>>::try_get(collection_id).is_ok()1871 }18721873 /// Fetch and decode a RMRK-scoped collection property value in bytes.1874 pub fn get_collection_property(1875 collection_id: CollectionId,1876 key: RmrkProperty,1877 ) -> Result<PropertyValue, DispatchError> {1878 let collection_property = <PalletCommon<T>>::collection_properties(collection_id)1879 .get(&Self::get_scoped_property_key(key)?)1880 .ok_or(<Error<T>>::CollectionUnknown)?1881 .clone();18821883 Ok(collection_property)1884 }18851886 /// Fetch a RMRK-scoped collection property and decode it from bytes into an appropriate type.1887 pub fn get_collection_property_decoded<V: Decode>(1888 collection_id: CollectionId,1889 key: RmrkProperty,1890 ) -> Result<V, DispatchError> {1891 Self::decode_property_value(&Self::get_collection_property(collection_id, key)?)1892 }18931894 /// Get the type of a collection stored as a scoped property.1895 ///1896 /// RMRK Core proxy differentiates between regular collections as well as RMRK Bases as collections.1897 pub fn get_collection_type(1898 collection_id: CollectionId,1899 ) -> Result<misc::CollectionType, DispatchError> {1900 Self::get_collection_property_decoded(collection_id, CollectionType).map_err(|err| {1901 if err != <Error<T>>::CollectionUnknown.into() {1902 <Error<T>>::CorruptedCollectionType.into()1903 } else {1904 err1905 }1906 })1907 }19081909 /// Ensure that the type of the collection equals the provided type,1910 /// otherwise return an error.1911 pub fn ensure_collection_type(1912 collection_id: CollectionId,1913 collection_type: misc::CollectionType,1914 ) -> DispatchResult {1915 let actual_type = Self::get_collection_type(collection_id)?;1916 ensure!(1917 actual_type == collection_type,1918 <CommonError<T>>::NoPermission1919 );19201921 Ok(())1922 }19231924 /// Fetch an NFT collection, but make sure it has the appropriate type.1925 pub fn get_typed_nft_collection(1926 collection_id: CollectionId,1927 collection_type: misc::CollectionType,1928 ) -> Result<NonfungibleHandle<T>, DispatchError> {1929 Self::ensure_collection_type(collection_id, collection_type)?;19301931 Self::get_nft_collection(collection_id)1932 }19331934 /// Same as [`get_typed_nft_collection`](crate::pallet::Pallet::get_typed_nft_collection),1935 /// but also return the Unique collection ID.1936 pub fn get_typed_nft_collection_mapped(1937 rmrk_collection_id: RmrkCollectionId,1938 collection_type: misc::CollectionType,1939 ) -> Result<(NonfungibleHandle<T>, CollectionId), DispatchError> {1940 let unique_collection_id = match collection_type {1941 misc::CollectionType::Regular => Self::unique_collection_id(rmrk_collection_id)?,1942 _ => rmrk_collection_id.into(),1943 };19441945 let collection = Self::get_typed_nft_collection(unique_collection_id, collection_type)?;19461947 Ok((collection, unique_collection_id))1948 }19491950 /// Fetch and decode a RMRK-scoped NFT property value in bytes.1951 pub fn get_nft_property(1952 collection_id: CollectionId,1953 nft_id: TokenId,1954 key: RmrkProperty,1955 ) -> Result<PropertyValue, DispatchError> {1956 let nft_property = <PalletNft<T>>::token_properties((collection_id, nft_id))1957 .get(&Self::get_scoped_property_key(key)?)1958 .ok_or(<Error<T>>::RmrkPropertyIsNotFound)?1959 .clone();19601961 Ok(nft_property)1962 }19631964 /// Fetch a RMRK-scoped NFT property and decode it from bytes into an appropriate type.1965 pub fn get_nft_property_decoded<V: Decode>(1966 collection_id: CollectionId,1967 nft_id: TokenId,1968 key: RmrkProperty,1969 ) -> Result<V, DispatchError> {1970 Self::decode_property_value(&Self::get_nft_property(collection_id, nft_id, key)?)1971 }19721973 /// Check that an NFT exists.1974 pub fn nft_exists(collection_id: CollectionId, nft_id: TokenId) -> bool {1975 <TokenData<T>>::contains_key((collection_id, nft_id))1976 }19771978 /// Get the type of an NFT stored as a scoped property.1979 ///1980 /// RMRK Core proxy differentiates between regular NFTs, and RMRK Parts and Themes.1981 pub fn get_nft_type(1982 collection_id: CollectionId,1983 token_id: TokenId,1984 ) -> Result<NftType, DispatchError> {1985 Self::get_nft_property_decoded(collection_id, token_id, TokenType)1986 .map_err(|_| <Error<T>>::NoAvailableNftId.into())1987 }19881989 /// Ensure that the type of the NFT equals the provided type, otherwise return an error.1990 pub fn ensure_nft_type(1991 collection_id: CollectionId,1992 token_id: TokenId,1993 nft_type: NftType,1994 ) -> DispatchResult {1995 let actual_type = Self::get_nft_type(collection_id, token_id)?;1996 ensure!(actual_type == nft_type, <Error<T>>::NoPermission);19971998 Ok(())1999 }20002001 /// Ensure that an account is the owner of the token, either directly2002 /// or at the top of the nesting hierarchy; return an error if it is not.2003 pub fn ensure_nft_owner(2004 collection_id: CollectionId,2005 token_id: TokenId,2006 possible_owner: &T::CrossAccountId,2007 nesting_budget: &dyn budget::Budget,2008 ) -> DispatchResult {2009 let is_owned = <PalletStructure<T>>::check_indirectly_owned(2010 possible_owner.clone(),2011 collection_id,2012 token_id,2013 None,2014 nesting_budget,2015 )2016 .map_err(Self::map_unique_err_to_proxy)?;20172018 ensure!(is_owned, <Error<T>>::NoPermission);20192020 Ok(())2021 }20222023 /// Fetch non-scoped properties of a collection or a token that match the filter keys supplied,2024 /// or, if None are provided, return all non-scoped properties.2025 pub fn filter_user_properties<Key, Value, R, Mapper>(2026 collection_id: CollectionId,2027 token_id: Option<TokenId>,2028 filter_keys: Option<Vec<RmrkPropertyKey>>,2029 mapper: Mapper,2030 ) -> Result<Vec<R>, DispatchError>2031 where2032 Key: TryFrom<RmrkPropertyKey> + AsRef<[u8]>,2033 Value: Decode + Default,2034 Mapper: Fn(Key, Value) -> R,2035 {2036 filter_keys2037 .map(|keys| {2038 let properties = keys2039 .into_iter()2040 .filter_map(|key| {2041 let key: Key = key.try_into().ok()?;20422043 let value = match token_id {2044 Some(token_id) => Self::get_nft_property_decoded(2045 collection_id,2046 token_id,2047 UserProperty(key.as_ref()),2048 ),2049 None => Self::get_collection_property_decoded(2050 collection_id,2051 UserProperty(key.as_ref()),2052 ),2053 }2054 .ok()?;20552056 Some(mapper(key, value))2057 })2058 .collect();20592060 Ok(properties)2061 })2062 .unwrap_or_else(|| {2063 let properties =2064 Self::iterate_user_properties(collection_id, token_id, mapper)?.collect();20652066 Ok(properties)2067 })2068 }20692070 /// Get all non-scoped properties from a collection or a token, and apply some transformation,2071 /// supplied by `mapper`, to each key-value pair.2072 pub fn iterate_user_properties<Key, Value, R, Mapper>(2073 collection_id: CollectionId,2074 token_id: Option<TokenId>,2075 mapper: Mapper,2076 ) -> Result<impl Iterator<Item = R>, DispatchError>2077 where2078 Key: TryFrom<RmrkPropertyKey> + AsRef<[u8]>,2079 Value: Decode + Default,2080 Mapper: Fn(Key, Value) -> R,2081 {2082 let properties = match token_id {2083 Some(token_id) => <PalletNft<T>>::token_properties((collection_id, token_id)),2084 None => <PalletCommon<T>>::collection_properties(collection_id),2085 };20862087 let properties = properties.into_iter().filter_map(move |(key, value)| {2088 let key = strip_key_prefix(&key, USER_PROPERTY_PREFIX)?;20892090 let key: Key = key.to_vec().try_into().ok()?;2091 let value: Value = value.decode().ok()?;20922093 Some(mapper(key, value))2094 });20952096 Ok(properties)2097 }20982099 /// Match Unique errors to RMRK's own and return the RMRK error if a match is successful.2100 fn map_unique_err_to_proxy(err: DispatchError) -> DispatchError {2101 map_unique_err_to_proxy! {2102 match err {2103 CommonError::NoPermission => NoPermission,2104 CommonError::CollectionTokenLimitExceeded => CollectionFullOrLocked,2105 CommonError::PublicMintingNotAllowed => NoPermission,2106 CommonError::TokenNotFound => NoAvailableNftId,2107 CommonError::ApprovedValueTooLow => NoPermission,2108 CommonError::CantDestroyNotEmptyCollection => CollectionNotEmpty,2109 StructureError::TokenNotFound => NoAvailableNftId,2110 StructureError::OuroborosDetected => CannotSendToDescendentOrSelf,2111 }2112 }2113 }2114}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # RMRK Core Proxy Pallet18//!19//! A pallet used as proxy for RMRK Core (<https://rmrk-team.github.io/rmrk-substrate/#/pallets/rmrk-core>).20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The RMRK Core Proxy pallet mirrors the functionality of RMRK Core,28//! binding its externalities to Unique's own underlying structure.29//! It is purposed to mimic RMRK Core exactly, allowing seamless integrations30//! of solutions based on RMRK.31//!32//! RMRK Core itself contains essential functionality for RMRK's nested and33//! multi-resourced NFTs.34//!35//! *Note*, that while RMRK itself is subject to active development and restructuring,36//! the proxy may be caught temporarily out of date.37//!38//! ### What is RMRK?39//!40//! RMRK is a set of NFT standards which compose several "NFT 2.0 lego" primitives.41//! Putting these legos together allows a user to create NFT systems of arbitrary complexity.42//!43//! Meaning, RMRK NFTs are dynamic, able to nest into each other and form a hierarchy,44//! make use of specific changeable and partially shared metadata in the form of resources,45//! and more.46//!47//! Visit RMRK documentation and repositories to learn more:48//! - Docs: <https://docs.rmrk.app/getting-started/>49//! - FAQ: <https://coda.io/@rmrk/faq>50//! - Substrate code repository: <https://github.com/rmrk-team/rmrk-substrate>51//! - RMRK specification repository: <https://github.com/rmrk-team/rmrk-spec>52//!53//! ## Terminology54//!55//! For more information on RMRK, see RMRK's own documentation.56//!57//! ### Intro to RMRK58//!59//! - **Resource:** Additional piece of metadata of an NFT usually serving to add60//! a piece of media on top of the root metadata (NFT's own), be it a different wing61//! on the root template bird or something entirely unrelated.62//!63//! - **Base:** A list of possible "components" - Parts, a combination of which can64//! be appended/equipped to/on an NFT.65//!66//! - **Part:** Something that, together with other Parts, can constitute an NFT.67//! Parts are defined in the Base to which they belong. Parts can be either68//! of the `slot` type or `fixed` type. Slots are intended for equippables.69//! Note that "part of something" and "Part of a Base" can be easily confused,70//! and so in this documentation these words are distinguished by the capital letter.71//!72//! - **Theme:** Named objects of variable => value pairs which get interpolated into73//! the Base's `themable` Parts. Themes can hold any value, but are often represented74//! in RMRK's examples as colors applied to visible Parts.75//!76//! ### Peculiarities in Unique77//!78//! - **Scoped properties:** Properties that are normally obscured from users.79//! Their purpose is to contain structured metadata that was not included in the Unique standard80//! for collections and tokens, meant to be operated on by proxies and other outliers.81//! Scoped property keys are prefixed with `some-scope:`, where `some-scope` is82//! an arbitrary keyword, like "rmrk". `:` is considered an unacceptable symbol in user-defined83//! properties, which, along with other safeguards, makes scoped ones impossible to tamper with.84//!85//! - **Auxiliary properties:** A slightly different structure of properties,86//! trading universality of use for more convenient storage, writes and access.87//! Meant to be inaccessible to end users.88//!89//! ## Proxy Implementation90//!91//! An external user is supposed to be able to utilize this proxy as they would92//! utilize RMRK, and get exactly the same results. Normally, Unique transactions93//! are off-limits to RMRK collections and tokens, and vice versa. However,94//! the information stored on chain can be freely interpreted by storage reads and Unique RPCs.95//!96//! ### ID Mapping97//!98//! RMRK's collections' IDs are counted independently of Unique's and start at 0.99//! Note that tokens' IDs still start at 1.100//! The collections themselves, as well as tokens, are stored as Unique collections,101//! and thus RMRK IDs are mapped to Unique IDs (but not vice versa).102//!103//! ### External/Internal Collection Insulation104//!105//! A Unique transaction cannot target collections purposed for RMRK,106//! and they are flagged as `external` to specify that. On the other hand,107//! due to the mapping, RMRK transactions and RPCs simply cannot reach Unique collections.108//!109//! ### Native Properties110//!111//! Many of RMRK's native parameters are stored as scoped properties of a collection112//! or an NFT on the chain. Scoped properties are prefixed with `rmrk:`, where `:`113//! is an unacceptable symbol in user-defined properties, which, along with other safeguards,114//! makes them impossible to tamper with.115//!116//! ### Collection and NFT Types, or Base, Parts and Themes Handling117//!118//! RMRK introduces the concept of a Base, which is a catalogue of Parts,119//! possible components of an NFT. Due to its similarity with the functionality120//! of a token collection, a Base is stored and handled as one, and the Base's Parts and Themes121//! are this collection's NFTs. See [`CollectionType`] and [`NftType`].122//!123//! ## Interface124//!125//! ### Dispatchables126//!127//! - `create_collection` - Create a new collection of NFTs.128//! - `destroy_collection` - Destroy a collection.129//! - `change_collection_issuer` - Change the issuer of a collection.130//! Analogous to Unique's collection's [`owner`](up_data_structs::Collection).131//! - `lock_collection` - "Lock" the collection and prevent new token creation. **Cannot be undone.**132//! - `mint_nft` - Mint an NFT in a specified collection.133//! - `burn_nft` - Burn an NFT, destroying it and its nested tokens.134//! - `send` - Transfer an NFT from an account/NFT A to another account/NFT B.135//! - `accept_nft` - Accept an NFT sent from another account to self or an owned NFT.136//! - `reject_nft` - Reject an NFT sent from another account to self or owned NFT and **burn it**.137//! - `accept_resource` - Accept the addition of a newly created pending resource to an existing NFT.138//! - `accept_resource_removal` - Accept the removal of a removal-pending resource from an NFT.139//! - `set_property` - Add or edit a custom user property of a token or a collection.140//! - `set_priority` - Set a different order of resource priorities for an NFT.141//! - `add_basic_resource` - Create and set/propose a basic resource for an NFT.142//! - `add_composable_resource` - Create and set/propose a composable resource for an NFT.143//! - `add_slot_resource` - Create and set/propose a slot resource for an NFT.144//! - `remove_resource` - Remove and erase a resource from an NFT.145146#![cfg_attr(not(feature = "std"), no_std)]147148use frame_support::{pallet_prelude::*, BoundedVec, dispatch::DispatchResult};149use frame_system::{pallet_prelude::*, ensure_signed};150use sp_runtime::{DispatchError, Permill, traits::StaticLookup};151use sp_std::{152 vec::Vec,153 collections::{btree_set::BTreeSet, btree_map::BTreeMap},154};155use up_data_structs::{*, mapping::TokenAddressMapping};156use pallet_common::{157 Pallet as PalletCommon, Error as CommonError, CollectionHandle, CommonCollectionOperations,158};159use pallet_nonfungible::{Pallet as PalletNft, NonfungibleHandle, TokenData};160use pallet_structure::{Pallet as PalletStructure, Error as StructureError};161use pallet_evm::account::CrossAccountId;162use core::convert::AsRef;163164pub use pallet::*;165166#[cfg(feature = "runtime-benchmarks")]167pub mod benchmarking;168pub mod misc;169pub mod property;170pub mod rpc;171pub mod weights;172173pub type SelfWeightOf<T> = <T as Config>::WeightInfo;174175use weights::WeightInfo;176use misc::*;177pub use property::*;178179use RmrkProperty::*;180181/// A maximum number of levels of depth in the token nesting tree.182pub const NESTING_BUDGET: u32 = 5;183184type PendingTarget = (CollectionId, TokenId);185type PendingChild = (RmrkCollectionId, RmrkNftId);186type PendingChildrenSet = BTreeSet<PendingChild>;187188type BasesMap = BTreeMap<RmrkBaseId, u32>;189190#[frame_support::pallet]191pub mod pallet {192 use super::*;193194 #[pallet::config]195 pub trait Config:196 frame_system::Config197 + pallet_common::Config198 + pallet_nonfungible::Config199 + pallet_evm::Config200 {201 /// Overarching event type.202 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;203204 /// The weight information of this pallet.205 type WeightInfo: WeightInfo;206 }207208 /// Latest yet-unused collection ID.209 #[pallet::storage]210 #[pallet::getter(fn collection_index)]211 pub type CollectionIndex<T: Config> = StorageValue<_, RmrkCollectionId, ValueQuery>;212213 /// Mapping from RMRK collection ID to Unique's.214 #[pallet::storage]215 pub type UniqueCollectionId<T: Config> =216 StorageMap<_, Twox64Concat, RmrkCollectionId, CollectionId, ValueQuery>;217218 #[pallet::pallet]219 #[pallet::generate_store(pub(super) trait Store)]220 pub struct Pallet<T>(_);221222 #[pallet::event]223 #[pallet::generate_deposit(pub(super) fn deposit_event)]224 pub enum Event<T: Config> {225 CollectionCreated {226 issuer: T::AccountId,227 collection_id: RmrkCollectionId,228 },229 CollectionDestroyed {230 issuer: T::AccountId,231 collection_id: RmrkCollectionId,232 },233 IssuerChanged {234 old_issuer: T::AccountId,235 new_issuer: T::AccountId,236 collection_id: RmrkCollectionId,237 },238 CollectionLocked {239 issuer: T::AccountId,240 collection_id: RmrkCollectionId,241 },242 NftMinted {243 owner: T::AccountId,244 collection_id: RmrkCollectionId,245 nft_id: RmrkNftId,246 },247 NFTBurned {248 owner: T::AccountId,249 nft_id: RmrkNftId,250 },251 NFTSent {252 sender: T::AccountId,253 recipient: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,254 collection_id: RmrkCollectionId,255 nft_id: RmrkNftId,256 approval_required: bool,257 },258 NFTAccepted {259 sender: T::AccountId,260 recipient: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,261 collection_id: RmrkCollectionId,262 nft_id: RmrkNftId,263 },264 NFTRejected {265 sender: T::AccountId,266 collection_id: RmrkCollectionId,267 nft_id: RmrkNftId,268 },269 PropertySet {270 collection_id: RmrkCollectionId,271 maybe_nft_id: Option<RmrkNftId>,272 key: RmrkKeyString,273 value: RmrkValueString,274 },275 ResourceAdded {276 nft_id: RmrkNftId,277 resource_id: RmrkResourceId,278 },279 ResourceRemoval {280 nft_id: RmrkNftId,281 resource_id: RmrkResourceId,282 },283 ResourceAccepted {284 nft_id: RmrkNftId,285 resource_id: RmrkResourceId,286 },287 ResourceRemovalAccepted {288 nft_id: RmrkNftId,289 resource_id: RmrkResourceId,290 },291 PrioritySet {292 collection_id: RmrkCollectionId,293 nft_id: RmrkNftId,294 },295 }296297 #[pallet::error]298 pub enum Error<T> {299 /* Unique proxy-specific events */300 /// Property of the type of RMRK collection could not be read successfully.301 CorruptedCollectionType,302 // NftTypeEncodeError,303 /// Too many symbols supplied as the property key. The maximum is [256](up_data_structs::MAX_PROPERTY_KEY_LENGTH).304 RmrkPropertyKeyIsTooLong,305 /// Too many bytes supplied as the property value. The maximum is [32768](up_data_structs::MAX_PROPERTY_VALUE_LENGTH).306 RmrkPropertyValueIsTooLong,307 /// Could not find a property by the supplied key.308 RmrkPropertyIsNotFound,309 /// Something went wrong when decoding encoded data from the storage.310 /// Perhaps, there was a wrong key supplied for the type, or the data was improperly stored.311 UnableToDecodeRmrkData,312313 /* RMRK compatible events */314 /// Only destroying collections without tokens is allowed.315 CollectionNotEmpty,316 /// Could not find an ID for a collection. It is likely there were too many collections created on the chain, causing an overflow.317 NoAvailableCollectionId,318 /// Token does not exist, or there is no suitable ID for it, likely too many tokens were created in a collection, causing an overflow.319 NoAvailableNftId,320 /// Collection does not exist, has a wrong type, or does not map to a Unique ID.321 CollectionUnknown,322 /// No permission to perform action.323 NoPermission,324 /// Token is marked as non-transferable, and thus cannot be transferred.325 NonTransferable,326 /// Too many tokens created in the collection, no new ones are allowed.327 CollectionFullOrLocked,328 /// No such resource found.329 ResourceDoesntExist,330 /// If an NFT is sent to a descendant, that would form a nesting loop, an ouroboros.331 /// Sending to self is redundant.332 CannotSendToDescendentOrSelf,333 /// Not the target owner of the sent NFT.334 CannotAcceptNonOwnedNft,335 /// Not the target owner of the sent NFT.336 CannotRejectNonOwnedNft,337 /// NFT was not sent and is not pending.338 CannotRejectNonPendingNft,339 /// Resource is not pending for the operation.340 ResourceNotPending,341 /// Could not find an ID for the resource. It is likely there were too many resources created on an NFT, causing an overflow.342 NoAvailableResourceId,343 }344345 #[pallet::call]346 impl<T: Config> Pallet<T> {347 // todo :refactor replace every collection_id with rmrk_collection_id (and nft_id) in arguments for uniformity?348349 /// Create a new collection of NFTs.350 ///351 /// # Permissions:352 /// * Anyone - will be assigned as the issuer of the collection.353 ///354 /// # Arguments:355 /// - `origin`: sender of the transaction356 /// - `metadata`: Metadata describing the collection, e.g. IPFS hash. Cannot be changed.357 /// - `max`: Optional maximum number of tokens.358 /// - `symbol`: UTF-8 string with token prefix, by which to represent the token in wallets and UIs.359 /// Analogous to Unique's [`token_prefix`](up_data_structs::Collection). Cannot be changed.360 #[pallet::call_index(0)]361 #[pallet::weight(<SelfWeightOf<T>>::create_collection())]362 pub fn create_collection(363 origin: OriginFor<T>,364 metadata: RmrkString,365 max: Option<u32>,366 symbol: RmrkCollectionSymbol,367 ) -> DispatchResult {368 let sender = ensure_signed(origin)?;369370 let limits = CollectionLimits {371 owner_can_transfer: Some(false),372 token_limit: max,373 ..Default::default()374 };375376 let data = CreateCollectionData {377 limits: Some(limits),378 token_prefix: symbol379 .into_inner()380 .try_into()381 .map_err(|_| <CommonError<T>>::CollectionTokenPrefixLimitExceeded)?,382 permissions: Some(CollectionPermissions {383 nesting: Some(NestingPermissions {384 token_owner: true,385 collection_admin: false,386 restricted: None,387 #[cfg(feature = "runtime-benchmarks")]388 permissive: false,389 }),390 ..Default::default()391 }),392 ..Default::default()393 };394395 let unique_collection_id = Self::init_collection(396 T::CrossAccountId::from_sub(sender.clone()),397 data,398 [399 Self::encode_rmrk_property(Metadata, &metadata)?,400 Self::encode_rmrk_property(CollectionType, &misc::CollectionType::Regular)?,401 ]402 .into_iter(),403 )?;404 let rmrk_collection_id = <CollectionIndex<T>>::get();405406 <UniqueCollectionId<T>>::insert(rmrk_collection_id, unique_collection_id);407408 <PalletCommon<T>>::set_scoped_collection_property(409 unique_collection_id,410 RMRK_SCOPE,411 Self::encode_rmrk_property(RmrkInternalCollectionId, &rmrk_collection_id)?,412 )?;413414 <CollectionIndex<T>>::mutate(|n| *n += 1);415416 Self::deposit_event(Event::CollectionCreated {417 issuer: sender,418 collection_id: rmrk_collection_id,419 });420421 Ok(())422 }423424 /// Destroy a collection.425 ///426 /// Only empty collections can be destroyed. If it has any tokens, they must be burned first.427 ///428 /// # Permissions:429 /// * Collection issuer430 ///431 /// # Arguments:432 /// - `origin`: sender of the transaction433 /// - `collection_id`: RMRK ID of the collection to destroy.434 #[pallet::call_index(1)]435 #[pallet::weight(<SelfWeightOf<T>>::destroy_collection())]436 pub fn destroy_collection(437 origin: OriginFor<T>,438 collection_id: RmrkCollectionId,439 ) -> DispatchResult {440 let sender = ensure_signed(origin)?;441 let cross_sender = T::CrossAccountId::from_sub(sender.clone());442443 let collection = Self::get_typed_nft_collection(444 Self::unique_collection_id(collection_id)?,445 misc::CollectionType::Regular,446 )?;447 collection.check_is_external()?;448449 <PalletNft<T>>::destroy_collection(collection, &cross_sender)450 .map_err(Self::map_unique_err_to_proxy)?;451452 Self::deposit_event(Event::CollectionDestroyed {453 issuer: sender,454 collection_id,455 });456457 Ok(())458 }459460 /// Change the issuer of a collection. Analogous to Unique's collection's [`owner`](up_data_structs::Collection).461 ///462 /// # Permissions:463 /// * Collection issuer464 ///465 /// # Arguments:466 /// - `origin`: sender of the transaction467 /// - `collection_id`: RMRK collection ID to change the issuer of.468 /// - `new_issuer`: Collection's new issuer.469 #[pallet::call_index(2)]470 #[pallet::weight(<SelfWeightOf<T>>::change_collection_issuer())]471 pub fn change_collection_issuer(472 origin: OriginFor<T>,473 collection_id: RmrkCollectionId,474 new_issuer: <T::Lookup as StaticLookup>::Source,475 ) -> DispatchResult {476 let sender = ensure_signed(origin)?;477478 let collection = Self::get_nft_collection(Self::unique_collection_id(collection_id)?)?;479 collection.check_is_external()?;480481 let new_issuer = T::Lookup::lookup(new_issuer)?;482483 Self::change_collection_owner(484 Self::unique_collection_id(collection_id)?,485 misc::CollectionType::Regular,486 sender.clone(),487 new_issuer.clone(),488 )?;489490 Self::deposit_event(Event::IssuerChanged {491 old_issuer: sender,492 new_issuer,493 collection_id,494 });495496 Ok(())497 }498499 /// "Lock" the collection and prevent new token creation. Cannot be undone.500 ///501 /// # Permissions:502 /// * Collection issuer503 ///504 /// # Arguments:505 /// - `origin`: sender of the transaction506 /// - `collection_id`: RMRK ID of the collection to lock.507 #[pallet::call_index(3)]508 #[pallet::weight(<SelfWeightOf<T>>::lock_collection())]509 pub fn lock_collection(510 origin: OriginFor<T>,511 collection_id: RmrkCollectionId,512 ) -> DispatchResult {513 let sender = ensure_signed(origin)?;514 let cross_sender = T::CrossAccountId::from_sub(sender.clone());515516 let collection = Self::get_typed_nft_collection(517 Self::unique_collection_id(collection_id)?,518 misc::CollectionType::Regular,519 )?;520 collection.check_is_external()?;521522 Self::check_collection_owner(&collection, &cross_sender)?;523524 let token_count = collection.total_supply();525526 let mut collection = collection.into_inner();527 collection.limits.token_limit = Some(token_count);528 collection.save()?;529530 Self::deposit_event(Event::CollectionLocked {531 issuer: sender,532 collection_id,533 });534535 Ok(())536 }537538 /// Mint an NFT in a specified collection.539 ///540 /// # Permissions:541 /// * Collection issuer542 ///543 /// # Arguments:544 /// - `origin`: sender of the transaction545 /// - `owner`: Owner account of the NFT. If set to None, defaults to the sender (collection issuer).546 /// - `collection_id`: RMRK collection ID for the NFT to be minted within. Cannot be changed.547 /// - `recipient`: Receiver account of the royalty. Has no effect if the `royalty_amount` is not set. Cannot be changed.548 /// - `royalty_amount`: Optional permillage reward from each trade for the `recipient`. Cannot be changed.549 /// - `metadata`: Arbitrary data about an NFT, e.g. IPFS hash. Cannot be changed.550 /// - `transferable`: Can this NFT be transferred? Cannot be changed.551 /// - `resources`: Resource data to be added to the NFT immediately after minting.552 #[pallet::call_index(4)]553 #[pallet::weight(<SelfWeightOf<T>>::mint_nft(resources.as_ref().map(|r| r.len() as u32).unwrap_or(0)))]554 pub fn mint_nft(555 origin: OriginFor<T>,556 owner: Option<T::AccountId>,557 collection_id: RmrkCollectionId,558 recipient: Option<T::AccountId>,559 royalty_amount: Option<Permill>,560 metadata: RmrkString,561 transferable: bool,562 resources: Option<BoundedVec<RmrkResourceTypes, MaxResourcesOnMint>>,563 ) -> DispatchResult {564 let sender = ensure_signed(origin)?;565 let cross_sender = T::CrossAccountId::from_sub(sender.clone());566567 let owner = owner.unwrap_or(sender.clone());568 let cross_owner = T::CrossAccountId::from_sub(owner.clone());569570 let collection = Self::get_typed_nft_collection(571 Self::unique_collection_id(collection_id)?,572 misc::CollectionType::Regular,573 )?;574 collection.check_is_external()?;575576 let royalty_info = royalty_amount.map(|amount| rmrk_traits::RoyaltyInfo {577 recipient: recipient.unwrap_or_else(|| owner.clone()),578 amount,579 });580581 let nft_id = Self::create_nft(582 &cross_sender,583 &cross_owner,584 &collection,585 [586 Self::encode_rmrk_property(TokenType, &NftType::Regular)?,587 Self::encode_rmrk_property(Transferable, &transferable)?,588 Self::encode_rmrk_property(PendingNftAccept, &None::<PendingTarget>)?,589 Self::encode_rmrk_property(RoyaltyInfo, &royalty_info)?,590 Self::encode_rmrk_property(Metadata, &metadata)?,591 Self::encode_rmrk_property(Equipped, &false)?,592 Self::encode_rmrk_property(ResourcePriorities, &<Vec<u8>>::new())?,593 Self::encode_rmrk_property(NextResourceId, &(0 as RmrkResourceId))?,594 Self::encode_rmrk_property(PendingChildren, &PendingChildrenSet::new())?,595 Self::encode_rmrk_property(AssociatedBases, &BasesMap::new())?,596 ]597 .into_iter(),598 )599 .map_err(|err| match err {600 DispatchError::Arithmetic(_) => <Error<T>>::NoAvailableNftId.into(),601 err => Self::map_unique_err_to_proxy(err),602 })?;603604 if let Some(resources) = resources {605 for resource in resources {606 Self::resource_add(sender.clone(), collection.id, nft_id, resource)?;607 }608 }609610 Self::deposit_event(Event::NftMinted {611 owner,612 collection_id,613 nft_id: nft_id.0,614 });615616 Ok(())617 }618619 /// Burn an NFT, destroying it and its nested tokens up to the specified limit.620 /// If the burning budget is exceeded, the transaction is reverted.621 ///622 /// This is the way to burn a nested token as well.623 ///624 /// For more information, see [`burn_recursively`](pallet_nonfungible::pallet::Pallet::burn_recursively).625 ///626 /// # Permissions:627 /// * Token owner628 ///629 /// # Arguments:630 /// - `origin`: sender of the transaction631 /// - `collection_id`: RMRK ID of the collection in which the NFT to burn belongs to.632 /// - `nft_id`: ID of the NFT to be destroyed.633 /// - `max_burns`: Maximum number of tokens to burn, assuming nesting. The transaction634 /// is reverted if there are more tokens to burn in the nesting tree than this number.635 /// This is primarily a mechanism of transaction weight control.636 #[pallet::call_index(5)]637 #[pallet::weight(<SelfWeightOf<T>>::burn_nft(*max_burns))]638 pub fn burn_nft(639 origin: OriginFor<T>,640 collection_id: RmrkCollectionId,641 nft_id: RmrkNftId,642 max_burns: u32,643 ) -> DispatchResult {644 let sender = ensure_signed(origin)?;645 let cross_sender = T::CrossAccountId::from_sub(sender.clone());646647 let collection = Self::get_typed_nft_collection(648 Self::unique_collection_id(collection_id)?,649 misc::CollectionType::Regular,650 )?;651 collection.check_is_external()?;652653 Self::destroy_nft(654 cross_sender,655 Self::unique_collection_id(collection_id)?,656 nft_id.into(),657 max_burns,658 <Error<T>>::NoPermission,659 )660 .map_err(|err| Self::map_unique_err_to_proxy(err.error))?;661662 Self::deposit_event(Event::NFTBurned {663 owner: sender,664 nft_id,665 });666667 Ok(())668 }669670 /// Transfer an NFT from an account/NFT A to another account/NFT B.671 /// The token must be transferable. Nesting cannot occur deeper than the [`NESTING_BUDGET`].672 ///673 /// If the target owner is an NFT owned by another account, then the NFT will enter674 /// the pending state and will have to be accepted by the other account.675 ///676 /// # Permissions:677 /// - Token owner678 ///679 /// # Arguments:680 /// - `origin`: sender of the transaction681 /// - `rmrk_collection_id`: RMRK ID of the collection of the NFT to be transferred.682 /// - `rmrk_nft_id`: ID of the NFT to be transferred.683 /// - `new_owner`: New owner of the nft which can be either an account or a NFT.684 #[pallet::call_index(6)]685 #[pallet::weight(<SelfWeightOf<T>>::send())]686 pub fn send(687 origin: OriginFor<T>,688 rmrk_collection_id: RmrkCollectionId,689 rmrk_nft_id: RmrkNftId,690 new_owner: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,691 ) -> DispatchResult {692 let sender = ensure_signed(origin.clone())?;693 let cross_sender = T::CrossAccountId::from_sub(sender.clone());694695 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;696 let nft_id = rmrk_nft_id.into();697698 let collection =699 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;700 collection.check_is_external()?;701702 let token_data =703 <TokenData<T>>::get((collection_id, nft_id)).ok_or(<Error<T>>::NoAvailableNftId)?;704705 let from = token_data.owner;706707 ensure!(708 Self::get_nft_property_decoded(collection_id, nft_id, RmrkProperty::Transferable)?,709 <Error<T>>::NonTransferable710 );711712 ensure!(713 Self::get_nft_property_decoded::<Option<PendingTarget>>(714 collection_id,715 nft_id,716 RmrkProperty::PendingNftAccept717 )?718 .is_none(),719 <Error<T>>::NoPermission720 );721722 let target_owner;723 let approval_required;724725 match new_owner {726 RmrkAccountIdOrCollectionNftTuple::AccountId(ref account_id) => {727 target_owner = T::CrossAccountId::from_sub(account_id.clone());728 approval_required = false;729 }730 RmrkAccountIdOrCollectionNftTuple::CollectionAndNftTuple(731 target_collection_id,732 target_nft_id,733 ) => {734 let target_collection_id = Self::unique_collection_id(target_collection_id)?;735736 let target_nft_budget = budget::Value::new(NESTING_BUDGET);737738 let target_nft_owner = <PalletStructure<T>>::get_checked_topmost_owner(739 target_collection_id,740 target_nft_id.into(),741 Some((collection_id, nft_id)),742 &target_nft_budget,743 )744 .map_err(Self::map_unique_err_to_proxy)?745 .ok_or::<DispatchError>(<Error<T>>::NoPermission.into())?;746747 approval_required = cross_sender != target_nft_owner;748749 if approval_required {750 target_owner = target_nft_owner;751752 <PalletNft<T>>::set_scoped_token_property(753 collection.id,754 nft_id,755 RMRK_SCOPE,756 Self::encode_rmrk_property::<Option<PendingTarget>>(757 PendingNftAccept,758 &Some((target_collection_id, target_nft_id.into())),759 )?,760 )?;761762 Self::insert_pending_child(763 (target_collection_id, target_nft_id.into()),764 (rmrk_collection_id, rmrk_nft_id),765 )?;766 } else {767 target_owner = T::CrossTokenAddressMapping::token_to_address(768 target_collection_id,769 target_nft_id.into(),770 );771 }772 }773 }774775 let src_nft_budget = budget::Value::new(NESTING_BUDGET);776777 <PalletNft<T>>::transfer_from(778 &collection,779 &cross_sender,780 &from,781 &target_owner,782 nft_id,783 &src_nft_budget,784 )785 .map_err(Self::map_unique_err_to_proxy)?;786787 Self::deposit_event(Event::NFTSent {788 sender,789 recipient: new_owner,790 collection_id: rmrk_collection_id,791 nft_id: rmrk_nft_id,792 approval_required,793 });794795 Ok(())796 }797798 /// Accept an NFT sent from another account to self or an owned NFT.799 ///800 /// The NFT in question must be pending, and, thus, be [sent](`Pallet::send`) first.801 ///802 /// # Permissions:803 /// - Token-owner-to-be804 ///805 /// # Arguments:806 /// - `origin`: sender of the transaction807 /// - `rmrk_collection_id`: RMRK collection ID of the NFT to be accepted.808 /// - `rmrk_nft_id`: ID of the NFT to be accepted.809 /// - `new_owner`: Either the sender's account ID or a sender-owned NFT,810 /// whichever the accepted NFT was sent to.811 #[pallet::call_index(7)]812 #[pallet::weight(<SelfWeightOf<T>>::accept_nft())]813 pub fn accept_nft(814 origin: OriginFor<T>,815 rmrk_collection_id: RmrkCollectionId,816 rmrk_nft_id: RmrkNftId,817 new_owner: RmrkAccountIdOrCollectionNftTuple<T::AccountId>,818 ) -> DispatchResult {819 let sender = ensure_signed(origin.clone())?;820 let cross_sender = T::CrossAccountId::from_sub(sender.clone());821822 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;823 let nft_id = rmrk_nft_id.into();824825 let collection =826 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;827 collection.check_is_external()?;828829 let new_cross_owner = match new_owner {830 RmrkAccountIdOrCollectionNftTuple::AccountId(ref account_id) => {831 T::CrossAccountId::from_sub(account_id.clone())832 }833 RmrkAccountIdOrCollectionNftTuple::CollectionAndNftTuple(834 target_collection_id,835 target_nft_id,836 ) => {837 let target_collection_id = Self::unique_collection_id(target_collection_id)?;838839 T::CrossTokenAddressMapping::token_to_address(840 target_collection_id,841 TokenId(target_nft_id),842 )843 }844 };845846 let budget = budget::Value::new(NESTING_BUDGET);847848 <PalletNft<T>>::transfer(849 &collection,850 &cross_sender,851 &new_cross_owner,852 nft_id,853 &budget,854 )855 .map_err(|err| {856 if err == <CommonError<T>>::UserIsNotAllowedToNest.into() {857 <Error<T>>::CannotAcceptNonOwnedNft.into()858 } else {859 Self::map_unique_err_to_proxy(err)860 }861 })?;862863 let pending_target = Self::get_nft_property_decoded::<Option<PendingTarget>>(864 collection_id,865 nft_id,866 RmrkProperty::PendingNftAccept,867 )?;868869 if let Some(pending_target) = pending_target {870 Self::remove_pending_child(pending_target, (rmrk_collection_id, rmrk_nft_id))?;871872 <PalletNft<T>>::set_scoped_token_property(873 collection.id,874 nft_id,875 RMRK_SCOPE,876 Self::encode_rmrk_property(PendingNftAccept, &None::<PendingTarget>)?,877 )?;878 }879880 Self::deposit_event(Event::NFTAccepted {881 sender,882 recipient: new_owner,883 collection_id: rmrk_collection_id,884 nft_id: rmrk_nft_id,885 });886887 Ok(())888 }889890 /// Reject an NFT sent from another account to self or owned NFT.891 /// The NFT in question will not be sent back and burnt instead.892 ///893 /// The NFT in question must be pending, and, thus, be [sent](`Pallet::send`) first.894 ///895 /// # Permissions:896 /// - Token-owner-to-be-not897 ///898 /// # Arguments:899 /// - `origin`: sender of the transaction900 /// - `rmrk_collection_id`: RMRK ID of the NFT to be rejected.901 /// - `rmrk_nft_id`: ID of the NFT to be rejected.902 #[pallet::call_index(8)]903 #[pallet::weight(<SelfWeightOf<T>>::reject_nft())]904 pub fn reject_nft(905 origin: OriginFor<T>,906 rmrk_collection_id: RmrkCollectionId,907 rmrk_nft_id: RmrkNftId,908 ) -> DispatchResult {909 let sender = ensure_signed(origin)?;910 let cross_sender = T::CrossAccountId::from_sub(sender.clone());911912 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;913 let nft_id = rmrk_nft_id.into();914915 let collection =916 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;917 collection.check_is_external()?;918919 ensure!(920 <TokenData<T>>::get((collection_id, nft_id)).is_some(),921 <Error<T>>::NoAvailableNftId922 );923924 let pending_target = Self::get_nft_property_decoded::<Option<PendingTarget>>(925 collection_id,926 nft_id,927 RmrkProperty::PendingNftAccept,928 )?;929930 match pending_target {931 Some(pending_target) => {932 Self::remove_pending_child(pending_target, (rmrk_collection_id, rmrk_nft_id))?933 }934 None => return Err(<Error<T>>::CannotRejectNonPendingNft.into()),935 }936937 Self::destroy_nft(938 cross_sender,939 collection_id,940 nft_id,941 NESTING_BUDGET,942 <Error<T>>::CannotRejectNonOwnedNft,943 )944 .map_err(|err| Self::map_unique_err_to_proxy(err.error))?;945946 Self::deposit_event(Event::NFTRejected {947 sender,948 collection_id: rmrk_collection_id,949 nft_id: rmrk_nft_id,950 });951952 Ok(())953 }954955 /// Accept the addition of a newly created pending resource to an existing NFT.956 ///957 /// This transaction is needed when a resource is created and assigned to an NFT958 /// by a non-owner, i.e. the collection issuer, with one of the959 /// [`add_...` transactions](Pallet::add_basic_resource).960 ///961 /// # Permissions:962 /// - Token owner963 ///964 /// # Arguments:965 /// - `origin`: sender of the transaction966 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.967 /// - `rmrk_nft_id`: ID of the NFT with a pending resource to be accepted.968 /// - `resource_id`: ID of the newly created pending resource.969 /// accept the addition of a new resource to an existing NFT970 #[pallet::call_index(9)]971 #[pallet::weight(<SelfWeightOf<T>>::accept_resource())]972 pub fn accept_resource(973 origin: OriginFor<T>,974 rmrk_collection_id: RmrkCollectionId,975 rmrk_nft_id: RmrkNftId,976 resource_id: RmrkResourceId,977 ) -> DispatchResult {978 let sender = ensure_signed(origin)?;979 let cross_sender = T::CrossAccountId::from_sub(sender);980981 let collection_id = Self::unique_collection_id(rmrk_collection_id)982 .map_err(|_| <Error<T>>::ResourceDoesntExist)?;983 let collection =984 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;985 collection.check_is_external()?;986987 let nft_id = rmrk_nft_id.into();988989 let budget = budget::Value::new(NESTING_BUDGET);990991 let nft_owner =992 <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)993 .map_err(|_| <Error<T>>::ResourceDoesntExist)?994 .ok_or::<DispatchError>(<Error<T>>::NoPermission.into())?;995996 Self::try_mutate_resource_info(collection_id, nft_id, resource_id, |res| {997 ensure!(res.pending, <Error<T>>::ResourceNotPending);998 ensure!(cross_sender == nft_owner, <Error<T>>::NoPermission);9991000 res.pending = false;10011002 Ok(())1003 })?;10041005 Self::deposit_event(Event::<T>::ResourceAccepted {1006 nft_id: rmrk_nft_id,1007 resource_id,1008 });10091010 Ok(())1011 }10121013 /// Accept the removal of a removal-pending resource from an NFT.1014 ///1015 /// This transaction is needed when a non-owner, i.e. the collection issuer,1016 /// requests a [removal](`Pallet::remove_resource`) of a resource from an NFT.1017 ///1018 /// # Permissions:1019 /// - Token owner1020 ///1021 /// # Arguments:1022 /// - `origin`: sender of the transaction1023 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1024 /// - `rmrk_nft_id`: ID of the NFT with a resource to be removed.1025 /// - `resource_id`: ID of the removal-pending resource.1026 #[pallet::call_index(10)]1027 #[pallet::weight(<SelfWeightOf<T>>::accept_resource_removal())]1028 pub fn accept_resource_removal(1029 origin: OriginFor<T>,1030 rmrk_collection_id: RmrkCollectionId,1031 rmrk_nft_id: RmrkNftId,1032 resource_id: RmrkResourceId,1033 ) -> DispatchResult {1034 let sender = ensure_signed(origin)?;1035 let cross_sender = T::CrossAccountId::from_sub(sender);10361037 let collection_id = Self::unique_collection_id(rmrk_collection_id)1038 .map_err(|_| <Error<T>>::ResourceDoesntExist)?;1039 let collection =1040 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1041 collection.check_is_external()?;10421043 let nft_id = rmrk_nft_id.into();10441045 let budget = budget::Value::new(NESTING_BUDGET);10461047 let nft_owner =1048 <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)1049 .map_err(|_| <Error<T>>::ResourceDoesntExist)?1050 .ok_or::<DispatchError>(<Error<T>>::NoPermission.into())?;10511052 ensure!(cross_sender == nft_owner, <Error<T>>::NoPermission);10531054 let resource_id_key = Self::get_scoped_property_key(ResourceId(resource_id))?;10551056 let resource_info = <PalletNft<T>>::token_aux_property((1057 collection_id,1058 nft_id,1059 RMRK_SCOPE,1060 resource_id_key.clone(),1061 ))1062 .ok_or(<Error<T>>::ResourceDoesntExist)?;10631064 let resource_info: RmrkResourceInfo = Self::decode_property_value(&resource_info)?;10651066 ensure!(1067 resource_info.pending_removal,1068 <Error<T>>::ResourceNotPending1069 );10701071 <PalletNft<T>>::remove_token_aux_property(1072 collection_id,1073 nft_id,1074 RMRK_SCOPE,1075 resource_id_key,1076 );10771078 if let RmrkResourceTypes::Composable(resource) = resource_info.resource {1079 let base_id = resource.base;10801081 Self::remove_associated_base_id(collection_id, nft_id, base_id)?;1082 }10831084 Self::deposit_event(Event::<T>::ResourceRemovalAccepted {1085 nft_id: rmrk_nft_id,1086 resource_id,1087 });10881089 Ok(())1090 }10911092 /// Add or edit a custom user property, a key-value pair, describing the metadata1093 /// of a token or a collection, on either one of these.1094 ///1095 /// Note that in this proxy implementation many details regarding RMRK are stored1096 /// as scoped properties prefixed with "rmrk:", normally inaccessible1097 /// to external transactions and RPCs.1098 ///1099 /// # Permissions:1100 /// - Collection issuer - in case of collection property1101 /// - Token owner - in case of NFT property1102 ///1103 /// # Arguments:1104 /// - `origin`: sender of the transaction1105 /// - `rmrk_collection_id`: RMRK collection ID.1106 /// - `maybe_nft_id`: Optional ID of the NFT. If left empty, then the property is set for the collection.1107 /// - `key`: Key of the custom property to be referenced by.1108 /// - `value`: Value of the custom property to be stored.1109 #[pallet::call_index(11)]1110 #[pallet::weight(<SelfWeightOf<T>>::set_property())]1111 pub fn set_property(1112 origin: OriginFor<T>,1113 #[pallet::compact] rmrk_collection_id: RmrkCollectionId,1114 maybe_nft_id: Option<RmrkNftId>,1115 key: RmrkKeyString,1116 value: RmrkValueString,1117 ) -> DispatchResult {1118 let sender = ensure_signed(origin)?;1119 let sender = T::CrossAccountId::from_sub(sender);11201121 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1122 let collection =1123 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1124 collection.check_is_external()?;11251126 let budget = budget::Value::new(NESTING_BUDGET);11271128 match maybe_nft_id {1129 Some(nft_id) => {1130 let token_id: TokenId = nft_id.into();11311132 Self::ensure_nft_type(collection_id, token_id, NftType::Regular)?;1133 Self::ensure_nft_owner(collection_id, token_id, &sender, &budget)?;11341135 <PalletNft<T>>::set_scoped_token_property(1136 collection_id,1137 token_id,1138 RMRK_SCOPE,1139 Self::encode_rmrk_property(UserProperty(key.as_slice()), &value)?,1140 )?;1141 }1142 None => {1143 let collection = Self::get_typed_nft_collection(1144 collection_id,1145 misc::CollectionType::Regular,1146 )?;11471148 Self::check_collection_owner(&collection, &sender)?;11491150 <PalletCommon<T>>::set_scoped_collection_property(1151 collection_id,1152 RMRK_SCOPE,1153 Self::encode_rmrk_property(UserProperty(key.as_slice()), &value)?,1154 )?;1155 }1156 }11571158 Self::deposit_event(Event::PropertySet {1159 collection_id: rmrk_collection_id,1160 maybe_nft_id,1161 key,1162 value,1163 });11641165 Ok(())1166 }11671168 /// Set a different order of resource priorities for an NFT. Priorities can be used,1169 /// for example, for order of rendering.1170 ///1171 /// Note that the priorities are not updated automatically, and are an empty vector1172 /// by default. There is no pre-set definition for the order to be particular,1173 /// it can be interpreted arbitrarily use-case by use-case.1174 ///1175 /// # Permissions:1176 /// - Token owner1177 ///1178 /// # Arguments:1179 /// - `origin`: sender of the transaction1180 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1181 /// - `rmrk_nft_id`: ID of the NFT to rearrange resource priorities for.1182 /// - `priorities`: Ordered vector of resource IDs.1183 #[pallet::call_index(12)]1184 #[pallet::weight(<SelfWeightOf<T>>::set_priority())]1185 pub fn set_priority(1186 origin: OriginFor<T>,1187 rmrk_collection_id: RmrkCollectionId,1188 rmrk_nft_id: RmrkNftId,1189 priorities: BoundedVec<RmrkResourceId, RmrkMaxPriorities>,1190 ) -> DispatchResult {1191 let sender = ensure_signed(origin)?;1192 let sender = T::CrossAccountId::from_sub(sender);11931194 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1195 let nft_id = rmrk_nft_id.into();11961197 let collection =1198 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1199 collection.check_is_external()?;12001201 let budget = budget::Value::new(NESTING_BUDGET);12021203 Self::ensure_nft_type(collection_id, nft_id, NftType::Regular)?;1204 Self::ensure_nft_owner(collection_id, nft_id, &sender, &budget)?;12051206 <PalletNft<T>>::set_scoped_token_property(1207 collection_id,1208 nft_id,1209 RMRK_SCOPE,1210 Self::encode_rmrk_property(ResourcePriorities, &priorities.into_inner())?,1211 )?;12121213 Self::deposit_event(Event::<T>::PrioritySet {1214 collection_id: rmrk_collection_id,1215 nft_id: rmrk_nft_id,1216 });12171218 Ok(())1219 }12201221 /// Create and set/propose a basic resource for an NFT.1222 ///1223 /// A basic resource is the simplest, lacking a Base and anything that comes with it.1224 /// See RMRK docs for more information and examples.1225 ///1226 /// # Permissions:1227 /// - Collection issuer - if not the token owner, adding the resource will warrant1228 /// the owner's [acceptance](Pallet::accept_resource).1229 ///1230 /// # Arguments:1231 /// - `origin`: sender of the transaction1232 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1233 /// - `nft_id`: ID of the NFT to assign a resource to.1234 /// - `resource`: Data of the resource to be created.1235 #[pallet::call_index(13)]1236 #[pallet::weight(<SelfWeightOf<T>>::add_basic_resource())]1237 pub fn add_basic_resource(1238 origin: OriginFor<T>,1239 rmrk_collection_id: RmrkCollectionId,1240 nft_id: RmrkNftId,1241 resource: RmrkBasicResource,1242 ) -> DispatchResult {1243 let sender = ensure_signed(origin.clone())?;12441245 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1246 let collection =1247 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1248 collection.check_is_external()?;12491250 let resource_id = Self::resource_add(1251 sender,1252 collection_id,1253 nft_id.into(),1254 RmrkResourceTypes::Basic(resource),1255 )?;12561257 Self::deposit_event(Event::ResourceAdded {1258 nft_id,1259 resource_id,1260 });1261 Ok(())1262 }12631264 /// Create and set/propose a composable resource for an NFT.1265 ///1266 /// A composable resource links to a Base and has a subset of its Parts it is composed of.1267 /// See RMRK docs for more information and examples.1268 ///1269 /// # Permissions:1270 /// - Collection issuer - if not the token owner, adding the resource will warrant1271 /// the owner's [acceptance](Pallet::accept_resource).1272 ///1273 /// # Arguments:1274 /// - `origin`: sender of the transaction1275 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1276 /// - `nft_id`: ID of the NFT to assign a resource to.1277 /// - `resource`: Data of the resource to be created.1278 #[pallet::call_index(14)]1279 #[pallet::weight(<SelfWeightOf<T>>::add_composable_resource())]1280 pub fn add_composable_resource(1281 origin: OriginFor<T>,1282 rmrk_collection_id: RmrkCollectionId,1283 nft_id: RmrkNftId,1284 resource: RmrkComposableResource,1285 ) -> DispatchResult {1286 let sender = ensure_signed(origin.clone())?;12871288 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1289 let collection =1290 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1291 collection.check_is_external()?;12921293 let base_id = resource.base;12941295 let resource_id = Self::resource_add(1296 sender,1297 collection_id,1298 nft_id.into(),1299 RmrkResourceTypes::Composable(resource),1300 )?;13011302 <PalletNft<T>>::try_mutate_token_aux_property(1303 collection_id,1304 nft_id.into(),1305 RMRK_SCOPE,1306 Self::get_scoped_property_key(AssociatedBases)?,1307 |value| -> DispatchResult {1308 let mut bases: BasesMap = match value {1309 Some(value) => Self::decode_property_value(value)?,1310 None => BasesMap::new(),1311 };13121313 *bases.entry(base_id).or_insert(0) += 1;13141315 *value = Some(Self::encode_property_value(&bases)?);1316 Ok(())1317 },1318 )?;13191320 Self::deposit_event(Event::ResourceAdded {1321 nft_id,1322 resource_id,1323 });1324 Ok(())1325 }13261327 /// Create and set/propose a slot resource for an NFT.1328 ///1329 /// A slot resource links to a Base and a slot ID in it which it can fit into.1330 /// See RMRK docs for more information and examples.1331 ///1332 /// # Permissions:1333 /// - Collection issuer - if not the token owner, adding the resource will warrant1334 /// the owner's [acceptance](Pallet::accept_resource).1335 ///1336 /// # Arguments:1337 /// - `origin`: sender of the transaction1338 /// - `rmrk_collection_id`: RMRK collection ID of the NFT.1339 /// - `nft_id`: ID of the NFT to assign a resource to.1340 /// - `resource`: Data of the resource to be created.1341 #[pallet::call_index(15)]1342 #[pallet::weight(<SelfWeightOf<T>>::add_slot_resource())]1343 pub fn add_slot_resource(1344 origin: OriginFor<T>,1345 rmrk_collection_id: RmrkCollectionId,1346 nft_id: RmrkNftId,1347 resource: RmrkSlotResource,1348 ) -> DispatchResult {1349 let sender = ensure_signed(origin.clone())?;13501351 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1352 let collection =1353 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1354 collection.check_is_external()?;13551356 let resource_id = Self::resource_add(1357 sender,1358 collection_id,1359 nft_id.into(),1360 RmrkResourceTypes::Slot(resource),1361 )?;13621363 Self::deposit_event(Event::ResourceAdded {1364 nft_id,1365 resource_id,1366 });1367 Ok(())1368 }13691370 /// Remove and erase a resource from an NFT.1371 ///1372 /// If the sender does not own the NFT, then it will be pending confirmation,1373 /// and will have to be [accepted](Pallet::accept_resource_removal) by the token owner.1374 ///1375 /// # Permissions1376 /// - Collection issuer1377 ///1378 /// # Arguments1379 /// - `origin`: sender of the transaction1380 /// - `rmrk_collection_id`: RMRK ID of a collection to which the NFT making use of the resource belongs to.1381 /// - `nft_id`: ID of the NFT with a resource to be removed.1382 /// - `resource_id`: ID of the resource to be removed.1383 #[pallet::call_index(16)]1384 #[pallet::weight(<SelfWeightOf<T>>::remove_resource())]1385 pub fn remove_resource(1386 origin: OriginFor<T>,1387 rmrk_collection_id: RmrkCollectionId,1388 nft_id: RmrkNftId,1389 resource_id: RmrkResourceId,1390 ) -> DispatchResult {1391 let sender = ensure_signed(origin.clone())?;13921393 let collection_id = Self::unique_collection_id(rmrk_collection_id)?;1394 let collection =1395 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1396 collection.check_is_external()?;13971398 Self::resource_remove(sender, collection_id, nft_id.into(), resource_id)?;13991400 Self::deposit_event(Event::ResourceRemoval {1401 nft_id,1402 resource_id,1403 });1404 Ok(())1405 }1406 }1407}14081409impl<T: Config> Pallet<T> {1410 /// Transform one of possible RMRK keys into a byte key with a RMRK scope.1411 pub fn get_scoped_property_key(rmrk_key: RmrkProperty) -> Result<PropertyKey, DispatchError> {1412 let key = rmrk_key.to_key::<T>()?;14131414 let scoped_key = RMRK_SCOPE1415 .apply(key)1416 .map_err(|_| <Error<T>>::RmrkPropertyKeyIsTooLong)?;14171418 Ok(scoped_key)1419 }14201421 /// Form a Unique property, transforming a RMRK key into bytes (without assigning the scope yet)1422 /// and encoding the value from an arbitrary type into bytes.1423 pub fn encode_rmrk_property<E: Encode>(1424 rmrk_key: RmrkProperty,1425 value: &E,1426 ) -> Result<Property, DispatchError> {1427 let key = rmrk_key.to_key::<T>()?;14281429 let value = Self::encode_property_value(value)?;14301431 let property = Property { key, value };14321433 Ok(property)1434 }14351436 /// Encode property value from an arbitrary type into bytes for storage.1437 pub fn encode_property_value<E: Encode, S: Get<u32>>(1438 value: &E,1439 ) -> Result<BoundedBytes<S>, DispatchError> {1440 let value = value1441 .encode()1442 .try_into()1443 .map_err(|_| <Error<T>>::RmrkPropertyValueIsTooLong)?;14441445 Ok(value)1446 }14471448 /// Decode property value from bytes into an arbitrary type.1449 pub fn decode_property_value<D: Decode, S: Get<u32>>(1450 vec: &BoundedBytes<S>,1451 ) -> Result<D, DispatchError> {1452 vec.decode()1453 .map_err(|_| <Error<T>>::UnableToDecodeRmrkData.into())1454 }14551456 /// Change the limit of a property value byte vector.1457 pub fn rebind<L, S>(vec: &BoundedVec<u8, L>) -> Result<BoundedVec<u8, S>, DispatchError>1458 where1459 BoundedVec<u8, S>: TryFrom<Vec<u8>>,1460 {1461 vec.rebind()1462 .map_err(|_| <Error<T>>::RmrkPropertyValueIsTooLong.into())1463 }14641465 /// Initialize a new NFT collection with certain RMRK-scoped properties.1466 ///1467 /// See [`init_collection`](pallet_nonfungible::pallet::Pallet::init_collection) for more details.1468 fn init_collection(1469 sender: T::CrossAccountId,1470 data: CreateCollectionData<T::AccountId>,1471 properties: impl Iterator<Item = Property>,1472 ) -> Result<CollectionId, DispatchError> {1473 let collection_id = <PalletNft<T>>::init_collection(1474 sender.clone(),1475 sender,1476 data,1477 up_data_structs::CollectionFlags {1478 external: true,1479 ..Default::default()1480 },1481 );14821483 if let Err(DispatchError::Arithmetic(_)) = &collection_id {1484 return Err(<Error<T>>::NoAvailableCollectionId.into());1485 }14861487 <PalletCommon<T>>::set_scoped_collection_properties(1488 collection_id?,1489 RMRK_SCOPE,1490 properties,1491 )?;14921493 collection_id1494 }14951496 /// Mint a new NFT with certain RMRK-scoped properties. Sender must be the collection owner.1497 ///1498 /// See [`create_item`](pallet_nonfungible::pallet::Pallet::create_item) for more details.1499 pub fn create_nft(1500 sender: &T::CrossAccountId,1501 owner: &T::CrossAccountId,1502 collection: &NonfungibleHandle<T>,1503 properties: impl Iterator<Item = Property>,1504 ) -> Result<TokenId, DispatchError> {1505 let data = CreateNftExData {1506 properties: BoundedVec::default(),1507 owner: owner.clone(),1508 };15091510 let budget = budget::Value::new(NESTING_BUDGET);15111512 <PalletNft<T>>::create_item(collection, sender, data, &budget)?;15131514 let nft_id = <PalletNft<T>>::current_token_id(collection.id);15151516 <PalletNft<T>>::set_scoped_token_properties(collection.id, nft_id, RMRK_SCOPE, properties)?;15171518 Ok(nft_id)1519 }15201521 /// Burn an NFT, along with its nested children, limited by `max_burns`. The sender must be the token owner.1522 ///1523 /// See [`burn_recursively`](pallet_nonfungible::pallet::Pallet::burn_recursively) for more details.1524 fn destroy_nft(1525 sender: T::CrossAccountId,1526 collection_id: CollectionId,1527 token_id: TokenId,1528 max_burns: u32,1529 error_if_not_owned: Error<T>,1530 ) -> DispatchResultWithPostInfo {1531 let collection =1532 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;15331534 let token_data =1535 <TokenData<T>>::get((collection_id, token_id)).ok_or(<Error<T>>::NoAvailableNftId)?;15361537 let from = token_data.owner;15381539 let owner_check_budget = budget::Value::new(NESTING_BUDGET);15401541 ensure!(1542 <PalletStructure<T>>::check_indirectly_owned(1543 sender.clone(),1544 collection_id,1545 token_id,1546 None,1547 &owner_check_budget1548 )?,1549 error_if_not_owned,1550 );15511552 let burns_budget = budget::Value::new(max_burns);1553 let breadth_budget = budget::Value::new(max_burns);15541555 <PalletNft<T>>::burn_recursively(1556 &collection,1557 &from,1558 token_id,1559 &burns_budget,1560 &breadth_budget,1561 )1562 }15631564 /// Add a sent token pending acceptance to the target owning token as a property.1565 fn insert_pending_child(1566 target: (CollectionId, TokenId),1567 child: (RmrkCollectionId, RmrkNftId),1568 ) -> DispatchResult {1569 Self::mutate_pending_children(target, |pending_children| {1570 pending_children.insert(child);1571 })1572 }15731574 /// Remove a sent token pending acceptance from the target token's properties.1575 fn remove_pending_child(1576 target: (CollectionId, TokenId),1577 child: (RmrkCollectionId, RmrkNftId),1578 ) -> DispatchResult {1579 Self::mutate_pending_children(target, |pending_children| {1580 pending_children.remove(&child);1581 })1582 }15831584 /// Apply a mutation to the property of a token containing sent tokens1585 /// that are currently pending acceptance.1586 fn mutate_pending_children(1587 (target_collection_id, target_nft_id): (CollectionId, TokenId),1588 f: impl FnOnce(&mut PendingChildrenSet),1589 ) -> DispatchResult {1590 <PalletNft<T>>::try_mutate_token_aux_property(1591 target_collection_id,1592 target_nft_id,1593 RMRK_SCOPE,1594 Self::get_scoped_property_key(PendingChildren)?,1595 |pending_children| -> DispatchResult {1596 let mut map = match pending_children {1597 Some(map) => Self::decode_property_value(map)?,1598 None => PendingChildrenSet::new(),1599 };16001601 f(&mut map);16021603 *pending_children = Some(Self::encode_property_value(&map)?);16041605 Ok(())1606 },1607 )1608 }16091610 /// Get an iterator from a token's property containing tokens sent to it1611 /// that are currently pending acceptance.1612 fn iterate_pending_children(1613 collection_id: CollectionId,1614 nft_id: TokenId,1615 ) -> Result<impl Iterator<Item = PendingChild>, DispatchError> {1616 let property = <PalletNft<T>>::token_aux_property((1617 collection_id,1618 nft_id,1619 RMRK_SCOPE,1620 Self::get_scoped_property_key(PendingChildren)?,1621 ));16221623 let pending_children = match property {1624 Some(map) => Self::decode_property_value(&map)?,1625 None => PendingChildrenSet::new(),1626 };16271628 Ok(pending_children.into_iter())1629 }16301631 /// Get incremented resource ID from within an NFT's properties and store the new latest ID.1632 /// Thus, the returned resource ID should be used.1633 ///1634 /// Resource IDs are unique only across an NFT.1635 fn acquire_next_resource_id(1636 collection_id: CollectionId,1637 nft_id: TokenId,1638 ) -> Result<RmrkResourceId, DispatchError> {1639 let resource_id: RmrkResourceId =1640 Self::get_nft_property_decoded(collection_id, nft_id, NextResourceId)?;16411642 let next_id = resource_id1643 .checked_add(1)1644 .ok_or(<Error<T>>::NoAvailableResourceId)?;16451646 <PalletNft<T>>::set_scoped_token_property(1647 collection_id,1648 nft_id,1649 RMRK_SCOPE,1650 Self::encode_rmrk_property(NextResourceId, &next_id)?,1651 )?;16521653 Ok(resource_id)1654 }16551656 /// Create and add a resource for a regular NFT, mark it as pending if the sender1657 /// is not the token owner. The sender must be the collection owner.1658 fn resource_add(1659 sender: T::AccountId,1660 collection_id: CollectionId,1661 nft_id: TokenId,1662 resource: RmrkResourceTypes,1663 ) -> Result<RmrkResourceId, DispatchError> {1664 let collection =1665 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1666 ensure!(collection.owner == sender, Error::<T>::NoPermission);16671668 let sender = T::CrossAccountId::from_sub(sender);1669 let budget = budget::Value::new(NESTING_BUDGET);16701671 let nft_owner = <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)1672 .map_err(Self::map_unique_err_to_proxy)?1673 .ok_or::<DispatchError>(<Error<T>>::NoPermission.into())?;16741675 let pending = sender != nft_owner;16761677 let id = Self::acquire_next_resource_id(collection_id, nft_id)?;16781679 let resource_info = RmrkResourceInfo {1680 id,1681 resource,1682 pending,1683 pending_removal: false,1684 };16851686 <PalletNft<T>>::try_mutate_token_aux_property(1687 collection_id,1688 nft_id,1689 RMRK_SCOPE,1690 Self::get_scoped_property_key(ResourceId(id))?,1691 |value| -> DispatchResult {1692 *value = Some(Self::encode_property_value(&resource_info)?);16931694 Ok(())1695 },1696 )?;16971698 Ok(id)1699 }17001701 /// Designate a resource for erasure from an NFT, and remove it if the sender is the token owner.1702 /// The sender must be the collection owner.1703 fn resource_remove(1704 sender: T::AccountId,1705 collection_id: CollectionId,1706 nft_id: TokenId,1707 resource_id: RmrkResourceId,1708 ) -> DispatchResult {1709 let collection =1710 Self::get_typed_nft_collection(collection_id, misc::CollectionType::Regular)?;1711 ensure!(collection.owner == sender, Error::<T>::NoPermission);17121713 let resource_id_key = Self::get_scoped_property_key(ResourceId(resource_id))?;17141715 let resource = <PalletNft<T>>::token_aux_property((1716 collection_id,1717 nft_id,1718 RMRK_SCOPE,1719 resource_id_key.clone(),1720 ))1721 .ok_or(<Error<T>>::ResourceDoesntExist)?;17221723 let resource_info: RmrkResourceInfo = Self::decode_property_value(&resource)?;17241725 let budget = up_data_structs::budget::Value::new(NESTING_BUDGET);1726 let topmost_owner =1727 <PalletStructure<T>>::find_topmost_owner(collection_id, nft_id, &budget)?1728 .ok_or::<DispatchError>(<Error<T>>::NoPermission.into())?;17291730 let sender = T::CrossAccountId::from_sub(sender);1731 if topmost_owner == sender {1732 <PalletNft<T>>::remove_token_aux_property(1733 collection_id,1734 nft_id,1735 RMRK_SCOPE,1736 Self::get_scoped_property_key(ResourceId(resource_id))?,1737 );17381739 if let RmrkResourceTypes::Composable(resource) = resource_info.resource {1740 let base_id = resource.base;17411742 Self::remove_associated_base_id(collection_id, nft_id, base_id)?;1743 }1744 } else {1745 Self::try_mutate_resource_info(collection_id, nft_id, resource_id, |res| {1746 res.pending_removal = true;17471748 Ok(())1749 })?;1750 }17511752 Ok(())1753 }17541755 /// Remove a Base ID from an NFT if they are associated.1756 /// The Base itself is deleted if the number of associated NFTs reaches 0.1757 fn remove_associated_base_id(1758 collection_id: CollectionId,1759 nft_id: TokenId,1760 base_id: RmrkBaseId,1761 ) -> DispatchResult {1762 <PalletNft<T>>::try_mutate_token_aux_property(1763 collection_id,1764 nft_id,1765 RMRK_SCOPE,1766 Self::get_scoped_property_key(AssociatedBases)?,1767 |value| -> DispatchResult {1768 let mut bases: BasesMap = match value {1769 Some(value) => Self::decode_property_value(value)?,1770 None => BasesMap::new(),1771 };17721773 let remaining = bases.get(&base_id);17741775 if let Some(remaining) = remaining {1776 if let Some(0) | None = remaining.checked_sub(1) {1777 bases.remove(&base_id);1778 }1779 }17801781 *value = Some(Self::encode_property_value(&bases)?);1782 Ok(())1783 },1784 )1785 }17861787 /// Apply a mutation to a resource stored in the token properties of an NFT.1788 fn try_mutate_resource_info(1789 collection_id: CollectionId,1790 nft_id: TokenId,1791 resource_id: RmrkResourceId,1792 f: impl FnOnce(&mut RmrkResourceInfo) -> DispatchResult,1793 ) -> DispatchResult {1794 <PalletNft<T>>::try_mutate_token_aux_property(1795 collection_id,1796 nft_id,1797 RMRK_SCOPE,1798 Self::get_scoped_property_key(ResourceId(resource_id))?,1799 |value| match value {1800 Some(value) => {1801 let mut resource_info: RmrkResourceInfo = Self::decode_property_value(value)?;18021803 f(&mut resource_info)?;18041805 *value = Self::encode_property_value(&resource_info)?;18061807 Ok(())1808 }1809 None => Err(<Error<T>>::ResourceDoesntExist.into()),1810 },1811 )1812 }18131814 /// Change the owner of an NFT collection, ensuring that the sender is the current owner.1815 fn change_collection_owner(1816 collection_id: CollectionId,1817 collection_type: misc::CollectionType,1818 sender: T::AccountId,1819 new_owner: T::AccountId,1820 ) -> DispatchResult {1821 let collection = Self::get_typed_nft_collection(collection_id, collection_type)?;1822 Self::check_collection_owner(&collection, &T::CrossAccountId::from_sub(sender))?;18231824 let mut collection = collection.into_inner();18251826 collection.owner = new_owner;1827 collection.save()1828 }18291830 /// Ensure that an account is the collection owner/issuer, return an error if not.1831 pub fn check_collection_owner(1832 collection: &NonfungibleHandle<T>,1833 account: &T::CrossAccountId,1834 ) -> DispatchResult {1835 collection1836 .check_is_owner(account)1837 .map_err(Self::map_unique_err_to_proxy)1838 }18391840 /// Get the latest yet-unused RMRK collection index from the storage.1841 pub fn last_collection_idx() -> RmrkCollectionId {1842 <CollectionIndex<T>>::get()1843 }18441845 /// Get a mapping from a RMRK collection ID to its corresponding Unique collection ID.1846 pub fn unique_collection_id(1847 rmrk_collection_id: RmrkCollectionId,1848 ) -> Result<CollectionId, DispatchError> {1849 <UniqueCollectionId<T>>::try_get(rmrk_collection_id)1850 .map_err(|_| <Error<T>>::CollectionUnknown.into())1851 }18521853 /// Get a mapping from a Unique collection ID to its RMRK collection ID counterpart, if it exists.1854 pub fn rmrk_collection_id(1855 unique_collection_id: CollectionId,1856 ) -> Result<RmrkCollectionId, DispatchError> {1857 Self::get_collection_property_decoded(unique_collection_id, RmrkInternalCollectionId)1858 }18591860 /// Fetch a Unique NFT collection.1861 pub fn get_nft_collection(1862 collection_id: CollectionId,1863 ) -> Result<NonfungibleHandle<T>, DispatchError> {1864 let collection = <CollectionHandle<T>>::try_get(collection_id)1865 .map_err(|_| <Error<T>>::CollectionUnknown)?;18661867 match collection.mode {1868 CollectionMode::NFT => Ok(NonfungibleHandle::cast(collection)),1869 _ => Err(<Error<T>>::CollectionUnknown.into()),1870 }1871 }18721873 /// Check if an NFT collection with such an ID exists.1874 pub fn collection_exists(collection_id: CollectionId) -> bool {1875 <CollectionHandle<T>>::try_get(collection_id).is_ok()1876 }18771878 /// Fetch and decode a RMRK-scoped collection property value in bytes.1879 pub fn get_collection_property(1880 collection_id: CollectionId,1881 key: RmrkProperty,1882 ) -> Result<PropertyValue, DispatchError> {1883 let collection_property = <PalletCommon<T>>::collection_properties(collection_id)1884 .get(&Self::get_scoped_property_key(key)?)1885 .ok_or(<Error<T>>::CollectionUnknown)?1886 .clone();18871888 Ok(collection_property)1889 }18901891 /// Fetch a RMRK-scoped collection property and decode it from bytes into an appropriate type.1892 pub fn get_collection_property_decoded<V: Decode>(1893 collection_id: CollectionId,1894 key: RmrkProperty,1895 ) -> Result<V, DispatchError> {1896 Self::decode_property_value(&Self::get_collection_property(collection_id, key)?)1897 }18981899 /// Get the type of a collection stored as a scoped property.1900 ///1901 /// RMRK Core proxy differentiates between regular collections as well as RMRK Bases as collections.1902 pub fn get_collection_type(1903 collection_id: CollectionId,1904 ) -> Result<misc::CollectionType, DispatchError> {1905 Self::get_collection_property_decoded(collection_id, CollectionType).map_err(|err| {1906 if err != <Error<T>>::CollectionUnknown.into() {1907 <Error<T>>::CorruptedCollectionType.into()1908 } else {1909 err1910 }1911 })1912 }19131914 /// Ensure that the type of the collection equals the provided type,1915 /// otherwise return an error.1916 pub fn ensure_collection_type(1917 collection_id: CollectionId,1918 collection_type: misc::CollectionType,1919 ) -> DispatchResult {1920 let actual_type = Self::get_collection_type(collection_id)?;1921 ensure!(1922 actual_type == collection_type,1923 <CommonError<T>>::NoPermission1924 );19251926 Ok(())1927 }19281929 /// Fetch an NFT collection, but make sure it has the appropriate type.1930 pub fn get_typed_nft_collection(1931 collection_id: CollectionId,1932 collection_type: misc::CollectionType,1933 ) -> Result<NonfungibleHandle<T>, DispatchError> {1934 Self::ensure_collection_type(collection_id, collection_type)?;19351936 Self::get_nft_collection(collection_id)1937 }19381939 /// Same as [`get_typed_nft_collection`](crate::pallet::Pallet::get_typed_nft_collection),1940 /// but also return the Unique collection ID.1941 pub fn get_typed_nft_collection_mapped(1942 rmrk_collection_id: RmrkCollectionId,1943 collection_type: misc::CollectionType,1944 ) -> Result<(NonfungibleHandle<T>, CollectionId), DispatchError> {1945 let unique_collection_id = match collection_type {1946 misc::CollectionType::Regular => Self::unique_collection_id(rmrk_collection_id)?,1947 _ => rmrk_collection_id.into(),1948 };19491950 let collection = Self::get_typed_nft_collection(unique_collection_id, collection_type)?;19511952 Ok((collection, unique_collection_id))1953 }19541955 /// Fetch and decode a RMRK-scoped NFT property value in bytes.1956 pub fn get_nft_property(1957 collection_id: CollectionId,1958 nft_id: TokenId,1959 key: RmrkProperty,1960 ) -> Result<PropertyValue, DispatchError> {1961 let nft_property = <PalletNft<T>>::token_properties((collection_id, nft_id))1962 .get(&Self::get_scoped_property_key(key)?)1963 .ok_or(<Error<T>>::RmrkPropertyIsNotFound)?1964 .clone();19651966 Ok(nft_property)1967 }19681969 /// Fetch a RMRK-scoped NFT property and decode it from bytes into an appropriate type.1970 pub fn get_nft_property_decoded<V: Decode>(1971 collection_id: CollectionId,1972 nft_id: TokenId,1973 key: RmrkProperty,1974 ) -> Result<V, DispatchError> {1975 Self::decode_property_value(&Self::get_nft_property(collection_id, nft_id, key)?)1976 }19771978 /// Check that an NFT exists.1979 pub fn nft_exists(collection_id: CollectionId, nft_id: TokenId) -> bool {1980 <TokenData<T>>::contains_key((collection_id, nft_id))1981 }19821983 /// Get the type of an NFT stored as a scoped property.1984 ///1985 /// RMRK Core proxy differentiates between regular NFTs, and RMRK Parts and Themes.1986 pub fn get_nft_type(1987 collection_id: CollectionId,1988 token_id: TokenId,1989 ) -> Result<NftType, DispatchError> {1990 Self::get_nft_property_decoded(collection_id, token_id, TokenType)1991 .map_err(|_| <Error<T>>::NoAvailableNftId.into())1992 }19931994 /// Ensure that the type of the NFT equals the provided type, otherwise return an error.1995 pub fn ensure_nft_type(1996 collection_id: CollectionId,1997 token_id: TokenId,1998 nft_type: NftType,1999 ) -> DispatchResult {2000 let actual_type = Self::get_nft_type(collection_id, token_id)?;2001 ensure!(actual_type == nft_type, <Error<T>>::NoPermission);20022003 Ok(())2004 }20052006 /// Ensure that an account is the owner of the token, either directly2007 /// or at the top of the nesting hierarchy; return an error if it is not.2008 pub fn ensure_nft_owner(2009 collection_id: CollectionId,2010 token_id: TokenId,2011 possible_owner: &T::CrossAccountId,2012 nesting_budget: &dyn budget::Budget,2013 ) -> DispatchResult {2014 let is_owned = <PalletStructure<T>>::check_indirectly_owned(2015 possible_owner.clone(),2016 collection_id,2017 token_id,2018 None,2019 nesting_budget,2020 )2021 .map_err(Self::map_unique_err_to_proxy)?;20222023 ensure!(is_owned, <Error<T>>::NoPermission);20242025 Ok(())2026 }20272028 /// Fetch non-scoped properties of a collection or a token that match the filter keys supplied,2029 /// or, if None are provided, return all non-scoped properties.2030 pub fn filter_user_properties<Key, Value, R, Mapper>(2031 collection_id: CollectionId,2032 token_id: Option<TokenId>,2033 filter_keys: Option<Vec<RmrkPropertyKey>>,2034 mapper: Mapper,2035 ) -> Result<Vec<R>, DispatchError>2036 where2037 Key: TryFrom<RmrkPropertyKey> + AsRef<[u8]>,2038 Value: Decode + Default,2039 Mapper: Fn(Key, Value) -> R,2040 {2041 filter_keys2042 .map(|keys| {2043 let properties = keys2044 .into_iter()2045 .filter_map(|key| {2046 let key: Key = key.try_into().ok()?;20472048 let value = match token_id {2049 Some(token_id) => Self::get_nft_property_decoded(2050 collection_id,2051 token_id,2052 UserProperty(key.as_ref()),2053 ),2054 None => Self::get_collection_property_decoded(2055 collection_id,2056 UserProperty(key.as_ref()),2057 ),2058 }2059 .ok()?;20602061 Some(mapper(key, value))2062 })2063 .collect();20642065 Ok(properties)2066 })2067 .unwrap_or_else(|| {2068 let properties =2069 Self::iterate_user_properties(collection_id, token_id, mapper)?.collect();20702071 Ok(properties)2072 })2073 }20742075 /// Get all non-scoped properties from a collection or a token, and apply some transformation,2076 /// supplied by `mapper`, to each key-value pair.2077 pub fn iterate_user_properties<Key, Value, R, Mapper>(2078 collection_id: CollectionId,2079 token_id: Option<TokenId>,2080 mapper: Mapper,2081 ) -> Result<impl Iterator<Item = R>, DispatchError>2082 where2083 Key: TryFrom<RmrkPropertyKey> + AsRef<[u8]>,2084 Value: Decode + Default,2085 Mapper: Fn(Key, Value) -> R,2086 {2087 let properties = match token_id {2088 Some(token_id) => <PalletNft<T>>::token_properties((collection_id, token_id)),2089 None => <PalletCommon<T>>::collection_properties(collection_id),2090 };20912092 let properties = properties.into_iter().filter_map(move |(key, value)| {2093 let key = strip_key_prefix(&key, USER_PROPERTY_PREFIX)?;20942095 let key: Key = key.to_vec().try_into().ok()?;2096 let value: Value = value.decode().ok()?;20972098 Some(mapper(key, value))2099 });21002101 Ok(properties)2102 }21032104 /// Match Unique errors to RMRK's own and return the RMRK error if a match is successful.2105 fn map_unique_err_to_proxy(err: DispatchError) -> DispatchError {2106 map_unique_err_to_proxy! {2107 match err {2108 CommonError::NoPermission => NoPermission,2109 CommonError::CollectionTokenLimitExceeded => CollectionFullOrLocked,2110 CommonError::PublicMintingNotAllowed => NoPermission,2111 CommonError::TokenNotFound => NoAvailableNftId,2112 CommonError::ApprovedValueTooLow => NoPermission,2113 CommonError::CantDestroyNotEmptyCollection => CollectionNotEmpty,2114 StructureError::TokenNotFound => NoAvailableNftId,2115 StructureError::OuroborosDetected => CannotSendToDescendentOrSelf,2116 }2117 }2118 }2119}pallets/proxy-rmrk-core/src/rpc.rsdiffbeforeafterboth--- a/pallets/proxy-rmrk-core/src/rpc.rs
+++ b/pallets/proxy-rmrk-core/src/rpc.rs
@@ -68,7 +68,7 @@
}
let owner = match collection.token_owner(nft_id) {
- Some(owner) => match T::CrossTokenAddressMapping::address_to_token(&owner) {
+ Ok(owner) => match T::CrossTokenAddressMapping::address_to_token(&owner) {
Some((col, tok)) => {
let rmrk_collection = <Pallet<T>>::rmrk_collection_id(col)?;
@@ -76,7 +76,7 @@
}
None => RmrkAccountIdOrCollectionNftTuple::AccountId(owner.as_sub().clone()),
},
- None => return Ok(None),
+ _ => return Ok(None),
};
Ok(Some(RmrkInstanceInfo {
pallets/refungible/src/common.rsdiffbeforeafterboth--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -21,7 +21,7 @@
use up_data_structs::{
CollectionId, TokenId, CreateItemExData, budget::Budget, Property, PropertyKey, PropertyValue,
PropertyKeyPermission, CollectionPropertiesVec, CreateRefungibleExMultipleOwners,
- CreateRefungibleExSingleOwner,
+ CreateRefungibleExSingleOwner, TokenOwnerError,
};
use pallet_common::{
CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight,
@@ -478,7 +478,7 @@
TokenId(<TokensMinted<T>>::get(self.id))
}
- fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId> {
+ fn token_owner(&self, token: TokenId) -> Result<T::CrossAccountId, TokenOwnerError> {
<Pallet<T>>::token_owner(self.id, token)
}
pallets/refungible/src/erc.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -43,7 +43,7 @@
use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};
use up_data_structs::{
CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,
- PropertyKeyPermission, PropertyPermission, TokenId,
+ PropertyKeyPermission, PropertyPermission, TokenId, TokenOwnerError,
};
use crate::{
@@ -411,9 +411,12 @@
self.consume_store_reads(2)?;
let token = token_id.try_into()?;
let owner = <Pallet<T>>::token_owner(self.id, token);
- Ok(owner
+ owner
.map(|address| *address.as_eth())
- .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))
+ .or_else(|err| match err {
+ TokenOwnerError::NotFound => Err(Error::Revert("token not found".into())),
+ TokenOwnerError::MultipleOwners => Ok(ADDRESS_FOR_PARTIALLY_OWNED_TOKENS),
+ })
}
/// @dev Not implemented
@@ -766,7 +769,12 @@
fn cross_owner_of(&self, token_id: U256) -> Result<eth::CrossAddress> {
Self::token_owner(&self, token_id.try_into()?)
.map(|o| eth::CrossAddress::from_sub_cross_account::<T>(&o))
- .ok_or(Error::Revert("key too large".into()))
+ .or_else(|err| match err {
+ TokenOwnerError::NotFound => Err(Error::Revert("token not found".into())),
+ TokenOwnerError::MultipleOwners => Ok(eth::CrossAddress::from_eth(
+ ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+ )),
+ })
}
/// Returns the token properties.
pallets/refungible/src/lib.rsdiffbeforeafterboth--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -107,7 +107,7 @@
AccessMode, budget::Budget, CollectionId, CollectionFlags, CreateCollectionData,
mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, Property, PropertyKey,
PropertyKeyPermission, PropertyPermission, PropertyScope, PropertyValue, TokenId,
- TrySetProperty, PropertiesPermissionMap, CreateRefungibleExMultipleOwners,
+ TrySetProperty, PropertiesPermissionMap, CreateRefungibleExMultipleOwners, TokenOwnerError,
};
pub use pallet::*;
@@ -480,7 +480,7 @@
<Balance<T>>::remove((collection.id, token, owner));
<AccountBalance<T>>::insert((collection.id, owner), account_balance);
- if let Some(user) = Self::token_owner(collection.id, token) {
+ if let Ok(user) = Self::token_owner(collection.id, token) {
<PalletEvm<T>>::deposit_log(
ERC721Events::Transfer {
from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
@@ -1365,17 +1365,20 @@
Ok(())
}
- fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {
+ fn token_owner(
+ collection_id: CollectionId,
+ token_id: TokenId,
+ ) -> Result<T::CrossAccountId, TokenOwnerError> {
let mut owner = None;
let mut count = 0;
for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {
count += 1;
if count > 1 {
- return None;
+ return Err(TokenOwnerError::MultipleOwners);
}
owner = Some(key);
}
- owner
+ owner.ok_or(TokenOwnerError::NotFound)
}
fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {
pallets/structure/src/lib.rsdiffbeforeafterboth--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -61,7 +61,9 @@
use frame_support::fail;
pub use pallet::*;
use pallet_common::{dispatch::CollectionDispatch, CollectionHandle};
-use up_data_structs::{CollectionId, TokenId, mapping::TokenAddressMapping, budget::Budget};
+use up_data_structs::{
+ CollectionId, TokenId, mapping::TokenAddressMapping, budget::Budget, TokenOwnerError,
+};
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
@@ -135,6 +137,8 @@
User(CrossAccountId),
/// Could not find the token provided as the owner.
TokenNotFound,
+ /// Nested token has multiple owners.
+ MultipleOwners,
/// Token owner is another token (still, the target token may not exist).
Token(CollectionId, TokenId),
}
@@ -159,11 +163,12 @@
let handle = handle.as_dyn();
Ok(match handle.token_owner(token) {
- Some(owner) => match T::CrossTokenAddressMapping::address_to_token(&owner) {
+ Ok(owner) => match T::CrossTokenAddressMapping::address_to_token(&owner) {
Some((collection, token)) => Parent::Token(collection, token),
None => Parent::User(owner),
},
- None => Parent::TokenNotFound,
+ Err(TokenOwnerError::MultipleOwners) => Parent::MultipleOwners,
+ Err(TokenOwnerError::NotFound) => Parent::TokenNotFound,
})
}
@@ -203,19 +208,27 @@
///
/// May return token address if parent token not yet exists
///
+ /// Returns `None` if the token has multiple owners.
+ ///
/// - `budget`: Limit for searching parents in depth.
pub fn find_topmost_owner(
collection: CollectionId,
token: TokenId,
budget: &dyn Budget,
- ) -> Result<T::CrossAccountId, DispatchError> {
+ ) -> Result<Option<T::CrossAccountId>, DispatchError> {
let owner = Self::parent_chain(collection, token)
.take_while(|_| budget.consume())
- .find(|p| matches!(p, Ok(Parent::User(_) | Parent::TokenNotFound)))
+ .find(|p| {
+ matches!(
+ p,
+ Ok(Parent::User(_) | Parent::TokenNotFound | Parent::MultipleOwners)
+ )
+ })
.ok_or(<Error<T>>::DepthLimit)??;
Ok(match owner {
- Parent::User(v) => v,
+ Parent::User(v) => Some(v),
+ Parent::MultipleOwners => None,
_ => fail!(<Error<T>>::TokenNotFound),
})
}
@@ -223,13 +236,15 @@
/// Find the topmost parent and check that assigning `for_nest` token as a child for
/// `token` wouldn't create a cycle.
///
+ /// Returns `None` if the token has multiple owners.
+ ///
/// - `budget`: Limit for searching parents in depth.
pub fn get_checked_topmost_owner(
collection: CollectionId,
token: TokenId,
for_nest: Option<(CollectionId, TokenId)>,
budget: &dyn Budget,
- ) -> Result<T::CrossAccountId, DispatchError> {
+ ) -> Result<Option<T::CrossAccountId>, DispatchError> {
// Tried to nest token in itself
if Some((collection, token)) == for_nest {
return Err(<Error<T>>::OuroborosDetected.into());
@@ -242,8 +257,9 @@
return Err(<Error<T>>::OuroborosDetected.into())
}
// Token is owned by other user
- Parent::User(user) => return Ok(user),
+ Parent::User(user) => return Ok(Some(user)),
Parent::TokenNotFound => return Err(<Error<T>>::TokenNotFound.into()),
+ Parent::MultipleOwners => return Ok(None),
// Continue parent chain
Parent::Token(_, _) => {}
}
@@ -284,12 +300,17 @@
budget: &dyn Budget,
) -> Result<bool, DispatchError> {
let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) {
- Some((collection, token)) => Self::find_topmost_owner(collection, token, budget)?,
+ Some((collection, token)) => match Self::find_topmost_owner(collection, token, budget)?
+ {
+ Some(topmost_owner) => topmost_owner,
+ None => return Ok(false),
+ },
None => user,
};
- Self::get_checked_topmost_owner(collection, token, for_nest, budget)
- .map(|indirect_owner| indirect_owner == target_parent)
+ Self::get_checked_topmost_owner(collection, token, for_nest, budget).map(|indirect_owner| {
+ indirect_owner.map_or(false, |indirect_owner| indirect_owner == target_parent)
+ })
}
/// Checks that `under` is valid token and that `token_id` could be nested under it
primitives/data-structs/src/lib.rsdiffbeforeafterboth--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -1099,6 +1099,13 @@
EmptyPropertyKey,
}
+/// Token owner error: it could be either `NotFound` ot `MultipleOwners`.
+#[derive(Debug)]
+pub enum TokenOwnerError {
+ NotFound,
+ MultipleOwners,
+}
+
/// Marker for scope of property.
///
/// Scoped property can't be changed by user. Used for external collections.
runtime/common/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/runtime_apis.rs
+++ b/runtime/common/runtime_apis.rs
@@ -16,11 +16,11 @@
#[macro_export]
macro_rules! dispatch_unique_runtime {
- ($collection:ident.$method:ident($($name:ident),*)) => {{
+ ($collection:ident.$method:ident($($name:ident),*) $($rest:tt)*) => {{
let collection = <Runtime as pallet_common::Config>::CollectionDispatch::dispatch(<pallet_common::CollectionHandle<Runtime>>::try_get($collection)?);
let dispatch = collection.as_dyn();
- Ok::<_, DispatchError>(dispatch.$method($($name),*))
+ Ok::<_, DispatchError>(dispatch.$method($($name),*) $($rest)*)
}};
}
@@ -73,7 +73,7 @@
}
fn token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
- dispatch_unique_runtime!(collection.token_owner(token))
+ dispatch_unique_runtime!(collection.token_owner(token).ok())
}
fn token_owners(collection: CollectionId, token: TokenId) -> Result<Vec::<CrossAccountId>, DispatchError> {
@@ -83,7 +83,7 @@
fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
let budget = up_data_structs::budget::Value::new(10);
- Ok(Some(<pallet_structure::Pallet<Runtime>>::find_topmost_owner(collection, token, &budget)?))
+ Ok(<pallet_structure::Pallet<Runtime>>::find_topmost_owner(collection, token, &budget)?)
}
fn token_children(collection: CollectionId, token: TokenId) -> Result<Vec<TokenChild>, DispatchError> {
Ok(<pallet_nonfungible::Pallet<Runtime>>::token_children_ids(collection, token))
tests/src/nesting/nest.test.tsdiffbeforeafterboth--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -15,7 +15,7 @@
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
import {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from '../util';
+import {expect, itSub, Pallets, usingPlaygrounds} from '../util';
describe('Integration Test: Composite nesting tests', () => {
let alice: IKeyringPair;
@@ -138,7 +138,7 @@
before(async () => {
await usingPlaygrounds(async (helper, privateKey) => {
const donor = await privateKey({filename: __filename});
- [alice, bob, charlie] = await helper.arrange.createAccounts([50n, 10n, 10n], donor);
+ [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 10n, 10n], donor);
});
});
@@ -288,6 +288,38 @@
await collectionFT.transfer(charlie, targetToken.nestingAccount(), 2n);
expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(7n);
});
+
+ itSub.ifWithPallets('ReFungible: getTopmostOwner works correctly with Nesting', [Pallets.ReFungible], async({helper}) => {
+ const collectionNFT = await helper.nft.mintCollection(alice, {
+ permissions: {
+ nesting: {
+ tokenOwner: true,
+ },
+ },
+ });
+ const collectionRFT = await helper.rft.mintCollection(alice);
+
+ const nft = await collectionNFT.mintToken(alice, {Substrate: alice.address});
+ const rft = await collectionRFT.mintToken(alice, 100n, {Substrate: alice.address});
+
+ expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
+
+ await rft.transfer(alice, nft.nestingAccount(), 40n);
+
+ expect(await rft.getTopmostOwner()).deep.equal(null);
+
+ await rft.transfer(alice, nft.nestingAccount(), 60n);
+
+ expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
+
+ await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 30n);
+
+ expect(await rft.getTopmostOwner()).deep.equal(null);
+
+ await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 70n);
+
+ expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
+ });
});
describe('Negative Test: Nesting', () => {