difftreelog
fix set prop for not existed token (#933)
in: master
* fix: set prop for not existed token * optimize token checking * remove comments * test(token properties): on token non-existence * fix PR comments * rename value * refactor(modify token properties): readability + grammar * revert: unused import used for try-runtime * fix prop permission check * Add self_mint flag * Add LazyValue * fix tests * fix unit tests * fix docker * fix mintCross sponsoring * Generalize next_token_id * fix: set sponsored properties ---------
20 files changed
.docker/Dockerfile-chain-dev-unitdiffbeforeafterboth--- a/.docker/Dockerfile-chain-dev-unit
+++ b/.docker/Dockerfile-chain-dev-unit
@@ -17,4 +17,4 @@
WORKDIR /dev_chain
-CMD cargo test --features=limit-testing --workspace
+CMD cargo test --features=limit-testing,tests --workspace
pallets/common/Cargo.tomldiffbeforeafterboth--- a/pallets/common/Cargo.toml
+++ b/pallets/common/Cargo.toml
@@ -36,4 +36,5 @@
"up-pov-estimate-rpc/std",
]
stubgen = ["evm-coder/stubgen"]
+tests = []
try-runtime = ["frame-support/try-runtime"]
pallets/common/src/eth.rsdiffbeforeafterboth--- a/pallets/common/src/eth.rs
+++ b/pallets/common/src/eth.rs
@@ -131,6 +131,18 @@
value: evm_coder::types::Bytes,
}
+impl Property {
+ /// Property key.
+ pub fn key(&self) -> &str {
+ self.key.as_str()
+ }
+
+ /// Property value.
+ pub fn value(&self) -> &[u8] {
+ self.value.0.as_slice()
+ }
+}
+
impl TryFrom<up_data_structs::Property> for Property {
type Error = pallet_evm_coder_substrate::execution::Error;
@@ -227,11 +239,9 @@
Some(value) => match value {
0 => Ok(Some(false)),
1 => Ok(Some(true)),
- _ => {
- return Err(Self::Error::Revert(format!(
- "can't convert value to boolean \"{value}\""
- )))
- }
+ _ => Err(Self::Error::Revert(format!(
+ "can't convert value to boolean \"{value}\""
+ ))),
},
None => Ok(None),
};
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -216,7 +216,6 @@
///
/// # Arguments
///
- /// * `sender`: Caller's account.
/// * `sponsor`: ID of the account of the sponsor-to-be.
pub fn force_set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {
self.check_is_internal()?;
@@ -867,6 +866,74 @@
>;
}
+/// Represents the change mode for the token property.
+pub enum SetPropertyMode {
+ /// The token already exists.
+ ExistingToken,
+
+ /// New token.
+ NewToken {
+ /// The creator of the token is the recipient.
+ mint_target_is_sender: bool,
+ },
+}
+
+/// Value representation with delayed initialization time.
+pub struct LazyValue<T, F: FnOnce() -> T> {
+ value: Option<T>,
+ f: Option<F>,
+}
+
+impl<T, F: FnOnce() -> T> LazyValue<T, F> {
+ /// Create a new LazyValue.
+ pub fn new(f: F) -> Self {
+ Self {
+ value: None,
+ f: Some(f),
+ }
+ }
+
+ /// Get the value. If it call furst time the value will be initialized.
+ pub fn value(&mut self) -> &T {
+ if self.value.is_none() {
+ self.value = Some(self.f.take().unwrap()())
+ }
+
+ self.value.as_ref().unwrap()
+ }
+
+ /// Is value initialized.
+ pub fn has_value(&self) -> bool {
+ self.value.is_some()
+ }
+}
+
+fn check_token_permissions<T, FCA, FTO, FTE>(
+ collection_admin_permitted: bool,
+ token_owner_permitted: bool,
+ is_collection_admin: &mut LazyValue<bool, FCA>,
+ is_token_owner: &mut LazyValue<Result<bool, DispatchError>, FTO>,
+ is_token_exist: &mut LazyValue<bool, FTE>,
+) -> DispatchResult
+where
+ T: Config,
+ FCA: FnOnce() -> bool,
+ FTO: FnOnce() -> Result<bool, DispatchError>,
+ FTE: FnOnce() -> bool,
+{
+ if !(collection_admin_permitted && *is_collection_admin.value()
+ || token_owner_permitted && (*is_token_owner.value())?)
+ {
+ fail!(<Error<T>>::NoPermission);
+ }
+
+ let token_certainly_exist = is_token_owner.has_value() && (*is_token_owner.value())?;
+ if !token_certainly_exist && !is_token_exist.value() {
+ fail!(<Error<T>>::TokenNotFound);
+ }
+ Ok(())
+}
+
impl<T: Config> Pallet<T> {
/// Enshure that receiver address is correct.
///
@@ -1218,10 +1285,6 @@
/// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`
/// * removes a property under the <key> if the value is `None` `(<key>, None)`.
///
- /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.
- /// - `is_token_create`: Indicates that method is called during token initialization.
- /// Allows to bypass ownership check.
- ///
/// All affected properties should have `mutable` permission
/// to be **deleted** or to be **set more than once**,
/// and the sender should have permission to edit those properties.
@@ -1229,35 +1292,36 @@
/// This function fires an event for each property change.
/// In case of an error, all the changes (including the events) will be reverted
/// since the function is transactional.
- pub fn modify_token_properties(
+ #[allow(clippy::too_many_arguments)]
+ pub fn modify_token_properties<FTO, FTE>(
collection: &CollectionHandle<T>,
sender: &T::CrossAccountId,
token_id: TokenId,
+ is_token_exist: &mut LazyValue<bool, FTE>,
properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,
- is_token_create: bool,
mut stored_properties: TokenProperties,
- is_token_owner: impl Fn() -> Result<bool, DispatchError>,
+ is_token_owner: &mut LazyValue<Result<bool, DispatchError>, FTO>,
set_token_properties: impl FnOnce(TokenProperties),
log: evm_coder::ethereum::Log,
- ) -> DispatchResult {
- let is_collection_admin = collection.is_owner_or_admin(sender);
+ ) -> DispatchResult
+ where
+ FTO: FnOnce() -> Result<bool, DispatchError>,
+ FTE: FnOnce() -> bool,
+ {
+ let mut is_collection_admin = LazyValue::new(|| collection.is_owner_or_admin(sender));
let permissions = Self::property_permissions(collection.id);
- let mut token_owner_result = None;
- let mut is_token_owner = || -> Result<bool, DispatchError> {
- *token_owner_result.get_or_insert_with(&is_token_owner)
- };
-
+ let mut changed = false;
for (key, value) in properties_updates {
let permission = permissions
.get(&key)
.cloned()
.unwrap_or_else(PropertyPermission::none);
- let is_property_exists = stored_properties.get(&key).is_some();
+ let property_exists = stored_properties.get(&key).is_some();
match permission {
- PropertyPermission { mutable: false, .. } if is_property_exists => {
+ PropertyPermission { mutable: false, .. } if property_exists => {
return Err(<Error<T>>::NoPermission.into());
}
@@ -1265,17 +1329,13 @@
collection_admin,
token_owner,
..
- } => {
- //TODO: investigate threats during public minting.
- let is_token_create =
- is_token_create && (collection_admin || token_owner) && value.is_some();
- if !(is_token_create
- || (collection_admin && is_collection_admin)
- || (token_owner && is_token_owner()?))
- {
- fail!(<Error<T>>::NoPermission);
- }
- }
+ } => check_token_permissions::<T, _, FTO, FTE>(
+ collection_admin,
+ token_owner,
+ &mut is_collection_admin,
+ is_token_owner,
+ is_token_exist,
+ )?,
}
match value {
@@ -1293,9 +1353,13 @@
}
}
- <PalletEvm<T>>::deposit_log(log.clone());
+ changed = true;
}
+ if changed {
+ <PalletEvm<T>>::deposit_log(log);
+ }
+
set_token_properties(stored_properties);
Ok(())
@@ -2322,3 +2386,86 @@
}
}
}
+
+#[cfg(feature = "tests")]
+pub mod tests {
+ use crate::{DispatchResult, DispatchError, LazyValue, Config};
+
+ const fn to_bool(u: u8) -> bool {
+ u != 0
+ }
+
+ #[derive(Debug)]
+ pub struct TestCase {
+ pub collection_admin: bool,
+ pub is_collection_admin: bool,
+ pub token_owner: bool,
+ pub is_token_owner: bool,
+ pub no_permission: bool,
+ }
+
+ impl TestCase {
+ const fn new(
+ collection_admin: u8,
+ is_collection_admin: u8,
+ token_owner: u8,
+ is_token_owner: u8,
+ no_permission: u8,
+ ) -> Self {
+ Self {
+ collection_admin: to_bool(collection_admin),
+ is_collection_admin: to_bool(is_collection_admin),
+ token_owner: to_bool(token_owner),
+ is_token_owner: to_bool(is_token_owner),
+ no_permission: to_bool(no_permission),
+ }
+ }
+ }
+
+ #[rustfmt::skip]
+ pub const table: [TestCase; 16] = [
+ // ┌╴collection_admin
+ // │ ┌╴is_collection_admin
+ // │ │ ┌╴token_owner
+ // │ │ │ ┌╴is_token_ownership
+ // │ │ │ │ ┌╴no_permission
+ /* 0*/ TestCase::new(0, 0, 0, 0, 1),
+ /* 1*/ TestCase::new(0, 0, 0, 1, 1),
+ /* 2*/ TestCase::new(0, 0, 1, 0, 1),
+ /* 3*/ TestCase::new(0, 0, 1, 1, 0),
+ /* 4*/ TestCase::new(0, 1, 0, 0, 1),
+ /* 5*/ TestCase::new(0, 1, 0, 1, 1),
+ /* 6*/ TestCase::new(0, 1, 1, 0, 1),
+ /* 7*/ TestCase::new(0, 1, 1, 1, 0),
+ /* 8*/ TestCase::new(1, 0, 0, 0, 1),
+ /* 9*/ TestCase::new(1, 0, 0, 1, 1),
+ /* 10*/ TestCase::new(1, 0, 1, 0, 1),
+ /* 11*/ TestCase::new(1, 0, 1, 1, 0),
+ /* 12*/ TestCase::new(1, 1, 0, 0, 0),
+ /* 13*/ TestCase::new(1, 1, 0, 1, 0),
+ /* 14*/ TestCase::new(1, 1, 1, 0, 0),
+ /* 15*/ TestCase::new(1, 1, 1, 1, 0),
+ ];
+
+ pub fn check_token_permissions<T, FCA, FTO, FTE>(
+ collection_admin_permitted: bool,
+ token_owner_permitted: bool,
+ is_collection_admin: &mut LazyValue<bool, FCA>,
+ check_token_ownership: &mut LazyValue<Result<bool, DispatchError>, FTO>,
+ check_token_existence: &mut LazyValue<bool, FTE>,
+ ) -> DispatchResult
+ where
+ T: Config,
+ FCA: FnOnce() -> bool,
+ FTO: FnOnce() -> Result<bool, DispatchError>,
+ FTE: FnOnce() -> bool,
+ {
+ crate::check_token_permissions::<T, FCA, FTO, FTE>(
+ collection_admin_permitted,
+ token_owner_permitted,
+ is_collection_admin,
+ check_token_ownership,
+ check_token_existence,
+ )
+ }
+}
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -245,7 +245,7 @@
&sender,
token_id,
properties.into_iter(),
- false,
+ pallet_common::SetPropertyMode::ExistingToken,
nesting_budget,
),
weight,
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -194,7 +194,7 @@
&caller,
TokenId(token_id),
properties.into_iter(),
- false,
+ pallet_common::SetPropertyMode::ExistingToken,
&nesting_budget,
)
.map_err(dispatch_to_evm::<T>)
@@ -939,9 +939,8 @@
/// @notice Returns next free NFT ID.
fn next_token_id(&self) -> Result<U256> {
self.consume_store_reads(1)?;
- Ok(<TokensMinted<T>>::get(self.id)
- .checked_add(1)
- .ok_or("item id overflow")?
+ Ok(<Pallet<T>>::next_token_id(self)
+ .map_err(dispatch_to_evm::<T>)?
.into())
}
pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -109,7 +109,7 @@
use pallet_common::{
Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,
eth::collection_id_to_address, SelfWeightOf as PalletCommonWeightOf,
- weights::WeightInfo as CommonWeightInfo, helpers::add_weight_to_post_info,
+ weights::WeightInfo as CommonWeightInfo, helpers::add_weight_to_post_info, SetPropertyMode,
};
use pallet_structure::{Pallet as PalletStructure, Error as StructureError};
use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
@@ -585,8 +585,6 @@
/// A batch operation to add, edit or remove properties for a token.
///
/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.
- /// - `is_token_create`: Indicates that method is called during token initialization.
- /// Allows to bypass ownership check.
///
/// All affected properties should have `mutable` permission
/// to be **deleted** or to be **set more than once**,
@@ -601,10 +599,17 @@
sender: &T::CrossAccountId,
token_id: TokenId,
properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,
- is_token_create: bool,
+ mode: SetPropertyMode,
nesting_budget: &dyn Budget,
) -> DispatchResult {
- let is_token_owner = || {
+ let mut is_token_owner = pallet_common::LazyValue::new(|| {
+ if let SetPropertyMode::NewToken {
+ mint_target_is_sender,
+ } = mode
+ {
+ return Ok(mint_target_is_sender);
+ }
+
let is_owned = <PalletStructure<T>>::check_indirectly_owned(
sender.clone(),
collection.id,
@@ -614,18 +619,21 @@
)?;
Ok(is_owned)
- };
+ });
+ let mut is_token_exist =
+ pallet_common::LazyValue::new(|| Self::token_exists(collection, token_id));
+
let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));
<PalletCommon<T>>::modify_token_properties(
collection,
sender,
token_id,
+ &mut is_token_exist,
properties_updates,
- is_token_create,
stored_properties,
- is_token_owner,
+ &mut is_token_owner,
|properties| <TokenProperties<T>>::set((collection.id, token_id), properties),
erc::ERC721TokenEvent::TokenChanged {
token_id: token_id.into(),
@@ -634,6 +642,19 @@
)
}
+ pub fn next_token_id(collection: &NonfungibleHandle<T>) -> Result<TokenId, DispatchError> {
+ let next_token_id = <TokensMinted<T>>::get(collection.id)
+ .checked_add(1)
+ .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;
+
+ ensure!(
+ collection.limits.token_limit() >= next_token_id,
+ <CommonError<T>>::CollectionTokenLimitExceeded
+ );
+
+ Ok(TokenId(next_token_id))
+ }
+
/// Batch operation to add or edit properties for the token
///
/// Same as [`modify_token_properties`] but doesn't allow to remove properties
@@ -644,7 +665,7 @@
sender: &T::CrossAccountId,
token_id: TokenId,
properties: impl Iterator<Item = Property>,
- is_token_create: bool,
+ mode: SetPropertyMode,
nesting_budget: &dyn Budget,
) -> DispatchResult {
Self::modify_token_properties(
@@ -652,7 +673,7 @@
sender,
token_id,
properties.map(|p| (p.key, Some(p.value))),
- is_token_create,
+ mode,
nesting_budget,
)
}
@@ -669,14 +690,12 @@
property: Property,
nesting_budget: &dyn Budget,
) -> DispatchResult {
- let is_token_create = false;
-
Self::set_token_properties(
collection,
sender,
token_id,
[property].into_iter(),
- is_token_create,
+ SetPropertyMode::ExistingToken,
nesting_budget,
)
}
@@ -693,14 +712,12 @@
property_keys: impl Iterator<Item = PropertyKey>,
nesting_budget: &dyn Budget,
) -> DispatchResult {
- let is_token_create = false;
-
Self::modify_token_properties(
collection,
sender,
token_id,
property_keys.into_iter().map(|key| (key, None)),
- is_token_create,
+ SetPropertyMode::ExistingToken,
nesting_budget,
)
}
@@ -985,7 +1002,9 @@
sender,
TokenId(token),
data.properties.clone().into_iter(),
- true,
+ SetPropertyMode::NewToken {
+ mint_target_is_sender: sender.conv_eq(&data.owner),
+ },
nesting_budget,
) {
return TransactionOutcome::Rollback(Err(e));
pallets/refungible/src/common.rsdiffbeforeafterboth--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -399,7 +399,7 @@
&sender,
token_id,
properties.into_iter(),
- false,
+ pallet_common::SetPropertyMode::ExistingToken,
nesting_budget,
),
weight,
pallets/refungible/src/erc.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -196,7 +196,7 @@
&caller,
TokenId(token_id),
properties.into_iter(),
- false,
+ pallet_common::SetPropertyMode::ExistingToken,
&nesting_budget,
)
.map_err(dispatch_to_evm::<T>)
@@ -973,9 +973,8 @@
/// @notice Returns next free RFT ID.
fn next_token_id(&self) -> Result<U256> {
self.consume_store_reads(1)?;
- Ok(<TokensMinted<T>>::get(self.id)
- .checked_add(1)
- .ok_or("item id overflow")?
+ Ok(<Pallet<T>>::next_token_id(self)
+ .map_err(dispatch_to_evm::<T>)?
.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 core::{ops::Deref, cmp::Ordering};94use evm_coder::ToLog;95use frame_support::{ensure, storage::with_transaction, transactional};96use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};97use pallet_evm_coder_substrate::WithRecorder;98use pallet_common::{99 CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address,100 Event as CommonEvent, Pallet as PalletCommon,101};102use pallet_structure::Pallet as PalletStructure;103use sp_core::{Get, H160};104use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};105use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};106use up_data_structs::{107 AccessMode, budget::Budget, CollectionId, CollectionFlags, CreateCollectionData,108 mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, Property, PropertyKey,109 PropertyKeyPermission, PropertyScope, PropertyValue, TokenId, TrySetProperty,110 PropertiesPermissionMap, CreateRefungibleExMultipleOwners, TokenOwnerError,111 TokenProperties as TokenPropertiesT,112};113114pub use pallet::*;115#[cfg(feature = "runtime-benchmarks")]116pub mod benchmarking;117pub mod common;118pub mod erc;119pub mod erc_token;120pub mod weights;121122pub type CreateItemData<T> =123 CreateRefungibleExMultipleOwners<<T as pallet_evm::Config>::CrossAccountId>;124pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;125126#[frame_support::pallet]127pub mod pallet {128 use super::*;129 use frame_support::{130 Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,131 traits::StorageVersion,132 };133 use up_data_structs::{CollectionId, TokenId};134 use super::weights::WeightInfo;135136 #[pallet::error]137 pub enum Error<T> {138 /// Not Refungible item data used to mint in Refungible collection.139 NotRefungibleDataUsedToMintFungibleCollectionToken,140 /// Maximum refungibility exceeded.141 WrongRefungiblePieces,142 /// Refungible token can't be repartitioned by user who isn't owns all pieces.143 RepartitionWhileNotOwningAllPieces,144 /// Refungible token can't nest other tokens.145 RefungibleDisallowsNesting,146 /// Setting item properties is not allowed.147 SettingPropertiesNotAllowed,148 }149150 #[pallet::config]151 pub trait Config:152 frame_system::Config + pallet_common::Config + pallet_structure::Config153 {154 type WeightInfo: WeightInfo;155 }156157 const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);158159 #[pallet::pallet]160 #[pallet::storage_version(STORAGE_VERSION)]161 pub struct Pallet<T>(_);162163 /// Total amount of minted tokens in a collection.164 #[pallet::storage]165 pub type TokensMinted<T: Config> =166 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;167168 /// Amount of tokens burnt in a collection.169 #[pallet::storage]170 pub type TokensBurnt<T: Config> =171 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;172173 /// Amount of pieces a refungible token is split into.174 #[pallet::storage]175 #[pallet::getter(fn token_properties)]176 pub type TokenProperties<T: Config> = StorageNMap<177 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),178 Value = TokenPropertiesT,179 QueryKind = ValueQuery,180 >;181182 /// Total amount of pieces for token183 #[pallet::storage]184 pub type TotalSupply<T: Config> = StorageNMap<185 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),186 Value = u128,187 QueryKind = ValueQuery,188 >;189190 /// Used to enumerate tokens owned by account.191 #[pallet::storage]192 pub type Owned<T: Config> = StorageNMap<193 Key = (194 Key<Twox64Concat, CollectionId>,195 Key<Blake2_128Concat, T::CrossAccountId>,196 Key<Twox64Concat, TokenId>,197 ),198 Value = bool,199 QueryKind = ValueQuery,200 >;201202 /// Amount of tokens (not pieces) partially owned by an account within a collection.203 #[pallet::storage]204 pub type AccountBalance<T: Config> = StorageNMap<205 Key = (206 Key<Twox64Concat, CollectionId>,207 // Owner208 Key<Blake2_128Concat, T::CrossAccountId>,209 ),210 Value = u32,211 QueryKind = ValueQuery,212 >;213214 /// Amount of token pieces owned by account.215 #[pallet::storage]216 pub type Balance<T: Config> = StorageNMap<217 Key = (218 Key<Twox64Concat, CollectionId>,219 Key<Twox64Concat, TokenId>,220 // Owner221 Key<Blake2_128Concat, T::CrossAccountId>,222 ),223 Value = u128,224 QueryKind = ValueQuery,225 >;226227 /// Allowance set by a token owner for another user to perform one of certain transactions on a number of pieces of a token.228 #[pallet::storage]229 pub type Allowance<T: Config> = StorageNMap<230 Key = (231 Key<Twox64Concat, CollectionId>,232 Key<Twox64Concat, TokenId>,233 // Owner234 Key<Blake2_128, T::CrossAccountId>,235 // Spender236 Key<Blake2_128Concat, T::CrossAccountId>,237 ),238 Value = u128,239 QueryKind = ValueQuery,240 >;241242 /// Spender set by a wallet owner that could perform certain transactions on all tokens in the wallet.243 #[pallet::storage]244 pub type CollectionAllowance<T: Config> = StorageNMap<245 Key = (246 Key<Twox64Concat, CollectionId>,247 Key<Blake2_128Concat, T::CrossAccountId>, // Owner248 Key<Blake2_128Concat, T::CrossAccountId>, // Spender249 ),250 Value = bool,251 QueryKind = ValueQuery,252 >;253}254255pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);256impl<T: Config> RefungibleHandle<T> {257 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {258 Self(inner)259 }260 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {261 self.0262 }263 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {264 &mut self.0265 }266}267268impl<T: Config> Deref for RefungibleHandle<T> {269 type Target = pallet_common::CollectionHandle<T>;270271 fn deref(&self) -> &Self::Target {272 &self.0273 }274}275276impl<T: Config> WithRecorder<T> for RefungibleHandle<T> {277 fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {278 self.0.recorder()279 }280 fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {281 self.0.into_recorder()282 }283}284285impl<T: Config> Pallet<T> {286 /// Get number of RFT tokens in collection287 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {288 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)289 }290291 /// Check that RFT token exists292 ///293 /// - `token`: Token ID.294 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {295 <TotalSupply<T>>::contains_key((collection.id, token))296 }297298 pub fn set_scoped_token_property(299 collection_id: CollectionId,300 token_id: TokenId,301 scope: PropertyScope,302 property: Property,303 ) -> DispatchResult {304 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {305 properties.try_scoped_set(scope, property.key, property.value)306 })307 .map_err(<CommonError<T>>::from)?;308309 Ok(())310 }311312 pub fn set_scoped_token_properties(313 collection_id: CollectionId,314 token_id: TokenId,315 scope: PropertyScope,316 properties: impl Iterator<Item = Property>,317 ) -> DispatchResult {318 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {319 stored_properties.try_scoped_set_from_iter(scope, properties)320 })321 .map_err(<CommonError<T>>::from)?;322323 Ok(())324 }325}326327// unchecked calls skips any permission checks328impl<T: Config> Pallet<T> {329 /// Create RFT collection330 ///331 /// `init_collection` will take non-refundable deposit for collection creation.332 ///333 /// - `data`: Contains settings for collection limits and permissions.334 pub fn init_collection(335 owner: T::CrossAccountId,336 payer: T::CrossAccountId,337 data: CreateCollectionData<T::AccountId>,338 flags: CollectionFlags,339 ) -> Result<CollectionId, DispatchError> {340 <PalletCommon<T>>::init_collection(owner, payer, data, flags)341 }342343 /// Destroy RFT collection344 ///345 /// `destroy_collection` will throw error if collection contains any tokens.346 /// Only owner can destroy collection.347 pub fn destroy_collection(348 collection: RefungibleHandle<T>,349 sender: &T::CrossAccountId,350 ) -> DispatchResult {351 let id = collection.id;352353 if Self::collection_has_tokens(id) {354 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());355 }356357 // =========358359 PalletCommon::destroy_collection(collection.0, sender)?;360361 <TokensMinted<T>>::remove(id);362 <TokensBurnt<T>>::remove(id);363 let _ = <TotalSupply<T>>::clear_prefix((id,), u32::MAX, None);364 let _ = <Balance<T>>::clear_prefix((id,), u32::MAX, None);365 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);366 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);367 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);368 Ok(())369 }370371 fn collection_has_tokens(collection_id: CollectionId) -> bool {372 <TotalSupply<T>>::iter_prefix((collection_id,))373 .next()374 .is_some()375 }376377 pub fn burn_token_unchecked(378 collection: &RefungibleHandle<T>,379 owner: &T::CrossAccountId,380 token_id: TokenId,381 ) -> DispatchResult {382 let burnt = <TokensBurnt<T>>::get(collection.id)383 .checked_add(1)384 .ok_or(ArithmeticError::Overflow)?;385386 <TokensBurnt<T>>::insert(collection.id, burnt);387 <TokenProperties<T>>::remove((collection.id, token_id));388 <TotalSupply<T>>::remove((collection.id, token_id));389 let _ = <Balance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);390 let _ = <Allowance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);391 <PalletEvm<T>>::deposit_log(392 ERC721Events::Transfer {393 from: *owner.as_eth(),394 to: H160::default(),395 token_id: token_id.into(),396 }397 .to_log(collection_id_to_address(collection.id)),398 );399 Ok(())400 }401402 /// Burn RFT token pieces403 ///404 /// `burn` will decrease total amount of token pieces and amount owned by sender.405 /// `burn` can be called even if there are multiple owners of the RFT token.406 /// If sender wouldn't have any pieces left after `burn` than she will stop being407 /// one of the owners of the token. If there is no account that owns any pieces of408 /// the token than token will be burned too.409 ///410 /// - `amount`: Amount of token pieces to burn.411 /// - `token`: Token who's pieces should be burned412 /// - `collection`: Collection that contains the token413 pub fn burn(414 collection: &RefungibleHandle<T>,415 owner: &T::CrossAccountId,416 token: TokenId,417 amount: u128,418 ) -> DispatchResult {419 if <Balance<T>>::get((collection.id, token, owner)) == 0 {420 return Err(<CommonError<T>>::TokenValueTooLow.into());421 }422423 let total_supply = <TotalSupply<T>>::get((collection.id, token))424 .checked_sub(amount)425 .ok_or(<CommonError<T>>::TokenValueTooLow)?;426427 // This was probally last owner of this token?428 if total_supply == 0 {429 // Ensure user actually owns this amount430 ensure!(431 <Balance<T>>::get((collection.id, token, owner)) == amount,432 <CommonError<T>>::TokenValueTooLow433 );434 let account_balance = <AccountBalance<T>>::get((collection.id, owner))435 .checked_sub(1)436 // Should not occur437 .ok_or(ArithmeticError::Underflow)?;438439 // =========440441 <Owned<T>>::remove((collection.id, owner, token));442 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);443 <AccountBalance<T>>::insert((collection.id, owner), account_balance);444 Self::burn_token_unchecked(collection, owner, token)?;445 <PalletEvm<T>>::deposit_log(446 ERC20Events::Transfer {447 from: *owner.as_eth(),448 to: H160::default(),449 value: amount.into(),450 }451 .to_log(collection_id_to_address(collection.id)),452 );453 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(454 collection.id,455 token,456 owner.clone(),457 amount,458 ));459 return Ok(());460 }461462 let balance = <Balance<T>>::get((collection.id, token, owner))463 .checked_sub(amount)464 .ok_or(<CommonError<T>>::TokenValueTooLow)?;465 let account_balance = if balance == 0 {466 <AccountBalance<T>>::get((collection.id, owner))467 .checked_sub(1)468 // Should not occur469 .ok_or(ArithmeticError::Underflow)?470 } else {471 0472 };473474 // =========475476 if balance == 0 {477 <Owned<T>>::remove((collection.id, owner, token));478 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);479 <Balance<T>>::remove((collection.id, token, owner));480 <AccountBalance<T>>::insert((collection.id, owner), account_balance);481482 if let Ok(user) = Self::token_owner(collection.id, token) {483 <PalletEvm<T>>::deposit_log(484 ERC721Events::Transfer {485 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,486 to: *user.as_eth(),487 token_id: token.into(),488 }489 .to_log(collection_id_to_address(collection.id)),490 );491 }492 } else {493 <Balance<T>>::insert((collection.id, token, owner), balance);494 }495 <TotalSupply<T>>::insert((collection.id, token), total_supply);496497 <PalletEvm<T>>::deposit_log(498 ERC20Events::Transfer {499 from: *owner.as_eth(),500 to: H160::default(),501 value: amount.into(),502 }503 .to_log(T::EvmTokenAddressMapping::token_to_address(504 collection.id,505 token,506 )),507 );508 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(509 collection.id,510 token,511 owner.clone(),512 amount,513 ));514 Ok(())515 }516517 /// A batch operation to add, edit or remove properties for a token.518 /// It sets or removes a token's properties according to519 /// `properties_updates` contents:520 /// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`521 /// * removes a property under the <key> if the value is `None` `(<key>, None)`.522 ///523 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.524 /// - `is_token_create`: Indicates that method is called during token initialization.525 /// Allows to bypass ownership check.526 ///527 /// All affected properties should have `mutable` permission528 /// to be **deleted** or to be **set more than once**,529 /// and the sender should have permission to edit those properties.530 ///531 /// This function fires an event for each property change.532 /// In case of an error, all the changes (including the events) will be reverted533 /// since the function is transactional.534 #[transactional]535 fn modify_token_properties(536 collection: &RefungibleHandle<T>,537 sender: &T::CrossAccountId,538 token_id: TokenId,539 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,540 is_token_create: bool,541 nesting_budget: &dyn Budget,542 ) -> DispatchResult {543 let is_token_owner = || -> Result<bool, DispatchError> {544 let balance = collection.balance(sender.clone(), token_id);545 let total_pieces: u128 =546 Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);547 if balance != total_pieces {548 return Ok(false);549 }550551 let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(552 sender.clone(),553 collection.id,554 token_id,555 None,556 nesting_budget,557 )?;558559 Ok(is_bundle_owner)560 };561562 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));563564 <PalletCommon<T>>::modify_token_properties(565 collection,566 sender,567 token_id,568 properties_updates,569 is_token_create,570 stored_properties,571 is_token_owner,572 |properties| <TokenProperties<T>>::set((collection.id, token_id), properties),573 erc::ERC721TokenEvent::TokenChanged {574 token_id: token_id.into(),575 }576 .to_log(T::ContractAddress::get()),577 )578 }579580 pub fn set_token_properties(581 collection: &RefungibleHandle<T>,582 sender: &T::CrossAccountId,583 token_id: TokenId,584 properties: impl Iterator<Item = Property>,585 is_token_create: bool,586 nesting_budget: &dyn Budget,587 ) -> DispatchResult {588 Self::modify_token_properties(589 collection,590 sender,591 token_id,592 properties.map(|p| (p.key, Some(p.value))),593 is_token_create,594 nesting_budget,595 )596 }597598 pub fn set_token_property(599 collection: &RefungibleHandle<T>,600 sender: &T::CrossAccountId,601 token_id: TokenId,602 property: Property,603 nesting_budget: &dyn Budget,604 ) -> DispatchResult {605 let is_token_create = false;606607 Self::set_token_properties(608 collection,609 sender,610 token_id,611 [property].into_iter(),612 is_token_create,613 nesting_budget,614 )615 }616617 pub fn delete_token_properties(618 collection: &RefungibleHandle<T>,619 sender: &T::CrossAccountId,620 token_id: TokenId,621 property_keys: impl Iterator<Item = PropertyKey>,622 nesting_budget: &dyn Budget,623 ) -> DispatchResult {624 let is_token_create = false;625626 Self::modify_token_properties(627 collection,628 sender,629 token_id,630 property_keys.into_iter().map(|key| (key, None)),631 is_token_create,632 nesting_budget,633 )634 }635636 pub fn delete_token_property(637 collection: &RefungibleHandle<T>,638 sender: &T::CrossAccountId,639 token_id: TokenId,640 property_key: PropertyKey,641 nesting_budget: &dyn Budget,642 ) -> DispatchResult {643 Self::delete_token_properties(644 collection,645 sender,646 token_id,647 [property_key].into_iter(),648 nesting_budget,649 )650 }651652 /// Transfer RFT token pieces from one account to another.653 ///654 /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.655 ///656 /// - `from`: Owner of token pieces to transfer.657 /// - `to`: Recepient of transfered token pieces.658 /// - `amount`: Amount of token pieces to transfer.659 /// - `token`: Token whos pieces should be transfered660 /// - `collection`: Collection that contains the token661 pub fn transfer(662 collection: &RefungibleHandle<T>,663 from: &T::CrossAccountId,664 to: &T::CrossAccountId,665 token: TokenId,666 amount: u128,667 nesting_budget: &dyn Budget,668 ) -> DispatchResult {669 ensure!(670 collection.limits.transfers_enabled(),671 <CommonError<T>>::TransferNotAllowed672 );673674 if collection.permissions.access() == AccessMode::AllowList {675 collection.check_allowlist(from)?;676 collection.check_allowlist(to)?;677 }678 <PalletCommon<T>>::ensure_correct_receiver(to)?;679680 let initial_balance_from = <Balance<T>>::get((collection.id, token, from));681682 if initial_balance_from == 0 {683 return Err(<CommonError<T>>::TokenValueTooLow.into());684 }685686 let updated_balance_from = initial_balance_from687 .checked_sub(amount)688 .ok_or(<CommonError<T>>::TokenValueTooLow)?;689 let mut create_target = false;690 let from_to_differ = from != to;691 let updated_balance_to = if from != to && amount != 0 {692 let old_balance = <Balance<T>>::get((collection.id, token, to));693 if old_balance == 0 {694 create_target = true;695 }696 Some(697 old_balance698 .checked_add(amount)699 .ok_or(ArithmeticError::Overflow)?,700 )701 } else {702 None703 };704705 let account_balance_from = if updated_balance_from == 0 {706 Some(707 <AccountBalance<T>>::get((collection.id, from))708 .checked_sub(1)709 // Should not occur710 .ok_or(ArithmeticError::Underflow)?,711 )712 } else {713 None714 };715 // Account data is created in token, AccountBalance should be increased716 // But only if from != to as we shouldn't check overflow in this case717 let account_balance_to = if create_target && from_to_differ {718 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))719 .checked_add(1)720 .ok_or(ArithmeticError::Overflow)?;721 ensure!(722 account_balance_to < collection.limits.account_token_ownership_limit(),723 <CommonError<T>>::AccountTokenLimitExceeded,724 );725726 Some(account_balance_to)727 } else {728 None729 };730731 // =========732733 if let Some(updated_balance_to) = updated_balance_to {734 // from != to && amount != 0735736 <PalletStructure<T>>::nest_if_sent_to_token(737 from.clone(),738 to,739 collection.id,740 token,741 nesting_budget,742 )?;743744 if updated_balance_from == 0 {745 <Balance<T>>::remove((collection.id, token, from));746 <PalletStructure<T>>::unnest_if_nested(from, collection.id, token);747 } else {748 <Balance<T>>::insert((collection.id, token, from), updated_balance_from);749 }750 <Balance<T>>::insert((collection.id, token, to), updated_balance_to);751 if let Some(account_balance_from) = account_balance_from {752 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);753 <Owned<T>>::remove((collection.id, from, token));754 }755 if let Some(account_balance_to) = account_balance_to {756 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);757 <Owned<T>>::insert((collection.id, to, token), true);758 }759 }760761 <PalletEvm<T>>::deposit_log(762 ERC20Events::Transfer {763 from: *from.as_eth(),764 to: *to.as_eth(),765 value: amount.into(),766 }767 .to_log(T::EvmTokenAddressMapping::token_to_address(768 collection.id,769 token,770 )),771 );772773 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(774 collection.id,775 token,776 from.clone(),777 to.clone(),778 amount,779 ));780781 let total_supply = <TotalSupply<T>>::get((collection.id, token));782783 if amount == total_supply {784 // if token was fully owned by `from` and will be fully owned by `to` after transfer785 <PalletEvm<T>>::deposit_log(786 ERC721Events::Transfer {787 from: *from.as_eth(),788 to: *to.as_eth(),789 token_id: token.into(),790 }791 .to_log(collection_id_to_address(collection.id)),792 );793 } else if let Some(updated_balance_to) = updated_balance_to {794 // if `from` not equals `to`. This condition is needed to avoid sending event795 // when `from` fully owns token and sends part of token pieces to itself.796 if initial_balance_from == total_supply {797 // if token was fully owned by `from` and will be only partially owned by `to`798 // and `from` after transfer799 <PalletEvm<T>>::deposit_log(800 ERC721Events::Transfer {801 from: *from.as_eth(),802 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,803 token_id: token.into(),804 }805 .to_log(collection_id_to_address(collection.id)),806 );807 } else if updated_balance_to == total_supply {808 // if token was partially owned by `from` and will be fully owned by `to` after transfer809 <PalletEvm<T>>::deposit_log(810 ERC721Events::Transfer {811 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,812 to: *to.as_eth(),813 token_id: token.into(),814 }815 .to_log(collection_id_to_address(collection.id)),816 );817 }818 }819820 Ok(())821 }822823 /// Batched operation to create multiple RFT tokens.824 ///825 /// Same as `create_item` but creates multiple tokens.826 ///827 /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.828 pub fn create_multiple_items(829 collection: &RefungibleHandle<T>,830 sender: &T::CrossAccountId,831 data: Vec<CreateItemData<T>>,832 nesting_budget: &dyn Budget,833 ) -> DispatchResult {834 if !collection.is_owner_or_admin(sender) {835 ensure!(836 collection.permissions.mint_mode(),837 <CommonError<T>>::PublicMintingNotAllowed838 );839 collection.check_allowlist(sender)?;840841 for item in data.iter() {842 for user in item.users.keys() {843 collection.check_allowlist(user)?;844 }845 }846 }847848 for item in data.iter() {849 for (owner, _) in item.users.iter() {850 <PalletCommon<T>>::ensure_correct_receiver(owner)?;851 }852 }853854 // Total pieces per tokens855 let totals = data856 .iter()857 .map(|data| {858 Ok(data859 .users860 .iter()861 .map(|u| u.1)862 .try_fold(0u128, |acc, v| acc.checked_add(*v))863 .ok_or(ArithmeticError::Overflow)?)864 })865 .collect::<Result<Vec<_>, DispatchError>>()?;866 for total in &totals {867 ensure!(868 *total <= MAX_REFUNGIBLE_PIECES,869 <Error<T>>::WrongRefungiblePieces870 );871 }872873 let first_token_id = <TokensMinted<T>>::get(collection.id);874 let tokens_minted = first_token_id875 .checked_add(data.len() as u32)876 .ok_or(ArithmeticError::Overflow)?;877 ensure!(878 tokens_minted < collection.limits.token_limit(),879 <CommonError<T>>::CollectionTokenLimitExceeded880 );881882 let mut balances = BTreeMap::new();883 for data in &data {884 for owner in data.users.keys() {885 let balance = balances886 .entry(owner)887 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));888 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;889890 ensure!(891 *balance <= collection.limits.account_token_ownership_limit(),892 <CommonError<T>>::AccountTokenLimitExceeded,893 );894 }895 }896897 for (i, token) in data.iter().enumerate() {898 let token_id = TokenId(first_token_id + i as u32 + 1);899 for (to, _) in token.users.iter() {900 <PalletStructure<T>>::check_nesting(901 sender.clone(),902 to,903 collection.id,904 token_id,905 nesting_budget,906 )?;907 }908 }909910 // =========911912 with_transaction(|| {913 for (i, data) in data.iter().enumerate() {914 let token_id = first_token_id + i as u32 + 1;915 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);916917 for (user, amount) in data.users.iter() {918 if *amount == 0 {919 continue;920 }921 <Balance<T>>::insert((collection.id, token_id, &user), amount);922 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);923 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(924 user,925 collection.id,926 TokenId(token_id),927 );928 }929930 if let Err(e) = Self::set_token_properties(931 collection,932 sender,933 TokenId(token_id),934 data.properties.clone().into_iter(),935 true,936 nesting_budget,937 ) {938 return TransactionOutcome::Rollback(Err(e));939 }940 }941 TransactionOutcome::Commit(Ok(()))942 })?;943944 <TokensMinted<T>>::insert(collection.id, tokens_minted);945946 for (account, balance) in balances {947 <AccountBalance<T>>::insert((collection.id, account), balance);948 }949950 for (i, token) in data.into_iter().enumerate() {951 let token_id = first_token_id + i as u32 + 1;952953 let receivers = token954 .users955 .into_iter()956 .filter(|(_, amount)| *amount > 0)957 .collect::<Vec<_>>();958959 if let [(user, _)] = receivers.as_slice() {960 // if there is exactly one receiver961 <PalletEvm<T>>::deposit_log(962 ERC721Events::Transfer {963 from: H160::default(),964 to: *user.as_eth(),965 token_id: token_id.into(),966 }967 .to_log(collection_id_to_address(collection.id)),968 );969 } else if let [_, ..] = receivers.as_slice() {970 // if there is more than one receiver971 <PalletEvm<T>>::deposit_log(972 ERC721Events::Transfer {973 from: H160::default(),974 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,975 token_id: token_id.into(),976 }977 .to_log(collection_id_to_address(collection.id)),978 );979 }980981 for (user, amount) in receivers.into_iter() {982 <PalletEvm<T>>::deposit_log(983 ERC20Events::Transfer {984 from: H160::default(),985 to: *user.as_eth(),986 value: amount.into(),987 }988 .to_log(T::EvmTokenAddressMapping::token_to_address(989 collection.id,990 TokenId(token_id),991 )),992 );993 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(994 collection.id,995 TokenId(token_id),996 user,997 amount,998 ));999 }1000 }1001 Ok(())1002 }10031004 pub fn set_allowance_unchecked(1005 collection: &RefungibleHandle<T>,1006 sender: &T::CrossAccountId,1007 spender: &T::CrossAccountId,1008 token: TokenId,1009 amount: u128,1010 ) {1011 if amount == 0 {1012 <Allowance<T>>::remove((collection.id, token, sender, spender));1013 } else {1014 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);1015 }10161017 <PalletEvm<T>>::deposit_log(1018 ERC20Events::Approval {1019 owner: *sender.as_eth(),1020 spender: *spender.as_eth(),1021 value: amount.into(),1022 }1023 .to_log(T::EvmTokenAddressMapping::token_to_address(1024 collection.id,1025 token,1026 )),1027 );1028 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1029 collection.id,1030 token,1031 sender.clone(),1032 spender.clone(),1033 amount,1034 ))1035 }10361037 /// Set allowance for the spender to `transfer` or `burn` sender's token pieces.1038 ///1039 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.1040 pub fn set_allowance(1041 collection: &RefungibleHandle<T>,1042 sender: &T::CrossAccountId,1043 spender: &T::CrossAccountId,1044 token: TokenId,1045 amount: u128,1046 ) -> DispatchResult {1047 if collection.permissions.access() == AccessMode::AllowList {1048 collection.check_allowlist(sender)?;1049 collection.check_allowlist(spender)?;1050 }10511052 <PalletCommon<T>>::ensure_correct_receiver(spender)?;10531054 if <Balance<T>>::get((collection.id, token, sender)) < amount {1055 ensure!(1056 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),1057 <CommonError<T>>::CantApproveMoreThanOwned1058 );1059 }10601061 // =========10621063 Self::set_allowance_unchecked(collection, sender, spender, token, amount);1064 Ok(())1065 }10661067 /// Set allowance to spend from sender's eth mirror1068 ///1069 /// - `from`: Address of sender's eth mirror.1070 /// - `to`: Adress of spender.1071 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.1072 pub fn set_allowance_from(1073 collection: &RefungibleHandle<T>,1074 sender: &T::CrossAccountId,1075 from: &T::CrossAccountId,1076 to: &T::CrossAccountId,1077 token_id: TokenId,1078 amount: u128,1079 ) -> DispatchResult {1080 if collection.permissions.access() == AccessMode::AllowList {1081 collection.check_allowlist(sender)?;1082 collection.check_allowlist(from)?;1083 collection.check_allowlist(to)?;1084 }10851086 <PalletCommon<T>>::ensure_correct_receiver(to)?;10871088 ensure!(1089 sender.conv_eq(from),1090 <CommonError<T>>::AddressIsNotEthMirror1091 );10921093 if <Balance<T>>::get((collection.id, token_id, from)) < amount {1094 ensure!(1095 collection.limits.owner_can_transfer()1096 && (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from))1097 && Self::token_exists(collection, token_id),1098 <CommonError<T>>::CantApproveMoreThanOwned1099 );1100 }11011102 // =========11031104 Self::set_allowance_unchecked(collection, from, to, token_id, amount);1105 Ok(())1106 }11071108 /// Returns allowance, which should be set after transaction1109 fn check_allowed(1110 collection: &RefungibleHandle<T>,1111 spender: &T::CrossAccountId,1112 from: &T::CrossAccountId,1113 token: TokenId,1114 amount: u128,1115 nesting_budget: &dyn Budget,1116 ) -> Result<Option<u128>, DispatchError> {1117 if spender.conv_eq(from) {1118 return Ok(None);1119 }1120 if collection.permissions.access() == AccessMode::AllowList {1121 // `from`, `to` checked in [`transfer`]1122 collection.check_allowlist(spender)?;1123 }11241125 if collection.ignores_token_restrictions(spender) {1126 return Ok(Self::compute_allowance_decrease(1127 collection, token, from, spender, amount,1128 ));1129 }11301131 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1132 // TODO: should collection owner be allowed to perform this transfer?1133 ensure!(1134 <PalletStructure<T>>::check_indirectly_owned(1135 spender.clone(),1136 source.0,1137 source.1,1138 None,1139 nesting_budget1140 )?,1141 <CommonError<T>>::ApprovedValueTooLow,1142 );1143 return Ok(None);1144 }11451146 let allowance = Self::compute_allowance_decrease(collection, token, from, spender, amount);1147 if allowance.is_some() {1148 return Ok(allowance);1149 }11501151 // Allowance (if any) would be reduced if spender is also wallet operator1152 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1153 return Ok(allowance);1154 }11551156 Err(<CommonError<T>>::ApprovedValueTooLow.into())1157 }11581159 /// Returns `Some(amount)` if the `spender` have allowance to spend this amount.1160 /// Otherwise, it returns `None`.1161 fn compute_allowance_decrease(1162 collection: &RefungibleHandle<T>,1163 token: TokenId,1164 from: &T::CrossAccountId,1165 spender: &T::CrossAccountId,1166 amount: u128,1167 ) -> Option<u128> {1168 <Allowance<T>>::get((collection.id, token, from, spender)).checked_sub(amount)1169 }11701171 /// Transfer RFT token pieces from one account to another.1172 ///1173 /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1174 /// The owner should set allowance for the spender to transfer pieces.1175 ///1176 /// [`transfer`]: struct.Pallet.html#method.transfer1177 pub fn transfer_from(1178 collection: &RefungibleHandle<T>,1179 spender: &T::CrossAccountId,1180 from: &T::CrossAccountId,1181 to: &T::CrossAccountId,1182 token: TokenId,1183 amount: u128,1184 nesting_budget: &dyn Budget,1185 ) -> DispatchResult {1186 let allowance =1187 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;11881189 // =========11901191 Self::transfer(collection, from, to, token, amount, nesting_budget)?;1192 if let Some(allowance) = allowance {1193 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1194 }1195 Ok(())1196 }11971198 /// Burn RFT token pieces from the account.1199 ///1200 /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1201 /// set allowance for the spender to burn pieces1202 ///1203 /// [`burn`]: struct.Pallet.html#method.burn1204 pub fn burn_from(1205 collection: &RefungibleHandle<T>,1206 spender: &T::CrossAccountId,1207 from: &T::CrossAccountId,1208 token: TokenId,1209 amount: u128,1210 nesting_budget: &dyn Budget,1211 ) -> DispatchResult {1212 let allowance =1213 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;12141215 // =========12161217 Self::burn(collection, from, token, amount)?;1218 if let Some(allowance) = allowance {1219 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1220 }1221 Ok(())1222 }12231224 /// Create RFT token.1225 ///1226 /// The sender should be the owner/admin of the collection or collection should be configured1227 /// to allow public minting.1228 ///1229 /// - `data`: Contains list of users who will become the owners of the token pieces and amount1230 /// of token pieces they will receive.1231 pub fn create_item(1232 collection: &RefungibleHandle<T>,1233 sender: &T::CrossAccountId,1234 data: CreateItemData<T>,1235 nesting_budget: &dyn Budget,1236 ) -> DispatchResult {1237 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1238 }12391240 /// Repartition RFT token.1241 ///1242 /// `repartition` will set token balance of the sender and total amount of token pieces.1243 /// Sender should own all of the token pieces. `repartition' could be done even if some1244 /// token pieces were burned before.1245 ///1246 /// - `amount`: Total amount of token pieces that the token will have after `repartition`.1247 pub fn repartition(1248 collection: &RefungibleHandle<T>,1249 owner: &T::CrossAccountId,1250 token: TokenId,1251 amount: u128,1252 ) -> DispatchResult {1253 ensure!(1254 amount <= MAX_REFUNGIBLE_PIECES,1255 <Error<T>>::WrongRefungiblePieces1256 );1257 ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1258 // Ensure user owns all pieces1259 let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1260 let balance = <Balance<T>>::get((collection.id, token, owner));1261 ensure!(1262 total_pieces == balance,1263 <Error<T>>::RepartitionWhileNotOwningAllPieces1264 );12651266 <Balance<T>>::insert((collection.id, token, owner), amount);1267 <TotalSupply<T>>::insert((collection.id, token), amount);12681269 match total_pieces.cmp(&amount) {1270 Ordering::Less => {1271 let mint_amount = amount - total_pieces;1272 <PalletEvm<T>>::deposit_log(1273 ERC20Events::Transfer {1274 from: H160::default(),1275 to: *owner.as_eth(),1276 value: mint_amount.into(),1277 }1278 .to_log(T::EvmTokenAddressMapping::token_to_address(1279 collection.id,1280 token,1281 )),1282 );1283 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1284 collection.id,1285 token,1286 owner.clone(),1287 mint_amount,1288 ));1289 }1290 Ordering::Greater => {1291 let burn_amount = total_pieces - amount;1292 <PalletEvm<T>>::deposit_log(1293 ERC20Events::Transfer {1294 from: *owner.as_eth(),1295 to: H160::default(),1296 value: burn_amount.into(),1297 }1298 .to_log(T::EvmTokenAddressMapping::token_to_address(1299 collection.id,1300 token,1301 )),1302 );1303 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(1304 collection.id,1305 token,1306 owner.clone(),1307 burn_amount,1308 ));1309 }1310 Ordering::Equal => {}1311 }13121313 Ok(())1314 }13151316 fn token_owner(1317 collection_id: CollectionId,1318 token_id: TokenId,1319 ) -> Result<T::CrossAccountId, TokenOwnerError> {1320 let mut owner = None;1321 let mut count = 0;1322 for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1323 count += 1;1324 if count > 1 {1325 return Err(TokenOwnerError::MultipleOwners);1326 }1327 owner = Some(key);1328 }1329 owner.ok_or(TokenOwnerError::NotFound)1330 }13311332 fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1333 <TotalSupply<T>>::try_get((collection_id, token_id)).ok()1334 }13351336 pub fn set_collection_properties(1337 collection: &RefungibleHandle<T>,1338 sender: &T::CrossAccountId,1339 properties: Vec<Property>,1340 ) -> DispatchResult {1341 <PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())1342 }13431344 pub fn delete_collection_properties(1345 collection: &RefungibleHandle<T>,1346 sender: &T::CrossAccountId,1347 property_keys: Vec<PropertyKey>,1348 ) -> DispatchResult {1349 <PalletCommon<T>>::delete_collection_properties(1350 collection,1351 sender,1352 property_keys.into_iter(),1353 )1354 }13551356 pub fn set_token_property_permissions(1357 collection: &RefungibleHandle<T>,1358 sender: &T::CrossAccountId,1359 property_permissions: Vec<PropertyKeyPermission>,1360 ) -> DispatchResult {1361 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1362 }13631364 pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {1365 <PalletCommon<T>>::property_permissions(collection_id)1366 }13671368 pub fn set_scoped_token_property_permissions(1369 collection: &RefungibleHandle<T>,1370 sender: &T::CrossAccountId,1371 scope: PropertyScope,1372 property_permissions: Vec<PropertyKeyPermission>,1373 ) -> DispatchResult {1374 <PalletCommon<T>>::set_scoped_token_property_permissions(1375 collection,1376 sender,1377 scope,1378 property_permissions,1379 )1380 }13811382 /// Returns 10 token in no particular order.1383 ///1384 /// There is no direct way to get token holders in ascending order,1385 /// since `iter_prefix` returns values in no particular order.1386 /// Therefore, getting the 10 largest holders with a large value of holders1387 /// can lead to impact memory allocation + sorting with `n * log (n)`.1388 pub fn token_owners(1389 collection_id: CollectionId,1390 token: TokenId,1391 ) -> Option<Vec<T::CrossAccountId>> {1392 let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1393 .map(|(owner, _amount)| owner)1394 .take(10)1395 .collect();13961397 if res.is_empty() {1398 None1399 } else {1400 Some(res)1401 }1402 }14031404 /// Sets or unsets the approval of a given operator.1405 ///1406 /// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1407 /// - `owner`: Token owner1408 /// - `operator`: Operator1409 /// - `approve`: Should operator status be granted or revoked?1410 pub fn set_allowance_for_all(1411 collection: &RefungibleHandle<T>,1412 owner: &T::CrossAccountId,1413 spender: &T::CrossAccountId,1414 approve: bool,1415 ) -> DispatchResult {1416 <PalletCommon<T>>::set_allowance_for_all(1417 collection,1418 owner,1419 spender,1420 approve,1421 || <CollectionAllowance<T>>::insert((collection.id, owner, spender), approve),1422 ERC721Events::ApprovalForAll {1423 owner: *owner.as_eth(),1424 operator: *spender.as_eth(),1425 approved: approve,1426 }1427 .to_log(collection_id_to_address(collection.id)),1428 )1429 }14301431 /// Tells whether the given `owner` approves the `operator`.1432 pub fn allowance_for_all(1433 collection: &RefungibleHandle<T>,1434 owner: &T::CrossAccountId,1435 spender: &T::CrossAccountId,1436 ) -> bool {1437 <CollectionAllowance<T>>::get((collection.id, owner, spender))1438 }14391440 pub fn repair_item(collection: &RefungibleHandle<T>, token: TokenId) -> DispatchResult {1441 <TokenProperties<T>>::mutate((collection.id, token), |properties| {1442 properties.recompute_consumed_space();1443 });14441445 Ok(())1446 }1447}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 core::{ops::Deref, cmp::Ordering};94use evm_coder::ToLog;95use frame_support::{ensure, storage::with_transaction, transactional};96use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};97use pallet_evm_coder_substrate::WithRecorder;98use pallet_common::{99 CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address,100 Event as CommonEvent, Pallet as PalletCommon, SetPropertyMode,101};102use pallet_structure::Pallet as PalletStructure;103use sp_core::{Get, H160};104use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};105use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};106use up_data_structs::{107 AccessMode, budget::Budget, CollectionId, CollectionFlags, CreateCollectionData,108 mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, Property, PropertyKey,109 PropertyKeyPermission, PropertyScope, PropertyValue, TokenId, TrySetProperty,110 PropertiesPermissionMap, CreateRefungibleExMultipleOwners, TokenOwnerError,111 TokenProperties as TokenPropertiesT,112};113114pub use pallet::*;115#[cfg(feature = "runtime-benchmarks")]116pub mod benchmarking;117pub mod common;118pub mod erc;119pub mod erc_token;120pub mod weights;121122pub type CreateItemData<T> =123 CreateRefungibleExMultipleOwners<<T as pallet_evm::Config>::CrossAccountId>;124pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;125126#[frame_support::pallet]127pub mod pallet {128 use super::*;129 use frame_support::{130 Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,131 traits::StorageVersion,132 };133 use up_data_structs::{CollectionId, TokenId};134 use super::weights::WeightInfo;135136 #[pallet::error]137 pub enum Error<T> {138 /// Not Refungible item data used to mint in Refungible collection.139 NotRefungibleDataUsedToMintFungibleCollectionToken,140 /// Maximum refungibility exceeded.141 WrongRefungiblePieces,142 /// Refungible token can't be repartitioned by user who isn't owns all pieces.143 RepartitionWhileNotOwningAllPieces,144 /// Refungible token can't nest other tokens.145 RefungibleDisallowsNesting,146 /// Setting item properties is not allowed.147 SettingPropertiesNotAllowed,148 }149150 #[pallet::config]151 pub trait Config:152 frame_system::Config + pallet_common::Config + pallet_structure::Config153 {154 type WeightInfo: WeightInfo;155 }156157 const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);158159 #[pallet::pallet]160 #[pallet::storage_version(STORAGE_VERSION)]161 pub struct Pallet<T>(_);162163 /// Total amount of minted tokens in a collection.164 #[pallet::storage]165 pub type TokensMinted<T: Config> =166 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;167168 /// Amount of tokens burnt in a collection.169 #[pallet::storage]170 pub type TokensBurnt<T: Config> =171 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;172173 /// Amount of pieces a refungible token is split into.174 #[pallet::storage]175 #[pallet::getter(fn token_properties)]176 pub type TokenProperties<T: Config> = StorageNMap<177 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),178 Value = TokenPropertiesT,179 QueryKind = ValueQuery,180 >;181182 /// Total amount of pieces for token183 #[pallet::storage]184 pub type TotalSupply<T: Config> = StorageNMap<185 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),186 Value = u128,187 QueryKind = ValueQuery,188 >;189190 /// Used to enumerate tokens owned by account.191 #[pallet::storage]192 pub type Owned<T: Config> = StorageNMap<193 Key = (194 Key<Twox64Concat, CollectionId>,195 Key<Blake2_128Concat, T::CrossAccountId>,196 Key<Twox64Concat, TokenId>,197 ),198 Value = bool,199 QueryKind = ValueQuery,200 >;201202 /// Amount of tokens (not pieces) partially owned by an account within a collection.203 #[pallet::storage]204 pub type AccountBalance<T: Config> = StorageNMap<205 Key = (206 Key<Twox64Concat, CollectionId>,207 // Owner208 Key<Blake2_128Concat, T::CrossAccountId>,209 ),210 Value = u32,211 QueryKind = ValueQuery,212 >;213214 /// Amount of token pieces owned by account.215 #[pallet::storage]216 pub type Balance<T: Config> = StorageNMap<217 Key = (218 Key<Twox64Concat, CollectionId>,219 Key<Twox64Concat, TokenId>,220 // Owner221 Key<Blake2_128Concat, T::CrossAccountId>,222 ),223 Value = u128,224 QueryKind = ValueQuery,225 >;226227 /// Allowance set by a token owner for another user to perform one of certain transactions on a number of pieces of a token.228 #[pallet::storage]229 pub type Allowance<T: Config> = StorageNMap<230 Key = (231 Key<Twox64Concat, CollectionId>,232 Key<Twox64Concat, TokenId>,233 // Owner234 Key<Blake2_128, T::CrossAccountId>,235 // Spender236 Key<Blake2_128Concat, T::CrossAccountId>,237 ),238 Value = u128,239 QueryKind = ValueQuery,240 >;241242 /// Spender set by a wallet owner that could perform certain transactions on all tokens in the wallet.243 #[pallet::storage]244 pub type CollectionAllowance<T: Config> = StorageNMap<245 Key = (246 Key<Twox64Concat, CollectionId>,247 Key<Blake2_128Concat, T::CrossAccountId>, // Owner248 Key<Blake2_128Concat, T::CrossAccountId>, // Spender249 ),250 Value = bool,251 QueryKind = ValueQuery,252 >;253}254255pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);256impl<T: Config> RefungibleHandle<T> {257 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {258 Self(inner)259 }260 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {261 self.0262 }263 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {264 &mut self.0265 }266}267268impl<T: Config> Deref for RefungibleHandle<T> {269 type Target = pallet_common::CollectionHandle<T>;270271 fn deref(&self) -> &Self::Target {272 &self.0273 }274}275276impl<T: Config> WithRecorder<T> for RefungibleHandle<T> {277 fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {278 self.0.recorder()279 }280 fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {281 self.0.into_recorder()282 }283}284285impl<T: Config> Pallet<T> {286 /// Get number of RFT tokens in collection287 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {288 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)289 }290291 /// Check that RFT token exists292 ///293 /// - `token`: Token ID.294 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {295 <TotalSupply<T>>::contains_key((collection.id, token))296 }297298 pub fn set_scoped_token_property(299 collection_id: CollectionId,300 token_id: TokenId,301 scope: PropertyScope,302 property: Property,303 ) -> DispatchResult {304 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {305 properties.try_scoped_set(scope, property.key, property.value)306 })307 .map_err(<CommonError<T>>::from)?;308309 Ok(())310 }311312 pub fn set_scoped_token_properties(313 collection_id: CollectionId,314 token_id: TokenId,315 scope: PropertyScope,316 properties: impl Iterator<Item = Property>,317 ) -> DispatchResult {318 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {319 stored_properties.try_scoped_set_from_iter(scope, properties)320 })321 .map_err(<CommonError<T>>::from)?;322323 Ok(())324 }325}326327// unchecked calls skips any permission checks328impl<T: Config> Pallet<T> {329 /// Create RFT collection330 ///331 /// `init_collection` will take non-refundable deposit for collection creation.332 ///333 /// - `data`: Contains settings for collection limits and permissions.334 pub fn init_collection(335 owner: T::CrossAccountId,336 payer: T::CrossAccountId,337 data: CreateCollectionData<T::AccountId>,338 flags: CollectionFlags,339 ) -> Result<CollectionId, DispatchError> {340 <PalletCommon<T>>::init_collection(owner, payer, data, flags)341 }342343 /// Destroy RFT collection344 ///345 /// `destroy_collection` will throw error if collection contains any tokens.346 /// Only owner can destroy collection.347 pub fn destroy_collection(348 collection: RefungibleHandle<T>,349 sender: &T::CrossAccountId,350 ) -> DispatchResult {351 let id = collection.id;352353 if Self::collection_has_tokens(id) {354 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());355 }356357 // =========358359 PalletCommon::destroy_collection(collection.0, sender)?;360361 <TokensMinted<T>>::remove(id);362 <TokensBurnt<T>>::remove(id);363 let _ = <TotalSupply<T>>::clear_prefix((id,), u32::MAX, None);364 let _ = <Balance<T>>::clear_prefix((id,), u32::MAX, None);365 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);366 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);367 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);368 Ok(())369 }370371 fn collection_has_tokens(collection_id: CollectionId) -> bool {372 <TotalSupply<T>>::iter_prefix((collection_id,))373 .next()374 .is_some()375 }376377 pub fn burn_token_unchecked(378 collection: &RefungibleHandle<T>,379 owner: &T::CrossAccountId,380 token_id: TokenId,381 ) -> DispatchResult {382 let burnt = <TokensBurnt<T>>::get(collection.id)383 .checked_add(1)384 .ok_or(ArithmeticError::Overflow)?;385386 <TokensBurnt<T>>::insert(collection.id, burnt);387 <TokenProperties<T>>::remove((collection.id, token_id));388 <TotalSupply<T>>::remove((collection.id, token_id));389 let _ = <Balance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);390 let _ = <Allowance<T>>::clear_prefix((collection.id, token_id), u32::MAX, None);391 <PalletEvm<T>>::deposit_log(392 ERC721Events::Transfer {393 from: *owner.as_eth(),394 to: H160::default(),395 token_id: token_id.into(),396 }397 .to_log(collection_id_to_address(collection.id)),398 );399 Ok(())400 }401402 /// Burn RFT token pieces403 ///404 /// `burn` will decrease total amount of token pieces and amount owned by sender.405 /// `burn` can be called even if there are multiple owners of the RFT token.406 /// If sender wouldn't have any pieces left after `burn` than she will stop being407 /// one of the owners of the token. If there is no account that owns any pieces of408 /// the token than token will be burned too.409 ///410 /// - `amount`: Amount of token pieces to burn.411 /// - `token`: Token who's pieces should be burned412 /// - `collection`: Collection that contains the token413 pub fn burn(414 collection: &RefungibleHandle<T>,415 owner: &T::CrossAccountId,416 token: TokenId,417 amount: u128,418 ) -> DispatchResult {419 if <Balance<T>>::get((collection.id, token, owner)) == 0 {420 return Err(<CommonError<T>>::TokenValueTooLow.into());421 }422423 let total_supply = <TotalSupply<T>>::get((collection.id, token))424 .checked_sub(amount)425 .ok_or(<CommonError<T>>::TokenValueTooLow)?;426427 // This was probally last owner of this token?428 if total_supply == 0 {429 // Ensure user actually owns this amount430 ensure!(431 <Balance<T>>::get((collection.id, token, owner)) == amount,432 <CommonError<T>>::TokenValueTooLow433 );434 let account_balance = <AccountBalance<T>>::get((collection.id, owner))435 .checked_sub(1)436 // Should not occur437 .ok_or(ArithmeticError::Underflow)?;438439 // =========440441 <Owned<T>>::remove((collection.id, owner, token));442 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);443 <AccountBalance<T>>::insert((collection.id, owner), account_balance);444 Self::burn_token_unchecked(collection, owner, token)?;445 <PalletEvm<T>>::deposit_log(446 ERC20Events::Transfer {447 from: *owner.as_eth(),448 to: H160::default(),449 value: amount.into(),450 }451 .to_log(collection_id_to_address(collection.id)),452 );453 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(454 collection.id,455 token,456 owner.clone(),457 amount,458 ));459 return Ok(());460 }461462 let balance = <Balance<T>>::get((collection.id, token, owner))463 .checked_sub(amount)464 .ok_or(<CommonError<T>>::TokenValueTooLow)?;465 let account_balance = if balance == 0 {466 <AccountBalance<T>>::get((collection.id, owner))467 .checked_sub(1)468 // Should not occur469 .ok_or(ArithmeticError::Underflow)?470 } else {471 0472 };473474 // =========475476 if balance == 0 {477 <Owned<T>>::remove((collection.id, owner, token));478 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);479 <Balance<T>>::remove((collection.id, token, owner));480 <AccountBalance<T>>::insert((collection.id, owner), account_balance);481482 if let Ok(user) = Self::token_owner(collection.id, token) {483 <PalletEvm<T>>::deposit_log(484 ERC721Events::Transfer {485 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,486 to: *user.as_eth(),487 token_id: token.into(),488 }489 .to_log(collection_id_to_address(collection.id)),490 );491 }492 } else {493 <Balance<T>>::insert((collection.id, token, owner), balance);494 }495 <TotalSupply<T>>::insert((collection.id, token), total_supply);496497 <PalletEvm<T>>::deposit_log(498 ERC20Events::Transfer {499 from: *owner.as_eth(),500 to: H160::default(),501 value: amount.into(),502 }503 .to_log(T::EvmTokenAddressMapping::token_to_address(504 collection.id,505 token,506 )),507 );508 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(509 collection.id,510 token,511 owner.clone(),512 amount,513 ));514 Ok(())515 }516517 /// A batch operation to add, edit or remove properties for a token.518 /// It sets or removes a token's properties according to519 /// `properties_updates` contents:520 /// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`521 /// * removes a property under the <key> if the value is `None` `(<key>, None)`.522 ///523 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.524 ///525 /// All affected properties should have `mutable` permission526 /// to be **deleted** or to be **set more than once**,527 /// and the sender should have permission to edit those properties.528 ///529 /// This function fires an event for each property change.530 /// In case of an error, all the changes (including the events) will be reverted531 /// since the function is transactional.532 #[transactional]533 fn modify_token_properties(534 collection: &RefungibleHandle<T>,535 sender: &T::CrossAccountId,536 token_id: TokenId,537 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,538 mode: SetPropertyMode,539 nesting_budget: &dyn Budget,540 ) -> DispatchResult {541 let mut is_token_owner =542 pallet_common::LazyValue::new(|| -> Result<bool, DispatchError> {543 if let SetPropertyMode::NewToken {544 mint_target_is_sender,545 } = mode546 {547 return Ok(mint_target_is_sender);548 }549550 let balance = collection.balance(sender.clone(), token_id);551 let total_pieces: u128 =552 Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);553 if balance != total_pieces {554 return Ok(false);555 }556557 let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(558 sender.clone(),559 collection.id,560 token_id,561 None,562 nesting_budget,563 )?;564565 Ok(is_bundle_owner)566 });567568 let mut is_token_exist =569 pallet_common::LazyValue::new(|| Self::token_exists(collection, token_id));570571 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));572573 <PalletCommon<T>>::modify_token_properties(574 collection,575 sender,576 token_id,577 &mut is_token_exist,578 properties_updates,579 stored_properties,580 &mut is_token_owner,581 |properties| <TokenProperties<T>>::set((collection.id, token_id), properties),582 erc::ERC721TokenEvent::TokenChanged {583 token_id: token_id.into(),584 }585 .to_log(T::ContractAddress::get()),586 )587 }588589 pub fn next_token_id(collection: &RefungibleHandle<T>) -> Result<TokenId, DispatchError> {590 let next_token_id = <TokensMinted<T>>::get(collection.id)591 .checked_add(1)592 .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;593594 ensure!(595 collection.limits.token_limit() >= next_token_id,596 <CommonError<T>>::CollectionTokenLimitExceeded597 );598599 Ok(TokenId(next_token_id))600 }601602 pub fn set_token_properties(603 collection: &RefungibleHandle<T>,604 sender: &T::CrossAccountId,605 token_id: TokenId,606 properties: impl Iterator<Item = Property>,607 mode: SetPropertyMode,608 nesting_budget: &dyn Budget,609 ) -> DispatchResult {610 Self::modify_token_properties(611 collection,612 sender,613 token_id,614 properties.map(|p| (p.key, Some(p.value))),615 mode,616 nesting_budget,617 )618 }619620 pub fn set_token_property(621 collection: &RefungibleHandle<T>,622 sender: &T::CrossAccountId,623 token_id: TokenId,624 property: Property,625 nesting_budget: &dyn Budget,626 ) -> DispatchResult {627 Self::set_token_properties(628 collection,629 sender,630 token_id,631 [property].into_iter(),632 SetPropertyMode::ExistingToken,633 nesting_budget,634 )635 }636637 pub fn delete_token_properties(638 collection: &RefungibleHandle<T>,639 sender: &T::CrossAccountId,640 token_id: TokenId,641 property_keys: impl Iterator<Item = PropertyKey>,642 nesting_budget: &dyn Budget,643 ) -> DispatchResult {644 Self::modify_token_properties(645 collection,646 sender,647 token_id,648 property_keys.into_iter().map(|key| (key, None)),649 SetPropertyMode::ExistingToken,650 nesting_budget,651 )652 }653654 pub fn delete_token_property(655 collection: &RefungibleHandle<T>,656 sender: &T::CrossAccountId,657 token_id: TokenId,658 property_key: PropertyKey,659 nesting_budget: &dyn Budget,660 ) -> DispatchResult {661 Self::delete_token_properties(662 collection,663 sender,664 token_id,665 [property_key].into_iter(),666 nesting_budget,667 )668 }669670 /// Transfer RFT token pieces from one account to another.671 ///672 /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.673 ///674 /// - `from`: Owner of token pieces to transfer.675 /// - `to`: Recepient of transfered token pieces.676 /// - `amount`: Amount of token pieces to transfer.677 /// - `token`: Token whos pieces should be transfered678 /// - `collection`: Collection that contains the token679 pub fn transfer(680 collection: &RefungibleHandle<T>,681 from: &T::CrossAccountId,682 to: &T::CrossAccountId,683 token: TokenId,684 amount: u128,685 nesting_budget: &dyn Budget,686 ) -> DispatchResult {687 ensure!(688 collection.limits.transfers_enabled(),689 <CommonError<T>>::TransferNotAllowed690 );691692 if collection.permissions.access() == AccessMode::AllowList {693 collection.check_allowlist(from)?;694 collection.check_allowlist(to)?;695 }696 <PalletCommon<T>>::ensure_correct_receiver(to)?;697698 let initial_balance_from = <Balance<T>>::get((collection.id, token, from));699700 if initial_balance_from == 0 {701 return Err(<CommonError<T>>::TokenValueTooLow.into());702 }703704 let updated_balance_from = initial_balance_from705 .checked_sub(amount)706 .ok_or(<CommonError<T>>::TokenValueTooLow)?;707 let mut create_target = false;708 let from_to_differ = from != to;709 let updated_balance_to = if from != to && amount != 0 {710 let old_balance = <Balance<T>>::get((collection.id, token, to));711 if old_balance == 0 {712 create_target = true;713 }714 Some(715 old_balance716 .checked_add(amount)717 .ok_or(ArithmeticError::Overflow)?,718 )719 } else {720 None721 };722723 let account_balance_from = if updated_balance_from == 0 {724 Some(725 <AccountBalance<T>>::get((collection.id, from))726 .checked_sub(1)727 // Should not occur728 .ok_or(ArithmeticError::Underflow)?,729 )730 } else {731 None732 };733 // Account data is created in token, AccountBalance should be increased734 // But only if from != to as we shouldn't check overflow in this case735 let account_balance_to = if create_target && from_to_differ {736 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))737 .checked_add(1)738 .ok_or(ArithmeticError::Overflow)?;739 ensure!(740 account_balance_to < collection.limits.account_token_ownership_limit(),741 <CommonError<T>>::AccountTokenLimitExceeded,742 );743744 Some(account_balance_to)745 } else {746 None747 };748749 // =========750751 if let Some(updated_balance_to) = updated_balance_to {752 // from != to && amount != 0753754 <PalletStructure<T>>::nest_if_sent_to_token(755 from.clone(),756 to,757 collection.id,758 token,759 nesting_budget,760 )?;761762 if updated_balance_from == 0 {763 <Balance<T>>::remove((collection.id, token, from));764 <PalletStructure<T>>::unnest_if_nested(from, collection.id, token);765 } else {766 <Balance<T>>::insert((collection.id, token, from), updated_balance_from);767 }768 <Balance<T>>::insert((collection.id, token, to), updated_balance_to);769 if let Some(account_balance_from) = account_balance_from {770 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);771 <Owned<T>>::remove((collection.id, from, token));772 }773 if let Some(account_balance_to) = account_balance_to {774 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);775 <Owned<T>>::insert((collection.id, to, token), true);776 }777 }778779 <PalletEvm<T>>::deposit_log(780 ERC20Events::Transfer {781 from: *from.as_eth(),782 to: *to.as_eth(),783 value: amount.into(),784 }785 .to_log(T::EvmTokenAddressMapping::token_to_address(786 collection.id,787 token,788 )),789 );790791 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(792 collection.id,793 token,794 from.clone(),795 to.clone(),796 amount,797 ));798799 let total_supply = <TotalSupply<T>>::get((collection.id, token));800801 if amount == total_supply {802 // if token was fully owned by `from` and will be fully owned by `to` after transfer803 <PalletEvm<T>>::deposit_log(804 ERC721Events::Transfer {805 from: *from.as_eth(),806 to: *to.as_eth(),807 token_id: token.into(),808 }809 .to_log(collection_id_to_address(collection.id)),810 );811 } else if let Some(updated_balance_to) = updated_balance_to {812 // if `from` not equals `to`. This condition is needed to avoid sending event813 // when `from` fully owns token and sends part of token pieces to itself.814 if initial_balance_from == total_supply {815 // if token was fully owned by `from` and will be only partially owned by `to`816 // and `from` after transfer817 <PalletEvm<T>>::deposit_log(818 ERC721Events::Transfer {819 from: *from.as_eth(),820 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,821 token_id: token.into(),822 }823 .to_log(collection_id_to_address(collection.id)),824 );825 } else if updated_balance_to == total_supply {826 // if token was partially owned by `from` and will be fully owned by `to` after transfer827 <PalletEvm<T>>::deposit_log(828 ERC721Events::Transfer {829 from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,830 to: *to.as_eth(),831 token_id: token.into(),832 }833 .to_log(collection_id_to_address(collection.id)),834 );835 }836 }837838 Ok(())839 }840841 /// Batched operation to create multiple RFT tokens.842 ///843 /// Same as `create_item` but creates multiple tokens.844 ///845 /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.846 pub fn create_multiple_items(847 collection: &RefungibleHandle<T>,848 sender: &T::CrossAccountId,849 data: Vec<CreateItemData<T>>,850 nesting_budget: &dyn Budget,851 ) -> DispatchResult {852 if !collection.is_owner_or_admin(sender) {853 ensure!(854 collection.permissions.mint_mode(),855 <CommonError<T>>::PublicMintingNotAllowed856 );857 collection.check_allowlist(sender)?;858859 for item in data.iter() {860 for user in item.users.keys() {861 collection.check_allowlist(user)?;862 }863 }864 }865866 for item in data.iter() {867 for (owner, _) in item.users.iter() {868 <PalletCommon<T>>::ensure_correct_receiver(owner)?;869 }870 }871872 // Total pieces per tokens873 let totals = data874 .iter()875 .map(|data| {876 Ok(data877 .users878 .iter()879 .map(|u| u.1)880 .try_fold(0u128, |acc, v| acc.checked_add(*v))881 .ok_or(ArithmeticError::Overflow)?)882 })883 .collect::<Result<Vec<_>, DispatchError>>()?;884 for total in &totals {885 ensure!(886 *total <= MAX_REFUNGIBLE_PIECES,887 <Error<T>>::WrongRefungiblePieces888 );889 }890891 let first_token_id = <TokensMinted<T>>::get(collection.id);892 let tokens_minted = first_token_id893 .checked_add(data.len() as u32)894 .ok_or(ArithmeticError::Overflow)?;895 ensure!(896 tokens_minted < collection.limits.token_limit(),897 <CommonError<T>>::CollectionTokenLimitExceeded898 );899900 let mut balances = BTreeMap::new();901 for data in &data {902 for owner in data.users.keys() {903 let balance = balances904 .entry(owner)905 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));906 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;907908 ensure!(909 *balance <= collection.limits.account_token_ownership_limit(),910 <CommonError<T>>::AccountTokenLimitExceeded,911 );912 }913 }914915 for (i, token) in data.iter().enumerate() {916 let token_id = TokenId(first_token_id + i as u32 + 1);917 for (to, _) in token.users.iter() {918 <PalletStructure<T>>::check_nesting(919 sender.clone(),920 to,921 collection.id,922 token_id,923 nesting_budget,924 )?;925 }926 }927928 // =========929930 with_transaction(|| {931 for (i, data) in data.iter().enumerate() {932 let token_id = first_token_id + i as u32 + 1;933 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);934935 let mut mint_target_is_sender = true;936 for (user, amount) in data.users.iter() {937 if *amount == 0 {938 continue;939 }940941 mint_target_is_sender = mint_target_is_sender && sender.conv_eq(user);942943 <Balance<T>>::insert((collection.id, token_id, &user), amount);944 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);945 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(946 user,947 collection.id,948 TokenId(token_id),949 );950 }951952 if let Err(e) = Self::set_token_properties(953 collection,954 sender,955 TokenId(token_id),956 data.properties.clone().into_iter(),957 SetPropertyMode::NewToken {958 mint_target_is_sender,959 },960 nesting_budget,961 ) {962 return TransactionOutcome::Rollback(Err(e));963 }964 }965 TransactionOutcome::Commit(Ok(()))966 })?;967968 <TokensMinted<T>>::insert(collection.id, tokens_minted);969970 for (account, balance) in balances {971 <AccountBalance<T>>::insert((collection.id, account), balance);972 }973974 for (i, token) in data.into_iter().enumerate() {975 let token_id = first_token_id + i as u32 + 1;976977 let receivers = token978 .users979 .into_iter()980 .filter(|(_, amount)| *amount > 0)981 .collect::<Vec<_>>();982983 if let [(user, _)] = receivers.as_slice() {984 // if there is exactly one receiver985 <PalletEvm<T>>::deposit_log(986 ERC721Events::Transfer {987 from: H160::default(),988 to: *user.as_eth(),989 token_id: token_id.into(),990 }991 .to_log(collection_id_to_address(collection.id)),992 );993 } else if let [_, ..] = receivers.as_slice() {994 // if there is more than one receiver995 <PalletEvm<T>>::deposit_log(996 ERC721Events::Transfer {997 from: H160::default(),998 to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,999 token_id: token_id.into(),1000 }1001 .to_log(collection_id_to_address(collection.id)),1002 );1003 }10041005 for (user, amount) in receivers.into_iter() {1006 <PalletEvm<T>>::deposit_log(1007 ERC20Events::Transfer {1008 from: H160::default(),1009 to: *user.as_eth(),1010 value: amount.into(),1011 }1012 .to_log(T::EvmTokenAddressMapping::token_to_address(1013 collection.id,1014 TokenId(token_id),1015 )),1016 );1017 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1018 collection.id,1019 TokenId(token_id),1020 user,1021 amount,1022 ));1023 }1024 }1025 Ok(())1026 }10271028 pub fn set_allowance_unchecked(1029 collection: &RefungibleHandle<T>,1030 sender: &T::CrossAccountId,1031 spender: &T::CrossAccountId,1032 token: TokenId,1033 amount: u128,1034 ) {1035 if amount == 0 {1036 <Allowance<T>>::remove((collection.id, token, sender, spender));1037 } else {1038 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);1039 }10401041 <PalletEvm<T>>::deposit_log(1042 ERC20Events::Approval {1043 owner: *sender.as_eth(),1044 spender: *spender.as_eth(),1045 value: amount.into(),1046 }1047 .to_log(T::EvmTokenAddressMapping::token_to_address(1048 collection.id,1049 token,1050 )),1051 );1052 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1053 collection.id,1054 token,1055 sender.clone(),1056 spender.clone(),1057 amount,1058 ))1059 }10601061 /// Set allowance for the spender to `transfer` or `burn` sender's token pieces.1062 ///1063 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.1064 pub fn set_allowance(1065 collection: &RefungibleHandle<T>,1066 sender: &T::CrossAccountId,1067 spender: &T::CrossAccountId,1068 token: TokenId,1069 amount: u128,1070 ) -> DispatchResult {1071 if collection.permissions.access() == AccessMode::AllowList {1072 collection.check_allowlist(sender)?;1073 collection.check_allowlist(spender)?;1074 }10751076 <PalletCommon<T>>::ensure_correct_receiver(spender)?;10771078 if <Balance<T>>::get((collection.id, token, sender)) < amount {1079 ensure!(1080 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),1081 <CommonError<T>>::CantApproveMoreThanOwned1082 );1083 }10841085 // =========10861087 Self::set_allowance_unchecked(collection, sender, spender, token, amount);1088 Ok(())1089 }10901091 /// Set allowance to spend from sender's eth mirror1092 ///1093 /// - `from`: Address of sender's eth mirror.1094 /// - `to`: Adress of spender.1095 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.1096 pub fn set_allowance_from(1097 collection: &RefungibleHandle<T>,1098 sender: &T::CrossAccountId,1099 from: &T::CrossAccountId,1100 to: &T::CrossAccountId,1101 token_id: TokenId,1102 amount: u128,1103 ) -> DispatchResult {1104 if collection.permissions.access() == AccessMode::AllowList {1105 collection.check_allowlist(sender)?;1106 collection.check_allowlist(from)?;1107 collection.check_allowlist(to)?;1108 }11091110 <PalletCommon<T>>::ensure_correct_receiver(to)?;11111112 ensure!(1113 sender.conv_eq(from),1114 <CommonError<T>>::AddressIsNotEthMirror1115 );11161117 if <Balance<T>>::get((collection.id, token_id, from)) < amount {1118 ensure!(1119 collection.limits.owner_can_transfer()1120 && (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from))1121 && Self::token_exists(collection, token_id),1122 <CommonError<T>>::CantApproveMoreThanOwned1123 );1124 }11251126 // =========11271128 Self::set_allowance_unchecked(collection, from, to, token_id, amount);1129 Ok(())1130 }11311132 /// Returns allowance, which should be set after transaction1133 fn check_allowed(1134 collection: &RefungibleHandle<T>,1135 spender: &T::CrossAccountId,1136 from: &T::CrossAccountId,1137 token: TokenId,1138 amount: u128,1139 nesting_budget: &dyn Budget,1140 ) -> Result<Option<u128>, DispatchError> {1141 if spender.conv_eq(from) {1142 return Ok(None);1143 }1144 if collection.permissions.access() == AccessMode::AllowList {1145 // `from`, `to` checked in [`transfer`]1146 collection.check_allowlist(spender)?;1147 }11481149 if collection.ignores_token_restrictions(spender) {1150 return Ok(Self::compute_allowance_decrease(1151 collection, token, from, spender, amount,1152 ));1153 }11541155 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1156 // TODO: should collection owner be allowed to perform this transfer?1157 ensure!(1158 <PalletStructure<T>>::check_indirectly_owned(1159 spender.clone(),1160 source.0,1161 source.1,1162 None,1163 nesting_budget1164 )?,1165 <CommonError<T>>::ApprovedValueTooLow,1166 );1167 return Ok(None);1168 }11691170 let allowance = Self::compute_allowance_decrease(collection, token, from, spender, amount);1171 if allowance.is_some() {1172 return Ok(allowance);1173 }11741175 // Allowance (if any) would be reduced if spender is also wallet operator1176 if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1177 return Ok(allowance);1178 }11791180 Err(<CommonError<T>>::ApprovedValueTooLow.into())1181 }11821183 /// Returns `Some(amount)` if the `spender` have allowance to spend this amount.1184 /// Otherwise, it returns `None`.1185 fn compute_allowance_decrease(1186 collection: &RefungibleHandle<T>,1187 token: TokenId,1188 from: &T::CrossAccountId,1189 spender: &T::CrossAccountId,1190 amount: u128,1191 ) -> Option<u128> {1192 <Allowance<T>>::get((collection.id, token, from, spender)).checked_sub(amount)1193 }11941195 /// Transfer RFT token pieces from one account to another.1196 ///1197 /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1198 /// The owner should set allowance for the spender to transfer pieces.1199 ///1200 /// [`transfer`]: struct.Pallet.html#method.transfer1201 pub fn transfer_from(1202 collection: &RefungibleHandle<T>,1203 spender: &T::CrossAccountId,1204 from: &T::CrossAccountId,1205 to: &T::CrossAccountId,1206 token: TokenId,1207 amount: u128,1208 nesting_budget: &dyn Budget,1209 ) -> DispatchResult {1210 let allowance =1211 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;12121213 // =========12141215 Self::transfer(collection, from, to, token, amount, nesting_budget)?;1216 if let Some(allowance) = allowance {1217 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1218 }1219 Ok(())1220 }12211222 /// Burn RFT token pieces from the account.1223 ///1224 /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1225 /// set allowance for the spender to burn pieces1226 ///1227 /// [`burn`]: struct.Pallet.html#method.burn1228 pub fn burn_from(1229 collection: &RefungibleHandle<T>,1230 spender: &T::CrossAccountId,1231 from: &T::CrossAccountId,1232 token: TokenId,1233 amount: u128,1234 nesting_budget: &dyn Budget,1235 ) -> DispatchResult {1236 let allowance =1237 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;12381239 // =========12401241 Self::burn(collection, from, token, amount)?;1242 if let Some(allowance) = allowance {1243 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1244 }1245 Ok(())1246 }12471248 /// Create RFT token.1249 ///1250 /// The sender should be the owner/admin of the collection or collection should be configured1251 /// to allow public minting.1252 ///1253 /// - `data`: Contains list of users who will become the owners of the token pieces and amount1254 /// of token pieces they will receive.1255 pub fn create_item(1256 collection: &RefungibleHandle<T>,1257 sender: &T::CrossAccountId,1258 data: CreateItemData<T>,1259 nesting_budget: &dyn Budget,1260 ) -> DispatchResult {1261 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1262 }12631264 /// Repartition RFT token.1265 ///1266 /// `repartition` will set token balance of the sender and total amount of token pieces.1267 /// Sender should own all of the token pieces. `repartition' could be done even if some1268 /// token pieces were burned before.1269 ///1270 /// - `amount`: Total amount of token pieces that the token will have after `repartition`.1271 pub fn repartition(1272 collection: &RefungibleHandle<T>,1273 owner: &T::CrossAccountId,1274 token: TokenId,1275 amount: u128,1276 ) -> DispatchResult {1277 ensure!(1278 amount <= MAX_REFUNGIBLE_PIECES,1279 <Error<T>>::WrongRefungiblePieces1280 );1281 ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1282 // Ensure user owns all pieces1283 let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1284 let balance = <Balance<T>>::get((collection.id, token, owner));1285 ensure!(1286 total_pieces == balance,1287 <Error<T>>::RepartitionWhileNotOwningAllPieces1288 );12891290 <Balance<T>>::insert((collection.id, token, owner), amount);1291 <TotalSupply<T>>::insert((collection.id, token), amount);12921293 match total_pieces.cmp(&amount) {1294 Ordering::Less => {1295 let mint_amount = amount - total_pieces;1296 <PalletEvm<T>>::deposit_log(1297 ERC20Events::Transfer {1298 from: H160::default(),1299 to: *owner.as_eth(),1300 value: mint_amount.into(),1301 }1302 .to_log(T::EvmTokenAddressMapping::token_to_address(1303 collection.id,1304 token,1305 )),1306 );1307 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1308 collection.id,1309 token,1310 owner.clone(),1311 mint_amount,1312 ));1313 }1314 Ordering::Greater => {1315 let burn_amount = total_pieces - amount;1316 <PalletEvm<T>>::deposit_log(1317 ERC20Events::Transfer {1318 from: *owner.as_eth(),1319 to: H160::default(),1320 value: burn_amount.into(),1321 }1322 .to_log(T::EvmTokenAddressMapping::token_to_address(1323 collection.id,1324 token,1325 )),1326 );1327 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(1328 collection.id,1329 token,1330 owner.clone(),1331 burn_amount,1332 ));1333 }1334 Ordering::Equal => {}1335 }13361337 Ok(())1338 }13391340 fn token_owner(1341 collection_id: CollectionId,1342 token_id: TokenId,1343 ) -> Result<T::CrossAccountId, TokenOwnerError> {1344 let mut owner = None;1345 let mut count = 0;1346 for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1347 count += 1;1348 if count > 1 {1349 return Err(TokenOwnerError::MultipleOwners);1350 }1351 owner = Some(key);1352 }1353 owner.ok_or(TokenOwnerError::NotFound)1354 }13551356 fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1357 <TotalSupply<T>>::try_get((collection_id, token_id)).ok()1358 }13591360 pub fn set_collection_properties(1361 collection: &RefungibleHandle<T>,1362 sender: &T::CrossAccountId,1363 properties: Vec<Property>,1364 ) -> DispatchResult {1365 <PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())1366 }13671368 pub fn delete_collection_properties(1369 collection: &RefungibleHandle<T>,1370 sender: &T::CrossAccountId,1371 property_keys: Vec<PropertyKey>,1372 ) -> DispatchResult {1373 <PalletCommon<T>>::delete_collection_properties(1374 collection,1375 sender,1376 property_keys.into_iter(),1377 )1378 }13791380 pub fn set_token_property_permissions(1381 collection: &RefungibleHandle<T>,1382 sender: &T::CrossAccountId,1383 property_permissions: Vec<PropertyKeyPermission>,1384 ) -> DispatchResult {1385 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1386 }13871388 pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {1389 <PalletCommon<T>>::property_permissions(collection_id)1390 }13911392 pub fn set_scoped_token_property_permissions(1393 collection: &RefungibleHandle<T>,1394 sender: &T::CrossAccountId,1395 scope: PropertyScope,1396 property_permissions: Vec<PropertyKeyPermission>,1397 ) -> DispatchResult {1398 <PalletCommon<T>>::set_scoped_token_property_permissions(1399 collection,1400 sender,1401 scope,1402 property_permissions,1403 )1404 }14051406 /// Returns 10 token in no particular order.1407 ///1408 /// There is no direct way to get token holders in ascending order,1409 /// since `iter_prefix` returns values in no particular order.1410 /// Therefore, getting the 10 largest holders with a large value of holders1411 /// can lead to impact memory allocation + sorting with `n * log (n)`.1412 pub fn token_owners(1413 collection_id: CollectionId,1414 token: TokenId,1415 ) -> Option<Vec<T::CrossAccountId>> {1416 let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1417 .map(|(owner, _amount)| owner)1418 .take(10)1419 .collect();14201421 if res.is_empty() {1422 None1423 } else {1424 Some(res)1425 }1426 }14271428 /// Sets or unsets the approval of a given operator.1429 ///1430 /// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1431 /// - `owner`: Token owner1432 /// - `operator`: Operator1433 /// - `approve`: Should operator status be granted or revoked?1434 pub fn set_allowance_for_all(1435 collection: &RefungibleHandle<T>,1436 owner: &T::CrossAccountId,1437 spender: &T::CrossAccountId,1438 approve: bool,1439 ) -> DispatchResult {1440 <PalletCommon<T>>::set_allowance_for_all(1441 collection,1442 owner,1443 spender,1444 approve,1445 || <CollectionAllowance<T>>::insert((collection.id, owner, spender), approve),1446 ERC721Events::ApprovalForAll {1447 owner: *owner.as_eth(),1448 operator: *spender.as_eth(),1449 approved: approve,1450 }1451 .to_log(collection_id_to_address(collection.id)),1452 )1453 }14541455 /// Tells whether the given `owner` approves the `operator`.1456 pub fn allowance_for_all(1457 collection: &RefungibleHandle<T>,1458 owner: &T::CrossAccountId,1459 spender: &T::CrossAccountId,1460 ) -> bool {1461 <CollectionAllowance<T>>::get((collection.id, owner, spender))1462 }14631464 pub fn repair_item(collection: &RefungibleHandle<T>, token: TokenId) -> DispatchResult {1465 <TokenProperties<T>>::mutate((collection.id, token), |properties| {1466 properties.recompute_consumed_space();1467 });14681469 Ok(())1470 }1471}runtime/common/ethereum/sponsoring.rsdiffbeforeafterboth--- a/runtime/common/ethereum/sponsoring.rs
+++ b/runtime/common/ethereum/sponsoring.rs
@@ -22,7 +22,7 @@
use pallet_evm::account::CrossAccountId;
use pallet_evm_transaction_payment::CallContext;
use pallet_nonfungible::{
- Config as NonfungibleConfig,
+ Config as NonfungibleConfig, Pallet as NonfungiblePallet, NonfungibleHandle,
erc::{
UniqueNFTCall, ERC721UniqueExtensionsCall, ERC721UniqueMintableCall, ERC721Call,
TokenPropertiesCall,
@@ -56,6 +56,8 @@
pub struct UniqueEthSponsorshipHandler<T: UniqueConfig>(PhantomData<*const T>);
impl<T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig>
SponsorshipHandler<T::CrossAccountId, CallContext> for UniqueEthSponsorshipHandler<T>
+where
+ T::AccountId: From<[u8; 32]>,
{
fn get_sponsor(
who: &T::CrossAccountId,
@@ -67,29 +69,71 @@
let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;
Some(T::CrossAccountId::from_sub(match &collection.mode {
CollectionMode::NFT => {
+ let collection = NonfungibleHandle::cast(collection);
let call = <UniqueNFTCall<T>>::parse(method_id, &mut reader).ok()??;
match call {
- UniqueNFTCall::TokenProperties(TokenPropertiesCall::SetProperty {
- token_id,
- key,
- value,
- ..
- }) => {
- let token_id: TokenId = token_id.try_into().ok()?;
- withdraw_set_token_property::<T>(
- &collection,
- who,
- &token_id,
- key.len() + value.len(),
- )
- .map(|()| sponsor)
- }
- UniqueNFTCall::ERC721UniqueExtensions(
- ERC721UniqueExtensionsCall::Transfer { token_id, .. },
- ) => {
- let token_id: TokenId = token_id.try_into().ok()?;
- withdraw_transfer::<T>(&collection, who, &token_id).map(|()| sponsor)
- }
+ UniqueNFTCall::TokenProperties(call) => match call {
+ TokenPropertiesCall::SetProperty {
+ token_id,
+ key,
+ value,
+ ..
+ } => {
+ let token_id: TokenId = token_id.try_into().ok()?;
+ withdraw_set_existing_token_property::<T>(
+ &collection,
+ who,
+ &token_id,
+ key.len() + value.len(),
+ )
+ .map(|()| sponsor)
+ }
+ TokenPropertiesCall::SetProperties {
+ token_id,
+ properties,
+ ..
+ } => {
+ let token_id: TokenId = token_id.try_into().ok()?;
+ let data_size = properties
+ .into_iter()
+ .map(|p| p.key().len() + p.value().len())
+ .sum();
+
+ withdraw_set_existing_token_property::<T>(
+ &collection,
+ who,
+ &token_id,
+ data_size,
+ )
+ .map(|()| sponsor)
+ }
+ _ => None,
+ },
+ UniqueNFTCall::ERC721UniqueExtensions(call) => match call {
+ ERC721UniqueExtensionsCall::Transfer { token_id, .. } => {
+ let token_id: TokenId = token_id.try_into().ok()?;
+ withdraw_transfer::<T>(&collection, who, &token_id)
+ .map(|()| sponsor)
+ }
+ ERC721UniqueExtensionsCall::MintCross { properties, .. } => {
+ withdraw_create_item::<T>(
+ &collection,
+ who,
+ &CreateItemData::NFT(CreateNftData::default()),
+ )?;
+
+ let token_id =
+ <NonfungiblePallet<T>>::next_token_id(&collection).ok()?;
+ let data_size: usize = properties
+ .into_iter()
+ .map(|p| p.key().len() + p.value().len())
+ .sum();
+
+ withdraw_set_token_property::<T>(&collection, &token_id, data_size)
+ .map(|()| sponsor)
+ }
+ _ => None,
+ },
UniqueNFTCall::ERC721UniqueMintable(
ERC721UniqueMintableCall::Mint { .. }
| ERC721UniqueMintableCall::MintCheckId { .. }
runtime/common/ethereum/sponsoring/refungible.rsdiffbeforeafterboth--- a/runtime/common/ethereum/sponsoring/refungible.rs
+++ b/runtime/common/ethereum/sponsoring/refungible.rs
@@ -94,7 +94,12 @@
..
} => {
let token_id = TokenId::try_from(token_id).ok()?;
- withdraw_set_token_property::<T>(&collection, who, &token_id, key.len() + value.len())
+ withdraw_set_existing_token_property::<T>(
+ &collection,
+ who,
+ &token_id,
+ key.len() + value.len(),
+ )
}
}
}
runtime/common/sponsoring.rsdiffbeforeafterboth--- a/runtime/common/sponsoring.rs
+++ b/runtime/common/sponsoring.rs
@@ -39,7 +39,7 @@
impl<T> Config for T where T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig {}
// TODO: permission check?
-pub fn withdraw_set_token_property<T: Config>(
+pub fn withdraw_set_existing_token_property<T: Config>(
collection: &CollectionHandle<T>,
who: &T::CrossAccountId,
item_id: &TokenId,
@@ -64,6 +64,17 @@
}
}
+ withdraw_set_token_property(collection, item_id, data_size)
+}
+
+pub fn withdraw_set_token_property<T: Config>(
+ collection: &CollectionHandle<T>,
+ item_id: &TokenId,
+ data_size: usize,
+) -> Option<()> {
+ if data_size == 0 {
+ return Some(());
+ }
if data_size > collection.limits.sponsored_data_size() as usize {
return None;
}
@@ -173,7 +184,6 @@
return None;
}
}
-
CreateItemBasket::<T>::insert((collection.id, who.as_sub()), block_number);
Some(())
@@ -237,7 +247,7 @@
..
} => {
let (sponsor, collection) = load::<T>(*collection_id)?;
- withdraw_set_token_property(
+ withdraw_set_existing_token_property(
&collection,
&T::CrossAccountId::from_sub(who.clone()),
token_id,
runtime/tests/Cargo.tomldiffbeforeafterboth--- a/runtime/tests/Cargo.toml
+++ b/runtime/tests/Cargo.toml
@@ -5,6 +5,7 @@
[features]
default = ['refungible']
+tests = ['pallet-common/tests']
refungible = []
runtime/tests/src/tests.rsdiffbeforeafterboth--- a/runtime/tests/src/tests.rs
+++ b/runtime/tests/src/tests.rs
@@ -1737,6 +1737,11 @@
let collection_id = create_test_collection(&CollectionMode::NFT, CollectionId(1));
let origin1 = RuntimeOrigin::signed(1);
+ assert_ok!(Unique::add_collection_admin(
+ origin1.clone(),
+ collection_id,
+ account(1)
+ ));
let data = default_nft_data();
create_test_item(collection_id, &data.into());
@@ -2610,3 +2615,67 @@
));
});
}
+
+mod check_token_permissions {
+ use super::*;
+ use frame_support::once_cell::sync::Lazy;
+ use pallet_common::LazyValue;
+ use sp_runtime::DispatchError;
+
+ fn test<FTE: FnOnce() -> bool>(
+ i: usize,
+ test_case: &pallet_common::tests::TestCase,
+ check_token_existence: &mut LazyValue<bool, FTE>,
+ ) {
+ let collection_admin = test_case.collection_admin;
+ let mut is_collection_admin = LazyValue::new(|| test_case.is_collection_admin);
+ let token_owner = test_case.token_owner;
+ let mut is_token_owner = LazyValue::new(|| Ok(test_case.is_token_owner));
+ let is_no_permission = test_case.no_permission;
+
+ let result = pallet_common::tests::check_token_permissions::<Test, _, _, FTE>(
+ collection_admin,
+ token_owner,
+ &mut is_collection_admin,
+ &mut is_token_owner,
+ check_token_existence,
+ );
+
+ if is_no_permission {
+ assert!(
+ result.is_err(),
+ "{i}: {test_case:?}, token_exist: {}",
+ check_token_existence.value()
+ );
+ assert_err!(result, pallet_common::Error::<Test>::NoPermission,);
+ } else if check_token_existence.has_value() && !check_token_existence.value() {
+ assert!(
+ result.is_err(),
+ "{i}: {test_case:?}, token_exist: {}",
+ check_token_existence.value()
+ );
+ assert_err!(result, pallet_common::Error::<Test>::TokenNotFound,);
+ }
+ }
+
+ #[test]
+ fn no_permission_only() {
+ new_test_ext().execute_with(|| {
+ let mut check_token_existence = LazyValue::new(|| true);
+ for (i, row) in pallet_common::tests::table.iter().enumerate() {
+ test(i, row, &mut check_token_existence);
+ }
+ });
+ }
+
+ #[test]
+ fn no_permission_and_token_not_found() {
+ new_test_ext().execute_with(|| {
+ for (i, row) in pallet_common::tests::table.iter().enumerate() {
+ // This is inside the loop to keep track of whether the lambda was called
+ let mut check_token_existence = LazyValue::new(|| false);
+ test(i, row, &mut check_token_existence);
+ }
+ });
+ }
+}
tests/src/createMultipleItemsEx.test.tsdiffbeforeafterboth--- a/tests/src/createMultipleItemsEx.test.ts
+++ b/tests/src/createMultipleItemsEx.test.ts
@@ -195,7 +195,7 @@
description: 'descr',
tokenPrefix: 'COL',
tokenPropertyPermissions: [
- {key: 'k', permission: {tokenOwner: true, mutable: false, collectionAdmin: false}},
+ {key: 'k', permission: {tokenOwner: false, mutable: false, collectionAdmin: true}},
],
});
tests/src/eth/collectionSponsoring.test.tsdiffbeforeafterboth--- a/tests/src/eth/collectionSponsoring.test.ts
+++ b/tests/src/eth/collectionSponsoring.test.ts
@@ -17,6 +17,7 @@
import {IKeyringPair} from '@polkadot/types/types';
import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../util/index';
import {itEth, expect} from './util';
+import {CollectionLimitField, TokenPermissionField} from './util/playgrounds/types';
describe('evm nft collection sponsoring', () => {
let donor: IKeyringPair;
@@ -138,8 +139,7 @@
expect(sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsorEth, true));
// Create user with no balance:
- const user = helper.eth.createAccount();
- const userCross = helper.ethCrossAccount.fromAddress(user);
+ const user = helper.ethCrossAccount.createAccount();
const nextTokenId = await collectionEvm.methods.nextTokenId().call();
expect(nextTokenId).to.be.equal('1');
@@ -149,20 +149,29 @@
expect(oldPermissions.access).to.be.equal('Normal');
await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});
- await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner});
+ await collectionEvm.methods.addToCollectionAllowListCross(user).send({from: owner});
await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});
+ await collectionEvm.methods.setCollectionLimit({field: CollectionLimitField.SponsoredDataRateLimit, value: {status: true, value: 30}}).send();
const newPermissions = (await collectionSub.getData())!.raw.permissions;
expect(newPermissions.mintMode).to.be.true;
expect(newPermissions.access).to.be.equal('AllowList');
+ // Set token permissions
+ await collectionEvm.methods.setTokenPropertyPermissions([
+ ['key', [
+ [TokenPermissionField.TokenOwner, true],
+ ],
+ ],
+ ]).send({from: owner});
+
const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));
- const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+ const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));
// User can mint token without balance:
{
- const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user});
+ const result = await collectionEvm.methods.mintCross(user, [{key: 'key', value: Buffer.from('Value')}]).send({from: user.eth});
const event = helper.eth.normalizeEvents(result.events)
.find(event => event.event === 'Transfer');
@@ -171,22 +180,102 @@
event: 'Transfer',
args: {
from: '0x0000000000000000000000000000000000000000',
- to: user,
+ to: user.eth,
tokenId: '1',
},
});
+ // await collectionEvm.methods.setProperties(1, [{key: 'key', value: Buffer.from('Value1')}]).send({from: user.eth});
+
const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));
- const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+ const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));
- expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+ expect(await collectionEvm.methods.properties(nextTokenId, []).call())
+ .to.be.like([
+ [
+ 'key',
+ '0x' + Buffer.from('Value').toString('hex'),
+ ],
+ ]);
expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);
expect(userBalanceAfter).to.be.eq(userBalanceBefore);
expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;
}
}));
+ itEth('Can sponsor [set token properties] via access list', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const sponsorEth = await helper.eth.createAccountWithBalance(donor);
+ const sponsorCrossEth = helper.ethCrossAccount.fromAddress(sponsorEth);
+
+ const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Sponsor collection', '1', '1', '');
+ const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner, false);
+
+ // Set collection sponsor:
+ await collectionEvm.methods.setCollectionSponsorCross(sponsorCrossEth).send({from: owner});
+
+ // Sponsor can confirm sponsorship:
+ await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsorEth});
+
+ // Create user with no balance:
+ const user = helper.ethCrossAccount.createAccount();
+ const nextTokenId = await collectionEvm.methods.nextTokenId().call();
+ expect(nextTokenId).to.be.equal('1');
+
+ // Set collection permissions:
+ await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});
+ await collectionEvm.methods.addToCollectionAllowListCross(user).send({from: owner});
+ await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});
+ await collectionEvm.methods.setCollectionLimit({field: CollectionLimitField.SponsoredDataRateLimit, value: {status: true, value: 30}}).send();
+
+ // Set token permissions
+ await collectionEvm.methods.setTokenPropertyPermissions([
+ ['key', [
+ [TokenPermissionField.TokenOwner, true],
+ ],
+ ],
+ ]).send({from: owner});
+
+ const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+ const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));
+ const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));
+
+ // User can mint token without balance:
+ {
+ const result = await collectionEvm.methods.mintCross(user, []).send({from: user.eth});
+ const event = helper.eth.normalizeEvents(result.events)
+ .find(event => event.event === 'Transfer');
+
+ expect(event).to.be.deep.equal({
+ address: collectionAddress,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: user.eth,
+ tokenId: '1',
+ },
+ });
+
+ await collectionEvm.methods.setProperties(1, [{key: 'key', value: Buffer.from('Value')}]).send({from: user.eth});
+
+ const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+ const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));
+ const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));
+
+ expect(await collectionEvm.methods.properties(nextTokenId, []).call())
+ .to.be.like([
+ [
+ 'key',
+ '0x' + Buffer.from('Value').toString('hex'),
+ ],
+ ]);
+ expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);
+ expect(userBalanceAfter).to.be.eq(userBalanceBefore);
+ expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;
+ }
+ });
+
// TODO: Temprorary off. Need refactor
// itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => {
// const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
@@ -456,6 +545,15 @@
expect(newPermissions.mintMode).to.be.true;
expect(newPermissions.access).to.be.equal('AllowList');
+ // Set token permissions
+ await collectionEvm.methods.setTokenPropertyPermissions([
+ ['URI', [
+ [TokenPermissionField.TokenOwner, true],
+ [TokenPermissionField.CollectionAdmin, true],
+ ],
+ ],
+ ]).send({from: owner});
+
const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));
const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
@@ -623,6 +721,15 @@
await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner});
await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});
+ // Set token permissions
+ await collectionEvm.methods.setTokenPropertyPermissions([
+ ['URI', [
+ [TokenPermissionField.TokenOwner, true],
+ [TokenPermissionField.CollectionAdmin, true],
+ ],
+ ],
+ ]).send({from: owner});
+
const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
const sponsorBalanceBefore = await helper.balance.getSubstrate(sponsor.address);
const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
tests/src/eth/tokenProperties.test.tsdiffbeforeafterboth--- a/tests/src/eth/tokenProperties.test.ts
+++ b/tests/src/eth/tokenProperties.test.ts
@@ -553,6 +553,63 @@
]).call({from: owner})).to.be.rejectedWith('NoPermission');
}
}));
+
+ [
+ {mode: 'nft' as const, requiredPallets: []},
+ {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itEth.ifWithPallets(`[${testCase.mode}] Can't be multiple set/read for non-existent token`, testCase.requiredPallets, async({helper}) => {
+ const caller = await helper.eth.createAccountWithBalance(donor);
+
+ const properties = Array(5).fill(0).map((_, i) => { return {key: `key_${i}`, value: Buffer.from(`value_${i}`)}; });
+ const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.key, permission: {tokenOwner: true,
+ collectionAdmin: true,
+ mutable: true}}; });
+
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPrefix: 'ethp',
+ tokenPropertyPermissions: permissions,
+ }) as UniqueNFTCollection | UniqueRFTCollection;
+
+ await collection.addAdmin(alice, {Ethereum: caller});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = await helper.ethNativeContract.collection(address, testCase.mode, caller);
+
+ await expect(contract.methods.setProperties(1, properties).call({from: caller})).to.be.rejectedWith('TokenNotFound');
+ }));
+
+ [
+ {mode: 'nft' as const, requiredPallets: []},
+ {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itEth.ifWithPallets(`[${testCase.mode}] Can't be deleted for non-existent token`, testCase.requiredPallets, async({helper}) => {
+ const caller = await helper.eth.createAccountWithBalance(donor);
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPropertyPermissions: [{
+ key: 'testKey',
+ permission: {
+ mutable: true,
+ collectionAdmin: true,
+ },
+ },
+ {
+ key: 'testKey_1',
+ permission: {
+ mutable: true,
+ collectionAdmin: true,
+ },
+ }],
+ });
+
+
+ await collection.addAdmin(alice, {Ethereum: caller});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = await helper.ethNativeContract.collection(address, testCase.mode, caller);
+
+ await expect(contract.methods.deleteProperties(1, ['testKey', 'testKey_1']).call({from: caller})).to.be.rejectedWith('TokenNotFound');
+ }));
});
tests/src/getPropertiesRpc.test.tsdiffbeforeafterboth--- a/tests/src/getPropertiesRpc.test.ts
+++ b/tests/src/getPropertiesRpc.test.ts
@@ -120,3 +120,31 @@
expect(propPermissions).to.be.deep.equal(tokenPropPermissions);
});
});
+
+[
+ {mode: 'nft' as const},
+ {mode: 'rft' as const},
+].map(testCase =>
+ describe('negative properties', () => {
+ let alice: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (_, privateKey) => {
+ alice = await privateKey({url: import.meta.url});
+ });
+ });
+
+ itSub(`[${testCase.mode}] set token property for non-existent token`, async ({helper}) => {
+ const collection = await helper[testCase.mode].mintCollection(alice);
+ await collection.setTokenPropertyPermissions(alice, [{key: 'key', permission: {mutable: true, tokenOwner: true, collectionAdmin: true}}]);
+ await expect(collection.setTokenProperties(alice, 1, [{key: 'key', value: 'value'}])).to.be.rejectedWith('common.TokenNotFound');
+ expect(await collection.getTokenProperties(1, ['key'])).to.be.empty;
+ });
+
+ itSub(`[${testCase.mode}] delete token property for non-existent token`, async ({helper}) => {
+ const collection = await helper[testCase.mode].mintCollection(alice);
+ await collection.setTokenPropertyPermissions(alice, [{key: 'key', permission: {mutable: true, tokenOwner: true, collectionAdmin: true}}]);
+ await expect(collection.deleteTokenProperties(alice, 1, ['key'])).to.be.rejectedWith('common.TokenNotFound');
+ expect(await collection.getTokenProperties(1, ['key'])).to.be.empty;
+ });
+ }));
\ No newline at end of file
tests/src/nesting/tokenProperties.test.tsdiffbeforeafterboth--- a/tests/src/nesting/tokenProperties.test.ts
+++ b/tests/src/nesting/tokenProperties.test.ts
@@ -448,6 +448,29 @@
expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(smallerProp);
expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);
}));
+
+ itSub('Set sponsored properties', async({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: true}}]});
+
+ await collection.setSponsor(alice, alice.address);
+ await collection.confirmSponsorship(alice);
+ await collection.setPermissions(alice, {access: 'AllowList', mintMode: true});
+ await collection.addToAllowList(alice, {Substrate: bob.address});
+ await collection.setLimits(alice, {sponsoredDataRateLimit: {blocks: 30}});
+
+ const token = await collection.mintToken(alice, {Substrate: bob.address});
+
+ const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
+ const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+
+ await token.setProperties(bob, [{key: 'k', value: 'val'}]);
+
+ const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
+ const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
+
+ expect(bobBalanceAfter).to.be.equal(bobBalanceBefore);
+ expect(aliceBalanceBefore > aliceBalanceAfter).to.be.true;
+ });
});
describe('Negative Integration Test: Token Properties', () => {
@@ -475,6 +498,27 @@
});
});
+ [
+ {mode: 'nft' as const, requiredPallets: [Pallets.NFT]},
+ {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itSub.ifWithPallets(`Forbids adding/deleting properties of a token if token doesn't exist (${testCase.mode.toLocaleUpperCase})`, testCase.requiredPallets, async({helper}) => {
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPropertyPermissions: constitution.slice(0, 1).map(({permission}) => ({key: '1', permission})),
+ });
+ const nonExistentToken = collection.getTokenObject(1);
+
+ await expect(
+ nonExistentToken.setProperties(alice, [{key: '1', value: 'Serotonin increase'}]),
+ 'on expecting failure whilst adding a property by alice',
+ ).to.be.rejectedWith(/common\.TokenNotFound/);
+
+ await expect(
+ nonExistentToken.deleteProperties(alice, ['1']),
+ 'on expecting failure whilst deleting a property by alice',
+ ).to.be.rejectedWith(/common\.TokenNotFound/);
+ }));
+
async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {
const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {
tokenPropertyPermissions: constitution.map(({permission}, i) => ({key: `${i+1}`, permission})),