git.delta.rocks / unique-network / refs/commits / 9bfc203a27ca

difftreelog

source

pallets/refungible/src/lib.rs16.1 KiBsourcehistory
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		/// Not Refungible item data used to mint in Refungible collection.45		NotRefungibleDataUsedToMintFungibleCollectionToken,46		/// Maximum refungibility exceeded47		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	/// Used to enumerate tokens owned by account81	#[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			// Owner97			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			// Owner109			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			// Owner121			Key<Blake2_128, T::CrossAccountId>,122			// Spender123			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}155156// unchecked calls skips any permission checks157impl<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		// TODO: ERC721 transfer event193		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		// This was probally last owner of this token?207		if total_supply == 0 {208			// Ensure user actually owns this amount209			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				// Should not occur216				.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				// Should not occur239				.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		// TODO: ERC20 transfer event255		<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					// Should not occur306					.ok_or(ArithmeticError::Underflow)?,307			)308		} else {309			None310		};311		// Account data is created in token, AccountBalance should be increased312		// But only if from != to as we shouldn't check overflow in this case313		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			// from != to331			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		// TODO: ERC20 transfer event348		<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		// Total pieces per tokens384		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.into(),440					variable_data: token.variable_data.into(),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				// TODO: ERC20 transfer event450				<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		// TODO: ERC20 approval event474		<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			// `from`, `to` checked in [`transfer`]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			// `from` checked in [`burn`]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	/// Delegated to `create_multiple_items`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}