difftreelog
Merge pull request #299 from UniqueNetwork/feature/CORE-296
in: master
CORE-296 Add error "NotSufficientFounds" and test it.
3 files changed
pallets/common/src/lib.rsdiffbeforeafterboth1#![cfg_attr(not(feature = "std"), no_std)]23use core::ops::{Deref, DerefMut};4use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};5use sp_std::vec::Vec;6use account::CrossAccountId;7use frame_support::{8 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo},9 ensure, fail,10 traits::{Imbalance, Get, Currency},11 BoundedVec,12};13use pallet_evm::GasWeightMapping;14use up_data_structs::{15 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement,16 MAX_TOKEN_PREFIX_LENGTH, COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo,17 TokenId, Weight, WithdrawReasons, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode,18 NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,19 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,20 CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData,21};22pub use pallet::*;23use sp_core::H160;24use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};25pub mod account;26#[cfg(feature = "runtime-benchmarks")]27pub mod benchmarking;28pub mod erc;29pub mod eth;3031#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]32pub struct CollectionHandle<T: Config> {33 pub id: CollectionId,34 collection: Collection<T::AccountId>,35 pub recorder: SubstrateRecorder<T>,36}37impl<T: Config> WithRecorder<T> for CollectionHandle<T> {38 fn recorder(&self) -> &SubstrateRecorder<T> {39 &self.recorder40 }41 fn into_recorder(self) -> SubstrateRecorder<T> {42 self.recorder43 }44}45impl<T: Config> CollectionHandle<T> {46 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {47 <CollectionById<T>>::get(id).map(|collection| Self {48 id,49 collection,50 recorder: SubstrateRecorder::new(eth::collection_id_to_address(id), gas_limit),51 })52 }53 pub fn new(id: CollectionId) -> Option<Self> {54 Self::new_with_gas_limit(id, u64::MAX)55 }56 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {57 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)58 }59 pub fn log_mirrored(&self, log: impl evm_coder::ToLog) {60 self.recorder.log_mirrored(log)61 }62 pub fn log_direct(&self, log: impl evm_coder::ToLog) {63 self.recorder.log_direct(log)64 }65 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {66 self.recorder67 .consume_gas(T::GasWeightMapping::weight_to_gas(68 <T as frame_system::Config>::DbWeight::get()69 .read70 .saturating_mul(reads),71 ))72 }73 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {74 self.recorder75 .consume_gas(T::GasWeightMapping::weight_to_gas(76 <T as frame_system::Config>::DbWeight::get()77 .write78 .saturating_mul(writes),79 ))80 }81 pub fn submit_logs(self) {82 self.recorder.submit_logs()83 }84 pub fn save(self) -> DispatchResult {85 self.recorder.submit_logs();86 <CollectionById<T>>::insert(self.id, self.collection);87 Ok(())88 }89}90impl<T: Config> Deref for CollectionHandle<T> {91 type Target = Collection<T::AccountId>;9293 fn deref(&self) -> &Self::Target {94 &self.collection95 }96}9798impl<T: Config> DerefMut for CollectionHandle<T> {99 fn deref_mut(&mut self) -> &mut Self::Target {100 &mut self.collection101 }102}103104impl<T: Config> CollectionHandle<T> {105 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {106 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);107 Ok(())108 }109 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {110 *subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))111 }112 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {113 ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);114 Ok(())115 }116 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {117 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)118 }119 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {120 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)121 }122 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {123 ensure!(124 <Allowlist<T>>::get((self.id, user)),125 <Error<T>>::AddressNotInAllowlist126 );127 Ok(())128 }129130 pub fn check_can_update_meta(131 &self,132 subject: &T::CrossAccountId,133 item_owner: &T::CrossAccountId,134 ) -> DispatchResult {135 match self.meta_update_permission {136 MetaUpdatePermission::ItemOwner => {137 ensure!(subject == item_owner, <Error<T>>::NoPermission);138 Ok(())139 }140 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),141 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),142 }143 }144}145146#[frame_support::pallet]147pub mod pallet {148 use super::*;149 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};150 use account::CrossAccountId;151 use frame_support::traits::Currency;152 use up_data_structs::TokenId;153 use scale_info::TypeInfo;154155 #[pallet::config]156 pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo {157 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;158159 type CrossAccountId: CrossAccountId<Self::AccountId>;160161 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;162 type EvmBackwardsAddressMapping: up_evm_mapping::EvmBackwardsAddressMapping<Self::AccountId>;163164 type Currency: Currency<Self::AccountId>;165166 #[pallet::constant]167 type CollectionCreationPrice: Get<168 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,169 >;170171 type TreasuryAccountId: Get<Self::AccountId>;172 }173174 #[pallet::pallet]175 #[pallet::generate_store(pub(super) trait Store)]176 pub struct Pallet<T>(_);177178 #[pallet::extra_constants]179 impl<T: Config> Pallet<T> {180 pub fn collection_admins_limit() -> u32 {181 COLLECTION_ADMINS_LIMIT182 }183 }184185 #[pallet::event]186 #[pallet::generate_deposit(pub fn deposit_event)]187 pub enum Event<T: Config> {188 /// New collection was created189 ///190 /// # Arguments191 ///192 /// * collection_id: Globally unique identifier of newly created collection.193 ///194 /// * mode: [CollectionMode] converted into u8.195 ///196 /// * account_id: Collection owner.197 CollectionCreated(CollectionId, u8, T::AccountId),198199 /// New collection was destroyed200 ///201 /// # Arguments202 ///203 /// * collection_id: Globally unique identifier of collection.204 CollectionDestroyed(CollectionId),205206 /// New item was created.207 ///208 /// # Arguments209 ///210 /// * collection_id: Id of the collection where item was created.211 ///212 /// * item_id: Id of an item. Unique within the collection.213 ///214 /// * recipient: Owner of newly created item215 ///216 /// * amount: Always 1 for NFT217 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),218219 /// Collection item was burned.220 ///221 /// # Arguments222 ///223 /// * collection_id.224 ///225 /// * item_id: Identifier of burned NFT.226 ///227 /// * owner: which user has destroyed its tokens228 ///229 /// * amount: Always 1 for NFT230 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),231232 /// Item was transferred233 ///234 /// * collection_id: Id of collection to which item is belong235 ///236 /// * item_id: Id of an item237 ///238 /// * sender: Original owner of item239 ///240 /// * recipient: New owner of item241 ///242 /// * amount: Always 1 for NFT243 Transfer(244 CollectionId,245 TokenId,246 T::CrossAccountId,247 T::CrossAccountId,248 u128,249 ),250251 /// * collection_id252 ///253 /// * item_id254 ///255 /// * sender256 ///257 /// * spender258 ///259 /// * amount260 Approved(261 CollectionId,262 TokenId,263 T::CrossAccountId,264 T::CrossAccountId,265 u128,266 ),267 }268269 #[pallet::error]270 pub enum Error<T> {271 /// This collection does not exist.272 CollectionNotFound,273 /// Sender parameter and item owner must be equal.274 MustBeTokenOwner,275 /// No permission to perform action276 NoPermission,277 /// Collection is not in mint mode.278 PublicMintingNotAllowed,279 /// Address is not in allow list.280 AddressNotInAllowlist,281282 /// Collection name can not be longer than 63 char.283 CollectionNameLimitExceeded,284 /// Collection description can not be longer than 255 char.285 CollectionDescriptionLimitExceeded,286 /// Token prefix can not be longer than 15 char.287 CollectionTokenPrefixLimitExceeded,288 /// Total collections bound exceeded.289 TotalCollectionsLimitExceeded,290 /// variable_data exceeded data limit.291 TokenVariableDataLimitExceeded,292 /// Exceeded max admin count293 CollectionAdminCountExceeded,294 /// Collection limit bounds per collection exceeded295 CollectionLimitBoundsExceeded,296 /// Tried to enable permissions which are only permitted to be disabled297 OwnerPermissionsCantBeReverted,298299 /// Collection settings not allowing items transferring300 TransferNotAllowed,301 /// Account token limit exceeded per collection302 AccountTokenLimitExceeded,303 /// Collection token limit exceeded304 CollectionTokenLimitExceeded,305 /// Metadata flag frozen306 MetadataFlagFrozen,307308 /// Item not exists.309 TokenNotFound,310 /// Item balance not enough.311 TokenValueTooLow,312 /// Requested value more than approved.313 ApprovedValueTooLow,314 /// Tried to approve more than owned315 CantApproveMoreThanOwned,316317 /// Can't transfer tokens to ethereum zero address318 AddressIsZero,319 /// Target collection doesn't supports this operation320 UnsupportedOperation,321 }322323 #[pallet::storage]324 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;325 #[pallet::storage]326 pub type DestroyedCollectionCount<T> =327 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;328329 /// Collection info330 #[pallet::storage]331 pub type CollectionById<T> = StorageMap<332 Hasher = Blake2_128Concat,333 Key = CollectionId,334 Value = Collection<<T as frame_system::Config>::AccountId>,335 QueryKind = OptionQuery,336 >;337338 #[pallet::storage]339 pub type AdminAmount<T> = StorageMap<340 Hasher = Blake2_128Concat,341 Key = CollectionId,342 Value = u32,343 QueryKind = ValueQuery,344 >;345346 /// List of collection admins347 #[pallet::storage]348 pub type IsAdmin<T: Config> = StorageNMap<349 Key = (350 Key<Blake2_128Concat, CollectionId>,351 Key<Blake2_128Concat, T::CrossAccountId>,352 ),353 Value = bool,354 QueryKind = ValueQuery,355 >;356357 /// Allowlisted collection users358 #[pallet::storage]359 pub type Allowlist<T: Config> = StorageNMap<360 Key = (361 Key<Blake2_128Concat, CollectionId>,362 Key<Blake2_128Concat, T::CrossAccountId>,363 ),364 Value = bool,365 QueryKind = ValueQuery,366 >;367368 /// Not used by code, exists only to provide some types to metadata369 #[pallet::storage]370 pub type DummyStorageValue<T> =371 StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;372}373374impl<T: Config> Pallet<T> {375 /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens376 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {377 ensure!(378 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,379 <Error<T>>::AddressIsZero380 );381 Ok(())382 }383 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {384 <IsAdmin<T>>::iter_prefix((collection,))385 .map(|(a, _)| a)386 .collect()387 }388 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {389 <Allowlist<T>>::iter_prefix((collection,))390 .map(|(a, _)| a)391 .collect()392 }393 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {394 <Allowlist<T>>::get((collection, user))395 }396 pub fn collection_stats() -> CollectionStats {397 let created = <CreatedCollectionCount<T>>::get();398 let destroyed = <DestroyedCollectionCount<T>>::get();399 CollectionStats {400 created: created.0,401 destroyed: destroyed.0,402 alive: created.0 - destroyed.0,403 }404 }405}406407impl<T: Config> Pallet<T> {408 pub fn init_collection(409 owner: T::AccountId,410 data: CreateCollectionData<T::AccountId>,411 ) -> Result<CollectionId, DispatchError> {412 {413 ensure!(414 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,415 Error::<T>::CollectionTokenPrefixLimitExceeded416 );417 }418419 let created_count = <CreatedCollectionCount<T>>::get()420 .0421 .checked_add(1)422 .ok_or(ArithmeticError::Overflow)?;423 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;424 let id = CollectionId(created_count);425426 // bound Total number of collections427 ensure!(428 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,429 <Error<T>>::TotalCollectionsLimitExceeded430 );431432 // =========433434 let collection = Collection {435 owner: owner.clone(),436 name: data.name,437 mode: data.mode.clone(),438 mint_mode: false,439 access: data.access.unwrap_or_default(),440 description: data.description,441 token_prefix: data.token_prefix,442 offchain_schema: data.offchain_schema,443 schema_version: data.schema_version.unwrap_or_default(),444 sponsorship: data445 .pending_sponsor446 .map(SponsorshipState::Unconfirmed)447 .unwrap_or_default(),448 variable_on_chain_schema: data.variable_on_chain_schema,449 const_on_chain_schema: data.const_on_chain_schema,450 limits: data451 .limits452 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))453 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,454 meta_update_permission: data.meta_update_permission.unwrap_or_default(),455 };456457 // Take a (non-refundable) deposit of collection creation458 {459 let mut imbalance =460 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();461 imbalance.subsume(462 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(463 &T::TreasuryAccountId::get(),464 T::CollectionCreationPrice::get(),465 ),466 );467 <T as Config>::Currency::settle(468 &owner,469 imbalance,470 WithdrawReasons::TRANSFER,471 ExistenceRequirement::KeepAlive,472 )473 .map_err(|_| Error::<T>::NoPermission)?;474 }475476 <CreatedCollectionCount<T>>::put(created_count);477 <Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));478 <CollectionById<T>>::insert(id, collection);479 Ok(id)480 }481482 pub fn destroy_collection(483 collection: CollectionHandle<T>,484 sender: &T::CrossAccountId,485 ) -> DispatchResult {486 ensure!(487 collection.limits.owner_can_destroy(),488 <Error<T>>::NoPermission,489 );490 collection.check_is_owner(sender)?;491492 let destroyed_collections = <DestroyedCollectionCount<T>>::get()493 .0494 .checked_add(1)495 .ok_or(ArithmeticError::Overflow)?;496497 // =========498499 <DestroyedCollectionCount<T>>::put(destroyed_collections);500 <CollectionById<T>>::remove(collection.id);501 <AdminAmount<T>>::remove(collection.id);502 <IsAdmin<T>>::remove_prefix((collection.id,), None);503 <Allowlist<T>>::remove_prefix((collection.id,), None);504505 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));506 Ok(())507 }508509 pub fn toggle_allowlist(510 collection: &CollectionHandle<T>,511 sender: &T::CrossAccountId,512 user: &T::CrossAccountId,513 allowed: bool,514 ) -> DispatchResult {515 collection.check_is_owner_or_admin(sender)?;516517 // =========518519 if allowed {520 <Allowlist<T>>::insert((collection.id, user), true);521 } else {522 <Allowlist<T>>::remove((collection.id, user));523 }524525 Ok(())526 }527528 pub fn toggle_admin(529 collection: &CollectionHandle<T>,530 sender: &T::CrossAccountId,531 user: &T::CrossAccountId,532 admin: bool,533 ) -> DispatchResult {534 collection.check_is_owner_or_admin(sender)?;535536 let was_admin = <IsAdmin<T>>::get((collection.id, user));537 if was_admin == admin {538 return Ok(());539 }540 let amount = <AdminAmount<T>>::get(collection.id);541542 if admin {543 let amount = amount544 .checked_add(1)545 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;546 ensure!(547 amount <= Self::collection_admins_limit(),548 <Error<T>>::CollectionAdminCountExceeded,549 );550551 // =========552553 <AdminAmount<T>>::insert(collection.id, amount);554 <IsAdmin<T>>::insert((collection.id, user), true);555 } else {556 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));557 <IsAdmin<T>>::remove((collection.id, user));558 }559560 Ok(())561 }562563 pub fn clamp_limits(564 mode: CollectionMode,565 old_limit: &CollectionLimits,566 mut new_limit: CollectionLimits,567 ) -> Result<CollectionLimits, DispatchError> {568 macro_rules! limit_default {569 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{570 $(571 if let Some($new) = $new.$field {572 let $old = $old.$field($($arg)?);573 let _ = $new;574 let _ = $old;575 $check576 } else {577 $new.$field = $old.$field578 }579 )*580 }};581 }582583 limit_default!(old_limit, new_limit,584 account_token_ownership_limit => ensure!(585 new_limit <= MAX_TOKEN_OWNERSHIP,586 <Error<T>>::CollectionLimitBoundsExceeded,587 ),588 sponsor_transfer_timeout(match mode {589 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,590 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,591 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,592 }) => ensure!(593 new_limit <= MAX_SPONSOR_TIMEOUT,594 <Error<T>>::CollectionLimitBoundsExceeded,595 ),596 sponsored_data_size => ensure!(597 new_limit <= CUSTOM_DATA_LIMIT,598 <Error<T>>::CollectionLimitBoundsExceeded,599 ),600 token_limit => ensure!(601 old_limit >= new_limit && new_limit > 0,602 <Error<T>>::CollectionTokenLimitExceeded603 ),604 owner_can_transfer => ensure!(605 old_limit || !new_limit,606 <Error<T>>::OwnerPermissionsCantBeReverted,607 ),608 owner_can_destroy => ensure!(609 old_limit || !new_limit,610 <Error<T>>::OwnerPermissionsCantBeReverted,611 ),612 sponsored_data_rate_limit => {},613 transfers_enabled => {},614 );615 Ok(new_limit)616 }617}618619#[macro_export]620macro_rules! unsupported {621 () => {622 Err(<Error<T>>::UnsupportedOperation.into())623 };624}625626/// Worst cases627pub trait CommonWeightInfo<CrossAccountId> {628 fn create_item() -> Weight;629 fn create_multiple_items(amount: u32) -> Weight;630 fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;631 fn burn_item() -> Weight;632 fn transfer() -> Weight;633 fn approve() -> Weight;634 fn transfer_from() -> Weight;635 fn burn_from() -> Weight;636 fn set_variable_metadata(bytes: u32) -> Weight;637}638639pub trait CommonCollectionOperations<T: Config> {640 fn create_item(641 &self,642 sender: T::CrossAccountId,643 to: T::CrossAccountId,644 data: CreateItemData,645 ) -> DispatchResultWithPostInfo;646 fn create_multiple_items(647 &self,648 sender: T::CrossAccountId,649 to: T::CrossAccountId,650 data: Vec<CreateItemData>,651 ) -> DispatchResultWithPostInfo;652 fn create_multiple_items_ex(653 &self,654 sender: T::CrossAccountId,655 data: CreateItemExData<T::CrossAccountId>,656 ) -> DispatchResultWithPostInfo;657 fn burn_item(658 &self,659 sender: T::CrossAccountId,660 token: TokenId,661 amount: u128,662 ) -> DispatchResultWithPostInfo;663664 fn transfer(665 &self,666 sender: T::CrossAccountId,667 to: T::CrossAccountId,668 token: TokenId,669 amount: u128,670 ) -> DispatchResultWithPostInfo;671 fn approve(672 &self,673 sender: T::CrossAccountId,674 spender: T::CrossAccountId,675 token: TokenId,676 amount: u128,677 ) -> DispatchResultWithPostInfo;678 fn transfer_from(679 &self,680 sender: T::CrossAccountId,681 from: T::CrossAccountId,682 to: T::CrossAccountId,683 token: TokenId,684 amount: u128,685 ) -> DispatchResultWithPostInfo;686 fn burn_from(687 &self,688 sender: T::CrossAccountId,689 from: T::CrossAccountId,690 token: TokenId,691 amount: u128,692 ) -> DispatchResultWithPostInfo;693694 fn set_variable_metadata(695 &self,696 sender: T::CrossAccountId,697 token: TokenId,698 data: BoundedVec<u8, CustomDataLimit>,699 ) -> DispatchResultWithPostInfo;700701 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;702 fn token_exists(&self, token: TokenId) -> bool;703 fn last_token_id(&self) -> TokenId;704705 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;706 fn const_metadata(&self, token: TokenId) -> Vec<u8>;707 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;708709 /// How many tokens collection contains (Applicable to nonfungible/refungible)710 fn collection_tokens(&self) -> u32;711 /// Amount of different tokens account has (Applicable to nonfungible/refungible)712 fn account_balance(&self, account: T::CrossAccountId) -> u32;713 /// Amount of specific token account have (Applicable to fungible/refungible)714 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;715 fn allowance(716 &self,717 sender: T::CrossAccountId,718 spender: T::CrossAccountId,719 token: TokenId,720 ) -> u128;721}722723// Flexible enough for implementing CommonCollectionOperations724pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {725 let post_info = PostDispatchInfo {726 actual_weight: Some(weight),727 pays_fee: Pays::Yes,728 };729 match res {730 Ok(()) => Ok(post_info),731 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),732 }733}1#![cfg_attr(not(feature = "std"), no_std)]23use core::ops::{Deref, DerefMut};4use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};5use sp_std::vec::Vec;6use account::CrossAccountId;7use frame_support::{8 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo},9 ensure, fail,10 traits::{Imbalance, Get, Currency},11 BoundedVec,12};13use pallet_evm::GasWeightMapping;14use up_data_structs::{15 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement,16 MAX_TOKEN_PREFIX_LENGTH, COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo,17 TokenId, Weight, WithdrawReasons, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode,18 NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,19 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,20 CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData,21};22pub use pallet::*;23use sp_core::H160;24use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};25pub mod account;26#[cfg(feature = "runtime-benchmarks")]27pub mod benchmarking;28pub mod erc;29pub mod eth;3031#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]32pub struct CollectionHandle<T: Config> {33 pub id: CollectionId,34 collection: Collection<T::AccountId>,35 pub recorder: SubstrateRecorder<T>,36}37impl<T: Config> WithRecorder<T> for CollectionHandle<T> {38 fn recorder(&self) -> &SubstrateRecorder<T> {39 &self.recorder40 }41 fn into_recorder(self) -> SubstrateRecorder<T> {42 self.recorder43 }44}45impl<T: Config> CollectionHandle<T> {46 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {47 <CollectionById<T>>::get(id).map(|collection| Self {48 id,49 collection,50 recorder: SubstrateRecorder::new(eth::collection_id_to_address(id), gas_limit),51 })52 }53 pub fn new(id: CollectionId) -> Option<Self> {54 Self::new_with_gas_limit(id, u64::MAX)55 }56 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {57 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)58 }59 pub fn log_mirrored(&self, log: impl evm_coder::ToLog) {60 self.recorder.log_mirrored(log)61 }62 pub fn log_direct(&self, log: impl evm_coder::ToLog) {63 self.recorder.log_direct(log)64 }65 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {66 self.recorder67 .consume_gas(T::GasWeightMapping::weight_to_gas(68 <T as frame_system::Config>::DbWeight::get()69 .read70 .saturating_mul(reads),71 ))72 }73 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {74 self.recorder75 .consume_gas(T::GasWeightMapping::weight_to_gas(76 <T as frame_system::Config>::DbWeight::get()77 .write78 .saturating_mul(writes),79 ))80 }81 pub fn submit_logs(self) {82 self.recorder.submit_logs()83 }84 pub fn save(self) -> DispatchResult {85 self.recorder.submit_logs();86 <CollectionById<T>>::insert(self.id, self.collection);87 Ok(())88 }89}90impl<T: Config> Deref for CollectionHandle<T> {91 type Target = Collection<T::AccountId>;9293 fn deref(&self) -> &Self::Target {94 &self.collection95 }96}9798impl<T: Config> DerefMut for CollectionHandle<T> {99 fn deref_mut(&mut self) -> &mut Self::Target {100 &mut self.collection101 }102}103104impl<T: Config> CollectionHandle<T> {105 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {106 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);107 Ok(())108 }109 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {110 *subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))111 }112 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {113 ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);114 Ok(())115 }116 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {117 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)118 }119 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {120 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)121 }122 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {123 ensure!(124 <Allowlist<T>>::get((self.id, user)),125 <Error<T>>::AddressNotInAllowlist126 );127 Ok(())128 }129130 pub fn check_can_update_meta(131 &self,132 subject: &T::CrossAccountId,133 item_owner: &T::CrossAccountId,134 ) -> DispatchResult {135 match self.meta_update_permission {136 MetaUpdatePermission::ItemOwner => {137 ensure!(subject == item_owner, <Error<T>>::NoPermission);138 Ok(())139 }140 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),141 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),142 }143 }144}145146#[frame_support::pallet]147pub mod pallet {148 use super::*;149 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};150 use account::CrossAccountId;151 use frame_support::traits::Currency;152 use up_data_structs::TokenId;153 use scale_info::TypeInfo;154155 #[pallet::config]156 pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo {157 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;158159 type CrossAccountId: CrossAccountId<Self::AccountId>;160161 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;162 type EvmBackwardsAddressMapping: up_evm_mapping::EvmBackwardsAddressMapping<Self::AccountId>;163164 type Currency: Currency<Self::AccountId>;165166 #[pallet::constant]167 type CollectionCreationPrice: Get<168 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,169 >;170171 type TreasuryAccountId: Get<Self::AccountId>;172 }173174 #[pallet::pallet]175 #[pallet::generate_store(pub(super) trait Store)]176 pub struct Pallet<T>(_);177178 #[pallet::extra_constants]179 impl<T: Config> Pallet<T> {180 pub fn collection_admins_limit() -> u32 {181 COLLECTION_ADMINS_LIMIT182 }183 }184185 #[pallet::event]186 #[pallet::generate_deposit(pub fn deposit_event)]187 pub enum Event<T: Config> {188 /// New collection was created189 ///190 /// # Arguments191 ///192 /// * collection_id: Globally unique identifier of newly created collection.193 ///194 /// * mode: [CollectionMode] converted into u8.195 ///196 /// * account_id: Collection owner.197 CollectionCreated(CollectionId, u8, T::AccountId),198199 /// New collection was destroyed200 ///201 /// # Arguments202 ///203 /// * collection_id: Globally unique identifier of collection.204 CollectionDestroyed(CollectionId),205206 /// New item was created.207 ///208 /// # Arguments209 ///210 /// * collection_id: Id of the collection where item was created.211 ///212 /// * item_id: Id of an item. Unique within the collection.213 ///214 /// * recipient: Owner of newly created item215 ///216 /// * amount: Always 1 for NFT217 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),218219 /// Collection item was burned.220 ///221 /// # Arguments222 ///223 /// * collection_id.224 ///225 /// * item_id: Identifier of burned NFT.226 ///227 /// * owner: which user has destroyed its tokens228 ///229 /// * amount: Always 1 for NFT230 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),231232 /// Item was transferred233 ///234 /// * collection_id: Id of collection to which item is belong235 ///236 /// * item_id: Id of an item237 ///238 /// * sender: Original owner of item239 ///240 /// * recipient: New owner of item241 ///242 /// * amount: Always 1 for NFT243 Transfer(244 CollectionId,245 TokenId,246 T::CrossAccountId,247 T::CrossAccountId,248 u128,249 ),250251 /// * collection_id252 ///253 /// * item_id254 ///255 /// * sender256 ///257 /// * spender258 ///259 /// * amount260 Approved(261 CollectionId,262 TokenId,263 T::CrossAccountId,264 T::CrossAccountId,265 u128,266 ),267 }268269 #[pallet::error]270 pub enum Error<T> {271 /// This collection does not exist.272 CollectionNotFound,273 /// Sender parameter and item owner must be equal.274 MustBeTokenOwner,275 /// No permission to perform action276 NoPermission,277 /// Collection is not in mint mode.278 PublicMintingNotAllowed,279 /// Address is not in allow list.280 AddressNotInAllowlist,281282 /// Collection name can not be longer than 63 char.283 CollectionNameLimitExceeded,284 /// Collection description can not be longer than 255 char.285 CollectionDescriptionLimitExceeded,286 /// Token prefix can not be longer than 15 char.287 CollectionTokenPrefixLimitExceeded,288 /// Total collections bound exceeded.289 TotalCollectionsLimitExceeded,290 /// variable_data exceeded data limit.291 TokenVariableDataLimitExceeded,292 /// Exceeded max admin count293 CollectionAdminCountExceeded,294 /// Collection limit bounds per collection exceeded295 CollectionLimitBoundsExceeded,296 /// Tried to enable permissions which are only permitted to be disabled297 OwnerPermissionsCantBeReverted,298299 /// Collection settings not allowing items transferring300 TransferNotAllowed,301 /// Account token limit exceeded per collection302 AccountTokenLimitExceeded,303 /// Collection token limit exceeded304 CollectionTokenLimitExceeded,305 /// Metadata flag frozen306 MetadataFlagFrozen,307308 /// Item not exists.309 TokenNotFound,310 /// Item balance not enough.311 TokenValueTooLow,312 /// Requested value more than approved.313 ApprovedValueTooLow,314 /// Tried to approve more than owned315 CantApproveMoreThanOwned,316317 /// Can't transfer tokens to ethereum zero address318 AddressIsZero,319 /// Target collection doesn't supports this operation320 UnsupportedOperation,321322 /// Not sufficient founds to perform action323 NotSufficientFounds,324 }325326 #[pallet::storage]327 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;328 #[pallet::storage]329 pub type DestroyedCollectionCount<T> =330 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;331332 /// Collection info333 #[pallet::storage]334 pub type CollectionById<T> = StorageMap<335 Hasher = Blake2_128Concat,336 Key = CollectionId,337 Value = Collection<<T as frame_system::Config>::AccountId>,338 QueryKind = OptionQuery,339 >;340341 #[pallet::storage]342 pub type AdminAmount<T> = StorageMap<343 Hasher = Blake2_128Concat,344 Key = CollectionId,345 Value = u32,346 QueryKind = ValueQuery,347 >;348349 /// List of collection admins350 #[pallet::storage]351 pub type IsAdmin<T: Config> = StorageNMap<352 Key = (353 Key<Blake2_128Concat, CollectionId>,354 Key<Blake2_128Concat, T::CrossAccountId>,355 ),356 Value = bool,357 QueryKind = ValueQuery,358 >;359360 /// Allowlisted collection users361 #[pallet::storage]362 pub type Allowlist<T: Config> = StorageNMap<363 Key = (364 Key<Blake2_128Concat, CollectionId>,365 Key<Blake2_128Concat, T::CrossAccountId>,366 ),367 Value = bool,368 QueryKind = ValueQuery,369 >;370371 /// Not used by code, exists only to provide some types to metadata372 #[pallet::storage]373 pub type DummyStorageValue<T> =374 StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;375}376377impl<T: Config> Pallet<T> {378 /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens379 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {380 ensure!(381 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,382 <Error<T>>::AddressIsZero383 );384 Ok(())385 }386 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {387 <IsAdmin<T>>::iter_prefix((collection,))388 .map(|(a, _)| a)389 .collect()390 }391 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {392 <Allowlist<T>>::iter_prefix((collection,))393 .map(|(a, _)| a)394 .collect()395 }396 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {397 <Allowlist<T>>::get((collection, user))398 }399 pub fn collection_stats() -> CollectionStats {400 let created = <CreatedCollectionCount<T>>::get();401 let destroyed = <DestroyedCollectionCount<T>>::get();402 CollectionStats {403 created: created.0,404 destroyed: destroyed.0,405 alive: created.0 - destroyed.0,406 }407 }408}409410impl<T: Config> Pallet<T> {411 pub fn init_collection(412 owner: T::AccountId,413 data: CreateCollectionData<T::AccountId>,414 ) -> Result<CollectionId, DispatchError> {415 {416 ensure!(417 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,418 Error::<T>::CollectionTokenPrefixLimitExceeded419 );420 }421422 let created_count = <CreatedCollectionCount<T>>::get()423 .0424 .checked_add(1)425 .ok_or(ArithmeticError::Overflow)?;426 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;427 let id = CollectionId(created_count);428429 // bound Total number of collections430 ensure!(431 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,432 <Error<T>>::TotalCollectionsLimitExceeded433 );434435 // =========436437 let collection = Collection {438 owner: owner.clone(),439 name: data.name,440 mode: data.mode.clone(),441 mint_mode: false,442 access: data.access.unwrap_or_default(),443 description: data.description,444 token_prefix: data.token_prefix,445 offchain_schema: data.offchain_schema,446 schema_version: data.schema_version.unwrap_or_default(),447 sponsorship: data448 .pending_sponsor449 .map(SponsorshipState::Unconfirmed)450 .unwrap_or_default(),451 variable_on_chain_schema: data.variable_on_chain_schema,452 const_on_chain_schema: data.const_on_chain_schema,453 limits: data454 .limits455 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))456 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,457 meta_update_permission: data.meta_update_permission.unwrap_or_default(),458 };459460 // Take a (non-refundable) deposit of collection creation461 {462 let mut imbalance =463 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();464 imbalance.subsume(465 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(466 &T::TreasuryAccountId::get(),467 T::CollectionCreationPrice::get(),468 ),469 );470 <T as Config>::Currency::settle(471 &owner,472 imbalance,473 WithdrawReasons::TRANSFER,474 ExistenceRequirement::KeepAlive,475 )476 .map_err(|_| Error::<T>::NotSufficientFounds)?;477 }478479 <CreatedCollectionCount<T>>::put(created_count);480 <Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));481 <CollectionById<T>>::insert(id, collection);482 Ok(id)483 }484485 pub fn destroy_collection(486 collection: CollectionHandle<T>,487 sender: &T::CrossAccountId,488 ) -> DispatchResult {489 ensure!(490 collection.limits.owner_can_destroy(),491 <Error<T>>::NoPermission,492 );493 collection.check_is_owner(sender)?;494495 let destroyed_collections = <DestroyedCollectionCount<T>>::get()496 .0497 .checked_add(1)498 .ok_or(ArithmeticError::Overflow)?;499500 // =========501502 <DestroyedCollectionCount<T>>::put(destroyed_collections);503 <CollectionById<T>>::remove(collection.id);504 <AdminAmount<T>>::remove(collection.id);505 <IsAdmin<T>>::remove_prefix((collection.id,), None);506 <Allowlist<T>>::remove_prefix((collection.id,), None);507508 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));509 Ok(())510 }511512 pub fn toggle_allowlist(513 collection: &CollectionHandle<T>,514 sender: &T::CrossAccountId,515 user: &T::CrossAccountId,516 allowed: bool,517 ) -> DispatchResult {518 collection.check_is_owner_or_admin(sender)?;519520 // =========521522 if allowed {523 <Allowlist<T>>::insert((collection.id, user), true);524 } else {525 <Allowlist<T>>::remove((collection.id, user));526 }527528 Ok(())529 }530531 pub fn toggle_admin(532 collection: &CollectionHandle<T>,533 sender: &T::CrossAccountId,534 user: &T::CrossAccountId,535 admin: bool,536 ) -> DispatchResult {537 collection.check_is_owner_or_admin(sender)?;538539 let was_admin = <IsAdmin<T>>::get((collection.id, user));540 if was_admin == admin {541 return Ok(());542 }543 let amount = <AdminAmount<T>>::get(collection.id);544545 if admin {546 let amount = amount547 .checked_add(1)548 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;549 ensure!(550 amount <= Self::collection_admins_limit(),551 <Error<T>>::CollectionAdminCountExceeded,552 );553554 // =========555556 <AdminAmount<T>>::insert(collection.id, amount);557 <IsAdmin<T>>::insert((collection.id, user), true);558 } else {559 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));560 <IsAdmin<T>>::remove((collection.id, user));561 }562563 Ok(())564 }565566 pub fn clamp_limits(567 mode: CollectionMode,568 old_limit: &CollectionLimits,569 mut new_limit: CollectionLimits,570 ) -> Result<CollectionLimits, DispatchError> {571 macro_rules! limit_default {572 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{573 $(574 if let Some($new) = $new.$field {575 let $old = $old.$field($($arg)?);576 let _ = $new;577 let _ = $old;578 $check579 } else {580 $new.$field = $old.$field581 }582 )*583 }};584 }585586 limit_default!(old_limit, new_limit,587 account_token_ownership_limit => ensure!(588 new_limit <= MAX_TOKEN_OWNERSHIP,589 <Error<T>>::CollectionLimitBoundsExceeded,590 ),591 sponsor_transfer_timeout(match mode {592 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,593 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,594 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,595 }) => ensure!(596 new_limit <= MAX_SPONSOR_TIMEOUT,597 <Error<T>>::CollectionLimitBoundsExceeded,598 ),599 sponsored_data_size => ensure!(600 new_limit <= CUSTOM_DATA_LIMIT,601 <Error<T>>::CollectionLimitBoundsExceeded,602 ),603 token_limit => ensure!(604 old_limit >= new_limit && new_limit > 0,605 <Error<T>>::CollectionTokenLimitExceeded606 ),607 owner_can_transfer => ensure!(608 old_limit || !new_limit,609 <Error<T>>::OwnerPermissionsCantBeReverted,610 ),611 owner_can_destroy => ensure!(612 old_limit || !new_limit,613 <Error<T>>::OwnerPermissionsCantBeReverted,614 ),615 sponsored_data_rate_limit => {},616 transfers_enabled => {},617 );618 Ok(new_limit)619 }620}621622#[macro_export]623macro_rules! unsupported {624 () => {625 Err(<Error<T>>::UnsupportedOperation.into())626 };627}628629/// Worst cases630pub trait CommonWeightInfo<CrossAccountId> {631 fn create_item() -> Weight;632 fn create_multiple_items(amount: u32) -> Weight;633 fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;634 fn burn_item() -> Weight;635 fn transfer() -> Weight;636 fn approve() -> Weight;637 fn transfer_from() -> Weight;638 fn burn_from() -> Weight;639 fn set_variable_metadata(bytes: u32) -> Weight;640}641642pub trait CommonCollectionOperations<T: Config> {643 fn create_item(644 &self,645 sender: T::CrossAccountId,646 to: T::CrossAccountId,647 data: CreateItemData,648 ) -> DispatchResultWithPostInfo;649 fn create_multiple_items(650 &self,651 sender: T::CrossAccountId,652 to: T::CrossAccountId,653 data: Vec<CreateItemData>,654 ) -> DispatchResultWithPostInfo;655 fn create_multiple_items_ex(656 &self,657 sender: T::CrossAccountId,658 data: CreateItemExData<T::CrossAccountId>,659 ) -> DispatchResultWithPostInfo;660 fn burn_item(661 &self,662 sender: T::CrossAccountId,663 token: TokenId,664 amount: u128,665 ) -> DispatchResultWithPostInfo;666667 fn transfer(668 &self,669 sender: T::CrossAccountId,670 to: T::CrossAccountId,671 token: TokenId,672 amount: u128,673 ) -> DispatchResultWithPostInfo;674 fn approve(675 &self,676 sender: T::CrossAccountId,677 spender: T::CrossAccountId,678 token: TokenId,679 amount: u128,680 ) -> DispatchResultWithPostInfo;681 fn transfer_from(682 &self,683 sender: T::CrossAccountId,684 from: T::CrossAccountId,685 to: T::CrossAccountId,686 token: TokenId,687 amount: u128,688 ) -> DispatchResultWithPostInfo;689 fn burn_from(690 &self,691 sender: T::CrossAccountId,692 from: T::CrossAccountId,693 token: TokenId,694 amount: u128,695 ) -> DispatchResultWithPostInfo;696697 fn set_variable_metadata(698 &self,699 sender: T::CrossAccountId,700 token: TokenId,701 data: BoundedVec<u8, CustomDataLimit>,702 ) -> DispatchResultWithPostInfo;703704 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;705 fn token_exists(&self, token: TokenId) -> bool;706 fn last_token_id(&self) -> TokenId;707708 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;709 fn const_metadata(&self, token: TokenId) -> Vec<u8>;710 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;711712 /// How many tokens collection contains (Applicable to nonfungible/refungible)713 fn collection_tokens(&self) -> u32;714 /// Amount of different tokens account has (Applicable to nonfungible/refungible)715 fn account_balance(&self, account: T::CrossAccountId) -> u32;716 /// Amount of specific token account have (Applicable to fungible/refungible)717 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;718 fn allowance(719 &self,720 sender: T::CrossAccountId,721 spender: T::CrossAccountId,722 token: TokenId,723 ) -> u128;724}725726// Flexible enough for implementing CommonCollectionOperations727pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {728 let post_info = PostDispatchInfo {729 actual_weight: Some(weight),730 pays_fee: Pays::Yes,731 };732 match res {733 Ok(()) => Ok(post_info),734 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),735 }736}pallets/unique/src/mock.rsdiffbeforeafterboth--- a/pallets/unique/src/mock.rs
+++ b/pallets/unique/src/mock.rs
@@ -108,7 +108,7 @@
}
parameter_types! {
- pub const CollectionCreationPrice: u32 = 0;
+ pub const CollectionCreationPrice: u32 = 100;
pub TreasuryAccountId: u64 = 1234;
pub EthereumChainId: u32 = 1111;
}
pallets/unique/src/tests.rsdiffbeforeafterboth--- a/pallets/unique/src/tests.rs
+++ b/pallets/unique/src/tests.rs
@@ -7,9 +7,26 @@
CreateReFungibleData, MAX_DECIMAL_POINTS, COLLECTION_ADMINS_LIMIT, MetaUpdatePermission,
TokenId, MAX_TOKEN_OWNERSHIP,
};
-use frame_support::{assert_noop, assert_ok};
+use frame_support::{assert_noop, assert_ok, assert_err};
use sp_std::convert::TryInto;
+use pallet_balances;
+fn add_balance(user: u64, value: u64) {
+ const DONOR_USER: u64 = 999;
+ assert_ok!(<pallet_balances::Pallet<Test>>::set_balance(
+ Origin::root(),
+ DONOR_USER,
+ value,
+ 0
+ ));
+ assert_ok!(<pallet_balances::Pallet<Test>>::force_transfer(
+ Origin::root(),
+ DONOR_USER,
+ user,
+ value
+ ));
+}
+
fn default_nft_data() -> CreateNftData {
CreateNftData {
const_data: vec![1, 2, 3].try_into().unwrap(),
@@ -34,6 +51,8 @@
owner: u64,
id: CollectionId,
) -> CollectionId {
+ add_balance(owner, CollectionCreationPrice::get() as u64 + 1);
+
let col_name1: Vec<u16> = "Test1\0".encode_utf16().collect::<Vec<u16>>();
let col_desc1: Vec<u16> = "TestDescription1\0".encode_utf16().collect::<Vec<u16>>();
let token_prefix1: Vec<u8> = b"token_prefix1\0".to_vec();
@@ -121,6 +140,30 @@
}
#[test]
+fn check_not_sufficient_founds() {
+ new_test_ext().execute_with(|| {
+ let acc: u64 = 1;
+ <pallet_balances::Pallet<Test>>::set_balance(Origin::root(), acc, 0, 0).unwrap();
+
+ let name: Vec<u16> = "Test1\0".encode_utf16().collect::<Vec<u16>>();
+ let description: Vec<u16> = "TestDescription1\0".encode_utf16().collect::<Vec<u16>>();
+ let token_prefix: Vec<u8> = b"token_prefix1\0".to_vec();
+
+ let data: CreateCollectionData<<Test as system::Config>::AccountId> =
+ CreateCollectionData {
+ name: name.try_into().unwrap(),
+ description: description.try_into().unwrap(),
+ token_prefix: token_prefix.try_into().unwrap(),
+ mode: CollectionMode::NFT,
+ ..Default::default()
+ };
+
+ let result = TemplateModule::create_collection_ex(Origin::signed(acc), data);
+ assert_err!(result, <CommonError<Test>>::NotSufficientFounds);
+ });
+}
+
+#[test]
fn create_fungible_collection_fails_with_large_decimal_numbers() {
new_test_ext().execute_with(|| {
let col_name1: Vec<u16> = "Test1\0".encode_utf16().collect::<Vec<u16>>();