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)))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)),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::{Blake2_128Concat, pallet_prelude::*, storage::Key};143 use account::{EvmBackwardsAddressMapping, CrossAccountId};144 use frame_support::traits::Currency;145 use nft_data_structs::TokenId;146 use scale_info::TypeInfo;147148 #[pallet::config]149 pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo {150 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;151152 type CrossAccountId: CrossAccountId<Self::AccountId>;153154 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;155 type EvmBackwardsAddressMapping: EvmBackwardsAddressMapping<Self::AccountId>;156157 type Currency: Currency<Self::AccountId>;158 type CollectionCreationPrice: Get<159 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,160 >;161 type TreasuryAccountId: Get<Self::AccountId>;162 }163164 #[pallet::pallet]165 #[pallet::generate_store(pub(super) trait Store)]166 pub struct Pallet<T>(_);167168 #[pallet::event]169 #[pallet::generate_deposit(pub fn deposit_event)]170 pub enum Event<T: Config> {171 172 173 174 175 176 177 178 179 180 CollectionCreated(CollectionId, u8, T::AccountId),181182 183 184 185 186 187 188 189 190 191 192 193 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),194195 196 197 198 199 200 201 202 203 204 205 206 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),207208 209 210 211 212 213 214 215 216 217 218 219 Transfer(220 CollectionId,221 TokenId,222 T::CrossAccountId,223 T::CrossAccountId,224 u128,225 ),226227 228 229 230 231 232 233 234 235 236 Approved(237 CollectionId,238 TokenId,239 T::CrossAccountId,240 T::CrossAccountId,241 u128,242 ),243 }244245 #[pallet::error]246 pub enum Error<T> {247 248 CollectionNotFound,249 250 MustBeTokenOwner,251 252 NoPermission,253 254 PublicMintingNotAllowed,255 256 AddressNotInAllowlist,257258 259 CollectionNameLimitExceeded,260 261 CollectionDescriptionLimitExceeded,262 263 CollectionTokenPrefixLimitExceeded,264 265 TotalCollectionsLimitExceeded,266 267 TokenVariableDataLimitExceeded,268269 270 TransferNotAllowed,271 272 AccountTokenLimitExceeded,273 274 CollectionTokenLimitExceeded,275 276 MetadataFlagFrozen,277278 279 TokenNotFound,280 281 TokenValueTooLow,282 283 TokenValueNotEnough,284 285 CantApproveMoreThanOwned,286287 288 AddressIsZero,289 290 UnsupportedOperation,291 }292293 #[pallet::storage]294 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;295 #[pallet::storage]296 pub type DestroyedCollectionCount<T> =297 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;298299 300 #[pallet::storage]301 pub type CollectionById<T> = StorageMap<302 Hasher = Blake2_128Concat,303 Key = CollectionId,304 Value = Collection<T>,305 QueryKind = OptionQuery,306 >;307308 309 #[pallet::storage]310 pub type IsAdmin<T: Config> = StorageNMap<311 Key = (312 Key<Blake2_128Concat, CollectionId>,313 Key<Blake2_128Concat, T::CrossAccountId>,314 ),315 Value = bool,316 QueryKind = ValueQuery,317 >;318319 320 #[pallet::storage]321 pub type Allowlist<T: Config> = StorageNMap<322 Key = (323 Key<Blake2_128Concat, CollectionId>,324 Key<Blake2_128Concat, T::CrossAccountId>,325 ),326 Value = bool,327 QueryKind = ValueQuery,328 >;329}330331impl<T: Config> Pallet<T> {332 333 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {334 ensure!(335 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,336 <Error<T>>::AddressIsZero337 );338 Ok(())339 }340}341342impl<T: Config> Pallet<T> {343 pub fn init_collection(data: Collection<T>) -> Result<CollectionId, DispatchError> {344 {345 ensure!(346 data.name.len() <= MAX_COLLECTION_NAME_LENGTH,347 Error::<T>::CollectionNameLimitExceeded348 );349 ensure!(350 data.description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH,351 Error::<T>::CollectionDescriptionLimitExceeded352 );353 ensure!(354 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH,355 Error::<T>::CollectionTokenPrefixLimitExceeded356 );357 }358359 let created_count = <CreatedCollectionCount<T>>::get()360 .0361 .checked_add(1)362 .ok_or(ArithmeticError::Overflow)?;363 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;364 let id = CollectionId(created_count);365366 367 ensure!(368 created_count - destroyed_count < COLLECTION_NUMBER_LIMIT,369 <Error<T>>::TotalCollectionsLimitExceeded370 );371372 373374 375 {376 let mut imbalance =377 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();378 imbalance.subsume(379 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(380 &T::TreasuryAccountId::get(),381 T::CollectionCreationPrice::get(),382 ),383 );384 <T as Config>::Currency::settle(385 &data.owner,386 imbalance,387 WithdrawReasons::TRANSFER,388 ExistenceRequirement::KeepAlive,389 )390 .map_err(|_| Error::<T>::NoPermission)?;391 }392393 <CreatedCollectionCount<T>>::put(created_count);394 <Pallet<T>>::deposit_event(Event::CollectionCreated(395 id,396 data.mode.id(),397 data.owner.clone(),398 ));399 <CollectionById<T>>::insert(id, data);400 Ok(id)401 }402403 pub fn destroy_collection(404 collection: CollectionHandle<T>,405 sender: &T::CrossAccountId,406 ) -> DispatchResult {407 ensure!(408 collection.limits.owner_can_destroy(),409 <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 burn_from() -> Weight;463 fn set_variable_metadata(bytes: u32) -> Weight;464}465466pub trait CommonCollectionOperations<T: Config> {467 fn create_item(468 &self,469 sender: T::CrossAccountId,470 to: T::CrossAccountId,471 data: CreateItemData,472 ) -> DispatchResultWithPostInfo;473 fn create_multiple_items(474 &self,475 sender: T::CrossAccountId,476 to: T::CrossAccountId,477 data: Vec<CreateItemData>,478 ) -> DispatchResultWithPostInfo;479 fn burn_item(480 &self,481 sender: T::CrossAccountId,482 token: TokenId,483 amount: u128,484 ) -> DispatchResultWithPostInfo;485486 fn transfer(487 &self,488 sender: T::CrossAccountId,489 to: T::CrossAccountId,490 token: TokenId,491 amount: u128,492 ) -> DispatchResultWithPostInfo;493 fn approve(494 &self,495 sender: T::CrossAccountId,496 spender: T::CrossAccountId,497 token: TokenId,498 amount: u128,499 ) -> DispatchResultWithPostInfo;500 fn transfer_from(501 &self,502 sender: T::CrossAccountId,503 from: T::CrossAccountId,504 to: T::CrossAccountId,505 token: TokenId,506 amount: u128,507 ) -> DispatchResultWithPostInfo;508 fn burn_from(509 &self,510 sender: T::CrossAccountId,511 from: T::CrossAccountId,512 token: TokenId,513 amount: u128,514 ) -> DispatchResultWithPostInfo;515516 fn set_variable_metadata(517 &self,518 sender: T::CrossAccountId,519 token: TokenId,520 data: Vec<u8>,521 ) -> DispatchResultWithPostInfo;522523 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;524 fn token_exists(&self, token: TokenId) -> bool;525 fn last_token_id(&self) -> TokenId;526527 fn token_owner(&self, token: TokenId) -> T::CrossAccountId;528 fn const_metadata(&self, token: TokenId) -> Vec<u8>;529 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;530531 532 fn collection_tokens(&self) -> u32;533 534 fn account_balance(&self, account: T::CrossAccountId) -> u32;535 536 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;537 fn allowance(538 &self,539 sender: T::CrossAccountId,540 spender: T::CrossAccountId,541 token: TokenId,542 ) -> u128;543}544545546pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {547 let post_info = PostDispatchInfo {548 actual_weight: Some(weight),549 pays_fee: Pays::Yes,550 };551 match res {552 Ok(()) => Ok(post_info),553 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),554 }555}