difftreelog
Merge pull request #355 from UniqueNetwork/feature/nft-children
in: master
Structure children map
12 files changed
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -353,6 +353,8 @@
MustBeTokenOwner,
/// No permission to perform action
NoPermission,
+ /// Destroying only empty collections is allowed
+ CantDestroyNotEmptyCollection,
/// Collection is not in mint mode.
PublicMintingNotAllowed,
/// Address is not in allow list.
@@ -1268,6 +1270,18 @@
budget: &dyn Budget,
) -> DispatchResult;
+ fn nest(
+ &self,
+ under: TokenId,
+ to_nest: (CollectionId, TokenId)
+ );
+
+ fn unnest(
+ &self,
+ under: TokenId,
+ to_nest: (CollectionId, TokenId)
+ );
+
fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;
fn collection_tokens(&self) -> Vec<TokenId>;
fn token_exists(&self, token: TokenId) -> bool;
pallets/fungible/src/common.rsdiffbeforeafterboth--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -298,6 +298,18 @@
fail!(<Error<T>>::FungibleDisallowsNesting)
}
+ fn nest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
+ fn unnest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
fn collection_tokens(&self) -> Vec<TokenId> {
vec![TokenId::default()]
}
pallets/fungible/src/lib.rsdiffbeforeafterboth--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -25,8 +25,8 @@
budget::Budget,
};
use pallet_common::{
- Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
- dispatch::CollectionDispatch, eth::collection_id_to_address,
+ Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
+ eth::collection_id_to_address,
};
use pallet_evm::Pallet as PalletEvm;
use pallet_structure::Pallet as PalletStructure;
@@ -145,6 +145,10 @@
) -> DispatchResult {
let id = collection.id;
+ if Self::collection_has_tokens(id) {
+ return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+ }
+
// =========
PalletCommon::destroy_collection(collection.0, sender)?;
@@ -155,6 +159,10 @@
Ok(())
}
+ fn collection_has_tokens(collection_id: CollectionId) -> bool {
+ <TotalSupply<T>>::get(collection_id) != 0
+ }
+
pub fn burn(
collection: &FungibleHandle<T>,
owner: &T::CrossAccountId,
@@ -176,6 +184,11 @@
if balance == 0 {
<Balance<T>>::remove((collection.id, owner));
+ <PalletStructure<T>>::unnest_if_nested(
+ owner,
+ collection.id,
+ TokenId::default()
+ );
} else {
<Balance<T>>::insert((collection.id, owner), balance);
}
@@ -229,25 +242,25 @@
None
};
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
+ // =========
- dispatch.check_nesting(
- from.clone(),
- (collection.id, TokenId::default()),
- target.1,
- nesting_budget,
- )?;
- }
+ <PalletStructure<T>>::nest_if_sent_to_token(
+ from.clone(),
+ to,
+ collection.id,
+ TokenId::default(),
+ nesting_budget
+ )?;
- // =========
-
if let Some(balance_to) = balance_to {
// from != to
if balance_from == 0 {
<Balance<T>>::remove((collection.id, from));
+ <PalletStructure<T>>::unnest_if_nested(
+ from,
+ collection.id,
+ TokenId::default()
+ );
} else {
<Balance<T>>::insert((collection.id, from), balance_from);
}
@@ -306,18 +319,13 @@
}
for (to, _) in balances.iter() {
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
-
- dispatch.check_nesting(
- sender.clone(),
- (collection.id, TokenId::default()),
- target.1,
- nesting_budget,
- )?;
- }
+ <PalletStructure<T>>::check_nesting(
+ sender.clone(),
+ to,
+ collection.id,
+ TokenId::default(),
+ nesting_budget,
+ )?;
}
// =========
@@ -325,7 +333,7 @@
<TotalSupply<T>>::insert(collection.id, total_supply);
for (user, amount) in balances {
<Balance<T>>::insert((collection.id, &user), amount);
-
+ <PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId::default());
<PalletEvm<T>>::deposit_log(
ERC20Events::Transfer {
from: H160::default(),
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -353,6 +353,22 @@
<Pallet<T>>::check_nesting(self, sender, from, under, budget)
}
+ fn nest(
+ &self,
+ under: TokenId,
+ to_nest: (CollectionId, TokenId)
+ ) {
+ <Pallet<T>>::nest((self.id, under), to_nest);
+ }
+
+ fn unnest(
+ &self,
+ under: TokenId,
+ to_unnest: (CollectionId, TokenId)
+ ) {
+ <Pallet<T>>::unnest((self.id, under), to_unnest);
+ }
+
fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
<Owned<T>>::iter_prefix((self.id, account))
.map(|(id, _)| id)
pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -27,7 +27,7 @@
use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
use pallet_common::{
Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,
- dispatch::CollectionDispatch, eth::collection_id_to_address,
+ eth::collection_id_to_address,
};
use pallet_structure::Pallet as PalletStructure;
use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
@@ -76,6 +76,8 @@
NotNonfungibleDataUsedToMintFungibleCollectionToken,
/// Used amount > 1 with NFT
NonfungibleItemsHaveNoAmount,
+ /// Unable to burn NFT with children
+ CantBurnNftWithChildren,
}
#[pallet::config]
@@ -127,7 +129,20 @@
QueryKind = ValueQuery,
>;
+ /// Used to enumerate token's children
#[pallet::storage]
+ #[pallet::getter(fn token_children)]
+ pub type TokenChildren<T: Config> = StorageNMap<
+ Key = (
+ Key<Twox64Concat, CollectionId>,
+ Key<Twox64Concat, TokenId>,
+ Key<Twox64Concat, (CollectionId, TokenId)>,
+ ),
+ Value = bool,
+ QueryKind = ValueQuery,
+ >;
+
+ #[pallet::storage]
pub type AccountBalance<T: Config> = StorageNMap<
Key = (
Key<Twox64Concat, CollectionId>,
@@ -277,11 +292,16 @@
) -> DispatchResult {
let id = collection.id;
+ if Self::collection_has_tokens(id) {
+ return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+ }
+
// =========
PalletCommon::destroy_collection(collection.0, sender)?;
<TokenData<T>>::remove_prefix((id,), None);
+ <TokenChildren<T>>::remove_prefix((id,), None);
<Owned<T>>::remove_prefix((id,), None);
<TokensMinted<T>>::remove(id);
<TokensBurnt<T>>::remove(id);
@@ -307,6 +327,10 @@
collection.check_allowlist(sender)?;
}
+ if Self::token_has_children(collection.id, token) {
+ return Err(<Error<T>>::CantBurnNftWithChildren.into());
+ }
+
let burnt = <TokensBurnt<T>>::get(collection.id)
.checked_add(1)
.ok_or(ArithmeticError::Overflow)?;
@@ -315,13 +339,20 @@
.checked_sub(1)
.ok_or(ArithmeticError::Overflow)?;
+ // =========
+
if balance == 0 {
<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));
} else {
<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);
}
- // =========
+ <PalletStructure<T>>::unnest_if_nested(
+ &token_data.owner,
+ collection.id,
+ token
+ );
+
<Owned<T>>::remove((collection.id, &token_data.owner, token));
<TokensBurnt<T>>::insert(collection.id, burnt);
<TokenData<T>>::remove((collection.id, token));
@@ -553,20 +584,21 @@
None
};
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
+ <PalletStructure<T>>::nest_if_sent_to_token(
+ from.clone(),
+ to,
+ collection.id,
+ token,
+ nesting_budget
+ )?;
- dispatch.check_nesting(
- from.clone(),
- (collection.id, token),
- target.1,
- nesting_budget,
- )?;
- }
+ // =========
- // =========
+ <PalletStructure<T>>::unnest_if_nested(
+ from,
+ collection.id,
+ token
+ );
<TokenData<T>>::insert(
(collection.id, token),
@@ -653,17 +685,14 @@
for (i, data) in data.iter().enumerate() {
let token = TokenId(first_token + i as u32 + 1);
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
- dispatch.check_nesting(
- sender.clone(),
- (collection.id, token),
- target.1,
- nesting_budget,
- )?;
- }
+
+ <PalletStructure<T>>::check_nesting(
+ sender.clone(),
+ &data.owner,
+ collection.id,
+ token,
+ nesting_budget,
+ )?;
}
// =========
@@ -680,6 +709,8 @@
},
);
+ <PalletStructure<T>>::nest_if_sent_to_token_unchecked(&data.owner, collection.id, TokenId(token));
+
if let Err(e) = Self::set_token_properties(
collection,
sender,
@@ -927,6 +958,33 @@
Ok(())
}
+ fn nest(
+ under: (CollectionId, TokenId),
+ to_nest: (CollectionId, TokenId),
+ ) {
+ <TokenChildren<T>>::insert(
+ (under.0, under.1, (to_nest.0, to_nest.1)),
+ true
+ );
+ }
+
+ fn unnest(
+ under: (CollectionId, TokenId),
+ to_unnest: (CollectionId, TokenId),
+ ) {
+ <TokenChildren<T>>::remove(
+ (under.0, under.1, to_unnest)
+ );
+ }
+
+ fn collection_has_tokens(collection_id: CollectionId) -> bool {
+ <TokenData<T>>::iter_prefix((collection_id,)).next().is_some()
+ }
+
+ fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {
+ <TokenChildren<T>>::iter_prefix((collection_id, token_id)).next().is_some()
+ }
+
/// Delegated to `create_multiple_items`
pub fn create_item(
collection: &NonfungibleHandle<T>,
pallets/proxy-rmrk-core/src/misc.rsdiffbeforeafterboth--- a/pallets/proxy-rmrk-core/src/misc.rs
+++ b/pallets/proxy-rmrk-core/src/misc.rs
@@ -26,6 +26,18 @@
}
}
+pub trait RmrkRebind<T, S> {
+ fn rebind(&self) -> BoundedVec<u8, S>;
+}
+
+impl<T, S> RmrkRebind<T, S> for BoundedVec<u8, T> where BoundedVec<u8, S>: TryFrom<Vec<u8>> {
+ fn rebind(&self) -> BoundedVec<u8, S> {
+ BoundedVec::<u8, S>::try_from(
+ self.clone().into_inner()
+ ).unwrap_or_default()
+ }
+}
+
#[derive(Encode, Decode, PartialEq, Eq)]
pub enum CollectionType {
Regular,
pallets/refungible/src/common.rsdiffbeforeafterboth--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -313,6 +313,18 @@
fail!(<Error<T>>::RefungibleDisallowsNesting)
}
+ fn nest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
+ fn unnest(
+ &self,
+ _under: TokenId,
+ _to_nest: (CollectionId, TokenId)
+ ) {}
+
fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
<Owned<T>>::iter_prefix((self.id, account))
.map(|(id, _)| id)
pallets/refungible/src/lib.rsdiffbeforeafterboth--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -23,8 +23,7 @@
};
use pallet_evm::account::CrossAccountId;
use pallet_common::{
- Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
- dispatch::CollectionDispatch,
+ Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
};
use pallet_structure::Pallet as PalletStructure;
use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
@@ -211,6 +210,10 @@
) -> DispatchResult {
let id = collection.id;
+ if Self::collection_has_tokens(id) {
+ return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+ }
+
// =========
PalletCommon::destroy_collection(collection.0, sender)?;
@@ -226,6 +229,10 @@
Ok(())
}
+ fn collection_has_tokens(collection_id: CollectionId) -> bool {
+ <TokenData<T>>::iter_prefix((collection_id,)).next().is_some()
+ }
+
pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {
let burnt = <TokensBurnt<T>>::get(collection.id)
.checked_add(1)
@@ -265,6 +272,7 @@
// =========
<Owned<T>>::remove((collection.id, owner, token));
+ <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
<AccountBalance<T>>::insert((collection.id, owner), account_balance);
Self::burn_token(collection, token)?;
<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
@@ -292,6 +300,7 @@
if balance == 0 {
<Owned<T>>::remove((collection.id, owner, token));
+ <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
<Balance<T>>::remove((collection.id, token, owner));
<AccountBalance<T>>::insert((collection.id, owner), account_balance);
} else {
@@ -372,25 +381,25 @@
None
};
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
+ // =========
- dispatch.check_nesting(
- from.clone(),
- (collection.id, token),
- target.1,
- nesting_budget,
- )?;
- }
-
- // =========
+ <PalletStructure<T>>::nest_if_sent_to_token(
+ from.clone(),
+ to,
+ collection.id,
+ token,
+ nesting_budget
+ )?;
if let Some(balance_to) = balance_to {
// from != to
if balance_from == 0 {
<Balance<T>>::remove((collection.id, token, from));
+ <PalletStructure<T>>::unnest_if_nested(
+ from,
+ collection.id,
+ token
+ );
} else {
<Balance<T>>::insert((collection.id, token, from), balance_from);
}
@@ -488,18 +497,14 @@
for (i, token) in data.iter().enumerate() {
let token_id = TokenId(first_token_id + i as u32 + 1);
for (to, _) in token.users.iter() {
- if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
- let handle = <CollectionHandle<T>>::try_get(target.0)?;
- let dispatch = T::CollectionDispatch::dispatch(handle);
- let dispatch = dispatch.as_dyn();
- dispatch.check_nesting(
- sender.clone(),
- (collection.id, token_id),
- target.1,
- nesting_budget,
- )?;
- }
+ <PalletStructure<T>>::check_nesting(
+ sender.clone(),
+ to,
+ collection.id,
+ token_id,
+ nesting_budget,
+ )?;
}
}
@@ -519,12 +524,15 @@
const_data: token.const_data,
},
);
+
for (user, amount) in token.users.into_iter() {
if amount == 0 {
continue;
}
<Balance<T>>::insert((collection.id, token_id, &user), amount);
<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);
+ <PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId(token_id));
+
// TODO: ERC20 transfer event
<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(
collection.id,
pallets/structure/src/lib.rsdiffbeforeafterboth--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -1,8 +1,9 @@
#![cfg_attr(not(feature = "std"), no_std)]
+use pallet_common::CommonCollectionOperations;
use sp_std::collections::btree_set::BTreeSet;
-use frame_support::dispatch::DispatchError;
+use frame_support::dispatch::{DispatchError, DispatchResult};
use frame_support::fail;
pub use pallet::*;
use pallet_common::{dispatch::CollectionDispatch, CollectionHandle};
@@ -155,8 +156,8 @@
budget: &dyn Budget,
) -> Result<bool, DispatchError> {
let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) {
- Some((collection, token)) => Parent::Token(collection, token),
- None => Parent::User(user),
+ Some((collection, token)) => Self::find_topmost_owner(collection, token, budget)?,
+ None => user,
};
// Tried to nest token in itself
@@ -171,10 +172,10 @@
return Err(<Error<T>>::OuroborosDetected.into())
}
// Found needed parent, token is indirecty owned
- v if v == target_parent => return Ok(true),
+ Parent::User(user) if user == target_parent => return Ok(true),
// Token is owned by other user
Parent::User(_) => return Ok(false),
- Parent::TokenNotFound => return Ok(false),
+ Parent::TokenNotFound => return Err(<Error<T>>::TokenNotFound.into()),
// Continue parent chain
Parent::Token(_, _) => {}
}
@@ -182,4 +183,113 @@
Err(<Error<T>>::DepthLimit.into())
}
+
+ pub fn check_nesting(
+ from: T::CrossAccountId,
+ under: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId,
+ nesting_budget: &dyn Budget
+ ) -> DispatchResult {
+ Self::try_exec_if_owner_is_valid_nft(
+ under,
+ |d, parent_id| d.check_nesting(
+ from,
+ (collection_id, token_id),
+ parent_id,
+ nesting_budget
+ )
+ )
+ }
+
+ pub fn nest_if_sent_to_token(
+ from: T::CrossAccountId,
+ under: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId,
+ nesting_budget: &dyn Budget
+ ) -> DispatchResult {
+ Self::try_exec_if_owner_is_valid_nft(
+ under,
+ |d, parent_id| {
+ d.check_nesting(
+ from,
+ (collection_id, token_id),
+ parent_id,
+ nesting_budget
+ )?;
+
+ d.nest(parent_id, (collection_id, token_id));
+
+ Ok(())
+ }
+ )
+ }
+
+ pub fn nest_if_sent_to_token_unchecked(
+ owner: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId
+ ) {
+ Self::exec_if_owner_is_valid_nft(
+ owner,
+ |d, parent_id| d.nest(
+ parent_id,
+ (collection_id, token_id)
+ )
+ );
+ }
+
+ pub fn unnest_if_nested(
+ owner: &T::CrossAccountId,
+ collection_id: CollectionId,
+ token_id: TokenId
+ ) {
+ Self::exec_if_owner_is_valid_nft(
+ owner,
+ |d, parent_id| d.unnest(
+ parent_id,
+ (collection_id, token_id)
+ )
+ );
+ }
+
+ fn exec_if_owner_is_valid_nft(
+ account: &T::CrossAccountId,
+ action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId)
+ ) {
+ Self::try_exec_if_owner_is_valid_nft(
+ account,
+ |d, id| {
+ action(d, id);
+ Ok(())
+ }
+ ).unwrap();
+ }
+
+ fn try_exec_if_owner_is_valid_nft(
+ account: &T::CrossAccountId,
+ action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult
+ ) -> DispatchResult {
+ let account = T::CrossTokenAddressMapping::address_to_token(account);
+
+ if account.is_none() {
+ return Ok(());
+ }
+
+ let account = account.unwrap();
+
+ let handle = <CollectionHandle<T>>::try_get(account.0);
+
+ if handle.is_err() {
+ return Ok(());
+ }
+
+ let handle = handle.unwrap();
+
+ let dispatch = T::CollectionDispatch::dispatch(handle);
+ let dispatch = dispatch.as_dyn();
+
+ action(dispatch, account.1)
+ }
}
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#![recursion_limit = "1024"]18#![cfg_attr(not(feature = "std"), no_std)]19#![allow(20 clippy::too_many_arguments,21 clippy::unnecessary_mut_passed,22 clippy::unused_unit23)]2425extern crate alloc;2627use frame_support::{28 decl_module, decl_storage, decl_error, decl_event,29 dispatch::DispatchResult,30 ensure,31 weights::{Weight},32 transactional,33 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},34 BoundedVec,35};36use scale_info::TypeInfo;37use frame_system::{self as system, ensure_signed};38use sp_runtime::{sp_std::prelude::Vec};39use up_data_structs::{40 MAX_COLLECTION_NAME_LENGTH,41 MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH, AccessMode, CreateItemData,42 CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId, SponsorshipState,43 CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,44 PropertyKeyPermission,45};46use pallet_evm::account::CrossAccountId;47use pallet_common::{48 CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_call,49 dispatch::CollectionDispatch,50};51pub mod eth;5253#[cfg(feature = "runtime-benchmarks")]54mod benchmarking;55pub mod weights;56use weights::WeightInfo;5758decl_error! {59 /// Error for non-fungible-token module.60 pub enum Error for Module<T: Config> {61 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.62 CollectionDecimalPointLimitExceeded,63 /// This address is not set as sponsor, use setCollectionSponsor first.64 ConfirmUnsetSponsorFail,65 /// Length of items properties must be greater than 0.66 EmptyArgument,67 }68}6970pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {71 type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;7273 /// Weight information for extrinsics in this pallet.74 type WeightInfo: WeightInfo;75 type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;76}7778decl_event! {79 pub enum Event<T>80 where81 <T as frame_system::Config>::AccountId,82 <T as pallet_evm::account::Config>::CrossAccountId,83 {84 /// Collection sponsor was removed85 ///86 /// # Arguments87 ///88 /// * collection_id: Globally unique collection identifier.89 CollectionSponsorRemoved(CollectionId),9091 /// Collection admin was added92 ///93 /// # Arguments94 ///95 /// * collection_id: Globally unique collection identifier.96 ///97 /// * admin: Admin address.98 CollectionAdminAdded(CollectionId, CrossAccountId),99100 /// Collection owned was change101 ///102 /// # Arguments103 ///104 /// * collection_id: Globally unique collection identifier.105 ///106 /// * owner: New owner address.107 CollectionOwnedChanged(CollectionId, AccountId),108109 /// Collection sponsor was set110 ///111 /// # Arguments112 ///113 /// * collection_id: Globally unique collection identifier.114 ///115 /// * owner: New sponsor address.116 CollectionSponsorSet(CollectionId, AccountId),117118 /// New sponsor was confirm119 ///120 /// # Arguments121 ///122 /// * collection_id: Globally unique collection identifier.123 ///124 /// * sponsor: New sponsor address.125 SponsorshipConfirmed(CollectionId, AccountId),126127 /// Collection admin was removed128 ///129 /// # Arguments130 ///131 /// * collection_id: Globally unique collection identifier.132 ///133 /// * admin: Admin address.134 CollectionAdminRemoved(CollectionId, CrossAccountId),135136 /// Address was remove from allow list137 ///138 /// # Arguments139 ///140 /// * collection_id: Globally unique collection identifier.141 ///142 /// * user: Address.143 AllowListAddressRemoved(CollectionId, CrossAccountId),144145 /// Address was add to allow list146 ///147 /// # Arguments148 ///149 /// * collection_id: Globally unique collection identifier.150 ///151 /// * user: Address.152 AllowListAddressAdded(CollectionId, CrossAccountId),153154 /// Collection limits was set155 ///156 /// # Arguments157 ///158 /// * collection_id: Globally unique collection identifier.159 CollectionLimitSet(CollectionId),160161 CollectionPermissionSet(CollectionId),162 }163}164165type SelfWeightOf<T> = <T as Config>::WeightInfo;166167// # Used definitions168//169// ## User control levels170//171// chain-controlled - key is uncontrolled by user172// i.e autoincrementing index173// can use non-cryptographic hash174// real - key is controlled by user175// but it is hard to generate enough colliding values, i.e owner of signed txs176// can use non-cryptographic hash177// controlled - key is completly controlled by users178// i.e maps with mutable keys179// should use cryptographic hash180//181// ## User control level downgrade reasons182//183// ?1 - chain-controlled -> controlled184// collections/tokens can be destroyed, resulting in massive holes185// ?2 - chain-controlled -> controlled186// same as ?1, but can be only added, resulting in easier exploitation187// ?3 - real -> controlled188// no confirmation required, so addresses can be easily generated189decl_storage! {190 trait Store for Module<T: Config> as Unique {191192 //#region Private members193 /// Used for migrations194 ChainVersion: u64;195 //#endregion196197 //#region Tokens transfer rate limit baskets198 /// (Collection id (controlled?2), who created (real))199 /// TODO: Off chain worker should remove from this map when collection gets removed200 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;201 /// Collection id (controlled?2), token id (controlled?2)202 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;203 /// Collection id (controlled?2), owning user (real)204 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;205 /// Collection id (controlled?2), token id (controlled?2)206 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>;207 //#endregion208209 /// Variable metadata sponsoring210 /// Collection id (controlled?2), token id (controlled?2)211 #[deprecated]212 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;213 pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;214215 /// Approval sponsoring216 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;217 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;218 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>;219 }220}221222decl_module! {223 pub struct Module<T: Config> for enum Call224 where225 origin: T::Origin226 {227 type Error = Error<T>;228229 fn deposit_event() = default;230231 fn on_initialize(_now: T::BlockNumber) -> Weight {232 0233 }234235 fn on_runtime_upgrade() -> Weight {236 let limit = None;237238 <VariableMetaDataBasket<T>>::remove_all(limit);239240 0241 }242243 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner of the collection is set to the address that signed the transaction and can be changed later.244 ///245 /// # Permissions246 ///247 /// * Anyone.248 ///249 /// # Arguments250 ///251 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.252 ///253 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.254 ///255 /// * token_prefix: UTF-8 string with token prefix.256 ///257 /// * mode: [CollectionMode] collection type and type dependent data.258 // returns collection ID259 #[weight = <SelfWeightOf<T>>::create_collection()]260 #[transactional]261 #[deprecated]262 pub fn create_collection(origin,263 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,264 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,265 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,266 mode: CollectionMode) -> DispatchResult {267 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {268 name: collection_name,269 description: collection_description,270 token_prefix,271 mode,272 ..Default::default()273 };274 Self::create_collection_ex(origin, data)275 }276277 /// This method creates a collection278 ///279 /// Prefer it to deprecated [`created_collection`] method280 #[weight = <SelfWeightOf<T>>::create_collection()]281 #[transactional]282 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {283 let sender = ensure_signed(origin)?;284285 // =========286287 T::CollectionDispatch::create(sender, data)?;288289 Ok(())290 }291292 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.293 ///294 /// # Permissions295 ///296 /// * Collection Owner.297 ///298 /// # Arguments299 ///300 /// * collection_id: collection to destroy.301 #[weight = <SelfWeightOf<T>>::destroy_collection()]302 #[transactional]303 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {304 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);305 let collection = <CollectionHandle<T>>::try_get(collection_id)?;306307 // =========308309 T::CollectionDispatch::destroy(sender, collection)?;310311 <NftTransferBasket<T>>::remove_prefix(collection_id, None);312 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);313 <ReFungibleTransferBasket<T>>::remove_prefix((collection_id,), None);314315 <NftApproveBasket<T>>::remove_prefix(collection_id, None);316 <FungibleApproveBasket<T>>::remove_prefix(collection_id, None);317 <RefungibleApproveBasket<T>>::remove_prefix((collection_id,), None);318319 Ok(())320 }321322 /// Add an address to allow list.323 ///324 /// # Permissions325 ///326 /// * Collection Owner327 /// * Collection Admin328 ///329 /// # Arguments330 ///331 /// * collection_id.332 ///333 /// * address.334 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]335 #[transactional]336 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{337338 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);339 let collection = <CollectionHandle<T>>::try_get(collection_id)?;340341 <PalletCommon<T>>::toggle_allowlist(342 &collection,343 &sender,344 &address,345 true,346 )?;347348 Self::deposit_event(Event::<T>::AllowListAddressAdded(349 collection_id,350 address351 ));352353 Ok(())354 }355356 /// Remove an address from allow list.357 ///358 /// # Permissions359 ///360 /// * Collection Owner361 /// * Collection Admin362 ///363 /// # Arguments364 ///365 /// * collection_id.366 ///367 /// * address.368 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]369 #[transactional]370 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{371372 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);373 let collection = <CollectionHandle<T>>::try_get(collection_id)?;374375 <PalletCommon<T>>::toggle_allowlist(376 &collection,377 &sender,378 &address,379 false,380 )?;381382 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(383 collection_id,384 address385 ));386387 Ok(())388 }389390 /// Change the owner of the collection.391 ///392 /// # Permissions393 ///394 /// * Collection Owner.395 ///396 /// # Arguments397 ///398 /// * collection_id.399 ///400 /// * new_owner.401 #[weight = <SelfWeightOf<T>>::change_collection_owner()]402 #[transactional]403 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {404405 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);406407 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;408 target_collection.check_is_owner(&sender)?;409410 target_collection.owner = new_owner.clone();411 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(412 collection_id,413 new_owner414 ));415416 target_collection.save()417 }418419 /// Adds an admin of the Collection.420 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.421 ///422 /// # Permissions423 ///424 /// * Collection Owner.425 /// * Collection Admin.426 ///427 /// # Arguments428 ///429 /// * collection_id: ID of the Collection to add admin for.430 ///431 /// * new_admin_id: Address of new admin to add.432 #[weight = <SelfWeightOf<T>>::add_collection_admin()]433 #[transactional]434 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {435 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);436 let collection = <CollectionHandle<T>>::try_get(collection_id)?;437438 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(439 collection_id,440 new_admin_id.clone()441 ));442443 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)444 }445446 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.447 ///448 /// # Permissions449 ///450 /// * Collection Owner.451 /// * Collection Admin.452 ///453 /// # Arguments454 ///455 /// * collection_id: ID of the Collection to remove admin for.456 ///457 /// * account_id: Address of admin to remove.458 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]459 #[transactional]460 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {461 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);462 let collection = <CollectionHandle<T>>::try_get(collection_id)?;463464 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(465 collection_id,466 account_id.clone()467 ));468469 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)470 }471472 /// # Permissions473 ///474 /// * Collection Owner475 ///476 /// # Arguments477 ///478 /// * collection_id.479 ///480 /// * new_sponsor.481 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]482 #[transactional]483 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {484 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);485486 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;487 target_collection.check_is_owner(&sender)?;488489 target_collection.set_sponsor(new_sponsor.clone());490491 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(492 collection_id,493 new_sponsor494 ));495496 target_collection.save()497 }498499 /// # Permissions500 ///501 /// * Sponsor.502 ///503 /// # Arguments504 ///505 /// * collection_id.506 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]507 #[transactional]508 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {509 let sender = ensure_signed(origin)?;510511 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;512 ensure!(513 target_collection.confirm_sponsorship(&sender),514 Error::<T>::ConfirmUnsetSponsorFail515 );516517 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(518 collection_id,519 sender520 ));521522 target_collection.save()523 }524525 /// Switch back to pay-per-own-transaction model.526 ///527 /// # Permissions528 ///529 /// * Collection owner.530 ///531 /// # Arguments532 ///533 /// * collection_id.534 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]535 #[transactional]536 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {537 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);538539 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;540 target_collection.check_is_owner(&sender)?;541542 target_collection.sponsorship = SponsorshipState::Disabled;543544 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(545 collection_id546 ));547 target_collection.save()548 }549550 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.551 ///552 /// # Permissions553 ///554 /// * Collection Owner.555 /// * Collection Admin.556 /// * Anyone if557 /// * Allow List is enabled, and558 /// * Address is added to allow list, and559 /// * MintPermission is enabled (see SetMintPermission method)560 ///561 /// # Arguments562 ///563 /// * collection_id: ID of the collection.564 ///565 /// * owner: Address, initial owner of the NFT.566 ///567 /// * data: Token data to store on chain.568 #[weight = T::CommonWeightInfo::create_item()]569 #[transactional]570 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {571 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);572 let budget = budget::Value::new(2);573574 dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))575 }576577 /// This method creates multiple items in a collection created with CreateCollection method.578 ///579 /// # Permissions580 ///581 /// * Collection Owner.582 /// * Collection Admin.583 /// * Anyone if584 /// * Allow List is enabled, and585 /// * Address is added to allow list, and586 /// * MintPermission is enabled (see SetMintPermission method)587 ///588 /// # Arguments589 ///590 /// * collection_id: ID of the collection.591 ///592 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].593 ///594 /// * owner: Address, initial owner of the NFT.595 #[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]596 #[transactional]597 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {598 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);599 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);600 let budget = budget::Value::new(2);601602 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))603 }604605 #[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]606 #[transactional]607 pub fn set_collection_properties(608 origin,609 collection_id: CollectionId,610 properties: Vec<Property>611 ) -> DispatchResultWithPostInfo {612 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);613614 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);615616 dispatch_call::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))617 }618619 #[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]620 #[transactional]621 pub fn delete_collection_properties(622 origin,623 collection_id: CollectionId,624 property_keys: Vec<PropertyKey>,625 ) -> DispatchResultWithPostInfo {626 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);627628 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);629630 dispatch_call::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))631 }632633 #[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]634 #[transactional]635 pub fn set_token_properties(636 origin,637 collection_id: CollectionId,638 token_id: TokenId,639 properties: Vec<Property>640 ) -> DispatchResultWithPostInfo {641 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);642643 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);644645 dispatch_call::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties))646 }647648 #[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]649 #[transactional]650 pub fn delete_token_properties(651 origin,652 collection_id: CollectionId,653 token_id: TokenId,654 property_keys: Vec<PropertyKey>655 ) -> DispatchResultWithPostInfo {656 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);657658 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);659660 dispatch_call::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys))661 }662663 #[weight = T::CommonWeightInfo::set_property_permissions(property_permissions.len() as u32)]664 #[transactional]665 pub fn set_property_permissions(666 origin,667 collection_id: CollectionId,668 property_permissions: Vec<PropertyKeyPermission>,669 ) -> DispatchResultWithPostInfo {670 ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);671672 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);673674 dispatch_call::<T, _>(collection_id, |d| d.set_property_permissions(&sender, property_permissions))675 }676677 #[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]678 #[transactional]679 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {680 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);681 let budget = budget::Value::new(2);682683 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))684 }685686 // TODO! transaction weight687688 /// Set transfers_enabled value for particular collection689 ///690 /// # Permissions691 ///692 /// * Collection Owner.693 ///694 /// # Arguments695 ///696 /// * collection_id: ID of the collection.697 ///698 /// * value: New flag value.699 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]700 #[transactional]701 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {702 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);703 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;704 target_collection.check_is_owner(&sender)?;705706 // =========707708 target_collection.limits.transfers_enabled = Some(value);709 target_collection.save()710 }711712 /// Destroys a concrete instance of NFT.713 ///714 /// # Permissions715 ///716 /// * Collection Owner.717 /// * Collection Admin.718 /// * Current NFT Owner.719 ///720 /// # Arguments721 ///722 /// * collection_id: ID of the collection.723 ///724 /// * item_id: ID of NFT to burn.725 #[weight = T::CommonWeightInfo::burn_item()]726 #[transactional]727 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {728 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);729730 let post_info = dispatch_call::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;731 if value == 1 {732 <NftTransferBasket<T>>::remove(collection_id, item_id);733 <NftApproveBasket<T>>::remove(collection_id, item_id);734 }735 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?736 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());737 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));738 Ok(post_info)739 }740741 /// Destroys a concrete instance of NFT on behalf of the owner742 /// See also: [`approve`]743 ///744 /// # Permissions745 ///746 /// * Collection Owner.747 /// * Collection Admin.748 /// * Current NFT Owner.749 ///750 /// # Arguments751 ///752 /// * collection_id: ID of the collection.753 ///754 /// * item_id: ID of NFT to burn.755 ///756 /// * from: owner of item757 #[weight = T::CommonWeightInfo::burn_from()]758 #[transactional]759 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {760 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);761 let budget = budget::Value::new(2);762763 dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))764 }765766 /// Change ownership of the token.767 ///768 /// # Permissions769 ///770 /// * Collection Owner771 /// * Collection Admin772 /// * Current NFT owner773 ///774 /// # Arguments775 ///776 /// * recipient: Address of token recipient.777 ///778 /// * collection_id.779 ///780 /// * item_id: ID of the item781 /// * Non-Fungible Mode: Required.782 /// * Fungible Mode: Ignored.783 /// * Re-Fungible Mode: Required.784 ///785 /// * value: Amount to transfer.786 /// * Non-Fungible Mode: Ignored787 /// * Fungible Mode: Must specify transferred amount788 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)789 #[weight = T::CommonWeightInfo::transfer()]790 #[transactional]791 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {792 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);793 let budget = budget::Value::new(2);794795 dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))796 }797798 /// Set, change, or remove approved address to transfer the ownership of the NFT.799 ///800 /// # Permissions801 ///802 /// * Collection Owner803 /// * Collection Admin804 /// * Current NFT owner805 ///806 /// # Arguments807 ///808 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).809 ///810 /// * collection_id.811 ///812 /// * item_id: ID of the item.813 #[weight = T::CommonWeightInfo::approve()]814 #[transactional]815 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {816 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);817818 dispatch_call::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))819 }820821 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.822 ///823 /// # Permissions824 /// * Collection Owner825 /// * Collection Admin826 /// * Current NFT owner827 /// * Address approved by current NFT owner828 ///829 /// # Arguments830 ///831 /// * from: Address that owns token.832 ///833 /// * recipient: Address of token recipient.834 ///835 /// * collection_id.836 ///837 /// * item_id: ID of the item.838 ///839 /// * value: Amount to transfer.840 #[weight = T::CommonWeightInfo::transfer_from()]841 #[transactional]842 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {843 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);844 let budget = budget::Value::new(2);845846 dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))847 }848849 #[weight = <SelfWeightOf<T>>::set_collection_limits()]850 #[transactional]851 pub fn set_collection_limits(852 origin,853 collection_id: CollectionId,854 new_limit: CollectionLimits,855 ) -> DispatchResult {856 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);857 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;858 target_collection.check_is_owner(&sender)?;859 let old_limit = &target_collection.limits;860861 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;862863 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(864 collection_id865 ));866867 target_collection.save()868 }869870 #[weight = <SelfWeightOf<T>>::set_collection_limits()]871 #[transactional]872 pub fn set_collection_permissions(873 origin,874 collection_id: CollectionId,875 new_limit: CollectionPermissions,876 ) -> DispatchResult {877 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);878 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;879 target_collection.check_is_owner(&sender)?;880 let old_limit = &target_collection.permissions;881882 target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_limit)?;883884 <Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(885 collection_id886 ));887888 target_collection.save()889 }890 }891}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#![recursion_limit = "1024"]18#![cfg_attr(not(feature = "std"), no_std)]19#![allow(20 clippy::too_many_arguments,21 clippy::unnecessary_mut_passed,22 clippy::unused_unit23)]2425extern crate alloc;2627use frame_support::{28 decl_module, decl_storage, decl_error, decl_event,29 dispatch::DispatchResult,30 ensure,31 weights::{Weight},32 transactional,33 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},34 BoundedVec,35};36use scale_info::TypeInfo;37use frame_system::{self as system, ensure_signed};38use sp_runtime::{sp_std::prelude::Vec};39use up_data_structs::{40 MAX_COLLECTION_NAME_LENGTH,41 MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH, AccessMode, CreateItemData,42 CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId, SponsorshipState,43 CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,44 PropertyKeyPermission,45};46use pallet_evm::account::CrossAccountId;47use pallet_common::{48 CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_call,49 dispatch::CollectionDispatch,50};51pub mod eth;5253#[cfg(feature = "runtime-benchmarks")]54mod benchmarking;55pub mod weights;56use weights::WeightInfo;5758const NESTING_BUDGET: u32 = 5;5960decl_error! {61 /// Error for non-fungible-token module.62 pub enum Error for Module<T: Config> {63 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.64 CollectionDecimalPointLimitExceeded,65 /// This address is not set as sponsor, use setCollectionSponsor first.66 ConfirmUnsetSponsorFail,67 /// Length of items properties must be greater than 0.68 EmptyArgument,69 }70}7172pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {73 type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;7475 /// Weight information for extrinsics in this pallet.76 type WeightInfo: WeightInfo;77 type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;78}7980decl_event! {81 pub enum Event<T>82 where83 <T as frame_system::Config>::AccountId,84 <T as pallet_evm::account::Config>::CrossAccountId,85 {86 /// Collection sponsor was removed87 ///88 /// # Arguments89 ///90 /// * collection_id: Globally unique collection identifier.91 CollectionSponsorRemoved(CollectionId),9293 /// Collection admin was added94 ///95 /// # Arguments96 ///97 /// * collection_id: Globally unique collection identifier.98 ///99 /// * admin: Admin address.100 CollectionAdminAdded(CollectionId, CrossAccountId),101102 /// Collection owned was change103 ///104 /// # Arguments105 ///106 /// * collection_id: Globally unique collection identifier.107 ///108 /// * owner: New owner address.109 CollectionOwnedChanged(CollectionId, AccountId),110111 /// Collection sponsor was set112 ///113 /// # Arguments114 ///115 /// * collection_id: Globally unique collection identifier.116 ///117 /// * owner: New sponsor address.118 CollectionSponsorSet(CollectionId, AccountId),119120 /// New sponsor was confirm121 ///122 /// # Arguments123 ///124 /// * collection_id: Globally unique collection identifier.125 ///126 /// * sponsor: New sponsor address.127 SponsorshipConfirmed(CollectionId, AccountId),128129 /// Collection admin was removed130 ///131 /// # Arguments132 ///133 /// * collection_id: Globally unique collection identifier.134 ///135 /// * admin: Admin address.136 CollectionAdminRemoved(CollectionId, CrossAccountId),137138 /// Address was remove from allow list139 ///140 /// # Arguments141 ///142 /// * collection_id: Globally unique collection identifier.143 ///144 /// * user: Address.145 AllowListAddressRemoved(CollectionId, CrossAccountId),146147 /// Address was add to allow list148 ///149 /// # Arguments150 ///151 /// * collection_id: Globally unique collection identifier.152 ///153 /// * user: Address.154 AllowListAddressAdded(CollectionId, CrossAccountId),155156 /// Collection limits was set157 ///158 /// # Arguments159 ///160 /// * collection_id: Globally unique collection identifier.161 CollectionLimitSet(CollectionId),162163 CollectionPermissionSet(CollectionId),164 }165}166167type SelfWeightOf<T> = <T as Config>::WeightInfo;168169// # Used definitions170//171// ## User control levels172//173// chain-controlled - key is uncontrolled by user174// i.e autoincrementing index175// can use non-cryptographic hash176// real - key is controlled by user177// but it is hard to generate enough colliding values, i.e owner of signed txs178// can use non-cryptographic hash179// controlled - key is completly controlled by users180// i.e maps with mutable keys181// should use cryptographic hash182//183// ## User control level downgrade reasons184//185// ?1 - chain-controlled -> controlled186// collections/tokens can be destroyed, resulting in massive holes187// ?2 - chain-controlled -> controlled188// same as ?1, but can be only added, resulting in easier exploitation189// ?3 - real -> controlled190// no confirmation required, so addresses can be easily generated191decl_storage! {192 trait Store for Module<T: Config> as Unique {193194 //#region Private members195 /// Used for migrations196 ChainVersion: u64;197 //#endregion198199 //#region Tokens transfer rate limit baskets200 /// (Collection id (controlled?2), who created (real))201 /// TODO: Off chain worker should remove from this map when collection gets removed202 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;203 /// Collection id (controlled?2), token id (controlled?2)204 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;205 /// Collection id (controlled?2), owning user (real)206 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;207 /// Collection id (controlled?2), token id (controlled?2)208 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>;209 //#endregion210211 /// Variable metadata sponsoring212 /// Collection id (controlled?2), token id (controlled?2)213 #[deprecated]214 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;215 pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;216217 /// Approval sponsoring218 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;219 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;220 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>;221 }222}223224decl_module! {225 pub struct Module<T: Config> for enum Call226 where227 origin: T::Origin228 {229 type Error = Error<T>;230231 fn deposit_event() = default;232233 fn on_initialize(_now: T::BlockNumber) -> Weight {234 0235 }236237 fn on_runtime_upgrade() -> Weight {238 let limit = None;239240 <VariableMetaDataBasket<T>>::remove_all(limit);241242 0243 }244245 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner of the collection is set to the address that signed the transaction and can be changed later.246 ///247 /// # Permissions248 ///249 /// * Anyone.250 ///251 /// # Arguments252 ///253 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.254 ///255 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.256 ///257 /// * token_prefix: UTF-8 string with token prefix.258 ///259 /// * mode: [CollectionMode] collection type and type dependent data.260 // returns collection ID261 #[weight = <SelfWeightOf<T>>::create_collection()]262 #[transactional]263 #[deprecated]264 pub fn create_collection(origin,265 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,266 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,267 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,268 mode: CollectionMode) -> DispatchResult {269 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {270 name: collection_name,271 description: collection_description,272 token_prefix,273 mode,274 ..Default::default()275 };276 Self::create_collection_ex(origin, data)277 }278279 /// This method creates a collection280 ///281 /// Prefer it to deprecated [`created_collection`] method282 #[weight = <SelfWeightOf<T>>::create_collection()]283 #[transactional]284 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {285 let sender = ensure_signed(origin)?;286287 // =========288289 T::CollectionDispatch::create(sender, data)?;290291 Ok(())292 }293294 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.295 ///296 /// # Permissions297 ///298 /// * Collection Owner.299 ///300 /// # Arguments301 ///302 /// * collection_id: collection to destroy.303 #[weight = <SelfWeightOf<T>>::destroy_collection()]304 #[transactional]305 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {306 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);307 let collection = <CollectionHandle<T>>::try_get(collection_id)?;308309 // =========310311 T::CollectionDispatch::destroy(sender, collection)?;312313 <NftTransferBasket<T>>::remove_prefix(collection_id, None);314 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);315 <ReFungibleTransferBasket<T>>::remove_prefix((collection_id,), None);316317 <NftApproveBasket<T>>::remove_prefix(collection_id, None);318 <FungibleApproveBasket<T>>::remove_prefix(collection_id, None);319 <RefungibleApproveBasket<T>>::remove_prefix((collection_id,), None);320321 Ok(())322 }323324 /// Add an address to allow list.325 ///326 /// # Permissions327 ///328 /// * Collection Owner329 /// * Collection Admin330 ///331 /// # Arguments332 ///333 /// * collection_id.334 ///335 /// * address.336 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]337 #[transactional]338 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{339340 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);341 let collection = <CollectionHandle<T>>::try_get(collection_id)?;342343 <PalletCommon<T>>::toggle_allowlist(344 &collection,345 &sender,346 &address,347 true,348 )?;349350 Self::deposit_event(Event::<T>::AllowListAddressAdded(351 collection_id,352 address353 ));354355 Ok(())356 }357358 /// Remove an address from allow list.359 ///360 /// # Permissions361 ///362 /// * Collection Owner363 /// * Collection Admin364 ///365 /// # Arguments366 ///367 /// * collection_id.368 ///369 /// * address.370 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]371 #[transactional]372 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{373374 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);375 let collection = <CollectionHandle<T>>::try_get(collection_id)?;376377 <PalletCommon<T>>::toggle_allowlist(378 &collection,379 &sender,380 &address,381 false,382 )?;383384 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(385 collection_id,386 address387 ));388389 Ok(())390 }391392 /// Change the owner of the collection.393 ///394 /// # Permissions395 ///396 /// * Collection Owner.397 ///398 /// # Arguments399 ///400 /// * collection_id.401 ///402 /// * new_owner.403 #[weight = <SelfWeightOf<T>>::change_collection_owner()]404 #[transactional]405 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {406407 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);408409 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;410 target_collection.check_is_owner(&sender)?;411412 target_collection.owner = new_owner.clone();413 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(414 collection_id,415 new_owner416 ));417418 target_collection.save()419 }420421 /// Adds an admin of the Collection.422 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.423 ///424 /// # Permissions425 ///426 /// * Collection Owner.427 /// * Collection Admin.428 ///429 /// # Arguments430 ///431 /// * collection_id: ID of the Collection to add admin for.432 ///433 /// * new_admin_id: Address of new admin to add.434 #[weight = <SelfWeightOf<T>>::add_collection_admin()]435 #[transactional]436 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {437 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);438 let collection = <CollectionHandle<T>>::try_get(collection_id)?;439440 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(441 collection_id,442 new_admin_id.clone()443 ));444445 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)446 }447448 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.449 ///450 /// # Permissions451 ///452 /// * Collection Owner.453 /// * Collection Admin.454 ///455 /// # Arguments456 ///457 /// * collection_id: ID of the Collection to remove admin for.458 ///459 /// * account_id: Address of admin to remove.460 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]461 #[transactional]462 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {463 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);464 let collection = <CollectionHandle<T>>::try_get(collection_id)?;465466 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(467 collection_id,468 account_id.clone()469 ));470471 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)472 }473474 /// # Permissions475 ///476 /// * Collection Owner477 ///478 /// # Arguments479 ///480 /// * collection_id.481 ///482 /// * new_sponsor.483 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]484 #[transactional]485 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {486 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);487488 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;489 target_collection.check_is_owner(&sender)?;490491 target_collection.set_sponsor(new_sponsor.clone());492493 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(494 collection_id,495 new_sponsor496 ));497498 target_collection.save()499 }500501 /// # Permissions502 ///503 /// * Sponsor.504 ///505 /// # Arguments506 ///507 /// * collection_id.508 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]509 #[transactional]510 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {511 let sender = ensure_signed(origin)?;512513 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;514 ensure!(515 target_collection.confirm_sponsorship(&sender),516 Error::<T>::ConfirmUnsetSponsorFail517 );518519 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(520 collection_id,521 sender522 ));523524 target_collection.save()525 }526527 /// Switch back to pay-per-own-transaction model.528 ///529 /// # Permissions530 ///531 /// * Collection owner.532 ///533 /// # Arguments534 ///535 /// * collection_id.536 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]537 #[transactional]538 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {539 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);540541 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;542 target_collection.check_is_owner(&sender)?;543544 target_collection.sponsorship = SponsorshipState::Disabled;545546 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(547 collection_id548 ));549 target_collection.save()550 }551552 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.553 ///554 /// # Permissions555 ///556 /// * Collection Owner.557 /// * Collection Admin.558 /// * Anyone if559 /// * Allow List is enabled, and560 /// * Address is added to allow list, and561 /// * MintPermission is enabled (see SetMintPermission method)562 ///563 /// # Arguments564 ///565 /// * collection_id: ID of the collection.566 ///567 /// * owner: Address, initial owner of the NFT.568 ///569 /// * data: Token data to store on chain.570 #[weight = T::CommonWeightInfo::create_item()]571 #[transactional]572 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {573 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);574 let budget = budget::Value::new(NESTING_BUDGET);575576 dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))577 }578579 /// This method creates multiple items in a collection created with CreateCollection method.580 ///581 /// # Permissions582 ///583 /// * Collection Owner.584 /// * Collection Admin.585 /// * Anyone if586 /// * Allow List is enabled, and587 /// * Address is added to allow list, and588 /// * MintPermission is enabled (see SetMintPermission method)589 ///590 /// # Arguments591 ///592 /// * collection_id: ID of the collection.593 ///594 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].595 ///596 /// * owner: Address, initial owner of the NFT.597 #[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]598 #[transactional]599 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {600 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);601 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);602 let budget = budget::Value::new(NESTING_BUDGET);603604 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))605 }606607 #[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]608 #[transactional]609 pub fn set_collection_properties(610 origin,611 collection_id: CollectionId,612 properties: Vec<Property>613 ) -> DispatchResultWithPostInfo {614 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);615616 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);617618 dispatch_call::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))619 }620621 #[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]622 #[transactional]623 pub fn delete_collection_properties(624 origin,625 collection_id: CollectionId,626 property_keys: Vec<PropertyKey>,627 ) -> DispatchResultWithPostInfo {628 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);629630 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);631632 dispatch_call::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))633 }634635 #[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]636 #[transactional]637 pub fn set_token_properties(638 origin,639 collection_id: CollectionId,640 token_id: TokenId,641 properties: Vec<Property>642 ) -> DispatchResultWithPostInfo {643 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);644645 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);646647 dispatch_call::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties))648 }649650 #[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]651 #[transactional]652 pub fn delete_token_properties(653 origin,654 collection_id: CollectionId,655 token_id: TokenId,656 property_keys: Vec<PropertyKey>657 ) -> DispatchResultWithPostInfo {658 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);659660 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);661662 dispatch_call::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys))663 }664665 #[weight = T::CommonWeightInfo::set_property_permissions(property_permissions.len() as u32)]666 #[transactional]667 pub fn set_property_permissions(668 origin,669 collection_id: CollectionId,670 property_permissions: Vec<PropertyKeyPermission>,671 ) -> DispatchResultWithPostInfo {672 ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);673674 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);675676 dispatch_call::<T, _>(collection_id, |d| d.set_property_permissions(&sender, property_permissions))677 }678679 #[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]680 #[transactional]681 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {682 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);683 let budget = budget::Value::new(NESTING_BUDGET);684685 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))686 }687688 // TODO! transaction weight689690 /// Set transfers_enabled value for particular collection691 ///692 /// # Permissions693 ///694 /// * Collection Owner.695 ///696 /// # Arguments697 ///698 /// * collection_id: ID of the collection.699 ///700 /// * value: New flag value.701 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]702 #[transactional]703 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {704 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);705 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;706 target_collection.check_is_owner(&sender)?;707708 // =========709710 target_collection.limits.transfers_enabled = Some(value);711 target_collection.save()712 }713714 /// Destroys a concrete instance of NFT.715 ///716 /// # Permissions717 ///718 /// * Collection Owner.719 /// * Collection Admin.720 /// * Current NFT Owner.721 ///722 /// # Arguments723 ///724 /// * collection_id: ID of the collection.725 ///726 /// * item_id: ID of NFT to burn.727 #[weight = T::CommonWeightInfo::burn_item()]728 #[transactional]729 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {730 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);731732 let post_info = dispatch_call::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;733 if value == 1 {734 <NftTransferBasket<T>>::remove(collection_id, item_id);735 <NftApproveBasket<T>>::remove(collection_id, item_id);736 }737 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?738 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());739 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));740 Ok(post_info)741 }742743 /// Destroys a concrete instance of NFT on behalf of the owner744 /// See also: [`approve`]745 ///746 /// # Permissions747 ///748 /// * Collection Owner.749 /// * Collection Admin.750 /// * Current NFT Owner.751 ///752 /// # Arguments753 ///754 /// * collection_id: ID of the collection.755 ///756 /// * item_id: ID of NFT to burn.757 ///758 /// * from: owner of item759 #[weight = T::CommonWeightInfo::burn_from()]760 #[transactional]761 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {762 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);763 let budget = budget::Value::new(NESTING_BUDGET);764765 dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))766 }767768 /// Change ownership of the token.769 ///770 /// # Permissions771 ///772 /// * Collection Owner773 /// * Collection Admin774 /// * Current NFT owner775 ///776 /// # Arguments777 ///778 /// * recipient: Address of token recipient.779 ///780 /// * collection_id.781 ///782 /// * item_id: ID of the item783 /// * Non-Fungible Mode: Required.784 /// * Fungible Mode: Ignored.785 /// * Re-Fungible Mode: Required.786 ///787 /// * value: Amount to transfer.788 /// * Non-Fungible Mode: Ignored789 /// * Fungible Mode: Must specify transferred amount790 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)791 #[weight = T::CommonWeightInfo::transfer()]792 #[transactional]793 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {794 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);795 let budget = budget::Value::new(NESTING_BUDGET);796797 dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))798 }799800 /// Set, change, or remove approved address to transfer the ownership of the NFT.801 ///802 /// # Permissions803 ///804 /// * Collection Owner805 /// * Collection Admin806 /// * Current NFT owner807 ///808 /// # Arguments809 ///810 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).811 ///812 /// * collection_id.813 ///814 /// * item_id: ID of the item.815 #[weight = T::CommonWeightInfo::approve()]816 #[transactional]817 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {818 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);819820 dispatch_call::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))821 }822823 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.824 ///825 /// # Permissions826 /// * Collection Owner827 /// * Collection Admin828 /// * Current NFT owner829 /// * Address approved by current NFT owner830 ///831 /// # Arguments832 ///833 /// * from: Address that owns token.834 ///835 /// * recipient: Address of token recipient.836 ///837 /// * collection_id.838 ///839 /// * item_id: ID of the item.840 ///841 /// * value: Amount to transfer.842 #[weight = T::CommonWeightInfo::transfer_from()]843 #[transactional]844 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {845 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);846 let budget = budget::Value::new(NESTING_BUDGET);847848 dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))849 }850851 #[weight = <SelfWeightOf<T>>::set_collection_limits()]852 #[transactional]853 pub fn set_collection_limits(854 origin,855 collection_id: CollectionId,856 new_limit: CollectionLimits,857 ) -> DispatchResult {858 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);859 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;860 target_collection.check_is_owner(&sender)?;861 let old_limit = &target_collection.limits;862863 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;864865 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(866 collection_id867 ));868869 target_collection.save()870 }871872 #[weight = <SelfWeightOf<T>>::set_collection_limits()]873 #[transactional]874 pub fn set_collection_permissions(875 origin,876 collection_id: CollectionId,877 new_limit: CollectionPermissions,878 ) -> DispatchResult {879 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);880 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;881 target_collection.check_is_owner(&sender)?;882 let old_limit = &target_collection.permissions;883884 target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_limit)?;885886 <Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(887 collection_id888 ));889890 target_collection.save()891 }892 }893}runtime/common/src/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -25,7 +25,7 @@
dispatch_unique_runtime!(collection.token_owner(token))
}
fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
- let budget = up_data_structs::budget::Value::new(5);
+ let budget = up_data_structs::budget::Value::new(10);
Ok(Some(<pallet_structure::Pallet<Runtime>>::find_topmost_owner(collection, token, &budget)?))
}
@@ -142,7 +142,7 @@
}
fn collection_by_id(collection_id: RmrkCollectionId) -> Result<Option<RmrkCollectionInfo<AccountId>>, DispatchError> {
- use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode}};
+ use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind}};
let collection_id = CollectionId(collection_id);
let collection = match RmrkCore::get_typed_nft_collection(collection_id, CollectionType::Regular) {
@@ -156,7 +156,7 @@
issuer: collection.owner.clone(),
metadata: RmrkCore::get_collection_property(collection_id, RmrkProperty::Metadata)?.decode_or_default(),
max: collection.limits.token_limit,
- symbol: collection.token_prefix.decode_or_default(),
+ symbol: collection.token_prefix.rebind(),
nfts_count
}))
}
@@ -204,22 +204,21 @@
}
fn nft_children(collection_id: RmrkCollectionId, nft_id: RmrkNftId) -> Result<Vec<RmrkNftChild>, DispatchError> {
- use up_data_structs::mapping::TokenAddressMapping;
-
let collection_id = CollectionId(collection_id);
let nft_id = TokenId(nft_id);
if !RmrkCore::nft_exists(collection_id, nft_id) { return Ok(Vec::new()); }
- let cross_account_id = CrossAccountId::from_eth(
- EvmTokenAddressMapping::token_to_address(collection_id, nft_id)
- );
-
Ok(
- pallet_nonfungible::Owned::<Runtime>::iter_prefix((collection_id, cross_account_id))
- .map(|(child_id, _)| RmrkNftChild {
- collection_id: collection_id.0, // todo make sure they're always from this collection // spoiler: they're not
- nft_id: child_id.0,
- }).collect()
+ pallet_nonfungible::TokenChildren::<Runtime>::iter_prefix((collection_id, nft_id))
+ .filter_map(|(child_id, is_child)|
+ match is_child {
+ true => Some(RmrkNftChild {
+ collection_id: child_id.0.0,
+ nft_id: child_id.1.0,
+ }),
+ false => None,
+ }
+ ).collect()
)
}
@@ -332,7 +331,7 @@
fn base(base_id: RmrkBaseId) -> Result<Option<RmrkBaseInfo<AccountId>>, DispatchError> {
use pallet_proxy_rmrk_core::{
- RmrkProperty, misc::{CollectionType, RmrkDecode},
+ RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind},
};
let collection_id = CollectionId(base_id);
@@ -344,7 +343,7 @@
Ok(Some(RmrkBaseInfo {
issuer: collection.owner.clone(),
base_type: RmrkCore::get_collection_property(collection_id, RmrkProperty::BaseType)?.decode_or_default(),
- symbol: collection.token_prefix.decode_or_default(),
+ symbol: collection.token_prefix.rebind(),
}))
}
tests/src/nesting/nest.test.tsdiffbeforeafterboth--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -41,7 +41,7 @@
// Create a token to be nested
const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-
+
// Nest
await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
@@ -111,8 +111,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
))).to.not.be.rejected;
@@ -134,8 +134,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
))).to.not.be.rejected;
@@ -158,8 +158,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
))).to.not.be.rejected;
@@ -181,7 +181,7 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
+ collectionRFT,
targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
))).to.not.be.rejected;
@@ -207,17 +207,29 @@
await setCollectionPermissionsExceptSuccess(alice, collection, {nesting: 'Owner'});
const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+ const maxNestingLevel = 5;
+ let prevToken = targetToken;
+
// Create a nested-token matryoshka
- const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
- const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
- // The nesting depth is limited by 2
+ for (let i = 0; i < maxNestingLevel; i++) {
+ const nestedToken = await createItemExpectSuccess(
+ alice,
+ collection,
+ 'NFT',
+ {Ethereum: tokenIdToAddress(collection, prevToken)},
+ );
+
+ prevToken = nestedToken;
+ }
+
+ // The nesting depth is limited by `maxNestingLevel`
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, nestedToken2)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, prevToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/);
- expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
+ expect(await getTopmostTokenOwner(api, collection, prevToken)).to.be.deep.equal({Substrate: alice.address});
});
});
@@ -231,8 +243,8 @@
// Try to create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
@@ -259,8 +271,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -285,8 +297,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -307,8 +319,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
{nft: {const_data: [], variable_data: []}} as any,
)), 'while creating nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
@@ -332,11 +344,11 @@
// Try to create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
-
+
// Create a token to be nested
const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
// Try to nest
@@ -366,8 +378,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -393,8 +405,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -417,8 +429,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
+ collectionFT,
+ targetAddress,
{Fungible: {Value: 10}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
@@ -441,8 +453,8 @@
// Create a nested token
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
@@ -477,8 +489,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -504,8 +516,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
@@ -528,8 +540,8 @@
// Try to create a nested token in the wrong collection
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionRFT,
- targetAddress,
+ collectionRFT,
+ targetAddress,
{ReFungible: {const_data: [], pieces: 100}},
)), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);