difftreelog
feat add ApproveForAll to Eth and Sub
in: master
39 files changed
client/rpc/src/lib.rsdiffbeforeafterboth--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -246,6 +246,16 @@
token_id: TokenId,
at: Option<BlockHash>,
) -> Result<Option<String>>;
+
+ /// Get whether an operator is approved by a given owner.
+ #[method(name = "unique_isApprovedForAll")]
+ fn is_approved_for_all(
+ &self,
+ collection: CollectionId,
+ owner: CrossAccountId,
+ operator: CrossAccountId,
+ at: Option<BlockHash>,
+ ) -> Result<bool>;
}
mod app_promotion_unique_rpc {
@@ -569,6 +579,7 @@
pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option<CollectionLimits>, unique_api);
pass_method!(total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<String> => |o| o.map(|number| number.to_string()) , unique_api);
pass_method!(token_owners(collection: CollectionId, token: TokenId) -> Vec<CrossAccountId>, unique_api);
+ pass_method!(is_approved_for_all(collection: CollectionId, owner: CrossAccountId, operator: CrossAccountId) -> bool, unique_api);
}
impl<C, Block, BlockNumber, CrossAccountId, AccountId>
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -472,6 +472,18 @@
u128,
),
+ /// Amount pieces of token owned by `sender` was approved for `spender`.
+ ApprovedForAll(
+ /// Id of collection to which item is belong.
+ CollectionId,
+ /// Owner of a wallet.
+ T::CrossAccountId,
+ /// Id for which operator status was granted or rewoked.
+ T::CrossAccountId,
+ /// Is operator status was granted or rewoked.
+ bool,
+ ),
+
/// The colletion property has been added or edited.
CollectionPropertySet(
/// Id of collection to which property has been set.
@@ -1521,6 +1533,9 @@
/// The price of retrieving token owner
fn token_owner() -> Weight;
+
+ /// The price of setting approval for all
+ fn set_approval_for_all() -> Weight;
}
/// Weight info extension trait for refungible pallet.
@@ -1828,6 +1843,20 @@
/// Get extension for RFT collection.
fn refungible_extensions(&self) -> Option<&dyn RefungibleExtensions<T>>;
+
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// * `owner` - Token owner
+ /// * `operator` - Operator
+ /// * `approve` - Is operator enabled or disabled
+ fn set_approval_for_all(
+ &self,
+ owner: T::CrossAccountId,
+ operator: T::CrossAccountId,
+ approve: bool,
+ ) -> DispatchResultWithPostInfo;
+
+ /// Tells whether an operator is approved by a given owner.
+ fn is_approved_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool;
}
/// Extension for RFT collection.
pallets/fungible/src/common.rsdiffbeforeafterboth--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -107,6 +107,10 @@
fn token_owner() -> Weight {
Weight::zero()
}
+
+ fn set_approval_for_all() -> Weight {
+ Weight::zero()
+ }
}
/// Implementation of `CommonCollectionOperations` for `FungibleHandle`. It wraps FungibleHandle Pallete
@@ -424,4 +428,17 @@
}
<TotalSupply<T>>::try_get(self.id).ok()
}
+
+ fn set_approval_for_all(
+ &self,
+ _owner: T::CrossAccountId,
+ _operator: T::CrossAccountId,
+ _approve: bool,
+ ) -> DispatchResultWithPostInfo {
+ fail!(<Error<T>>::SettingApprovalForAllNotAllowed)
+ }
+
+ fn is_approved_for_all(&self, _owner: T::CrossAccountId, _operator: T::CrossAccountId) -> bool {
+ false
+ }
}
pallets/fungible/src/lib.rsdiffbeforeafterboth--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -127,6 +127,8 @@
FungibleDisallowsNesting,
/// Setting item properties is not allowed.
SettingPropertiesNotAllowed,
+ /// Setting approval for all is not allowed.
+ SettingApprovalForAllNotAllowed,
}
#[pallet::config]
pallets/nonfungible/src/benchmarking.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/benchmarking.rs
+++ b/pallets/nonfungible/src/benchmarking.rs
@@ -222,4 +222,18 @@
let item = create_max_item(&collection, &owner, owner.clone())?;
}: {collection.token_owner(item)}
+
+ set_approval_for_all {
+ bench_init!{
+ owner: sub; collection: collection(owner);
+ operator: cross_from_sub(owner); owner: cross_sub;
+ };
+ }: {<Pallet<T>>::set_approval_for_all(&collection, &owner, &operator, true)}
+
+ is_approved_for_all {
+ bench_init!{
+ owner: sub; collection: collection(owner);
+ operator: cross_from_sub(owner); owner: cross_sub;
+ };
+ }: {<Pallet<T>>::is_approved_for_all(&collection, &owner, &operator)}
}
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -122,6 +122,10 @@
fn token_owner() -> Weight {
<SelfWeightOf<T>>::token_owner()
}
+
+ fn set_approval_for_all() -> Weight {
+ <SelfWeightOf<T>>::set_approval_for_all()
+ }
}
fn map_create_data<T: Config>(
@@ -512,4 +516,20 @@
None
}
}
+
+ fn set_approval_for_all(
+ &self,
+ owner: T::CrossAccountId,
+ operator: T::CrossAccountId,
+ approve: bool,
+ ) -> DispatchResultWithPostInfo {
+ with_weight(
+ <Pallet<T>>::set_approval_for_all(self, &owner, &operator, approve),
+ <CommonWeights<T>>::set_approval_for_all(),
+ )
+ }
+
+ fn is_approved_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool {
+ <Pallet<T>>::is_approved_for_all(self, &owner, &operator)
+ }
}
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -469,15 +469,23 @@
Ok(())
}
- /// @dev Not implemented
+ /// @notice Sets or unsets the approval of a given operator.
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// @param operator Operator
+ /// @param approved Is operator enabled or disabled
+ #[weight(<SelfWeightOf<T>>::set_approval_for_all())]
fn set_approval_for_all(
&mut self,
- _caller: caller,
- _operator: address,
- _approved: bool,
+ caller: caller,
+ operator: address,
+ approved: bool,
) -> Result<void> {
- // TODO: Not implemetable
- Err("not implemented".into())
+ let caller = T::CrossAccountId::from_eth(caller);
+ let operator = T::CrossAccountId::from_eth(operator);
+
+ <Pallet<T>>::set_approval_for_all(self, &caller, &operator, approved)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(())
}
/// @dev Not implemented
@@ -486,10 +494,13 @@
Err("not implemented".into())
}
- /// @dev Not implemented
- fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {
- // TODO: Not implemetable
- Err("not implemented".into())
+ /// @notice Tells whether an operator is approved by a given owner.
+ #[weight(<SelfWeightOf<T>>::is_approved_for_all())]
+ fn is_approved_for_all(&self, owner: address, operator: address) -> Result<bool> {
+ let owner = T::CrossAccountId::from_eth(owner);
+ let operator = T::CrossAccountId::from_eth(operator);
+
+ Ok(<Pallet<T>>::is_approved_for_all(self, &owner, &operator))
}
/// @notice Returns collection helper contract address
pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -272,6 +272,18 @@
QueryKind = OptionQuery,
>;
+ /// Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.
+ #[pallet::storage]
+ pub type WalletOperator<T: Config> = StorageNMap<
+ Key = (
+ Key<Twox64Concat, CollectionId>,
+ Key<Blake2_128Concat, T::CrossAccountId>,
+ Key<Blake2_128Concat, T::CrossAccountId>,
+ ),
+ Value = bool,
+ QueryKind = OptionQuery,
+ >;
+
/// Upgrade from the old schema to properties.
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
@@ -438,6 +450,7 @@
<TokensBurnt<T>>::remove(id);
let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);
let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);
+ let _ = <WalletOperator<T>>::clear_prefix((id,), u32::MAX, None);
Ok(())
}
@@ -1193,6 +1206,9 @@
if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {
return Ok(());
}
+ if <WalletOperator<T>>::get((collection.id, from, spender)) == Some(true) {
+ return Ok(());
+ }
ensure!(
collection.ignores_allowance(spender),
<CommonError<T>>::ApprovedValueTooLow
@@ -1326,4 +1342,52 @@
) -> DispatchResult {
Self::create_multiple_items(collection, sender, vec![data], nesting_budget)
}
+
+ /// Sets or unsets the approval of a given operator.
+ ///
+ /// An operator is allowed to transfer all token pieces of the sender on their behalf.
+ /// - `owner`: Token owner
+ /// - `operator`: Operator
+ /// - `approve`: Is operator enabled or disabled
+ pub fn set_approval_for_all(
+ collection: &NonfungibleHandle<T>,
+ owner: &T::CrossAccountId,
+ operator: &T::CrossAccountId,
+ approve: bool,
+ ) -> DispatchResult {
+ if collection.permissions.access() == AccessMode::AllowList {
+ collection.check_allowlist(owner)?;
+ collection.check_allowlist(operator)?;
+ }
+
+ <PalletCommon<T>>::ensure_correct_receiver(operator)?;
+
+ // =========
+
+ <WalletOperator<T>>::insert((collection.id, owner, operator), approve);
+ <PalletEvm<T>>::deposit_log(
+ ERC721Events::ApprovalForAll {
+ owner: *owner.as_eth(),
+ operator: *operator.as_eth(),
+ approved: approve,
+ }
+ .to_log(collection_id_to_address(collection.id)),
+ );
+ <PalletCommon<T>>::deposit_event(CommonEvent::ApprovedForAll(
+ collection.id,
+ owner.clone(),
+ operator.clone(),
+ approve,
+ ));
+ Ok(())
+ }
+
+ /// Tells whether an operator is approved by a given owner.
+ pub fn is_approved_for_all(
+ collection: &NonfungibleHandle<T>,
+ owner: &T::CrossAccountId,
+ operator: &T::CrossAccountId,
+ ) -> bool {
+ <WalletOperator<T>>::get((collection.id, owner, operator)).unwrap_or(false)
+ }
}
pallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterbothbinary blob — no preview
pallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -1020,7 +1020,10 @@
dummy = 0;
}
- /// @dev Not implemented
+ /// @notice Sets or unsets the approval of a given operator.
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// @param operator Operator
+ /// @param approved Is operator enabled or disabled
/// @dev EVM selector for this function is: 0xa22cb465,
/// or in textual repr: setApprovalForAll(address,bool)
function setApprovalForAll(address operator, bool approved) public {
@@ -1040,15 +1043,15 @@
return 0x0000000000000000000000000000000000000000;
}
- /// @dev Not implemented
+ /// @notice Tells whether an operator is approved by a given owner.
/// @dev EVM selector for this function is: 0xe985e9c5,
/// or in textual repr: isApprovedForAll(address,address)
- function isApprovedForAll(address owner, address operator) public view returns (address) {
+ function isApprovedForAll(address owner, address operator) public view returns (bool) {
require(false, stub_error);
owner;
operator;
dummy;
- return 0x0000000000000000000000000000000000000000;
+ return false;
}
/// @notice Returns collection helper contract address
pallets/nonfungible/src/weights.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/weights.rs
+++ b/pallets/nonfungible/src/weights.rs
@@ -26,6 +26,7 @@
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
+#![allow(missing_docs)]
#![allow(clippy::unnecessary_cast)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
@@ -47,6 +48,8 @@
fn set_token_properties(b: u32, ) -> Weight;
fn delete_token_properties(b: u32, ) -> Weight;
fn token_owner() -> Weight;
+ fn set_approval_for_all() -> Weight;
+ fn is_approved_for_all() -> Weight;
}
/// Weights for pallet_nonfungible using the Substrate node and recommended hardware.
@@ -195,6 +198,16 @@
Weight::from_ref_time(4_366_000)
.saturating_add(T::DbWeight::get().reads(1 as u64))
}
+ // Storage: Nonfungible WalletOperator (r:0 w:1)
+ fn set_approval_for_all() -> Weight {
+ Weight::from_ref_time(16_231_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Nonfungible WalletOperator (r:1 w:0)
+ fn is_approved_for_all() -> Weight {
+ Weight::from_ref_time(6_161_000 as u64)
+ .saturating_add(T::DbWeight::get().reads(1 as u64))
+ }
}
// For backwards compatibility and tests
@@ -342,4 +355,14 @@
Weight::from_ref_time(4_366_000)
.saturating_add(RocksDbWeight::get().reads(1 as u64))
}
+ // Storage: Nonfungible WalletOperator (r:0 w:1)
+ fn set_approval_for_all() -> Weight {
+ Weight::from_ref_time(16_231_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Nonfungible WalletOperator (r:1 w:0)
+ fn is_approved_for_all() -> Weight {
+ Weight::from_ref_time(6_161_000 as u64)
+ .saturating_add(RocksDbWeight::get().reads(1 as u64))
+ }
}
pallets/refungible/src/benchmarking.rsdiffbeforeafterboth--- a/pallets/refungible/src/benchmarking.rs
+++ b/pallets/refungible/src/benchmarking.rs
@@ -290,4 +290,18 @@
};
let item = create_max_item(&collection, &sender, [(owner.clone(), 100)])?;
}: {<Pallet<T>>::token_owner(collection.id, item)}
+
+ set_approval_for_all {
+ bench_init!{
+ owner: sub; collection: collection(owner);
+ operator: cross_from_sub(owner); owner: cross_sub;
+ };
+ }: {<Pallet<T>>::set_approval_for_all(&collection, &owner, &operator, true)}
+
+ is_approved_for_all {
+ bench_init!{
+ owner: sub; collection: collection(owner);
+ operator: cross_from_sub(owner); owner: cross_sub;
+ };
+ }: {<Pallet<T>>::is_approved_for_all(&collection, &owner, &operator)}
}
pallets/refungible/src/common.rsdiffbeforeafterboth--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -152,6 +152,10 @@
fn token_owner() -> Weight {
<SelfWeightOf<T>>::token_owner()
}
+
+ fn set_approval_for_all() -> Weight {
+ <SelfWeightOf<T>>::set_approval_for_all()
+ }
}
fn map_create_data<T: Config>(
@@ -516,6 +520,22 @@
fn total_pieces(&self, token: TokenId) -> Option<u128> {
<Pallet<T>>::total_pieces(self.id, token)
}
+
+ fn set_approval_for_all(
+ &self,
+ owner: T::CrossAccountId,
+ operator: T::CrossAccountId,
+ approve: bool,
+ ) -> DispatchResultWithPostInfo {
+ with_weight(
+ <Pallet<T>>::set_approval_for_all(self, &owner, &operator, approve),
+ <CommonWeights<T>>::set_approval_for_all(),
+ )
+ }
+
+ fn is_approved_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool {
+ <Pallet<T>>::is_approved_for_all(self, &owner, &operator)
+ }
}
impl<T: Config> RefungibleExtensions<T> for RefungibleHandle<T> {
pallets/refungible/src/erc.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -461,15 +461,23 @@
Err("not implemented".into())
}
- /// @dev Not implemented
+ /// @notice Sets or unsets the approval of a given operator.
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// @param operator Operator
+ /// @param approved Is operator enabled or disabled
+ #[weight(<SelfWeightOf<T>>::set_approval_for_all())]
fn set_approval_for_all(
&mut self,
- _caller: caller,
- _operator: address,
- _approved: bool,
+ caller: caller,
+ operator: address,
+ approved: bool,
) -> Result<void> {
- // TODO: Not implemetable
- Err("not implemented".into())
+ let caller = T::CrossAccountId::from_eth(caller);
+ let operator = T::CrossAccountId::from_eth(operator);
+
+ <Pallet<T>>::set_approval_for_all(self, &caller, &operator, approved)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(())
}
/// @dev Not implemented
@@ -478,10 +486,13 @@
Err("not implemented".into())
}
- /// @dev Not implemented
- fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {
- // TODO: Not implemetable
- Err("not implemented".into())
+ /// @notice Tells whether an operator is approved by a given owner.
+ #[weight(<SelfWeightOf<T>>::is_approved_for_all())]
+ fn is_approved_for_all(&self, owner: address, operator: address) -> Result<bool> {
+ let owner = T::CrossAccountId::from_eth(owner);
+ let operator = T::CrossAccountId::from_eth(operator);
+
+ Ok(<Pallet<T>>::is_approved_for_all(self, &owner, &operator))
}
/// @notice Returns collection helper contract address
pallets/refungible/src/lib.rsdiffbeforeafterboth--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -273,6 +273,18 @@
QueryKind = ValueQuery,
>;
+ /// Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.
+ #[pallet::storage]
+ pub type WalletOperator<T: Config> = StorageNMap<
+ Key = (
+ Key<Twox64Concat, CollectionId>,
+ Key<Blake2_128Concat, T::CrossAccountId>,
+ Key<Blake2_128Concat, T::CrossAccountId>,
+ ),
+ Value = bool,
+ QueryKind = OptionQuery,
+ >;
+
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_runtime_upgrade() -> Weight {
@@ -1161,6 +1173,12 @@
}
let allowance =
<Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);
+
+ // Allowance if any would be reduced if spender is also wallet operator
+ if <WalletOperator<T>>::get((collection.id, from, spender)) == Some(true) {
+ return Ok(allowance);
+ }
+
if allowance.is_none() {
ensure!(
collection.ignores_allowance(spender),
@@ -1387,4 +1405,52 @@
Some(res)
}
}
+
+ /// Sets or unsets the approval of a given operator.
+ ///
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// - `owner`: Token owner
+ /// - `operator`: Operator
+ /// - `approve`: Is operator enabled or disabled
+ pub fn set_approval_for_all(
+ collection: &RefungibleHandle<T>,
+ owner: &T::CrossAccountId,
+ operator: &T::CrossAccountId,
+ approve: bool,
+ ) -> DispatchResult {
+ if collection.permissions.access() == AccessMode::AllowList {
+ collection.check_allowlist(owner)?;
+ collection.check_allowlist(operator)?;
+ }
+
+ <PalletCommon<T>>::ensure_correct_receiver(operator)?;
+
+ // =========
+
+ <WalletOperator<T>>::insert((collection.id, owner, operator), approve);
+ <PalletEvm<T>>::deposit_log(
+ ERC721Events::ApprovalForAll {
+ owner: *owner.as_eth(),
+ operator: *operator.as_eth(),
+ approved: approve,
+ }
+ .to_log(collection_id_to_address(collection.id)),
+ );
+ <PalletCommon<T>>::deposit_event(CommonEvent::ApprovedForAll(
+ collection.id,
+ owner.clone(),
+ operator.clone(),
+ approve,
+ ));
+ Ok(())
+ }
+
+ /// Tells whether an operator is approved by a given owner.
+ pub fn is_approved_for_all(
+ collection: &RefungibleHandle<T>,
+ owner: &T::CrossAccountId,
+ operator: &T::CrossAccountId,
+ ) -> bool {
+ <WalletOperator<T>>::get((collection.id, owner, operator)).unwrap_or(false)
+ }
}
pallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterbothbinary blob — no preview
pallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -1017,7 +1017,10 @@
dummy = 0;
}
- /// @dev Not implemented
+ /// @notice Sets or unsets the approval of a given operator.
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// @param operator Operator
+ /// @param approved Is operator enabled or disabled
/// @dev EVM selector for this function is: 0xa22cb465,
/// or in textual repr: setApprovalForAll(address,bool)
function setApprovalForAll(address operator, bool approved) public {
@@ -1037,15 +1040,15 @@
return 0x0000000000000000000000000000000000000000;
}
- /// @dev Not implemented
+ /// @notice Tells whether an operator is approved by a given owner.
/// @dev EVM selector for this function is: 0xe985e9c5,
/// or in textual repr: isApprovedForAll(address,address)
- function isApprovedForAll(address owner, address operator) public view returns (address) {
+ function isApprovedForAll(address owner, address operator) public view returns (bool) {
require(false, stub_error);
owner;
operator;
dummy;
- return 0x0000000000000000000000000000000000000000;
+ return false;
}
/// @notice Returns collection helper contract address
pallets/refungible/src/weights.rsdiffbeforeafterboth--- a/pallets/refungible/src/weights.rs
+++ b/pallets/refungible/src/weights.rs
@@ -3,7 +3,7 @@
//! Autogenerated weights for pallet_refungible
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2022-08-15, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2022-11-25, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
// Executed Command:
@@ -26,6 +26,7 @@
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
+#![allow(missing_docs)]
#![allow(clippy::unnecessary_cast)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
@@ -54,6 +55,8 @@
fn delete_token_properties(b: u32, ) -> Weight;
fn repartition_item() -> Weight;
fn token_owner() -> Weight;
+ fn set_approval_for_all() -> Weight;
+ fn is_approved_for_all() -> Weight;
}
/// Weights for pallet_refungible using the Substrate node and recommended hardware.
@@ -259,6 +262,16 @@
Weight::from_ref_time(9_431_000)
.saturating_add(T::DbWeight::get().reads(2 as u64))
}
+ // Storage: Refungible WalletOperator (r:0 w:1)
+ fn set_approval_for_all() -> Weight {
+ Weight::from_ref_time(16_150_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Refungible WalletOperator (r:1 w:0)
+ fn is_approved_for_all() -> Weight {
+ Weight::from_ref_time(5_901_000 as u64)
+ .saturating_add(T::DbWeight::get().reads(1 as u64))
+ }
}
// For backwards compatibility and tests
@@ -463,4 +476,14 @@
Weight::from_ref_time(9_431_000)
.saturating_add(RocksDbWeight::get().reads(2 as u64))
}
+ // Storage: Refungible WalletOperator (r:0 w:1)
+ fn set_approval_for_all() -> Weight {
+ Weight::from_ref_time(16_150_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Refungible WalletOperator (r:1 w:0)
+ fn is_approved_for_all() -> Weight {
+ Weight::from_ref_time(5_901_000 as u64)
+ .saturating_add(RocksDbWeight::get().reads(1 as u64))
+ }
}
pallets/unique/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//! # Unique Pallet18//!19//! A pallet governing Unique transactions.20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The Unique pallet's purpose is to be the primary interface between28//! external users and the inner structure of the Unique chains.29//!30//! It also contains an implementation of [`CollectionHelpers`][`eth`],31//! an Ethereum contract dealing with collection operations.32//!33//! ## Interface34//!35//! ### Dispatchables36//!37//! - `create_collection` - Create a collection of tokens. **Deprecated**, use `create_collection_ex`.38//! - `create_collection_ex` - Create a collection of tokens with explicit parameters.39//! - `destroy_collection` - Destroy a collection if no tokens exist within.40//! - `add_to_allow_list` - Add an address to allow list.41//! - `remove_from_allow_list` - Remove an address from allow list.42//! - `change_collection_owner` - Change the owner of the collection.43//! - `add_collection_admin` - Add an admin to a collection.44//! - `remove_collection_admin` - Remove admin of a collection.45//! - `set_collection_sponsor` - Invite a new collection sponsor.46//! - `confirm_sponsorship` - Confirm own sponsorship of a collection, becoming the sponsor.47//! - `remove_collection_sponsor` - Remove a sponsor from a collection.48//! - `create_item` - Create an item within a collection.49//! - `create_multiple_items` - Create multiple items within a collection.50//! - `set_collection_properties` - Add or change collection properties.51//! - `delete_collection_properties` - Delete specified collection properties.52//! - `set_token_properties` - Add or change token properties.53//! - `delete_token_properties` - Delete token properties.54//! - `set_token_property_permissions` - Add or change token property permissions of a collection.55//! - `create_multiple_items_ex` - Create multiple items within a collection with explicitly specified initial parameters.56//! - `set_transfers_enabled_flag` - Completely allow or disallow transfers for a particular collection.57//! - `burn_item` - Destroy an item.58//! - `burn_from` - Destroy an item on behalf of the owner as a non-owner account.59//! - `transfer` - Change ownership of the token.60//! - `transfer_from` - Change ownership of the token on behalf of the owner as a non-owner account.61//! - `approve` - Allow a non-permissioned address to transfer or burn an item.62//! - `set_collection_limits` - Set specific limits of a collection.63//! - `set_collection_permissions` - Set specific permissions of a collection.64//! - `repartition` - Re-partition a refungible token, while owning all of its parts.6566#![recursion_limit = "1024"]67#![cfg_attr(not(feature = "std"), no_std)]68#![allow(69 clippy::too_many_arguments,70 clippy::unnecessary_mut_passed,71 clippy::unused_unit72)]7374extern crate alloc;7576use frame_support::{77 decl_module, decl_storage, decl_error, decl_event,78 dispatch::DispatchResult,79 ensure, fail,80 weights::{Weight},81 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},82 BoundedVec,83};84use scale_info::TypeInfo;85use frame_system::{self as system, ensure_signed};86use sp_std::{vec, vec::Vec};87use up_data_structs::{88 MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH,89 MAX_PROPERTIES_PER_ITEM, MAX_PROPERTY_KEY_LENGTH, MAX_PROPERTY_VALUE_LENGTH,90 MAX_COLLECTION_PROPERTIES_SIZE, COLLECTION_ADMINS_LIMIT, MAX_TOKEN_PROPERTIES_SIZE,91 CreateItemData, CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId,92 SponsorshipState, CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,93 PropertyKeyPermission,94};95use pallet_evm::account::CrossAccountId;96use pallet_common::{97 CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_tx,98 dispatch::CollectionDispatch, RefungibleExtensionsWeightInfo,99};100pub mod eth;101102#[cfg(feature = "runtime-benchmarks")]103pub mod benchmarking;104pub mod weights;105use weights::WeightInfo;106107/// A maximum number of levels of depth in the token nesting tree.108pub const NESTING_BUDGET: u32 = 5;109110decl_error! {111 /// Errors for the common Unique transactions.112 pub enum Error for Module<T: Config> {113 /// Decimal_points parameter must be lower than [`up_data_structs::MAX_DECIMAL_POINTS`].114 CollectionDecimalPointLimitExceeded,115 /// This address is not set as sponsor, use setCollectionSponsor first.116 ConfirmUnsetSponsorFail,117 /// Length of items properties must be greater than 0.118 EmptyArgument,119 /// Repertition is only supported by refungible collection.120 RepartitionCalledOnNonRefungibleCollection,121 }122}123124/// Configuration trait of this pallet.125pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {126 /// Overarching event type.127 type RuntimeEvent: From<Event<Self>> + Into<<Self as frame_system::Config>::RuntimeEvent>;128129 /// Weight information for extrinsics in this pallet.130 type WeightInfo: WeightInfo;131132 /// Weight information for common pallet operations.133 type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;134135 /// Weight info information for extra refungible pallet operations.136 type RefungibleExtensionsWeightInfo: RefungibleExtensionsWeightInfo;137}138139decl_event! {140 pub enum Event<T>141 where142 <T as frame_system::Config>::AccountId,143 <T as pallet_evm::Config>::CrossAccountId,144 {145 /// Collection sponsor was removed146 ///147 /// # Arguments148 /// * collection_id: ID of the affected collection.149 CollectionSponsorRemoved(CollectionId),150151 /// Collection admin was added152 ///153 /// # Arguments154 /// * collection_id: ID of the affected collection.155 /// * admin: Admin address.156 CollectionAdminAdded(CollectionId, CrossAccountId),157158 /// Collection owned was changed159 ///160 /// # Arguments161 /// * collection_id: ID of the affected collection.162 /// * owner: New owner address.163 CollectionOwnedChanged(CollectionId, AccountId),164165 /// Collection sponsor was set166 ///167 /// # Arguments168 /// * collection_id: ID of the affected collection.169 /// * owner: New sponsor address.170 CollectionSponsorSet(CollectionId, AccountId),171172 /// New sponsor was confirm173 ///174 /// # Arguments175 /// * collection_id: ID of the affected collection.176 /// * sponsor: New sponsor address.177 SponsorshipConfirmed(CollectionId, AccountId),178179 /// Collection admin was removed180 ///181 /// # Arguments182 /// * collection_id: ID of the affected collection.183 /// * admin: Removed admin address.184 CollectionAdminRemoved(CollectionId, CrossAccountId),185186 /// Address was removed from the allow list187 ///188 /// # Arguments189 /// * collection_id: ID of the affected collection.190 /// * user: Address of the removed account.191 AllowListAddressRemoved(CollectionId, CrossAccountId),192193 /// Address was added to the allow list194 ///195 /// # Arguments196 /// * collection_id: ID of the affected collection.197 /// * user: Address of the added account.198 AllowListAddressAdded(CollectionId, CrossAccountId),199200 /// Collection limits were set201 ///202 /// # Arguments203 /// * collection_id: ID of the affected collection.204 CollectionLimitSet(CollectionId),205206 /// Collection permissions were set207 ///208 /// # Arguments209 /// * collection_id: ID of the affected collection.210 CollectionPermissionSet(CollectionId),211 }212}213214type SelfWeightOf<T> = <T as Config>::WeightInfo;215216// # Used definitions217//218// ## User control levels219//220// chain-controlled - key is uncontrolled by user221// i.e autoincrementing index222// can use non-cryptographic hash223// real - key is controlled by user224// but it is hard to generate enough colliding values, i.e owner of signed txs225// can use non-cryptographic hash226// controlled - key is completly controlled by users227// i.e maps with mutable keys228// should use cryptographic hash229//230// ## User control level downgrade reasons231//232// ?1 - chain-controlled -> controlled233// collections/tokens can be destroyed, resulting in massive holes234// ?2 - chain-controlled -> controlled235// same as ?1, but can be only added, resulting in easier exploitation236// ?3 - real -> controlled237// no confirmation required, so addresses can be easily generated238decl_storage! {239 trait Store for Module<T: Config> as Unique {240241 //#region Private members242 /// Used for migrations243 ChainVersion: u64;244 //#endregion245246 //#region Tokens transfer sponosoring rate limit baskets247 /// (Collection id (controlled?2), who created (real))248 /// TODO: Off chain worker should remove from this map when collection gets removed249 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;250 /// Collection id (controlled?2), token id (controlled?2)251 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;252 /// Collection id (controlled?2), owning user (real)253 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;254 /// Collection id (controlled?2), token id (controlled?2)255 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;256 //#endregion257258 /// Variable metadata sponsoring259 /// Collection id (controlled?2), token id (controlled?2)260 #[deprecated]261 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;262 /// Last sponsoring of token property setting // todo:doc rephrase this and the following263 pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;264265 /// Last sponsoring of NFT approval in a collection266 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;267 /// Last sponsoring of fungible tokens approval in a collection268 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;269 /// Last sponsoring of RFT approval in a collection270 pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;271 }272}273274decl_module! {275 /// Type alias to Pallet, to be used by construct_runtime.276 pub struct Module<T: Config> for enum Call277 where278 origin: T::RuntimeOrigin279 {280 type Error = Error<T>;281282 #[doc = "A maximum number of levels of depth in the token nesting tree."]283 const NESTING_BUDGET: u32 = NESTING_BUDGET;284285 #[doc = "Maximal length of a collection name."]286 const MAX_COLLECTION_NAME_LENGTH: u32 = MAX_COLLECTION_NAME_LENGTH;287288 #[doc = "Maximal length of a collection description."]289 const MAX_COLLECTION_DESCRIPTION_LENGTH: u32 = MAX_COLLECTION_DESCRIPTION_LENGTH;290291 #[doc = "Maximal length of a token prefix."]292 const MAX_TOKEN_PREFIX_LENGTH: u32 = MAX_TOKEN_PREFIX_LENGTH;293294 #[doc = "Maximum admins per collection."]295 const COLLECTION_ADMINS_LIMIT: u32 = COLLECTION_ADMINS_LIMIT;296297 #[doc = "Maximal length of a property key."]298 const MAX_PROPERTY_KEY_LENGTH: u32 = MAX_PROPERTY_KEY_LENGTH;299300 #[doc = "Maximal length of a property value."]301 const MAX_PROPERTY_VALUE_LENGTH: u32 = MAX_PROPERTY_VALUE_LENGTH;302303 #[doc = "A maximum number of token properties."]304 const MAX_PROPERTIES_PER_ITEM: u32 = MAX_PROPERTIES_PER_ITEM;305306 #[doc = "Maximum size for all collection properties."]307 const MAX_COLLECTION_PROPERTIES_SIZE: u32 = MAX_COLLECTION_PROPERTIES_SIZE;308309 #[doc = "Maximum size of all token properties."]310 const MAX_TOKEN_PROPERTIES_SIZE: u32 = MAX_TOKEN_PROPERTIES_SIZE;311312 #[doc = "Default NFT collection limit."]313 const NFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::NFT);314315 #[doc = "Default RFT collection limit."]316 const RFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::ReFungible);317318 #[doc = "Default FT collection limit."]319 const FT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::Fungible(0));320321322 pub fn deposit_event() = default;323324 fn on_initialize(_now: T::BlockNumber) -> Weight {325 Weight::zero()326 }327328 fn on_runtime_upgrade() -> Weight {329 Weight::zero()330 }331332 /// Create a collection of tokens.333 ///334 /// Each Token may have multiple properties encoded as an array of bytes335 /// of certain length. The initial owner of the collection is set336 /// to the address that signed the transaction and can be changed later.337 ///338 /// Prefer the more advanced [`create_collection_ex`][`Pallet::create_collection_ex`] instead.339 ///340 /// # Permissions341 ///342 /// * Anyone - becomes the owner of the new collection.343 ///344 /// # Arguments345 ///346 /// * `collection_name`: Wide-character string with collection name347 /// (limit [`MAX_COLLECTION_NAME_LENGTH`]).348 /// * `collection_description`: Wide-character string with collection description349 /// (limit [`MAX_COLLECTION_DESCRIPTION_LENGTH`]).350 /// * `token_prefix`: Byte string containing the token prefix to mark a collection351 /// to which a token belongs (limit [`MAX_TOKEN_PREFIX_LENGTH`]).352 /// * `mode`: Type of items stored in the collection and type dependent data.353 // returns collection ID354 #[weight = <SelfWeightOf<T>>::create_collection()]355 #[deprecated(note = "`create_collection_ex` is more up-to-date and advanced, prefer it instead")]356 pub fn create_collection(357 origin,358 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,359 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,360 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,361 mode: CollectionMode362 ) -> DispatchResult {363 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {364 name: collection_name,365 description: collection_description,366 token_prefix,367 mode,368 ..Default::default()369 };370 Self::create_collection_ex(origin, data)371 }372373 /// Create a collection with explicit parameters.374 ///375 /// Prefer it to the deprecated [`create_collection`][`Pallet::create_collection`] method.376 ///377 /// # Permissions378 ///379 /// * Anyone - becomes the owner of the new collection.380 ///381 /// # Arguments382 ///383 /// * `data`: Explicit data of a collection used for its creation.384 #[weight = <SelfWeightOf<T>>::create_collection()]385 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {386 let sender = ensure_signed(origin)?;387388 // =========389 let sender = T::CrossAccountId::from_sub(sender);390 let _id = T::CollectionDispatch::create(sender.clone(), sender, data, Default::default())?;391392 Ok(())393 }394395 /// Destroy a collection if no tokens exist within.396 ///397 /// # Permissions398 ///399 /// * Collection owner400 ///401 /// # Arguments402 ///403 /// * `collection_id`: Collection to destroy.404 #[weight = <SelfWeightOf<T>>::destroy_collection()]405 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {406 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);407408 Self::destroy_collection_internal(sender, collection_id)409 }410411 /// Add an address to allow list.412 ///413 /// # Permissions414 ///415 /// * Collection owner416 /// * Collection admin417 ///418 /// # Arguments419 ///420 /// * `collection_id`: ID of the modified collection.421 /// * `address`: ID of the address to be added to the allowlist.422 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]423 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{424425 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);426 let collection = <CollectionHandle<T>>::try_get(collection_id)?;427 collection.check_is_internal()?;428429 <PalletCommon<T>>::toggle_allowlist(430 &collection,431 &sender,432 &address,433 true,434 )?;435436 Self::deposit_event(Event::<T>::AllowListAddressAdded(437 collection_id,438 address439 ));440441 Ok(())442 }443444 /// Remove an address from allow list.445 ///446 /// # Permissions447 ///448 /// * Collection owner449 /// * Collection admin450 ///451 /// # Arguments452 ///453 /// * `collection_id`: ID of the modified collection.454 /// * `address`: ID of the address to be removed from the allowlist.455 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]456 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{457458 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);459 let collection = <CollectionHandle<T>>::try_get(collection_id)?;460 collection.check_is_internal()?;461462 <PalletCommon<T>>::toggle_allowlist(463 &collection,464 &sender,465 &address,466 false,467 )?;468469 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(470 collection_id,471 address472 ));473474 Ok(())475 }476477 /// Change the owner of the collection.478 ///479 /// # Permissions480 ///481 /// * Collection owner482 ///483 /// # Arguments484 ///485 /// * `collection_id`: ID of the modified collection.486 /// * `new_owner`: ID of the account that will become the owner.487 #[weight = <SelfWeightOf<T>>::change_collection_owner()]488 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {489490 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);491492 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;493 target_collection.check_is_internal()?;494 target_collection.check_is_owner(&sender)?;495496 target_collection.owner = new_owner.clone();497 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(498 collection_id,499 new_owner500 ));501502 target_collection.save()503 }504505 /// Add an admin to a collection.506 ///507 /// NFT Collection can be controlled by multiple admin addresses508 /// (some which can also be servers, for example). Admins can issue509 /// and burn NFTs, as well as add and remove other admins,510 /// but cannot change NFT or Collection ownership.511 ///512 /// # Permissions513 ///514 /// * Collection owner515 /// * Collection admin516 ///517 /// # Arguments518 ///519 /// * `collection_id`: ID of the Collection to add an admin for.520 /// * `new_admin`: Address of new admin to add.521 #[weight = <SelfWeightOf<T>>::add_collection_admin()]522 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {523 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);524 let collection = <CollectionHandle<T>>::try_get(collection_id)?;525 collection.check_is_internal()?;526527 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(528 collection_id,529 new_admin_id.clone()530 ));531532 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)533 }534535 /// Remove admin of a collection.536 ///537 /// An admin address can remove itself. List of admins may become empty,538 /// in which case only Collection Owner will be able to add an Admin.539 ///540 /// # Permissions541 ///542 /// * Collection owner543 /// * Collection admin544 ///545 /// # Arguments546 ///547 /// * `collection_id`: ID of the collection to remove the admin for.548 /// * `account_id`: Address of the admin to remove.549 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]550 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {551 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);552 let collection = <CollectionHandle<T>>::try_get(collection_id)?;553 collection.check_is_internal()?;554555 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(556 collection_id,557 account_id.clone()558 ));559560 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)561 }562563 /// Set (invite) a new collection sponsor.564 ///565 /// If successful, confirmation from the sponsor-to-be will be pending.566 ///567 /// # Permissions568 ///569 /// * Collection owner570 /// * Collection admin571 ///572 /// # Arguments573 ///574 /// * `collection_id`: ID of the modified collection.575 /// * `new_sponsor`: ID of the account of the sponsor-to-be.576 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]577 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {578 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);579580 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;581 target_collection.check_is_owner_or_admin(&sender)?;582 target_collection.check_is_internal()?;583584 target_collection.set_sponsor(new_sponsor.clone())?;585586 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(587 collection_id,588 new_sponsor589 ));590591 target_collection.save()592 }593594 /// Confirm own sponsorship of a collection, becoming the sponsor.595 ///596 /// An invitation must be pending, see [`set_collection_sponsor`][`Pallet::set_collection_sponsor`].597 /// Sponsor can pay the fees of a transaction instead of the sender,598 /// but only within specified limits.599 ///600 /// # Permissions601 ///602 /// * Sponsor-to-be603 ///604 /// # Arguments605 ///606 /// * `collection_id`: ID of the collection with the pending sponsor.607 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]608 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {609 let sender = ensure_signed(origin)?;610611 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;612 target_collection.check_is_internal()?;613 ensure!(614 target_collection.confirm_sponsorship(&sender)?,615 Error::<T>::ConfirmUnsetSponsorFail616 );617618 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(619 collection_id,620 sender621 ));622623 target_collection.save()624 }625626 /// Remove a collection's a sponsor, making everyone pay for their own transactions.627 ///628 /// # Permissions629 ///630 /// * Collection owner631 ///632 /// # Arguments633 ///634 /// * `collection_id`: ID of the collection with the sponsor to remove.635 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]636 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {637 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);638639 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;640 target_collection.check_is_internal()?;641 target_collection.check_is_owner(&sender)?;642643 target_collection.sponsorship = SponsorshipState::Disabled;644645 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(646 collection_id647 ));648 target_collection.save()649 }650651 /// Mint an item within a collection.652 ///653 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].654 ///655 /// # Permissions656 ///657 /// * Collection owner658 /// * Collection admin659 /// * Anyone if660 /// * Allow List is enabled, and661 /// * Address is added to allow list, and662 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])663 ///664 /// # Arguments665 ///666 /// * `collection_id`: ID of the collection to which an item would belong.667 /// * `owner`: Address of the initial owner of the item.668 /// * `data`: Token data describing the item to store on chain.669 #[weight = T::CommonWeightInfo::create_item()]670 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {671 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);672 let budget = budget::Value::new(NESTING_BUDGET);673674 dispatch_tx::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))675 }676677 /// Create multiple items within a collection.678 ///679 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].680 ///681 /// # Permissions682 ///683 /// * Collection owner684 /// * Collection admin685 /// * Anyone if686 /// * Allow List is enabled, and687 /// * Address is added to the allow list, and688 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])689 ///690 /// # Arguments691 ///692 /// * `collection_id`: ID of the collection to which the tokens would belong.693 /// * `owner`: Address of the initial owner of the tokens.694 /// * `items_data`: Vector of data describing each item to be created.695 #[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]696 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {697 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);698 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);699 let budget = budget::Value::new(NESTING_BUDGET);700701 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))702 }703704 /// Add or change collection properties.705 ///706 /// # Permissions707 ///708 /// * Collection owner709 /// * Collection admin710 ///711 /// # Arguments712 ///713 /// * `collection_id`: ID of the modified collection.714 /// * `properties`: Vector of key-value pairs stored as the collection's metadata.715 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.716 #[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]717 pub fn set_collection_properties(718 origin,719 collection_id: CollectionId,720 properties: Vec<Property>721 ) -> DispatchResultWithPostInfo {722 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);723724 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);725726 dispatch_tx::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))727 }728729 /// Delete specified collection properties.730 ///731 /// # Permissions732 ///733 /// * Collection Owner734 /// * Collection Admin735 ///736 /// # Arguments737 ///738 /// * `collection_id`: ID of the modified collection.739 /// * `property_keys`: Vector of keys of the properties to be deleted.740 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.741 #[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]742 pub fn delete_collection_properties(743 origin,744 collection_id: CollectionId,745 property_keys: Vec<PropertyKey>,746 ) -> DispatchResultWithPostInfo {747 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);748749 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);750751 dispatch_tx::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))752 }753754 /// Add or change token properties according to collection's permissions.755 /// Currently properties only work with NFTs.756 ///757 /// # Permissions758 ///759 /// * Depends on collection's token property permissions and specified property mutability:760 /// * Collection owner761 /// * Collection admin762 /// * Token owner763 ///764 /// See [`set_token_property_permissions`][`Pallet::set_token_property_permissions`].765 ///766 /// # Arguments767 ///768 /// * `collection_id: ID of the collection to which the token belongs.769 /// * `token_id`: ID of the modified token.770 /// * `properties`: Vector of key-value pairs stored as the token's metadata.771 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.772 #[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]773 pub fn set_token_properties(774 origin,775 collection_id: CollectionId,776 token_id: TokenId,777 properties: Vec<Property>778 ) -> DispatchResultWithPostInfo {779 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);780781 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);782 let budget = budget::Value::new(NESTING_BUDGET);783784 dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties, &budget))785 }786787 /// Delete specified token properties. Currently properties only work with NFTs.788 ///789 /// # Permissions790 ///791 /// * Depends on collection's token property permissions and specified property mutability:792 /// * Collection owner793 /// * Collection admin794 /// * Token owner795 ///796 /// # Arguments797 ///798 /// * `collection_id`: ID of the collection to which the token belongs.799 /// * `token_id`: ID of the modified token.800 /// * `property_keys`: Vector of keys of the properties to be deleted.801 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.802 #[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]803 pub fn delete_token_properties(804 origin,805 collection_id: CollectionId,806 token_id: TokenId,807 property_keys: Vec<PropertyKey>808 ) -> DispatchResultWithPostInfo {809 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);810811 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);812 let budget = budget::Value::new(NESTING_BUDGET);813814 dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys, &budget))815 }816817 /// Add or change token property permissions of a collection.818 ///819 /// Without a permission for a particular key, a property with that key820 /// cannot be created in a token.821 ///822 /// # Permissions823 ///824 /// * Collection owner825 /// * Collection admin826 ///827 /// # Arguments828 ///829 /// * `collection_id`: ID of the modified collection.830 /// * `property_permissions`: Vector of permissions for property keys.831 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.832 #[weight = T::CommonWeightInfo::set_token_property_permissions(property_permissions.len() as u32)]833 pub fn set_token_property_permissions(834 origin,835 collection_id: CollectionId,836 property_permissions: Vec<PropertyKeyPermission>,837 ) -> DispatchResultWithPostInfo {838 ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);839840 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);841842 dispatch_tx::<T, _>(collection_id, |d| d.set_token_property_permissions(&sender, property_permissions))843 }844845 /// Create multiple items within a collection with explicitly specified initial parameters.846 ///847 /// # Permissions848 ///849 /// * Collection owner850 /// * Collection admin851 /// * Anyone if852 /// * Allow List is enabled, and853 /// * Address is added to allow list, and854 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])855 ///856 /// # Arguments857 ///858 /// * `collection_id`: ID of the collection to which the tokens would belong.859 /// * `data`: Explicit item creation data.860 #[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]861 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {862 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);863 let budget = budget::Value::new(NESTING_BUDGET);864865 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))866 }867868 /// Completely allow or disallow transfers for a particular collection.869 ///870 /// # Permissions871 ///872 /// * Collection owner873 ///874 /// # Arguments875 ///876 /// * `collection_id`: ID of the collection.877 /// * `value`: New value of the flag, are transfers allowed?878 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]879 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {880 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);881 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;882 target_collection.check_is_internal()?;883 target_collection.check_is_owner(&sender)?;884885 // =========886887 target_collection.limits.transfers_enabled = Some(value);888 target_collection.save()889 }890891 /// Destroy an item.892 ///893 /// # Permissions894 ///895 /// * Collection owner896 /// * Collection admin897 /// * Current item owner898 ///899 /// # Arguments900 ///901 /// * `collection_id`: ID of the collection to which the item belongs.902 /// * `item_id`: ID of item to burn.903 /// * `value`: Number of pieces of the item to destroy.904 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.905 /// * Fungible Mode: The desired number of pieces to burn.906 /// * Re-Fungible Mode: The desired number of pieces to burn.907 #[weight = T::CommonWeightInfo::burn_item()]908 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {909 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);910911 let post_info = dispatch_tx::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;912 if value == 1 {913 <NftTransferBasket<T>>::remove(collection_id, item_id);914 <NftApproveBasket<T>>::remove(collection_id, item_id);915 }916 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?917 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());918 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));919 Ok(post_info)920 }921922 /// Destroy a token on behalf of the owner as a non-owner account.923 ///924 /// See also: [`approve`][`Pallet::approve`].925 ///926 /// After this method executes, one approval is removed from the total so that927 /// the approved address will not be able to transfer this item again from this owner.928 ///929 /// # Permissions930 ///931 /// * Collection owner932 /// * Collection admin933 /// * Current token owner934 /// * Address approved by current item owner935 ///936 /// # Arguments937 ///938 /// * `from`: The owner of the burning item.939 /// * `collection_id`: ID of the collection to which the item belongs.940 /// * `item_id`: ID of item to burn.941 /// * `value`: Number of pieces to burn.942 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.943 /// * Fungible Mode: The desired number of pieces to burn.944 /// * Re-Fungible Mode: The desired number of pieces to burn.945 #[weight = T::CommonWeightInfo::burn_from()]946 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {947 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);948 let budget = budget::Value::new(NESTING_BUDGET);949950 dispatch_tx::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))951 }952953 /// Change ownership of the token.954 ///955 /// # Permissions956 ///957 /// * Collection owner958 /// * Collection admin959 /// * Current token owner960 ///961 /// # Arguments962 ///963 /// * `recipient`: Address of token recipient.964 /// * `collection_id`: ID of the collection the item belongs to.965 /// * `item_id`: ID of the item.966 /// * Non-Fungible Mode: Required.967 /// * Fungible Mode: Ignored.968 /// * Re-Fungible Mode: Required.969 ///970 /// * `value`: Amount to transfer.971 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.972 /// * Fungible Mode: The desired number of pieces to transfer.973 /// * Re-Fungible Mode: The desired number of pieces to transfer.974 #[weight = T::CommonWeightInfo::transfer()]975 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {976 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);977 let budget = budget::Value::new(NESTING_BUDGET);978979 dispatch_tx::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))980 }981982 /// Allow a non-permissioned address to transfer or burn an item.983 ///984 /// # Permissions985 ///986 /// * Collection owner987 /// * Collection admin988 /// * Current item owner989 ///990 /// # Arguments991 ///992 /// * `spender`: Account to be approved to make specific transactions on non-owned tokens.993 /// * `collection_id`: ID of the collection the item belongs to.994 /// * `item_id`: ID of the item transactions on which are now approved.995 /// * `amount`: Number of pieces of the item approved for a transaction (maximum of 1 for NFTs).996 /// Set to 0 to revoke the approval.997 #[weight = T::CommonWeightInfo::approve()]998 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {999 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);10001001 dispatch_tx::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))1002 }10031004 /// Change ownership of an item on behalf of the owner as a non-owner account.1005 ///1006 /// See the [`approve`][`Pallet::approve`] method for additional information.1007 ///1008 /// After this method executes, one approval is removed from the total so that1009 /// the approved address will not be able to transfer this item again from this owner.1010 ///1011 /// # Permissions1012 ///1013 /// * Collection owner1014 /// * Collection admin1015 /// * Current item owner1016 /// * Address approved by current item owner1017 ///1018 /// # Arguments1019 ///1020 /// * `from`: Address that currently owns the token.1021 /// * `recipient`: Address of the new token-owner-to-be.1022 /// * `collection_id`: ID of the collection the item.1023 /// * `item_id`: ID of the item to be transferred.1024 /// * `value`: Amount to transfer.1025 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.1026 /// * Fungible Mode: The desired number of pieces to transfer.1027 /// * Re-Fungible Mode: The desired number of pieces to transfer.1028 #[weight = T::CommonWeightInfo::transfer_from()]1029 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {1030 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1031 let budget = budget::Value::new(NESTING_BUDGET);10321033 dispatch_tx::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))1034 }10351036 /// Set specific limits of a collection. Empty, or None fields mean chain default.1037 ///1038 /// # Permissions1039 ///1040 /// * Collection owner1041 /// * Collection admin1042 ///1043 /// # Arguments1044 ///1045 /// * `collection_id`: ID of the modified collection.1046 /// * `new_limit`: New limits of the collection. Fields that are not set (None)1047 /// will not overwrite the old ones.1048 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1049 pub fn set_collection_limits(1050 origin,1051 collection_id: CollectionId,1052 new_limit: CollectionLimits,1053 ) -> DispatchResult {1054 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1055 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1056 target_collection.check_is_internal()?;1057 target_collection.check_is_owner_or_admin(&sender)?;1058 let old_limit = &target_collection.limits;10591060 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;10611062 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(1063 collection_id1064 ));10651066 target_collection.save()1067 }10681069 /// Set specific permissions of a collection. Empty, or None fields mean chain default.1070 ///1071 /// # Permissions1072 ///1073 /// * Collection owner1074 /// * Collection admin1075 ///1076 /// # Arguments1077 ///1078 /// * `collection_id`: ID of the modified collection.1079 /// * `new_permission`: New permissions of the collection. Fields that are not set (None)1080 /// will not overwrite the old ones.1081 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1082 pub fn set_collection_permissions(1083 origin,1084 collection_id: CollectionId,1085 new_permission: CollectionPermissions,1086 ) -> DispatchResult {1087 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1088 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1089 target_collection.check_is_internal()?;1090 target_collection.check_is_owner_or_admin(&sender)?;1091 let old_limit = &target_collection.permissions;10921093 target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_permission)?;10941095 <Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(1096 collection_id1097 ));10981099 target_collection.save()1100 }11011102 /// Re-partition a refungible token, while owning all of its parts/pieces.1103 ///1104 /// # Permissions1105 ///1106 /// * Token owner (must own every part)1107 ///1108 /// # Arguments1109 ///1110 /// * `collection_id`: ID of the collection the RFT belongs to.1111 /// * `token_id`: ID of the RFT.1112 /// * `amount`: New number of parts/pieces into which the token shall be partitioned.1113 #[weight = T::RefungibleExtensionsWeightInfo::repartition()]1114 pub fn repartition(1115 origin,1116 collection_id: CollectionId,1117 token_id: TokenId,1118 amount: u128,1119 ) -> DispatchResultWithPostInfo {1120 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1121 dispatch_tx::<T, _>(collection_id, |d| {1122 if let Some(refungible_extensions) = d.refungible_extensions() {1123 refungible_extensions.repartition(&sender, token_id, amount)1124 } else {1125 fail!(<Error<T>>::RepartitionCalledOnNonRefungibleCollection)1126 }1127 })1128 }1129 }1130}11311132impl<T: Config> Pallet<T> {1133 /// Force set `sponsor` for `collection`.1134 ///1135 /// Differs from [`set_collection_sponsor`][`Pallet::set_collection_sponsor`] in that confirmation1136 /// from the `sponsor` is not required.1137 ///1138 /// # Arguments1139 ///1140 /// * `sponsor`: ID of the account of the sponsor-to-be.1141 /// * `collection_id`: ID of the modified collection.1142 pub fn force_set_sponsor(sponsor: T::AccountId, collection_id: CollectionId) -> DispatchResult {1143 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1144 target_collection.check_is_internal()?;1145 target_collection.set_sponsor(sponsor.clone())?;11461147 Self::deposit_event(Event::<T>::CollectionSponsorSet(1148 collection_id,1149 sponsor.clone(),1150 ));11511152 ensure!(1153 target_collection.confirm_sponsorship(&sponsor)?,1154 Error::<T>::ConfirmUnsetSponsorFail1155 );11561157 Self::deposit_event(Event::<T>::SponsorshipConfirmed(collection_id, sponsor));11581159 target_collection.save()1160 }11611162 /// Force remove `sponsor` for `collection`.1163 ///1164 /// Differs from `remove_sponsor` in that1165 /// it doesn't require consent from the `owner` of the collection.1166 ///1167 /// # Arguments1168 ///1169 /// * `collection_id`: ID of the modified collection.1170 pub fn force_remove_collection_sponsor(collection_id: CollectionId) -> DispatchResult {1171 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1172 target_collection.check_is_internal()?;1173 target_collection.sponsorship = SponsorshipState::Disabled;11741175 Self::deposit_event(Event::<T>::CollectionSponsorRemoved(collection_id));11761177 target_collection.save()1178 }11791180 #[inline(always)]1181 pub(crate) fn destroy_collection_internal(1182 sender: T::CrossAccountId,1183 collection_id: CollectionId,1184 ) -> DispatchResult {1185 let collection = <CollectionHandle<T>>::try_get(collection_id)?;1186 collection.check_is_internal()?;11871188 T::CollectionDispatch::destroy(sender, collection)?;11891190 // TODO: basket cleanup should be moved elsewhere1191 // Maybe runtime dispatch.rs should perform it?11921193 let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1194 let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1195 let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);11961197 let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1198 let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1199 let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);12001201 Ok(())1202 }1203}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//! # Unique Pallet18//!19//! A pallet governing Unique transactions.20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The Unique pallet's purpose is to be the primary interface between28//! external users and the inner structure of the Unique chains.29//!30//! It also contains an implementation of [`CollectionHelpers`][`eth`],31//! an Ethereum contract dealing with collection operations.32//!33//! ## Interface34//!35//! ### Dispatchables36//!37//! - `create_collection` - Create a collection of tokens. **Deprecated**, use `create_collection_ex`.38//! - `create_collection_ex` - Create a collection of tokens with explicit parameters.39//! - `destroy_collection` - Destroy a collection if no tokens exist within.40//! - `add_to_allow_list` - Add an address to allow list.41//! - `remove_from_allow_list` - Remove an address from allow list.42//! - `change_collection_owner` - Change the owner of the collection.43//! - `add_collection_admin` - Add an admin to a collection.44//! - `remove_collection_admin` - Remove admin of a collection.45//! - `set_collection_sponsor` - Invite a new collection sponsor.46//! - `confirm_sponsorship` - Confirm own sponsorship of a collection, becoming the sponsor.47//! - `remove_collection_sponsor` - Remove a sponsor from a collection.48//! - `create_item` - Create an item within a collection.49//! - `create_multiple_items` - Create multiple items within a collection.50//! - `set_collection_properties` - Add or change collection properties.51//! - `delete_collection_properties` - Delete specified collection properties.52//! - `set_token_properties` - Add or change token properties.53//! - `delete_token_properties` - Delete token properties.54//! - `set_token_property_permissions` - Add or change token property permissions of a collection.55//! - `create_multiple_items_ex` - Create multiple items within a collection with explicitly specified initial parameters.56//! - `set_transfers_enabled_flag` - Completely allow or disallow transfers for a particular collection.57//! - `burn_item` - Destroy an item.58//! - `burn_from` - Destroy an item on behalf of the owner as a non-owner account.59//! - `transfer` - Change ownership of the token.60//! - `transfer_from` - Change ownership of the token on behalf of the owner as a non-owner account.61//! - `approve` - Allow a non-permissioned address to transfer or burn an item.62//! - `set_collection_limits` - Set specific limits of a collection.63//! - `set_collection_permissions` - Set specific permissions of a collection.64//! - `repartition` - Re-partition a refungible token, while owning all of its parts.6566#![recursion_limit = "1024"]67#![cfg_attr(not(feature = "std"), no_std)]68#![allow(69 clippy::too_many_arguments,70 clippy::unnecessary_mut_passed,71 clippy::unused_unit72)]7374extern crate alloc;7576use frame_support::{77 decl_module, decl_storage, decl_error, decl_event,78 dispatch::DispatchResult,79 ensure, fail,80 weights::{Weight},81 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},82 BoundedVec,83};84use scale_info::TypeInfo;85use frame_system::{self as system, ensure_signed};86use sp_std::{vec, vec::Vec};87use up_data_structs::{88 MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH,89 MAX_PROPERTIES_PER_ITEM, MAX_PROPERTY_KEY_LENGTH, MAX_PROPERTY_VALUE_LENGTH,90 MAX_COLLECTION_PROPERTIES_SIZE, COLLECTION_ADMINS_LIMIT, MAX_TOKEN_PROPERTIES_SIZE,91 CreateItemData, CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId,92 SponsorshipState, CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,93 PropertyKeyPermission,94};95use pallet_evm::account::CrossAccountId;96use pallet_common::{97 CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_tx,98 dispatch::CollectionDispatch, RefungibleExtensionsWeightInfo,99};100pub mod eth;101102#[cfg(feature = "runtime-benchmarks")]103pub mod benchmarking;104pub mod weights;105use weights::WeightInfo;106107/// A maximum number of levels of depth in the token nesting tree.108pub const NESTING_BUDGET: u32 = 5;109110decl_error! {111 /// Errors for the common Unique transactions.112 pub enum Error for Module<T: Config> {113 /// Decimal_points parameter must be lower than [`up_data_structs::MAX_DECIMAL_POINTS`].114 CollectionDecimalPointLimitExceeded,115 /// This address is not set as sponsor, use setCollectionSponsor first.116 ConfirmUnsetSponsorFail,117 /// Length of items properties must be greater than 0.118 EmptyArgument,119 /// Repertition is only supported by refungible collection.120 RepartitionCalledOnNonRefungibleCollection,121 }122}123124/// Configuration trait of this pallet.125pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {126 /// Overarching event type.127 type RuntimeEvent: From<Event<Self>> + Into<<Self as frame_system::Config>::RuntimeEvent>;128129 /// Weight information for extrinsics in this pallet.130 type WeightInfo: WeightInfo;131132 /// Weight information for common pallet operations.133 type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;134135 /// Weight info information for extra refungible pallet operations.136 type RefungibleExtensionsWeightInfo: RefungibleExtensionsWeightInfo;137}138139decl_event! {140 pub enum Event<T>141 where142 <T as frame_system::Config>::AccountId,143 <T as pallet_evm::Config>::CrossAccountId,144 {145 /// Collection sponsor was removed146 ///147 /// # Arguments148 /// * collection_id: ID of the affected collection.149 CollectionSponsorRemoved(CollectionId),150151 /// Collection admin was added152 ///153 /// # Arguments154 /// * collection_id: ID of the affected collection.155 /// * admin: Admin address.156 CollectionAdminAdded(CollectionId, CrossAccountId),157158 /// Collection owned was changed159 ///160 /// # Arguments161 /// * collection_id: ID of the affected collection.162 /// * owner: New owner address.163 CollectionOwnedChanged(CollectionId, AccountId),164165 /// Collection sponsor was set166 ///167 /// # Arguments168 /// * collection_id: ID of the affected collection.169 /// * owner: New sponsor address.170 CollectionSponsorSet(CollectionId, AccountId),171172 /// New sponsor was confirm173 ///174 /// # Arguments175 /// * collection_id: ID of the affected collection.176 /// * sponsor: New sponsor address.177 SponsorshipConfirmed(CollectionId, AccountId),178179 /// Collection admin was removed180 ///181 /// # Arguments182 /// * collection_id: ID of the affected collection.183 /// * admin: Removed admin address.184 CollectionAdminRemoved(CollectionId, CrossAccountId),185186 /// Address was removed from the allow list187 ///188 /// # Arguments189 /// * collection_id: ID of the affected collection.190 /// * user: Address of the removed account.191 AllowListAddressRemoved(CollectionId, CrossAccountId),192193 /// Address was added to the allow list194 ///195 /// # Arguments196 /// * collection_id: ID of the affected collection.197 /// * user: Address of the added account.198 AllowListAddressAdded(CollectionId, CrossAccountId),199200 /// Collection limits were set201 ///202 /// # Arguments203 /// * collection_id: ID of the affected collection.204 CollectionLimitSet(CollectionId),205206 /// Collection permissions were set207 ///208 /// # Arguments209 /// * collection_id: ID of the affected collection.210 CollectionPermissionSet(CollectionId),211 }212}213214type SelfWeightOf<T> = <T as Config>::WeightInfo;215216// # Used definitions217//218// ## User control levels219//220// chain-controlled - key is uncontrolled by user221// i.e autoincrementing index222// can use non-cryptographic hash223// real - key is controlled by user224// but it is hard to generate enough colliding values, i.e owner of signed txs225// can use non-cryptographic hash226// controlled - key is completly controlled by users227// i.e maps with mutable keys228// should use cryptographic hash229//230// ## User control level downgrade reasons231//232// ?1 - chain-controlled -> controlled233// collections/tokens can be destroyed, resulting in massive holes234// ?2 - chain-controlled -> controlled235// same as ?1, but can be only added, resulting in easier exploitation236// ?3 - real -> controlled237// no confirmation required, so addresses can be easily generated238decl_storage! {239 trait Store for Module<T: Config> as Unique {240241 //#region Private members242 /// Used for migrations243 ChainVersion: u64;244 //#endregion245246 //#region Tokens transfer sponosoring rate limit baskets247 /// (Collection id (controlled?2), who created (real))248 /// TODO: Off chain worker should remove from this map when collection gets removed249 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;250 /// Collection id (controlled?2), token id (controlled?2)251 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;252 /// Collection id (controlled?2), owning user (real)253 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;254 /// Collection id (controlled?2), token id (controlled?2)255 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;256 //#endregion257258 /// Variable metadata sponsoring259 /// Collection id (controlled?2), token id (controlled?2)260 #[deprecated]261 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;262 /// Last sponsoring of token property setting // todo:doc rephrase this and the following263 pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;264265 /// Last sponsoring of NFT approval in a collection266 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;267 /// Last sponsoring of fungible tokens approval in a collection268 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;269 /// Last sponsoring of RFT approval in a collection270 pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;271 }272}273274decl_module! {275 /// Type alias to Pallet, to be used by construct_runtime.276 pub struct Module<T: Config> for enum Call277 where278 origin: T::RuntimeOrigin279 {280 type Error = Error<T>;281282 #[doc = "A maximum number of levels of depth in the token nesting tree."]283 const NESTING_BUDGET: u32 = NESTING_BUDGET;284285 #[doc = "Maximal length of a collection name."]286 const MAX_COLLECTION_NAME_LENGTH: u32 = MAX_COLLECTION_NAME_LENGTH;287288 #[doc = "Maximal length of a collection description."]289 const MAX_COLLECTION_DESCRIPTION_LENGTH: u32 = MAX_COLLECTION_DESCRIPTION_LENGTH;290291 #[doc = "Maximal length of a token prefix."]292 const MAX_TOKEN_PREFIX_LENGTH: u32 = MAX_TOKEN_PREFIX_LENGTH;293294 #[doc = "Maximum admins per collection."]295 const COLLECTION_ADMINS_LIMIT: u32 = COLLECTION_ADMINS_LIMIT;296297 #[doc = "Maximal length of a property key."]298 const MAX_PROPERTY_KEY_LENGTH: u32 = MAX_PROPERTY_KEY_LENGTH;299300 #[doc = "Maximal length of a property value."]301 const MAX_PROPERTY_VALUE_LENGTH: u32 = MAX_PROPERTY_VALUE_LENGTH;302303 #[doc = "A maximum number of token properties."]304 const MAX_PROPERTIES_PER_ITEM: u32 = MAX_PROPERTIES_PER_ITEM;305306 #[doc = "Maximum size for all collection properties."]307 const MAX_COLLECTION_PROPERTIES_SIZE: u32 = MAX_COLLECTION_PROPERTIES_SIZE;308309 #[doc = "Maximum size of all token properties."]310 const MAX_TOKEN_PROPERTIES_SIZE: u32 = MAX_TOKEN_PROPERTIES_SIZE;311312 #[doc = "Default NFT collection limit."]313 const NFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::NFT);314315 #[doc = "Default RFT collection limit."]316 const RFT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::ReFungible);317318 #[doc = "Default FT collection limit."]319 const FT_DEFAULT_COLLECTION_LIMITS: CollectionLimits = CollectionLimits::with_default_limits(CollectionMode::Fungible(0));320321322 pub fn deposit_event() = default;323324 fn on_initialize(_now: T::BlockNumber) -> Weight {325 Weight::zero()326 }327328 fn on_runtime_upgrade() -> Weight {329 Weight::zero()330 }331332 /// Create a collection of tokens.333 ///334 /// Each Token may have multiple properties encoded as an array of bytes335 /// of certain length. The initial owner of the collection is set336 /// to the address that signed the transaction and can be changed later.337 ///338 /// Prefer the more advanced [`create_collection_ex`][`Pallet::create_collection_ex`] instead.339 ///340 /// # Permissions341 ///342 /// * Anyone - becomes the owner of the new collection.343 ///344 /// # Arguments345 ///346 /// * `collection_name`: Wide-character string with collection name347 /// (limit [`MAX_COLLECTION_NAME_LENGTH`]).348 /// * `collection_description`: Wide-character string with collection description349 /// (limit [`MAX_COLLECTION_DESCRIPTION_LENGTH`]).350 /// * `token_prefix`: Byte string containing the token prefix to mark a collection351 /// to which a token belongs (limit [`MAX_TOKEN_PREFIX_LENGTH`]).352 /// * `mode`: Type of items stored in the collection and type dependent data.353 // returns collection ID354 #[weight = <SelfWeightOf<T>>::create_collection()]355 #[deprecated(note = "`create_collection_ex` is more up-to-date and advanced, prefer it instead")]356 pub fn create_collection(357 origin,358 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,359 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,360 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,361 mode: CollectionMode362 ) -> DispatchResult {363 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {364 name: collection_name,365 description: collection_description,366 token_prefix,367 mode,368 ..Default::default()369 };370 Self::create_collection_ex(origin, data)371 }372373 /// Create a collection with explicit parameters.374 ///375 /// Prefer it to the deprecated [`create_collection`][`Pallet::create_collection`] method.376 ///377 /// # Permissions378 ///379 /// * Anyone - becomes the owner of the new collection.380 ///381 /// # Arguments382 ///383 /// * `data`: Explicit data of a collection used for its creation.384 #[weight = <SelfWeightOf<T>>::create_collection()]385 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {386 let sender = ensure_signed(origin)?;387388 // =========389 let sender = T::CrossAccountId::from_sub(sender);390 let _id = T::CollectionDispatch::create(sender.clone(), sender, data, Default::default())?;391392 Ok(())393 }394395 /// Destroy a collection if no tokens exist within.396 ///397 /// # Permissions398 ///399 /// * Collection owner400 ///401 /// # Arguments402 ///403 /// * `collection_id`: Collection to destroy.404 #[weight = <SelfWeightOf<T>>::destroy_collection()]405 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {406 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);407408 Self::destroy_collection_internal(sender, collection_id)409 }410411 /// Add an address to allow list.412 ///413 /// # Permissions414 ///415 /// * Collection owner416 /// * Collection admin417 ///418 /// # Arguments419 ///420 /// * `collection_id`: ID of the modified collection.421 /// * `address`: ID of the address to be added to the allowlist.422 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]423 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{424425 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);426 let collection = <CollectionHandle<T>>::try_get(collection_id)?;427 collection.check_is_internal()?;428429 <PalletCommon<T>>::toggle_allowlist(430 &collection,431 &sender,432 &address,433 true,434 )?;435436 Self::deposit_event(Event::<T>::AllowListAddressAdded(437 collection_id,438 address439 ));440441 Ok(())442 }443444 /// Remove an address from allow list.445 ///446 /// # Permissions447 ///448 /// * Collection owner449 /// * Collection admin450 ///451 /// # Arguments452 ///453 /// * `collection_id`: ID of the modified collection.454 /// * `address`: ID of the address to be removed from the allowlist.455 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]456 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{457458 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);459 let collection = <CollectionHandle<T>>::try_get(collection_id)?;460 collection.check_is_internal()?;461462 <PalletCommon<T>>::toggle_allowlist(463 &collection,464 &sender,465 &address,466 false,467 )?;468469 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(470 collection_id,471 address472 ));473474 Ok(())475 }476477 /// Change the owner of the collection.478 ///479 /// # Permissions480 ///481 /// * Collection owner482 ///483 /// # Arguments484 ///485 /// * `collection_id`: ID of the modified collection.486 /// * `new_owner`: ID of the account that will become the owner.487 #[weight = <SelfWeightOf<T>>::change_collection_owner()]488 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {489490 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);491492 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;493 target_collection.check_is_internal()?;494 target_collection.check_is_owner(&sender)?;495496 target_collection.owner = new_owner.clone();497 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(498 collection_id,499 new_owner500 ));501502 target_collection.save()503 }504505 /// Add an admin to a collection.506 ///507 /// NFT Collection can be controlled by multiple admin addresses508 /// (some which can also be servers, for example). Admins can issue509 /// and burn NFTs, as well as add and remove other admins,510 /// but cannot change NFT or Collection ownership.511 ///512 /// # Permissions513 ///514 /// * Collection owner515 /// * Collection admin516 ///517 /// # Arguments518 ///519 /// * `collection_id`: ID of the Collection to add an admin for.520 /// * `new_admin`: Address of new admin to add.521 #[weight = <SelfWeightOf<T>>::add_collection_admin()]522 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {523 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);524 let collection = <CollectionHandle<T>>::try_get(collection_id)?;525 collection.check_is_internal()?;526527 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(528 collection_id,529 new_admin_id.clone()530 ));531532 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)533 }534535 /// Remove admin of a collection.536 ///537 /// An admin address can remove itself. List of admins may become empty,538 /// in which case only Collection Owner will be able to add an Admin.539 ///540 /// # Permissions541 ///542 /// * Collection owner543 /// * Collection admin544 ///545 /// # Arguments546 ///547 /// * `collection_id`: ID of the collection to remove the admin for.548 /// * `account_id`: Address of the admin to remove.549 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]550 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {551 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);552 let collection = <CollectionHandle<T>>::try_get(collection_id)?;553 collection.check_is_internal()?;554555 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(556 collection_id,557 account_id.clone()558 ));559560 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)561 }562563 /// Set (invite) a new collection sponsor.564 ///565 /// If successful, confirmation from the sponsor-to-be will be pending.566 ///567 /// # Permissions568 ///569 /// * Collection owner570 /// * Collection admin571 ///572 /// # Arguments573 ///574 /// * `collection_id`: ID of the modified collection.575 /// * `new_sponsor`: ID of the account of the sponsor-to-be.576 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]577 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {578 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);579580 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;581 target_collection.check_is_owner_or_admin(&sender)?;582 target_collection.check_is_internal()?;583584 target_collection.set_sponsor(new_sponsor.clone())?;585586 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(587 collection_id,588 new_sponsor589 ));590591 target_collection.save()592 }593594 /// Confirm own sponsorship of a collection, becoming the sponsor.595 ///596 /// An invitation must be pending, see [`set_collection_sponsor`][`Pallet::set_collection_sponsor`].597 /// Sponsor can pay the fees of a transaction instead of the sender,598 /// but only within specified limits.599 ///600 /// # Permissions601 ///602 /// * Sponsor-to-be603 ///604 /// # Arguments605 ///606 /// * `collection_id`: ID of the collection with the pending sponsor.607 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]608 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {609 let sender = ensure_signed(origin)?;610611 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;612 target_collection.check_is_internal()?;613 ensure!(614 target_collection.confirm_sponsorship(&sender)?,615 Error::<T>::ConfirmUnsetSponsorFail616 );617618 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(619 collection_id,620 sender621 ));622623 target_collection.save()624 }625626 /// Remove a collection's a sponsor, making everyone pay for their own transactions.627 ///628 /// # Permissions629 ///630 /// * Collection owner631 ///632 /// # Arguments633 ///634 /// * `collection_id`: ID of the collection with the sponsor to remove.635 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]636 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {637 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);638639 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;640 target_collection.check_is_internal()?;641 target_collection.check_is_owner(&sender)?;642643 target_collection.sponsorship = SponsorshipState::Disabled;644645 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(646 collection_id647 ));648 target_collection.save()649 }650651 /// Mint an item within a collection.652 ///653 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].654 ///655 /// # Permissions656 ///657 /// * Collection owner658 /// * Collection admin659 /// * Anyone if660 /// * Allow List is enabled, and661 /// * Address is added to allow list, and662 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])663 ///664 /// # Arguments665 ///666 /// * `collection_id`: ID of the collection to which an item would belong.667 /// * `owner`: Address of the initial owner of the item.668 /// * `data`: Token data describing the item to store on chain.669 #[weight = T::CommonWeightInfo::create_item()]670 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {671 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);672 let budget = budget::Value::new(NESTING_BUDGET);673674 dispatch_tx::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))675 }676677 /// Create multiple items within a collection.678 ///679 /// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].680 ///681 /// # Permissions682 ///683 /// * Collection owner684 /// * Collection admin685 /// * Anyone if686 /// * Allow List is enabled, and687 /// * Address is added to the allow list, and688 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])689 ///690 /// # Arguments691 ///692 /// * `collection_id`: ID of the collection to which the tokens would belong.693 /// * `owner`: Address of the initial owner of the tokens.694 /// * `items_data`: Vector of data describing each item to be created.695 #[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]696 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {697 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);698 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);699 let budget = budget::Value::new(NESTING_BUDGET);700701 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))702 }703704 /// Add or change collection properties.705 ///706 /// # Permissions707 ///708 /// * Collection owner709 /// * Collection admin710 ///711 /// # Arguments712 ///713 /// * `collection_id`: ID of the modified collection.714 /// * `properties`: Vector of key-value pairs stored as the collection's metadata.715 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.716 #[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]717 pub fn set_collection_properties(718 origin,719 collection_id: CollectionId,720 properties: Vec<Property>721 ) -> DispatchResultWithPostInfo {722 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);723724 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);725726 dispatch_tx::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))727 }728729 /// Delete specified collection properties.730 ///731 /// # Permissions732 ///733 /// * Collection Owner734 /// * Collection Admin735 ///736 /// # Arguments737 ///738 /// * `collection_id`: ID of the modified collection.739 /// * `property_keys`: Vector of keys of the properties to be deleted.740 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.741 #[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]742 pub fn delete_collection_properties(743 origin,744 collection_id: CollectionId,745 property_keys: Vec<PropertyKey>,746 ) -> DispatchResultWithPostInfo {747 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);748749 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);750751 dispatch_tx::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))752 }753754 /// Add or change token properties according to collection's permissions.755 /// Currently properties only work with NFTs.756 ///757 /// # Permissions758 ///759 /// * Depends on collection's token property permissions and specified property mutability:760 /// * Collection owner761 /// * Collection admin762 /// * Token owner763 ///764 /// See [`set_token_property_permissions`][`Pallet::set_token_property_permissions`].765 ///766 /// # Arguments767 ///768 /// * `collection_id: ID of the collection to which the token belongs.769 /// * `token_id`: ID of the modified token.770 /// * `properties`: Vector of key-value pairs stored as the token's metadata.771 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.772 #[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]773 pub fn set_token_properties(774 origin,775 collection_id: CollectionId,776 token_id: TokenId,777 properties: Vec<Property>778 ) -> DispatchResultWithPostInfo {779 ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);780781 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);782 let budget = budget::Value::new(NESTING_BUDGET);783784 dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties, &budget))785 }786787 /// Delete specified token properties. Currently properties only work with NFTs.788 ///789 /// # Permissions790 ///791 /// * Depends on collection's token property permissions and specified property mutability:792 /// * Collection owner793 /// * Collection admin794 /// * Token owner795 ///796 /// # Arguments797 ///798 /// * `collection_id`: ID of the collection to which the token belongs.799 /// * `token_id`: ID of the modified token.800 /// * `property_keys`: Vector of keys of the properties to be deleted.801 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.802 #[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]803 pub fn delete_token_properties(804 origin,805 collection_id: CollectionId,806 token_id: TokenId,807 property_keys: Vec<PropertyKey>808 ) -> DispatchResultWithPostInfo {809 ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);810811 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);812 let budget = budget::Value::new(NESTING_BUDGET);813814 dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys, &budget))815 }816817 /// Add or change token property permissions of a collection.818 ///819 /// Without a permission for a particular key, a property with that key820 /// cannot be created in a token.821 ///822 /// # Permissions823 ///824 /// * Collection owner825 /// * Collection admin826 ///827 /// # Arguments828 ///829 /// * `collection_id`: ID of the modified collection.830 /// * `property_permissions`: Vector of permissions for property keys.831 /// Keys support Latin letters, `-`, `_`, and `.` as symbols.832 #[weight = T::CommonWeightInfo::set_token_property_permissions(property_permissions.len() as u32)]833 pub fn set_token_property_permissions(834 origin,835 collection_id: CollectionId,836 property_permissions: Vec<PropertyKeyPermission>,837 ) -> DispatchResultWithPostInfo {838 ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);839840 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);841842 dispatch_tx::<T, _>(collection_id, |d| d.set_token_property_permissions(&sender, property_permissions))843 }844845 /// Create multiple items within a collection with explicitly specified initial parameters.846 ///847 /// # Permissions848 ///849 /// * Collection owner850 /// * Collection admin851 /// * Anyone if852 /// * Allow List is enabled, and853 /// * Address is added to allow list, and854 /// * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])855 ///856 /// # Arguments857 ///858 /// * `collection_id`: ID of the collection to which the tokens would belong.859 /// * `data`: Explicit item creation data.860 #[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]861 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {862 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);863 let budget = budget::Value::new(NESTING_BUDGET);864865 dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))866 }867868 /// Completely allow or disallow transfers for a particular collection.869 ///870 /// # Permissions871 ///872 /// * Collection owner873 ///874 /// # Arguments875 ///876 /// * `collection_id`: ID of the collection.877 /// * `value`: New value of the flag, are transfers allowed?878 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]879 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {880 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);881 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;882 target_collection.check_is_internal()?;883 target_collection.check_is_owner(&sender)?;884885 // =========886887 target_collection.limits.transfers_enabled = Some(value);888 target_collection.save()889 }890891 /// Destroy an item.892 ///893 /// # Permissions894 ///895 /// * Collection owner896 /// * Collection admin897 /// * Current item owner898 ///899 /// # Arguments900 ///901 /// * `collection_id`: ID of the collection to which the item belongs.902 /// * `item_id`: ID of item to burn.903 /// * `value`: Number of pieces of the item to destroy.904 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.905 /// * Fungible Mode: The desired number of pieces to burn.906 /// * Re-Fungible Mode: The desired number of pieces to burn.907 #[weight = T::CommonWeightInfo::burn_item()]908 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {909 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);910911 let post_info = dispatch_tx::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;912 if value == 1 {913 <NftTransferBasket<T>>::remove(collection_id, item_id);914 <NftApproveBasket<T>>::remove(collection_id, item_id);915 }916 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?917 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());918 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));919 Ok(post_info)920 }921922 /// Destroy a token on behalf of the owner as a non-owner account.923 ///924 /// See also: [`approve`][`Pallet::approve`].925 ///926 /// After this method executes, one approval is removed from the total so that927 /// the approved address will not be able to transfer this item again from this owner.928 ///929 /// # Permissions930 ///931 /// * Collection owner932 /// * Collection admin933 /// * Current token owner934 /// * Address approved by current item owner935 ///936 /// # Arguments937 ///938 /// * `from`: The owner of the burning item.939 /// * `collection_id`: ID of the collection to which the item belongs.940 /// * `item_id`: ID of item to burn.941 /// * `value`: Number of pieces to burn.942 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.943 /// * Fungible Mode: The desired number of pieces to burn.944 /// * Re-Fungible Mode: The desired number of pieces to burn.945 #[weight = T::CommonWeightInfo::burn_from()]946 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {947 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);948 let budget = budget::Value::new(NESTING_BUDGET);949950 dispatch_tx::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))951 }952953 /// Change ownership of the token.954 ///955 /// # Permissions956 ///957 /// * Collection owner958 /// * Collection admin959 /// * Current token owner960 ///961 /// # Arguments962 ///963 /// * `recipient`: Address of token recipient.964 /// * `collection_id`: ID of the collection the item belongs to.965 /// * `item_id`: ID of the item.966 /// * Non-Fungible Mode: Required.967 /// * Fungible Mode: Ignored.968 /// * Re-Fungible Mode: Required.969 ///970 /// * `value`: Amount to transfer.971 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.972 /// * Fungible Mode: The desired number of pieces to transfer.973 /// * Re-Fungible Mode: The desired number of pieces to transfer.974 #[weight = T::CommonWeightInfo::transfer()]975 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {976 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);977 let budget = budget::Value::new(NESTING_BUDGET);978979 dispatch_tx::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))980 }981982 /// Allow a non-permissioned address to transfer or burn an item.983 ///984 /// # Permissions985 ///986 /// * Collection owner987 /// * Collection admin988 /// * Current item owner989 ///990 /// # Arguments991 ///992 /// * `spender`: Account to be approved to make specific transactions on non-owned tokens.993 /// * `collection_id`: ID of the collection the item belongs to.994 /// * `item_id`: ID of the item transactions on which are now approved.995 /// * `amount`: Number of pieces of the item approved for a transaction (maximum of 1 for NFTs).996 /// Set to 0 to revoke the approval.997 #[weight = T::CommonWeightInfo::approve()]998 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {999 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);10001001 dispatch_tx::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))1002 }10031004 /// Change ownership of an item on behalf of the owner as a non-owner account.1005 ///1006 /// See the [`approve`][`Pallet::approve`] method for additional information.1007 ///1008 /// After this method executes, one approval is removed from the total so that1009 /// the approved address will not be able to transfer this item again from this owner.1010 ///1011 /// # Permissions1012 ///1013 /// * Collection owner1014 /// * Collection admin1015 /// * Current item owner1016 /// * Address approved by current item owner1017 ///1018 /// # Arguments1019 ///1020 /// * `from`: Address that currently owns the token.1021 /// * `recipient`: Address of the new token-owner-to-be.1022 /// * `collection_id`: ID of the collection the item.1023 /// * `item_id`: ID of the item to be transferred.1024 /// * `value`: Amount to transfer.1025 /// * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.1026 /// * Fungible Mode: The desired number of pieces to transfer.1027 /// * Re-Fungible Mode: The desired number of pieces to transfer.1028 #[weight = T::CommonWeightInfo::transfer_from()]1029 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {1030 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1031 let budget = budget::Value::new(NESTING_BUDGET);10321033 dispatch_tx::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))1034 }10351036 /// Set specific limits of a collection. Empty, or None fields mean chain default.1037 ///1038 /// # Permissions1039 ///1040 /// * Collection owner1041 /// * Collection admin1042 ///1043 /// # Arguments1044 ///1045 /// * `collection_id`: ID of the modified collection.1046 /// * `new_limit`: New limits of the collection. Fields that are not set (None)1047 /// will not overwrite the old ones.1048 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1049 pub fn set_collection_limits(1050 origin,1051 collection_id: CollectionId,1052 new_limit: CollectionLimits,1053 ) -> DispatchResult {1054 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1055 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1056 target_collection.check_is_internal()?;1057 target_collection.check_is_owner_or_admin(&sender)?;1058 let old_limit = &target_collection.limits;10591060 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;10611062 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(1063 collection_id1064 ));10651066 target_collection.save()1067 }10681069 /// Set specific permissions of a collection. Empty, or None fields mean chain default.1070 ///1071 /// # Permissions1072 ///1073 /// * Collection owner1074 /// * Collection admin1075 ///1076 /// # Arguments1077 ///1078 /// * `collection_id`: ID of the modified collection.1079 /// * `new_permission`: New permissions of the collection. Fields that are not set (None)1080 /// will not overwrite the old ones.1081 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1082 pub fn set_collection_permissions(1083 origin,1084 collection_id: CollectionId,1085 new_permission: CollectionPermissions,1086 ) -> DispatchResult {1087 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1088 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1089 target_collection.check_is_internal()?;1090 target_collection.check_is_owner_or_admin(&sender)?;1091 let old_limit = &target_collection.permissions;10921093 target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_permission)?;10941095 <Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(1096 collection_id1097 ));10981099 target_collection.save()1100 }11011102 /// Re-partition a refungible token, while owning all of its parts/pieces.1103 ///1104 /// # Permissions1105 ///1106 /// * Token owner (must own every part)1107 ///1108 /// # Arguments1109 ///1110 /// * `collection_id`: ID of the collection the RFT belongs to.1111 /// * `token_id`: ID of the RFT.1112 /// * `amount`: New number of parts/pieces into which the token shall be partitioned.1113 #[weight = T::RefungibleExtensionsWeightInfo::repartition()]1114 pub fn repartition(1115 origin,1116 collection_id: CollectionId,1117 token_id: TokenId,1118 amount: u128,1119 ) -> DispatchResultWithPostInfo {1120 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1121 dispatch_tx::<T, _>(collection_id, |d| {1122 if let Some(refungible_extensions) = d.refungible_extensions() {1123 refungible_extensions.repartition(&sender, token_id, amount)1124 } else {1125 fail!(<Error<T>>::RepartitionCalledOnNonRefungibleCollection)1126 }1127 })1128 }11291130 /// Sets or unsets the approval of a given operator.1131 ///1132 /// An operator is allowed to transfer all tokens of the sender on their behalf.1133 ///1134 /// # Arguments1135 ///1136 /// * `owner`: Token owner1137 /// * `operator`: Operator1138 /// * `approve`: Is operator enabled or disabled1139 #[weight = T::CommonWeightInfo::set_approval_for_all()]1140 pub fn set_approval_for_all(1141 origin,1142 collection_id: CollectionId,1143 operator: T::CrossAccountId,1144 approve: bool,1145 ) -> DispatchResultWithPostInfo {1146 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1147 dispatch_tx::<T, _>(collection_id, |d| {1148 d.set_approval_for_all(sender, operator, approve)1149 })1150 }1151 }1152}11531154impl<T: Config> Pallet<T> {1155 /// Force set `sponsor` for `collection`.1156 ///1157 /// Differs from [`set_collection_sponsor`][`Pallet::set_collection_sponsor`] in that confirmation1158 /// from the `sponsor` is not required.1159 ///1160 /// # Arguments1161 ///1162 /// * `sponsor`: ID of the account of the sponsor-to-be.1163 /// * `collection_id`: ID of the modified collection.1164 pub fn force_set_sponsor(sponsor: T::AccountId, collection_id: CollectionId) -> DispatchResult {1165 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1166 target_collection.check_is_internal()?;1167 target_collection.set_sponsor(sponsor.clone())?;11681169 Self::deposit_event(Event::<T>::CollectionSponsorSet(1170 collection_id,1171 sponsor.clone(),1172 ));11731174 ensure!(1175 target_collection.confirm_sponsorship(&sponsor)?,1176 Error::<T>::ConfirmUnsetSponsorFail1177 );11781179 Self::deposit_event(Event::<T>::SponsorshipConfirmed(collection_id, sponsor));11801181 target_collection.save()1182 }11831184 /// Force remove `sponsor` for `collection`.1185 ///1186 /// Differs from `remove_sponsor` in that1187 /// it doesn't require consent from the `owner` of the collection.1188 ///1189 /// # Arguments1190 ///1191 /// * `collection_id`: ID of the modified collection.1192 pub fn force_remove_collection_sponsor(collection_id: CollectionId) -> DispatchResult {1193 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1194 target_collection.check_is_internal()?;1195 target_collection.sponsorship = SponsorshipState::Disabled;11961197 Self::deposit_event(Event::<T>::CollectionSponsorRemoved(collection_id));11981199 target_collection.save()1200 }12011202 #[inline(always)]1203 pub(crate) fn destroy_collection_internal(1204 sender: T::CrossAccountId,1205 collection_id: CollectionId,1206 ) -> DispatchResult {1207 let collection = <CollectionHandle<T>>::try_get(collection_id)?;1208 collection.check_is_internal()?;12091210 T::CollectionDispatch::destroy(sender, collection)?;12111212 // TODO: basket cleanup should be moved elsewhere1213 // Maybe runtime dispatch.rs should perform it?12141215 let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1216 let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1217 let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);12181219 let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1220 let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1221 let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);12221223 Ok(())1224 }1225}primitives/rpc/src/lib.rsdiffbeforeafterboth--- a/primitives/rpc/src/lib.rs
+++ b/primitives/rpc/src/lib.rs
@@ -132,5 +132,8 @@
fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Result<Option<u128>>;
fn token_owners(collection: CollectionId, token: TokenId) -> Result<Vec<CrossAccountId>>;
+
+ /// Get whether an operator is approved by a given owner.
+ fn is_approved_for_all(collection: CollectionId, owner: CrossAccountId, operator: CrossAccountId) -> Result<bool>;
}
}
runtime/common/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/runtime_apis.rs
+++ b/runtime/common/runtime_apis.rs
@@ -187,6 +187,10 @@
fn total_pieces(collection: CollectionId, token_id: TokenId) -> Result<Option<u128>, DispatchError> {
dispatch_unique_runtime!(collection.total_pieces(token_id))
}
+
+ fn is_approved_for_all(collection: CollectionId, owner: CrossAccountId, operator: CrossAccountId) -> Result<bool, DispatchError> {
+ dispatch_unique_runtime!(collection.is_approved_for_all(owner, operator))
+ }
}
impl app_promotion_rpc::AppPromotionApi<Block, BlockNumber, CrossAccountId, AccountId> for Runtime {
runtime/common/weights.rsdiffbeforeafterboth--- a/runtime/common/weights.rs
+++ b/runtime/common/weights.rs
@@ -120,6 +120,10 @@
fn token_owner() -> Weight {
max_weight_of!(token_owner())
}
+
+ fn set_approval_for_all() -> Weight {
+ max_weight_of!(set_approval_for_all())
+ }
}
#[cfg(feature = "refungible")]
tests/src/approve.test.tsdiffbeforeafterboth--- a/tests/src/approve.test.ts
+++ b/tests/src/approve.test.ts
@@ -603,3 +603,40 @@
await expect(approveTx()).to.be.rejected;
});
});
+
+describe('Normal user can approve other users to be wallet operator:', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({filename: __filename});
+ [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
+ });
+ });
+
+ itSub('[nft] Enable and disable approval', async ({helper}) => {
+ const {collectionId} = await helper.nft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+ await helper.nft.mintToken(alice, {collectionId: collectionId, owner: alice.address});
+ const checkBeforeApprovalTx = () => helper.nft.isApprovedForAll(collectionId, {Substrate: alice.address}, {Substrate: bob.address});
+ expect(await checkBeforeApprovalTx()).to.be.false;
+ await helper.nft.setApprovalForAll(alice, collectionId, {Substrate: bob.address}, true);
+ const checkAfterApprovalTx = () => helper.nft.isApprovedForAll(collectionId, {Substrate: alice.address}, {Substrate: bob.address});
+ expect(await checkAfterApprovalTx()).to.be.true;
+ await helper.nft.setApprovalForAll(alice, collectionId, {Substrate: bob.address}, false);
+ const checkAfterDisapprovalTx = () => helper.nft.isApprovedForAll(collectionId, {Substrate: alice.address}, {Substrate: bob.address});
+ expect(await checkAfterDisapprovalTx()).to.be.false;
+ });
+
+ itSub.ifWithPallets('[rft] Enable and disable approval', [Pallets.ReFungible], async ({helper}) => {
+ const {collectionId} = await helper.rft.mintCollection(alice, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+ const checkBeforeApprovalTx = () => helper.rft.isApprovedForAll(collectionId, {Substrate: alice.address}, {Substrate: bob.address});
+ expect(await checkBeforeApprovalTx()).to.be.false;
+ await helper.rft.setApprovalForAll(alice, collectionId, {Substrate: bob.address}, true);
+ const checkAfterApprovalTx = () => helper.rft.isApprovedForAll(collectionId, {Substrate: alice.address}, {Substrate: bob.address});
+ expect(await checkAfterApprovalTx()).to.be.true;
+ await helper.rft.setApprovalForAll(alice, collectionId, {Substrate: bob.address}, false);
+ const checkAfterDisapprovalTx = () => helper.rft.isApprovedForAll(collectionId, {Substrate: alice.address}, {Substrate: bob.address});
+ expect(await checkAfterDisapprovalTx()).to.be.false;
+ });
+});
tests/src/eth/abi/nonFungible.jsondiffbeforeafterboth--- a/tests/src/eth/abi/nonFungible.json
+++ b/tests/src/eth/abi/nonFungible.json
@@ -384,7 +384,7 @@
{ "internalType": "address", "name": "operator", "type": "address" }
],
"name": "isApprovedForAll",
- "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
tests/src/eth/abi/reFungible.jsondiffbeforeafterboth--- a/tests/src/eth/abi/reFungible.json
+++ b/tests/src/eth/abi/reFungible.json
@@ -366,7 +366,7 @@
{ "internalType": "address", "name": "operator", "type": "address" }
],
"name": "isApprovedForAll",
- "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
tests/src/eth/api/UniqueNFT.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -671,7 +671,10 @@
/// or in textual repr: approve(address,uint256)
function approve(address approved, uint256 tokenId) external;
- /// @dev Not implemented
+ /// @notice Sets or unsets the approval of a given operator.
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// @param operator Operator
+ /// @param approved Is operator enabled or disabled
/// @dev EVM selector for this function is: 0xa22cb465,
/// or in textual repr: setApprovalForAll(address,bool)
function setApprovalForAll(address operator, bool approved) external;
@@ -681,10 +684,10 @@
/// or in textual repr: getApproved(uint256)
function getApproved(uint256 tokenId) external view returns (address);
- /// @dev Not implemented
+ /// @notice Tells whether an operator is approved by a given owner.
/// @dev EVM selector for this function is: 0xe985e9c5,
/// or in textual repr: isApprovedForAll(address,address)
- function isApprovedForAll(address owner, address operator) external view returns (address);
+ function isApprovedForAll(address owner, address operator) external view returns (bool);
/// @notice Returns collection helper contract address
/// @dev EVM selector for this function is: 0x1896cce6,
tests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -668,7 +668,10 @@
/// or in textual repr: approve(address,uint256)
function approve(address approved, uint256 tokenId) external;
- /// @dev Not implemented
+ /// @notice Sets or unsets the approval of a given operator.
+ /// An operator is allowed to transfer all tokens of the sender on their behalf.
+ /// @param operator Operator
+ /// @param approved Is operator enabled or disabled
/// @dev EVM selector for this function is: 0xa22cb465,
/// or in textual repr: setApprovalForAll(address,bool)
function setApprovalForAll(address operator, bool approved) external;
@@ -678,10 +681,10 @@
/// or in textual repr: getApproved(uint256)
function getApproved(uint256 tokenId) external view returns (address);
- /// @dev Not implemented
+ /// @notice Tells whether an operator is approved by a given owner.
/// @dev EVM selector for this function is: 0xe985e9c5,
/// or in textual repr: isApprovedForAll(address,address)
- function isApprovedForAll(address owner, address operator) external view returns (address);
+ function isApprovedForAll(address owner, address operator) external view returns (bool);
/// @notice Returns collection helper contract address
/// @dev EVM selector for this function is: 0x1896cce6,
tests/src/eth/nonFungible.test.tsdiffbeforeafterboth--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -249,6 +249,114 @@
}
});
+ itEth('Can perform setApprovalForAll()', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const operator = helper.eth.createAccount();
+
+ const collection = await helper.nft.mintCollection(minter, {});
+
+ const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+
+ const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();
+ expect(approvedBefore).to.be.equal(false);
+
+ {
+ const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});
+
+ expect(result.events.ApprovalForAll).to.be.like({
+ address: collectionAddress,
+ event: 'ApprovalForAll',
+ returnValues: {
+ owner,
+ operator,
+ approved: true,
+ },
+ });
+
+ const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();
+ expect(approvedAfter).to.be.equal(true);
+ }
+
+ {
+ const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});
+
+ expect(result.events.ApprovalForAll).to.be.like({
+ address: collectionAddress,
+ event: 'ApprovalForAll',
+ returnValues: {
+ owner,
+ operator,
+ approved: false,
+ },
+ });
+
+ const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();
+ expect(approvedAfter).to.be.equal(false);
+ }
+ });
+
+ itEth('Can perform burn with ApprovalForAll', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const operator = await helper.eth.createAccountWithBalance(donor, 100n);
+
+ const token = await collection.mintToken(minter, {Ethereum: owner});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'nft');
+
+ {
+ await contract.methods.setApprovalForAll(operator, true).send({from: owner});
+ const ownerCross = helper.ethCrossAccount.fromAddress(owner);
+ const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});
+ const events = result.events.Transfer;
+
+ expect(events).to.be.like({
+ address,
+ event: 'Transfer',
+ returnValues: {
+ from: owner,
+ to: '0x0000000000000000000000000000000000000000',
+ tokenId: token.tokenId.toString(),
+ },
+ });
+ }
+ });
+
+ itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const operator = await helper.eth.createAccountWithBalance(donor);
+ const receiver = charlie;
+
+ const token = await collection.mintToken(minter, {Ethereum: owner});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'nft');
+
+ {
+ await contract.methods.setApprovalForAll(operator, true).send({from: owner});
+ const ownerCross = helper.ethCrossAccount.fromAddress(owner);
+ const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);
+ const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});
+ const event = result.events.Transfer;
+ expect(event).to.be.like({
+ address: helper.ethAddress.fromCollectionId(collection.collectionId),
+ event: 'Transfer',
+ returnValues: {
+ from: owner,
+ to: helper.address.substrateToEth(receiver.address),
+ tokenId: token.tokenId.toString(),
+ },
+ });
+ }
+
+ expect(await token.getOwner()).to.be.like({Substrate: receiver.address});
+ });
+
itEth('Can perform burnFromCross()', async ({helper}) => {
const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
const ownerSub = bob;
@@ -822,3 +930,53 @@
expect(symbol).to.equal('CHANGE');
});
});
+
+describe('Negative tests', () => {
+ let donor: IKeyringPair;
+ let minter: IKeyringPair;
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async function() {
+ await usingEthPlaygrounds(async (helper, privateKey) => {
+ donor = await privateKey({filename: __filename});
+ [minter, alice, bob] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
+ });
+ });
+
+ itEth('[negative] Cant perform burn without approval', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const owner = bob;
+ const spender = await helper.eth.createAccountWithBalance(donor, 100n);
+
+ const token = await collection.mintToken(minter, {Substrate: owner.address});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'nft');
+
+ {
+ const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);
+ await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;
+ }
+ });
+
+ itEth('[negative] Cant perform transfer without approval', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+ const owner = bob;
+ const receiver = alice;
+
+ const spender = await helper.eth.createAccountWithBalance(donor, 100n);
+
+ const token = await collection.mintToken(minter, {Substrate: owner.address});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'nft');
+
+ {
+ const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);
+ const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);
+ await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;
+ }
+ });
+});
tests/src/eth/reFungible.test.tsdiffbeforeafterboth--- a/tests/src/eth/reFungible.test.ts
+++ b/tests/src/eth/reFungible.test.ts
@@ -169,6 +169,136 @@
}
});
+ itEth('Can perform setApprovalForAll()', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const operator = helper.eth.createAccount();
+
+ const collection = await helper.rft.mintCollection(minter, {});
+
+ const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
+
+ const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();
+ expect(approvedBefore).to.be.equal(false);
+
+ {
+ const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});
+
+ expect(result.events.ApprovalForAll).to.be.like({
+ address: collectionAddress,
+ event: 'ApprovalForAll',
+ returnValues: {
+ owner,
+ operator,
+ approved: true,
+ },
+ });
+
+ const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();
+ expect(approvedAfter).to.be.equal(true);
+ }
+
+ {
+ const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});
+
+ expect(result.events.ApprovalForAll).to.be.like({
+ address: collectionAddress,
+ event: 'ApprovalForAll',
+ returnValues: {
+ owner,
+ operator,
+ approved: false,
+ },
+ });
+
+ const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();
+ expect(approvedAfter).to.be.equal(false);
+ }
+ });
+
+ itEth('Can perform burn with ApprovalForAll', async ({helper}) => {
+ const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const operator = await helper.eth.createAccountWithBalance(donor, 100n);
+
+ const token = await collection.mintToken(minter, 100n, {Ethereum: owner});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'rft');
+
+ {
+ await contract.methods.setApprovalForAll(operator, true).send({from: owner});
+ const ownerCross = helper.ethCrossAccount.fromAddress(owner);
+ const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});
+ const events = result.events.Transfer;
+
+ expect(events).to.be.like({
+ address,
+ event: 'Transfer',
+ returnValues: {
+ from: owner,
+ to: '0x0000000000000000000000000000000000000000',
+ tokenId: token.tokenId.toString(),
+ },
+ });
+ }
+ });
+
+ itEth('Can perform burn with approve and approvalForAll', async ({helper}) => {
+ const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const operator = await helper.eth.createAccountWithBalance(donor, 100n);
+
+ const token = await collection.mintToken(minter, 100n, {Ethereum: owner});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'rft');
+
+ const rftToken = helper.ethNativeContract.rftTokenById(token.collectionId, token.tokenId, owner);
+
+ {
+ await rftToken.methods.approve(operator, 15n).send({from: owner});
+ await contract.methods.setApprovalForAll(operator, true).send({from: owner});
+ await rftToken.methods.burnFrom(owner, 10n).send({from: operator});
+ const allowance = await rftToken.methods.allowance(owner, operator).call();
+ expect(allowance).to.be.equal('5');
+ }
+ });
+
+ itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {
+ const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const operator = await helper.eth.createAccountWithBalance(donor);
+ const receiver = charlie;
+
+ const token = await collection.mintToken(minter, 100n, {Ethereum: owner});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'rft');
+
+ {
+ await contract.methods.setApprovalForAll(operator, true).send({from: owner});
+ const ownerCross = helper.ethCrossAccount.fromAddress(owner);
+ const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);
+ const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});
+ const event = result.events.Transfer;
+ expect(event).to.be.like({
+ address: helper.ethAddress.fromCollectionId(collection.collectionId),
+ event: 'Transfer',
+ returnValues: {
+ from: owner,
+ to: helper.address.substrateToEth(receiver.address),
+ tokenId: token.tokenId.toString(),
+ },
+ });
+ }
+
+ expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]);
+ });
+
itEth('Can perform burn()', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Burny', '6', '6');
@@ -594,3 +724,52 @@
expect(symbol).to.equal('12');
});
});
+
+describe('Negative tests', () => {
+ let donor: IKeyringPair;
+ let minter: IKeyringPair;
+ let alice: IKeyringPair;
+
+ before(async function() {
+ await usingEthPlaygrounds(async (helper, privateKey) => {
+ donor = await privateKey({filename: __filename});
+ [minter, alice] = await helper.arrange.createAccounts([100n, 100n], donor);
+ });
+ });
+
+ itEth('[negative] Cant perform burn without approval', async ({helper}) => {
+ const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const owner = await helper.eth.createAccountWithBalance(donor, 100n);
+ const spender = await helper.eth.createAccountWithBalance(donor, 100n);
+
+ const token = await collection.mintToken(minter, 100n, {Ethereum: owner});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'rft');
+
+ {
+ const ownerCross = helper.ethCrossAccount.fromAddress(owner);
+ await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;
+ }
+ });
+
+ itEth('[negative] Cant perform transfer without approval', async ({helper}) => {
+ const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});
+ const owner = await helper.eth.createAccountWithBalance(donor, 100n);
+ const receiver = alice;
+
+ const spender = await helper.eth.createAccountWithBalance(donor, 100n);
+
+ const token = await collection.mintToken(minter, 100n, {Ethereum: owner});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'rft');
+
+ {
+ const ownerCross = helper.ethCrossAccount.fromAddress(owner);
+ const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);
+ await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;
+ }
+ });
+});
tests/src/interfaces/augment-api-errors.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-errors.ts
+++ b/tests/src/interfaces/augment-api-errors.ts
@@ -386,6 +386,10 @@
**/
NotFungibleDataUsedToMintFungibleCollectionToken: AugmentedError<ApiType>;
/**
+ * Setting approval for all is not allowed.
+ **/
+ SettingApprovalForAllNotAllowed: AugmentedError<ApiType>;
+ /**
* Setting item properties is not allowed.
**/
SettingPropertiesNotAllowed: AugmentedError<ApiType>;
tests/src/interfaces/augment-api-events.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-events.ts
+++ b/tests/src/interfaces/augment-api-events.ts
@@ -107,6 +107,10 @@
**/
Approved: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
/**
+ * Amount pieces of token owned by `sender` was approved for `spender`.
+ **/
+ ApprovedForAll: AugmentedEvent<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, bool]>;
+ /**
* New collection was created
**/
CollectionCreated: AugmentedEvent<ApiType, [u32, u8, AccountId32]>;
tests/src/interfaces/augment-api-query.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-query.ts
+++ b/tests/src/interfaces/augment-api-query.ts
@@ -442,6 +442,10 @@
**/
tokensMinted: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
/**
+ * Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.
+ **/
+ walletOperator: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, arg3: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<Option<bool>>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]>;
+ /**
* Generic query
**/
[key: string]: QueryableStorageEntry<ApiType>;
@@ -645,6 +649,10 @@
**/
totalSupply: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<u128>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
/**
+ * Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.
+ **/
+ walletOperator: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, arg3: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<Option<bool>>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]>;
+ /**
* Generic query
**/
[key: string]: QueryableStorageEntry<ApiType>;
tests/src/interfaces/augment-api-rpc.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-rpc.ts
+++ b/tests/src/interfaces/augment-api-rpc.ts
@@ -720,6 +720,10 @@
**/
effectiveCollectionLimits: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Option<UpDataStructsCollectionLimits>>>;
/**
+ * Tells whether an operator is approved by a given owner.
+ **/
+ isApprovedForAll: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, owner: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, operator: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Option<bool>>>;
+ /**
* Get the last token ID created in a collection
**/
lastTokenId: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<u32>>;
tests/src/interfaces/augment-api-tx.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-tx.ts
+++ b/tests/src/interfaces/augment-api-tx.ts
@@ -1545,6 +1545,18 @@
**/
repartition: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, amount: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, u32, u128]>;
/**
+ * Sets or unsets the approval of a given operator.
+ *
+ * An operator is allowed to transfer all tokens of the sender on their behalf.
+ *
+ * # Arguments
+ *
+ * * `owner`: Token owner
+ * * `operator`: Operator
+ * * `approve`: Is operator enabled or disabled
+ **/
+ setApprovalForAll: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, operator: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, approve: bool | boolean | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, bool]>;
+ /**
* Set specific limits of a collection. Empty, or None fields mean chain default.
*
* # Permissions
tests/src/interfaces/default/types.tsdiffbeforeafterboth--- a/tests/src/interfaces/default/types.ts
+++ b/tests/src/interfaces/default/types.ts
@@ -1286,6 +1286,8 @@
readonly asTransfer: ITuple<[u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
readonly isApproved: boolean;
readonly asApproved: ITuple<[u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
+ readonly isApprovedForAll: boolean;
+ readonly asApprovedForAll: ITuple<[u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, bool]>;
readonly isCollectionPropertySet: boolean;
readonly asCollectionPropertySet: ITuple<[u32, Bytes]>;
readonly isCollectionPropertyDeleted: boolean;
@@ -1296,7 +1298,7 @@
readonly asTokenPropertyDeleted: ITuple<[u32, u32, Bytes]>;
readonly isPropertyPermissionSet: boolean;
readonly asPropertyPermissionSet: ITuple<[u32, Bytes]>;
- readonly type: 'CollectionCreated' | 'CollectionDestroyed' | 'ItemCreated' | 'ItemDestroyed' | 'Transfer' | 'Approved' | 'CollectionPropertySet' | 'CollectionPropertyDeleted' | 'TokenPropertySet' | 'TokenPropertyDeleted' | 'PropertyPermissionSet';
+ readonly type: 'CollectionCreated' | 'CollectionDestroyed' | 'ItemCreated' | 'ItemDestroyed' | 'Transfer' | 'Approved' | 'ApprovedForAll' | 'CollectionPropertySet' | 'CollectionPropertyDeleted' | 'TokenPropertySet' | 'TokenPropertyDeleted' | 'PropertyPermissionSet';
}
/** @name PalletConfigurationCall */
@@ -1603,7 +1605,8 @@
readonly isFungibleItemsDontHaveData: boolean;
readonly isFungibleDisallowsNesting: boolean;
readonly isSettingPropertiesNotAllowed: boolean;
- readonly type: 'NotFungibleDataUsedToMintFungibleCollectionToken' | 'FungibleItemsHaveNoId' | 'FungibleItemsDontHaveData' | 'FungibleDisallowsNesting' | 'SettingPropertiesNotAllowed';
+ readonly isSettingApprovalForAllNotAllowed: boolean;
+ readonly type: 'NotFungibleDataUsedToMintFungibleCollectionToken' | 'FungibleItemsHaveNoId' | 'FungibleItemsDontHaveData' | 'FungibleDisallowsNesting' | 'SettingPropertiesNotAllowed' | 'SettingApprovalForAllNotAllowed';
}
/** @name PalletInflationCall */
@@ -2309,7 +2312,13 @@
readonly tokenId: u32;
readonly amount: u128;
} & Struct;
- readonly type: 'CreateCollection' | 'CreateCollectionEx' | 'DestroyCollection' | 'AddToAllowList' | 'RemoveFromAllowList' | 'ChangeCollectionOwner' | 'AddCollectionAdmin' | 'RemoveCollectionAdmin' | 'SetCollectionSponsor' | 'ConfirmSponsorship' | 'RemoveCollectionSponsor' | 'CreateItem' | 'CreateMultipleItems' | 'SetCollectionProperties' | 'DeleteCollectionProperties' | 'SetTokenProperties' | 'DeleteTokenProperties' | 'SetTokenPropertyPermissions' | 'CreateMultipleItemsEx' | 'SetTransfersEnabledFlag' | 'BurnItem' | 'BurnFrom' | 'Transfer' | 'Approve' | 'TransferFrom' | 'SetCollectionLimits' | 'SetCollectionPermissions' | 'Repartition';
+ readonly isSetApprovalForAll: boolean;
+ readonly asSetApprovalForAll: {
+ readonly collectionId: u32;
+ readonly operator: PalletEvmAccountBasicCrossAccountIdRepr;
+ readonly approve: bool;
+ } & Struct;
+ readonly type: 'CreateCollection' | 'CreateCollectionEx' | 'DestroyCollection' | 'AddToAllowList' | 'RemoveFromAllowList' | 'ChangeCollectionOwner' | 'AddCollectionAdmin' | 'RemoveCollectionAdmin' | 'SetCollectionSponsor' | 'ConfirmSponsorship' | 'RemoveCollectionSponsor' | 'CreateItem' | 'CreateMultipleItems' | 'SetCollectionProperties' | 'DeleteCollectionProperties' | 'SetTokenProperties' | 'DeleteTokenProperties' | 'SetTokenPropertyPermissions' | 'CreateMultipleItemsEx' | 'SetTransfersEnabledFlag' | 'BurnItem' | 'BurnFrom' | 'Transfer' | 'Approve' | 'TransferFrom' | 'SetCollectionLimits' | 'SetCollectionPermissions' | 'Repartition' | 'SetApprovalForAll';
}
/** @name PalletUniqueError */
tests/src/interfaces/lookup.tsdiffbeforeafterboth--- a/tests/src/interfaces/lookup.ts
+++ b/tests/src/interfaces/lookup.ts
@@ -1046,6 +1046,7 @@
ItemDestroyed: '(u32,u32,PalletEvmAccountBasicCrossAccountIdRepr,u128)',
Transfer: '(u32,u32,PalletEvmAccountBasicCrossAccountIdRepr,PalletEvmAccountBasicCrossAccountIdRepr,u128)',
Approved: '(u32,u32,PalletEvmAccountBasicCrossAccountIdRepr,PalletEvmAccountBasicCrossAccountIdRepr,u128)',
+ ApprovedForAll: '(u32,PalletEvmAccountBasicCrossAccountIdRepr,PalletEvmAccountBasicCrossAccountIdRepr,bool)',
CollectionPropertySet: '(u32,Bytes)',
CollectionPropertyDeleted: '(u32,Bytes)',
TokenPropertySet: '(u32,u32,Bytes)',
@@ -1054,7 +1055,7 @@
}
},
/**
- * Lookup99: pallet_structure::pallet::Event<T>
+ * Lookup100: pallet_structure::pallet::Event<T>
**/
PalletStructureEvent: {
_enum: {
@@ -1062,7 +1063,7 @@
}
},
/**
- * Lookup100: pallet_rmrk_core::pallet::Event<T>
+ * Lookup101: pallet_rmrk_core::pallet::Event<T>
**/
PalletRmrkCoreEvent: {
_enum: {
@@ -1139,7 +1140,7 @@
}
},
/**
- * Lookup101: rmrk_traits::nft::AccountIdOrCollectionNftTuple<sp_core::crypto::AccountId32>
+ * Lookup102: rmrk_traits::nft::AccountIdOrCollectionNftTuple<sp_core::crypto::AccountId32>
**/
RmrkTraitsNftAccountIdOrCollectionNftTuple: {
_enum: {
@@ -2302,7 +2303,12 @@
repartition: {
collectionId: 'u32',
tokenId: 'u32',
- amount: 'u128'
+ amount: 'u128',
+ },
+ set_approval_for_all: {
+ collectionId: 'u32',
+ operator: 'PalletEvmAccountBasicCrossAccountIdRepr',
+ approve: 'bool'
}
}
},
@@ -3445,7 +3451,7 @@
* Lookup430: pallet_fungible::pallet::Error<T>
**/
PalletFungibleError: {
- _enum: ['NotFungibleDataUsedToMintFungibleCollectionToken', 'FungibleItemsHaveNoId', 'FungibleItemsDontHaveData', 'FungibleDisallowsNesting', 'SettingPropertiesNotAllowed']
+ _enum: ['NotFungibleDataUsedToMintFungibleCollectionToken', 'FungibleItemsHaveNoId', 'FungibleItemsDontHaveData', 'FungibleDisallowsNesting', 'SettingPropertiesNotAllowed', 'SettingApprovalForAllNotAllowed']
},
/**
* Lookup431: pallet_refungible::ItemData
tests/src/interfaces/types-lookup.tsdiffbeforeafterboth--- a/tests/src/interfaces/types-lookup.ts
+++ b/tests/src/interfaces/types-lookup.ts
@@ -1182,6 +1182,8 @@
readonly asTransfer: ITuple<[u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
readonly isApproved: boolean;
readonly asApproved: ITuple<[u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
+ readonly isApprovedForAll: boolean;
+ readonly asApprovedForAll: ITuple<[u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, bool]>;
readonly isCollectionPropertySet: boolean;
readonly asCollectionPropertySet: ITuple<[u32, Bytes]>;
readonly isCollectionPropertyDeleted: boolean;
@@ -1192,17 +1194,17 @@
readonly asTokenPropertyDeleted: ITuple<[u32, u32, Bytes]>;
readonly isPropertyPermissionSet: boolean;
readonly asPropertyPermissionSet: ITuple<[u32, Bytes]>;
- readonly type: 'CollectionCreated' | 'CollectionDestroyed' | 'ItemCreated' | 'ItemDestroyed' | 'Transfer' | 'Approved' | 'CollectionPropertySet' | 'CollectionPropertyDeleted' | 'TokenPropertySet' | 'TokenPropertyDeleted' | 'PropertyPermissionSet';
+ readonly type: 'CollectionCreated' | 'CollectionDestroyed' | 'ItemCreated' | 'ItemDestroyed' | 'Transfer' | 'Approved' | 'ApprovedForAll' | 'CollectionPropertySet' | 'CollectionPropertyDeleted' | 'TokenPropertySet' | 'TokenPropertyDeleted' | 'PropertyPermissionSet';
}
- /** @name PalletStructureEvent (99) */
+ /** @name PalletStructureEvent (100) */
interface PalletStructureEvent extends Enum {
readonly isExecuted: boolean;
readonly asExecuted: Result<Null, SpRuntimeDispatchError>;
readonly type: 'Executed';
}
- /** @name PalletRmrkCoreEvent (100) */
+ /** @name PalletRmrkCoreEvent (101) */
interface PalletRmrkCoreEvent extends Enum {
readonly isCollectionCreated: boolean;
readonly asCollectionCreated: {
@@ -1292,7 +1294,7 @@
readonly type: 'CollectionCreated' | 'CollectionDestroyed' | 'IssuerChanged' | 'CollectionLocked' | 'NftMinted' | 'NftBurned' | 'NftSent' | 'NftAccepted' | 'NftRejected' | 'PropertySet' | 'ResourceAdded' | 'ResourceRemoval' | 'ResourceAccepted' | 'ResourceRemovalAccepted' | 'PrioritySet';
}
- /** @name RmrkTraitsNftAccountIdOrCollectionNftTuple (101) */
+ /** @name RmrkTraitsNftAccountIdOrCollectionNftTuple (102) */
interface RmrkTraitsNftAccountIdOrCollectionNftTuple extends Enum {
readonly isAccountId: boolean;
readonly asAccountId: AccountId32;
@@ -2539,7 +2541,13 @@
readonly tokenId: u32;
readonly amount: u128;
} & Struct;
- readonly type: 'CreateCollection' | 'CreateCollectionEx' | 'DestroyCollection' | 'AddToAllowList' | 'RemoveFromAllowList' | 'ChangeCollectionOwner' | 'AddCollectionAdmin' | 'RemoveCollectionAdmin' | 'SetCollectionSponsor' | 'ConfirmSponsorship' | 'RemoveCollectionSponsor' | 'CreateItem' | 'CreateMultipleItems' | 'SetCollectionProperties' | 'DeleteCollectionProperties' | 'SetTokenProperties' | 'DeleteTokenProperties' | 'SetTokenPropertyPermissions' | 'CreateMultipleItemsEx' | 'SetTransfersEnabledFlag' | 'BurnItem' | 'BurnFrom' | 'Transfer' | 'Approve' | 'TransferFrom' | 'SetCollectionLimits' | 'SetCollectionPermissions' | 'Repartition';
+ readonly isSetApprovalForAll: boolean;
+ readonly asSetApprovalForAll: {
+ readonly collectionId: u32;
+ readonly operator: PalletEvmAccountBasicCrossAccountIdRepr;
+ readonly approve: bool;
+ } & Struct;
+ readonly type: 'CreateCollection' | 'CreateCollectionEx' | 'DestroyCollection' | 'AddToAllowList' | 'RemoveFromAllowList' | 'ChangeCollectionOwner' | 'AddCollectionAdmin' | 'RemoveCollectionAdmin' | 'SetCollectionSponsor' | 'ConfirmSponsorship' | 'RemoveCollectionSponsor' | 'CreateItem' | 'CreateMultipleItems' | 'SetCollectionProperties' | 'DeleteCollectionProperties' | 'SetTokenProperties' | 'DeleteTokenProperties' | 'SetTokenPropertyPermissions' | 'CreateMultipleItemsEx' | 'SetTransfersEnabledFlag' | 'BurnItem' | 'BurnFrom' | 'Transfer' | 'Approve' | 'TransferFrom' | 'SetCollectionLimits' | 'SetCollectionPermissions' | 'Repartition' | 'SetApprovalForAll';
}
/** @name UpDataStructsCollectionMode (240) */
@@ -3655,7 +3663,8 @@
readonly isFungibleItemsDontHaveData: boolean;
readonly isFungibleDisallowsNesting: boolean;
readonly isSettingPropertiesNotAllowed: boolean;
- readonly type: 'NotFungibleDataUsedToMintFungibleCollectionToken' | 'FungibleItemsHaveNoId' | 'FungibleItemsDontHaveData' | 'FungibleDisallowsNesting' | 'SettingPropertiesNotAllowed';
+ readonly isSettingApprovalForAllNotAllowed: boolean;
+ readonly type: 'NotFungibleDataUsedToMintFungibleCollectionToken' | 'FungibleItemsHaveNoId' | 'FungibleItemsDontHaveData' | 'FungibleDisallowsNesting' | 'SettingPropertiesNotAllowed' | 'SettingApprovalForAllNotAllowed';
}
/** @name PalletRefungibleItemData (431) */
tests/src/interfaces/unique/definitions.tsdiffbeforeafterboth--- a/tests/src/interfaces/unique/definitions.ts
+++ b/tests/src/interfaces/unique/definitions.ts
@@ -175,5 +175,10 @@
[collectionParam, tokenParam],
'Option<u128>',
),
+ isApprovedForAll: fun(
+ 'Tells whether an operator is approved by a given owner.',
+ [collectionParam, crossAccountParam('owner'), crossAccountParam('operator')],
+ 'Option<bool>',
+ ),
},
};
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -1413,6 +1413,32 @@
getTokenObject(_collectionId: number, _tokenId: number): any {
return null;
}
+
+ /**
+ * Tells whether an operator is approved by a given owner.
+ * @param collectionId ID of collection
+ * @param owner owner address
+ * @param operator operator addrees
+ * @returns true if operator is enabled
+ */
+ async isApprovedForAll(collectionId: number, owner: ICrossAccountId, operator: ICrossAccountId): Promise<boolean> {
+ return (await this.helper.callRpc('api.rpc.unique.isApprovedForAll', [collectionId, owner, operator])).toJSON();
+ }
+
+ /** Sets or unsets the approval of a given operator.
+ * An operator is allowed to transfer all tokens of the sender on their behalf.
+ * @param operator Operator
+ * @param approved Is operator enabled or disabled
+ * @returns ```true``` if extrinsic success, otherwise ```false```
+ */
+ async setApprovalForAll(signer: TSigner, collectionId: number, operator: ICrossAccountId, approved: boolean): Promise<boolean> {
+ const result = await this.helper.executeExtrinsic(
+ signer,
+ 'api.tx.unique.setApprovalForAll', [collectionId, operator, approved],
+ true,
+ );
+ return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'ApprovedForAll');
+ }
}