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.rsdiffbeforeafterboth--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -1126,6 +1126,28 @@
}
})
}
+
+ /// 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
+ #[weight = T::CommonWeightInfo::set_approval_for_all()]
+ pub fn set_approval_for_all(
+ origin,
+ collection_id: CollectionId,
+ operator: T::CrossAccountId,
+ approve: bool,
+ ) -> DispatchResultWithPostInfo {
+ let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
+ dispatch_tx::<T, _>(collection_id, |d| {
+ d.set_approval_for_all(sender, operator, approve)
+ })
+ }
}
}
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.tsdiffbeforeafterboth1// 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/>.1617import {itEth, usingEthPlaygrounds, expect, EthUniqueHelper} from './util';18import {IKeyringPair} from '@polkadot/types/types';19import {Contract} from 'web3-eth-contract';202122describe('NFT: Information getting', () => {23 let donor: IKeyringPair;24 let alice: IKeyringPair;2526 before(async function() {27 await usingEthPlaygrounds(async (helper, privateKey) => {28 donor = await privateKey({filename: __filename});29 [alice] = await helper.arrange.createAccounts([10n], donor);30 });31 });3233 itEth('totalSupply', async ({helper}) => {34 const collection = await helper.nft.mintCollection(alice, {});35 await collection.mintToken(alice);3637 const caller = await helper.eth.createAccountWithBalance(donor);3839 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);40 const totalSupply = await contract.methods.totalSupply().call();4142 expect(totalSupply).to.equal('1');43 });4445 itEth('balanceOf', async ({helper}) => {46 const collection = await helper.nft.mintCollection(alice, {});47 const caller = await helper.eth.createAccountWithBalance(donor);4849 await collection.mintToken(alice, {Ethereum: caller});50 await collection.mintToken(alice, {Ethereum: caller});51 await collection.mintToken(alice, {Ethereum: caller});5253 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);54 const balance = await contract.methods.balanceOf(caller).call();5556 expect(balance).to.equal('3');57 });5859 itEth('ownerOf', async ({helper}) => {60 const collection = await helper.nft.mintCollection(alice, {});61 const caller = await helper.eth.createAccountWithBalance(donor);6263 const token = await collection.mintToken(alice, {Ethereum: caller});6465 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);6667 const owner = await contract.methods.ownerOf(token.tokenId).call();6869 expect(owner).to.equal(caller);70 });7172 itEth('name/symbol is available regardless of ERC721Metadata support', async ({helper}) => {73 const collection = await helper.nft.mintCollection(alice, {name: 'test', tokenPrefix: 'TEST'});74 const caller = helper.eth.createAccount();7576 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);7778 expect(await contract.methods.name().call()).to.equal('test');79 expect(await contract.methods.symbol().call()).to.equal('TEST');80 });81});8283describe('Check ERC721 token URI for NFT', () => {84 let donor: IKeyringPair;8586 before(async function() {87 await usingEthPlaygrounds(async (_helper, privateKey) => {88 donor = await privateKey({filename: __filename});89 });90 });9192 async function setup(helper: EthUniqueHelper, baseUri: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> {93 const owner = await helper.eth.createAccountWithBalance(donor);94 const receiver = helper.eth.createAccount();9596 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', 'a', 'b', baseUri);97 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);9899 const result = await contract.methods.mint(receiver).send();100 const tokenId = result.events.Transfer.returnValues.tokenId;101 expect(tokenId).to.be.equal('1');102103 if (propertyKey && propertyValue) {104 // Set URL or suffix105 await contract.methods.setProperties(tokenId, [{key: propertyKey, value: Buffer.from(propertyValue)}]).send();106 }107108 const event = result.events.Transfer;109 expect(event.address).to.be.equal(collectionAddress);110 expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');111 expect(event.returnValues.to).to.be.equal(receiver);112 expect(event.returnValues.tokenId).to.be.equal(tokenId);113114 return {contract, nextTokenId: tokenId};115 }116117 itEth('Empty tokenURI', async ({helper}) => {118 const {contract, nextTokenId} = await setup(helper, '');119 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('');120 });121122 itEth('TokenURI from url', async ({helper}) => {123 const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URI', 'Token URI');124 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI');125 });126127 itEth('TokenURI from baseURI', async ({helper}) => {128 const {contract, nextTokenId} = await setup(helper, 'BaseURI_');129 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_');130 });131132 itEth('TokenURI from baseURI + suffix', async ({helper}) => {133 const suffix = '/some/suffix';134 const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URISuffix', suffix);135 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix);136 });137});138139describe('NFT: Plain calls', () => {140 let donor: IKeyringPair;141 let minter: IKeyringPair;142 let bob: IKeyringPair;143 let charlie: IKeyringPair;144145 before(async function() {146 await usingEthPlaygrounds(async (helper, privateKey) => {147 donor = await privateKey({filename: __filename});148 [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);149 });150 });151152 itEth('Can perform mint() & get crossOwner()', async ({helper}) => {153 const owner = await helper.eth.createAccountWithBalance(donor);154 const receiver = helper.eth.createAccount();155156 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', '6', '6', '');157 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);158159 const result = await contract.methods.mintWithTokenURI(receiver, 'Test URI').send();160 const tokenId = result.events.Transfer.returnValues.tokenId;161 expect(tokenId).to.be.equal('1');162163 const event = result.events.Transfer;164 expect(event.address).to.be.equal(collectionAddress);165 expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');166 expect(event.returnValues.to).to.be.equal(receiver);167168 expect(await contract.methods.tokenURI(tokenId).call()).to.be.equal('Test URI');169 console.log(await contract.methods.crossOwnerOf(tokenId).call());170 expect(await contract.methods.crossOwnerOf(tokenId).call()).to.be.like([receiver, '0']);171 // TODO: this wont work right now, need release 919000 first172 // await helper.methods.setOffchainSchema(collectionIdAddress, 'https://offchain-service.local/token-info/{id}').send();173 // const tokenUri = await contract.methods.tokenURI(nextTokenId).call();174 // expect(tokenUri).to.be.equal(`https://offchain-service.local/token-info/${nextTokenId}`);175 });176177 //TODO: CORE-302 add eth methods178 itEth.skip('Can perform mintBulk()', async ({helper}) => {179 const caller = await helper.eth.createAccountWithBalance(donor);180 const receiver = helper.eth.createAccount();181182 const collection = await helper.nft.mintCollection(minter);183 await collection.addAdmin(minter, {Ethereum: caller});184185 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);186 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', caller);187 {188 const bulkSize = 3;189 const nextTokenId = await contract.methods.nextTokenId().call();190 expect(nextTokenId).to.be.equal('1');191 const result = await contract.methods.mintBulkWithTokenURI(192 receiver,193 Array.from({length: bulkSize}, (_, i) => (194 [+nextTokenId + i, `Test URI ${i}`]195 )),196 ).send({from: caller});197198 const events = result.events.Transfer.sort((a: any, b: any) => +a.returnValues.tokenId - b.returnValues.tokenId);199 for (let i = 0; i < bulkSize; i++) {200 const event = events[i];201 expect(event.address).to.equal(collectionAddress);202 expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');203 expect(event.returnValues.to).to.equal(receiver);204 expect(event.returnValues.tokenId).to.equal(`${+nextTokenId+i}`);205206 expect(await contract.methods.tokenURI(+nextTokenId + i).call()).to.be.equal(`Test URI ${i}`);207 }208 }209 });210211 itEth('Can perform burn()', async ({helper}) => {212 const caller = await helper.eth.createAccountWithBalance(donor);213214 const collection = await helper.nft.mintCollection(minter, {});215 const {tokenId} = await collection.mintToken(minter, {Ethereum: caller});216217 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);218 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', caller);219220 {221 const result = await contract.methods.burn(tokenId).send({from: caller});222223 const event = result.events.Transfer;224 expect(event.address).to.be.equal(collectionAddress);225 expect(event.returnValues.from).to.be.equal(caller);226 expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');227 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);228 }229 });230231 itEth('Can perform approve()', async ({helper}) => {232 const owner = await helper.eth.createAccountWithBalance(donor);233 const spender = helper.eth.createAccount();234235 const collection = await helper.nft.mintCollection(minter, {});236 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});237238 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);239 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);240241 {242 const result = await contract.methods.approve(spender, tokenId).send({from: owner});243244 const event = result.events.Approval;245 expect(event.address).to.be.equal(collectionAddress);246 expect(event.returnValues.owner).to.be.equal(owner);247 expect(event.returnValues.approved).to.be.equal(spender);248 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);249 }250 });251252 itEth('Can perform burnFromCross()', async ({helper}) => {253 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});254 const ownerSub = bob;255 const ownerCrossSub = helper.ethCrossAccount.fromKeyringPair(ownerSub);256 const ownerEth = await helper.eth.createAccountWithBalance(donor, 100n);257 const ownerCrossEth = helper.ethCrossAccount.fromAddress(ownerEth);258259 const burnerEth = await helper.eth.createAccountWithBalance(donor, 100n);260 const burnerCrossEth = helper.ethCrossAccount.fromAddress(burnerEth);261262 const token1 = await collection.mintToken(minter, {Substrate: ownerSub.address});263 const token2 = await collection.mintToken(minter, {Ethereum: ownerEth});264265 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);266 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft');267268 // Approve tokens from substrate and ethereum:269 await token1.approve(ownerSub, {Ethereum: burnerEth});270 await collectionEvm.methods.approveCross(burnerCrossEth, token2.tokenId).send({from: ownerEth});271272 // can burnFromCross:273 const result1 = await collectionEvm.methods.burnFromCross(ownerCrossSub, token1.tokenId).send({from: burnerEth});274 const result2 = await collectionEvm.methods.burnFromCross(ownerCrossEth, token2.tokenId).send({from: burnerEth});275 const events1 = result1.events.Transfer;276 const events2 = result2.events.Transfer;277278 // Check events for burnFromCross (substrate and ethereum):279 [280 [events1, token1, helper.address.substrateToEth(ownerSub.address)], 281 [events2, token2, ownerEth],282 ].map(burnData => {283 expect(burnData[0]).to.be.like({284 address: collectionAddress,285 event: 'Transfer',286 returnValues: {287 from: burnData[2],288 to: '0x0000000000000000000000000000000000000000',289 tokenId: burnData[1].tokenId.toString(),290 },291 });292 });293294 expect(await token1.doesExist()).to.be.false;295 expect(await token2.doesExist()).to.be.false;296 });297298 itEth('Can perform approveCross()', async ({helper}) => {299 // arrange: create accounts300 const owner = await helper.eth.createAccountWithBalance(donor, 100n);301 const ownerCross = helper.ethCrossAccount.fromAddress(owner);302 const receiverSub = charlie;303 const recieverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);304 const receiverEth = await helper.eth.createAccountWithBalance(donor, 100n);305 const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);306307 // arrange: create collection and tokens:308 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});309 const token1 = await collection.mintToken(minter, {Ethereum: owner});310 const token2 = await collection.mintToken(minter, {Ethereum: owner});311312 const collectionEvm = helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');313314 // Can approveCross substrate and ethereum address:315 const resultSub = await collectionEvm.methods.approveCross(recieverCrossSub, token1.tokenId).send({from: owner});316 const resultEth = await collectionEvm.methods.approveCross(receiverCrossEth, token2.tokenId).send({from: owner});317 const eventSub = resultSub.events.Approval;318 const eventEth = resultEth.events.Approval;319 expect(eventSub).to.be.like({320 address: helper.ethAddress.fromCollectionId(collection.collectionId),321 event: 'Approval',322 returnValues: {323 owner,324 approved: helper.address.substrateToEth(receiverSub.address),325 tokenId: token1.tokenId.toString(),326 },327 });328 expect(eventEth).to.be.like({329 address: helper.ethAddress.fromCollectionId(collection.collectionId),330 event: 'Approval',331 returnValues: {332 owner,333 approved: receiverEth,334 tokenId: token2.tokenId.toString(),335 },336 });337338 // Substrate address can transferFrom approved tokens:339 await helper.nft.transferTokenFrom(receiverSub, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiverSub.address});340 expect(await helper.nft.getTokenOwner(collection.collectionId, token1.tokenId)).to.deep.eq({Substrate: receiverSub.address});341 // Ethereum address can transferFromCross approved tokens:342 await collectionEvm.methods.transferFromCross(ownerCross, receiverCrossEth, token2.tokenId).send({from: receiverEth});343 expect(await helper.nft.getTokenOwner(collection.collectionId, token2.tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});344 });345346 itEth('Can reaffirm approved address', async ({helper}) => {347 const owner = await helper.eth.createAccountWithBalance(donor, 100n);348 const ownerCrossEth = helper.ethCrossAccount.fromAddress(owner);349 const [receiver1, receiver2] = await helper.arrange.createAccounts([100n, 100n], donor);350 const receiver1Cross = helper.ethCrossAccount.fromKeyringPair(receiver1);351 const receiver2Cross = helper.ethCrossAccount.fromKeyringPair(receiver2);352 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});353 const token1 = await collection.mintToken(minter, {Ethereum: owner});354 const token2 = await collection.mintToken(minter, {Ethereum: owner});355 const collectionEvm = helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');356357 // Can approve and reaffirm approved address:358 await collectionEvm.methods.approveCross(receiver1Cross, token1.tokenId).send({from: owner});359 await collectionEvm.methods.approveCross(receiver2Cross, token1.tokenId).send({from: owner});360361 // receiver1 cannot transferFrom:362 await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;363 // receiver2 can transferFrom:364 await helper.nft.transferTokenFrom(receiver2, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver2.address});365366 // can set approved address to self address to remove approval:367 await collectionEvm.methods.approveCross(receiver1Cross, token2.tokenId).send({from: owner});368 await collectionEvm.methods.approveCross(ownerCrossEth, token2.tokenId).send({from: owner});369370 // receiver1 cannot transfer token anymore:371 await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token2.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;372 });373374 itEth('Can perform transferFrom()', async ({helper}) => {375 const owner = await helper.eth.createAccountWithBalance(donor);376 const spender = await helper.eth.createAccountWithBalance(donor);377 const receiver = helper.eth.createAccount();378379 const collection = await helper.nft.mintCollection(minter, {});380 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});381382 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);383 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);384385 await contract.methods.approve(spender, tokenId).send({from: owner});386387 {388 const result = await contract.methods.transferFrom(owner, receiver, tokenId).send({from: spender});389390 const event = result.events.Transfer;391 expect(event.address).to.be.equal(collectionAddress);392 expect(event.returnValues.from).to.be.equal(owner);393 expect(event.returnValues.to).to.be.equal(receiver);394 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);395 }396397 {398 const balance = await contract.methods.balanceOf(receiver).call();399 expect(+balance).to.equal(1);400 }401402 {403 const balance = await contract.methods.balanceOf(owner).call();404 expect(+balance).to.equal(0);405 }406 });407408 itEth('Can perform transferFromCross()', async ({helper}) => {409 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});410411 const [owner, receiver] = await helper.arrange.createAccounts([100n, 100n], donor);412 const spender = await helper.eth.createAccountWithBalance(donor);413414 const token = await collection.mintToken(minter, {Substrate: owner.address});415416 const address = helper.ethAddress.fromCollectionId(collection.collectionId);417 const contract = helper.ethNativeContract.collection(address, 'nft');418419 await token.approve(owner, {Ethereum: spender});420421 {422 const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);423 const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);424 const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});425 const event = result.events.Transfer;426 expect(event).to.be.like({427 address: helper.ethAddress.fromCollectionId(collection.collectionId),428 event: 'Transfer',429 returnValues: {430 from: helper.address.substrateToEth(owner.address),431 to: helper.address.substrateToEth(receiver.address),432 tokenId: token.tokenId.toString(),433 },434 });435 }436437 expect(await token.getOwner()).to.be.like({Substrate: receiver.address});438 });439440 itEth('Can perform transfer()', async ({helper}) => {441 const collection = await helper.nft.mintCollection(minter, {});442 const owner = await helper.eth.createAccountWithBalance(donor);443 const receiver = helper.eth.createAccount();444445 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});446447 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);448 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);449450 {451 const result = await contract.methods.transfer(receiver, tokenId).send({from: owner});452453 const event = result.events.Transfer;454 expect(event.address).to.be.equal(collectionAddress);455 expect(event.returnValues.from).to.be.equal(owner);456 expect(event.returnValues.to).to.be.equal(receiver);457 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);458 }459460 {461 const balance = await contract.methods.balanceOf(owner).call();462 expect(+balance).to.equal(0);463 }464465 {466 const balance = await contract.methods.balanceOf(receiver).call();467 expect(+balance).to.equal(1);468 }469 });470 471 itEth('Can perform transferCross()', async ({helper}) => {472 const collection = await helper.nft.mintCollection(minter, {});473 const owner = await helper.eth.createAccountWithBalance(donor);474 const receiverEth = await helper.eth.createAccountWithBalance(donor);475 const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);476 const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);477 478 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});479480 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);481 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);482483 {484 // Can transferCross to ethereum address:485 const result = await collectionEvm.methods.transferCross(receiverCrossEth, tokenId).send({from: owner});486 // Check events:487 const event = result.events.Transfer;488 expect(event.address).to.be.equal(collectionAddress);489 expect(event.returnValues.from).to.be.equal(owner);490 expect(event.returnValues.to).to.be.equal(receiverEth);491 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);492 493 // owner has balance = 0:494 const ownerBalance = await collectionEvm.methods.balanceOf(owner).call();495 expect(+ownerBalance).to.equal(0);496 // receiver owns token:497 const receiverBalance = await collectionEvm.methods.balanceOf(receiverEth).call();498 expect(+receiverBalance).to.equal(1);499 expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});500 }501 502 {503 // Can transferCross to substrate address:504 const substrateResult = await collectionEvm.methods.transferCross(receiverCrossSub, tokenId).send({from: receiverEth});505 // Check events:506 const event = substrateResult.events.Transfer;507 expect(event.address).to.be.equal(collectionAddress);508 expect(event.returnValues.from).to.be.equal(receiverEth);509 expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));510 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);511 512 // owner has balance = 0:513 const ownerBalance = await collectionEvm.methods.balanceOf(receiverEth).call();514 expect(+ownerBalance).to.equal(0);515 // receiver owns token:516 const receiverBalance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});517 expect(receiverBalance).to.contain(tokenId);518 }519 });520521 ['transfer', 'transferCross'].map(testCase => itEth(`Cannot ${testCase} non-owned token`, async ({helper}) => {522 const sender = await helper.eth.createAccountWithBalance(donor);523 const tokenOwner = await helper.eth.createAccountWithBalance(donor);524 const receiverSub = minter;525 const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);526527 const collection = await helper.nft.mintCollection(minter, {});528 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);529 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', sender);530531 await collection.mintToken(minter, {Ethereum: sender});532 const nonSendersToken = await collection.mintToken(minter, {Ethereum: tokenOwner});533534 // Cannot transferCross someone else's token:535 const receiver = testCase === 'transfer' ? helper.address.substrateToEth(receiverSub.address) : receiverCrossSub;536 await expect(collectionEvm.methods[testCase](receiver, nonSendersToken.tokenId).send({from: sender})).to.be.rejected;537 // Cannot transfer token if it does not exist:538 await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected;539 }));540});541542describe('NFT: Fees', () => {543 let donor: IKeyringPair;544 let alice: IKeyringPair;545 let bob: IKeyringPair;546 let charlie: IKeyringPair;547548 before(async function() {549 await usingEthPlaygrounds(async (helper, privateKey) => {550 donor = await privateKey({filename: __filename});551 [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);552 });553 });554555 itEth('approve() call fee is less than 0.2UNQ', async ({helper}) => {556 const owner = await helper.eth.createAccountWithBalance(donor);557 const spender = helper.eth.createAccount();558559 const collection = await helper.nft.mintCollection(alice, {});560 const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});561562 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);563564 const cost = await helper.eth.recordCallFee(owner, () => contract.methods.approve(spender, tokenId).send({from: owner}));565 expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));566 });567568 itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {569 const owner = await helper.eth.createAccountWithBalance(donor);570 const spender = await helper.eth.createAccountWithBalance(donor);571572 const collection = await helper.nft.mintCollection(alice, {});573 const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});574575 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);576577 await contract.methods.approve(spender, tokenId).send({from: owner});578579 const cost = await helper.eth.recordCallFee(spender, () => contract.methods.transferFrom(owner, spender, tokenId).send({from: spender}));580 expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));581 });582583 itEth('Can perform transferFromCross()', async ({helper}) => {584 const collectionMinter = alice;585 const owner = bob;586 const receiver = charlie;587 const collection = await helper.nft.mintCollection(collectionMinter, {name: 'A', description: 'B', tokenPrefix: 'C'});588589 const spender = await helper.eth.createAccountWithBalance(donor, 100n);590591 const token = await collection.mintToken(collectionMinter, {Substrate: owner.address});592593 const address = helper.ethAddress.fromCollectionId(collection.collectionId);594 const contract = helper.ethNativeContract.collection(address, 'nft');595596 await token.approve(owner, {Ethereum: spender});597598 {599 const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);600 const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);601 const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});602 const event = result.events.Transfer;603 expect(event).to.be.like({604 address: helper.ethAddress.fromCollectionId(collection.collectionId),605 event: 'Transfer',606 returnValues: {607 from: helper.address.substrateToEth(owner.address),608 to: helper.address.substrateToEth(receiver.address),609 tokenId: token.tokenId.toString(),610 },611 });612 }613614 expect(await token.getOwner()).to.be.like({Substrate: receiver.address});615 });616617 itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {618 const owner = await helper.eth.createAccountWithBalance(donor);619 const receiver = helper.eth.createAccount();620621 const collection = await helper.nft.mintCollection(alice, {});622 const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});623624 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);625626 const cost = await helper.eth.recordCallFee(owner, () => contract.methods.transfer(receiver, tokenId).send({from: owner}));627 expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));628 });629});630631describe('NFT: Substrate calls', () => {632 let donor: IKeyringPair;633 let alice: IKeyringPair;634635 before(async function() {636 await usingEthPlaygrounds(async (helper, privateKey) => {637 donor = await privateKey({filename: __filename});638 [alice] = await helper.arrange.createAccounts([20n], donor);639 });640 });641642 itEth('Events emitted for mint()', async ({helper}) => {643 const collection = await helper.nft.mintCollection(alice, {});644 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);645 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');646647 const events: any = [];648 contract.events.allEvents((_: any, event: any) => {649 events.push(event);650 });651652 const {tokenId} = await collection.mintToken(alice);653 if (events.length == 0) await helper.wait.newBlocks(1);654 const event = events[0];655656 expect(event.event).to.be.equal('Transfer');657 expect(event.address).to.be.equal(collectionAddress);658 expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');659 expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(alice.address));660 expect(event.returnValues.tokenId).to.be.equal(tokenId.toString());661 });662663 itEth('Events emitted for burn()', async ({helper}) => {664 const collection = await helper.nft.mintCollection(alice, {});665 const token = await collection.mintToken(alice);666667 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);668 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');669670 const events: any = [];671 contract.events.allEvents((_: any, event: any) => {672 events.push(event);673 });674675 await token.burn(alice);676 if (events.length == 0) await helper.wait.newBlocks(1);677 const event = events[0];678679 expect(event.event).to.be.equal('Transfer');680 expect(event.address).to.be.equal(collectionAddress);681 expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));682 expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');683 expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());684 });685686 itEth('Events emitted for approve()', async ({helper}) => {687 const receiver = helper.eth.createAccount();688689 const collection = await helper.nft.mintCollection(alice, {});690 const token = await collection.mintToken(alice);691692 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);693 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');694695 const events: any = [];696 contract.events.allEvents((_: any, event: any) => {697 events.push(event);698 });699700 await token.approve(alice, {Ethereum: receiver});701 if (events.length == 0) await helper.wait.newBlocks(1);702 const event = events[0];703704 expect(event.event).to.be.equal('Approval');705 expect(event.address).to.be.equal(collectionAddress);706 expect(event.returnValues.owner).to.be.equal(helper.address.substrateToEth(alice.address));707 expect(event.returnValues.approved).to.be.equal(receiver);708 expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());709 });710711 itEth('Events emitted for transferFrom()', async ({helper}) => {712 const [bob] = await helper.arrange.createAccounts([10n], donor);713 const receiver = helper.eth.createAccount();714715 const collection = await helper.nft.mintCollection(alice, {});716 const token = await collection.mintToken(alice);717 await token.approve(alice, {Substrate: bob.address});718719 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);720 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');721722 const events: any = [];723 contract.events.allEvents((_: any, event: any) => {724 events.push(event);725 });726727 await token.transferFrom(bob, {Substrate: alice.address}, {Ethereum: receiver});728729 if (events.length == 0) await helper.wait.newBlocks(1);730 const event = events[0];731732 expect(event.address).to.be.equal(collectionAddress);733 expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));734 expect(event.returnValues.to).to.be.equal(receiver);735 expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);736 });737738 itEth('Events emitted for transfer()', async ({helper}) => {739 const receiver = helper.eth.createAccount();740741 const collection = await helper.nft.mintCollection(alice, {});742 const token = await collection.mintToken(alice);743744 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);745 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');746747 const events: any = [];748 contract.events.allEvents((_: any, event: any) => {749 events.push(event);750 });751752 await token.transfer(alice, {Ethereum: receiver});753754 if (events.length == 0) await helper.wait.newBlocks(1);755 const event = events[0];756757 expect(event.address).to.be.equal(collectionAddress);758 expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));759 expect(event.returnValues.to).to.be.equal(receiver);760 expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);761 });762});763764describe('Common metadata', () => {765 let donor: IKeyringPair;766 let alice: IKeyringPair;767768 before(async function() {769 await usingEthPlaygrounds(async (helper, privateKey) => {770 donor = await privateKey({filename: __filename});771 [alice] = await helper.arrange.createAccounts([20n], donor);772 });773 });774775 itEth('Returns collection name', async ({helper}) => {776 const caller = await helper.eth.createAccountWithBalance(donor);777 const tokenPropertyPermissions = [{778 key: 'URI',779 permission: {780 mutable: true,781 collectionAdmin: true,782 tokenOwner: false,783 },784 }];785 const collection = await helper.nft.mintCollection(786 alice,787 {788 name: 'oh River',789 tokenPrefix: 'CHANGE',790 properties: [{key: 'ERC721Metadata', value: '1'}],791 tokenPropertyPermissions,792 },793 );794795 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);796 const name = await contract.methods.name().call();797 expect(name).to.equal('oh River');798 });799800 itEth('Returns symbol name', async ({helper}) => {801 const caller = await helper.eth.createAccountWithBalance(donor);802 const tokenPropertyPermissions = [{803 key: 'URI',804 permission: {805 mutable: true,806 collectionAdmin: true,807 tokenOwner: false,808 },809 }];810 const collection = await helper.nft.mintCollection(811 alice,812 {813 name: 'oh River',814 tokenPrefix: 'CHANGE',815 properties: [{key: 'ERC721Metadata', value: '1'}],816 tokenPropertyPermissions,817 },818 );819820 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);821 const symbol = await contract.methods.symbol().call();822 expect(symbol).to.equal('CHANGE');823 });824});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/>.1617import {itEth, usingEthPlaygrounds, expect, EthUniqueHelper} from './util';18import {IKeyringPair} from '@polkadot/types/types';19import {Contract} from 'web3-eth-contract';202122describe('NFT: Information getting', () => {23 let donor: IKeyringPair;24 let alice: IKeyringPair;2526 before(async function() {27 await usingEthPlaygrounds(async (helper, privateKey) => {28 donor = await privateKey({filename: __filename});29 [alice] = await helper.arrange.createAccounts([10n], donor);30 });31 });3233 itEth('totalSupply', async ({helper}) => {34 const collection = await helper.nft.mintCollection(alice, {});35 await collection.mintToken(alice);3637 const caller = await helper.eth.createAccountWithBalance(donor);3839 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);40 const totalSupply = await contract.methods.totalSupply().call();4142 expect(totalSupply).to.equal('1');43 });4445 itEth('balanceOf', async ({helper}) => {46 const collection = await helper.nft.mintCollection(alice, {});47 const caller = await helper.eth.createAccountWithBalance(donor);4849 await collection.mintToken(alice, {Ethereum: caller});50 await collection.mintToken(alice, {Ethereum: caller});51 await collection.mintToken(alice, {Ethereum: caller});5253 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);54 const balance = await contract.methods.balanceOf(caller).call();5556 expect(balance).to.equal('3');57 });5859 itEth('ownerOf', async ({helper}) => {60 const collection = await helper.nft.mintCollection(alice, {});61 const caller = await helper.eth.createAccountWithBalance(donor);6263 const token = await collection.mintToken(alice, {Ethereum: caller});6465 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);6667 const owner = await contract.methods.ownerOf(token.tokenId).call();6869 expect(owner).to.equal(caller);70 });7172 itEth('name/symbol is available regardless of ERC721Metadata support', async ({helper}) => {73 const collection = await helper.nft.mintCollection(alice, {name: 'test', tokenPrefix: 'TEST'});74 const caller = helper.eth.createAccount();7576 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);7778 expect(await contract.methods.name().call()).to.equal('test');79 expect(await contract.methods.symbol().call()).to.equal('TEST');80 });81});8283describe('Check ERC721 token URI for NFT', () => {84 let donor: IKeyringPair;8586 before(async function() {87 await usingEthPlaygrounds(async (_helper, privateKey) => {88 donor = await privateKey({filename: __filename});89 });90 });9192 async function setup(helper: EthUniqueHelper, baseUri: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> {93 const owner = await helper.eth.createAccountWithBalance(donor);94 const receiver = helper.eth.createAccount();9596 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', 'a', 'b', baseUri);97 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);9899 const result = await contract.methods.mint(receiver).send();100 const tokenId = result.events.Transfer.returnValues.tokenId;101 expect(tokenId).to.be.equal('1');102103 if (propertyKey && propertyValue) {104 // Set URL or suffix105 await contract.methods.setProperties(tokenId, [{key: propertyKey, value: Buffer.from(propertyValue)}]).send();106 }107108 const event = result.events.Transfer;109 expect(event.address).to.be.equal(collectionAddress);110 expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');111 expect(event.returnValues.to).to.be.equal(receiver);112 expect(event.returnValues.tokenId).to.be.equal(tokenId);113114 return {contract, nextTokenId: tokenId};115 }116117 itEth('Empty tokenURI', async ({helper}) => {118 const {contract, nextTokenId} = await setup(helper, '');119 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('');120 });121122 itEth('TokenURI from url', async ({helper}) => {123 const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URI', 'Token URI');124 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI');125 });126127 itEth('TokenURI from baseURI', async ({helper}) => {128 const {contract, nextTokenId} = await setup(helper, 'BaseURI_');129 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_');130 });131132 itEth('TokenURI from baseURI + suffix', async ({helper}) => {133 const suffix = '/some/suffix';134 const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URISuffix', suffix);135 expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix);136 });137});138139describe('NFT: Plain calls', () => {140 let donor: IKeyringPair;141 let minter: IKeyringPair;142 let bob: IKeyringPair;143 let charlie: IKeyringPair;144145 before(async function() {146 await usingEthPlaygrounds(async (helper, privateKey) => {147 donor = await privateKey({filename: __filename});148 [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);149 });150 });151152 itEth('Can perform mint() & get crossOwner()', async ({helper}) => {153 const owner = await helper.eth.createAccountWithBalance(donor);154 const receiver = helper.eth.createAccount();155156 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', '6', '6', '');157 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);158159 const result = await contract.methods.mintWithTokenURI(receiver, 'Test URI').send();160 const tokenId = result.events.Transfer.returnValues.tokenId;161 expect(tokenId).to.be.equal('1');162163 const event = result.events.Transfer;164 expect(event.address).to.be.equal(collectionAddress);165 expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');166 expect(event.returnValues.to).to.be.equal(receiver);167168 expect(await contract.methods.tokenURI(tokenId).call()).to.be.equal('Test URI');169 console.log(await contract.methods.crossOwnerOf(tokenId).call());170 expect(await contract.methods.crossOwnerOf(tokenId).call()).to.be.like([receiver, '0']);171 // TODO: this wont work right now, need release 919000 first172 // await helper.methods.setOffchainSchema(collectionIdAddress, 'https://offchain-service.local/token-info/{id}').send();173 // const tokenUri = await contract.methods.tokenURI(nextTokenId).call();174 // expect(tokenUri).to.be.equal(`https://offchain-service.local/token-info/${nextTokenId}`);175 });176177 //TODO: CORE-302 add eth methods178 itEth.skip('Can perform mintBulk()', async ({helper}) => {179 const caller = await helper.eth.createAccountWithBalance(donor);180 const receiver = helper.eth.createAccount();181182 const collection = await helper.nft.mintCollection(minter);183 await collection.addAdmin(minter, {Ethereum: caller});184185 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);186 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', caller);187 {188 const bulkSize = 3;189 const nextTokenId = await contract.methods.nextTokenId().call();190 expect(nextTokenId).to.be.equal('1');191 const result = await contract.methods.mintBulkWithTokenURI(192 receiver,193 Array.from({length: bulkSize}, (_, i) => (194 [+nextTokenId + i, `Test URI ${i}`]195 )),196 ).send({from: caller});197198 const events = result.events.Transfer.sort((a: any, b: any) => +a.returnValues.tokenId - b.returnValues.tokenId);199 for (let i = 0; i < bulkSize; i++) {200 const event = events[i];201 expect(event.address).to.equal(collectionAddress);202 expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');203 expect(event.returnValues.to).to.equal(receiver);204 expect(event.returnValues.tokenId).to.equal(`${+nextTokenId+i}`);205206 expect(await contract.methods.tokenURI(+nextTokenId + i).call()).to.be.equal(`Test URI ${i}`);207 }208 }209 });210211 itEth('Can perform burn()', async ({helper}) => {212 const caller = await helper.eth.createAccountWithBalance(donor);213214 const collection = await helper.nft.mintCollection(minter, {});215 const {tokenId} = await collection.mintToken(minter, {Ethereum: caller});216217 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);218 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', caller);219220 {221 const result = await contract.methods.burn(tokenId).send({from: caller});222223 const event = result.events.Transfer;224 expect(event.address).to.be.equal(collectionAddress);225 expect(event.returnValues.from).to.be.equal(caller);226 expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');227 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);228 }229 });230231 itEth('Can perform approve()', async ({helper}) => {232 const owner = await helper.eth.createAccountWithBalance(donor);233 const spender = helper.eth.createAccount();234235 const collection = await helper.nft.mintCollection(minter, {});236 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});237238 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);239 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);240241 {242 const result = await contract.methods.approve(spender, tokenId).send({from: owner});243244 const event = result.events.Approval;245 expect(event.address).to.be.equal(collectionAddress);246 expect(event.returnValues.owner).to.be.equal(owner);247 expect(event.returnValues.approved).to.be.equal(spender);248 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);249 }250 });251252 itEth('Can perform setApprovalForAll()', async ({helper}) => {253 const owner = await helper.eth.createAccountWithBalance(donor);254 const operator = helper.eth.createAccount();255256 const collection = await helper.nft.mintCollection(minter, {});257258 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);259 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);260261 const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();262 expect(approvedBefore).to.be.equal(false);263264 {265 const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});266267 expect(result.events.ApprovalForAll).to.be.like({268 address: collectionAddress,269 event: 'ApprovalForAll',270 returnValues: {271 owner,272 operator,273 approved: true,274 },275 });276277 const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();278 expect(approvedAfter).to.be.equal(true);279 }280281 {282 const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});283284 expect(result.events.ApprovalForAll).to.be.like({285 address: collectionAddress,286 event: 'ApprovalForAll',287 returnValues: {288 owner,289 operator,290 approved: false,291 },292 });293294 const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();295 expect(approvedAfter).to.be.equal(false);296 }297 });298299 itEth('Can perform burn with ApprovalForAll', async ({helper}) => {300 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});301302 const owner = await helper.eth.createAccountWithBalance(donor);303 const operator = await helper.eth.createAccountWithBalance(donor, 100n);304305 const token = await collection.mintToken(minter, {Ethereum: owner});306307 const address = helper.ethAddress.fromCollectionId(collection.collectionId);308 const contract = helper.ethNativeContract.collection(address, 'nft');309310 {311 await contract.methods.setApprovalForAll(operator, true).send({from: owner});312 const ownerCross = helper.ethCrossAccount.fromAddress(owner);313 const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});314 const events = result.events.Transfer;315316 expect(events).to.be.like({317 address,318 event: 'Transfer',319 returnValues: {320 from: owner,321 to: '0x0000000000000000000000000000000000000000',322 tokenId: token.tokenId.toString(),323 },324 });325 }326 });327 328 itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {329 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});330331 const owner = await helper.eth.createAccountWithBalance(donor);332 const operator = await helper.eth.createAccountWithBalance(donor);333 const receiver = charlie;334335 const token = await collection.mintToken(minter, {Ethereum: owner});336337 const address = helper.ethAddress.fromCollectionId(collection.collectionId);338 const contract = helper.ethNativeContract.collection(address, 'nft');339340 {341 await contract.methods.setApprovalForAll(operator, true).send({from: owner});342 const ownerCross = helper.ethCrossAccount.fromAddress(owner);343 const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);344 const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});345 const event = result.events.Transfer;346 expect(event).to.be.like({347 address: helper.ethAddress.fromCollectionId(collection.collectionId),348 event: 'Transfer',349 returnValues: {350 from: owner,351 to: helper.address.substrateToEth(receiver.address),352 tokenId: token.tokenId.toString(),353 },354 });355 }356357 expect(await token.getOwner()).to.be.like({Substrate: receiver.address});358 });359360 itEth('Can perform burnFromCross()', async ({helper}) => {361 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});362 const ownerSub = bob;363 const ownerCrossSub = helper.ethCrossAccount.fromKeyringPair(ownerSub);364 const ownerEth = await helper.eth.createAccountWithBalance(donor, 100n);365 const ownerCrossEth = helper.ethCrossAccount.fromAddress(ownerEth);366367 const burnerEth = await helper.eth.createAccountWithBalance(donor, 100n);368 const burnerCrossEth = helper.ethCrossAccount.fromAddress(burnerEth);369370 const token1 = await collection.mintToken(minter, {Substrate: ownerSub.address});371 const token2 = await collection.mintToken(minter, {Ethereum: ownerEth});372373 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);374 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft');375376 // Approve tokens from substrate and ethereum:377 await token1.approve(ownerSub, {Ethereum: burnerEth});378 await collectionEvm.methods.approveCross(burnerCrossEth, token2.tokenId).send({from: ownerEth});379380 // can burnFromCross:381 const result1 = await collectionEvm.methods.burnFromCross(ownerCrossSub, token1.tokenId).send({from: burnerEth});382 const result2 = await collectionEvm.methods.burnFromCross(ownerCrossEth, token2.tokenId).send({from: burnerEth});383 const events1 = result1.events.Transfer;384 const events2 = result2.events.Transfer;385386 // Check events for burnFromCross (substrate and ethereum):387 [388 [events1, token1, helper.address.substrateToEth(ownerSub.address)], 389 [events2, token2, ownerEth],390 ].map(burnData => {391 expect(burnData[0]).to.be.like({392 address: collectionAddress,393 event: 'Transfer',394 returnValues: {395 from: burnData[2],396 to: '0x0000000000000000000000000000000000000000',397 tokenId: burnData[1].tokenId.toString(),398 },399 });400 });401402 expect(await token1.doesExist()).to.be.false;403 expect(await token2.doesExist()).to.be.false;404 });405406 itEth('Can perform approveCross()', async ({helper}) => {407 // arrange: create accounts408 const owner = await helper.eth.createAccountWithBalance(donor, 100n);409 const ownerCross = helper.ethCrossAccount.fromAddress(owner);410 const receiverSub = charlie;411 const recieverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);412 const receiverEth = await helper.eth.createAccountWithBalance(donor, 100n);413 const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);414415 // arrange: create collection and tokens:416 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});417 const token1 = await collection.mintToken(minter, {Ethereum: owner});418 const token2 = await collection.mintToken(minter, {Ethereum: owner});419420 const collectionEvm = helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');421422 // Can approveCross substrate and ethereum address:423 const resultSub = await collectionEvm.methods.approveCross(recieverCrossSub, token1.tokenId).send({from: owner});424 const resultEth = await collectionEvm.methods.approveCross(receiverCrossEth, token2.tokenId).send({from: owner});425 const eventSub = resultSub.events.Approval;426 const eventEth = resultEth.events.Approval;427 expect(eventSub).to.be.like({428 address: helper.ethAddress.fromCollectionId(collection.collectionId),429 event: 'Approval',430 returnValues: {431 owner,432 approved: helper.address.substrateToEth(receiverSub.address),433 tokenId: token1.tokenId.toString(),434 },435 });436 expect(eventEth).to.be.like({437 address: helper.ethAddress.fromCollectionId(collection.collectionId),438 event: 'Approval',439 returnValues: {440 owner,441 approved: receiverEth,442 tokenId: token2.tokenId.toString(),443 },444 });445446 // Substrate address can transferFrom approved tokens:447 await helper.nft.transferTokenFrom(receiverSub, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiverSub.address});448 expect(await helper.nft.getTokenOwner(collection.collectionId, token1.tokenId)).to.deep.eq({Substrate: receiverSub.address});449 // Ethereum address can transferFromCross approved tokens:450 await collectionEvm.methods.transferFromCross(ownerCross, receiverCrossEth, token2.tokenId).send({from: receiverEth});451 expect(await helper.nft.getTokenOwner(collection.collectionId, token2.tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});452 });453454 itEth('Can reaffirm approved address', async ({helper}) => {455 const owner = await helper.eth.createAccountWithBalance(donor, 100n);456 const ownerCrossEth = helper.ethCrossAccount.fromAddress(owner);457 const [receiver1, receiver2] = await helper.arrange.createAccounts([100n, 100n], donor);458 const receiver1Cross = helper.ethCrossAccount.fromKeyringPair(receiver1);459 const receiver2Cross = helper.ethCrossAccount.fromKeyringPair(receiver2);460 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});461 const token1 = await collection.mintToken(minter, {Ethereum: owner});462 const token2 = await collection.mintToken(minter, {Ethereum: owner});463 const collectionEvm = helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');464465 // Can approve and reaffirm approved address:466 await collectionEvm.methods.approveCross(receiver1Cross, token1.tokenId).send({from: owner});467 await collectionEvm.methods.approveCross(receiver2Cross, token1.tokenId).send({from: owner});468469 // receiver1 cannot transferFrom:470 await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;471 // receiver2 can transferFrom:472 await helper.nft.transferTokenFrom(receiver2, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver2.address});473474 // can set approved address to self address to remove approval:475 await collectionEvm.methods.approveCross(receiver1Cross, token2.tokenId).send({from: owner});476 await collectionEvm.methods.approveCross(ownerCrossEth, token2.tokenId).send({from: owner});477478 // receiver1 cannot transfer token anymore:479 await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token2.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;480 });481482 itEth('Can perform transferFrom()', async ({helper}) => {483 const owner = await helper.eth.createAccountWithBalance(donor);484 const spender = await helper.eth.createAccountWithBalance(donor);485 const receiver = helper.eth.createAccount();486487 const collection = await helper.nft.mintCollection(minter, {});488 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});489490 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);491 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);492493 await contract.methods.approve(spender, tokenId).send({from: owner});494495 {496 const result = await contract.methods.transferFrom(owner, receiver, tokenId).send({from: spender});497498 const event = result.events.Transfer;499 expect(event.address).to.be.equal(collectionAddress);500 expect(event.returnValues.from).to.be.equal(owner);501 expect(event.returnValues.to).to.be.equal(receiver);502 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);503 }504505 {506 const balance = await contract.methods.balanceOf(receiver).call();507 expect(+balance).to.equal(1);508 }509510 {511 const balance = await contract.methods.balanceOf(owner).call();512 expect(+balance).to.equal(0);513 }514 });515516 itEth('Can perform transferFromCross()', async ({helper}) => {517 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});518519 const [owner, receiver] = await helper.arrange.createAccounts([100n, 100n], donor);520 const spender = await helper.eth.createAccountWithBalance(donor);521522 const token = await collection.mintToken(minter, {Substrate: owner.address});523524 const address = helper.ethAddress.fromCollectionId(collection.collectionId);525 const contract = helper.ethNativeContract.collection(address, 'nft');526527 await token.approve(owner, {Ethereum: spender});528529 {530 const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);531 const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);532 const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});533 const event = result.events.Transfer;534 expect(event).to.be.like({535 address: helper.ethAddress.fromCollectionId(collection.collectionId),536 event: 'Transfer',537 returnValues: {538 from: helper.address.substrateToEth(owner.address),539 to: helper.address.substrateToEth(receiver.address),540 tokenId: token.tokenId.toString(),541 },542 });543 }544545 expect(await token.getOwner()).to.be.like({Substrate: receiver.address});546 });547548 itEth('Can perform transfer()', async ({helper}) => {549 const collection = await helper.nft.mintCollection(minter, {});550 const owner = await helper.eth.createAccountWithBalance(donor);551 const receiver = helper.eth.createAccount();552553 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});554555 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);556 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);557558 {559 const result = await contract.methods.transfer(receiver, tokenId).send({from: owner});560561 const event = result.events.Transfer;562 expect(event.address).to.be.equal(collectionAddress);563 expect(event.returnValues.from).to.be.equal(owner);564 expect(event.returnValues.to).to.be.equal(receiver);565 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);566 }567568 {569 const balance = await contract.methods.balanceOf(owner).call();570 expect(+balance).to.equal(0);571 }572573 {574 const balance = await contract.methods.balanceOf(receiver).call();575 expect(+balance).to.equal(1);576 }577 });578 579 itEth('Can perform transferCross()', async ({helper}) => {580 const collection = await helper.nft.mintCollection(minter, {});581 const owner = await helper.eth.createAccountWithBalance(donor);582 const receiverEth = await helper.eth.createAccountWithBalance(donor);583 const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);584 const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);585 586 const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});587588 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);589 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);590591 {592 // Can transferCross to ethereum address:593 const result = await collectionEvm.methods.transferCross(receiverCrossEth, tokenId).send({from: owner});594 // Check events:595 const event = result.events.Transfer;596 expect(event.address).to.be.equal(collectionAddress);597 expect(event.returnValues.from).to.be.equal(owner);598 expect(event.returnValues.to).to.be.equal(receiverEth);599 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);600 601 // owner has balance = 0:602 const ownerBalance = await collectionEvm.methods.balanceOf(owner).call();603 expect(+ownerBalance).to.equal(0);604 // receiver owns token:605 const receiverBalance = await collectionEvm.methods.balanceOf(receiverEth).call();606 expect(+receiverBalance).to.equal(1);607 expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});608 }609 610 {611 // Can transferCross to substrate address:612 const substrateResult = await collectionEvm.methods.transferCross(receiverCrossSub, tokenId).send({from: receiverEth});613 // Check events:614 const event = substrateResult.events.Transfer;615 expect(event.address).to.be.equal(collectionAddress);616 expect(event.returnValues.from).to.be.equal(receiverEth);617 expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));618 expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);619 620 // owner has balance = 0:621 const ownerBalance = await collectionEvm.methods.balanceOf(receiverEth).call();622 expect(+ownerBalance).to.equal(0);623 // receiver owns token:624 const receiverBalance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});625 expect(receiverBalance).to.contain(tokenId);626 }627 });628629 ['transfer', 'transferCross'].map(testCase => itEth(`Cannot ${testCase} non-owned token`, async ({helper}) => {630 const sender = await helper.eth.createAccountWithBalance(donor);631 const tokenOwner = await helper.eth.createAccountWithBalance(donor);632 const receiverSub = minter;633 const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);634635 const collection = await helper.nft.mintCollection(minter, {});636 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);637 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', sender);638639 await collection.mintToken(minter, {Ethereum: sender});640 const nonSendersToken = await collection.mintToken(minter, {Ethereum: tokenOwner});641642 // Cannot transferCross someone else's token:643 const receiver = testCase === 'transfer' ? helper.address.substrateToEth(receiverSub.address) : receiverCrossSub;644 await expect(collectionEvm.methods[testCase](receiver, nonSendersToken.tokenId).send({from: sender})).to.be.rejected;645 // Cannot transfer token if it does not exist:646 await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected;647 }));648});649650describe('NFT: Fees', () => {651 let donor: IKeyringPair;652 let alice: IKeyringPair;653 let bob: IKeyringPair;654 let charlie: IKeyringPair;655656 before(async function() {657 await usingEthPlaygrounds(async (helper, privateKey) => {658 donor = await privateKey({filename: __filename});659 [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);660 });661 });662663 itEth('approve() call fee is less than 0.2UNQ', async ({helper}) => {664 const owner = await helper.eth.createAccountWithBalance(donor);665 const spender = helper.eth.createAccount();666667 const collection = await helper.nft.mintCollection(alice, {});668 const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});669670 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);671672 const cost = await helper.eth.recordCallFee(owner, () => contract.methods.approve(spender, tokenId).send({from: owner}));673 expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));674 });675676 itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {677 const owner = await helper.eth.createAccountWithBalance(donor);678 const spender = await helper.eth.createAccountWithBalance(donor);679680 const collection = await helper.nft.mintCollection(alice, {});681 const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});682683 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);684685 await contract.methods.approve(spender, tokenId).send({from: owner});686687 const cost = await helper.eth.recordCallFee(spender, () => contract.methods.transferFrom(owner, spender, tokenId).send({from: spender}));688 expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));689 });690691 itEth('Can perform transferFromCross()', async ({helper}) => {692 const collectionMinter = alice;693 const owner = bob;694 const receiver = charlie;695 const collection = await helper.nft.mintCollection(collectionMinter, {name: 'A', description: 'B', tokenPrefix: 'C'});696697 const spender = await helper.eth.createAccountWithBalance(donor, 100n);698699 const token = await collection.mintToken(collectionMinter, {Substrate: owner.address});700701 const address = helper.ethAddress.fromCollectionId(collection.collectionId);702 const contract = helper.ethNativeContract.collection(address, 'nft');703704 await token.approve(owner, {Ethereum: spender});705706 {707 const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);708 const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);709 const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});710 const event = result.events.Transfer;711 expect(event).to.be.like({712 address: helper.ethAddress.fromCollectionId(collection.collectionId),713 event: 'Transfer',714 returnValues: {715 from: helper.address.substrateToEth(owner.address),716 to: helper.address.substrateToEth(receiver.address),717 tokenId: token.tokenId.toString(),718 },719 });720 }721722 expect(await token.getOwner()).to.be.like({Substrate: receiver.address});723 });724725 itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {726 const owner = await helper.eth.createAccountWithBalance(donor);727 const receiver = helper.eth.createAccount();728729 const collection = await helper.nft.mintCollection(alice, {});730 const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});731732 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);733734 const cost = await helper.eth.recordCallFee(owner, () => contract.methods.transfer(receiver, tokenId).send({from: owner}));735 expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));736 });737});738739describe('NFT: Substrate calls', () => {740 let donor: IKeyringPair;741 let alice: IKeyringPair;742743 before(async function() {744 await usingEthPlaygrounds(async (helper, privateKey) => {745 donor = await privateKey({filename: __filename});746 [alice] = await helper.arrange.createAccounts([20n], donor);747 });748 });749750 itEth('Events emitted for mint()', async ({helper}) => {751 const collection = await helper.nft.mintCollection(alice, {});752 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);753 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');754755 const events: any = [];756 contract.events.allEvents((_: any, event: any) => {757 events.push(event);758 });759760 const {tokenId} = await collection.mintToken(alice);761 if (events.length == 0) await helper.wait.newBlocks(1);762 const event = events[0];763764 expect(event.event).to.be.equal('Transfer');765 expect(event.address).to.be.equal(collectionAddress);766 expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');767 expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(alice.address));768 expect(event.returnValues.tokenId).to.be.equal(tokenId.toString());769 });770771 itEth('Events emitted for burn()', async ({helper}) => {772 const collection = await helper.nft.mintCollection(alice, {});773 const token = await collection.mintToken(alice);774775 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);776 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');777778 const events: any = [];779 contract.events.allEvents((_: any, event: any) => {780 events.push(event);781 });782783 await token.burn(alice);784 if (events.length == 0) await helper.wait.newBlocks(1);785 const event = events[0];786787 expect(event.event).to.be.equal('Transfer');788 expect(event.address).to.be.equal(collectionAddress);789 expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));790 expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');791 expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());792 });793794 itEth('Events emitted for approve()', async ({helper}) => {795 const receiver = helper.eth.createAccount();796797 const collection = await helper.nft.mintCollection(alice, {});798 const token = await collection.mintToken(alice);799800 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);801 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');802803 const events: any = [];804 contract.events.allEvents((_: any, event: any) => {805 events.push(event);806 });807808 await token.approve(alice, {Ethereum: receiver});809 if (events.length == 0) await helper.wait.newBlocks(1);810 const event = events[0];811812 expect(event.event).to.be.equal('Approval');813 expect(event.address).to.be.equal(collectionAddress);814 expect(event.returnValues.owner).to.be.equal(helper.address.substrateToEth(alice.address));815 expect(event.returnValues.approved).to.be.equal(receiver);816 expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());817 });818819 itEth('Events emitted for transferFrom()', async ({helper}) => {820 const [bob] = await helper.arrange.createAccounts([10n], donor);821 const receiver = helper.eth.createAccount();822823 const collection = await helper.nft.mintCollection(alice, {});824 const token = await collection.mintToken(alice);825 await token.approve(alice, {Substrate: bob.address});826827 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);828 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');829830 const events: any = [];831 contract.events.allEvents((_: any, event: any) => {832 events.push(event);833 });834835 await token.transferFrom(bob, {Substrate: alice.address}, {Ethereum: receiver});836837 if (events.length == 0) await helper.wait.newBlocks(1);838 const event = events[0];839840 expect(event.address).to.be.equal(collectionAddress);841 expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));842 expect(event.returnValues.to).to.be.equal(receiver);843 expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);844 });845846 itEth('Events emitted for transfer()', async ({helper}) => {847 const receiver = helper.eth.createAccount();848849 const collection = await helper.nft.mintCollection(alice, {});850 const token = await collection.mintToken(alice);851852 const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);853 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');854855 const events: any = [];856 contract.events.allEvents((_: any, event: any) => {857 events.push(event);858 });859860 await token.transfer(alice, {Ethereum: receiver});861862 if (events.length == 0) await helper.wait.newBlocks(1);863 const event = events[0];864865 expect(event.address).to.be.equal(collectionAddress);866 expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));867 expect(event.returnValues.to).to.be.equal(receiver);868 expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);869 });870});871872describe('Common metadata', () => {873 let donor: IKeyringPair;874 let alice: IKeyringPair;875876 before(async function() {877 await usingEthPlaygrounds(async (helper, privateKey) => {878 donor = await privateKey({filename: __filename});879 [alice] = await helper.arrange.createAccounts([20n], donor);880 });881 });882883 itEth('Returns collection name', async ({helper}) => {884 const caller = await helper.eth.createAccountWithBalance(donor);885 const tokenPropertyPermissions = [{886 key: 'URI',887 permission: {888 mutable: true,889 collectionAdmin: true,890 tokenOwner: false,891 },892 }];893 const collection = await helper.nft.mintCollection(894 alice,895 {896 name: 'oh River',897 tokenPrefix: 'CHANGE',898 properties: [{key: 'ERC721Metadata', value: '1'}],899 tokenPropertyPermissions,900 },901 );902903 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);904 const name = await contract.methods.name().call();905 expect(name).to.equal('oh River');906 });907908 itEth('Returns symbol name', async ({helper}) => {909 const caller = await helper.eth.createAccountWithBalance(donor);910 const tokenPropertyPermissions = [{911 key: 'URI',912 permission: {913 mutable: true,914 collectionAdmin: true,915 tokenOwner: false,916 },917 }];918 const collection = await helper.nft.mintCollection(919 alice,920 {921 name: 'oh River',922 tokenPrefix: 'CHANGE',923 properties: [{key: 'ERC721Metadata', value: '1'}],924 tokenPropertyPermissions,925 },926 );927928 const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);929 const symbol = await contract.methods.symbol().call();930 expect(symbol).to.equal('CHANGE');931 });932});933934describe('Negative tests', () => {935 let donor: IKeyringPair;936 let minter: IKeyringPair;937 let alice: IKeyringPair;938 let bob: IKeyringPair;939940 before(async function() {941 await usingEthPlaygrounds(async (helper, privateKey) => {942 donor = await privateKey({filename: __filename});943 [minter, alice, bob] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);944 });945 });946947 itEth('[negative] Cant perform burn without approval', async ({helper}) => {948 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});949950 const owner = bob;951 const spender = await helper.eth.createAccountWithBalance(donor, 100n);952953 const token = await collection.mintToken(minter, {Substrate: owner.address});954955 const address = helper.ethAddress.fromCollectionId(collection.collectionId);956 const contract = helper.ethNativeContract.collection(address, 'nft');957958 {959 const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);960 await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;961 }962 });963964 itEth('[negative] Cant perform transfer without approval', async ({helper}) => {965 const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});966 const owner = bob;967 const receiver = alice;968969 const spender = await helper.eth.createAccountWithBalance(donor, 100n);970971 const token = await collection.mintToken(minter, {Substrate: owner.address});972973 const address = helper.ethAddress.fromCollectionId(collection.collectionId);974 const contract = helper.ethNativeContract.collection(address, 'nft');975976 {977 const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);978 const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);979 await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;980 }981 });982});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');
+ }
}