difftreelog
feat implement collection nesting rules field
in: master
3 files changed
pallets/common/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 core::ops::{Deref, DerefMut};20use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};21use sp_std::vec::Vec;22use pallet_evm::account::CrossAccountId;23use frame_support::{24 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},25 ensure, fail,26 traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},27 BoundedVec,28 weights::Pays,29};30use pallet_evm::GasWeightMapping;31use up_data_structs::{32 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH,33 COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, TokenId, CollectionStats, MAX_TOKEN_OWNERSHIP,34 CollectionMode, NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,35 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,36 CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit,37};38pub use pallet::*;39use sp_core::H160;40use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};41#[cfg(feature = "runtime-benchmarks")]42pub mod benchmarking;43pub mod dispatch;44pub mod erc;45pub mod eth;4647#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]48pub struct CollectionHandle<T: Config> {49 pub id: CollectionId,50 collection: Collection<T::AccountId>,51 pub recorder: SubstrateRecorder<T>,52}53impl<T: Config> WithRecorder<T> for CollectionHandle<T> {54 fn recorder(&self) -> &SubstrateRecorder<T> {55 &self.recorder56 }57 fn into_recorder(self) -> SubstrateRecorder<T> {58 self.recorder59 }60}61impl<T: Config> CollectionHandle<T> {62 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {63 <CollectionById<T>>::get(id).map(|collection| Self {64 id,65 collection,66 recorder: SubstrateRecorder::new(eth::collection_id_to_address(id), gas_limit),67 })68 }69 pub fn new(id: CollectionId) -> Option<Self> {70 Self::new_with_gas_limit(id, u64::MAX)71 }72 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {73 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)74 }75 pub fn log_mirrored(&self, log: impl evm_coder::ToLog) {76 self.recorder.log_mirrored(log)77 }78 pub fn log_direct(&self, log: impl evm_coder::ToLog) {79 self.recorder.log_direct(log)80 }81 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {82 self.recorder83 .consume_gas(T::GasWeightMapping::weight_to_gas(84 <T as frame_system::Config>::DbWeight::get()85 .read86 .saturating_mul(reads),87 ))88 }89 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {90 self.recorder91 .consume_gas(T::GasWeightMapping::weight_to_gas(92 <T as frame_system::Config>::DbWeight::get()93 .write94 .saturating_mul(writes),95 ))96 }97 pub fn submit_logs(self) {98 self.recorder.submit_logs()99 }100 pub fn save(self) -> DispatchResult {101 self.recorder.submit_logs();102 <CollectionById<T>>::insert(self.id, self.collection);103 Ok(())104 }105}106impl<T: Config> Deref for CollectionHandle<T> {107 type Target = Collection<T::AccountId>;108109 fn deref(&self) -> &Self::Target {110 &self.collection111 }112}113114impl<T: Config> DerefMut for CollectionHandle<T> {115 fn deref_mut(&mut self) -> &mut Self::Target {116 &mut self.collection117 }118}119120impl<T: Config> CollectionHandle<T> {121 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {122 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);123 Ok(())124 }125 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {126 *subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))127 }128 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {129 ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);130 Ok(())131 }132 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {133 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)134 }135 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {136 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)137 }138 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {139 ensure!(140 <Allowlist<T>>::get((self.id, user)),141 <Error<T>>::AddressNotInAllowlist142 );143 Ok(())144 }145146 pub fn check_can_update_meta(147 &self,148 subject: &T::CrossAccountId,149 item_owner: &T::CrossAccountId,150 ) -> DispatchResult {151 match self.meta_update_permission {152 MetaUpdatePermission::ItemOwner => {153 ensure!(subject == item_owner, <Error<T>>::NoPermission);154 Ok(())155 }156 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),157 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),158 }159 }160}161162#[frame_support::pallet]163pub mod pallet {164 use super::*;165 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};166 use pallet_evm::account;167 use dispatch::CollectionDispatch;168 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};169 use frame_system::pallet_prelude::*;170 use frame_support::traits::Currency;171 use up_data_structs::{TokenId, mapping::TokenAddressMapping};172 use scale_info::TypeInfo;173 use up_evm_mapping::CrossAccountId;174175 #[pallet::config]176 pub trait Config:177 frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo + account::Config178 {179 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;180181 type Currency: Currency<Self::AccountId>;182183 #[pallet::constant]184 type CollectionCreationPrice: Get<185 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,186 >;187 type CollectionDispatch: CollectionDispatch<Self>;188189 type TreasuryAccountId: Get<Self::AccountId>;190191 type EvmTokenAddressMapping: TokenAddressMapping<H160>;192 type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;193 }194195 #[pallet::pallet]196 #[pallet::generate_store(pub(super) trait Store)]197 pub struct Pallet<T>(_);198199 #[pallet::extra_constants]200 impl<T: Config> Pallet<T> {201 pub fn collection_admins_limit() -> u32 {202 COLLECTION_ADMINS_LIMIT203 }204 }205206 #[pallet::event]207 #[pallet::generate_deposit(pub fn deposit_event)]208 pub enum Event<T: Config> {209 /// New collection was created210 ///211 /// # Arguments212 ///213 /// * collection_id: Globally unique identifier of newly created collection.214 ///215 /// * mode: [CollectionMode] converted into u8.216 ///217 /// * account_id: Collection owner.218 CollectionCreated(CollectionId, u8, T::AccountId),219220 /// New collection was destroyed221 ///222 /// # Arguments223 ///224 /// * collection_id: Globally unique identifier of collection.225 CollectionDestroyed(CollectionId),226227 /// New item was created.228 ///229 /// # Arguments230 ///231 /// * collection_id: Id of the collection where item was created.232 ///233 /// * item_id: Id of an item. Unique within the collection.234 ///235 /// * recipient: Owner of newly created item236 ///237 /// * amount: Always 1 for NFT238 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),239240 /// Collection item was burned.241 ///242 /// # Arguments243 ///244 /// * collection_id.245 ///246 /// * item_id: Identifier of burned NFT.247 ///248 /// * owner: which user has destroyed its tokens249 ///250 /// * amount: Always 1 for NFT251 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),252253 /// Item was transferred254 ///255 /// * collection_id: Id of collection to which item is belong256 ///257 /// * item_id: Id of an item258 ///259 /// * sender: Original owner of item260 ///261 /// * recipient: New owner of item262 ///263 /// * amount: Always 1 for NFT264 Transfer(265 CollectionId,266 TokenId,267 T::CrossAccountId,268 T::CrossAccountId,269 u128,270 ),271272 /// * collection_id273 ///274 /// * item_id275 ///276 /// * sender277 ///278 /// * spender279 ///280 /// * amount281 Approved(282 CollectionId,283 TokenId,284 T::CrossAccountId,285 T::CrossAccountId,286 u128,287 ),288 }289290 #[pallet::error]291 pub enum Error<T> {292 /// This collection does not exist.293 CollectionNotFound,294 /// Sender parameter and item owner must be equal.295 MustBeTokenOwner,296 /// No permission to perform action297 NoPermission,298 /// Collection is not in mint mode.299 PublicMintingNotAllowed,300 /// Address is not in allow list.301 AddressNotInAllowlist,302303 /// Collection name can not be longer than 63 char.304 CollectionNameLimitExceeded,305 /// Collection description can not be longer than 255 char.306 CollectionDescriptionLimitExceeded,307 /// Token prefix can not be longer than 15 char.308 CollectionTokenPrefixLimitExceeded,309 /// Total collections bound exceeded.310 TotalCollectionsLimitExceeded,311 /// variable_data exceeded data limit.312 TokenVariableDataLimitExceeded,313 /// Exceeded max admin count314 CollectionAdminCountExceeded,315 /// Collection limit bounds per collection exceeded316 CollectionLimitBoundsExceeded,317 /// Tried to enable permissions which are only permitted to be disabled318 OwnerPermissionsCantBeReverted,319320 /// Collection settings not allowing items transferring321 TransferNotAllowed,322 /// Account token limit exceeded per collection323 AccountTokenLimitExceeded,324 /// Collection token limit exceeded325 CollectionTokenLimitExceeded,326 /// Metadata flag frozen327 MetadataFlagFrozen,328329 /// Item not exists.330 TokenNotFound,331 /// Item balance not enough.332 TokenValueTooLow,333 /// Requested value more than approved.334 ApprovedValueTooLow,335 /// Tried to approve more than owned336 CantApproveMoreThanOwned,337338 /// Can't transfer tokens to ethereum zero address339 AddressIsZero,340 /// Target collection doesn't supports this operation341 UnsupportedOperation,342343 /// Not sufficient founds to perform action344 NotSufficientFounds,345 }346347 #[pallet::storage]348 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;349 #[pallet::storage]350 pub type DestroyedCollectionCount<T> =351 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;352353 /// Collection info354 #[pallet::storage]355 pub type CollectionById<T> = StorageMap<356 Hasher = Blake2_128Concat,357 Key = CollectionId,358 Value = Collection<<T as frame_system::Config>::AccountId>,359 QueryKind = OptionQuery,360 >;361362 #[pallet::storage]363 pub type AdminAmount<T> = StorageMap<364 Hasher = Blake2_128Concat,365 Key = CollectionId,366 Value = u32,367 QueryKind = ValueQuery,368 >;369370 /// List of collection admins371 #[pallet::storage]372 pub type IsAdmin<T: Config> = StorageNMap<373 Key = (374 Key<Blake2_128Concat, CollectionId>,375 Key<Blake2_128Concat, T::CrossAccountId>,376 ),377 Value = bool,378 QueryKind = ValueQuery,379 >;380381 /// Allowlisted collection users382 #[pallet::storage]383 pub type Allowlist<T: Config> = StorageNMap<384 Key = (385 Key<Blake2_128Concat, CollectionId>,386 Key<Blake2_128Concat, T::CrossAccountId>,387 ),388 Value = bool,389 QueryKind = ValueQuery,390 >;391392 /// Not used by code, exists only to provide some types to metadata393 #[pallet::storage]394 pub type DummyStorageValue<T> =395 StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;396}397398impl<T: Config> Pallet<T> {399 /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens400 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {401 ensure!(402 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,403 <Error<T>>::AddressIsZero404 );405 Ok(())406 }407 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {408 <IsAdmin<T>>::iter_prefix((collection,))409 .map(|(a, _)| a)410 .collect()411 }412 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {413 <Allowlist<T>>::iter_prefix((collection,))414 .map(|(a, _)| a)415 .collect()416 }417 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {418 <Allowlist<T>>::get((collection, user))419 }420 pub fn collection_stats() -> CollectionStats {421 let created = <CreatedCollectionCount<T>>::get();422 let destroyed = <DestroyedCollectionCount<T>>::get();423 CollectionStats {424 created: created.0,425 destroyed: destroyed.0,426 alive: created.0 - destroyed.0,427 }428 }429430 pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {431 let collection = <CollectionById<T>>::get(collection);432 if collection.is_none() {433 return None;434 }435436 let collection = collection.unwrap();437 let limits = collection.limits;438 let effective_limits = CollectionLimits {439 account_token_ownership_limit: Some(limits.account_token_ownership_limit()),440 sponsored_data_size: Some(limits.sponsored_data_size()),441 sponsored_data_rate_limit: Some(442 limits443 .sponsored_data_rate_limit444 .unwrap_or(SponsoringRateLimit::SponsoringDisabled),445 ),446 token_limit: Some(limits.token_limit()),447 sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(448 match collection.mode {449 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,450 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,451 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,452 },453 )),454 sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),455 owner_can_transfer: Some(limits.owner_can_transfer()),456 owner_can_destroy: Some(limits.owner_can_destroy()),457 transfers_enabled: Some(limits.transfers_enabled()),458 };459460 Some(effective_limits)461 }462}463464impl<T: Config> Pallet<T> {465 pub fn init_collection(466 owner: T::AccountId,467 data: CreateCollectionData<T::AccountId>,468 ) -> Result<CollectionId, DispatchError> {469 {470 ensure!(471 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,472 Error::<T>::CollectionTokenPrefixLimitExceeded473 );474 }475476 let created_count = <CreatedCollectionCount<T>>::get()477 .0478 .checked_add(1)479 .ok_or(ArithmeticError::Overflow)?;480 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;481 let id = CollectionId(created_count);482483 // bound Total number of collections484 ensure!(485 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,486 <Error<T>>::TotalCollectionsLimitExceeded487 );488489 // =========490491 let collection = Collection {492 owner: owner.clone(),493 name: data.name,494 mode: data.mode.clone(),495 mint_mode: false,496 access: data.access.unwrap_or_default(),497 description: data.description,498 token_prefix: data.token_prefix,499 offchain_schema: data.offchain_schema,500 schema_version: data.schema_version.unwrap_or_default(),501 sponsorship: data502 .pending_sponsor503 .map(SponsorshipState::Unconfirmed)504 .unwrap_or_default(),505 variable_on_chain_schema: data.variable_on_chain_schema,506 const_on_chain_schema: data.const_on_chain_schema,507 limits: data508 .limits509 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))510 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,511 meta_update_permission: data.meta_update_permission.unwrap_or_default(),512 };513514 // Take a (non-refundable) deposit of collection creation515 {516 let mut imbalance =517 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();518 imbalance.subsume(519 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(520 &T::TreasuryAccountId::get(),521 T::CollectionCreationPrice::get(),522 ),523 );524 <T as Config>::Currency::settle(525 &owner,526 imbalance,527 WithdrawReasons::TRANSFER,528 ExistenceRequirement::KeepAlive,529 )530 .map_err(|_| Error::<T>::NotSufficientFounds)?;531 }532533 <CreatedCollectionCount<T>>::put(created_count);534 <Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));535 <CollectionById<T>>::insert(id, collection);536 Ok(id)537 }538539 pub fn destroy_collection(540 collection: CollectionHandle<T>,541 sender: &T::CrossAccountId,542 ) -> DispatchResult {543 ensure!(544 collection.limits.owner_can_destroy(),545 <Error<T>>::NoPermission,546 );547 collection.check_is_owner(sender)?;548549 let destroyed_collections = <DestroyedCollectionCount<T>>::get()550 .0551 .checked_add(1)552 .ok_or(ArithmeticError::Overflow)?;553554 // =========555556 <DestroyedCollectionCount<T>>::put(destroyed_collections);557 <CollectionById<T>>::remove(collection.id);558 <AdminAmount<T>>::remove(collection.id);559 <IsAdmin<T>>::remove_prefix((collection.id,), None);560 <Allowlist<T>>::remove_prefix((collection.id,), None);561562 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));563 Ok(())564 }565566 pub fn toggle_allowlist(567 collection: &CollectionHandle<T>,568 sender: &T::CrossAccountId,569 user: &T::CrossAccountId,570 allowed: bool,571 ) -> DispatchResult {572 collection.check_is_owner_or_admin(sender)?;573574 // =========575576 if allowed {577 <Allowlist<T>>::insert((collection.id, user), true);578 } else {579 <Allowlist<T>>::remove((collection.id, user));580 }581582 Ok(())583 }584585 pub fn toggle_admin(586 collection: &CollectionHandle<T>,587 sender: &T::CrossAccountId,588 user: &T::CrossAccountId,589 admin: bool,590 ) -> DispatchResult {591 collection.check_is_owner_or_admin(sender)?;592593 let was_admin = <IsAdmin<T>>::get((collection.id, user));594 if was_admin == admin {595 return Ok(());596 }597 let amount = <AdminAmount<T>>::get(collection.id);598599 if admin {600 let amount = amount601 .checked_add(1)602 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;603 ensure!(604 amount <= Self::collection_admins_limit(),605 <Error<T>>::CollectionAdminCountExceeded,606 );607608 // =========609610 <AdminAmount<T>>::insert(collection.id, amount);611 <IsAdmin<T>>::insert((collection.id, user), true);612 } else {613 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));614 <IsAdmin<T>>::remove((collection.id, user));615 }616617 Ok(())618 }619620 pub fn clamp_limits(621 mode: CollectionMode,622 old_limit: &CollectionLimits,623 mut new_limit: CollectionLimits,624 ) -> Result<CollectionLimits, DispatchError> {625 macro_rules! limit_default {626 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{627 $(628 if let Some($new) = $new.$field {629 let $old = $old.$field($($arg)?);630 let _ = $new;631 let _ = $old;632 $check633 } else {634 $new.$field = $old.$field635 }636 )*637 }};638 }639640 limit_default!(old_limit, new_limit,641 account_token_ownership_limit => ensure!(642 new_limit <= MAX_TOKEN_OWNERSHIP,643 <Error<T>>::CollectionLimitBoundsExceeded,644 ),645 sponsor_transfer_timeout(match mode {646 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,647 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,648 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,649 }) => ensure!(650 new_limit <= MAX_SPONSOR_TIMEOUT,651 <Error<T>>::CollectionLimitBoundsExceeded,652 ),653 sponsored_data_size => ensure!(654 new_limit <= CUSTOM_DATA_LIMIT,655 <Error<T>>::CollectionLimitBoundsExceeded,656 ),657 token_limit => ensure!(658 old_limit >= new_limit && new_limit > 0,659 <Error<T>>::CollectionTokenLimitExceeded660 ),661 owner_can_transfer => ensure!(662 old_limit || !new_limit,663 <Error<T>>::OwnerPermissionsCantBeReverted,664 ),665 owner_can_destroy => ensure!(666 old_limit || !new_limit,667 <Error<T>>::OwnerPermissionsCantBeReverted,668 ),669 sponsored_data_rate_limit => {},670 transfers_enabled => {},671 );672 Ok(new_limit)673 }674}675676#[macro_export]677macro_rules! unsupported {678 () => {679 Err(<Error<T>>::UnsupportedOperation.into())680 };681}682683/// Worst cases684pub trait CommonWeightInfo<CrossAccountId> {685 fn create_item() -> Weight;686 fn create_multiple_items(amount: u32) -> Weight;687 fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;688 fn burn_item() -> Weight;689 fn transfer() -> Weight;690 fn approve() -> Weight;691 fn transfer_from() -> Weight;692 fn burn_from() -> Weight;693 fn set_variable_metadata(bytes: u32) -> Weight;694}695696pub trait CommonCollectionOperations<T: Config> {697 fn create_item(698 &self,699 sender: T::CrossAccountId,700 to: T::CrossAccountId,701 data: CreateItemData,702 ) -> DispatchResultWithPostInfo;703 fn create_multiple_items(704 &self,705 sender: T::CrossAccountId,706 to: T::CrossAccountId,707 data: Vec<CreateItemData>,708 ) -> DispatchResultWithPostInfo;709 fn create_multiple_items_ex(710 &self,711 sender: T::CrossAccountId,712 data: CreateItemExData<T::CrossAccountId>,713 ) -> DispatchResultWithPostInfo;714 fn burn_item(715 &self,716 sender: T::CrossAccountId,717 token: TokenId,718 amount: u128,719 ) -> DispatchResultWithPostInfo;720721 fn transfer(722 &self,723 sender: T::CrossAccountId,724 to: T::CrossAccountId,725 token: TokenId,726 amount: u128,727 ) -> DispatchResultWithPostInfo;728 fn approve(729 &self,730 sender: T::CrossAccountId,731 spender: T::CrossAccountId,732 token: TokenId,733 amount: u128,734 ) -> DispatchResultWithPostInfo;735 fn transfer_from(736 &self,737 sender: T::CrossAccountId,738 from: T::CrossAccountId,739 to: T::CrossAccountId,740 token: TokenId,741 amount: u128,742 ) -> DispatchResultWithPostInfo;743 fn burn_from(744 &self,745 sender: T::CrossAccountId,746 from: T::CrossAccountId,747 token: TokenId,748 amount: u128,749 ) -> DispatchResultWithPostInfo;750751 fn set_variable_metadata(752 &self,753 sender: T::CrossAccountId,754 token: TokenId,755 data: BoundedVec<u8, CustomDataLimit>,756 ) -> DispatchResultWithPostInfo;757758 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;759 fn token_exists(&self, token: TokenId) -> bool;760 fn last_token_id(&self) -> TokenId;761762 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;763 fn const_metadata(&self, token: TokenId) -> Vec<u8>;764 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;765766 /// How many tokens collection contains (Applicable to nonfungible/refungible)767 fn collection_tokens(&self) -> u32;768 /// Amount of different tokens account has (Applicable to nonfungible/refungible)769 fn account_balance(&self, account: T::CrossAccountId) -> u32;770 /// Amount of specific token account have (Applicable to fungible/refungible)771 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;772 fn allowance(773 &self,774 sender: T::CrossAccountId,775 spender: T::CrossAccountId,776 token: TokenId,777 ) -> u128;778}779780// Flexible enough for implementing CommonCollectionOperations781pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {782 let post_info = PostDispatchInfo {783 actual_weight: Some(weight),784 pays_fee: Pays::Yes,785 };786 match res {787 Ok(()) => Ok(post_info),788 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),789 }790}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 core::ops::{Deref, DerefMut};20use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};21use sp_std::vec::Vec;22use pallet_evm::account::CrossAccountId;23use frame_support::{24 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},25 ensure, fail,26 traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},27 BoundedVec,28 weights::Pays,29};30use pallet_evm::GasWeightMapping;31use up_data_structs::{32 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH,33 COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, TokenId, CollectionStats, MAX_TOKEN_OWNERSHIP,34 CollectionMode, NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,35 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,36 CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit,37};38pub use pallet::*;39use sp_core::H160;40use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};41#[cfg(feature = "runtime-benchmarks")]42pub mod benchmarking;43pub mod dispatch;44pub mod erc;45pub mod eth;4647#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]48pub struct CollectionHandle<T: Config> {49 pub id: CollectionId,50 collection: Collection<T::AccountId>,51 pub recorder: SubstrateRecorder<T>,52}53impl<T: Config> WithRecorder<T> for CollectionHandle<T> {54 fn recorder(&self) -> &SubstrateRecorder<T> {55 &self.recorder56 }57 fn into_recorder(self) -> SubstrateRecorder<T> {58 self.recorder59 }60}61impl<T: Config> CollectionHandle<T> {62 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {63 <CollectionById<T>>::get(id).map(|collection| Self {64 id,65 collection,66 recorder: SubstrateRecorder::new(eth::collection_id_to_address(id), gas_limit),67 })68 }69 pub fn new(id: CollectionId) -> Option<Self> {70 Self::new_with_gas_limit(id, u64::MAX)71 }72 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {73 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)74 }75 pub fn log_mirrored(&self, log: impl evm_coder::ToLog) {76 self.recorder.log_mirrored(log)77 }78 pub fn log_direct(&self, log: impl evm_coder::ToLog) {79 self.recorder.log_direct(log)80 }81 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {82 self.recorder83 .consume_gas(T::GasWeightMapping::weight_to_gas(84 <T as frame_system::Config>::DbWeight::get()85 .read86 .saturating_mul(reads),87 ))88 }89 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {90 self.recorder91 .consume_gas(T::GasWeightMapping::weight_to_gas(92 <T as frame_system::Config>::DbWeight::get()93 .write94 .saturating_mul(writes),95 ))96 }97 pub fn submit_logs(self) {98 self.recorder.submit_logs()99 }100 pub fn save(self) -> DispatchResult {101 self.recorder.submit_logs();102 <CollectionById<T>>::insert(self.id, self.collection);103 Ok(())104 }105}106impl<T: Config> Deref for CollectionHandle<T> {107 type Target = Collection<T::AccountId>;108109 fn deref(&self) -> &Self::Target {110 &self.collection111 }112}113114impl<T: Config> DerefMut for CollectionHandle<T> {115 fn deref_mut(&mut self) -> &mut Self::Target {116 &mut self.collection117 }118}119120impl<T: Config> CollectionHandle<T> {121 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {122 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);123 Ok(())124 }125 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {126 *subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))127 }128 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {129 ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);130 Ok(())131 }132 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {133 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)134 }135 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {136 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)137 }138 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {139 ensure!(140 <Allowlist<T>>::get((self.id, user)),141 <Error<T>>::AddressNotInAllowlist142 );143 Ok(())144 }145146 pub fn check_can_update_meta(147 &self,148 subject: &T::CrossAccountId,149 item_owner: &T::CrossAccountId,150 ) -> DispatchResult {151 match self.meta_update_permission {152 MetaUpdatePermission::ItemOwner => {153 ensure!(subject == item_owner, <Error<T>>::NoPermission);154 Ok(())155 }156 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),157 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),158 }159 }160}161162#[frame_support::pallet]163pub mod pallet {164 use super::*;165 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};166 use pallet_evm::account;167 use dispatch::CollectionDispatch;168 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};169 use frame_system::pallet_prelude::*;170 use frame_support::traits::Currency;171 use up_data_structs::{TokenId, mapping::TokenAddressMapping};172 use scale_info::TypeInfo;173 use up_evm_mapping::CrossAccountId;174175 #[pallet::config]176 pub trait Config:177 frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo + account::Config178 {179 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;180181 type Currency: Currency<Self::AccountId>;182183 #[pallet::constant]184 type CollectionCreationPrice: Get<185 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,186 >;187 type CollectionDispatch: CollectionDispatch<Self>;188189 type TreasuryAccountId: Get<Self::AccountId>;190191 type EvmTokenAddressMapping: TokenAddressMapping<H160>;192 type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;193 }194195 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);196197 #[pallet::pallet]198 #[pallet::storage_version(STORAGE_VERSION)]199 #[pallet::generate_store(pub(super) trait Store)]200 pub struct Pallet<T>(_);201202 #[pallet::extra_constants]203 impl<T: Config> Pallet<T> {204 pub fn collection_admins_limit() -> u32 {205 COLLECTION_ADMINS_LIMIT206 }207 }208209 #[pallet::event]210 #[pallet::generate_deposit(pub fn deposit_event)]211 pub enum Event<T: Config> {212 /// New collection was created213 ///214 /// # Arguments215 ///216 /// * collection_id: Globally unique identifier of newly created collection.217 ///218 /// * mode: [CollectionMode] converted into u8.219 ///220 /// * account_id: Collection owner.221 CollectionCreated(CollectionId, u8, T::AccountId),222223 /// New collection was destroyed224 ///225 /// # Arguments226 ///227 /// * collection_id: Globally unique identifier of collection.228 CollectionDestroyed(CollectionId),229230 /// New item was created.231 ///232 /// # Arguments233 ///234 /// * collection_id: Id of the collection where item was created.235 ///236 /// * item_id: Id of an item. Unique within the collection.237 ///238 /// * recipient: Owner of newly created item239 ///240 /// * amount: Always 1 for NFT241 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),242243 /// Collection item was burned.244 ///245 /// # Arguments246 ///247 /// * collection_id.248 ///249 /// * item_id: Identifier of burned NFT.250 ///251 /// * owner: which user has destroyed its tokens252 ///253 /// * amount: Always 1 for NFT254 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),255256 /// Item was transferred257 ///258 /// * collection_id: Id of collection to which item is belong259 ///260 /// * item_id: Id of an item261 ///262 /// * sender: Original owner of item263 ///264 /// * recipient: New owner of item265 ///266 /// * amount: Always 1 for NFT267 Transfer(268 CollectionId,269 TokenId,270 T::CrossAccountId,271 T::CrossAccountId,272 u128,273 ),274275 /// * collection_id276 ///277 /// * item_id278 ///279 /// * sender280 ///281 /// * spender282 ///283 /// * amount284 Approved(285 CollectionId,286 TokenId,287 T::CrossAccountId,288 T::CrossAccountId,289 u128,290 ),291 }292293 #[pallet::error]294 pub enum Error<T> {295 /// This collection does not exist.296 CollectionNotFound,297 /// Sender parameter and item owner must be equal.298 MustBeTokenOwner,299 /// No permission to perform action300 NoPermission,301 /// Collection is not in mint mode.302 PublicMintingNotAllowed,303 /// Address is not in allow list.304 AddressNotInAllowlist,305306 /// Collection name can not be longer than 63 char.307 CollectionNameLimitExceeded,308 /// Collection description can not be longer than 255 char.309 CollectionDescriptionLimitExceeded,310 /// Token prefix can not be longer than 15 char.311 CollectionTokenPrefixLimitExceeded,312 /// Total collections bound exceeded.313 TotalCollectionsLimitExceeded,314 /// variable_data exceeded data limit.315 TokenVariableDataLimitExceeded,316 /// Exceeded max admin count317 CollectionAdminCountExceeded,318 /// Collection limit bounds per collection exceeded319 CollectionLimitBoundsExceeded,320 /// Tried to enable permissions which are only permitted to be disabled321 OwnerPermissionsCantBeReverted,322323 /// Collection settings not allowing items transferring324 TransferNotAllowed,325 /// Account token limit exceeded per collection326 AccountTokenLimitExceeded,327 /// Collection token limit exceeded328 CollectionTokenLimitExceeded,329 /// Metadata flag frozen330 MetadataFlagFrozen,331332 /// Item not exists.333 TokenNotFound,334 /// Item balance not enough.335 TokenValueTooLow,336 /// Requested value more than approved.337 ApprovedValueTooLow,338 /// Tried to approve more than owned339 CantApproveMoreThanOwned,340341 /// Can't transfer tokens to ethereum zero address342 AddressIsZero,343 /// Target collection doesn't supports this operation344 UnsupportedOperation,345346 /// Not sufficient founds to perform action347 NotSufficientFounds,348 }349350 #[pallet::storage]351 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;352 #[pallet::storage]353 pub type DestroyedCollectionCount<T> =354 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;355356 /// Collection info357 #[pallet::storage]358 pub type CollectionById<T> = StorageMap<359 Hasher = Blake2_128Concat,360 Key = CollectionId,361 Value = Collection<<T as frame_system::Config>::AccountId>,362 QueryKind = OptionQuery,363 >;364365 #[pallet::storage]366 pub type AdminAmount<T> = StorageMap<367 Hasher = Blake2_128Concat,368 Key = CollectionId,369 Value = u32,370 QueryKind = ValueQuery,371 >;372373 /// List of collection admins374 #[pallet::storage]375 pub type IsAdmin<T: Config> = StorageNMap<376 Key = (377 Key<Blake2_128Concat, CollectionId>,378 Key<Blake2_128Concat, T::CrossAccountId>,379 ),380 Value = bool,381 QueryKind = ValueQuery,382 >;383384 /// Allowlisted collection users385 #[pallet::storage]386 pub type Allowlist<T: Config> = StorageNMap<387 Key = (388 Key<Blake2_128Concat, CollectionId>,389 Key<Blake2_128Concat, T::CrossAccountId>,390 ),391 Value = bool,392 QueryKind = ValueQuery,393 >;394395 /// Not used by code, exists only to provide some types to metadata396 #[pallet::storage]397 pub type DummyStorageValue<T> =398 StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;399400 #[pallet::hooks]401 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {402 fn on_runtime_upgrade() -> Weight {403 let mut weight = 0;404405 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {406 use up_data_structs::{CollectionVersion1, CollectionVersion2};407 <CollectionById<T>>::translate_values::<CollectionVersion1<T::AccountId>, _>(|v| {408 Some(CollectionVersion2::from(v))409 });410 }411412 weight413 }414 }415}416417impl<T: Config> Pallet<T> {418 /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens419 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {420 ensure!(421 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,422 <Error<T>>::AddressIsZero423 );424 Ok(())425 }426 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {427 <IsAdmin<T>>::iter_prefix((collection,))428 .map(|(a, _)| a)429 .collect()430 }431 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {432 <Allowlist<T>>::iter_prefix((collection,))433 .map(|(a, _)| a)434 .collect()435 }436 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {437 <Allowlist<T>>::get((collection, user))438 }439 pub fn collection_stats() -> CollectionStats {440 let created = <CreatedCollectionCount<T>>::get();441 let destroyed = <DestroyedCollectionCount<T>>::get();442 CollectionStats {443 created: created.0,444 destroyed: destroyed.0,445 alive: created.0 - destroyed.0,446 }447 }448449 pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {450 let collection = <CollectionById<T>>::get(collection);451 if collection.is_none() {452 return None;453 }454455 let collection = collection.unwrap();456 let limits = collection.limits;457 let effective_limits = CollectionLimits {458 account_token_ownership_limit: Some(limits.account_token_ownership_limit()),459 sponsored_data_size: Some(limits.sponsored_data_size()),460 sponsored_data_rate_limit: Some(461 limits462 .sponsored_data_rate_limit463 .unwrap_or(SponsoringRateLimit::SponsoringDisabled),464 ),465 token_limit: Some(limits.token_limit()),466 sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(467 match collection.mode {468 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,469 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,470 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,471 },472 )),473 sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),474 owner_can_transfer: Some(limits.owner_can_transfer()),475 owner_can_destroy: Some(limits.owner_can_destroy()),476 transfers_enabled: Some(limits.transfers_enabled()),477 };478479 Some(effective_limits)480 }481}482483impl<T: Config> Pallet<T> {484 pub fn init_collection(485 owner: T::AccountId,486 data: CreateCollectionData<T::AccountId>,487 ) -> Result<CollectionId, DispatchError> {488 {489 ensure!(490 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,491 Error::<T>::CollectionTokenPrefixLimitExceeded492 );493 }494495 let created_count = <CreatedCollectionCount<T>>::get()496 .0497 .checked_add(1)498 .ok_or(ArithmeticError::Overflow)?;499 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;500 let id = CollectionId(created_count);501502 // bound Total number of collections503 ensure!(504 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,505 <Error<T>>::TotalCollectionsLimitExceeded506 );507508 // =========509510 let collection = Collection {511 owner: owner.clone(),512 name: data.name,513 mode: data.mode.clone(),514 mint_mode: false,515 access: data.access.unwrap_or_default(),516 description: data.description,517 token_prefix: data.token_prefix,518 offchain_schema: data.offchain_schema,519 schema_version: data.schema_version.unwrap_or_default(),520 sponsorship: data521 .pending_sponsor522 .map(SponsorshipState::Unconfirmed)523 .unwrap_or_default(),524 variable_on_chain_schema: data.variable_on_chain_schema,525 const_on_chain_schema: data.const_on_chain_schema,526 limits: data527 .limits528 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))529 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,530 meta_update_permission: data.meta_update_permission.unwrap_or_default(),531 };532533 // Take a (non-refundable) deposit of collection creation534 {535 let mut imbalance =536 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();537 imbalance.subsume(538 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(539 &T::TreasuryAccountId::get(),540 T::CollectionCreationPrice::get(),541 ),542 );543 <T as Config>::Currency::settle(544 &owner,545 imbalance,546 WithdrawReasons::TRANSFER,547 ExistenceRequirement::KeepAlive,548 )549 .map_err(|_| Error::<T>::NotSufficientFounds)?;550 }551552 <CreatedCollectionCount<T>>::put(created_count);553 <Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));554 <CollectionById<T>>::insert(id, collection);555 Ok(id)556 }557558 pub fn destroy_collection(559 collection: CollectionHandle<T>,560 sender: &T::CrossAccountId,561 ) -> DispatchResult {562 ensure!(563 collection.limits.owner_can_destroy(),564 <Error<T>>::NoPermission,565 );566 collection.check_is_owner(sender)?;567568 let destroyed_collections = <DestroyedCollectionCount<T>>::get()569 .0570 .checked_add(1)571 .ok_or(ArithmeticError::Overflow)?;572573 // =========574575 <DestroyedCollectionCount<T>>::put(destroyed_collections);576 <CollectionById<T>>::remove(collection.id);577 <AdminAmount<T>>::remove(collection.id);578 <IsAdmin<T>>::remove_prefix((collection.id,), None);579 <Allowlist<T>>::remove_prefix((collection.id,), None);580581 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));582 Ok(())583 }584585 pub fn toggle_allowlist(586 collection: &CollectionHandle<T>,587 sender: &T::CrossAccountId,588 user: &T::CrossAccountId,589 allowed: bool,590 ) -> DispatchResult {591 collection.check_is_owner_or_admin(sender)?;592593 // =========594595 if allowed {596 <Allowlist<T>>::insert((collection.id, user), true);597 } else {598 <Allowlist<T>>::remove((collection.id, user));599 }600601 Ok(())602 }603604 pub fn toggle_admin(605 collection: &CollectionHandle<T>,606 sender: &T::CrossAccountId,607 user: &T::CrossAccountId,608 admin: bool,609 ) -> DispatchResult {610 collection.check_is_owner_or_admin(sender)?;611612 let was_admin = <IsAdmin<T>>::get((collection.id, user));613 if was_admin == admin {614 return Ok(());615 }616 let amount = <AdminAmount<T>>::get(collection.id);617618 if admin {619 let amount = amount620 .checked_add(1)621 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;622 ensure!(623 amount <= Self::collection_admins_limit(),624 <Error<T>>::CollectionAdminCountExceeded,625 );626627 // =========628629 <AdminAmount<T>>::insert(collection.id, amount);630 <IsAdmin<T>>::insert((collection.id, user), true);631 } else {632 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));633 <IsAdmin<T>>::remove((collection.id, user));634 }635636 Ok(())637 }638639 pub fn clamp_limits(640 mode: CollectionMode,641 old_limit: &CollectionLimits,642 mut new_limit: CollectionLimits,643 ) -> Result<CollectionLimits, DispatchError> {644 macro_rules! limit_default {645 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{646 $(647 if let Some($new) = $new.$field {648 let $old = $old.$field($($arg)?);649 let _ = $new;650 let _ = $old;651 $check652 } else {653 $new.$field = $old.$field654 }655 )*656 }};657 }658659 limit_default!(old_limit, new_limit,660 account_token_ownership_limit => ensure!(661 new_limit <= MAX_TOKEN_OWNERSHIP,662 <Error<T>>::CollectionLimitBoundsExceeded,663 ),664 sponsor_transfer_timeout(match mode {665 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,666 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,667 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,668 }) => ensure!(669 new_limit <= MAX_SPONSOR_TIMEOUT,670 <Error<T>>::CollectionLimitBoundsExceeded,671 ),672 sponsored_data_size => ensure!(673 new_limit <= CUSTOM_DATA_LIMIT,674 <Error<T>>::CollectionLimitBoundsExceeded,675 ),676 token_limit => ensure!(677 old_limit >= new_limit && new_limit > 0,678 <Error<T>>::CollectionTokenLimitExceeded679 ),680 owner_can_transfer => ensure!(681 old_limit || !new_limit,682 <Error<T>>::OwnerPermissionsCantBeReverted,683 ),684 owner_can_destroy => ensure!(685 old_limit || !new_limit,686 <Error<T>>::OwnerPermissionsCantBeReverted,687 ),688 sponsored_data_rate_limit => {},689 transfers_enabled => {},690 );691 Ok(new_limit)692 }693}694695#[macro_export]696macro_rules! unsupported {697 () => {698 Err(<Error<T>>::UnsupportedOperation.into())699 };700}701702/// Worst cases703pub trait CommonWeightInfo<CrossAccountId> {704 fn create_item() -> Weight;705 fn create_multiple_items(amount: u32) -> Weight;706 fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;707 fn burn_item() -> Weight;708 fn transfer() -> Weight;709 fn approve() -> Weight;710 fn transfer_from() -> Weight;711 fn burn_from() -> Weight;712 fn set_variable_metadata(bytes: u32) -> Weight;713}714715pub trait CommonCollectionOperations<T: Config> {716 fn create_item(717 &self,718 sender: T::CrossAccountId,719 to: T::CrossAccountId,720 data: CreateItemData,721 ) -> DispatchResultWithPostInfo;722 fn create_multiple_items(723 &self,724 sender: T::CrossAccountId,725 to: T::CrossAccountId,726 data: Vec<CreateItemData>,727 ) -> DispatchResultWithPostInfo;728 fn create_multiple_items_ex(729 &self,730 sender: T::CrossAccountId,731 data: CreateItemExData<T::CrossAccountId>,732 ) -> DispatchResultWithPostInfo;733 fn burn_item(734 &self,735 sender: T::CrossAccountId,736 token: TokenId,737 amount: u128,738 ) -> DispatchResultWithPostInfo;739740 fn transfer(741 &self,742 sender: T::CrossAccountId,743 to: T::CrossAccountId,744 token: TokenId,745 amount: u128,746 ) -> DispatchResultWithPostInfo;747 fn approve(748 &self,749 sender: T::CrossAccountId,750 spender: T::CrossAccountId,751 token: TokenId,752 amount: u128,753 ) -> DispatchResultWithPostInfo;754 fn transfer_from(755 &self,756 sender: T::CrossAccountId,757 from: T::CrossAccountId,758 to: T::CrossAccountId,759 token: TokenId,760 amount: u128,761 ) -> DispatchResultWithPostInfo;762 fn burn_from(763 &self,764 sender: T::CrossAccountId,765 from: T::CrossAccountId,766 token: TokenId,767 amount: u128,768 ) -> DispatchResultWithPostInfo;769770 fn set_variable_metadata(771 &self,772 sender: T::CrossAccountId,773 token: TokenId,774 data: BoundedVec<u8, CustomDataLimit>,775 ) -> DispatchResultWithPostInfo;776777 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;778 fn token_exists(&self, token: TokenId) -> bool;779 fn last_token_id(&self) -> TokenId;780781 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;782 fn const_metadata(&self, token: TokenId) -> Vec<u8>;783 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;784785 /// How many tokens collection contains (Applicable to nonfungible/refungible)786 fn collection_tokens(&self) -> u32;787 /// Amount of different tokens account has (Applicable to nonfungible/refungible)788 fn account_balance(&self, account: T::CrossAccountId) -> u32;789 /// Amount of specific token account have (Applicable to fungible/refungible)790 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;791 fn allowance(792 &self,793 sender: T::CrossAccountId,794 spender: T::CrossAccountId,795 token: TokenId,796 ) -> u128;797}798799// Flexible enough for implementing CommonCollectionOperations800pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {801 let post_info = PostDispatchInfo {802 actual_weight: Some(weight),803 pays_fee: Pays::Yes,804 };805 match res {806 Ok(()) => Ok(post_info),807 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),808 }809}primitives/data-structs/Cargo.tomldiffbeforeafterboth--- a/primitives/data-structs/Cargo.toml
+++ b/primitives/data-structs/Cargo.toml
@@ -24,6 +24,7 @@
sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" }
sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" }
derivative = "2.2.0"
+struct-versioning = { path = "../../crates/struct-versioning" }
[features]
default = ["std"]
primitives/data-structs/src/lib.rsdiffbeforeafterboth--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -248,6 +248,7 @@
}
}
+#[struct_versioning::versioned(version = 2, upper)]
#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct Collection<AccountId> {
@@ -265,7 +266,12 @@
pub offchain_schema: BoundedVec<u8, ConstU32<OFFCHAIN_SCHEMA_LIMIT>>,
pub schema_version: SchemaVersion,
pub sponsorship: SponsorshipState<AccountId>,
- pub limits: CollectionLimits, // Collection private restrictions
+
+ #[version(..2)]
+ pub limits: CollectionLimitsVersion1, // Collection private restrictions
+ #[version(2.., upper(limits.into()))]
+ pub limits: CollectionLimitsVersion2,
+
#[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))]
pub variable_on_chain_schema: BoundedVec<u8, ConstU32<VARIABLE_ON_CHAIN_SCHEMA_LIMIT>>,
#[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))]
@@ -321,6 +327,7 @@
}
/// All fields are wrapped in `Option`s, where None means chain default
+#[struct_versioning::versioned(version = 2, upper)]
#[derive(Encode, Decode, Debug, Default, Clone, PartialEq, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct CollectionLimits {
@@ -338,6 +345,9 @@
pub owner_can_transfer: Option<bool>,
pub owner_can_destroy: Option<bool>,
pub transfers_enabled: Option<bool>,
+
+ #[version(2.., upper(None))]
+ pub nesting_rule: Option<NestingRule>,
}
impl CollectionLimits {
@@ -384,6 +394,26 @@
SponsoringRateLimit::Blocks(v) => Some(v.min(MAX_SPONSOR_TIMEOUT)),
}
}
+ pub fn nesting_rule(&self) -> &NestingRule {
+ static DEFAULT: NestingRule = NestingRule::Owner;
+ self.nesting_rule.as_ref().unwrap_or(&DEFAULT)
+ }
+}
+
+#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, MaxEncodedLen, Derivative)]
+#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
+#[derivative(Debug)]
+pub enum NestingRule {
+ /// No one can nest tokens
+ Disabled,
+ /// Owner can nest any tokens
+ Owner,
+ /// Owner can nest tokens from specified collections
+ OwnerRestricted(
+ #[cfg_attr(feature = "serde1", serde(with = "bounded_set_serde"))]
+ #[derivative(Debug(format_with = "bounded_set_debug"))]
+ BoundedBTreeSet<CollectionId, ConstU32<16>>,
+ ),
}
#[derive(Encode, Decode, Debug, Clone, Copy, PartialEq, TypeInfo, MaxEncodedLen)]
@@ -482,6 +512,50 @@
(&v as &BTreeMap<K, V>).fmt(f)
}
+#[cfg(feature = "serde1")]
+#[allow(dead_code)]
+mod bounded_set_serde {
+ use core::convert::TryFrom;
+ use sp_std::collections::btree_set::BTreeSet;
+ use frame_support::{traits::Get, storage::bounded_btree_set::BoundedBTreeSet};
+ use serde::{
+ ser::{self, Serialize},
+ de::{self, Deserialize, Error},
+ };
+ pub fn serialize<D, K, S>(
+ value: &BoundedBTreeSet<K, S>,
+ serializer: D,
+ ) -> Result<D::Ok, D::Error>
+ where
+ D: ser::Serializer,
+ K: Serialize + Ord,
+ {
+ (value as &BTreeSet<_>).serialize(serializer)
+ }
+
+ pub fn deserialize<'de, D, K, S>(deserializer: D) -> Result<BoundedBTreeSet<K, S>, D::Error>
+ where
+ D: de::Deserializer<'de>,
+ K: de::Deserialize<'de> + Ord,
+ S: Get<u32>,
+ {
+ let map = <BTreeSet<K>>::deserialize(deserializer)?;
+ let len = map.len();
+ TryFrom::try_from(map).map_err(|_| D::Error::invalid_length(len, &"lesser size"))
+ }
+}
+
+fn bounded_set_debug<K, S>(
+ v: &BoundedBTreeSet<K, S>,
+ f: &mut fmt::Formatter,
+) -> Result<(), fmt::Error>
+where
+ K: fmt::Debug + Ord,
+{
+ use core::fmt::Debug;
+ (&v as &BTreeSet<K>).fmt(f)
+}
+
#[derive(Encode, Decode, MaxEncodedLen, Default, PartialEq, Clone, Derivative, TypeInfo)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[derivative(Debug)]