// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! This module contains helper methods to perform the transfer functionalities //! of the NFTs pallet. use crate::*; use frame_support::pallet_prelude::*; impl, I: 'static> Pallet { /// Transfer an NFT to the specified destination account. /// /// - `collection`: The ID of the collection to which the NFT belongs. /// - `item`: The ID of the NFT to transfer. /// - `dest`: The destination account to which the NFT will be transferred. /// - `with_details`: A closure that provides access to the collection and item details, /// allowing customization of the transfer process. /// /// This function performs the actual transfer of an NFT to the destination account. /// It checks various conditions like item lock status and transferability settings /// for the collection and item before transferring the NFT. /// /// # Errors /// /// This function returns a dispatch error in the following cases: /// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)). /// - If the item ID is invalid ([`UnknownItem`](crate::Error::UnknownItem)). /// - If the item is locked or transferring it is disabled /// ([`ItemLocked`](crate::Error::ItemLocked)). /// - If the collection or item is non-transferable /// ([`ItemsNonTransferable`](crate::Error::ItemsNonTransferable)). pub fn do_transfer( collection: T::CollectionId, item: T::ItemId, dest: T::AccountId, with_details: impl FnOnce( &CollectionDetailsFor, &mut ItemDetailsFor, ) -> DispatchResult, ) -> DispatchResult { // Retrieve collection details. let collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; // Ensure the item is not locked. ensure!(!T::Locker::is_locked(collection, item), Error::::ItemLocked); // Ensure the item is not transfer disabled on the system level attribute. ensure!( !Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?, Error::::ItemLocked ); // Retrieve collection config and check if items are transferable. let collection_config = Self::get_collection_config(&collection)?; ensure!( collection_config.is_setting_enabled(CollectionSetting::TransferableItems), Error::::ItemsNonTransferable ); // Retrieve item config and check if the item is transferable. let item_config = Self::get_item_config(&collection, &item)?; ensure!( item_config.is_setting_enabled(ItemSetting::Transferable), Error::::ItemLocked ); // Retrieve the item details. let mut details = Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; // Perform the transfer with custom details using the provided closure. with_details(&collection_details, &mut details)?; // Update account ownership information. Account::::remove((&details.owner, &collection, &item)); Account::::insert((&dest, &collection, &item), ()); let origin = details.owner; details.owner = dest; // The approved accounts have to be reset to `None`, because otherwise pre-approve attack // would be possible, where the owner can approve their second account before making the // transaction and then claiming the item back. details.approvals.clear(); // Update item details. Item::::insert(&collection, &item, &details); ItemPriceOf::::remove(&collection, &item); PendingSwapOf::::remove(&collection, &item); // Emit `Transferred` event. Self::deposit_event(Event::Transferred { collection, item, from: origin, to: details.owner, }); Ok(()) } /// Transfer ownership of a collection to another account. /// /// - `origin`: The account requesting the transfer. /// - `collection`: The ID of the collection to transfer ownership. /// - `owner`: The new account that will become the owner of the collection. /// /// This function transfers the ownership of a collection to the specified account. /// It performs checks to ensure that the `origin` is the current owner and that the /// new owner is an acceptable account based on the collection's acceptance settings. pub(crate) fn do_transfer_ownership( origin: T::AccountId, collection: T::CollectionId, new_owner: T::AccountId, ) -> DispatchResult { // Check if the new owner is acceptable based on the collection's acceptance settings. let acceptable_collection = OwnershipAcceptance::::get(&new_owner); ensure!(acceptable_collection.as_ref() == Some(&collection), Error::::Unaccepted); // Try to retrieve and mutate the collection details. Collection::::try_mutate(collection, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; // Check if the `origin` is the current owner of the collection. ensure!(origin == details.owner, Error::::NoPermission); if details.owner == new_owner { return Ok(()) } // Move the deposit to the new owner. T::Currency::repatriate_reserved( &details.owner, &new_owner, details.owner_deposit, Reserved, )?; // Update account ownership information. CollectionAccount::::remove(&details.owner, &collection); CollectionAccount::::insert(&new_owner, &collection, ()); details.owner = new_owner.clone(); OwnershipAcceptance::::remove(&new_owner); frame_system::Pallet::::dec_consumers(&new_owner); // Emit `OwnerChanged` event. Self::deposit_event(Event::OwnerChanged { collection, new_owner }); Ok(()) }) } /// Set or unset the ownership acceptance for an account regarding a specific collection. /// /// - `who`: The account for which to set or unset the ownership acceptance. /// - `maybe_collection`: An optional collection ID to set the ownership acceptance. /// /// If `maybe_collection` is `Some(collection)`, then the account `who` will accept /// ownership transfers for the specified collection. If `maybe_collection` is `None`, /// then the account `who` will unset the ownership acceptance, effectively refusing /// ownership transfers for any collection. pub(crate) fn do_set_accept_ownership( who: T::AccountId, maybe_collection: Option, ) -> DispatchResult { let exists = OwnershipAcceptance::::contains_key(&who); match (exists, maybe_collection.is_some()) { (false, true) => { frame_system::Pallet::::inc_consumers(&who)?; }, (true, false) => { frame_system::Pallet::::dec_consumers(&who); }, _ => {}, } if let Some(collection) = maybe_collection.as_ref() { OwnershipAcceptance::::insert(&who, collection); } else { OwnershipAcceptance::::remove(&who); } // Emit `OwnershipAcceptanceChanged` event. Self::deposit_event(Event::OwnershipAcceptanceChanged { who, maybe_collection }); Ok(()) } /// Forcefully change the owner of a collection. /// /// - `collection`: The ID of the collection to change ownership. /// - `owner`: The new account that will become the owner of the collection. /// /// This function allows for changing the ownership of a collection without any checks. /// It moves the deposit to the new owner, updates the collection's owner, and emits /// an `OwnerChanged` event. pub(crate) fn do_force_collection_owner( collection: T::CollectionId, owner: T::AccountId, ) -> DispatchResult { // Try to retrieve and mutate the collection details. Collection::::try_mutate(collection, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; if details.owner == owner { return Ok(()) } // Move the deposit to the new owner. T::Currency::repatriate_reserved( &details.owner, &owner, details.owner_deposit, Reserved, )?; // Update collection accounts and set the new owner. CollectionAccount::::remove(&details.owner, &collection); CollectionAccount::::insert(&owner, &collection, ()); details.owner = owner.clone(); // Emit `OwnerChanged` event. Self::deposit_event(Event::OwnerChanged { collection, new_owner: owner }); Ok(()) }) } }