difftreelog
fix separate depositor from sender in transfer_internal (#1013)
in: master
* fix: transfer-from * fix: remove Option from nester type * fix: check_nesting comment * fix: rename nester to depositor, add docs
9 files changed
pallets/balances-adapter/src/common.rsdiffbeforeafterboth--- a/pallets/balances-adapter/src/common.rs
+++ b/pallets/balances-adapter/src/common.rs
@@ -221,7 +221,7 @@
fn check_nesting(
&self,
- _sender: <T>::CrossAccountId,
+ _sender: &<T>::CrossAccountId,
_from: (up_data_structs::CollectionId, TokenId),
_under: TokenId,
_budget: &dyn up_data_structs::budget::Budget,
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -2194,7 +2194,7 @@
/// * `budget` - The maximum budget that can be spent on the check.
fn check_nesting(
&self,
- sender: T::CrossAccountId,
+ sender: &T::CrossAccountId,
from: (CollectionId, TokenId),
under: TokenId,
budget: &dyn Budget,
pallets/fungible/src/common.rsdiffbeforeafterboth--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -335,7 +335,7 @@
fn check_nesting(
&self,
- _sender: <T>::CrossAccountId,
+ _sender: &<T>::CrossAccountId,
_from: (CollectionId, TokenId),
_under: TokenId,
_nesting_budget: &dyn Budget,
pallets/fungible/src/lib.rsdiffbeforeafterboth--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -384,6 +384,21 @@
amount: u128,
nesting_budget: &dyn Budget,
) -> DispatchResultWithPostInfo {
+ let depositor = from;
+ Self::transfer_internal(collection, depositor, from, to, amount, nesting_budget)
+ }
+
+ /// Transfers tokens from the `from` account to the `to` account.
+ /// The `depositor` is the account who deposits the tokens.
+ /// For instance, the nesting rules will be checked against the `depositor`'s permissions.
+ fn transfer_internal(
+ collection: &FungibleHandle<T>,
+ depositor: &T::CrossAccountId,
+ from: &T::CrossAccountId,
+ to: &T::CrossAccountId,
+ amount: u128,
+ nesting_budget: &dyn Budget,
+ ) -> DispatchResultWithPostInfo {
ensure!(
collection.limits.transfers_enabled(),
<CommonError<T>>::TransferNotAllowed,
@@ -416,7 +431,7 @@
// from != to && amount != 0
<PalletStructure<T>>::nest_if_sent_to_token(
- from.clone(),
+ depositor,
to,
collection.id,
TokenId::default(),
@@ -473,7 +488,7 @@
for (to, _) in data.iter() {
<PalletStructure<T>>::check_nesting(
- sender.clone(),
+ sender,
to,
collection.id,
TokenId::default(),
@@ -730,7 +745,8 @@
// =========
- let mut result = Self::transfer(collection, from, to, amount, nesting_budget);
+ let mut result =
+ Self::transfer_internal(collection, spender, from, to, amount, nesting_budget);
add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());
result?;
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -407,7 +407,7 @@
fn check_nesting(
&self,
- sender: T::CrossAccountId,
+ sender: &T::CrossAccountId,
from: (CollectionId, TokenId),
under: TokenId,
nesting_budget: &dyn Budget,
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 /// Create NFT collection387 ///388 /// `init_collection` will take non-refundable deposit for collection creation.389 ///390 /// - `data`: Contains settings for collection limits and permissions.391 pub fn init_collection(392 owner: T::CrossAccountId,393 payer: T::CrossAccountId,394 data: CreateCollectionData<T::CrossAccountId>,395 ) -> Result<CollectionId, DispatchError> {396 <PalletCommon<T>>::init_collection(owner, payer, data)397 }398399 /// Destroy NFT collection400 ///401 /// `destroy_collection` will throw error if collection contains any tokens.402 /// Only owner can destroy collection.403 pub fn destroy_collection(404 collection: NonfungibleHandle<T>,405 sender: &T::CrossAccountId,406 ) -> DispatchResult {407 let id = collection.id;408409 if Self::collection_has_tokens(id) {410 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());411 }412413 // =========414415 PalletCommon::destroy_collection(collection.0, sender)?;416417 let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);418 let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);419 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);420 <TokensMinted<T>>::remove(id);421 <TokensBurnt<T>>::remove(id);422 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);423 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);424 let _ = <CollectionAllowance<T>>::clear_prefix((id,), u32::MAX, None);425 Ok(())426 }427428 /// Burn NFT token429 ///430 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token431 /// if the token is nested.432 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.433 /// Also removes all corresponding properties and auxiliary properties.434 ///435 /// - `token`: Token that should be burned436 /// - `collection`: Collection that contains the token437 pub fn burn(438 collection: &NonfungibleHandle<T>,439 sender: &T::CrossAccountId,440 token: TokenId,441 ) -> DispatchResult {442 let token_data =443 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;444 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);445446 if collection.permissions.access() == AccessMode::AllowList {447 collection.check_allowlist(sender)?;448 }449450 if Self::token_has_children(collection.id, token) {451 return Err(<Error<T>>::CantBurnNftWithChildren.into());452 }453454 let burnt = <TokensBurnt<T>>::get(collection.id)455 .checked_add(1)456 .ok_or(ArithmeticError::Overflow)?;457458 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))459 .checked_sub(1)460 .ok_or(ArithmeticError::Overflow)?;461462 // =========463464 if balance == 0 {465 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));466 } else {467 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);468 }469470 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);471472 <Owned<T>>::remove((collection.id, &token_data.owner, token));473 <TokensBurnt<T>>::insert(collection.id, burnt);474 <TokenData<T>>::remove((collection.id, token));475 <TokenProperties<T>>::remove((collection.id, token));476 let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);477 let old_spender = <Allowance<T>>::take((collection.id, token));478479 if let Some(old_spender) = old_spender {480 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(481 collection.id,482 token,483 token_data.owner.clone(),484 old_spender,485 0,486 ));487 }488489 <PalletEvm<T>>::deposit_log(490 ERC721Events::Transfer {491 from: *token_data.owner.as_eth(),492 to: H160::default(),493 token_id: token.into(),494 }495 .to_log(collection_id_to_address(collection.id)),496 );497 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(498 collection.id,499 token,500 token_data.owner,501 1,502 ));503 Ok(())504 }505506 /// A batch operation to add, edit or remove properties for a token.507 ///508 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.509 ///510 /// All affected properties should have `mutable` permission511 /// to be **deleted** or to be **set more than once**,512 /// and the sender should have permission to edit those properties.513 ///514 /// This function fires an event for each property change.515 /// In case of an error, all the changes (including the events) will be reverted516 /// since the function is transactional.517 #[transactional]518 fn modify_token_properties(519 collection: &NonfungibleHandle<T>,520 sender: &T::CrossAccountId,521 token_id: TokenId,522 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,523 nesting_budget: &dyn Budget,524 ) -> DispatchResult {525 let mut property_writer =526 pallet_common::ExistingTokenPropertyWriter::new(collection, sender);527528 property_writer.write_token_properties(529 sender,530 token_id,531 properties_updates,532 nesting_budget,533 erc::ERC721TokenEvent::TokenChanged {534 token_id: token_id.into(),535 }536 .to_log(T::ContractAddress::get()),537 )538 }539540 pub fn next_token_id(collection: &NonfungibleHandle<T>) -> Result<TokenId, DispatchError> {541 let next_token_id = <TokensMinted<T>>::get(collection.id)542 .checked_add(1)543 .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;544545 ensure!(546 collection.limits.token_limit() >= next_token_id,547 <CommonError<T>>::CollectionTokenLimitExceeded548 );549550 Ok(TokenId(next_token_id))551 }552553 /// Batch operation to add or edit properties for the token554 ///555 /// Same as [`modify_token_properties`] but doesn't allow to remove properties556 ///557 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties558 pub fn set_token_properties(559 collection: &NonfungibleHandle<T>,560 sender: &T::CrossAccountId,561 token_id: TokenId,562 properties: impl Iterator<Item = Property>,563 nesting_budget: &dyn Budget,564 ) -> DispatchResult {565 Self::modify_token_properties(566 collection,567 sender,568 token_id,569 properties.map(|p| (p.key, Some(p.value))),570 nesting_budget,571 )572 }573574 /// Add or edit single property for the token575 ///576 /// Calls [`set_token_properties`] internally577 ///578 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties579 pub fn set_token_property(580 collection: &NonfungibleHandle<T>,581 sender: &T::CrossAccountId,582 token_id: TokenId,583 property: Property,584 nesting_budget: &dyn Budget,585 ) -> DispatchResult {586 Self::set_token_properties(587 collection,588 sender,589 token_id,590 [property].into_iter(),591 nesting_budget,592 )593 }594595 /// Batch operation to remove properties from the token596 ///597 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties598 ///599 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties600 pub fn delete_token_properties(601 collection: &NonfungibleHandle<T>,602 sender: &T::CrossAccountId,603 token_id: TokenId,604 property_keys: impl Iterator<Item = PropertyKey>,605 nesting_budget: &dyn Budget,606 ) -> DispatchResult {607 Self::modify_token_properties(608 collection,609 sender,610 token_id,611 property_keys.into_iter().map(|key| (key, None)),612 nesting_budget,613 )614 }615616 /// Remove single property from the token617 ///618 /// Calls [`delete_token_properties`] internally619 ///620 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties621 pub fn delete_token_property(622 collection: &NonfungibleHandle<T>,623 sender: &T::CrossAccountId,624 token_id: TokenId,625 property_key: PropertyKey,626 nesting_budget: &dyn Budget,627 ) -> DispatchResult {628 Self::delete_token_properties(629 collection,630 sender,631 token_id,632 [property_key].into_iter(),633 nesting_budget,634 )635 }636637 /// Add or edit properties for the collection638 pub fn set_collection_properties(639 collection: &NonfungibleHandle<T>,640 sender: &T::CrossAccountId,641 properties: Vec<Property>,642 ) -> DispatchResult {643 <PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())644 }645646 /// Remove properties from the collection647 pub fn delete_collection_properties(648 collection: &CollectionHandle<T>,649 sender: &T::CrossAccountId,650 property_keys: Vec<PropertyKey>,651 ) -> DispatchResult {652 <PalletCommon<T>>::delete_collection_properties(653 collection,654 sender,655 property_keys.into_iter(),656 )657 }658659 /// Set property permissions for the token.660 ///661 /// Sender should be the owner or admin of token's collection.662 pub fn set_token_property_permissions(663 collection: &CollectionHandle<T>,664 sender: &T::CrossAccountId,665 property_permissions: Vec<PropertyKeyPermission>,666 ) -> DispatchResult {667 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)668 }669670 /// Set property permissions for the token with scope.671 ///672 /// Sender should be the owner or admin of token's collection.673 pub fn set_scoped_token_property_permissions(674 collection: &CollectionHandle<T>,675 sender: &T::CrossAccountId,676 scope: PropertyScope,677 property_permissions: Vec<PropertyKeyPermission>,678 ) -> DispatchResult {679 <PalletCommon<T>>::set_scoped_token_property_permissions(680 collection,681 sender,682 scope,683 property_permissions,684 )685 }686687 pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {688 <PalletCommon<T>>::property_permissions(collection_id)689 }690691 pub fn check_token_immediate_ownership(692 collection: &NonfungibleHandle<T>,693 token: TokenId,694 possible_owner: &T::CrossAccountId,695 ) -> DispatchResult {696 let token_data =697 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;698 ensure!(699 &token_data.owner == possible_owner,700 <CommonError<T>>::NoPermission701 );702 Ok(())703 }704705 /// Transfer NFT token from one account to another.706 ///707 /// `from` account stops being the owner and `to` account becomes the owner of the token.708 /// If `to` is token than `to` becomes owner of the token and the token become nested.709 /// Unnests token from previous parent if it was nested before.710 /// Removes allowance for the token if there was any.711 /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.712 ///713 /// - `nesting_budget`: Limit for token nesting depth714 pub fn transfer(715 collection: &NonfungibleHandle<T>,716 from: &T::CrossAccountId,717 to: &T::CrossAccountId,718 token: TokenId,719 nesting_budget: &dyn Budget,720 ) -> DispatchResultWithPostInfo {721 ensure!(722 collection.limits.transfers_enabled(),723 <CommonError<T>>::TransferNotAllowed724 );725726 let mut actual_weight = <SelfWeightOf<T>>::transfer_raw();727 let token_data =728 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;729 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);730731 if collection.permissions.access() == AccessMode::AllowList {732 collection.check_allowlist(from)?;733 collection.check_allowlist(to)?;734 actual_weight += <PalletCommonWeightOf<T>>::check_accesslist() * 2;735 }736 <PalletCommon<T>>::ensure_correct_receiver(to)?;737738 let balance_from = <AccountBalance<T>>::get((collection.id, from))739 .checked_sub(1)740 .ok_or(<CommonError<T>>::TokenValueTooLow)?;741 let balance_to = if from != to {742 let balance_to = <AccountBalance<T>>::get((collection.id, to))743 .checked_add(1)744 .ok_or(ArithmeticError::Overflow)?;745746 ensure!(747 balance_to < collection.limits.account_token_ownership_limit(),748 <CommonError<T>>::AccountTokenLimitExceeded,749 );750751 Some(balance_to)752 } else {753 None754 };755756 <PalletStructure<T>>::nest_if_sent_to_token(757 from.clone(),758 to,759 collection.id,760 token,761 nesting_budget,762 )?;763764 // =========765766 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);767768 <TokenData<T>>::insert((collection.id, token), ItemData { owner: to.clone() });769770 if let Some(balance_to) = balance_to {771 // from != to772 if balance_from == 0 {773 <AccountBalance<T>>::remove((collection.id, from));774 } else {775 <AccountBalance<T>>::insert((collection.id, from), balance_from);776 }777 <AccountBalance<T>>::insert((collection.id, to), balance_to);778 <Owned<T>>::remove((collection.id, from, token));779 <Owned<T>>::insert((collection.id, to, token), true);780 }781 Self::set_allowance_unchecked(collection, from, token, None, true);782783 <PalletEvm<T>>::deposit_log(784 ERC721Events::Transfer {785 from: *from.as_eth(),786 to: *to.as_eth(),787 token_id: token.into(),788 }789 .to_log(collection_id_to_address(collection.id)),790 );791 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(792 collection.id,793 token,794 from.clone(),795 to.clone(),796 1,797 ));798799 Ok(PostDispatchInfo {800 actual_weight: Some(actual_weight),801 pays_fee: Pays::Yes,802 })803 }804805 /// Batch operation to mint multiple NFT tokens.806 ///807 /// The sender should be the owner/admin of the collection or collection should be configured808 /// to allow public minting.809 /// Throws if amount of tokens reached it's limit for the collection or if caller reached810 /// token ownership limit.811 ///812 /// - `data`: Contains list of token properties and users who will become the owners of the813 /// corresponging tokens.814 /// - `nesting_budget`: Limit for token nesting depth815 pub fn create_multiple_items(816 collection: &NonfungibleHandle<T>,817 sender: &T::CrossAccountId,818 data: Vec<CreateItemData<T>>,819 nesting_budget: &dyn Budget,820 ) -> DispatchResult {821 if !collection.is_owner_or_admin(sender) {822 ensure!(823 collection.permissions.mint_mode(),824 <CommonError<T>>::PublicMintingNotAllowed825 );826 collection.check_allowlist(sender)?;827828 for item in data.iter() {829 collection.check_allowlist(&item.owner)?;830 }831 }832833 for data in data.iter() {834 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;835 }836837 let first_token = <TokensMinted<T>>::get(collection.id);838 let tokens_minted = first_token839 .checked_add(data.len() as u32)840 .ok_or(ArithmeticError::Overflow)?;841 ensure!(842 tokens_minted <= collection.limits.token_limit(),843 <CommonError<T>>::CollectionTokenLimitExceeded844 );845846 let mut balances = BTreeMap::new();847 for data in &data {848 let balance = balances849 .entry(&data.owner)850 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));851 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;852853 ensure!(854 *balance <= collection.limits.account_token_ownership_limit(),855 <CommonError<T>>::AccountTokenLimitExceeded,856 );857 }858859 for (i, data) in data.iter().enumerate() {860 let token = TokenId(first_token + i as u32 + 1);861862 <PalletStructure<T>>::check_nesting(863 sender.clone(),864 &data.owner,865 collection.id,866 token,867 nesting_budget,868 )?;869 }870871 // =========872873 let mut property_writer = pallet_common::NewTokenPropertyWriter::new(collection, sender);874875 with_transaction(|| {876 for (i, data) in data.iter().enumerate() {877 let token = first_token + i as u32 + 1;878879 <TokenData<T>>::insert(880 (collection.id, token),881 ItemData {882 // const_data: data.const_data.clone(),883 owner: data.owner.clone(),884 },885 );886887 let token = TokenId(token);888889 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(890 &data.owner,891 collection.id,892 token,893 );894895 if let Err(e) = property_writer.write_token_properties(896 sender.conv_eq(&data.owner),897 token,898 data.properties.clone().into_iter(),899 erc::ERC721TokenEvent::TokenChanged {900 token_id: token.into(),901 }902 .to_log(T::ContractAddress::get()),903 ) {904 return TransactionOutcome::Rollback(Err(e));905 }906 }907 TransactionOutcome::Commit(Ok(()))908 })?;909910 <TokensMinted<T>>::insert(collection.id, tokens_minted);911 for (account, balance) in balances {912 <AccountBalance<T>>::insert((collection.id, account), balance);913 }914 for (i, data) in data.into_iter().enumerate() {915 let token = first_token + i as u32 + 1;916 <Owned<T>>::insert((collection.id, &data.owner, token), true);917918 <PalletEvm<T>>::deposit_log(919 ERC721Events::Transfer {920 from: H160::default(),921 to: *data.owner.as_eth(),922 token_id: token.into(),923 }924 .to_log(collection_id_to_address(collection.id)),925 );926 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(927 collection.id,928 TokenId(token),929 data.owner.clone(),930 1,931 ));932 }933 Ok(())934 }935936 pub fn set_allowance_unchecked(937 collection: &NonfungibleHandle<T>,938 sender: &T::CrossAccountId,939 token: TokenId,940 spender: Option<&T::CrossAccountId>,941 assume_implicit_eth: bool,942 ) {943 if let Some(spender) = spender {944 let old_spender = <Allowance<T>>::get((collection.id, token));945 <Allowance<T>>::insert((collection.id, token), spender);946 // In ERC721 there is only one possible approved user of token, so we set947 // approved user to spender948 <PalletEvm<T>>::deposit_log(949 ERC721Events::Approval {950 owner: *sender.as_eth(),951 approved: *spender.as_eth(),952 token_id: token.into(),953 }954 .to_log(collection_id_to_address(collection.id)),955 );956 // In Unique chain, any token can have any amount of approved users, so we need to957 // set allowance of old owner to 0, and allowance of new owner to 1958 if old_spender.as_ref() != Some(spender) {959 if let Some(old_owner) = old_spender {960 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(961 collection.id,962 token,963 sender.clone(),964 old_owner,965 0,966 ));967 }968 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(969 collection.id,970 token,971 sender.clone(),972 spender.clone(),973 1,974 ));975 }976 } else {977 let old_spender = <Allowance<T>>::take((collection.id, token));978 if !assume_implicit_eth {979 // In ERC721 there is only one possible approved user of token, so we set980 // approved user to zero address981 <PalletEvm<T>>::deposit_log(982 ERC721Events::Approval {983 owner: *sender.as_eth(),984 approved: H160::default(),985 token_id: token.into(),986 }987 .to_log(collection_id_to_address(collection.id)),988 );989 }990 // In Unique chain, any token can have any amount of approved users, so we need to991 // set allowance of old owner to 0992 if let Some(old_spender) = old_spender {993 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(994 collection.id,995 token,996 sender.clone(),997 old_spender,998 0,999 ));1000 }1001 }1002 }10031004 pub fn get_allowance(1005 collection: &NonfungibleHandle<T>,1006 token_id: TokenId,1007 ) -> Result<Option<T::CrossAccountId>, DispatchError> {1008 ensure!(1009 <TokenData<T>>::get((collection.id, token_id)).is_some(),1010 <CommonError<T>>::TokenNotFound1011 );1012 Ok(<Allowance<T>>::get((collection.id, token_id)))1013 }10141015 /// Set allowance for the spender to `transfer` or `burn` sender's token.1016 ///1017 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1018 pub fn set_allowance(1019 collection: &NonfungibleHandle<T>,1020 sender: &T::CrossAccountId,1021 token: TokenId,1022 spender: Option<&T::CrossAccountId>,1023 ) -> DispatchResult {1024 if collection.permissions.access() == AccessMode::AllowList {1025 collection.check_allowlist(sender)?;1026 if let Some(spender) = spender {1027 collection.check_allowlist(spender)?;1028 }1029 }10301031 if let Some(spender) = spender {1032 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1033 }10341035 let token_data =1036 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1037 if &token_data.owner != sender {1038 ensure!(1039 collection.ignores_owned_amount(sender),1040 <CommonError<T>>::CantApproveMoreThanOwned1041 );1042 }10431044 // =========10451046 Self::set_allowance_unchecked(collection, sender, token, spender, false);1047 Ok(())1048 }10491050 /// Set allowance for the spender to `transfer` or `burn` sender's token from eth mirror.1051 ///1052 /// - `from`: Address of sender's eth mirror.1053 /// - `to`: Adress of spender.1054 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1055 pub fn set_allowance_from(1056 collection: &NonfungibleHandle<T>,1057 sender: &T::CrossAccountId,1058 from: &T::CrossAccountId,1059 token: TokenId,1060 to: Option<&T::CrossAccountId>,1061 ) -> DispatchResult {1062 if collection.permissions.access() == AccessMode::AllowList {1063 collection.check_allowlist(sender)?;1064 collection.check_allowlist(from)?;1065 if let Some(to) = to {1066 collection.check_allowlist(to)?;1067 }1068 }10691070 if let Some(to) = to {1071 <PalletCommon<T>>::ensure_correct_receiver(to)?;1072 }10731074 ensure!(1075 sender.conv_eq(from),1076 <CommonError<T>>::AddressIsNotEthMirror1077 );10781079 let token_data =1080 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1081 if token_data.owner != *from {1082 ensure!(1083 collection.limits.owner_can_transfer()1084 && (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from)),1085 <CommonError<T>>::CantApproveMoreThanOwned1086 );1087 }10881089 // =========10901091 Self::set_allowance_unchecked(collection, from, token, to, false);1092 Ok(())1093 }10941095 /// Checks allowance for the spender to use the token.1096 fn check_allowed(1097 collection: &NonfungibleHandle<T>,1098 spender: &T::CrossAccountId,1099 from: &T::CrossAccountId,1100 token: TokenId,1101 nesting_budget: &dyn Budget,1102 ) -> DispatchResult {1103 if spender.conv_eq(from) {1104 return Ok(());1105 }1106 if collection.permissions.access() == AccessMode::AllowList {1107 // `from`, `to` checked in [`transfer`]1108 collection.check_allowlist(spender)?;1109 }11101111 if collection.ignores_token_restrictions(spender) {1112 return Ok(());1113 }11141115 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1116 ensure!(1117 <PalletStructure<T>>::check_indirectly_owned(1118 spender.clone(),1119 source.0,1120 source.1,1121 None,1122 nesting_budget1123 )?,1124 <CommonError<T>>::ApprovedValueTooLow,1125 );1126 return Ok(());1127 }1128 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1129 return Ok(());1130 }1131 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1132 return Ok(());1133 }11341135 Err(<CommonError<T>>::ApprovedValueTooLow.into())1136 }11371138 /// Transfer NFT token from one account to another.1139 ///1140 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1141 /// The owner should set allowance for the spender to transfer token.1142 ///1143 /// [`transfer`]: struct.Pallet.html#method.transfer1144 pub fn transfer_from(1145 collection: &NonfungibleHandle<T>,1146 spender: &T::CrossAccountId,1147 from: &T::CrossAccountId,1148 to: &T::CrossAccountId,1149 token: TokenId,1150 nesting_budget: &dyn Budget,1151 ) -> DispatchResultWithPostInfo {1152 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11531154 // =========11551156 // Allowance is reset in [`transfer`]1157 let mut result = Self::transfer(collection, from, to, token, nesting_budget);1158 add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());1159 result1160 }11611162 /// Burn NFT token for `from` account.1163 ///1164 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1165 /// set allowance for the spender to burn token.1166 ///1167 /// [`burn`]: struct.Pallet.html#method.burn1168 pub fn burn_from(1169 collection: &NonfungibleHandle<T>,1170 spender: &T::CrossAccountId,1171 from: &T::CrossAccountId,1172 token: TokenId,1173 nesting_budget: &dyn Budget,1174 ) -> DispatchResult {1175 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11761177 // =========11781179 Self::burn(collection, from, token)1180 }11811182 /// Check that `from` token could be nested in `under` token.1183 ///1184 pub fn check_nesting(1185 handle: &NonfungibleHandle<T>,1186 sender: T::CrossAccountId,1187 from: (CollectionId, TokenId),1188 under: TokenId,1189 nesting_budget: &dyn Budget,1190 ) -> DispatchResult {1191 let nesting = handle.permissions.nesting();11921193 #[cfg(not(feature = "runtime-benchmarks"))]1194 let permissive = false;1195 #[cfg(feature = "runtime-benchmarks")]1196 let permissive = nesting.permissive;11971198 if permissive {1199 ensure!(1200 <TokenData<T>>::contains_key((handle.id, under)),1201 <CommonError<T>>::TokenNotFound1202 );1203 } else if nesting.token_owner1204 && <PalletStructure<T>>::check_indirectly_owned(1205 sender.clone(),1206 handle.id,1207 under,1208 Some(from),1209 nesting_budget,1210 )? {1211 // Pass, token existence and ouroboros checks are done in `check_indirectly_owned`1212 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1213 // token existence and ouroboros checks are done in `get_checked_topmost_owner`1214 let _ = <PalletStructure<T>>::get_checked_topmost_owner(1215 handle.id,1216 under,1217 Some(from),1218 nesting_budget,1219 )?1220 .ok_or(<CommonError<T>>::TokenNotFound)?;1221 } else {1222 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1223 }12241225 if let Some(whitelist) = &nesting.restricted {1226 ensure!(1227 whitelist.contains(&from.0),1228 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1229 );1230 }1231 Ok(())1232 }12331234 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1235 if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1236 <TokenChildren<T>>::insert((under.0, under.1, to_nest), true);1237 }1238 }12391240 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1241 if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1242 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1243 }1244 }12451246 fn collection_has_tokens(collection_id: CollectionId) -> bool {1247 <TokenData<T>>::iter_prefix((collection_id,))1248 .next()1249 .is_some()1250 }12511252 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1253 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1254 .next()1255 .is_some()1256 }12571258 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1259 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1260 .map(|((child_collection_id, child_id), _)| TokenChild {1261 collection: child_collection_id,1262 token: child_id,1263 })1264 .collect()1265 }12661267 /// Mint single NFT token.1268 ///1269 /// Delegated to [`create_multiple_items`]1270 ///1271 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1272 pub fn create_item(1273 collection: &NonfungibleHandle<T>,1274 sender: &T::CrossAccountId,1275 data: CreateItemData<T>,1276 nesting_budget: &dyn Budget,1277 ) -> DispatchResult {1278 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1279 }12801281 /// Sets or unsets the approval of a given operator.1282 ///1283 /// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1284 /// - `owner`: Token owner1285 /// - `operator`: Operator1286 /// - `approve`: Should operator status be granted or revoked?1287 pub fn set_allowance_for_all(1288 collection: &NonfungibleHandle<T>,1289 owner: &T::CrossAccountId,1290 operator: &T::CrossAccountId,1291 approve: bool,1292 ) -> DispatchResult {1293 <PalletCommon<T>>::set_allowance_for_all(1294 collection,1295 owner,1296 operator,1297 approve,1298 || <CollectionAllowance<T>>::insert((collection.id, owner, operator), approve),1299 ERC721Events::ApprovalForAll {1300 owner: *owner.as_eth(),1301 operator: *operator.as_eth(),1302 approved: approve,1303 }1304 .to_log(collection_id_to_address(collection.id)),1305 )1306 }13071308 /// Tells whether the given `owner` approves the `operator`.1309 pub fn allowance_for_all(1310 collection: &NonfungibleHandle<T>,1311 owner: &T::CrossAccountId,1312 operator: &T::CrossAccountId,1313 ) -> bool {1314 <CollectionAllowance<T>>::get((collection.id, owner, operator))1315 }13161317 pub fn repair_item(collection: &NonfungibleHandle<T>, token: TokenId) -> DispatchResult {1318 <TokenProperties<T>>::mutate((collection.id, token), |properties| {1319 if let Some(properties) = properties {1320 properties.recompute_consumed_space();1321 }1322 });13231324 Ok(())1325 }1326}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 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 /// Create NFT collection387 ///388 /// `init_collection` will take non-refundable deposit for collection creation.389 ///390 /// - `data`: Contains settings for collection limits and permissions.391 pub fn init_collection(392 owner: T::CrossAccountId,393 payer: T::CrossAccountId,394 data: CreateCollectionData<T::CrossAccountId>,395 ) -> Result<CollectionId, DispatchError> {396 <PalletCommon<T>>::init_collection(owner, payer, data)397 }398399 /// Destroy NFT collection400 ///401 /// `destroy_collection` will throw error if collection contains any tokens.402 /// Only owner can destroy collection.403 pub fn destroy_collection(404 collection: NonfungibleHandle<T>,405 sender: &T::CrossAccountId,406 ) -> DispatchResult {407 let id = collection.id;408409 if Self::collection_has_tokens(id) {410 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());411 }412413 // =========414415 PalletCommon::destroy_collection(collection.0, sender)?;416417 let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);418 let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);419 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);420 <TokensMinted<T>>::remove(id);421 <TokensBurnt<T>>::remove(id);422 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);423 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);424 let _ = <CollectionAllowance<T>>::clear_prefix((id,), u32::MAX, None);425 Ok(())426 }427428 /// Burn NFT token429 ///430 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token431 /// if the token is nested.432 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.433 /// Also removes all corresponding properties and auxiliary properties.434 ///435 /// - `token`: Token that should be burned436 /// - `collection`: Collection that contains the token437 pub fn burn(438 collection: &NonfungibleHandle<T>,439 sender: &T::CrossAccountId,440 token: TokenId,441 ) -> DispatchResult {442 let token_data =443 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;444 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);445446 if collection.permissions.access() == AccessMode::AllowList {447 collection.check_allowlist(sender)?;448 }449450 if Self::token_has_children(collection.id, token) {451 return Err(<Error<T>>::CantBurnNftWithChildren.into());452 }453454 let burnt = <TokensBurnt<T>>::get(collection.id)455 .checked_add(1)456 .ok_or(ArithmeticError::Overflow)?;457458 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))459 .checked_sub(1)460 .ok_or(ArithmeticError::Overflow)?;461462 // =========463464 if balance == 0 {465 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));466 } else {467 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);468 }469470 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);471472 <Owned<T>>::remove((collection.id, &token_data.owner, token));473 <TokensBurnt<T>>::insert(collection.id, burnt);474 <TokenData<T>>::remove((collection.id, token));475 <TokenProperties<T>>::remove((collection.id, token));476 let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);477 let old_spender = <Allowance<T>>::take((collection.id, token));478479 if let Some(old_spender) = old_spender {480 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(481 collection.id,482 token,483 token_data.owner.clone(),484 old_spender,485 0,486 ));487 }488489 <PalletEvm<T>>::deposit_log(490 ERC721Events::Transfer {491 from: *token_data.owner.as_eth(),492 to: H160::default(),493 token_id: token.into(),494 }495 .to_log(collection_id_to_address(collection.id)),496 );497 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(498 collection.id,499 token,500 token_data.owner,501 1,502 ));503 Ok(())504 }505506 /// A batch operation to add, edit or remove properties for a token.507 ///508 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.509 ///510 /// All affected properties should have `mutable` permission511 /// to be **deleted** or to be **set more than once**,512 /// and the sender should have permission to edit those properties.513 ///514 /// This function fires an event for each property change.515 /// In case of an error, all the changes (including the events) will be reverted516 /// since the function is transactional.517 #[transactional]518 fn modify_token_properties(519 collection: &NonfungibleHandle<T>,520 sender: &T::CrossAccountId,521 token_id: TokenId,522 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,523 nesting_budget: &dyn Budget,524 ) -> DispatchResult {525 let mut property_writer =526 pallet_common::ExistingTokenPropertyWriter::new(collection, sender);527528 property_writer.write_token_properties(529 sender,530 token_id,531 properties_updates,532 nesting_budget,533 erc::ERC721TokenEvent::TokenChanged {534 token_id: token_id.into(),535 }536 .to_log(T::ContractAddress::get()),537 )538 }539540 pub fn next_token_id(collection: &NonfungibleHandle<T>) -> Result<TokenId, DispatchError> {541 let next_token_id = <TokensMinted<T>>::get(collection.id)542 .checked_add(1)543 .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;544545 ensure!(546 collection.limits.token_limit() >= next_token_id,547 <CommonError<T>>::CollectionTokenLimitExceeded548 );549550 Ok(TokenId(next_token_id))551 }552553 /// Batch operation to add or edit properties for the token554 ///555 /// Same as [`modify_token_properties`] but doesn't allow to remove properties556 ///557 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties558 pub fn set_token_properties(559 collection: &NonfungibleHandle<T>,560 sender: &T::CrossAccountId,561 token_id: TokenId,562 properties: impl Iterator<Item = Property>,563 nesting_budget: &dyn Budget,564 ) -> DispatchResult {565 Self::modify_token_properties(566 collection,567 sender,568 token_id,569 properties.map(|p| (p.key, Some(p.value))),570 nesting_budget,571 )572 }573574 /// Add or edit single property for the token575 ///576 /// Calls [`set_token_properties`] internally577 ///578 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties579 pub fn set_token_property(580 collection: &NonfungibleHandle<T>,581 sender: &T::CrossAccountId,582 token_id: TokenId,583 property: Property,584 nesting_budget: &dyn Budget,585 ) -> DispatchResult {586 Self::set_token_properties(587 collection,588 sender,589 token_id,590 [property].into_iter(),591 nesting_budget,592 )593 }594595 /// Batch operation to remove properties from the token596 ///597 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties598 ///599 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties600 pub fn delete_token_properties(601 collection: &NonfungibleHandle<T>,602 sender: &T::CrossAccountId,603 token_id: TokenId,604 property_keys: impl Iterator<Item = PropertyKey>,605 nesting_budget: &dyn Budget,606 ) -> DispatchResult {607 Self::modify_token_properties(608 collection,609 sender,610 token_id,611 property_keys.into_iter().map(|key| (key, None)),612 nesting_budget,613 )614 }615616 /// Remove single property from the token617 ///618 /// Calls [`delete_token_properties`] internally619 ///620 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties621 pub fn delete_token_property(622 collection: &NonfungibleHandle<T>,623 sender: &T::CrossAccountId,624 token_id: TokenId,625 property_key: PropertyKey,626 nesting_budget: &dyn Budget,627 ) -> DispatchResult {628 Self::delete_token_properties(629 collection,630 sender,631 token_id,632 [property_key].into_iter(),633 nesting_budget,634 )635 }636637 /// Add or edit properties for the collection638 pub fn set_collection_properties(639 collection: &NonfungibleHandle<T>,640 sender: &T::CrossAccountId,641 properties: Vec<Property>,642 ) -> DispatchResult {643 <PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())644 }645646 /// Remove properties from the collection647 pub fn delete_collection_properties(648 collection: &CollectionHandle<T>,649 sender: &T::CrossAccountId,650 property_keys: Vec<PropertyKey>,651 ) -> DispatchResult {652 <PalletCommon<T>>::delete_collection_properties(653 collection,654 sender,655 property_keys.into_iter(),656 )657 }658659 /// Set property permissions for the token.660 ///661 /// Sender should be the owner or admin of token's collection.662 pub fn set_token_property_permissions(663 collection: &CollectionHandle<T>,664 sender: &T::CrossAccountId,665 property_permissions: Vec<PropertyKeyPermission>,666 ) -> DispatchResult {667 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)668 }669670 /// Set property permissions for the token with scope.671 ///672 /// Sender should be the owner or admin of token's collection.673 pub fn set_scoped_token_property_permissions(674 collection: &CollectionHandle<T>,675 sender: &T::CrossAccountId,676 scope: PropertyScope,677 property_permissions: Vec<PropertyKeyPermission>,678 ) -> DispatchResult {679 <PalletCommon<T>>::set_scoped_token_property_permissions(680 collection,681 sender,682 scope,683 property_permissions,684 )685 }686687 pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {688 <PalletCommon<T>>::property_permissions(collection_id)689 }690691 pub fn check_token_immediate_ownership(692 collection: &NonfungibleHandle<T>,693 token: TokenId,694 possible_owner: &T::CrossAccountId,695 ) -> DispatchResult {696 let token_data =697 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;698 ensure!(699 &token_data.owner == possible_owner,700 <CommonError<T>>::NoPermission701 );702 Ok(())703 }704705 /// Transfer NFT token from one account to another.706 ///707 /// `from` account stops being the owner and `to` account becomes the owner of the token.708 /// If `to` is token than `to` becomes owner of the token and the token become nested.709 /// Unnests token from previous parent if it was nested before.710 /// Removes allowance for the token if there was any.711 /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.712 ///713 /// - `nesting_budget`: Limit for token nesting depth714 pub fn transfer(715 collection: &NonfungibleHandle<T>,716 from: &T::CrossAccountId,717 to: &T::CrossAccountId,718 token: TokenId,719 nesting_budget: &dyn Budget,720 ) -> DispatchResultWithPostInfo {721 let depositor = from;722 Self::transfer_internal(collection, depositor, from, to, token, nesting_budget)723 }724725 /// Transfers an NFT from the `from` account to the `to` account.726 /// The `depositor` is the account who deposits the NFT.727 /// For instance, the nesting rules will be checked against the `depositor`'s permissions.728 pub fn transfer_internal(729 collection: &NonfungibleHandle<T>,730 depositor: &T::CrossAccountId,731 from: &T::CrossAccountId,732 to: &T::CrossAccountId,733 token: TokenId,734 nesting_budget: &dyn Budget,735 ) -> DispatchResultWithPostInfo {736 ensure!(737 collection.limits.transfers_enabled(),738 <CommonError<T>>::TransferNotAllowed739 );740741 let mut actual_weight = <SelfWeightOf<T>>::transfer_raw();742 let token_data =743 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;744 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);745746 if collection.permissions.access() == AccessMode::AllowList {747 collection.check_allowlist(from)?;748 collection.check_allowlist(to)?;749 actual_weight += <PalletCommonWeightOf<T>>::check_accesslist() * 2;750 }751 <PalletCommon<T>>::ensure_correct_receiver(to)?;752753 let balance_from = <AccountBalance<T>>::get((collection.id, from))754 .checked_sub(1)755 .ok_or(<CommonError<T>>::TokenValueTooLow)?;756 let balance_to = if from != to {757 let balance_to = <AccountBalance<T>>::get((collection.id, to))758 .checked_add(1)759 .ok_or(ArithmeticError::Overflow)?;760761 ensure!(762 balance_to < collection.limits.account_token_ownership_limit(),763 <CommonError<T>>::AccountTokenLimitExceeded,764 );765766 Some(balance_to)767 } else {768 None769 };770771 <PalletStructure<T>>::nest_if_sent_to_token(772 depositor,773 to,774 collection.id,775 token,776 nesting_budget,777 )?;778779 // =========780781 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);782783 <TokenData<T>>::insert((collection.id, token), ItemData { owner: to.clone() });784785 if let Some(balance_to) = balance_to {786 // from != to787 if balance_from == 0 {788 <AccountBalance<T>>::remove((collection.id, from));789 } else {790 <AccountBalance<T>>::insert((collection.id, from), balance_from);791 }792 <AccountBalance<T>>::insert((collection.id, to), balance_to);793 <Owned<T>>::remove((collection.id, from, token));794 <Owned<T>>::insert((collection.id, to, token), true);795 }796 Self::set_allowance_unchecked(collection, from, token, None, true);797798 <PalletEvm<T>>::deposit_log(799 ERC721Events::Transfer {800 from: *from.as_eth(),801 to: *to.as_eth(),802 token_id: token.into(),803 }804 .to_log(collection_id_to_address(collection.id)),805 );806 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(807 collection.id,808 token,809 from.clone(),810 to.clone(),811 1,812 ));813814 Ok(PostDispatchInfo {815 actual_weight: Some(actual_weight),816 pays_fee: Pays::Yes,817 })818 }819820 /// Batch operation to mint multiple NFT tokens.821 ///822 /// The sender should be the owner/admin of the collection or collection should be configured823 /// to allow public minting.824 /// Throws if amount of tokens reached it's limit for the collection or if caller reached825 /// token ownership limit.826 ///827 /// - `data`: Contains list of token properties and users who will become the owners of the828 /// corresponging tokens.829 /// - `nesting_budget`: Limit for token nesting depth830 pub fn create_multiple_items(831 collection: &NonfungibleHandle<T>,832 sender: &T::CrossAccountId,833 data: Vec<CreateItemData<T>>,834 nesting_budget: &dyn Budget,835 ) -> DispatchResult {836 if !collection.is_owner_or_admin(sender) {837 ensure!(838 collection.permissions.mint_mode(),839 <CommonError<T>>::PublicMintingNotAllowed840 );841 collection.check_allowlist(sender)?;842843 for item in data.iter() {844 collection.check_allowlist(&item.owner)?;845 }846 }847848 for data in data.iter() {849 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;850 }851852 let first_token = <TokensMinted<T>>::get(collection.id);853 let tokens_minted = first_token854 .checked_add(data.len() as u32)855 .ok_or(ArithmeticError::Overflow)?;856 ensure!(857 tokens_minted <= collection.limits.token_limit(),858 <CommonError<T>>::CollectionTokenLimitExceeded859 );860861 let mut balances = BTreeMap::new();862 for data in &data {863 let balance = balances864 .entry(&data.owner)865 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));866 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;867868 ensure!(869 *balance <= collection.limits.account_token_ownership_limit(),870 <CommonError<T>>::AccountTokenLimitExceeded,871 );872 }873874 for (i, data) in data.iter().enumerate() {875 let token = TokenId(first_token + i as u32 + 1);876877 <PalletStructure<T>>::check_nesting(878 sender,879 &data.owner,880 collection.id,881 token,882 nesting_budget,883 )?;884 }885886 // =========887888 let mut property_writer = pallet_common::NewTokenPropertyWriter::new(collection, sender);889890 with_transaction(|| {891 for (i, data) in data.iter().enumerate() {892 let token = first_token + i as u32 + 1;893894 <TokenData<T>>::insert(895 (collection.id, token),896 ItemData {897 // const_data: data.const_data.clone(),898 owner: data.owner.clone(),899 },900 );901902 let token = TokenId(token);903904 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(905 &data.owner,906 collection.id,907 token,908 );909910 if let Err(e) = property_writer.write_token_properties(911 sender.conv_eq(&data.owner),912 token,913 data.properties.clone().into_iter(),914 erc::ERC721TokenEvent::TokenChanged {915 token_id: token.into(),916 }917 .to_log(T::ContractAddress::get()),918 ) {919 return TransactionOutcome::Rollback(Err(e));920 }921 }922 TransactionOutcome::Commit(Ok(()))923 })?;924925 <TokensMinted<T>>::insert(collection.id, tokens_minted);926 for (account, balance) in balances {927 <AccountBalance<T>>::insert((collection.id, account), balance);928 }929 for (i, data) in data.into_iter().enumerate() {930 let token = first_token + i as u32 + 1;931 <Owned<T>>::insert((collection.id, &data.owner, token), true);932933 <PalletEvm<T>>::deposit_log(934 ERC721Events::Transfer {935 from: H160::default(),936 to: *data.owner.as_eth(),937 token_id: token.into(),938 }939 .to_log(collection_id_to_address(collection.id)),940 );941 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(942 collection.id,943 TokenId(token),944 data.owner.clone(),945 1,946 ));947 }948 Ok(())949 }950951 pub fn set_allowance_unchecked(952 collection: &NonfungibleHandle<T>,953 sender: &T::CrossAccountId,954 token: TokenId,955 spender: Option<&T::CrossAccountId>,956 assume_implicit_eth: bool,957 ) {958 if let Some(spender) = spender {959 let old_spender = <Allowance<T>>::get((collection.id, token));960 <Allowance<T>>::insert((collection.id, token), spender);961 // In ERC721 there is only one possible approved user of token, so we set962 // approved user to spender963 <PalletEvm<T>>::deposit_log(964 ERC721Events::Approval {965 owner: *sender.as_eth(),966 approved: *spender.as_eth(),967 token_id: token.into(),968 }969 .to_log(collection_id_to_address(collection.id)),970 );971 // In Unique chain, any token can have any amount of approved users, so we need to972 // set allowance of old owner to 0, and allowance of new owner to 1973 if old_spender.as_ref() != Some(spender) {974 if let Some(old_owner) = old_spender {975 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(976 collection.id,977 token,978 sender.clone(),979 old_owner,980 0,981 ));982 }983 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(984 collection.id,985 token,986 sender.clone(),987 spender.clone(),988 1,989 ));990 }991 } else {992 let old_spender = <Allowance<T>>::take((collection.id, token));993 if !assume_implicit_eth {994 // In ERC721 there is only one possible approved user of token, so we set995 // approved user to zero address996 <PalletEvm<T>>::deposit_log(997 ERC721Events::Approval {998 owner: *sender.as_eth(),999 approved: H160::default(),1000 token_id: token.into(),1001 }1002 .to_log(collection_id_to_address(collection.id)),1003 );1004 }1005 // In Unique chain, any token can have any amount of approved users, so we need to1006 // set allowance of old owner to 01007 if let Some(old_spender) = old_spender {1008 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1009 collection.id,1010 token,1011 sender.clone(),1012 old_spender,1013 0,1014 ));1015 }1016 }1017 }10181019 pub fn get_allowance(1020 collection: &NonfungibleHandle<T>,1021 token_id: TokenId,1022 ) -> Result<Option<T::CrossAccountId>, DispatchError> {1023 ensure!(1024 <TokenData<T>>::get((collection.id, token_id)).is_some(),1025 <CommonError<T>>::TokenNotFound1026 );1027 Ok(<Allowance<T>>::get((collection.id, token_id)))1028 }10291030 /// Set allowance for the spender to `transfer` or `burn` sender's token.1031 ///1032 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1033 pub fn set_allowance(1034 collection: &NonfungibleHandle<T>,1035 sender: &T::CrossAccountId,1036 token: TokenId,1037 spender: Option<&T::CrossAccountId>,1038 ) -> DispatchResult {1039 if collection.permissions.access() == AccessMode::AllowList {1040 collection.check_allowlist(sender)?;1041 if let Some(spender) = spender {1042 collection.check_allowlist(spender)?;1043 }1044 }10451046 if let Some(spender) = spender {1047 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1048 }10491050 let token_data =1051 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1052 if &token_data.owner != sender {1053 ensure!(1054 collection.ignores_owned_amount(sender),1055 <CommonError<T>>::CantApproveMoreThanOwned1056 );1057 }10581059 // =========10601061 Self::set_allowance_unchecked(collection, sender, token, spender, false);1062 Ok(())1063 }10641065 /// Set allowance for the spender to `transfer` or `burn` sender's token from eth mirror.1066 ///1067 /// - `from`: Address of sender's eth mirror.1068 /// - `to`: Adress of spender.1069 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1070 pub fn set_allowance_from(1071 collection: &NonfungibleHandle<T>,1072 sender: &T::CrossAccountId,1073 from: &T::CrossAccountId,1074 token: TokenId,1075 to: Option<&T::CrossAccountId>,1076 ) -> DispatchResult {1077 if collection.permissions.access() == AccessMode::AllowList {1078 collection.check_allowlist(sender)?;1079 collection.check_allowlist(from)?;1080 if let Some(to) = to {1081 collection.check_allowlist(to)?;1082 }1083 }10841085 if let Some(to) = to {1086 <PalletCommon<T>>::ensure_correct_receiver(to)?;1087 }10881089 ensure!(1090 sender.conv_eq(from),1091 <CommonError<T>>::AddressIsNotEthMirror1092 );10931094 let token_data =1095 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1096 if token_data.owner != *from {1097 ensure!(1098 collection.limits.owner_can_transfer()1099 && (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from)),1100 <CommonError<T>>::CantApproveMoreThanOwned1101 );1102 }11031104 // =========11051106 Self::set_allowance_unchecked(collection, from, token, to, false);1107 Ok(())1108 }11091110 /// Checks allowance for the spender to use the token.1111 fn check_allowed(1112 collection: &NonfungibleHandle<T>,1113 spender: &T::CrossAccountId,1114 from: &T::CrossAccountId,1115 token: TokenId,1116 nesting_budget: &dyn Budget,1117 ) -> DispatchResult {1118 if spender.conv_eq(from) {1119 return Ok(());1120 }1121 if collection.permissions.access() == AccessMode::AllowList {1122 // `from`, `to` checked in [`transfer`]1123 collection.check_allowlist(spender)?;1124 }11251126 if collection.ignores_token_restrictions(spender) {1127 return Ok(());1128 }11291130 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1131 ensure!(1132 <PalletStructure<T>>::check_indirectly_owned(1133 spender.clone(),1134 source.0,1135 source.1,1136 None,1137 nesting_budget1138 )?,1139 <CommonError<T>>::ApprovedValueTooLow,1140 );1141 return Ok(());1142 }1143 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1144 return Ok(());1145 }1146 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1147 return Ok(());1148 }11491150 Err(<CommonError<T>>::ApprovedValueTooLow.into())1151 }11521153 /// Transfer NFT token from one account to another.1154 ///1155 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1156 /// The owner should set allowance for the spender to transfer token.1157 ///1158 /// [`transfer`]: struct.Pallet.html#method.transfer1159 pub fn transfer_from(1160 collection: &NonfungibleHandle<T>,1161 spender: &T::CrossAccountId,1162 from: &T::CrossAccountId,1163 to: &T::CrossAccountId,1164 token: TokenId,1165 nesting_budget: &dyn Budget,1166 ) -> DispatchResultWithPostInfo {1167 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11681169 // =========11701171 // Allowance is reset in [`transfer`]1172 let mut result =1173 Self::transfer_internal(collection, spender, from, to, token, nesting_budget);1174 add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());1175 result1176 }11771178 /// Burn NFT token for `from` account.1179 ///1180 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1181 /// set allowance for the spender to burn token.1182 ///1183 /// [`burn`]: struct.Pallet.html#method.burn1184 pub fn burn_from(1185 collection: &NonfungibleHandle<T>,1186 spender: &T::CrossAccountId,1187 from: &T::CrossAccountId,1188 token: TokenId,1189 nesting_budget: &dyn Budget,1190 ) -> DispatchResult {1191 Self::check_allowed(collection, spender, from, token, nesting_budget)?;11921193 // =========11941195 Self::burn(collection, from, token)1196 }11971198 /// Check that `from` token could be nested in `under` token.1199 ///1200 pub fn check_nesting(1201 handle: &NonfungibleHandle<T>,1202 sender: &T::CrossAccountId,1203 from: (CollectionId, TokenId),1204 under: TokenId,1205 nesting_budget: &dyn Budget,1206 ) -> DispatchResult {1207 let nesting = handle.permissions.nesting();12081209 #[cfg(not(feature = "runtime-benchmarks"))]1210 let permissive = false;1211 #[cfg(feature = "runtime-benchmarks")]1212 let permissive = nesting.permissive;12131214 if permissive {1215 ensure!(1216 <TokenData<T>>::contains_key((handle.id, under)),1217 <CommonError<T>>::TokenNotFound1218 );1219 } else if nesting.token_owner1220 && <PalletStructure<T>>::check_indirectly_owned(1221 sender.clone(),1222 handle.id,1223 under,1224 Some(from),1225 nesting_budget,1226 )? {1227 // Pass, token existence and ouroboros checks are done in `check_indirectly_owned`1228 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1229 // token existence and ouroboros checks are done in `get_checked_topmost_owner`1230 let _ = <PalletStructure<T>>::get_checked_topmost_owner(1231 handle.id,1232 under,1233 Some(from),1234 nesting_budget,1235 )?1236 .ok_or(<CommonError<T>>::TokenNotFound)?;1237 } else {1238 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1239 }12401241 if let Some(whitelist) = &nesting.restricted {1242 ensure!(1243 whitelist.contains(&from.0),1244 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1245 );1246 }1247 Ok(())1248 }12491250 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1251 if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1252 <TokenChildren<T>>::insert((under.0, under.1, to_nest), true);1253 }1254 }12551256 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1257 if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1258 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1259 }1260 }12611262 fn collection_has_tokens(collection_id: CollectionId) -> bool {1263 <TokenData<T>>::iter_prefix((collection_id,))1264 .next()1265 .is_some()1266 }12671268 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1269 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1270 .next()1271 .is_some()1272 }12731274 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1275 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1276 .map(|((child_collection_id, child_id), _)| TokenChild {1277 collection: child_collection_id,1278 token: child_id,1279 })1280 .collect()1281 }12821283 /// Mint single NFT token.1284 ///1285 /// Delegated to [`create_multiple_items`]1286 ///1287 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1288 pub fn create_item(1289 collection: &NonfungibleHandle<T>,1290 sender: &T::CrossAccountId,1291 data: CreateItemData<T>,1292 nesting_budget: &dyn Budget,1293 ) -> DispatchResult {1294 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1295 }12961297 /// Sets or unsets the approval of a given operator.1298 ///1299 /// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1300 /// - `owner`: Token owner1301 /// - `operator`: Operator1302 /// - `approve`: Should operator status be granted or revoked?1303 pub fn set_allowance_for_all(1304 collection: &NonfungibleHandle<T>,1305 owner: &T::CrossAccountId,1306 operator: &T::CrossAccountId,1307 approve: bool,1308 ) -> DispatchResult {1309 <PalletCommon<T>>::set_allowance_for_all(1310 collection,1311 owner,1312 operator,1313 approve,1314 || <CollectionAllowance<T>>::insert((collection.id, owner, operator), approve),1315 ERC721Events::ApprovalForAll {1316 owner: *owner.as_eth(),1317 operator: *operator.as_eth(),1318 approved: approve,1319 }1320 .to_log(collection_id_to_address(collection.id)),1321 )1322 }13231324 /// Tells whether the given `owner` approves the `operator`.1325 pub fn allowance_for_all(1326 collection: &NonfungibleHandle<T>,1327 owner: &T::CrossAccountId,1328 operator: &T::CrossAccountId,1329 ) -> bool {1330 <CollectionAllowance<T>>::get((collection.id, owner, operator))1331 }13321333 pub fn repair_item(collection: &NonfungibleHandle<T>, token: TokenId) -> DispatchResult {1334 <TokenProperties<T>>::mutate((collection.id, token), |properties| {1335 if let Some(properties) = properties {1336 properties.recompute_consumed_space();1337 }1338 });13391340 Ok(())1341 }1342}pallets/refungible/src/common.rsdiffbeforeafterboth--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -416,7 +416,7 @@
fn check_nesting(
&self,
- _sender: <T>::CrossAccountId,
+ _sender: &<T>::CrossAccountId,
_from: (CollectionId, TokenId),
_under: TokenId,
_nesting_budget: &dyn Budget,
pallets/refungible/src/lib.rsdiffbeforeafterboth--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -615,6 +615,30 @@
amount: u128,
nesting_budget: &dyn Budget,
) -> DispatchResult {
+ let depositor = from;
+ Self::transfer_internal(
+ collection,
+ depositor,
+ from,
+ to,
+ token,
+ amount,
+ nesting_budget,
+ )
+ }
+
+ /// Transfers RFT tokens from the `from` account to the `to` account.
+ /// The `depositor` is the account who deposits the tokens.
+ /// For instance, the nesting rules will be checked against the `depositor`'s permissions.
+ pub fn transfer_internal(
+ collection: &RefungibleHandle<T>,
+ depositor: &T::CrossAccountId,
+ from: &T::CrossAccountId,
+ to: &T::CrossAccountId,
+ token: TokenId,
+ amount: u128,
+ nesting_budget: &dyn Budget,
+ ) -> DispatchResult {
ensure!(
collection.limits.transfers_enabled(),
<CommonError<T>>::TransferNotAllowed
@@ -683,7 +707,7 @@
// from != to && amount != 0
<PalletStructure<T>>::nest_if_sent_to_token(
- from.clone(),
+ depositor,
to,
collection.id,
token,
@@ -847,7 +871,7 @@
let token_id = TokenId(first_token_id + i as u32 + 1);
for (to, _) in token.users.iter() {
<PalletStructure<T>>::check_nesting(
- sender.clone(),
+ sender,
to,
collection.id,
token_id,
@@ -1146,7 +1170,7 @@
// =========
- Self::transfer(collection, from, to, token, amount, nesting_budget)?;
+ Self::transfer_internal(collection, spender, from, to, token, amount, nesting_budget)?;
if let Some(allowance) = allowance {
Self::set_allowance_unchecked(collection, from, spender, token, allowance);
}
pallets/structure/src/lib.rsdiffbeforeafterboth--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -300,7 +300,7 @@
///
/// - `nesting_budget`: Limit for searching parents in depth.
pub fn check_nesting(
- from: T::CrossAccountId,
+ from: &T::CrossAccountId,
under: &T::CrossAccountId,
collection_id: CollectionId,
token_id: TokenId,
@@ -317,7 +317,7 @@
///
/// - `nesting_budget`: Limit for searching parents in depth.
pub fn nest_if_sent_to_token(
- from: T::CrossAccountId,
+ from: &T::CrossAccountId,
under: &T::CrossAccountId,
collection_id: CollectionId,
token_id: TokenId,