difftreelog
feat(refungible) transfer from parent token
in: master
2 files changed
pallets/refungible/Cargo.tomldiffbeforeafterboth--- a/pallets/refungible/Cargo.toml
+++ b/pallets/refungible/Cargo.toml
@@ -18,6 +18,7 @@
sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" }
pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.20" }
pallet-common = { default-features = false, path = '../common' }
+pallet-structure = { default-features = false, path = '../structure' }
up-data-structs = { default-features = false, path = '../../primitives/data-structs' }
frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" }
scale-info = { version = "2.0.1", default-features = false, features = [
@@ -33,6 +34,7 @@
"sp-std/std",
"up-data-structs/std",
"pallet-common/std",
+ "pallet-structure/std",
'frame-benchmarking/std',
"pallet-evm/std",
]
pallets/refungible/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use frame_support::{ensure, BoundedVec};20use up_data_structs::{21 AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,22 CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping,23};24use pallet_evm::account::CrossAccountId;25use pallet_common::{26 Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,27 CollectionHandle, dispatch::CollectionDispatch,28};29use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};30use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};31use core::ops::Deref;32use codec::{Encode, Decode, MaxEncodedLen};33use scale_info::TypeInfo;3435pub use pallet::*;36#[cfg(feature = "runtime-benchmarks")]37pub mod benchmarking;38pub mod common;39pub mod erc;40pub mod weights;41pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;4243#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]44pub struct ItemData {45 pub const_data: BoundedVec<u8, CustomDataLimit>,46 pub variable_data: BoundedVec<u8, CustomDataLimit>,47}4849#[frame_support::pallet]50pub mod pallet {51 use super::*;52 use frame_support::{Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};53 use up_data_structs::{CollectionId, TokenId};54 use super::weights::WeightInfo;5556 #[pallet::error]57 pub enum Error<T> {58 /// Not Refungible item data used to mint in Refungible collection.59 NotRefungibleDataUsedToMintFungibleCollectionToken,60 /// Maximum refungibility exceeded61 WrongRefungiblePieces,62 /// Refungible token can't nest other tokens63 RefungibleDisallowsNesting,64 }6566 #[pallet::config]67 pub trait Config: frame_system::Config + pallet_common::Config {68 type WeightInfo: WeightInfo;69 }7071 #[pallet::pallet]72 #[pallet::generate_store(pub(super) trait Store)]73 pub struct Pallet<T>(_);7475 #[pallet::storage]76 pub type TokensMinted<T: Config> =77 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;78 #[pallet::storage]79 pub type TokensBurnt<T: Config> =80 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;8182 #[pallet::storage]83 pub type TokenData<T: Config> = StorageNMap<84 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),85 Value = ItemData,86 QueryKind = ValueQuery,87 >;8889 #[pallet::storage]90 pub type TotalSupply<T: Config> = StorageNMap<91 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),92 Value = u128,93 QueryKind = ValueQuery,94 >;9596 /// Used to enumerate tokens owned by account97 #[pallet::storage]98 pub type Owned<T: Config> = StorageNMap<99 Key = (100 Key<Twox64Concat, CollectionId>,101 Key<Blake2_128Concat, T::CrossAccountId>,102 Key<Twox64Concat, TokenId>,103 ),104 Value = bool,105 QueryKind = ValueQuery,106 >;107108 #[pallet::storage]109 pub type AccountBalance<T: Config> = StorageNMap<110 Key = (111 Key<Twox64Concat, CollectionId>,112 // Owner113 Key<Blake2_128Concat, T::CrossAccountId>,114 ),115 Value = u32,116 QueryKind = ValueQuery,117 >;118119 #[pallet::storage]120 pub type Balance<T: Config> = StorageNMap<121 Key = (122 Key<Twox64Concat, CollectionId>,123 Key<Twox64Concat, TokenId>,124 // Owner125 Key<Blake2_128Concat, T::CrossAccountId>,126 ),127 Value = u128,128 QueryKind = ValueQuery,129 >;130131 #[pallet::storage]132 pub type Allowance<T: Config> = StorageNMap<133 Key = (134 Key<Twox64Concat, CollectionId>,135 Key<Twox64Concat, TokenId>,136 // Owner137 Key<Blake2_128, T::CrossAccountId>,138 // Spender139 Key<Blake2_128Concat, T::CrossAccountId>,140 ),141 Value = u128,142 QueryKind = ValueQuery,143 >;144}145146pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);147impl<T: Config> RefungibleHandle<T> {148 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {149 Self(inner)150 }151 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {152 self.0153 }154}155impl<T: Config> Deref for RefungibleHandle<T> {156 type Target = pallet_common::CollectionHandle<T>;157158 fn deref(&self) -> &Self::Target {159 &self.0160 }161}162163impl<T: Config> Pallet<T> {164 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {165 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)166 }167 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {168 <TotalSupply<T>>::contains_key((collection.id, token))169 }170}171172// unchecked calls skips any permission checks173impl<T: Config> Pallet<T> {174 pub fn init_collection(175 owner: T::AccountId,176 data: CreateCollectionData<T::AccountId>,177 ) -> Result<CollectionId, DispatchError> {178 <PalletCommon<T>>::init_collection(owner, data)179 }180 pub fn destroy_collection(181 collection: RefungibleHandle<T>,182 sender: &T::CrossAccountId,183 ) -> DispatchResult {184 let id = collection.id;185186 // =========187188 PalletCommon::destroy_collection(collection.0, sender)?;189190 <TokensMinted<T>>::remove(id);191 <TokensBurnt<T>>::remove(id);192 <TokenData<T>>::remove_prefix((id,), None);193 <TotalSupply<T>>::remove_prefix((id,), None);194 <Balance<T>>::remove_prefix((id,), None);195 <Allowance<T>>::remove_prefix((id,), None);196 <Owned<T>>::remove_prefix((id,), None);197 <AccountBalance<T>>::remove_prefix((id,), None);198 Ok(())199 }200201 pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {202 let burnt = <TokensBurnt<T>>::get(collection.id)203 .checked_add(1)204 .ok_or(ArithmeticError::Overflow)?;205206 <TokensBurnt<T>>::insert(collection.id, burnt);207 <TokenData<T>>::remove((collection.id, token_id));208 <TotalSupply<T>>::remove((collection.id, token_id));209 <Balance<T>>::remove_prefix((collection.id, token_id), None);210 <Allowance<T>>::remove_prefix((collection.id, token_id), None);211 // TODO: ERC721 transfer event212 Ok(())213 }214215 pub fn burn(216 collection: &RefungibleHandle<T>,217 owner: &T::CrossAccountId,218 token: TokenId,219 amount: u128,220 ) -> DispatchResult {221 let total_supply = <TotalSupply<T>>::get((collection.id, token))222 .checked_sub(amount)223 .ok_or(<CommonError<T>>::TokenValueTooLow)?;224225 // This was probally last owner of this token?226 if total_supply == 0 {227 // Ensure user actually owns this amount228 ensure!(229 <Balance<T>>::get((collection.id, token, owner)) == amount,230 <CommonError<T>>::TokenValueTooLow231 );232 let account_balance = <AccountBalance<T>>::get((collection.id, owner))233 .checked_sub(1)234 // Should not occur235 .ok_or(ArithmeticError::Underflow)?;236237 // =========238239 <Owned<T>>::remove((collection.id, owner, token));240 <AccountBalance<T>>::insert((collection.id, owner), account_balance);241 Self::burn_token(collection, token)?;242 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(243 collection.id,244 token,245 owner.clone(),246 amount,247 ));248 return Ok(());249 }250251 let balance = <Balance<T>>::get((collection.id, token, owner))252 .checked_sub(amount)253 .ok_or(<CommonError<T>>::TokenValueTooLow)?;254 let account_balance = if balance == 0 {255 <AccountBalance<T>>::get((collection.id, owner))256 .checked_sub(1)257 // Should not occur258 .ok_or(ArithmeticError::Underflow)?259 } else {260 0261 };262263 // =========264265 if balance == 0 {266 <Owned<T>>::remove((collection.id, owner, token));267 <Balance<T>>::remove((collection.id, token, owner));268 <AccountBalance<T>>::insert((collection.id, owner), account_balance);269 } else {270 <Balance<T>>::insert((collection.id, token, owner), balance);271 }272 <TotalSupply<T>>::insert((collection.id, token), total_supply);273 // TODO: ERC20 transfer event274 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(275 collection.id,276 token,277 owner.clone(),278 amount,279 ));280 Ok(())281 }282283 pub fn transfer(284 collection: &RefungibleHandle<T>,285 from: &T::CrossAccountId,286 to: &T::CrossAccountId,287 token: TokenId,288 amount: u128,289 ) -> DispatchResult {290 ensure!(291 collection.limits.transfers_enabled(),292 <CommonError<T>>::TransferNotAllowed293 );294295 if collection.access == AccessMode::AllowList {296 collection.check_allowlist(from)?;297 collection.check_allowlist(to)?;298 }299 <PalletCommon<T>>::ensure_correct_receiver(to)?;300301 let balance_from = <Balance<T>>::get((collection.id, token, from))302 .checked_sub(amount)303 .ok_or(<CommonError<T>>::TokenValueTooLow)?;304 let mut create_target = false;305 let from_to_differ = from != to;306 let balance_to = if from != to {307 let old_balance = <Balance<T>>::get((collection.id, token, to));308 if old_balance == 0 {309 create_target = true;310 }311 Some(312 old_balance313 .checked_add(amount)314 .ok_or(ArithmeticError::Overflow)?,315 )316 } else {317 None318 };319320 let account_balance_from = if balance_from == 0 {321 Some(322 <AccountBalance<T>>::get((collection.id, from))323 .checked_sub(1)324 // Should not occur325 .ok_or(ArithmeticError::Underflow)?,326 )327 } else {328 None329 };330 // Account data is created in token, AccountBalance should be increased331 // But only if from != to as we shouldn't check overflow in this case332 let account_balance_to = if create_target && from_to_differ {333 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))334 .checked_add(1)335 .ok_or(ArithmeticError::Overflow)?;336 ensure!(337 account_balance_to < collection.limits.account_token_ownership_limit(),338 <CommonError<T>>::AccountTokenLimitExceeded,339 );340341 Some(account_balance_to)342 } else {343 None344 };345346 if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {347 let handle = <CollectionHandle<T>>::try_get(target.0)?;348 let dispatch = T::CollectionDispatch::dispatch(handle);349 let dispatch = dispatch.as_dyn();350351 // =========352353 dispatch.nest_token(from.clone(), (collection.id, token), target.1)?;354 }355356 if let Some(balance_to) = balance_to {357 // from != to358 if balance_from == 0 {359 <Balance<T>>::remove((collection.id, token, from));360 } else {361 <Balance<T>>::insert((collection.id, token, from), balance_from);362 }363 <Balance<T>>::insert((collection.id, token, to), balance_to);364 if let Some(account_balance_from) = account_balance_from {365 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);366 <Owned<T>>::remove((collection.id, from, token));367 }368 if let Some(account_balance_to) = account_balance_to {369 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);370 <Owned<T>>::insert((collection.id, to, token), true);371 }372 }373374 // TODO: ERC20 transfer event375 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(376 collection.id,377 token,378 from.clone(),379 to.clone(),380 amount,381 ));382 Ok(())383 }384385 pub fn create_multiple_items(386 collection: &RefungibleHandle<T>,387 sender: &T::CrossAccountId,388 data: Vec<CreateRefungibleExData<T::CrossAccountId>>,389 ) -> DispatchResult {390 if !collection.is_owner_or_admin(sender) {391 ensure!(392 collection.mint_mode,393 <CommonError<T>>::PublicMintingNotAllowed394 );395 collection.check_allowlist(sender)?;396397 for item in data.iter() {398 for user in item.users.keys() {399 collection.check_allowlist(user)?;400 }401 }402 }403404 for item in data.iter() {405 for (owner, _) in item.users.iter() {406 <PalletCommon<T>>::ensure_correct_receiver(owner)?;407 }408 }409410 // Total pieces per tokens411 let totals = data412 .iter()413 .map(|data| {414 Ok(data415 .users416 .iter()417 .map(|u| u.1)418 .try_fold(0u128, |acc, v| acc.checked_add(*v))419 .ok_or(ArithmeticError::Overflow)?)420 })421 .collect::<Result<Vec<_>, DispatchError>>()?;422 for total in &totals {423 ensure!(424 *total <= MAX_REFUNGIBLE_PIECES,425 <Error<T>>::WrongRefungiblePieces426 );427 }428429 let first_token_id = <TokensMinted<T>>::get(collection.id);430 let tokens_minted = first_token_id431 .checked_add(data.len() as u32)432 .ok_or(ArithmeticError::Overflow)?;433 ensure!(434 tokens_minted < collection.limits.token_limit(),435 <CommonError<T>>::CollectionTokenLimitExceeded436 );437438 let mut balances = BTreeMap::new();439 for data in &data {440 for owner in data.users.keys() {441 let balance = balances442 .entry(owner)443 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));444 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;445446 ensure!(447 *balance <= collection.limits.account_token_ownership_limit(),448 <CommonError<T>>::AccountTokenLimitExceeded,449 );450 }451 }452453 // =========454455 <TokensMinted<T>>::insert(collection.id, tokens_minted);456 for (account, balance) in balances {457 <AccountBalance<T>>::insert((collection.id, account), balance);458 }459 for (i, token) in data.into_iter().enumerate() {460 let token_id = first_token_id + i as u32 + 1;461 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);462463 <TokenData<T>>::insert(464 (collection.id, token_id),465 ItemData {466 const_data: token.const_data,467 variable_data: token.variable_data,468 },469 );470 for (user, amount) in token.users.into_iter() {471 if amount == 0 {472 continue;473 }474 <Balance<T>>::insert((collection.id, token_id, &user), amount);475 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);476 // TODO: ERC20 transfer event477 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(478 collection.id,479 TokenId(token_id),480 user,481 amount,482 ));483 }484 }485 Ok(())486 }487488 pub fn set_allowance_unchecked(489 collection: &RefungibleHandle<T>,490 sender: &T::CrossAccountId,491 spender: &T::CrossAccountId,492 token: TokenId,493 amount: u128,494 ) {495 if amount == 0 {496 <Allowance<T>>::remove((collection.id, token, sender, spender));497 } else {498 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);499 }500 // TODO: ERC20 approval event501 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(502 collection.id,503 token,504 sender.clone(),505 spender.clone(),506 amount,507 ))508 }509510 pub fn set_allowance(511 collection: &RefungibleHandle<T>,512 sender: &T::CrossAccountId,513 spender: &T::CrossAccountId,514 token: TokenId,515 amount: u128,516 ) -> DispatchResult {517 if collection.access == AccessMode::AllowList {518 collection.check_allowlist(sender)?;519 collection.check_allowlist(spender)?;520 }521522 <PalletCommon<T>>::ensure_correct_receiver(spender)?;523524 if <Balance<T>>::get((collection.id, token, sender)) < amount {525 ensure!(526 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),527 <CommonError<T>>::CantApproveMoreThanOwned528 );529 }530531 // =========532533 Self::set_allowance_unchecked(collection, sender, spender, token, amount);534 Ok(())535 }536537 pub fn transfer_from(538 collection: &RefungibleHandle<T>,539 spender: &T::CrossAccountId,540 from: &T::CrossAccountId,541 to: &T::CrossAccountId,542 token: TokenId,543 amount: u128,544 ) -> DispatchResult {545 if spender.conv_eq(from) {546 return Self::transfer(collection, from, to, token, amount);547 }548 if collection.access == AccessMode::AllowList {549 // `from`, `to` checked in [`transfer`]550 collection.check_allowlist(spender)?;551 }552553 let allowance =554 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);555 if allowance.is_none() {556 ensure!(557 collection.ignores_allowance(spender),558 <CommonError<T>>::ApprovedValueTooLow559 );560 }561562 // =========563564 Self::transfer(collection, from, to, token, amount)?;565 if let Some(allowance) = allowance {566 Self::set_allowance_unchecked(collection, from, spender, token, allowance);567 }568 Ok(())569 }570571 pub fn burn_from(572 collection: &RefungibleHandle<T>,573 spender: &T::CrossAccountId,574 from: &T::CrossAccountId,575 token: TokenId,576 amount: u128,577 ) -> DispatchResult {578 if spender.conv_eq(from) {579 return Self::burn(collection, from, token, amount);580 }581 if collection.access == AccessMode::AllowList {582 // `from` checked in [`burn`]583 collection.check_allowlist(spender)?;584 }585586 let allowance =587 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);588 if allowance.is_none() {589 ensure!(590 collection.ignores_allowance(spender),591 <CommonError<T>>::ApprovedValueTooLow592 );593 }594595 // =========596597 Self::burn(collection, from, token, amount)?;598 if let Some(allowance) = allowance {599 Self::set_allowance_unchecked(collection, from, spender, token, allowance);600 }601 Ok(())602 }603604 pub fn set_variable_metadata(605 collection: &RefungibleHandle<T>,606 sender: &T::CrossAccountId,607 token: TokenId,608 data: BoundedVec<u8, CustomDataLimit>,609 ) -> DispatchResult {610 collection.check_can_update_meta(611 sender,612 &T::CrossAccountId::from_sub(collection.owner.clone()),613 )?;614615 let token_data = <TokenData<T>>::get((collection.id, token));616617 // =========618619 <TokenData<T>>::insert(620 (collection.id, token),621 ItemData {622 variable_data: data,623 ..token_data624 },625 );626 Ok(())627 }628629 /// Delegated to `create_multiple_items`630 pub fn create_item(631 collection: &RefungibleHandle<T>,632 sender: &T::CrossAccountId,633 data: CreateRefungibleExData<T::CrossAccountId>,634 ) -> DispatchResult {635 Self::create_multiple_items(collection, sender, vec![data])636 }637}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use frame_support::{ensure, BoundedVec};20use up_data_structs::{21 AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,22 CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping,23};24use pallet_evm::account::CrossAccountId;25use pallet_common::{26 Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,27 CollectionHandle, dispatch::CollectionDispatch,28};29use pallet_structure::Pallet as PalletStructure;30use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};31use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};32use core::ops::Deref;33use codec::{Encode, Decode, MaxEncodedLen};34use scale_info::TypeInfo;3536pub use pallet::*;37#[cfg(feature = "runtime-benchmarks")]38pub mod benchmarking;39pub mod common;40pub mod erc;41pub mod weights;42pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;4344#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]45pub struct ItemData {46 pub const_data: BoundedVec<u8, CustomDataLimit>,47 pub variable_data: BoundedVec<u8, CustomDataLimit>,48}4950#[frame_support::pallet]51pub mod pallet {52 use super::*;53 use frame_support::{Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};54 use up_data_structs::{CollectionId, TokenId};55 use super::weights::WeightInfo;5657 #[pallet::error]58 pub enum Error<T> {59 /// Not Refungible item data used to mint in Refungible collection.60 NotRefungibleDataUsedToMintFungibleCollectionToken,61 /// Maximum refungibility exceeded62 WrongRefungiblePieces,63 /// Refungible token can't nest other tokens64 RefungibleDisallowsNesting,65 }6667 #[pallet::config]68 pub trait Config:69 frame_system::Config + pallet_common::Config + pallet_structure::Config70 {71 type WeightInfo: WeightInfo;72 }7374 #[pallet::pallet]75 #[pallet::generate_store(pub(super) trait Store)]76 pub struct Pallet<T>(_);7778 #[pallet::storage]79 pub type TokensMinted<T: Config> =80 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;81 #[pallet::storage]82 pub type TokensBurnt<T: Config> =83 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;8485 #[pallet::storage]86 pub type TokenData<T: Config> = StorageNMap<87 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),88 Value = ItemData,89 QueryKind = ValueQuery,90 >;9192 #[pallet::storage]93 pub type TotalSupply<T: Config> = StorageNMap<94 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),95 Value = u128,96 QueryKind = ValueQuery,97 >;9899 /// Used to enumerate tokens owned by account100 #[pallet::storage]101 pub type Owned<T: Config> = StorageNMap<102 Key = (103 Key<Twox64Concat, CollectionId>,104 Key<Blake2_128Concat, T::CrossAccountId>,105 Key<Twox64Concat, TokenId>,106 ),107 Value = bool,108 QueryKind = ValueQuery,109 >;110111 #[pallet::storage]112 pub type AccountBalance<T: Config> = StorageNMap<113 Key = (114 Key<Twox64Concat, CollectionId>,115 // Owner116 Key<Blake2_128Concat, T::CrossAccountId>,117 ),118 Value = u32,119 QueryKind = ValueQuery,120 >;121122 #[pallet::storage]123 pub type Balance<T: Config> = StorageNMap<124 Key = (125 Key<Twox64Concat, CollectionId>,126 Key<Twox64Concat, TokenId>,127 // Owner128 Key<Blake2_128Concat, T::CrossAccountId>,129 ),130 Value = u128,131 QueryKind = ValueQuery,132 >;133134 #[pallet::storage]135 pub type Allowance<T: Config> = StorageNMap<136 Key = (137 Key<Twox64Concat, CollectionId>,138 Key<Twox64Concat, TokenId>,139 // Owner140 Key<Blake2_128, T::CrossAccountId>,141 // Spender142 Key<Blake2_128Concat, T::CrossAccountId>,143 ),144 Value = u128,145 QueryKind = ValueQuery,146 >;147}148149pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);150impl<T: Config> RefungibleHandle<T> {151 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {152 Self(inner)153 }154 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {155 self.0156 }157}158impl<T: Config> Deref for RefungibleHandle<T> {159 type Target = pallet_common::CollectionHandle<T>;160161 fn deref(&self) -> &Self::Target {162 &self.0163 }164}165166impl<T: Config> Pallet<T> {167 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {168 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)169 }170 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {171 <TotalSupply<T>>::contains_key((collection.id, token))172 }173}174175// unchecked calls skips any permission checks176impl<T: Config> Pallet<T> {177 pub fn init_collection(178 owner: T::AccountId,179 data: CreateCollectionData<T::AccountId>,180 ) -> Result<CollectionId, DispatchError> {181 <PalletCommon<T>>::init_collection(owner, data)182 }183 pub fn destroy_collection(184 collection: RefungibleHandle<T>,185 sender: &T::CrossAccountId,186 ) -> DispatchResult {187 let id = collection.id;188189 // =========190191 PalletCommon::destroy_collection(collection.0, sender)?;192193 <TokensMinted<T>>::remove(id);194 <TokensBurnt<T>>::remove(id);195 <TokenData<T>>::remove_prefix((id,), None);196 <TotalSupply<T>>::remove_prefix((id,), None);197 <Balance<T>>::remove_prefix((id,), None);198 <Allowance<T>>::remove_prefix((id,), None);199 <Owned<T>>::remove_prefix((id,), None);200 <AccountBalance<T>>::remove_prefix((id,), None);201 Ok(())202 }203204 pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {205 let burnt = <TokensBurnt<T>>::get(collection.id)206 .checked_add(1)207 .ok_or(ArithmeticError::Overflow)?;208209 <TokensBurnt<T>>::insert(collection.id, burnt);210 <TokenData<T>>::remove((collection.id, token_id));211 <TotalSupply<T>>::remove((collection.id, token_id));212 <Balance<T>>::remove_prefix((collection.id, token_id), None);213 <Allowance<T>>::remove_prefix((collection.id, token_id), None);214 // TODO: ERC721 transfer event215 Ok(())216 }217218 pub fn burn(219 collection: &RefungibleHandle<T>,220 owner: &T::CrossAccountId,221 token: TokenId,222 amount: u128,223 ) -> DispatchResult {224 let total_supply = <TotalSupply<T>>::get((collection.id, token))225 .checked_sub(amount)226 .ok_or(<CommonError<T>>::TokenValueTooLow)?;227228 // This was probally last owner of this token?229 if total_supply == 0 {230 // Ensure user actually owns this amount231 ensure!(232 <Balance<T>>::get((collection.id, token, owner)) == amount,233 <CommonError<T>>::TokenValueTooLow234 );235 let account_balance = <AccountBalance<T>>::get((collection.id, owner))236 .checked_sub(1)237 // Should not occur238 .ok_or(ArithmeticError::Underflow)?;239240 // =========241242 <Owned<T>>::remove((collection.id, owner, token));243 <AccountBalance<T>>::insert((collection.id, owner), account_balance);244 Self::burn_token(collection, token)?;245 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(246 collection.id,247 token,248 owner.clone(),249 amount,250 ));251 return Ok(());252 }253254 let balance = <Balance<T>>::get((collection.id, token, owner))255 .checked_sub(amount)256 .ok_or(<CommonError<T>>::TokenValueTooLow)?;257 let account_balance = if balance == 0 {258 <AccountBalance<T>>::get((collection.id, owner))259 .checked_sub(1)260 // Should not occur261 .ok_or(ArithmeticError::Underflow)?262 } else {263 0264 };265266 // =========267268 if balance == 0 {269 <Owned<T>>::remove((collection.id, owner, token));270 <Balance<T>>::remove((collection.id, token, owner));271 <AccountBalance<T>>::insert((collection.id, owner), account_balance);272 } else {273 <Balance<T>>::insert((collection.id, token, owner), balance);274 }275 <TotalSupply<T>>::insert((collection.id, token), total_supply);276 // TODO: ERC20 transfer event277 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(278 collection.id,279 token,280 owner.clone(),281 amount,282 ));283 Ok(())284 }285286 pub fn transfer(287 collection: &RefungibleHandle<T>,288 from: &T::CrossAccountId,289 to: &T::CrossAccountId,290 token: TokenId,291 amount: u128,292 ) -> DispatchResult {293 ensure!(294 collection.limits.transfers_enabled(),295 <CommonError<T>>::TransferNotAllowed296 );297298 if collection.access == AccessMode::AllowList {299 collection.check_allowlist(from)?;300 collection.check_allowlist(to)?;301 }302 <PalletCommon<T>>::ensure_correct_receiver(to)?;303304 let balance_from = <Balance<T>>::get((collection.id, token, from))305 .checked_sub(amount)306 .ok_or(<CommonError<T>>::TokenValueTooLow)?;307 let mut create_target = false;308 let from_to_differ = from != to;309 let balance_to = if from != to {310 let old_balance = <Balance<T>>::get((collection.id, token, to));311 if old_balance == 0 {312 create_target = true;313 }314 Some(315 old_balance316 .checked_add(amount)317 .ok_or(ArithmeticError::Overflow)?,318 )319 } else {320 None321 };322323 let account_balance_from = if balance_from == 0 {324 Some(325 <AccountBalance<T>>::get((collection.id, from))326 .checked_sub(1)327 // Should not occur328 .ok_or(ArithmeticError::Underflow)?,329 )330 } else {331 None332 };333 // Account data is created in token, AccountBalance should be increased334 // But only if from != to as we shouldn't check overflow in this case335 let account_balance_to = if create_target && from_to_differ {336 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))337 .checked_add(1)338 .ok_or(ArithmeticError::Overflow)?;339 ensure!(340 account_balance_to < collection.limits.account_token_ownership_limit(),341 <CommonError<T>>::AccountTokenLimitExceeded,342 );343344 Some(account_balance_to)345 } else {346 None347 };348349 if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {350 let handle = <CollectionHandle<T>>::try_get(target.0)?;351 let dispatch = T::CollectionDispatch::dispatch(handle);352 let dispatch = dispatch.as_dyn();353354 // =========355356 dispatch.nest_token(from.clone(), (collection.id, token), target.1)?;357 }358359 if let Some(balance_to) = balance_to {360 // from != to361 if balance_from == 0 {362 <Balance<T>>::remove((collection.id, token, from));363 } else {364 <Balance<T>>::insert((collection.id, token, from), balance_from);365 }366 <Balance<T>>::insert((collection.id, token, to), balance_to);367 if let Some(account_balance_from) = account_balance_from {368 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);369 <Owned<T>>::remove((collection.id, from, token));370 }371 if let Some(account_balance_to) = account_balance_to {372 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);373 <Owned<T>>::insert((collection.id, to, token), true);374 }375 }376377 // TODO: ERC20 transfer event378 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(379 collection.id,380 token,381 from.clone(),382 to.clone(),383 amount,384 ));385 Ok(())386 }387388 pub fn create_multiple_items(389 collection: &RefungibleHandle<T>,390 sender: &T::CrossAccountId,391 data: Vec<CreateRefungibleExData<T::CrossAccountId>>,392 ) -> DispatchResult {393 if !collection.is_owner_or_admin(sender) {394 ensure!(395 collection.mint_mode,396 <CommonError<T>>::PublicMintingNotAllowed397 );398 collection.check_allowlist(sender)?;399400 for item in data.iter() {401 for user in item.users.keys() {402 collection.check_allowlist(user)?;403 }404 }405 }406407 for item in data.iter() {408 for (owner, _) in item.users.iter() {409 <PalletCommon<T>>::ensure_correct_receiver(owner)?;410 }411 }412413 // Total pieces per tokens414 let totals = data415 .iter()416 .map(|data| {417 Ok(data418 .users419 .iter()420 .map(|u| u.1)421 .try_fold(0u128, |acc, v| acc.checked_add(*v))422 .ok_or(ArithmeticError::Overflow)?)423 })424 .collect::<Result<Vec<_>, DispatchError>>()?;425 for total in &totals {426 ensure!(427 *total <= MAX_REFUNGIBLE_PIECES,428 <Error<T>>::WrongRefungiblePieces429 );430 }431432 let first_token_id = <TokensMinted<T>>::get(collection.id);433 let tokens_minted = first_token_id434 .checked_add(data.len() as u32)435 .ok_or(ArithmeticError::Overflow)?;436 ensure!(437 tokens_minted < collection.limits.token_limit(),438 <CommonError<T>>::CollectionTokenLimitExceeded439 );440441 let mut balances = BTreeMap::new();442 for data in &data {443 for owner in data.users.keys() {444 let balance = balances445 .entry(owner)446 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));447 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;448449 ensure!(450 *balance <= collection.limits.account_token_ownership_limit(),451 <CommonError<T>>::AccountTokenLimitExceeded,452 );453 }454 }455456 // =========457458 <TokensMinted<T>>::insert(collection.id, tokens_minted);459 for (account, balance) in balances {460 <AccountBalance<T>>::insert((collection.id, account), balance);461 }462 for (i, token) in data.into_iter().enumerate() {463 let token_id = first_token_id + i as u32 + 1;464 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);465466 <TokenData<T>>::insert(467 (collection.id, token_id),468 ItemData {469 const_data: token.const_data,470 variable_data: token.variable_data,471 },472 );473 for (user, amount) in token.users.into_iter() {474 if amount == 0 {475 continue;476 }477 <Balance<T>>::insert((collection.id, token_id, &user), amount);478 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);479 // TODO: ERC20 transfer event480 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(481 collection.id,482 TokenId(token_id),483 user,484 amount,485 ));486 }487 }488 Ok(())489 }490491 pub fn set_allowance_unchecked(492 collection: &RefungibleHandle<T>,493 sender: &T::CrossAccountId,494 spender: &T::CrossAccountId,495 token: TokenId,496 amount: u128,497 ) {498 if amount == 0 {499 <Allowance<T>>::remove((collection.id, token, sender, spender));500 } else {501 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);502 }503 // TODO: ERC20 approval event504 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(505 collection.id,506 token,507 sender.clone(),508 spender.clone(),509 amount,510 ))511 }512513 pub fn set_allowance(514 collection: &RefungibleHandle<T>,515 sender: &T::CrossAccountId,516 spender: &T::CrossAccountId,517 token: TokenId,518 amount: u128,519 ) -> DispatchResult {520 if collection.access == AccessMode::AllowList {521 collection.check_allowlist(sender)?;522 collection.check_allowlist(spender)?;523 }524525 <PalletCommon<T>>::ensure_correct_receiver(spender)?;526527 if <Balance<T>>::get((collection.id, token, sender)) < amount {528 ensure!(529 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),530 <CommonError<T>>::CantApproveMoreThanOwned531 );532 }533534 // =========535536 Self::set_allowance_unchecked(collection, sender, spender, token, amount);537 Ok(())538 }539540 /// Returns allowance, which should be set after transaction541 fn check_allowed(542 collection: &RefungibleHandle<T>,543 spender: &T::CrossAccountId,544 from: &T::CrossAccountId,545 token: TokenId,546 amount: u128,547 ) -> Result<Option<u128>, DispatchError> {548 if spender.conv_eq(from) {549 return Ok(None);550 }551 if collection.access == AccessMode::AllowList {552 // `from`, `to` checked in [`transfer`]553 collection.check_allowlist(spender)?;554 }555 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {556 // TODO: should collection owner be allowed to perform this transfer?557 ensure!(558 <PalletStructure<T>>::indirectly_owned(spender.clone(), source.0, source.1, 1)?,559 <CommonError<T>>::ApprovedValueTooLow,560 );561 return Ok(None);562 }563 let allowance =564 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);565 if allowance.is_none() {566 ensure!(567 collection.ignores_allowance(spender),568 <CommonError<T>>::ApprovedValueTooLow569 );570 }571 Ok(allowance)572 }573574 pub fn transfer_from(575 collection: &RefungibleHandle<T>,576 spender: &T::CrossAccountId,577 from: &T::CrossAccountId,578 to: &T::CrossAccountId,579 token: TokenId,580 amount: u128,581 ) -> DispatchResult {582 let allowance = Self::check_allowed(collection, spender, from, token, amount)?;583584 // =========585586 Self::transfer(collection, from, to, token, amount)?;587 if let Some(allowance) = allowance {588 Self::set_allowance_unchecked(collection, from, spender, token, allowance);589 }590 Ok(())591 }592593 pub fn burn_from(594 collection: &RefungibleHandle<T>,595 spender: &T::CrossAccountId,596 from: &T::CrossAccountId,597 token: TokenId,598 amount: u128,599 ) -> DispatchResult {600 let allowance = Self::check_allowed(collection, spender, from, token, amount)?;601602 // =========603604 Self::burn(collection, from, token, amount)?;605 if let Some(allowance) = allowance {606 Self::set_allowance_unchecked(collection, from, spender, token, allowance);607 }608 Ok(())609 }610611 pub fn set_variable_metadata(612 collection: &RefungibleHandle<T>,613 sender: &T::CrossAccountId,614 token: TokenId,615 data: BoundedVec<u8, CustomDataLimit>,616 ) -> DispatchResult {617 collection.check_can_update_meta(618 sender,619 &T::CrossAccountId::from_sub(collection.owner.clone()),620 )?;621622 let token_data = <TokenData<T>>::get((collection.id, token));623624 // =========625626 <TokenData<T>>::insert(627 (collection.id, token),628 ItemData {629 variable_data: data,630 ..token_data631 },632 );633 Ok(())634 }635636 /// Delegated to `create_multiple_items`637 pub fn create_item(638 collection: &RefungibleHandle<T>,639 sender: &T::CrossAccountId,640 data: CreateRefungibleExData<T::CrossAccountId>,641 ) -> DispatchResult {642 Self::create_multiple_items(collection, sender, vec![data])643 }644}