--- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -755,6 +755,17 @@ .map_err(|_| Error::Revert("token not found".into())) } + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An cross address for whom to query the balance + /// @return The number of NFTs owned by `owner`, possibly zero + fn cross_balance_of(&self, owner: eth::CrossAddress) -> Result { + self.consume_store_reads(1)?; + let balance = >::get((self.id, owner.into_sub_cross_account::()?)); + Ok(balance.into()) + } + /// Returns the token properties. /// /// @param tokenId Id for the token. --- a/pallets/nonfungible/src/stubs/UniqueNFT.sol +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -774,7 +774,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x16de3152 +/// @dev the ERC-165 identifier for this interface is 0x323b1db8 contract ERC721UniqueExtensions is Dummy, ERC165 { /// @notice A descriptive name for a collection of NFTs in this contract /// @dev EVM selector for this function is: 0x06fdde03, @@ -815,6 +815,20 @@ return CrossAddress(0x0000000000000000000000000000000000000000, 0); } + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An cross address for whom to query the balance + /// @return The number of NFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x24e52cea, + /// or in textual repr: crossBalanceOf((address,uint256)) + function crossBalanceOf(CrossAddress memory owner) public view returns (uint256) { + require(false, stub_error); + owner; + dummy; + return 0; + } + /// Returns the token properties. /// /// @param tokenId Id for the token. --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -794,6 +794,17 @@ }) } + /// @notice Count all RFTs assigned to an owner + /// @dev RFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An cross address for whom to query the balance + /// @return The number of RFTs owned by `owner`, possibly zero + fn cross_balance_of(&self, owner: eth::CrossAddress) -> Result { + self.consume_store_reads(1)?; + let balance = >::get((self.id, owner.into_sub_cross_account::()?)); + Ok(balance.into()) + } + /// Returns the token properties. /// /// @param tokenId Id for the token. --- a/pallets/refungible/src/stubs/UniqueRefungible.sol +++ b/pallets/refungible/src/stubs/UniqueRefungible.sol @@ -774,7 +774,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xb365c124 +/// @dev the ERC-165 identifier for this interface is 0x9780edce contract ERC721UniqueExtensions is Dummy, ERC165 { /// @notice A descriptive name for a collection of NFTs in this contract /// @dev EVM selector for this function is: 0x06fdde03, @@ -815,6 +815,20 @@ return CrossAddress(0x0000000000000000000000000000000000000000, 0); } + /// @notice Count all RFTs assigned to an owner + /// @dev RFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An cross address for whom to query the balance + /// @return The number of RFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x24e52cea, + /// or in textual repr: crossBalanceOf((address,uint256)) + function crossBalanceOf(CrossAddress memory owner) public view returns (uint256) { + require(false, stub_error); + owner; + dummy; + return 0; + } + /// Returns the token properties. /// /// @param tokenId Id for the token. --- a/runtime/common/ethereum/sponsoring/refungible.rs +++ b/runtime/common/ethereum/sponsoring/refungible.rs @@ -227,6 +227,7 @@ | Symbol | Description | CrossOwnerOf { .. } + | CrossBalanceOf { .. } | Properties { .. } | NextTokenId | TokenContractAddress { .. } --- a/tests/src/eth/abi/nonFungible.json +++ b/tests/src/eth/abi/nonFungible.json @@ -386,6 +386,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "eth", "type": "address" }, + { "internalType": "uint256", "name": "sub", "type": "uint256" } + ], + "internalType": "struct CrossAddress", + "name": "owner", + "type": "tuple" + } + ], + "name": "crossBalanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "crossOwnerOf", --- a/tests/src/eth/api/UniqueNFT.sol +++ b/tests/src/eth/api/UniqueNFT.sol @@ -533,7 +533,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x16de3152 +/// @dev the ERC-165 identifier for this interface is 0x323b1db8 interface ERC721UniqueExtensions is Dummy, ERC165 { /// @notice A descriptive name for a collection of NFTs in this contract /// @dev EVM selector for this function is: 0x06fdde03, @@ -557,6 +557,15 @@ /// or in textual repr: crossOwnerOf(uint256) function crossOwnerOf(uint256 tokenId) external view returns (CrossAddress memory); + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An cross address for whom to query the balance + /// @return The number of NFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x24e52cea, + /// or in textual repr: crossBalanceOf((address,uint256)) + function crossBalanceOf(CrossAddress memory owner) external view returns (uint256); + /// Returns the token properties. /// /// @param tokenId Id for the token. --- a/tests/src/eth/api/UniqueRefungible.sol +++ b/tests/src/eth/api/UniqueRefungible.sol @@ -533,7 +533,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xb365c124 +/// @dev the ERC-165 identifier for this interface is 0x9780edce interface ERC721UniqueExtensions is Dummy, ERC165 { /// @notice A descriptive name for a collection of NFTs in this contract /// @dev EVM selector for this function is: 0x06fdde03, @@ -557,6 +557,15 @@ /// or in textual repr: crossOwnerOf(uint256) function crossOwnerOf(uint256 tokenId) external view returns (CrossAddress memory); + /// @notice Count all RFTs assigned to an owner + /// @dev RFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An cross address for whom to query the balance + /// @return The number of RFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x24e52cea, + /// or in textual repr: crossBalanceOf((address,uint256)) + function crossBalanceOf(CrossAddress memory owner) external view returns (uint256); + /// Returns the token properties. /// /// @param tokenId Id for the token. --- a/tests/src/eth/nonFungible.test.ts +++ b/tests/src/eth/nonFungible.test.ts @@ -662,6 +662,38 @@ // Cannot transfer token if it does not exist: await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected; })); + + itEth('Check crossBalanceOf()', async ({helper}) => { + const collection = await helper.nft.mintCollection(minter, {}); + const owner = await helper.ethCrossAccount.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth); + + expect(BigInt(await collectionEvm.methods.crossBalanceOf(owner).call({from: owner.eth})) === 0n).to.be.true; + + for (let i = 1n; i < 100n; i++) { + await collection.mintToken(minter, {Ethereum: owner.eth}); + expect(BigInt(await collectionEvm.methods.crossBalanceOf(owner).call({from: owner.eth})) === i).to.be.true; + } + }); + + itEth('Check crossOwnerOf()', async ({helper}) => { + const collection = await helper.nft.mintCollection(minter, {}); + let owner = await helper.ethCrossAccount.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth); + const {tokenId} = await collection.mintToken(minter, {Ethereum: owner.eth}); + + for (let i = 1n; i < 100n; i++) { + const ownerCross = await collectionEvm.methods.crossOwnerOf(tokenId).call({from: owner.eth}); + expect(ownerCross.eth).to.be.eq(owner.eth); + expect(ownerCross.sub).to.be.eq(owner.sub); + + const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor); + await collectionEvm.methods.transferCross(newOwner, tokenId).send({from: owner.eth}); + owner = newOwner; + } + }); }); describe('NFT: Fees', () => { --- a/tests/src/eth/reFungible.test.ts +++ b/tests/src/eth/reFungible.test.ts @@ -584,6 +584,46 @@ expect(event.returnValues.to).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'); expect(event.returnValues.tokenId).to.equal(tokenId.toString()); }); + + itEth('Check crossBalanceOf()', async ({helper}) => { + const collection = await helper.rft.mintCollection(minter, {}); + const owner = await helper.ethCrossAccount.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth); + + expect(BigInt(await collectionEvm.methods.crossBalanceOf(owner).call({from: owner.eth})) === 0n).to.be.true; + + for (let i = 1n; i < 100n; i++) { + await collection.mintToken(minter, 100n, {Ethereum: owner.eth}); + expect(BigInt(await collectionEvm.methods.crossBalanceOf(owner).call({from: owner.eth})) === i).to.be.true; + } + }); + + itEth.only('Check crossOwnerOf()', async ({helper}) => { + const collection = await helper.rft.mintCollection(minter, {}); + let owner = await helper.ethCrossAccount.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth); + const {tokenId} = await collection.mintToken(minter, 100n,{Ethereum: owner.eth}); + + for (let i = 1n; i < 100n; i++) { + const ownerCross = await collectionEvm.methods.crossOwnerOf(tokenId).call({from: owner.eth}); + expect(ownerCross.eth).to.be.eq(owner.eth); + expect(ownerCross.sub).to.be.eq(owner.sub); + + const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor); + await collectionEvm.methods.transferCross(newOwner, tokenId).send({from: owner.eth}); + owner = newOwner; + } + + const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, tokenId); + const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, owner.eth, true); + const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor); + await tokenContract.methods.transferCross(newOwner, 50).send({from: owner.eth}); + const ownerCross = await collectionEvm.methods.crossOwnerOf(tokenId).call({from: owner.eth}); + expect(ownerCross.eth.toUpperCase()).to.be.eq('0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'); + expect(ownerCross.sub).to.be.eq('0'); + }); }); describe('RFT: Fees', () => {