difftreelog
feat(rpc) token children
in: master
9 files changed
client/rpc/src/lib.rsdiffbeforeafterboth--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -21,7 +21,7 @@
use jsonrpc_derive::rpc;
use up_data_structs::{
RpcCollection, CollectionId, CollectionStats, CollectionLimits, TokenId, Property,
- PropertyKeyPermission, TokenData,
+ PropertyKeyPermission, TokenData, TokenChild,
};
use sp_api::{BlockId, BlockT, ProvideRuntimeApi, ApiExt};
use sp_blockchain::HeaderBackend;
@@ -72,6 +72,13 @@
token: TokenId,
at: Option<BlockHash>,
) -> Result<Option<CrossAccountId>>;
+ #[rpc(name = "unique_tokenChildren")]
+ fn token_children(
+ &self,
+ collection: CollectionId,
+ token: TokenId,
+ at: Option<BlockHash>,
+ ) -> Result<Vec<TokenChild>>;
#[rpc(name = "unique_collectionProperties")]
fn collection_properties(
@@ -411,6 +418,7 @@
pass_method!(
topmost_token_owner(collection: CollectionId, token: TokenId) -> Option<CrossAccountId>, unique_api
);
+ pass_method!(token_children(collection: CollectionId, token: TokenId) -> Vec<TokenChild>, unique_api);
pass_method!(total_supply(collection: CollectionId) -> u32, unique_api);
pass_method!(account_balance(collection: CollectionId, account: CrossAccountId) -> u32, unique_api);
pass_method!(balance(collection: CollectionId, account: CrossAccountId, token: TokenId) -> String => |v| v.to_string(), unique_api);
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -40,6 +40,7 @@
MAX_TOKEN_PREFIX_LENGTH,
COLLECTION_ADMINS_LIMIT,
TokenId,
+ TokenChild,
CollectionStats,
MAX_TOKEN_OWNERSHIP,
CollectionMode,
@@ -502,6 +503,7 @@
CollectionStats,
CollectionId,
TokenId,
+ TokenChild,
PhantomType<(
TokenData<T::CrossAccountId>,
RpcCollection<T::AccountId>,
pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -22,7 +22,7 @@
use up_data_structs::{
AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,
mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,
- PropertyKey, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty,
+ PropertyKey, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild,
};
use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
use pallet_common::{
@@ -988,6 +988,15 @@
.is_some()
}
+ pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {
+ <TokenChildren<T>>::iter_prefix((collection_id, token_id))
+ .map(|((child_collection_id, child_id), _)| TokenChild {
+ collection: child_collection_id,
+ token: child_id,
+ })
+ .collect()
+ }
+
/// Delegated to `create_multiple_items`
pub fn create_item(
collection: &NonfungibleHandle<T>,
primitives/data-structs/src/lib.rsdiffbeforeafterboth--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -585,6 +585,14 @@
#[derive(Encode, Decode, MaxEncodedLen, PartialEq, Clone, Debug, TypeInfo)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
+// todo possibly rename to be used generally as an address pair
+pub struct TokenChild {
+ pub token: TokenId,
+ pub collection: CollectionId,
+}
+
+#[derive(Encode, Decode, MaxEncodedLen, PartialEq, Clone, Debug, TypeInfo)]
+#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct CollectionStats {
pub created: u32,
pub destroyed: u32,
primitives/rpc/src/lib.rsdiffbeforeafterboth--- a/primitives/rpc/src/lib.rs
+++ b/primitives/rpc/src/lib.rs
@@ -18,7 +18,7 @@
use up_data_structs::{
CollectionId, TokenId, RpcCollection, CollectionStats, CollectionLimits, Property,
- PropertyKeyPermission, TokenData,
+ PropertyKeyPermission, TokenData, TokenChild,
};
use sp_std::vec::Vec;
use codec::Decode;
@@ -41,6 +41,7 @@
fn token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>>;
fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>>;
+ fn token_children(collection: CollectionId, token: TokenId) -> Result<Vec<TokenChild>>;
fn collection_properties(collection: CollectionId, properties: Option<Vec<Vec<u8>>>) -> Result<Vec<Property>>;
runtime/common/src/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -29,7 +29,9 @@
Ok(Some(<pallet_structure::Pallet<Runtime>>::find_topmost_owner(collection, token, &budget)?))
}
-
+ fn token_children(collection: CollectionId, token: TokenId) -> Result<Vec<TokenChild>, DispatchError> {
+ Ok(<pallet_nonfungible::Pallet<Runtime>>::token_children_ids(collection, token))
+ }
fn collection_properties(
collection: CollectionId,
keys: Option<Vec<Vec<u8>>>
tests/src/interfaces/unique/definitions.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617type RpcParam = {18 name: string;19 type: string;20 isOptional?: true;21};2223const CROSS_ACCOUNT_ID_TYPE = 'PalletEvmAccountBasicCrossAccountIdRepr';2425const collectionParam = {name: 'collection', type: 'u32'};26const tokenParam = {name: 'tokenId', type: 'u32'};27const propertyKeysParam = {name: 'propertyKeys', type: 'Vec<String>', isOptional: true};28const crossAccountParam = (name = 'account') => ({name, type: CROSS_ACCOUNT_ID_TYPE});29const atParam = {name: 'at', type: 'Hash', isOptional: true};3031const fun = (description: string, params: RpcParam[], type: string) => ({32 description,33 params: [...params, atParam],34 type,35});3637export default {38 types: {},39 rpc: {40 adminlist: fun('Get admin list', [collectionParam], 'Vec<PalletEvmAccountBasicCrossAccountIdRepr>'),41 allowlist: fun('Get allowlist', [collectionParam], 'Vec<PalletEvmAccountBasicCrossAccountIdRepr>'),4243 accountTokens: fun('Get tokens owned by account', [collectionParam, crossAccountParam()], 'Vec<u32>'),44 collectionTokens: fun('Get tokens contained in collection', [collectionParam], 'Vec<u32>'),4546 lastTokenId: fun('Get last token id', [collectionParam], 'u32'),47 totalSupply: fun('Get amount of unique collection tokens', [collectionParam], 'u32'),48 accountBalance: fun('Get amount of different user tokens', [collectionParam, crossAccountParam()], 'u32'),49 balance: fun('Get amount of specific account token', [collectionParam, crossAccountParam(), tokenParam], 'u128'),50 allowance: fun('Get allowed amount', [collectionParam, crossAccountParam('sender'), crossAccountParam('spender'), tokenParam], 'u128'),51 tokenOwner: fun('Get token owner', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`),52 topmostTokenOwner: fun('Get token owner, in case of nested token - find parent recursive', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`),53 constMetadata: fun('Get token constant metadata', [collectionParam, tokenParam], 'Vec<u8>'),54 variableMetadata: fun('Get token variable metadata', [collectionParam, tokenParam], 'Vec<u8>'),55 collectionProperties: fun(56 'Get collection properties',57 [collectionParam, propertyKeysParam],58 'Vec<UpDataStructsProperty>',59 ),60 tokenProperties: fun(61 'Get token properties',62 [collectionParam, tokenParam, propertyKeysParam],63 'Vec<UpDataStructsProperty>',64 ),65 propertyPermissions: fun(66 'Get property permissions',67 [collectionParam, propertyKeysParam],68 'Vec<UpDataStructsPropertyKeyPermission>',69 ),70 tokenData: fun(71 'Get token data',72 [collectionParam, tokenParam, propertyKeysParam],73 'UpDataStructsTokenData',74 ),75 tokenExists: fun('Check if token exists', [collectionParam, tokenParam], 'bool'),76 collectionById: fun('Get collection by specified id', [collectionParam], 'Option<UpDataStructsRpcCollection>'),77 collectionStats: fun('Get collection stats', [], 'UpDataStructsCollectionStats'),78 allowed: fun('Check if user is allowed to use collection', [collectionParam, crossAccountParam()], 'bool'),79 nextSponsored: fun('Get number of blocks when sponsored transaction is available', [collectionParam, crossAccountParam(), tokenParam], 'Option<u64>'),80 effectiveCollectionLimits: fun('Get effective collection limits', [collectionParam], 'Option<UpDataStructsCollectionLimits>'),81 },82};1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617type RpcParam = {18 name: string;19 type: string;20 isOptional?: true;21};2223const CROSS_ACCOUNT_ID_TYPE = 'PalletEvmAccountBasicCrossAccountIdRepr';2425const collectionParam = {name: 'collection', type: 'u32'};26const tokenParam = {name: 'tokenId', type: 'u32'};27const propertyKeysParam = {name: 'propertyKeys', type: 'Vec<String>', isOptional: true};28const crossAccountParam = (name = 'account') => ({name, type: CROSS_ACCOUNT_ID_TYPE});29const atParam = {name: 'at', type: 'Hash', isOptional: true};3031const fun = (description: string, params: RpcParam[], type: string) => ({32 description,33 params: [...params, atParam],34 type,35});3637export default {38 types: {},39 rpc: {40 adminlist: fun('Get admin list', [collectionParam], 'Vec<PalletEvmAccountBasicCrossAccountIdRepr>'),41 allowlist: fun('Get allowlist', [collectionParam], 'Vec<PalletEvmAccountBasicCrossAccountIdRepr>'),4243 accountTokens: fun('Get tokens owned by account', [collectionParam, crossAccountParam()], 'Vec<u32>'),44 collectionTokens: fun('Get tokens contained in collection', [collectionParam], 'Vec<u32>'),4546 lastTokenId: fun('Get last token id', [collectionParam], 'u32'),47 totalSupply: fun('Get amount of unique collection tokens', [collectionParam], 'u32'),48 accountBalance: fun('Get amount of different user tokens', [collectionParam, crossAccountParam()], 'u32'),49 balance: fun('Get amount of specific account token', [collectionParam, crossAccountParam(), tokenParam], 'u128'),50 allowance: fun('Get allowed amount', [collectionParam, crossAccountParam('sender'), crossAccountParam('spender'), tokenParam], 'u128'),51 tokenOwner: fun('Get token owner', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`),52 topmostTokenOwner: fun('Get token owner, in case of nested token - find parent recursive', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`),53 tokenChildren: fun('Get tokens nested directly into the token', [collectionParam, tokenParam], 'Vec<UpDataStructsTokenChild>'),54 constMetadata: fun('Get token constant metadata', [collectionParam, tokenParam], 'Vec<u8>'),55 variableMetadata: fun('Get token variable metadata', [collectionParam, tokenParam], 'Vec<u8>'),56 collectionProperties: fun(57 'Get collection properties',58 [collectionParam, propertyKeysParam],59 'Vec<UpDataStructsProperty>',60 ),61 tokenProperties: fun(62 'Get token properties',63 [collectionParam, tokenParam, propertyKeysParam],64 'Vec<UpDataStructsProperty>',65 ),66 propertyPermissions: fun(67 'Get property permissions',68 [collectionParam, propertyKeysParam],69 'Vec<UpDataStructsPropertyKeyPermission>',70 ),71 tokenData: fun(72 'Get token data',73 [collectionParam, tokenParam, propertyKeysParam],74 'UpDataStructsTokenData',75 ),76 tokenExists: fun('Check if token exists', [collectionParam, tokenParam], 'bool'),77 collectionById: fun('Get collection by specified id', [collectionParam], 'Option<UpDataStructsRpcCollection>'),78 collectionStats: fun('Get collection stats', [], 'UpDataStructsCollectionStats'),79 allowed: fun('Check if user is allowed to use collection', [collectionParam, crossAccountParam()], 'bool'),80 nextSponsored: fun('Get number of blocks when sponsored transaction is available', [collectionParam, crossAccountParam(), tokenParam], 'Option<u64>'),81 effectiveCollectionLimits: fun('Get effective collection limits', [collectionParam], 'Option<UpDataStructsCollectionLimits>'),82 },83};tests/src/nesting/nest.test.tsdiffbeforeafterboth--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -8,6 +8,7 @@
createItemExpectSuccess,
enableAllowListExpectSuccess,
enablePublicMintingExpectSuccess,
+ getTokenChildren,
getTokenOwner,
getTopmostTokenOwner,
normalizeAccountId,
@@ -89,6 +90,63 @@
});
});
+ it('Checks token children', async () => {
+ await usingApi(async api => {
+ const collectionA = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+ await setCollectionPermissionsExpectSuccess(alice, collectionA, {nesting: 'Owner'});
+ const collectionB = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+
+ const targetToken = await createItemExpectSuccess(alice, collectionA, 'NFT');
+ const targetAddress = {Ethereum: tokenIdToAddress(collectionA, targetToken)};
+ let children = await getTokenChildren(api, collectionA, targetToken);
+ expect(children.length).to.be.equal(0, 'Children length check at creation');
+
+ // Create a nested NFT token
+ const tokenA = await createItemExpectSuccess(alice, collectionA, 'NFT', targetAddress);
+ children = await getTokenChildren(api, collectionA, targetToken);
+ expect(children.length).to.be.equal(1, 'Children length check at nesting #1');
+ expect(children).to.have.deep.members([
+ {token: tokenA, collection: collectionA},
+ ], 'Children contents check at nesting #1');
+
+ // Create then nest
+ const tokenB = await createItemExpectSuccess(alice, collectionA, 'NFT');
+ await transferExpectSuccess(collectionA, tokenB, alice, targetAddress);
+ children = await getTokenChildren(api, collectionA, targetToken);
+ expect(children.length).to.be.equal(2, 'Children length check at nesting #2');
+ expect(children).to.have.deep.members([
+ {token: tokenA, collection: collectionA},
+ {token: tokenB, collection: collectionA},
+ ], 'Children contents check at nesting #2');
+
+ // Move token B to a different user outside the nesting tree
+ await transferFromExpectSuccess(collectionA, tokenB, alice, targetAddress, bob);
+ children = await getTokenChildren(api, collectionA, targetToken);
+ expect(children.length).to.be.equal(1, 'Children length check at unnesting');
+ expect(children).to.be.have.deep.members([
+ {token: tokenA, collection: collectionA},
+ ], 'Children contents check at unnesting');
+
+ // Create a fungible token in another collection and then nest
+ const tokenC = await createItemExpectSuccess(alice, collectionB, 'Fungible');
+ await transferExpectSuccess(collectionB, tokenC, alice, targetAddress, 1, 'Fungible');
+ children = await getTokenChildren(api, collectionA, targetToken);
+ expect(children.length).to.be.equal(2, 'Children length check at nesting #3 (from another collection)');
+ expect(children).to.be.have.deep.members([
+ {token: tokenA, collection: collectionA},
+ {token: tokenC, collection: collectionB},
+ ], 'Children contents check at nesting #3 (from another collection)');
+
+ // Move the fungible token inside token A deeper in the nesting tree
+ await transferFromExpectSuccess(collectionB, tokenC, alice, targetAddress, {Ethereum: tokenIdToAddress(collectionA, tokenA)}, 1, 'Fungible');
+ children = await getTokenChildren(api, collectionA, targetToken);
+ expect(children.length).to.be.equal(1, 'Children length check at deeper nesting');
+ expect(children).to.be.have.deep.members([
+ {token: tokenA, collection: collectionA},
+ ], 'Children contents check at deeper nesting');
+ });
+ });
+
// ---------- Non-Fungible ----------
it('NFT: allows an Owner to nest/unnest their token', async () => {
@@ -232,6 +290,20 @@
});
});
+ // TODO delete if this is actually wrong
+ // TODO remake all other nesting tests if this is right
+ it('Affirms that transfer is disallowed to transfer nested tokens', async () => {
+ await usingApi(async () => {
+ const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+ await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: 'Owner'});
+
+ const tokenA = await createItemExpectSuccess(alice, collection, 'NFT');
+ const tokenB = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, tokenA)});
+
+ await transferExpectFailure(collection, tokenB, alice, bob);
+ });
+ });
+
it('Disallows excessive token nesting', async () => {
await usingApi(async api => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
tests/src/util/helpers.tsdiffbeforeafterboth--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -28,6 +28,7 @@
import {default as usingApi, executeTransaction, submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api';
import {hexToStr, strToUTF16, utf16ToStr} from './util';
import {UpDataStructsRpcCollection, UpDataStructsCreateItemData, UpDataStructsProperty} from '@polkadot/types/lookup';
+import {UpDataStructsTokenChild} from '../interfaces';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -1072,6 +1073,13 @@
if (owner == null) throw new Error('owner == null');
return normalizeAccountId(owner);
}
+export async function getTokenChildren(
+ api: ApiPromise,
+ collectionId: number,
+ tokenId: number,
+): Promise<UpDataStructsTokenChild[]> {
+ return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any;
+}
export async function isTokenExists(
api: ApiPromise,
collectionId: number,