1#![cfg_attr(not(feature = "std"), no_std)]23use frame_support::{ensure, BoundedVec};4use up_data_structs::{5 AccessMode, Collection, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,6};7use pallet_common::{8 Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, account::CrossAccountId,9};10use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};11use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};12use core::ops::Deref;13use codec::{Encode, Decode, MaxEncodedLen};14use scale_info::TypeInfo;1516pub use pallet::*;17#[cfg(feature = "runtime-benchmarks")]18pub mod benchmarking;19pub mod common;20pub mod erc;21pub mod weights;22pub struct CreateItemData<T: Config> {23 pub const_data: BoundedVec<u8, CustomDataLimit>,24 pub variable_data: BoundedVec<u8, CustomDataLimit>,25 pub users: BTreeMap<T::CrossAccountId, u128>,26}27pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;2829#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]30pub struct ItemData {31 pub const_data: BoundedVec<u8, CustomDataLimit>,32 pub variable_data: BoundedVec<u8, CustomDataLimit>,33}3435#[frame_support::pallet]36pub mod pallet {37 use super::*;38 use frame_support::{Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};39 use up_data_structs::{CollectionId, TokenId};40 use super::weights::WeightInfo;4142 #[pallet::error]43 pub enum Error<T> {44 45 NotRefungibleDataUsedToMintFungibleCollectionToken,46 47 WrongRefungiblePieces,48 }4950 #[pallet::config]51 pub trait Config: frame_system::Config + pallet_common::Config {52 type WeightInfo: WeightInfo;53 }5455 #[pallet::pallet]56 #[pallet::generate_store(pub(super) trait Store)]57 pub struct Pallet<T>(_);5859 #[pallet::storage]60 pub type TokensMinted<T: Config> =61 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;62 #[pallet::storage]63 pub type TokensBurnt<T: Config> =64 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;6566 #[pallet::storage]67 pub type TokenData<T: Config> = StorageNMap<68 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),69 Value = ItemData,70 QueryKind = ValueQuery,71 >;7273 #[pallet::storage]74 pub type TotalSupply<T: Config> = StorageNMap<75 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),76 Value = u128,77 QueryKind = ValueQuery,78 >;7980 81 #[pallet::storage]82 pub type Owned<T: Config> = StorageNMap<83 Key = (84 Key<Twox64Concat, CollectionId>,85 Key<Blake2_128Concat, T::CrossAccountId>,86 Key<Twox64Concat, TokenId>,87 ),88 Value = bool,89 QueryKind = ValueQuery,90 >;9192 #[pallet::storage]93 pub type AccountBalance<T: Config> = StorageNMap<94 Key = (95 Key<Twox64Concat, CollectionId>,96 97 Key<Blake2_128Concat, T::CrossAccountId>,98 ),99 Value = u32,100 QueryKind = ValueQuery,101 >;102103 #[pallet::storage]104 pub type Balance<T: Config> = StorageNMap<105 Key = (106 Key<Twox64Concat, CollectionId>,107 Key<Twox64Concat, TokenId>,108 109 Key<Blake2_128Concat, T::CrossAccountId>,110 ),111 Value = u128,112 QueryKind = ValueQuery,113 >;114115 #[pallet::storage]116 pub type Allowance<T: Config> = StorageNMap<117 Key = (118 Key<Twox64Concat, CollectionId>,119 Key<Twox64Concat, TokenId>,120 121 Key<Blake2_128, T::CrossAccountId>,122 123 Key<Blake2_128Concat, T::CrossAccountId>,124 ),125 Value = u128,126 QueryKind = ValueQuery,127 >;128}129130pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);131impl<T: Config> RefungibleHandle<T> {132 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {133 Self(inner)134 }135 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {136 self.0137 }138}139impl<T: Config> Deref for RefungibleHandle<T> {140 type Target = pallet_common::CollectionHandle<T>;141142 fn deref(&self) -> &Self::Target {143 &self.0144 }145}146147impl<T: Config> Pallet<T> {148 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {149 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)150 }151 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {152 <TotalSupply<T>>::contains_key((collection.id, token))153 }154}155156157impl<T: Config> Pallet<T> {158 pub fn init_collection(data: Collection<T::AccountId>) -> Result<CollectionId, DispatchError> {159 <PalletCommon<T>>::init_collection(data)160 }161 pub fn destroy_collection(162 collection: RefungibleHandle<T>,163 sender: &T::CrossAccountId,164 ) -> DispatchResult {165 let id = collection.id;166167 168169 PalletCommon::destroy_collection(collection.0, sender)?;170171 <TokensMinted<T>>::remove(id);172 <TokensBurnt<T>>::remove(id);173 <TokenData<T>>::remove_prefix((id,), None);174 <TotalSupply<T>>::remove_prefix((id,), None);175 <Balance<T>>::remove_prefix((id,), None);176 <Allowance<T>>::remove_prefix((id,), None);177 <Owned<T>>::remove_prefix((id,), None);178 <AccountBalance<T>>::remove_prefix((id,), None);179 Ok(())180 }181182 pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {183 let burnt = <TokensBurnt<T>>::get(collection.id)184 .checked_add(1)185 .ok_or(ArithmeticError::Overflow)?;186187 <TokensBurnt<T>>::insert(collection.id, burnt);188 <TokenData<T>>::remove((collection.id, token_id));189 <TotalSupply<T>>::remove((collection.id, token_id));190 <Balance<T>>::remove_prefix((collection.id, token_id), None);191 <Allowance<T>>::remove_prefix((collection.id, token_id), None);192 193 Ok(())194 }195196 pub fn burn(197 collection: &RefungibleHandle<T>,198 owner: &T::CrossAccountId,199 token: TokenId,200 amount: u128,201 ) -> DispatchResult {202 let total_supply = <TotalSupply<T>>::get((collection.id, token))203 .checked_sub(amount)204 .ok_or(<CommonError<T>>::TokenValueTooLow)?;205206 207 if total_supply == 0 {208 209 ensure!(210 <Balance<T>>::get((collection.id, token, owner)) == amount,211 <CommonError<T>>::TokenValueTooLow212 );213 let account_balance = <AccountBalance<T>>::get((collection.id, owner))214 .checked_sub(1)215 216 .ok_or(ArithmeticError::Underflow)?;217218 219220 <Owned<T>>::remove((collection.id, owner, token));221 <AccountBalance<T>>::insert((collection.id, owner), account_balance);222 Self::burn_token(collection, token)?;223 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(224 collection.id,225 token,226 owner.clone(),227 amount,228 ));229 return Ok(());230 }231232 let balance = <Balance<T>>::get((collection.id, token, owner))233 .checked_sub(amount)234 .ok_or(<CommonError<T>>::TokenValueTooLow)?;235 let account_balance = if balance == 0 {236 <AccountBalance<T>>::get((collection.id, owner))237 .checked_sub(1)238 239 .ok_or(ArithmeticError::Underflow)?240 } else {241 0242 };243244 245246 if balance == 0 {247 <Owned<T>>::remove((collection.id, owner, token));248 <Balance<T>>::remove((collection.id, token, owner));249 <AccountBalance<T>>::insert((collection.id, owner), account_balance);250 } else {251 <Balance<T>>::insert((collection.id, token, owner), balance);252 }253 <TotalSupply<T>>::insert((collection.id, token), total_supply);254 255 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(256 collection.id,257 token,258 owner.clone(),259 amount,260 ));261 Ok(())262 }263264 pub fn transfer(265 collection: &RefungibleHandle<T>,266 from: &T::CrossAccountId,267 to: &T::CrossAccountId,268 token: TokenId,269 amount: u128,270 ) -> DispatchResult {271 ensure!(272 collection.limits.transfers_enabled(),273 <CommonError<T>>::TransferNotAllowed274 );275276 if collection.access == AccessMode::AllowList {277 collection.check_allowlist(from)?;278 collection.check_allowlist(to)?;279 }280 <PalletCommon<T>>::ensure_correct_receiver(to)?;281282 let balance_from = <Balance<T>>::get((collection.id, token, from))283 .checked_sub(amount)284 .ok_or(<CommonError<T>>::TokenValueTooLow)?;285 let mut create_target = false;286 let from_to_differ = from != to;287 let balance_to = if from != to {288 let old_balance = <Balance<T>>::get((collection.id, token, to));289 if old_balance == 0 {290 create_target = true;291 }292 Some(293 old_balance294 .checked_add(amount)295 .ok_or(ArithmeticError::Overflow)?,296 )297 } else {298 None299 };300301 let account_balance_from = if balance_from == 0 {302 Some(303 <AccountBalance<T>>::get((collection.id, from))304 .checked_sub(1)305 306 .ok_or(ArithmeticError::Underflow)?,307 )308 } else {309 None310 };311 312 313 let account_balance_to = if create_target && from_to_differ {314 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))315 .checked_add(1)316 .ok_or(ArithmeticError::Overflow)?;317 ensure!(318 account_balance_to < collection.limits.account_token_ownership_limit(),319 <CommonError<T>>::AccountTokenLimitExceeded,320 );321322 Some(account_balance_to)323 } else {324 None325 };326327 328329 if let Some(balance_to) = balance_to {330 331 if balance_from == 0 {332 <Balance<T>>::remove((collection.id, token, from));333 } else {334 <Balance<T>>::insert((collection.id, token, from), balance_from);335 }336 <Balance<T>>::insert((collection.id, token, to), balance_to);337 if let Some(account_balance_from) = account_balance_from {338 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);339 <Owned<T>>::remove((collection.id, from, token));340 }341 if let Some(account_balance_to) = account_balance_to {342 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);343 <Owned<T>>::insert((collection.id, to, token), true);344 }345 }346347 348 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(349 collection.id,350 token,351 from.clone(),352 to.clone(),353 amount,354 ));355 Ok(())356 }357358 pub fn create_multiple_items(359 collection: &RefungibleHandle<T>,360 sender: &T::CrossAccountId,361 data: Vec<CreateItemData<T>>,362 ) -> DispatchResult {363 if !collection.is_owner_or_admin(sender) {364 ensure!(365 collection.mint_mode,366 <CommonError<T>>::PublicMintingNotAllowed367 );368 collection.check_allowlist(sender)?;369370 for item in data.iter() {371 for user in item.users.keys() {372 collection.check_allowlist(user)?;373 }374 }375 }376377 for item in data.iter() {378 for (owner, _) in item.users.iter() {379 <PalletCommon<T>>::ensure_correct_receiver(owner)?;380 }381 }382383 384 let totals = data385 .iter()386 .map(|data| {387 Ok(data388 .users389 .iter()390 .map(|u| u.1)391 .try_fold(0u128, |acc, v| acc.checked_add(*v))392 .ok_or(ArithmeticError::Overflow)?)393 })394 .collect::<Result<Vec<_>, DispatchError>>()?;395 for total in &totals {396 ensure!(397 *total <= MAX_REFUNGIBLE_PIECES,398 <Error<T>>::WrongRefungiblePieces399 );400 }401402 let first_token_id = <TokensMinted<T>>::get(collection.id);403 let tokens_minted = first_token_id404 .checked_add(data.len() as u32)405 .ok_or(ArithmeticError::Overflow)?;406 ensure!(407 tokens_minted < collection.limits.token_limit(),408 <CommonError<T>>::CollectionTokenLimitExceeded409 );410411 let mut balances = BTreeMap::new();412 for data in &data {413 for owner in data.users.keys() {414 let balance = balances415 .entry(owner)416 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));417 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;418419 ensure!(420 *balance <= collection.limits.account_token_ownership_limit(),421 <CommonError<T>>::AccountTokenLimitExceeded,422 );423 }424 }425426 427428 <TokensMinted<T>>::insert(collection.id, tokens_minted);429 for (account, balance) in balances {430 <AccountBalance<T>>::insert((collection.id, account), balance);431 }432 for (i, token) in data.into_iter().enumerate() {433 let token_id = first_token_id + i as u32 + 1;434 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);435436 <TokenData<T>>::insert(437 (collection.id, token_id),438 ItemData {439 const_data: token.const_data,440 variable_data: token.variable_data,441 },442 );443 for (user, amount) in token.users.into_iter() {444 if amount == 0 {445 continue;446 }447 <Balance<T>>::insert((collection.id, token_id, &user), amount);448 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);449 450 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(451 collection.id,452 TokenId(token_id),453 user,454 amount,455 ));456 }457 }458 Ok(())459 }460461 pub fn set_allowance_unchecked(462 collection: &RefungibleHandle<T>,463 sender: &T::CrossAccountId,464 spender: &T::CrossAccountId,465 token: TokenId,466 amount: u128,467 ) {468 if amount == 0 {469 <Allowance<T>>::remove((collection.id, token, sender, spender));470 } else {471 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);472 }473 474 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(475 collection.id,476 token,477 sender.clone(),478 spender.clone(),479 amount,480 ))481 }482483 pub fn set_allowance(484 collection: &RefungibleHandle<T>,485 sender: &T::CrossAccountId,486 spender: &T::CrossAccountId,487 token: TokenId,488 amount: u128,489 ) -> DispatchResult {490 if collection.access == AccessMode::AllowList {491 collection.check_allowlist(sender)?;492 collection.check_allowlist(spender)?;493 }494495 <PalletCommon<T>>::ensure_correct_receiver(spender)?;496497 if <Balance<T>>::get((collection.id, token, sender)) < amount {498 ensure!(499 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),500 <CommonError<T>>::CantApproveMoreThanOwned501 );502 }503504 505506 Self::set_allowance_unchecked(collection, sender, spender, token, amount);507 Ok(())508 }509510 pub fn transfer_from(511 collection: &RefungibleHandle<T>,512 spender: &T::CrossAccountId,513 from: &T::CrossAccountId,514 to: &T::CrossAccountId,515 token: TokenId,516 amount: u128,517 ) -> DispatchResult {518 if spender.conv_eq(from) {519 return Self::transfer(collection, from, to, token, amount);520 }521 if collection.access == AccessMode::AllowList {522 523 collection.check_allowlist(spender)?;524 }525526 let allowance =527 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);528 if allowance.is_none() {529 ensure!(530 collection.ignores_allowance(spender),531 <CommonError<T>>::TokenValueNotEnough532 );533 }534535 536537 Self::transfer(collection, from, to, token, amount)?;538 if let Some(allowance) = allowance {539 Self::set_allowance_unchecked(collection, from, spender, token, allowance);540 }541 Ok(())542 }543544 pub fn burn_from(545 collection: &RefungibleHandle<T>,546 spender: &T::CrossAccountId,547 from: &T::CrossAccountId,548 token: TokenId,549 amount: u128,550 ) -> DispatchResult {551 if spender.conv_eq(from) {552 return Self::burn(collection, from, token, amount);553 }554 if collection.access == AccessMode::AllowList {555 556 collection.check_allowlist(spender)?;557 }558559 let allowance =560 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);561 if allowance.is_none() {562 ensure!(563 collection.ignores_allowance(spender),564 <CommonError<T>>::TokenValueNotEnough565 );566 }567568 569570 Self::burn(collection, from, token, amount)?;571 if let Some(allowance) = allowance {572 Self::set_allowance_unchecked(collection, from, spender, token, allowance);573 }574 Ok(())575 }576577 pub fn set_variable_metadata(578 collection: &RefungibleHandle<T>,579 sender: &T::CrossAccountId,580 token: TokenId,581 data: BoundedVec<u8, CustomDataLimit>,582 ) -> DispatchResult {583 collection.check_can_update_meta(584 sender,585 &T::CrossAccountId::from_sub(collection.owner.clone()),586 )?;587588 let token_data = <TokenData<T>>::get((collection.id, token));589590 591592 <TokenData<T>>::insert(593 (collection.id, token),594 ItemData {595 variable_data: data,596 ..token_data597 },598 );599 Ok(())600 }601602 603 pub fn create_item(604 collection: &RefungibleHandle<T>,605 sender: &T::CrossAccountId,606 data: CreateItemData<T>,607 ) -> DispatchResult {608 Self::create_multiple_items(collection, sender, vec![data])609 }610}