difftreelog
refactor Generalization some operations.
in: master
13 files changed
pallets/common/src/erc.rsdiffbeforeafterboth--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -222,12 +222,11 @@
fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {
self.consume_store_reads_and_writes(1, 1)?;
- check_is_owner_or_admin(caller, self)?;
+ let caller = T::CrossAccountId::from_eth(caller);
let sponsor = T::CrossAccountId::from_eth(sponsor);
- self.set_sponsor(sponsor.as_sub().clone())
- .map_err(dispatch_to_evm::<T>)?;
- save(self)
+ self.set_sponsor(&caller, sponsor.as_sub().clone())
+ .map_err(dispatch_to_evm::<T>)
}
/// Set the sponsor of the collection.
@@ -242,12 +241,11 @@
) -> Result<void> {
self.consume_store_reads_and_writes(1, 1)?;
- check_is_owner_or_admin(caller, self)?;
+ let caller = T::CrossAccountId::from_eth(caller);
let sponsor = sponsor.into_sub_cross_account::<T>()?;
- self.set_sponsor(sponsor.as_sub().clone())
- .map_err(dispatch_to_evm::<T>)?;
- save(self)
+ self.set_sponsor(&caller, sponsor.as_sub().clone())
+ .map_err(dispatch_to_evm::<T>)
}
/// Whether there is a pending sponsor.
@@ -265,21 +263,15 @@
self.consume_store_writes(1)?;
let caller = T::CrossAccountId::from_eth(caller);
- if !self
- .confirm_sponsorship(caller.as_sub())
- .map_err(dispatch_to_evm::<T>)?
- {
- return Err("caller is not set as sponsor".into());
- }
- save(self)
+ self.confirm_sponsorship(caller.as_sub())
+ .map_err(dispatch_to_evm::<T>)
}
/// Remove collection sponsor.
fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {
self.consume_store_reads_and_writes(1, 1)?;
- check_is_owner_or_admin(caller, self)?;
- self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;
- save(self)
+ let caller = T::CrossAccountId::from_eth(caller);
+ self.remove_sponsor(&caller).map_err(dispatch_to_evm::<T>)
}
/// Get current sponsor.
@@ -333,7 +325,6 @@
}
};
- check_is_owner_or_admin(caller, self)?;
let mut limits = self.limits.clone();
match limit.as_str() {
@@ -366,9 +357,9 @@
}
_ => return Err(Error::Revert(format!("unknown limit \"{}\"", limit))),
}
- self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)
- .map_err(dispatch_to_evm::<T>)?;
- save(self)
+
+ let caller = T::CrossAccountId::from_eth(caller);
+ <Pallet<T>>::update_limits(&caller, self, limits).map_err(dispatch_to_evm::<T>)
}
/// Get contract address.
@@ -383,7 +374,7 @@
caller: caller,
new_admin: EthCrossAccount,
) -> Result<void> {
- self.consume_store_writes(2)?;
+ self.consume_store_reads_and_writes(2, 2)?;
let caller = T::CrossAccountId::from_eth(caller);
let new_admin = new_admin.into_sub_cross_account::<T>()?;
@@ -398,7 +389,7 @@
caller: caller,
admin: EthCrossAccount,
) -> Result<void> {
- self.consume_store_writes(2)?;
+ self.consume_store_reads_and_writes(2, 2)?;
let caller = T::CrossAccountId::from_eth(caller);
let admin = admin.into_sub_cross_account::<T>()?;
@@ -410,7 +401,7 @@
/// @param newAdmin Address of the added administrator.
#[solidity(hide)]
fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {
- self.consume_store_writes(2)?;
+ self.consume_store_reads_and_writes(2, 2)?;
let caller = T::CrossAccountId::from_eth(caller);
let new_admin = T::CrossAccountId::from_eth(new_admin);
@@ -423,7 +414,7 @@
/// @param admin Address of the removed administrator.
#[solidity(hide)]
fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {
- self.consume_store_writes(2)?;
+ self.consume_store_reads_and_writes(2, 2)?;
let caller = T::CrossAccountId::from_eth(caller);
let admin = T::CrossAccountId::from_eth(admin);
@@ -438,7 +429,7 @@
fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {
self.consume_store_reads_and_writes(1, 1)?;
- check_is_owner_or_admin(caller, self)?;
+ let caller = T::CrossAccountId::from_eth(caller);
let mut permissions = self.collection.permissions.clone();
let mut nesting = permissions.nesting().clone();
@@ -446,14 +437,7 @@
nesting.restricted = None;
permissions.nesting = Some(nesting);
- self.collection.permissions = <Pallet<T>>::clamp_permissions(
- self.collection.mode.clone(),
- &self.collection.permissions,
- permissions,
- )
- .map_err(dispatch_to_evm::<T>)?;
-
- save(self)
+ <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)
}
/// Toggle accessibility of collection nesting.
@@ -472,7 +456,7 @@
if collections.is_empty() {
return Err("no addresses provided".into());
}
- check_is_owner_or_admin(caller, self)?;
+ let caller = T::CrossAccountId::from_eth(caller);
let mut permissions = self.collection.permissions.clone();
match enable {
@@ -497,14 +481,7 @@
}
};
- self.collection.permissions = <Pallet<T>>::clamp_permissions(
- self.collection.mode.clone(),
- &self.collection.permissions,
- permissions,
- )
- .map_err(dispatch_to_evm::<T>)?;
-
- save(self)
+ <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)
}
/// Set the collection access method.
@@ -514,7 +491,7 @@
fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {
self.consume_store_reads_and_writes(1, 1)?;
- check_is_owner_or_admin(caller, self)?;
+ let caller = T::CrossAccountId::from_eth(caller);
let permissions = CollectionPermissions {
access: Some(match mode {
0 => AccessMode::Normal,
@@ -523,14 +500,7 @@
}),
..Default::default()
};
- self.collection.permissions = <Pallet<T>>::clamp_permissions(
- self.collection.mode.clone(),
- &self.collection.permissions,
- permissions,
- )
- .map_err(dispatch_to_evm::<T>)?;
-
- save(self)
+ <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)
}
/// Checks that user allowed to operate with collection.
@@ -605,19 +575,12 @@
fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {
self.consume_store_reads_and_writes(1, 1)?;
- check_is_owner_or_admin(caller, self)?;
+ let caller = T::CrossAccountId::from_eth(caller);
let permissions = CollectionPermissions {
mint_mode: Some(mode),
..Default::default()
};
- self.collection.permissions = <Pallet<T>>::clamp_permissions(
- self.collection.mode.clone(),
- &self.collection.permissions,
- permissions,
- )
- .map_err(dispatch_to_evm::<T>)?;
-
- save(self)
+ <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)
}
/// Check that account is the owner or admin of the collection
@@ -671,7 +634,7 @@
let caller = T::CrossAccountId::from_eth(caller);
let new_owner = T::CrossAccountId::from_eth(new_owner);
- self.set_owner_internal(caller, new_owner)
+ self.change_owner(caller, new_owner)
.map_err(dispatch_to_evm::<T>)
}
@@ -699,7 +662,7 @@
let caller = T::CrossAccountId::from_eth(caller);
let new_owner = new_owner.into_sub_cross_account::<T>()?;
- self.set_owner_internal(caller, new_owner)
+ self.change_owner(caller, new_owner)
.map_err(dispatch_to_evm::<T>)
}
}
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -229,28 +229,111 @@
/// Unique collections allows sponsoring for certain actions.
/// This method allows you to set the sponsor of the collection.
/// In order for sponsorship to become active, it must be confirmed through [`Self::confirm_sponsorship`].
- pub fn set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {
- self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);
- Ok(())
+ pub fn set_sponsor(
+ &mut self,
+ sender: &T::CrossAccountId,
+ sponsor: T::AccountId,
+ ) -> DispatchResult {
+ self.check_is_internal()?;
+ self.check_is_owner_or_admin(sender)?;
+
+ self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor.clone());
+
+ <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(self.id, sponsor));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(self.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
+ self.save()
+ }
+
+ /// Force set `sponsor`.
+ ///
+ /// Differs from [`set_sponsor`][`Self::set_sponsor`] in that confirmation
+ /// from the `sponsor` is not required.
+ ///
+ /// # Arguments
+ ///
+ /// * `sender`: Caller's account.
+ /// * `sponsor`: ID of the account of the sponsor-to-be.
+ pub fn force_set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {
+ self.check_is_internal()?;
+
+ self.collection.sponsorship = SponsorshipState::Confirmed(sponsor.clone());
+
+ <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(self.id, sponsor.clone()));
+ <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(self.id, sponsor));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(self.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
+ self.save()
}
/// Confirm sponsorship
///
/// In order for the sponsorship to become active, the user set as the sponsor must confirm their participation.
/// Before confirming sponsorship, the user must be specified as the sponsor of the collection via [`Self::set_sponsor`].
- pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> Result<bool, DispatchError> {
- if self.collection.sponsorship.pending_sponsor() != Some(sender) {
- return Ok(false);
- }
+ pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> DispatchResult {
+ self.check_is_internal()?;
+ ensure!(
+ self.collection.sponsorship.pending_sponsor() == Some(sender),
+ Error::<T>::ConfirmUnsetSponsorFail
+ );
self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());
- Ok(true)
+
+ <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(self.id, sender.clone()));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(self.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
+ self.save()
}
/// Remove collection sponsor.
- pub fn remove_sponsor(&mut self) -> DispatchResult {
+ pub fn remove_sponsor(&mut self, sender: &T::CrossAccountId) -> DispatchResult {
+ self.check_is_internal()?;
+ self.check_is_owner(sender)?;
+
+ self.collection.sponsorship = SponsorshipState::Disabled;
+
+ <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(self.id));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(self.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+ self.save()
+ }
+
+ /// Force remove `sponsor`.
+ ///
+ /// Differs from `remove_sponsor` in that
+ /// it doesn't require consent from the `owner` of the collection.
+ pub fn force_remove_sponsor(&mut self) -> DispatchResult {
+ self.check_is_internal()?;
+
self.collection.sponsorship = SponsorshipState::Disabled;
- Ok(())
+
+ <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(self.id));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(self.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+ self.save()
}
/// Checks that the collection was created with, and must be operated upon through **Unique API**.
@@ -328,13 +411,26 @@
/// Changes collection owner to another account
/// #### Store read/writes
/// 1 writes
- fn set_owner_internal(
+ pub fn change_owner(
&mut self,
caller: T::CrossAccountId,
new_owner: T::CrossAccountId,
) -> DispatchResult {
+ self.check_is_internal()?;
self.check_is_owner(&caller)?;
self.collection.owner = new_owner.as_sub().clone();
+
+ <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(
+ self.id,
+ new_owner.as_sub().clone(),
+ ));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(self.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
self.save()
}
}
@@ -527,6 +623,80 @@
/// The property permission that was set.
PropertyKey,
),
+
+ /// Address was added to the allow list.
+ AllowListAddressAdded(
+ /// ID of the affected collection.
+ CollectionId,
+ /// Address of the added account.
+ T::CrossAccountId,
+ ),
+
+ /// Address was removed from the allow list.
+ AllowListAddressRemoved(
+ /// ID of the affected collection.
+ CollectionId,
+ /// Address of the removed account.
+ T::CrossAccountId,
+ ),
+
+ /// Collection admin was added.
+ CollectionAdminAdded(
+ /// ID of the affected collection.
+ CollectionId,
+ /// Admin address.
+ T::CrossAccountId,
+ ),
+
+ /// Collection admin was removed.
+ CollectionAdminRemoved(
+ /// ID of the affected collection.
+ CollectionId,
+ /// Removed admin address.
+ T::CrossAccountId,
+ ),
+
+ /// Collection limits were set.
+ CollectionLimitSet(
+ /// ID of the affected collection.
+ CollectionId,
+ ),
+
+ /// Collection owned was changed.
+ CollectionOwnedChanged(
+ /// ID of the affected collection.
+ CollectionId,
+ /// New owner address.
+ T::AccountId,
+ ),
+
+ /// Collection permissions were set.
+ CollectionPermissionSet(
+ /// ID of the affected collection.
+ CollectionId,
+ ),
+
+ /// Collection sponsor was set.
+ CollectionSponsorSet(
+ /// ID of the affected collection.
+ CollectionId,
+ /// New sponsor address.
+ T::AccountId,
+ ),
+
+ /// New sponsor was confirm.
+ SponsorshipConfirmed(
+ /// ID of the affected collection.
+ CollectionId,
+ /// New sponsor address.
+ T::AccountId,
+ ),
+
+ /// Collection sponsor was removed.
+ CollectionSponsorRemoved(
+ /// ID of the affected collection.
+ CollectionId,
+ ),
}
#[pallet::error]
@@ -613,6 +783,12 @@
/// Tried to access an internal collection with an external API
CollectionIsInternal,
+
+ /// This address is not set as sponsor, use setCollectionSponsor first.
+ ConfirmUnsetSponsorFail,
+
+ /// The user is not an administrator.
+ UserIsNotAdmin,
}
/// Storage of the count of created collections. Essentially contains the last collection ID.
@@ -1363,27 +1539,47 @@
if allowed {
<Allowlist<T>>::insert((collection.id, user), true);
+ Self::deposit_event(Event::<T>::AllowListAddressAdded(
+ collection.id,
+ user.clone(),
+ ));
} else {
<Allowlist<T>>::remove((collection.id, user));
+ Self::deposit_event(Event::<T>::AllowListAddressRemoved(
+ collection.id,
+ user.clone(),
+ ));
}
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(collection.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
Ok(())
}
/// Toggle `user` participation in the `collection`'s admin list.
/// #### Store read/writes
- /// 2 writes
+ /// 2 reads, 2 writes
pub fn toggle_admin(
collection: &CollectionHandle<T>,
sender: &T::CrossAccountId,
user: &T::CrossAccountId,
admin: bool,
) -> DispatchResult {
+ collection.check_is_internal()?;
collection.check_is_owner(sender)?;
- let was_admin = <IsAdmin<T>>::get((collection.id, user));
- if was_admin == admin {
- return Ok(());
+ let is_admin = <IsAdmin<T>>::get((collection.id, user));
+ if is_admin == admin {
+ if admin {
+ return Ok(());
+ } else {
+ ensure!(false, Error::<T>::UserIsNotAdmin);
+ }
}
let amount = <AdminAmount<T>>::get(collection.id);
@@ -1400,16 +1596,56 @@
<AdminAmount<T>>::insert(collection.id, amount);
<IsAdmin<T>>::insert((collection.id, user), true);
+
+ Self::deposit_event(Event::<T>::CollectionAdminAdded(
+ collection.id,
+ user.clone(),
+ ));
} else {
<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));
<IsAdmin<T>>::remove((collection.id, user));
+
+ Self::deposit_event(Event::<T>::CollectionAdminRemoved(
+ collection.id,
+ user.clone(),
+ ));
}
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(collection.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
Ok(())
}
+ /// Update collection limits.
+ pub fn update_limits(
+ user: &T::CrossAccountId,
+ collection: &mut CollectionHandle<T>,
+ new_limit: CollectionLimits,
+ ) -> DispatchResult {
+ collection.check_is_internal()?;
+ collection.check_is_owner_or_admin(user)?;
+
+ collection.limits =
+ Self::clamp_limits(collection.mode.clone(), &collection.limits, new_limit)?;
+
+ Self::deposit_event(Event::<T>::CollectionLimitSet(collection.id));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(collection.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
+ collection.save()
+ }
+
/// Merge set fields from `new_limit` to `old_limit`.
- pub fn clamp_limits(
+ fn clamp_limits(
mode: CollectionMode,
old_limit: &CollectionLimits,
mut new_limit: CollectionLimits,
@@ -1454,8 +1690,33 @@
Ok(new_limit)
}
+ /// Update collection permissions.
+ pub fn update_permissions(
+ user: &T::CrossAccountId,
+ collection: &mut CollectionHandle<T>,
+ new_permission: CollectionPermissions,
+ ) -> DispatchResult {
+ collection.check_is_internal()?;
+ collection.check_is_owner_or_admin(user)?;
+ collection.permissions = Self::clamp_permissions(
+ collection.mode.clone(),
+ &collection.permissions,
+ new_permission,
+ )?;
+
+ Self::deposit_event(Event::<T>::CollectionPermissionSet(collection.id));
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionChanged {
+ collection_id: eth::collection_id_to_address(collection.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
+
+ collection.save()
+ }
+
/// Merge set fields from `new_permission` to `old_permission`.
- pub fn clamp_permissions(
+ fn clamp_permissions(
_mode: CollectionMode,
old_permission: &CollectionPermissions,
mut new_permission: CollectionPermissions,
pallets/unique/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Unique Pallet18//!19//! A pallet governing Unique transactions.20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The Unique pallet's purpose is to be the primary interface between28//! external users and the inner structure of the Unique chains.29//!30//! It also contains an implementation of [`CollectionHelpers`][`eth`],31//! an Ethereum contract dealing with collection operations.32//!33//! ## Interface34//!35//! ### Dispatchables36//!37//! - `create_collection` - Create a collection of tokens. **Deprecated**, use `create_collection_ex`.38//! - `create_collection_ex` - Create a collection of tokens with explicit parameters.39//! - `destroy_collection` - Destroy a collection if no tokens exist within.40//! - `add_to_allow_list` - Add an address to allow list.41//! - `remove_from_allow_list` - Remove an address from allow list.42//! - `change_collection_owner` - Change the owner of the collection.43//! - `add_collection_admin` - Add an admin to a collection.44//! - `remove_collection_admin` - Remove admin of a collection.45//! - `set_collection_sponsor` - Invite a new collection sponsor.46//! - `confirm_sponsorship` - Confirm own sponsorship of a collection, becoming the sponsor.47//! - `remove_collection_sponsor` - Remove a sponsor from a collection.48//! - `create_item` - Create an item within a collection.49//! - `create_multiple_items` - Create multiple items within a collection.50//! - `set_collection_properties` - Add or change collection properties.51//! - `delete_collection_properties` - Delete specified collection properties.52//! - `set_token_properties` - Add or change token properties.53//! - `delete_token_properties` - Delete token properties.54//! - `set_token_property_permissions` - Add or change token property permissions of a collection.55//! - `create_multiple_items_ex` - Create multiple items within a collection with explicitly specified initial parameters.56//! - `set_transfers_enabled_flag` - Completely allow or disallow transfers for a particular collection.57//! - `burn_item` - Destroy an item.58//! - `burn_from` - Destroy an item on behalf of the owner as a non-owner account.59//! - `transfer` - Change ownership of the token.60//! - `transfer_from` - Change ownership of the token on behalf of the owner as a non-owner account.61//! - `approve` - Allow a non-permissioned address to transfer or burn an item.62//! - `set_collection_limits` - Set specific limits of a collection.63//! - `set_collection_permissions` - Set specific permissions of a collection.64//! - `repartition` - Re-partition a refungible token, while owning all of its parts.6566#![recursion_limit = "1024"]67#![cfg_attr(not(feature = "std"), no_std)]68#![allow(69 clippy::too_many_arguments,70 clippy::unnecessary_mut_passed,71 clippy::unused_unit72)]7374extern crate alloc;7576use frame_support::{77 decl_module, decl_storage, decl_error, decl_event,78 dispatch::DispatchResult,79 ensure, fail,80 weights::{Weight},81 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},82 BoundedVec,83};84use scale_info::TypeInfo;85use frame_system::{self as system, ensure_signed};86use sp_std::{vec, vec::Vec};87use up_data_structs::{88 MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH,89 MAX_PROPERTIES_PER_ITEM, MAX_PROPERTY_KEY_LENGTH, MAX_PROPERTY_VALUE_LENGTH,90 MAX_COLLECTION_PROPERTIES_SIZE, COLLECTION_ADMINS_LIMIT, MAX_TOKEN_PROPERTIES_SIZE,91 CreateItemData, CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId,92 SponsorshipState, CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,93 PropertyKeyPermission,94};95use pallet_evm::account::CrossAccountId;96use pallet_common::{97 CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_tx,98 dispatch::CollectionDispatch, RefungibleExtensionsWeightInfo,99};100pub mod eth;101102#[cfg(feature = "runtime-benchmarks")]103pub mod benchmarking;104pub mod weights;105use weights::WeightInfo;106107/// A maximum number of levels of depth in the token nesting tree.108pub const NESTING_BUDGET: u32 = 5;109110decl_error! {111 /// Errors for the common Unique transactions.112 pub enum Error for Module<T: Config> {113 /// Decimal_points parameter must be lower than [`up_data_structs::MAX_DECIMAL_POINTS`].114 CollectionDecimalPointLimitExceeded,115 /// This address is not set as sponsor, use setCollectionSponsor first.116 ConfirmUnsetSponsorFail,117 /// Length of items properties must be greater than 0.118 EmptyArgument,119 /// Repertition is only supported by refungible collection.120 RepartitionCalledOnNonRefungibleCollection,121 }122}123124/// Configuration trait of this pallet.125pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {126 /// Overarching event type.127 type RuntimeEvent: From<Event<Self>> + Into<<Self as frame_system::Config>::RuntimeEvent>;128129 /// Weight information for extrinsics in this pallet.130 type WeightInfo: WeightInfo;131132 /// Weight information for common pallet operations.133 type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;134135 /// Weight info information for extra refungible pallet operations.136 type RefungibleExtensionsWeightInfo: RefungibleExtensionsWeightInfo;137}138139decl_event! {140 pub enum Event<T>141 where142 <T as frame_system::Config>::AccountId,143 <T as pallet_evm::Config>::CrossAccountId,144 {145 /// Collection sponsor was removed146 ///147 /// # Arguments148 /// * collection_id: ID of the affected collection.149 CollectionSponsorRemoved(CollectionId),150151 /// Collection admin was added152 ///153 /// # Arguments154 /// * collection_id: ID of the affected collection.155 /// * admin: Admin address.156 CollectionAdminAdded(CollectionId, CrossAccountId),157158 /// Collection owned was changed159 ///160 /// # Arguments161 /// * collection_id: ID of the affected collection.162 /// * owner: New owner address.163 CollectionOwnedChanged(CollectionId, AccountId),164165 /// Collection sponsor was set166 ///167 /// # Arguments168 /// * collection_id: ID of the affected collection.169 /// * owner: New sponsor address.170 CollectionSponsorSet(CollectionId, AccountId),171172 /// New sponsor was confirm173 ///174 /// # Arguments175 /// * collection_id: ID of the affected collection.176 /// * sponsor: New sponsor address.177 SponsorshipConfirmed(CollectionId, AccountId),178179 /// Collection admin was removed180 ///181 /// # Arguments182 /// * collection_id: ID of the affected collection.183 /// * admin: Removed admin address.184 CollectionAdminRemoved(CollectionId, CrossAccountId),185186 /// Address was removed from the allow list187 ///188 /// # Arguments189 /// * collection_id: ID of the affected collection.190 /// * user: Address of the removed account.191 AllowListAddressRemoved(CollectionId, CrossAccountId),192193 /// Address was added to the allow list194 ///195 /// # Arguments196 /// * collection_id: ID of the affected collection.197 /// * user: Address of the added account.198 AllowListAddressAdded(CollectionId, CrossAccountId),199200 /// Collection limits were set201 ///202 /// # Arguments203 /// * collection_id: ID of the affected collection.204 CollectionLimitSet(CollectionId),205206 /// Collection permissions were set207 ///208 /// # Arguments209 /// * collection_id: ID of the affected collection.210 CollectionPermissionSet(CollectionId),211 }212}213214type SelfWeightOf<T> = <T as Config>::WeightInfo;215216// # Used definitions217//218// ## User control levels219//220// chain-controlled - key is uncontrolled by user221// i.e autoincrementing index222// can use non-cryptographic hash223// real - key is controlled by user224// but it is hard to generate enough colliding values, i.e owner of signed txs225// can use non-cryptographic hash226// controlled - key is completly controlled by users227// i.e maps with mutable keys228// should use cryptographic hash229//230// ## User control level downgrade reasons231//232// ?1 - chain-controlled -> controlled233// collections/tokens can be destroyed, resulting in massive holes234// ?2 - chain-controlled -> controlled235// same as ?1, but can be only added, resulting in easier exploitation236// ?3 - real -> controlled237// no confirmation required, so addresses can be easily generated238decl_storage! {239 trait Store for Module<T: Config> as Unique {240241 //#region Private members242 /// Used for migrations243 ChainVersion: u64;244 //#endregion245246 //#region Tokens transfer sponosoring rate limit baskets247 /// (Collection id (controlled?2), who created (real))248 /// TODO: Off chain worker should remove from this map when collection gets removed249 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;250 /// Collection id (controlled?2), token id (controlled?2)251 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;252 /// Collection id (controlled?2), owning user (real)253 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;254 /// Collection id (controlled?2), token id (controlled?2)255 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;256 //#endregion257258 /// Variable metadata sponsoring259 /// Collection id (controlled?2), token id (controlled?2)260 #[deprecated]261 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;262 /// Last sponsoring of token property setting // todo:doc rephrase this and the following263 pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;264265 /// Last sponsoring of NFT approval in a collection266 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;267 /// Last sponsoring of fungible tokens approval in a collection268 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;269 /// Last sponsoring of RFT approval in a collection270 pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;271 }272}273274decl_module! {275 /// Type alias to Pallet, to be used by construct_runtime.276 pub struct Module<T: Config> for enum Call277 where278 origin: T::RuntimeOrigin279 {280 type Error = Error<T>;281282 #[doc = "A maximum number of levels of depth in the token nesting tree."]283 const NESTING_BUDGET: u32 = NESTING_BUDGET;284285 #[doc = "Maximal length of a collection name."]286 const MAX_COLLECTION_NAME_LENGTH: u32 = MAX_COLLECTION_NAME_LENGTH;287288 #[doc = "Maximal length of a collection description."]289 const MAX_COLLECTION_DESCRIPTION_LENGTH: u32 = MAX_COLLECTION_DESCRIPTION_LENGTH;290291 #[doc = "Maximal length of a token prefix."]292 const MAX_TOKEN_PREFIX_LENGTH: u32 = MAX_TOKEN_PREFIX_LENGTH;293294 #[doc = "Maximum admins per collection."]295 const COLLECTION_ADMINS_LIMIT: u32 = COLLECTION_ADMINS_LIMIT;296297 #[doc = "Maximal length of a property key."]298 const MAX_PROPERTY_KEY_LENGTH: u32 = MAX_PROPERTY_KEY_LENGTH;299300 #[doc = "Maximal length of a property value."]301 const MAX_PROPERTY_VALUE_LENGTH: u32 = MAX_PROPERTY_VALUE_LENGTH;302303 #[doc = "A maximum number of token properties."]304 const MAX_PROPERTIES_PER_ITEM: u32 = MAX_PROPERTIES_PER_ITEM;305306 #[doc = "Maximum size for all collection properties."]307 const MAX_COLLECTION_PROPERTIES_SIZE: u32 = MAX_COLLECTION_PROPERTIES_SIZE;308309 #[doc = "Maximum size of all token properties."]310 const MAX_TOKEN_PROPERTIES_SIZE: u32 = MAX_TOKEN_PROPERTIES_SIZE;311312 #[doc = "Default NFT collection limit."]313 const NFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::NFT);314315 #[doc = "Default RFT collection limit."]316 const RFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::ReFungible);317318 #[doc = "Default FT collection limit."]319 const FT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::Fungible(0));320321322 pub fn deposit_event() = default;323324 fn on_initialize(_now: T::BlockNumber) -> Weight {325 Weight::zero()326 }327328 fn on_runtime_upgrade() -> Weight {329 Weight::zero()330 }331332 /// Create a collection of tokens.333 ///334 /// Each Token may have multiple properties encoded as an array of bytes335 /// of certain length. The initial owner of the collection is set336 /// to the address that signed the transaction and can be changed later.337 ///338 /// Prefer the more advanced [`create_collection_ex`][`Pallet::create_collection_ex`] instead.339 ///340 /// # Permissions341 ///342 /// * Anyone - becomes the owner of the new collection.343 ///344 /// # Arguments345 ///346 /// * `collection_name`: Wide-character string with collection name347 /// (limit [`MAX_COLLECTION_NAME_LENGTH`]).348 /// * `collection_description`: Wide-character string with collection description349 /// (limit [`MAX_COLLECTION_DESCRIPTION_LENGTH`]).350 /// * `token_prefix`: Byte string containing the token prefix to mark a collection351 /// to which a token belongs (limit [`MAX_TOKEN_PREFIX_LENGTH`]).352 /// * `mode`: Type of items stored in the collection and type dependent data.353 // returns collection ID354 #[weight = <SelfWeightOf<T>>::create_collection()]355 #[deprecated(note = "`create_collection_ex` is more up-to-date and advanced, prefer it instead")]356 pub fn create_collection(357 origin,358 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,359 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,360 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,361 mode: CollectionMode362 ) -> DispatchResult {363 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {364 name: collection_name,365 description: collection_description,366 token_prefix,367 mode,368 ..Default::default()369 };370 Self::create_collection_ex(origin, data)371 }372373 /// Create a collection with explicit parameters.374 ///375 /// Prefer it to the deprecated [`create_collection`][`Pallet::create_collection`] method.376 ///377 /// # Permissions378 ///379 /// * Anyone - becomes the owner of the new collection.380 ///381 /// # Arguments382 ///383 /// * `data`: Explicit data of a collection used for its creation.384 #[weight = <SelfWeightOf<T>>::create_collection()]385 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {386 let sender = ensure_signed(origin)?;387388 // =========389 let sender = T::CrossAccountId::from_sub(sender);390 let _id = T::CollectionDispatch::create(sender.clone(), sender, data, Default::default())?;391392 Ok(())393 }394395 /// Destroy a collection if no tokens exist within.396 ///397 /// # Permissions398 ///399 /// * Collection owner400 ///401 /// # Arguments402 ///403 /// * `collection_id`: Collection to destroy.404 #[weight = <SelfWeightOf<T>>::destroy_collection()]405 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {406 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);407408 Self::destroy_collection_internal(sender, collection_id)409 }410411 /// Add an address to allow list.412 ///413 /// # Permissions414 ///415 /// * Collection owner416 /// * Collection admin417 ///418 /// # Arguments419 ///420 /// * `collection_id`: ID of the modified collection.421 /// * `address`: ID of the address to be added to the allowlist.422 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]423 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{424425 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);426 let collection = <CollectionHandle<T>>::try_get(collection_id)?;427 collection.check_is_internal()?;428429 <PalletCommon<T>>::toggle_allowlist(430 &collection,431 &sender,432 &address,433 true,434 )?;435436 Self::deposit_event(Event::<T>::AllowListAddressAdded(437 collection_id,438 address439 ));440441 Ok(())442 }443444 /// Remove an address from allow list.445 ///446 /// # Permissions447 ///448 /// * Collection owner449 /// * Collection admin450 ///451 /// # Arguments452 ///453 /// * `collection_id`: ID of the modified collection.454 /// * `address`: ID of the address to be removed from the allowlist.455 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]456 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{457458 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);459 let collection = <CollectionHandle<T>>::try_get(collection_id)?;460 collection.check_is_internal()?;461462 <PalletCommon<T>>::toggle_allowlist(463 &collection,464 &sender,465 &address,466 false,467 )?;468469 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(470 collection_id,471 address472 ));473474 Ok(())475 }476477 /// Change the owner of the collection.478 ///479 /// # Permissions480 ///481 /// * Collection owner482 ///483 /// # Arguments484 ///485 /// * `collection_id`: ID of the modified collection.486 /// * `new_owner`: ID of the account that will become the owner.487 #[weight = <SelfWeightOf<T>>::change_collection_owner()]488 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {489490 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);491492 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;493 target_collection.check_is_internal()?;494 target_collection.check_is_owner(&sender)?;495496 target_collection.owner = new_owner.clone();497 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(498 collection_id,499 new_owner500 ));501502 target_collection.save()503 }504505 /// Add an admin to a collection.506 ///507 /// NFT Collection can be controlled by multiple admin addresses508 /// (some which can also be servers, for example). Admins can issue509 /// and burn NFTs, as well as add and remove other admins,510 /// but cannot change NFT or Collection ownership.511 ///512 /// # Permissions513 ///514 /// * Collection owner515 /// * Collection admin516 ///517 /// # Arguments518 ///519 /// * `collection_id`: ID of the Collection to add an admin for.520 /// * `new_admin`: Address of new admin to add.521 #[weight = <SelfWeightOf<T>>::add_collection_admin()]522 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {523 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);524 let collection = <CollectionHandle<T>>::try_get(collection_id)?;525 collection.check_is_internal()?;526527 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(528 collection_id,529 new_admin_id.clone()530 ));531532 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)533 }534535 /// Remove admin of a collection.536 ///537 /// An admin address can remove itself. List of admins may become empty,538 /// in which case only Collection Owner will be able to add an Admin.539 ///540 /// # Permissions541 ///542 /// * Collection owner543 /// * Collection admin544 ///545 /// # Arguments546 ///547 /// * `collection_id`: ID of the collection to remove the admin for.548 /// * `account_id`: Address of the admin to remove.549 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]550 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {551 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);552 let collection = <CollectionHandle<T>>::try_get(collection_id)?;553 collection.check_is_internal()?;554555 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(556 collection_id,557 account_id.clone()558 ));559560 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)561 }562563 /// Set (invite) a new collection sponsor.564 ///565 /// If successful, confirmation from the sponsor-to-be will be pending.566 ///567 /// # Permissions568 ///569 /// * Collection owner570 /// * Collection admin571 ///572 /// # Arguments573 ///574 /// * `collection_id`: ID of the modified collection.575 /// * `new_sponsor`: ID of the account of the sponsor-to-be.576 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]577 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {578 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);579580 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;581 target_collection.check_is_owner_or_admin(&sender)?;582 target_collection.check_is_internal()?;583584 target_collection.set_sponsor(new_sponsor.clone())?;585586 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(587 collection_id,588 new_sponsor589 ));590591 target_collection.save()592 }593594 /// Confirm own sponsorship of a collection, becoming the sponsor.595 ///596 /// An invitation must be pending, see [`set_collection_sponsor`][`Pallet::set_collection_sponsor`].597 /// Sponsor can pay the fees of a transaction instead of the sender,598 /// but only within specified limits.599 ///600 /// # Permissions601 ///602 /// * Sponsor-to-be603 ///604 /// # Arguments605 ///606 /// * `collection_id`: ID of the collection with the pending sponsor.607 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]608 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {609 let sender = ensure_signed(origin)?;610611 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;612 target_collection.check_is_internal()?;613 ensure!(614 target_collection.confirm_sponsorship(&sender)?,615 Error::<T>::ConfirmUnsetSponsorFail616 );617618 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(619 collection_id,620 sender621 ));622623 target_collection.save()624 }625626 /// Remove a collection's a sponsor, making everyone pay for their own transactions.627 ///628 /// # Permissions629 ///630 /// * Collection owner631 ///632 /// # Arguments633 ///634 /// * `collection_id`: ID of the collection with the sponsor to remove.635 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]636 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {637 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);638639 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;640 target_collection.check_is_internal()?;641 target_collection.check_is_owner(&sender)?;642643 target_collection.sponsorship = SponsorshipState::Disabled;644645 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(646 collection_id647 ));648 target_collection.save()649 }650651 /// Mint an item within a collection.652 ///653 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].654 ///655 /// # Permissions656 ///657 /// * Collection owner658 /// * Collection admin659 /// * Anyone if660 /// * Allow List is enabled, and661 /// * Address is added to allow list, and662 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])663 ///664 /// # Arguments665 ///666 /// * `collection_id`: ID of the collection to which an item would belong.667 /// * `owner`: Address of the initial owner of the item.668 /// * `data`: Token data describing the item to store on chain.669 #[weight = T::CommonWeightInfo::create_item()]670 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {671 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);672 let budget = budget::Value::new(NESTING_BUDGET);673674 dispatch_tx::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))675 }676677 /// Create multiple items within a collection.678 ///679 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].680 ///681 /// # Permissions682 ///683 /// * Collection owner684 /// * Collection admin685 /// * Anyone if686 /// * Allow List is enabled, and687 /// * Address is added to the allow list, and688 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])689 ///690 /// # Arguments691 ///692 /// * `collection_id`: ID of the collection to which the tokens would belong.693 /// * `owner`: Address of the initial owner of the tokens.694 /// * `items_data`: Vector of data describing each item to be created.695 #[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]696 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {697 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);698 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);699 let budget = budget::Value::new(NESTING_BUDGET);700701 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))702 }703704 /// Add or change collection properties.705 ///706 /// # Permissions707 ///708 /// * Collection owner709 /// * Collection admin710 ///711 /// # Arguments712 ///713 /// * `collection_id`: ID of the modified collection.714 /// * `properties`: Vector of key-value pairs stored as the collection's metadata.715 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.716 #[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]717 pub fn set_collection_properties(718 origin,719 collection_id: CollectionId,720 properties: Vec<Property>721 ) -> DispatchResultWithPostInfo {722 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);723724 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);725726 dispatch_tx::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))727 }728729 /// Delete specified collection properties.730 ///731 /// # Permissions732 ///733 /// * Collection Owner734 /// * Collection Admin735 ///736 /// # Arguments737 ///738 /// * `collection_id`: ID of the modified collection.739 /// * `property_keys`: Vector of keys of the properties to be deleted.740 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.741 #[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]742 pub fn delete_collection_properties(743 origin,744 collection_id: CollectionId,745 property_keys: Vec<PropertyKey>,746 ) -> DispatchResultWithPostInfo {747 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);748749 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);750751 dispatch_tx::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))752 }753754 /// Add or change token properties according to collection's permissions.755 /// Currently properties only work with NFTs.756 ///757 /// # Permissions758 ///759 /// * Depends on collection's token property permissions and specified property mutability:760 /// * Collection owner761 /// * Collection admin762 /// * Token owner763 ///764 /// See [`set_token_property_permissions`][`Pallet::set_token_property_permissions`].765 ///766 /// # Arguments767 ///768 /// * `collection_id: ID of the collection to which the token belongs.769 /// * `token_id`: ID of the modified token.770 /// * `properties`: Vector of key-value pairs stored as the token's metadata.771 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.772 #[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]773 pub fn set_token_properties(774 origin,775 collection_id: CollectionId,776 token_id: TokenId,777 properties: Vec<Property>778 ) -> DispatchResultWithPostInfo {779 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);780781 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);782 let budget = budget::Value::new(NESTING_BUDGET);783784 dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties, &budget))785 }786787 /// Delete specified token properties. Currently properties only work with NFTs.788 ///789 /// # Permissions790 ///791 /// * Depends on collection's token property permissions and specified property mutability:792 /// * Collection owner793 /// * Collection admin794 /// * Token owner795 ///796 /// # Arguments797 ///798 /// * `collection_id`: ID of the collection to which the token belongs.799 /// * `token_id`: ID of the modified token.800 /// * `property_keys`: Vector of keys of the properties to be deleted.801 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.802 #[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]803 pub fn delete_token_properties(804 origin,805 collection_id: CollectionId,806 token_id: TokenId,807 property_keys: Vec<PropertyKey>808 ) -> DispatchResultWithPostInfo {809 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);810811 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);812 let budget = budget::Value::new(NESTING_BUDGET);813814 dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys, &budget))815 }816817 /// Add or change token property permissions of a collection.818 ///819 /// Without a permission for a particular key, a property with that key820 /// cannot be created in a token.821 ///822 /// # Permissions823 ///824 /// * Collection owner825 /// * Collection admin826 ///827 /// # Arguments828 ///829 /// * `collection_id`: ID of the modified collection.830 /// * `property_permissions`: Vector of permissions for property keys.831 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.832 #[weight = T::CommonWeightInfo::set_token_property_permissions(property_permissions.len() as u32)]833 pub fn set_token_property_permissions(834 origin,835 collection_id: CollectionId,836 property_permissions: Vec<PropertyKeyPermission>,837 ) -> DispatchResultWithPostInfo {838 ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);839840 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);841842 dispatch_tx::<T, _>(collection_id, |d| d.set_token_property_permissions(&sender, property_permissions))843 }844845 /// Create multiple items within a collection with explicitly specified initial parameters.846 ///847 /// # Permissions848 ///849 /// * Collection owner850 /// * Collection admin851 /// * Anyone if852 /// * Allow List is enabled, and853 /// * Address is added to allow list, and854 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])855 ///856 /// # Arguments857 ///858 /// * `collection_id`: ID of the collection to which the tokens would belong.859 /// * `data`: Explicit item creation data.860 #[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]861 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {862 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);863 let budget = budget::Value::new(NESTING_BUDGET);864865 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))866 }867868 /// Completely allow or disallow transfers for a particular collection.869 ///870 /// # Permissions871 ///872 /// * Collection owner873 ///874 /// # Arguments875 ///876 /// * `collection_id`: ID of the collection.877 /// * `value`: New value of the flag, are transfers allowed?878 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]879 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {880 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);881 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;882 target_collection.check_is_internal()?;883 target_collection.check_is_owner(&sender)?;884885 // =========886887 target_collection.limits.transfers_enabled = Some(value);888 target_collection.save()889 }890891 /// Destroy an item.892 ///893 /// # Permissions894 ///895 /// * Collection owner896 /// * Collection admin897 /// * Current item owner898 ///899 /// # Arguments900 ///901 /// * `collection_id`: ID of the collection to which the item belongs.902 /// * `item_id`: ID of item to burn.903 /// * `value`: Number of pieces of the item to destroy.904 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.905 /// * Fungible Mode: The desired number of pieces to burn.906 /// * Re-Fungible Mode: The desired number of pieces to burn.907 #[weight = T::CommonWeightInfo::burn_item()]908 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {909 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);910911 let post_info = dispatch_tx::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;912 if value == 1 {913 <NftTransferBasket<T>>::remove(collection_id, item_id);914 <NftApproveBasket<T>>::remove(collection_id, item_id);915 }916 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?917 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());918 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));919 Ok(post_info)920 }921922 /// Destroy a token on behalf of the owner as a non-owner account.923 ///924 /// See also: [`approve`][`Pallet::approve`].925 ///926 /// After this method executes, one approval is removed from the total so that927 /// the approved address will not be able to transfer this item again from this owner.928 ///929 /// # Permissions930 ///931 /// * Collection owner932 /// * Collection admin933 /// * Current token owner934 /// * Address approved by current item owner935 ///936 /// # Arguments937 ///938 /// * `from`: The owner of the burning item.939 /// * `collection_id`: ID of the collection to which the item belongs.940 /// * `item_id`: ID of item to burn.941 /// * `value`: Number of pieces to burn.942 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.943 /// * Fungible Mode: The desired number of pieces to burn.944 /// * Re-Fungible Mode: The desired number of pieces to burn.945 #[weight = T::CommonWeightInfo::burn_from()]946 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {947 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);948 let budget = budget::Value::new(NESTING_BUDGET);949950 dispatch_tx::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))951 }952953 /// Change ownership of the token.954 ///955 /// # Permissions956 ///957 /// * Collection owner958 /// * Collection admin959 /// * Current token owner960 ///961 /// # Arguments962 ///963 /// * `recipient`: Address of token recipient.964 /// * `collection_id`: ID of the collection the item belongs to.965 /// * `item_id`: ID of the item.966 /// * Non-Fungible Mode: Required.967 /// * Fungible Mode: Ignored.968 /// * Re-Fungible Mode: Required.969 ///970 /// * `value`: Amount to transfer.971 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.972 /// * Fungible Mode: The desired number of pieces to transfer.973 /// * Re-Fungible Mode: The desired number of pieces to transfer.974 #[weight = T::CommonWeightInfo::transfer()]975 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {976 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);977 let budget = budget::Value::new(NESTING_BUDGET);978979 dispatch_tx::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))980 }981982 /// Allow a non-permissioned address to transfer or burn an item.983 ///984 /// # Permissions985 ///986 /// * Collection owner987 /// * Collection admin988 /// * Current item owner989 ///990 /// # Arguments991 ///992 /// * `spender`: Account to be approved to make specific transactions on non-owned tokens.993 /// * `collection_id`: ID of the collection the item belongs to.994 /// * `item_id`: ID of the item transactions on which are now approved.995 /// * `amount`: Number of pieces of the item approved for a transaction (maximum of 1 for NFTs).996 /// Set to 0 to revoke the approval.997 #[weight = T::CommonWeightInfo::approve()]998 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {999 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);10001001 dispatch_tx::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))1002 }10031004 /// Change ownership of an item on behalf of the owner as a non-owner account.1005 ///1006 /// See the [`approve`][`Pallet::approve`] method for additional information.1007 ///1008 /// After this method executes, one approval is removed from the total so that1009 /// the approved address will not be able to transfer this item again from this owner.1010 ///1011 /// # Permissions1012 ///1013 /// * Collection owner1014 /// * Collection admin1015 /// * Current item owner1016 /// * Address approved by current item owner1017 ///1018 /// # Arguments1019 ///1020 /// * `from`: Address that currently owns the token.1021 /// * `recipient`: Address of the new token-owner-to-be.1022 /// * `collection_id`: ID of the collection the item.1023 /// * `item_id`: ID of the item to be transferred.1024 /// * `value`: Amount to transfer.1025 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.1026 /// * Fungible Mode: The desired number of pieces to transfer.1027 /// * Re-Fungible Mode: The desired number of pieces to transfer.1028 #[weight = T::CommonWeightInfo::transfer_from()]1029 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {1030 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1031 let budget = budget::Value::new(NESTING_BUDGET);10321033 dispatch_tx::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))1034 }10351036 /// Set specific limits of a collection. Empty, or None fields mean chain default.1037 ///1038 /// # Permissions1039 ///1040 /// * Collection owner1041 /// * Collection admin1042 ///1043 /// # Arguments1044 ///1045 /// * `collection_id`: ID of the modified collection.1046 /// * `new_limit`: New limits of the collection. Fields that are not set (None)1047 /// will not overwrite the old ones.1048 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1049 pub fn set_collection_limits(1050 origin,1051 collection_id: CollectionId,1052 new_limit: CollectionLimits,1053 ) -> DispatchResult {1054 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1055 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1056 target_collection.check_is_internal()?;1057 target_collection.check_is_owner_or_admin(&sender)?;1058 let old_limit = &target_collection.limits;10591060 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;10611062 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(1063 collection_id1064 ));10651066 target_collection.save()1067 }10681069 /// Set specific permissions of a collection. Empty, or None fields mean chain default.1070 ///1071 /// # Permissions1072 ///1073 /// * Collection owner1074 /// * Collection admin1075 ///1076 /// # Arguments1077 ///1078 /// * `collection_id`: ID of the modified collection.1079 /// * `new_permission`: New permissions of the collection. Fields that are not set (None)1080 /// will not overwrite the old ones.1081 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1082 pub fn set_collection_permissions(1083 origin,1084 collection_id: CollectionId,1085 new_permission: CollectionPermissions,1086 ) -> DispatchResult {1087 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1088 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1089 target_collection.check_is_internal()?;1090 target_collection.check_is_owner_or_admin(&sender)?;1091 let old_limit = &target_collection.permissions;10921093 target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_permission)?;10941095 <Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(1096 collection_id1097 ));10981099 target_collection.save()1100 }11011102 /// Re-partition a refungible token, while owning all of its parts/pieces.1103 ///1104 /// # Permissions1105 ///1106 /// * Token owner (must own every part)1107 ///1108 /// # Arguments1109 ///1110 /// * `collection_id`: ID of the collection the RFT belongs to.1111 /// * `token_id`: ID of the RFT.1112 /// * `amount`: New number of parts/pieces into which the token shall be partitioned.1113 #[weight = T::RefungibleExtensionsWeightInfo::repartition()]1114 pub fn repartition(1115 origin,1116 collection_id: CollectionId,1117 token_id: TokenId,1118 amount: u128,1119 ) -> DispatchResultWithPostInfo {1120 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1121 dispatch_tx::<T, _>(collection_id, |d| {1122 if let Some(refungible_extensions) = d.refungible_extensions() {1123 refungible_extensions.repartition(&sender, token_id, amount)1124 } else {1125 fail!(<Error<T>>::RepartitionCalledOnNonRefungibleCollection)1126 }1127 })1128 }11291130 /// Sets or unsets the approval of a given operator.1131 ///1132 /// The `operator` is allowed to transfer all tokens of the `owner` on their behalf.1133 ///1134 /// # Arguments1135 ///1136 /// * `owner`: Token owner1137 /// * `operator`: Operator1138 /// * `approve`: Should operator status be granted or revoked?1139 #[weight = T::CommonWeightInfo::set_allowance_for_all()]1140 pub fn set_allowance_for_all(1141 origin,1142 collection_id: CollectionId,1143 operator: T::CrossAccountId,1144 approve: bool,1145 ) -> DispatchResultWithPostInfo {1146 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1147 dispatch_tx::<T, _>(collection_id, |d| {1148 d.set_allowance_for_all(sender, operator, approve)1149 })1150 }1151 }1152}11531154impl<T: Config> Pallet<T> {1155 /// Force set `sponsor` for `collection`.1156 ///1157 /// Differs from [`set_collection_sponsor`][`Pallet::set_collection_sponsor`] in that confirmation1158 /// from the `sponsor` is not required.1159 ///1160 /// # Arguments1161 ///1162 /// * `sponsor`: ID of the account of the sponsor-to-be.1163 /// * `collection_id`: ID of the modified collection.1164 pub fn force_set_sponsor(sponsor: T::AccountId, collection_id: CollectionId) -> DispatchResult {1165 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1166 target_collection.check_is_internal()?;1167 target_collection.set_sponsor(sponsor.clone())?;11681169 Self::deposit_event(Event::<T>::CollectionSponsorSet(1170 collection_id,1171 sponsor.clone(),1172 ));11731174 ensure!(1175 target_collection.confirm_sponsorship(&sponsor)?,1176 Error::<T>::ConfirmUnsetSponsorFail1177 );11781179 Self::deposit_event(Event::<T>::SponsorshipConfirmed(collection_id, sponsor));11801181 target_collection.save()1182 }11831184 /// Force remove `sponsor` for `collection`.1185 ///1186 /// Differs from `remove_sponsor` in that1187 /// it doesn't require consent from the `owner` of the collection.1188 ///1189 /// # Arguments1190 ///1191 /// * `collection_id`: ID of the modified collection.1192 pub fn force_remove_collection_sponsor(collection_id: CollectionId) -> DispatchResult {1193 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1194 target_collection.check_is_internal()?;1195 target_collection.sponsorship = SponsorshipState::Disabled;11961197 Self::deposit_event(Event::<T>::CollectionSponsorRemoved(collection_id));11981199 target_collection.save()1200 }12011202 #[inline(always)]1203 pub(crate) fn destroy_collection_internal(1204 sender: T::CrossAccountId,1205 collection_id: CollectionId,1206 ) -> DispatchResult {1207 let collection = <CollectionHandle<T>>::try_get(collection_id)?;1208 collection.check_is_internal()?;12091210 T::CollectionDispatch::destroy(sender, collection)?;12111212 // TODO: basket cleanup should be moved elsewhere1213 // Maybe runtime dispatch.rs should perform it?12141215 let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1216 let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1217 let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);12181219 let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1220 let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1221 let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);12221223 Ok(())1224 }1225}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Unique Pallet18//!19//! A pallet governing Unique transactions.20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The Unique pallet's purpose is to be the primary interface between28//! external users and the inner structure of the Unique chains.29//!30//! It also contains an implementation of [`CollectionHelpers`][`eth`],31//! an Ethereum contract dealing with collection operations.32//!33//! ## Interface34//!35//! ### Dispatchables36//!37//! - `create_collection` - Create a collection of tokens. **Deprecated**, use `create_collection_ex`.38//! - `create_collection_ex` - Create a collection of tokens with explicit parameters.39//! - `destroy_collection` - Destroy a collection if no tokens exist within.40//! - `add_to_allow_list` - Add an address to allow list.41//! - `remove_from_allow_list` - Remove an address from allow list.42//! - `change_collection_owner` - Change the owner of the collection.43//! - `add_collection_admin` - Add an admin to a collection.44//! - `remove_collection_admin` - Remove admin of a collection.45//! - `set_collection_sponsor` - Invite a new collection sponsor.46//! - `confirm_sponsorship` - Confirm own sponsorship of a collection, becoming the sponsor.47//! - `remove_collection_sponsor` - Remove a sponsor from a collection.48//! - `create_item` - Create an item within a collection.49//! - `create_multiple_items` - Create multiple items within a collection.50//! - `set_collection_properties` - Add or change collection properties.51//! - `delete_collection_properties` - Delete specified collection properties.52//! - `set_token_properties` - Add or change token properties.53//! - `delete_token_properties` - Delete token properties.54//! - `set_token_property_permissions` - Add or change token property permissions of a collection.55//! - `create_multiple_items_ex` - Create multiple items within a collection with explicitly specified initial parameters.56//! - `set_transfers_enabled_flag` - Completely allow or disallow transfers for a particular collection.57//! - `burn_item` - Destroy an item.58//! - `burn_from` - Destroy an item on behalf of the owner as a non-owner account.59//! - `transfer` - Change ownership of the token.60//! - `transfer_from` - Change ownership of the token on behalf of the owner as a non-owner account.61//! - `approve` - Allow a non-permissioned address to transfer or burn an item.62//! - `set_collection_limits` - Set specific limits of a collection.63//! - `set_collection_permissions` - Set specific permissions of a collection.64//! - `repartition` - Re-partition a refungible token, while owning all of its parts.6566#![recursion_limit = "1024"]67#![cfg_attr(not(feature = "std"), no_std)]68#![allow(69 clippy::too_many_arguments,70 clippy::unnecessary_mut_passed,71 clippy::unused_unit72)]7374extern crate alloc;7576use frame_support::{77 decl_module, decl_storage, decl_error, decl_event,78 dispatch::DispatchResult,79 ensure, fail,80 weights::{Weight},81 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},82 BoundedVec,83};84use scale_info::TypeInfo;85use frame_system::{self as system, ensure_signed};86use sp_std::{vec, vec::Vec};87use up_data_structs::{88 MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH,89 MAX_PROPERTIES_PER_ITEM, MAX_PROPERTY_KEY_LENGTH, MAX_PROPERTY_VALUE_LENGTH,90 MAX_COLLECTION_PROPERTIES_SIZE, COLLECTION_ADMINS_LIMIT, MAX_TOKEN_PROPERTIES_SIZE,91 CreateItemData, CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId,92 CreateCollectionData, CreateItemExData, budget, Property, PropertyKey, PropertyKeyPermission,93};94use pallet_evm::{account::CrossAccountId};95use pallet_common::{96 CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_tx,97 dispatch::CollectionDispatch, RefungibleExtensionsWeightInfo,98};99pub mod eth;100101#[cfg(feature = "runtime-benchmarks")]102pub mod benchmarking;103pub mod weights;104use weights::WeightInfo;105106/// A maximum number of levels of depth in the token nesting tree.107pub const NESTING_BUDGET: u32 = 5;108109decl_error! {110 /// Errors for the common Unique transactions.111 pub enum Error for Module<T: Config> {112 /// Decimal_points parameter must be lower than [`up_data_structs::MAX_DECIMAL_POINTS`].113 CollectionDecimalPointLimitExceeded,114 /// Length of items properties must be greater than 0.115 EmptyArgument,116 /// Repertition is only supported by refungible collection.117 RepartitionCalledOnNonRefungibleCollection,118 }119}120121/// Configuration trait of this pallet.122pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {123 /// Overarching event type.124 type RuntimeEvent: From<Event<Self>> + Into<<Self as frame_system::Config>::RuntimeEvent>;125126 /// Weight information for extrinsics in this pallet.127 type WeightInfo: WeightInfo;128129 /// Weight information for common pallet operations.130 type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;131132 /// Weight info information for extra refungible pallet operations.133 type RefungibleExtensionsWeightInfo: RefungibleExtensionsWeightInfo;134}135136decl_event! {137 pub enum Event<T>138 where139 <T as frame_system::Config>::AccountId,140 {141 /// Collection sponsor was removed142 ///143 /// # Arguments144 /// * collection_id: ID of the affected collection.145 CollectionSponsorRemoved(CollectionId),146147 /// Collection sponsor was set148 ///149 /// # Arguments150 /// * collection_id: ID of the affected collection.151 /// * owner: New sponsor address.152 CollectionSponsorSet(CollectionId, AccountId),153154 }155}156157type SelfWeightOf<T> = <T as Config>::WeightInfo;158159// # Used definitions160//161// ## User control levels162//163// chain-controlled - key is uncontrolled by user164// i.e autoincrementing index165// can use non-cryptographic hash166// real - key is controlled by user167// but it is hard to generate enough colliding values, i.e owner of signed txs168// can use non-cryptographic hash169// controlled - key is completly controlled by users170// i.e maps with mutable keys171// should use cryptographic hash172//173// ## User control level downgrade reasons174//175// ?1 - chain-controlled -> controlled176// collections/tokens can be destroyed, resulting in massive holes177// ?2 - chain-controlled -> controlled178// same as ?1, but can be only added, resulting in easier exploitation179// ?3 - real -> controlled180// no confirmation required, so addresses can be easily generated181decl_storage! {182 trait Store for Module<T: Config> as Unique {183184 //#region Private members185 /// Used for migrations186 ChainVersion: u64;187 //#endregion188189 //#region Tokens transfer sponosoring rate limit baskets190 /// (Collection id (controlled?2), who created (real))191 /// TODO: Off chain worker should remove from this map when collection gets removed192 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;193 /// Collection id (controlled?2), token id (controlled?2)194 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;195 /// Collection id (controlled?2), owning user (real)196 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;197 /// Collection id (controlled?2), token id (controlled?2)198 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;199 //#endregion200201 /// Variable metadata sponsoring202 /// Collection id (controlled?2), token id (controlled?2)203 #[deprecated]204 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;205 /// Last sponsoring of token property setting // todo:doc rephrase this and the following206 pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;207208 /// Last sponsoring of NFT approval in a collection209 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;210 /// Last sponsoring of fungible tokens approval in a collection211 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;212 /// Last sponsoring of RFT approval in a collection213 pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;214 }215}216217decl_module! {218 /// Type alias to Pallet, to be used by construct_runtime.219 pub struct Module<T: Config> for enum Call220 where221 origin: T::RuntimeOrigin222 {223 type Error = Error<T>;224225 #[doc = "A maximum number of levels of depth in the token nesting tree."]226 const NESTING_BUDGET: u32 = NESTING_BUDGET;227228 #[doc = "Maximal length of a collection name."]229 const MAX_COLLECTION_NAME_LENGTH: u32 = MAX_COLLECTION_NAME_LENGTH;230231 #[doc = "Maximal length of a collection description."]232 const MAX_COLLECTION_DESCRIPTION_LENGTH: u32 = MAX_COLLECTION_DESCRIPTION_LENGTH;233234 #[doc = "Maximal length of a token prefix."]235 const MAX_TOKEN_PREFIX_LENGTH: u32 = MAX_TOKEN_PREFIX_LENGTH;236237 #[doc = "Maximum admins per collection."]238 const COLLECTION_ADMINS_LIMIT: u32 = COLLECTION_ADMINS_LIMIT;239240 #[doc = "Maximal length of a property key."]241 const MAX_PROPERTY_KEY_LENGTH: u32 = MAX_PROPERTY_KEY_LENGTH;242243 #[doc = "Maximal length of a property value."]244 const MAX_PROPERTY_VALUE_LENGTH: u32 = MAX_PROPERTY_VALUE_LENGTH;245246 #[doc = "A maximum number of token properties."]247 const MAX_PROPERTIES_PER_ITEM: u32 = MAX_PROPERTIES_PER_ITEM;248249 #[doc = "Maximum size for all collection properties."]250 const MAX_COLLECTION_PROPERTIES_SIZE: u32 = MAX_COLLECTION_PROPERTIES_SIZE;251252 #[doc = "Maximum size of all token properties."]253 const MAX_TOKEN_PROPERTIES_SIZE: u32 = MAX_TOKEN_PROPERTIES_SIZE;254255 #[doc = "Default NFT collection limit."]256 const NFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::NFT);257258 #[doc = "Default RFT collection limit."]259 const RFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::ReFungible);260261 #[doc = "Default FT collection limit."]262 const FT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::Fungible(0));263264265 pub fn deposit_event() = default;266267 fn on_initialize(_now: T::BlockNumber) -> Weight {268 Weight::zero()269 }270271 fn on_runtime_upgrade() -> Weight {272 Weight::zero()273 }274275 /// Create a collection of tokens.276 ///277 /// Each Token may have multiple properties encoded as an array of bytes278 /// of certain length. The initial owner of the collection is set279 /// to the address that signed the transaction and can be changed later.280 ///281 /// Prefer the more advanced [`create_collection_ex`][`Pallet::create_collection_ex`] instead.282 ///283 /// # Permissions284 ///285 /// * Anyone - becomes the owner of the new collection.286 ///287 /// # Arguments288 ///289 /// * `collection_name`: Wide-character string with collection name290 /// (limit [`MAX_COLLECTION_NAME_LENGTH`]).291 /// * `collection_description`: Wide-character string with collection description292 /// (limit [`MAX_COLLECTION_DESCRIPTION_LENGTH`]).293 /// * `token_prefix`: Byte string containing the token prefix to mark a collection294 /// to which a token belongs (limit [`MAX_TOKEN_PREFIX_LENGTH`]).295 /// * `mode`: Type of items stored in the collection and type dependent data.296 // returns collection ID297 #[weight = <SelfWeightOf<T>>::create_collection()]298 #[deprecated(note = "`create_collection_ex` is more up-to-date and advanced, prefer it instead")]299 pub fn create_collection(300 origin,301 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,302 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,303 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,304 mode: CollectionMode305 ) -> DispatchResult {306 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {307 name: collection_name,308 description: collection_description,309 token_prefix,310 mode,311 ..Default::default()312 };313 Self::create_collection_ex(origin, data)314 }315316 /// Create a collection with explicit parameters.317 ///318 /// Prefer it to the deprecated [`create_collection`][`Pallet::create_collection`] method.319 ///320 /// # Permissions321 ///322 /// * Anyone - becomes the owner of the new collection.323 ///324 /// # Arguments325 ///326 /// * `data`: Explicit data of a collection used for its creation.327 #[weight = <SelfWeightOf<T>>::create_collection()]328 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {329 let sender = ensure_signed(origin)?;330331 // =========332 let sender = T::CrossAccountId::from_sub(sender);333 let _id = T::CollectionDispatch::create(sender.clone(), sender, data, Default::default())?;334335 Ok(())336 }337338 /// Destroy a collection if no tokens exist within.339 ///340 /// # Permissions341 ///342 /// * Collection owner343 ///344 /// # Arguments345 ///346 /// * `collection_id`: Collection to destroy.347 #[weight = <SelfWeightOf<T>>::destroy_collection()]348 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {349 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);350351 Self::destroy_collection_internal(sender, collection_id)352 }353354 /// Add an address to allow list.355 ///356 /// # Permissions357 ///358 /// * Collection owner359 /// * Collection admin360 ///361 /// # Arguments362 ///363 /// * `collection_id`: ID of the modified collection.364 /// * `address`: ID of the address to be added to the allowlist.365 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]366 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{367368 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);369 let collection = <CollectionHandle<T>>::try_get(collection_id)?;370 collection.check_is_internal()?;371372 <PalletCommon<T>>::toggle_allowlist(373 &collection,374 &sender,375 &address,376 true,377 )?;378379 Ok(())380 }381382 /// Remove an address from allow list.383 ///384 /// # Permissions385 ///386 /// * Collection owner387 /// * Collection admin388 ///389 /// # Arguments390 ///391 /// * `collection_id`: ID of the modified collection.392 /// * `address`: ID of the address to be removed from the allowlist.393 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]394 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{395396 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);397 let collection = <CollectionHandle<T>>::try_get(collection_id)?;398 collection.check_is_internal()?;399400 <PalletCommon<T>>::toggle_allowlist(401 &collection,402 &sender,403 &address,404 false,405 )?;406407 Ok(())408 }409410 /// Change the owner of the collection.411 ///412 /// # Permissions413 ///414 /// * Collection owner415 ///416 /// # Arguments417 ///418 /// * `collection_id`: ID of the modified collection.419 /// * `new_owner`: ID of the account that will become the owner.420 #[weight = <SelfWeightOf<T>>::change_collection_owner()]421 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {422 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);423 let new_owner = T::CrossAccountId::from_sub(new_owner);424 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;425 target_collection.change_owner(sender, new_owner.clone())426 }427428 /// Add an admin to a collection.429 ///430 /// NFT Collection can be controlled by multiple admin addresses431 /// (some which can also be servers, for example). Admins can issue432 /// and burn NFTs, as well as add and remove other admins,433 /// but cannot change NFT or Collection ownership.434 ///435 /// # Permissions436 ///437 /// * Collection owner438 /// * Collection admin439 ///440 /// # Arguments441 ///442 /// * `collection_id`: ID of the Collection to add an admin for.443 /// * `new_admin`: Address of new admin to add.444 #[weight = <SelfWeightOf<T>>::add_collection_admin()]445 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {446 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);447 let collection = <CollectionHandle<T>>::try_get(collection_id)?;448 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)449 }450451 /// Remove admin of a collection.452 ///453 /// An admin address can remove itself. List of admins may become empty,454 /// in which case only Collection Owner will be able to add an Admin.455 ///456 /// # Permissions457 ///458 /// * Collection owner459 /// * Collection admin460 ///461 /// # Arguments462 ///463 /// * `collection_id`: ID of the collection to remove the admin for.464 /// * `account_id`: Address of the admin to remove.465 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]466 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {467 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);468 let collection = <CollectionHandle<T>>::try_get(collection_id)?;469 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)470 }471472 /// Set (invite) a new collection sponsor.473 ///474 /// If successful, confirmation from the sponsor-to-be will be pending.475 ///476 /// # Permissions477 ///478 /// * Collection owner479 /// * Collection admin480 ///481 /// # Arguments482 ///483 /// * `collection_id`: ID of the modified collection.484 /// * `new_sponsor`: ID of the account of the sponsor-to-be.485 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]486 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {487 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);488 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;489 target_collection.set_sponsor(&sender, new_sponsor.clone())490 }491492 /// Confirm own sponsorship of a collection, becoming the sponsor.493 ///494 /// An invitation must be pending, see [`set_collection_sponsor`][`Pallet::set_collection_sponsor`].495 /// Sponsor can pay the fees of a transaction instead of the sender,496 /// but only within specified limits.497 ///498 /// # Permissions499 ///500 /// * Sponsor-to-be501 ///502 /// # Arguments503 ///504 /// * `collection_id`: ID of the collection with the pending sponsor.505 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]506 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {507 let sender = ensure_signed(origin)?;508 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;509 target_collection.confirm_sponsorship(&sender)510 }511512 /// Remove a collection's a sponsor, making everyone pay for their own transactions.513 ///514 /// # Permissions515 ///516 /// * Collection owner517 ///518 /// # Arguments519 ///520 /// * `collection_id`: ID of the collection with the sponsor to remove.521 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]522 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {523 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);524 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;525 target_collection.remove_sponsor(&sender)526 }527528 /// Mint an item within a collection.529 ///530 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].531 ///532 /// # Permissions533 ///534 /// * Collection owner535 /// * Collection admin536 /// * Anyone if537 /// * Allow List is enabled, and538 /// * Address is added to allow list, and539 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])540 ///541 /// # Arguments542 ///543 /// * `collection_id`: ID of the collection to which an item would belong.544 /// * `owner`: Address of the initial owner of the item.545 /// * `data`: Token data describing the item to store on chain.546 #[weight = T::CommonWeightInfo::create_item()]547 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {548 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);549 let budget = budget::Value::new(NESTING_BUDGET);550551 dispatch_tx::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))552 }553554 /// Create multiple items within a collection.555 ///556 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].557 ///558 /// # Permissions559 ///560 /// * Collection owner561 /// * Collection admin562 /// * Anyone if563 /// * Allow List is enabled, and564 /// * Address is added to the allow list, and565 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])566 ///567 /// # Arguments568 ///569 /// * `collection_id`: ID of the collection to which the tokens would belong.570 /// * `owner`: Address of the initial owner of the tokens.571 /// * `items_data`: Vector of data describing each item to be created.572 #[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]573 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {574 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);575 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);576 let budget = budget::Value::new(NESTING_BUDGET);577578 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))579 }580581 /// Add or change collection properties.582 ///583 /// # Permissions584 ///585 /// * Collection owner586 /// * Collection admin587 ///588 /// # Arguments589 ///590 /// * `collection_id`: ID of the modified collection.591 /// * `properties`: Vector of key-value pairs stored as the collection's metadata.592 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.593 #[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]594 pub fn set_collection_properties(595 origin,596 collection_id: CollectionId,597 properties: Vec<Property>598 ) -> DispatchResultWithPostInfo {599 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);600601 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);602603 dispatch_tx::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))604 }605606 /// Delete specified collection properties.607 ///608 /// # Permissions609 ///610 /// * Collection Owner611 /// * Collection Admin612 ///613 /// # Arguments614 ///615 /// * `collection_id`: ID of the modified collection.616 /// * `property_keys`: Vector of keys of the properties to be deleted.617 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.618 #[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]619 pub fn delete_collection_properties(620 origin,621 collection_id: CollectionId,622 property_keys: Vec<PropertyKey>,623 ) -> DispatchResultWithPostInfo {624 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);625626 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);627628 dispatch_tx::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))629 }630631 /// Add or change token properties according to collection's permissions.632 /// Currently properties only work with NFTs.633 ///634 /// # Permissions635 ///636 /// * Depends on collection's token property permissions and specified property mutability:637 /// * Collection owner638 /// * Collection admin639 /// * Token owner640 ///641 /// See [`set_token_property_permissions`][`Pallet::set_token_property_permissions`].642 ///643 /// # Arguments644 ///645 /// * `collection_id: ID of the collection to which the token belongs.646 /// * `token_id`: ID of the modified token.647 /// * `properties`: Vector of key-value pairs stored as the token's metadata.648 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.649 #[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]650 pub fn set_token_properties(651 origin,652 collection_id: CollectionId,653 token_id: TokenId,654 properties: Vec<Property>655 ) -> DispatchResultWithPostInfo {656 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);657658 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);659 let budget = budget::Value::new(NESTING_BUDGET);660661 dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties, &budget))662 }663664 /// Delete specified token properties. Currently properties only work with NFTs.665 ///666 /// # Permissions667 ///668 /// * Depends on collection's token property permissions and specified property mutability:669 /// * Collection owner670 /// * Collection admin671 /// * Token owner672 ///673 /// # Arguments674 ///675 /// * `collection_id`: ID of the collection to which the token belongs.676 /// * `token_id`: ID of the modified token.677 /// * `property_keys`: Vector of keys of the properties to be deleted.678 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.679 #[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]680 pub fn delete_token_properties(681 origin,682 collection_id: CollectionId,683 token_id: TokenId,684 property_keys: Vec<PropertyKey>685 ) -> DispatchResultWithPostInfo {686 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);687688 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);689 let budget = budget::Value::new(NESTING_BUDGET);690691 dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys, &budget))692 }693694 /// Add or change token property permissions of a collection.695 ///696 /// Without a permission for a particular key, a property with that key697 /// cannot be created in a token.698 ///699 /// # Permissions700 ///701 /// * Collection owner702 /// * Collection admin703 ///704 /// # Arguments705 ///706 /// * `collection_id`: ID of the modified collection.707 /// * `property_permissions`: Vector of permissions for property keys.708 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.709 #[weight = T::CommonWeightInfo::set_token_property_permissions(property_permissions.len() as u32)]710 pub fn set_token_property_permissions(711 origin,712 collection_id: CollectionId,713 property_permissions: Vec<PropertyKeyPermission>,714 ) -> DispatchResultWithPostInfo {715 ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);716717 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);718719 dispatch_tx::<T, _>(collection_id, |d| d.set_token_property_permissions(&sender, property_permissions))720 }721722 /// Create multiple items within a collection with explicitly specified initial parameters.723 ///724 /// # Permissions725 ///726 /// * Collection owner727 /// * Collection admin728 /// * Anyone if729 /// * Allow List is enabled, and730 /// * Address is added to allow list, and731 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])732 ///733 /// # Arguments734 ///735 /// * `collection_id`: ID of the collection to which the tokens would belong.736 /// * `data`: Explicit item creation data.737 #[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]738 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {739 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);740 let budget = budget::Value::new(NESTING_BUDGET);741742 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))743 }744745 /// Completely allow or disallow transfers for a particular collection.746 ///747 /// # Permissions748 ///749 /// * Collection owner750 ///751 /// # Arguments752 ///753 /// * `collection_id`: ID of the collection.754 /// * `value`: New value of the flag, are transfers allowed?755 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]756 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {757 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);758 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;759 target_collection.check_is_internal()?;760 target_collection.check_is_owner(&sender)?;761762 // =========763764 target_collection.limits.transfers_enabled = Some(value);765 target_collection.save()766 }767768 /// Destroy an item.769 ///770 /// # Permissions771 ///772 /// * Collection owner773 /// * Collection admin774 /// * Current item owner775 ///776 /// # Arguments777 ///778 /// * `collection_id`: ID of the collection to which the item belongs.779 /// * `item_id`: ID of item to burn.780 /// * `value`: Number of pieces of the item to destroy.781 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.782 /// * Fungible Mode: The desired number of pieces to burn.783 /// * Re-Fungible Mode: The desired number of pieces to burn.784 #[weight = T::CommonWeightInfo::burn_item()]785 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {786 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);787788 let post_info = dispatch_tx::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;789 if value == 1 {790 <NftTransferBasket<T>>::remove(collection_id, item_id);791 <NftApproveBasket<T>>::remove(collection_id, item_id);792 }793 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?794 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());795 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));796 Ok(post_info)797 }798799 /// Destroy a token on behalf of the owner as a non-owner account.800 ///801 /// See also: [`approve`][`Pallet::approve`].802 ///803 /// After this method executes, one approval is removed from the total so that804 /// the approved address will not be able to transfer this item again from this owner.805 ///806 /// # Permissions807 ///808 /// * Collection owner809 /// * Collection admin810 /// * Current token owner811 /// * Address approved by current item owner812 ///813 /// # Arguments814 ///815 /// * `from`: The owner of the burning item.816 /// * `collection_id`: ID of the collection to which the item belongs.817 /// * `item_id`: ID of item to burn.818 /// * `value`: Number of pieces to burn.819 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.820 /// * Fungible Mode: The desired number of pieces to burn.821 /// * Re-Fungible Mode: The desired number of pieces to burn.822 #[weight = T::CommonWeightInfo::burn_from()]823 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {824 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);825 let budget = budget::Value::new(NESTING_BUDGET);826827 dispatch_tx::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))828 }829830 /// Change ownership of the token.831 ///832 /// # Permissions833 ///834 /// * Collection owner835 /// * Collection admin836 /// * Current token owner837 ///838 /// # Arguments839 ///840 /// * `recipient`: Address of token recipient.841 /// * `collection_id`: ID of the collection the item belongs to.842 /// * `item_id`: ID of the item.843 /// * Non-Fungible Mode: Required.844 /// * Fungible Mode: Ignored.845 /// * Re-Fungible Mode: Required.846 ///847 /// * `value`: Amount to transfer.848 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.849 /// * Fungible Mode: The desired number of pieces to transfer.850 /// * Re-Fungible Mode: The desired number of pieces to transfer.851 #[weight = T::CommonWeightInfo::transfer()]852 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {853 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);854 let budget = budget::Value::new(NESTING_BUDGET);855856 dispatch_tx::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))857 }858859 /// Allow a non-permissioned address to transfer or burn an item.860 ///861 /// # Permissions862 ///863 /// * Collection owner864 /// * Collection admin865 /// * Current item owner866 ///867 /// # Arguments868 ///869 /// * `spender`: Account to be approved to make specific transactions on non-owned tokens.870 /// * `collection_id`: ID of the collection the item belongs to.871 /// * `item_id`: ID of the item transactions on which are now approved.872 /// * `amount`: Number of pieces of the item approved for a transaction (maximum of 1 for NFTs).873 /// Set to 0 to revoke the approval.874 #[weight = T::CommonWeightInfo::approve()]875 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {876 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);877878 dispatch_tx::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))879 }880881 /// Change ownership of an item on behalf of the owner as a non-owner account.882 ///883 /// See the [`approve`][`Pallet::approve`] method for additional information.884 ///885 /// After this method executes, one approval is removed from the total so that886 /// the approved address will not be able to transfer this item again from this owner.887 ///888 /// # Permissions889 ///890 /// * Collection owner891 /// * Collection admin892 /// * Current item owner893 /// * Address approved by current item owner894 ///895 /// # Arguments896 ///897 /// * `from`: Address that currently owns the token.898 /// * `recipient`: Address of the new token-owner-to-be.899 /// * `collection_id`: ID of the collection the item.900 /// * `item_id`: ID of the item to be transferred.901 /// * `value`: Amount to transfer.902 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.903 /// * Fungible Mode: The desired number of pieces to transfer.904 /// * Re-Fungible Mode: The desired number of pieces to transfer.905 #[weight = T::CommonWeightInfo::transfer_from()]906 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {907 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);908 let budget = budget::Value::new(NESTING_BUDGET);909910 dispatch_tx::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))911 }912913 /// Set specific limits of a collection. Empty, or None fields mean chain default.914 ///915 /// # Permissions916 ///917 /// * Collection owner918 /// * Collection admin919 ///920 /// # Arguments921 ///922 /// * `collection_id`: ID of the modified collection.923 /// * `new_limit`: New limits of the collection. Fields that are not set (None)924 /// will not overwrite the old ones.925 #[weight = <SelfWeightOf<T>>::set_collection_limits()]926 pub fn set_collection_limits(927 origin,928 collection_id: CollectionId,929 new_limit: CollectionLimits,930 ) -> DispatchResult {931 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);932 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;933 <PalletCommon<T>>::update_limits(&sender, &mut target_collection, new_limit)934 }935936 /// Set specific permissions of a collection. Empty, or None fields mean chain default.937 ///938 /// # Permissions939 ///940 /// * Collection owner941 /// * Collection admin942 ///943 /// # Arguments944 ///945 /// * `collection_id`: ID of the modified collection.946 /// * `new_permission`: New permissions of the collection. Fields that are not set (None)947 /// will not overwrite the old ones.948 #[weight = <SelfWeightOf<T>>::set_collection_limits()]949 pub fn set_collection_permissions(950 origin,951 collection_id: CollectionId,952 new_permission: CollectionPermissions,953 ) -> DispatchResult {954 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);955 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;956 <PalletCommon<T>>::update_permissions(957 &sender,958 &mut target_collection,959 new_permission960 )961 }962963 /// Re-partition a refungible token, while owning all of its parts/pieces.964 ///965 /// # Permissions966 ///967 /// * Token owner (must own every part)968 ///969 /// # Arguments970 ///971 /// * `collection_id`: ID of the collection the RFT belongs to.972 /// * `token_id`: ID of the RFT.973 /// * `amount`: New number of parts/pieces into which the token shall be partitioned.974 #[weight = T::RefungibleExtensionsWeightInfo::repartition()]975 pub fn repartition(976 origin,977 collection_id: CollectionId,978 token_id: TokenId,979 amount: u128,980 ) -> DispatchResultWithPostInfo {981 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);982 dispatch_tx::<T, _>(collection_id, |d| {983 if let Some(refungible_extensions) = d.refungible_extensions() {984 refungible_extensions.repartition(&sender, token_id, amount)985 } else {986 fail!(<Error<T>>::RepartitionCalledOnNonRefungibleCollection)987 }988 })989 }990991 /// Sets or unsets the approval of a given operator.992 ///993 /// The `operator` is allowed to transfer all tokens of the `owner` on their behalf.994 ///995 /// # Arguments996 ///997 /// * `owner`: Token owner998 /// * `operator`: Operator999 /// * `approve`: Should operator status be granted or revoked?1000 #[weight = T::CommonWeightInfo::set_allowance_for_all()]1001 pub fn set_allowance_for_all(1002 origin,1003 collection_id: CollectionId,1004 operator: T::CrossAccountId,1005 approve: bool,1006 ) -> DispatchResultWithPostInfo {1007 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1008 dispatch_tx::<T, _>(collection_id, |d| {1009 d.set_allowance_for_all(sender, operator, approve)1010 })1011 }1012 }1013}10141015impl<T: Config> Pallet<T> {1016 /// Force set `sponsor` for `collection`.1017 ///1018 /// Differs from [`set_collection_sponsor`][`Pallet::set_collection_sponsor`] in that confirmation1019 /// from the `sponsor` is not required.1020 ///1021 /// # Arguments1022 ///1023 /// * `sponsor`: ID of the account of the sponsor-to-be.1024 /// * `collection_id`: ID of the modified collection.1025 pub fn force_set_sponsor(sponsor: T::AccountId, collection_id: CollectionId) -> DispatchResult {1026 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1027 target_collection.force_set_sponsor(sponsor.clone())1028 }10291030 /// Force remove `sponsor` for `collection`.1031 ///1032 /// Differs from `remove_sponsor` in that1033 /// it doesn't require consent from the `owner` of the collection.1034 ///1035 /// # Arguments1036 ///1037 /// * `collection_id`: ID of the modified collection.1038 pub fn force_remove_collection_sponsor(collection_id: CollectionId) -> DispatchResult {1039 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1040 target_collection.force_remove_sponsor()1041 }10421043 #[inline(always)]1044 pub(crate) fn destroy_collection_internal(1045 sender: T::CrossAccountId,1046 collection_id: CollectionId,1047 ) -> DispatchResult {1048 let collection = <CollectionHandle<T>>::try_get(collection_id)?;1049 collection.check_is_internal()?;10501051 T::CollectionDispatch::destroy(sender, collection)?;10521053 // TODO: basket cleanup should be moved elsewhere1054 // Maybe runtime dispatch.rs should perform it?10551056 let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1057 let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1058 let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);10591060 let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1061 let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1062 let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);10631064 Ok(())1065 }1066}tests/src/change-collection-owner.test.tsdiffbeforeafterboth--- a/tests/src/change-collection-owner.test.ts
+++ b/tests/src/change-collection-owner.test.ts
@@ -146,7 +146,7 @@
const confirmSponsorshipTx = () => collection.confirmSponsorship(alice);
const removeSponsorTx = () => collection.removeSponsor(alice);
await expect(setSponsorTx()).to.be.rejectedWith(/common\.NoPermission/);
- await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+ await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.ConfirmUnsetSponsorFail/);
await expect(removeSponsorTx()).to.be.rejectedWith(/common\.NoPermission/);
const limits = {
tests/src/confirmSponsorship.test.tsdiffbeforeafterboth--- a/tests/src/confirmSponsorship.test.ts
+++ b/tests/src/confirmSponsorship.test.ts
@@ -207,14 +207,14 @@
const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
await collection.setSponsor(alice, bob.address);
const confirmSponsorshipTx = () => collection.confirmSponsorship(charlie);
- await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+ await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.ConfirmUnsetSponsorFail/);
});
itSub('(!negative test!) Confirm sponsorship using owner address', async ({helper}) => {
const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
await collection.setSponsor(alice, bob.address);
const confirmSponsorshipTx = () => collection.confirmSponsorship(alice);
- await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+ await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.ConfirmUnsetSponsorFail/);
});
itSub('(!negative test!) Confirm sponsorship by collection admin', async ({helper}) => {
@@ -222,13 +222,13 @@
await collection.setSponsor(alice, bob.address);
await collection.addAdmin(alice, {Substrate: charlie.address});
const confirmSponsorshipTx = () => collection.confirmSponsorship(charlie);
- await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+ await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.ConfirmUnsetSponsorFail/);
});
itSub('(!negative test!) Confirm sponsorship without sponsor being set with setCollectionSponsor', async ({helper}) => {
const collection = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
const confirmSponsorshipTx = () => collection.confirmSponsorship(charlie);
- await expect(confirmSponsorshipTx()).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+ await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.ConfirmUnsetSponsorFail/);
});
itSub('(!negative test!) Confirm sponsorship in a collection that was destroyed', async ({helper}) => {
tests/src/eth/collectionSponsoring.test.tsdiffbeforeafterboth--- a/tests/src/eth/collectionSponsoring.test.ts
+++ b/tests/src/eth/collectionSponsoring.test.ts
@@ -258,7 +258,7 @@
await collectionEvm.methods[testCase](testCase === 'setCollectionSponsor' ? sponsor : sponsorCross).send();
let collectionData = (await collectionSub.getData())!;
expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
- await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+ await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});
collectionData = (await collectionSub.getData())!;
tests/src/eth/createFTCollection.test.tsdiffbeforeafterboth--- a/tests/src/eth/createFTCollection.test.ts
+++ b/tests/src/eth/createFTCollection.test.ts
@@ -46,7 +46,7 @@
let data = (await helper.rft.getData(collectionId))!;
expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));
- await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+ await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor, true);
await sponsorCollection.methods.confirmCollectionSponsorship().send();
@@ -69,7 +69,7 @@
let data = (await helper.rft.getData(collectionId))!;
expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));
- await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+ await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);
await sponsorCollection.methods.confirmCollectionSponsorship().send();
@@ -192,11 +192,11 @@
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'ft', sponsor, true);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
- .call()).to.be.rejectedWith('caller is not set as sponsor');
+ .call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
}
{
await expect(peasantCollection.methods
- .setCollectionLimit('account_token_ownership_limit', '1000')
+ .setCollectionLimit('accountTokenOwnershipLimit', '1000')
.call()).to.be.rejectedWith(EXPECTED_ERROR);
}
});
@@ -217,11 +217,11 @@
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'ft', sponsor);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
- .call()).to.be.rejectedWith('caller is not set as sponsor');
+ .call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
}
{
await expect(peasantCollection.methods
- .setCollectionLimit('account_token_ownership_limit', '1000')
+ .setCollectionLimit('accountTokenOwnershipLimit', '1000')
.call()).to.be.rejectedWith(EXPECTED_ERROR);
}
});
tests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth--- a/tests/src/eth/createNFTCollection.test.ts
+++ b/tests/src/eth/createNFTCollection.test.ts
@@ -86,7 +86,7 @@
let data = (await helper.nft.getData(collectionId))!;
expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));
- await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+ await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor, true);
await sponsorCollection.methods.confirmCollectionSponsorship().send();
@@ -109,7 +109,7 @@
let data = (await helper.nft.getData(collectionId))!;
expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));
- await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+ await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);
await sponsorCollection.methods.confirmCollectionSponsorship().send();
@@ -203,11 +203,11 @@
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor, true);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
- .call()).to.be.rejectedWith('caller is not set as sponsor');
+ .call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
}
{
await expect(malfeasantCollection.methods
- .setCollectionLimit('account_token_ownership_limit', '1000')
+ .setCollectionLimit('accountTokenOwnershipLimit', '1000')
.call()).to.be.rejectedWith(EXPECTED_ERROR);
}
});
@@ -228,11 +228,11 @@
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
- .call()).to.be.rejectedWith('caller is not set as sponsor');
+ .call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
}
{
await expect(malfeasantCollection.methods
- .setCollectionLimit('account_token_ownership_limit', '1000')
+ .setCollectionLimit('accountTokenOwnershipLimit', '1000')
.call()).to.be.rejectedWith(EXPECTED_ERROR);
}
});
tests/src/eth/createRFTCollection.test.tsdiffbeforeafterboth--- a/tests/src/eth/createRFTCollection.test.ts
+++ b/tests/src/eth/createRFTCollection.test.ts
@@ -121,7 +121,7 @@
let data = (await helper.rft.getData(collectionId))!;
expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));
- await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+ await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor, true);
await sponsorCollection.methods.confirmCollectionSponsorship().send();
@@ -143,7 +143,7 @@
let data = (await helper.rft.getData(collectionId))!;
expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));
- await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+ await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);
await sponsorCollection.methods.confirmCollectionSponsorship().send();
@@ -235,11 +235,11 @@
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor, true);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
- .call()).to.be.rejectedWith('caller is not set as sponsor');
+ .call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
}
{
await expect(peasantCollection.methods
- .setCollectionLimit('account_token_ownership_limit', '1000')
+ .setCollectionLimit('accountTokenOwnershipLimit', '1000')
.call()).to.be.rejectedWith(EXPECTED_ERROR);
}
});
@@ -260,11 +260,11 @@
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
- .call()).to.be.rejectedWith('caller is not set as sponsor');
+ .call()).to.be.rejectedWith('ConfirmUnsetSponsorFail');
}
{
await expect(peasantCollection.methods
- .setCollectionLimit('account_token_ownership_limit', '1000')
+ .setCollectionLimit('accountTokenOwnershipLimit', '1000')
.call()).to.be.rejectedWith(EXPECTED_ERROR);
}
});
tests/src/eth/events.test.tsdiffbeforeafterboth--- a/tests/src/eth/events.test.ts
+++ b/tests/src/eth/events.test.ts
@@ -14,11 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+import { expect } from 'chai';
import {IKeyringPair} from '@polkadot/types/types';
-import { expect } from 'chai';
import { itEth, usingEthPlaygrounds } from './util';
-describe.only('NFT events', () => {
+describe('NFT events', () => {
let donor: IKeyringPair;
before(async function () {
@@ -29,8 +29,9 @@
itEth('Create event', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, events} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
- expect(events).to.be.like([
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['CollectionCreated']}]);
+ const {collectionAddress, events: ethEvents} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ expect(ethEvents).to.be.like([
{
event: 'CollectionCreated',
args: {
@@ -39,20 +40,25 @@
}
}
]);
+ expect(subEvents).to.be.like([{method: 'CollectionCreated'}]);
+ unsubscribe();
});
itEth('Destroy event', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
- let resutl = await collectionHelper.methods.destroyCollection(collectionAddress).send({from:owner});
- expect(resutl.events).to.be.like({
+ const {unsubscribe, collectedEvents} = await helper.subscribeEvents([{section: 'common', names: ['CollectionDestroyed']}]);
+ let result = await collectionHelper.methods.destroyCollection(collectionAddress).send({from:owner});
+ expect(result.events).to.be.like({
CollectionDestroyed: {
returnValues: {
collectionId: collectionAddress
}
}
});
+ expect(collectedEvents).to.be.like([{method: 'CollectionDestroyed'}]);
+ unsubscribe();
});
itEth('CollectionChanged event for CollectionPropertySet and CollectionPropertyDeleted', async ({helper}) => {
@@ -61,13 +67,14 @@
const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ let {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['CollectionPropertySet', 'CollectionPropertyDeleted']}]);
{
- const events: any = [];
+ const ethEvents: any = [];
collectionHelper.events.allEvents((_: any, event: any) => {
- events.push(event);
+ ethEvents.push(event);
});
await collection.methods.setCollectionProperties([{key: 'A', value: [0,1,2,3]}]).send({from:owner});
- expect(events).to.be.like([
+ expect(ethEvents).to.be.like([
{
event: 'CollectionChanged',
returnValues: {
@@ -75,14 +82,16 @@
}
}
]);
+ expect(subEvents).to.be.like([{method: 'CollectionPropertySet'}]);
+ subEvents.pop();
}
{
- const events: any = [];
+ const ethEvents: any = [];
collectionHelper.events.allEvents((_: any, event: any) => {
- events.push(event);
+ ethEvents.push(event);
});
await collection.methods.deleteCollectionProperties(['A']).send({from:owner});
- expect(events).to.be.like([
+ expect(ethEvents).to.be.like([
{
event: 'CollectionChanged',
returnValues: {
@@ -90,8 +99,9 @@
}
}
]);
+ expect(subEvents).to.be.like([{method: 'CollectionPropertyDeleted'}]);
}
-
+ unsubscribe();
});
itEth('CollectionChanged event for PropertyPermissionSet', async ({helper}) => {
@@ -99,12 +109,13 @@
const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
- const events: any = [];
+ const eethEvents: any = [];
collectionHelper.events.allEvents((_: any, event: any) => {
- events.push(event);
+ eethEvents.push(event);
});
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['PropertyPermissionSet']}]);
await collection.methods.setTokenPropertyPermission('testKey', true, true, true).send({from: owner});
- expect(events).to.be.like([
+ expect(eethEvents).to.be.like([
{
event: 'CollectionChanged',
returnValues: {
@@ -112,28 +123,236 @@
}
}
]);
+ expect(subEvents).to.be.like([{method: 'PropertyPermissionSet'}]);
+ unsubscribe();
+ });
+
+ itEth('CollectionChanged event for AllowListAddressAdded, AllowListAddressRemoved', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const user = helper.ethCrossAccount.createAccount();
+ const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ const ethEvents: any[] = [];
+ collectionHelper.events.allEvents((_: any, event: any) => {
+ ethEvents.push(event);
+ });
+
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['AllowListAddressAdded', 'AllowListAddressRemoved']}]);
+ {
+ await collection.methods.addToCollectionAllowListCross(user).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'AllowListAddressAdded'}]);
+ ethEvents.pop();
+ subEvents.pop();
+ }
+ {
+ await collection.methods.removeFromCollectionAllowListCross(user).send({from: owner});
+ expect(ethEvents.length).to.be.eq(1);
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'AllowListAddressRemoved'}]);
+ }
+ unsubscribe();
});
- // itEth('CollectionChanged event for AllowListAddressAdded', async ({helper}) => {
- // const owner = await helper.eth.createAccountWithBalance(donor);
- // const user = await helper.eth.createAccount();
- // const userCross = helper.ethCrossAccount.fromAddress(user);
- // const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
- // // allow list does not need to be enabled to add someone in advance
- // const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
- // const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
- // const events: any = [];
- // collectionHelper.events.allEvents((_: any, event: any) => {
- // events.push(event);
- // });
- // await helper.nft.addToAllowList(alice, collectionId, {Substrate: bob.address});
- // expect(events).to.be.like([
- // {
- // event: 'CollectionChanged',
- // returnValues: {
- // collectionId: collectionAddress
- // }
- // }
- // ]);
- // });
+ itEth('CollectionChanged event for CollectionAdminAdded, CollectionAdminRemoved', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const user = helper.ethCrossAccount.createAccount();
+ const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ const ethEvents: any = [];
+ collectionHelper.events.allEvents((_: any, event: any) => {
+ ethEvents.push(event);
+ });
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['CollectionAdminAdded', 'CollectionAdminRemoved']}]);
+ {
+ await collection.methods.addCollectionAdminCross(user).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionAdminAdded'}]);
+ ethEvents.pop();
+ subEvents.pop();
+ }
+ {
+ await collection.methods.removeCollectionAdminCross(user).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionAdminRemoved'}]);
+ }
+ unsubscribe();
+ });
+
+ itEth('CollectionChanged event for CollectionLimitSet', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ const ethEvents: any = [];
+ collectionHelper.events.allEvents((_: any, event: any) => {
+ ethEvents.push(event);
+ });
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['CollectionLimitSet']}]);
+ {
+ await collection.methods.setCollectionLimit('ownerCanTransfer', 0n).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionLimitSet'}]);
+ }
+ unsubscribe();
+ });
+
+ itEth('CollectionChanged event for CollectionOwnedChanged', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const new_owner = helper.ethCrossAccount.createAccount();
+ const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ const ethEvents: any = [];
+ collectionHelper.events.allEvents((_: any, event: any) => {
+ ethEvents.push(event);
+ });
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['CollectionOwnedChanged']}]);
+ {
+ await collection.methods.changeCollectionOwnerCross(new_owner).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionOwnedChanged'}]);
+ }
+ unsubscribe();
+ });
+
+ itEth('CollectionChanged event for CollectionPermissionSet', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ const ethEvents: any = [];
+ collectionHelper.events.allEvents((_: any, event: any) => {
+ ethEvents.push(event);
+ });
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['CollectionPermissionSet']}]);
+ {
+ await collection.methods.setCollectionMintMode(true).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionPermissionSet'}]);
+ ethEvents.pop();
+ subEvents.pop();
+ }
+ {
+ await collection.methods.setCollectionAccess(1).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionPermissionSet'}]);
+ }
+ unsubscribe();
+ });
+
+ itEth('CollectionChanged event for CollectionSponsorSet, SponsorshipConfirmed, CollectionSponsorRemoved', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const sponsor = await helper.ethCrossAccount.createAccountWithBalance(donor);
+ const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ const ethEvents: any = [];
+ collectionHelper.events.allEvents((_: any, event: any) => {
+ ethEvents.push(event);
+ });
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{
+ section: 'common', names: ['CollectionSponsorSet', 'SponsorshipConfirmed', 'CollectionSponsorRemoved'
+ ]}]);
+ {
+ await collection.methods.setCollectionSponsorCross(sponsor).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionSponsorSet'}]);
+ ethEvents.pop();
+ subEvents.pop();
+ }
+ {
+ await collection.methods.confirmCollectionSponsorship().send({from: sponsor.eth});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'SponsorshipConfirmed'}]);
+ ethEvents.pop();
+ subEvents.pop();
+ }
+ {
+ await collection.methods.removeCollectionSponsor().send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'CollectionChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'CollectionSponsorRemoved'}]);
+ }
+ unsubscribe();
+ });
+
});
\ No newline at end of file
tests/src/removeCollectionAdmin.test.tsdiffbeforeafterboth--- a/tests/src/removeCollectionAdmin.test.ts
+++ b/tests/src/removeCollectionAdmin.test.ts
@@ -51,7 +51,7 @@
const adminListBeforeAddAdmin = await collection.getAdmins();
expect(adminListBeforeAddAdmin).to.have.lengthOf(0);
- await collection.removeAdmin(alice, {Substrate: alice.address});
+ await expect(collection.removeAdmin(alice, {Substrate: alice.address})).to.be.rejectedWith('common.UserIsNotAdmin');
});
});
tests/src/removeCollectionSponsor.test.tsdiffbeforeafterboth--- a/tests/src/removeCollectionSponsor.test.ts
+++ b/tests/src/removeCollectionSponsor.test.ts
@@ -112,7 +112,7 @@
const collection = await helper.nft.mintCollection(alice, {name: 'RemoveCollectionSponsor-Neg-4', tokenPrefix: 'RCS'});
await collection.setSponsor(alice, bob.address);
await collection.removeSponsor(alice);
- await expect(collection.confirmSponsorship(bob)).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+ await expect(collection.confirmSponsorship(bob)).to.be.rejectedWith(/common\.ConfirmUnsetSponsorFail/);
});
itSub('Set - confirm - remove - confirm: Sponsor cannot come back', async ({helper}) => {
@@ -120,6 +120,6 @@
await collection.setSponsor(alice, bob.address);
await collection.confirmSponsorship(bob);
await collection.removeSponsor(alice);
- await expect(collection.confirmSponsorship(bob)).to.be.rejectedWith(/unique\.ConfirmUnsetSponsorFail/);
+ await expect(collection.confirmSponsorship(bob)).to.be.rejectedWith(/common\.ConfirmUnsetSponsorFail/);
});
});
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -43,6 +43,8 @@
IEthCrossAccountId,
} from './types';
import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';
+import type {Vec} from '@polkadot/types-codec';
+import { FrameSystemEventRecord } from '@polkadot/types/lookup';
export class CrossAccountId implements ICrossAccountId {
Substrate?: TSubstrateAccount;
@@ -404,6 +406,21 @@
return this.api;
}
+ async subscribeEvents(expectedEvents: {section: string, names: string[]}[]) {
+ const collectedEvents: IEvent[] = [];
+ const unsubscribe = await this.getApi().query.system.events((events: Vec<FrameSystemEventRecord>) => {
+ const ievents = this.eventHelper.extractEvents(events);
+ ievents.forEach((event) => {
+ expectedEvents.forEach((e => {
+ if (event.section === e.section && e.names.includes(event.method)) {
+ collectedEvents.push(event);
+ }
+ }))
+ });
+ });
+ return {unsubscribe: unsubscribe as any, collectedEvents};
+}
+
clearChainLog(): void {
this.chainLog = [];
}
@@ -834,7 +851,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionSponsorSet');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionSponsorSet');
}
/**
@@ -852,7 +869,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'SponsorshipConfirmed');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'SponsorshipConfirmed');
}
/**
@@ -870,7 +887,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionSponsorRemoved');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionSponsorRemoved');
}
/**
@@ -897,7 +914,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionLimitSet');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionLimitSet');
}
/**
@@ -916,7 +933,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionOwnedChanged');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionOwnedChanged');
}
/**
@@ -935,7 +952,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminAdded');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionAdminAdded');
}
/**
@@ -954,7 +971,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionAdminRemoved');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionAdminRemoved');
}
/**
@@ -983,7 +1000,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressAdded');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'AllowListAddressAdded');
}
/**
@@ -1001,7 +1018,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressRemoved');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'AllowListAddressRemoved');
}
/**
@@ -1020,7 +1037,7 @@
true,
);
- return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'CollectionPermissionSet');
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPermissionSet');
}
/**