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,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>,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>;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::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: up_evm_mapping::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>,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 >;347}348349impl<T: Config> Pallet<T> {350 351 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {352 ensure!(353 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,354 <Error<T>>::AddressIsZero355 );356 Ok(())357 }358}359360impl<T: Config> Pallet<T> {361 pub fn init_collection(data: Collection<T>) -> Result<CollectionId, DispatchError> {362 {363 ensure!(364 data.name.len() <= MAX_COLLECTION_NAME_LENGTH,365 Error::<T>::CollectionNameLimitExceeded366 );367 ensure!(368 data.description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH,369 Error::<T>::CollectionDescriptionLimitExceeded370 );371 ensure!(372 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH,373 Error::<T>::CollectionTokenPrefixLimitExceeded374 );375 }376377 let created_count = <CreatedCollectionCount<T>>::get()378 .0379 .checked_add(1)380 .ok_or(ArithmeticError::Overflow)?;381 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;382 let id = CollectionId(created_count);383384 385 ensure!(386 created_count - destroyed_count < COLLECTION_NUMBER_LIMIT,387 <Error<T>>::TotalCollectionsLimitExceeded388 );389390 391392 393 {394 let mut imbalance =395 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();396 imbalance.subsume(397 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(398 &T::TreasuryAccountId::get(),399 T::CollectionCreationPrice::get(),400 ),401 );402 <T as Config>::Currency::settle(403 &data.owner,404 imbalance,405 WithdrawReasons::TRANSFER,406 ExistenceRequirement::KeepAlive,407 )408 .map_err(|_| Error::<T>::NoPermission)?;409 }410411 <CreatedCollectionCount<T>>::put(created_count);412 <Pallet<T>>::deposit_event(Event::CollectionCreated(413 id,414 data.mode.id(),415 data.owner.clone(),416 ));417 <CollectionById<T>>::insert(id, data);418 Ok(id)419 }420421 pub fn destroy_collection(422 collection: CollectionHandle<T>,423 sender: &T::CrossAccountId,424 ) -> DispatchResult {425 ensure!(426 collection.limits.owner_can_destroy(),427 <Error<T>>::NoPermission,428 );429 collection.check_is_owner(&sender)?;430431 let destroyed_collections = <DestroyedCollectionCount<T>>::get()432 .0433 .checked_add(1)434 .ok_or(ArithmeticError::Overflow)?;435436 437438 <DestroyedCollectionCount<T>>::put(destroyed_collections);439 <CollectionById<T>>::remove(collection.id);440 <AdminAmount<T>>::remove(collection.id);441 <IsAdmin<T>>::remove_prefix((collection.id,), None);442 <Allowlist<T>>::remove_prefix((collection.id,), None);443 Ok(())444 }445446 pub fn toggle_allowlist(447 collection: &CollectionHandle<T>,448 sender: &T::CrossAccountId,449 user: &T::CrossAccountId,450 allowed: bool,451 ) -> DispatchResult {452 collection.check_is_owner_or_admin(&sender)?;453454 455456 if allowed {457 <Allowlist<T>>::insert((collection.id, user), true);458 } else {459 <Allowlist<T>>::remove((collection.id, user));460 }461462 Ok(())463 }464465 pub fn toggle_admin(466 collection: &CollectionHandle<T>,467 sender: &T::CrossAccountId,468 user: &T::CrossAccountId,469 admin: bool,470 ) -> DispatchResult {471 collection.check_is_owner_or_admin(&sender)?;472473 let was_admin = <IsAdmin<T>>::get((collection.id, user));474 if was_admin == admin {475 return Ok(());476 }477 let amount = <AdminAmount<T>>::get(collection.id);478479 if admin {480 let amount = amount481 .checked_add(1)482 .ok_or(<Error<T>>::CollectionAdminAmountExceeded)?;483 ensure!(484 amount <= Self::collection_admins_limit(),485 <Error<T>>::CollectionAdminAmountExceeded,486 );487488 489490 <AdminAmount<T>>::insert(collection.id, amount);491 <IsAdmin<T>>::insert((collection.id, user), true);492 } else {493 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));494 <IsAdmin<T>>::remove((collection.id, user));495 }496497 Ok(())498 }499}500501#[macro_export]502macro_rules! unsupported {503 () => {504 Err(<Error<T>>::UnsupportedOperation.into())505 };506}507508509pub trait CommonWeightInfo {510 fn create_item() -> Weight;511 fn create_multiple_items(amount: u32) -> Weight;512 fn burn_item() -> Weight;513 fn transfer() -> Weight;514 fn approve() -> Weight;515 fn transfer_from() -> Weight;516 fn burn_from() -> Weight;517 fn set_variable_metadata(bytes: u32) -> Weight;518}519520pub trait CommonCollectionOperations<T: Config> {521 fn create_item(522 &self,523 sender: T::CrossAccountId,524 to: T::CrossAccountId,525 data: CreateItemData,526 ) -> DispatchResultWithPostInfo;527 fn create_multiple_items(528 &self,529 sender: T::CrossAccountId,530 to: T::CrossAccountId,531 data: Vec<CreateItemData>,532 ) -> DispatchResultWithPostInfo;533 fn burn_item(534 &self,535 sender: T::CrossAccountId,536 token: TokenId,537 amount: u128,538 ) -> DispatchResultWithPostInfo;539540 fn transfer(541 &self,542 sender: T::CrossAccountId,543 to: T::CrossAccountId,544 token: TokenId,545 amount: u128,546 ) -> DispatchResultWithPostInfo;547 fn approve(548 &self,549 sender: T::CrossAccountId,550 spender: T::CrossAccountId,551 token: TokenId,552 amount: u128,553 ) -> DispatchResultWithPostInfo;554 fn transfer_from(555 &self,556 sender: T::CrossAccountId,557 from: T::CrossAccountId,558 to: T::CrossAccountId,559 token: TokenId,560 amount: u128,561 ) -> DispatchResultWithPostInfo;562 fn burn_from(563 &self,564 sender: T::CrossAccountId,565 from: T::CrossAccountId,566 token: TokenId,567 amount: u128,568 ) -> DispatchResultWithPostInfo;569570 fn set_variable_metadata(571 &self,572 sender: T::CrossAccountId,573 token: TokenId,574 data: Vec<u8>,575 ) -> DispatchResultWithPostInfo;576577 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;578 fn token_exists(&self, token: TokenId) -> bool;579 fn last_token_id(&self) -> TokenId;580581 fn token_owner(&self, token: TokenId) -> T::CrossAccountId;582 fn const_metadata(&self, token: TokenId) -> Vec<u8>;583 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;584585 586 fn collection_tokens(&self) -> u32;587 588 fn account_balance(&self, account: T::CrossAccountId) -> u32;589 590 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;591 fn allowance(592 &self,593 sender: T::CrossAccountId,594 spender: T::CrossAccountId,595 token: TokenId,596 ) -> u128;597}598599600pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {601 let post_info = PostDispatchInfo {602 actual_weight: Some(weight),603 pays_fee: Pays::Yes,604 };605 match res {606 Ok(()) => Ok(post_info),607 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),608 }609}