--- a/client/rpc/src/lib.rs +++ b/client/rpc/src/lib.rs @@ -246,6 +246,16 @@ token_id: TokenId, at: Option, ) -> Result>; + + /// 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, + ) -> Result; } mod app_promotion_unique_rpc { @@ -569,6 +579,7 @@ pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option, unique_api); pass_method!(total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option => |o| o.map(|number| number.to_string()) , unique_api); pass_method!(token_owners(collection: CollectionId, token: TokenId) -> Vec, unique_api); + pass_method!(is_approved_for_all(collection: CollectionId, owner: CrossAccountId, operator: CrossAccountId) -> bool, unique_api); } impl --- 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>; + + /// 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. --- 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 @@ } >::try_get(self.id).ok() } + + fn set_approval_for_all( + &self, + _owner: T::CrossAccountId, + _operator: T::CrossAccountId, + _approve: bool, + ) -> DispatchResultWithPostInfo { + fail!(>::SettingApprovalForAllNotAllowed) + } + + fn is_approved_for_all(&self, _owner: T::CrossAccountId, _operator: T::CrossAccountId) -> bool { + false + } } --- 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] --- 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; + }; + }: {>::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; + }; + }: {>::is_approved_for_all(&collection, &owner, &operator)} } --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -122,6 +122,10 @@ fn token_owner() -> Weight { >::token_owner() } + + fn set_approval_for_all() -> Weight { + >::set_approval_for_all() + } } fn map_create_data( @@ -512,4 +516,20 @@ None } } + + fn set_approval_for_all( + &self, + owner: T::CrossAccountId, + operator: T::CrossAccountId, + approve: bool, + ) -> DispatchResultWithPostInfo { + with_weight( + >::set_approval_for_all(self, &owner, &operator, approve), + >::set_approval_for_all(), + ) + } + + fn is_approved_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool { + >::is_approved_for_all(self, &owner, &operator) + } } --- 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(>::set_approval_for_all())] fn set_approval_for_all( &mut self, - _caller: caller, - _operator: address, - _approved: bool, + caller: caller, + operator: address, + approved: bool, ) -> Result { - // TODO: Not implemetable - Err("not implemented".into()) + let caller = T::CrossAccountId::from_eth(caller); + let operator = T::CrossAccountId::from_eth(operator); + + >::set_approval_for_all(self, &caller, &operator, approved) + .map_err(dispatch_to_evm::)?; + 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
{ - // TODO: Not implemetable - Err("not implemented".into()) + /// @notice Tells whether an operator is approved by a given owner. + #[weight(>::is_approved_for_all())] + fn is_approved_for_all(&self, owner: address, operator: address) -> Result { + let owner = T::CrossAccountId::from_eth(owner); + let operator = T::CrossAccountId::from_eth(operator); + + Ok(>::is_approved_for_all(self, &owner, &operator)) } /// @notice Returns collection helper contract address --- 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 = StorageNMap< + Key = ( + Key, + Key, + Key, + ), + Value = bool, + QueryKind = OptionQuery, + >; + /// Upgrade from the old schema to properties. #[pallet::hooks] impl Hooks> for Pallet { @@ -438,6 +450,7 @@ >::remove(id); let _ = >::clear_prefix((id,), u32::MAX, None); let _ = >::clear_prefix((id,), u32::MAX, None); + let _ = >::clear_prefix((id,), u32::MAX, None); Ok(()) } @@ -1193,6 +1206,9 @@ if >::get((collection.id, token)).as_ref() == Some(spender) { return Ok(()); } + if >::get((collection.id, from, spender)) == Some(true) { + return Ok(()); + } ensure!( collection.ignores_allowance(spender), >::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, + owner: &T::CrossAccountId, + operator: &T::CrossAccountId, + approve: bool, + ) -> DispatchResult { + if collection.permissions.access() == AccessMode::AllowList { + collection.check_allowlist(owner)?; + collection.check_allowlist(operator)?; + } + + >::ensure_correct_receiver(operator)?; + + // ========= + + >::insert((collection.id, owner, operator), approve); + >::deposit_log( + ERC721Events::ApprovalForAll { + owner: *owner.as_eth(), + operator: *operator.as_eth(), + approved: approve, + } + .to_log(collection_id_to_address(collection.id)), + ); + >::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, + owner: &T::CrossAccountId, + operator: &T::CrossAccountId, + ) -> bool { + >::get((collection.id, owner, operator)).unwrap_or(false) + } } --- 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 --- 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)) + } } --- 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)])?; }: {>::token_owner(collection.id, item)} + + set_approval_for_all { + bench_init!{ + owner: sub; collection: collection(owner); + operator: cross_from_sub(owner); owner: cross_sub; + }; + }: {>::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; + }; + }: {>::is_approved_for_all(&collection, &owner, &operator)} } --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -152,6 +152,10 @@ fn token_owner() -> Weight { >::token_owner() } + + fn set_approval_for_all() -> Weight { + >::set_approval_for_all() + } } fn map_create_data( @@ -516,6 +520,22 @@ fn total_pieces(&self, token: TokenId) -> Option { >::total_pieces(self.id, token) } + + fn set_approval_for_all( + &self, + owner: T::CrossAccountId, + operator: T::CrossAccountId, + approve: bool, + ) -> DispatchResultWithPostInfo { + with_weight( + >::set_approval_for_all(self, &owner, &operator, approve), + >::set_approval_for_all(), + ) + } + + fn is_approved_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool { + >::is_approved_for_all(self, &owner, &operator) + } } impl RefungibleExtensions for RefungibleHandle { --- 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(>::set_approval_for_all())] fn set_approval_for_all( &mut self, - _caller: caller, - _operator: address, - _approved: bool, + caller: caller, + operator: address, + approved: bool, ) -> Result { - // TODO: Not implemetable - Err("not implemented".into()) + let caller = T::CrossAccountId::from_eth(caller); + let operator = T::CrossAccountId::from_eth(operator); + + >::set_approval_for_all(self, &caller, &operator, approved) + .map_err(dispatch_to_evm::)?; + 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
{ - // TODO: Not implemetable - Err("not implemented".into()) + /// @notice Tells whether an operator is approved by a given owner. + #[weight(>::is_approved_for_all())] + fn is_approved_for_all(&self, owner: address, operator: address) -> Result { + let owner = T::CrossAccountId::from_eth(owner); + let operator = T::CrossAccountId::from_eth(operator); + + Ok(>::is_approved_for_all(self, &owner, &operator)) } /// @notice Returns collection helper contract address --- 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 = StorageNMap< + Key = ( + Key, + Key, + Key, + ), + Value = bool, + QueryKind = OptionQuery, + >; + #[pallet::hooks] impl Hooks> for Pallet { fn on_runtime_upgrade() -> Weight { @@ -1161,6 +1173,12 @@ } let allowance = >::get((collection.id, token, from, &spender)).checked_sub(amount); + + // Allowance if any would be reduced if spender is also wallet operator + if >::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, + owner: &T::CrossAccountId, + operator: &T::CrossAccountId, + approve: bool, + ) -> DispatchResult { + if collection.permissions.access() == AccessMode::AllowList { + collection.check_allowlist(owner)?; + collection.check_allowlist(operator)?; + } + + >::ensure_correct_receiver(operator)?; + + // ========= + + >::insert((collection.id, owner, operator), approve); + >::deposit_log( + ERC721Events::ApprovalForAll { + owner: *owner.as_eth(), + operator: *operator.as_eth(), + approved: approve, + } + .to_log(collection_id_to_address(collection.id)), + ); + >::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, + owner: &T::CrossAccountId, + operator: &T::CrossAccountId, + ) -> bool { + >::get((collection.id, owner, operator)).unwrap_or(false) + } } --- 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 --- 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)) + } } --- 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::(collection_id, |d| { + d.set_approval_for_all(sender, operator, approve) + }) + } } } --- 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>; fn token_owners(collection: CollectionId, token: TokenId) -> Result>; + + /// Get whether an operator is approved by a given owner. + fn is_approved_for_all(collection: CollectionId, owner: CrossAccountId, operator: CrossAccountId) -> Result; } } --- 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, DispatchError> { dispatch_unique_runtime!(collection.total_pieces(token_id)) } + + fn is_approved_for_all(collection: CollectionId, owner: CrossAccountId, operator: CrossAccountId) -> Result { + dispatch_unique_runtime!(collection.is_approved_for_all(owner, operator)) + } } impl app_promotion_rpc::AppPromotionApi for Runtime { --- 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")] --- 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; + }); +}); --- 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" }, --- 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" }, --- 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, --- 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, --- a/tests/src/eth/nonFungible.test.ts +++ b/tests/src/eth/nonFungible.test.ts @@ -249,6 +249,114 @@ } }); + itEth('Can perform setApprovalForAll()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const operator = helper.eth.createAccount(); + + const collection = await helper.nft.mintCollection(minter, {}); + + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); + + const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call(); + expect(approvedBefore).to.be.equal(false); + + { + const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner}); + + expect(result.events.ApprovalForAll).to.be.like({ + address: collectionAddress, + event: 'ApprovalForAll', + returnValues: { + owner, + operator, + approved: true, + }, + }); + + const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call(); + expect(approvedAfter).to.be.equal(true); + } + + { + const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner}); + + expect(result.events.ApprovalForAll).to.be.like({ + address: collectionAddress, + event: 'ApprovalForAll', + returnValues: { + owner, + operator, + approved: false, + }, + }); + + const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call(); + expect(approvedAfter).to.be.equal(false); + } + }); + + itEth('Can perform burn with ApprovalForAll', async ({helper}) => { + const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await helper.eth.createAccountWithBalance(donor); + const operator = await helper.eth.createAccountWithBalance(donor, 100n); + + const token = await collection.mintToken(minter, {Ethereum: owner}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + { + await contract.methods.setApprovalForAll(operator, true).send({from: owner}); + const ownerCross = helper.ethCrossAccount.fromAddress(owner); + const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator}); + const events = result.events.Transfer; + + expect(events).to.be.like({ + address, + event: 'Transfer', + returnValues: { + from: owner, + to: '0x0000000000000000000000000000000000000000', + tokenId: token.tokenId.toString(), + }, + }); + } + }); + + itEth('Can perform transfer with ApprovalForAll', async ({helper}) => { + const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await helper.eth.createAccountWithBalance(donor); + const operator = await helper.eth.createAccountWithBalance(donor); + const receiver = charlie; + + const token = await collection.mintToken(minter, {Ethereum: owner}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + { + await contract.methods.setApprovalForAll(operator, true).send({from: owner}); + const ownerCross = helper.ethCrossAccount.fromAddress(owner); + const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver); + const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator}); + const event = result.events.Transfer; + expect(event).to.be.like({ + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Transfer', + returnValues: { + from: owner, + to: helper.address.substrateToEth(receiver.address), + tokenId: token.tokenId.toString(), + }, + }); + } + + expect(await token.getOwner()).to.be.like({Substrate: receiver.address}); + }); + itEth('Can perform burnFromCross()', async ({helper}) => { const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); const ownerSub = bob; @@ -822,3 +930,53 @@ expect(symbol).to.equal('CHANGE'); }); }); + +describe('Negative tests', () => { + let donor: IKeyringPair; + let minter: IKeyringPair; + let alice: IKeyringPair; + let bob: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + donor = await privateKey({filename: __filename}); + [minter, alice, bob] = await helper.arrange.createAccounts([100n, 100n, 100n], donor); + }); + }); + + itEth('[negative] Cant perform burn without approval', async ({helper}) => { + const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = bob; + const spender = await helper.eth.createAccountWithBalance(donor, 100n); + + const token = await collection.mintToken(minter, {Substrate: owner.address}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + { + const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner); + await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected; + } + }); + + itEth('[negative] Cant perform transfer without approval', async ({helper}) => { + const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'}); + const owner = bob; + const receiver = alice; + + const spender = await helper.eth.createAccountWithBalance(donor, 100n); + + const token = await collection.mintToken(minter, {Substrate: owner.address}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + { + const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner); + const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver); + await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected; + } + }); +}); --- 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; + } + }); +}); --- a/tests/src/interfaces/augment-api-errors.ts +++ b/tests/src/interfaces/augment-api-errors.ts @@ -386,6 +386,10 @@ **/ NotFungibleDataUsedToMintFungibleCollectionToken: AugmentedError; /** + * Setting approval for all is not allowed. + **/ + SettingApprovalForAllNotAllowed: AugmentedError; + /** * Setting item properties is not allowed. **/ SettingPropertiesNotAllowed: AugmentedError; --- a/tests/src/interfaces/augment-api-events.ts +++ b/tests/src/interfaces/augment-api-events.ts @@ -107,6 +107,10 @@ **/ Approved: AugmentedEvent; /** + * Amount pieces of token owned by `sender` was approved for `spender`. + **/ + ApprovedForAll: AugmentedEvent; + /** * New collection was created **/ CollectionCreated: AugmentedEvent; --- a/tests/src/interfaces/augment-api-query.ts +++ b/tests/src/interfaces/augment-api-query.ts @@ -442,6 +442,10 @@ **/ tokensMinted: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; /** + * Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet. + **/ + walletOperator: AugmentedQuery Observable>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; + /** * Generic query **/ [key: string]: QueryableStorageEntry; @@ -645,6 +649,10 @@ **/ totalSupply: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; /** + * Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet. + **/ + walletOperator: AugmentedQuery Observable>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; + /** * Generic query **/ [key: string]: QueryableStorageEntry; --- 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>>; /** + * 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>>; + /** * Get the last token ID created in a collection **/ lastTokenId: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable>; --- 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, [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, [u32, PalletEvmAccountBasicCrossAccountIdRepr, bool]>; + /** * Set specific limits of a collection. Empty, or None fields mean chain default. * * # Permissions --- 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 */ --- 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 + * Lookup100: pallet_structure::pallet::Event **/ PalletStructureEvent: { _enum: { @@ -1062,7 +1063,7 @@ } }, /** - * Lookup100: pallet_rmrk_core::pallet::Event + * Lookup101: pallet_rmrk_core::pallet::Event **/ PalletRmrkCoreEvent: { _enum: { @@ -1139,7 +1140,7 @@ } }, /** - * Lookup101: rmrk_traits::nft::AccountIdOrCollectionNftTuple + * Lookup102: rmrk_traits::nft::AccountIdOrCollectionNftTuple **/ 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 **/ PalletFungibleError: { - _enum: ['NotFungibleDataUsedToMintFungibleCollectionToken', 'FungibleItemsHaveNoId', 'FungibleItemsDontHaveData', 'FungibleDisallowsNesting', 'SettingPropertiesNotAllowed'] + _enum: ['NotFungibleDataUsedToMintFungibleCollectionToken', 'FungibleItemsHaveNoId', 'FungibleItemsDontHaveData', 'FungibleDisallowsNesting', 'SettingPropertiesNotAllowed', 'SettingApprovalForAllNotAllowed'] }, /** * Lookup431: pallet_refungible::ItemData --- 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; 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) */ --- a/tests/src/interfaces/unique/definitions.ts +++ b/tests/src/interfaces/unique/definitions.ts @@ -175,5 +175,10 @@ [collectionParam, tokenParam], 'Option', ), + isApprovedForAll: fun( + 'Tells whether an operator is approved by a given owner.', + [collectionParam, crossAccountParam('owner'), crossAccountParam('operator')], + 'Option', + ), }, }; --- 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 { + 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 { + 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'); + } }