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 MetaUpdatePermission, Pays, PostDispatchInfo, TokenId, Weight, WithdrawReasons,15};16pub use pallet::*;17use sp_core::H160;18use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};19pub mod account;20#[cfg(feature = "runtime-benchmarks")]21pub mod benchmarking;22pub mod erc;23pub mod eth;2425#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]26pub struct CollectionHandle<T: Config> {27 pub id: CollectionId,28 collection: Collection<T>,29 pub recorder: pallet_evm_coder_substrate::SubstrateRecorder<T>,30}31impl<T: Config> CollectionHandle<T> {32 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {33 <CollectionById<T>>::get(id).map(|collection| Self {34 id,35 collection,36 recorder: pallet_evm_coder_substrate::SubstrateRecorder::new(37 eth::collection_id_to_address(id),38 gas_limit,39 ),40 })41 }42 pub fn new(id: CollectionId) -> Option<Self> {43 Self::new_with_gas_limit(id, u64::MAX)44 }45 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {46 Ok(Self::new(id).ok_or_else(|| <Error<T>>::CollectionNotFound)?)47 }48 pub fn log(&self, log: impl evm_coder::ToLog) -> DispatchResult {49 self.recorder.log_sub(log)50 }51 pub fn log_infallible(&self, log: impl evm_coder::ToLog) {52 self.recorder.log_infallible(log)53 }54 #[allow(dead_code)]55 fn consume_gas(&self, gas: u64) -> DispatchResult {56 self.recorder.consume_gas_sub(gas)57 }58 pub fn consume_sload(&self) -> DispatchResult {59 self.recorder.consume_sload_sub()60 }61 pub fn consume_sstores(&self, amount: usize) -> DispatchResult {62 self.recorder.consume_sstores_sub(amount)63 }64 pub fn consume_sstore(&self) -> DispatchResult {65 self.recorder.consume_sstore_sub()66 }67 pub fn consume_log(&self, topics: usize, data: usize) -> DispatchResult {68 self.recorder.consume_log_sub(topics, data)69 }70 pub fn submit_logs(self) -> DispatchResult {71 self.recorder.submit_logs()72 }73 pub fn save(self) -> DispatchResult {74 self.recorder.submit_logs()?;75 <CollectionById<T>>::insert(self.id, self.collection);76 Ok(())77 }78}79impl<T: Config> Deref for CollectionHandle<T> {80 type Target = Collection<T>;8182 fn deref(&self) -> &Self::Target {83 &self.collection84 }85}8687impl<T: Config> DerefMut for CollectionHandle<T> {88 fn deref_mut(&mut self) -> &mut Self::Target {89 &mut self.collection90 }91}9293impl<T: Config> CollectionHandle<T> {94 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {95 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);96 Ok(())97 }98 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> Result<bool, DispatchError> {99 self.consume_sload()?;100101 Ok(*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject.as_sub())))102 }103 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {104 ensure!(self.is_owner_or_admin(subject)?, <Error<T>>::NoPermission);105 Ok(())106 }107 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> Result<bool, DispatchError> {108 Ok(self.limits.owner_can_transfer() && self.is_owner_or_admin(user)?)109 }110 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> Result<bool, DispatchError> {111 Ok(self.limits.owner_can_transfer() && self.is_owner_or_admin(user)?)112 }113 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {114 self.consume_sload()?;115116 ensure!(117 <Allowlist<T>>::get((self.id, user.as_sub())),118 <Error<T>>::AddressNotInAllowlist119 );120 Ok(())121 }122123 pub fn check_can_update_meta(124 &self,125 subject: &T::CrossAccountId,126 item_owner: &T::CrossAccountId,127 ) -> DispatchResult {128 match self.meta_update_permission {129 MetaUpdatePermission::ItemOwner => {130 ensure!(subject == item_owner, <Error<T>>::NoPermission);131 Ok(())132 }133 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),134 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),135 }136 }137}138139#[frame_support::pallet]140pub mod pallet {141 use super::*;142 use frame_support::{pallet_prelude::*};143 use frame_support::{Blake2_128Concat, 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::event]170 #[pallet::generate_deposit(pub fn deposit_event)]171 pub enum Event<T: Config> {172 173 174 175 176 177 178 179 180 181 CollectionCreated(CollectionId, u8, T::AccountId),182183 184 185 186 187 188 189 190 191 192 193 194 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),195196 197 198 199 200 201 202 203 204 205 206 207 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),208209 210 211 212 213 214 215 216 217 218 219 220 Transfer(221 CollectionId,222 TokenId,223 T::CrossAccountId,224 T::CrossAccountId,225 u128,226 ),227228 229 230 231 232 233 234 235 236 237 Approved(238 CollectionId,239 TokenId,240 T::CrossAccountId,241 T::CrossAccountId,242 u128,243 ),244 }245246 #[pallet::error]247 pub enum Error<T> {248 249 CollectionNotFound,250 251 MustBeTokenOwner,252 253 NoPermission,254 255 PublicMintingNotAllowed,256 257 AddressNotInAllowlist,258259 260 CollectionNameLimitExceeded,261 262 CollectionDescriptionLimitExceeded,263 264 CollectionTokenPrefixLimitExceeded,265 266 TotalCollectionsLimitExceeded,267 268 TokenVariableDataLimitExceeded,269270 271 TransferNotAllowed,272 273 AccountTokenLimitExceeded,274 275 CollectionTokenLimitExceeded,276 277 MetadataFlagFrozen,278279 280 TokenNotFound,281 282 TokenValueTooLow,283 284 TokenValueNotEnough,285 286 CantApproveMoreThanOwned,287288 289 AddressIsZero,290 291 UnsupportedOperation,292 }293294 #[pallet::storage]295 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;296 #[pallet::storage]297 pub type DestroyedCollectionCount<T> =298 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;299300 301 #[pallet::storage]302 pub type CollectionById<T> = StorageMap<303 Hasher = Blake2_128Concat,304 Key = CollectionId,305 Value = Collection<T>,306 QueryKind = OptionQuery,307 >;308309 310 #[pallet::storage]311 pub type IsAdmin<T: Config> = StorageNMap<312 Key = (313 Key<Blake2_128Concat, CollectionId>,314 Key<Blake2_128Concat, T::AccountId>,315 ),316 Value = bool,317 QueryKind = ValueQuery,318 >;319320 321 #[pallet::storage]322 pub type Allowlist<T: Config> = StorageNMap<323 Key = (324 Key<Blake2_128Concat, CollectionId>,325 Key<Blake2_128Concat, T::AccountId>,326 ),327 Value = bool,328 QueryKind = ValueQuery,329 >;330}331332impl<T: Config> Pallet<T> {333 334 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {335 ensure!(336 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,337 <Error<T>>::AddressIsZero338 );339 Ok(())340 }341}342343impl<T: Config> Pallet<T> {344 pub fn init_collection(data: Collection<T>) -> Result<CollectionId, DispatchError> {345 {346 ensure!(347 data.name.len() <= MAX_COLLECTION_NAME_LENGTH,348 Error::<T>::CollectionNameLimitExceeded349 );350 ensure!(351 data.description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH,352 Error::<T>::CollectionDescriptionLimitExceeded353 );354 ensure!(355 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH,356 Error::<T>::CollectionTokenPrefixLimitExceeded357 );358 }359360 let created_count = <CreatedCollectionCount<T>>::get()361 .0362 .checked_add(1)363 .ok_or(ArithmeticError::Overflow)?;364 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;365 let id = CollectionId(created_count);366367 368 ensure!(369 created_count - destroyed_count < COLLECTION_NUMBER_LIMIT,370 <Error<T>>::TotalCollectionsLimitExceeded371 );372373 374375 376 {377 let mut imbalance =378 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();379 imbalance.subsume(380 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(381 &T::TreasuryAccountId::get(),382 T::CollectionCreationPrice::get(),383 ),384 );385 <T as Config>::Currency::settle(386 &data.owner,387 imbalance,388 WithdrawReasons::TRANSFER,389 ExistenceRequirement::KeepAlive,390 )391 .map_err(|_| Error::<T>::NoPermission)?;392 }393394 <CreatedCollectionCount<T>>::put(created_count);395 <Pallet<T>>::deposit_event(Event::CollectionCreated(396 id,397 data.mode.id(),398 data.owner.clone(),399 ));400 <CollectionById<T>>::insert(id, data);401 Ok(id)402 }403404 pub fn destroy_collection(405 collection: CollectionHandle<T>,406 sender: &T::CrossAccountId,407 ) -> DispatchResult {408 ensure!(409 collection.limits.owner_can_destroy(),410 <Error<T>>::NoPermission,411 );412 collection.check_is_owner(&sender)?;413414 let destroyed_collections = <DestroyedCollectionCount<T>>::get()415 .0416 .checked_add(1)417 .ok_or(ArithmeticError::Overflow)?;418419 420421 <DestroyedCollectionCount<T>>::put(destroyed_collections);422 <CollectionById<T>>::remove(collection.id);423 <IsAdmin<T>>::remove_prefix((collection.id,), None);424 <Allowlist<T>>::remove_prefix((collection.id,), None);425 Ok(())426 }427428 pub fn toggle_allowlist(429 collection: &CollectionHandle<T>,430 sender: &T::CrossAccountId,431 user: &T::CrossAccountId,432 allowed: bool,433 ) -> DispatchResult {434 collection.check_is_owner_or_admin(&sender)?;435436 437438 if allowed {439 <Allowlist<T>>::insert((collection.id, user.as_sub()), true);440 } else {441 <Allowlist<T>>::remove((collection.id, user.as_sub()));442 }443444 Ok(())445 }446}447448#[macro_export]449macro_rules! unsupported {450 () => {451 Err(<Error<T>>::UnsupportedOperation.into())452 };453}454455456pub trait CommonWeightInfo {457 fn create_item() -> Weight;458 fn create_multiple_items(amount: u32) -> Weight;459 fn burn_item() -> Weight;460 fn transfer() -> Weight;461 fn approve() -> Weight;462 fn transfer_from() -> Weight;463 fn burn_from() -> Weight;464 fn set_variable_metadata(bytes: u32) -> Weight;465}466467pub trait CommonCollectionOperations<T: Config> {468 fn create_item(469 &self,470 sender: T::CrossAccountId,471 to: T::CrossAccountId,472 data: CreateItemData,473 ) -> DispatchResultWithPostInfo;474 fn create_multiple_items(475 &self,476 sender: T::CrossAccountId,477 to: T::CrossAccountId,478 data: Vec<CreateItemData>,479 ) -> DispatchResultWithPostInfo;480 fn burn_item(481 &self,482 sender: T::CrossAccountId,483 token: TokenId,484 amount: u128,485 ) -> DispatchResultWithPostInfo;486487 fn transfer(488 &self,489 sender: T::CrossAccountId,490 to: T::CrossAccountId,491 token: TokenId,492 amount: u128,493 ) -> DispatchResultWithPostInfo;494 fn approve(495 &self,496 sender: T::CrossAccountId,497 spender: T::CrossAccountId,498 token: TokenId,499 amount: u128,500 ) -> DispatchResultWithPostInfo;501 fn transfer_from(502 &self,503 sender: T::CrossAccountId,504 from: T::CrossAccountId,505 to: T::CrossAccountId,506 token: TokenId,507 amount: u128,508 ) -> DispatchResultWithPostInfo;509 fn burn_from(510 &self,511 sender: T::CrossAccountId,512 from: T::CrossAccountId,513 token: TokenId,514 amount: u128,515 ) -> DispatchResultWithPostInfo;516517 fn set_variable_metadata(518 &self,519 sender: T::CrossAccountId,520 token: TokenId,521 data: Vec<u8>,522 ) -> DispatchResultWithPostInfo;523524 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;525 fn token_exists(&self, token: TokenId) -> bool;526 fn last_token_id(&self) -> TokenId;527528 fn token_owner(&self, token: TokenId) -> T::CrossAccountId;529 fn const_metadata(&self, token: TokenId) -> Vec<u8>;530 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;531532 533 fn collection_tokens(&self) -> u32;534 535 fn account_balance(&self, account: T::CrossAccountId) -> u32;536 537 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;538 fn allowance(539 &self,540 sender: T::CrossAccountId,541 spender: T::CrossAccountId,542 token: TokenId,543 ) -> u128;544}545546547pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {548 let post_info = PostDispatchInfo {549 actual_weight: Some(weight),550 pays_fee: Pays::Yes,551 };552 match res {553 Ok(()) => Ok(post_info),554 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),555 }556}