difftreelog
doc: add requested code review changes
in: master
2 files changed
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -137,6 +137,7 @@
/// @dev Throws error if key not found
/// @param tokenId ID of the token.
/// @param key Property key.
+ /// @return Property value bytes
fn property(&self, token_id: uint256, key: string) -> Result<bytes> {
let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
let key = <Vec<u8>>::from(key)
pallets/nonfungible/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//! # Nonfungible Pallet18//!19//! The Nonfungible pallet provides functionality for handling nonfungible collections and tokens.20//!21//! - [`Config`]22//! - [`NonfungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Nonfungible pallet provides functions for:29//!30//! - NFT collection creation and removal31//! - Minting and burning of NFT tokens32//! - Retrieving account balances33//! - Transfering NFT tokens34//! - Setting and checking allowance for NFT tokens35//! - Setting properties and permissions for NFT collections and tokens36//! - Nesting and unnesting tokens37//!38//! ### Terminology39//!40//! - **NFT token:** Non fungible token.41//!42//! - **NFT Collection:** A collection of NFT tokens. All NFT tokens are part of a collection.43//! Each collection can define it's own properties, properties for it's tokens and set of permissions.44//!45//! - **Balance:** Number of NFT tokens owned by an account46//!47//! - **Allowance:** NFT tokens owned by one account that another account is allowed to make operations on48//!49//! - **Burning:** The process of “deleting” a token from a collection and from50//! an account balance of the owner.51//!52//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting53//! owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in54//! it's child token i.e. parent-child relationship graph shouldn't have cycles.55//!56//! - **Properties:** Key-Values pairs. Token properties are attached to a token. Collection properties are57//! attached to a collection. Set of permissions could be defined for each property.58//!59//! ### Implementations60//!61//! The Nonfungible pallet provides implementations for the following traits. If these traits provide62//! the functionality that you need, then you can avoid coupling with the Nonfungible pallet.63//!64//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight65//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing66//! with collections67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create NFT collection. NFT collection can be configured to allow or deny access for73//! some accounts.74//! - `destroy_collection` - Destroy exising NFT collection. There should be no tokens in the collection.75//! - `burn` - Burn NFT token owned by account.76//! - `transfer` - Transfer NFT token. Transfers should be enabled for NFT collection.77//! Nests the NFT token if it is sent to another token.78//! - `create_item` - Mint NFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account.80//! - `set_token_property` - Set token property value.81//! - `delete_token_property` - Remove property from the token.82//! - `set_collection_properties` - Set collection properties.83//! - `delete_collection_properties` - Remove properties from the collection.84//! - `set_property_permission` - Set collection property permission.85//! - `set_token_property_permissions` - Set token property permissions.86//!87//! ## Assumptions88//!89//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.90//! * Sender should be in collection's allow list to perform operations on tokens.9192#![cfg_attr(not(feature = "std"), no_std)]9394use erc::ERC721Events;95use evm_coder::ToLog;96use frame_support::{97 BoundedVec, ensure, fail, transactional,98 storage::with_transaction,99 pallet_prelude::DispatchResultWithPostInfo,100 pallet_prelude::Weight,101 weights::{PostDispatchInfo, Pays},102};103use up_data_structs::{104 AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,105 mapping::TokenAddressMapping, budget::Budget, Property, PropertyPermission, PropertyKey,106 PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild,107 AuxPropertyValue,108};109use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};110use pallet_common::{111 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,112 eth::collection_id_to_address,113};114use pallet_structure::{Pallet as PalletStructure, Error as StructureError};115use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};116use sp_core::H160;117use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};118use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap, collections::btree_set::BTreeSet};119use core::ops::Deref;120use codec::{Encode, Decode, MaxEncodedLen};121use scale_info::TypeInfo;122123pub use pallet::*;124use weights::WeightInfo;125#[cfg(feature = "runtime-benchmarks")]126pub mod benchmarking;127pub mod common;128pub mod erc;129pub mod weights;130131pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;132pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;133134#[struct_versioning::versioned(version = 2, upper)]135#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]136pub struct ItemData<CrossAccountId> {137 #[version(..2)]138 pub const_data: BoundedVec<u8, CustomDataLimit>,139140 #[version(..2)]141 pub variable_data: BoundedVec<u8, CustomDataLimit>,142143 pub owner: CrossAccountId,144}145146#[frame_support::pallet]147pub mod pallet {148 use super::*;149 use frame_support::{150 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,151 };152 use frame_system::pallet_prelude::*;153 use up_data_structs::{CollectionId, TokenId};154 use super::weights::WeightInfo;155156 #[pallet::error]157 pub enum Error<T> {158 /// Not Nonfungible item data used to mint in Nonfungible collection.159 NotNonfungibleDataUsedToMintFungibleCollectionToken,160 /// Used amount > 1 with NFT161 NonfungibleItemsHaveNoAmount,162 /// Unable to burn NFT with children163 CantBurnNftWithChildren,164 }165166 #[pallet::config]167 pub trait Config:168 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config169 {170 type WeightInfo: WeightInfo;171 }172173 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);174175 #[pallet::pallet]176 #[pallet::storage_version(STORAGE_VERSION)]177 #[pallet::generate_store(pub(super) trait Store)]178 pub struct Pallet<T>(_);179180 /// Amount of tokens minted for collection.181 #[pallet::storage]182 pub type TokensMinted<T: Config> =183 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;184185 /// Amount of burnt tokens for collection.186 #[pallet::storage]187 pub type TokensBurnt<T: Config> =188 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;189190 /// Custom data serialized to bytes for token.191 #[pallet::storage]192 pub type TokenData<T: Config> = StorageNMap<193 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),194 Value = ItemData<T::CrossAccountId>,195 QueryKind = OptionQuery,196 >;197198 /// Key-Value map stored for token.199 #[pallet::storage]200 #[pallet::getter(fn token_properties)]201 pub type TokenProperties<T: Config> = StorageNMap<202 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),203 Value = Properties,204 QueryKind = ValueQuery,205 OnEmpty = up_data_structs::TokenProperties,206 >;207208 /// Custom data that is serialized to bytes and attached to a token property.209 #[pallet::storage]210 #[pallet::getter(fn token_aux_property)]211 pub type TokenAuxProperties<T: Config> = StorageNMap<212 Key = (213 Key<Twox64Concat, CollectionId>,214 Key<Twox64Concat, TokenId>,215 Key<Twox64Concat, PropertyScope>,216 Key<Twox64Concat, PropertyKey>,217 ),218 Value = AuxPropertyValue,219 QueryKind = OptionQuery,220 >;221222 /// Used to enumerate tokens owned by account.223 #[pallet::storage]224 pub type Owned<T: Config> = StorageNMap<225 Key = (226 Key<Twox64Concat, CollectionId>,227 Key<Blake2_128Concat, T::CrossAccountId>,228 Key<Twox64Concat, TokenId>,229 ),230 Value = bool,231 QueryKind = ValueQuery,232 >;233234 /// Used to enumerate token's children.235 #[pallet::storage]236 #[pallet::getter(fn token_children)]237 pub type TokenChildren<T: Config> = StorageNMap<238 Key = (239 Key<Twox64Concat, CollectionId>,240 Key<Twox64Concat, TokenId>,241 Key<Twox64Concat, (CollectionId, TokenId)>,242 ),243 Value = bool,244 QueryKind = ValueQuery,245 >;246247 /// Amount of tokens owned by account.248 #[pallet::storage]249 pub type AccountBalance<T: Config> = StorageNMap<250 Key = (251 Key<Twox64Concat, CollectionId>,252 Key<Blake2_128Concat, T::CrossAccountId>,253 ),254 Value = u32,255 QueryKind = ValueQuery,256 >;257258 /// Allowance set by an owner for a spender for a token.259 #[pallet::storage]260 pub type Allowance<T: Config> = StorageNMap<261 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),262 Value = T::CrossAccountId,263 QueryKind = OptionQuery,264 >;265266 #[pallet::hooks]267 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {268 fn on_runtime_upgrade() -> Weight {269 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {270 let mut had_consts = BTreeSet::new();271 <TokenData<T>>::translate::<ItemDataVersion1<T::CrossAccountId>, _>(272 |(collection, token), v| {273 let mut props = vec![];274 if !v.const_data.is_empty() {275 props.push(Property {276 key: b"_old_constData".to_vec().try_into().unwrap(),277 value: v278 .const_data279 .clone()280 .into_inner()281 .try_into()282 .expect("const too long"),283 });284 had_consts.insert(collection);285 }286 if !v.variable_data.is_empty() {287 props.push(Property {288 key: b"_old_variableData".to_vec().try_into().unwrap(),289 value: v290 .variable_data291 .clone()292 .into_inner()293 .try_into()294 .expect("variable too long"),295 })296 }297 if !props.is_empty() {298 Self::set_scoped_token_properties(299 collection,300 token,301 PropertyScope::None,302 props.into_iter(),303 )304 .expect("existing token data exceeds property storage");305 }306 Some(<ItemDataVersion2<T::CrossAccountId>>::from(v))307 },308 );309 for collection in had_consts {310 <PalletCommon<T>>::set_property_permission_unchecked(311 collection,312 PropertyKeyPermission {313 key: b"_old_constData".to_vec().try_into().unwrap(),314 permission: PropertyPermission {315 mutable: false,316 collection_admin: true,317 token_owner: false,318 },319 },320 )321 .expect("failed to configure permission");322 }323 }324325 0326 }327 }328}329330pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);331impl<T: Config> NonfungibleHandle<T> {332 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {333 Self(inner)334 }335 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {336 self.0337 }338 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {339 &mut self.0340 }341}342impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {343 fn recorder(&self) -> &SubstrateRecorder<T> {344 self.0.recorder()345 }346 fn into_recorder(self) -> SubstrateRecorder<T> {347 self.0.into_recorder()348 }349}350impl<T: Config> Deref for NonfungibleHandle<T> {351 type Target = pallet_common::CollectionHandle<T>;352353 fn deref(&self) -> &Self::Target {354 &self.0355 }356}357358impl<T: Config> Pallet<T> {359 /// Get number of NFT tokens in collection.360 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {361 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)362 }363364 /// Check that NFT token exists.365 ///366 /// - `token`: Token ID.367 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {368 <TokenData<T>>::contains_key((collection.id, token))369 }370371 /// Set the token property with the scope.372 ///373 /// - `property`: Contains key-value pair.374 pub fn set_scoped_token_property(375 collection_id: CollectionId,376 token_id: TokenId,377 scope: PropertyScope,378 property: Property,379 ) -> DispatchResult {380 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {381 properties.try_scoped_set(scope, property.key, property.value)382 })383 .map_err(<CommonError<T>>::from)?;384385 Ok(())386 }387388 /// Batch operation to set multiple properties with the same scope.389 pub fn set_scoped_token_properties(390 collection_id: CollectionId,391 token_id: TokenId,392 scope: PropertyScope,393 properties: impl Iterator<Item = Property>,394 ) -> DispatchResult {395 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {396 stored_properties.try_scoped_set_from_iter(scope, properties)397 })398 .map_err(<CommonError<T>>::from)?;399400 Ok(())401 }402403 /// Add or edit auxiliary data for the property.404 ///405 /// - `f`: function that adds or edits auxiliary data.406 pub fn try_mutate_token_aux_property<R, E>(407 collection_id: CollectionId,408 token_id: TokenId,409 scope: PropertyScope,410 key: PropertyKey,411 f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,412 ) -> Result<R, E> {413 <TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)414 }415416 /// Remove auxiliary data for the property.417 pub fn remove_token_aux_property(418 collection_id: CollectionId,419 token_id: TokenId,420 scope: PropertyScope,421 key: PropertyKey,422 ) {423 <TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));424 }425426 /// Get all auxiliary data in a given scope.427 ///428 /// Returns iterator over Property Key - Data pairs.429 pub fn iterate_token_aux_properties(430 collection_id: CollectionId,431 token_id: TokenId,432 scope: PropertyScope,433 ) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {434 <TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))435 }436437 /// Get ID of the last minted token438 pub fn current_token_id(collection_id: CollectionId) -> TokenId {439 TokenId(<TokensMinted<T>>::get(collection_id))440 }441}442443// unchecked calls skips any permission checks444impl<T: Config> Pallet<T> {445 /// Create ТFT collection446 ///447 /// `init_collection` will take non-refundable deposit for collection creation.448 ///449 /// - `data`: Contains settings for collection limits and permissions.450 pub fn init_collection(451 owner: T::CrossAccountId,452 data: CreateCollectionData<T::AccountId>,453 is_external: bool,454 ) -> Result<CollectionId, DispatchError> {455 <PalletCommon<T>>::init_collection(owner, data, is_external)456 }457458 /// Destroy ТFT collection459 ///460 /// `destroy_collection` will throw error if collection contains any tokens.461 /// Only owner can destroy collection.462 pub fn destroy_collection(463 collection: NonfungibleHandle<T>,464 sender: &T::CrossAccountId,465 ) -> DispatchResult {466 let id = collection.id;467468 if Self::collection_has_tokens(id) {469 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());470 }471472 // =========473474 PalletCommon::destroy_collection(collection.0, sender)?;475476 <TokenData<T>>::remove_prefix((id,), None);477 <TokenChildren<T>>::remove_prefix((id,), None);478 <Owned<T>>::remove_prefix((id,), None);479 <TokensMinted<T>>::remove(id);480 <TokensBurnt<T>>::remove(id);481 <Allowance<T>>::remove_prefix((id,), None);482 <AccountBalance<T>>::remove_prefix((id,), None);483 Ok(())484 }485486 /// Burn NFT token487 ///488 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token489 /// if the token is nested.490 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.491 /// Also removes all corresponding properties and auxiliary properties.492 ///493 /// - `token`: Token that should be burned494 /// - `collection`: Collection that contains the token495 pub fn burn(496 collection: &NonfungibleHandle<T>,497 sender: &T::CrossAccountId,498 token: TokenId,499 ) -> DispatchResult {500 let token_data =501 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;502 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);503504 if collection.permissions.access() == AccessMode::AllowList {505 collection.check_allowlist(sender)?;506 }507508 if Self::token_has_children(collection.id, token) {509 return Err(<Error<T>>::CantBurnNftWithChildren.into());510 }511512 let burnt = <TokensBurnt<T>>::get(collection.id)513 .checked_add(1)514 .ok_or(ArithmeticError::Overflow)?;515516 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))517 .checked_sub(1)518 .ok_or(ArithmeticError::Overflow)?;519520 // =========521522 if balance == 0 {523 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));524 } else {525 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);526 }527528 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);529530 <Owned<T>>::remove((collection.id, &token_data.owner, token));531 <TokensBurnt<T>>::insert(collection.id, burnt);532 <TokenData<T>>::remove((collection.id, token));533 <TokenProperties<T>>::remove((collection.id, token));534 <TokenAuxProperties<T>>::remove_prefix((collection.id, token), None);535 let old_spender = <Allowance<T>>::take((collection.id, token));536537 if let Some(old_spender) = old_spender {538 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(539 collection.id,540 token,541 token_data.owner.clone(),542 old_spender,543 0,544 ));545 }546547 <PalletEvm<T>>::deposit_log(548 ERC721Events::Transfer {549 from: *token_data.owner.as_eth(),550 to: H160::default(),551 token_id: token.into(),552 }553 .to_log(collection_id_to_address(collection.id)),554 );555 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(556 collection.id,557 token,558 token_data.owner,559 1,560 ));561 Ok(())562 }563564 /// Same as [`burn`] but burns all the tokens that are nested in the token first565 ///566 /// - `self_budget`: Limit for searching children in depth.567 /// - `breadth_budget`: Limit of breadth of searching children.568 ///569 /// [`burn`]: struct.Pallet.html#method.burn570 #[transactional]571 pub fn burn_recursively(572 collection: &NonfungibleHandle<T>,573 sender: &T::CrossAccountId,574 token: TokenId,575 self_budget: &dyn Budget,576 breadth_budget: &dyn Budget,577 ) -> DispatchResultWithPostInfo {578 ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);579580 let current_token_account =581 T::CrossTokenAddressMapping::token_to_address(collection.id, token);582583 let mut weight = 0 as Weight;584585 // This method is transactional, if user in fact doesn't have permissions to remove token -586 // tokens removed here will be restored after rejected transaction587 for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {588 ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);589 let PostDispatchInfo { actual_weight, .. } =590 <PalletStructure<T>>::burn_item_recursively(591 current_token_account.clone(),592 collection,593 token,594 self_budget,595 breadth_budget,596 )?;597 if let Some(actual_weight) = actual_weight {598 weight = weight.saturating_add(actual_weight);599 }600 }601602 Self::burn(collection, sender, token)?;603 DispatchResultWithPostInfo::Ok(PostDispatchInfo {604 actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),605 pays_fee: Pays::Yes,606 })607 }608609 /// Batch operation to add, edit or remove properties for the token610 ///611 /// All affected properties should have mutable permission and sender should have612 /// permission to edit those properties.613 ///614 /// - `nesting_budget`: Limit for searching parents in depth to check ownership.615 /// - `is_token_create`: Indicates that method is called during token initialization.616 /// Allows to bypass ownership check.617 #[transactional]618 fn modify_token_properties(619 collection: &NonfungibleHandle<T>,620 sender: &T::CrossAccountId,621 token_id: TokenId,622 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,623 is_token_create: bool,624 nesting_budget: &dyn Budget,625 ) -> DispatchResult {626 let mut collection_admin_status = None;627 let mut token_owner_result = None;628629 let mut is_collection_admin =630 || *collection_admin_status.get_or_insert_with(|| collection.is_owner_or_admin(sender));631632 let mut is_token_owner = || {633 *token_owner_result.get_or_insert_with(|| -> Result<bool, DispatchError> {634 let is_owned = <PalletStructure<T>>::check_indirectly_owned(635 sender.clone(),636 collection.id,637 token_id,638 None,639 nesting_budget,640 )?;641642 Ok(is_owned)643 })644 };645646 for (key, value) in properties {647 let permission = <PalletCommon<T>>::property_permissions(collection.id)648 .get(&key)649 .cloned()650 .unwrap_or_else(PropertyPermission::none);651652 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))653 .get(&key)654 .is_some();655656 match permission {657 PropertyPermission { mutable: false, .. } if is_property_exists => {658 return Err(<CommonError<T>>::NoPermission.into());659 }660661 PropertyPermission {662 collection_admin,663 token_owner,664 ..665 } => {666 //TODO: investigate threats during public minting.667 if is_token_create && (collection_admin || token_owner) && value.is_some() {668 // Pass669 } else if collection_admin && is_collection_admin() {670 // Pass671 } else if token_owner && is_token_owner()? {672 // Pass673 } else {674 fail!(<CommonError<T>>::NoPermission);675 }676 }677 }678679 match value {680 Some(value) => {681 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {682 properties.try_set(key.clone(), value)683 })684 .map_err(<CommonError<T>>::from)?;685686 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(687 collection.id,688 token_id,689 key,690 ));691 }692 None => {693 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {694 properties.remove(&key)695 })696 .map_err(<CommonError<T>>::from)?;697698 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(699 collection.id,700 token_id,701 key,702 ));703 }704 }705 }706707 Ok(())708 }709710 /// Batch operation to add or edit properties for the token711 ///712 /// Same as [`modify_token_properties`] but doesn't allow to remove properties713 ///714 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties715 pub fn set_token_properties(716 collection: &NonfungibleHandle<T>,717 sender: &T::CrossAccountId,718 token_id: TokenId,719 properties: impl Iterator<Item = Property>,720 is_token_create: bool,721 nesting_budget: &dyn Budget,722 ) -> DispatchResult {723 Self::modify_token_properties(724 collection,725 sender,726 token_id,727 properties.map(|p| (p.key, Some(p.value))),728 is_token_create,729 nesting_budget,730 )731 }732733 /// Add or edit single property for the token734 ///735 /// Calls [`set_token_properties`] internally736 ///737 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties738 pub fn set_token_property(739 collection: &NonfungibleHandle<T>,740 sender: &T::CrossAccountId,741 token_id: TokenId,742 property: Property,743 nesting_budget: &dyn Budget,744 ) -> DispatchResult {745 let is_token_create = false;746747 Self::set_token_properties(748 collection,749 sender,750 token_id,751 [property].into_iter(),752 is_token_create,753 nesting_budget,754 )755 }756757 /// Batch operation to remove properties from the token758 ///759 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties760 ///761 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties762 pub fn delete_token_properties(763 collection: &NonfungibleHandle<T>,764 sender: &T::CrossAccountId,765 token_id: TokenId,766 property_keys: impl Iterator<Item = PropertyKey>,767 nesting_budget: &dyn Budget,768 ) -> DispatchResult {769 let is_token_create = false;770771 Self::modify_token_properties(772 collection,773 sender,774 token_id,775 property_keys.into_iter().map(|key| (key, None)),776 is_token_create,777 nesting_budget,778 )779 }780781 /// Remove single property from the token782 ///783 /// Calls [`delete_token_properties`] internally784 ///785 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties786 pub fn delete_token_property(787 collection: &NonfungibleHandle<T>,788 sender: &T::CrossAccountId,789 token_id: TokenId,790 property_key: PropertyKey,791 nesting_budget: &dyn Budget,792 ) -> DispatchResult {793 Self::delete_token_properties(794 collection,795 sender,796 token_id,797 [property_key].into_iter(),798 nesting_budget,799 )800 }801802 /// Add or edit properties for the collection803 pub fn set_collection_properties(804 collection: &NonfungibleHandle<T>,805 sender: &T::CrossAccountId,806 properties: Vec<Property>,807 ) -> DispatchResult {808 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)809 }810811 /// Remove properties from the collection812 pub fn delete_collection_properties(813 collection: &CollectionHandle<T>,814 sender: &T::CrossAccountId,815 property_keys: Vec<PropertyKey>,816 ) -> DispatchResult {817 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)818 }819820 /// Set property permissions for the token.821 ///822 /// Sender should be the owner or admin of token's collection.823 pub fn set_token_property_permissions(824 collection: &CollectionHandle<T>,825 sender: &T::CrossAccountId,826 property_permissions: Vec<PropertyKeyPermission>,827 ) -> DispatchResult {828 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)829 }830831 /// Set property permissions for the collection.832 ///833 /// Sender should be the owner or admin of the collection.834 pub fn set_property_permission(835 collection: &CollectionHandle<T>,836 sender: &T::CrossAccountId,837 permission: PropertyKeyPermission,838 ) -> DispatchResult {839 <PalletCommon<T>>::set_property_permission(collection, sender, permission)840 }841842 /// Transfer NFT token from one account to another.843 ///844 /// `from` account stops being the owner and `to` account becomes the owner of the token.845 /// If `to` is token than `to` becomes owner of the token and the token become nested.846 /// Unnests token from previous parent if it was nested before.847 /// Removes allowance for the token if there was any.848 ///849 /// - `nesting_budget`: Limit for token nesting depth850 pub fn transfer(851 collection: &NonfungibleHandle<T>,852 from: &T::CrossAccountId,853 to: &T::CrossAccountId,854 token: TokenId,855 nesting_budget: &dyn Budget,856 ) -> DispatchResult {857 ensure!(858 collection.limits.transfers_enabled(),859 <CommonError<T>>::TransferNotAllowed860 );861862 let token_data =863 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;864 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);865866 if collection.permissions.access() == AccessMode::AllowList {867 collection.check_allowlist(from)?;868 collection.check_allowlist(to)?;869 }870 <PalletCommon<T>>::ensure_correct_receiver(to)?;871872 let balance_from = <AccountBalance<T>>::get((collection.id, from))873 .checked_sub(1)874 .ok_or(<CommonError<T>>::TokenValueTooLow)?;875 let balance_to = if from != to {876 let balance_to = <AccountBalance<T>>::get((collection.id, to))877 .checked_add(1)878 .ok_or(ArithmeticError::Overflow)?;879880 ensure!(881 balance_to < collection.limits.account_token_ownership_limit(),882 <CommonError<T>>::AccountTokenLimitExceeded,883 );884885 Some(balance_to)886 } else {887 None888 };889890 <PalletStructure<T>>::nest_if_sent_to_token(891 from.clone(),892 to,893 collection.id,894 token,895 nesting_budget,896 )?;897898 // =========899900 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);901902 <TokenData<T>>::insert(903 (collection.id, token),904 ItemData {905 owner: to.clone(),906 ..token_data907 },908 );909910 if let Some(balance_to) = balance_to {911 // from != to912 if balance_from == 0 {913 <AccountBalance<T>>::remove((collection.id, from));914 } else {915 <AccountBalance<T>>::insert((collection.id, from), balance_from);916 }917 <AccountBalance<T>>::insert((collection.id, to), balance_to);918 <Owned<T>>::remove((collection.id, from, token));919 <Owned<T>>::insert((collection.id, to, token), true);920 }921 Self::set_allowance_unchecked(collection, from, token, None, true);922923 <PalletEvm<T>>::deposit_log(924 ERC721Events::Transfer {925 from: *from.as_eth(),926 to: *to.as_eth(),927 token_id: token.into(),928 }929 .to_log(collection_id_to_address(collection.id)),930 );931 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(932 collection.id,933 token,934 from.clone(),935 to.clone(),936 1,937 ));938 Ok(())939 }940941 /// Batch operation to mint multiple NFT tokens.942 ///943 /// The sender should be the owner/admin of the collection or collection should be configured944 /// to allow public minting.945 ///946 /// - `data`: Contains list of token properties and users who will become the owners of the corresponging tokens.947 /// - `nesting_budget`: Limit for token nesting depth948 pub fn create_multiple_items(949 collection: &NonfungibleHandle<T>,950 sender: &T::CrossAccountId,951 data: Vec<CreateItemData<T>>,952 nesting_budget: &dyn Budget,953 ) -> DispatchResult {954 if !collection.is_owner_or_admin(sender) {955 ensure!(956 collection.permissions.mint_mode(),957 <CommonError<T>>::PublicMintingNotAllowed958 );959 collection.check_allowlist(sender)?;960961 for item in data.iter() {962 collection.check_allowlist(&item.owner)?;963 }964 }965966 for data in data.iter() {967 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;968 }969970 let first_token = <TokensMinted<T>>::get(collection.id);971 let tokens_minted = first_token972 .checked_add(data.len() as u32)973 .ok_or(ArithmeticError::Overflow)?;974 ensure!(975 tokens_minted <= collection.limits.token_limit(),976 <CommonError<T>>::CollectionTokenLimitExceeded977 );978979 let mut balances = BTreeMap::new();980 for data in &data {981 let balance = balances982 .entry(&data.owner)983 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));984 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;985986 ensure!(987 *balance <= collection.limits.account_token_ownership_limit(),988 <CommonError<T>>::AccountTokenLimitExceeded,989 );990 }991992 for (i, data) in data.iter().enumerate() {993 let token = TokenId(first_token + i as u32 + 1);994995 <PalletStructure<T>>::check_nesting(996 sender.clone(),997 &data.owner,998 collection.id,999 token,1000 nesting_budget,1001 )?;1002 }10031004 // =========10051006 with_transaction(|| {1007 for (i, data) in data.iter().enumerate() {1008 let token = first_token + i as u32 + 1;10091010 <TokenData<T>>::insert(1011 (collection.id, token),1012 ItemData {1013 // const_data: data.const_data.clone(),1014 owner: data.owner.clone(),1015 },1016 );10171018 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(1019 &data.owner,1020 collection.id,1021 TokenId(token),1022 );10231024 if let Err(e) = Self::set_token_properties(1025 collection,1026 sender,1027 TokenId(token),1028 data.properties.clone().into_iter(),1029 true,1030 nesting_budget,1031 ) {1032 return TransactionOutcome::Rollback(Err(e));1033 }1034 }1035 TransactionOutcome::Commit(Ok(()))1036 })?;10371038 <TokensMinted<T>>::insert(collection.id, tokens_minted);1039 for (account, balance) in balances {1040 <AccountBalance<T>>::insert((collection.id, account), balance);1041 }1042 for (i, data) in data.into_iter().enumerate() {1043 let token = first_token + i as u32 + 1;1044 <Owned<T>>::insert((collection.id, &data.owner, token), true);10451046 <PalletEvm<T>>::deposit_log(1047 ERC721Events::Transfer {1048 from: H160::default(),1049 to: *data.owner.as_eth(),1050 token_id: token.into(),1051 }1052 .to_log(collection_id_to_address(collection.id)),1053 );1054 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1055 collection.id,1056 TokenId(token),1057 data.owner.clone(),1058 1,1059 ));1060 }1061 Ok(())1062 }10631064 pub fn set_allowance_unchecked(1065 collection: &NonfungibleHandle<T>,1066 sender: &T::CrossAccountId,1067 token: TokenId,1068 spender: Option<&T::CrossAccountId>,1069 assume_implicit_eth: bool,1070 ) {1071 if let Some(spender) = spender {1072 let old_spender = <Allowance<T>>::get((collection.id, token));1073 <Allowance<T>>::insert((collection.id, token), spender);1074 // In ERC721 there is only one possible approved user of token, so we set1075 // approved user to spender1076 <PalletEvm<T>>::deposit_log(1077 ERC721Events::Approval {1078 owner: *sender.as_eth(),1079 approved: *spender.as_eth(),1080 token_id: token.into(),1081 }1082 .to_log(collection_id_to_address(collection.id)),1083 );1084 // In Unique chain, any token can have any amount of approved users, so we need to1085 // set allowance of old owner to 0, and allowance of new owner to 11086 if old_spender.as_ref() != Some(spender) {1087 if let Some(old_owner) = old_spender {1088 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1089 collection.id,1090 token,1091 sender.clone(),1092 old_owner,1093 0,1094 ));1095 }1096 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1097 collection.id,1098 token,1099 sender.clone(),1100 spender.clone(),1101 1,1102 ));1103 }1104 } else {1105 let old_spender = <Allowance<T>>::take((collection.id, token));1106 if !assume_implicit_eth {1107 // In ERC721 there is only one possible approved user of token, so we set1108 // approved user to zero address1109 <PalletEvm<T>>::deposit_log(1110 ERC721Events::Approval {1111 owner: *sender.as_eth(),1112 approved: H160::default(),1113 token_id: token.into(),1114 }1115 .to_log(collection_id_to_address(collection.id)),1116 );1117 }1118 // In Unique chain, any token can have any amount of approved users, so we need to1119 // set allowance of old owner to 01120 if let Some(old_spender) = old_spender {1121 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1122 collection.id,1123 token,1124 sender.clone(),1125 old_spender,1126 0,1127 ));1128 }1129 }1130 }11311132 /// Set allowance for the spender to `transfer` or `burn` sender's token.1133 ///1134 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1135 pub fn set_allowance(1136 collection: &NonfungibleHandle<T>,1137 sender: &T::CrossAccountId,1138 token: TokenId,1139 spender: Option<&T::CrossAccountId>,1140 ) -> DispatchResult {1141 if collection.permissions.access() == AccessMode::AllowList {1142 collection.check_allowlist(sender)?;1143 if let Some(spender) = spender {1144 collection.check_allowlist(spender)?;1145 }1146 }11471148 if let Some(spender) = spender {1149 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1150 }11511152 let token_data =1153 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1154 if &token_data.owner != sender {1155 ensure!(1156 collection.ignores_owned_amount(sender),1157 <CommonError<T>>::CantApproveMoreThanOwned1158 );1159 }11601161 // =========11621163 Self::set_allowance_unchecked(collection, sender, token, spender, false);1164 Ok(())1165 }11661167 /// Checks allowance for the spender to use the token.1168 fn check_allowed(1169 collection: &NonfungibleHandle<T>,1170 spender: &T::CrossAccountId,1171 from: &T::CrossAccountId,1172 token: TokenId,1173 nesting_budget: &dyn Budget,1174 ) -> DispatchResult {1175 if spender.conv_eq(from) {1176 return Ok(());1177 }1178 if collection.permissions.access() == AccessMode::AllowList {1179 // `from`, `to` checked in [`transfer`]1180 collection.check_allowlist(spender)?;1181 }11821183 if collection.limits.owner_can_transfer() && collection.is_owner_or_admin(spender) {1184 return Ok(());1185 }11861187 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1188 ensure!(1189 <PalletStructure<T>>::check_indirectly_owned(1190 spender.clone(),1191 source.0,1192 source.1,1193 None,1194 nesting_budget1195 )?,1196 <CommonError<T>>::ApprovedValueTooLow,1197 );1198 return Ok(());1199 }1200 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1201 return Ok(());1202 }1203 ensure!(1204 collection.ignores_allowance(spender),1205 <CommonError<T>>::ApprovedValueTooLow1206 );1207 Ok(())1208 }12091210 /// Transfer NFT token from one account to another.1211 ///1212 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1213 /// The owner should set allowance for the spender to transfer token.1214 ///1215 /// [`transfer`]: struct.Pallet.html#method.transfer1216 pub fn transfer_from(1217 collection: &NonfungibleHandle<T>,1218 spender: &T::CrossAccountId,1219 from: &T::CrossAccountId,1220 to: &T::CrossAccountId,1221 token: TokenId,1222 nesting_budget: &dyn Budget,1223 ) -> DispatchResult {1224 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12251226 // =========12271228 // Allowance is reset in [`transfer`]1229 Self::transfer(collection, from, to, token, nesting_budget)1230 }12311232 /// Burn NFT token for `from` account.1233 ///1234 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1235 /// set allowance for the spender to burn token.1236 ///1237 /// [`burn`]: struct.Pallet.html#method.burn1238 pub fn burn_from(1239 collection: &NonfungibleHandle<T>,1240 spender: &T::CrossAccountId,1241 from: &T::CrossAccountId,1242 token: TokenId,1243 nesting_budget: &dyn Budget,1244 ) -> DispatchResult {1245 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12461247 // =========12481249 Self::burn(collection, from, token)1250 }12511252 /// Check that `from` token could be nested in `under` token.1253 ///1254 pub fn check_nesting(1255 handle: &NonfungibleHandle<T>,1256 sender: T::CrossAccountId,1257 from: (CollectionId, TokenId),1258 under: TokenId,1259 nesting_budget: &dyn Budget,1260 ) -> DispatchResult {1261 let nesting = handle.permissions.nesting();12621263 #[cfg(not(feature = "runtime-benchmarks"))]1264 let permissive = false;1265 #[cfg(feature = "runtime-benchmarks")]1266 let permissive = nesting.permissive;12671268 if permissive {1269 // Pass1270 } else if nesting.token_owner1271 && <PalletStructure<T>>::check_indirectly_owned(1272 sender.clone(),1273 handle.id,1274 under,1275 Some(from),1276 nesting_budget,1277 )? {1278 // Pass1279 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1280 // Pass1281 } else {1282 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1283 }12841285 if let Some(whitelist) = &nesting.restricted {1286 ensure!(1287 whitelist.contains(&from.0),1288 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1289 );1290 }1291 Ok(())1292 }12931294 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1295 <TokenChildren<T>>::insert((under.0, under.1, (to_nest.0, to_nest.1)), true);1296 }12971298 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1299 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1300 }13011302 fn collection_has_tokens(collection_id: CollectionId) -> bool {1303 <TokenData<T>>::iter_prefix((collection_id,))1304 .next()1305 .is_some()1306 }13071308 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1309 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1310 .next()1311 .is_some()1312 }13131314 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1315 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1316 .map(|((child_collection_id, child_id), _)| TokenChild {1317 collection: child_collection_id,1318 token: child_id,1319 })1320 .collect()1321 }13221323 /// Mint single NFT token.1324 ///1325 /// Delegated to [`create_multiple_items`]1326 ///1327 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1328 pub fn create_item(1329 collection: &NonfungibleHandle<T>,1330 sender: &T::CrossAccountId,1331 data: CreateItemData<T>,1332 nesting_budget: &dyn Budget,1333 ) -> DispatchResult {1334 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1335 }1336}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//! # Nonfungible Pallet18//!19//! The Nonfungible pallet provides functionality for handling nonfungible collections and tokens.20//!21//! - [`Config`]22//! - [`NonfungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Nonfungible pallet provides functions for:29//!30//! - NFT collection creation and removal31//! - Minting and burning of NFT tokens32//! - Retrieving account balances33//! - Transfering NFT tokens34//! - Setting and checking allowance for NFT tokens35//! - Setting properties and permissions for NFT collections and tokens36//! - Nesting and unnesting tokens37//!38//! ### Terminology39//!40//! - **NFT token:** Non fungible token.41//!42//! - **NFT Collection:** A collection of NFT tokens. All NFT tokens are part of a collection.43//! Each collection can define it's own properties, properties for it's tokens and set of permissions.44//!45//! - **Balance:** Number of NFT tokens owned by an account46//!47//! - **Allowance:** NFT tokens owned by one account that another account is allowed to make operations on48//!49//! - **Burning:** The process of “deleting” a token from a collection and from50//! an account balance of the owner.51//!52//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting53//! owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in54//! it's child token i.e. parent-child relationship graph shouldn't have cycles.55//!56//! - **Properties:** Key-Values pairs. Token properties are attached to a token. Collection properties are57//! attached to a collection. Set of permissions could be defined for each property.58//!59//! ### Implementations60//!61//! The Nonfungible pallet provides implementations for the following traits. If these traits provide62//! the functionality that you need, then you can avoid coupling with the Nonfungible pallet.63//!64//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight65//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing66//! with collections67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create NFT collection. NFT collection can be configured to allow or deny access for73//! some accounts.74//! - `destroy_collection` - Destroy exising NFT collection. There should be no tokens in the collection.75//! - `burn` - Burn NFT token owned by account.76//! - `transfer` - Transfer NFT token. Transfers should be enabled for NFT collection.77//! Nests the NFT token if it is sent to another token.78//! - `create_item` - Mint NFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account.80//! - `set_token_property` - Set token property value.81//! - `delete_token_property` - Remove property from the token.82//! - `set_collection_properties` - Set collection properties.83//! - `delete_collection_properties` - Remove properties from the collection.84//! - `set_property_permission` - Set collection property permission.85//! - `set_token_property_permissions` - Set token property permissions.86//!87//! ## Assumptions88//!89//! * Sender should be in collection's allow list to perform operations on tokens.9091#![cfg_attr(not(feature = "std"), no_std)]9293use erc::ERC721Events;94use evm_coder::ToLog;95use frame_support::{96 BoundedVec, ensure, fail, transactional,97 storage::with_transaction,98 pallet_prelude::DispatchResultWithPostInfo,99 pallet_prelude::Weight,100 weights::{PostDispatchInfo, Pays},101};102use up_data_structs::{103 AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,104 mapping::TokenAddressMapping, budget::Budget, Property, PropertyPermission, PropertyKey,105 PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild,106 AuxPropertyValue,107};108use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};109use pallet_common::{110 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,111 eth::collection_id_to_address,112};113use pallet_structure::{Pallet as PalletStructure, Error as StructureError};114use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};115use sp_core::H160;116use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};117use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap, collections::btree_set::BTreeSet};118use core::ops::Deref;119use codec::{Encode, Decode, MaxEncodedLen};120use scale_info::TypeInfo;121122pub use pallet::*;123use weights::WeightInfo;124#[cfg(feature = "runtime-benchmarks")]125pub mod benchmarking;126pub mod common;127pub mod erc;128pub mod weights;129130pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;131pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;132133#[struct_versioning::versioned(version = 2, upper)]134#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]135pub struct ItemData<CrossAccountId> {136 #[version(..2)]137 pub const_data: BoundedVec<u8, CustomDataLimit>,138139 #[version(..2)]140 pub variable_data: BoundedVec<u8, CustomDataLimit>,141142 pub owner: CrossAccountId,143}144145#[frame_support::pallet]146pub mod pallet {147 use super::*;148 use frame_support::{149 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,150 };151 use frame_system::pallet_prelude::*;152 use up_data_structs::{CollectionId, TokenId};153 use super::weights::WeightInfo;154155 #[pallet::error]156 pub enum Error<T> {157 /// Not Nonfungible item data used to mint in Nonfungible collection.158 NotNonfungibleDataUsedToMintFungibleCollectionToken,159 /// Used amount > 1 with NFT160 NonfungibleItemsHaveNoAmount,161 /// Unable to burn NFT with children162 CantBurnNftWithChildren,163 }164165 #[pallet::config]166 pub trait Config:167 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config168 {169 type WeightInfo: WeightInfo;170 }171172 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);173174 #[pallet::pallet]175 #[pallet::storage_version(STORAGE_VERSION)]176 #[pallet::generate_store(pub(super) trait Store)]177 pub struct Pallet<T>(_);178179 /// Amount of tokens minted for collection.180 #[pallet::storage]181 pub type TokensMinted<T: Config> =182 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;183184 /// Amount of burnt tokens for collection.185 #[pallet::storage]186 pub type TokensBurnt<T: Config> =187 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;188189 /// Custom data serialized to bytes for token.190 #[pallet::storage]191 pub type TokenData<T: Config> = StorageNMap<192 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),193 Value = ItemData<T::CrossAccountId>,194 QueryKind = OptionQuery,195 >;196197 /// Key-Value map stored for token.198 #[pallet::storage]199 #[pallet::getter(fn token_properties)]200 pub type TokenProperties<T: Config> = StorageNMap<201 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),202 Value = Properties,203 QueryKind = ValueQuery,204 OnEmpty = up_data_structs::TokenProperties,205 >;206207 /// Custom data that is serialized to bytes and attached to a token property.208 #[pallet::storage]209 #[pallet::getter(fn token_aux_property)]210 pub type TokenAuxProperties<T: Config> = StorageNMap<211 Key = (212 Key<Twox64Concat, CollectionId>,213 Key<Twox64Concat, TokenId>,214 Key<Twox64Concat, PropertyScope>,215 Key<Twox64Concat, PropertyKey>,216 ),217 Value = AuxPropertyValue,218 QueryKind = OptionQuery,219 >;220221 /// Used to enumerate tokens owned by account.222 #[pallet::storage]223 pub type Owned<T: Config> = StorageNMap<224 Key = (225 Key<Twox64Concat, CollectionId>,226 Key<Blake2_128Concat, T::CrossAccountId>,227 Key<Twox64Concat, TokenId>,228 ),229 Value = bool,230 QueryKind = ValueQuery,231 >;232233 /// Used to enumerate token's children.234 #[pallet::storage]235 #[pallet::getter(fn token_children)]236 pub type TokenChildren<T: Config> = StorageNMap<237 Key = (238 Key<Twox64Concat, CollectionId>,239 Key<Twox64Concat, TokenId>,240 Key<Twox64Concat, (CollectionId, TokenId)>,241 ),242 Value = bool,243 QueryKind = ValueQuery,244 >;245246 /// Amount of tokens owned by account.247 #[pallet::storage]248 pub type AccountBalance<T: Config> = StorageNMap<249 Key = (250 Key<Twox64Concat, CollectionId>,251 Key<Blake2_128Concat, T::CrossAccountId>,252 ),253 Value = u32,254 QueryKind = ValueQuery,255 >;256257 /// Allowance set by an owner for a spender for a token.258 #[pallet::storage]259 pub type Allowance<T: Config> = StorageNMap<260 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),261 Value = T::CrossAccountId,262 QueryKind = OptionQuery,263 >;264265 #[pallet::hooks]266 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {267 fn on_runtime_upgrade() -> Weight {268 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {269 let mut had_consts = BTreeSet::new();270 <TokenData<T>>::translate::<ItemDataVersion1<T::CrossAccountId>, _>(271 |(collection, token), v| {272 let mut props = vec![];273 if !v.const_data.is_empty() {274 props.push(Property {275 key: b"_old_constData".to_vec().try_into().unwrap(),276 value: v277 .const_data278 .clone()279 .into_inner()280 .try_into()281 .expect("const too long"),282 });283 had_consts.insert(collection);284 }285 if !v.variable_data.is_empty() {286 props.push(Property {287 key: b"_old_variableData".to_vec().try_into().unwrap(),288 value: v289 .variable_data290 .clone()291 .into_inner()292 .try_into()293 .expect("variable too long"),294 })295 }296 if !props.is_empty() {297 Self::set_scoped_token_properties(298 collection,299 token,300 PropertyScope::None,301 props.into_iter(),302 )303 .expect("existing token data exceeds property storage");304 }305 Some(<ItemDataVersion2<T::CrossAccountId>>::from(v))306 },307 );308 for collection in had_consts {309 <PalletCommon<T>>::set_property_permission_unchecked(310 collection,311 PropertyKeyPermission {312 key: b"_old_constData".to_vec().try_into().unwrap(),313 permission: PropertyPermission {314 mutable: false,315 collection_admin: true,316 token_owner: false,317 },318 },319 )320 .expect("failed to configure permission");321 }322 }323324 0325 }326 }327}328329pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);330impl<T: Config> NonfungibleHandle<T> {331 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {332 Self(inner)333 }334 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {335 self.0336 }337 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {338 &mut self.0339 }340}341impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {342 fn recorder(&self) -> &SubstrateRecorder<T> {343 self.0.recorder()344 }345 fn into_recorder(self) -> SubstrateRecorder<T> {346 self.0.into_recorder()347 }348}349impl<T: Config> Deref for NonfungibleHandle<T> {350 type Target = pallet_common::CollectionHandle<T>;351352 fn deref(&self) -> &Self::Target {353 &self.0354 }355}356357impl<T: Config> Pallet<T> {358 /// Get number of NFT tokens in collection.359 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {360 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)361 }362363 /// Check that NFT token exists.364 ///365 /// - `token`: Token ID.366 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {367 <TokenData<T>>::contains_key((collection.id, token))368 }369370 /// Set the token property with the scope.371 ///372 /// - `property`: Contains key-value pair.373 pub fn set_scoped_token_property(374 collection_id: CollectionId,375 token_id: TokenId,376 scope: PropertyScope,377 property: Property,378 ) -> DispatchResult {379 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {380 properties.try_scoped_set(scope, property.key, property.value)381 })382 .map_err(<CommonError<T>>::from)?;383384 Ok(())385 }386387 /// Batch operation to set multiple properties with the same scope.388 pub fn set_scoped_token_properties(389 collection_id: CollectionId,390 token_id: TokenId,391 scope: PropertyScope,392 properties: impl Iterator<Item = Property>,393 ) -> DispatchResult {394 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {395 stored_properties.try_scoped_set_from_iter(scope, properties)396 })397 .map_err(<CommonError<T>>::from)?;398399 Ok(())400 }401402 /// Add or edit auxiliary data for the property.403 ///404 /// - `f`: function that adds or edits auxiliary data.405 pub fn try_mutate_token_aux_property<R, E>(406 collection_id: CollectionId,407 token_id: TokenId,408 scope: PropertyScope,409 key: PropertyKey,410 f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,411 ) -> Result<R, E> {412 <TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)413 }414415 /// Remove auxiliary data for the property.416 pub fn remove_token_aux_property(417 collection_id: CollectionId,418 token_id: TokenId,419 scope: PropertyScope,420 key: PropertyKey,421 ) {422 <TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));423 }424425 /// Get all auxiliary data in a given scope.426 ///427 /// Returns iterator over Property Key - Data pairs.428 pub fn iterate_token_aux_properties(429 collection_id: CollectionId,430 token_id: TokenId,431 scope: PropertyScope,432 ) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {433 <TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))434 }435436 /// Get ID of the last minted token437 pub fn current_token_id(collection_id: CollectionId) -> TokenId {438 TokenId(<TokensMinted<T>>::get(collection_id))439 }440}441442// unchecked calls skips any permission checks443impl<T: Config> Pallet<T> {444 /// Create ТFT collection445 ///446 /// `init_collection` will take non-refundable deposit for collection creation.447 ///448 /// - `data`: Contains settings for collection limits and permissions.449 pub fn init_collection(450 owner: T::CrossAccountId,451 data: CreateCollectionData<T::AccountId>,452 is_external: bool,453 ) -> Result<CollectionId, DispatchError> {454 <PalletCommon<T>>::init_collection(owner, data, is_external)455 }456457 /// Destroy ТFT collection458 ///459 /// `destroy_collection` will throw error if collection contains any tokens.460 /// Only owner can destroy collection.461 pub fn destroy_collection(462 collection: NonfungibleHandle<T>,463 sender: &T::CrossAccountId,464 ) -> DispatchResult {465 let id = collection.id;466467 if Self::collection_has_tokens(id) {468 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());469 }470471 // =========472473 PalletCommon::destroy_collection(collection.0, sender)?;474475 <TokenData<T>>::remove_prefix((id,), None);476 <TokenChildren<T>>::remove_prefix((id,), None);477 <Owned<T>>::remove_prefix((id,), None);478 <TokensMinted<T>>::remove(id);479 <TokensBurnt<T>>::remove(id);480 <Allowance<T>>::remove_prefix((id,), None);481 <AccountBalance<T>>::remove_prefix((id,), None);482 Ok(())483 }484485 /// Burn NFT token486 ///487 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token488 /// if the token is nested.489 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.490 /// Also removes all corresponding properties and auxiliary properties.491 ///492 /// - `token`: Token that should be burned493 /// - `collection`: Collection that contains the token494 pub fn burn(495 collection: &NonfungibleHandle<T>,496 sender: &T::CrossAccountId,497 token: TokenId,498 ) -> DispatchResult {499 let token_data =500 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;501 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);502503 if collection.permissions.access() == AccessMode::AllowList {504 collection.check_allowlist(sender)?;505 }506507 if Self::token_has_children(collection.id, token) {508 return Err(<Error<T>>::CantBurnNftWithChildren.into());509 }510511 let burnt = <TokensBurnt<T>>::get(collection.id)512 .checked_add(1)513 .ok_or(ArithmeticError::Overflow)?;514515 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))516 .checked_sub(1)517 .ok_or(ArithmeticError::Overflow)?;518519 // =========520521 if balance == 0 {522 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));523 } else {524 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);525 }526527 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);528529 <Owned<T>>::remove((collection.id, &token_data.owner, token));530 <TokensBurnt<T>>::insert(collection.id, burnt);531 <TokenData<T>>::remove((collection.id, token));532 <TokenProperties<T>>::remove((collection.id, token));533 <TokenAuxProperties<T>>::remove_prefix((collection.id, token), None);534 let old_spender = <Allowance<T>>::take((collection.id, token));535536 if let Some(old_spender) = old_spender {537 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(538 collection.id,539 token,540 token_data.owner.clone(),541 old_spender,542 0,543 ));544 }545546 <PalletEvm<T>>::deposit_log(547 ERC721Events::Transfer {548 from: *token_data.owner.as_eth(),549 to: H160::default(),550 token_id: token.into(),551 }552 .to_log(collection_id_to_address(collection.id)),553 );554 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(555 collection.id,556 token,557 token_data.owner,558 1,559 ));560 Ok(())561 }562563 /// Same as [`burn`] but burns all the tokens that are nested in the token first564 ///565 /// - `self_budget`: Limit for searching children in depth.566 /// - `breadth_budget`: Limit of breadth of searching children.567 ///568 /// [`burn`]: struct.Pallet.html#method.burn569 #[transactional]570 pub fn burn_recursively(571 collection: &NonfungibleHandle<T>,572 sender: &T::CrossAccountId,573 token: TokenId,574 self_budget: &dyn Budget,575 breadth_budget: &dyn Budget,576 ) -> DispatchResultWithPostInfo {577 ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);578579 let current_token_account =580 T::CrossTokenAddressMapping::token_to_address(collection.id, token);581582 let mut weight = 0 as Weight;583584 // This method is transactional, if user in fact doesn't have permissions to remove token -585 // tokens removed here will be restored after rejected transaction586 for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {587 ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);588 let PostDispatchInfo { actual_weight, .. } =589 <PalletStructure<T>>::burn_item_recursively(590 current_token_account.clone(),591 collection,592 token,593 self_budget,594 breadth_budget,595 )?;596 if let Some(actual_weight) = actual_weight {597 weight = weight.saturating_add(actual_weight);598 }599 }600601 Self::burn(collection, sender, token)?;602 DispatchResultWithPostInfo::Ok(PostDispatchInfo {603 actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),604 pays_fee: Pays::Yes,605 })606 }607608 /// Batch operation to add, edit or remove properties for the token609 ///610 /// All affected properties should have mutable permission and sender should have611 /// permission to edit those properties.612 ///613 /// - `nesting_budget`: Limit for searching parents in depth to check ownership.614 /// - `is_token_create`: Indicates that method is called during token initialization.615 /// Allows to bypass ownership check.616 #[transactional]617 fn modify_token_properties(618 collection: &NonfungibleHandle<T>,619 sender: &T::CrossAccountId,620 token_id: TokenId,621 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,622 is_token_create: bool,623 nesting_budget: &dyn Budget,624 ) -> DispatchResult {625 let mut collection_admin_status = None;626 let mut token_owner_result = None;627628 let mut is_collection_admin =629 || *collection_admin_status.get_or_insert_with(|| collection.is_owner_or_admin(sender));630631 let mut is_token_owner = || {632 *token_owner_result.get_or_insert_with(|| -> Result<bool, DispatchError> {633 let is_owned = <PalletStructure<T>>::check_indirectly_owned(634 sender.clone(),635 collection.id,636 token_id,637 None,638 nesting_budget,639 )?;640641 Ok(is_owned)642 })643 };644645 for (key, value) in properties {646 let permission = <PalletCommon<T>>::property_permissions(collection.id)647 .get(&key)648 .cloned()649 .unwrap_or_else(PropertyPermission::none);650651 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))652 .get(&key)653 .is_some();654655 match permission {656 PropertyPermission { mutable: false, .. } if is_property_exists => {657 return Err(<CommonError<T>>::NoPermission.into());658 }659660 PropertyPermission {661 collection_admin,662 token_owner,663 ..664 } => {665 //TODO: investigate threats during public minting.666 if is_token_create && (collection_admin || token_owner) && value.is_some() {667 // Pass668 } else if collection_admin && is_collection_admin() {669 // Pass670 } else if token_owner && is_token_owner()? {671 // Pass672 } else {673 fail!(<CommonError<T>>::NoPermission);674 }675 }676 }677678 match value {679 Some(value) => {680 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {681 properties.try_set(key.clone(), value)682 })683 .map_err(<CommonError<T>>::from)?;684685 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(686 collection.id,687 token_id,688 key,689 ));690 }691 None => {692 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {693 properties.remove(&key)694 })695 .map_err(<CommonError<T>>::from)?;696697 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(698 collection.id,699 token_id,700 key,701 ));702 }703 }704 }705706 Ok(())707 }708709 /// Batch operation to add or edit properties for the token710 ///711 /// Same as [`modify_token_properties`] but doesn't allow to remove properties712 ///713 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties714 pub fn set_token_properties(715 collection: &NonfungibleHandle<T>,716 sender: &T::CrossAccountId,717 token_id: TokenId,718 properties: impl Iterator<Item = Property>,719 is_token_create: bool,720 nesting_budget: &dyn Budget,721 ) -> DispatchResult {722 Self::modify_token_properties(723 collection,724 sender,725 token_id,726 properties.map(|p| (p.key, Some(p.value))),727 is_token_create,728 nesting_budget,729 )730 }731732 /// Add or edit single property for the token733 ///734 /// Calls [`set_token_properties`] internally735 ///736 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties737 pub fn set_token_property(738 collection: &NonfungibleHandle<T>,739 sender: &T::CrossAccountId,740 token_id: TokenId,741 property: Property,742 nesting_budget: &dyn Budget,743 ) -> DispatchResult {744 let is_token_create = false;745746 Self::set_token_properties(747 collection,748 sender,749 token_id,750 [property].into_iter(),751 is_token_create,752 nesting_budget,753 )754 }755756 /// Batch operation to remove properties from the token757 ///758 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties759 ///760 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties761 pub fn delete_token_properties(762 collection: &NonfungibleHandle<T>,763 sender: &T::CrossAccountId,764 token_id: TokenId,765 property_keys: impl Iterator<Item = PropertyKey>,766 nesting_budget: &dyn Budget,767 ) -> DispatchResult {768 let is_token_create = false;769770 Self::modify_token_properties(771 collection,772 sender,773 token_id,774 property_keys.into_iter().map(|key| (key, None)),775 is_token_create,776 nesting_budget,777 )778 }779780 /// Remove single property from the token781 ///782 /// Calls [`delete_token_properties`] internally783 ///784 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties785 pub fn delete_token_property(786 collection: &NonfungibleHandle<T>,787 sender: &T::CrossAccountId,788 token_id: TokenId,789 property_key: PropertyKey,790 nesting_budget: &dyn Budget,791 ) -> DispatchResult {792 Self::delete_token_properties(793 collection,794 sender,795 token_id,796 [property_key].into_iter(),797 nesting_budget,798 )799 }800801 /// Add or edit properties for the collection802 pub fn set_collection_properties(803 collection: &NonfungibleHandle<T>,804 sender: &T::CrossAccountId,805 properties: Vec<Property>,806 ) -> DispatchResult {807 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)808 }809810 /// Remove properties from the collection811 pub fn delete_collection_properties(812 collection: &CollectionHandle<T>,813 sender: &T::CrossAccountId,814 property_keys: Vec<PropertyKey>,815 ) -> DispatchResult {816 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)817 }818819 /// Set property permissions for the token.820 ///821 /// Sender should be the owner or admin of token's collection.822 pub fn set_token_property_permissions(823 collection: &CollectionHandle<T>,824 sender: &T::CrossAccountId,825 property_permissions: Vec<PropertyKeyPermission>,826 ) -> DispatchResult {827 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)828 }829830 /// Set property permissions for the collection.831 ///832 /// Sender should be the owner or admin of the collection.833 pub fn set_property_permission(834 collection: &CollectionHandle<T>,835 sender: &T::CrossAccountId,836 permission: PropertyKeyPermission,837 ) -> DispatchResult {838 <PalletCommon<T>>::set_property_permission(collection, sender, permission)839 }840841 /// Transfer NFT token from one account to another.842 ///843 /// `from` account stops being the owner and `to` account becomes the owner of the token.844 /// If `to` is token than `to` becomes owner of the token and the token become nested.845 /// Unnests token from previous parent if it was nested before.846 /// Removes allowance for the token if there was any.847 /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.848 ///849 /// - `nesting_budget`: Limit for token nesting depth850 pub fn transfer(851 collection: &NonfungibleHandle<T>,852 from: &T::CrossAccountId,853 to: &T::CrossAccountId,854 token: TokenId,855 nesting_budget: &dyn Budget,856 ) -> DispatchResult {857 ensure!(858 collection.limits.transfers_enabled(),859 <CommonError<T>>::TransferNotAllowed860 );861862 let token_data =863 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;864 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);865866 if collection.permissions.access() == AccessMode::AllowList {867 collection.check_allowlist(from)?;868 collection.check_allowlist(to)?;869 }870 <PalletCommon<T>>::ensure_correct_receiver(to)?;871872 let balance_from = <AccountBalance<T>>::get((collection.id, from))873 .checked_sub(1)874 .ok_or(<CommonError<T>>::TokenValueTooLow)?;875 let balance_to = if from != to {876 let balance_to = <AccountBalance<T>>::get((collection.id, to))877 .checked_add(1)878 .ok_or(ArithmeticError::Overflow)?;879880 ensure!(881 balance_to < collection.limits.account_token_ownership_limit(),882 <CommonError<T>>::AccountTokenLimitExceeded,883 );884885 Some(balance_to)886 } else {887 None888 };889890 <PalletStructure<T>>::nest_if_sent_to_token(891 from.clone(),892 to,893 collection.id,894 token,895 nesting_budget,896 )?;897898 // =========899900 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);901902 <TokenData<T>>::insert(903 (collection.id, token),904 ItemData {905 owner: to.clone(),906 ..token_data907 },908 );909910 if let Some(balance_to) = balance_to {911 // from != to912 if balance_from == 0 {913 <AccountBalance<T>>::remove((collection.id, from));914 } else {915 <AccountBalance<T>>::insert((collection.id, from), balance_from);916 }917 <AccountBalance<T>>::insert((collection.id, to), balance_to);918 <Owned<T>>::remove((collection.id, from, token));919 <Owned<T>>::insert((collection.id, to, token), true);920 }921 Self::set_allowance_unchecked(collection, from, token, None, true);922923 <PalletEvm<T>>::deposit_log(924 ERC721Events::Transfer {925 from: *from.as_eth(),926 to: *to.as_eth(),927 token_id: token.into(),928 }929 .to_log(collection_id_to_address(collection.id)),930 );931 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(932 collection.id,933 token,934 from.clone(),935 to.clone(),936 1,937 ));938 Ok(())939 }940941 /// Batch operation to mint multiple NFT tokens.942 ///943 /// The sender should be the owner/admin of the collection or collection should be configured944 /// to allow public minting.945 /// Throws if amount of tokens reached it's limit for the collection or if caller reached946 /// token ownership limit.947 ///948 /// - `data`: Contains list of token properties and users who will become the owners of the949 /// corresponging tokens.950 /// - `nesting_budget`: Limit for token nesting depth951 pub fn create_multiple_items(952 collection: &NonfungibleHandle<T>,953 sender: &T::CrossAccountId,954 data: Vec<CreateItemData<T>>,955 nesting_budget: &dyn Budget,956 ) -> DispatchResult {957 if !collection.is_owner_or_admin(sender) {958 ensure!(959 collection.permissions.mint_mode(),960 <CommonError<T>>::PublicMintingNotAllowed961 );962 collection.check_allowlist(sender)?;963964 for item in data.iter() {965 collection.check_allowlist(&item.owner)?;966 }967 }968969 for data in data.iter() {970 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;971 }972973 let first_token = <TokensMinted<T>>::get(collection.id);974 let tokens_minted = first_token975 .checked_add(data.len() as u32)976 .ok_or(ArithmeticError::Overflow)?;977 ensure!(978 tokens_minted <= collection.limits.token_limit(),979 <CommonError<T>>::CollectionTokenLimitExceeded980 );981982 let mut balances = BTreeMap::new();983 for data in &data {984 let balance = balances985 .entry(&data.owner)986 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));987 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;988989 ensure!(990 *balance <= collection.limits.account_token_ownership_limit(),991 <CommonError<T>>::AccountTokenLimitExceeded,992 );993 }994995 for (i, data) in data.iter().enumerate() {996 let token = TokenId(first_token + i as u32 + 1);997998 <PalletStructure<T>>::check_nesting(999 sender.clone(),1000 &data.owner,1001 collection.id,1002 token,1003 nesting_budget,1004 )?;1005 }10061007 // =========10081009 with_transaction(|| {1010 for (i, data) in data.iter().enumerate() {1011 let token = first_token + i as u32 + 1;10121013 <TokenData<T>>::insert(1014 (collection.id, token),1015 ItemData {1016 // const_data: data.const_data.clone(),1017 owner: data.owner.clone(),1018 },1019 );10201021 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(1022 &data.owner,1023 collection.id,1024 TokenId(token),1025 );10261027 if let Err(e) = Self::set_token_properties(1028 collection,1029 sender,1030 TokenId(token),1031 data.properties.clone().into_iter(),1032 true,1033 nesting_budget,1034 ) {1035 return TransactionOutcome::Rollback(Err(e));1036 }1037 }1038 TransactionOutcome::Commit(Ok(()))1039 })?;10401041 <TokensMinted<T>>::insert(collection.id, tokens_minted);1042 for (account, balance) in balances {1043 <AccountBalance<T>>::insert((collection.id, account), balance);1044 }1045 for (i, data) in data.into_iter().enumerate() {1046 let token = first_token + i as u32 + 1;1047 <Owned<T>>::insert((collection.id, &data.owner, token), true);10481049 <PalletEvm<T>>::deposit_log(1050 ERC721Events::Transfer {1051 from: H160::default(),1052 to: *data.owner.as_eth(),1053 token_id: token.into(),1054 }1055 .to_log(collection_id_to_address(collection.id)),1056 );1057 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1058 collection.id,1059 TokenId(token),1060 data.owner.clone(),1061 1,1062 ));1063 }1064 Ok(())1065 }10661067 pub fn set_allowance_unchecked(1068 collection: &NonfungibleHandle<T>,1069 sender: &T::CrossAccountId,1070 token: TokenId,1071 spender: Option<&T::CrossAccountId>,1072 assume_implicit_eth: bool,1073 ) {1074 if let Some(spender) = spender {1075 let old_spender = <Allowance<T>>::get((collection.id, token));1076 <Allowance<T>>::insert((collection.id, token), spender);1077 // In ERC721 there is only one possible approved user of token, so we set1078 // approved user to spender1079 <PalletEvm<T>>::deposit_log(1080 ERC721Events::Approval {1081 owner: *sender.as_eth(),1082 approved: *spender.as_eth(),1083 token_id: token.into(),1084 }1085 .to_log(collection_id_to_address(collection.id)),1086 );1087 // In Unique chain, any token can have any amount of approved users, so we need to1088 // set allowance of old owner to 0, and allowance of new owner to 11089 if old_spender.as_ref() != Some(spender) {1090 if let Some(old_owner) = old_spender {1091 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1092 collection.id,1093 token,1094 sender.clone(),1095 old_owner,1096 0,1097 ));1098 }1099 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1100 collection.id,1101 token,1102 sender.clone(),1103 spender.clone(),1104 1,1105 ));1106 }1107 } else {1108 let old_spender = <Allowance<T>>::take((collection.id, token));1109 if !assume_implicit_eth {1110 // In ERC721 there is only one possible approved user of token, so we set1111 // approved user to zero address1112 <PalletEvm<T>>::deposit_log(1113 ERC721Events::Approval {1114 owner: *sender.as_eth(),1115 approved: H160::default(),1116 token_id: token.into(),1117 }1118 .to_log(collection_id_to_address(collection.id)),1119 );1120 }1121 // In Unique chain, any token can have any amount of approved users, so we need to1122 // set allowance of old owner to 01123 if let Some(old_spender) = old_spender {1124 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1125 collection.id,1126 token,1127 sender.clone(),1128 old_spender,1129 0,1130 ));1131 }1132 }1133 }11341135 /// Set allowance for the spender to `transfer` or `burn` sender's token.1136 ///1137 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1138 pub fn set_allowance(1139 collection: &NonfungibleHandle<T>,1140 sender: &T::CrossAccountId,1141 token: TokenId,1142 spender: Option<&T::CrossAccountId>,1143 ) -> DispatchResult {1144 if collection.permissions.access() == AccessMode::AllowList {1145 collection.check_allowlist(sender)?;1146 if let Some(spender) = spender {1147 collection.check_allowlist(spender)?;1148 }1149 }11501151 if let Some(spender) = spender {1152 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1153 }11541155 let token_data =1156 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1157 if &token_data.owner != sender {1158 ensure!(1159 collection.ignores_owned_amount(sender),1160 <CommonError<T>>::CantApproveMoreThanOwned1161 );1162 }11631164 // =========11651166 Self::set_allowance_unchecked(collection, sender, token, spender, false);1167 Ok(())1168 }11691170 /// Checks allowance for the spender to use the token.1171 fn check_allowed(1172 collection: &NonfungibleHandle<T>,1173 spender: &T::CrossAccountId,1174 from: &T::CrossAccountId,1175 token: TokenId,1176 nesting_budget: &dyn Budget,1177 ) -> DispatchResult {1178 if spender.conv_eq(from) {1179 return Ok(());1180 }1181 if collection.permissions.access() == AccessMode::AllowList {1182 // `from`, `to` checked in [`transfer`]1183 collection.check_allowlist(spender)?;1184 }11851186 if collection.limits.owner_can_transfer() && collection.is_owner_or_admin(spender) {1187 return Ok(());1188 }11891190 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1191 ensure!(1192 <PalletStructure<T>>::check_indirectly_owned(1193 spender.clone(),1194 source.0,1195 source.1,1196 None,1197 nesting_budget1198 )?,1199 <CommonError<T>>::ApprovedValueTooLow,1200 );1201 return Ok(());1202 }1203 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1204 return Ok(());1205 }1206 ensure!(1207 collection.ignores_allowance(spender),1208 <CommonError<T>>::ApprovedValueTooLow1209 );1210 Ok(())1211 }12121213 /// Transfer NFT token from one account to another.1214 ///1215 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1216 /// The owner should set allowance for the spender to transfer token.1217 ///1218 /// [`transfer`]: struct.Pallet.html#method.transfer1219 pub fn transfer_from(1220 collection: &NonfungibleHandle<T>,1221 spender: &T::CrossAccountId,1222 from: &T::CrossAccountId,1223 to: &T::CrossAccountId,1224 token: TokenId,1225 nesting_budget: &dyn Budget,1226 ) -> DispatchResult {1227 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12281229 // =========12301231 // Allowance is reset in [`transfer`]1232 Self::transfer(collection, from, to, token, nesting_budget)1233 }12341235 /// Burn NFT token for `from` account.1236 ///1237 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1238 /// set allowance for the spender to burn token.1239 ///1240 /// [`burn`]: struct.Pallet.html#method.burn1241 pub fn burn_from(1242 collection: &NonfungibleHandle<T>,1243 spender: &T::CrossAccountId,1244 from: &T::CrossAccountId,1245 token: TokenId,1246 nesting_budget: &dyn Budget,1247 ) -> DispatchResult {1248 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12491250 // =========12511252 Self::burn(collection, from, token)1253 }12541255 /// Check that `from` token could be nested in `under` token.1256 ///1257 pub fn check_nesting(1258 handle: &NonfungibleHandle<T>,1259 sender: T::CrossAccountId,1260 from: (CollectionId, TokenId),1261 under: TokenId,1262 nesting_budget: &dyn Budget,1263 ) -> DispatchResult {1264 let nesting = handle.permissions.nesting();12651266 #[cfg(not(feature = "runtime-benchmarks"))]1267 let permissive = false;1268 #[cfg(feature = "runtime-benchmarks")]1269 let permissive = nesting.permissive;12701271 if permissive {1272 // Pass1273 } else if nesting.token_owner1274 && <PalletStructure<T>>::check_indirectly_owned(1275 sender.clone(),1276 handle.id,1277 under,1278 Some(from),1279 nesting_budget,1280 )? {1281 // Pass1282 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1283 // Pass1284 } else {1285 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1286 }12871288 if let Some(whitelist) = &nesting.restricted {1289 ensure!(1290 whitelist.contains(&from.0),1291 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1292 );1293 }1294 Ok(())1295 }12961297 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1298 <TokenChildren<T>>::insert((under.0, under.1, (to_nest.0, to_nest.1)), true);1299 }13001301 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1302 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1303 }13041305 fn collection_has_tokens(collection_id: CollectionId) -> bool {1306 <TokenData<T>>::iter_prefix((collection_id,))1307 .next()1308 .is_some()1309 }13101311 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1312 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1313 .next()1314 .is_some()1315 }13161317 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1318 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1319 .map(|((child_collection_id, child_id), _)| TokenChild {1320 collection: child_collection_id,1321 token: child_id,1322 })1323 .collect()1324 }13251326 /// Mint single NFT token.1327 ///1328 /// Delegated to [`create_multiple_items`]1329 ///1330 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1331 pub fn create_item(1332 collection: &NonfungibleHandle<T>,1333 sender: &T::CrossAccountId,1334 data: CreateItemData<T>,1335 nesting_budget: &dyn Budget,1336 ) -> DispatchResult {1337 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1338 }1339}