feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,175 @@
// This file is part of Bizinikiwi.
// 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 functions for the approval logic implemented in the NFTs pallet.
//! The bitflag [`PalletFeature::Approvals`] needs to be set in [`Config::Features`] for NFTs
//! to have the functionality defined in this module.
use crate::*;
use pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Approves the transfer of an item to a delegate.
///
/// This function is used to approve the transfer of the specified `item` in the `collection` to
/// a `delegate`. If `maybe_check_origin` is specified, the function ensures that the
/// `check_origin` account is the owner of the item, granting them permission to approve the
/// transfer. The `delegate` is the account that will be allowed to take control of the item.
/// Optionally, a `deadline` can be specified to set a time limit for the approval. The
/// `deadline` is expressed in block numbers and is added to the current block number to
/// determine the absolute deadline for the approval. After approving the transfer, the function
/// emits the `TransferApproved` event.
///
/// - `maybe_check_origin`: The optional account that is required to be the owner of the item,
/// granting permission to approve the transfer. If `None`, no permission check is performed.
/// - `collection`: The identifier of the collection containing the item to be transferred.
/// - `item`: The identifier of the item to be transferred.
/// - `delegate`: The account that will be allowed to take control of the item.
/// - `maybe_deadline`: The optional deadline (in block numbers) specifying the time limit for
/// the approval.
pub(crate) fn do_approve_transfer(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
item: T::ItemId,
delegate: T::AccountId,
maybe_deadline: Option<BlockNumberFor<T, I>>,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Approvals),
Error::<T, I>::MethodDisabled
);
let mut details =
Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
let collection_config = Self::get_collection_config(&collection)?;
ensure!(
collection_config.is_setting_enabled(CollectionSetting::TransferableItems),
Error::<T, I>::ItemsNonTransferable
);
if let Some(check_origin) = maybe_check_origin {
ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
}
let now = T::BlockNumberProvider::current_block_number();
let deadline = maybe_deadline.map(|d| d.saturating_add(now));
details
.approvals
.try_insert(delegate.clone(), deadline)
.map_err(|_| Error::<T, I>::ReachedApprovalLimit)?;
Item::<T, I>::insert(&collection, &item, &details);
Self::deposit_event(Event::TransferApproved {
collection,
item,
owner: details.owner,
delegate,
deadline,
});
Ok(())
}
/// Cancels the approval for the transfer of an item to a delegate.
///
/// This function is used to cancel the approval for the transfer of the specified `item` in the
/// `collection` to a `delegate`. If `maybe_check_origin` is specified, the function ensures
/// that the `check_origin` account is the owner of the item or that the approval is past its
/// deadline, granting permission to cancel the approval. After canceling the approval, the
/// function emits the `ApprovalCancelled` event.
///
/// - `maybe_check_origin`: The optional account that is required to be the owner of the item or
/// that the approval is past its deadline, granting permission to cancel the approval. If
/// `None`, no permission check is performed.
/// - `collection`: The identifier of the collection containing the item.
/// - `item`: The identifier of the item.
/// - `delegate`: The account that was previously allowed to take control of the item.
pub(crate) fn do_cancel_approval(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
item: T::ItemId,
delegate: T::AccountId,
) -> DispatchResult {
let mut details =
Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
let maybe_deadline = details.approvals.get(&delegate).ok_or(Error::<T, I>::NotDelegate)?;
let is_past_deadline = if let Some(deadline) = maybe_deadline {
let now = T::BlockNumberProvider::current_block_number();
now > *deadline
} else {
false
};
if !is_past_deadline {
if let Some(check_origin) = maybe_check_origin {
ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
}
}
details.approvals.remove(&delegate);
Item::<T, I>::insert(&collection, &item, &details);
Self::deposit_event(Event::ApprovalCancelled {
collection,
item,
owner: details.owner,
delegate,
});
Ok(())
}
/// Clears all transfer approvals for an item.
///
/// This function is used to clear all transfer approvals for the specified `item` in the
/// `collection`. If `maybe_check_origin` is specified, the function ensures that the
/// `check_origin` account is the owner of the item, granting permission to clear all transfer
/// approvals. After clearing all approvals, the function emits the `AllApprovalsCancelled`
/// event.
///
/// - `maybe_check_origin`: The optional account that is required to be the owner of the item,
/// granting permission to clear all transfer approvals. If `None`, no permission check is
/// performed.
/// - `collection`: The collection ID containing the item.
/// - `item`: The item ID for which transfer approvals will be cleared.
pub(crate) fn do_clear_all_transfer_approvals(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
item: T::ItemId,
) -> DispatchResult {
let mut details =
Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownCollection)?;
if let Some(check_origin) = maybe_check_origin {
ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
}
details.approvals.clear();
Item::<T, I>::insert(&collection, &item, &details);
Self::deposit_event(Event::AllApprovalsCancelled {
collection,
item,
owner: details.owner,
});
Ok(())
}
}
@@ -0,0 +1,234 @@
// This file is part of Bizinikiwi.
// 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 functions for performing atomic swaps implemented in the NFTs
//! pallet.
//! The bitflag [`PalletFeature::Swaps`] needs to be set in [`Config::Features`] for NFTs
//! to have the functionality defined in this module.
use crate::*;
use pezframe_support::{
pezpallet_prelude::*,
traits::{Currency, ExistenceRequirement::KeepAlive},
};
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Creates a new swap offer for the specified item.
///
/// This function is used to create a new swap offer for the specified item. The `caller`
/// account must be the owner of the item. The swap offer specifies the `offered_collection`,
/// `offered_item`, `desired_collection`, `maybe_desired_item`, `maybe_price`, and `duration`.
/// The `duration` specifies the deadline by which the swap must be claimed. If
/// `maybe_desired_item` is `Some`, the specified item is expected in return for the swap. If
/// `maybe_desired_item` is `None`, it indicates that any item from the `desired_collection` can
/// be offered in return. The `maybe_price` specifies an optional price for the swap. If
/// specified, the other party must offer the specified `price` or higher for the swap. After
/// creating the swap, the function emits the `SwapCreated` event.
///
/// - `caller`: The account creating the swap offer, which must be the owner of the item.
/// - `offered_collection_id`: The collection ID containing the offered item.
/// - `offered_item_id`: The item ID offered for the swap.
/// - `desired_collection_id`: The collection ID containing the desired item (if any).
/// - `maybe_desired_item_id`: The ID of the desired item (if any).
/// - `maybe_price`: The optional price for the swap.
/// - `duration`: The duration (in block numbers) specifying the deadline for the swap claim.
pub(crate) fn do_create_swap(
caller: T::AccountId,
offered_collection_id: T::CollectionId,
offered_item_id: T::ItemId,
desired_collection_id: T::CollectionId,
maybe_desired_item_id: Option<T::ItemId>,
maybe_price: Option<PriceWithDirection<ItemPrice<T, I>>>,
duration: BlockNumberFor<T, I>,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Swaps),
Error::<T, I>::MethodDisabled
);
ensure!(duration <= T::MaxDeadlineDuration::get(), Error::<T, I>::WrongDuration);
let item = Item::<T, I>::get(&offered_collection_id, &offered_item_id)
.ok_or(Error::<T, I>::UnknownItem)?;
ensure!(item.owner == caller, Error::<T, I>::NoPermission);
match maybe_desired_item_id {
Some(desired_item_id) => ensure!(
Item::<T, I>::contains_key(&desired_collection_id, &desired_item_id),
Error::<T, I>::UnknownItem
),
None => ensure!(
Collection::<T, I>::contains_key(&desired_collection_id),
Error::<T, I>::UnknownCollection
),
};
let now = T::BlockNumberProvider::current_block_number();
let deadline = duration.saturating_add(now);
PendingSwapOf::<T, I>::insert(
&offered_collection_id,
&offered_item_id,
PendingSwap {
desired_collection: desired_collection_id,
desired_item: maybe_desired_item_id,
price: maybe_price.clone(),
deadline,
},
);
Self::deposit_event(Event::SwapCreated {
offered_collection: offered_collection_id,
offered_item: offered_item_id,
desired_collection: desired_collection_id,
desired_item: maybe_desired_item_id,
price: maybe_price,
deadline,
});
Ok(())
}
/// Cancels the specified swap offer.
///
/// This function is used to cancel the specified swap offer created by the `caller` account. If
/// the swap offer's deadline has not yet passed, the `caller` must be the owner of the offered
/// item; otherwise, anyone can cancel an expired offer.
/// After canceling the swap offer, the function emits the `SwapCancelled` event.
///
/// - `caller`: The account canceling the swap offer.
/// - `offered_collection_id`: The collection ID containing the offered item.
/// - `offered_item_id`: The item ID offered for the swap.
pub(crate) fn do_cancel_swap(
caller: T::AccountId,
offered_collection_id: T::CollectionId,
offered_item_id: T::ItemId,
) -> DispatchResult {
let swap = PendingSwapOf::<T, I>::get(&offered_collection_id, &offered_item_id)
.ok_or(Error::<T, I>::UnknownSwap)?;
let now = T::BlockNumberProvider::current_block_number();
if swap.deadline > now {
let item = Item::<T, I>::get(&offered_collection_id, &offered_item_id)
.ok_or(Error::<T, I>::UnknownItem)?;
ensure!(item.owner == caller, Error::<T, I>::NoPermission);
}
PendingSwapOf::<T, I>::remove(&offered_collection_id, &offered_item_id);
Self::deposit_event(Event::SwapCancelled {
offered_collection: offered_collection_id,
offered_item: offered_item_id,
desired_collection: swap.desired_collection,
desired_item: swap.desired_item,
price: swap.price,
deadline: swap.deadline,
});
Ok(())
}
/// Claims the specified swap offer.
///
/// This function is used to claim a swap offer specified by the `send_collection_id`,
/// `send_item_id`, `receive_collection_id`, and `receive_item_id`. The `caller` account must be
/// the owner of the item specified by `send_collection_id` and `send_item_id`. If the claimed
/// swap has an associated `price`, it will be transferred between the owners of the two items
/// based on the `price.direction`. After the swap is completed, the function emits the
/// `SwapClaimed` event.
///
/// - `caller`: The account claiming the swap offer, which must be the owner of the sent item.
/// - `send_collection_id`: The identifier of the collection containing the item being sent.
/// - `send_item_id`: The identifier of the item being sent for the swap.
/// - `receive_collection_id`: The identifier of the collection containing the item being
/// received.
/// - `receive_item_id`: The identifier of the item being received in the swap.
/// - `witness_price`: The optional witness price for the swap (price that was offered in the
/// swap).
pub(crate) fn do_claim_swap(
caller: T::AccountId,
send_collection_id: T::CollectionId,
send_item_id: T::ItemId,
receive_collection_id: T::CollectionId,
receive_item_id: T::ItemId,
witness_price: Option<PriceWithDirection<ItemPrice<T, I>>>,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Swaps),
Error::<T, I>::MethodDisabled
);
let send_item = Item::<T, I>::get(&send_collection_id, &send_item_id)
.ok_or(Error::<T, I>::UnknownItem)?;
let receive_item = Item::<T, I>::get(&receive_collection_id, &receive_item_id)
.ok_or(Error::<T, I>::UnknownItem)?;
let swap = PendingSwapOf::<T, I>::get(&receive_collection_id, &receive_item_id)
.ok_or(Error::<T, I>::UnknownSwap)?;
ensure!(send_item.owner == caller, Error::<T, I>::NoPermission);
ensure!(
swap.desired_collection == send_collection_id && swap.price == witness_price,
Error::<T, I>::UnknownSwap
);
if let Some(desired_item) = swap.desired_item {
ensure!(desired_item == send_item_id, Error::<T, I>::UnknownSwap);
}
let now = T::BlockNumberProvider::current_block_number();
ensure!(now <= swap.deadline, Error::<T, I>::DeadlineExpired);
if let Some(ref price) = swap.price {
match price.direction {
PriceDirection::Send => T::Currency::transfer(
&receive_item.owner,
&send_item.owner,
price.amount,
KeepAlive,
)?,
PriceDirection::Receive => T::Currency::transfer(
&send_item.owner,
&receive_item.owner,
price.amount,
KeepAlive,
)?,
};
}
// This also removes the swap.
Self::do_transfer(send_collection_id, send_item_id, receive_item.owner.clone(), |_, _| {
Ok(())
})?;
Self::do_transfer(
receive_collection_id,
receive_item_id,
send_item.owner.clone(),
|_, _| Ok(()),
)?;
Self::deposit_event(Event::SwapClaimed {
sent_collection: send_collection_id,
sent_item: send_item_id,
sent_item_owner: send_item.owner,
received_collection: receive_collection_id,
received_item: receive_item_id,
received_item_owner: receive_item.owner,
price: swap.price,
deadline: swap.deadline,
});
Ok(())
}
}
@@ -0,0 +1,525 @@
// This file is part of Bizinikiwi.
// 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 configure attributes for items and collections in the
//! NFTs pallet.
//! The bitflag [`PalletFeature::Attributes`] needs to be set in [`Config::Features`] for NFTs
//! to have the functionality defined in this module.
use crate::*;
use pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Sets the attribute of an item or a collection.
///
/// This function is used to set an attribute for an item or a collection. It checks the
/// provided `namespace` and verifies the permission of the caller to perform the action. The
/// `collection` and `maybe_item` parameters specify the target for the attribute.
///
/// - `origin`: The account attempting to set the attribute.
/// - `collection`: The identifier of the collection to which the item belongs, or the
/// collection itself if setting a collection attribute.
/// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if
/// setting a collection attribute.
/// - `namespace`: The namespace in which the attribute is being set. It can be either
/// `CollectionOwner`, `ItemOwner`, or `Account` (pre-approved external address).
/// - `key`: The key of the attribute. It should be a vector of bytes within the limits defined
/// by `T::KeyLimit`.
/// - `value`: The value of the attribute. It should be a vector of bytes within the limits
/// defined by `T::ValueLimit`.
/// - `depositor`: The account that is paying the deposit for the attribute.
///
/// Note: For the `CollectionOwner` namespace, the collection/item must have the
/// `UnlockedAttributes` setting enabled.
/// The deposit for setting an attribute is based on the `T::DepositPerByte` and
/// `T::AttributeDepositBase` configuration.
pub(crate) fn do_set_attribute(
origin: T::AccountId,
collection: T::CollectionId,
maybe_item: Option<T::ItemId>,
namespace: AttributeNamespace<T::AccountId>,
key: BoundedVec<u8, T::KeyLimit>,
value: BoundedVec<u8, T::ValueLimit>,
depositor: T::AccountId,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Attributes),
Error::<T, I>::MethodDisabled
);
ensure!(
Self::is_valid_namespace(&origin, &namespace, &collection, &maybe_item)?,
Error::<T, I>::NoPermission
);
let collection_config = Self::get_collection_config(&collection)?;
// for the `CollectionOwner` namespace we need to check if the collection/item is not locked
match namespace {
AttributeNamespace::CollectionOwner => match maybe_item {
None => {
ensure!(
collection_config.is_setting_enabled(CollectionSetting::UnlockedAttributes),
Error::<T, I>::LockedCollectionAttributes
)
},
Some(item) => {
let maybe_is_locked = Self::get_item_config(&collection, &item)
.map(|c| c.has_disabled_setting(ItemSetting::UnlockedAttributes))?;
ensure!(!maybe_is_locked, Error::<T, I>::LockedItemAttributes);
},
},
_ => (),
}
let mut collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
let attribute = Attribute::<T, I>::get((collection, maybe_item, &namespace, &key));
let attribute_exists = attribute.is_some();
if !attribute_exists {
collection_details.attributes.saturating_inc();
}
let old_deposit =
attribute.map_or(AttributeDeposit { account: None, amount: Zero::zero() }, |m| m.1);
let mut deposit = Zero::zero();
// disabled DepositRequired setting only affects the CollectionOwner namespace
if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) ||
namespace != AttributeNamespace::CollectionOwner
{
deposit = T::DepositPerByte::get()
.saturating_mul(((key.len() + value.len()) as u32).into())
.saturating_add(T::AttributeDepositBase::get());
}
let is_collection_owner_namespace = namespace == AttributeNamespace::CollectionOwner;
let is_depositor_collection_owner =
is_collection_owner_namespace && collection_details.owner == depositor;
// NOTE: in the CollectionOwner namespace if the depositor is `None` that means the deposit
// was paid by the collection's owner.
let old_depositor =
if is_collection_owner_namespace && old_deposit.account.is_none() && attribute_exists {
Some(collection_details.owner.clone())
} else {
old_deposit.account
};
let depositor_has_changed = old_depositor != Some(depositor.clone());
// NOTE: when we transfer an item, we don't move attributes in the ItemOwner namespace.
// When the new owner updates the same attribute, we will update the depositor record
// and return the deposit to the previous owner.
if depositor_has_changed {
if let Some(old_depositor) = old_depositor {
T::Currency::unreserve(&old_depositor, old_deposit.amount);
}
T::Currency::reserve(&depositor, deposit)?;
} else if deposit > old_deposit.amount {
T::Currency::reserve(&depositor, deposit - old_deposit.amount)?;
} else if deposit < old_deposit.amount {
T::Currency::unreserve(&depositor, old_deposit.amount - deposit);
}
if is_depositor_collection_owner {
if !depositor_has_changed {
collection_details.owner_deposit.saturating_reduce(old_deposit.amount);
}
collection_details.owner_deposit.saturating_accrue(deposit);
}
let new_deposit_owner = match is_depositor_collection_owner {
true => None,
false => Some(depositor),
};
Attribute::<T, I>::insert(
(&collection, maybe_item, &namespace, &key),
(&value, AttributeDeposit { account: new_deposit_owner, amount: deposit }),
);
Collection::<T, I>::insert(collection, &collection_details);
Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace });
Ok(())
}
/// Sets the attribute of an item or a collection without performing deposit checks.
///
/// This function is used to force-set an attribute for an item or a collection without
/// performing the deposit checks. It bypasses the deposit requirement and should only be used
/// in specific situations where deposit checks are not necessary or handled separately.
///
/// - `set_as`: The account that would normally pay for the deposit.
/// - `collection`: The identifier of the collection to which the item belongs, or the
/// collection itself if setting a collection attribute.
/// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if
/// setting a collection attribute.
/// - `namespace`: The namespace in which the attribute is being set. It can be either
/// `CollectionOwner`, `ItemOwner`, or `Account` (pre-approved external address).
/// - `key`: The key of the attribute. It should be a vector of bytes within the limits defined
/// by `T::KeyLimit`.
/// - `value`: The value of the attribute. It should be a vector of bytes within the limits
/// defined by `T::ValueLimit`.
pub(crate) fn do_force_set_attribute(
set_as: Option<T::AccountId>,
collection: T::CollectionId,
maybe_item: Option<T::ItemId>,
namespace: AttributeNamespace<T::AccountId>,
key: BoundedVec<u8, T::KeyLimit>,
value: BoundedVec<u8, T::ValueLimit>,
) -> DispatchResult {
let mut collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
let attribute = Attribute::<T, I>::get((collection, maybe_item, &namespace, &key));
if let Some((_, deposit)) = attribute {
if deposit.account != set_as && deposit.amount != Zero::zero() {
if let Some(deposit_account) = deposit.account {
T::Currency::unreserve(&deposit_account, deposit.amount);
}
}
} else {
collection_details.attributes.saturating_inc();
}
Attribute::<T, I>::insert(
(&collection, maybe_item, &namespace, &key),
(&value, AttributeDeposit { account: set_as, amount: Zero::zero() }),
);
Collection::<T, I>::insert(collection, &collection_details);
Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace });
Ok(())
}
/// Sets multiple attributes for an item or a collection.
///
/// This function checks the pre-signed data is valid and updates the attributes of an item or
/// collection. It is limited by [`Config::MaxAttributesPerCall`] to prevent excessive storage
/// consumption in a single transaction.
///
/// - `origin`: The account initiating the transaction.
/// - `data`: The data containing the details of the pre-signed attributes to be set.
/// - `signer`: The account of the pre-signed attributes signer.
pub(crate) fn do_set_attributes_pre_signed(
origin: T::AccountId,
data: PreSignedAttributesOf<T, I>,
signer: T::AccountId,
) -> DispatchResult {
let PreSignedAttributes { collection, item, attributes, namespace, deadline } = data;
ensure!(
attributes.len() <= T::MaxAttributesPerCall::get() as usize,
Error::<T, I>::MaxAttributesLimitReached
);
let now = T::BlockNumberProvider::current_block_number();
ensure!(deadline >= now, Error::<T, I>::DeadlineExpired);
let item_details =
Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
ensure!(item_details.owner == origin, Error::<T, I>::NoPermission);
// Only the CollectionOwner and Account() namespaces could be updated in this way.
// For the Account() namespace we check and set the approval if it wasn't set before.
match &namespace {
AttributeNamespace::CollectionOwner => {},
AttributeNamespace::Account(account) => {
ensure!(account == &signer, Error::<T, I>::NoPermission);
let approvals = ItemAttributesApprovalsOf::<T, I>::get(&collection, &item);
if !approvals.contains(account) {
Self::do_approve_item_attributes(
origin.clone(),
collection,
item,
account.clone(),
)?;
}
},
_ => return Err(Error::<T, I>::WrongNamespace.into()),
}
for (key, value) in attributes {
Self::do_set_attribute(
signer.clone(),
collection,
Some(item),
namespace.clone(),
Self::construct_attribute_key(key)?,
Self::construct_attribute_value(value)?,
origin.clone(),
)?;
}
Self::deposit_event(Event::PreSignedAttributesSet { collection, item, namespace });
Ok(())
}
/// Clears an attribute of an item or a collection.
///
/// This function allows clearing an attribute from an item or a collection. It verifies the
/// permission of the caller to perform the action based on the provided `namespace` and
/// `depositor` account. The deposit associated with the attribute, if any, will be unreserved.
///
/// - `maybe_check_origin`: An optional account that acts as an additional security check when
/// clearing the attribute. This can be `None` if no additional check is required.
/// - `collection`: The identifier of the collection to which the item belongs, or the
/// collection itself if clearing a collection attribute.
/// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if
/// clearing a collection attribute.
/// - `namespace`: The namespace in which the attribute is being cleared. It can be either
/// `CollectionOwner`, `ItemOwner`, or `Account`.
/// - `key`: The key of the attribute to be cleared. It should be a vector of bytes within the
/// limits defined by `T::KeyLimit`.
pub(crate) fn do_clear_attribute(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
maybe_item: Option<T::ItemId>,
namespace: AttributeNamespace<T::AccountId>,
key: BoundedVec<u8, T::KeyLimit>,
) -> DispatchResult {
let (_, deposit) = Attribute::<T, I>::take((collection, maybe_item, &namespace, &key))
.ok_or(Error::<T, I>::AttributeNotFound)?;
if let Some(check_origin) = &maybe_check_origin {
// validate the provided namespace when it's not a root call and the caller is not
// the same as the `deposit.account` (e.g. the deposit was paid by different account)
if deposit.account != maybe_check_origin {
ensure!(
Self::is_valid_namespace(&check_origin, &namespace, &collection, &maybe_item)?,
Error::<T, I>::NoPermission
);
}
// can't clear `CollectionOwner` type attributes if the collection/item is locked
match namespace {
AttributeNamespace::CollectionOwner => match maybe_item {
None => {
let collection_config = Self::get_collection_config(&collection)?;
ensure!(
collection_config
.is_setting_enabled(CollectionSetting::UnlockedAttributes),
Error::<T, I>::LockedCollectionAttributes
)
},
Some(item) => {
// NOTE: if the item was previously burned, the ItemConfigOf record
// might not exist. In that case, we allow to clear the attribute.
let maybe_is_locked = Self::get_item_config(&collection, &item)
.map_or(None, |c| {
Some(c.has_disabled_setting(ItemSetting::UnlockedAttributes))
});
if let Some(is_locked) = maybe_is_locked {
ensure!(!is_locked, Error::<T, I>::LockedItemAttributes);
// Only the collection's admin can clear attributes in that namespace.
// e.g. in off-chain mints, the attribute's depositor will be the item's
// owner, that's why we need to do this extra check.
ensure!(
Self::has_role(&collection, &check_origin, CollectionRole::Admin),
Error::<T, I>::NoPermission
);
}
},
},
_ => (),
};
}
let mut collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
collection_details.attributes.saturating_dec();
match deposit.account {
Some(deposit_account) => {
T::Currency::unreserve(&deposit_account, deposit.amount);
},
None if namespace == AttributeNamespace::CollectionOwner => {
collection_details.owner_deposit.saturating_reduce(deposit.amount);
T::Currency::unreserve(&collection_details.owner, deposit.amount);
},
_ => (),
}
Collection::<T, I>::insert(collection, &collection_details);
Self::deposit_event(Event::AttributeCleared { collection, maybe_item, key, namespace });
Ok(())
}
/// Approves a delegate to set attributes on behalf of the item's owner.
///
/// This function allows the owner of an item to approve a delegate to set attributes in the
/// `Account(delegate)` namespace. The maximum number of approvals is determined by
/// the configuration `T::MaxAttributesApprovals`.
///
/// - `check_origin`: The account of the item's owner attempting to approve the delegate.
/// - `collection`: The identifier of the collection to which the item belongs.
/// - `item`: The identifier of the item for which the delegate is being approved.
/// - `delegate`: The account that is being approved to set attributes on behalf of the item's
/// owner.
pub(crate) fn do_approve_item_attributes(
check_origin: T::AccountId,
collection: T::CollectionId,
item: T::ItemId,
delegate: T::AccountId,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Attributes),
Error::<T, I>::MethodDisabled
);
let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
ItemAttributesApprovalsOf::<T, I>::try_mutate(collection, item, |approvals| {
approvals
.try_insert(delegate.clone())
.map_err(|_| Error::<T, I>::ReachedApprovalLimit)?;
Self::deposit_event(Event::ItemAttributesApprovalAdded { collection, item, delegate });
Ok(())
})
}
/// Cancels the approval of an item's attributes by a delegate.
///
/// This function allows the owner of an item to cancel the approval of a delegate to set
/// attributes in the `Account(delegate)` namespace. The delegate's approval is removed, in
/// addition to attributes the `delegate` previously created, and any unreserved deposit
/// is returned. The number of attributes that the delegate has set for the item must
/// not exceed the `account_attributes` provided in the `witness`.
/// This function is used to prevent unintended or malicious cancellations.
///
/// - `check_origin`: The account of the item's owner attempting to cancel the delegate's
/// approval.
/// - `collection`: The identifier of the collection to which the item belongs.
/// - `item`: The identifier of the item for which the delegate's approval is being canceled.
/// - `delegate`: The account whose approval is being canceled.
/// - `witness`: The witness containing the number of attributes set by the delegate for the
/// item.
pub(crate) fn do_cancel_item_attributes_approval(
check_origin: T::AccountId,
collection: T::CollectionId,
item: T::ItemId,
delegate: T::AccountId,
witness: CancelAttributesApprovalWitness,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Attributes),
Error::<T, I>::MethodDisabled
);
let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
ItemAttributesApprovalsOf::<T, I>::try_mutate(collection, item, |approvals| {
approvals.remove(&delegate);
let mut attributes: u32 = 0;
let mut deposited: DepositBalanceOf<T, I> = Zero::zero();
for (_, (_, deposit)) in Attribute::<T, I>::drain_prefix((
&collection,
Some(item),
AttributeNamespace::Account(delegate.clone()),
)) {
attributes.saturating_inc();
deposited = deposited.saturating_add(deposit.amount);
}
ensure!(attributes <= witness.account_attributes, Error::<T, I>::BadWitness);
if !deposited.is_zero() {
T::Currency::unreserve(&delegate, deposited);
}
Self::deposit_event(Event::ItemAttributesApprovalRemoved {
collection,
item,
delegate,
});
Ok(())
})
}
/// A helper method to check whether an attribute namespace is valid.
fn is_valid_namespace(
origin: &T::AccountId,
namespace: &AttributeNamespace<T::AccountId>,
collection: &T::CollectionId,
maybe_item: &Option<T::ItemId>,
) -> Result<bool, DispatchError> {
let mut result = false;
match namespace {
AttributeNamespace::CollectionOwner =>
result = Self::has_role(&collection, &origin, CollectionRole::Admin),
AttributeNamespace::ItemOwner =>
if let Some(item) = maybe_item {
let item_details =
Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
result = origin == &item_details.owner
},
AttributeNamespace::Account(account_id) =>
if let Some(item) = maybe_item {
let approvals = ItemAttributesApprovalsOf::<T, I>::get(&collection, &item);
result = account_id == origin && approvals.contains(&origin)
},
_ => (),
};
Ok(result)
}
/// A helper method to construct an attribute's key.
///
/// # Errors
///
/// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the
/// provided attribute `key` is too long.
pub fn construct_attribute_key(
key: Vec<u8>,
) -> Result<BoundedVec<u8, T::KeyLimit>, DispatchError> {
Ok(BoundedVec::try_from(key).map_err(|_| Error::<T, I>::IncorrectData)?)
}
/// A helper method to construct an attribute's value.
///
/// # Errors
///
/// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the
/// provided `value` is too long.
pub fn construct_attribute_value(
value: Vec<u8>,
) -> Result<BoundedVec<u8, T::ValueLimit>, DispatchError> {
Ok(BoundedVec::try_from(value).map_err(|_| Error::<T, I>::IncorrectData)?)
}
/// A helper method to check whether a system attribute is set for a given item.
///
/// # Errors
///
/// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the
/// provided pallet attribute is too long.
pub fn has_system_attribute(
collection: &T::CollectionId,
item: &T::ItemId,
attribute_key: PalletAttributes<T::CollectionId>,
) -> Result<bool, DispatchError> {
let attribute = (
&collection,
Some(item),
AttributeNamespace::Pallet,
&Self::construct_attribute_key(attribute_key.encode())?,
);
Ok(Attribute::<T, I>::contains_key(attribute))
}
}
@@ -0,0 +1,172 @@
// This file is part of Bizinikiwi.
// 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 functions to perform the buy and sell functionalities of the NFTs
//! pallet.
//! The bitflag [`PalletFeature::Trading`] needs to be set in the [`Config::Features`] for NFTs
//! to have the functionality defined in this module.
use crate::*;
use pezframe_support::{
pezpallet_prelude::*,
traits::{Currency, ExistenceRequirement, ExistenceRequirement::KeepAlive},
};
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Pays the specified tips to the corresponding receivers.
///
/// This function is used to pay tips from the `sender` account to multiple receivers. The tips
/// are specified as a `BoundedVec` of `ItemTipOf` with a maximum length of `T::MaxTips`. For
/// each tip, the function transfers the `amount` to the `receiver` account. The sender is
/// responsible for ensuring the validity of the provided tips.
///
/// - `sender`: The account that pays the tips.
/// - `tips`: A `BoundedVec` containing the tips to be paid, where each tip contains the
/// `collection`, `item`, `receiver`, and `amount`.
pub(crate) fn do_pay_tips(
sender: T::AccountId,
tips: BoundedVec<ItemTipOf<T, I>, T::MaxTips>,
) -> DispatchResult {
for tip in tips {
let ItemTip { collection, item, receiver, amount } = tip;
T::Currency::transfer(&sender, &receiver, amount, KeepAlive)?;
Self::deposit_event(Event::TipSent {
collection,
item,
sender: sender.clone(),
receiver,
amount,
});
}
Ok(())
}
/// Sets the price and whitelists a buyer for an item in the specified collection.
///
/// This function is used to set the price and whitelist a buyer for an item in the
/// specified `collection`. The `sender` account must be the owner of the item. The item's price
/// and the whitelisted buyer can be set to allow trading the item. If `price` is `None`, the
/// item will be marked as not for sale.
///
/// - `collection`: The identifier of the collection containing the item.
/// - `item`: The identifier of the item for which the price and whitelist information will be
/// set.
/// - `sender`: The account that sets the price and whitelist information for the item.
/// - `price`: The optional price for the item.
/// - `whitelisted_buyer`: The optional account that is whitelisted to buy the item at the set
/// price.
pub(crate) fn do_set_price(
collection: T::CollectionId,
item: T::ItemId,
sender: T::AccountId,
price: Option<ItemPrice<T, I>>,
whitelisted_buyer: Option<T::AccountId>,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Trading),
Error::<T, I>::MethodDisabled
);
let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
ensure!(details.owner == sender, Error::<T, I>::NoPermission);
let collection_config = Self::get_collection_config(&collection)?;
ensure!(
collection_config.is_setting_enabled(CollectionSetting::TransferableItems),
Error::<T, I>::ItemsNonTransferable
);
let item_config = Self::get_item_config(&collection, &item)?;
ensure!(
item_config.is_setting_enabled(ItemSetting::Transferable),
Error::<T, I>::ItemLocked
);
if let Some(ref price) = price {
ItemPriceOf::<T, I>::insert(&collection, &item, (price, whitelisted_buyer.clone()));
Self::deposit_event(Event::ItemPriceSet {
collection,
item,
price: *price,
whitelisted_buyer,
});
} else {
ItemPriceOf::<T, I>::remove(&collection, &item);
Self::deposit_event(Event::ItemPriceRemoved { collection, item });
}
Ok(())
}
/// Buys the specified item from the collection.
///
/// This function is used to buy an item from the specified `collection`. The `buyer` account
/// will attempt to buy the item with the provided `bid_price`. The item's current owner will
/// receive the bid price if it is equal to or higher than the item's set price. If
/// `whitelisted_buyer` is specified in the item's price information, only that account is
/// allowed to buy the item. If the item is not for sale, or the bid price is too low, the
/// function will return an error.
///
/// - `collection`: The identifier of the collection containing the item to be bought.
/// - `item`: The identifier of the item to be bought.
/// - `buyer`: The account that attempts to buy the item.
/// - `bid_price`: The bid price offered by the buyer for the item.
pub(crate) fn do_buy_item(
collection: T::CollectionId,
item: T::ItemId,
buyer: T::AccountId,
bid_price: ItemPrice<T, I>,
) -> DispatchResult {
ensure!(
Self::is_pallet_feature_enabled(PalletFeature::Trading),
Error::<T, I>::MethodDisabled
);
let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
ensure!(details.owner != buyer, Error::<T, I>::NoPermission);
let price_info =
ItemPriceOf::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::NotForSale)?;
ensure!(bid_price >= price_info.0, Error::<T, I>::BidTooLow);
if let Some(only_buyer) = price_info.1 {
ensure!(only_buyer == buyer, Error::<T, I>::NoPermission);
}
T::Currency::transfer(
&buyer,
&details.owner,
price_info.0,
ExistenceRequirement::KeepAlive,
)?;
let old_owner = details.owner.clone();
Self::do_transfer(collection, item, buyer.clone(), |_, _| Ok(()))?;
Self::deposit_event(Event::ItemBought {
collection,
item,
price: price_info.0,
seller: old_owner,
buyer,
});
Ok(())
}
}
@@ -0,0 +1,153 @@
// This file is part of Bizinikiwi.
// 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 functionality associated with creating and
//! destroying collections for the NFTs pallet.
use crate::*;
use pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Create a new collection with the given `collection`, `owner`, `admin`, `config`, `deposit`,
/// and `event`.
///
/// This function creates a new collection with the provided parameters. It reserves the
/// required deposit from the owner's account, sets the collection details, assigns admin roles,
/// and inserts the provided configuration. Finally, it emits the specified event upon success.
///
/// # Errors
///
/// This function returns a [`CollectionIdInUse`](crate::Error::CollectionIdInUse) error if the
/// collection ID is already in use.
pub fn do_create_collection(
collection: T::CollectionId,
owner: T::AccountId,
admin: T::AccountId,
config: CollectionConfigFor<T, I>,
deposit: DepositBalanceOf<T, I>,
event: Event<T, I>,
) -> DispatchResult {
ensure!(!Collection::<T, I>::contains_key(collection), Error::<T, I>::CollectionIdInUse);
T::Currency::reserve(&owner, deposit)?;
Collection::<T, I>::insert(
collection,
CollectionDetails {
owner: owner.clone(),
owner_deposit: deposit,
items: 0,
item_metadatas: 0,
item_configs: 0,
attributes: 0,
},
);
CollectionRoleOf::<T, I>::insert(
collection,
admin,
CollectionRoles(
CollectionRole::Admin | CollectionRole::Freezer | CollectionRole::Issuer,
),
);
CollectionConfigOf::<T, I>::insert(&collection, config);
CollectionAccount::<T, I>::insert(&owner, &collection, ());
Self::deposit_event(event);
if let Some(max_supply) = config.max_supply {
Self::deposit_event(Event::CollectionMaxSupplySet { collection, max_supply });
}
Ok(())
}
/// Destroy the specified collection with the given `collection`, `witness`, and
/// `maybe_check_owner`.
///
/// This function destroys the specified collection if it exists and meets the necessary
/// conditions. It checks the provided `witness` against the actual collection details and
/// removes the collection along with its associated metadata, attributes, and configurations.
/// The necessary deposits are returned to the corresponding accounts, and the roles and
/// configurations for the collection are cleared. Finally, it emits the `Destroyed` event upon
/// successful destruction.
///
/// # Errors
///
/// This function returns a dispatch error in the following cases:
/// - If the collection ID is not found
/// ([`UnknownCollection`](crate::Error::UnknownCollection)).
/// - If the provided `maybe_check_owner` does not match the actual owner
/// ([`NoPermission`](crate::Error::NoPermission)).
/// - If the collection is not empty (contains items)
/// ([`CollectionNotEmpty`](crate::Error::CollectionNotEmpty)).
/// - If the `witness` does not match the actual collection details
/// ([`BadWitness`](crate::Error::BadWitness)).
pub fn do_destroy_collection(
collection: T::CollectionId,
witness: DestroyWitness,
maybe_check_owner: Option<T::AccountId>,
) -> Result<DestroyWitness, DispatchError> {
Collection::<T, I>::try_mutate_exists(collection, |maybe_details| {
let collection_details =
maybe_details.take().ok_or(Error::<T, I>::UnknownCollection)?;
if let Some(check_owner) = maybe_check_owner {
ensure!(collection_details.owner == check_owner, Error::<T, I>::NoPermission);
}
ensure!(collection_details.items == 0, Error::<T, I>::CollectionNotEmpty);
ensure!(collection_details.attributes == witness.attributes, Error::<T, I>::BadWitness);
ensure!(
collection_details.item_metadatas == witness.item_metadatas,
Error::<T, I>::BadWitness
);
ensure!(
collection_details.item_configs == witness.item_configs,
Error::<T, I>::BadWitness
);
for (_, metadata) in ItemMetadataOf::<T, I>::drain_prefix(&collection) {
if let Some(depositor) = metadata.deposit.account {
T::Currency::unreserve(&depositor, metadata.deposit.amount);
}
}
CollectionMetadataOf::<T, I>::remove(&collection);
Self::clear_roles(&collection)?;
for (_, (_, deposit)) in Attribute::<T, I>::drain_prefix((&collection,)) {
if !deposit.amount.is_zero() {
if let Some(account) = deposit.account {
T::Currency::unreserve(&account, deposit.amount);
}
}
}
CollectionAccount::<T, I>::remove(&collection_details.owner, &collection);
T::Currency::unreserve(&collection_details.owner, collection_details.owner_deposit);
CollectionConfigOf::<T, I>::remove(&collection);
let _ = ItemConfigOf::<T, I>::clear_prefix(&collection, witness.item_configs, None);
Self::deposit_event(Event::Destroyed { collection });
Ok(DestroyWitness {
item_metadatas: collection_details.item_metadatas,
item_configs: collection_details.item_configs,
attributes: collection_details.attributes,
})
})
}
}
@@ -0,0 +1,273 @@
// This file is part of Bizinikiwi.
// 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 functionality associated with minting and burning
//! items for the NFTs pallet.
use crate::*;
use pezframe_support::{pezpallet_prelude::*, traits::ExistenceRequirement};
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Mint a new unique item with the given `collection`, `item`, and other minting configuration
/// details.
///
/// This function performs the minting of a new unique item. It checks if the item does not
/// already exist in the given collection, and if the max supply limit (if configured) is not
/// reached. It also reserves the required deposit for the item and sets the item details
/// accordingly.
///
/// # Errors
///
/// This function returns a dispatch error in the following cases:
/// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)).
/// - If the item already exists in the collection
/// ([`AlreadyExists`](crate::Error::AlreadyExists)).
/// - If the item configuration already exists
/// ([`InconsistentItemConfig`](crate::Error::InconsistentItemConfig)).
/// - If the max supply limit (if configured) for the collection is reached
/// ([`MaxSupplyReached`](crate::Error::MaxSupplyReached)).
/// - If any error occurs in the `with_details_and_config` closure.
pub fn do_mint(
collection: T::CollectionId,
item: T::ItemId,
maybe_depositor: Option<T::AccountId>,
mint_to: T::AccountId,
item_config: ItemConfig,
with_details_and_config: impl FnOnce(
&CollectionDetailsFor<T, I>,
&CollectionConfigFor<T, I>,
) -> DispatchResult,
) -> DispatchResult {
ensure!(!Item::<T, I>::contains_key(collection, item), Error::<T, I>::AlreadyExists);
Collection::<T, I>::try_mutate(
&collection,
|maybe_collection_details| -> DispatchResult {
let collection_details =
maybe_collection_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
let collection_config = Self::get_collection_config(&collection)?;
with_details_and_config(collection_details, &collection_config)?;
if let Some(max_supply) = collection_config.max_supply {
ensure!(collection_details.items < max_supply, Error::<T, I>::MaxSupplyReached);
}
collection_details.items.saturating_inc();
let collection_config = Self::get_collection_config(&collection)?;
let deposit_amount = match collection_config
.is_setting_enabled(CollectionSetting::DepositRequired)
{
true => T::ItemDeposit::get(),
false => Zero::zero(),
};
let deposit_account = match maybe_depositor {
None => collection_details.owner.clone(),
Some(depositor) => depositor,
};
let item_owner = mint_to.clone();
Account::<T, I>::insert((&item_owner, &collection, &item), ());
if let Ok(existing_config) = ItemConfigOf::<T, I>::try_get(&collection, &item) {
ensure!(existing_config == item_config, Error::<T, I>::InconsistentItemConfig);
} else {
ItemConfigOf::<T, I>::insert(&collection, &item, item_config);
collection_details.item_configs.saturating_inc();
}
T::Currency::reserve(&deposit_account, deposit_amount)?;
let deposit = ItemDeposit { account: deposit_account, amount: deposit_amount };
let details = ItemDetails {
owner: item_owner,
approvals: ApprovalsOf::<T, I>::default(),
deposit,
};
Item::<T, I>::insert(&collection, &item, details);
Ok(())
},
)?;
Self::deposit_event(Event::Issued { collection, item, owner: mint_to });
Ok(())
}
/// Mints a new item using a pre-signed message.
///
/// This function allows minting a new item using a pre-signed message. The minting process is
/// similar to the regular minting process, but it is performed by a pre-authorized account. The
/// `mint_to` account receives the newly minted item. The minting process is configurable
/// through the provided `mint_data`. The attributes, metadata, and price of the item are set
/// according to the provided `mint_data`. The `with_details_and_config` closure is called to
/// validate the provided `collection_details` and `collection_config` before minting the item.
///
/// - `mint_to`: The account that receives the newly minted item.
/// - `mint_data`: The pre-signed minting data containing the `collection`, `item`,
/// `attributes`, `metadata`, `deadline`, `only_account`, and `mint_price`.
/// - `signer`: The account that is authorized to mint the item using the pre-signed message.
pub(crate) fn do_mint_pre_signed(
mint_to: T::AccountId,
mint_data: PreSignedMintOf<T, I>,
signer: T::AccountId,
) -> DispatchResult {
let PreSignedMint {
collection,
item,
attributes,
metadata,
deadline,
only_account,
mint_price,
} = mint_data;
let metadata = Self::construct_metadata(metadata)?;
ensure!(
attributes.len() <= T::MaxAttributesPerCall::get() as usize,
Error::<T, I>::MaxAttributesLimitReached
);
if let Some(account) = only_account {
ensure!(account == mint_to, Error::<T, I>::WrongOrigin);
}
let now = T::BlockNumberProvider::current_block_number();
ensure!(deadline >= now, Error::<T, I>::DeadlineExpired);
ensure!(
Self::has_role(&collection, &signer, CollectionRole::Issuer),
Error::<T, I>::NoPermission
);
let item_config = ItemConfig { settings: Self::get_default_item_settings(&collection)? };
Self::do_mint(
collection,
item,
Some(mint_to.clone()),
mint_to.clone(),
item_config,
|collection_details, _| {
if let Some(price) = mint_price {
T::Currency::transfer(
&mint_to,
&collection_details.owner,
price,
ExistenceRequirement::KeepAlive,
)?;
}
Ok(())
},
)?;
let admin_account = Self::find_account_by_role(&collection, CollectionRole::Admin);
if let Some(admin_account) = admin_account {
for (key, value) in attributes {
Self::do_set_attribute(
admin_account.clone(),
collection,
Some(item),
AttributeNamespace::CollectionOwner,
Self::construct_attribute_key(key)?,
Self::construct_attribute_value(value)?,
mint_to.clone(),
)?;
}
if !metadata.len().is_zero() {
Self::do_set_item_metadata(
Some(admin_account.clone()),
collection,
item,
metadata,
Some(mint_to.clone()),
)?;
}
}
Ok(())
}
/// Burns the specified item with the given `collection`, `item`, and `with_details`.
///
/// # Errors
///
/// This function returns a dispatch error in the following cases:
/// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)).
/// - If the item is locked ([`ItemLocked`](crate::Error::ItemLocked)).
pub fn do_burn(
collection: T::CollectionId,
item: T::ItemId,
with_details: impl FnOnce(&ItemDetailsFor<T, I>) -> DispatchResult,
) -> DispatchResult {
ensure!(!T::Locker::is_locked(collection, item), Error::<T, I>::ItemLocked);
ensure!(
!Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?,
Error::<T, I>::ItemLocked
);
let item_config = Self::get_item_config(&collection, &item)?;
// NOTE: if item's settings are not empty (e.g. item's metadata is locked)
// then we keep the config record and don't remove it
let remove_config = !item_config.has_disabled_settings();
let owner = Collection::<T, I>::try_mutate(
&collection,
|maybe_collection_details| -> Result<T::AccountId, DispatchError> {
let collection_details =
maybe_collection_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
let details = Item::<T, I>::get(&collection, &item)
.ok_or(Error::<T, I>::UnknownCollection)?;
with_details(&details)?;
// Return the deposit.
T::Currency::unreserve(&details.deposit.account, details.deposit.amount);
collection_details.items.saturating_dec();
if remove_config {
collection_details.item_configs.saturating_dec();
}
// Clear the metadata if it's not locked.
if item_config.is_setting_enabled(ItemSetting::UnlockedMetadata) {
if let Some(metadata) = ItemMetadataOf::<T, I>::take(&collection, &item) {
let depositor_account =
metadata.deposit.account.unwrap_or(collection_details.owner.clone());
T::Currency::unreserve(&depositor_account, metadata.deposit.amount);
collection_details.item_metadatas.saturating_dec();
if depositor_account == collection_details.owner {
collection_details
.owner_deposit
.saturating_reduce(metadata.deposit.amount);
}
}
}
Ok(details.owner)
},
)?;
Item::<T, I>::remove(&collection, &item);
Account::<T, I>::remove((&owner, &collection, &item));
ItemPriceOf::<T, I>::remove(&collection, &item);
PendingSwapOf::<T, I>::remove(&collection, &item);
ItemAttributesApprovalsOf::<T, I>::remove(&collection, &item);
if remove_config {
ItemConfigOf::<T, I>::remove(&collection, &item);
}
Self::deposit_event(Event::Burned { collection, item, owner });
Ok(())
}
}
@@ -0,0 +1,167 @@
// This file is part of Bizinikiwi.
// 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 configure locks on collections and items for the NFTs
//! pallet.
use crate::*;
use pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Locks a collection with specified settings.
///
/// The origin must be the owner of the collection to lock it. This function disables certain
/// settings on the collection. The only setting that can't be disabled is `DepositRequired`.
///
/// Note: it's possible only to lock the setting, but not to unlock it after.
///
/// - `origin`: The origin of the transaction, representing the account attempting to lock the
/// collection.
/// - `collection`: The identifier of the collection to be locked.
/// - `lock_settings`: The collection settings to be locked.
pub(crate) fn do_lock_collection(
origin: T::AccountId,
collection: T::CollectionId,
lock_settings: CollectionSettings,
) -> DispatchResult {
ensure!(Self::collection_owner(collection) == Some(origin), Error::<T, I>::NoPermission);
ensure!(
!lock_settings.is_disabled(CollectionSetting::DepositRequired),
Error::<T, I>::WrongSetting
);
CollectionConfigOf::<T, I>::try_mutate(collection, |maybe_config| {
let config = maybe_config.as_mut().ok_or(Error::<T, I>::NoConfig)?;
for setting in lock_settings.get_disabled() {
config.disable_setting(setting);
}
Self::deposit_event(Event::<T, I>::CollectionLocked { collection });
Ok(())
})
}
/// Locks the transfer of an item within a collection.
///
/// The origin must have the `Freezer` role within the collection to lock the transfer of the
/// item. This function disables the `Transferable` setting on the item, preventing it from
/// being transferred to other accounts.
///
/// - `origin`: The origin of the transaction, representing the account attempting to lock the
/// item transfer.
/// - `collection`: The identifier of the collection to which the item belongs.
/// - `item`: The identifier of the item to be locked for transfer.
pub(crate) fn do_lock_item_transfer(
origin: T::AccountId,
collection: T::CollectionId,
item: T::ItemId,
) -> DispatchResult {
ensure!(
Self::has_role(&collection, &origin, CollectionRole::Freezer),
Error::<T, I>::NoPermission
);
let mut config = Self::get_item_config(&collection, &item)?;
if !config.has_disabled_setting(ItemSetting::Transferable) {
config.disable_setting(ItemSetting::Transferable);
}
ItemConfigOf::<T, I>::insert(&collection, &item, config);
Self::deposit_event(Event::<T, I>::ItemTransferLocked { collection, item });
Ok(())
}
/// Unlocks the transfer of an item within a collection.
///
/// The origin must have the `Freezer` role within the collection to unlock the transfer of the
/// item. This function enables the `Transferable` setting on the item, allowing it to be
/// transferred to other accounts.
///
/// - `origin`: The origin of the transaction, representing the account attempting to unlock the
/// item transfer.
/// - `collection`: The identifier of the collection to which the item belongs.
/// - `item`: The identifier of the item to be unlocked for transfer.
pub(crate) fn do_unlock_item_transfer(
origin: T::AccountId,
collection: T::CollectionId,
item: T::ItemId,
) -> DispatchResult {
ensure!(
Self::has_role(&collection, &origin, CollectionRole::Freezer),
Error::<T, I>::NoPermission
);
let mut config = Self::get_item_config(&collection, &item)?;
if config.has_disabled_setting(ItemSetting::Transferable) {
config.enable_setting(ItemSetting::Transferable);
}
ItemConfigOf::<T, I>::insert(&collection, &item, config);
Self::deposit_event(Event::<T, I>::ItemTransferUnlocked { collection, item });
Ok(())
}
/// Locks the metadata and attributes of an item within a collection.
///
/// The origin must have the `Admin` role within the collection to lock the metadata and
/// attributes of the item. This function disables the `UnlockedMetadata` and
/// `UnlockedAttributes` settings on the item, preventing modifications to its metadata and
/// attributes.
///
/// - `maybe_check_origin`: An optional origin representing the account attempting to lock the
/// item properties. If provided, this account must have the `Admin` role within the
/// collection. If `None`, no permission check is performed, and the function can be called
/// from any origin.
/// - `collection`: The identifier of the collection to which the item belongs.
/// - `item`: The identifier of the item to be locked for properties.
/// - `lock_metadata`: A boolean indicating whether to lock the metadata of the item.
/// - `lock_attributes`: A boolean indicating whether to lock the attributes of the item.
pub(crate) fn do_lock_item_properties(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
item: T::ItemId,
lock_metadata: bool,
lock_attributes: bool,
) -> DispatchResult {
if let Some(check_origin) = &maybe_check_origin {
ensure!(
Self::has_role(&collection, &check_origin, CollectionRole::Admin),
Error::<T, I>::NoPermission
);
}
ItemConfigOf::<T, I>::try_mutate(collection, item, |maybe_config| {
let config = maybe_config.as_mut().ok_or(Error::<T, I>::UnknownItem)?;
if lock_metadata {
config.disable_setting(ItemSetting::UnlockedMetadata);
}
if lock_attributes {
config.disable_setting(ItemSetting::UnlockedAttributes);
}
Self::deposit_event(Event::<T, I>::ItemPropertiesLocked {
collection,
item,
lock_metadata,
lock_attributes,
});
Ok(())
})
}
}
@@ -0,0 +1,282 @@
// This file is part of Bizinikiwi.
// 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 configure the metadata of collections and items.
use crate::*;
use alloc::vec::Vec;
use pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Sets the metadata for a specific item within a collection.
///
/// - `maybe_check_origin`: An optional account ID that is allowed to set the metadata. If
/// `None`, it's considered the root account.
/// - `collection`: The ID of the collection to which the item belongs.
/// - `item`: The ID of the item to set the metadata for.
/// - `data`: The metadata to set for the item.
/// - `maybe_depositor`: An optional account ID that will provide the deposit for the metadata.
/// If `None`, the collection's owner provides the deposit.
///
/// Emits `ItemMetadataSet` event upon successful setting of the metadata.
/// Returns `Ok(())` on success, or one of the following dispatch errors:
/// - `UnknownCollection`: The specified collection does not exist.
/// - `UnknownItem`: The specified item does not exist within the collection.
/// - `LockedItemMetadata`: The metadata for the item is locked and cannot be modified.
/// - `NoPermission`: The caller does not have the required permission to set the metadata.
/// - `DepositExceeded`: The deposit amount exceeds the maximum allowed value.
pub(crate) fn do_set_item_metadata(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
item: T::ItemId,
data: BoundedVec<u8, T::StringLimit>,
maybe_depositor: Option<T::AccountId>,
) -> DispatchResult {
if let Some(check_origin) = &maybe_check_origin {
ensure!(
Self::has_role(&collection, &check_origin, CollectionRole::Admin),
Error::<T, I>::NoPermission
);
}
let is_root = maybe_check_origin.is_none();
let mut collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
let item_config = Self::get_item_config(&collection, &item)?;
ensure!(
is_root || item_config.is_setting_enabled(ItemSetting::UnlockedMetadata),
Error::<T, I>::LockedItemMetadata
);
let collection_config = Self::get_collection_config(&collection)?;
ItemMetadataOf::<T, I>::try_mutate_exists(collection, item, |metadata| {
if metadata.is_none() {
collection_details.item_metadatas.saturating_inc();
}
let old_deposit = metadata
.take()
.map_or(ItemMetadataDeposit { account: None, amount: Zero::zero() }, |m| m.deposit);
let mut deposit = Zero::zero();
if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) && !is_root
{
deposit = T::DepositPerByte::get()
.saturating_mul(((data.len()) as u32).into())
.saturating_add(T::MetadataDepositBase::get());
}
let depositor = maybe_depositor.clone().unwrap_or(collection_details.owner.clone());
let old_depositor = old_deposit.account.unwrap_or(collection_details.owner.clone());
if depositor != old_depositor {
T::Currency::unreserve(&old_depositor, old_deposit.amount);
T::Currency::reserve(&depositor, deposit)?;
} else if deposit > old_deposit.amount {
T::Currency::reserve(&depositor, deposit - old_deposit.amount)?;
} else if deposit < old_deposit.amount {
T::Currency::unreserve(&depositor, old_deposit.amount - deposit);
}
if maybe_depositor.is_none() {
collection_details.owner_deposit.saturating_accrue(deposit);
collection_details.owner_deposit.saturating_reduce(old_deposit.amount);
}
*metadata = Some(ItemMetadata {
deposit: ItemMetadataDeposit { account: maybe_depositor, amount: deposit },
data: data.clone(),
});
Collection::<T, I>::insert(&collection, &collection_details);
Self::deposit_event(Event::ItemMetadataSet { collection, item, data });
Ok(())
})
}
/// Clears the metadata for a specific item within a collection.
///
/// - `maybe_check_origin`: An optional account ID that is allowed to clear the metadata. If
/// `None`, it's considered the root account.
/// - `collection`: The ID of the collection to which the item belongs.
/// - `item`: The ID of the item for which to clear the metadata.
///
/// Emits `ItemMetadataCleared` event upon successful clearing of the metadata.
/// Returns `Ok(())` on success, or one of the following dispatch errors:
/// - `UnknownCollection`: The specified collection does not exist.
/// - `MetadataNotFound`: The metadata for the specified item was not found.
/// - `LockedItemMetadata`: The metadata for the item is locked and cannot be modified.
/// - `NoPermission`: The caller does not have the required permission to clear the metadata.
pub(crate) fn do_clear_item_metadata(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
item: T::ItemId,
) -> DispatchResult {
if let Some(check_origin) = &maybe_check_origin {
ensure!(
Self::has_role(&collection, &check_origin, CollectionRole::Admin),
Error::<T, I>::NoPermission
);
}
let is_root = maybe_check_origin.is_none();
let metadata = ItemMetadataOf::<T, I>::take(collection, item)
.ok_or(Error::<T, I>::MetadataNotFound)?;
let mut collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
let depositor_account =
metadata.deposit.account.unwrap_or(collection_details.owner.clone());
// NOTE: if the item was previously burned, the ItemConfigOf record might not exist
let is_locked = Self::get_item_config(&collection, &item)
.map_or(false, |c| c.has_disabled_setting(ItemSetting::UnlockedMetadata));
ensure!(is_root || !is_locked, Error::<T, I>::LockedItemMetadata);
collection_details.item_metadatas.saturating_dec();
T::Currency::unreserve(&depositor_account, metadata.deposit.amount);
if depositor_account == collection_details.owner {
collection_details.owner_deposit.saturating_reduce(metadata.deposit.amount);
}
Collection::<T, I>::insert(&collection, &collection_details);
Self::deposit_event(Event::ItemMetadataCleared { collection, item });
Ok(())
}
/// Sets the metadata for a specific collection.
///
/// - `maybe_check_origin`: An optional account ID that is allowed to set the collection
/// metadata. If `None`, it's considered the root account.
/// - `collection`: The ID of the collection for which to set the metadata.
/// - `data`: The metadata to set for the collection.
///
/// Emits `CollectionMetadataSet` event upon successful setting of the metadata.
/// Returns `Ok(())` on success, or one of the following dispatch errors:
/// - `UnknownCollection`: The specified collection does not exist.
/// - `LockedCollectionMetadata`: The metadata for the collection is locked and cannot be
/// modified.
/// - `NoPermission`: The caller does not have the required permission to set the metadata.
pub(crate) fn do_set_collection_metadata(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
data: BoundedVec<u8, T::StringLimit>,
) -> DispatchResult {
if let Some(check_origin) = &maybe_check_origin {
ensure!(
Self::has_role(&collection, &check_origin, CollectionRole::Admin),
Error::<T, I>::NoPermission
);
}
let is_root = maybe_check_origin.is_none();
let collection_config = Self::get_collection_config(&collection)?;
ensure!(
is_root || collection_config.is_setting_enabled(CollectionSetting::UnlockedMetadata),
Error::<T, I>::LockedCollectionMetadata
);
let mut details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
CollectionMetadataOf::<T, I>::try_mutate_exists(collection, |metadata| {
let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit);
details.owner_deposit.saturating_reduce(old_deposit);
let mut deposit = Zero::zero();
if !is_root && collection_config.is_setting_enabled(CollectionSetting::DepositRequired)
{
deposit = T::DepositPerByte::get()
.saturating_mul(((data.len()) as u32).into())
.saturating_add(T::MetadataDepositBase::get());
}
if deposit > old_deposit {
T::Currency::reserve(&details.owner, deposit - old_deposit)?;
} else if deposit < old_deposit {
T::Currency::unreserve(&details.owner, old_deposit - deposit);
}
details.owner_deposit.saturating_accrue(deposit);
Collection::<T, I>::insert(&collection, details);
*metadata = Some(CollectionMetadata { deposit, data: data.clone() });
Self::deposit_event(Event::CollectionMetadataSet { collection, data });
Ok(())
})
}
/// Clears the metadata for a specific collection.
///
/// - `maybe_check_origin`: An optional account ID that is allowed to clear the collection
/// metadata. If `None`, it's considered the root account.
/// - `collection`: The ID of the collection for which to clear the metadata.
///
/// Emits `CollectionMetadataCleared` event upon successful clearing of the metadata.
/// Returns `Ok(())` on success, or one of the following dispatch errors:
/// - `UnknownCollection`: The specified collection does not exist.
/// - `MetadataNotFound`: The metadata for the collection was not found.
/// - `LockedCollectionMetadata`: The metadata for the collection is locked and cannot be
/// modified.
/// - `NoPermission`: The caller does not have the required permission to clear the metadata.
pub(crate) fn do_clear_collection_metadata(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
) -> DispatchResult {
if let Some(check_origin) = &maybe_check_origin {
ensure!(
Self::has_role(&collection, &check_origin, CollectionRole::Admin),
Error::<T, I>::NoPermission
);
}
let mut details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
let collection_config = Self::get_collection_config(&collection)?;
ensure!(
maybe_check_origin.is_none() ||
collection_config.is_setting_enabled(CollectionSetting::UnlockedMetadata),
Error::<T, I>::LockedCollectionMetadata
);
CollectionMetadataOf::<T, I>::try_mutate_exists(collection, |metadata| {
let deposit = metadata.take().ok_or(Error::<T, I>::UnknownCollection)?.deposit;
T::Currency::unreserve(&details.owner, deposit);
details.owner_deposit.saturating_reduce(deposit);
Collection::<T, I>::insert(&collection, details);
Self::deposit_event(Event::CollectionMetadataCleared { collection });
Ok(())
})
}
/// A helper method to construct metadata.
///
/// # Errors
///
/// This function returns an [`IncorrectMetadata`](crate::Error::IncorrectMetadata) dispatch
/// error if the provided metadata is too long.
pub fn construct_metadata(
metadata: Vec<u8>,
) -> Result<BoundedVec<u8, T::StringLimit>, DispatchError> {
Ok(BoundedVec::try_from(metadata).map_err(|_| Error::<T, I>::IncorrectMetadata)?)
}
}
@@ -0,0 +1,28 @@
// This file is part of Bizinikiwi.
// 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.
pub mod approvals;
pub mod atomic_swap;
pub mod attributes;
pub mod buy_sell;
pub mod create_delete_collection;
pub mod create_delete_item;
pub mod lock;
pub mod metadata;
pub mod roles;
pub mod settings;
pub mod transfer;
@@ -0,0 +1,152 @@
// This file is part of Bizinikiwi.
// 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 configure account roles for existing collections.
use crate::*;
use alloc::{collections::btree_map::BTreeMap, vec::Vec};
use pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Set the team roles for a specific collection.
///
/// - `maybe_check_owner`: An optional account ID used to check ownership permission. If `None`,
/// it is considered as the root.
/// - `collection`: The ID of the collection for which to set the team roles.
/// - `issuer`: An optional account ID representing the issuer role.
/// - `admin`: An optional account ID representing the admin role.
/// - `freezer`: An optional account ID representing the freezer role.
///
/// This function allows the owner or the root (when `maybe_check_owner` is `None`) to set the
/// team roles for a specific collection. The root can change the role from `None` to
/// `Some(account)`, but other roles can only be updated by the root or an account with an
/// existing role in the collection.
pub(crate) fn do_set_team(
maybe_check_owner: Option<T::AccountId>,
collection: T::CollectionId,
issuer: Option<T::AccountId>,
admin: Option<T::AccountId>,
freezer: Option<T::AccountId>,
) -> DispatchResult {
Collection::<T, I>::try_mutate(collection, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
let is_root = maybe_check_owner.is_none();
if let Some(check_origin) = maybe_check_owner {
ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
}
let roles_map = [
(issuer.clone(), CollectionRole::Issuer),
(admin.clone(), CollectionRole::Admin),
(freezer.clone(), CollectionRole::Freezer),
];
// only root can change the role from `None` to `Some(account)`
if !is_root {
for (account, role) in roles_map.iter() {
if account.is_some() {
ensure!(
Self::find_account_by_role(&collection, *role).is_some(),
Error::<T, I>::NoPermission
);
}
}
}
let roles = roles_map
.into_iter()
.filter_map(|(account, role)| account.map(|account| (account, role)))
.collect();
let account_to_role = Self::group_roles_by_account(roles);
// Delete the previous records.
Self::clear_roles(&collection)?;
// Insert new records.
for (account, roles) in account_to_role {
CollectionRoleOf::<T, I>::insert(&collection, &account, roles);
}
Self::deposit_event(Event::TeamChanged { collection, issuer, admin, freezer });
Ok(())
})
}
/// Clears all the roles in a specified collection.
///
/// - `collection_id`: A collection to clear the roles in.
///
/// This function clears all the roles associated with the given `collection_id`. It throws an
/// error if some of the roles were left in storage, indicating that the maximum number of roles
/// may need to be adjusted.
pub(crate) fn clear_roles(collection_id: &T::CollectionId) -> Result<(), DispatchError> {
let res = CollectionRoleOf::<T, I>::clear_prefix(
&collection_id,
CollectionRoles::max_roles() as u32,
None,
);
ensure!(res.maybe_cursor.is_none(), Error::<T, I>::RolesNotCleared);
Ok(())
}
/// Returns true if a specified account has a provided role within that collection.
///
/// - `collection_id`: A collection to check the role in.
/// - `account_id`: An account to check the role for.
/// - `role`: A role to validate.
///
/// Returns `true` if the account has the specified role, `false` otherwise.
pub(crate) fn has_role(
collection_id: &T::CollectionId,
account_id: &T::AccountId,
role: CollectionRole,
) -> bool {
CollectionRoleOf::<T, I>::get(&collection_id, &account_id)
.map_or(false, |roles| roles.has_role(role))
}
/// Finds the account by a provided role within a collection.
///
/// - `collection_id`: A collection to check the role in.
/// - `role`: A role to find the account for.
///
/// Returns `Some(T::AccountId)` if the record was found, `None` otherwise.
pub(crate) fn find_account_by_role(
collection_id: &T::CollectionId,
role: CollectionRole,
) -> Option<T::AccountId> {
CollectionRoleOf::<T, I>::iter_prefix(&collection_id).into_iter().find_map(
|(account, roles)| if roles.has_role(role) { Some(account.clone()) } else { None },
)
}
/// Groups provided roles by account, given one account could have multiple roles.
///
/// - `input`: A vector of (Account, Role) tuples.
///
/// Returns a grouped vector of `(Account, Roles)` tuples.
pub fn group_roles_by_account(
input: Vec<(T::AccountId, CollectionRole)>,
) -> Vec<(T::AccountId, CollectionRoles)> {
let mut result = BTreeMap::new();
for (account, role) in input.into_iter() {
result.entry(account).or_insert(CollectionRoles::none()).add_role(role);
}
result.into_iter().collect()
}
}
@@ -0,0 +1,174 @@
// This file is part of Bizinikiwi.
// 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 provides helper methods to configure collection settings for the NFTs pallet.
use crate::*;
use pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Forcefully change the configuration of a collection.
///
/// - `collection`: The ID of the collection for which to update the configuration.
/// - `config`: The new collection configuration to set.
///
/// This function allows for changing the configuration of a collection without any checks.
/// It updates the collection configuration and emits a `CollectionConfigChanged` event.
pub(crate) fn do_force_collection_config(
collection: T::CollectionId,
config: CollectionConfigFor<T, I>,
) -> DispatchResult {
ensure!(Collection::<T, I>::contains_key(&collection), Error::<T, I>::UnknownCollection);
CollectionConfigOf::<T, I>::insert(&collection, config);
Self::deposit_event(Event::CollectionConfigChanged { collection });
Ok(())
}
/// Set the maximum supply for a collection.
///
/// - `maybe_check_owner`: An optional account ID used to check permissions.
/// - `collection`: The ID of the collection for which to set the maximum supply.
/// - `max_supply`: The new maximum supply to set for the collection.
///
/// This function checks if the setting `UnlockedMaxSupply` is enabled in the collection
/// configuration. If it is not enabled, it returns an `Error::MaxSupplyLocked`. If
/// `maybe_check_owner` is `Some(owner)`, it checks if the caller of the function is the
/// owner of the collection. If the caller is not the owner and the `maybe_check_owner`
/// parameter is provided, it returns an `Error::NoPermission`.
///
/// It also checks if the new maximum supply is greater than the current number of items in
/// the collection, and if not, it returns an `Error::MaxSupplyTooSmall`. If all checks pass,
/// it updates the collection configuration with the new maximum supply and emits a
/// `CollectionMaxSupplySet` event.
pub(crate) fn do_set_collection_max_supply(
maybe_check_owner: Option<T::AccountId>,
collection: T::CollectionId,
max_supply: u32,
) -> DispatchResult {
let collection_config = Self::get_collection_config(&collection)?;
ensure!(
collection_config.is_setting_enabled(CollectionSetting::UnlockedMaxSupply),
Error::<T, I>::MaxSupplyLocked
);
let details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
if let Some(check_owner) = &maybe_check_owner {
ensure!(check_owner == &details.owner, Error::<T, I>::NoPermission);
}
ensure!(details.items <= max_supply, Error::<T, I>::MaxSupplyTooSmall);
CollectionConfigOf::<T, I>::try_mutate(collection, |maybe_config| {
let config = maybe_config.as_mut().ok_or(Error::<T, I>::NoConfig)?;
config.max_supply = Some(max_supply);
Self::deposit_event(Event::CollectionMaxSupplySet { collection, max_supply });
Ok(())
})
}
/// Update the mint settings for a collection.
///
/// - `maybe_check_origin`: An optional account ID used to check issuer permissions.
/// - `collection`: The ID of the collection for which to update the mint settings.
/// - `mint_settings`: The new mint settings to set for the collection.
///
/// This function updates the mint settings for a collection. If `maybe_check_origin` is
/// `Some(origin)`, it checks if the caller of the function has the `CollectionRole::Issuer`
/// for the given collection. If the caller doesn't have the required permission and
/// `maybe_check_origin` is provided, it returns an `Error::NoPermission`. If all checks
/// pass, it updates the collection configuration with the new mint settings and emits a
/// `CollectionMintSettingsUpdated` event.
pub(crate) fn do_update_mint_settings(
maybe_check_origin: Option<T::AccountId>,
collection: T::CollectionId,
mint_settings: MintSettings<BalanceOf<T, I>, BlockNumberFor<T, I>, T::CollectionId>,
) -> DispatchResult {
if let Some(check_origin) = &maybe_check_origin {
ensure!(
Self::has_role(&collection, &check_origin, CollectionRole::Issuer),
Error::<T, I>::NoPermission
);
}
CollectionConfigOf::<T, I>::try_mutate(collection, |maybe_config| {
let config = maybe_config.as_mut().ok_or(Error::<T, I>::NoConfig)?;
config.mint_settings = mint_settings;
Self::deposit_event(Event::CollectionMintSettingsUpdated { collection });
Ok(())
})
}
/// Get the configuration for a specific collection.
///
/// - `collection_id`: The ID of the collection for which to retrieve the configuration.
///
/// This function attempts to fetch the configuration (`CollectionConfigFor`) associated
/// with the given `collection_id`. If the configuration exists, it returns `Ok(config)`,
/// otherwise, it returns a `DispatchError` with `Error::NoConfig`.
pub(crate) fn get_collection_config(
collection_id: &T::CollectionId,
) -> Result<CollectionConfigFor<T, I>, DispatchError> {
let config =
CollectionConfigOf::<T, I>::get(&collection_id).ok_or(Error::<T, I>::NoConfig)?;
Ok(config)
}
/// Get the configuration for a specific item within a collection.
///
/// - `collection_id`: The ID of the collection to which the item belongs.
/// - `item_id`: The ID of the item for which to retrieve the configuration.
///
/// This function attempts to fetch the configuration (`ItemConfig`) associated with the given
/// `collection_id` and `item_id`. If the configuration exists, it returns `Ok(config)`,
/// otherwise, it returns a `DispatchError` with `Error::UnknownItem`.
pub(crate) fn get_item_config(
collection_id: &T::CollectionId,
item_id: &T::ItemId,
) -> Result<ItemConfig, DispatchError> {
let config = ItemConfigOf::<T, I>::get(&collection_id, &item_id)
.ok_or(Error::<T, I>::UnknownItem)?;
Ok(config)
}
/// Get the default item settings for a specific collection.
///
/// - `collection_id`: The ID of the collection for which to retrieve the default item settings.
///
/// This function fetches the `default_item_settings` from the collection configuration
/// associated with the given `collection_id`. If the collection configuration exists, it
/// returns `Ok(default_item_settings)`, otherwise, it returns a `DispatchError` with
/// `Error::NoConfig`.
pub(crate) fn get_default_item_settings(
collection_id: &T::CollectionId,
) -> Result<ItemSettings, DispatchError> {
let collection_config = Self::get_collection_config(collection_id)?;
Ok(collection_config.mint_settings.default_item_settings)
}
/// Check if a specified pallet feature is enabled.
///
/// - `feature`: The feature to check.
///
/// This function checks if the given `feature` is enabled in the runtime using the
/// pallet's `T::Features::get()` function. It returns `true` if the feature is enabled,
/// otherwise it returns `false`.
pub(crate) fn is_pallet_feature_enabled(feature: PalletFeature) -> bool {
let features = T::Features::get();
return features.is_enabled(feature);
}
}
@@ -0,0 +1,234 @@
// This file is part of Bizinikiwi.
// 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 pezframe_support::pezpallet_prelude::*;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// 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<T, I>,
&mut ItemDetailsFor<T, I>,
) -> DispatchResult,
) -> DispatchResult {
// Retrieve collection details.
let collection_details =
Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
// Ensure the item is not locked.
ensure!(!T::Locker::is_locked(collection, item), Error::<T, I>::ItemLocked);
// Ensure the item is not transfer disabled on the system level attribute.
ensure!(
!Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?,
Error::<T, I>::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::<T, I>::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::<T, I>::ItemLocked
);
// Retrieve the item details.
let mut details =
Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
// Perform the transfer with custom details using the provided closure.
with_details(&collection_details, &mut details)?;
// Update account ownership information.
Account::<T, I>::remove((&details.owner, &collection, &item));
Account::<T, I>::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::<T, I>::insert(&collection, &item, &details);
ItemPriceOf::<T, I>::remove(&collection, &item);
PendingSwapOf::<T, I>::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::<T, I>::get(&new_owner);
ensure!(acceptable_collection.as_ref() == Some(&collection), Error::<T, I>::Unaccepted);
// Try to retrieve and mutate the collection details.
Collection::<T, I>::try_mutate(collection, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
// Check if the `origin` is the current owner of the collection.
ensure!(origin == details.owner, Error::<T, I>::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::<T, I>::remove(&details.owner, &collection);
CollectionAccount::<T, I>::insert(&new_owner, &collection, ());
details.owner = new_owner.clone();
OwnershipAcceptance::<T, I>::remove(&new_owner);
pezframe_system::Pallet::<T>::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<T::CollectionId>,
) -> DispatchResult {
let exists = OwnershipAcceptance::<T, I>::contains_key(&who);
match (exists, maybe_collection.is_some()) {
(false, true) => {
pezframe_system::Pallet::<T>::inc_consumers(&who)?;
},
(true, false) => {
pezframe_system::Pallet::<T>::dec_consumers(&who);
},
_ => {},
}
if let Some(collection) = maybe_collection.as_ref() {
OwnershipAcceptance::<T, I>::insert(&who, collection);
} else {
OwnershipAcceptance::<T, I>::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::<T, I>::try_mutate(collection, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T, I>::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::<T, I>::remove(&details.owner, &collection);
CollectionAccount::<T, I>::insert(&owner, &collection, ());
details.owner = owner.clone();
// Emit `OwnerChanged` event.
Self::deposit_event(Event::OwnerChanged { collection, new_owner: owner });
Ok(())
})
}
}