1#![cfg_attr(not(feature = "std"), no_std)]23use core::ops::{Deref, DerefMut};4use sp_std::vec::Vec;5use account::CrossAccountId;6use frame_support::{7 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo},8 ensure, fail,9 traits::{Imbalance, Get, Currency},10};11use nft_data_structs::{12 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement,13 MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_COLLECTION_NAME_LENGTH, MAX_TOKEN_PREFIX_LENGTH,14 COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo, TokenId, Weight,15 WithdrawReasons, CollectionStats,16};17pub use pallet::*;18use sp_core::H160;19use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};20pub mod account;21#[cfg(feature = "runtime-benchmarks")]22pub mod benchmarking;23pub mod erc;24pub mod eth;2526#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]27pub struct CollectionHandle<T: Config> {28 pub id: CollectionId,29 collection: Collection<T::AccountId>,30 pub recorder: pallet_evm_coder_substrate::SubstrateRecorder<T>,31}32impl<T: Config> CollectionHandle<T> {33 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {34 <CollectionById<T>>::get(id).map(|collection| Self {35 id,36 collection,37 recorder: pallet_evm_coder_substrate::SubstrateRecorder::new(38 eth::collection_id_to_address(id),39 gas_limit,40 ),41 })42 }43 pub fn new(id: CollectionId) -> Option<Self> {44 Self::new_with_gas_limit(id, u64::MAX)45 }46 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {47 Ok(Self::new(id).ok_or_else(|| <Error<T>>::CollectionNotFound)?)48 }49 pub fn log(&self, log: impl evm_coder::ToLog) -> DispatchResult {50 self.recorder.log_sub(log)51 }52 pub fn log_infallible(&self, log: impl evm_coder::ToLog) {53 self.recorder.log_infallible(log)54 }55 #[allow(dead_code)]56 fn consume_gas(&self, gas: u64) -> DispatchResult {57 self.recorder.consume_gas_sub(gas)58 }59 pub fn consume_sload(&self) -> DispatchResult {60 self.recorder.consume_sload_sub()61 }62 pub fn consume_sstores(&self, amount: usize) -> DispatchResult {63 self.recorder.consume_sstores_sub(amount)64 }65 pub fn consume_sstore(&self) -> DispatchResult {66 self.recorder.consume_sstore_sub()67 }68 pub fn consume_log(&self, topics: usize, data: usize) -> DispatchResult {69 self.recorder.consume_log_sub(topics, data)70 }71 pub fn submit_logs(self) -> DispatchResult {72 self.recorder.submit_logs()73 }74 pub fn save(self) -> DispatchResult {75 self.recorder.submit_logs()?;76 <CollectionById<T>>::insert(self.id, self.collection);77 Ok(())78 }79}80impl<T: Config> Deref for CollectionHandle<T> {81 type Target = Collection<T::AccountId>;8283 fn deref(&self) -> &Self::Target {84 &self.collection85 }86}8788impl<T: Config> DerefMut for CollectionHandle<T> {89 fn deref_mut(&mut self) -> &mut Self::Target {90 &mut self.collection91 }92}9394impl<T: Config> CollectionHandle<T> {95 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {96 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);97 Ok(())98 }99 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> Result<bool, DispatchError> {100 self.consume_sload()?;101102 Ok(*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject)))103 }104 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {105 ensure!(self.is_owner_or_admin(subject)?, <Error<T>>::NoPermission);106 Ok(())107 }108 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> Result<bool, DispatchError> {109 Ok(self.limits.owner_can_transfer() && self.is_owner_or_admin(user)?)110 }111 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> Result<bool, DispatchError> {112 Ok(self.limits.owner_can_transfer() && self.is_owner_or_admin(user)?)113 }114 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {115 self.consume_sload()?;116117 ensure!(118 <Allowlist<T>>::get((self.id, user)),119 <Error<T>>::AddressNotInAllowlist120 );121 Ok(())122 }123124 pub fn check_can_update_meta(125 &self,126 subject: &T::CrossAccountId,127 item_owner: &T::CrossAccountId,128 ) -> DispatchResult {129 match self.meta_update_permission {130 MetaUpdatePermission::ItemOwner => {131 ensure!(subject == item_owner, <Error<T>>::NoPermission);132 Ok(())133 }134 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),135 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),136 }137 }138}139140#[frame_support::pallet]141pub mod pallet {142 use super::*;143 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};144 use account::{EvmBackwardsAddressMapping, CrossAccountId};145 use frame_support::traits::Currency;146 use nft_data_structs::TokenId;147 use scale_info::TypeInfo;148149 #[pallet::config]150 pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo {151 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;152153 type CrossAccountId: CrossAccountId<Self::AccountId>;154155 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;156 type EvmBackwardsAddressMapping: EvmBackwardsAddressMapping<Self::AccountId>;157158 type Currency: Currency<Self::AccountId>;159 type CollectionCreationPrice: Get<160 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,161 >;162 type TreasuryAccountId: Get<Self::AccountId>;163 }164165 #[pallet::pallet]166 #[pallet::generate_store(pub(super) trait Store)]167 pub struct Pallet<T>(_);168169 #[pallet::extra_constants]170 impl<T: Config> Pallet<T> {171 pub fn collection_admins_limit() -> u32 {172 COLLECTION_ADMINS_LIMIT173 }174 }175176 #[pallet::event]177 #[pallet::generate_deposit(pub fn deposit_event)]178 pub enum Event<T: Config> {179 180 181 182 183 184 185 186 187 188 CollectionCreated(CollectionId, u8, T::AccountId),189190 191 192 193 194 195 196 197 198 199 200 201 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),202203 204 205 206 207 208 209 210 211 212 213 214 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),215216 217 218 219 220 221 222 223 224 225 226 227 Transfer(228 CollectionId,229 TokenId,230 T::CrossAccountId,231 T::CrossAccountId,232 u128,233 ),234235 236 237 238 239 240 241 242 243 244 Approved(245 CollectionId,246 TokenId,247 T::CrossAccountId,248 T::CrossAccountId,249 u128,250 ),251 }252253 #[pallet::error]254 pub enum Error<T> {255 256 CollectionNotFound,257 258 MustBeTokenOwner,259 260 NoPermission,261 262 PublicMintingNotAllowed,263 264 AddressNotInAllowlist,265266 267 CollectionNameLimitExceeded,268 269 CollectionDescriptionLimitExceeded,270 271 CollectionTokenPrefixLimitExceeded,272 273 TotalCollectionsLimitExceeded,274 275 TokenVariableDataLimitExceeded,276 277 CollectionAdminAmountExceeded,278279 280 TransferNotAllowed,281 282 AccountTokenLimitExceeded,283 284 CollectionTokenLimitExceeded,285 286 MetadataFlagFrozen,287288 289 TokenNotFound,290 291 TokenValueTooLow,292 293 TokenValueNotEnough,294 295 CantApproveMoreThanOwned,296297 298 AddressIsZero,299 300 UnsupportedOperation,301 }302303 #[pallet::storage]304 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;305 #[pallet::storage]306 pub type DestroyedCollectionCount<T> =307 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;308309 310 #[pallet::storage]311 pub type CollectionById<T> = StorageMap<312 Hasher = Blake2_128Concat,313 Key = CollectionId,314 Value = Collection<<T as frame_system::Config>::AccountId>,315 QueryKind = OptionQuery,316 >;317318 #[pallet::storage]319 pub type AdminAmount<T> = StorageMap<320 Hasher = Blake2_128Concat,321 Key = CollectionId,322 Value = u32,323 QueryKind = ValueQuery,324 >;325326 327 #[pallet::storage]328 pub type IsAdmin<T: Config> = StorageNMap<329 Key = (330 Key<Blake2_128Concat, CollectionId>,331 Key<Blake2_128Concat, T::CrossAccountId>,332 ),333 Value = bool,334 QueryKind = ValueQuery,335 >;336337 338 #[pallet::storage]339 pub type Allowlist<T: Config> = StorageNMap<340 Key = (341 Key<Blake2_128Concat, CollectionId>,342 Key<Blake2_128Concat, T::CrossAccountId>,343 ),344 Value = bool,345 QueryKind = ValueQuery,346 >;347348 349 #[pallet::storage]350 pub type DummyStorageValue<T> = StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;351}352353impl<T: Config> Pallet<T> {354 355 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {356 ensure!(357 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,358 <Error<T>>::AddressIsZero359 );360 Ok(())361 }362 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {363 <IsAdmin<T>>::iter_prefix((collection,))364 .map(|(a, _)| a)365 .collect()366 }367 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {368 <Allowlist<T>>::iter_prefix((collection,))369 .map(|(a, _)| a)370 .collect()371 }372 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {373 <Allowlist<T>>::get((collection, user))374 }375 pub fn collection_stats() -> CollectionStats {376 let created = <CreatedCollectionCount<T>>::get();377 let destroyed = <DestroyedCollectionCount<T>>::get();378 CollectionStats {379 created: created.0,380 destroyed: destroyed.0,381 alive: created.0 - destroyed.0,382 }383 }384}385386impl<T: Config> Pallet<T> {387 pub fn init_collection(data: Collection<T::AccountId>) -> Result<CollectionId, DispatchError> {388 {389 ensure!(390 data.name.len() <= MAX_COLLECTION_NAME_LENGTH,391 Error::<T>::CollectionNameLimitExceeded392 );393 ensure!(394 data.description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH,395 Error::<T>::CollectionDescriptionLimitExceeded396 );397 ensure!(398 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH,399 Error::<T>::CollectionTokenPrefixLimitExceeded400 );401 }402403 let created_count = <CreatedCollectionCount<T>>::get()404 .0405 .checked_add(1)406 .ok_or(ArithmeticError::Overflow)?;407 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;408 let id = CollectionId(created_count);409410 411 ensure!(412 created_count - destroyed_count < COLLECTION_NUMBER_LIMIT,413 <Error<T>>::TotalCollectionsLimitExceeded414 );415416 417418 419 {420 let mut imbalance =421 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();422 imbalance.subsume(423 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(424 &T::TreasuryAccountId::get(),425 T::CollectionCreationPrice::get(),426 ),427 );428 <T as Config>::Currency::settle(429 &data.owner,430 imbalance,431 WithdrawReasons::TRANSFER,432 ExistenceRequirement::KeepAlive,433 )434 .map_err(|_| Error::<T>::NoPermission)?;435 }436437 <CreatedCollectionCount<T>>::put(created_count);438 <Pallet<T>>::deposit_event(Event::CollectionCreated(439 id,440 data.mode.id(),441 data.owner.clone(),442 ));443 <CollectionById<T>>::insert(id, data);444 Ok(id)445 }446447 pub fn destroy_collection(448 collection: CollectionHandle<T>,449 sender: &T::CrossAccountId,450 ) -> DispatchResult {451 ensure!(452 collection.limits.owner_can_destroy(),453 <Error<T>>::NoPermission,454 );455 collection.check_is_owner(&sender)?;456457 let destroyed_collections = <DestroyedCollectionCount<T>>::get()458 .0459 .checked_add(1)460 .ok_or(ArithmeticError::Overflow)?;461462 463464 <DestroyedCollectionCount<T>>::put(destroyed_collections);465 <CollectionById<T>>::remove(collection.id);466 <AdminAmount<T>>::remove(collection.id);467 <IsAdmin<T>>::remove_prefix((collection.id,), None);468 <Allowlist<T>>::remove_prefix((collection.id,), None);469 Ok(())470 }471472 pub fn toggle_allowlist(473 collection: &CollectionHandle<T>,474 sender: &T::CrossAccountId,475 user: &T::CrossAccountId,476 allowed: bool,477 ) -> DispatchResult {478 collection.check_is_owner_or_admin(&sender)?;479480 481482 if allowed {483 <Allowlist<T>>::insert((collection.id, user), true);484 } else {485 <Allowlist<T>>::remove((collection.id, user));486 }487488 Ok(())489 }490491 pub fn toggle_admin(492 collection: &CollectionHandle<T>,493 sender: &T::CrossAccountId,494 user: &T::CrossAccountId,495 admin: bool,496 ) -> DispatchResult {497 collection.check_is_owner_or_admin(&sender)?;498499 let was_admin = <IsAdmin<T>>::get((collection.id, user));500 if was_admin == admin {501 return Ok(());502 }503 let amount = <AdminAmount<T>>::get(collection.id);504505 if admin {506 let amount = amount507 .checked_add(1)508 .ok_or(<Error<T>>::CollectionAdminAmountExceeded)?;509 ensure!(510 amount <= Self::collection_admins_limit(),511 <Error<T>>::CollectionAdminAmountExceeded,512 );513514 515516 <AdminAmount<T>>::insert(collection.id, amount);517 <IsAdmin<T>>::insert((collection.id, user), true);518 } else {519 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));520 <IsAdmin<T>>::remove((collection.id, user));521 }522523 Ok(())524 }525}526527#[macro_export]528macro_rules! unsupported {529 () => {530 Err(<Error<T>>::UnsupportedOperation.into())531 };532}533534535pub trait CommonWeightInfo {536 fn create_item() -> Weight;537 fn create_multiple_items(amount: u32) -> Weight;538 fn burn_item() -> Weight;539 fn transfer() -> Weight;540 fn approve() -> Weight;541 fn transfer_from() -> Weight;542 fn burn_from() -> Weight;543 fn set_variable_metadata(bytes: u32) -> Weight;544}545546pub trait CommonCollectionOperations<T: Config> {547 fn create_item(548 &self,549 sender: T::CrossAccountId,550 to: T::CrossAccountId,551 data: CreateItemData,552 ) -> DispatchResultWithPostInfo;553 fn create_multiple_items(554 &self,555 sender: T::CrossAccountId,556 to: T::CrossAccountId,557 data: Vec<CreateItemData>,558 ) -> DispatchResultWithPostInfo;559 fn burn_item(560 &self,561 sender: T::CrossAccountId,562 token: TokenId,563 amount: u128,564 ) -> DispatchResultWithPostInfo;565566 fn transfer(567 &self,568 sender: T::CrossAccountId,569 to: T::CrossAccountId,570 token: TokenId,571 amount: u128,572 ) -> DispatchResultWithPostInfo;573 fn approve(574 &self,575 sender: T::CrossAccountId,576 spender: T::CrossAccountId,577 token: TokenId,578 amount: u128,579 ) -> DispatchResultWithPostInfo;580 fn transfer_from(581 &self,582 sender: T::CrossAccountId,583 from: T::CrossAccountId,584 to: T::CrossAccountId,585 token: TokenId,586 amount: u128,587 ) -> DispatchResultWithPostInfo;588 fn burn_from(589 &self,590 sender: T::CrossAccountId,591 from: T::CrossAccountId,592 token: TokenId,593 amount: u128,594 ) -> DispatchResultWithPostInfo;595596 fn set_variable_metadata(597 &self,598 sender: T::CrossAccountId,599 token: TokenId,600 data: Vec<u8>,601 ) -> DispatchResultWithPostInfo;602603 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;604 fn token_exists(&self, token: TokenId) -> bool;605 fn last_token_id(&self) -> TokenId;606607 fn token_owner(&self, token: TokenId) -> T::CrossAccountId;608 fn const_metadata(&self, token: TokenId) -> Vec<u8>;609 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;610611 612 fn collection_tokens(&self) -> u32;613 614 fn account_balance(&self, account: T::CrossAccountId) -> u32;615 616 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;617 fn allowance(618 &self,619 sender: T::CrossAccountId,620 spender: T::CrossAccountId,621 token: TokenId,622 ) -> u128;623}624625626pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {627 let post_info = PostDispatchInfo {628 actual_weight: Some(weight),629 pays_fee: Pays::Yes,630 };631 match res {632 Ok(()) => Ok(post_info),633 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),634 }635}