diff --git a/package-lock.json b/package-lock.json index ee23848..903113f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "template-server", "dependencies": { - "@dcl/schemas": "^7.5.0", + "@dcl/schemas": "^8.2.2", "@well-known-components/env-config-provider": "^1.1.1", "@well-known-components/http-server": "^1.1.6", "@well-known-components/interfaces": "^1.2.0", @@ -690,9 +690,9 @@ } }, "node_modules/@dcl/schemas": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-7.6.0.tgz", - "integrity": "sha512-TDIk7tDc4VB9ky0EgVUjFB/EBKkgjadcQ/ia4b9kU5vYlx7XUSB2kao/EDNRFfR17W1pssmZlUDtjmzFWTYSgQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.2.2.tgz", + "integrity": "sha512-IZqcT1YOKxw5XWs6LW6Uw+7Ue5vHCVERPMwefAdt26jW1OTH818od0rBc1tQzzfBTwsrAvbgFJvpbZedieu00g==", "dependencies": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", @@ -11513,9 +11513,9 @@ } }, "@dcl/schemas": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-7.6.0.tgz", - "integrity": "sha512-TDIk7tDc4VB9ky0EgVUjFB/EBKkgjadcQ/ia4b9kU5vYlx7XUSB2kao/EDNRFfR17W1pssmZlUDtjmzFWTYSgQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.2.2.tgz", + "integrity": "sha512-IZqcT1YOKxw5XWs6LW6Uw+7Ue5vHCVERPMwefAdt26jW1OTH818od0rBc1tQzzfBTwsrAvbgFJvpbZedieu00g==", "requires": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", diff --git a/package.json b/package.json index 2275fb0..f569aec 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "typescript": "^4.9.5" }, "dependencies": { - "@dcl/schemas": "^7.5.0", + "@dcl/schemas": "^8.2.2", "@well-known-components/env-config-provider": "^1.1.1", "@well-known-components/http-server": "^1.1.6", "@well-known-components/interfaces": "^1.2.0", diff --git a/src/logic/contracts.ts b/src/logic/contracts.ts index 0900b61..a6cda4b 100644 --- a/src/logic/contracts.ts +++ b/src/logic/contracts.ts @@ -1,4 +1,5 @@ import { ChainId, Contract, Network, NFTCategory } from '@dcl/schemas' +import { getPolygonChainId } from './chainIds' export function getMarketplaceContracts(chainId: ChainId): Contract[] { switch (chainId) { @@ -353,7 +354,51 @@ export function getMarketplaceContracts(chainId: ChainId): Contract[] { } ] } + case ChainId.ETHEREUM_SEPOLIA: { + return [ + { + name: 'LAND', + address: '0x42f4ba48791e2de32f5fbf553441c2672864bb33', + category: NFTCategory.PARCEL, + network: Network.ETHEREUM, + chainId: ChainId.ETHEREUM_GOERLI + }, + { + name: 'Estates', + address: '0x369a7fbe718c870c79f99fb423882e8dd8b20486', + category: NFTCategory.ESTATE, + network: Network.ETHEREUM, + chainId: ChainId.ETHEREUM_GOERLI + }, + { + name: 'Names', + address: '0x7518456ae93eb98f3e64571b689c626616bb7f30', + category: NFTCategory.ENS, + network: Network.ETHEREUM, + chainId: ChainId.ETHEREUM_GOERLI + }, + { + name: 'Exclusive Masks', + address: '0x11a970e744ff69db8f461c2d0fc91d4293914301', + network: Network.ETHEREUM, + chainId: ChainId.ETHEREUM_GOERLI, + category: NFTCategory.WEARABLE + } + ] + } default: return [] } } + +export const getCollectionStoreAddress = () => { + const chainId = getPolygonChainId() + switch (chainId) { + case ChainId.MATIC_MAINNET: + return '0x214ffc0f0103735728dc66b61a22e4f163e275ae' + case ChainId.MATIC_MUMBAI: + return '0x6ddf1b1924dad850adbc1c02026535464be06b0c' + default: + return '' + } +} diff --git a/src/ports/catalog/component.ts b/src/ports/catalog/component.ts index e8647ca..6dd9eaf 100644 --- a/src/ports/catalog/component.ts +++ b/src/ports/catalog/component.ts @@ -17,7 +17,8 @@ export async function createCatalogComponent(components: Pick { + const networks = network ? [network] : creator && creator.length ? [Network.MATIC] : [Network.ETHEREUM, Network.MATIC] + const sources = networks.reduce((acc, curr) => { acc[curr] = getChainName(curr === Network.ETHEREUM ? marketplaceChainId : collectionsChainId) || '' return acc }, {} as Record) @@ -45,7 +46,8 @@ export async function createCatalogComponent(components: Pick id)] } } - const results = await client.query(getCatalogQuery(reducedSchemas, filters)) + const query = getCatalogQuery(reducedSchemas, filters) + const results = await client.query(query) catalogItems = results.rows.map(res => fromCollectionsItemDbResultToCatalogItem(res, network)) total = results.rows[0]?.total ?? results.rows[0]?.total_rows ?? 0 diff --git a/src/ports/catalog/queries.ts b/src/ports/catalog/queries.ts index 05fce53..2064e02 100644 --- a/src/ports/catalog/queries.ts +++ b/src/ports/catalog/queries.ts @@ -11,6 +11,7 @@ import { Network, WearableCategory } from '@dcl/schemas' +import { getCollectionStoreAddress } from '../../logic/contracts' import { CatalogQueryFilters } from './types' import { FragmentItemType } from './utils' @@ -71,7 +72,7 @@ export function getOrderBy(filters: CatalogFilters) { let sortByQuery: SQLStatement | string = `ORDER BY first_listed_at ${sortDirectionParam}\n` switch (sortByParam) { case CatalogSortBy.NEWEST: - sortByQuery = 'ORDER BY first_listed_at desc NULLS last \n' + sortByQuery = 'ORDER BY first_listed_at desc NULLS last, id \n' break case CatalogSortBy.MOST_EXPENSIVE: sortByQuery = 'ORDER BY max_price desc \n' @@ -118,10 +119,10 @@ const getMultiNetworkQuery = (schemas: Record, filters: CatalogQ queries.forEach((query, index) => { unionQuery.append(query) if (queries[index + 1]) { - unionQuery.append(SQL`\n UNION ALL \n`) + unionQuery.append(SQL`\n UNION ALL ( \n`) } }) - unionQuery.append(SQL`\n) as temp \n`) + unionQuery.append(SQL`\n)) as temp \n`) addQuerySort(unionQuery, filters) if (limit !== undefined && offset !== undefined) { unionQuery.append(SQL`LIMIT ${limit} OFFSET ${offset}`) @@ -175,10 +176,34 @@ export const getIsSoldOutWhere = () => { return SQL`items.available = 0` } -export const getIsOnSale = (filters: CatalogFilters) => { - return filters.isOnSale - ? SQL`((search_is_store_minter = true AND available > 0) OR listings_count IS NOT NULL)` - : SQL`((search_is_store_minter = false OR available = 0) AND listings_count IS NULL)` +export const getIsOnSaleJoin = (schemaVersion: string, _filters: CatalogFilters) => { + return SQL` + LEFT JOIN ( + SELECT collection_id, + value, + timestamp, + ROW_NUMBER() OVER (PARTITION BY collection_id ORDER BY timestamp DESC) AS row_num + FROM `.append(schemaVersion).append(SQL`.collection_set_global_minter_events + WHERE search_is_store_minter = true + AND minter = ${getCollectionStoreAddress()} + ) AS collection_minters ON items.collection = collection_minters.collection_id AND collection_minters.row_num = 1 + `) +} + +export const getIsCollectionApprovedJoin = (schemaVersion: string) => { + return SQL` + JOIN ( + SELECT + collection_id, + value, + timestamp, + ROW_NUMBER() OVER ( + PARTITION BY collection_id + ORDER BY timestamp DESC + ) AS row_num + FROM `.append(schemaVersion).append(SQL`.collection_set_approved_events + WHERE value = true + ) AS collection_set_approved_events ON items.collection = collection_set_approved_events.collection_id AND collection_set_approved_events.row_num = 1`) } export const getisWearableHeadAccessoryWhere = () => { @@ -250,7 +275,6 @@ export const getCollectionsQueryWhere = (filters: CatalogFilters) => { filters.rarities?.length ? getRaritiesWhere(filters) : undefined, filters.creator?.length ? getCreatorWhere(filters) : undefined, filters.isSoldOut ? getIsSoldOutWhere() : undefined, - filters.isOnSale !== undefined ? getIsOnSale(filters) : undefined, filters.isWearableHead ? getisWearableHeadAccessoryWhere() : undefined, filters.isWearableAccessory ? getWearableAccessoryWhere() : undefined, filters.wearableCategory ? getWearableCategoryWhere(filters) : undefined, @@ -266,7 +290,9 @@ export const getCollectionsQueryWhere = (filters: CatalogFilters) => { ].filter(Boolean) const result = - filters.network !== Network.ETHEREUM ? SQL`WHERE (item_set_minter_event.value = true OR collection_minters.value = true) ` : SQL`` + filters.network !== Network.ETHEREUM && filters.isOnSale + ? SQL`WHERE (item_set_minter_event.value = true OR collection_minters.value = true) ` + : SQL`` if (!conditions.length) { return result } else { @@ -285,23 +311,23 @@ export const getCollectionsQueryWhere = (filters: CatalogFilters) => { } const getMinPriceCase = (filters: CatalogQueryFilters) => { - return SQL`CASE - WHEN (items.max_supply::numeric - COALESCE(nfts.nfts_count, 0)) > 0 AND item_set_minter_event.value = true - `.append(filters.minPrice ? SQL`AND items.price >= ${filters.minPrice}` : SQL``) - .append(` THEN LEAST(items.price, nfts_with_orders.min_price) - ELSE nfts_with_orders.min_price - END AS min_price - `) + return SQL` + CASE + WHEN (items.max_supply::numeric - COALESCE(nfts.nfts_count, 0)) > 0 AND item_set_minter_event.value = true + `.append(filters.minPrice ? SQL`AND COALESCE(latest_prices.price, items.price) >= ${filters.minPrice}` : SQL``) + .append(`THEN LEAST(COALESCE(latest_prices.price, items.price), nfts_with_orders.min_price) + ELSE nfts_with_orders.min_price + END AS min_price`) } const getMaxPriceCase = (filters: CatalogQueryFilters) => { - return SQL`CASE - WHEN (items.max_supply::numeric - COALESCE(nfts.nfts_count, 0)) > 0 AND item_set_minter_event.value = true - `.append(filters.maxPrice ? SQL`AND items.price <= ${filters.maxPrice}` : SQL``) - .append(` THEN GREATEST(items.price, nfts_with_orders.max_price) + return SQL` + CASE + WHEN (items.max_supply::numeric - COALESCE(nfts.nfts_count, 0)) > 0 AND item_set_minter_event.value = true + `.append(filters.maxPrice ? SQL`AND COALESCE(latest_prices.price, items.price) <= ${filters.maxPrice}` : SQL``) + .append(`THEN GREATEST(COALESCE(latest_prices.price, items.price), nfts_with_orders.max_price) ELSE nfts_with_orders.max_price - END AS max_price - `) + END AS max_price`) } const getOwnersJoin = (schemaVersion: string) => { @@ -311,181 +337,220 @@ const getOwnersJoin = (schemaVersion: string) => { .append('.nfts as nfts GROUP BY nfts.item) AS nfts ON nfts.item = items.id ') } -const getCollectionsJoin = (schemaVersion: string) => { - return SQL`LEFT JOIN `.append(schemaVersion).append(SQL`.collections as collections ON collections.id = items.collection `) +const getNFTsJoin = () => { + return SQL` + LEFT JOIN nfts ON nfts.item = items.id ` } -const getNFTsJoin = () => { - return SQL`LEFT JOIN nfts ON nfts.item = items.id ` +const getLatestMetadataJoin = () => { + return SQL` + LEFT JOIN latest_metadata ON latest_metadata.item_id = items.id ` } const getEventsTableJoins = (schemaVersion: string) => { return SQL` - LEFT JOIN ( - SELECT item_id, value, timestamp, - ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY timestamp DESC) AS row_num - FROM ` + LEFT JOIN ( + SELECT item_id, value, timestamp, + ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY timestamp DESC) AS row_num + FROM ` .append(schemaVersion) .append( SQL`.item_minters - WHERE minter = '0x214ffc0f0103735728dc66b61a22e4f163e275ae' - ) AS item_set_minter_event ON items.id = item_set_minter_event.item_id AND item_set_minter_event.row_num = 1 - - LEFT JOIN ( - SELECT collection_id, - value, - ROW_NUMBER() OVER (PARTITION BY collection_id ORDER BY timestamp DESC) AS row_num - FROM `.append(schemaVersion).append(SQL`.collection_set_global_minter_events - WHERE search_is_store_minter = true - -- WHERE minter = '0x214ffc0f0103735728dc66b61a22e4f163e275ae' - ) AS collection_minters ON collections.id = collection_minters.collection_id AND collection_minters.row_num = 1 - `) + WHERE minter = ${getCollectionStoreAddress()} + ) AS item_set_minter_event ON items.id = item_set_minter_event.item_id AND item_set_minter_event.row_num = 1 + ` ) } -export const getCollectionsItemsCatalogQuery = (schemaVersion: string, filters: CatalogQueryFilters) => { - const query = SQL` - WITH nfts AS ( - SELECT item, COUNT(*) AS nfts_count - FROM ` +const ordersJoin = (schemaVersion: string, filters: CatalogQueryFilters) => { + return SQL` + LEFT JOIN ( + SELECT + nfts_with_orders.item, + COUNT(nfts_with_orders.id) AS listings_count, + MIN(nfts_with_orders.price) AS min_price, + MAX(nfts_with_orders.price) AS max_price, + MAX(nfts_with_orders.created_at) AS max_order_created_at + FROM ( + SELECT orders.item, + orders.id, + orders.price, + orders.created_at + FROM ` + .append(schemaVersion) .append( - SQL`.nfts - GROUP BY item - ) - SELECT - COUNT(*) OVER() as total_rows, - items.id, - items.blockchain_id, - items.search_is_collection_approved, - to_json( - CASE WHEN ( - items.item_type = 'wearable_v1' OR items.item_type = 'wearable_v2' OR items.item_type = 'smart_wearable_v1') THEN metadata_wearable - ELSE metadata_emote - END - ) as metadata, - items.image, - items.collection, - items.rarity, - items.item_type::text, - items.price, - (items.max_supply::numeric - COALESCE(nfts.nfts_count, 0)) AS available, - items.creator, - items.beneficiary, - items.created_at, - items.updated_at, - items.reviewed_at, - items.sold_at, - ${filters.network} as network, - items.first_listed_at, - nfts_with_orders.min_price AS min_listing_price, - nfts_with_orders.max_price AS max_listing_price, - COALESCE(nfts_with_orders.listings_count,0) as listings_count,` - .append(filters.isOnSale === false ? SQL`nfts.owners_count,` : SQL``) - .append( - ` - nfts_with_orders.max_order_created_at as max_order_created_at, - ` - ) - .append(getMinPriceCase(filters)) - .append( - `, - ` - ) - .append(getMaxPriceCase(filters)) - .append( - ` - FROM ` - ) - .append(schemaVersion) - .append('.items AS items ') - .append(filters.isOnSale === false ? getOwnersJoin(schemaVersion) : SQL``) - .append(getCollectionsJoin(schemaVersion)) - .append(getNFTsJoin()) - .append( - ` - LEFT JOIN ( - SELECT - nfts_with_orders.item, - COUNT(nfts_with_orders.id) AS listings_count, - MIN(nfts_with_orders.price) AS min_price, - MAX(nfts_with_orders.price) AS max_price, - MAX(nfts_with_orders.created_at) AS max_order_created_at - FROM ( - SELECT orders.item, - orders.id, - orders.price, - orders.created_at, - ROW_NUMBER() OVER (PARTITION BY orders.item ORDER BY orders.created_at DESC) AS row_num - FROM ` - ) - .append(schemaVersion) - .append( - SQL`.orders AS orders - WHERE - orders.status = 'open' - AND orders.expires_at < ` - ) - .append(MAX_ORDER_TIMESTAMP) - .append( - ` - AND to_timestamp(orders.expires_at / 1000.0) > now() - ` - ) - .append(getOrderRangePriceWhere(filters)) - .append( - SQL` - ) as nfts_with_orders - WHERE nfts_with_orders.row_num = 1 - GROUP BY nfts_with_orders.item + SQL` .orders AS orders + WHERE orders.status = 'open' AND orders.expires_at BETWEEN (EXTRACT(EPOCH FROM now()) * 1000)::bigint AND ${MAX_ORDER_TIMESTAMP}::numeric AND expires_at > EXTRACT(EPOCH FROM now()) * 1000 + ` + ) + .append(getOrderRangePriceWhere(filters)) + .append( + SQL` + ) as nfts_with_orders + GROUP BY nfts_with_orders.item ) as nfts_with_orders ON nfts_with_orders.item = items.id` - ) - .append( - ` - LEFT JOIN ( - SELECT - metadata.id, - wearable.description, - wearable.category, - wearable.body_shapes, - wearable.rarity, - wearable.name - FROM ` - ) - .append(schemaVersion) - .append( - `.wearable AS wearable - JOIN ` - ) - .append(schemaVersion) - .append( - `.metadata AS metadata ON metadata.wearable = wearable.id - ) AS metadata_wearable ON metadata_wearable.id = items.metadata AND (items.item_type = 'wearable_v1' OR items.item_type = 'wearable_v2' OR items.item_type = 'smart_wearable_v1') - LEFT JOIN ( - SELECT - metadata.id, - emote.description, - emote.category, - emote.body_shapes, - emote.rarity, - emote.name, - emote.loop - FROM ` - ) - .append(schemaVersion) - .append( - `.emote AS emote - JOIN ` - ) - .append(schemaVersion) - .append( - `.metadata AS metadata ON metadata.emote = emote.id - ) AS metadata_emote ON metadata_emote.id = items.metadata AND items.item_type = 'emote_v1' ` - ) - .append(getEventsTableJoins(schemaVersion)) - .append(getCollectionsQueryWhere(filters)) ) +} + +const getLatestPriceJoin = () => { + return SQL` + LEFT JOIN ( + SELECT item_id, price, timestamp, ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY timestamp DESC) AS row_num + FROM latest_prices + ) AS latest_prices ON latest_prices.item_id = items.id AND latest_prices.row_num = 1 + ` +} + +const addMetadataJoins = (schemaVersion: string, filters: CatalogQueryFilters) => { + const wearablesJoin = SQL` + LEFT JOIN ( + SELECT + metadata.id, + wearable.description, + wearable.category, + wearable.body_shapes, + wearable.name + FROM ` + .append(schemaVersion) + .append('.wearable AS wearable JOIN ') + .append(schemaVersion).append(SQL`.metadata AS metadata ON metadata.wearable = wearable.id + ) AS metadata_wearable ON metadata_wearable.id = latest_metadata.id AND (items.item_type = 'wearable_v1' OR items.item_type = 'wearable_v2' OR items.item_type = 'smart_wearable_v1') + `) + + const emoteJoin = SQL` + LEFT JOIN ( + SELECT + metadata.id, + emote.description, + emote.category, + emote.body_shapes, + emote.name, + emote.loop + FROM ` + .append(schemaVersion) + .append('.emote AS emote JOIN ') + .append(schemaVersion).append(SQL`.metadata AS metadata ON metadata.emote = emote.id + ) AS metadata_emote ON metadata_emote.id = latest_metadata.id AND items.item_type = 'emote_v1' + `) + + switch (filters.category) { + case NFTCategory.WEARABLE: + return wearablesJoin + case NFTCategory.EMOTE: + return emoteJoin + default: + return wearablesJoin.append(emoteJoin) + } +} + +// CTEs +const getNFTsCTE = (schemaVersion: string) => { + return SQL`nfts AS (SELECT item, COUNT(*) AS nfts_count FROM `.append(schemaVersion).append(SQL`.nfts GROUP BY item) + `) +} +const getLatestPricesCTE = (schemaVersion: string) => { + return SQL`latest_prices AS (SELECT DISTINCT ON (item_id) item_id, price, timestamp FROM `.append(schemaVersion) + .append(SQL`.update_item_data_events ORDER BY item_id, timestamp DESC) + `) +} + +const getLatestMetadataCTE = (schemaVersion: string) => { + return SQL`latest_metadata AS (SELECT DISTINCT ON (item_id) item_id, id, item_type, wearable, emote, timestamp FROM `.append( + schemaVersion + ).append(SQL`.metadata ORDER BY item_id, timestamp DESC) + `) +} + +const getCTEs = (schemaVersion: string) => { + return SQL`WITH ` + .append(getNFTsCTE(schemaVersion)) + .append(SQL`,`) + .append(getLatestPricesCTE(schemaVersion)) + .append(SQL`,`) + .append(getLatestMetadataCTE(schemaVersion)) +} + +const getMetadataSelect = (filters: CatalogQueryFilters) => { + switch (filters.category) { + case NFTCategory.WEARABLE: + return SQL`to_json(metadata_wearable) as metadata,` + case NFTCategory.EMOTE: + return SQL`to_json(metadata_emote) as metadata,` + default: + return SQL` + to_json( + CASE + WHEN latest_metadata.item_type IN ('wearable_v1', 'wearable_v2', 'smart_wearable_v1') THEN metadata_wearable + WHEN latest_metadata.item_type = 'emote_v1' THEN metadata_emote + ELSE null + END + ) as metadata, + ` + } +} + +export const getCollectionsItemsCatalogQuery = (schemaVersion: string, filters: CatalogQueryFilters) => { + const query = getCTEs(schemaVersion).append( + SQL` + SELECT + COUNT(*) OVER() as total_rows, + items.id, + items.blockchain_id, + ` + .append(getMetadataSelect(filters)) + .append( + SQL` + items.image, + items.collection, + items.rarity, + items.item_type::text, + COALESCE(latest_prices.price, items.price) AS price, + (items.max_supply::numeric - COALESCE(nfts.nfts_count, 0)) AS available, + items.creator, + items.beneficiary, + items.created_at, + items.updated_at, + items.reviewed_at, + items.sold_at, + ${filters.network} as network, + CASE + WHEN (items.max_supply::numeric - COALESCE(nfts.nfts_count, 0)) > 0 AND collection_minters.value = true THEN collection_minters.timestamp + ELSE item_set_minter_event.timestamp + END AS first_listed_at, + nfts_with_orders.min_price AS min_listing_price, + nfts_with_orders.max_price AS max_listing_price, + COALESCE(nfts_with_orders.listings_count,0) as listings_count,` + .append(filters.isOnSale === false ? SQL`nfts.owners_count,` : SQL``) + .append( + ` + nfts_with_orders.max_order_created_at as max_order_created_at,` + ) + .append(getMinPriceCase(filters)) + .append(',') + .append(getMaxPriceCase(filters)) + .append( + ` + FROM ` + ) + .append(schemaVersion) + .append( + `.items AS items + ` + ) + .append(filters.isOnSale === false ? getOwnersJoin(schemaVersion) : SQL``) + .append(getNFTsJoin()) + .append(getLatestMetadataJoin()) + .append(ordersJoin(schemaVersion, filters)) + .append(addMetadataJoins(schemaVersion, filters)) + .append(getLatestPriceJoin()) + .append(getIsCollectionApprovedJoin(schemaVersion)) + .append(getIsOnSaleJoin(schemaVersion, filters)) + .append(getEventsTableJoins(schemaVersion)) + .append(getCollectionsQueryWhere(filters)) + ) + ) addQuerySort(query, filters) addQueryPagination(query, filters) return query diff --git a/src/ports/catalog/types.ts b/src/ports/catalog/types.ts index d4b8cae..0fac1f1 100644 --- a/src/ports/catalog/types.ts +++ b/src/ports/catalog/types.ts @@ -4,6 +4,7 @@ export type CollectionsItemDBResult = { total?: number // for UNION queries, this field will be defined total_rows: number id: string + urn: string image: string collection: string blockchain_id: string diff --git a/src/ports/catalog/utils.ts b/src/ports/catalog/utils.ts index 0d45124..1b0a20f 100644 --- a/src/ports/catalog/utils.ts +++ b/src/ports/catalog/utils.ts @@ -18,7 +18,7 @@ export function fromCollectionsItemDbResultToCatalogItem(dbItem: CollectionsItem case FragmentItemType.WEARABLE_V1: case FragmentItemType.WEARABLE_V2: case FragmentItemType.SMART_WEARABLE_V1: { - const { name: wearableName, body_shapes, description, rarity, category: wearableCategory } = dbItem.metadata + const { name: wearableName, body_shapes, description, rarity, category: wearableCategory } = dbItem.metadata || {} name = wearableName category = NFTCategory.WEARABLE data = { @@ -33,7 +33,7 @@ export function fromCollectionsItemDbResultToCatalogItem(dbItem: CollectionsItem break } case FragmentItemType.EMOTE_V1: { - const { name: emoteName, body_shapes, description, rarity, loop, category: emoteCategory } = dbItem.metadata + const { name: emoteName, body_shapes, description, rarity, loop, category: emoteCategory } = dbItem.metadata || {} ;(name = emoteName), (category = NFTCategory.EMOTE) data = { emote: { @@ -59,6 +59,7 @@ export function fromCollectionsItemDbResultToCatalogItem(dbItem: CollectionsItem name, thumbnail: dbItem.image, url: `/contracts/${dbItem.collection}/items/${dbItem.blockchain_id}`, + urn: dbItem.urn, category, contractAddress: dbItem.collection, rarity: dbItem.rarity as Rarity,