--- a/Cargo.lock +++ b/Cargo.lock @@ -5953,7 +5953,7 @@ [[package]] name = "pallet-fungible" -version = "0.1.0" +version = "0.1.1" dependencies = [ "ethereum", "evm-coder", @@ -6198,7 +6198,7 @@ [[package]] name = "pallet-nonfungible" -version = "0.1.0" +version = "0.1.1" dependencies = [ "ethereum", "evm-coder", @@ -12377,7 +12377,7 @@ [[package]] name = "uc-rpc" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "jsonrpsee", @@ -12682,7 +12682,7 @@ [[package]] name = "unique-runtime-common" -version = "0.9.24" +version = "0.9.25" dependencies = [ "evm-coder", "fp-rpc", @@ -12754,7 +12754,7 @@ [[package]] name = "up-rpc" -version = "0.1.0" +version = "0.1.1" dependencies = [ "pallet-common", "pallet-evm", --- /dev/null +++ b/client/rpc/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.1.1] - 2022-07-14 + +### Added + + - Implementation of RPC method `token_owners` returning 10 owners in no particular order. + This was an internal request to improve the web interface and support fractionalization event. + \ No newline at end of file --- a/client/rpc/Cargo.toml +++ b/client/rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uc-rpc" -version = "0.1.0" +version = "0.1.1" license = "GPLv3" edition = "2021" --- a/client/rpc/src/lib.rs +++ b/client/rpc/src/lib.rs @@ -70,6 +70,16 @@ token: TokenId, at: Option, ) -> Result>; + + /// Returns 10 tokens owners in no particular order. + #[method(name = "unique_tokenOwners")] + fn token_owners( + &self, + collection: CollectionId, + token: TokenId, + at: Option, + ) -> Result>; + #[method(name = "unique_topmostTokenOwner")] fn topmost_token_owner( &self, @@ -473,6 +483,7 @@ pass_method!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option, unique_api); pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option, unique_api); pass_method!(total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option, unique_api); + pass_method!(token_owners(collection: CollectionId, token: TokenId) -> Vec, unique_api); } #[allow(deprecated)] --- /dev/null +++ b/pallets/common/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.1.1] - 2022-07-14 + +### Added + + - Implementation of RPC method `token_owners` returning 10 owners in no particular order. + This was an internal request to improve the web interface and support fractionalization event. + + \ No newline at end of file --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -1748,6 +1748,11 @@ /// * `token` - The token for which you need to find out the owner. fn token_owner(&self, token: TokenId) -> Option; + /// Returns 10 tokens owners in no particular order. + /// + /// * `token` - The token for which you need to find out the owners. + fn token_owners(&self, token: TokenId) -> Vec; + /// Get the value of the token property by key. /// /// * `token` - Token with the property to get. --- /dev/null +++ b/pallets/fungible/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.1.1] - 2022-07-14 + +### Added + + - Implementation of RPC method `token_owners` returning 10 owners in no particular order. + This was an internal request to improve the web interface and support fractionalization event. + + + \ No newline at end of file --- a/pallets/fungible/Cargo.toml +++ b/pallets/fungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-fungible" -version = "0.1.0" +version = "0.1.1" license = "GPLv3" edition = "2021" --- a/pallets/fungible/src/common.rs +++ b/pallets/fungible/src/common.rs @@ -362,6 +362,11 @@ None } + /// Returns 10 tokens owners in no particular order. + fn token_owners(&self, token: TokenId) -> Vec { + >::token_owners(self.id, token).unwrap_or_default() + } + fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option { None } --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -95,7 +95,7 @@ use pallet_evm_coder_substrate::WithRecorder; use sp_core::H160; use sp_runtime::{ArithmeticError, DispatchError, DispatchResult}; -use sp_std::{collections::btree_map::BTreeMap}; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; pub use pallet::*; @@ -613,4 +613,26 @@ nesting_budget, ) } + + /// Returns 10 tokens owners in no particular order + /// + /// There is no direct way to get token holders in ascending order, + /// since `iter_prefix` returns values in no particular order. + /// Therefore, getting the 10 largest holders with a large value of holders + /// can lead to impact memory allocation + sorting with `n * log (n)`. + pub fn token_owners( + collection: CollectionId, + _token: TokenId, + ) -> Option> { + let res: Vec = >::iter_prefix((collection,)) + .map(|(owner, _amount)| owner) + .take(10) + .collect(); + + if res.is_empty() { + None + } else { + Some(res) + } + } } --- /dev/null +++ b/pallets/nonfungible/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.1.1] - 2022-07-14 + +### Added + +- Implementation of RPC method `token_owners`. + For reasons of compatibility with this pallet, returns only one owner if token exists. + This was an internal request to improve the web interface and support fractionalization event. \ No newline at end of file --- a/pallets/nonfungible/Cargo.toml +++ b/pallets/nonfungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-nonfungible" -version = "0.1.0" +version = "0.1.1" license = "GPLv3" edition = "2021" --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -26,7 +26,7 @@ weights::WeightInfo as _, }; use sp_runtime::DispatchError; -use sp_std::vec::Vec; +use sp_std::{vec::Vec, vec}; use crate::{ AccountBalance, Allowance, Config, CreateItemData, Error, NonfungibleHandle, Owned, Pallet, @@ -422,6 +422,11 @@ >::get((self.id, token)).map(|t| t.owner) } + /// Returns token owners. + fn token_owners(&self, token: TokenId) -> Vec { + self.token_owner(token).map_or_else(|| vec![], |t| vec![t]) + } + fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option { >::token_properties((self.id, token_id)) .get(key) --- a/pallets/refungible/CHANGELOG.md +++ b/pallets/refungible/CHANGELOG.md @@ -1,6 +1,18 @@ -## v0.1.2 - 2022-07 +# Change Log -### Refungible Pallet +All notable changes to this project will be documented in this file. +## [v0.1.2] - 2022-07-14 + +### Other changes + feat(refungible-pallet): add ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413)) -test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413)) \ No newline at end of file +test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413)) + +## [v0.1.1] - 2022-07-14 + +### Other changes + +- feat: RPC method `token_owners` returning 10 owners in no particular order. + +This was an internal request to improve the web interface and support fractionalization event. --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -438,6 +438,11 @@ >::token_owner(self.id, token) } + /// Returns 10 token in no particular order. + fn token_owners(&self, token: TokenId) -> Vec { + >::token_owners(self.id, token).unwrap_or_default() + } + fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option { None } --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -1235,4 +1235,26 @@ ) -> DispatchResult { >::set_token_property_permissions(collection, sender, property_permissions) } + + /// Returns 10 token in no particular order. + /// + /// There is no direct way to get token holders in ascending order, + /// since `iter_prefix` returns values in no particular order. + /// Therefore, getting the 10 largest holders with a large value of holders + /// can lead to impact memory allocation + sorting with `n * log (n)`. + pub fn token_owners( + collection_id: CollectionId, + token: TokenId, + ) -> Option> { + let res: Vec = >::iter_prefix((collection_id, token)) + .map(|(owner, _amount)| owner) + .take(10) + .collect(); + + if res.is_empty() { + None + } else { + Some(res) + } + } } --- /dev/null +++ b/primitives/rpc/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.1.1] - 2022-07-14 + +### Added + + - Implementation of RPC method `token_owners` returning 10 owners in no particular order. + This was an internal request to improve the web interface and support fractionalization event. \ No newline at end of file --- a/primitives/rpc/Cargo.toml +++ b/primitives/rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "up-rpc" -version = "0.1.0" +version = "0.1.1" license = "GPLv3" edition = "2021" --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -81,5 +81,6 @@ fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result>; fn effective_collection_limits(collection_id: CollectionId) -> Result>; fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Result>; + fn token_owners(collection: CollectionId, token: TokenId) -> Result>; } } --- /dev/null +++ b/runtime/common/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.9.25] - 2022-07-14 + +### Added + + - Implementation of RPC method `token_owners` returning 10 owners in no particular order. + This was an internal request to improve the web interface and support fractionalization event. + + + \ No newline at end of file --- a/runtime/common/src/runtime_apis.rs +++ b/runtime/common/src/runtime_apis.rs @@ -40,6 +40,11 @@ fn token_owner(collection: CollectionId, token: TokenId) -> Result, DispatchError> { dispatch_unique_runtime!(collection.token_owner(token)) } + + fn token_owners(collection: CollectionId, token: TokenId) -> Result, DispatchError> { + dispatch_unique_runtime!(collection.token_owners(token)) + } + fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result, DispatchError> { let budget = up_data_structs::budget::Value::new(10); --- /dev/null +++ b/tests/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## 2022-07-14 + +### Added + - Integrintegration tests of RPC method `token_owners`. + - Integrintegration tests of Fungible Pallet. + + \ No newline at end of file --- a/tests/package.json +++ b/tests/package.json @@ -80,6 +80,8 @@ "testLimits": "mocha --timeout 9999999 -r ts-node/register ./**/limits.test.ts", "testEthCreateCollection": "mocha --timeout 9999999 -r ts-node/register ./**/eth/createCollection.test.ts", "testRFT": "mocha --timeout 9999999 -r ts-node/register ./**/refungible.test.ts", + "testFT": "mocha --timeout 9999999 -r ts-node/register ./**/fungible.test.ts", + "testRPC": "mocha --timeout 9999999 -r ts-node/register ./**/rpc.test.ts", "polkadot-types-fetch-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/interfaces/metadata.json", "polkadot-types-from-defs": "ts-node ./node_modules/.bin/polkadot-types-from-defs --endpoint src/interfaces/metadata.json --input src/interfaces/ --package .", "polkadot-types-from-chain": "ts-node ./node_modules/.bin/polkadot-types-from-chain --endpoint src/interfaces/metadata.json --output src/interfaces/ --package .", --- /dev/null +++ b/tests/src/fungible.test.ts @@ -0,0 +1,184 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {default as usingApi} from './substrate/substrate-api'; +import {IKeyringPair} from '@polkadot/types/types'; +import { + getBalance, + createMultipleItemsExpectSuccess, + isTokenExists, + getLastTokenId, + getAllowance, + approve, + transferFrom, + createCollection, + transfer, + burnItem, + normalizeAccountId, + CrossAccountId, + createFungibleItemExpectSuccess, + U128_MAX, + burnFromExpectSuccess, +} from './util/helpers'; + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised); +const expect = chai.expect; + +let alice: IKeyringPair; +let bob: IKeyringPair; + +describe('integration test: Fungible functionality:', () => { + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + }); + }); + + it('Create fungible collection and token', async () => { + await usingApi(async api => { + const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}}); + expect(createCollectionResult.success).to.be.true; + const collectionId = createCollectionResult.collectionId; + const defaultTokenId = await getLastTokenId(api, collectionId); + const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address); + const aliceBalance = await getBalance(api, collectionId, alice, aliceTokenId); + const itemCountAfter = await getLastTokenId(api, collectionId); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(itemCountAfter).to.be.equal(defaultTokenId); + expect(aliceBalance).to.be.equal(U128_MAX); + }); + }); + + it('RPC method tokenOnewrs for fungible collection and token', async () => { + await usingApi(async (api, privateKeyWrapper) => { + const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'}; + const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString()))); + + const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}}); + const collectionId = createCollectionResult.collectionId; + const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address); + + await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n); + await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n); + + for (let i = 0; i < 7; i++) { + await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 1); + } + + const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId); + const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s)); + const aliceID = normalizeAccountId(alice); + const bobId = normalizeAccountId(bob); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]); + expect(owners.length == 10).to.be.true; + + const eleven = privateKeyWrapper('11'); + expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true; + expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10); + }); + }); + + it('Transfer token', async () => { + await usingApi(async api => { + const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'}; + const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId; + const tokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address); + + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n); + expect(await transfer(api, collectionId, tokenId, alice, bob, 60n)).to.be.true; + expect(await transfer(api, collectionId, tokenId, alice, ethAcc, 140n)).to.be.true; + + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(300n); + expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(60n); + expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(140n); + await expect(transfer(api, collectionId, tokenId, alice, bob, 350n)).to.eventually.be.rejected; + }); + }); + + it('Tokens multiple creation', async () => { + await usingApi(async api => { + const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId; + + const args = [ + {Fungible: {Value: 500n}}, + {Fungible: {Value: 400n}}, + {Fungible: {Value: 300n}}, + ]; + + await createMultipleItemsExpectSuccess(alice, collectionId, args); + expect(await getBalance(api, collectionId, alice, 0)).to.be.equal(1200n); + }); + }); + + it('Burn some tokens ', async () => { + await usingApi(async api => { + const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId; + const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address)); + expect(await isTokenExists(api, collectionId, tokenId)).to.be.true; + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n); + expect(await burnItem(api, alice, collectionId, tokenId, 499n)).to.be.true; + expect(await isTokenExists(api, collectionId, tokenId)).to.be.true; + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(1n); + }); + }); + + it('Burn all tokens ', async () => { + await usingApi(async api => { + const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId; + const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address)); + expect(await isTokenExists(api, collectionId, tokenId)).to.be.true; + expect(await burnItem(api, alice, collectionId, tokenId, 500n)).to.be.true; + expect(await isTokenExists(api, collectionId, tokenId)).to.be.true; + + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(0n); + expect((await api.rpc.unique.totalPieces(collectionId, tokenId)).value.toBigInt()).to.be.equal(0n); + }); + }); + + it('Set allowance for token', async () => { + await usingApi(async api => { + const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId; + const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'}; + + const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 100n}, alice.address)); + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n); + + expect(await approve(api, collectionId, tokenId, alice, bob, 60n)).to.be.true; + expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(60n); + expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(0n); + + expect(await transferFrom(api, collectionId, tokenId, bob, alice, bob, 20n)).to.be.true; + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(80n); + expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(20n); + expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(40n); + + await burnFromExpectSuccess(bob, alice, collectionId, tokenId, 10n); + + expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(70n); + expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(30n); + expect(await transferFrom(api, collectionId, tokenId, bob, alice, ethAcc, 10n)).to.be.true; + expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(10n); + }); + }); +}); --- a/tests/src/interfaces/augment-api-consts.ts +++ b/tests/src/interfaces/augment-api-consts.ts @@ -29,7 +29,13 @@ [key: string]: Codec; }; common: { + /** + * Maximum admins per collection. + **/ collectionAdminsLimit: u32 & AugmentedConst; + /** + * Set price to create a collection. + **/ collectionCreationPrice: u128 & AugmentedConst; /** * Generic const --- a/tests/src/interfaces/augment-api-events.ts +++ b/tests/src/interfaces/augment-api-events.ts @@ -59,82 +59,47 @@ }; common: { /** - * * collection_id - * - * * item_id - * - * * sender - * - * * spender - * - * * amount + * Amount pieces of token owned by `sender` was approved for `spender`. **/ Approved: AugmentedEvent; /** * New collection was created - * - * # Arguments - * - * * collection_id: Globally unique identifier of newly created collection. - * - * * mode: [CollectionMode] converted into u8. - * - * * account_id: Collection owner. **/ CollectionCreated: AugmentedEvent; /** * New collection was destroyed - * - * # Arguments - * - * * collection_id: Globally unique identifier of collection. **/ CollectionDestroyed: AugmentedEvent; + /** + * The property has been deleted. + **/ CollectionPropertyDeleted: AugmentedEvent; + /** + * The colletion property has been set. + **/ CollectionPropertySet: AugmentedEvent; /** * New item was created. - * - * # Arguments - * - * * collection_id: Id of the collection where item was created. - * - * * item_id: Id of an item. Unique within the collection. - * - * * recipient: Owner of newly created item - * - * * amount: Always 1 for NFT **/ ItemCreated: AugmentedEvent; /** * Collection item was burned. - * - * # Arguments - * - * * collection_id. - * - * * item_id: Identifier of burned NFT. - * - * * owner: which user has destroyed its tokens - * - * * amount: Always 1 for NFT **/ ItemDestroyed: AugmentedEvent; + /** + * The colletion property permission has been set. + **/ PropertyPermissionSet: AugmentedEvent; + /** + * The token property has been deleted. + **/ TokenPropertyDeleted: AugmentedEvent; + /** + * The token property has been set. + **/ TokenPropertySet: AugmentedEvent; /** * Item was transferred - * - * * collection_id: Id of collection to which item is belong - * - * * item_id: Id of an item - * - * * sender: Original owner of item - * - * * recipient: New owner of item - * - * * amount: Always 1 for NFT **/ Transfer: AugmentedEvent; /** --- a/tests/src/interfaces/augment-api-query.ts +++ b/tests/src/interfaces/augment-api-query.ts @@ -69,24 +69,36 @@ [key: string]: QueryableStorageEntry; }; common: { + /** + * Storage of collection admins count. + **/ adminAmount: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; /** * Allowlisted collection users **/ allowlist: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; /** - * Collection info + * Storage of collection info. **/ collectionById: AugmentedQuery Observable>, [u32]> & QueryableStorageEntry; /** - * Collection properties + * Storage of collection properties. **/ collectionProperties: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; + /** + * Storage of collection properties permissions. + **/ collectionPropertyPermissions: AugmentedQuery Observable>, [u32]> & QueryableStorageEntry; + /** + * Storage of the count of created collections. + **/ createdCollectionCount: AugmentedQuery Observable, []> & QueryableStorageEntry; + /** + * Storage of the count of deleted collections. + **/ destroyedCollectionCount: AugmentedQuery Observable, []> & QueryableStorageEntry; /** - * Not used by code, exists only to provide some types to metadata + * Not used by code, exists only to provide some types to metadata. **/ dummyStorageValue: AugmentedQuery Observable>>, []> & QueryableStorageEntry; /** @@ -231,20 +243,42 @@ [key: string]: QueryableStorageEntry; }; nonfungible: { + /** + * Amount of tokens owned by account. + **/ accountBalance: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry; + /** + * Allowance set by an owner for a spender for a token. + **/ allowance: AugmentedQuery Observable>, [u32, u32]> & QueryableStorageEntry; /** - * Used to enumerate tokens owned by account + * Used to enumerate tokens owned by account. **/ owned: AugmentedQuery Observable, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32]> & QueryableStorageEntry; + /** + * Custom data that is serialized to bytes and attached to a token property. + * Currently used to store RMRK data. + **/ tokenAuxProperties: AugmentedQuery Observable>, [u32, u32, UpDataStructsPropertyScope, Bytes]> & QueryableStorageEntry; /** - * Used to enumerate token's children + * Used to enumerate token's children. **/ tokenChildren: AugmentedQuery | [u32 | AnyNumber | Uint8Array, u32 | AnyNumber | Uint8Array]) => Observable, [u32, u32, ITuple<[u32, u32]>]> & QueryableStorageEntry]>; + /** + * Custom data serialized to bytes for token. + **/ tokenData: AugmentedQuery Observable>, [u32, u32]> & QueryableStorageEntry; + /** + * Key-Value map stored for token. + **/ tokenProperties: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; + /** + * Amount of burnt tokens for collection. + **/ tokensBurnt: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; + /** + * Amount of tokens minted for collection. + **/ tokensMinted: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; /** * Generic query @@ -430,6 +464,9 @@ **/ tokenData: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; tokenProperties: AugmentedQuery Observable, [u32, u32]> & QueryableStorageEntry; + /** + * Amount of burnt tokens for collection + **/ tokensBurnt: AugmentedQuery Observable, [u32]> & QueryableStorageEntry; /** * Amount of tokens minted for collection --- a/tests/src/interfaces/augment-api-rpc.ts +++ b/tests/src/interfaces/augment-api-rpc.ts @@ -709,6 +709,10 @@ **/ tokenOwner: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable>>; /** + * Returns 10 tokens owners in no particular order + **/ + tokenOwners: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable>>; + /** * Get token properties **/ tokenProperties: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, propertyKeys?: Vec | (Text | string)[], at?: Hash | string | Uint8Array) => Observable>>; --- a/tests/src/interfaces/unique/definitions.ts +++ b/tests/src/interfaces/unique/definitions.ts @@ -49,6 +49,7 @@ balance: fun('Get amount of specific account token', [collectionParam, crossAccountParam(), tokenParam], 'u128'), allowance: fun('Get allowed amount', [collectionParam, crossAccountParam('sender'), crossAccountParam('spender'), tokenParam], 'u128'), tokenOwner: fun('Get token owner', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`), + tokenOwners: fun('Returns 10 tokens owners in no particular order', [collectionParam, tokenParam], `Vec<${CROSS_ACCOUNT_ID_TYPE}>`), topmostTokenOwner: fun('Get token owner, in case of nested token - find parent recursive', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`), tokenChildren: fun('Get tokens nested directly into the token', [collectionParam, tokenParam], 'Vec'), constMetadata: fun('Get token constant metadata', [collectionParam, tokenParam], 'Vec'), --- a/tests/src/refungible.test.ts +++ b/tests/src/refungible.test.ts @@ -32,6 +32,8 @@ repartitionRFT, createCollectionWithPropsExpectSuccess, getDetailedCollectionInfo, + normalizeAccountId, + CrossAccountId, getCreateItemsResult, getDestroyItemsResult, } from './util/helpers'; @@ -55,14 +57,14 @@ it('Create refungible collection and token', async () => { await usingApi(async api => { const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}}); - expect(createCollectionResult.success).to.be.true; + expect(createCollectionResult.success).to.be.true; const collectionId = createCollectionResult.collectionId; - + const itemCountBefore = await getLastTokenId(api, collectionId); const result = await createRefungibleToken(api, alice, collectionId, 100n); - + const itemCountAfter = await getLastTokenId(api, collectionId); - + // What to expect // tslint:disable-next-line:no-unused-expression expect(result.success).to.be.true; @@ -71,7 +73,43 @@ expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString()); }); }); - + + it('RPC method tokenOnewrs for refungible collection and token', async () => { + await usingApi(async (api, privateKeyWrapper) => { + const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'}; + const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString()))); + + const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}}); + const collectionId = createCollectionResult.collectionId; + + const result = await createRefungibleToken(api, alice, collectionId, 10_000n); + const aliceTokenId = result.itemId; + + + await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n); + await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n); + + for (let i = 0; i < 7; i++) { + await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 50*(i+1)); + } + + const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId); + const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s)); + + const aliceID = normalizeAccountId(alice); + const bobId = normalizeAccountId(bob); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]); + expect(owners.length).to.be.equal(10); + + const eleven = privateKeyWrapper('11'); + expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true; + expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10); + }); + }); + it('Transfer token pieces', async () => { await usingApi(async api => { const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId; --- a/tests/src/rpc.test.ts +++ b/tests/src/rpc.test.ts @@ -1,12 +1,57 @@ +import {IKeyringPair} from '@polkadot/types/types'; import {expect} from 'chai'; import usingApi from './substrate/substrate-api'; -import {createCollectionExpectSuccess, getTokenOwner} from './util/helpers'; +import {createCollection, createCollectionExpectSuccess, createFungibleItemExpectSuccess, CrossAccountId, getTokenOwner, normalizeAccountId, transfer, U128_MAX} from './util/helpers'; + +let alice: IKeyringPair; +let bob: IKeyringPair; -describe('getTokenOwner', () => { + +describe('integration test: RPC methods', () => { + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + bob = privateKeyWrapper('//Bob'); + }); + }); + + it('returns None for fungible collection', async () => { await usingApi(async api => { const collection = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); await expect(getTokenOwner(api, collection, 0)).to.be.rejectedWith(/^owner == null$/); }); }); + + it('RPC method tokenOwners for fungible collection and token', async () => { + await usingApi(async (api, privateKeyWrapper) => { + const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'}; + const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString()))); + + const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}}); + const collectionId = createCollectionResult.collectionId; + const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address); + + await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n); + await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n); + + for (let i = 0; i < 7; i++) { + await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 1); + } + + const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId); + const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s)); + const aliceID = normalizeAccountId(alice); + const bobId = normalizeAccountId(bob); + + // What to expect + // tslint:disable-next-line:no-unused-expression + expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]); + expect(owners.length == 10).to.be.true; + + const eleven = privateKeyWrapper('11'); + expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true; + expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10); + }); + }); }); \ No newline at end of file