// This file is part of Substrate. // Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Unique (Assets) Module //! //! A simple, secure module for dealing with non-fungible assets. //! //! ## Related Modules //! //! * [`System`](../frame_system/index.html) //! * [`Support`](../frame_support/index.html) // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] #[cfg(feature = "runtime-benchmarks")] mod benchmarking; #[cfg(test)] pub mod mock; #[cfg(test)] mod tests; pub mod weights; mod functions; mod impl_nonfungibles; mod types; pub use types::*; use codec::{Decode, Encode, HasCompact}; use frame_support::traits::{BalanceStatus::Reserved, Currency, ReservableCurrency}; use frame_system::Config as SystemConfig; use sp_runtime::{ traits::{Saturating, StaticLookup, Zero}, ArithmeticError, RuntimeDebug, }; use sp_std::prelude::*; pub use pallet::*; pub use weights::WeightInfo; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); #[pallet::config] /// The module configuration trait. pub trait Config: frame_system::Config { /// The overarching event type. type Event: From> + IsType<::Event>; /// Identifier for the class of asset. type ClassId: Member + Parameter + Default + Copy + HasCompact; /// The type used to identify a unique asset within an asset class. type InstanceId: Member + Parameter + Default + Copy + HasCompact + From; /// The currency mechanism, used for paying for reserves. type Currency: ReservableCurrency; /// The origin which may forcibly create or destroy an asset or otherwise alter privileged /// attributes. type ForceOrigin: EnsureOrigin; /// The basic amount of funds that must be reserved for an asset class. #[pallet::constant] type ClassDeposit: Get>; /// The basic amount of funds that must be reserved for an asset instance. #[pallet::constant] type InstanceDeposit: Get>; /// The basic amount of funds that must be reserved when adding metadata to your asset. #[pallet::constant] type MetadataDepositBase: Get>; /// The basic amount of funds that must be reserved when adding an attribute to an asset. #[pallet::constant] type AttributeDepositBase: Get>; /// The additional funds that must be reserved for the number of bytes store in metadata, /// either "normal" metadata or attribute metadata. #[pallet::constant] type DepositPerByte: Get>; /// The maximum length of data stored on-chain. #[pallet::constant] type StringLimit: Get; /// The maximum length of an attribute key. #[pallet::constant] type KeyLimit: Get; /// The maximum length of an attribute value. #[pallet::constant] type ValueLimit: Get; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } #[pallet::storage] /// Details of an asset class. pub(super) type Class, I: 'static = ()> = StorageMap< _, Blake2_128Concat, T::ClassId, ClassDetails>, >; #[pallet::storage] /// The assets held by any given account; set out this way so that assets owned by a single /// account can be enumerated. pub(super) type Account, I: 'static = ()> = StorageNMap< _, ( NMapKey, // owner NMapKey, NMapKey, ), (), OptionQuery, >; #[pallet::storage] /// The assets in existence and their ownership details. pub(super) type Asset, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, T::ClassId, Blake2_128Concat, T::InstanceId, InstanceDetails>, OptionQuery, >; #[pallet::storage] /// Metadata of an asset class. pub(super) type ClassMetadataOf, I: 'static = ()> = StorageMap< _, Blake2_128Concat, T::ClassId, ClassMetadata, T::StringLimit>, OptionQuery, >; #[pallet::storage] /// Metadata of an asset instance. pub(super) type InstanceMetadataOf, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, T::ClassId, Blake2_128Concat, T::InstanceId, InstanceMetadata, T::StringLimit>, OptionQuery, >; #[pallet::storage] /// Metadata of an asset class. pub(super) type Attribute, I: 'static = ()> = StorageNMap< _, ( NMapKey, NMapKey>, NMapKey>, ), (BoundedVec, DepositBalanceOf), OptionQuery, >; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::metadata( T::AccountId = "AccountId", T::ClassId = "ClassId", T::InstanceId = "InstanceId" )] pub enum Event, I: 'static = ()> { /// An asset class was created. \[ class, creator, owner \] Created(T::ClassId, T::AccountId, T::AccountId), /// An asset class was force-created. \[ class, owner \] ForceCreated(T::ClassId, T::AccountId), /// An asset `class` was destroyed. \[ class \] Destroyed(T::ClassId), /// An asset `instance` was issued. \[ class, instance, owner \] Issued(T::ClassId, T::InstanceId, T::AccountId), /// An asset `instance` was transferred. \[ class, instance, from, to \] Transferred(T::ClassId, T::InstanceId, T::AccountId, T::AccountId), /// An asset `instance` was destroyed. \[ class, instance, owner \] Burned(T::ClassId, T::InstanceId, T::AccountId), /// Some asset `instance` was frozen. \[ class, instance \] Frozen(T::ClassId, T::InstanceId), /// Some asset `instance` was thawed. \[ class, instance \] Thawed(T::ClassId, T::InstanceId), /// Some asset `class` was frozen. \[ class \] ClassFrozen(T::ClassId), /// Some asset `class` was thawed. \[ class \] ClassThawed(T::ClassId), /// The owner changed \[ class, new_owner \] OwnerChanged(T::ClassId, T::AccountId), /// The management team changed \[ class, issuer, admin, freezer \] TeamChanged(T::ClassId, T::AccountId, T::AccountId, T::AccountId), /// An `instance` of an asset `class` has been approved by the `owner` for transfer by a /// `delegate`. /// \[ class, instance, owner, delegate \] ApprovedTransfer(T::ClassId, T::InstanceId, T::AccountId, T::AccountId), /// An approval for a `delegate` account to transfer the `instance` of an asset `class` was /// cancelled by its `owner`. /// \[ class, instance, owner, delegate \] ApprovalCancelled(T::ClassId, T::InstanceId, T::AccountId, T::AccountId), /// An asset `class` has had its attributes changed by the `Force` origin. /// \[ class \] AssetStatusChanged(T::ClassId), /// New metadata has been set for an asset class. \[ class, data, is_frozen \] ClassMetadataSet(T::ClassId, BoundedVec, bool), /// Metadata has been cleared for an asset class. \[ class \] ClassMetadataCleared(T::ClassId), /// New metadata has been set for an asset instance. /// \[ class, instance, data, is_frozen \] MetadataSet(T::ClassId, T::InstanceId, BoundedVec, bool), /// Metadata has been cleared for an asset instance. \[ class, instance \] MetadataCleared(T::ClassId, T::InstanceId), /// Metadata has been cleared for an asset instance. \[ class, successful_instances \] Redeposited(T::ClassId, Vec), /// New attribute metadata has been set for an asset class or instance. /// \[ class, maybe_instance, key, value \] AttributeSet( T::ClassId, Option, BoundedVec, BoundedVec, ), /// Attribute metadata has been cleared for an asset class or instance. /// \[ class, maybe_instance, key, maybe_value \] AttributeCleared(T::ClassId, Option, BoundedVec), } #[pallet::error] pub enum Error { /// The signing account has no permission to do the operation. NoPermission, /// The given asset ID is unknown. Unknown, /// The asset instance ID has already been used for an asset. AlreadyExists, /// The owner turned out to be different to what was expected. WrongOwner, /// Invalid witness data given. BadWitness, /// The asset ID is already taken. InUse, /// The asset instance or class is frozen. Frozen, /// The delegate turned out to be different to what was expected. WrongDelegate, /// There is no delegate approved. NoDelegate, /// No approval exists that would allow the transfer. Unapproved, } #[pallet::hooks] impl, I: 'static> Hooks> for Pallet {} impl, I: 'static> Pallet { /// Get the owner of the asset instance, if the asset exists. pub fn owner(class: T::ClassId, instance: T::InstanceId) -> Option { Asset::::get(class, instance).map(|i| i.owner) } } #[pallet::call] impl, I: 'static> Pallet { /// Issue a new class of non-fungible assets from a public origin. /// /// This new asset class has no assets initially and its owner is the origin. /// /// The origin must be Signed and the sender must have sufficient funds free. /// /// `AssetDeposit` funds of sender are reserved. /// /// Parameters: /// - `class`: The identifier of the new asset class. This must not be currently in use. /// - `admin`: The admin of this class of assets. The admin is the initial address of each /// member of the asset class's admin team. /// /// Emits `Created` event when successful. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::create())] pub fn create( origin: OriginFor, #[pallet::compact] class: T::ClassId, admin: ::Source, ) -> DispatchResult { let owner = ensure_signed(origin)?; let admin = T::Lookup::lookup(admin)?; Self::do_create_class( class, owner.clone(), admin.clone(), T::ClassDeposit::get(), false, Event::Created(class, owner, admin), ) } /// Issue a new class of non-fungible assets from a privileged origin. /// /// This new asset class has no assets initially. /// /// The origin must conform to `ForceOrigin`. /// /// Unlike `create`, no funds are reserved. /// /// - `class`: The identifier of the new asset. This must not be currently in use. /// - `owner`: The owner of this class of assets. The owner has full superuser permissions /// over this asset, but may later change and configure the permissions using /// `transfer_ownership` and `set_team`. /// /// Emits `ForceCreated` event when successful. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::force_create())] pub fn force_create( origin: OriginFor, #[pallet::compact] class: T::ClassId, owner: ::Source, free_holding: bool, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; let owner = T::Lookup::lookup(owner)?; Self::do_create_class( class, owner.clone(), owner.clone(), Zero::zero(), free_holding, Event::ForceCreated(class, owner), ) } /// Destroy a class of fungible assets. /// /// The origin must conform to `ForceOrigin` or must be `Signed` and the sender must be the /// owner of the asset `class`. /// /// - `class`: The identifier of the asset class to be destroyed. /// - `witness`: Information on the instances minted in the asset class. This must be /// correct. /// /// Emits `Destroyed` event when successful. /// /// Weight: `O(n + m)` where: /// - `n = witness.instances` /// - `m = witness.instance_metadatas` /// - `a = witness.attributes` #[pallet::weight(T::WeightInfo::destroy( witness.instances, witness.instance_metadatas, witness.attributes, ))] pub fn destroy( origin: OriginFor, #[pallet::compact] class: T::ClassId, witness: DestroyWitness, ) -> DispatchResult { let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { Ok(_) => None, Err(origin) => Some(ensure_signed(origin)?), }; Class::::try_mutate_exists(class, |maybe_details| { let class_details = maybe_details.take().ok_or(Error::::Unknown)?; if let Some(check_owner) = maybe_check_owner { ensure!(class_details.owner == check_owner, Error::::NoPermission); } ensure!(class_details.instances == witness.instances, Error::::BadWitness); ensure!( class_details.instance_metadatas == witness.instance_metadatas, Error::::BadWitness ); ensure!(class_details.attributes == witness.attributes, Error::::BadWitness); for (instance, details) in Asset::::drain_prefix(&class) { Account::::remove((&details.owner, &class, &instance)); } InstanceMetadataOf::::remove_prefix(&class, None); ClassMetadataOf::::remove(&class); Attribute::::remove_prefix((&class,), None); T::Currency::unreserve(&class_details.owner, class_details.total_deposit); Self::deposit_event(Event::Destroyed(class)); // NOTE: could use postinfo to reflect the actual number of // accounts/sufficient/approvals Ok(()) }) } /// Mint an asset instance of a particular class. /// /// The origin must be Signed and the sender must be the Issuer of the asset `class`. /// /// - `class`: The class of the asset to be minted. /// - `instance`: The instance value of the asset to be minted. /// - `beneficiary`: The initial owner of the minted asset. /// /// Emits `Issued` event when successful. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::mint())] pub fn mint( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, owner: ::Source, ) -> DispatchResult { let origin = ensure_signed(origin)?; let owner = T::Lookup::lookup(owner)?; Self::do_mint(class, instance, owner, |class_details| { ensure!(class_details.issuer == origin, Error::::NoPermission); Ok(()) }) } /// Destroy a single asset instance. /// /// Origin must be Signed and the sender should be the Admin of the asset `class`. /// /// - `class`: The class of the asset to be burned. /// - `instance`: The instance of the asset to be burned. /// - `check_owner`: If `Some` then the operation will fail with `WrongOwner` unless the /// asset is owned by this value. /// /// Emits `Burned` with the actual amount burned. /// /// Weight: `O(1)` /// Modes: `check_owner.is_some()`. #[pallet::weight(T::WeightInfo::burn())] pub fn burn( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, check_owner: Option<::Source>, ) -> DispatchResult { let origin = ensure_signed(origin)?; let check_owner = check_owner.map(T::Lookup::lookup).transpose()?; Self::do_burn(class, instance, |class_details, details| { let is_permitted = class_details.admin == origin || details.owner == origin; ensure!(is_permitted, Error::::NoPermission); ensure!( check_owner.map_or(true, |o| o == details.owner), Error::::WrongOwner ); Ok(()) }) } /// Move an asset from the sender account to another. /// /// Origin must be Signed and the signing account must be either: /// - the Admin of the asset `class`; /// - the Owner of the asset `instance`; /// - the approved delegate for the asset `instance` (in this case, the approval is reset). /// /// Arguments: /// - `class`: The class of the asset to be transferred. /// - `instance`: The instance of the asset to be transferred. /// - `dest`: The account to receive ownership of the asset. /// /// Emits `Transferred`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::transfer())] pub fn transfer( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, dest: ::Source, ) -> DispatchResult { let origin = ensure_signed(origin)?; let dest = T::Lookup::lookup(dest)?; Self::do_transfer(class, instance, dest, |class_details, details| { if details.owner != origin && class_details.admin != origin { let approved = details.approved.take().map_or(false, |i| i == origin); ensure!(approved, Error::::NoPermission); } Ok(()) }) } /// Reevaluate the deposits on some assets. /// /// Origin must be Signed and the sender should be the Owner of the asset `class`. /// /// - `class`: The class of the asset to be frozen. /// - `instances`: The instances of the asset class whose deposits will be reevaluated. /// /// NOTE: This exists as a best-effort function. Any asset instances which are unknown or /// in the case that the owner account does not have reservable funds to pay for a /// deposit increase are ignored. Generally the owner isn't going to call this on instances /// whose existing deposit is less than the refreshed deposit as it would only cost them, /// so it's of little consequence. /// /// It will still return an error in the case that the class is unknown of the signer is /// not permitted to call it. /// /// Weight: `O(instances.len())` #[pallet::weight(T::WeightInfo::redeposit(instances.len() as u32))] pub fn redeposit( origin: OriginFor, #[pallet::compact] class: T::ClassId, instances: Vec, ) -> DispatchResult { let origin = ensure_signed(origin)?; let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; ensure!(class_details.owner == origin, Error::::NoPermission); let deposit = match class_details.free_holding { true => Zero::zero(), false => T::InstanceDeposit::get(), }; let mut successful = Vec::with_capacity(instances.len()); for instance in instances.into_iter() { let mut details = match Asset::::get(&class, &instance) { Some(x) => x, None => continue, }; let old = details.deposit; if old > deposit { T::Currency::unreserve(&class_details.owner, old - deposit); } else if deposit > old { if T::Currency::reserve(&class_details.owner, deposit - old).is_err() { // NOTE: No alterations made to class_details in this iteration so far, so // this is OK to do. continue } } else { continue } class_details.total_deposit.saturating_accrue(deposit); class_details.total_deposit.saturating_reduce(old); details.deposit = deposit; Asset::::insert(&class, &instance, &details); successful.push(instance); } Class::::insert(&class, &class_details); Self::deposit_event(Event::::Redeposited(class, successful)); Ok(()) } /// Disallow further unprivileged transfer of an asset instance. /// /// Origin must be Signed and the sender should be the Freezer of the asset `class`. /// /// - `class`: The class of the asset to be frozen. /// - `instance`: The instance of the asset to be frozen. /// /// Emits `Frozen`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::freeze())] pub fn freeze( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, ) -> DispatchResult { let origin = ensure_signed(origin)?; let mut details = Asset::::get(&class, &instance).ok_or(Error::::Unknown)?; let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; ensure!(class_details.freezer == origin, Error::::NoPermission); details.is_frozen = true; Asset::::insert(&class, &instance, &details); Self::deposit_event(Event::::Frozen(class, instance)); Ok(()) } /// Re-allow unprivileged transfer of an asset instance. /// /// Origin must be Signed and the sender should be the Freezer of the asset `class`. /// /// - `class`: The class of the asset to be thawed. /// - `instance`: The instance of the asset to be thawed. /// /// Emits `Thawed`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::thaw())] pub fn thaw( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, ) -> DispatchResult { let origin = ensure_signed(origin)?; let mut details = Asset::::get(&class, &instance).ok_or(Error::::Unknown)?; let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; ensure!(class_details.admin == origin, Error::::NoPermission); details.is_frozen = false; Asset::::insert(&class, &instance, &details); Self::deposit_event(Event::::Thawed(class, instance)); Ok(()) } /// Disallow further unprivileged transfers for a whole asset class. /// /// Origin must be Signed and the sender should be the Freezer of the asset `class`. /// /// - `class`: The asset class to be frozen. /// /// Emits `ClassFrozen`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::freeze_class())] pub fn freeze_class( origin: OriginFor, #[pallet::compact] class: T::ClassId, ) -> DispatchResult { let origin = ensure_signed(origin)?; Class::::try_mutate(class, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &details.freezer, Error::::NoPermission); details.is_frozen = true; Self::deposit_event(Event::::ClassFrozen(class)); Ok(()) }) } /// Re-allow unprivileged transfers for a whole asset class. /// /// Origin must be Signed and the sender should be the Admin of the asset `class`. /// /// - `class`: The class to be thawed. /// /// Emits `ClassThawed`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::thaw_class())] pub fn thaw_class( origin: OriginFor, #[pallet::compact] class: T::ClassId, ) -> DispatchResult { let origin = ensure_signed(origin)?; Class::::try_mutate(class, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &details.admin, Error::::NoPermission); details.is_frozen = false; Self::deposit_event(Event::::ClassThawed(class)); Ok(()) }) } /// Change the Owner of an asset class. /// /// Origin must be Signed and the sender should be the Owner of the asset `class`. /// /// - `class`: The asset class whose owner should be changed. /// - `owner`: The new Owner of this asset class. /// /// Emits `OwnerChanged`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::transfer_ownership())] pub fn transfer_ownership( origin: OriginFor, #[pallet::compact] class: T::ClassId, owner: ::Source, ) -> DispatchResult { let origin = ensure_signed(origin)?; let owner = T::Lookup::lookup(owner)?; Class::::try_mutate(class, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &details.owner, Error::::NoPermission); if details.owner == owner { return Ok(()) } // Move the deposit to the new owner. T::Currency::repatriate_reserved( &details.owner, &owner, details.total_deposit, Reserved, )?; details.owner = owner.clone(); Self::deposit_event(Event::OwnerChanged(class, owner)); Ok(()) }) } /// Change the Issuer, Admin and Freezer of an asset class. /// /// Origin must be Signed and the sender should be the Owner of the asset `class`. /// /// - `class`: The asset class whose team should be changed. /// - `issuer`: The new Issuer of this asset class. /// - `admin`: The new Admin of this asset class. /// - `freezer`: The new Freezer of this asset class. /// /// Emits `TeamChanged`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::set_team())] pub fn set_team( origin: OriginFor, #[pallet::compact] class: T::ClassId, issuer: ::Source, admin: ::Source, freezer: ::Source, ) -> DispatchResult { let origin = ensure_signed(origin)?; let issuer = T::Lookup::lookup(issuer)?; let admin = T::Lookup::lookup(admin)?; let freezer = T::Lookup::lookup(freezer)?; Class::::try_mutate(class, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &details.owner, Error::::NoPermission); details.issuer = issuer.clone(); details.admin = admin.clone(); details.freezer = freezer.clone(); Self::deposit_event(Event::TeamChanged(class, issuer, admin, freezer)); Ok(()) }) } /// Approve an instance to be transferred by a delegated third-party account. /// /// Origin must be Signed and must be the owner of the asset `instance`. /// /// - `class`: The class of the asset to be approved for delegated transfer. /// - `instance`: The instance of the asset to be approved for delegated transfer. /// - `delegate`: The account to delegate permission to transfer the asset. /// /// Emits `ApprovedTransfer` on success. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::approve_transfer())] pub fn approve_transfer( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, delegate: ::Source, ) -> DispatchResult { let maybe_check: Option = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; let delegate = T::Lookup::lookup(delegate)?; let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; let mut details = Asset::::get(&class, &instance).ok_or(Error::::Unknown)?; if let Some(check) = maybe_check { let permitted = &check == &class_details.admin || &check == &details.owner; ensure!(permitted, Error::::NoPermission); } details.approved = Some(delegate); Asset::::insert(&class, &instance, &details); let delegate = details.approved.expect("set as Some above; qed"); Self::deposit_event(Event::ApprovedTransfer(class, instance, details.owner, delegate)); Ok(()) } /// Cancel the prior approval for the transfer of an asset by a delegate. /// /// Origin must be either: /// - the `Force` origin; /// - `Signed` with the signer being the Admin of the asset `class`; /// - `Signed` with the signer being the Owner of the asset `instance`; /// /// Arguments: /// - `class`: The class of the asset of whose approval will be cancelled. /// - `instance`: The instance of the asset of whose approval will be cancelled. /// - `maybe_check_delegate`: If `Some` will ensure that the given account is the one to /// which permission of transfer is delegated. /// /// Emits `ApprovalCancelled` on success. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::cancel_approval())] pub fn cancel_approval( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, maybe_check_delegate: Option<::Source>, ) -> DispatchResult { let maybe_check: Option = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; let mut details = Asset::::get(&class, &instance).ok_or(Error::::Unknown)?; if let Some(check) = maybe_check { let permitted = &check == &class_details.admin || &check == &details.owner; ensure!(permitted, Error::::NoPermission); } let maybe_check_delegate = maybe_check_delegate.map(T::Lookup::lookup).transpose()?; let old = details.approved.take().ok_or(Error::::NoDelegate)?; if let Some(check_delegate) = maybe_check_delegate { ensure!(check_delegate == old, Error::::WrongDelegate); } Asset::::insert(&class, &instance, &details); Self::deposit_event(Event::ApprovalCancelled(class, instance, details.owner, old)); Ok(()) } /// Alter the attributes of a given asset. /// /// Origin must be `ForceOrigin`. /// /// - `class`: The identifier of the asset. /// - `owner`: The new Owner of this asset. /// - `issuer`: The new Issuer of this asset. /// - `admin`: The new Admin of this asset. /// - `freezer`: The new Freezer of this asset. /// - `free_holding`: Whether a deposit is taken for holding an instance of this asset /// class. /// - `is_frozen`: Whether this asset class is frozen except for permissioned/admin /// instructions. /// /// Emits `AssetStatusChanged` with the identity of the asset. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::force_asset_status())] pub fn force_asset_status( origin: OriginFor, #[pallet::compact] class: T::ClassId, owner: ::Source, issuer: ::Source, admin: ::Source, freezer: ::Source, free_holding: bool, is_frozen: bool, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; Class::::try_mutate(class, |maybe_asset| { let mut asset = maybe_asset.take().ok_or(Error::::Unknown)?; asset.owner = T::Lookup::lookup(owner)?; asset.issuer = T::Lookup::lookup(issuer)?; asset.admin = T::Lookup::lookup(admin)?; asset.freezer = T::Lookup::lookup(freezer)?; asset.free_holding = free_holding; asset.is_frozen = is_frozen; *maybe_asset = Some(asset); Self::deposit_event(Event::AssetStatusChanged(class)); Ok(()) }) } /// Set an attribute for an asset class or instance. /// /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the /// asset `class`. /// /// If the origin is Signed, then funds of signer are reserved according to the formula: /// `MetadataDepositBase + DepositPerByte * (key.len + value.len)` taking into /// account any already reserved funds. /// /// - `class`: The identifier of the asset class whose instance's metadata to set. /// - `maybe_instance`: The identifier of the asset instance whose metadata to set. /// - `key`: The key of the attribute. /// - `value`: The value to which to set the attribute. /// /// Emits `AttributeSet`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::set_attribute())] pub fn set_attribute( origin: OriginFor, #[pallet::compact] class: T::ClassId, maybe_instance: Option, key: BoundedVec, value: BoundedVec, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &class_details.owner, Error::::NoPermission); } let maybe_is_frozen = match maybe_instance { None => ClassMetadataOf::::get(class).map(|v| v.is_frozen), Some(instance) => InstanceMetadataOf::::get(class, instance).map(|v| v.is_frozen), }; ensure!(!maybe_is_frozen.unwrap_or(false), Error::::Frozen); let attribute = Attribute::::get((class, maybe_instance, &key)); if attribute.is_none() { class_details.attributes.saturating_inc(); } let old_deposit = attribute.map_or(Zero::zero(), |m| m.1); class_details.total_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); if !class_details.free_holding && maybe_check_owner.is_some() { deposit = T::DepositPerByte::get() .saturating_mul(((key.len() + value.len()) as u32).into()) .saturating_add(T::AttributeDepositBase::get()); } class_details.total_deposit.saturating_accrue(deposit); if deposit > old_deposit { T::Currency::reserve(&class_details.owner, deposit - old_deposit)?; } else if deposit < old_deposit { T::Currency::unreserve(&class_details.owner, old_deposit - deposit); } Attribute::::insert((&class, maybe_instance, &key), (&value, deposit)); Class::::insert(class, &class_details); Self::deposit_event(Event::AttributeSet(class, maybe_instance, key, value)); Ok(()) } /// Set an attribute for an asset class or instance. /// /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the /// asset `class`. /// /// If the origin is Signed, then funds of signer are reserved according to the formula: /// `MetadataDepositBase + DepositPerByte * (key.len + value.len)` taking into /// account any already reserved funds. /// /// - `class`: The identifier of the asset class whose instance's metadata to set. /// - `instance`: The identifier of the asset instance whose metadata to set. /// - `key`: The key of the attribute. /// - `value`: The value to which to set the attribute. /// /// Emits `AttributeSet`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::clear_attribute())] pub fn clear_attribute( origin: OriginFor, #[pallet::compact] class: T::ClassId, maybe_instance: Option, key: BoundedVec, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &class_details.owner, Error::::NoPermission); } let maybe_is_frozen = match maybe_instance { None => ClassMetadataOf::::get(class).map(|v| v.is_frozen), Some(instance) => InstanceMetadataOf::::get(class, instance).map(|v| v.is_frozen), }; ensure!(!maybe_is_frozen.unwrap_or(false), Error::::Frozen); if let Some((_, deposit)) = Attribute::::take((class, maybe_instance, &key)) { class_details.attributes.saturating_dec(); class_details.total_deposit.saturating_reduce(deposit); T::Currency::unreserve(&class_details.owner, deposit); Class::::insert(class, &class_details); Self::deposit_event(Event::AttributeCleared(class, maybe_instance, key)); } Ok(()) } /// Set the metadata for an asset instance. /// /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the /// asset `class`. /// /// If the origin is Signed, then funds of signer are reserved according to the formula: /// `MetadataDepositBase + DepositPerByte * data.len` taking into /// account any already reserved funds. /// /// - `class`: The identifier of the asset class whose instance's metadata to set. /// - `instance`: The identifier of the asset instance whose metadata to set. /// - `data`: The general information of this asset. Limited in length by `StringLimit`. /// - `is_frozen`: Whether the metadata should be frozen against further changes. /// /// Emits `MetadataSet`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::set_metadata())] pub fn set_metadata( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, data: BoundedVec, is_frozen: bool, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &class_details.owner, Error::::NoPermission); } InstanceMetadataOf::::try_mutate_exists(class, instance, |metadata| { let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); if metadata.is_none() { class_details.instance_metadatas.saturating_inc(); } let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); class_details.total_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); if !class_details.free_holding && maybe_check_owner.is_some() { deposit = T::DepositPerByte::get() .saturating_mul(((data.len()) as u32).into()) .saturating_add(T::MetadataDepositBase::get()); } if deposit > old_deposit { T::Currency::reserve(&class_details.owner, deposit - old_deposit)?; } else if deposit < old_deposit { T::Currency::unreserve(&class_details.owner, old_deposit - deposit); } class_details.total_deposit.saturating_accrue(deposit); *metadata = Some(InstanceMetadata { deposit, data: data.clone(), is_frozen }); Class::::insert(&class, &class_details); Self::deposit_event(Event::MetadataSet(class, instance, data, is_frozen)); Ok(()) }) } /// Clear the metadata for an asset instance. /// /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the /// asset `instance`. /// /// Any deposit is freed for the asset class owner. /// /// - `class`: The identifier of the asset class whose instance's metadata to clear. /// - `instance`: The identifier of the asset instance whose metadata to clear. /// /// Emits `MetadataCleared`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::clear_metadata())] pub fn clear_metadata( origin: OriginFor, #[pallet::compact] class: T::ClassId, #[pallet::compact] instance: T::InstanceId, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &class_details.owner, Error::::NoPermission); } InstanceMetadataOf::::try_mutate_exists(class, instance, |metadata| { let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); if metadata.is_some() { class_details.instance_metadatas.saturating_dec(); } let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; T::Currency::unreserve(&class_details.owner, deposit); class_details.total_deposit.saturating_reduce(deposit); Class::::insert(&class, &class_details); Self::deposit_event(Event::MetadataCleared(class, instance)); Ok(()) }) } /// Set the metadata for an asset class. /// /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of /// the asset `class`. /// /// If the origin is `Signed`, then funds of signer are reserved according to the formula: /// `MetadataDepositBase + DepositPerByte * data.len` taking into /// account any already reserved funds. /// /// - `class`: The identifier of the asset whose metadata to update. /// - `data`: The general information of this asset. Limited in length by `StringLimit`. /// - `is_frozen`: Whether the metadata should be frozen against further changes. /// /// Emits `ClassMetadataSet`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::set_class_metadata())] pub fn set_class_metadata( origin: OriginFor, #[pallet::compact] class: T::ClassId, data: BoundedVec, is_frozen: bool, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; let mut details = Class::::get(&class).ok_or(Error::::Unknown)?; if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &details.owner, Error::::NoPermission); } ClassMetadataOf::::try_mutate_exists(class, |metadata| { let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); details.total_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); if maybe_check_owner.is_some() && !details.free_holding { deposit = T::DepositPerByte::get() .saturating_mul(((data.len()) as u32).into()) .saturating_add(T::MetadataDepositBase::get()); } if deposit > old_deposit { T::Currency::reserve(&details.owner, deposit - old_deposit)?; } else if deposit < old_deposit { T::Currency::unreserve(&details.owner, old_deposit - deposit); } details.total_deposit.saturating_accrue(deposit); Class::::insert(&class, details); *metadata = Some(ClassMetadata { deposit, data: data.clone(), is_frozen }); Self::deposit_event(Event::ClassMetadataSet(class, data, is_frozen)); Ok(()) }) } /// Clear the metadata for an asset class. /// /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of /// the asset `class`. /// /// Any deposit is freed for the asset class owner. /// /// - `class`: The identifier of the asset class whose metadata to clear. /// /// Emits `ClassMetadataCleared`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::clear_class_metadata())] pub fn clear_class_metadata( origin: OriginFor, #[pallet::compact] class: T::ClassId, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some))?; let details = Class::::get(&class).ok_or(Error::::Unknown)?; if let Some(check_owner) = &maybe_check_owner { ensure!(check_owner == &details.owner, Error::::NoPermission); } ClassMetadataOf::::try_mutate_exists(class, |metadata| { let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; T::Currency::unreserve(&details.owner, deposit); Self::deposit_event(Event::ClassMetadataCleared(class)); Ok(()) }) } } }