difftreelog
feat xcm nft support
in: master
5 files changed
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -2351,6 +2351,11 @@
/// Is the collection a foreign one?
fn is_foreign(&self) -> bool;
+ /// Does the token have children?
+ fn token_has_children(&self, _token: TokenId) -> bool {
+ false
+ }
+
/// Create a collection's item using a transaction.
///
/// This function performs additional XCM-related checks before the actual creation.
pallets/foreign-assets/src/lib.rsdiffbeforeafterboth--- a/pallets/foreign-assets/src/lib.rs
+++ b/pallets/foreign-assets/src/lib.rs
@@ -364,7 +364,7 @@
fn check_out(_dest: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}
- fn deposit_asset(what: &MultiAsset, to: &MultiLocation, context: &XcmContext) -> XcmResult {
+ fn deposit_asset(what: &MultiAsset, to: &MultiLocation, _context: &XcmContext) -> XcmResult {
let collection_id = Self::multiasset_to_collection(what)?;
let dispatch =
T::CollectionDispatch::dispatch(collection_id).map_err(|_| XcmError::AssetNotFound)?;
@@ -397,7 +397,42 @@
from: &MultiLocation,
_maybe_context: Option<&XcmContext>,
) -> Result<staging_xcm_executor::Assets, XcmError> {
- Err(XcmError::Unimplemented)
+ let from = T::LocationToAccountId::convert_location(from)
+ .ok_or(XcmExecutorError::AccountIdConversionFailed)?;
+
+ let collection_id = Self::multiasset_to_collection(what)?;
+ let dispatch =
+ T::CollectionDispatch::dispatch(collection_id).map_err(|_| XcmError::AssetNotFound)?;
+
+ let collection = dispatch.as_dyn();
+ let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;
+
+ match what.fun {
+ Fungibility::Fungible(amount) => xcm_ext
+ .burn_item(from, TokenId::default(), amount)
+ .map_err(|_| XcmError::FailedToTransactAsset("fungible item withdraw failed"))?,
+
+ Fungibility::NonFungible(asset_instance) => {
+ let token_id =
+ Self::asset_instance_to_token_id(xcm_ext, collection_id, &asset_instance)?
+ .ok_or(XcmError::AssetNotFound)?;
+
+ if xcm_ext.token_has_children(token_id) {
+ return Err(XcmError::Unimplemented);
+ }
+
+ let depositor = &from;
+ let to = Self::pallet_account();
+ let amount = 1;
+ xcm_ext
+ .transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)
+ .map_err(|_| {
+ XcmError::FailedToTransactAsset("nonfungible item withdraw failed")
+ })?;
+ }
+ }
+
+ Ok(what.clone().into())
}
fn internal_transfer_asset(
@@ -406,7 +441,49 @@
to: &MultiLocation,
_context: &XcmContext,
) -> Result<staging_xcm_executor::Assets, XcmError> {
- Err(XcmError::Unimplemented)
+ let collection_id = Self::multiasset_to_collection(what)?;
+
+ let dispatch =
+ T::CollectionDispatch::dispatch(collection_id).map_err(|_| XcmError::AssetNotFound)?;
+ let collection = dispatch.as_dyn();
+ let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;
+
+ let from = T::LocationToAccountId::convert_location(from)
+ .ok_or(XcmExecutorError::AccountIdConversionFailed)?;
+
+ let to = T::LocationToAccountId::convert_location(to)
+ .ok_or(XcmExecutorError::AccountIdConversionFailed)?;
+
+ let depositor = &from;
+
+ match what.fun {
+ Fungibility::Fungible(amount) => xcm_ext
+ .transfer_item(
+ depositor,
+ &from,
+ &to,
+ TokenId::default(),
+ amount,
+ &ZeroBudget,
+ )
+ .map_err(|_| XcmError::FailedToTransactAsset("fungible item transfer failed"))?,
+
+ Fungibility::NonFungible(asset_instance) => {
+ let token_id =
+ Self::asset_instance_to_token_id(xcm_ext, collection_id, &asset_instance)?
+ .ok_or(XcmError::AssetNotFound)?;
+
+ let amount = 1;
+
+ xcm_ext
+ .transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)
+ .map_err(|_| {
+ XcmError::FailedToTransactAsset("nonfungible item transfer failed")
+ })?;
+ }
+ }
+
+ Ok(what.clone().into())
}
}
pallets/fungible/src/lib.rsdiffbeforeafterboth--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -95,9 +95,8 @@
use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
use sp_std::{collections::btree_map::BTreeMap, vec::Vec};
use up_data_structs::{
- budget::{Budget, ZeroBudget},
- mapping::TokenAddressMapping,
- AccessMode, CollectionId, CreateCollectionData, Property, PropertyKey, TokenId,
+ budget::Budget, mapping::TokenAddressMapping, AccessMode, CollectionId, Property, PropertyKey,
+ TokenId,
};
use weights::WeightInfo;
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -576,6 +576,10 @@
self.flags.foreign
}
+ fn token_has_children(&self, token: TokenId) -> bool {
+ <Pallet<T>>::token_has_children(self.id, token)
+ }
+
fn create_item_internal(
&self,
depositor: &<T>::CrossAccountId,
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`](common::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//! * To perform operations on tokens sender should be in collection's allow list if collection access mode is `AllowList`.9091#![cfg_attr(not(feature = "std"), no_std)]9293use core::ops::Deref;9495use erc::ERC721Events;96use evm_coder::ToLog;97use frame_support::{98 dispatch::{Pays, PostDispatchInfo},99 ensure, fail,100 pallet_prelude::*,101 storage::with_transaction,102 transactional, BoundedVec,103};104pub use pallet::*;105use pallet_common::{106 eth::collection_id_to_address, helpers::add_weight_to_post_info,107 weights::WeightInfo as CommonWeightInfo, CollectionHandle, Error as CommonError,108 Event as CommonEvent, Pallet as PalletCommon, SelfWeightOf as PalletCommonWeightOf,109};110use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};111use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};112use pallet_structure::Pallet as PalletStructure;113use parity_scale_codec::{Decode, Encode, MaxEncodedLen};114use scale_info::TypeInfo;115use sp_core::{Get, H160};116use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};117use sp_std::{collections::btree_map::BTreeMap, vec, vec::Vec};118use up_data_structs::{119 budget::Budget, mapping::TokenAddressMapping, AccessMode, AuxPropertyValue, CollectionId,120 CreateCollectionData, CreateNftExData, CustomDataLimit, PropertiesPermissionMap, Property,121 PropertyKey, PropertyKeyPermission, PropertyScope, PropertyValue, TokenChild, TokenId,122 TokenProperties as TokenPropertiesT,123};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::Config>::CrossAccountId>;132pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;133134/// Token data, stored independently from other data used to describe it135/// for the convenience of database access. Notably contains the owner account address.136#[struct_versioning::versioned(version = 2, upper)]137#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]138pub struct ItemData<CrossAccountId> {139 #[version(..2)]140 pub const_data: BoundedVec<u8, CustomDataLimit>,141142 #[version(..2)]143 pub variable_data: BoundedVec<u8, CustomDataLimit>,144145 pub owner: CrossAccountId,146}147148#[frame_support::pallet]149pub mod pallet {150 use frame_support::{151 pallet_prelude::*, storage::Key, traits::StorageVersion, Blake2_128Concat, Twox64Concat,152 };153 use up_data_structs::{CollectionId, TokenId};154155 use super::{weights::WeightInfo, *};156157 #[pallet::error]158 pub enum Error<T> {159 /// Not Nonfungible item data used to mint in Nonfungible collection.160 NotNonfungibleDataUsedToMintFungibleCollectionToken,161 /// Used amount > 1 with NFT162 NonfungibleItemsHaveNoAmount,163 /// Unable to burn NFT with children164 CantBurnNftWithChildren,165 }166167 #[pallet::config]168 pub trait Config:169 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config170 {171 type WeightInfo: WeightInfo;172 }173174 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);175176 #[pallet::pallet]177 #[pallet::storage_version(STORAGE_VERSION)]178 pub struct Pallet<T>(_);179180 /// Total amount of minted tokens in a 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 in a collection.186 #[pallet::storage]187 pub type TokensBurnt<T: Config> =188 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;189190 /// Token data, used to partially describe a 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 /// Map of key-value pairs, describing the metadata of a 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 = TokenPropertiesT,204 QueryKind = OptionQuery,205 >;206207 /// Custom data of a token that is serialized to bytes,208 /// primarily reserved for on-chain operations,209 /// normally obscured from the external users.210 ///211 /// Auxiliary properties are slightly different from212 /// usual [`TokenProperties`] due to an unlimited number213 /// and separately stored and written-to key-value pairs.214 ///215 /// Currently unused.216 #[pallet::storage]217 #[pallet::getter(fn token_aux_property)]218 pub type TokenAuxProperties<T: Config> = StorageNMap<219 Key = (220 Key<Twox64Concat, CollectionId>,221 Key<Twox64Concat, TokenId>,222 Key<Twox64Concat, PropertyScope>,223 Key<Twox64Concat, PropertyKey>,224 ),225 Value = AuxPropertyValue,226 QueryKind = OptionQuery,227 >;228229 /// Used to enumerate tokens owned by account.230 #[pallet::storage]231 pub type Owned<T: Config> = StorageNMap<232 Key = (233 Key<Twox64Concat, CollectionId>,234 Key<Blake2_128Concat, T::CrossAccountId>,235 Key<Twox64Concat, TokenId>,236 ),237 Value = bool,238 QueryKind = ValueQuery,239 >;240241 /// Used to enumerate token's children.242 #[pallet::storage]243 #[pallet::getter(fn token_children)]244 pub type TokenChildren<T: Config> = StorageNMap<245 Key = (246 Key<Twox64Concat, CollectionId>,247 Key<Twox64Concat, TokenId>,248 Key<Twox64Concat, (CollectionId, TokenId)>,249 ),250 Value = bool,251 QueryKind = ValueQuery,252 >;253254 /// Amount of tokens owned by an account in a collection.255 #[pallet::storage]256 pub type AccountBalance<T: Config> = StorageNMap<257 Key = (258 Key<Twox64Concat, CollectionId>,259 Key<Blake2_128Concat, T::CrossAccountId>,260 ),261 Value = u32,262 QueryKind = ValueQuery,263 >;264265 /// Allowance set by a token owner for another user to perform one of certain transactions on a token.266 #[pallet::storage]267 pub type Allowance<T: Config> = StorageNMap<268 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),269 Value = T::CrossAccountId,270 QueryKind = OptionQuery,271 >;272273 /// Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.274 #[pallet::storage]275 pub type CollectionAllowance<T: Config> = StorageNMap<276 Key = (277 Key<Twox64Concat, CollectionId>,278 Key<Blake2_128Concat, T::CrossAccountId>,279 Key<Blake2_128Concat, T::CrossAccountId>,280 ),281 Value = bool,282 QueryKind = ValueQuery,283 >;284285 #[pallet::genesis_config]286 pub struct GenesisConfig<T>(PhantomData<T>);287288 impl<T: Config> Default for GenesisConfig<T> {289 fn default() -> Self {290 Self(Default::default())291 }292 }293294 #[pallet::genesis_build]295 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {296 fn build(&self) {297 StorageVersion::new(1).put::<Pallet<T>>();298 }299 }300}301302pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);303impl<T: Config> NonfungibleHandle<T> {304 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {305 Self(inner)306 }307 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {308 self.0309 }310 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {311 &mut self.0312 }313}314315impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {316 fn recorder(&self) -> &SubstrateRecorder<T> {317 self.0.recorder()318 }319 fn into_recorder(self) -> SubstrateRecorder<T> {320 self.0.into_recorder()321 }322}323impl<T: Config> Deref for NonfungibleHandle<T> {324 type Target = pallet_common::CollectionHandle<T>;325326 fn deref(&self) -> &Self::Target {327 &self.0328 }329}330331impl<T: Config> Pallet<T> {332 /// Get number of NFT tokens in collection.333 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {334 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)335 }336337 /// Check that NFT token exists.338 ///339 /// - `token`: Token ID.340 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {341 <TokenData<T>>::contains_key((collection.id, token))342 }343344 /// Add or edit auxiliary data for the property.345 ///346 /// - `f`: function that adds or edits auxiliary data.347 pub fn try_mutate_token_aux_property<R, E>(348 collection_id: CollectionId,349 token_id: TokenId,350 scope: PropertyScope,351 key: PropertyKey,352 f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,353 ) -> Result<R, E> {354 <TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)355 }356357 /// Remove auxiliary data for the property.358 pub fn remove_token_aux_property(359 collection_id: CollectionId,360 token_id: TokenId,361 scope: PropertyScope,362 key: PropertyKey,363 ) {364 <TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));365 }366367 /// Get all auxiliary data in a given scope.368 ///369 /// Returns iterator over Property Key - Data pairs.370 pub fn iterate_token_aux_properties(371 collection_id: CollectionId,372 token_id: TokenId,373 scope: PropertyScope,374 ) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {375 <TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))376 }377378 /// Get ID of the last minted token379 pub fn current_token_id(collection_id: CollectionId) -> TokenId {380 TokenId(<TokensMinted<T>>::get(collection_id))381 }382}383384// unchecked calls skips any permission checks385impl<T: Config> Pallet<T> {386 /// Destroy NFT collection387 ///388 /// `destroy_collection` will throw error if collection contains any tokens.389 /// Only owner can destroy collection.390 pub fn destroy_collection(391 collection: NonfungibleHandle<T>,392 sender: &T::CrossAccountId,393 ) -> DispatchResult {394 let id = collection.id;395396 if Self::collection_has_tokens(id) {397 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());398 }399400 // =========401402 PalletCommon::destroy_collection(collection.0, sender)?;403404 let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);405 let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);406 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);407 <TokensMinted<T>>::remove(id);408 <TokensBurnt<T>>::remove(id);409 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);410 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);411 let _ = <CollectionAllowance<T>>::clear_prefix((id,), u32::MAX, None);412 Ok(())413 }414415 /// Burn NFT token416 ///417 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token418 /// if the token is nested.419 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.420 /// Also removes all corresponding properties and auxiliary properties.421 ///422 /// - `token`: Token that should be burned423 /// - `collection`: Collection that contains the token424 pub fn burn(425 collection: &NonfungibleHandle<T>,426 sender: &T::CrossAccountId,427 token: TokenId,428 ) -> DispatchResult {429 let token_data =430 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;431 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);432433 if collection.permissions.access() == AccessMode::AllowList {434 collection.check_allowlist(sender)?;435 }436437 if Self::token_has_children(collection.id, token) {438 return Err(<Error<T>>::CantBurnNftWithChildren.into());439 }440441 let burnt = <TokensBurnt<T>>::get(collection.id)442 .checked_add(1)443 .ok_or(ArithmeticError::Overflow)?;444445 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))446 .checked_sub(1)447 .ok_or(ArithmeticError::Overflow)?;448449 // =========450451 if balance == 0 {452 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));453 } else {454 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);455 }456457 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);458459 <Owned<T>>::remove((collection.id, &token_data.owner, token));460 <TokensBurnt<T>>::insert(collection.id, burnt);461 <TokenData<T>>::remove((collection.id, token));462 <TokenProperties<T>>::remove((collection.id, token));463 let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);464 let old_spender = <Allowance<T>>::take((collection.id, token));465466 if let Some(old_spender) = old_spender {467 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(468 collection.id,469 token,470 token_data.owner.clone(),471 old_spender,472 0,473 ));474 }475476 <PalletEvm<T>>::deposit_log(477 ERC721Events::Transfer {478 from: *token_data.owner.as_eth(),479 to: H160::default(),480 token_id: token.into(),481 }482 .to_log(collection_id_to_address(collection.id)),483 );484 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(485 collection.id,486 token,487 token_data.owner,488 1,489 ));490 Ok(())491 }492493 /// A batch operation to add, edit or remove properties for a token.494 ///495 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.496 ///497 /// All affected properties should have `mutable` permission498 /// to be **deleted** or to be **set more than once**,499 /// and the sender should have permission to edit those properties.500 ///501 /// This function fires an event for each property change.502 /// In case of an error, all the changes (including the events) will be reverted503 /// since the function is transactional.504 #[transactional]505 fn modify_token_properties(506 collection: &NonfungibleHandle<T>,507 sender: &T::CrossAccountId,508 token_id: TokenId,509 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,510 nesting_budget: &dyn Budget,511 ) -> DispatchResult {512 let mut property_writer =513 pallet_common::ExistingTokenPropertyWriter::new(collection, sender);514515 property_writer.write_token_properties(516 sender,517 token_id,518 properties_updates,519 nesting_budget,520 erc::ERC721TokenEvent::TokenChanged {521 token_id: token_id.into(),522 }523 .to_log(T::ContractAddress::get()),524 )525 }526527 pub fn next_token_id(collection: &NonfungibleHandle<T>) -> Result<TokenId, DispatchError> {528 let next_token_id = <TokensMinted<T>>::get(collection.id)529 .checked_add(1)530 .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;531532 ensure!(533 collection.limits.token_limit() >= next_token_id,534 <CommonError<T>>::CollectionTokenLimitExceeded535 );536537 Ok(TokenId(next_token_id))538 }539540 /// Batch operation to add or edit properties for the token541 ///542 /// Same as [`modify_token_properties`] but doesn't allow to remove properties543 ///544 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties545 pub fn set_token_properties(546 collection: &NonfungibleHandle<T>,547 sender: &T::CrossAccountId,548 token_id: TokenId,549 properties: impl Iterator<Item = Property>,550 nesting_budget: &dyn Budget,551 ) -> DispatchResult {552 Self::modify_token_properties(553 collection,554 sender,555 token_id,556 properties.map(|p| (p.key, Some(p.value))),557 nesting_budget,558 )559 }560561 /// Add or edit single property for the token562 ///563 /// Calls [`set_token_properties`] internally564 ///565 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties566 pub fn set_token_property(567 collection: &NonfungibleHandle<T>,568 sender: &T::CrossAccountId,569 token_id: TokenId,570 property: Property,571 nesting_budget: &dyn Budget,572 ) -> DispatchResult {573 Self::set_token_properties(574 collection,575 sender,576 token_id,577 [property].into_iter(),578 nesting_budget,579 )580 }581582 /// Batch operation to remove properties from the token583 ///584 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties585 ///586 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties587 pub fn delete_token_properties(588 collection: &NonfungibleHandle<T>,589 sender: &T::CrossAccountId,590 token_id: TokenId,591 property_keys: impl Iterator<Item = PropertyKey>,592 nesting_budget: &dyn Budget,593 ) -> DispatchResult {594 Self::modify_token_properties(595 collection,596 sender,597 token_id,598 property_keys.into_iter().map(|key| (key, None)),599 nesting_budget,600 )601 }602603 /// Remove single property from the token604 ///605 /// Calls [`delete_token_properties`] internally606 ///607 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties608 pub fn delete_token_property(609 collection: &NonfungibleHandle<T>,610 sender: &T::CrossAccountId,611 token_id: TokenId,612 property_key: PropertyKey,613 nesting_budget: &dyn Budget,614 ) -> DispatchResult {615 Self::delete_token_properties(616 collection,617 sender,618 token_id,619 [property_key].into_iter(),620 nesting_budget,621 )622 }623624 /// Add or edit properties for the collection625 pub fn set_collection_properties(626 collection: &NonfungibleHandle<T>,627 sender: &T::CrossAccountId,628 properties: Vec<Property>,629 ) -> DispatchResult {630 <PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())631 }632633 /// Remove properties from the collection634 pub fn delete_collection_properties(635 collection: &CollectionHandle<T>,636 sender: &T::CrossAccountId,637 property_keys: Vec<PropertyKey>,638 ) -> DispatchResult {639 <PalletCommon<T>>::delete_collection_properties(640 collection,641 sender,642 property_keys.into_iter(),643 )644 }645646 /// Set property permissions for the token.647 ///648 /// Sender should be the owner or admin of token's collection.649 pub fn set_token_property_permissions(650 collection: &CollectionHandle<T>,651 sender: &T::CrossAccountId,652 property_permissions: Vec<PropertyKeyPermission>,653 ) -> DispatchResult {654 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)655 }656657 /// Set property permissions for the token with scope.658 ///659 /// Sender should be the owner or admin of token's collection.660 pub fn set_scoped_token_property_permissions(661 collection: &CollectionHandle<T>,662 sender: &T::CrossAccountId,663 scope: PropertyScope,664 property_permissions: Vec<PropertyKeyPermission>,665 ) -> DispatchResult {666 <PalletCommon<T>>::set_scoped_token_property_permissions(667 collection,668 sender,669 scope,670 property_permissions,671 )672 }673674 pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {675 <PalletCommon<T>>::property_permissions(collection_id)676 }677678 pub fn check_token_immediate_ownership(679 collection: &NonfungibleHandle<T>,680 token: TokenId,681 possible_owner: &T::CrossAccountId,682 ) -> DispatchResult {683 let token_data =684 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;685 ensure!(686 &token_data.owner == possible_owner,687 <CommonError<T>>::NoPermission688 );689 Ok(())690 }691692 /// Transfer NFT token from one account to another.693 ///694 /// `from` account stops being the owner and `to` account becomes the owner of the token.695 /// If `to` is token than `to` becomes owner of the token and the token become nested.696 /// Unnests token from previous parent if it was nested before.697 /// Removes allowance for the token if there was any.698 /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.699 ///700 /// - `nesting_budget`: Limit for token nesting depth701 pub fn transfer(702 collection: &NonfungibleHandle<T>,703 from: &T::CrossAccountId,704 to: &T::CrossAccountId,705 token: TokenId,706 nesting_budget: &dyn Budget,707 ) -> DispatchResultWithPostInfo {708 let depositor = from;709 Self::transfer_internal(collection, depositor, from, to, token, nesting_budget)710 }711712 /// Transfers an NFT from the `from` account to the `to` account.713 /// The `depositor` is the account who deposits the NFT.714 /// For instance, the nesting rules will be checked against the `depositor`'s permissions.715 pub fn transfer_internal(716 collection: &NonfungibleHandle<T>,717 depositor: &T::CrossAccountId,718 from: &T::CrossAccountId,719 to: &T::CrossAccountId,720 token: TokenId,721 nesting_budget: &dyn Budget,722 ) -> DispatchResultWithPostInfo {723 ensure!(724 collection.limits.transfers_enabled(),725 <CommonError<T>>::TransferNotAllowed726 );727728 let mut actual_weight = <SelfWeightOf<T>>::transfer_raw();729 let token_data =730 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;731 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);732733 if collection.permissions.access() == AccessMode::AllowList {734 collection.check_allowlist(from)?;735 collection.check_allowlist(to)?;736 actual_weight += <PalletCommonWeightOf<T>>::check_accesslist() * 2;737 }738 <PalletCommon<T>>::ensure_correct_receiver(to)?;739740 let balance_from = <AccountBalance<T>>::get((collection.id, from))741 .checked_sub(1)742 .ok_or(<CommonError<T>>::TokenValueTooLow)?;743 let balance_to = if from != to {744 let balance_to = <AccountBalance<T>>::get((collection.id, to))745 .checked_add(1)746 .ok_or(ArithmeticError::Overflow)?;747748 ensure!(749 balance_to < collection.limits.account_token_ownership_limit(),750 <CommonError<T>>::AccountTokenLimitExceeded,751 );752753 Some(balance_to)754 } else {755 None756 };757758 <PalletStructure<T>>::nest_if_sent_to_token(759 depositor,760 to,761 collection.id,762 token,763 nesting_budget,764 )?;765766 // =========767768 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);769770 <TokenData<T>>::insert((collection.id, token), ItemData { owner: to.clone() });771772 if let Some(balance_to) = balance_to {773 // from != to774 if balance_from == 0 {775 <AccountBalance<T>>::remove((collection.id, from));776 } else {777 <AccountBalance<T>>::insert((collection.id, from), balance_from);778 }779 <AccountBalance<T>>::insert((collection.id, to), balance_to);780 <Owned<T>>::remove((collection.id, from, token));781 <Owned<T>>::insert((collection.id, to, token), true);782 }783 Self::set_allowance_unchecked(collection, from, token, None, true);784785 <PalletEvm<T>>::deposit_log(786 ERC721Events::Transfer {787 from: *from.as_eth(),788 to: *to.as_eth(),789 token_id: token.into(),790 }791 .to_log(collection_id_to_address(collection.id)),792 );793 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(794 collection.id,795 token,796 from.clone(),797 to.clone(),798 1,799 ));800801 Ok(PostDispatchInfo {802 actual_weight: Some(actual_weight),803 pays_fee: Pays::Yes,804 })805 }806807 /// Batch operation to mint multiple NFT tokens.808 ///809 /// The sender should be the owner/admin of the collection or collection should be configured810 /// to allow public minting.811 /// Throws if amount of tokens reached it's limit for the collection or if caller reached812 /// token ownership limit.813 ///814 /// - `data`: Contains list of token properties and users who will become the owners of the815 /// corresponging tokens.816 /// - `nesting_budget`: Limit for token nesting depth817 pub fn create_multiple_items(818 collection: &NonfungibleHandle<T>,819 sender: &T::CrossAccountId,820 data: Vec<CreateItemData<T>>,821 nesting_budget: &dyn Budget,822 ) -> DispatchResult {823 if !collection.is_owner_or_admin(sender) {824 ensure!(825 collection.permissions.mint_mode(),826 <CommonError<T>>::PublicMintingNotAllowed827 );828 collection.check_allowlist(sender)?;829830 for item in data.iter() {831 collection.check_allowlist(&item.owner)?;832 }833 }834835 for data in data.iter() {836 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;837 }838839 let first_token = <TokensMinted<T>>::get(collection.id);840 let tokens_minted = first_token841 .checked_add(data.len() as u32)842 .ok_or(ArithmeticError::Overflow)?;843 ensure!(844 tokens_minted <= collection.limits.token_limit(),845 <CommonError<T>>::CollectionTokenLimitExceeded846 );847848 let mut balances = BTreeMap::new();849 for data in &data {850 let balance = balances851 .entry(&data.owner)852 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));853 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;854855 ensure!(856 *balance <= collection.limits.account_token_ownership_limit(),857 <CommonError<T>>::AccountTokenLimitExceeded,858 );859 }860861 for (i, data) in data.iter().enumerate() {862 let token = TokenId(first_token + i as u32 + 1);863864 <PalletStructure<T>>::check_nesting(865 sender,866 &data.owner,867 collection.id,868 token,869 nesting_budget,870 )?;871 }872873 // =========874875 let mut property_writer = pallet_common::NewTokenPropertyWriter::new(collection, sender);876877 with_transaction(|| {878 for (i, data) in data.iter().enumerate() {879 let token = first_token + i as u32 + 1;880881 <TokenData<T>>::insert(882 (collection.id, token),883 ItemData {884 // const_data: data.const_data.clone(),885 owner: data.owner.clone(),886 },887 );888889 let token = TokenId(token);890891 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(892 &data.owner,893 collection.id,894 token,895 );896897 if let Err(e) = property_writer.write_token_properties(898 sender.conv_eq(&data.owner),899 token,900 data.properties.clone().into_iter(),901 erc::ERC721TokenEvent::TokenChanged {902 token_id: token.into(),903 }904 .to_log(T::ContractAddress::get()),905 ) {906 return TransactionOutcome::Rollback(Err(e));907 }908 }909 TransactionOutcome::Commit(Ok(()))910 })?;911912 <TokensMinted<T>>::insert(collection.id, tokens_minted);913 for (account, balance) in balances {914 <AccountBalance<T>>::insert((collection.id, account), balance);915 }916 for (i, data) in data.into_iter().enumerate() {917 let token = first_token + i as u32 + 1;918 <Owned<T>>::insert((collection.id, &data.owner, token), true);919920 <PalletEvm<T>>::deposit_log(921 ERC721Events::Transfer {922 from: H160::default(),923 to: *data.owner.as_eth(),924 token_id: token.into(),925 }926 .to_log(collection_id_to_address(collection.id)),927 );928 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(929 collection.id,930 TokenId(token),931 data.owner.clone(),932 1,933 ));934 }935 Ok(())936 }937938 pub fn set_allowance_unchecked(939 collection: &NonfungibleHandle<T>,940 sender: &T::CrossAccountId,941 token: TokenId,942 spender: Option<&T::CrossAccountId>,943 assume_implicit_eth: bool,944 ) {945 if let Some(spender) = spender {946 let old_spender = <Allowance<T>>::get((collection.id, token));947 <Allowance<T>>::insert((collection.id, token), spender);948 // In ERC721 there is only one possible approved user of token, so we set949 // approved user to spender950 <PalletEvm<T>>::deposit_log(951 ERC721Events::Approval {952 owner: *sender.as_eth(),953 approved: *spender.as_eth(),954 token_id: token.into(),955 }956 .to_log(collection_id_to_address(collection.id)),957 );958 // In Unique chain, any token can have any amount of approved users, so we need to959 // set allowance of old owner to 0, and allowance of new owner to 1960 if old_spender.as_ref() != Some(spender) {961 if let Some(old_owner) = old_spender {962 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(963 collection.id,964 token,965 sender.clone(),966 old_owner,967 0,968 ));969 }970 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(971 collection.id,972 token,973 sender.clone(),974 spender.clone(),975 1,976 ));977 }978 } else {979 let old_spender = <Allowance<T>>::take((collection.id, token));980 if !assume_implicit_eth {981 // In ERC721 there is only one possible approved user of token, so we set982 // approved user to zero address983 <PalletEvm<T>>::deposit_log(984 ERC721Events::Approval {985 owner: *sender.as_eth(),986 approved: H160::default(),987 token_id: token.into(),988 }989 .to_log(collection_id_to_address(collection.id)),990 );991 }992 // In Unique chain, any token can have any amount of approved users, so we need to993 // set allowance of old owner to 0994 if let Some(old_spender) = old_spender {995 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(996 collection.id,997 token,998 sender.clone(),999 old_spender,1000 0,1001 ));1002 }1003 }1004 }10051006 pub fn get_allowance(1007 collection: &NonfungibleHandle<T>,1008 token_id: TokenId,1009 ) -> Result<Option<T::CrossAccountId>, DispatchError> {1010 ensure!(1011 <TokenData<T>>::get((collection.id, token_id)).is_some(),1012 <CommonError<T>>::TokenNotFound1013 );1014 Ok(<Allowance<T>>::get((collection.id, token_id)))1015 }10161017 /// Set allowance for the spender to `transfer` or `burn` sender's token.1018 ///1019 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1020 pub fn set_allowance(1021 collection: &NonfungibleHandle<T>,1022 sender: &T::CrossAccountId,1023 token: TokenId,1024 spender: Option<&T::CrossAccountId>,1025 ) -> DispatchResult {1026 if collection.permissions.access() == AccessMode::AllowList {1027 collection.check_allowlist(sender)?;1028 if let Some(spender) = spender {1029 collection.check_allowlist(spender)?;1030 }1031 }10321033 if let Some(spender) = spender {1034 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1035 }10361037 let token_data =1038 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1039 if &token_data.owner != sender {1040 ensure!(1041 collection.ignores_owned_amount(sender),1042 <CommonError<T>>::CantApproveMoreThanOwned1043 );1044 }10451046 // =========10471048 Self::set_allowance_unchecked(collection, sender, token, spender, false);1049 Ok(())1050 }10511052 /// Set allowance for the spender to `transfer` or `burn` sender's token from eth mirror.1053 ///1054 /// - `from`: Address of sender's eth mirror.1055 /// - `to`: Adress of spender.1056 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1057 pub fn set_allowance_from(1058 collection: &NonfungibleHandle<T>,1059 sender: &T::CrossAccountId,1060 from: &T::CrossAccountId,1061 token: TokenId,1062 to: Option<&T::CrossAccountId>,1063 ) -> DispatchResult {1064 if collection.permissions.access() == AccessMode::AllowList {1065 collection.check_allowlist(sender)?;1066 collection.check_allowlist(from)?;1067 if let Some(to) = to {1068 collection.check_allowlist(to)?;1069 }1070 }10711072 if let Some(to) = to {1073 <PalletCommon<T>>::ensure_correct_receiver(to)?;1074 }10751076 ensure!(1077 sender.conv_eq(from),1078 <CommonError<T>>::AddressIsNotEthMirror1079 );10801081 let token_data =1082 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1083 if token_data.owner != *from {1084 ensure!(1085 collection.limits.owner_can_transfer()1086 && (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from)),1087 <CommonError<T>>::CantApproveMoreThanOwned1088 );1089 }10901091 // =========10921093 Self::set_allowance_unchecked(collection, from, token, to, false);1094 Ok(())1095 }10961097 /// Checks allowance for the spender to use the token.1098 fn check_allowed(1099 collection: &NonfungibleHandle<T>,1100 spender: &T::CrossAccountId,1101 from: &T::CrossAccountId,1102 token: TokenId,1103 nesting_budget: &dyn Budget,1104 ) -> DispatchResult {1105 if spender.conv_eq(from) {1106 return Ok(());1107 }1108 if collection.permissions.access() == AccessMode::AllowList {1109 // `from`, `to` checked in [`transfer`]1110 collection.check_allowlist(spender)?;1111 }11121113 if collection.ignores_token_restrictions(spender) {1114 return Ok(());1115 }11161117 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1118 ensure!(1119 <PalletStructure<T>>::check_indirectly_owned(1120 spender.clone(),1121 source.0,1122 source.1,1123 None,1124 nesting_budget1125 )?,1126 <CommonError<T>>::ApprovedValueTooLow,1127 );1128 return Ok(());1129 }1130 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1131 return Ok(());1132 }1133 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1134 return Ok(());1135 }11361137 Err(<CommonError<T>>::ApprovedValueTooLow.into())1138 }11391140 /// Transfer NFT token from one account to another.1141 ///1142 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1143 /// The owner should set allowance for the spender to transfer token.1144 ///1145 /// [`transfer`]: struct.Pallet.html#method.transfer1146 pub fn transfer_from(1147 collection: &NonfungibleHandle<T>,1148 spender: &T::CrossAccountId,1149 from: &T::CrossAccountId,1150 to: &T::CrossAccountId,1151 token: TokenId,1152 nesting_budget: &dyn Budget,1153 ) -> DispatchResultWithPostInfo {1154 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11551156 // =========11571158 // Allowance is reset in [`transfer`]1159 let mut result =1160 Self::transfer_internal(collection, spender, from, to, token, nesting_budget);1161 add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());1162 result1163 }11641165 /// Burn NFT token for `from` account.1166 ///1167 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1168 /// set allowance for the spender to burn token.1169 ///1170 /// [`burn`]: struct.Pallet.html#method.burn1171 pub fn burn_from(1172 collection: &NonfungibleHandle<T>,1173 spender: &T::CrossAccountId,1174 from: &T::CrossAccountId,1175 token: TokenId,1176 nesting_budget: &dyn Budget,1177 ) -> DispatchResult {1178 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11791180 // =========11811182 Self::burn(collection, from, token)1183 }11841185 /// Check that `from` token could be nested in `under` token.1186 ///1187 pub fn check_nesting(1188 handle: &NonfungibleHandle<T>,1189 sender: &T::CrossAccountId,1190 from: (CollectionId, TokenId),1191 under: TokenId,1192 nesting_budget: &dyn Budget,1193 ) -> DispatchResult {1194 let nesting = handle.permissions.nesting();11951196 #[cfg(not(feature = "runtime-benchmarks"))]1197 let permissive = false;1198 #[cfg(feature = "runtime-benchmarks")]1199 let permissive = nesting.permissive;12001201 if permissive {1202 ensure!(1203 <TokenData<T>>::contains_key((handle.id, under)),1204 <CommonError<T>>::TokenNotFound1205 );1206 } else if nesting.token_owner1207 && <PalletStructure<T>>::check_indirectly_owned(1208 sender.clone(),1209 handle.id,1210 under,1211 Some(from),1212 nesting_budget,1213 )? {1214 // Pass, token existence and ouroboros checks are done in `check_indirectly_owned`1215 } else if nesting.collection_admin && handle.is_owner_or_admin(sender) {1216 // token existence and ouroboros checks are done in `get_checked_topmost_owner`1217 let _ = <PalletStructure<T>>::get_checked_topmost_owner(1218 handle.id,1219 under,1220 Some(from),1221 nesting_budget,1222 )?1223 .ok_or(<CommonError<T>>::TokenNotFound)?;1224 } else {1225 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1226 }12271228 if let Some(whitelist) = &nesting.restricted {1229 ensure!(1230 whitelist.contains(&from.0),1231 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1232 );1233 }1234 Ok(())1235 }12361237 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1238 if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1239 <TokenChildren<T>>::insert((under.0, under.1, to_nest), true);1240 }1241 }12421243 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1244 if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1245 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1246 }1247 }12481249 fn collection_has_tokens(collection_id: CollectionId) -> bool {1250 <TokenData<T>>::iter_prefix((collection_id,))1251 .next()1252 .is_some()1253 }12541255 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1256 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1257 .next()1258 .is_some()1259 }12601261 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1262 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1263 .map(|((child_collection_id, child_id), _)| TokenChild {1264 collection: child_collection_id,1265 token: child_id,1266 })1267 .collect()1268 }12691270 /// Mint single NFT token.1271 ///1272 /// Delegated to [`create_multiple_items`]1273 ///1274 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1275 pub fn create_item(1276 collection: &NonfungibleHandle<T>,1277 sender: &T::CrossAccountId,1278 data: CreateItemData<T>,1279 nesting_budget: &dyn Budget,1280 ) -> DispatchResult {1281 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1282 }12831284 /// Sets or unsets the approval of a given operator.1285 ///1286 /// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1287 /// - `owner`: Token owner1288 /// - `operator`: Operator1289 /// - `approve`: Should operator status be granted or revoked?1290 pub fn set_allowance_for_all(1291 collection: &NonfungibleHandle<T>,1292 owner: &T::CrossAccountId,1293 operator: &T::CrossAccountId,1294 approve: bool,1295 ) -> DispatchResult {1296 <PalletCommon<T>>::set_allowance_for_all(1297 collection,1298 owner,1299 operator,1300 approve,1301 || <CollectionAllowance<T>>::insert((collection.id, owner, operator), approve),1302 ERC721Events::ApprovalForAll {1303 owner: *owner.as_eth(),1304 operator: *operator.as_eth(),1305 approved: approve,1306 }1307 .to_log(collection_id_to_address(collection.id)),1308 )1309 }13101311 /// Tells whether the given `owner` approves the `operator`.1312 pub fn allowance_for_all(1313 collection: &NonfungibleHandle<T>,1314 owner: &T::CrossAccountId,1315 operator: &T::CrossAccountId,1316 ) -> bool {1317 <CollectionAllowance<T>>::get((collection.id, owner, operator))1318 }13191320 pub fn repair_item(collection: &NonfungibleHandle<T>, token: TokenId) -> DispatchResult {1321 <TokenProperties<T>>::mutate((collection.id, token), |properties| {1322 if let Some(properties) = properties {1323 properties.recompute_consumed_space();1324 }1325 });13261327 Ok(())1328 }1329}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`](common::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//! * To perform operations on tokens sender should be in collection's allow list if collection access mode is `AllowList`.9091#![cfg_attr(not(feature = "std"), no_std)]9293use core::ops::Deref;9495use erc::ERC721Events;96use evm_coder::ToLog;97use frame_support::{98 dispatch::{Pays, PostDispatchInfo},99 ensure, fail,100 pallet_prelude::*,101 storage::with_transaction,102 transactional, BoundedVec,103};104pub use pallet::*;105use pallet_common::{106 eth::collection_id_to_address, helpers::add_weight_to_post_info,107 weights::WeightInfo as CommonWeightInfo, CollectionHandle, Error as CommonError,108 Event as CommonEvent, Pallet as PalletCommon, SelfWeightOf as PalletCommonWeightOf,109};110use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};111use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};112use pallet_structure::Pallet as PalletStructure;113use parity_scale_codec::{Decode, Encode, MaxEncodedLen};114use scale_info::TypeInfo;115use sp_core::{Get, H160};116use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};117use sp_std::{collections::btree_map::BTreeMap, vec, vec::Vec};118use up_data_structs::{119 budget::Budget, mapping::TokenAddressMapping, AccessMode, AuxPropertyValue, CollectionId,120 CreateNftExData, CustomDataLimit, PropertiesPermissionMap, Property, PropertyKey,121 PropertyKeyPermission, PropertyScope, PropertyValue, TokenChild, TokenId,122 TokenProperties as TokenPropertiesT,123};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::Config>::CrossAccountId>;132pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;133134/// Token data, stored independently from other data used to describe it135/// for the convenience of database access. Notably contains the owner account address.136#[struct_versioning::versioned(version = 2, upper)]137#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]138pub struct ItemData<CrossAccountId> {139 #[version(..2)]140 pub const_data: BoundedVec<u8, CustomDataLimit>,141142 #[version(..2)]143 pub variable_data: BoundedVec<u8, CustomDataLimit>,144145 pub owner: CrossAccountId,146}147148#[frame_support::pallet]149pub mod pallet {150 use frame_support::{151 pallet_prelude::*, storage::Key, traits::StorageVersion, Blake2_128Concat, Twox64Concat,152 };153 use up_data_structs::{CollectionId, TokenId};154155 use super::{weights::WeightInfo, *};156157 #[pallet::error]158 pub enum Error<T> {159 /// Not Nonfungible item data used to mint in Nonfungible collection.160 NotNonfungibleDataUsedToMintFungibleCollectionToken,161 /// Used amount > 1 with NFT162 NonfungibleItemsHaveNoAmount,163 /// Unable to burn NFT with children164 CantBurnNftWithChildren,165 }166167 #[pallet::config]168 pub trait Config:169 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config170 {171 type WeightInfo: WeightInfo;172 }173174 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);175176 #[pallet::pallet]177 #[pallet::storage_version(STORAGE_VERSION)]178 pub struct Pallet<T>(_);179180 /// Total amount of minted tokens in a 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 in a collection.186 #[pallet::storage]187 pub type TokensBurnt<T: Config> =188 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;189190 /// Token data, used to partially describe a 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 /// Map of key-value pairs, describing the metadata of a 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 = TokenPropertiesT,204 QueryKind = OptionQuery,205 >;206207 /// Custom data of a token that is serialized to bytes,208 /// primarily reserved for on-chain operations,209 /// normally obscured from the external users.210 ///211 /// Auxiliary properties are slightly different from212 /// usual [`TokenProperties`] due to an unlimited number213 /// and separately stored and written-to key-value pairs.214 ///215 /// Currently unused.216 #[pallet::storage]217 #[pallet::getter(fn token_aux_property)]218 pub type TokenAuxProperties<T: Config> = StorageNMap<219 Key = (220 Key<Twox64Concat, CollectionId>,221 Key<Twox64Concat, TokenId>,222 Key<Twox64Concat, PropertyScope>,223 Key<Twox64Concat, PropertyKey>,224 ),225 Value = AuxPropertyValue,226 QueryKind = OptionQuery,227 >;228229 /// Used to enumerate tokens owned by account.230 #[pallet::storage]231 pub type Owned<T: Config> = StorageNMap<232 Key = (233 Key<Twox64Concat, CollectionId>,234 Key<Blake2_128Concat, T::CrossAccountId>,235 Key<Twox64Concat, TokenId>,236 ),237 Value = bool,238 QueryKind = ValueQuery,239 >;240241 /// Used to enumerate token's children.242 #[pallet::storage]243 #[pallet::getter(fn token_children)]244 pub type TokenChildren<T: Config> = StorageNMap<245 Key = (246 Key<Twox64Concat, CollectionId>,247 Key<Twox64Concat, TokenId>,248 Key<Twox64Concat, (CollectionId, TokenId)>,249 ),250 Value = bool,251 QueryKind = ValueQuery,252 >;253254 /// Amount of tokens owned by an account in a collection.255 #[pallet::storage]256 pub type AccountBalance<T: Config> = StorageNMap<257 Key = (258 Key<Twox64Concat, CollectionId>,259 Key<Blake2_128Concat, T::CrossAccountId>,260 ),261 Value = u32,262 QueryKind = ValueQuery,263 >;264265 /// Allowance set by a token owner for another user to perform one of certain transactions on a token.266 #[pallet::storage]267 pub type Allowance<T: Config> = StorageNMap<268 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),269 Value = T::CrossAccountId,270 QueryKind = OptionQuery,271 >;272273 /// Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.274 #[pallet::storage]275 pub type CollectionAllowance<T: Config> = StorageNMap<276 Key = (277 Key<Twox64Concat, CollectionId>,278 Key<Blake2_128Concat, T::CrossAccountId>,279 Key<Blake2_128Concat, T::CrossAccountId>,280 ),281 Value = bool,282 QueryKind = ValueQuery,283 >;284285 #[pallet::genesis_config]286 pub struct GenesisConfig<T>(PhantomData<T>);287288 impl<T: Config> Default for GenesisConfig<T> {289 fn default() -> Self {290 Self(Default::default())291 }292 }293294 #[pallet::genesis_build]295 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {296 fn build(&self) {297 StorageVersion::new(1).put::<Pallet<T>>();298 }299 }300}301302pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);303impl<T: Config> NonfungibleHandle<T> {304 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {305 Self(inner)306 }307 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {308 self.0309 }310 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {311 &mut self.0312 }313}314315impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {316 fn recorder(&self) -> &SubstrateRecorder<T> {317 self.0.recorder()318 }319 fn into_recorder(self) -> SubstrateRecorder<T> {320 self.0.into_recorder()321 }322}323impl<T: Config> Deref for NonfungibleHandle<T> {324 type Target = pallet_common::CollectionHandle<T>;325326 fn deref(&self) -> &Self::Target {327 &self.0328 }329}330331impl<T: Config> Pallet<T> {332 /// Get number of NFT tokens in collection.333 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {334 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)335 }336337 /// Check that NFT token exists.338 ///339 /// - `token`: Token ID.340 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {341 <TokenData<T>>::contains_key((collection.id, token))342 }343344 /// Add or edit auxiliary data for the property.345 ///346 /// - `f`: function that adds or edits auxiliary data.347 pub fn try_mutate_token_aux_property<R, E>(348 collection_id: CollectionId,349 token_id: TokenId,350 scope: PropertyScope,351 key: PropertyKey,352 f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,353 ) -> Result<R, E> {354 <TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)355 }356357 /// Remove auxiliary data for the property.358 pub fn remove_token_aux_property(359 collection_id: CollectionId,360 token_id: TokenId,361 scope: PropertyScope,362 key: PropertyKey,363 ) {364 <TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));365 }366367 /// Get all auxiliary data in a given scope.368 ///369 /// Returns iterator over Property Key - Data pairs.370 pub fn iterate_token_aux_properties(371 collection_id: CollectionId,372 token_id: TokenId,373 scope: PropertyScope,374 ) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {375 <TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))376 }377378 /// Get ID of the last minted token379 pub fn current_token_id(collection_id: CollectionId) -> TokenId {380 TokenId(<TokensMinted<T>>::get(collection_id))381 }382}383384// unchecked calls skips any permission checks385impl<T: Config> Pallet<T> {386 /// Destroy NFT collection387 ///388 /// `destroy_collection` will throw error if collection contains any tokens.389 /// Only owner can destroy collection.390 pub fn destroy_collection(391 collection: NonfungibleHandle<T>,392 sender: &T::CrossAccountId,393 ) -> DispatchResult {394 let id = collection.id;395396 if Self::collection_has_tokens(id) {397 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());398 }399400 // =========401402 PalletCommon::destroy_collection(collection.0, sender)?;403404 let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);405 let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);406 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);407 <TokensMinted<T>>::remove(id);408 <TokensBurnt<T>>::remove(id);409 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);410 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);411 let _ = <CollectionAllowance<T>>::clear_prefix((id,), u32::MAX, None);412 Ok(())413 }414415 /// Burn NFT token416 ///417 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token418 /// if the token is nested.419 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.420 /// Also removes all corresponding properties and auxiliary properties.421 ///422 /// - `token`: Token that should be burned423 /// - `collection`: Collection that contains the token424 pub fn burn(425 collection: &NonfungibleHandle<T>,426 sender: &T::CrossAccountId,427 token: TokenId,428 ) -> DispatchResult {429 let token_data =430 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;431 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);432433 if collection.permissions.access() == AccessMode::AllowList {434 collection.check_allowlist(sender)?;435 }436437 if Self::token_has_children(collection.id, token) {438 return Err(<Error<T>>::CantBurnNftWithChildren.into());439 }440441 let burnt = <TokensBurnt<T>>::get(collection.id)442 .checked_add(1)443 .ok_or(ArithmeticError::Overflow)?;444445 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))446 .checked_sub(1)447 .ok_or(ArithmeticError::Overflow)?;448449 // =========450451 if balance == 0 {452 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));453 } else {454 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);455 }456457 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);458459 <Owned<T>>::remove((collection.id, &token_data.owner, token));460 <TokensBurnt<T>>::insert(collection.id, burnt);461 <TokenData<T>>::remove((collection.id, token));462 <TokenProperties<T>>::remove((collection.id, token));463 let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);464 let old_spender = <Allowance<T>>::take((collection.id, token));465466 if let Some(old_spender) = old_spender {467 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(468 collection.id,469 token,470 token_data.owner.clone(),471 old_spender,472 0,473 ));474 }475476 <PalletEvm<T>>::deposit_log(477 ERC721Events::Transfer {478 from: *token_data.owner.as_eth(),479 to: H160::default(),480 token_id: token.into(),481 }482 .to_log(collection_id_to_address(collection.id)),483 );484 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(485 collection.id,486 token,487 token_data.owner,488 1,489 ));490 Ok(())491 }492493 /// A batch operation to add, edit or remove properties for a token.494 ///495 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.496 ///497 /// All affected properties should have `mutable` permission498 /// to be **deleted** or to be **set more than once**,499 /// and the sender should have permission to edit those properties.500 ///501 /// This function fires an event for each property change.502 /// In case of an error, all the changes (including the events) will be reverted503 /// since the function is transactional.504 #[transactional]505 fn modify_token_properties(506 collection: &NonfungibleHandle<T>,507 sender: &T::CrossAccountId,508 token_id: TokenId,509 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,510 nesting_budget: &dyn Budget,511 ) -> DispatchResult {512 let mut property_writer =513 pallet_common::ExistingTokenPropertyWriter::new(collection, sender);514515 property_writer.write_token_properties(516 sender,517 token_id,518 properties_updates,519 nesting_budget,520 erc::ERC721TokenEvent::TokenChanged {521 token_id: token_id.into(),522 }523 .to_log(T::ContractAddress::get()),524 )525 }526527 pub fn next_token_id(collection: &NonfungibleHandle<T>) -> Result<TokenId, DispatchError> {528 let next_token_id = <TokensMinted<T>>::get(collection.id)529 .checked_add(1)530 .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;531532 ensure!(533 collection.limits.token_limit() >= next_token_id,534 <CommonError<T>>::CollectionTokenLimitExceeded535 );536537 Ok(TokenId(next_token_id))538 }539540 /// Batch operation to add or edit properties for the token541 ///542 /// Same as [`modify_token_properties`] but doesn't allow to remove properties543 ///544 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties545 pub fn set_token_properties(546 collection: &NonfungibleHandle<T>,547 sender: &T::CrossAccountId,548 token_id: TokenId,549 properties: impl Iterator<Item = Property>,550 nesting_budget: &dyn Budget,551 ) -> DispatchResult {552 Self::modify_token_properties(553 collection,554 sender,555 token_id,556 properties.map(|p| (p.key, Some(p.value))),557 nesting_budget,558 )559 }560561 /// Add or edit single property for the token562 ///563 /// Calls [`set_token_properties`] internally564 ///565 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties566 pub fn set_token_property(567 collection: &NonfungibleHandle<T>,568 sender: &T::CrossAccountId,569 token_id: TokenId,570 property: Property,571 nesting_budget: &dyn Budget,572 ) -> DispatchResult {573 Self::set_token_properties(574 collection,575 sender,576 token_id,577 [property].into_iter(),578 nesting_budget,579 )580 }581582 /// Batch operation to remove properties from the token583 ///584 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties585 ///586 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties587 pub fn delete_token_properties(588 collection: &NonfungibleHandle<T>,589 sender: &T::CrossAccountId,590 token_id: TokenId,591 property_keys: impl Iterator<Item = PropertyKey>,592 nesting_budget: &dyn Budget,593 ) -> DispatchResult {594 Self::modify_token_properties(595 collection,596 sender,597 token_id,598 property_keys.into_iter().map(|key| (key, None)),599 nesting_budget,600 )601 }602603 /// Remove single property from the token604 ///605 /// Calls [`delete_token_properties`] internally606 ///607 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties608 pub fn delete_token_property(609 collection: &NonfungibleHandle<T>,610 sender: &T::CrossAccountId,611 token_id: TokenId,612 property_key: PropertyKey,613 nesting_budget: &dyn Budget,614 ) -> DispatchResult {615 Self::delete_token_properties(616 collection,617 sender,618 token_id,619 [property_key].into_iter(),620 nesting_budget,621 )622 }623624 /// Add or edit properties for the collection625 pub fn set_collection_properties(626 collection: &NonfungibleHandle<T>,627 sender: &T::CrossAccountId,628 properties: Vec<Property>,629 ) -> DispatchResult {630 <PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())631 }632633 /// Remove properties from the collection634 pub fn delete_collection_properties(635 collection: &CollectionHandle<T>,636 sender: &T::CrossAccountId,637 property_keys: Vec<PropertyKey>,638 ) -> DispatchResult {639 <PalletCommon<T>>::delete_collection_properties(640 collection,641 sender,642 property_keys.into_iter(),643 )644 }645646 /// Set property permissions for the token.647 ///648 /// Sender should be the owner or admin of token's collection.649 pub fn set_token_property_permissions(650 collection: &CollectionHandle<T>,651 sender: &T::CrossAccountId,652 property_permissions: Vec<PropertyKeyPermission>,653 ) -> DispatchResult {654 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)655 }656657 /// Set property permissions for the token with scope.658 ///659 /// Sender should be the owner or admin of token's collection.660 pub fn set_scoped_token_property_permissions(661 collection: &CollectionHandle<T>,662 sender: &T::CrossAccountId,663 scope: PropertyScope,664 property_permissions: Vec<PropertyKeyPermission>,665 ) -> DispatchResult {666 <PalletCommon<T>>::set_scoped_token_property_permissions(667 collection,668 sender,669 scope,670 property_permissions,671 )672 }673674 pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {675 <PalletCommon<T>>::property_permissions(collection_id)676 }677678 pub fn check_token_immediate_ownership(679 collection: &NonfungibleHandle<T>,680 token: TokenId,681 possible_owner: &T::CrossAccountId,682 ) -> DispatchResult {683 let token_data =684 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;685 ensure!(686 &token_data.owner == possible_owner,687 <CommonError<T>>::NoPermission688 );689 Ok(())690 }691692 /// Transfer NFT token from one account to another.693 ///694 /// `from` account stops being the owner and `to` account becomes the owner of the token.695 /// If `to` is token than `to` becomes owner of the token and the token become nested.696 /// Unnests token from previous parent if it was nested before.697 /// Removes allowance for the token if there was any.698 /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.699 ///700 /// - `nesting_budget`: Limit for token nesting depth701 pub fn transfer(702 collection: &NonfungibleHandle<T>,703 from: &T::CrossAccountId,704 to: &T::CrossAccountId,705 token: TokenId,706 nesting_budget: &dyn Budget,707 ) -> DispatchResultWithPostInfo {708 let depositor = from;709 Self::transfer_internal(collection, depositor, from, to, token, nesting_budget)710 }711712 /// Transfers an NFT from the `from` account to the `to` account.713 /// The `depositor` is the account who deposits the NFT.714 /// For instance, the nesting rules will be checked against the `depositor`'s permissions.715 pub fn transfer_internal(716 collection: &NonfungibleHandle<T>,717 depositor: &T::CrossAccountId,718 from: &T::CrossAccountId,719 to: &T::CrossAccountId,720 token: TokenId,721 nesting_budget: &dyn Budget,722 ) -> DispatchResultWithPostInfo {723 ensure!(724 collection.limits.transfers_enabled(),725 <CommonError<T>>::TransferNotAllowed726 );727728 let mut actual_weight = <SelfWeightOf<T>>::transfer_raw();729 let token_data =730 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;731 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);732733 if collection.permissions.access() == AccessMode::AllowList {734 collection.check_allowlist(from)?;735 collection.check_allowlist(to)?;736 actual_weight += <PalletCommonWeightOf<T>>::check_accesslist() * 2;737 }738 <PalletCommon<T>>::ensure_correct_receiver(to)?;739740 let balance_from = <AccountBalance<T>>::get((collection.id, from))741 .checked_sub(1)742 .ok_or(<CommonError<T>>::TokenValueTooLow)?;743 let balance_to = if from != to {744 let balance_to = <AccountBalance<T>>::get((collection.id, to))745 .checked_add(1)746 .ok_or(ArithmeticError::Overflow)?;747748 ensure!(749 balance_to < collection.limits.account_token_ownership_limit(),750 <CommonError<T>>::AccountTokenLimitExceeded,751 );752753 Some(balance_to)754 } else {755 None756 };757758 <PalletStructure<T>>::nest_if_sent_to_token(759 depositor,760 to,761 collection.id,762 token,763 nesting_budget,764 )?;765766 // =========767768 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);769770 <TokenData<T>>::insert((collection.id, token), ItemData { owner: to.clone() });771772 if let Some(balance_to) = balance_to {773 // from != to774 if balance_from == 0 {775 <AccountBalance<T>>::remove((collection.id, from));776 } else {777 <AccountBalance<T>>::insert((collection.id, from), balance_from);778 }779 <AccountBalance<T>>::insert((collection.id, to), balance_to);780 <Owned<T>>::remove((collection.id, from, token));781 <Owned<T>>::insert((collection.id, to, token), true);782 }783 Self::set_allowance_unchecked(collection, from, token, None, true);784785 <PalletEvm<T>>::deposit_log(786 ERC721Events::Transfer {787 from: *from.as_eth(),788 to: *to.as_eth(),789 token_id: token.into(),790 }791 .to_log(collection_id_to_address(collection.id)),792 );793 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(794 collection.id,795 token,796 from.clone(),797 to.clone(),798 1,799 ));800801 Ok(PostDispatchInfo {802 actual_weight: Some(actual_weight),803 pays_fee: Pays::Yes,804 })805 }806807 /// Batch operation to mint multiple NFT tokens.808 ///809 /// The sender should be the owner/admin of the collection or collection should be configured810 /// to allow public minting.811 /// Throws if amount of tokens reached it's limit for the collection or if caller reached812 /// token ownership limit.813 ///814 /// - `data`: Contains list of token properties and users who will become the owners of the815 /// corresponging tokens.816 /// - `nesting_budget`: Limit for token nesting depth817 pub fn create_multiple_items(818 collection: &NonfungibleHandle<T>,819 sender: &T::CrossAccountId,820 data: Vec<CreateItemData<T>>,821 nesting_budget: &dyn Budget,822 ) -> DispatchResult {823 if !collection.is_owner_or_admin(sender) {824 ensure!(825 collection.permissions.mint_mode(),826 <CommonError<T>>::PublicMintingNotAllowed827 );828 collection.check_allowlist(sender)?;829830 for item in data.iter() {831 collection.check_allowlist(&item.owner)?;832 }833 }834835 for data in data.iter() {836 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;837 }838839 let first_token = <TokensMinted<T>>::get(collection.id);840 let tokens_minted = first_token841 .checked_add(data.len() as u32)842 .ok_or(ArithmeticError::Overflow)?;843 ensure!(844 tokens_minted <= collection.limits.token_limit(),845 <CommonError<T>>::CollectionTokenLimitExceeded846 );847848 let mut balances = BTreeMap::new();849 for data in &data {850 let balance = balances851 .entry(&data.owner)852 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));853 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;854855 ensure!(856 *balance <= collection.limits.account_token_ownership_limit(),857 <CommonError<T>>::AccountTokenLimitExceeded,858 );859 }860861 for (i, data) in data.iter().enumerate() {862 let token = TokenId(first_token + i as u32 + 1);863864 <PalletStructure<T>>::check_nesting(865 sender,866 &data.owner,867 collection.id,868 token,869 nesting_budget,870 )?;871 }872873 // =========874875 let mut property_writer = pallet_common::NewTokenPropertyWriter::new(collection, sender);876877 with_transaction(|| {878 for (i, data) in data.iter().enumerate() {879 let token = first_token + i as u32 + 1;880881 <TokenData<T>>::insert(882 (collection.id, token),883 ItemData {884 // const_data: data.const_data.clone(),885 owner: data.owner.clone(),886 },887 );888889 let token = TokenId(token);890891 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(892 &data.owner,893 collection.id,894 token,895 );896897 if let Err(e) = property_writer.write_token_properties(898 sender.conv_eq(&data.owner),899 token,900 data.properties.clone().into_iter(),901 erc::ERC721TokenEvent::TokenChanged {902 token_id: token.into(),903 }904 .to_log(T::ContractAddress::get()),905 ) {906 return TransactionOutcome::Rollback(Err(e));907 }908 }909 TransactionOutcome::Commit(Ok(()))910 })?;911912 <TokensMinted<T>>::insert(collection.id, tokens_minted);913 for (account, balance) in balances {914 <AccountBalance<T>>::insert((collection.id, account), balance);915 }916 for (i, data) in data.into_iter().enumerate() {917 let token = first_token + i as u32 + 1;918 <Owned<T>>::insert((collection.id, &data.owner, token), true);919920 <PalletEvm<T>>::deposit_log(921 ERC721Events::Transfer {922 from: H160::default(),923 to: *data.owner.as_eth(),924 token_id: token.into(),925 }926 .to_log(collection_id_to_address(collection.id)),927 );928 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(929 collection.id,930 TokenId(token),931 data.owner.clone(),932 1,933 ));934 }935 Ok(())936 }937938 pub fn set_allowance_unchecked(939 collection: &NonfungibleHandle<T>,940 sender: &T::CrossAccountId,941 token: TokenId,942 spender: Option<&T::CrossAccountId>,943 assume_implicit_eth: bool,944 ) {945 if let Some(spender) = spender {946 let old_spender = <Allowance<T>>::get((collection.id, token));947 <Allowance<T>>::insert((collection.id, token), spender);948 // In ERC721 there is only one possible approved user of token, so we set949 // approved user to spender950 <PalletEvm<T>>::deposit_log(951 ERC721Events::Approval {952 owner: *sender.as_eth(),953 approved: *spender.as_eth(),954 token_id: token.into(),955 }956 .to_log(collection_id_to_address(collection.id)),957 );958 // In Unique chain, any token can have any amount of approved users, so we need to959 // set allowance of old owner to 0, and allowance of new owner to 1960 if old_spender.as_ref() != Some(spender) {961 if let Some(old_owner) = old_spender {962 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(963 collection.id,964 token,965 sender.clone(),966 old_owner,967 0,968 ));969 }970 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(971 collection.id,972 token,973 sender.clone(),974 spender.clone(),975 1,976 ));977 }978 } else {979 let old_spender = <Allowance<T>>::take((collection.id, token));980 if !assume_implicit_eth {981 // In ERC721 there is only one possible approved user of token, so we set982 // approved user to zero address983 <PalletEvm<T>>::deposit_log(984 ERC721Events::Approval {985 owner: *sender.as_eth(),986 approved: H160::default(),987 token_id: token.into(),988 }989 .to_log(collection_id_to_address(collection.id)),990 );991 }992 // In Unique chain, any token can have any amount of approved users, so we need to993 // set allowance of old owner to 0994 if let Some(old_spender) = old_spender {995 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(996 collection.id,997 token,998 sender.clone(),999 old_spender,1000 0,1001 ));1002 }1003 }1004 }10051006 pub fn get_allowance(1007 collection: &NonfungibleHandle<T>,1008 token_id: TokenId,1009 ) -> Result<Option<T::CrossAccountId>, DispatchError> {1010 ensure!(1011 <TokenData<T>>::get((collection.id, token_id)).is_some(),1012 <CommonError<T>>::TokenNotFound1013 );1014 Ok(<Allowance<T>>::get((collection.id, token_id)))1015 }10161017 /// Set allowance for the spender to `transfer` or `burn` sender's token.1018 ///1019 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1020 pub fn set_allowance(1021 collection: &NonfungibleHandle<T>,1022 sender: &T::CrossAccountId,1023 token: TokenId,1024 spender: Option<&T::CrossAccountId>,1025 ) -> DispatchResult {1026 if collection.permissions.access() == AccessMode::AllowList {1027 collection.check_allowlist(sender)?;1028 if let Some(spender) = spender {1029 collection.check_allowlist(spender)?;1030 }1031 }10321033 if let Some(spender) = spender {1034 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1035 }10361037 let token_data =1038 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1039 if &token_data.owner != sender {1040 ensure!(1041 collection.ignores_owned_amount(sender),1042 <CommonError<T>>::CantApproveMoreThanOwned1043 );1044 }10451046 // =========10471048 Self::set_allowance_unchecked(collection, sender, token, spender, false);1049 Ok(())1050 }10511052 /// Set allowance for the spender to `transfer` or `burn` sender's token from eth mirror.1053 ///1054 /// - `from`: Address of sender's eth mirror.1055 /// - `to`: Adress of spender.1056 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1057 pub fn set_allowance_from(1058 collection: &NonfungibleHandle<T>,1059 sender: &T::CrossAccountId,1060 from: &T::CrossAccountId,1061 token: TokenId,1062 to: Option<&T::CrossAccountId>,1063 ) -> DispatchResult {1064 if collection.permissions.access() == AccessMode::AllowList {1065 collection.check_allowlist(sender)?;1066 collection.check_allowlist(from)?;1067 if let Some(to) = to {1068 collection.check_allowlist(to)?;1069 }1070 }10711072 if let Some(to) = to {1073 <PalletCommon<T>>::ensure_correct_receiver(to)?;1074 }10751076 ensure!(1077 sender.conv_eq(from),1078 <CommonError<T>>::AddressIsNotEthMirror1079 );10801081 let token_data =1082 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1083 if token_data.owner != *from {1084 ensure!(1085 collection.limits.owner_can_transfer()1086 && (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from)),1087 <CommonError<T>>::CantApproveMoreThanOwned1088 );1089 }10901091 // =========10921093 Self::set_allowance_unchecked(collection, from, token, to, false);1094 Ok(())1095 }10961097 /// Checks allowance for the spender to use the token.1098 fn check_allowed(1099 collection: &NonfungibleHandle<T>,1100 spender: &T::CrossAccountId,1101 from: &T::CrossAccountId,1102 token: TokenId,1103 nesting_budget: &dyn Budget,1104 ) -> DispatchResult {1105 if spender.conv_eq(from) {1106 return Ok(());1107 }1108 if collection.permissions.access() == AccessMode::AllowList {1109 // `from`, `to` checked in [`transfer`]1110 collection.check_allowlist(spender)?;1111 }11121113 if collection.ignores_token_restrictions(spender) {1114 return Ok(());1115 }11161117 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1118 ensure!(1119 <PalletStructure<T>>::check_indirectly_owned(1120 spender.clone(),1121 source.0,1122 source.1,1123 None,1124 nesting_budget1125 )?,1126 <CommonError<T>>::ApprovedValueTooLow,1127 );1128 return Ok(());1129 }1130 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1131 return Ok(());1132 }1133 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1134 return Ok(());1135 }11361137 Err(<CommonError<T>>::ApprovedValueTooLow.into())1138 }11391140 /// Transfer NFT token from one account to another.1141 ///1142 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1143 /// The owner should set allowance for the spender to transfer token.1144 ///1145 /// [`transfer`]: struct.Pallet.html#method.transfer1146 pub fn transfer_from(1147 collection: &NonfungibleHandle<T>,1148 spender: &T::CrossAccountId,1149 from: &T::CrossAccountId,1150 to: &T::CrossAccountId,1151 token: TokenId,1152 nesting_budget: &dyn Budget,1153 ) -> DispatchResultWithPostInfo {1154 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11551156 // =========11571158 // Allowance is reset in [`transfer`]1159 let mut result =1160 Self::transfer_internal(collection, spender, from, to, token, nesting_budget);1161 add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());1162 result1163 }11641165 /// Burn NFT token for `from` account.1166 ///1167 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1168 /// set allowance for the spender to burn token.1169 ///1170 /// [`burn`]: struct.Pallet.html#method.burn1171 pub fn burn_from(1172 collection: &NonfungibleHandle<T>,1173 spender: &T::CrossAccountId,1174 from: &T::CrossAccountId,1175 token: TokenId,1176 nesting_budget: &dyn Budget,1177 ) -> DispatchResult {1178 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11791180 // =========11811182 Self::burn(collection, from, token)1183 }11841185 /// Check that `from` token could be nested in `under` token.1186 ///1187 pub fn check_nesting(1188 handle: &NonfungibleHandle<T>,1189 sender: &T::CrossAccountId,1190 from: (CollectionId, TokenId),1191 under: TokenId,1192 nesting_budget: &dyn Budget,1193 ) -> DispatchResult {1194 let nesting = handle.permissions.nesting();11951196 #[cfg(not(feature = "runtime-benchmarks"))]1197 let permissive = false;1198 #[cfg(feature = "runtime-benchmarks")]1199 let permissive = nesting.permissive;12001201 if permissive {1202 ensure!(1203 <TokenData<T>>::contains_key((handle.id, under)),1204 <CommonError<T>>::TokenNotFound1205 );1206 } else if nesting.token_owner1207 && <PalletStructure<T>>::check_indirectly_owned(1208 sender.clone(),1209 handle.id,1210 under,1211 Some(from),1212 nesting_budget,1213 )? {1214 // Pass, token existence and ouroboros checks are done in `check_indirectly_owned`1215 } else if nesting.collection_admin && handle.is_owner_or_admin(sender) {1216 // token existence and ouroboros checks are done in `get_checked_topmost_owner`1217 let _ = <PalletStructure<T>>::get_checked_topmost_owner(1218 handle.id,1219 under,1220 Some(from),1221 nesting_budget,1222 )?1223 .ok_or(<CommonError<T>>::TokenNotFound)?;1224 } else {1225 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1226 }12271228 if let Some(whitelist) = &nesting.restricted {1229 ensure!(1230 whitelist.contains(&from.0),1231 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1232 );1233 }1234 Ok(())1235 }12361237 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1238 if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1239 <TokenChildren<T>>::insert((under.0, under.1, to_nest), true);1240 }1241 }12421243 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1244 if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1245 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1246 }1247 }12481249 fn collection_has_tokens(collection_id: CollectionId) -> bool {1250 <TokenData<T>>::iter_prefix((collection_id,))1251 .next()1252 .is_some()1253 }12541255 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1256 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1257 .next()1258 .is_some()1259 }12601261 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1262 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1263 .map(|((child_collection_id, child_id), _)| TokenChild {1264 collection: child_collection_id,1265 token: child_id,1266 })1267 .collect()1268 }12691270 /// Mint single NFT token.1271 ///1272 /// Delegated to [`create_multiple_items`]1273 ///1274 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1275 pub fn create_item(1276 collection: &NonfungibleHandle<T>,1277 sender: &T::CrossAccountId,1278 data: CreateItemData<T>,1279 nesting_budget: &dyn Budget,1280 ) -> DispatchResult {1281 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1282 }12831284 /// Sets or unsets the approval of a given operator.1285 ///1286 /// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1287 /// - `owner`: Token owner1288 /// - `operator`: Operator1289 /// - `approve`: Should operator status be granted or revoked?1290 pub fn set_allowance_for_all(1291 collection: &NonfungibleHandle<T>,1292 owner: &T::CrossAccountId,1293 operator: &T::CrossAccountId,1294 approve: bool,1295 ) -> DispatchResult {1296 <PalletCommon<T>>::set_allowance_for_all(1297 collection,1298 owner,1299 operator,1300 approve,1301 || <CollectionAllowance<T>>::insert((collection.id, owner, operator), approve),1302 ERC721Events::ApprovalForAll {1303 owner: *owner.as_eth(),1304 operator: *operator.as_eth(),1305 approved: approve,1306 }1307 .to_log(collection_id_to_address(collection.id)),1308 )1309 }13101311 /// Tells whether the given `owner` approves the `operator`.1312 pub fn allowance_for_all(1313 collection: &NonfungibleHandle<T>,1314 owner: &T::CrossAccountId,1315 operator: &T::CrossAccountId,1316 ) -> bool {1317 <CollectionAllowance<T>>::get((collection.id, owner, operator))1318 }13191320 pub fn repair_item(collection: &NonfungibleHandle<T>, token: TokenId) -> DispatchResult {1321 <TokenProperties<T>>::mutate((collection.id, token), |properties| {1322 if let Some(properties) = properties {1323 properties.recompute_consumed_space();1324 }1325 });13261327 Ok(())1328 }1329}