difftreelog
feature: make collection creation methods `payable`
in: master
19 files changed
crates/evm-coder/procedural/src/solidity_interface.rsdiffbeforeafterboth--- a/crates/evm-coder/procedural/src/solidity_interface.rs
+++ b/crates/evm-coder/procedural/src/solidity_interface.rs
@@ -560,6 +560,7 @@
selector: u32,
args: Vec<MethodArg>,
has_normal_args: bool,
+ has_value_args: bool,
mutability: Mutability,
result: Type,
weight: Option<Expr>,
@@ -647,14 +648,20 @@
.unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string()));
let mut selector_str = camel_name.clone();
selector_str.push('(');
- let mut has_normal_args = false;
- for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() {
- if i != 0 {
- selector_str.push(',');
+ let mut normal_args_count = 0u32;
+ let mut has_value_args = false;
+ for arg in args.iter() {
+ if arg.is_value() {
+ has_value_args = true;
+ } else if !arg.is_special() {
+ if normal_args_count != 0 {
+ selector_str.push(',');
+ }
+ write!(selector_str, "{}", arg.selector_ty()).unwrap();
+ normal_args_count = normal_args_count.saturating_add(1);
}
- write!(selector_str, "{}", arg.selector_ty()).unwrap();
- has_normal_args = true;
}
+ let has_normal_args = normal_args_count > 0;
selector_str.push(')');
let selector = fn_selector_str(&selector_str);
@@ -667,6 +674,7 @@
selector,
args,
has_normal_args,
+ has_value_args,
mutability,
result: result.clone(),
weight,
@@ -823,7 +831,7 @@
let docs = &self.docs;
let selector_str = &self.selector_str;
let selector = self.selector;
-
+ let is_payable = self.has_value_args;
quote! {
SolidityFunction {
docs: &[#(#docs),*],
@@ -831,6 +839,7 @@
selector: #selector,
name: #camel_name,
mutability: #mutability,
+ is_payable: #is_payable,
args: (
#(
#args,
crates/evm-coder/src/solidity.rsdiffbeforeafterboth--- a/crates/evm-coder/src/solidity.rs
+++ b/crates/evm-coder/src/solidity.rs
@@ -422,6 +422,7 @@
pub args: A,
pub result: R,
pub mutability: SolidityMutability,
+ pub is_payable: bool,
}
impl<A: SolidityArguments, R: SolidityArguments> SolidityFunctions for SolidityFunction<A, R> {
fn solidity_name(
@@ -452,6 +453,9 @@
SolidityMutability::View => write!(writer, " view")?,
SolidityMutability::Mutable => {}
}
+ if self.is_payable {
+ write!(writer, " payable")?;
+ }
if !self.result.is_empty() {
write!(writer, " returns (")?;
self.result.solidity_name(writer, tc)?;
pallets/common/src/dispatch.rsdiffbeforeafterboth--- a/pallets/common/src/dispatch.rs
+++ b/pallets/common/src/dispatch.rs
@@ -78,6 +78,7 @@
/// * `data` - Description of the created collection.
fn create(
sender: T::CrossAccountId,
+ payer: T::CrossAccountId,
data: CreateCollectionData<T::AccountId>,
) -> Result<CollectionId, DispatchError>;
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -866,6 +866,7 @@
/// * `flags` - Extra flags to store.
pub fn init_collection(
owner: T::CrossAccountId,
+ payer: T::CrossAccountId,
data: CreateCollectionData<T::AccountId>,
flags: CollectionFlags,
) -> Result<CollectionId, DispatchError> {
@@ -939,7 +940,7 @@
),
);
<T as Config>::Currency::settle(
- owner.as_sub(),
+ payer.as_sub(),
imbalance,
WithdrawReasons::TRANSFER,
ExistenceRequirement::KeepAlive,
pallets/foreign-assets/src/lib.rsdiffbeforeafterboth--- a/pallets/foreign-assets/src/lib.rs
+++ b/pallets/foreign-assets/src/lib.rs
@@ -309,9 +309,10 @@
mode: CollectionMode::Fungible(md.decimals),
..Default::default()
};
-
+ let owner = T::CrossAccountId::from_sub(owner);
let bounded_collection_id = <PalletFungible<T>>::init_foreign_collection(
- CrossAccountId::from_sub(owner),
+ owner.clone(),
+ owner,
data,
)?;
let foreign_asset_id =
pallets/fungible/src/lib.rsdiffbeforeafterboth--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -210,18 +210,21 @@
/// Initializes the collection. Returns [CollectionId] on success, [DispatchError] otherwise.
pub fn init_collection(
owner: T::CrossAccountId,
+ payer: T::CrossAccountId,
data: CreateCollectionData<T::AccountId>,
) -> Result<CollectionId, DispatchError> {
- <PalletCommon<T>>::init_collection(owner, data, CollectionFlags::default())
+ <PalletCommon<T>>::init_collection(owner, payer, data, CollectionFlags::default())
}
/// Initializes the collection with ForeignCollection flag. Returns [CollectionId] on success, [DispatchError] otherwise.
pub fn init_foreign_collection(
owner: T::CrossAccountId,
+ payer: T::CrossAccountId,
data: CreateCollectionData<T::AccountId>,
) -> Result<CollectionId, DispatchError> {
let id = <PalletCommon<T>>::init_collection(
owner,
+ payer,
data,
CollectionFlags {
foreign: true,
pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -405,11 +405,13 @@
/// - `data`: Contains settings for collection limits and permissions.
pub fn init_collection(
owner: T::CrossAccountId,
+ payer: T::CrossAccountId,
data: CreateCollectionData<T::AccountId>,
is_external: bool,
) -> Result<CollectionId, DispatchError> {
<PalletCommon<T>>::init_collection(
owner,
+ payer,
data,
CollectionFlags {
external: is_external,
pallets/proxy-rmrk-core/src/lib.rsdiffbeforeafterboth--- a/pallets/proxy-rmrk-core/src/lib.rs
+++ b/pallets/proxy-rmrk-core/src/lib.rs
@@ -1448,7 +1448,7 @@
data: CreateCollectionData<T::AccountId>,
properties: impl Iterator<Item = Property>,
) -> Result<CollectionId, DispatchError> {
- let collection_id = <PalletNft<T>>::init_collection(sender, data, true);
+ let collection_id = <PalletNft<T>>::init_collection(sender.clone(), sender, data, true);
if let Err(DispatchError::Arithmetic(_)) = &collection_id {
return Err(<Error<T>>::NoAvailableCollectionId.into());
pallets/proxy-rmrk-equip/src/lib.rsdiffbeforeafterboth--- a/pallets/proxy-rmrk-equip/src/lib.rs
+++ b/pallets/proxy-rmrk-equip/src/lib.rs
@@ -251,7 +251,7 @@
};
let collection_id_res =
- <PalletNft<T>>::init_collection(cross_sender.clone(), data, true);
+ <PalletNft<T>>::init_collection(cross_sender.clone(), cross_sender.clone(), data, true);
if let Err(DispatchError::Arithmetic(_)) = &collection_id_res {
return Err(<Error<T>>::NoAvailableBaseId.into());
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`](common::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 crate::erc_token::ERC20Events;91use crate::erc::ERC721Events;9293use codec::{Encode, Decode, MaxEncodedLen};94use core::ops::Deref;95use evm_coder::ToLog;96use frame_support::{97 BoundedVec, ensure, fail, storage::with_transaction, transactional, pallet_prelude::ConstU32,98};99use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};100use pallet_evm_coder_substrate::WithRecorder;101use pallet_common::{102 CollectionHandle, CommonCollectionOperations,103 dispatch::CollectionDispatch,104 erc::static_property::{key, property_value_from_bytes},105 Error as CommonError,106 eth::collection_id_to_address,107 Event as CommonEvent, Pallet as PalletCommon,108};109use pallet_structure::Pallet as PalletStructure;110use scale_info::TypeInfo;111use sp_core::H160;112use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};113use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};114use up_data_structs::{115 AccessMode, budget::Budget, CollectionId, CollectionMode, CollectionFlags,116 CollectionPropertiesVec, CreateCollectionData, CustomDataLimit, mapping::TokenAddressMapping,117 MAX_ITEMS_PER_BATCH, MAX_REFUNGIBLE_PIECES, Property, PropertyKey, PropertyKeyPermission,118 PropertyPermission, PropertyScope, PropertyValue, TokenId, TrySetProperty,119};120use frame_support::BoundedBTreeMap;121use derivative::Derivative;122123pub use pallet::*;124#[cfg(feature = "runtime-benchmarks")]125pub mod benchmarking;126pub mod common;127pub mod erc;128pub mod erc_token;129pub mod weights;130131#[derive(Derivative, Clone)]132pub struct CreateItemData<CrossAccountId> {133 #[derivative(Debug(format_with = "bounded::map_debug"))]134 pub users: BoundedBTreeMap<CrossAccountId, u128, ConstU32<MAX_ITEMS_PER_BATCH>>,135 #[derivative(Debug(format_with = "bounded::vec_debug"))]136 pub properties: CollectionPropertiesVec,137}138pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;139140/// Token data, stored independently from other data used to describe it141/// for the convenience of database access. Notably contains the token metadata.142#[struct_versioning::versioned(version = 2, upper)]143#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]144#[deprecated(since = "0.2.0", note = "ItemData is no more contains usefull data")]145pub struct ItemData {146 pub const_data: BoundedVec<u8, CustomDataLimit>,147148 #[version(..2)]149 pub variable_data: BoundedVec<u8, CustomDataLimit>,150}151152#[frame_support::pallet]153pub mod pallet {154 use super::*;155 use frame_support::{156 Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,157 traits::StorageVersion,158 };159 use frame_system::pallet_prelude::*;160 use up_data_structs::{CollectionId, TokenId};161 use super::weights::WeightInfo;162163 #[pallet::error]164 pub enum Error<T> {165 /// Not Refungible item data used to mint in Refungible collection.166 NotRefungibleDataUsedToMintFungibleCollectionToken,167 /// Maximum refungibility exceeded.168 WrongRefungiblePieces,169 /// Refungible token can't be repartitioned by user who isn't owns all pieces.170 RepartitionWhileNotOwningAllPieces,171 /// Refungible token can't nest other tokens.172 RefungibleDisallowsNesting,173 /// Setting item properties is not allowed.174 SettingPropertiesNotAllowed,175 }176177 #[pallet::config]178 pub trait Config:179 frame_system::Config + pallet_common::Config + pallet_structure::Config180 {181 type WeightInfo: WeightInfo;182 }183184 const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);185186 #[pallet::pallet]187 #[pallet::storage_version(STORAGE_VERSION)]188 #[pallet::generate_store(pub(super) trait Store)]189 pub struct Pallet<T>(_);190191 /// Total amount of minted tokens in a collection.192 #[pallet::storage]193 pub type TokensMinted<T: Config> =194 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;195196 /// Amount of tokens burnt in a collection.197 #[pallet::storage]198 pub type TokensBurnt<T: Config> =199 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;200201 /// Token data, used to partially describe a token.202 // TODO: remove203 #[pallet::storage]204 #[deprecated(since = "0.2.0", note = "ItemData is no more contains usefull data")]205 pub type TokenData<T: Config> = StorageNMap<206 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),207 Value = ItemData,208 QueryKind = ValueQuery,209 >;210211 /// Amount of pieces a refungible token is split into.212 #[pallet::storage]213 #[pallet::getter(fn token_properties)]214 pub type TokenProperties<T: Config> = StorageNMap<215 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),216 Value = up_data_structs::Properties,217 QueryKind = ValueQuery,218 OnEmpty = up_data_structs::TokenProperties,219 >;220221 /// Total amount of pieces for token222 #[pallet::storage]223 pub type TotalSupply<T: Config> = StorageNMap<224 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),225 Value = u128,226 QueryKind = ValueQuery,227 >;228229 /// Used to enumerate tokens owned by account.230 #[pallet::storage]231 pub type Owned<T: Config> = StorageNMap<232 Key = (233 Key<Twox64Concat, CollectionId>,234 Key<Blake2_128Concat, T::CrossAccountId>,235 Key<Twox64Concat, TokenId>,236 ),237 Value = bool,238 QueryKind = ValueQuery,239 >;240241 /// Amount of tokens (not pieces) partially owned by an account within a collection.242 #[pallet::storage]243 pub type AccountBalance<T: Config> = StorageNMap<244 Key = (245 Key<Twox64Concat, CollectionId>,246 // Owner247 Key<Blake2_128Concat, T::CrossAccountId>,248 ),249 Value = u32,250 QueryKind = ValueQuery,251 >;252253 /// Amount of token pieces owned by account.254 #[pallet::storage]255 pub type Balance<T: Config> = StorageNMap<256 Key = (257 Key<Twox64Concat, CollectionId>,258 Key<Twox64Concat, TokenId>,259 // Owner260 Key<Blake2_128Concat, T::CrossAccountId>,261 ),262 Value = u128,263 QueryKind = ValueQuery,264 >;265266 /// Allowance set by a token owner for another user to perform one of certain transactions on a number of pieces of a token.267 #[pallet::storage]268 pub type Allowance<T: Config> = StorageNMap<269 Key = (270 Key<Twox64Concat, CollectionId>,271 Key<Twox64Concat, TokenId>,272 // Owner273 Key<Blake2_128, T::CrossAccountId>,274 // Spender275 Key<Blake2_128Concat, T::CrossAccountId>,276 ),277 Value = u128,278 QueryKind = ValueQuery,279 >;280281 #[pallet::hooks]282 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {283 fn on_runtime_upgrade() -> Weight {284 let storage_version = StorageVersion::get::<Pallet<T>>();285 if storage_version < StorageVersion::new(2) {286 <TokenData<T>>::remove_all(None);287 }288 StorageVersion::new(2).put::<Pallet<T>>();289290 0291 }292 }293}294295pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);296impl<T: Config> RefungibleHandle<T> {297 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {298 Self(inner)299 }300 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {301 self.0302 }303 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {304 &mut self.0305 }306}307308impl<T: Config> Deref for RefungibleHandle<T> {309 type Target = pallet_common::CollectionHandle<T>;310311 fn deref(&self) -> &Self::Target {312 &self.0313 }314}315316impl<T: Config> WithRecorder<T> for RefungibleHandle<T> {317 fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {318 self.0.recorder()319 }320 fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {321 self.0.into_recorder()322 }323}324325impl<T: Config> Pallet<T> {326 /// Get number of RFT tokens in collection327 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {328 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)329 }330331 /// Check that RFT token exists332 ///333 /// - `token`: Token ID.334 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {335 <TotalSupply<T>>::contains_key((collection.id, token))336 }337338 pub fn set_scoped_token_property(339 collection_id: CollectionId,340 token_id: TokenId,341 scope: PropertyScope,342 property: Property,343 ) -> DispatchResult {344 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {345 properties.try_scoped_set(scope, property.key, property.value)346 })347 .map_err(<CommonError<T>>::from)?;348349 Ok(())350 }351352 pub fn set_scoped_token_properties(353 collection_id: CollectionId,354 token_id: TokenId,355 scope: PropertyScope,356 properties: impl Iterator<Item = Property>,357 ) -> DispatchResult {358 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {359 stored_properties.try_scoped_set_from_iter(scope, properties)360 })361 .map_err(<CommonError<T>>::from)?;362363 Ok(())364 }365}366367// unchecked calls skips any permission checks368impl<T: Config> Pallet<T> {369 /// Create RFT collection370 ///371 /// `init_collection` will take non-refundable deposit for collection creation.372 ///373 /// - `data`: Contains settings for collection limits and permissions.374 pub fn init_collection(375 owner: T::CrossAccountId,376 data: CreateCollectionData<T::AccountId>,377 ) -> Result<CollectionId, DispatchError> {378 <PalletCommon<T>>::init_collection(owner, data, CollectionFlags::default())379 }380381 /// Destroy RFT collection382 ///383 /// `destroy_collection` will throw error if collection contains any tokens.384 /// Only owner can destroy collection.385 pub fn destroy_collection(386 collection: RefungibleHandle<T>,387 sender: &T::CrossAccountId,388 ) -> DispatchResult {389 let id = collection.id;390391 if Self::collection_has_tokens(id) {392 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());393 }394395 // =========396397 PalletCommon::destroy_collection(collection.0, sender)?;398399 <TokensMinted<T>>::remove(id);400 <TokensBurnt<T>>::remove(id);401 let _ = <TotalSupply<T>>::clear_prefix((id,), u32::MAX, None);402 let _ = <Balance<T>>::clear_prefix((id,), u32::MAX, None);403 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);404 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);405 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);406 Ok(())407 }408409 fn collection_has_tokens(collection_id: CollectionId) -> bool {410 <TotalSupply<T>>::iter_prefix((collection_id,))411 .next()412 .is_some()413 }414415 pub fn burn_token_unchecked(416 collection: &RefungibleHandle<T>,417 owner: &T::CrossAccountId,418 token_id: TokenId,419 ) -> DispatchResult {420 let burnt = <TokensBurnt<T>>::get(collection.id)421 .checked_add(1)422 .ok_or(ArithmeticError::Overflow)?;423424 <TokensBurnt<T>>::insert(collection.id, burnt);425 <TokenProperties<T>>::remove((collection.id, token_id));426 <TotalSupply<T>>::remove((collection.id, token_id));427 let _ = <Balance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);428 let _ = <Allowance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);429 <PalletEvm<T>>::deposit_log(430 ERC721Events::Transfer {431 from: *owner.as_eth(),432 to: H160::default(),433 token_id: token_id.into(),434 }435 .to_log(collection_id_to_address(collection.id)),436 );437 Ok(())438 }439440 /// Burn RFT token pieces441 ///442 /// `burn` will decrease total amount of token pieces and amount owned by sender.443 /// `burn` can be called even if there are multiple owners of the RFT token.444 /// If sender wouldn't have any pieces left after `burn` than she will stop being445 /// one of the owners of the token. If there is no account that owns any pieces of446 /// the token than token will be burned too.447 ///448 /// - `amount`: Amount of token pieces to burn.449 /// - `token`: Token who's pieces should be burned450 /// - `collection`: Collection that contains the token451 pub fn burn(452 collection: &RefungibleHandle<T>,453 owner: &T::CrossAccountId,454 token: TokenId,455 amount: u128,456 ) -> DispatchResult {457 let total_supply = <TotalSupply<T>>::get((collection.id, token))458 .checked_sub(amount)459 .ok_or(<CommonError<T>>::TokenValueTooLow)?;460461 // This was probally last owner of this token?462 if total_supply == 0 {463 // Ensure user actually owns this amount464 ensure!(465 <Balance<T>>::get((collection.id, token, owner)) == amount,466 <CommonError<T>>::TokenValueTooLow467 );468 let account_balance = <AccountBalance<T>>::get((collection.id, owner))469 .checked_sub(1)470 // Should not occur471 .ok_or(ArithmeticError::Underflow)?;472473 // =========474475 <Owned<T>>::remove((collection.id, owner, token));476 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);477 <AccountBalance<T>>::insert((collection.id, owner), account_balance);478 Self::burn_token_unchecked(collection, owner, token)?;479 <PalletEvm<T>>::deposit_log(480 ERC20Events::Transfer {481 from: *owner.as_eth(),482 to: H160::default(),483 value: amount.into(),484 }485 .to_log(collection_id_to_address(collection.id)),486 );487 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(488 collection.id,489 token,490 owner.clone(),491 amount,492 ));493 return Ok(());494 }495496 let balance = <Balance<T>>::get((collection.id, token, owner))497 .checked_sub(amount)498 .ok_or(<CommonError<T>>::TokenValueTooLow)?;499 let account_balance = if balance == 0 {500 <AccountBalance<T>>::get((collection.id, owner))501 .checked_sub(1)502 // Should not occur503 .ok_or(ArithmeticError::Underflow)?504 } else {505 0506 };507508 // =========509510 if balance == 0 {511 <Owned<T>>::remove((collection.id, owner, token));512 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);513 <Balance<T>>::remove((collection.id, token, owner));514 <AccountBalance<T>>::insert((collection.id, owner), account_balance);515516 if let Some(user) = Self::token_owner(collection.id, token) {517 <PalletEvm<T>>::deposit_log(518 ERC721Events::Transfer {519 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,520 to: *user.as_eth(),521 token_id: token.into(),522 }523 .to_log(collection_id_to_address(collection.id)),524 );525 }526 } else {527 <Balance<T>>::insert((collection.id, token, owner), balance);528 }529 <TotalSupply<T>>::insert((collection.id, token), total_supply);530531 <PalletEvm<T>>::deposit_log(532 ERC20Events::Transfer {533 from: *owner.as_eth(),534 to: H160::default(),535 value: amount.into(),536 }537 .to_log(T::EvmTokenAddressMapping::token_to_address(538 collection.id,539 token,540 )),541 );542 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(543 collection.id,544 token,545 owner.clone(),546 amount,547 ));548 Ok(())549 }550551 #[transactional]552 fn modify_token_properties(553 collection: &RefungibleHandle<T>,554 sender: &T::CrossAccountId,555 token_id: TokenId,556 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,557 is_token_create: bool,558 nesting_budget: &dyn Budget,559 ) -> DispatchResult {560 let is_collection_admin = || collection.is_owner_or_admin(sender);561 let is_token_owner = || -> Result<bool, DispatchError> {562 let balance = collection.balance(sender.clone(), token_id);563 let total_pieces: u128 =564 Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);565 if balance != total_pieces {566 return Ok(false);567 }568569 let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(570 sender.clone(),571 collection.id,572 token_id,573 None,574 nesting_budget,575 )?;576577 Ok(is_bundle_owner)578 };579580 for (key, value) in properties {581 let permission = <PalletCommon<T>>::property_permissions(collection.id)582 .get(&key)583 .cloned()584 .unwrap_or_else(PropertyPermission::none);585586 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))587 .get(&key)588 .is_some();589590 match permission {591 PropertyPermission { mutable: false, .. } if is_property_exists => {592 return Err(<CommonError<T>>::NoPermission.into());593 }594595 PropertyPermission {596 collection_admin,597 token_owner,598 ..599 } => {600 //TODO: investigate threats during public minting.601 let is_token_create =602 is_token_create && (collection_admin || token_owner) && value.is_some();603 if !(is_token_create604 || (collection_admin && is_collection_admin())605 || (token_owner && is_token_owner()?))606 {607 fail!(<CommonError<T>>::NoPermission);608 }609 }610 }611612 match value {613 Some(value) => {614 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {615 properties.try_set(key.clone(), value)616 })617 .map_err(<CommonError<T>>::from)?;618619 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(620 collection.id,621 token_id,622 key,623 ));624 }625 None => {626 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {627 properties.remove(&key)628 })629 .map_err(<CommonError<T>>::from)?;630631 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(632 collection.id,633 token_id,634 key,635 ));636 }637 }638 }639640 Ok(())641 }642643 pub fn set_token_properties(644 collection: &RefungibleHandle<T>,645 sender: &T::CrossAccountId,646 token_id: TokenId,647 properties: impl Iterator<Item = Property>,648 is_token_create: bool,649 nesting_budget: &dyn Budget,650 ) -> DispatchResult {651 Self::modify_token_properties(652 collection,653 sender,654 token_id,655 properties.map(|p| (p.key, Some(p.value))),656 is_token_create,657 nesting_budget,658 )659 }660661 pub fn set_token_property(662 collection: &RefungibleHandle<T>,663 sender: &T::CrossAccountId,664 token_id: TokenId,665 property: Property,666 nesting_budget: &dyn Budget,667 ) -> DispatchResult {668 let is_token_create = false;669670 Self::set_token_properties(671 collection,672 sender,673 token_id,674 [property].into_iter(),675 is_token_create,676 nesting_budget,677 )678 }679680 pub fn delete_token_properties(681 collection: &RefungibleHandle<T>,682 sender: &T::CrossAccountId,683 token_id: TokenId,684 property_keys: impl Iterator<Item = PropertyKey>,685 nesting_budget: &dyn Budget,686 ) -> DispatchResult {687 let is_token_create = false;688689 Self::modify_token_properties(690 collection,691 sender,692 token_id,693 property_keys.into_iter().map(|key| (key, None)),694 is_token_create,695 nesting_budget,696 )697 }698699 pub fn delete_token_property(700 collection: &RefungibleHandle<T>,701 sender: &T::CrossAccountId,702 token_id: TokenId,703 property_key: PropertyKey,704 nesting_budget: &dyn Budget,705 ) -> DispatchResult {706 Self::delete_token_properties(707 collection,708 sender,709 token_id,710 [property_key].into_iter(),711 nesting_budget,712 )713 }714715 /// Transfer RFT token pieces from one account to another.716 ///717 /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.718 ///719 /// - `from`: Owner of token pieces to transfer.720 /// - `to`: Recepient of transfered token pieces.721 /// - `amount`: Amount of token pieces to transfer.722 /// - `token`: Token whos pieces should be transfered723 /// - `collection`: Collection that contains the token724 pub fn transfer(725 collection: &RefungibleHandle<T>,726 from: &T::CrossAccountId,727 to: &T::CrossAccountId,728 token: TokenId,729 amount: u128,730 nesting_budget: &dyn Budget,731 ) -> DispatchResult {732 ensure!(733 collection.limits.transfers_enabled(),734 <CommonError<T>>::TransferNotAllowed735 );736737 if collection.permissions.access() == AccessMode::AllowList {738 collection.check_allowlist(from)?;739 collection.check_allowlist(to)?;740 }741 <PalletCommon<T>>::ensure_correct_receiver(to)?;742743 let initial_balance_from = <Balance<T>>::get((collection.id, token, from));744 let updated_balance_from = initial_balance_from745 .checked_sub(amount)746 .ok_or(<CommonError<T>>::TokenValueTooLow)?;747 let mut create_target = false;748 let from_to_differ = from != to;749 let updated_balance_to = if from != to {750 let old_balance = <Balance<T>>::get((collection.id, token, to));751 if old_balance == 0 {752 create_target = true;753 }754 Some(755 old_balance756 .checked_add(amount)757 .ok_or(ArithmeticError::Overflow)?,758 )759 } else {760 None761 };762763 let account_balance_from = if updated_balance_from == 0 {764 Some(765 <AccountBalance<T>>::get((collection.id, from))766 .checked_sub(1)767 // Should not occur768 .ok_or(ArithmeticError::Underflow)?,769 )770 } else {771 None772 };773 // Account data is created in token, AccountBalance should be increased774 // But only if from != to as we shouldn't check overflow in this case775 let account_balance_to = if create_target && from_to_differ {776 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))777 .checked_add(1)778 .ok_or(ArithmeticError::Overflow)?;779 ensure!(780 account_balance_to < collection.limits.account_token_ownership_limit(),781 <CommonError<T>>::AccountTokenLimitExceeded,782 );783784 Some(account_balance_to)785 } else {786 None787 };788789 // =========790791 <PalletStructure<T>>::nest_if_sent_to_token(792 from.clone(),793 to,794 collection.id,795 token,796 nesting_budget,797 )?;798799 if let Some(updated_balance_to) = updated_balance_to {800 // from != to801 if updated_balance_from == 0 {802 <Balance<T>>::remove((collection.id, token, from));803 <PalletStructure<T>>::unnest_if_nested(from, collection.id, token);804 } else {805 <Balance<T>>::insert((collection.id, token, from), updated_balance_from);806 }807 <Balance<T>>::insert((collection.id, token, to), updated_balance_to);808 if let Some(account_balance_from) = account_balance_from {809 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);810 <Owned<T>>::remove((collection.id, from, token));811 }812 if let Some(account_balance_to) = account_balance_to {813 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);814 <Owned<T>>::insert((collection.id, to, token), true);815 }816 }817818 <PalletEvm<T>>::deposit_log(819 ERC20Events::Transfer {820 from: *from.as_eth(),821 to: *to.as_eth(),822 value: amount.into(),823 }824 .to_log(T::EvmTokenAddressMapping::token_to_address(825 collection.id,826 token,827 )),828 );829830 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(831 collection.id,832 token,833 from.clone(),834 to.clone(),835 amount,836 ));837838 let total_supply = <TotalSupply<T>>::get((collection.id, token));839840 if amount == total_supply {841 // if token was fully owned by `from` and will be fully owned by `to` after transfer842 <PalletEvm<T>>::deposit_log(843 ERC721Events::Transfer {844 from: *from.as_eth(),845 to: *to.as_eth(),846 token_id: token.into(),847 }848 .to_log(collection_id_to_address(collection.id)),849 );850 } else if let Some(updated_balance_to) = updated_balance_to {851 // if `from` not equals `to`. This condition is needed to avoid sending event852 // when `from` fully owns token and sends part of token pieces to itself.853 if initial_balance_from == total_supply {854 // if token was fully owned by `from` and will be only partially owned by `to`855 // and `from` after transfer856 <PalletEvm<T>>::deposit_log(857 ERC721Events::Transfer {858 from: *from.as_eth(),859 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,860 token_id: token.into(),861 }862 .to_log(collection_id_to_address(collection.id)),863 );864 } else if updated_balance_to == total_supply {865 // if token was partially owned by `from` and will be fully owned by `to` after transfer866 <PalletEvm<T>>::deposit_log(867 ERC721Events::Transfer {868 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,869 to: *to.as_eth(),870 token_id: token.into(),871 }872 .to_log(collection_id_to_address(collection.id)),873 );874 }875 }876877 Ok(())878 }879880 /// Batched operation to create multiple RFT tokens.881 ///882 /// Same as `create_item` but creates multiple tokens.883 ///884 /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.885 pub fn create_multiple_items(886 collection: &RefungibleHandle<T>,887 sender: &T::CrossAccountId,888 data: Vec<CreateItemData<T::CrossAccountId>>,889 nesting_budget: &dyn Budget,890 ) -> DispatchResult {891 if !collection.is_owner_or_admin(sender) {892 ensure!(893 collection.permissions.mint_mode(),894 <CommonError<T>>::PublicMintingNotAllowed895 );896 collection.check_allowlist(sender)?;897898 for item in data.iter() {899 for user in item.users.keys() {900 collection.check_allowlist(user)?;901 }902 }903 }904905 for item in data.iter() {906 for (owner, _) in item.users.iter() {907 <PalletCommon<T>>::ensure_correct_receiver(owner)?;908 }909 }910911 // Total pieces per tokens912 let totals = data913 .iter()914 .map(|data| {915 Ok(data916 .users917 .iter()918 .map(|u| u.1)919 .try_fold(0u128, |acc, v| acc.checked_add(*v))920 .ok_or(ArithmeticError::Overflow)?)921 })922 .collect::<Result<Vec<_>, DispatchError>>()?;923 for total in &totals {924 ensure!(925 *total <= MAX_REFUNGIBLE_PIECES,926 <Error<T>>::WrongRefungiblePieces927 );928 }929930 let first_token_id = <TokensMinted<T>>::get(collection.id);931 let tokens_minted = first_token_id932 .checked_add(data.len() as u32)933 .ok_or(ArithmeticError::Overflow)?;934 ensure!(935 tokens_minted < collection.limits.token_limit(),936 <CommonError<T>>::CollectionTokenLimitExceeded937 );938939 let mut balances = BTreeMap::new();940 for data in &data {941 for owner in data.users.keys() {942 let balance = balances943 .entry(owner)944 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));945 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;946947 ensure!(948 *balance <= collection.limits.account_token_ownership_limit(),949 <CommonError<T>>::AccountTokenLimitExceeded,950 );951 }952 }953954 for (i, token) in data.iter().enumerate() {955 let token_id = TokenId(first_token_id + i as u32 + 1);956 for (to, _) in token.users.iter() {957 <PalletStructure<T>>::check_nesting(958 sender.clone(),959 to,960 collection.id,961 token_id,962 nesting_budget,963 )?;964 }965 }966967 // =========968969 with_transaction(|| {970 for (i, data) in data.iter().enumerate() {971 let token_id = first_token_id + i as u32 + 1;972 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);973974 for (user, amount) in data.users.iter() {975 if *amount == 0 {976 continue;977 }978 <Balance<T>>::insert((collection.id, token_id, &user), amount);979 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);980 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(981 user,982 collection.id,983 TokenId(token_id),984 );985 }986987 if let Err(e) = Self::set_token_properties(988 collection,989 sender,990 TokenId(token_id),991 data.properties.clone().into_iter(),992 true,993 nesting_budget,994 ) {995 return TransactionOutcome::Rollback(Err(e));996 }997 }998 TransactionOutcome::Commit(Ok(()))999 })?;10001001 <TokensMinted<T>>::insert(collection.id, tokens_minted);10021003 for (account, balance) in balances {1004 <AccountBalance<T>>::insert((collection.id, account), balance);1005 }10061007 for (i, token) in data.into_iter().enumerate() {1008 let token_id = first_token_id + i as u32 + 1;10091010 let receivers = token1011 .users1012 .into_iter()1013 .filter(|(_, amount)| *amount > 0)1014 .collect::<Vec<_>>();10151016 if let [(user, _)] = receivers.as_slice() {1017 // if there is exactly one receiver1018 <PalletEvm<T>>::deposit_log(1019 ERC721Events::Transfer {1020 from: H160::default(),1021 to: *user.as_eth(),1022 token_id: token_id.into(),1023 }1024 .to_log(collection_id_to_address(collection.id)),1025 );1026 } else if let [_, ..] = receivers.as_slice() {1027 // if there is more than one receiver1028 <PalletEvm<T>>::deposit_log(1029 ERC721Events::Transfer {1030 from: H160::default(),1031 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,1032 token_id: token_id.into(),1033 }1034 .to_log(collection_id_to_address(collection.id)),1035 );1036 }10371038 for (user, amount) in receivers.into_iter() {1039 <PalletEvm<T>>::deposit_log(1040 ERC20Events::Transfer {1041 from: H160::default(),1042 to: *user.as_eth(),1043 value: amount.into(),1044 }1045 .to_log(T::EvmTokenAddressMapping::token_to_address(1046 collection.id,1047 TokenId(token_id),1048 )),1049 );1050 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1051 collection.id,1052 TokenId(token_id),1053 user,1054 amount,1055 ));1056 }1057 }1058 Ok(())1059 }10601061 pub fn set_allowance_unchecked(1062 collection: &RefungibleHandle<T>,1063 sender: &T::CrossAccountId,1064 spender: &T::CrossAccountId,1065 token: TokenId,1066 amount: u128,1067 ) {1068 if amount == 0 {1069 <Allowance<T>>::remove((collection.id, token, sender, spender));1070 } else {1071 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);1072 }10731074 <PalletEvm<T>>::deposit_log(1075 ERC20Events::Approval {1076 owner: *sender.as_eth(),1077 spender: *spender.as_eth(),1078 value: amount.into(),1079 }1080 .to_log(T::EvmTokenAddressMapping::token_to_address(1081 collection.id,1082 token,1083 )),1084 );1085 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1086 collection.id,1087 token,1088 sender.clone(),1089 spender.clone(),1090 amount,1091 ))1092 }10931094 /// Set allowance for the spender to `transfer` or `burn` sender's token pieces.1095 ///1096 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.1097 pub fn set_allowance(1098 collection: &RefungibleHandle<T>,1099 sender: &T::CrossAccountId,1100 spender: &T::CrossAccountId,1101 token: TokenId,1102 amount: u128,1103 ) -> DispatchResult {1104 if collection.permissions.access() == AccessMode::AllowList {1105 collection.check_allowlist(sender)?;1106 collection.check_allowlist(spender)?;1107 }11081109 <PalletCommon<T>>::ensure_correct_receiver(spender)?;11101111 if <Balance<T>>::get((collection.id, token, sender)) < amount {1112 ensure!(1113 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),1114 <CommonError<T>>::CantApproveMoreThanOwned1115 );1116 }11171118 // =========11191120 Self::set_allowance_unchecked(collection, sender, spender, token, amount);1121 Ok(())1122 }11231124 /// Returns allowance, which should be set after transaction1125 fn check_allowed(1126 collection: &RefungibleHandle<T>,1127 spender: &T::CrossAccountId,1128 from: &T::CrossAccountId,1129 token: TokenId,1130 amount: u128,1131 nesting_budget: &dyn Budget,1132 ) -> Result<Option<u128>, DispatchError> {1133 if spender.conv_eq(from) {1134 return Ok(None);1135 }1136 if collection.permissions.access() == AccessMode::AllowList {1137 // `from`, `to` checked in [`transfer`]1138 collection.check_allowlist(spender)?;1139 }1140 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1141 // TODO: should collection owner be allowed to perform this transfer?1142 ensure!(1143 <PalletStructure<T>>::check_indirectly_owned(1144 spender.clone(),1145 source.0,1146 source.1,1147 None,1148 nesting_budget1149 )?,1150 <CommonError<T>>::ApprovedValueTooLow,1151 );1152 return Ok(None);1153 }1154 let allowance =1155 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);1156 if allowance.is_none() {1157 ensure!(1158 collection.ignores_allowance(spender),1159 <CommonError<T>>::ApprovedValueTooLow1160 );1161 }1162 Ok(allowance)1163 }11641165 /// Transfer RFT token pieces from one account to another.1166 ///1167 /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1168 /// The owner should set allowance for the spender to transfer pieces.1169 ///1170 /// [`transfer`]: struct.Pallet.html#method.transfer1171 pub fn transfer_from(1172 collection: &RefungibleHandle<T>,1173 spender: &T::CrossAccountId,1174 from: &T::CrossAccountId,1175 to: &T::CrossAccountId,1176 token: TokenId,1177 amount: u128,1178 nesting_budget: &dyn Budget,1179 ) -> DispatchResult {1180 let allowance =1181 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;11821183 // =========11841185 Self::transfer(collection, from, to, token, amount, nesting_budget)?;1186 if let Some(allowance) = allowance {1187 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1188 }1189 Ok(())1190 }11911192 /// Burn RFT token pieces from the account.1193 ///1194 /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1195 /// set allowance for the spender to burn pieces1196 ///1197 /// [`burn`]: struct.Pallet.html#method.burn1198 pub fn burn_from(1199 collection: &RefungibleHandle<T>,1200 spender: &T::CrossAccountId,1201 from: &T::CrossAccountId,1202 token: TokenId,1203 amount: u128,1204 nesting_budget: &dyn Budget,1205 ) -> DispatchResult {1206 let allowance =1207 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;12081209 // =========12101211 Self::burn(collection, from, token, amount)?;1212 if let Some(allowance) = allowance {1213 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1214 }1215 Ok(())1216 }12171218 /// Create RFT token.1219 ///1220 /// The sender should be the owner/admin of the collection or collection should be configured1221 /// to allow public minting.1222 ///1223 /// - `data`: Contains list of users who will become the owners of the token pieces and amount1224 /// of token pieces they will receive.1225 pub fn create_item(1226 collection: &RefungibleHandle<T>,1227 sender: &T::CrossAccountId,1228 data: CreateItemData<T::CrossAccountId>,1229 nesting_budget: &dyn Budget,1230 ) -> DispatchResult {1231 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1232 }12331234 /// Repartition RFT token.1235 ///1236 /// `repartition` will set token balance of the sender and total amount of token pieces.1237 /// Sender should own all of the token pieces. `repartition' could be done even if some1238 /// token pieces were burned before.1239 ///1240 /// - `amount`: Total amount of token pieces that the token will have after `repartition`.1241 pub fn repartition(1242 collection: &RefungibleHandle<T>,1243 owner: &T::CrossAccountId,1244 token: TokenId,1245 amount: u128,1246 ) -> DispatchResult {1247 ensure!(1248 amount <= MAX_REFUNGIBLE_PIECES,1249 <Error<T>>::WrongRefungiblePieces1250 );1251 ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1252 // Ensure user owns all pieces1253 let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1254 let balance = <Balance<T>>::get((collection.id, token, owner));1255 ensure!(1256 total_pieces == balance,1257 <Error<T>>::RepartitionWhileNotOwningAllPieces1258 );12591260 <Balance<T>>::insert((collection.id, token, owner), amount);1261 <TotalSupply<T>>::insert((collection.id, token), amount);12621263 if amount > total_pieces {1264 let mint_amount = amount - total_pieces;1265 <PalletEvm<T>>::deposit_log(1266 ERC20Events::Transfer {1267 from: H160::default(),1268 to: *owner.as_eth(),1269 value: mint_amount.into(),1270 }1271 .to_log(T::EvmTokenAddressMapping::token_to_address(1272 collection.id,1273 token,1274 )),1275 );1276 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1277 collection.id,1278 token,1279 owner.clone(),1280 mint_amount,1281 ));1282 } else if total_pieces > amount {1283 let burn_amount = total_pieces - amount;1284 <PalletEvm<T>>::deposit_log(1285 ERC20Events::Transfer {1286 from: *owner.as_eth(),1287 to: H160::default(),1288 value: burn_amount.into(),1289 }1290 .to_log(T::EvmTokenAddressMapping::token_to_address(1291 collection.id,1292 token,1293 )),1294 );1295 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(1296 collection.id,1297 token,1298 owner.clone(),1299 burn_amount,1300 ));1301 }13021303 Ok(())1304 }13051306 fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {1307 let mut owner = None;1308 let mut count = 0;1309 for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1310 count += 1;1311 if count > 1 {1312 return None;1313 }1314 owner = Some(key);1315 }1316 owner1317 }13181319 fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1320 <TotalSupply<T>>::try_get((collection_id, token_id)).ok()1321 }13221323 pub fn set_collection_properties(1324 collection: &RefungibleHandle<T>,1325 sender: &T::CrossAccountId,1326 properties: Vec<Property>,1327 ) -> DispatchResult {1328 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)1329 }13301331 pub fn delete_collection_properties(1332 collection: &RefungibleHandle<T>,1333 sender: &T::CrossAccountId,1334 property_keys: Vec<PropertyKey>,1335 ) -> DispatchResult {1336 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1337 }13381339 pub fn set_token_property_permissions(1340 collection: &RefungibleHandle<T>,1341 sender: &T::CrossAccountId,1342 property_permissions: Vec<PropertyKeyPermission>,1343 ) -> DispatchResult {1344 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1345 }13461347 pub fn set_scoped_token_property_permissions(1348 collection: &RefungibleHandle<T>,1349 sender: &T::CrossAccountId,1350 scope: PropertyScope,1351 property_permissions: Vec<PropertyKeyPermission>,1352 ) -> DispatchResult {1353 <PalletCommon<T>>::set_scoped_token_property_permissions(1354 collection,1355 sender,1356 scope,1357 property_permissions,1358 )1359 }13601361 /// Returns 10 token in no particular order.1362 ///1363 /// There is no direct way to get token holders in ascending order,1364 /// since `iter_prefix` returns values in no particular order.1365 /// Therefore, getting the 10 largest holders with a large value of holders1366 /// can lead to impact memory allocation + sorting with `n * log (n)`.1367 pub fn token_owners(1368 collection_id: CollectionId,1369 token: TokenId,1370 ) -> Option<Vec<T::CrossAccountId>> {1371 let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1372 .map(|(owner, _amount)| owner)1373 .take(10)1374 .collect();13751376 if res.is_empty() {1377 None1378 } else {1379 Some(res)1380 }1381 }1382}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//! # Refungible Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`](common::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 crate::erc_token::ERC20Events;91use crate::erc::ERC721Events;9293use codec::{Encode, Decode, MaxEncodedLen};94use core::ops::Deref;95use evm_coder::ToLog;96use frame_support::{97 BoundedVec, ensure, fail, storage::with_transaction, transactional, pallet_prelude::ConstU32,98};99use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};100use pallet_evm_coder_substrate::WithRecorder;101use pallet_common::{102 CollectionHandle, CommonCollectionOperations,103 dispatch::CollectionDispatch,104 erc::static_property::{key, property_value_from_bytes},105 Error as CommonError,106 eth::collection_id_to_address,107 Event as CommonEvent, Pallet as PalletCommon,108};109use pallet_structure::Pallet as PalletStructure;110use scale_info::TypeInfo;111use sp_core::H160;112use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};113use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};114use up_data_structs::{115 AccessMode, budget::Budget, CollectionId, CollectionMode, CollectionFlags,116 CollectionPropertiesVec, CreateCollectionData, CustomDataLimit, mapping::TokenAddressMapping,117 MAX_ITEMS_PER_BATCH, MAX_REFUNGIBLE_PIECES, Property, PropertyKey, PropertyKeyPermission,118 PropertyPermission, PropertyScope, PropertyValue, TokenId, TrySetProperty,119};120use frame_support::BoundedBTreeMap;121use derivative::Derivative;122123pub use pallet::*;124#[cfg(feature = "runtime-benchmarks")]125pub mod benchmarking;126pub mod common;127pub mod erc;128pub mod erc_token;129pub mod weights;130131#[derive(Derivative, Clone)]132pub struct CreateItemData<CrossAccountId> {133 #[derivative(Debug(format_with = "bounded::map_debug"))]134 pub users: BoundedBTreeMap<CrossAccountId, u128, ConstU32<MAX_ITEMS_PER_BATCH>>,135 #[derivative(Debug(format_with = "bounded::vec_debug"))]136 pub properties: CollectionPropertiesVec,137}138pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;139140/// Token data, stored independently from other data used to describe it141/// for the convenience of database access. Notably contains the token metadata.142#[struct_versioning::versioned(version = 2, upper)]143#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]144#[deprecated(since = "0.2.0", note = "ItemData is no more contains usefull data")]145pub struct ItemData {146 pub const_data: BoundedVec<u8, CustomDataLimit>,147148 #[version(..2)]149 pub variable_data: BoundedVec<u8, CustomDataLimit>,150}151152#[frame_support::pallet]153pub mod pallet {154 use super::*;155 use frame_support::{156 Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,157 traits::StorageVersion,158 };159 use frame_system::pallet_prelude::*;160 use up_data_structs::{CollectionId, TokenId};161 use super::weights::WeightInfo;162163 #[pallet::error]164 pub enum Error<T> {165 /// Not Refungible item data used to mint in Refungible collection.166 NotRefungibleDataUsedToMintFungibleCollectionToken,167 /// Maximum refungibility exceeded.168 WrongRefungiblePieces,169 /// Refungible token can't be repartitioned by user who isn't owns all pieces.170 RepartitionWhileNotOwningAllPieces,171 /// Refungible token can't nest other tokens.172 RefungibleDisallowsNesting,173 /// Setting item properties is not allowed.174 SettingPropertiesNotAllowed,175 }176177 #[pallet::config]178 pub trait Config:179 frame_system::Config + pallet_common::Config + pallet_structure::Config180 {181 type WeightInfo: WeightInfo;182 }183184 const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);185186 #[pallet::pallet]187 #[pallet::storage_version(STORAGE_VERSION)]188 #[pallet::generate_store(pub(super) trait Store)]189 pub struct Pallet<T>(_);190191 /// Total amount of minted tokens in a collection.192 #[pallet::storage]193 pub type TokensMinted<T: Config> =194 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;195196 /// Amount of tokens burnt in a collection.197 #[pallet::storage]198 pub type TokensBurnt<T: Config> =199 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;200201 /// Token data, used to partially describe a token.202 // TODO: remove203 #[pallet::storage]204 #[deprecated(since = "0.2.0", note = "ItemData is no more contains usefull data")]205 pub type TokenData<T: Config> = StorageNMap<206 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),207 Value = ItemData,208 QueryKind = ValueQuery,209 >;210211 /// Amount of pieces a refungible token is split into.212 #[pallet::storage]213 #[pallet::getter(fn token_properties)]214 pub type TokenProperties<T: Config> = StorageNMap<215 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),216 Value = up_data_structs::Properties,217 QueryKind = ValueQuery,218 OnEmpty = up_data_structs::TokenProperties,219 >;220221 /// Total amount of pieces for token222 #[pallet::storage]223 pub type TotalSupply<T: Config> = StorageNMap<224 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),225 Value = u128,226 QueryKind = ValueQuery,227 >;228229 /// Used to enumerate tokens owned by account.230 #[pallet::storage]231 pub type Owned<T: Config> = StorageNMap<232 Key = (233 Key<Twox64Concat, CollectionId>,234 Key<Blake2_128Concat, T::CrossAccountId>,235 Key<Twox64Concat, TokenId>,236 ),237 Value = bool,238 QueryKind = ValueQuery,239 >;240241 /// Amount of tokens (not pieces) partially owned by an account within a collection.242 #[pallet::storage]243 pub type AccountBalance<T: Config> = StorageNMap<244 Key = (245 Key<Twox64Concat, CollectionId>,246 // Owner247 Key<Blake2_128Concat, T::CrossAccountId>,248 ),249 Value = u32,250 QueryKind = ValueQuery,251 >;252253 /// Amount of token pieces owned by account.254 #[pallet::storage]255 pub type Balance<T: Config> = StorageNMap<256 Key = (257 Key<Twox64Concat, CollectionId>,258 Key<Twox64Concat, TokenId>,259 // Owner260 Key<Blake2_128Concat, T::CrossAccountId>,261 ),262 Value = u128,263 QueryKind = ValueQuery,264 >;265266 /// Allowance set by a token owner for another user to perform one of certain transactions on a number of pieces of a token.267 #[pallet::storage]268 pub type Allowance<T: Config> = StorageNMap<269 Key = (270 Key<Twox64Concat, CollectionId>,271 Key<Twox64Concat, TokenId>,272 // Owner273 Key<Blake2_128, T::CrossAccountId>,274 // Spender275 Key<Blake2_128Concat, T::CrossAccountId>,276 ),277 Value = u128,278 QueryKind = ValueQuery,279 >;280281 #[pallet::hooks]282 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {283 fn on_runtime_upgrade() -> Weight {284 let storage_version = StorageVersion::get::<Pallet<T>>();285 if storage_version < StorageVersion::new(2) {286 <TokenData<T>>::remove_all(None);287 }288 StorageVersion::new(2).put::<Pallet<T>>();289290 0291 }292 }293}294295pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);296impl<T: Config> RefungibleHandle<T> {297 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {298 Self(inner)299 }300 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {301 self.0302 }303 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {304 &mut self.0305 }306}307308impl<T: Config> Deref for RefungibleHandle<T> {309 type Target = pallet_common::CollectionHandle<T>;310311 fn deref(&self) -> &Self::Target {312 &self.0313 }314}315316impl<T: Config> WithRecorder<T> for RefungibleHandle<T> {317 fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {318 self.0.recorder()319 }320 fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {321 self.0.into_recorder()322 }323}324325impl<T: Config> Pallet<T> {326 /// Get number of RFT tokens in collection327 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {328 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)329 }330331 /// Check that RFT token exists332 ///333 /// - `token`: Token ID.334 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {335 <TotalSupply<T>>::contains_key((collection.id, token))336 }337338 pub fn set_scoped_token_property(339 collection_id: CollectionId,340 token_id: TokenId,341 scope: PropertyScope,342 property: Property,343 ) -> DispatchResult {344 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {345 properties.try_scoped_set(scope, property.key, property.value)346 })347 .map_err(<CommonError<T>>::from)?;348349 Ok(())350 }351352 pub fn set_scoped_token_properties(353 collection_id: CollectionId,354 token_id: TokenId,355 scope: PropertyScope,356 properties: impl Iterator<Item = Property>,357 ) -> DispatchResult {358 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {359 stored_properties.try_scoped_set_from_iter(scope, properties)360 })361 .map_err(<CommonError<T>>::from)?;362363 Ok(())364 }365}366367// unchecked calls skips any permission checks368impl<T: Config> Pallet<T> {369 /// Create RFT collection370 ///371 /// `init_collection` will take non-refundable deposit for collection creation.372 ///373 /// - `data`: Contains settings for collection limits and permissions.374 pub fn init_collection(375 owner: T::CrossAccountId,376 payer: T::CrossAccountId,377 data: CreateCollectionData<T::AccountId>,378 ) -> Result<CollectionId, DispatchError> {379 <PalletCommon<T>>::init_collection(owner, payer, data, CollectionFlags::default())380 }381382 /// Destroy RFT collection383 ///384 /// `destroy_collection` will throw error if collection contains any tokens.385 /// Only owner can destroy collection.386 pub fn destroy_collection(387 collection: RefungibleHandle<T>,388 sender: &T::CrossAccountId,389 ) -> DispatchResult {390 let id = collection.id;391392 if Self::collection_has_tokens(id) {393 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());394 }395396 // =========397398 PalletCommon::destroy_collection(collection.0, sender)?;399400 <TokensMinted<T>>::remove(id);401 <TokensBurnt<T>>::remove(id);402 let _ = <TotalSupply<T>>::clear_prefix((id,), u32::MAX, None);403 let _ = <Balance<T>>::clear_prefix((id,), u32::MAX, None);404 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);405 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);406 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);407 Ok(())408 }409410 fn collection_has_tokens(collection_id: CollectionId) -> bool {411 <TotalSupply<T>>::iter_prefix((collection_id,))412 .next()413 .is_some()414 }415416 pub fn burn_token_unchecked(417 collection: &RefungibleHandle<T>,418 owner: &T::CrossAccountId,419 token_id: TokenId,420 ) -> DispatchResult {421 let burnt = <TokensBurnt<T>>::get(collection.id)422 .checked_add(1)423 .ok_or(ArithmeticError::Overflow)?;424425 <TokensBurnt<T>>::insert(collection.id, burnt);426 <TokenProperties<T>>::remove((collection.id, token_id));427 <TotalSupply<T>>::remove((collection.id, token_id));428 let _ = <Balance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);429 let _ = <Allowance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);430 <PalletEvm<T>>::deposit_log(431 ERC721Events::Transfer {432 from: *owner.as_eth(),433 to: H160::default(),434 token_id: token_id.into(),435 }436 .to_log(collection_id_to_address(collection.id)),437 );438 Ok(())439 }440441 /// Burn RFT token pieces442 ///443 /// `burn` will decrease total amount of token pieces and amount owned by sender.444 /// `burn` can be called even if there are multiple owners of the RFT token.445 /// If sender wouldn't have any pieces left after `burn` than she will stop being446 /// one of the owners of the token. If there is no account that owns any pieces of447 /// the token than token will be burned too.448 ///449 /// - `amount`: Amount of token pieces to burn.450 /// - `token`: Token who's pieces should be burned451 /// - `collection`: Collection that contains the token452 pub fn burn(453 collection: &RefungibleHandle<T>,454 owner: &T::CrossAccountId,455 token: TokenId,456 amount: u128,457 ) -> DispatchResult {458 let total_supply = <TotalSupply<T>>::get((collection.id, token))459 .checked_sub(amount)460 .ok_or(<CommonError<T>>::TokenValueTooLow)?;461462 // This was probally last owner of this token?463 if total_supply == 0 {464 // Ensure user actually owns this amount465 ensure!(466 <Balance<T>>::get((collection.id, token, owner)) == amount,467 <CommonError<T>>::TokenValueTooLow468 );469 let account_balance = <AccountBalance<T>>::get((collection.id, owner))470 .checked_sub(1)471 // Should not occur472 .ok_or(ArithmeticError::Underflow)?;473474 // =========475476 <Owned<T>>::remove((collection.id, owner, token));477 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);478 <AccountBalance<T>>::insert((collection.id, owner), account_balance);479 Self::burn_token_unchecked(collection, owner, token)?;480 <PalletEvm<T>>::deposit_log(481 ERC20Events::Transfer {482 from: *owner.as_eth(),483 to: H160::default(),484 value: amount.into(),485 }486 .to_log(collection_id_to_address(collection.id)),487 );488 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(489 collection.id,490 token,491 owner.clone(),492 amount,493 ));494 return Ok(());495 }496497 let balance = <Balance<T>>::get((collection.id, token, owner))498 .checked_sub(amount)499 .ok_or(<CommonError<T>>::TokenValueTooLow)?;500 let account_balance = if balance == 0 {501 <AccountBalance<T>>::get((collection.id, owner))502 .checked_sub(1)503 // Should not occur504 .ok_or(ArithmeticError::Underflow)?505 } else {506 0507 };508509 // =========510511 if balance == 0 {512 <Owned<T>>::remove((collection.id, owner, token));513 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);514 <Balance<T>>::remove((collection.id, token, owner));515 <AccountBalance<T>>::insert((collection.id, owner), account_balance);516517 if let Some(user) = Self::token_owner(collection.id, token) {518 <PalletEvm<T>>::deposit_log(519 ERC721Events::Transfer {520 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,521 to: *user.as_eth(),522 token_id: token.into(),523 }524 .to_log(collection_id_to_address(collection.id)),525 );526 }527 } else {528 <Balance<T>>::insert((collection.id, token, owner), balance);529 }530 <TotalSupply<T>>::insert((collection.id, token), total_supply);531532 <PalletEvm<T>>::deposit_log(533 ERC20Events::Transfer {534 from: *owner.as_eth(),535 to: H160::default(),536 value: amount.into(),537 }538 .to_log(T::EvmTokenAddressMapping::token_to_address(539 collection.id,540 token,541 )),542 );543 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(544 collection.id,545 token,546 owner.clone(),547 amount,548 ));549 Ok(())550 }551552 #[transactional]553 fn modify_token_properties(554 collection: &RefungibleHandle<T>,555 sender: &T::CrossAccountId,556 token_id: TokenId,557 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,558 is_token_create: bool,559 nesting_budget: &dyn Budget,560 ) -> DispatchResult {561 let is_collection_admin = || collection.is_owner_or_admin(sender);562 let is_token_owner = || -> Result<bool, DispatchError> {563 let balance = collection.balance(sender.clone(), token_id);564 let total_pieces: u128 =565 Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);566 if balance != total_pieces {567 return Ok(false);568 }569570 let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(571 sender.clone(),572 collection.id,573 token_id,574 None,575 nesting_budget,576 )?;577578 Ok(is_bundle_owner)579 };580581 for (key, value) in properties {582 let permission = <PalletCommon<T>>::property_permissions(collection.id)583 .get(&key)584 .cloned()585 .unwrap_or_else(PropertyPermission::none);586587 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))588 .get(&key)589 .is_some();590591 match permission {592 PropertyPermission { mutable: false, .. } if is_property_exists => {593 return Err(<CommonError<T>>::NoPermission.into());594 }595596 PropertyPermission {597 collection_admin,598 token_owner,599 ..600 } => {601 //TODO: investigate threats during public minting.602 let is_token_create =603 is_token_create && (collection_admin || token_owner) && value.is_some();604 if !(is_token_create605 || (collection_admin && is_collection_admin())606 || (token_owner && is_token_owner()?))607 {608 fail!(<CommonError<T>>::NoPermission);609 }610 }611 }612613 match value {614 Some(value) => {615 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {616 properties.try_set(key.clone(), value)617 })618 .map_err(<CommonError<T>>::from)?;619620 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(621 collection.id,622 token_id,623 key,624 ));625 }626 None => {627 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {628 properties.remove(&key)629 })630 .map_err(<CommonError<T>>::from)?;631632 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(633 collection.id,634 token_id,635 key,636 ));637 }638 }639 }640641 Ok(())642 }643644 pub fn set_token_properties(645 collection: &RefungibleHandle<T>,646 sender: &T::CrossAccountId,647 token_id: TokenId,648 properties: impl Iterator<Item = Property>,649 is_token_create: bool,650 nesting_budget: &dyn Budget,651 ) -> DispatchResult {652 Self::modify_token_properties(653 collection,654 sender,655 token_id,656 properties.map(|p| (p.key, Some(p.value))),657 is_token_create,658 nesting_budget,659 )660 }661662 pub fn set_token_property(663 collection: &RefungibleHandle<T>,664 sender: &T::CrossAccountId,665 token_id: TokenId,666 property: Property,667 nesting_budget: &dyn Budget,668 ) -> DispatchResult {669 let is_token_create = false;670671 Self::set_token_properties(672 collection,673 sender,674 token_id,675 [property].into_iter(),676 is_token_create,677 nesting_budget,678 )679 }680681 pub fn delete_token_properties(682 collection: &RefungibleHandle<T>,683 sender: &T::CrossAccountId,684 token_id: TokenId,685 property_keys: impl Iterator<Item = PropertyKey>,686 nesting_budget: &dyn Budget,687 ) -> DispatchResult {688 let is_token_create = false;689690 Self::modify_token_properties(691 collection,692 sender,693 token_id,694 property_keys.into_iter().map(|key| (key, None)),695 is_token_create,696 nesting_budget,697 )698 }699700 pub fn delete_token_property(701 collection: &RefungibleHandle<T>,702 sender: &T::CrossAccountId,703 token_id: TokenId,704 property_key: PropertyKey,705 nesting_budget: &dyn Budget,706 ) -> DispatchResult {707 Self::delete_token_properties(708 collection,709 sender,710 token_id,711 [property_key].into_iter(),712 nesting_budget,713 )714 }715716 /// Transfer RFT token pieces from one account to another.717 ///718 /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.719 ///720 /// - `from`: Owner of token pieces to transfer.721 /// - `to`: Recepient of transfered token pieces.722 /// - `amount`: Amount of token pieces to transfer.723 /// - `token`: Token whos pieces should be transfered724 /// - `collection`: Collection that contains the token725 pub fn transfer(726 collection: &RefungibleHandle<T>,727 from: &T::CrossAccountId,728 to: &T::CrossAccountId,729 token: TokenId,730 amount: u128,731 nesting_budget: &dyn Budget,732 ) -> DispatchResult {733 ensure!(734 collection.limits.transfers_enabled(),735 <CommonError<T>>::TransferNotAllowed736 );737738 if collection.permissions.access() == AccessMode::AllowList {739 collection.check_allowlist(from)?;740 collection.check_allowlist(to)?;741 }742 <PalletCommon<T>>::ensure_correct_receiver(to)?;743744 let initial_balance_from = <Balance<T>>::get((collection.id, token, from));745 let updated_balance_from = initial_balance_from746 .checked_sub(amount)747 .ok_or(<CommonError<T>>::TokenValueTooLow)?;748 let mut create_target = false;749 let from_to_differ = from != to;750 let updated_balance_to = if from != to {751 let old_balance = <Balance<T>>::get((collection.id, token, to));752 if old_balance == 0 {753 create_target = true;754 }755 Some(756 old_balance757 .checked_add(amount)758 .ok_or(ArithmeticError::Overflow)?,759 )760 } else {761 None762 };763764 let account_balance_from = if updated_balance_from == 0 {765 Some(766 <AccountBalance<T>>::get((collection.id, from))767 .checked_sub(1)768 // Should not occur769 .ok_or(ArithmeticError::Underflow)?,770 )771 } else {772 None773 };774 // Account data is created in token, AccountBalance should be increased775 // But only if from != to as we shouldn't check overflow in this case776 let account_balance_to = if create_target && from_to_differ {777 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))778 .checked_add(1)779 .ok_or(ArithmeticError::Overflow)?;780 ensure!(781 account_balance_to < collection.limits.account_token_ownership_limit(),782 <CommonError<T>>::AccountTokenLimitExceeded,783 );784785 Some(account_balance_to)786 } else {787 None788 };789790 // =========791792 <PalletStructure<T>>::nest_if_sent_to_token(793 from.clone(),794 to,795 collection.id,796 token,797 nesting_budget,798 )?;799800 if let Some(updated_balance_to) = updated_balance_to {801 // from != to802 if updated_balance_from == 0 {803 <Balance<T>>::remove((collection.id, token, from));804 <PalletStructure<T>>::unnest_if_nested(from, collection.id, token);805 } else {806 <Balance<T>>::insert((collection.id, token, from), updated_balance_from);807 }808 <Balance<T>>::insert((collection.id, token, to), updated_balance_to);809 if let Some(account_balance_from) = account_balance_from {810 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);811 <Owned<T>>::remove((collection.id, from, token));812 }813 if let Some(account_balance_to) = account_balance_to {814 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);815 <Owned<T>>::insert((collection.id, to, token), true);816 }817 }818819 <PalletEvm<T>>::deposit_log(820 ERC20Events::Transfer {821 from: *from.as_eth(),822 to: *to.as_eth(),823 value: amount.into(),824 }825 .to_log(T::EvmTokenAddressMapping::token_to_address(826 collection.id,827 token,828 )),829 );830831 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(832 collection.id,833 token,834 from.clone(),835 to.clone(),836 amount,837 ));838839 let total_supply = <TotalSupply<T>>::get((collection.id, token));840841 if amount == total_supply {842 // if token was fully owned by `from` and will be fully owned by `to` after transfer843 <PalletEvm<T>>::deposit_log(844 ERC721Events::Transfer {845 from: *from.as_eth(),846 to: *to.as_eth(),847 token_id: token.into(),848 }849 .to_log(collection_id_to_address(collection.id)),850 );851 } else if let Some(updated_balance_to) = updated_balance_to {852 // if `from` not equals `to`. This condition is needed to avoid sending event853 // when `from` fully owns token and sends part of token pieces to itself.854 if initial_balance_from == total_supply {855 // if token was fully owned by `from` and will be only partially owned by `to`856 // and `from` after transfer857 <PalletEvm<T>>::deposit_log(858 ERC721Events::Transfer {859 from: *from.as_eth(),860 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,861 token_id: token.into(),862 }863 .to_log(collection_id_to_address(collection.id)),864 );865 } else if updated_balance_to == total_supply {866 // if token was partially owned by `from` and will be fully owned by `to` after transfer867 <PalletEvm<T>>::deposit_log(868 ERC721Events::Transfer {869 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,870 to: *to.as_eth(),871 token_id: token.into(),872 }873 .to_log(collection_id_to_address(collection.id)),874 );875 }876 }877878 Ok(())879 }880881 /// Batched operation to create multiple RFT tokens.882 ///883 /// Same as `create_item` but creates multiple tokens.884 ///885 /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.886 pub fn create_multiple_items(887 collection: &RefungibleHandle<T>,888 sender: &T::CrossAccountId,889 data: Vec<CreateItemData<T::CrossAccountId>>,890 nesting_budget: &dyn Budget,891 ) -> DispatchResult {892 if !collection.is_owner_or_admin(sender) {893 ensure!(894 collection.permissions.mint_mode(),895 <CommonError<T>>::PublicMintingNotAllowed896 );897 collection.check_allowlist(sender)?;898899 for item in data.iter() {900 for user in item.users.keys() {901 collection.check_allowlist(user)?;902 }903 }904 }905906 for item in data.iter() {907 for (owner, _) in item.users.iter() {908 <PalletCommon<T>>::ensure_correct_receiver(owner)?;909 }910 }911912 // Total pieces per tokens913 let totals = data914 .iter()915 .map(|data| {916 Ok(data917 .users918 .iter()919 .map(|u| u.1)920 .try_fold(0u128, |acc, v| acc.checked_add(*v))921 .ok_or(ArithmeticError::Overflow)?)922 })923 .collect::<Result<Vec<_>, DispatchError>>()?;924 for total in &totals {925 ensure!(926 *total <= MAX_REFUNGIBLE_PIECES,927 <Error<T>>::WrongRefungiblePieces928 );929 }930931 let first_token_id = <TokensMinted<T>>::get(collection.id);932 let tokens_minted = first_token_id933 .checked_add(data.len() as u32)934 .ok_or(ArithmeticError::Overflow)?;935 ensure!(936 tokens_minted < collection.limits.token_limit(),937 <CommonError<T>>::CollectionTokenLimitExceeded938 );939940 let mut balances = BTreeMap::new();941 for data in &data {942 for owner in data.users.keys() {943 let balance = balances944 .entry(owner)945 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));946 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;947948 ensure!(949 *balance <= collection.limits.account_token_ownership_limit(),950 <CommonError<T>>::AccountTokenLimitExceeded,951 );952 }953 }954955 for (i, token) in data.iter().enumerate() {956 let token_id = TokenId(first_token_id + i as u32 + 1);957 for (to, _) in token.users.iter() {958 <PalletStructure<T>>::check_nesting(959 sender.clone(),960 to,961 collection.id,962 token_id,963 nesting_budget,964 )?;965 }966 }967968 // =========969970 with_transaction(|| {971 for (i, data) in data.iter().enumerate() {972 let token_id = first_token_id + i as u32 + 1;973 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);974975 for (user, amount) in data.users.iter() {976 if *amount == 0 {977 continue;978 }979 <Balance<T>>::insert((collection.id, token_id, &user), amount);980 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);981 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(982 user,983 collection.id,984 TokenId(token_id),985 );986 }987988 if let Err(e) = Self::set_token_properties(989 collection,990 sender,991 TokenId(token_id),992 data.properties.clone().into_iter(),993 true,994 nesting_budget,995 ) {996 return TransactionOutcome::Rollback(Err(e));997 }998 }999 TransactionOutcome::Commit(Ok(()))1000 })?;10011002 <TokensMinted<T>>::insert(collection.id, tokens_minted);10031004 for (account, balance) in balances {1005 <AccountBalance<T>>::insert((collection.id, account), balance);1006 }10071008 for (i, token) in data.into_iter().enumerate() {1009 let token_id = first_token_id + i as u32 + 1;10101011 let receivers = token1012 .users1013 .into_iter()1014 .filter(|(_, amount)| *amount > 0)1015 .collect::<Vec<_>>();10161017 if let [(user, _)] = receivers.as_slice() {1018 // if there is exactly one receiver1019 <PalletEvm<T>>::deposit_log(1020 ERC721Events::Transfer {1021 from: H160::default(),1022 to: *user.as_eth(),1023 token_id: token_id.into(),1024 }1025 .to_log(collection_id_to_address(collection.id)),1026 );1027 } else if let [_, ..] = receivers.as_slice() {1028 // if there is more than one receiver1029 <PalletEvm<T>>::deposit_log(1030 ERC721Events::Transfer {1031 from: H160::default(),1032 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,1033 token_id: token_id.into(),1034 }1035 .to_log(collection_id_to_address(collection.id)),1036 );1037 }10381039 for (user, amount) in receivers.into_iter() {1040 <PalletEvm<T>>::deposit_log(1041 ERC20Events::Transfer {1042 from: H160::default(),1043 to: *user.as_eth(),1044 value: amount.into(),1045 }1046 .to_log(T::EvmTokenAddressMapping::token_to_address(1047 collection.id,1048 TokenId(token_id),1049 )),1050 );1051 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1052 collection.id,1053 TokenId(token_id),1054 user,1055 amount,1056 ));1057 }1058 }1059 Ok(())1060 }10611062 pub fn set_allowance_unchecked(1063 collection: &RefungibleHandle<T>,1064 sender: &T::CrossAccountId,1065 spender: &T::CrossAccountId,1066 token: TokenId,1067 amount: u128,1068 ) {1069 if amount == 0 {1070 <Allowance<T>>::remove((collection.id, token, sender, spender));1071 } else {1072 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);1073 }10741075 <PalletEvm<T>>::deposit_log(1076 ERC20Events::Approval {1077 owner: *sender.as_eth(),1078 spender: *spender.as_eth(),1079 value: amount.into(),1080 }1081 .to_log(T::EvmTokenAddressMapping::token_to_address(1082 collection.id,1083 token,1084 )),1085 );1086 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1087 collection.id,1088 token,1089 sender.clone(),1090 spender.clone(),1091 amount,1092 ))1093 }10941095 /// Set allowance for the spender to `transfer` or `burn` sender's token pieces.1096 ///1097 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.1098 pub fn set_allowance(1099 collection: &RefungibleHandle<T>,1100 sender: &T::CrossAccountId,1101 spender: &T::CrossAccountId,1102 token: TokenId,1103 amount: u128,1104 ) -> DispatchResult {1105 if collection.permissions.access() == AccessMode::AllowList {1106 collection.check_allowlist(sender)?;1107 collection.check_allowlist(spender)?;1108 }11091110 <PalletCommon<T>>::ensure_correct_receiver(spender)?;11111112 if <Balance<T>>::get((collection.id, token, sender)) < amount {1113 ensure!(1114 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),1115 <CommonError<T>>::CantApproveMoreThanOwned1116 );1117 }11181119 // =========11201121 Self::set_allowance_unchecked(collection, sender, spender, token, amount);1122 Ok(())1123 }11241125 /// Returns allowance, which should be set after transaction1126 fn check_allowed(1127 collection: &RefungibleHandle<T>,1128 spender: &T::CrossAccountId,1129 from: &T::CrossAccountId,1130 token: TokenId,1131 amount: u128,1132 nesting_budget: &dyn Budget,1133 ) -> Result<Option<u128>, DispatchError> {1134 if spender.conv_eq(from) {1135 return Ok(None);1136 }1137 if collection.permissions.access() == AccessMode::AllowList {1138 // `from`, `to` checked in [`transfer`]1139 collection.check_allowlist(spender)?;1140 }1141 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1142 // TODO: should collection owner be allowed to perform this transfer?1143 ensure!(1144 <PalletStructure<T>>::check_indirectly_owned(1145 spender.clone(),1146 source.0,1147 source.1,1148 None,1149 nesting_budget1150 )?,1151 <CommonError<T>>::ApprovedValueTooLow,1152 );1153 return Ok(None);1154 }1155 let allowance =1156 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);1157 if allowance.is_none() {1158 ensure!(1159 collection.ignores_allowance(spender),1160 <CommonError<T>>::ApprovedValueTooLow1161 );1162 }1163 Ok(allowance)1164 }11651166 /// Transfer RFT token pieces from one account to another.1167 ///1168 /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1169 /// The owner should set allowance for the spender to transfer pieces.1170 ///1171 /// [`transfer`]: struct.Pallet.html#method.transfer1172 pub fn transfer_from(1173 collection: &RefungibleHandle<T>,1174 spender: &T::CrossAccountId,1175 from: &T::CrossAccountId,1176 to: &T::CrossAccountId,1177 token: TokenId,1178 amount: u128,1179 nesting_budget: &dyn Budget,1180 ) -> DispatchResult {1181 let allowance =1182 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;11831184 // =========11851186 Self::transfer(collection, from, to, token, amount, nesting_budget)?;1187 if let Some(allowance) = allowance {1188 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1189 }1190 Ok(())1191 }11921193 /// Burn RFT token pieces from the account.1194 ///1195 /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1196 /// set allowance for the spender to burn pieces1197 ///1198 /// [`burn`]: struct.Pallet.html#method.burn1199 pub fn burn_from(1200 collection: &RefungibleHandle<T>,1201 spender: &T::CrossAccountId,1202 from: &T::CrossAccountId,1203 token: TokenId,1204 amount: u128,1205 nesting_budget: &dyn Budget,1206 ) -> DispatchResult {1207 let allowance =1208 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;12091210 // =========12111212 Self::burn(collection, from, token, amount)?;1213 if let Some(allowance) = allowance {1214 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1215 }1216 Ok(())1217 }12181219 /// Create RFT token.1220 ///1221 /// The sender should be the owner/admin of the collection or collection should be configured1222 /// to allow public minting.1223 ///1224 /// - `data`: Contains list of users who will become the owners of the token pieces and amount1225 /// of token pieces they will receive.1226 pub fn create_item(1227 collection: &RefungibleHandle<T>,1228 sender: &T::CrossAccountId,1229 data: CreateItemData<T::CrossAccountId>,1230 nesting_budget: &dyn Budget,1231 ) -> DispatchResult {1232 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1233 }12341235 /// Repartition RFT token.1236 ///1237 /// `repartition` will set token balance of the sender and total amount of token pieces.1238 /// Sender should own all of the token pieces. `repartition' could be done even if some1239 /// token pieces were burned before.1240 ///1241 /// - `amount`: Total amount of token pieces that the token will have after `repartition`.1242 pub fn repartition(1243 collection: &RefungibleHandle<T>,1244 owner: &T::CrossAccountId,1245 token: TokenId,1246 amount: u128,1247 ) -> DispatchResult {1248 ensure!(1249 amount <= MAX_REFUNGIBLE_PIECES,1250 <Error<T>>::WrongRefungiblePieces1251 );1252 ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1253 // Ensure user owns all pieces1254 let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1255 let balance = <Balance<T>>::get((collection.id, token, owner));1256 ensure!(1257 total_pieces == balance,1258 <Error<T>>::RepartitionWhileNotOwningAllPieces1259 );12601261 <Balance<T>>::insert((collection.id, token, owner), amount);1262 <TotalSupply<T>>::insert((collection.id, token), amount);12631264 if amount > total_pieces {1265 let mint_amount = amount - total_pieces;1266 <PalletEvm<T>>::deposit_log(1267 ERC20Events::Transfer {1268 from: H160::default(),1269 to: *owner.as_eth(),1270 value: mint_amount.into(),1271 }1272 .to_log(T::EvmTokenAddressMapping::token_to_address(1273 collection.id,1274 token,1275 )),1276 );1277 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1278 collection.id,1279 token,1280 owner.clone(),1281 mint_amount,1282 ));1283 } else if total_pieces > amount {1284 let burn_amount = total_pieces - amount;1285 <PalletEvm<T>>::deposit_log(1286 ERC20Events::Transfer {1287 from: *owner.as_eth(),1288 to: H160::default(),1289 value: burn_amount.into(),1290 }1291 .to_log(T::EvmTokenAddressMapping::token_to_address(1292 collection.id,1293 token,1294 )),1295 );1296 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(1297 collection.id,1298 token,1299 owner.clone(),1300 burn_amount,1301 ));1302 }13031304 Ok(())1305 }13061307 fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {1308 let mut owner = None;1309 let mut count = 0;1310 for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1311 count += 1;1312 if count > 1 {1313 return None;1314 }1315 owner = Some(key);1316 }1317 owner1318 }13191320 fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1321 <TotalSupply<T>>::try_get((collection_id, token_id)).ok()1322 }13231324 pub fn set_collection_properties(1325 collection: &RefungibleHandle<T>,1326 sender: &T::CrossAccountId,1327 properties: Vec<Property>,1328 ) -> DispatchResult {1329 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)1330 }13311332 pub fn delete_collection_properties(1333 collection: &RefungibleHandle<T>,1334 sender: &T::CrossAccountId,1335 property_keys: Vec<PropertyKey>,1336 ) -> DispatchResult {1337 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1338 }13391340 pub fn set_token_property_permissions(1341 collection: &RefungibleHandle<T>,1342 sender: &T::CrossAccountId,1343 property_permissions: Vec<PropertyKeyPermission>,1344 ) -> DispatchResult {1345 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1346 }13471348 pub fn set_scoped_token_property_permissions(1349 collection: &RefungibleHandle<T>,1350 sender: &T::CrossAccountId,1351 scope: PropertyScope,1352 property_permissions: Vec<PropertyKeyPermission>,1353 ) -> DispatchResult {1354 <PalletCommon<T>>::set_scoped_token_property_permissions(1355 collection,1356 sender,1357 scope,1358 property_permissions,1359 )1360 }13611362 /// Returns 10 token in no particular order.1363 ///1364 /// There is no direct way to get token holders in ascending order,1365 /// since `iter_prefix` returns values in no particular order.1366 /// Therefore, getting the 10 largest holders with a large value of holders1367 /// can lead to impact memory allocation + sorting with `n * log (n)`.1368 pub fn token_owners(1369 collection_id: CollectionId,1370 token: TokenId,1371 ) -> Option<Vec<T::CrossAccountId>> {1372 let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1373 .map(|(owner, _amount)| owner)1374 .take(10)1375 .collect();13761377 if res.is_empty() {1378 None1379 } else {1380 Some(res)1381 }1382 }1383}pallets/unique/src/eth/mod.rsdiffbeforeafterboth--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -28,7 +28,7 @@
static_property::{key, value as property_value},
},
};
-use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
+use pallet_evm_coder_substrate::{dispatch_to_evm, SubstrateRecorder, WithRecorder};
use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult};
use up_data_structs::{
CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData,
@@ -156,6 +156,7 @@
T: Config + pallet_nonfungible::Config + pallet_refungible::Config,
>(
caller: caller,
+ value: value,
name: string,
description: string,
token_prefix: string,
@@ -172,8 +173,16 @@
base_uri_value,
add_properties,
)?;
+ let value = value.as_u128();
+ let creation_price: Result<u128> = T::CollectionCreationPrice::get()
+ .try_into()
+ .map_err(|_| "collection creation price should be convertible to u128".into());
+ if value != creation_price? {
+ return Err("Sent amount not equals to collection creation price".into());
+ }
+ let collection_helpers_address = T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());
- let collection_id = T::CollectionDispatch::create(caller.clone(), data)
+ let collection_id = T::CollectionDispatch::create(caller.clone(), collection_helpers_address, data)
.map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
let address = pallet_common::eth::collection_id_to_address(collection_id);
Ok(address)
@@ -183,7 +192,7 @@
#[solidity_interface(name = CollectionHelpers, events(CollectionHelpersEvents))]
impl<T> EvmCollectionHelpers<T>
where
- T: Config + pallet_nonfungible::Config + pallet_refungible::Config,
+ T: Config + pallet_common::Config + pallet_nonfungible::Config + pallet_refungible::Config,
{
/// Create an NFT collection
/// @param name Name of the collection
@@ -209,8 +218,17 @@
Default::default(),
false,
)?;
- let collection_id = T::CollectionDispatch::create(caller, data)
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
+ let value = value.as_u128();
+ let creation_price: Result<u128> = T::CollectionCreationPrice::get()
+ .try_into()
+ .map_err(|_| "collection creation price should be convertible to u128".into());
+ let creation_price = creation_price?;
+ if value != creation_price {
+ return Err(format!("Sent amount not equals to collection creation price ({0})", creation_price).into());
+ }
+ let collection_helpers_address = T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());
+ let collection_id =
+ T::CollectionDispatch::create(caller, collection_helpers_address, data).map_err(dispatch_to_evm::<T>)?;
let address = pallet_common::eth::collection_id_to_address(collection_id);
Ok(address)
@@ -221,6 +239,7 @@
fn create_nonfungible_collection_with_properties(
&mut self,
caller: caller,
+ value: value,
name: string,
description: string,
token_prefix: string,
@@ -236,7 +255,15 @@
base_uri_value,
true,
)?;
- let collection_id = T::CollectionDispatch::create(caller, data)
+ let value = value.as_u128();
+ let creation_price: Result<u128> = T::CollectionCreationPrice::get()
+ .try_into()
+ .map_err(|_| "collection creation price should be convertible to u128".into());
+ if value != creation_price? {
+ return Err("Sent amount not equals to collection creation price".into());
+ }
+ let collection_helpers_address = T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());
+ let collection_id = T::CollectionDispatch::create(caller, collection_helpers_address, data)
.map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
let address = pallet_common::eth::collection_id_to_address(collection_id);
@@ -248,12 +275,14 @@
fn create_refungible_collection(
&mut self,
caller: caller,
+ value: value,
name: string,
description: string,
token_prefix: string,
) -> Result<address> {
create_refungible_collection_internal::<T>(
caller,
+ value,
name,
description,
token_prefix,
@@ -267,6 +296,7 @@
fn create_refungible_collection_with_properties(
&mut self,
caller: caller,
+ value: value,
name: string,
description: string,
token_prefix: string,
@@ -274,6 +304,7 @@
) -> Result<address> {
create_refungible_collection_internal::<T>(
caller,
+ value,
name,
description,
token_prefix,
pallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -36,7 +36,7 @@
string memory name,
string memory description,
string memory tokenPrefix
- ) public returns (address) {
+ ) public payable returns (address) {
require(false, stub_error);
name;
description;
@@ -52,7 +52,7 @@
string memory description,
string memory tokenPrefix,
string memory baseUri
- ) public returns (address) {
+ ) public payable returns (address) {
require(false, stub_error);
name;
description;
@@ -68,7 +68,7 @@
string memory name,
string memory description,
string memory tokenPrefix
- ) public returns (address) {
+ ) public payable returns (address) {
require(false, stub_error);
name;
description;
@@ -84,7 +84,7 @@
string memory description,
string memory tokenPrefix,
string memory baseUri
- ) public returns (address) {
+ ) public payable returns (address) {
require(false, stub_error);
name;
description;
pallets/unique/src/lib.rsdiffbeforeafterboth--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -344,8 +344,8 @@
let sender = ensure_signed(origin)?;
// =========
-
- let _id = T::CollectionDispatch::create(T::CrossAccountId::from_sub(sender), data)?;
+ let sender = T::CrossAccountId::from_sub(sender);
+ let _id = T::CollectionDispatch::create(sender.clone(), sender, data)?;
Ok(())
}
runtime/common/dispatch.rsdiffbeforeafterboth--- a/runtime/common/dispatch.rs
+++ b/runtime/common/dispatch.rs
@@ -55,21 +55,22 @@
{
fn create(
sender: T::CrossAccountId,
+ payer: T::CrossAccountId,
data: CreateCollectionData<T::AccountId>,
) -> Result<CollectionId, DispatchError> {
let id = match data.mode {
- CollectionMode::NFT => <PalletNonfungible<T>>::init_collection(sender, data, false)?,
+ CollectionMode::NFT => <PalletNonfungible<T>>::init_collection(sender, payer, data, false)?,
CollectionMode::Fungible(decimal_points) => {
// check params
ensure!(
decimal_points <= MAX_DECIMAL_POINTS,
pallet_unique::Error::<T>::CollectionDecimalPointLimitExceeded
);
- <PalletFungible<T>>::init_collection(sender, data)?
+ <PalletFungible<T>>::init_collection(sender, payer, data)?
}
#[cfg(feature = "refungible")]
- CollectionMode::ReFungible => <PalletRefungible<T>>::init_collection(sender, data)?,
+ CollectionMode::ReFungible => <PalletRefungible<T>>::init_collection(sender, payer, data)?,
#[cfg(not(feature = "refungible"))]
CollectionMode::ReFungible => return unsupported!(T),
tests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -31,7 +31,7 @@
string memory name,
string memory description,
string memory tokenPrefix
- ) external returns (address);
+ ) external payable returns (address);
/// @dev EVM selector for this function is: 0xa634a5f9,
/// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string)
@@ -40,7 +40,7 @@
string memory description,
string memory tokenPrefix,
string memory baseUri
- ) external returns (address);
+ ) external payable returns (address);
/// @dev EVM selector for this function is: 0xab173450,
/// or in textual repr: createRFTCollection(string,string,string)
@@ -48,7 +48,7 @@
string memory name,
string memory description,
string memory tokenPrefix
- ) external returns (address);
+ ) external payable returns (address);
/// @dev EVM selector for this function is: 0xa5596388,
/// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string)
@@ -57,7 +57,7 @@
string memory description,
string memory tokenPrefix,
string memory baseUri
- ) external returns (address);
+ ) external payable returns (address);
/// Check if a collection exists
/// @param collectionAddress Address of the collection in question
tests/src/eth/collectionHelpersAbi.jsondiffbeforeafterboth--- a/tests/src/eth/collectionHelpersAbi.json
+++ b/tests/src/eth/collectionHelpersAbi.json
@@ -27,7 +27,7 @@
],
"name": "createERC721MetadataCompatibleCollection",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
- "stateMutability": "nonpayable",
+ "stateMutability": "payable",
"type": "function"
},
{
@@ -39,7 +39,7 @@
],
"name": "createERC721MetadataCompatibleRFTCollection",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
- "stateMutability": "nonpayable",
+ "stateMutability": "payable",
"type": "function"
},
{
@@ -50,7 +50,7 @@
],
"name": "createNonfungibleCollection",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
- "stateMutability": "nonpayable",
+ "stateMutability": "payable",
"type": "function"
},
{
@@ -61,7 +61,7 @@
],
"name": "createRFTCollection",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
- "stateMutability": "nonpayable",
+ "stateMutability": "payable",
"type": "function"
},
{
tests/src/eth/fractionalizer/Fractionalizer.soldiffbeforeafterboth--- a/tests/src/eth/fractionalizer/Fractionalizer.sol
+++ b/tests/src/eth/fractionalizer/Fractionalizer.sol
@@ -18,7 +18,8 @@
mapping(address => bool) nftCollectionAllowList;
mapping(address => mapping(uint256 => uint256)) public nft2rftMapping;
mapping(address => Token) public rft2nftMapping;
- bytes32 refungibleCollectionType = keccak256(bytes("ReFungible"));
+ //use constant to reduce gas cost
+ bytes32 constant refungibleCollectionType = keccak256(bytes("ReFungible"));
receive() external payable onlyOwner {}
@@ -51,11 +52,12 @@
/// Throws if `msg.sender` is not owner or admin of provided RFT collection.
/// Can only be called by contract owner.
/// @param _collection address of RFT collection.
- function setRFTCollection(address _collection) public onlyOwner {
+ function setRFTCollection(address _collection) external onlyOwner {
require(rftCollection == address(0), "RFT collection is already set");
UniqueRefungible refungibleContract = UniqueRefungible(_collection);
string memory collectionType = refungibleContract.uniqueCollectionType();
+ // compare hashed to reduce gas cost
require(
keccak256(bytes(collectionType)) == refungibleCollectionType,
"Wrong collection type. Collection is not refungible."
@@ -79,7 +81,7 @@
string calldata _name,
string calldata _description,
string calldata _tokenPrefix
- ) public onlyOwner {
+ ) external onlyOwner {
require(rftCollection == address(0), "RFT collection is already set");
address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F;
rftCollection = CollectionHelpers(collectionHelpers).createRFTCollection(_name, _description, _tokenPrefix);
@@ -90,7 +92,7 @@
/// @dev Can only be called by contract owner.
/// @param collection NFT token address.
/// @param status `true` to allow and `false` to disallow NFT token.
- function setNftCollectionIsAllowed(address collection, bool status) public onlyOwner {
+ function setNftCollectionIsAllowed(address collection, bool status) external onlyOwner {
nftCollectionAllowList[collection] = status;
emit AllowListSet(collection, status);
}
@@ -109,7 +111,7 @@
address _collection,
uint256 _token,
uint128 _pieces
- ) public {
+ ) external {
require(rftCollection != address(0), "RFT collection is not set");
UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection);
require(
@@ -148,7 +150,7 @@
/// Throws if `msg.sender` isn't owner of all RFT token pieces.
/// @param _collection RFT collection address
/// @param _token id of RFT token
- function rft2nft(address _collection, uint256 _token) public {
+ function rft2nft(address _collection, uint256 _token) external {
require(rftCollection != address(0), "RFT collection is not set");
require(rftCollection == _collection, "Wrong RFT collection");
UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection);
tests/src/eth/payable.test.tsdiffbeforeafterboth--- a/tests/src/eth/payable.test.ts
+++ b/tests/src/eth/payable.test.ts
@@ -140,16 +140,13 @@
});
itEth('Fee for nested calls to native methods is withdrawn from the user', async({helper}) => {
- const CONTRACT_BALANCE = 3n * helper.balance.getOneTokenNominal();
+ const CONTRACT_BALANCE = 2n * helper.balance.getOneTokenNominal();
const deployer = await helper.eth.createAccountWithBalance(donor);
const caller = await helper.eth.createAccountWithBalance(donor);
const contract = await deployProxyContract(helper, deployer);
-
- const web3 = helper.getWeb3();
- await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: CONTRACT_BALANCE.toString(), gas: helper.eth.DEFAULT_GAS});
- const collectionAddress = (await contract.methods.createNonfungibleCollection().send({from: caller})).events.CollectionCreated.returnValues.collection;
+ const collectionAddress = (await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)})).events.CollectionCreated.returnValues.collection;
const initialCallerBalance = await helper.balance.getEthereum(caller);
const initialContractBalance = await helper.balance.getEthereum(contract.options.address);
await contract.methods.mintNftToken(collectionAddress).send({from: caller});
@@ -160,22 +157,18 @@
});
itEth('Fee for nested calls to create*Collection methods is withdrawn from the user and from the contract', async({helper}) => {
- const CONTRACT_BALANCE = 3n * helper.balance.getOneTokenNominal();
-
+ const CONTRACT_BALANCE = 2n * helper.balance.getOneTokenNominal();
const deployer = await helper.eth.createAccountWithBalance(donor);
const caller = await helper.eth.createAccountWithBalance(donor);
const contract = await deployProxyContract(helper, deployer);
-
- const web3 = helper.getWeb3();
- await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: CONTRACT_BALANCE.toString(), gas: helper.eth.DEFAULT_GAS});
const initialCallerBalance = await helper.balance.getEthereum(caller);
const initialContractBalance = await helper.balance.getEthereum(contract.options.address);
- await contract.methods.createNonfungibleCollection().send({from: caller});
+ await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)});
const finalCallerBalance = await helper.balance.getEthereum(caller);
const finalContractBalance = await helper.balance.getEthereum(contract.options.address);
expect(finalCallerBalance < initialCallerBalance).to.be.true;
- expect(finalContractBalance < initialContractBalance).to.be.true;
+ expect(finalContractBalance == initialContractBalance).to.be.true;
});
async function deployProxyContract(helper: EthUniqueHelper, deployer: string) {
@@ -189,6 +182,8 @@
import {CollectionHelpers} from "../api/CollectionHelpers.sol";
import {UniqueNFT} from "../api/UniqueNFT.sol";
+ error Value(uint256 value);
+
contract ProxyContract {
bool value = false;
address flipper;
@@ -207,30 +202,30 @@
Flipper(flipper).flip();
}
- function createNonfungibleCollection() public {
+ function createNonfungibleCollection() external payable {
address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F;
- address nftCollection = CollectionHelpers(collectionHelpers).createNonfungibleCollection("A", "B", "C");
+ address nftCollection = CollectionHelpers(collectionHelpers).createNonfungibleCollection{value: msg.value}("A", "B", "C");
emit CollectionCreated(nftCollection);
}
- function mintNftToken(address collectionAddress) public {
+ function mintNftToken(address collectionAddress) external {
UniqueNFT collection = UniqueNFT(collectionAddress);
uint256 tokenId = collection.nextTokenId();
collection.mint(msg.sender, tokenId);
emit TokenMinted(tokenId);
}
- function getValue() public view returns (bool) {
+ function getValue() external view returns (bool) {
return Flipper(flipper).getValue();
}
}
contract Flipper {
bool value = false;
- function flip() public {
+ function flip() external {
value = !value;
}
- function getValue() public view returns (bool) {
+ function getValue() external view returns (bool) {
return value;
}
}