diff --git a/.env.defaults b/.env.defaults index e95c9b2..6f05373 100644 --- a/.env.defaults +++ b/.env.defaults @@ -8,3 +8,4 @@ SIGNATURES_SERVER_URL= MARKETPLACE_CHAIN_ID=1 COLLECTIONS_CHAIN_ID=137 MIN_SALE_VALUE_IN_WEI= +DCL_LISTS_SERVER=https://dcl-lists.decentraland.org diff --git a/src/index.ts b/src/index.ts index df034e7..1df64b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -358,6 +358,7 @@ async function initComponents(): Promise { // nfts const marketplaceNFTs = createNFTComponent({ subgraph: marketplaceSubgraph, + listsServer: await config.requireString('DCL_LISTS_SERVER'), fragmentName: 'marketplaceFragment', getFragment: getMarketplaceFragment, fromFragment: fromMarketplaceNFTFragment, diff --git a/src/ports/nfts/component.ts b/src/ports/nfts/component.ts index fe019ea..8e92c62 100644 --- a/src/ports/nfts/component.ts +++ b/src/ports/nfts/component.ts @@ -1,4 +1,5 @@ -import { NFTFilters, NFTSortBy } from '@dcl/schemas' +import nodeFetch from 'node-fetch' +import { NFTCategory, NFTFilters, NFTSortBy } from '@dcl/schemas' import { ISubgraphComponent } from '@well-known-components/thegraph-component' import { INFTsComponent, NFTResult } from './types' import { @@ -10,6 +11,7 @@ import { export function createNFTComponent(options: { subgraph: ISubgraphComponent + listsServer?: string fragmentName: string getFragment: () => string fromFragment(fragment: T, caller?: string): NFTResult @@ -27,6 +29,7 @@ export function createNFTComponent(options: { getExtraVariables, fromFragment, getShouldFetch, + listsServer, } = options function getFragmentFetcher(filters: NFTFilters & { caller?: string }) { @@ -37,7 +40,8 @@ export function createNFTComponent(options: { getFragment, getExtraVariables, getExtraWhere, - isCount + isCount, + filters.category === NFTCategory.ENS ? await getBannedNames() : [] ) const variables = getQueryVariables(filters, getSortByProp) const { nfts: fragments } = await subgraph.query<{ @@ -47,6 +51,20 @@ export function createNFTComponent(options: { } } + async function getBannedNames() { + try { + const bannedNames = await nodeFetch(`${listsServer}/banned-names`, { + method: 'POST', + }) + + const data: { data: string[] } = await bannedNames.json() + return data.data + } catch (error) { + // if there was an error fetching the lists server, return an empty array + return [] + } + } + async function fetch( options: NFTFilters & { caller?: string } ): Promise { diff --git a/src/ports/nfts/utils.spec.ts b/src/ports/nfts/utils.spec.ts index 0e59b93..8a7e936 100644 --- a/src/ports/nfts/utils.spec.ts +++ b/src/ports/nfts/utils.spec.ts @@ -2,6 +2,26 @@ import { EmotePlayMode, GenderFilterOption, NFTCategory } from '@dcl/schemas' import { getFetchQuery } from './utils' describe('#getFetchQuery', () => { + let bannedNames = ['aBannedName'] + describe('when the array of banned list is passed', () => { + fit('should add the name_not_in filter to the query', () => { + expect( + getFetchQuery( + {}, + '', + () => '', + undefined, + undefined, + false, + bannedNames + ) + ).toEqual( + expect.stringContaining( + `name_not_in: [${bannedNames.map((name) => `"${name}"`).join(', ')}]` + ) + ) + }) + }) describe('when emotePlayMode is defined', () => { describe('and it only has one property', () => { it('should add loop as true to the query for LOOP mode', () => { diff --git a/src/ports/nfts/utils.ts b/src/ports/nfts/utils.ts index 2d3045c..994f94e 100644 --- a/src/ports/nfts/utils.ts +++ b/src/ports/nfts/utils.ts @@ -184,11 +184,18 @@ export function getFetchQuery( getNFTFragment: () => string, getExtraVariables?: (options: NFTFilters) => string[], getExtraWhere?: (options: NFTFilters) => string[], - isCount = false + isCount = false, + bannedNames: string[] = [] ) { const where: string[] = [] let wrapWhere = false + if (bannedNames.length) { + where.push( + `name_not_in: [${bannedNames.map((name) => `"${name}"`).join(', ')}]` + ) + } + if (filters.owner) { where.push('owner: $owner') } diff --git a/src/tests/ports/nfts.spec.ts b/src/tests/ports/nfts.spec.ts index dacc174..eed992c 100644 --- a/src/tests/ports/nfts.spec.ts +++ b/src/tests/ports/nfts.spec.ts @@ -1,3 +1,4 @@ +import * as nodeFetch from 'node-fetch' import { BodyShape, EmoteCategory, @@ -27,6 +28,8 @@ import { INFTsComponent } from '../../ports/nfts/types' import { FragmentItemType } from '../../ports/items/types' import { getFetchQuery, getQueryVariables } from '../../ports/nfts/utils' +jest.mock('node-fetch') + let marketplaceSubgraphMock: ISubgraphComponent let collectionSubgraphMock: ISubgraphComponent let queryMock: jest.Mock @@ -229,3 +232,82 @@ describe('when fetching emotes', () => { }) }) }) + +describe('when fetching nfts', () => { + let nftFragments: CollectionsFragment[] + let filters: NFTFilters = { + category: NFTCategory.EMOTE, + } + + beforeEach(() => { + const contractAddress = `0x0` + nftFragments = Array.from({ length: 2 }, (_, i) => ({ + id: `${contractAddress}-${i}`, + itemType: FragmentItemType.EMOTE_V1, + image: `https://peer.decentraland.zone/lambdas/collections/contents/urn:decentraland:mumbai:collections-v2:${contractAddress}:${i}/thumbnail`, + contractAddress, + tokenId: i.toString(), + owner: { address: '0x0' }, + metadata: { + wearable: null, + emote: { + name: 'emote', + description: '', + category: EmoteCategory.DANCE, + rarity: Rarity.COMMON, + bodyShapes: [BodyShape.MALE, BodyShape.FEMALE], + loop: false, + }, + }, + createdAt: Date.now().toString(), + updatedAt: Date.now().toString(), + soldAt: '', + searchOrderPrice: null, + searchOrderCreatedAt: null, + itemBlockchainId: i.toString(), + issuedId: i.toString(), + activeOrder: null, + openRentalId: null, + urn: `urn:decentraland:mumbai:collections-v2:${contractAddress}:${i}`, + })) + }) + + describe('and fetching ENS', () => { + let bannedNames: string[] + beforeEach(() => { + bannedNames = ['bannedName1', 'bannedName1'] + const mockedResponse = { + json: async () => ({ + data: bannedNames, + }), + } + + ;(nodeFetch as unknown as jest.Mock).mockResolvedValue(mockedResponse) + + filters = { + ...filters, + category: NFTCategory.ENS, + } + queryMock.mockResolvedValue({ nfts: nftFragments }) + }) + + it('should fetch the banned names and filter the query based on them', async () => { + const fetchQuery = getFetchQuery( + filters, + collectionsFragment, + getCollectionsFragment, + getCollectionsExtraVariables, + getCollectionsExtraWhere, + false, + bannedNames + ) + const variableQuery = getQueryVariables(filters, getCollectionsOrderBy) + const result = await collectionsNFTsMock.fetch(filters) + expect(result).toEqual( + nftFragments.map((f) => fromCollectionsFragment(f)) + ) + expect(nodeFetch).toHaveBeenCalled() + return expect(queryMock).toBeCalledWith(fetchQuery, variableQuery) + }) + }) +})