diff --git a/clients/js/src/decorator.ts b/clients/js/src/decorator.ts index 1095edf..c15774f 100644 --- a/clients/js/src/decorator.ts +++ b/clients/js/src/decorator.ts @@ -12,22 +12,24 @@ import { GetAssetSignaturesRpcResponse, GetAssetProofsRpcResponse, GetAssetSignaturesRpcInput, + GetAssetsRpcInput, + GetAssetRpcInput, } from './types'; export interface DasApiInterface { /** * Return the metadata information of a compressed/standard asset. * - * @param assetId the id of the asset to fetch + * @param input the input parameters for the RPC call */ - getAsset(assetId: PublicKey): Promise; + getAsset(input: GetAssetRpcInput | PublicKey): Promise; /** * Return the metadata information of multiple compressed/standard assets. * - * @param assetIds Array of the ids of the assets to fetch + * @param input the input parameters for the RPC call */ - getAssets(assetIds: PublicKey[]): Promise; + getAssets(input: GetAssetsRpcInput | PublicKey[]): Promise; /** * Return the merkle tree proof information for a compressed asset. @@ -94,191 +96,214 @@ export interface DasApiInterface { export const createDasApiDecorator = ( rpc: RpcInterface -): RpcInterface & DasApiInterface => ({ - ...rpc, - getAsset: async (assetId: PublicKey) => { - const asset = await rpc.call('getAsset', [assetId]); - if (!asset) throw new DasApiError(`Asset not found: ${assetId}`); - return asset; - }, - getAssets: async (assetIds: PublicKey[]) => { - const assets = await rpc.call('getAssets', [ - assetIds, - ]); - if (!assets) throw new DasApiError(`No assets found: ${assetIds}`); - return assets; - }, - getAssetProof: async (assetId: PublicKey) => { - const proof = await rpc.call( - 'getAssetProof', - [assetId] - ); - if (!proof) throw new DasApiError(`No proof found for asset: ${assetId}`); - return proof; - }, - getAssetProofs: async (assetIds: PublicKey[]) => { - const proofs = await rpc.call( - 'getAssetProofs', - [assetIds] - ); - if (!proofs) - throw new DasApiError(`No proofs found for assets: ${assetIds}`); - return proofs; - }, - getAssetsByAuthority: async (input: GetAssetsByAuthorityRpcInput) => { - if (typeof input.page === 'number' && (input.before || input.after)) { +): RpcInterface & DasApiInterface => { + const validatePagination = ( + page: number | null | undefined, + before?: string | null, + after?: string | null + ) => { + if (typeof page === 'number' && (before || after)) { throw new DasApiError( 'Pagination Error. Please use either page or before/after, but not both.' ); } - const assetList = await rpc.call( - 'getAssetsByAuthority', - [ - input.authority, - input.sortBy ?? null, - input.limit ?? null, - input.page ?? 1, - input.before ?? null, - input.after ?? null, - ] - ); - if (!assetList) { - throw new DasApiError( - `No assets found for authority: ${input.authority}` + }; + + return { + ...rpc, + getAsset: async (input: GetAssetRpcInput | PublicKey) => { + const assetId = + typeof input === 'object' && 'assetId' in input ? input.assetId : input; + const displayOptions = + typeof input === 'object' && 'displayOptions' in input + ? input.displayOptions + : {}; + + const asset = await rpc.call('getAsset', [ + assetId, + displayOptions, + ]); + if (!asset) throw new DasApiError(`Asset not found: ${assetId}`); + return asset; + }, + getAssets: async (input: GetAssetsRpcInput | PublicKey[]) => { + const assetIds = Array.isArray(input) ? input : input.assetIds; + const displayOptions = Array.isArray(input) + ? {} + : input.displayOptions ?? {}; + + const assets = await rpc.call('getAssets', [ + assetIds, + displayOptions, + ]); + if (!assets) throw new DasApiError(`No assets found: ${assetIds}`); + return assets; + }, + getAssetProof: async (assetId: PublicKey) => { + const proof = await rpc.call( + 'getAssetProof', + [assetId] ); - } - return assetList; - }, - getAssetsByCreator: async (input: GetAssetsByCreatorRpcInput) => { - if (typeof input.page === 'number' && (input.before || input.after)) { - throw new DasApiError( - 'Pagination Error. Please use either page or before/after, but not both.' + if (!proof) throw new DasApiError(`No proof found for asset: ${assetId}`); + return proof; + }, + getAssetProofs: async (assetIds: PublicKey[]) => { + const proofs = await rpc.call( + 'getAssetProofs', + [assetIds] ); - } - const assetList = await rpc.call( - 'getAssetsByCreator', - [ - input.creator, - input.onlyVerified, - input.sortBy ?? null, - input.limit ?? null, - input.page ?? 1, - input.before ?? null, - input.after ?? null, - ] - ); - if (!assetList) { - throw new DasApiError(`No assets found for creator: ${input.creator}`); - } - return assetList; - }, - getAssetsByGroup: async (input: GetAssetsByGroupRpcInput) => { - if (typeof input.page === 'number' && (input.before || input.after)) { - throw new DasApiError( - 'Pagination Error. Please use either page or before/after, but not both.' + if (!proofs) + throw new DasApiError(`No proofs found for assets: ${assetIds}`); + return proofs; + }, + getAssetsByAuthority: async (input: GetAssetsByAuthorityRpcInput) => { + validatePagination(input.page, input.before, input.after); + const assetList = await rpc.call( + 'getAssetsByAuthority', + [ + input.authority, + input.sortBy ?? null, + input.limit ?? null, + input.page ?? 1, + input.before ?? null, + input.after ?? null, + input.displayOptions ?? {}, + input.cursor ?? null, + ] ); - } - const assetList = await rpc.call( - 'getAssetsByGroup', - [ - input.groupKey, - input.groupValue, - input.sortBy ?? null, - input.limit ?? null, - input.page ?? 1, - input.before ?? null, - input.after ?? null, - ] - ); - if (!assetList) { - throw new DasApiError( - `No assets found for group: ${input.groupKey} => ${input.groupValue}` + if (!assetList) { + throw new DasApiError( + `No assets found for authority: ${input.authority}` + ); + } + return assetList; + }, + getAssetsByCreator: async (input: GetAssetsByCreatorRpcInput) => { + validatePagination(input.page, input.before, input.after); + const assetList = await rpc.call( + 'getAssetsByCreator', + [ + input.creator, + input.onlyVerified, + input.sortBy ?? null, + input.limit ?? null, + input.page ?? 1, + input.before ?? null, + input.after ?? null, + input.displayOptions ?? {}, + input.cursor ?? null, + ] ); - } - return assetList; - }, - getAssetsByOwner: async (input: GetAssetsByOwnerRpcInput) => { - if (typeof input.page === 'number' && (input.before || input.after)) { - throw new DasApiError( - 'Pagination Error. Please use either page or before/after, but not both.' + if (!assetList) { + throw new DasApiError(`No assets found for creator: ${input.creator}`); + } + return assetList; + }, + getAssetsByGroup: async (input: GetAssetsByGroupRpcInput) => { + validatePagination(input.page, input.before, input.after); + const assetList = await rpc.call( + 'getAssetsByGroup', + [ + input.groupKey, + input.groupValue, + input.sortBy ?? null, + input.limit ?? null, + input.page ?? 1, + input.before ?? null, + input.after ?? null, + input.displayOptions ?? {}, + input.cursor ?? null, + ] ); - } - const assetList = await rpc.call( - 'getAssetsByOwner', - [ - input.owner, + if (!assetList) { + throw new DasApiError( + `No assets found for group: ${input.groupKey} => ${input.groupValue}` + ); + } + return assetList; + }, + getAssetsByOwner: async (input: GetAssetsByOwnerRpcInput) => { + validatePagination(input.page, input.before, input.after); + const assetList = await rpc.call( + 'getAssetsByOwner', + [ + input.owner, + input.sortBy ?? null, + input.limit ?? null, + input.page ?? 1, + input.before ?? null, + input.after ?? null, + input.displayOptions ?? {}, + input.cursor ?? null, + ] + ); + if (!assetList) { + throw new DasApiError(`No assets found for owner: ${input.owner}`); + } + return assetList; + }, + searchAssets: async (input: SearchAssetsRpcInput) => { + validatePagination(input.page, input.before, input.after); + const assetList = await rpc.call('searchAssets', [ + input.negate ?? null, + input.conditionType ?? null, + input.interface ?? null, + input.owner ?? null, + input.ownerType ?? null, + input.creator ?? null, + input.creatorVerified ?? null, + input.authority ?? null, + input.grouping ?? null, + input.delegate ?? null, + input.frozen ?? null, + input.supply ?? null, + input.supplyMint ?? null, + input.compressed ?? null, + input.compressible ?? null, + input.royaltyModel ?? null, + input.royaltyTarget ?? null, + input.royaltyAmount ?? null, + input.burnt ?? null, input.sortBy ?? null, input.limit ?? null, input.page ?? 1, input.before ?? null, input.after ?? null, - ] - ); - if (!assetList) { - throw new DasApiError(`No assets found for owner: ${input.owner}`); - } - return assetList; - }, - searchAssets: async (input: SearchAssetsRpcInput) => { - if (typeof input.page === 'number' && (input.before || input.after)) { - throw new DasApiError( - 'Pagination Error. Please use either page or before/after, but not both.' - ); - } - const assetList = await rpc.call('searchAssets', [ - input.negate ?? null, - input.conditionType ?? null, - input.interface ?? null, - input.owner ?? null, - input.ownerType ?? null, - input.creator ?? null, - input.creatorVerified ?? null, - input.authority ?? null, - input.grouping ?? null, - input.delegate ?? null, - input.frozen ?? null, - input.supply ?? null, - input.supplyMint ?? null, - input.compressed ?? null, - input.compressible ?? null, - input.royaltyModel ?? null, - input.royaltyTarget ?? null, - input.royaltyAmount ?? null, - input.burnt ?? null, - input.sortBy ?? null, - input.limit ?? null, - input.page ?? 1, - input.before ?? null, - input.after ?? null, - input.jsonUri ?? null, - ]); - if (!assetList) { - throw new DasApiError('No assets found for the given search criteria'); - } - return assetList; - }, - getAssetSignatures: async (input: GetAssetSignaturesRpcInput) => { - const signatures = await rpc.call( - 'getAssetSignaturesV2', - [ - 'assetId' in input ? input.assetId : null, - input.limit ?? null, - input.page ?? null, - input.before ?? null, - input.after ?? null, - 'tree' in input ? input.tree : null, - 'tree' in input ? input.leaf_index : null, + input.jsonUri ?? null, input.cursor ?? null, - input.sort_direction ?? null, - ] - ); - if (!signatures) { - const identifier = - 'assetId' in input - ? `asset: ${input.assetId}` - : `tree: ${input.tree}, leaf_index: ${input.leaf_index}`; - throw new DasApiError(`No signatures found for ${identifier}`); - } - return signatures; - }, -}); + input.name ?? null, + input.displayOptions ?? {}, + input.tokenType ?? null, + ]); + if (!assetList) { + throw new DasApiError('No assets found for the given search criteria'); + } + return assetList; + }, + getAssetSignatures: async (input: GetAssetSignaturesRpcInput) => { + validatePagination(input.page, input.before, input.after); + const signatures = await rpc.call( + 'getAssetSignaturesV2', + [ + 'assetId' in input ? input.assetId : null, + input.limit ?? null, + input.page ?? null, + input.before ?? null, + input.after ?? null, + 'tree' in input ? input.tree : null, + 'tree' in input ? input.leaf_index : null, + input.cursor ?? null, + input.sort_direction ?? null, + ] + ); + if (!signatures) { + const identifier = + 'assetId' in input + ? `asset: ${input.assetId}` + : `tree: ${input.tree}, leaf_index: ${input.leaf_index}`; + throw new DasApiError(`No signatures found for ${identifier}`); + } + return signatures; + }, + }; +}; diff --git a/clients/js/src/types.ts b/clients/js/src/types.ts index b23e41a..dc59347 100644 --- a/clients/js/src/types.ts +++ b/clients/js/src/types.ts @@ -4,11 +4,38 @@ import { Nullable, PublicKey } from '@metaplex-foundation/umi'; // RPC input. // // ---------------------------------------- // +/** + * Display options for asset queries + */ +export type DisplayOptions = { + /** + * Whether to show unverified collections + */ + showUnverifiedCollections?: boolean; + /** + * Whether to show collection metadata + */ + showCollectionMetadata?: boolean; + /** + * Whether to show fungible assets + */ + showFungible?: boolean; + /** + * Whether to show inscription data + */ + showInscription?: boolean; +}; + export type GetAssetsByAuthorityRpcInput = { /** * The address of the authority of the assets. */ authority: PublicKey; + + /** + * Display options for the query + */ + displayOptions?: DisplayOptions; } & Pagination; export type GetAssetsByCreatorRpcInput = { @@ -21,6 +48,11 @@ export type GetAssetsByCreatorRpcInput = { * Indicates whether to retrieve only verified assets or not. */ onlyVerified: boolean; + + /** + * Display options for the query + */ + displayOptions?: DisplayOptions; } & Pagination; export type GetAssetsByGroupRpcInput = { @@ -33,6 +65,11 @@ export type GetAssetsByGroupRpcInput = { * The value of the group */ groupValue: string; + + /** + * Display options for the query + */ + displayOptions?: DisplayOptions; } & Pagination; export type GetAssetsByOwnerRpcInput = { @@ -40,58 +77,63 @@ export type GetAssetsByOwnerRpcInput = { * The address of the owner of the assets. */ owner: PublicKey; + + /** + * Display options for the query + */ + displayOptions?: DisplayOptions; } & Pagination; export type SearchAssetsRpcInput = { /** - * Indicates whether the search criteria should be inverted or not. + * The address of the authority. */ - negate?: Nullable; + authority?: Nullable; /** - * Indicates whether to retrieve all or any asset that matches the search criteria. + * The address of the creator. */ - conditionType?: Nullable<'all' | 'any'>; + creator?: Nullable; /** - * The interface value of the asset. + * Indicates whether the creator must be verified or not. */ - interface?: Nullable; + creatorVerified?: Nullable; /** - * The value for the JSON URI. + * The grouping (`key`, `value`) pair. */ - jsonUri?: Nullable; + grouping?: Nullable<[string, string]>; /** - * The address of the owner. + * The interface value of the asset. */ - owner?: Nullable; + interface?: Nullable; /** - * Type of ownership. + * Indicates whether the search criteria should be inverted or not. */ - ownerType?: Nullable<'single' | 'token'>; + negate?: Nullable; /** - * The address of the creator. + * The name of the asset. */ - creator?: Nullable; + name?: Nullable; /** - * Indicates whether the creator must be verified or not. + * Indicates whether to retrieve all or any asset that matches the search criteria. */ - creatorVerified?: Nullable; + conditionType?: Nullable<'all' | 'any'>; /** - * The address of the authority. + * The address of the owner. */ - authority?: Nullable; + owner?: Nullable; /** - * The grouping (`key`, `value`) pair. + * Type of ownership. */ - grouping?: Nullable<[string, string]>; + ownerType?: Nullable<'single' | 'token'>; /** * The address of the delegate. @@ -113,6 +155,11 @@ export type SearchAssetsRpcInput = { */ supplyMint?: Nullable; + /** + * The type of token to search for. + */ + tokenType?: Nullable; + /** * Indicates whether the asset is compressed or not. */ @@ -142,8 +189,48 @@ export type SearchAssetsRpcInput = { * Indicates whether the asset is burnt or not. */ burnt?: Nullable; + + /** + * The value for the JSON URI. + */ + jsonUri?: Nullable; + + /** + * Display options for the query + */ + displayOptions?: DisplayOptions; } & Pagination; +/** + * Input parameters for getAsset RPC call + */ +export type GetAssetRpcInput = { + /** + * The asset ID to fetch + */ + assetId: PublicKey; + + /** + * Display options for the query + */ + displayOptions?: DisplayOptions; +}; + +/** + * Input parameters for getAssets RPC call + */ +export type GetAssetsRpcInput = { + /** + * Array of asset IDs to fetch + */ + assetIds: PublicKey[]; + + /** + * Display options for the query + */ + displayOptions?: DisplayOptions; +}; + // ---------------------------------------- // // Result types. // // ---------------------------------------- // @@ -321,6 +408,11 @@ type Pagination = { * Retrieve assets after the specified `ID` value. */ after?: Nullable; + + /** + * + */ + cursor?: Nullable; }; /** @@ -337,6 +429,7 @@ export type DasApiAssetInterface = | 'LEGACY_NFT' | 'V2_NFT' | 'FungibleAsset' + | 'FungibleToken' | 'Custom' | 'Identity' | 'Executable' @@ -407,6 +500,13 @@ export type DasApiPropGroupKey = 'collection'; export type DasApiAssetGrouping = { group_key: DasApiPropGroupKey; group_value: string; + verified?: boolean; + collection_metadata?: { + name: string; + symbol: string; + description: string; + image: string; + }; }; export type DasApiAuthorityScope = @@ -531,3 +631,10 @@ export type GetAssetSignaturesRpcResponse = { */ items: DasApiTransactionSignature[]; }; + +export type TokenType = + | 'Fungible' + | 'NonFungible' + | 'regularNFT' + | 'compressedNFT' + | 'All'; diff --git a/clients/js/test/_setup.ts b/clients/js/test/_setup.ts index a9a410c..c3d448a 100644 --- a/clients/js/test/_setup.ts +++ b/clients/js/test/_setup.ts @@ -7,8 +7,10 @@ export const DAS_API_ENDPOINTS: { name: string; url: string }[] = []; Object.keys(process.env).forEach(function (key) { if (key.startsWith('DAS_API_')) { const name = key.substring('DAS_API_'.length); - const url = process.env[key]!; - DAS_API_ENDPOINTS.push({ name, url }); + const url = process.env[key]; + if (url) { + DAS_API_ENDPOINTS.push({ name, url }); + } } }); diff --git a/clients/js/test/getAsset.test.ts b/clients/js/test/getAsset.test.ts index b931c34..fac1969 100644 --- a/clients/js/test/getAsset.test.ts +++ b/clients/js/test/getAsset.test.ts @@ -48,14 +48,14 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { test(`it can fetch a regular asset by ID (${endpoint.name})`, async (t) => { // Given a minted NFT. const umi = createUmi(endpoint.url); - const assetId = publicKey('8bFQbnBrzeiYQabEJ1ghy5T7uFpqFzPjUGsVi3SzSMHB'); + const assetId = publicKey('Hu9vvgNjVDxRo6F8iTEo6sRJikhqoM2zVswR86WAf4C'); // When we fetch the asset using its ID. const asset = await umi.rpc.getAsset(assetId); // Then we expect the following data. t.like(asset, { - interface: 'ProgrammableNFT', + interface: 'V1_NFT', id: assetId, content: { metadata: { @@ -66,12 +66,54 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { t.like(asset.compression, { compressed: false, }); - t.deepEqual(asset.grouping.length, 1); + t.deepEqual(asset.mutable, true); + t.deepEqual(asset.burnt, false); + }); + + test(`it can fetch a regular asset by ID not showing unverified collection data using showUnverifiedCollections false (${endpoint.name})`, async (t) => { + // Given a minted NFT. + const umi = createUmi(endpoint.url); + const assetId = publicKey('5smGnzgaMsQ3JV7jWCvSxnRkHjP2dJoi1uczHTx87tji'); + + // When we fetch the asset using its ID with display options. + await t.throwsAsync( + async () => { + await umi.rpc.getAsset({ + assetId, + displayOptions: { showUnverifiedCollections: false }, + }); + }, + { + message: /Asset not found/, + } + ); + }); + + test(`it can fetch a regular asset by ID with unverified collection data using showUnverifiedCollections true (${endpoint.name})`, async (t) => { + // Given a minted NFT. + const umi = createUmi(endpoint.url); + const assetId = publicKey('5smGnzgaMsQ3JV7jWCvSxnRkHjP2dJoi1uczHTx87tji'); + + // When we fetch the asset using its ID. + const asset = await umi.rpc.getAsset({ + assetId, + displayOptions: { showUnverifiedCollections: true }, + }); + + t.like(asset, { + interface: 'V1_NFT', + id: assetId, + content: { + metadata: { + name: 'unverified Azure 55', + }, + }, + }); + t.is(asset.grouping.length, 1); t.like(asset.grouping[0], { group_key: 'collection', - group_value: '5RT4e9uHUgG9h13cSc3L4YvkDc9qXSznoLaX4Tx8cpWS', + group_value: '5g2h8NuNNdb2riSuAKC3JJrrJKGJUH9dxM23fqdYgGt2', + verified: false, }); - t.deepEqual(asset.mutable, true); - t.deepEqual(asset.burnt, false); }); }); diff --git a/clients/js/test/getAssets.test.ts b/clients/js/test/getAssets.test.ts index 7a5c07e..22f88a9 100644 --- a/clients/js/test/getAssets.test.ts +++ b/clients/js/test/getAssets.test.ts @@ -11,23 +11,32 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { 'GGRbPQhwmo3dXBkJSAjMFc1QYTKGBt8qc11tTp3LkEKA' ); const regularAssetId = publicKey( - '8bFQbnBrzeiYQabEJ1ghy5T7uFpqFzPjUGsVi3SzSMHB' + 'Hu9vvgNjVDxRo6F8iTEo6sRJikhqoM2zVswR86WAf4C' ); // When we fetch the asset using its ID. const assets = await umi.rpc.getAssets([compressedAssetId, regularAssetId]); // Then we expect the following data. - t.deepEqual(assets[0].interface, 'V1_NFT'); - t.deepEqual(assets[0].id, compressedAssetId); - t.like(assets[0].content, { + const compressedAsset = assets.find( + (asset) => asset.id === compressedAssetId + ); + const regularAsset = assets.find((asset) => asset.id === regularAssetId); + + t.truthy(compressedAsset, 'Expected to find compressed asset'); + t.truthy(regularAsset, 'Expected to find regular asset'); + + // Compressed asset assertions + t.deepEqual(compressedAsset!.interface, 'V1_NFT'); + t.deepEqual(compressedAsset!.id, compressedAssetId); + t.like(compressedAsset!.content, { json_uri: 'https://arweave.net/c9aGs5fOk7gD4wWnSvmzeqgtfxAGRgtI1jYzvl8-IVs/chiaki-violet-azure-common.json', metadata: { name: 'Chiaki Azure 55', }, }); - t.deepEqual(assets[0].compression, { + t.deepEqual(compressedAsset!.compression, { eligible: false, compressed: true, data_hash: '29BdgNWxNB1sinkfmWKFQi3zWXRpsotp2FKoZhoqVa9F', @@ -37,30 +46,130 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { seq: 1, leaf_id: 0, }); - t.deepEqual(assets[0].grouping.length, 1); - t.like(assets[0].grouping[0], { + t.deepEqual(compressedAsset!.grouping.length, 1); + t.like(compressedAsset!.grouping[0], { group_key: 'collection', group_value: 'Dm1TRVw82roqpfqpzsFxSsWg6a4z3dku6ebVHSHuVo1c', }); - t.deepEqual(assets[0].mutable, true); - t.deepEqual(assets[0].burnt, false); + t.deepEqual(compressedAsset!.mutable, true); + t.deepEqual(compressedAsset!.burnt, false); - t.deepEqual(assets[1].interface, 'ProgrammableNFT'); - t.deepEqual(assets[1].id, regularAssetId); - t.like(assets[1].content, { + // Regular asset assertions + t.deepEqual(regularAsset!.interface, 'V1_NFT'); + t.deepEqual(regularAsset!.id, regularAssetId); + t.like(regularAsset!.content, { metadata: { name: 'Chiaki Azure 55', }, }); - t.like(assets[1].compression, { + t.like(regularAsset!.compression, { compressed: false, }); + t.deepEqual(regularAsset!.grouping.length, 1); + t.like(regularAsset!.grouping[0], { + group_key: 'collection', + group_value: '5g2h8NuNNdb2riSuAKC3JJrrJKGJUH9dxM23fqdYgGt2', + }); + t.deepEqual(regularAsset!.mutable, true); + t.deepEqual(regularAsset!.burnt, false); + }); + + test(`it can fetch multiple assets by ID with showUnverifiedCollections true (${endpoint.name})`, async (t) => { + // Given a minted NFT. + const umi = createUmi(endpoint.url); + const compressedAssetId = publicKey( + 'GGRbPQhwmo3dXBkJSAjMFc1QYTKGBt8qc11tTp3LkEKA' + ); + const regularAssetId = publicKey( + '8bFQbnBrzeiYQabEJ1ghy5T7uFpqFzPjUGsVi3SzSMHB' + ); + + // When we fetch the assets using their IDs with display options. + const assets = await umi.rpc.getAssets({ + assetIds: [compressedAssetId, regularAssetId], + displayOptions: { + showUnverifiedCollections: true, + }, + }); + + // Then we expect to get the assets back. + t.is(assets.length, 2); + t.deepEqual(assets[0].id, compressedAssetId); + t.deepEqual(assets[1].id, regularAssetId); + + //And asset1 should have grouping data t.deepEqual(assets[1].grouping.length, 1); - t.like(assets[1].grouping[0], { + }); + + test(`it can fetch multiple assets by ID with showCollectionMetadata true (${endpoint.name})`, async (t) => { + // Given a minted NFT. + const umi = createUmi(endpoint.url); + const compressedAssetId = publicKey( + 'GGRbPQhwmo3dXBkJSAjMFc1QYTKGBt8qc11tTp3LkEKA' + ); + const regularAssetId = publicKey( + 'Hu9vvgNjVDxRo6F8iTEo6sRJikhqoM2zVswR86WAf4C' + ); + + // When we fetch the assets using their IDs with display options. + const assets = await umi.rpc.getAssets({ + assetIds: [compressedAssetId, regularAssetId], + displayOptions: { + showCollectionMetadata: true, + }, + }); + + // Then we expect to get the assets back with collection metadata. + t.is(assets.length, 2); + t.deepEqual(assets[1].id, regularAssetId); + + // Verify collection metadata is present in the grouping + const assetWithCollectionMetadata = assets.find( + (asset) => asset.grouping[0]?.collection_metadata + ); + t.truthy( + assetWithCollectionMetadata, + 'Expected to find an asset with collection metadata' + ); + if (!assetWithCollectionMetadata) return; + t.like(assetWithCollectionMetadata.grouping[0], { group_key: 'collection', - group_value: '5RT4e9uHUgG9h13cSc3L4YvkDc9qXSznoLaX4Tx8cpWS', + group_value: 'Dm1TRVw82roqpfqpzsFxSsWg6a4z3dku6ebVHSHuVo1c', + verified: true, + collection_metadata: { + name: 'My cNFT Collection', + symbol: '', + image: + 'https://gateway.irys.xyz/8da3Er9Q39QRkdNhBNP7w5hDo5ZnydLNxLqe9i6s1Nak', + description: '', + }, }); - t.deepEqual(assets[1].mutable, true); - t.deepEqual(assets[1].burnt, false); + }); + + test(`it can fetch multiple assets by ID with showFungible true (${endpoint.name})`, async (t) => { + // Given a minted NFT and a fungible token. + const umi = createUmi(endpoint.url); + const nftAssetId = publicKey( + 'GGRbPQhwmo3dXBkJSAjMFc1QYTKGBt8qc11tTp3LkEKA' + ); + const fungibleAssetId = publicKey( + '4oZjhZTiKAbuLtfCPukCgTDAcUngDUyccctpLVT9zJuP' + ); + + // When we fetch the assets using their IDs with showFungible true. + const assets = await umi.rpc.getAssets({ + assetIds: [nftAssetId, fungibleAssetId], + displayOptions: { + showFungible: true, + }, + }); + + // Then we expect to get both assets back. + t.is(assets.length, 2); + const fungibleAsset = assets.find( + (asset) => asset.interface === 'FungibleToken' + ); + t.truthy(fungibleAsset, 'Expected to find a fungible token'); + t.deepEqual(fungibleAsset?.id, fungibleAssetId); }); }); diff --git a/clients/js/test/getAssetsByAuthority.test.ts b/clients/js/test/getAssetsByAuthority.test.ts index 3b8863b..b54dcb0 100644 --- a/clients/js/test/getAssetsByAuthority.test.ts +++ b/clients/js/test/getAssetsByAuthority.test.ts @@ -9,7 +9,96 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { const authority = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); // When we fetch the asset using the authority. - const assets = await umi.rpc.getAssetsByAuthority({ authority }); + const assets = await umi.rpc.getAssetsByAuthority({ + authority, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + // And the authority should be present. + assets.items.forEach((asset) => { + t.true(asset.authorities.some((other) => other.address === authority)); + }); + }); + + test(`it can fetch assets by authority with showUnverifiedCollections true (${endpoint.name})`, async (t) => { + // Given an authority address. + const umi = createUmi(endpoint.url); + const authority = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the authority with display options. + const assets = await umi.rpc.getAssetsByAuthority({ + authority, + displayOptions: { + showUnverifiedCollections: true, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // And that the authority is present + assets.items.forEach((asset) => { + t.true(asset.authorities.some((other) => other.address === authority)); + }); + + // and that at least one asset with unverified collection exists + const assetWithUnverifiedCollection = assets.items.find((asset) => + asset.grouping?.some( + (group) => group.group_key === 'collection' && group.verified === false + ) + ); + t.truthy( + assetWithUnverifiedCollection, + 'Expected to find at least one asset with an unverified collection' + ); + }); + + test(`it can fetch assets by authority with showUnverifiedCollections false (${endpoint.name})`, async (t) => { + // Given an authority address. + const umi = createUmi(endpoint.url); + const authority = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the authority with display options. + const assets = await umi.rpc.getAssetsByAuthority({ + authority, + displayOptions: { + showUnverifiedCollections: false, + }, + }); + + // Then we expect to find assets + t.true(assets.items.length > 0); + + // And the authority should be present. + assets.items.forEach((asset) => { + t.true(asset.authorities.some((other) => other.address === authority)); + }); + // And any collection groupings present are verified + assets.items.forEach((asset) => { + asset.grouping.forEach((group) => { + if (group.group_key === 'collection') { + t.true( + group.verified, + 'All present collection groupings should be verified' + ); + } + }); + }); + }); + + test(`it can fetch assets by authority with showCollectionMetadata true (${endpoint.name})`, async (t) => { + // Given an authority address. + const umi = createUmi(endpoint.url); + const authority = publicKey('DAS7Wnf86QNmwKWacTe8KShU7V6iw7wwcPjG9qXLPkEU'); + + // When we fetch the asset using the authority with display options. + const assets = await umi.rpc.getAssetsByAuthority({ + authority, + displayOptions: { + showCollectionMetadata: true, + }, + }); // Then we expect to find assets. t.true(assets.items.length > 0); @@ -18,5 +107,32 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { assets.items.forEach((asset) => { t.true(asset.authorities.some((other) => other.address === authority)); }); + + // And collection metadata should be present in the grouping + const assetWithCollectionMetadata = assets.items.find( + (asset) => asset.grouping.length > 0 + ); + t.truthy( + assetWithCollectionMetadata, + 'Expected to find an asset with grouping' + ); + if (!assetWithCollectionMetadata) return; + + const collectionGroup = assetWithCollectionMetadata.grouping.find( + (group) => group.group_key === 'collection' + ); + t.truthy(collectionGroup, 'Expected to find a collection group'); + t.like(collectionGroup, { + group_key: 'collection', + group_value: 'Ce9hnNkbwNP7URw6TkhpopcKeNm8s4SchbBJS3m8tTu2', + collection_metadata: { + name: 'Chiaki Azure 55 Collection', + symbol: '', + image: + 'https://arweave.net/fFcYDkRHF-936IbAZ3pLTmFAmxF1WlW3KwWndYPgI8Q/chiaki-violet-azure-common.png', + description: + 'MONMONMON is a collection from the creativity of Peelander Yellow. Each MONMONMON has unique and kind abilities that can be used to help others and play with your friends. There are secrets in each MONMONMON. We love you.', + }, + }); }); }); diff --git a/clients/js/test/getAssetsByCreator.test.ts b/clients/js/test/getAssetsByCreator.test.ts index bd1060d..f93e12f 100644 --- a/clients/js/test/getAssetsByCreator.test.ts +++ b/clients/js/test/getAssetsByCreator.test.ts @@ -23,4 +23,163 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { t.true(asset.creators.some((other) => other.address === creator)); }); }); + + test(`it can fetch assets by creator not limiting to verified creators (${endpoint.name})`, async (t) => { + // Given an creator address. + const umi = createUmi(endpoint.url); + const creator = publicKey('Ex2A8tN3DbdA8N2F1PC6jLZmpfNBKAVRMEivBAwxcatC'); + + // When we fetch the asset using the creator. + const assets = await umi.rpc.getAssetsByCreator({ + creator, + onlyVerified: false, + limit: 10, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // And the creator should be present. + assets.items.forEach((asset) => { + const creatorAccount = asset.creators.find( + (other) => other.address === creator + ); + t.true(creatorAccount !== undefined); + t.false(creatorAccount?.verified); + }); + }); + + test(`it can fetch assets by creator limiting to verified creators (${endpoint.name})`, async (t) => { + // Given an creator address. + const umi = createUmi(endpoint.url); + const creator = publicKey('Ex2A8tN3DbdA8N2F1PC6jLZmpfNBKAVRMEivBAwxcatC'); + + // When we fetch the asset using the creator. + const assets = await umi.rpc.getAssetsByCreator({ + creator, + onlyVerified: true, + limit: 10, + }); + + // Then we don't expect to find assets. + t.true(assets.items.length === 0); + }); + + test(`it can fetch assets by creator with showUnverifiedCollections true (${endpoint.name})`, async (t) => { + // Given an creator address. + const umi = createUmi(endpoint.url); + const creator = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the creator with display options. + const assets = await umi.rpc.getAssetsByCreator({ + creator, + onlyVerified: true, + limit: 10, + displayOptions: { + showUnverifiedCollections: true, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 1); + + // And the creator should be present. + assets.items.forEach((asset) => { + t.true(asset.creators.some((other) => other.address === creator)); + }); + + // and that at least one asset with unverified collection exists + const assetWithUnverifiedCollection = assets.items.find((asset) => + asset.grouping?.some( + (group) => group.group_key === 'collection' && group.verified === false + ) + ); + t.truthy( + assetWithUnverifiedCollection, + 'Expected to find at least one asset with an unverified collection' + ); + }); + + test(`it can fetch assets by creator with showUnverifiedCollections false (${endpoint.name})`, async (t) => { + // Given an creator address. + const umi = createUmi(endpoint.url); + const creator = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the creator with display options. + const assets = await umi.rpc.getAssetsByCreator({ + creator, + onlyVerified: true, + limit: 10, + displayOptions: { + showUnverifiedCollections: false, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 1); + + // And the creator should be present. + assets.items.forEach((asset) => { + t.true(asset.creators.some((other) => other.address === creator)); + + // And all collections should be verified + const collectionGroups = + asset.grouping?.filter((group) => group.group_key === 'collection') ?? + []; + collectionGroups.forEach((group) => { + t.true(group.verified, 'Expected all collection groups to be verified'); + }); + }); + }); + + test(`it can fetch assets by creator with showCollectionMetadata true (${endpoint.name})`, async (t) => { + // Given an creator address. + const umi = createUmi(endpoint.url); + const creator = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the creator with display options. + const assets = await umi.rpc.getAssetsByCreator({ + creator, + onlyVerified: true, + limit: 10, + displayOptions: { + showCollectionMetadata: true, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // And the creator should be present. + assets.items.forEach((asset) => { + t.true(asset.creators.some((other) => other.address === creator)); + }); + + // And collection metadata should be present in the grouping for assets that have collections + const assetWithCollection = assets.items.find((asset) => + asset.grouping?.some((group) => group.group_key === 'collection') + ); + t.truthy( + assetWithCollection, + 'Expected to find at least one asset with a collection' + ); + + // We've verified assetWithCollection exists with t.truthy above + const collectionGroup = assetWithCollection!.grouping.find( + (group) => group.group_key === 'collection' + ); + t.truthy(collectionGroup, 'Expected to find a collection group'); + t.like(collectionGroup, { + group_key: 'collection', + group_value: 'Dm1TRVw82roqpfqpzsFxSsWg6a4z3dku6ebVHSHuVo1c', + collection_metadata: { + name: 'My cNFT Collection', + symbol: '', + image: + 'https://gateway.irys.xyz/8da3Er9Q39QRkdNhBNP7w5hDo5ZnydLNxLqe9i6s1Nak', + description: '', + external_url: '', + }, + }); + }); }); diff --git a/clients/js/test/getAssetsByGroup.test.ts b/clients/js/test/getAssetsByGroup.test.ts index 2e40e3c..d8b73df 100644 --- a/clients/js/test/getAssetsByGroup.test.ts +++ b/clients/js/test/getAssetsByGroup.test.ts @@ -30,4 +30,127 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { }); }); }); + + test(`it can fetch assets by group hiding unverified collections with showUnverifiedCollections false (${endpoint.name})`, async (t) => { + // Given a group (key, value) pair. + const umi = createUmi(endpoint.url); + const groupKey = 'collection'; + const groupValue = publicKey( + '5g2h8NuNNdb2riSuAKC3JJrrJKGJUH9dxM23fqdYgGt2' + ); + + // When we fetch the asset using the group information with display options. + const assets = await umi.rpc.getAssetsByGroup({ + groupKey, + groupValue, + displayOptions: { + showUnverifiedCollections: false, + }, + }); + + // Then we expect to find exactly one asset. + t.true(assets.items.length === 1); + + // And the collection should match and be verified + assets.items.forEach((asset) => { + t.like(assets.items[0], { + grouping: [ + { + group_key: groupKey, + group_value: groupValue.toString(), + }, + ], + }); + }); + }); + + test(`it can fetch assets by group showing unverified collections with showUnverifiedCollections true (${endpoint.name})`, async (t) => { + // Given a group (key, value) pair. + const umi = createUmi(endpoint.url); + const groupKey = 'collection'; + const groupValue = publicKey( + '5g2h8NuNNdb2riSuAKC3JJrrJKGJUH9dxM23fqdYgGt2' + ); + + // When we fetch the asset using the group information with display options. + const assets = await umi.rpc.getAssetsByGroup({ + groupKey, + groupValue, + displayOptions: { + showUnverifiedCollections: true, + }, + }); + + // Then we expect to find exactly two assets. + t.true(assets.items.length === 2); + + // And at least one asset should have an unverified collection + const assetWithUnverifiedCollection = assets.items.find((asset) => + asset.grouping?.some( + (group) => + group.group_key === groupKey && + group.group_value === groupValue.toString() && + group.verified === false + ) + ); + t.truthy( + assetWithUnverifiedCollection, + 'Expected to find at least one asset with an unverified collection' + ); + }); + + test(`it can fetch assets by group with collection metadata when showCollectionMetadata is true (${endpoint.name})`, async (t) => { + // Given a group (key, value) pair. + const umi = createUmi(endpoint.url); + const groupKey = 'collection'; + const groupValue = publicKey( + '5g2h8NuNNdb2riSuAKC3JJrrJKGJUH9dxM23fqdYgGt2' + ); + + // When we fetch the asset using the group information with display options. + const assets = await umi.rpc.getAssetsByGroup({ + groupKey, + groupValue, + displayOptions: { + showCollectionMetadata: true, + }, + }); + + // Then we expect to find assets + t.true(assets.items.length > 0); + + // And the collection grouping should include collection metadata + assets.items.forEach((asset) => { + const collectionGroup = asset.grouping?.find( + (group) => + group.group_key === groupKey && + group.group_value === groupValue.toString() + ); + t.truthy(collectionGroup, 'Expected to find collection group'); + if (!collectionGroup) return; + + t.truthy( + collectionGroup.collection_metadata, + 'Expected collection group to have collection_metadata' + ); + if (!collectionGroup.collection_metadata) return; + + t.true( + typeof collectionGroup.collection_metadata.name === 'string', + 'Expected collection_metadata to have a name' + ); + t.true( + typeof collectionGroup.collection_metadata.description === 'string', + 'Expected collection_metadata to have a description' + ); + t.true( + typeof collectionGroup.collection_metadata.image === 'string', + 'Expected collection_metadata to have an image' + ); + t.true( + typeof collectionGroup.collection_metadata.symbol === 'string', + 'Expected collection_metadata to have a symbol' + ); + }); + }); }); diff --git a/clients/js/test/getAssetsByOwner.test.ts b/clients/js/test/getAssetsByOwner.test.ts index 9de7451..b6b2c82 100644 --- a/clients/js/test/getAssetsByOwner.test.ts +++ b/clients/js/test/getAssetsByOwner.test.ts @@ -24,4 +24,157 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { }); }); }); + + test(`it can fetch compressed assets by owner with showUnverifiedCollections true (${endpoint.name})`, async (t) => { + // Given an owner address. + const umi = createUmi(endpoint.url); + const owner = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the owner with display options. + const assets = await umi.rpc.getAssetsByOwner({ + owner, + displayOptions: { + showUnverifiedCollections: true, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // And the owner should match. + assets.items.forEach((asset) => { + t.like(assets.items[0], { + ownership: { + owner, + }, + }); + }); + // And at least one asset should have an unverified collection + const assetWithUnverifiedCollection = assets.items.find((asset) => + asset.grouping?.some((group) => group.verified === false) + ); + t.truthy( + assetWithUnverifiedCollection, + 'Expected to find at least one asset with an unverified collection' + ); + }); + + test(`it can fetch assets by owner with showCollectionMetadata true (${endpoint.name})`, async (t) => { + // Given an owner address. + const umi = createUmi(endpoint.url); + const owner = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the owner with display options. + const assets = await umi.rpc.getAssetsByOwner({ + owner, + displayOptions: { + showCollectionMetadata: true, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // And the owner should match. + assets.items.forEach((asset) => { + t.like(assets.items[0], { + ownership: { + owner, + }, + }); + }); + + // And collection metadata should be present in the grouping for assets that have collections + const assetWithCollection = assets.items.find((asset) => + asset.grouping?.some((group) => group.group_key === 'collection') + ); + t.truthy( + assetWithCollection, + 'Expected to find at least one asset with a collection' + ); + + const collectionGroup = assetWithCollection!.grouping.find( + (group) => group.group_key === 'collection' + ); + t.truthy(collectionGroup, 'Expected to find a collection group'); + t.like(collectionGroup, { + group_key: 'collection', + group_value: 'Dm1TRVw82roqpfqpzsFxSsWg6a4z3dku6ebVHSHuVo1c', + collection_metadata: { + name: 'My cNFT Collection', + symbol: '', + image: + 'https://gateway.irys.xyz/8da3Er9Q39QRkdNhBNP7w5hDo5ZnydLNxLqe9i6s1Nak', + description: '', + external_url: '', + }, + }); + }); + + test(`it can fetch assets by owner with showFungible true (${endpoint.name})`, async (t) => { + // Given an owner address. + const umi = createUmi(endpoint.url); + const owner = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the owner with display options. + const assets = await umi.rpc.getAssetsByOwner({ + owner, + displayOptions: { + showFungible: true, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // And the owner should match. + assets.items.forEach((asset) => { + t.like(assets.items[0], { + ownership: { + owner, + }, + }); + }); + + // And at least one asset should be a fungible token + const fungibleAsset = assets.items.find( + (asset) => asset.interface === 'FungibleToken' + ); + t.truthy( + fungibleAsset, + 'Expected to find at least one fungible token asset' + ); + }); + + test(`it can fetch assets by owner with showFungible false (${endpoint.name})`, async (t) => { + // Given an owner address. + const umi = createUmi(endpoint.url); + const owner = publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'); + + // When we fetch the asset using the owner with display options. + const assets = await umi.rpc.getAssetsByOwner({ + owner, + displayOptions: { + showFungible: false, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // And the owner should match. + assets.items.forEach((asset) => { + t.like(assets.items[0], { + ownership: { + owner, + }, + }); + }); + + // And no asset should be a fungible token + const fungibleAsset = assets.items.find( + (asset) => asset.interface === 'FungibleToken' + ); + t.falsy(fungibleAsset, 'Expected not to find any fungible token assets'); + }); }); diff --git a/clients/js/test/searchAssets.test.ts b/clients/js/test/searchAssets.test.ts index 2f294da..d650673 100644 --- a/clients/js/test/searchAssets.test.ts +++ b/clients/js/test/searchAssets.test.ts @@ -64,14 +64,12 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { }, }); }); -}); -DAS_API_ENDPOINTS.forEach((endpoint) => { test(`it can search a compressed asset by collection (${endpoint.name})`, async (t) => { // Given a DAS API endpoint. const umi = createUmi(endpoint.url); - // When we search for assetw given their collection. + // When we search for assets given their collection. const assets = await umi.rpc.searchAssets({ grouping: ['collection', 'Dm1TRVw82roqpfqpzsFxSsWg6a4z3dku6ebVHSHuVo1c'], }); @@ -91,4 +89,61 @@ DAS_API_ENDPOINTS.forEach((endpoint) => { }); }); }); + + test(`it can search assets with showUnverifiedCollections true (${endpoint.name})`, async (t) => { + // Given a DAS API endpoint. + const umi = createUmi(endpoint.url); + + // When we search for assets with display options. + const assets = await umi.rpc.searchAssets({ + owner: publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'), + displayOptions: { + showUnverifiedCollections: true, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // Find the specific asset + const specificAsset = assets.items.find( + (asset) => asset.id === '8bFQbnBrzeiYQabEJ1ghy5T7uFpqFzPjUGsVi3SzSMHB' + ); + + // Assert the asset exists and has group_definition + t.truthy(specificAsset, 'Expected to find the specific asset'); + if (specificAsset) { + t.is( + (specificAsset as any).group_definition, + undefined, + 'Expected group_definition to be undefined when showUnverifiedCollections is true' + ); + } + }); + + test(`it can search assets with showUnverifiedCollections false (${endpoint.name})`, async (t) => { + // Given a DAS API endpoint. + const umi = createUmi(endpoint.url); + + // When we search for assets with display options. + const assets = await umi.rpc.searchAssets({ + owner: publicKey('DASPQfEAVcHp55eFmfstRduMT3dSfoTirFFsMHwUaWaz'), + displayOptions: { + showUnverifiedCollections: false, + }, + }); + + // Then we expect to find assets. + t.true(assets.items.length > 0); + + // But not the specific asset which is not a verified part of a collection + const specificAsset = assets.items.find( + (asset) => asset.id === '8bFQbnBrzeiYQabEJ1ghy5T7uFpqFzPjUGsVi3SzSMHB' + ); + + t.assert( + specificAsset === undefined, + 'Expected to not find the specific asset' + ); + }); });