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 if !collection.limits.owner_can_destroy {409 fail!(Error::<T>::NoPermission);410 }411 collection.check_is_owner(&sender)?;412413 let destroyed_collections = <DestroyedCollectionCount<T>>::get()414 .0415 .checked_add(1)416 .ok_or(ArithmeticError::Overflow)?;417418 419420 <DestroyedCollectionCount<T>>::put(destroyed_collections);421 <CollectionById<T>>::remove(collection.id);422 <IsAdmin<T>>::remove_prefix((collection.id,), None);423 <Allowlist<T>>::remove_prefix((collection.id,), None);424 Ok(())425 }426427 pub fn toggle_allowlist(428 collection: &CollectionHandle<T>,429 sender: &T::CrossAccountId,430 user: &T::CrossAccountId,431 allowed: bool,432 ) -> DispatchResult {433 collection.check_is_owner_or_admin(&sender)?;434435 436437 if allowed {438 <Allowlist<T>>::insert((collection.id, user.as_sub()), true);439 } else {440 <Allowlist<T>>::remove((collection.id, user.as_sub()));441 }442443 Ok(())444 }445}446447#[macro_export]448macro_rules! unsupported {449 () => {450 Err(<Error<T>>::UnsupportedOperation.into())451 };452}453454455pub trait CommonWeightInfo {456 fn create_item() -> Weight;457 fn create_multiple_items(amount: u32) -> Weight;458 fn burn_item() -> Weight;459 fn transfer() -> Weight;460 fn approve() -> Weight;461 fn transfer_from() -> Weight;462 fn set_variable_metadata(bytes: u32) -> Weight;463}464465pub trait CommonCollectionOperations<T: Config> {466 fn create_item(467 &self,468 sender: T::CrossAccountId,469 to: T::CrossAccountId,470 data: CreateItemData,471 ) -> DispatchResultWithPostInfo;472 fn create_multiple_items(473 &self,474 sender: T::CrossAccountId,475 to: T::CrossAccountId,476 data: Vec<CreateItemData>,477 ) -> DispatchResultWithPostInfo;478 fn burn_item(479 &self,480 sender: T::CrossAccountId,481 token: TokenId,482 amount: u128,483 ) -> DispatchResultWithPostInfo;484485 fn transfer(486 &self,487 sender: T::CrossAccountId,488 to: T::CrossAccountId,489 token: TokenId,490 amount: u128,491 ) -> DispatchResultWithPostInfo;492 fn approve(493 &self,494 sender: T::CrossAccountId,495 spender: T::CrossAccountId,496 token: TokenId,497 amount: u128,498 ) -> DispatchResultWithPostInfo;499 fn transfer_from(500 &self,501 sender: T::CrossAccountId,502 from: T::CrossAccountId,503 to: T::CrossAccountId,504 token: TokenId,505 amount: u128,506 ) -> DispatchResultWithPostInfo;507508 fn set_variable_metadata(509 &self,510 sender: T::CrossAccountId,511 token: TokenId,512 data: Vec<u8>,513 ) -> DispatchResultWithPostInfo;514515 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;516 fn token_exists(&self, token: TokenId) -> bool;517 fn last_token_id(&self) -> TokenId;518519 fn token_owner(&self, token: TokenId) -> T::CrossAccountId;520 fn const_metadata(&self, token: TokenId) -> Vec<u8>;521 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;522523 524 fn collection_tokens(&self) -> u32;525 526 fn account_balance(&self, account: T::CrossAccountId) -> u32;527 528 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;529 fn allowance(530 &self,531 sender: T::CrossAccountId,532 spender: T::CrossAccountId,533 token: TokenId,534 ) -> u128;535}536537538pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {539 let post_info = PostDispatchInfo {540 actual_weight: Some(weight),541 pays_fee: Pays::Yes,542 };543 match res {544 Ok(()) => Ok(post_info),545 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),546 }547}