--- 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, ) -> Result>; + #[rpc(name = "unique_tokenChildren")] + fn token_children( + &self, + collection: CollectionId, + token: TokenId, + at: Option, + ) -> Result>; #[rpc(name = "unique_collectionProperties")] fn collection_properties( @@ -411,6 +418,7 @@ pass_method!( topmost_token_owner(collection: CollectionId, token: TokenId) -> Option, unique_api ); + pass_method!(token_children(collection: CollectionId, token: TokenId) -> Vec, 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); --- 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, RpcCollection, --- 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 { + >::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, --- 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, --- 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>; fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result>; + fn token_children(collection: CollectionId, token: TokenId) -> Result>; fn collection_properties(collection: CollectionId, properties: Option>>) -> Result>; --- a/runtime/common/src/runtime_apis.rs +++ b/runtime/common/src/runtime_apis.rs @@ -29,7 +29,9 @@ Ok(Some(>::find_topmost_owner(collection, token, &budget)?)) } - + fn token_children(collection: CollectionId, token: TokenId) -> Result, DispatchError> { + Ok(>::token_children_ids(collection, token)) + } fn collection_properties( collection: CollectionId, keys: Option>> --- a/tests/src/interfaces/unique/definitions.ts +++ b/tests/src/interfaces/unique/definitions.ts @@ -50,6 +50,7 @@ allowance: fun('Get allowed amount', [collectionParam, crossAccountParam('sender'), crossAccountParam('spender'), tokenParam], 'u128'), tokenOwner: fun('Get token owner', [collectionParam, tokenParam], `Option<${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'), variableMetadata: fun('Get token variable metadata', [collectionParam, tokenParam], 'Vec'), collectionProperties: fun( --- 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'}}); --- 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 { + return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any; +} export async function isTokenExists( api: ApiPromise, collectionId: number,