difftreelog
CORE-386 Fix create item with public minting
in: master
4 files changed
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -223,7 +223,7 @@
let weight = <CommonWeights<T>>::set_token_properties(properties.len() as u32);
with_weight(
- <Pallet<T>>::set_token_properties(self, &sender, token_id, properties),
+ <Pallet<T>>::set_token_properties(self, &sender, token_id, properties, false),
weight,
)
}
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -82,7 +82,7 @@
.map_err(|_| "key too long")?;
let value = value.try_into().map_err(|_| "value too long")?;
- <Pallet<T>>::set_token_property(self, &caller, TokenId(token_id), Property { key, value })
+ <Pallet<T>>::set_token_property(self, &caller, TokenId(token_id), Property { key, value }, false)
.map_err(dispatch_to_evm::<T>)
}
pallets/nonfungible/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use evm_coder::ToLog;21use frame_support::{22 BoundedVec, ensure, fail, transactional,23 storage::with_transaction,24 pallet_prelude::DispatchResultWithPostInfo,25 pallet_prelude::Weight,26 weights::{PostDispatchInfo, Pays},27};28use up_data_structs::{29 AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,30 mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,31 PropertyKey, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild,32};33use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};34use pallet_common::{35 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,36 eth::collection_id_to_address,37};38use pallet_structure::{Pallet as PalletStructure, Error as StructureError};39use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};40use sp_core::H160;41use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};42use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap, collections::btree_set::BTreeSet};43use core::ops::Deref;44use codec::{Encode, Decode, MaxEncodedLen};45use scale_info::TypeInfo;4647pub use pallet::*;48use weights::WeightInfo;49#[cfg(feature = "runtime-benchmarks")]50pub mod benchmarking;51pub mod common;52pub mod erc;53pub mod weights;5455pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;56pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5758#[struct_versioning::versioned(version = 2, upper)]59#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]60pub struct ItemData<CrossAccountId> {61 #[version(..2)]62 pub const_data: BoundedVec<u8, CustomDataLimit>,6364 #[version(..2)]65 pub variable_data: BoundedVec<u8, CustomDataLimit>,6667 pub owner: CrossAccountId,68}6970#[frame_support::pallet]71pub mod pallet {72 use super::*;73 use frame_support::{74 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,75 };76 use frame_system::pallet_prelude::*;77 use up_data_structs::{CollectionId, TokenId};78 use super::weights::WeightInfo;7980 #[pallet::error]81 pub enum Error<T> {82 /// Not Nonfungible item data used to mint in Nonfungible collection.83 NotNonfungibleDataUsedToMintFungibleCollectionToken,84 /// Used amount > 1 with NFT85 NonfungibleItemsHaveNoAmount,86 /// Unable to burn NFT with children87 CantBurnNftWithChildren,88 }8990 #[pallet::config]91 pub trait Config:92 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config93 {94 type WeightInfo: WeightInfo;95 }9697 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);9899 #[pallet::pallet]100 #[pallet::storage_version(STORAGE_VERSION)]101 #[pallet::generate_store(pub(super) trait Store)]102 pub struct Pallet<T>(_);103104 #[pallet::storage]105 pub type TokensMinted<T: Config> =106 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;107 #[pallet::storage]108 pub type TokensBurnt<T: Config> =109 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;110111 #[pallet::storage]112 pub type TokenData<T: Config> = StorageNMap<113 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),114 Value = ItemData<T::CrossAccountId>,115 QueryKind = OptionQuery,116 >;117118 #[pallet::storage]119 #[pallet::getter(fn token_properties)]120 pub type TokenProperties<T: Config> = StorageNMap<121 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),122 Value = Properties,123 QueryKind = ValueQuery,124 OnEmpty = up_data_structs::TokenProperties,125 >;126127 /// Used to enumerate tokens owned by account128 #[pallet::storage]129 pub type Owned<T: Config> = StorageNMap<130 Key = (131 Key<Twox64Concat, CollectionId>,132 Key<Blake2_128Concat, T::CrossAccountId>,133 Key<Twox64Concat, TokenId>,134 ),135 Value = bool,136 QueryKind = ValueQuery,137 >;138139 /// Used to enumerate token's children140 #[pallet::storage]141 #[pallet::getter(fn token_children)]142 pub type TokenChildren<T: Config> = StorageNMap<143 Key = (144 Key<Twox64Concat, CollectionId>,145 Key<Twox64Concat, TokenId>,146 Key<Twox64Concat, (CollectionId, TokenId)>,147 ),148 Value = bool,149 QueryKind = ValueQuery,150 >;151152 #[pallet::storage]153 pub type AccountBalance<T: Config> = StorageNMap<154 Key = (155 Key<Twox64Concat, CollectionId>,156 Key<Blake2_128Concat, T::CrossAccountId>,157 ),158 Value = u32,159 QueryKind = ValueQuery,160 >;161162 #[pallet::storage]163 pub type Allowance<T: Config> = StorageNMap<164 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),165 Value = T::CrossAccountId,166 QueryKind = OptionQuery,167 >;168169 #[pallet::hooks]170 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {171 fn on_runtime_upgrade() -> Weight {172 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {173 let mut had_consts = BTreeSet::new();174 <TokenData<T>>::translate::<ItemDataVersion1<T::CrossAccountId>, _>(175 |(collection, token), v| {176 let mut props = vec![];177 if !v.const_data.is_empty() {178 props.push(Property {179 key: b"_old_constData".to_vec().try_into().unwrap(),180 value: v181 .const_data182 .clone()183 .into_inner()184 .try_into()185 .expect("const too long"),186 });187 had_consts.insert(collection);188 }189 if !v.variable_data.is_empty() {190 props.push(Property {191 key: b"_old_variableData".to_vec().try_into().unwrap(),192 value: v193 .variable_data194 .clone()195 .into_inner()196 .try_into()197 .expect("variable too long"),198 })199 }200 if !props.is_empty() {201 Self::set_scoped_token_properties(202 collection,203 token,204 PropertyScope::None,205 props.into_iter(),206 )207 .expect("existing token data exceeds property storage");208 }209 Some(<ItemDataVersion2<T::CrossAccountId>>::from(v))210 },211 );212 for collection in had_consts {213 <PalletCommon<T>>::set_property_permission_unchecked(214 collection,215 PropertyKeyPermission {216 key: b"_old_constData".to_vec().try_into().unwrap(),217 permission: PropertyPermission {218 mutable: false,219 collection_admin: true,220 token_owner: false,221 },222 },223 )224 .expect("failed to configure permission");225 }226 }227228 0229 }230 }231}232233pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);234impl<T: Config> NonfungibleHandle<T> {235 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {236 Self(inner)237 }238 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {239 self.0240 }241 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {242 &mut self.0243 }244}245impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {246 fn recorder(&self) -> &SubstrateRecorder<T> {247 self.0.recorder()248 }249 fn into_recorder(self) -> SubstrateRecorder<T> {250 self.0.into_recorder()251 }252}253impl<T: Config> Deref for NonfungibleHandle<T> {254 type Target = pallet_common::CollectionHandle<T>;255256 fn deref(&self) -> &Self::Target {257 &self.0258 }259}260261impl<T: Config> Pallet<T> {262 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {263 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)264 }265 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {266 <TokenData<T>>::contains_key((collection.id, token))267 }268269 pub fn set_scoped_token_property(270 collection_id: CollectionId,271 token_id: TokenId,272 scope: PropertyScope,273 property: Property,274 ) -> DispatchResult {275 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {276 properties.try_scoped_set(scope, property.key, property.value)277 })278 .map_err(<CommonError<T>>::from)?;279280 Ok(())281 }282283 pub fn set_scoped_token_properties(284 collection_id: CollectionId,285 token_id: TokenId,286 scope: PropertyScope,287 properties: impl Iterator<Item = Property>,288 ) -> DispatchResult {289 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {290 stored_properties.try_scoped_set_from_iter(scope, properties)291 })292 .map_err(<CommonError<T>>::from)?;293294 Ok(())295 }296297 pub fn current_token_id(collection_id: CollectionId) -> TokenId {298 TokenId(<TokensMinted<T>>::get(collection_id))299 }300}301302// unchecked calls skips any permission checks303impl<T: Config> Pallet<T> {304 pub fn init_collection(305 owner: T::CrossAccountId,306 data: CreateCollectionData<T::AccountId>,307 ) -> Result<CollectionId, DispatchError> {308 <PalletCommon<T>>::init_collection(owner, data)309 }310 pub fn destroy_collection(311 collection: NonfungibleHandle<T>,312 sender: &T::CrossAccountId,313 ) -> DispatchResult {314 let id = collection.id;315316 if Self::collection_has_tokens(id) {317 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());318 }319320 // =========321322 PalletCommon::destroy_collection(collection.0, sender)?;323324 <TokenData<T>>::remove_prefix((id,), None);325 <TokenChildren<T>>::remove_prefix((id,), None);326 <Owned<T>>::remove_prefix((id,), None);327 <TokensMinted<T>>::remove(id);328 <TokensBurnt<T>>::remove(id);329 <Allowance<T>>::remove_prefix((id,), None);330 <AccountBalance<T>>::remove_prefix((id,), None);331 Ok(())332 }333334 pub fn burn(335 collection: &NonfungibleHandle<T>,336 sender: &T::CrossAccountId,337 token: TokenId,338 ) -> DispatchResult {339 let token_data =340 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;341 ensure!(342 &token_data.owner == sender343 || (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),344 <CommonError<T>>::NoPermission345 );346347 if collection.permissions.access() == AccessMode::AllowList {348 collection.check_allowlist(sender)?;349 }350351 if Self::token_has_children(collection.id, token) {352 return Err(<Error<T>>::CantBurnNftWithChildren.into());353 }354355 let burnt = <TokensBurnt<T>>::get(collection.id)356 .checked_add(1)357 .ok_or(ArithmeticError::Overflow)?;358359 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))360 .checked_sub(1)361 .ok_or(ArithmeticError::Overflow)?;362363 // =========364365 if balance == 0 {366 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));367 } else {368 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);369 }370371 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);372373 <Owned<T>>::remove((collection.id, &token_data.owner, token));374 <TokensBurnt<T>>::insert(collection.id, burnt);375 <TokenData<T>>::remove((collection.id, token));376 <TokenProperties<T>>::remove((collection.id, token));377 let old_spender = <Allowance<T>>::take((collection.id, token));378379 if let Some(old_spender) = old_spender {380 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(381 collection.id,382 token,383 token_data.owner.clone(),384 old_spender,385 0,386 ));387 }388389 <PalletEvm<T>>::deposit_log(390 ERC721Events::Transfer {391 from: *token_data.owner.as_eth(),392 to: H160::default(),393 token_id: token.into(),394 }395 .to_log(collection_id_to_address(collection.id)),396 );397 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(398 collection.id,399 token,400 token_data.owner,401 1,402 ));403 Ok(())404 }405406 #[transactional]407 pub fn burn_recursively(408 collection: &NonfungibleHandle<T>,409 sender: &T::CrossAccountId,410 token: TokenId,411 self_budget: &dyn Budget,412 breadth_budget: &dyn Budget,413 ) -> DispatchResultWithPostInfo {414 ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);415416 let current_token_account =417 T::CrossTokenAddressMapping::token_to_address(collection.id, token);418419 let mut weight = 0 as Weight;420421 // This method is transactional, if user in fact doesn't have permissions to remove token -422 // tokens removed here will be restored after rejected transaction423 for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {424 ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);425 let PostDispatchInfo { actual_weight, .. } =426 <PalletStructure<T>>::burn_item_recursively(427 current_token_account.clone(),428 collection,429 token,430 self_budget,431 breadth_budget,432 )?;433 if let Some(actual_weight) = actual_weight {434 weight = weight.saturating_add(actual_weight);435 }436 }437438 Self::burn(collection, sender, token)?;439 DispatchResultWithPostInfo::Ok(PostDispatchInfo {440 actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),441 pays_fee: Pays::Yes,442 })443 }444445 pub fn set_token_property(446 collection: &NonfungibleHandle<T>,447 sender: &T::CrossAccountId,448 token_id: TokenId,449 property: Property,450 ) -> DispatchResult {451 Self::check_token_change_permission(collection, sender, token_id, &property.key)?;452453 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {454 let property = property.clone();455 properties.try_set(property.key, property.value)456 })457 .map_err(<CommonError<T>>::from)?;458459 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(460 collection.id,461 token_id,462 property.key,463 ));464465 Ok(())466 }467468 #[transactional]469 pub fn set_token_properties(470 collection: &NonfungibleHandle<T>,471 sender: &T::CrossAccountId,472 token_id: TokenId,473 properties: Vec<Property>,474 ) -> DispatchResult {475 for property in properties {476 Self::set_token_property(collection, sender, token_id, property)?;477 }478479 Ok(())480 }481482 pub fn delete_token_property(483 collection: &NonfungibleHandle<T>,484 sender: &T::CrossAccountId,485 token_id: TokenId,486 property_key: PropertyKey,487 ) -> DispatchResult {488 Self::check_token_change_permission(collection, sender, token_id, &property_key)?;489490 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {491 properties.remove(&property_key)492 })493 .map_err(<CommonError<T>>::from)?;494495 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(496 collection.id,497 token_id,498 property_key,499 ));500501 Ok(())502 }503504 fn check_token_change_permission(505 collection: &NonfungibleHandle<T>,506 sender: &T::CrossAccountId,507 token_id: TokenId,508 property_key: &PropertyKey,509 ) -> DispatchResult {510 let permission = <PalletCommon<T>>::property_permissions(collection.id)511 .get(property_key)512 .cloned()513 .unwrap_or_else(PropertyPermission::none);514515 let token_data = <TokenData<T>>::get((collection.id, token_id))516 .ok_or(<CommonError<T>>::TokenNotFound)?;517518 let check_token_owner = || -> DispatchResult {519 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);520 Ok(())521 };522523 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))524 .get(property_key)525 .is_some();526527 match permission {528 PropertyPermission { mutable: false, .. } if is_property_exists => {529 Err(<CommonError<T>>::NoPermission.into())530 }531532 PropertyPermission {533 collection_admin,534 token_owner,535 ..536 } => {537 let mut check_result = Err(<CommonError<T>>::NoPermission.into());538539 if collection_admin {540 check_result = collection.check_is_owner_or_admin(sender);541 }542543 if token_owner {544 check_result.or_else(|_| check_token_owner())545 } else {546 check_result547 }548 }549 }550 }551552 #[transactional]553 pub fn delete_token_properties(554 collection: &NonfungibleHandle<T>,555 sender: &T::CrossAccountId,556 token_id: TokenId,557 property_keys: Vec<PropertyKey>,558 ) -> DispatchResult {559 for key in property_keys {560 Self::delete_token_property(collection, sender, token_id, key)?;561 }562563 Ok(())564 }565566 pub fn set_collection_properties(567 collection: &NonfungibleHandle<T>,568 sender: &T::CrossAccountId,569 properties: Vec<Property>,570 ) -> DispatchResult {571 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)572 }573574 pub fn delete_collection_properties(575 collection: &CollectionHandle<T>,576 sender: &T::CrossAccountId,577 property_keys: Vec<PropertyKey>,578 ) -> DispatchResult {579 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)580 }581582 pub fn set_property_permissions(583 collection: &CollectionHandle<T>,584 sender: &T::CrossAccountId,585 property_permissions: Vec<PropertyKeyPermission>,586 ) -> DispatchResult {587 <PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)588 }589590 pub fn set_property_permission(591 collection: &CollectionHandle<T>,592 sender: &T::CrossAccountId,593 permission: PropertyKeyPermission,594 ) -> DispatchResult {595 <PalletCommon<T>>::set_property_permission(collection, sender, permission)596 }597598 pub fn transfer(599 collection: &NonfungibleHandle<T>,600 from: &T::CrossAccountId,601 to: &T::CrossAccountId,602 token: TokenId,603 nesting_budget: &dyn Budget,604 ) -> DispatchResult {605 ensure!(606 collection.limits.transfers_enabled(),607 <CommonError<T>>::TransferNotAllowed608 );609610 let token_data =611 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;612 // TODO: require sender to be token, owner, require admins to go through transfer_from613 ensure!(614 &token_data.owner == from615 || (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),616 <CommonError<T>>::NoPermission617 );618619 if collection.permissions.access() == AccessMode::AllowList {620 collection.check_allowlist(from)?;621 collection.check_allowlist(to)?;622 }623 <PalletCommon<T>>::ensure_correct_receiver(to)?;624625 let balance_from = <AccountBalance<T>>::get((collection.id, from))626 .checked_sub(1)627 .ok_or(<CommonError<T>>::TokenValueTooLow)?;628 let balance_to = if from != to {629 let balance_to = <AccountBalance<T>>::get((collection.id, to))630 .checked_add(1)631 .ok_or(ArithmeticError::Overflow)?;632633 ensure!(634 balance_to < collection.limits.account_token_ownership_limit(),635 <CommonError<T>>::AccountTokenLimitExceeded,636 );637638 Some(balance_to)639 } else {640 None641 };642643 <PalletStructure<T>>::nest_if_sent_to_token(644 from.clone(),645 to,646 collection.id,647 token,648 nesting_budget,649 )?;650651 // =========652653 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);654655 <TokenData<T>>::insert(656 (collection.id, token),657 ItemData {658 owner: to.clone(),659 ..token_data660 },661 );662663 if let Some(balance_to) = balance_to {664 // from != to665 if balance_from == 0 {666 <AccountBalance<T>>::remove((collection.id, from));667 } else {668 <AccountBalance<T>>::insert((collection.id, from), balance_from);669 }670 <AccountBalance<T>>::insert((collection.id, to), balance_to);671 <Owned<T>>::remove((collection.id, from, token));672 <Owned<T>>::insert((collection.id, to, token), true);673 }674 Self::set_allowance_unchecked(collection, from, token, None, true);675676 <PalletEvm<T>>::deposit_log(677 ERC721Events::Transfer {678 from: *from.as_eth(),679 to: *to.as_eth(),680 token_id: token.into(),681 }682 .to_log(collection_id_to_address(collection.id)),683 );684 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(685 collection.id,686 token,687 from.clone(),688 to.clone(),689 1,690 ));691 Ok(())692 }693694 pub fn create_multiple_items(695 collection: &NonfungibleHandle<T>,696 sender: &T::CrossAccountId,697 data: Vec<CreateItemData<T>>,698 nesting_budget: &dyn Budget,699 ) -> DispatchResult {700 if !collection.is_owner_or_admin(sender) {701 ensure!(702 collection.permissions.mint_mode(),703 <CommonError<T>>::PublicMintingNotAllowed704 );705 collection.check_allowlist(sender)?;706707 for item in data.iter() {708 collection.check_allowlist(&item.owner)?;709 }710 }711712 for data in data.iter() {713 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;714 }715716 let first_token = <TokensMinted<T>>::get(collection.id);717 let tokens_minted = first_token718 .checked_add(data.len() as u32)719 .ok_or(ArithmeticError::Overflow)?;720 ensure!(721 tokens_minted <= collection.limits.token_limit(),722 <CommonError<T>>::CollectionTokenLimitExceeded723 );724725 let mut balances = BTreeMap::new();726 for data in &data {727 let balance = balances728 .entry(&data.owner)729 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));730 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;731732 ensure!(733 *balance <= collection.limits.account_token_ownership_limit(),734 <CommonError<T>>::AccountTokenLimitExceeded,735 );736 }737738 for (i, data) in data.iter().enumerate() {739 let token = TokenId(first_token + i as u32 + 1);740741 <PalletStructure<T>>::check_nesting(742 sender.clone(),743 &data.owner,744 collection.id,745 token,746 nesting_budget,747 )?;748 }749750 // =========751752 with_transaction(|| {753 for (i, data) in data.iter().enumerate() {754 let token = first_token + i as u32 + 1;755756 <TokenData<T>>::insert(757 (collection.id, token),758 ItemData {759 // const_data: data.const_data.clone(),760 owner: data.owner.clone(),761 },762 );763764 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(765 &data.owner,766 collection.id,767 TokenId(token),768 );769770 if let Err(e) = Self::set_token_properties(771 collection,772 sender,773 TokenId(token),774 data.properties.clone().into_inner(),775 ) {776 return TransactionOutcome::Rollback(Err(e));777 }778 }779 TransactionOutcome::Commit(Ok(()))780 })?;781782 <TokensMinted<T>>::insert(collection.id, tokens_minted);783 for (account, balance) in balances {784 <AccountBalance<T>>::insert((collection.id, account), balance);785 }786 for (i, data) in data.into_iter().enumerate() {787 let token = first_token + i as u32 + 1;788 <Owned<T>>::insert((collection.id, &data.owner, token), true);789790 <PalletEvm<T>>::deposit_log(791 ERC721Events::Transfer {792 from: H160::default(),793 to: *data.owner.as_eth(),794 token_id: token.into(),795 }796 .to_log(collection_id_to_address(collection.id)),797 );798 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(799 collection.id,800 TokenId(token),801 data.owner.clone(),802 1,803 ));804 }805 Ok(())806 }807808 pub fn set_allowance_unchecked(809 collection: &NonfungibleHandle<T>,810 sender: &T::CrossAccountId,811 token: TokenId,812 spender: Option<&T::CrossAccountId>,813 assume_implicit_eth: bool,814 ) {815 if let Some(spender) = spender {816 let old_spender = <Allowance<T>>::get((collection.id, token));817 <Allowance<T>>::insert((collection.id, token), spender);818 // In ERC721 there is only one possible approved user of token, so we set819 // approved user to spender820 <PalletEvm<T>>::deposit_log(821 ERC721Events::Approval {822 owner: *sender.as_eth(),823 approved: *spender.as_eth(),824 token_id: token.into(),825 }826 .to_log(collection_id_to_address(collection.id)),827 );828 // In Unique chain, any token can have any amount of approved users, so we need to829 // set allowance of old owner to 0, and allowance of new owner to 1830 if old_spender.as_ref() != Some(spender) {831 if let Some(old_owner) = old_spender {832 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(833 collection.id,834 token,835 sender.clone(),836 old_owner,837 0,838 ));839 }840 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(841 collection.id,842 token,843 sender.clone(),844 spender.clone(),845 1,846 ));847 }848 } else {849 let old_spender = <Allowance<T>>::take((collection.id, token));850 if !assume_implicit_eth {851 // In ERC721 there is only one possible approved user of token, so we set852 // approved user to zero address853 <PalletEvm<T>>::deposit_log(854 ERC721Events::Approval {855 owner: *sender.as_eth(),856 approved: H160::default(),857 token_id: token.into(),858 }859 .to_log(collection_id_to_address(collection.id)),860 );861 }862 // In Unique chain, any token can have any amount of approved users, so we need to863 // set allowance of old owner to 0864 if let Some(old_spender) = old_spender {865 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(866 collection.id,867 token,868 sender.clone(),869 old_spender,870 0,871 ));872 }873 }874 }875876 pub fn set_allowance(877 collection: &NonfungibleHandle<T>,878 sender: &T::CrossAccountId,879 token: TokenId,880 spender: Option<&T::CrossAccountId>,881 ) -> DispatchResult {882 if collection.permissions.access() == AccessMode::AllowList {883 collection.check_allowlist(sender)?;884 if let Some(spender) = spender {885 collection.check_allowlist(spender)?;886 }887 }888889 if let Some(spender) = spender {890 <PalletCommon<T>>::ensure_correct_receiver(spender)?;891 }892893 let token_data =894 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;895 if &token_data.owner != sender {896 ensure!(897 collection.ignores_owned_amount(sender),898 <CommonError<T>>::CantApproveMoreThanOwned899 );900 }901902 // =========903904 Self::set_allowance_unchecked(collection, sender, token, spender, false);905 Ok(())906 }907908 fn check_allowed(909 collection: &NonfungibleHandle<T>,910 spender: &T::CrossAccountId,911 from: &T::CrossAccountId,912 token: TokenId,913 nesting_budget: &dyn Budget,914 ) -> DispatchResult {915 if spender.conv_eq(from) {916 return Ok(());917 }918 if collection.permissions.access() == AccessMode::AllowList {919 // `from`, `to` checked in [`transfer`]920 collection.check_allowlist(spender)?;921 }922 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {923 // TODO: should collection owner be allowed to perform this transfer?924 ensure!(925 <PalletStructure<T>>::check_indirectly_owned(926 spender.clone(),927 source.0,928 source.1,929 None,930 nesting_budget931 )?,932 <CommonError<T>>::ApprovedValueTooLow,933 );934 return Ok(());935 }936 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {937 return Ok(());938 }939 ensure!(940 collection.ignores_allowance(spender),941 <CommonError<T>>::ApprovedValueTooLow942 );943 Ok(())944 }945946 pub fn transfer_from(947 collection: &NonfungibleHandle<T>,948 spender: &T::CrossAccountId,949 from: &T::CrossAccountId,950 to: &T::CrossAccountId,951 token: TokenId,952 nesting_budget: &dyn Budget,953 ) -> DispatchResult {954 Self::check_allowed(collection, spender, from, token, nesting_budget)?;955956 // =========957958 // Allowance is reset in [`transfer`]959 Self::transfer(collection, from, to, token, nesting_budget)960 }961962 pub fn burn_from(963 collection: &NonfungibleHandle<T>,964 spender: &T::CrossAccountId,965 from: &T::CrossAccountId,966 token: TokenId,967 nesting_budget: &dyn Budget,968 ) -> DispatchResult {969 Self::check_allowed(collection, spender, from, token, nesting_budget)?;970971 // =========972973 Self::burn(collection, from, token)974 }975976 pub fn check_nesting(977 handle: &NonfungibleHandle<T>,978 sender: T::CrossAccountId,979 from: (CollectionId, TokenId),980 under: TokenId,981 nesting_budget: &dyn Budget,982 ) -> DispatchResult {983 fn ensure_sender_allowed<T: Config>(984 collection: CollectionId,985 token: TokenId,986 for_nest: (CollectionId, TokenId),987 sender: T::CrossAccountId,988 budget: &dyn Budget,989 ) -> DispatchResult {990 ensure!(991 <PalletStructure<T>>::check_indirectly_owned(992 sender,993 collection,994 token,995 Some(for_nest),996 budget997 )?,998 <CommonError<T>>::OnlyOwnerAllowedToNest,999 );1000 Ok(())1001 }1002 match handle.permissions.nesting() {1003 NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),1004 NestingRule::Owner => {1005 ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?1006 }1007 NestingRule::OwnerRestricted(whitelist) => {1008 ensure!(1009 whitelist.contains(&from.0),1010 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1011 );1012 ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?1013 }1014 NestingRule::Permissive => {}1015 }1016 Ok(())1017 }10181019 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1020 <TokenChildren<T>>::insert((under.0, under.1, (to_nest.0, to_nest.1)), true);1021 }10221023 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1024 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1025 }10261027 fn collection_has_tokens(collection_id: CollectionId) -> bool {1028 <TokenData<T>>::iter_prefix((collection_id,))1029 .next()1030 .is_some()1031 }10321033 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1034 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1035 .next()1036 .is_some()1037 }10381039 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1040 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1041 .map(|((child_collection_id, child_id), _)| TokenChild {1042 collection: child_collection_id,1043 token: child_id,1044 })1045 .collect()1046 }10471048 /// Delegated to `create_multiple_items`1049 pub fn create_item(1050 collection: &NonfungibleHandle<T>,1051 sender: &T::CrossAccountId,1052 data: CreateItemData<T>,1053 nesting_budget: &dyn Budget,1054 ) -> DispatchResult {1055 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1056 }1057}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#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use evm_coder::ToLog;21use frame_support::{22 BoundedVec, ensure, fail, transactional,23 storage::with_transaction,24 pallet_prelude::DispatchResultWithPostInfo,25 pallet_prelude::Weight,26 weights::{PostDispatchInfo, Pays},27};28use up_data_structs::{29 AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,30 mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,31 PropertyKey, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild,32};33use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};34use pallet_common::{35 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,36 eth::collection_id_to_address,37};38use pallet_structure::{Pallet as PalletStructure, Error as StructureError};39use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};40use sp_core::H160;41use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};42use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap, collections::btree_set::BTreeSet};43use core::ops::Deref;44use codec::{Encode, Decode, MaxEncodedLen};45use scale_info::TypeInfo;4647pub use pallet::*;48use weights::WeightInfo;49#[cfg(feature = "runtime-benchmarks")]50pub mod benchmarking;51pub mod common;52pub mod erc;53pub mod weights;5455pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;56pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5758#[struct_versioning::versioned(version = 2, upper)]59#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]60pub struct ItemData<CrossAccountId> {61 #[version(..2)]62 pub const_data: BoundedVec<u8, CustomDataLimit>,6364 #[version(..2)]65 pub variable_data: BoundedVec<u8, CustomDataLimit>,6667 pub owner: CrossAccountId,68}6970#[frame_support::pallet]71pub mod pallet {72 use super::*;73 use frame_support::{74 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,75 };76 use frame_system::pallet_prelude::*;77 use up_data_structs::{CollectionId, TokenId};78 use super::weights::WeightInfo;7980 #[pallet::error]81 pub enum Error<T> {82 /// Not Nonfungible item data used to mint in Nonfungible collection.83 NotNonfungibleDataUsedToMintFungibleCollectionToken,84 /// Used amount > 1 with NFT85 NonfungibleItemsHaveNoAmount,86 /// Unable to burn NFT with children87 CantBurnNftWithChildren,88 }8990 #[pallet::config]91 pub trait Config:92 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config93 {94 type WeightInfo: WeightInfo;95 }9697 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);9899 #[pallet::pallet]100 #[pallet::storage_version(STORAGE_VERSION)]101 #[pallet::generate_store(pub(super) trait Store)]102 pub struct Pallet<T>(_);103104 #[pallet::storage]105 pub type TokensMinted<T: Config> =106 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;107 #[pallet::storage]108 pub type TokensBurnt<T: Config> =109 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;110111 #[pallet::storage]112 pub type TokenData<T: Config> = StorageNMap<113 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),114 Value = ItemData<T::CrossAccountId>,115 QueryKind = OptionQuery,116 >;117118 #[pallet::storage]119 #[pallet::getter(fn token_properties)]120 pub type TokenProperties<T: Config> = StorageNMap<121 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),122 Value = Properties,123 QueryKind = ValueQuery,124 OnEmpty = up_data_structs::TokenProperties,125 >;126127 /// Used to enumerate tokens owned by account128 #[pallet::storage]129 pub type Owned<T: Config> = StorageNMap<130 Key = (131 Key<Twox64Concat, CollectionId>,132 Key<Blake2_128Concat, T::CrossAccountId>,133 Key<Twox64Concat, TokenId>,134 ),135 Value = bool,136 QueryKind = ValueQuery,137 >;138139 /// Used to enumerate token's children140 #[pallet::storage]141 #[pallet::getter(fn token_children)]142 pub type TokenChildren<T: Config> = StorageNMap<143 Key = (144 Key<Twox64Concat, CollectionId>,145 Key<Twox64Concat, TokenId>,146 Key<Twox64Concat, (CollectionId, TokenId)>,147 ),148 Value = bool,149 QueryKind = ValueQuery,150 >;151152 #[pallet::storage]153 pub type AccountBalance<T: Config> = StorageNMap<154 Key = (155 Key<Twox64Concat, CollectionId>,156 Key<Blake2_128Concat, T::CrossAccountId>,157 ),158 Value = u32,159 QueryKind = ValueQuery,160 >;161162 #[pallet::storage]163 pub type Allowance<T: Config> = StorageNMap<164 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),165 Value = T::CrossAccountId,166 QueryKind = OptionQuery,167 >;168169 #[pallet::hooks]170 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {171 fn on_runtime_upgrade() -> Weight {172 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {173 let mut had_consts = BTreeSet::new();174 <TokenData<T>>::translate::<ItemDataVersion1<T::CrossAccountId>, _>(175 |(collection, token), v| {176 let mut props = vec![];177 if !v.const_data.is_empty() {178 props.push(Property {179 key: b"_old_constData".to_vec().try_into().unwrap(),180 value: v181 .const_data182 .clone()183 .into_inner()184 .try_into()185 .expect("const too long"),186 });187 had_consts.insert(collection);188 }189 if !v.variable_data.is_empty() {190 props.push(Property {191 key: b"_old_variableData".to_vec().try_into().unwrap(),192 value: v193 .variable_data194 .clone()195 .into_inner()196 .try_into()197 .expect("variable too long"),198 })199 }200 if !props.is_empty() {201 Self::set_scoped_token_properties(202 collection,203 token,204 PropertyScope::None,205 props.into_iter(),206 )207 .expect("existing token data exceeds property storage");208 }209 Some(<ItemDataVersion2<T::CrossAccountId>>::from(v))210 },211 );212 for collection in had_consts {213 <PalletCommon<T>>::set_property_permission_unchecked(214 collection,215 PropertyKeyPermission {216 key: b"_old_constData".to_vec().try_into().unwrap(),217 permission: PropertyPermission {218 mutable: false,219 collection_admin: true,220 token_owner: false,221 },222 },223 )224 .expect("failed to configure permission");225 }226 }227228 0229 }230 }231}232233pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);234impl<T: Config> NonfungibleHandle<T> {235 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {236 Self(inner)237 }238 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {239 self.0240 }241 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {242 &mut self.0243 }244}245impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {246 fn recorder(&self) -> &SubstrateRecorder<T> {247 self.0.recorder()248 }249 fn into_recorder(self) -> SubstrateRecorder<T> {250 self.0.into_recorder()251 }252}253impl<T: Config> Deref for NonfungibleHandle<T> {254 type Target = pallet_common::CollectionHandle<T>;255256 fn deref(&self) -> &Self::Target {257 &self.0258 }259}260261impl<T: Config> Pallet<T> {262 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {263 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)264 }265 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {266 <TokenData<T>>::contains_key((collection.id, token))267 }268269 pub fn set_scoped_token_property(270 collection_id: CollectionId,271 token_id: TokenId,272 scope: PropertyScope,273 property: Property,274 ) -> DispatchResult {275 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {276 properties.try_scoped_set(scope, property.key, property.value)277 })278 .map_err(<CommonError<T>>::from)?;279280 Ok(())281 }282283 pub fn set_scoped_token_properties(284 collection_id: CollectionId,285 token_id: TokenId,286 scope: PropertyScope,287 properties: impl Iterator<Item = Property>,288 ) -> DispatchResult {289 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {290 stored_properties.try_scoped_set_from_iter(scope, properties)291 })292 .map_err(<CommonError<T>>::from)?;293294 Ok(())295 }296297 pub fn current_token_id(collection_id: CollectionId) -> TokenId {298 TokenId(<TokensMinted<T>>::get(collection_id))299 }300}301302// unchecked calls skips any permission checks303impl<T: Config> Pallet<T> {304 pub fn init_collection(305 owner: T::CrossAccountId,306 data: CreateCollectionData<T::AccountId>,307 ) -> Result<CollectionId, DispatchError> {308 <PalletCommon<T>>::init_collection(owner, data)309 }310 pub fn destroy_collection(311 collection: NonfungibleHandle<T>,312 sender: &T::CrossAccountId,313 ) -> DispatchResult {314 let id = collection.id;315316 if Self::collection_has_tokens(id) {317 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());318 }319320 // =========321322 PalletCommon::destroy_collection(collection.0, sender)?;323324 <TokenData<T>>::remove_prefix((id,), None);325 <TokenChildren<T>>::remove_prefix((id,), None);326 <Owned<T>>::remove_prefix((id,), None);327 <TokensMinted<T>>::remove(id);328 <TokensBurnt<T>>::remove(id);329 <Allowance<T>>::remove_prefix((id,), None);330 <AccountBalance<T>>::remove_prefix((id,), None);331 Ok(())332 }333334 pub fn burn(335 collection: &NonfungibleHandle<T>,336 sender: &T::CrossAccountId,337 token: TokenId,338 ) -> DispatchResult {339 let token_data =340 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;341 ensure!(342 &token_data.owner == sender343 || (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),344 <CommonError<T>>::NoPermission345 );346347 if collection.permissions.access() == AccessMode::AllowList {348 collection.check_allowlist(sender)?;349 }350351 if Self::token_has_children(collection.id, token) {352 return Err(<Error<T>>::CantBurnNftWithChildren.into());353 }354355 let burnt = <TokensBurnt<T>>::get(collection.id)356 .checked_add(1)357 .ok_or(ArithmeticError::Overflow)?;358359 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))360 .checked_sub(1)361 .ok_or(ArithmeticError::Overflow)?;362363 // =========364365 if balance == 0 {366 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));367 } else {368 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);369 }370371 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);372373 <Owned<T>>::remove((collection.id, &token_data.owner, token));374 <TokensBurnt<T>>::insert(collection.id, burnt);375 <TokenData<T>>::remove((collection.id, token));376 <TokenProperties<T>>::remove((collection.id, token));377 let old_spender = <Allowance<T>>::take((collection.id, token));378379 if let Some(old_spender) = old_spender {380 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(381 collection.id,382 token,383 token_data.owner.clone(),384 old_spender,385 0,386 ));387 }388389 <PalletEvm<T>>::deposit_log(390 ERC721Events::Transfer {391 from: *token_data.owner.as_eth(),392 to: H160::default(),393 token_id: token.into(),394 }395 .to_log(collection_id_to_address(collection.id)),396 );397 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(398 collection.id,399 token,400 token_data.owner,401 1,402 ));403 Ok(())404 }405406 #[transactional]407 pub fn burn_recursively(408 collection: &NonfungibleHandle<T>,409 sender: &T::CrossAccountId,410 token: TokenId,411 self_budget: &dyn Budget,412 breadth_budget: &dyn Budget,413 ) -> DispatchResultWithPostInfo {414 ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);415416 let current_token_account =417 T::CrossTokenAddressMapping::token_to_address(collection.id, token);418419 let mut weight = 0 as Weight;420421 // This method is transactional, if user in fact doesn't have permissions to remove token -422 // tokens removed here will be restored after rejected transaction423 for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {424 ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);425 let PostDispatchInfo { actual_weight, .. } =426 <PalletStructure<T>>::burn_item_recursively(427 current_token_account.clone(),428 collection,429 token,430 self_budget,431 breadth_budget,432 )?;433 if let Some(actual_weight) = actual_weight {434 weight = weight.saturating_add(actual_weight);435 }436 }437438 Self::burn(collection, sender, token)?;439 DispatchResultWithPostInfo::Ok(PostDispatchInfo {440 actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),441 pays_fee: Pays::Yes,442 })443 }444445 pub fn set_token_property(446 collection: &NonfungibleHandle<T>,447 sender: &T::CrossAccountId,448 token_id: TokenId,449 property: Property,450 is_token_create: bool,451 ) -> DispatchResult {452 Self::check_token_change_permission(collection, sender, token_id, &property.key, is_token_create)?;453454 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {455 let property = property.clone();456 properties.try_set(property.key, property.value)457 })458 .map_err(<CommonError<T>>::from)?;459460 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(461 collection.id,462 token_id,463 property.key,464 ));465466 Ok(())467 }468469 #[transactional]470 pub fn set_token_properties(471 collection: &NonfungibleHandle<T>,472 sender: &T::CrossAccountId,473 token_id: TokenId,474 properties: Vec<Property>,475 is_token_create: bool,476 ) -> DispatchResult {477 for property in properties {478 Self::set_token_property(collection, sender, token_id, property, is_token_create)?;479 }480481 Ok(())482 }483484 pub fn delete_token_property(485 collection: &NonfungibleHandle<T>,486 sender: &T::CrossAccountId,487 token_id: TokenId,488 property_key: PropertyKey,489 ) -> DispatchResult {490 Self::check_token_change_permission(collection, sender, token_id, &property_key, false)?;491492 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {493 properties.remove(&property_key)494 })495 .map_err(<CommonError<T>>::from)?;496497 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(498 collection.id,499 token_id,500 property_key,501 ));502503 Ok(())504 }505506 fn check_token_change_permission(507 collection: &NonfungibleHandle<T>,508 sender: &T::CrossAccountId,509 token_id: TokenId,510 property_key: &PropertyKey,511 is_token_create: bool,512 ) -> DispatchResult {513 let permission = <PalletCommon<T>>::property_permissions(collection.id)514 .get(property_key)515 .cloned()516 .unwrap_or_else(PropertyPermission::none);517518 let token_data = <TokenData<T>>::get((collection.id, token_id))519 .ok_or(<CommonError<T>>::TokenNotFound)?;520521 let check_token_owner = || -> DispatchResult {522 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);523 Ok(())524 };525526 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))527 .get(property_key)528 .is_some();529530 match permission {531 PropertyPermission { mutable: false, .. } if is_property_exists => {532 Err(<CommonError<T>>::NoPermission.into())533 }534535 PropertyPermission {536 collection_admin,537 token_owner,538 ..539 } => {540 if is_token_create && (collection_admin || token_owner) {541 return Ok(());542 }543544 let mut check_result = Err(<CommonError<T>>::NoPermission.into());545546 if collection_admin {547 check_result = collection.check_is_owner_or_admin(sender);548 }549550 if token_owner {551 check_result.or_else(|_| check_token_owner())552 } else {553 check_result554 }555 }556 }557 }558559 #[transactional]560 pub fn delete_token_properties(561 collection: &NonfungibleHandle<T>,562 sender: &T::CrossAccountId,563 token_id: TokenId,564 property_keys: Vec<PropertyKey>,565 ) -> DispatchResult {566 for key in property_keys {567 Self::delete_token_property(collection, sender, token_id, key)?;568 }569570 Ok(())571 }572573 pub fn set_collection_properties(574 collection: &NonfungibleHandle<T>,575 sender: &T::CrossAccountId,576 properties: Vec<Property>,577 ) -> DispatchResult {578 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)579 }580581 pub fn delete_collection_properties(582 collection: &CollectionHandle<T>,583 sender: &T::CrossAccountId,584 property_keys: Vec<PropertyKey>,585 ) -> DispatchResult {586 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)587 }588589 pub fn set_property_permissions(590 collection: &CollectionHandle<T>,591 sender: &T::CrossAccountId,592 property_permissions: Vec<PropertyKeyPermission>,593 ) -> DispatchResult {594 <PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)595 }596597 pub fn set_property_permission(598 collection: &CollectionHandle<T>,599 sender: &T::CrossAccountId,600 permission: PropertyKeyPermission,601 ) -> DispatchResult {602 <PalletCommon<T>>::set_property_permission(collection, sender, permission)603 }604605 pub fn transfer(606 collection: &NonfungibleHandle<T>,607 from: &T::CrossAccountId,608 to: &T::CrossAccountId,609 token: TokenId,610 nesting_budget: &dyn Budget,611 ) -> DispatchResult {612 ensure!(613 collection.limits.transfers_enabled(),614 <CommonError<T>>::TransferNotAllowed615 );616617 let token_data =618 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;619 // TODO: require sender to be token, owner, require admins to go through transfer_from620 ensure!(621 &token_data.owner == from622 || (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),623 <CommonError<T>>::NoPermission624 );625626 if collection.permissions.access() == AccessMode::AllowList {627 collection.check_allowlist(from)?;628 collection.check_allowlist(to)?;629 }630 <PalletCommon<T>>::ensure_correct_receiver(to)?;631632 let balance_from = <AccountBalance<T>>::get((collection.id, from))633 .checked_sub(1)634 .ok_or(<CommonError<T>>::TokenValueTooLow)?;635 let balance_to = if from != to {636 let balance_to = <AccountBalance<T>>::get((collection.id, to))637 .checked_add(1)638 .ok_or(ArithmeticError::Overflow)?;639640 ensure!(641 balance_to < collection.limits.account_token_ownership_limit(),642 <CommonError<T>>::AccountTokenLimitExceeded,643 );644645 Some(balance_to)646 } else {647 None648 };649650 <PalletStructure<T>>::nest_if_sent_to_token(651 from.clone(),652 to,653 collection.id,654 token,655 nesting_budget,656 )?;657658 // =========659660 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);661662 <TokenData<T>>::insert(663 (collection.id, token),664 ItemData {665 owner: to.clone(),666 ..token_data667 },668 );669670 if let Some(balance_to) = balance_to {671 // from != to672 if balance_from == 0 {673 <AccountBalance<T>>::remove((collection.id, from));674 } else {675 <AccountBalance<T>>::insert((collection.id, from), balance_from);676 }677 <AccountBalance<T>>::insert((collection.id, to), balance_to);678 <Owned<T>>::remove((collection.id, from, token));679 <Owned<T>>::insert((collection.id, to, token), true);680 }681 Self::set_allowance_unchecked(collection, from, token, None, true);682683 <PalletEvm<T>>::deposit_log(684 ERC721Events::Transfer {685 from: *from.as_eth(),686 to: *to.as_eth(),687 token_id: token.into(),688 }689 .to_log(collection_id_to_address(collection.id)),690 );691 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(692 collection.id,693 token,694 from.clone(),695 to.clone(),696 1,697 ));698 Ok(())699 }700701 pub fn create_multiple_items(702 collection: &NonfungibleHandle<T>,703 sender: &T::CrossAccountId,704 data: Vec<CreateItemData<T>>,705 nesting_budget: &dyn Budget,706 ) -> DispatchResult {707 if !collection.is_owner_or_admin(sender) {708 ensure!(709 collection.permissions.mint_mode(),710 <CommonError<T>>::PublicMintingNotAllowed711 );712 collection.check_allowlist(sender)?;713714 for item in data.iter() {715 collection.check_allowlist(&item.owner)?;716 }717 }718719 for data in data.iter() {720 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;721 }722723 let first_token = <TokensMinted<T>>::get(collection.id);724 let tokens_minted = first_token725 .checked_add(data.len() as u32)726 .ok_or(ArithmeticError::Overflow)?;727 ensure!(728 tokens_minted <= collection.limits.token_limit(),729 <CommonError<T>>::CollectionTokenLimitExceeded730 );731732 let mut balances = BTreeMap::new();733 for data in &data {734 let balance = balances735 .entry(&data.owner)736 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));737 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;738739 ensure!(740 *balance <= collection.limits.account_token_ownership_limit(),741 <CommonError<T>>::AccountTokenLimitExceeded,742 );743 }744745 for (i, data) in data.iter().enumerate() {746 let token = TokenId(first_token + i as u32 + 1);747748 <PalletStructure<T>>::check_nesting(749 sender.clone(),750 &data.owner,751 collection.id,752 token,753 nesting_budget,754 )?;755 }756757 // =========758759 with_transaction(|| {760 for (i, data) in data.iter().enumerate() {761 let token = first_token + i as u32 + 1;762763 <TokenData<T>>::insert(764 (collection.id, token),765 ItemData {766 // const_data: data.const_data.clone(),767 owner: data.owner.clone(),768 },769 );770771 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(772 &data.owner,773 collection.id,774 TokenId(token),775 );776777 if let Err(e) = Self::set_token_properties(778 collection,779 sender,780 TokenId(token),781 data.properties.clone().into_inner(),782 true,783 ) {784 return TransactionOutcome::Rollback(Err(e));785 }786 }787 TransactionOutcome::Commit(Ok(()))788 })?;789790 <TokensMinted<T>>::insert(collection.id, tokens_minted);791 for (account, balance) in balances {792 <AccountBalance<T>>::insert((collection.id, account), balance);793 }794 for (i, data) in data.into_iter().enumerate() {795 let token = first_token + i as u32 + 1;796 <Owned<T>>::insert((collection.id, &data.owner, token), true);797798 <PalletEvm<T>>::deposit_log(799 ERC721Events::Transfer {800 from: H160::default(),801 to: *data.owner.as_eth(),802 token_id: token.into(),803 }804 .to_log(collection_id_to_address(collection.id)),805 );806 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(807 collection.id,808 TokenId(token),809 data.owner.clone(),810 1,811 ));812 }813 Ok(())814 }815816 pub fn set_allowance_unchecked(817 collection: &NonfungibleHandle<T>,818 sender: &T::CrossAccountId,819 token: TokenId,820 spender: Option<&T::CrossAccountId>,821 assume_implicit_eth: bool,822 ) {823 if let Some(spender) = spender {824 let old_spender = <Allowance<T>>::get((collection.id, token));825 <Allowance<T>>::insert((collection.id, token), spender);826 // In ERC721 there is only one possible approved user of token, so we set827 // approved user to spender828 <PalletEvm<T>>::deposit_log(829 ERC721Events::Approval {830 owner: *sender.as_eth(),831 approved: *spender.as_eth(),832 token_id: token.into(),833 }834 .to_log(collection_id_to_address(collection.id)),835 );836 // In Unique chain, any token can have any amount of approved users, so we need to837 // set allowance of old owner to 0, and allowance of new owner to 1838 if old_spender.as_ref() != Some(spender) {839 if let Some(old_owner) = old_spender {840 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(841 collection.id,842 token,843 sender.clone(),844 old_owner,845 0,846 ));847 }848 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(849 collection.id,850 token,851 sender.clone(),852 spender.clone(),853 1,854 ));855 }856 } else {857 let old_spender = <Allowance<T>>::take((collection.id, token));858 if !assume_implicit_eth {859 // In ERC721 there is only one possible approved user of token, so we set860 // approved user to zero address861 <PalletEvm<T>>::deposit_log(862 ERC721Events::Approval {863 owner: *sender.as_eth(),864 approved: H160::default(),865 token_id: token.into(),866 }867 .to_log(collection_id_to_address(collection.id)),868 );869 }870 // In Unique chain, any token can have any amount of approved users, so we need to871 // set allowance of old owner to 0872 if let Some(old_spender) = old_spender {873 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(874 collection.id,875 token,876 sender.clone(),877 old_spender,878 0,879 ));880 }881 }882 }883884 pub fn set_allowance(885 collection: &NonfungibleHandle<T>,886 sender: &T::CrossAccountId,887 token: TokenId,888 spender: Option<&T::CrossAccountId>,889 ) -> DispatchResult {890 if collection.permissions.access() == AccessMode::AllowList {891 collection.check_allowlist(sender)?;892 if let Some(spender) = spender {893 collection.check_allowlist(spender)?;894 }895 }896897 if let Some(spender) = spender {898 <PalletCommon<T>>::ensure_correct_receiver(spender)?;899 }900901 let token_data =902 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;903 if &token_data.owner != sender {904 ensure!(905 collection.ignores_owned_amount(sender),906 <CommonError<T>>::CantApproveMoreThanOwned907 );908 }909910 // =========911912 Self::set_allowance_unchecked(collection, sender, token, spender, false);913 Ok(())914 }915916 fn check_allowed(917 collection: &NonfungibleHandle<T>,918 spender: &T::CrossAccountId,919 from: &T::CrossAccountId,920 token: TokenId,921 nesting_budget: &dyn Budget,922 ) -> DispatchResult {923 if spender.conv_eq(from) {924 return Ok(());925 }926 if collection.permissions.access() == AccessMode::AllowList {927 // `from`, `to` checked in [`transfer`]928 collection.check_allowlist(spender)?;929 }930 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {931 // TODO: should collection owner be allowed to perform this transfer?932 ensure!(933 <PalletStructure<T>>::check_indirectly_owned(934 spender.clone(),935 source.0,936 source.1,937 None,938 nesting_budget939 )?,940 <CommonError<T>>::ApprovedValueTooLow,941 );942 return Ok(());943 }944 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {945 return Ok(());946 }947 ensure!(948 collection.ignores_allowance(spender),949 <CommonError<T>>::ApprovedValueTooLow950 );951 Ok(())952 }953954 pub fn transfer_from(955 collection: &NonfungibleHandle<T>,956 spender: &T::CrossAccountId,957 from: &T::CrossAccountId,958 to: &T::CrossAccountId,959 token: TokenId,960 nesting_budget: &dyn Budget,961 ) -> DispatchResult {962 Self::check_allowed(collection, spender, from, token, nesting_budget)?;963964 // =========965966 // Allowance is reset in [`transfer`]967 Self::transfer(collection, from, to, token, nesting_budget)968 }969970 pub fn burn_from(971 collection: &NonfungibleHandle<T>,972 spender: &T::CrossAccountId,973 from: &T::CrossAccountId,974 token: TokenId,975 nesting_budget: &dyn Budget,976 ) -> DispatchResult {977 Self::check_allowed(collection, spender, from, token, nesting_budget)?;978979 // =========980981 Self::burn(collection, from, token)982 }983984 pub fn check_nesting(985 handle: &NonfungibleHandle<T>,986 sender: T::CrossAccountId,987 from: (CollectionId, TokenId),988 under: TokenId,989 nesting_budget: &dyn Budget,990 ) -> DispatchResult {991 fn ensure_sender_allowed<T: Config>(992 collection: CollectionId,993 token: TokenId,994 for_nest: (CollectionId, TokenId),995 sender: T::CrossAccountId,996 budget: &dyn Budget,997 ) -> DispatchResult {998 ensure!(999 <PalletStructure<T>>::check_indirectly_owned(1000 sender,1001 collection,1002 token,1003 Some(for_nest),1004 budget1005 )?,1006 <CommonError<T>>::OnlyOwnerAllowedToNest,1007 );1008 Ok(())1009 }1010 match handle.permissions.nesting() {1011 NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),1012 NestingRule::Owner => {1013 ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?1014 }1015 NestingRule::OwnerRestricted(whitelist) => {1016 ensure!(1017 whitelist.contains(&from.0),1018 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1019 );1020 ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?1021 }1022 NestingRule::Permissive => {}1023 }1024 Ok(())1025 }10261027 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1028 <TokenChildren<T>>::insert((under.0, under.1, (to_nest.0, to_nest.1)), true);1029 }10301031 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1032 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1033 }10341035 fn collection_has_tokens(collection_id: CollectionId) -> bool {1036 <TokenData<T>>::iter_prefix((collection_id,))1037 .next()1038 .is_some()1039 }10401041 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1042 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1043 .next()1044 .is_some()1045 }10461047 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1048 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1049 .map(|((child_collection_id, child_id), _)| TokenChild {1050 collection: child_collection_id,1051 token: child_id,1052 })1053 .collect()1054 }10551056 /// Delegated to `create_multiple_items`1057 pub fn create_item(1058 collection: &NonfungibleHandle<T>,1059 sender: &T::CrossAccountId,1060 data: CreateItemData<T>,1061 nesting_budget: &dyn Budget,1062 ) -> DispatchResult {1063 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1064 }1065}tests/src/eth/contractSponsoring.test.tsdiffbeforeafterboth--- a/tests/src/eth/contractSponsoring.test.ts
+++ b/tests/src/eth/contractSponsoring.test.ts
@@ -221,68 +221,71 @@
expect(await helpers.methods.getSponsoringRateLimit(flipper.options.address).call()).to.be.equals('7200');
});
- //TODO: CORE-302 add eth methods
- itWeb3.skip('Sponsoring collection from evm address via access list', async ({api, web3}) => {
+ itWeb3('Sponsoring collection from evm address via access list', async ({api, web3}) => {
const owner = await createEthAccountWithBalance(api, web3);
const collectionHelpers = evmCollectionHelpers(web3, owner);
let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send();
const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
const sponsor = await createEthAccountWithBalance(api, web3);
const collectionEvm = evmCollection(web3, owner, collectionIdAddress);
- result = await collectionEvm.methods.ethSetSponsor(sponsor).send({from: owner});
+ result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner});
let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!;
expect(collectionSub.sponsorship.isUnconfirmed).to.be.true;
expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor));
- await expect(collectionEvm.methods.ethConfirmSponsorship().call()).to.be.rejectedWith('Caller is not set as sponsor');
+ await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('Caller is not set as sponsor');
- await collectionEvm.methods.ethConfirmSponsorship().send({from: sponsor});
+ await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});
collectionSub = (await getDetailedCollectionInfo(api, collectionId))!;
expect(collectionSub.sponsorship.isConfirmed).to.be.true;
expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor));
const user = createEthAccount(web3);
- const nextTokenId = await collectionEvm.methods.nextTokenId().call();
+ let nextTokenId = await collectionEvm.methods.nextTokenId().call();
expect(nextTokenId).to.be.equal('1');
const oldPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman();
expect(oldPermissions.mintMode).to.be.false;
expect(oldPermissions.access).to.be.equal('Normal');
- await collectionEvm.methods.setAccess('AllowList').send({from: owner});
- await collectionEvm.methods.addToAllowList(user).send({from: owner});
- await collectionEvm.methods.setMintMode(true).send({from: owner});
+ await collectionEvm.methods.setCollectionAccess('AllowList').send({from: owner});
+ await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner});
+ await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});
const newPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman();
expect(newPermissions.mintMode).to.be.true;
expect(newPermissions.access).to.be.equal('AllowList');
- // const [alicesBalanceBefore] = await getBalance(api, [alicesPublicKey]);
+ const ownerBalanceBefore = await ethBalanceViaSub(api, owner);
+ const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor);
- {
- const nextTokenId = await collectionEvm.methods.nextTokenId().call();
- expect(nextTokenId).to.be.equal('1');
- const result = await collectionEvm.methods.mintWithTokenURI(
- user,
- nextTokenId,
- 'Test URI',
- ).call({from: user});
- console.log(result);
- const events = normalizeEvents(result.events);
+ nextTokenId = await collectionEvm.methods.nextTokenId().call({from: user});
+ expect(nextTokenId).to.be.equal('1');
+ result = await collectionEvm.methods.mintWithTokenURI(
+ user,
+ nextTokenId,
+ 'Test URI',
+ ).send({from: user});
+ const events = normalizeEvents(result.events);
+ events[0].address = events[0].address.toLocaleLowerCase();
- expect(events).to.be.deep.equal([
- {
- collectionIdAddress,
- event: 'Transfer',
- args: {
- from: '0x0000000000000000000000000000000000000000',
- to: user,
- tokenId: nextTokenId,
- },
+ expect(events).to.be.deep.equal([
+ {
+ address: collectionIdAddress.toLocaleLowerCase(),
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: user,
+ tokenId: nextTokenId,
},
- ]);
+ },
+ ]);
- expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
- }
+ expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+
+ const ownerBalanceAfter = await ethBalanceViaSub(api, owner);
+ expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);
+ const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor);
+ expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
});
itWeb3('Check that transaction via EVM spend money from sponsor address', async ({api, web3}) => {
@@ -308,7 +311,6 @@
const ownerBalanceBefore = await ethBalanceViaSub(api, owner);
const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor);
-
const userCollectionEvm = evmCollection(web3, user, collectionIdAddress);
const nextTokenId = await userCollectionEvm.methods.nextTokenId().call();