difftreelog
CORE-410 Implement token_owner for Refungible token
in: master
2 files changed
pallets/refungible/src/common.rsdiffbeforeafterboth--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -413,8 +413,8 @@
TokenId(<TokensMinted<T>>::get(self.id))
}
- fn token_owner(&self, _token: TokenId) -> Option<T::CrossAccountId> {
- None
+ fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId> {
+ <Pallet<T>>::token_owner(self.id, token)
}
fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option<PropertyValue> {
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//! # Refungible Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Refungible pallet provides functions for:29//!30//! - RFT collection creation and removal31//! - Minting and burning of RFT tokens32//! - Partition and repartition of RFT tokens33//! - Retrieving number of pieces of RFT token34//! - Retrieving account balances35//! - Transfering RFT token pieces36//! - Burning RFT token pieces37//! - Setting and checking allowance for RFT tokens38//!39//! ### Terminology40//!41//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all42//! of the RFT token pieces than it owns the RFT token and can repartition it.43//!44//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.45//! Each collection has its own settings and set of permissions.46//!47//! - **RFT token piece:** A fungible part of an RFT token.48//!49//! - **Balance:** RFT token pieces owned by an account50//!51//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to52//! transfer from the balance of another account53//!54//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from55//! an account balance.56//!57//! ### Implementations58//!59//! The Refungible pallet provides implementations for the following traits. If these traits provide60//! the functionality that you need, then you can avoid coupling with the Refungible pallet.61//!62//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight63//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing64//! with collections65//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible66//! collection67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for73//! some accounts.74//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.75//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.76//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.77//! Nests the RFT token if RFT token pieces are sent to another token.78//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.80//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.81//!82//! ## Assumptions83//!84//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.85//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.86//! * Sender should be in collection's allow list to perform operations on tokens.8788#![cfg_attr(not(feature = "std"), no_std)]8990use frame_support::{ensure, fail, BoundedVec, transactional, storage::with_transaction};91use up_data_structs::{92 AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,93 CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget,94 Property, PropertyScope, TrySetProperty, PropertyKey, PropertyValue, PropertyPermission,95 PropertyKeyPermission,96};97use pallet_evm::account::CrossAccountId;98use pallet_common::{99 Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,100 CommonCollectionOperations as _,101};102use pallet_structure::Pallet as PalletStructure;103use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};104use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};105use core::ops::Deref;106use codec::{Encode, Decode, MaxEncodedLen};107use scale_info::TypeInfo;108109pub use pallet::*;110#[cfg(feature = "runtime-benchmarks")]111pub mod benchmarking;112pub mod common;113pub mod erc;114pub mod weights;115pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;116117#[struct_versioning::versioned(version = 2, upper)]118#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]119pub struct ItemData {120 pub const_data: BoundedVec<u8, CustomDataLimit>,121122 #[version(..2)]123 pub variable_data: BoundedVec<u8, CustomDataLimit>,124}125126#[frame_support::pallet]127pub mod pallet {128 use super::*;129 use frame_support::{130 Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,131 traits::StorageVersion,132 };133 use frame_system::pallet_prelude::*;134 use up_data_structs::{CollectionId, TokenId};135 use super::weights::WeightInfo;136137 #[pallet::error]138 pub enum Error<T> {139 /// Not Refungible item data used to mint in Refungible collection.140 NotRefungibleDataUsedToMintFungibleCollectionToken,141 /// Maximum refungibility exceeded142 WrongRefungiblePieces,143 /// Refungible token can't be repartitioned by user who isn't owns all pieces144 RepartitionWhileNotOwningAllPieces,145 /// Refungible token can't nest other tokens146 RefungibleDisallowsNesting,147 /// Setting item properties is not allowed148 SettingPropertiesNotAllowed,149 }150151 #[pallet::config]152 pub trait Config:153 frame_system::Config + pallet_common::Config + pallet_structure::Config154 {155 type WeightInfo: WeightInfo;156 }157158 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);159160 #[pallet::pallet]161 #[pallet::storage_version(STORAGE_VERSION)]162 #[pallet::generate_store(pub(super) trait Store)]163 pub struct Pallet<T>(_);164165 /// Amount of tokens minted for collection166 #[pallet::storage]167 pub type TokensMinted<T: Config> =168 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;169170 /// Amount of burnt tokens for collection171 #[pallet::storage]172 pub type TokensBurnt<T: Config> =173 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;174175 /// Custom data serialized to bytes for token176 #[pallet::storage]177 pub type TokenData<T: Config> = StorageNMap<178 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),179 Value = ItemData,180 QueryKind = ValueQuery,181 >;182183 #[pallet::storage]184 #[pallet::getter(fn token_properties)]185 pub type TokenProperties<T: Config> = StorageNMap<186 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),187 Value = up_data_structs::Properties,188 QueryKind = ValueQuery,189 OnEmpty = up_data_structs::TokenProperties,190 >;191192 /// Total amount of pieces for token193 #[pallet::storage]194 pub type TotalSupply<T: Config> = StorageNMap<195 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),196 Value = u128,197 QueryKind = ValueQuery,198 >;199200 /// Used to enumerate tokens owned by account201 #[pallet::storage]202 pub type Owned<T: Config> = StorageNMap<203 Key = (204 Key<Twox64Concat, CollectionId>,205 Key<Blake2_128Concat, T::CrossAccountId>,206 Key<Twox64Concat, TokenId>,207 ),208 Value = bool,209 QueryKind = ValueQuery,210 >;211212 /// Amount of tokens owned by account213 #[pallet::storage]214 pub type AccountBalance<T: Config> = StorageNMap<215 Key = (216 Key<Twox64Concat, CollectionId>,217 // Owner218 Key<Blake2_128Concat, T::CrossAccountId>,219 ),220 Value = u32,221 QueryKind = ValueQuery,222 >;223224 /// Amount of token pieces owned by account225 #[pallet::storage]226 pub type Balance<T: Config> = StorageNMap<227 Key = (228 Key<Twox64Concat, CollectionId>,229 Key<Twox64Concat, TokenId>,230 // Owner231 Key<Blake2_128Concat, T::CrossAccountId>,232 ),233 Value = u128,234 QueryKind = ValueQuery,235 >;236237 /// Allowance set by an owner for a spender for a token238 #[pallet::storage]239 pub type Allowance<T: Config> = StorageNMap<240 Key = (241 Key<Twox64Concat, CollectionId>,242 Key<Twox64Concat, TokenId>,243 // Owner244 Key<Blake2_128, T::CrossAccountId>,245 // Spender246 Key<Blake2_128Concat, T::CrossAccountId>,247 ),248 Value = u128,249 QueryKind = ValueQuery,250 >;251252 #[pallet::hooks]253 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {254 fn on_runtime_upgrade() -> Weight {255 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {256 <TokenData<T>>::translate_values::<ItemDataVersion1, _>(|v| {257 Some(<ItemDataVersion2>::from(v))258 })259 }260261 0262 }263 }264}265266pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);267impl<T: Config> RefungibleHandle<T> {268 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {269 Self(inner)270 }271 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {272 self.0273 }274}275impl<T: Config> Deref for RefungibleHandle<T> {276 type Target = pallet_common::CollectionHandle<T>;277278 fn deref(&self) -> &Self::Target {279 &self.0280 }281}282283impl<T: Config> Pallet<T> {284 /// Get number of RFT tokens in collection285 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {286 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)287 }288289 /// Check that RFT token exists290 ///291 /// - `token`: Token ID.292 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {293 <TotalSupply<T>>::contains_key((collection.id, token))294 }295296 pub fn set_scoped_token_property(297 collection_id: CollectionId,298 token_id: TokenId,299 scope: PropertyScope,300 property: Property,301 ) -> DispatchResult {302 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {303 properties.try_scoped_set(scope, property.key, property.value)304 })305 .map_err(<CommonError<T>>::from)?;306307 Ok(())308 }309310 pub fn set_scoped_token_properties(311 collection_id: CollectionId,312 token_id: TokenId,313 scope: PropertyScope,314 properties: impl Iterator<Item = Property>,315 ) -> DispatchResult {316 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {317 stored_properties.try_scoped_set_from_iter(scope, properties)318 })319 .map_err(<CommonError<T>>::from)?;320321 Ok(())322 }323}324325// unchecked calls skips any permission checks326impl<T: Config> Pallet<T> {327 /// Create RFT collection328 ///329 /// `init_collection` will take non-refundable deposit for collection creation.330 ///331 /// - `data`: Contains settings for collection limits and permissions.332 pub fn init_collection(333 owner: T::CrossAccountId,334 data: CreateCollectionData<T::AccountId>,335 ) -> Result<CollectionId, DispatchError> {336 <PalletCommon<T>>::init_collection(owner, data, false)337 }338339 /// Destroy RFT collection340 ///341 /// `destroy_collection` will throw error if collection contains any tokens.342 /// Only owner can destroy collection.343 pub fn destroy_collection(344 collection: RefungibleHandle<T>,345 sender: &T::CrossAccountId,346 ) -> DispatchResult {347 let id = collection.id;348349 if Self::collection_has_tokens(id) {350 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());351 }352353 // =========354355 PalletCommon::destroy_collection(collection.0, sender)?;356357 <TokensMinted<T>>::remove(id);358 <TokensBurnt<T>>::remove(id);359 <TokenData<T>>::remove_prefix((id,), None);360 <TotalSupply<T>>::remove_prefix((id,), None);361 <Balance<T>>::remove_prefix((id,), None);362 <Allowance<T>>::remove_prefix((id,), None);363 <Owned<T>>::remove_prefix((id,), None);364 <AccountBalance<T>>::remove_prefix((id,), None);365 Ok(())366 }367368 fn collection_has_tokens(collection_id: CollectionId) -> bool {369 <TokenData<T>>::iter_prefix((collection_id,))370 .next()371 .is_some()372 }373374 pub fn burn_token_unchecked(375 collection: &RefungibleHandle<T>,376 token_id: TokenId,377 ) -> DispatchResult {378 let burnt = <TokensBurnt<T>>::get(collection.id)379 .checked_add(1)380 .ok_or(ArithmeticError::Overflow)?;381382 <TokensBurnt<T>>::insert(collection.id, burnt);383 <TokenData<T>>::remove((collection.id, token_id));384 <TokenProperties<T>>::remove((collection.id, token_id));385 <TotalSupply<T>>::remove((collection.id, token_id));386 <Balance<T>>::remove_prefix((collection.id, token_id), None);387 <Allowance<T>>::remove_prefix((collection.id, token_id), None);388 // TODO: ERC721 transfer event389 Ok(())390 }391392 /// Burn RFT token pieces393 ///394 /// `burn` will decrease total amount of token pieces and amount owned by sender.395 /// `burn` can be called even if there are multiple owners of the RFT token.396 /// If sender wouldn't have any pieces left after `burn` than she will stop being397 /// one of the owners of the token. If there is no account that owns any pieces of398 /// the token than token will be burned too.399 ///400 /// - `amount`: Amount of token pieces to burn.401 /// - `token`: Token who's pieces should be burned402 /// - `collection`: Collection that contains the token403 pub fn burn(404 collection: &RefungibleHandle<T>,405 owner: &T::CrossAccountId,406 token: TokenId,407 amount: u128,408 ) -> DispatchResult {409 let total_supply = <TotalSupply<T>>::get((collection.id, token))410 .checked_sub(amount)411 .ok_or(<CommonError<T>>::TokenValueTooLow)?;412413 // This was probally last owner of this token?414 if total_supply == 0 {415 // Ensure user actually owns this amount416 ensure!(417 <Balance<T>>::get((collection.id, token, owner)) == amount,418 <CommonError<T>>::TokenValueTooLow419 );420 let account_balance = <AccountBalance<T>>::get((collection.id, owner))421 .checked_sub(1)422 // Should not occur423 .ok_or(ArithmeticError::Underflow)?;424425 // =========426427 <Owned<T>>::remove((collection.id, owner, token));428 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);429 <AccountBalance<T>>::insert((collection.id, owner), account_balance);430 Self::burn_token_unchecked(collection, token)?;431 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(432 collection.id,433 token,434 owner.clone(),435 amount,436 ));437 return Ok(());438 }439440 let balance = <Balance<T>>::get((collection.id, token, owner))441 .checked_sub(amount)442 .ok_or(<CommonError<T>>::TokenValueTooLow)?;443 let account_balance = if balance == 0 {444 <AccountBalance<T>>::get((collection.id, owner))445 .checked_sub(1)446 // Should not occur447 .ok_or(ArithmeticError::Underflow)?448 } else {449 0450 };451452 // =========453454 if balance == 0 {455 <Owned<T>>::remove((collection.id, owner, token));456 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);457 <Balance<T>>::remove((collection.id, token, owner));458 <AccountBalance<T>>::insert((collection.id, owner), account_balance);459 } else {460 <Balance<T>>::insert((collection.id, token, owner), balance);461 }462 <TotalSupply<T>>::insert((collection.id, token), total_supply);463 // TODO: ERC20 transfer event464 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(465 collection.id,466 token,467 owner.clone(),468 amount,469 ));470 Ok(())471 }472473 #[transactional]474 fn modify_token_properties(475 collection: &RefungibleHandle<T>,476 sender: &T::CrossAccountId,477 token_id: TokenId,478 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,479 is_token_create: bool,480 nesting_budget: &dyn Budget,481 ) -> DispatchResult {482 let is_collection_admin = || collection.is_owner_or_admin(sender);483 let is_token_owner = || -> Result<bool, DispatchError> {484 let balance = collection.balance(sender.clone(), token_id);485 let total_pieces: u128 =486 Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);487 if balance != total_pieces {488 return Ok(false);489 }490491 let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(492 sender.clone(),493 collection.id,494 token_id,495 None,496 nesting_budget,497 )?;498499 Ok(is_bundle_owner)500 };501502 for (key, value) in properties {503 let permission = <PalletCommon<T>>::property_permissions(collection.id)504 .get(&key)505 .cloned()506 .unwrap_or_else(PropertyPermission::none);507508 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))509 .get(&key)510 .is_some();511512 match permission {513 PropertyPermission { mutable: false, .. } if is_property_exists => {514 return Err(<CommonError<T>>::NoPermission.into());515 }516517 PropertyPermission {518 collection_admin,519 token_owner,520 ..521 } => {522 //TODO: investigate threats during public minting.523 let is_token_create =524 is_token_create && (collection_admin || token_owner) && value.is_some();525 if !(is_token_create526 || (collection_admin && is_collection_admin())527 || (token_owner && is_token_owner()?))528 {529 fail!(<CommonError<T>>::NoPermission);530 }531 }532 }533534 match value {535 Some(value) => {536 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {537 properties.try_set(key.clone(), value)538 })539 .map_err(<CommonError<T>>::from)?;540541 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(542 collection.id,543 token_id,544 key,545 ));546 }547 None => {548 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {549 properties.remove(&key)550 })551 .map_err(<CommonError<T>>::from)?;552553 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(554 collection.id,555 token_id,556 key,557 ));558 }559 }560 }561562 Ok(())563 }564565 pub fn set_token_properties(566 collection: &RefungibleHandle<T>,567 sender: &T::CrossAccountId,568 token_id: TokenId,569 properties: impl Iterator<Item = Property>,570 is_token_create: bool,571 nesting_budget: &dyn Budget,572 ) -> DispatchResult {573 Self::modify_token_properties(574 collection,575 sender,576 token_id,577 properties.map(|p| (p.key, Some(p.value))),578 is_token_create,579 nesting_budget,580 )581 }582583 pub fn set_token_property(584 collection: &RefungibleHandle<T>,585 sender: &T::CrossAccountId,586 token_id: TokenId,587 property: Property,588 nesting_budget: &dyn Budget,589 ) -> DispatchResult {590 let is_token_create = false;591592 Self::set_token_properties(593 collection,594 sender,595 token_id,596 [property].into_iter(),597 is_token_create,598 nesting_budget,599 )600 }601602 pub fn delete_token_properties(603 collection: &RefungibleHandle<T>,604 sender: &T::CrossAccountId,605 token_id: TokenId,606 property_keys: impl Iterator<Item = PropertyKey>,607 nesting_budget: &dyn Budget,608 ) -> DispatchResult {609 let is_token_create = false;610611 Self::modify_token_properties(612 collection,613 sender,614 token_id,615 property_keys.into_iter().map(|key| (key, None)),616 is_token_create,617 nesting_budget,618 )619 }620621 pub fn delete_token_property(622 collection: &RefungibleHandle<T>,623 sender: &T::CrossAccountId,624 token_id: TokenId,625 property_key: PropertyKey,626 nesting_budget: &dyn Budget,627 ) -> DispatchResult {628 Self::delete_token_properties(629 collection,630 sender,631 token_id,632 [property_key].into_iter(),633 nesting_budget,634 )635 }636637 /// Transfer RFT token pieces from one account to another.638 ///639 /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.640 ///641 /// - `from`: Owner of token pieces to transfer.642 /// - `to`: Recepient of transfered token pieces.643 /// - `amount`: Amount of token pieces to transfer.644 /// - `token`: Token whos pieces should be transfered645 /// - `collection`: Collection that contains the token646 pub fn transfer(647 collection: &RefungibleHandle<T>,648 from: &T::CrossAccountId,649 to: &T::CrossAccountId,650 token: TokenId,651 amount: u128,652 nesting_budget: &dyn Budget,653 ) -> DispatchResult {654 ensure!(655 collection.limits.transfers_enabled(),656 <CommonError<T>>::TransferNotAllowed657 );658659 if collection.permissions.access() == AccessMode::AllowList {660 collection.check_allowlist(from)?;661 collection.check_allowlist(to)?;662 }663 <PalletCommon<T>>::ensure_correct_receiver(to)?;664665 let balance_from = <Balance<T>>::get((collection.id, token, from))666 .checked_sub(amount)667 .ok_or(<CommonError<T>>::TokenValueTooLow)?;668 let mut create_target = false;669 let from_to_differ = from != to;670 let balance_to = if from != to {671 let old_balance = <Balance<T>>::get((collection.id, token, to));672 if old_balance == 0 {673 create_target = true;674 }675 Some(676 old_balance677 .checked_add(amount)678 .ok_or(ArithmeticError::Overflow)?,679 )680 } else {681 None682 };683684 let account_balance_from = if balance_from == 0 {685 Some(686 <AccountBalance<T>>::get((collection.id, from))687 .checked_sub(1)688 // Should not occur689 .ok_or(ArithmeticError::Underflow)?,690 )691 } else {692 None693 };694 // Account data is created in token, AccountBalance should be increased695 // But only if from != to as we shouldn't check overflow in this case696 let account_balance_to = if create_target && from_to_differ {697 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))698 .checked_add(1)699 .ok_or(ArithmeticError::Overflow)?;700 ensure!(701 account_balance_to < collection.limits.account_token_ownership_limit(),702 <CommonError<T>>::AccountTokenLimitExceeded,703 );704705 Some(account_balance_to)706 } else {707 None708 };709710 // =========711712 <PalletStructure<T>>::nest_if_sent_to_token(713 from.clone(),714 to,715 collection.id,716 token,717 nesting_budget,718 )?;719720 if let Some(balance_to) = balance_to {721 // from != to722 if balance_from == 0 {723 <Balance<T>>::remove((collection.id, token, from));724 <PalletStructure<T>>::unnest_if_nested(from, collection.id, token);725 } else {726 <Balance<T>>::insert((collection.id, token, from), balance_from);727 }728 <Balance<T>>::insert((collection.id, token, to), balance_to);729 if let Some(account_balance_from) = account_balance_from {730 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);731 <Owned<T>>::remove((collection.id, from, token));732 }733 if let Some(account_balance_to) = account_balance_to {734 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);735 <Owned<T>>::insert((collection.id, to, token), true);736 }737 }738739 // TODO: ERC20 transfer event740 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(741 collection.id,742 token,743 from.clone(),744 to.clone(),745 amount,746 ));747 Ok(())748 }749750 /// Batched operation to create multiple RFT tokens.751 ///752 /// Same as `create_item` but creates multiple tokens.753 ///754 /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.755 pub fn create_multiple_items(756 collection: &RefungibleHandle<T>,757 sender: &T::CrossAccountId,758 data: Vec<CreateRefungibleExData<T::CrossAccountId>>,759 nesting_budget: &dyn Budget,760 ) -> DispatchResult {761 if !collection.is_owner_or_admin(sender) {762 ensure!(763 collection.permissions.mint_mode(),764 <CommonError<T>>::PublicMintingNotAllowed765 );766 collection.check_allowlist(sender)?;767768 for item in data.iter() {769 for user in item.users.keys() {770 collection.check_allowlist(user)?;771 }772 }773 }774775 for item in data.iter() {776 for (owner, _) in item.users.iter() {777 <PalletCommon<T>>::ensure_correct_receiver(owner)?;778 }779 }780781 // Total pieces per tokens782 let totals = data783 .iter()784 .map(|data| {785 Ok(data786 .users787 .iter()788 .map(|u| u.1)789 .try_fold(0u128, |acc, v| acc.checked_add(*v))790 .ok_or(ArithmeticError::Overflow)?)791 })792 .collect::<Result<Vec<_>, DispatchError>>()?;793 for total in &totals {794 ensure!(795 *total <= MAX_REFUNGIBLE_PIECES,796 <Error<T>>::WrongRefungiblePieces797 );798 }799800 let first_token_id = <TokensMinted<T>>::get(collection.id);801 let tokens_minted = first_token_id802 .checked_add(data.len() as u32)803 .ok_or(ArithmeticError::Overflow)?;804 ensure!(805 tokens_minted < collection.limits.token_limit(),806 <CommonError<T>>::CollectionTokenLimitExceeded807 );808809 let mut balances = BTreeMap::new();810 for data in &data {811 for owner in data.users.keys() {812 let balance = balances813 .entry(owner)814 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));815 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;816817 ensure!(818 *balance <= collection.limits.account_token_ownership_limit(),819 <CommonError<T>>::AccountTokenLimitExceeded,820 );821 }822 }823824 for (i, token) in data.iter().enumerate() {825 let token_id = TokenId(first_token_id + i as u32 + 1);826 for (to, _) in token.users.iter() {827 <PalletStructure<T>>::check_nesting(828 sender.clone(),829 to,830 collection.id,831 token_id,832 nesting_budget,833 )?;834 }835 }836837 // =========838839 with_transaction(|| {840 for (i, data) in data.iter().enumerate() {841 let token_id = first_token_id + i as u32 + 1;842 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);843844 <TokenData<T>>::insert(845 (collection.id, token_id),846 ItemData {847 const_data: data.const_data.clone(),848 },849 );850851 for (user, amount) in data.users.iter() {852 if *amount == 0 {853 continue;854 }855 <Balance<T>>::insert((collection.id, token_id, &user), amount);856 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);857 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(858 user,859 collection.id,860 TokenId(token_id),861 );862 }863864 if let Err(e) = Self::set_token_properties(865 collection,866 sender,867 TokenId(token_id),868 data.properties.clone().into_iter(),869 true,870 nesting_budget,871 ) {872 return TransactionOutcome::Rollback(Err(e));873 }874 }875 TransactionOutcome::Commit(Ok(()))876 })?;877878 <TokensMinted<T>>::insert(collection.id, tokens_minted);879880 for (account, balance) in balances {881 <AccountBalance<T>>::insert((collection.id, account), balance);882 }883884 for (i, token) in data.into_iter().enumerate() {885 let token_id = first_token_id + i as u32 + 1;886887 for (user, amount) in token.users.into_iter() {888 if amount == 0 {889 continue;890 }891892 // TODO: ERC20 transfer event893 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(894 collection.id,895 TokenId(token_id),896 user,897 amount,898 ));899 }900 }901 Ok(())902 }903904 pub fn set_allowance_unchecked(905 collection: &RefungibleHandle<T>,906 sender: &T::CrossAccountId,907 spender: &T::CrossAccountId,908 token: TokenId,909 amount: u128,910 ) {911 if amount == 0 {912 <Allowance<T>>::remove((collection.id, token, sender, spender));913 } else {914 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);915 }916 // TODO: ERC20 approval event917 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(918 collection.id,919 token,920 sender.clone(),921 spender.clone(),922 amount,923 ))924 }925926 /// Set allowance for the spender to `transfer` or `burn` sender's token pieces.927 ///928 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.929 pub fn set_allowance(930 collection: &RefungibleHandle<T>,931 sender: &T::CrossAccountId,932 spender: &T::CrossAccountId,933 token: TokenId,934 amount: u128,935 ) -> DispatchResult {936 if collection.permissions.access() == AccessMode::AllowList {937 collection.check_allowlist(sender)?;938 collection.check_allowlist(spender)?;939 }940941 <PalletCommon<T>>::ensure_correct_receiver(spender)?;942943 if <Balance<T>>::get((collection.id, token, sender)) < amount {944 ensure!(945 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),946 <CommonError<T>>::CantApproveMoreThanOwned947 );948 }949950 // =========951952 Self::set_allowance_unchecked(collection, sender, spender, token, amount);953 Ok(())954 }955956 /// Returns allowance, which should be set after transaction957 fn check_allowed(958 collection: &RefungibleHandle<T>,959 spender: &T::CrossAccountId,960 from: &T::CrossAccountId,961 token: TokenId,962 amount: u128,963 nesting_budget: &dyn Budget,964 ) -> Result<Option<u128>, DispatchError> {965 if spender.conv_eq(from) {966 return Ok(None);967 }968 if collection.permissions.access() == AccessMode::AllowList {969 // `from`, `to` checked in [`transfer`]970 collection.check_allowlist(spender)?;971 }972 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {973 // TODO: should collection owner be allowed to perform this transfer?974 ensure!(975 <PalletStructure<T>>::check_indirectly_owned(976 spender.clone(),977 source.0,978 source.1,979 None,980 nesting_budget981 )?,982 <CommonError<T>>::ApprovedValueTooLow,983 );984 return Ok(None);985 }986 let allowance =987 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);988 if allowance.is_none() {989 ensure!(990 collection.ignores_allowance(spender),991 <CommonError<T>>::ApprovedValueTooLow992 );993 }994 Ok(allowance)995 }996997 /// Transfer RFT token pieces from one account to another.998 ///999 /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1000 /// The owner should set allowance for the spender to transfer pieces.1001 ///1002 /// [`transfer`]: struct.Pallet.html#method.transfer1003 pub fn transfer_from(1004 collection: &RefungibleHandle<T>,1005 spender: &T::CrossAccountId,1006 from: &T::CrossAccountId,1007 to: &T::CrossAccountId,1008 token: TokenId,1009 amount: u128,1010 nesting_budget: &dyn Budget,1011 ) -> DispatchResult {1012 let allowance =1013 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10141015 // =========10161017 Self::transfer(collection, from, to, token, amount, nesting_budget)?;1018 if let Some(allowance) = allowance {1019 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1020 }1021 Ok(())1022 }10231024 /// Burn RFT token pieces from the account.1025 ///1026 /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1027 /// set allowance for the spender to burn pieces1028 ///1029 /// [`burn`]: struct.Pallet.html#method.burn1030 pub fn burn_from(1031 collection: &RefungibleHandle<T>,1032 spender: &T::CrossAccountId,1033 from: &T::CrossAccountId,1034 token: TokenId,1035 amount: u128,1036 nesting_budget: &dyn Budget,1037 ) -> DispatchResult {1038 let allowance =1039 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10401041 // =========10421043 Self::burn(collection, from, token, amount)?;1044 if let Some(allowance) = allowance {1045 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1046 }1047 Ok(())1048 }10491050 /// Create RFT token.1051 ///1052 /// The sender should be the owner/admin of the collection or collection should be configured1053 /// to allow public minting.1054 ///1055 /// - `data`: Contains list of users who will become the owners of the token pieces and amount1056 /// of token pieces they will receive.1057 pub fn create_item(1058 collection: &RefungibleHandle<T>,1059 sender: &T::CrossAccountId,1060 data: CreateRefungibleExData<T::CrossAccountId>,1061 nesting_budget: &dyn Budget,1062 ) -> DispatchResult {1063 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1064 }10651066 /// Repartition RFT token.1067 ///1068 /// `repartition` will set token balance of the sender and total amount of token pieces.1069 /// Sender should own all of the token pieces. `repartition' could be done even if some1070 /// token pieces were burned before.1071 ///1072 /// - `amount`: Total amount of token pieces that the token will have after `repartition`.1073 pub fn repartition(1074 collection: &RefungibleHandle<T>,1075 owner: &T::CrossAccountId,1076 token: TokenId,1077 amount: u128,1078 ) -> DispatchResult {1079 ensure!(1080 amount <= MAX_REFUNGIBLE_PIECES,1081 <Error<T>>::WrongRefungiblePieces1082 );1083 ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1084 // Ensure user owns all pieces1085 let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1086 let balance = <Balance<T>>::get((collection.id, token, owner));1087 ensure!(1088 total_pieces == balance,1089 <Error<T>>::RepartitionWhileNotOwningAllPieces1090 );10911092 <Balance<T>>::insert((collection.id, token, owner), amount);1093 <TotalSupply<T>>::insert((collection.id, token), amount);1094 Ok(())1095 }10961097 fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1098 <TotalSupply<T>>::try_get((collection_id, token_id)).ok()1099 }11001101 pub fn set_collection_properties(1102 collection: &RefungibleHandle<T>,1103 sender: &T::CrossAccountId,1104 properties: Vec<Property>,1105 ) -> DispatchResult {1106 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)1107 }11081109 pub fn delete_collection_properties(1110 collection: &RefungibleHandle<T>,1111 sender: &T::CrossAccountId,1112 property_keys: Vec<PropertyKey>,1113 ) -> DispatchResult {1114 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1115 }11161117 pub fn set_token_property_permissions(1118 collection: &RefungibleHandle<T>,1119 sender: &T::CrossAccountId,1120 property_permissions: Vec<PropertyKeyPermission>,1121 ) -> DispatchResult {1122 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1123 }1124}