From 2ae497e1ca73fabe6c2db66d638c0685a1358aca Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio <1120791+LautaroPetaccio@users.noreply.github.com> Date: Thu, 2 May 2024 09:25:50 -0300 Subject: [PATCH] feat: Fetch and add the utility property (#375) * feat: Fetch and add the utility property * fix: Remove double quotes --- .env.defaults | 1 + .env.spec | 1 + package-lock.json | 14 +-- package.json | 2 +- src/index.ts | 12 ++- src/ports/builder/component.ts | 42 +++++++++ src/ports/builder/index.ts | 2 + src/ports/builder/types.ts | 6 ++ src/ports/items/component.ts | 18 +++- src/ports/items/index.ts | 2 + src/ports/nfts/component.ts | 22 +++-- src/tests/components.ts | 27 ++++-- src/tests/ports/builder.spec.ts | 125 +++++++++++++++++++++++++ src/tests/ports/items.spec.ts | 160 ++++++++++++++++++++++++++++++++ src/tests/ports/nfts.spec.ts | 102 +++++++++++++++++++- src/types.ts | 2 + 16 files changed, 513 insertions(+), 25 deletions(-) create mode 100644 src/ports/builder/component.ts create mode 100644 src/ports/builder/index.ts create mode 100644 src/ports/builder/types.ts create mode 100644 src/ports/items/index.ts create mode 100644 src/tests/ports/builder.spec.ts create mode 100644 src/tests/ports/items.spec.ts diff --git a/.env.defaults b/.env.defaults index bad8d94b..fb5a31da 100644 --- a/.env.defaults +++ b/.env.defaults @@ -1,5 +1,6 @@ CORS_ORIGIN=^http:\/\/localhost:[0-9]{1,10}$ CORS_METHOD=* +BUILDER_SERVER_URL=https://builder-api.decentraland.org MARKETPLACE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/marketplace COLLECTIONS_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/collections-matic-mainnet RENTALS_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/rentals-ethereum-mainnet diff --git a/.env.spec b/.env.spec index c992da34..7a2a15bc 100644 --- a/.env.spec +++ b/.env.spec @@ -1,5 +1,6 @@ CORS_ORIGIN=* CORS_METHOD=* +BUILDER_SERVER_URL=https://builder-api.decentraland.org MARKETPLACE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/marketplace COLLECTIONS_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/collections-matic-mainnet RENTALS_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/rentals-ethereum-mainnet diff --git a/package-lock.json b/package-lock.json index 43f783ae..5d6a8d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@dcl/schemas": "^11.4.0", + "@dcl/schemas": "^11.7.0", "@well-known-components/env-config-provider": "^1.2.0", "@well-known-components/http-requests-logger-component": "^2.1.0", "@well-known-components/http-server": "^1.1.6", @@ -617,9 +617,9 @@ } }, "node_modules/@dcl/schemas": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-11.4.0.tgz", - "integrity": "sha512-pCMENllY9vcl4HS1ta/TpVqjWUov/oa9xab+Ki1iMur48nE5+xXfa7+laCPhWLOKSZEkC59i5S6zQGTSWFPYew==", + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-11.7.0.tgz", + "integrity": "sha512-Gsv4FFnr6vL56k5ozzxH1CcOCzOUHMj602IL5y69q/Iy0rtn+7OF2TckgTDHGeifzakhaVwDOiGFYeJcaMaEXg==", "dependencies": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", @@ -12812,9 +12812,9 @@ } }, "@dcl/schemas": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-11.4.0.tgz", - "integrity": "sha512-pCMENllY9vcl4HS1ta/TpVqjWUov/oa9xab+Ki1iMur48nE5+xXfa7+laCPhWLOKSZEkC59i5S6zQGTSWFPYew==", + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-11.7.0.tgz", + "integrity": "sha512-Gsv4FFnr6vL56k5ozzxH1CcOCzOUHMj602IL5y69q/Iy0rtn+7OF2TckgTDHGeifzakhaVwDOiGFYeJcaMaEXg==", "requires": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", diff --git a/package.json b/package.json index f3df0179..693993a9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "printWidth": 80 }, "dependencies": { - "@dcl/schemas": "^11.4.0", + "@dcl/schemas": "^11.7.0", "@well-known-components/env-config-provider": "^1.2.0", "@well-known-components/http-requests-logger-component": "^2.1.0", "@well-known-components/http-server": "^1.1.6", diff --git a/src/index.ts b/src/index.ts index 760abba6..e17ac7fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,6 +170,7 @@ import { createOwnersComponent } from './ports/owner/component' import { createFavoritesComponent } from './ports/favorites/components' import { ItemOptions } from './ports/items/types' import { createCatalogComponent } from './ports/catalog/component' +import { createBuilderComponent } from './ports/builder' async function initComponents(): Promise { // Default config @@ -231,6 +232,13 @@ async function initComponents(): Promise { const marketplaceChainId = getMarketplaceChainId() const collectionsChainId = getCollectionsChainId() + // Builder component + const builder = createBuilderComponent({ + url: await config.requireString('BUILDER_SERVER_URL'), + logs, + fetcher: fetch, + }) + // subgraphs const marketplaceSubgraph = await createSubgraphComponent( { logs, config, fetch, metrics }, @@ -375,6 +383,7 @@ async function initComponents(): Promise { }) const collectionsNFTs = createNFTComponent({ + builder, subgraph: collectionsSubgraph, fragmentName: 'collectionsFragment', getFragment: getCollectionsFragment, @@ -446,7 +455,7 @@ async function initComponents(): Promise { ) // items - const collectionsItems = createItemsComponent([ + const collectionsItems = createItemsComponent({ builder }, [ { subgraph: collectionsSubgraph, network: Network.MATIC, @@ -732,6 +741,7 @@ async function initComponents(): Promise { return { config, + builder, logs, server, statusChecks, diff --git a/src/ports/builder/component.ts b/src/ports/builder/component.ts new file mode 100644 index 00000000..88541fc4 --- /dev/null +++ b/src/ports/builder/component.ts @@ -0,0 +1,42 @@ +import { URL } from 'url' +import { + IFetchComponent, + ILoggerComponent, +} from '@well-known-components/interfaces' +import { IBuilderComponent } from './types' + +export function createBuilderComponent(options: { + url: string + logs: ILoggerComponent + fetcher: IFetchComponent +}): IBuilderComponent { + const { fetcher, url, logs } = options + const logger = logs.getLogger('builder-component') + + return { + async getItemUtility( + collectionAddress: string, + itemId: string + ): Promise { + try { + const baseUrl = new URL(url) + baseUrl.pathname = `/v1/published-collections/${collectionAddress}/items/${itemId}/utility` + const response = await fetcher.fetch(baseUrl.toString()) + if (!response.ok) { + throw new Error( + `Failed to fetch utility for item: ${response.status}` + ) + } + const utility = (await response.json()) as { + data: { utility: string | null } + } + return utility.data.utility ?? undefined + } catch (_e) { + logger.info( + `Failed looking for the utility of the item: ${collectionAddress} - ${itemId}.` + ) + return undefined + } + }, + } +} diff --git a/src/ports/builder/index.ts b/src/ports/builder/index.ts new file mode 100644 index 00000000..3f163d2a --- /dev/null +++ b/src/ports/builder/index.ts @@ -0,0 +1,2 @@ +export * from './component' +export * from './types' diff --git a/src/ports/builder/types.ts b/src/ports/builder/types.ts new file mode 100644 index 00000000..29073a84 --- /dev/null +++ b/src/ports/builder/types.ts @@ -0,0 +1,6 @@ +export interface IBuilderComponent { + getItemUtility( + collectionAddress: string, + itemId: string + ): Promise +} diff --git a/src/ports/items/component.ts b/src/ports/items/component.ts index 71d9a7d7..055c99d5 100644 --- a/src/ports/items/component.ts +++ b/src/ports/items/component.ts @@ -1,20 +1,22 @@ import { ChainId, ItemFilters, Network } from '@dcl/schemas' import { ISubgraphComponent } from '@well-known-components/thegraph-component' -import { AssetsNetworks } from '../../types' +import { AppComponents, AssetsNetworks } from '../../types' import { IItemsComponent, ItemFragment } from './types' import { fromItemFragment, getItemsQuery, getSubgraph } from './utils' export function createItemsComponent( + components: Pick, options: { subgraph: ISubgraphComponent network: AssetsNetworks chainId: ChainId }[] ): IItemsComponent { + const { builder } = components + async function fetch(filters: ItemFilters) { const option = getSubgraph(filters, options) if (!option) return [] - const { subgraph, network, chainId } = option if (filters.network && filters.network !== network) { @@ -25,6 +27,10 @@ export function createItemsComponent( const { items: fragments } = await subgraph.query<{ items: ItemFragment[] }>(query) + + const isFetchingASingleItem = + filters.contractAddresses?.length === 1 && filters.itemId + const items = fragments.map((fragment) => fromItemFragment( fragment, @@ -32,6 +38,14 @@ export function createItemsComponent( chainId ) ) + + if (items.length > 0 && isFetchingASingleItem) { + items[0].utility = await builder.getItemUtility( + items[0].contractAddress, + items[0].itemId + ) + } + return items } diff --git a/src/ports/items/index.ts b/src/ports/items/index.ts new file mode 100644 index 00000000..3f163d2a --- /dev/null +++ b/src/ports/items/index.ts @@ -0,0 +1,2 @@ +export * from './component' +export * from './types' diff --git a/src/ports/nfts/component.ts b/src/ports/nfts/component.ts index 72384ee9..3953f036 100644 --- a/src/ports/nfts/component.ts +++ b/src/ports/nfts/component.ts @@ -3,6 +3,12 @@ import { PoolClient } from 'pg' import { NFTCategory, NFTFilters, NFTSortBy } from '@dcl/schemas' import { IPgComponent } from '@well-known-components/pg-component' import { ISubgraphComponent } from '@well-known-components/thegraph-component' +import { getMarketplaceChainId } from '../../logic/chainIds' +import { + getLatestSubgraphSchema, + getMarketplaceSubgraphNameChain, +} from '../../subgraphUtils' +import { IBuilderComponent } from '../builder' import { INFTsComponent, NFTResult } from './types' import { getByTokenIdQuery, @@ -11,14 +17,10 @@ import { getFuzzySearchQueryForENS, getQueryVariables, } from './utils' -import { getMarketplaceChainId } from '../../logic/chainIds' -import { - getLatestSubgraphSchema, - getMarketplaceSubgraphNameChain, -} from '../../subgraphUtils' export function createNFTComponent(options: { subgraph: ISubgraphComponent + builder?: IBuilderComponent db?: IPgComponent listsServer?: string fragmentName: string @@ -31,6 +33,7 @@ export function createNFTComponent(options: { }): INFTsComponent { const { subgraph, + builder, db, fragmentName, getFragment, @@ -163,7 +166,14 @@ export function createNFTComponent(options: { if (fragments.length === 0) { return null } else { - return fromFragment(fragments[0], caller) + const nftResult = fromFragment(fragments[0], caller) + if (builder && nftResult.nft.itemId) { + nftResult.nft.utility = await builder.getItemUtility( + contractAddress, + nftResult.nft.itemId + ) + } + return nftResult } } diff --git a/src/tests/components.ts b/src/tests/components.ts index 89992672..47ba0c6e 100644 --- a/src/tests/components.ts +++ b/src/tests/components.ts @@ -149,6 +149,7 @@ import { import { createOwnersComponent } from '../ports/owner/component' import { createFavoritesComponent } from '../ports/favorites/components' import { createCatalogComponent } from '../ports/catalog/component' +import { createBuilderComponent } from '../ports/builder' // start TCP port for listeners let lastUsedPort = 19000 + parseInt(process.env.JEST_WORKER_ID || '1') * 1000 @@ -375,14 +376,27 @@ export async function initComponents(): Promise { MARKETPLACE_FAVORITES_SERVER_URL ) + // Builder component + const BUILDER_SERVER_URL = await config.requireString('BUILDER_SERVER_URL') + const builder = createBuilderComponent({ + fetcher: fetchComponent, + logs, + url: BUILDER_SERVER_URL, + }) + // items - const collectionsItems = createItemsComponent([ + const collectionsItems = createItemsComponent( { - subgraph: collectionsSubgraph, - network: Network.MATIC, - chainId: collectionsChainId, + builder, }, - ]) + [ + { + subgraph: collectionsSubgraph, + network: Network.MATIC, + chainId: collectionsChainId, + }, + ] + ) const items = createMergerComponent({ sources: [ @@ -579,7 +593,7 @@ export async function initComponents(): Promise { const marketplacePrices = createPricesComponent({ subgraph: marketplaceSubgraph, queryGetter: getMarketplacePricesQuery, - customValidation: getMarketplacePriceFiltersValidation + customValidation: getMarketplacePriceFiltersValidation, }) const collectionsPrices = createPricesComponent({ @@ -625,6 +639,7 @@ export async function initComponents(): Promise { return { config, logs, + builder, tracer, server, statusChecks, diff --git a/src/tests/ports/builder.spec.ts b/src/tests/ports/builder.spec.ts new file mode 100644 index 00000000..afb2adf6 --- /dev/null +++ b/src/tests/ports/builder.spec.ts @@ -0,0 +1,125 @@ +import { IBuilderComponent, createBuilderComponent } from '../../ports/builder' + +let builderComponent: IBuilderComponent +let fetchMock: jest.Mock +const builderUrl = 'http://example.com' + +describe('when getting the wearable utility', () => { + let blockchainItemId: string + let collectionAddress: string + + beforeEach(() => { + blockchainItemId = '1' + collectionAddress = '0x123' + fetchMock = jest.fn() + builderComponent = createBuilderComponent({ + url: builderUrl, + logs: { + getLogger: () => ({ + info: jest.fn(), + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }), + }, + fetcher: { + fetch: fetchMock, + }, + }) + }) + + describe('when performing the request', () => { + beforeEach(() => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ data: { utility: 'This is a utility' } }), + }) + }) + + it('should query the utility endpoint with the contract address and item id', async () => { + expect( + await builderComponent.getItemUtility( + collectionAddress, + blockchainItemId + ) + ).toEqual(expect.anything()) + expect(fetchMock).toHaveBeenCalledWith( + `${builderUrl}/v1/published-collections/${collectionAddress}/items/${blockchainItemId}/utility` + ) + }) + }) + + describe('and the request is successful', () => { + let data: { utility: string | null } + beforeEach(() => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ data }), + }) + }) + + describe('and the utility is null', () => { + beforeEach(() => { + data = { utility: null } + }) + + it('should resolve to undefined', async () => { + expect( + await builderComponent.getItemUtility( + collectionAddress, + blockchainItemId + ) + ).toBeUndefined() + }) + }) + + describe('and the utility is not null', () => { + beforeEach(() => { + data = { utility: 'This is a utility' } + }) + + it('should resolve to the utility', async () => { + expect( + await builderComponent.getItemUtility( + collectionAddress, + blockchainItemId + ) + ).toEqual(data.utility) + }) + }) + }) + + describe("and the request fails with a status different than a 200's", () => { + beforeEach(() => { + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 404, + }) + }) + + it('should resolve to undefined', async () => { + expect( + await builderComponent.getItemUtility( + collectionAddress, + blockchainItemId + ) + ).toBeUndefined() + }) + }) + + describe('and the request fails', () => { + beforeEach(() => { + fetchMock.mockRejectedValueOnce(new Error('An error occurred')) + }) + + it('should resolve to undefined', async () => { + expect( + await builderComponent.getItemUtility( + collectionAddress, + blockchainItemId + ) + ).toBeUndefined() + }) + }) +}) diff --git a/src/tests/ports/items.spec.ts b/src/tests/ports/items.spec.ts new file mode 100644 index 00000000..7d227031 --- /dev/null +++ b/src/tests/ports/items.spec.ts @@ -0,0 +1,160 @@ +import { + ChainId, + ItemFilters, + Network, + Rarity, + WearableCategory, +} from '@dcl/schemas' +import { ISubgraphComponent } from '@well-known-components/thegraph-component' +import { + FragmentItemType, + IItemsComponent, + ItemFragment, + createItemsComponent, +} from '../../ports/items' +import { IBuilderComponent } from '../../ports/builder' +import { fromItemFragment } from '../../ports/items/utils' + +let itemsComponent: IItemsComponent +let builderComponentMock: IBuilderComponent +let subgraphComponentMock: ISubgraphComponent +let getItemUtilityMock: jest.Mock +let querySubgraphMock: jest.Mock +let itemFragment: ItemFragment + +beforeEach(() => { + getItemUtilityMock = jest.fn() + querySubgraphMock = jest.fn() + + builderComponentMock = { + getItemUtility: getItemUtilityMock, + } + subgraphComponentMock = { + query: querySubgraphMock, + } + + itemsComponent = createItemsComponent( + { + builder: builderComponentMock, + }, + [ + { + subgraph: subgraphComponentMock, + network: Network.MATIC, + chainId: ChainId.MATIC_MAINNET, + }, + ] + ) + itemFragment = { + id: '1', + itemType: FragmentItemType.WEARABLE_V2, + searchWearableBodyShapes: [], + rarity: Rarity.MYTHIC, + collection: { + id: '0x123', + creator: '0x234', + }, + beneficiary: '0x0000000000000000000000000000000000000000', + price: '0', + image: '', + available: '0', + searchEmoteBodyShapes: null, + createdAt: '0', + updatedAt: '0', + reviewedAt: '0', + soldAt: '0', + firstListedAt: null, + urn: '', + searchIsStoreMinter: false, + blockchainId: '1', + metadata: { + wearable: { + name: 'aWearableName', + description: '', + category: WearableCategory.UPPER_BODY, + }, + emote: null, + }, + } +}) + +describe('when fetching multiple items', () => { + let filters: ItemFilters + + beforeEach(() => { + filters = { + network: Network.MATIC, + } + }) + + describe('and no items where found in the graph', () => { + beforeEach(() => { + querySubgraphMock.mockResolvedValue({ items: [] }) + }) + + it('should return an empty array of items without having fetched the item utility', async () => { + expect(await itemsComponent.fetch(filters)).toEqual([]) + expect(getItemUtilityMock).not.toHaveBeenCalled() + }) + }) + + describe('and some items were found in the graph', () => { + beforeEach(() => { + querySubgraphMock.mockResolvedValue({ + items: [itemFragment], + }) + }) + + it('should resolve with the items without having requested their utility', async () => { + expect(await itemsComponent.fetch(filters)).toEqual([ + fromItemFragment(itemFragment, Network.MATIC, ChainId.MATIC_MAINNET), + ]) + expect(getItemUtilityMock).not.toHaveBeenCalled() + }) + }) +}) + +describe('when fetching a single item', () => { + let filters: ItemFilters + + beforeEach(() => { + filters = { + contractAddresses: ['0x123'], + itemId: '1', + network: Network.MATIC, + } + }) + + describe('and no items where found in the graph', () => { + beforeEach(() => { + querySubgraphMock.mockResolvedValue({ items: [] }) + }) + + it('should resolve into an empty array of items without having fetched the item utility', async () => { + expect(await itemsComponent.fetch(filters)).toEqual([]) + expect(getItemUtilityMock).not.toHaveBeenCalled() + }) + }) + + describe('and a item was found in the graph', () => { + beforeEach(() => { + querySubgraphMock.mockResolvedValue({ + items: [itemFragment], + }) + getItemUtilityMock.mockResolvedValue('This is a utility') + }) + + it('should resolve the item with the utility', async () => { + expect(await itemsComponent.fetch(filters)).toEqual([ + { + ...fromItemFragment( + itemFragment, + Network.MATIC, + ChainId.MATIC_MAINNET + ), + utility: 'This is a utility', + }, + ]) + }) + }) +}) diff --git a/src/tests/ports/nfts.spec.ts b/src/tests/ports/nfts.spec.ts index df8a64a5..8c85db22 100644 --- a/src/tests/ports/nfts.spec.ts +++ b/src/tests/ports/nfts.spec.ts @@ -27,6 +27,7 @@ import { createNFTComponent } from '../../../src/ports/nfts/component' import { INFTsComponent } from '../../ports/nfts/types' import { FragmentItemType } from '../../ports/items/types' import { getFetchQuery, getQueryVariables } from '../../ports/nfts/utils' +import { IBuilderComponent } from '../../ports/builder' jest.mock('node-fetch') @@ -35,6 +36,8 @@ let collectionSubgraphMock: ISubgraphComponent let queryMock: jest.Mock let marketplaceNFTsMock: INFTsComponent let collectionsNFTsMock: INFTsComponent +let builderMock: IBuilderComponent +let getItemUtilityMock: jest.Mock let dateNowSpy: jest.SpyInstance const expiresAt = Date.now() @@ -44,12 +47,16 @@ const collectionsFragment = 'collectionsFragment' beforeEach(() => { dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => expiresAt) queryMock = jest.fn() + getItemUtilityMock = jest.fn() marketplaceSubgraphMock = { query: queryMock, } collectionSubgraphMock = { query: queryMock, } + builderMock = { + getItemUtility: getItemUtilityMock, + } marketplaceNFTsMock = createNFTComponent({ subgraph: marketplaceSubgraphMock, @@ -62,6 +69,7 @@ beforeEach(() => { }) collectionsNFTsMock = createNFTComponent({ + builder: builderMock, subgraph: collectionSubgraphMock, fragmentName: collectionsFragment, getFragment: getCollectionsFragment, @@ -156,7 +164,7 @@ describe('when fetching emotes', () => { bodyShapes: [BodyShape.MALE, BodyShape.FEMALE], loop: false, hasGeometry: false, - hasSound: false + hasSound: false, }, }, createdAt: Date.now().toString(), @@ -260,7 +268,7 @@ describe('when fetching nfts', () => { bodyShapes: [BodyShape.MALE, BodyShape.FEMALE], loop: false, hasGeometry: false, - hasSound: false + hasSound: false, }, }, createdAt: Date.now().toString(), @@ -315,3 +323,93 @@ describe('when fetching nfts', () => { }) }) }) + +describe('when fetching one NFT', () => { + const contractAddress = '0x0' + const tokenId = 'someTokenId' + const itemBlockchainId = '1' + + describe('and the query request fails', () => { + beforeEach(() => { + queryMock.mockRejectedValueOnce(new Error('An error occurred')) + }) + + it('should propagate the error', () => { + return expect( + collectionsNFTsMock.fetchOne(contractAddress, tokenId) + ).rejects.toThrowError('An error occurred') + }) + }) + + describe('and the query request is successful', () => { + let data: { nfts: CollectionsFragment[] } + + beforeEach(() => { + data = { nfts: [] } + queryMock.mockResolvedValueOnce(data) + }) + + describe('and no nfts were found', () => { + beforeEach(() => { + data.nfts = [] + }) + + it('should resolve to null without requesting the item utility', async () => { + expect( + await collectionsNFTsMock.fetchOne(contractAddress, tokenId) + ).toBeNull() + expect(getItemUtilityMock).not.toHaveBeenCalled() + }) + }) + + describe('and one nft is found', () => { + let nftFragment: CollectionsFragment + let utility: string + + beforeEach(() => { + utility = 'This is a utility' + nftFragment = { + id: '1', + itemType: FragmentItemType.EMOTE_V1, + image: '', + contractAddress, + tokenId, + owner: { address: '0x0' }, + metadata: { + wearable: null, + emote: { + name: 'emote', + description: '', + category: EmoteCategory.DANCE, + rarity: Rarity.COMMON, + bodyShapes: [BodyShape.MALE, BodyShape.FEMALE], + loop: false, + hasGeometry: false, + hasSound: false, + }, + }, + createdAt: '0', + updatedAt: '0', + soldAt: '', + searchOrderPrice: null, + searchOrderCreatedAt: null, + itemBlockchainId, + issuedId: 'anIssueId', + activeOrder: null, + openRentalId: null, + urn: '', + } + ;(data.nfts = [nftFragment]), + getItemUtilityMock.mockResolvedValue(utility) + }) + + it('should resolve with the nft having the item utility', async () => { + const nftResult = fromCollectionsFragment(nftFragment) + + expect( + await collectionsNFTsMock.fetchOne(contractAddress, tokenId) + ).toEqual({ ...nftResult, nft: { ...nftResult.nft, utility } }) + }) + }) + }) +}) diff --git a/src/types.ts b/src/types.ts index 8cd6da5a..d21be424 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,7 @@ import { IOwnerDataComponent } from './ports/owner/types' import { ItemOptions } from './ports/items/types' import { IFavoritesComponent } from './ports/favorites/types' import { ICatalogComponent } from './ports/catalog/types' +import { IBuilderComponent } from './ports/builder' export type AssetsNetworks = Network.MATIC | Network.ETHEREUM @@ -97,6 +98,7 @@ export type AppComponents = { favorites: IFavoritesComponent satsumaDatabase: IPgComponent catalog: ICatalogComponent + builder: IBuilderComponent } export type Context = RoutedContext<