1#![cfg_attr(not(feature = "std"), no_std)]23use erc::ERC721Events;4use frame_support::{BoundedVec, ensure};5use up_data_structs::{6 AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,7};8use pallet_common::{9 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, account::CrossAccountId,10};11use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};12use sp_core::H160;13use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};14use sp_std::{vec::Vec, vec};15use core::ops::Deref;16use sp_std::collections::btree_map::BTreeMap;17use codec::{Encode, Decode, MaxEncodedLen};18use scale_info::TypeInfo;1920pub use pallet::*;21#[cfg(feature = "runtime-benchmarks")]22pub mod benchmarking;23pub mod common;24pub mod erc;25pub mod weights;2627pub type CreateItemData<T> = CreateNftExData<<T as pallet_common::Config>::CrossAccountId>;28pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;2930#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]31pub struct ItemData<CrossAccountId> {32 pub const_data: BoundedVec<u8, CustomDataLimit>,33 pub variable_data: BoundedVec<u8, CustomDataLimit>,34 pub owner: CrossAccountId,35}3637#[frame_support::pallet]38pub mod pallet {39 use super::*;40 use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};41 use up_data_structs::{CollectionId, TokenId};42 use super::weights::WeightInfo;4344 #[pallet::error]45 pub enum Error<T> {46 47 NotNonfungibleDataUsedToMintFungibleCollectionToken,48 49 NonfungibleItemsHaveNoAmount,50 }5152 #[pallet::config]53 pub trait Config: frame_system::Config + pallet_common::Config {54 type WeightInfo: WeightInfo;55 }5657 #[pallet::pallet]58 #[pallet::generate_store(pub(super) trait Store)]59 pub struct Pallet<T>(_);6061 #[pallet::storage]62 pub type TokensMinted<T: Config> =63 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;64 #[pallet::storage]65 pub type TokensBurnt<T: Config> =66 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;6768 #[pallet::storage]69 pub type TokenData<T: Config> = StorageNMap<70 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),71 Value = ItemData<T::CrossAccountId>,72 QueryKind = OptionQuery,73 >;7475 76 #[pallet::storage]77 pub type Owned<T: Config> = StorageNMap<78 Key = (79 Key<Twox64Concat, CollectionId>,80 Key<Blake2_128Concat, T::CrossAccountId>,81 Key<Twox64Concat, TokenId>,82 ),83 Value = bool,84 QueryKind = ValueQuery,85 >;8687 #[pallet::storage]88 pub type AccountBalance<T: Config> = StorageNMap<89 Key = (90 Key<Twox64Concat, CollectionId>,91 Key<Blake2_128Concat, T::CrossAccountId>,92 ),93 Value = u32,94 QueryKind = ValueQuery,95 >;9697 #[pallet::storage]98 pub type Allowance<T: Config> = StorageNMap<99 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),100 Value = T::CrossAccountId,101 QueryKind = OptionQuery,102 >;103}104105pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);106impl<T: Config> NonfungibleHandle<T> {107 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {108 Self(inner)109 }110 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {111 self.0112 }113}114impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {115 fn recorder(&self) -> &SubstrateRecorder<T> {116 self.0.recorder()117 }118 fn into_recorder(self) -> SubstrateRecorder<T> {119 self.0.into_recorder()120 }121}122impl<T: Config> Deref for NonfungibleHandle<T> {123 type Target = pallet_common::CollectionHandle<T>;124125 fn deref(&self) -> &Self::Target {126 &self.0127 }128}129130impl<T: Config> Pallet<T> {131 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {132 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)133 }134 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {135 <TokenData<T>>::contains_key((collection.id, token))136 }137}138139140impl<T: Config> Pallet<T> {141 pub fn init_collection(142 owner: T::AccountId,143 data: CreateCollectionData<T::AccountId>,144 ) -> Result<CollectionId, DispatchError> {145 <PalletCommon<T>>::init_collection(owner, data)146 }147 pub fn destroy_collection(148 collection: NonfungibleHandle<T>,149 sender: &T::CrossAccountId,150 ) -> DispatchResult {151 let id = collection.id;152153 154155 PalletCommon::destroy_collection(collection.0, sender)?;156157 <TokenData<T>>::remove_prefix((id,), None);158 <Owned<T>>::remove_prefix((id,), None);159 <TokensMinted<T>>::remove(id);160 <TokensBurnt<T>>::remove(id);161 <Allowance<T>>::remove_prefix((id,), None);162 <AccountBalance<T>>::remove_prefix((id,), None);163 Ok(())164 }165166 pub fn burn(167 collection: &NonfungibleHandle<T>,168 sender: &T::CrossAccountId,169 token: TokenId,170 ) -> DispatchResult {171 let token_data =172 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;173 ensure!(174 &token_data.owner == sender175 || (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),176 <CommonError<T>>::NoPermission177 );178179 if collection.access == AccessMode::AllowList {180 collection.check_allowlist(sender)?;181 }182183 let burnt = <TokensBurnt<T>>::get(collection.id)184 .checked_add(1)185 .ok_or(ArithmeticError::Overflow)?;186187 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))188 .checked_sub(1)189 .ok_or(ArithmeticError::Overflow)?;190191 if balance == 0 {192 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));193 } else {194 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);195 }196 197198 <Owned<T>>::remove((collection.id, &token_data.owner, token));199 <TokensBurnt<T>>::insert(collection.id, burnt);200 <TokenData<T>>::remove((collection.id, token));201 let old_spender = <Allowance<T>>::take((collection.id, token));202203 if let Some(old_spender) = old_spender {204 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(205 collection.id,206 token,207 sender.clone(),208 old_spender,209 0,210 ));211 }212213 collection.log_mirrored(ERC721Events::Transfer {214 from: *token_data.owner.as_eth(),215 to: H160::default(),216 token_id: token.into(),217 });218 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(219 collection.id,220 token,221 token_data.owner,222 1,223 ));224 Ok(())225 }226227 pub fn transfer(228 collection: &NonfungibleHandle<T>,229 from: &T::CrossAccountId,230 to: &T::CrossAccountId,231 token: TokenId,232 ) -> DispatchResult {233 ensure!(234 collection.limits.transfers_enabled(),235 <CommonError<T>>::TransferNotAllowed236 );237238 let token_data =239 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;240 ensure!(241 &token_data.owner == from242 || (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),243 <CommonError<T>>::NoPermission244 );245246 if collection.access == AccessMode::AllowList {247 collection.check_allowlist(from)?;248 collection.check_allowlist(to)?;249 }250 <PalletCommon<T>>::ensure_correct_receiver(to)?;251252 let balance_from = <AccountBalance<T>>::get((collection.id, from))253 .checked_sub(1)254 .ok_or(<CommonError<T>>::TokenValueTooLow)?;255 let balance_to = if from != to {256 let balance_to = <AccountBalance<T>>::get((collection.id, to))257 .checked_add(1)258 .ok_or(ArithmeticError::Overflow)?;259260 ensure!(261 balance_to < collection.limits.account_token_ownership_limit(),262 <CommonError<T>>::AccountTokenLimitExceeded,263 );264265 Some(balance_to)266 } else {267 None268 };269270 271272 <TokenData<T>>::insert(273 (collection.id, token),274 ItemData {275 owner: to.clone(),276 ..token_data277 },278 );279280 if let Some(balance_to) = balance_to {281 282 if balance_from == 0 {283 <AccountBalance<T>>::remove((collection.id, from));284 } else {285 <AccountBalance<T>>::insert((collection.id, from), balance_from);286 }287 <AccountBalance<T>>::insert((collection.id, to), balance_to);288 <Owned<T>>::remove((collection.id, from, token));289 <Owned<T>>::insert((collection.id, to, token), true);290 }291 Self::set_allowance_unchecked(collection, from, token, None, true);292293 collection.log_mirrored(ERC721Events::Transfer {294 from: *from.as_eth(),295 to: *to.as_eth(),296 token_id: token.into(),297 });298 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(299 collection.id,300 token,301 from.clone(),302 to.clone(),303 1,304 ));305 Ok(())306 }307308 pub fn create_multiple_items(309 collection: &NonfungibleHandle<T>,310 sender: &T::CrossAccountId,311 data: Vec<CreateItemData<T>>,312 ) -> DispatchResult {313 if !collection.is_owner_or_admin(sender) {314 ensure!(315 collection.mint_mode,316 <CommonError<T>>::PublicMintingNotAllowed317 );318 collection.check_allowlist(sender)?;319320 for item in data.iter() {321 collection.check_allowlist(&item.owner)?;322 }323 }324325 for data in data.iter() {326 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;327 }328329 let first_token = <TokensMinted<T>>::get(collection.id);330 let tokens_minted = first_token331 .checked_add(data.len() as u32)332 .ok_or(ArithmeticError::Overflow)?;333 ensure!(334 tokens_minted <= collection.limits.token_limit(),335 <CommonError<T>>::CollectionTokenLimitExceeded336 );337338 let mut balances = BTreeMap::new();339 for data in &data {340 let balance = balances341 .entry(&data.owner)342 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));343 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;344345 ensure!(346 *balance <= collection.limits.account_token_ownership_limit(),347 <CommonError<T>>::AccountTokenLimitExceeded,348 );349 }350351 352353 <TokensMinted<T>>::insert(collection.id, tokens_minted);354 for (account, balance) in balances {355 <AccountBalance<T>>::insert((collection.id, account), balance);356 }357 for (i, data) in data.into_iter().enumerate() {358 let token = first_token + i as u32 + 1;359360 <TokenData<T>>::insert(361 (collection.id, token),362 ItemData {363 const_data: data.const_data,364 variable_data: data.variable_data,365 owner: data.owner.clone(),366 },367 );368 <Owned<T>>::insert((collection.id, &data.owner, token), true);369370 collection.log_mirrored(ERC721Events::Transfer {371 from: H160::default(),372 to: *data.owner.as_eth(),373 token_id: token.into(),374 });375 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(376 collection.id,377 TokenId(token),378 data.owner.clone(),379 1,380 ));381 }382 Ok(())383 }384385 pub fn set_allowance_unchecked(386 collection: &NonfungibleHandle<T>,387 sender: &T::CrossAccountId,388 token: TokenId,389 spender: Option<&T::CrossAccountId>,390 assume_implicit_eth: bool,391 ) {392 if let Some(spender) = spender {393 let old_spender = <Allowance<T>>::get((collection.id, token));394 <Allowance<T>>::insert((collection.id, token), spender);395 396 397 collection.log_mirrored(ERC721Events::Approval {398 owner: *sender.as_eth(),399 approved: *spender.as_eth(),400 token_id: token.into(),401 });402 403 404 if old_spender.as_ref() != Some(spender) {405 if let Some(old_owner) = old_spender {406 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(407 collection.id,408 token,409 sender.clone(),410 old_owner,411 0,412 ));413 }414 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(415 collection.id,416 token,417 sender.clone(),418 spender.clone(),419 1,420 ));421 }422 } else {423 let old_spender = <Allowance<T>>::take((collection.id, token));424 if !assume_implicit_eth {425 426 427 collection.log_mirrored(ERC721Events::Approval {428 owner: *sender.as_eth(),429 approved: H160::default(),430 token_id: token.into(),431 });432 }433 434 435 if let Some(old_spender) = old_spender {436 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(437 collection.id,438 token,439 sender.clone(),440 old_spender,441 0,442 ));443 }444 }445 }446447 pub fn set_allowance(448 collection: &NonfungibleHandle<T>,449 sender: &T::CrossAccountId,450 token: TokenId,451 spender: Option<&T::CrossAccountId>,452 ) -> DispatchResult {453 if collection.access == AccessMode::AllowList {454 collection.check_allowlist(sender)?;455 if let Some(spender) = spender {456 collection.check_allowlist(spender)?;457 }458 }459460 if let Some(spender) = spender {461 <PalletCommon<T>>::ensure_correct_receiver(spender)?;462 }463 let token_data =464 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;465 if &token_data.owner != sender {466 ensure!(467 collection.ignores_owned_amount(sender),468 <CommonError<T>>::CantApproveMoreThanOwned469 );470 }471472 473474 Self::set_allowance_unchecked(collection, sender, token, spender, false);475 Ok(())476 }477478 pub fn transfer_from(479 collection: &NonfungibleHandle<T>,480 spender: &T::CrossAccountId,481 from: &T::CrossAccountId,482 to: &T::CrossAccountId,483 token: TokenId,484 ) -> DispatchResult {485 if spender.conv_eq(from) {486 return Self::transfer(collection, from, to, token);487 }488 if collection.access == AccessMode::AllowList {489 490 collection.check_allowlist(spender)?;491 }492493 if <Allowance<T>>::get((collection.id, token)).as_ref() != Some(spender) {494 ensure!(495 collection.ignores_allowance(spender),496 <CommonError<T>>::ApprovedValueTooLow497 );498 }499500 501502 Self::transfer(collection, from, to, token)?;503 504 Ok(())505 }506507 pub fn burn_from(508 collection: &NonfungibleHandle<T>,509 spender: &T::CrossAccountId,510 from: &T::CrossAccountId,511 token: TokenId,512 ) -> DispatchResult {513 if spender.conv_eq(from) {514 return Self::burn(collection, from, token);515 }516 if collection.access == AccessMode::AllowList {517 518 collection.check_allowlist(spender)?;519 }520521 if <Allowance<T>>::get((collection.id, token)).as_ref() != Some(spender) {522 ensure!(523 collection.ignores_allowance(spender),524 <CommonError<T>>::ApprovedValueTooLow525 );526 }527528 529530 Self::burn(collection, from, token)531 }532533 pub fn set_variable_metadata(534 collection: &NonfungibleHandle<T>,535 sender: &T::CrossAccountId,536 token: TokenId,537 data: BoundedVec<u8, CustomDataLimit>,538 ) -> DispatchResult {539 let token_data =540 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;541 collection.check_can_update_meta(sender, &token_data.owner)?;542543 544545 <TokenData<T>>::insert(546 (collection.id, token),547 ItemData {548 variable_data: data,549 ..token_data550 },551 );552 Ok(())553 }554555 556 pub fn create_item(557 collection: &NonfungibleHandle<T>,558 sender: &T::CrossAccountId,559 data: CreateItemData<T>,560 ) -> DispatchResult {561 Self::create_multiple_items(collection, sender, vec![data])562 }563}