diff --git a/src/index.ts b/src/index.ts index 2638edf1..25878d41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,7 +114,10 @@ import { SALE_DEFAULT_SORT_BY } from './ports/sales/utils' import { createSalesComponent } from './ports/sales/component' import { createCollectionsComponent } from './ports/collections/component' import { createCollectionsSource } from './adapters/sources/collections' -import { COLLECTION_DEFAULT_SORT_BY } from './ports/collections/utils' +import { + COLLECTION_DEFAULT_SORT_BY, + getIsEthereumCollection, +} from './ports/collections/utils' import { createAccountsComponent } from './ports/accounts/component' import { createAccountsSource } from './adapters/sources/accounts' import { ACCOUNT_DEFAULT_SORT_BY } from './ports/accounts/utils' @@ -496,6 +499,14 @@ async function initComponents(): Promise { subgraph: marketplaceSubgraph, network: Network.ETHEREUM, chainId: marketplaceChainId, + useLegacySchema: true, + shouldFetch: (filters: SaleFilters) => { + return ( + !filters.contractAddress || + (!!filters.contractAddress && + !getIsEthereumCollection(filters.contractAddress, marketplaceChainId)) + ) + }, }) const collectionsSales = createSalesComponent({ @@ -504,10 +515,17 @@ async function initComponents(): Promise { chainId: collectionsChainId, }) + const collectionsEthereumSales = createSalesComponent({ + subgraph: collectionsEthereumSubgraph, + network: Network.MATIC, // we'use MATIC here so it uses the right fragment when querying + chainId: marketplaceChainId, + }) + const sales = createMergerComponent({ sources: [ createSalesSource(marketplaceSales), createSalesSource(collectionsSales), + createSalesSource(collectionsEthereumSales), ], defaultSortBy: SALE_DEFAULT_SORT_BY, directions: { diff --git a/src/ports/collections/utils.ts b/src/ports/collections/utils.ts index 56a14e57..f828b3fd 100644 --- a/src/ports/collections/utils.ts +++ b/src/ports/collections/utils.ts @@ -5,6 +5,7 @@ import { CollectionSortBy, Network, } from '@dcl/schemas' +import { getMarketplaceContracts } from '../../logic/contracts' import { SortDirection } from '../merger/types' import { CollectionFragment } from './types' @@ -150,3 +151,16 @@ export function getCollectionsQuery( ${isCount ? '' : getCollectionFragment()} ` } + +export function getIsEthereumCollection( + contractAddress: string, + chainId: ChainId +) { + const contracts = getMarketplaceContracts(chainId) + const contract = contracts.find( + (contract) => + contract.address.toLocaleLowerCase() === + contractAddress.toLocaleLowerCase() + ) + return !!contract +} diff --git a/src/ports/sales/component.ts b/src/ports/sales/component.ts index f3b0858c..7a4789cc 100644 --- a/src/ports/sales/component.ts +++ b/src/ports/sales/component.ts @@ -7,8 +7,16 @@ export function createSalesComponent(options: { subgraph: ISubgraphComponent network: Network chainId: ChainId + useLegacySchema?: boolean + shouldFetch?: (filters: SaleFilters) => boolean }): ISalesComponent { - const { subgraph, network, chainId } = options + const { + subgraph, + network, + chainId, + shouldFetch, + useLegacySchema = false, + } = options function isValid(network: Network, filters: SaleFilters) { return ( @@ -20,11 +28,11 @@ export function createSalesComponent(options: { } async function fetch(filters: SaleFilters) { - if (!isValid(network, filters)) { + if (!isValid(network, filters) || (shouldFetch && !shouldFetch(filters))) { return [] } - const query = getSalesQuery(filters, false, network) + const query = getSalesQuery(filters, false, useLegacySchema) const { sales: fragments } = await subgraph.query<{ sales: SaleFragment[] }>(query) @@ -37,11 +45,11 @@ export function createSalesComponent(options: { } async function count(filters: SaleFilters) { - if (!isValid(network, filters)) { + if (!isValid(network, filters) || (shouldFetch && !shouldFetch(filters))) { return 0 } - const query = getSalesQuery(filters, true, network) + const query = getSalesQuery(filters, true, useLegacySchema) const { sales: fragments } = await subgraph.query<{ sales: SaleFragment[] }>(query) diff --git a/src/ports/sales/utils.ts b/src/ports/sales/utils.ts index df8d6947..2e4cc84f 100644 --- a/src/ports/sales/utils.ts +++ b/src/ports/sales/utils.ts @@ -26,7 +26,7 @@ export function fromSaleFragment( return sale } -export const getSaleFragment = (network: Network) => ` +export const getSaleFragment = (useLegacySchema: boolean) => ` fragment saleFragment on Sale { id type @@ -35,7 +35,7 @@ export const getSaleFragment = (network: Network) => ` price timestamp txHash - ${network === Network.MATIC ? 'searchItemId' : ''} + ${useLegacySchema ? '' : 'searchItemId'} searchTokenId searchContractAddress } @@ -44,7 +44,7 @@ export const getSaleFragment = (network: Network) => ` export function getSalesQuery( filters: SaleFilters, isCount = false, - network: Network + useLegacySchema: boolean ) { const { first, @@ -69,7 +69,7 @@ export function getSalesQuery( where.push(`searchTokenId: "${tokenId}"`) } - if (itemId && network === Network.MATIC) { + if (itemId && !useLegacySchema) { where.push(`searchItemId: "${itemId}"`) } @@ -149,6 +149,6 @@ export function getSalesQuery( }) { ${isCount ? 'id' : `...saleFragment`} } } - ${isCount ? '' : getSaleFragment(network)} + ${isCount ? '' : getSaleFragment(useLegacySchema)} ` } diff --git a/src/tests/ports/sales.spec.ts b/src/tests/ports/sales.spec.ts new file mode 100644 index 00000000..c86439b3 --- /dev/null +++ b/src/tests/ports/sales.spec.ts @@ -0,0 +1,223 @@ +import { ISubgraphComponent } from '@well-known-components/thegraph-component' +import { ChainId, Network, SaleType } from '@dcl/schemas' +import { ISalesComponent, SaleFragment } from '../../ports/sales/types' +import { createSalesComponent } from '../../ports/sales/component' +import { fromSaleFragment } from '../../ports/sales/utils' + +let subgraphMock: ISubgraphComponent +let salesComponent: ISalesComponent + +// Mock the subgraph query response +const mockSalesFragments: SaleFragment[] = [ + { + id: 'sale1', + price: '100', + buyer: 'buyer1', + seller: 'seller1', + timestamp: '1234567890', + txHash: '0x1234567890', + type: SaleType.ORDER, + searchContractAddress: '0x1234567890', + searchTokenId: 'token1', + searchItemId: 'itemId1', + }, + { + id: 'sale2', + price: '101', + buyer: 'buyer2', + seller: 'seller2', + timestamp: '1234567891', + txHash: '0x1234567890', + type: SaleType.ORDER, + searchContractAddress: '0x1234567890', + searchTokenId: 'token2', + searchItemId: 'itemId2', + }, +] + +beforeEach(() => { + subgraphMock = { + query: jest.fn(), + } + + salesComponent = createSalesComponent({ + subgraph: subgraphMock, + network: Network.ETHEREUM, + chainId: ChainId.ETHEREUM_MAINNET, + }) +}) + +describe('when fetching the sales', () => { + describe('and the request is not valid', () => { + describe("because the component's network is different to the filters' network", () => { + beforeEach(() => { + salesComponent = createSalesComponent({ + subgraph: subgraphMock, + network: Network.MATIC, // changes the network to MATIC + chainId: ChainId.MATIC_MAINNET, + }) + }) + it('should return an empty array', async () => { + // Define invalid filters that won't trigger a fetch + const invalidFilters = { + network: Network.ETHEREUM, + } + + const result = await salesComponent.fetch(invalidFilters) + // Expect the subgraph query not to be called + expect(subgraphMock.query).not.toHaveBeenCalled() + // Expect the result to be an empty array + expect(result).toEqual([]) + }) + }) + describe('because the filters are for a SaleType.MINT on Ethereum', () => { + it('should return an empty array', async () => { + // Define invalid filters that won't trigger a fetch + const invalidFilters = { + type: SaleType.MINT, + } + + // Call the fetch method with invalid filters + const result = await salesComponent.fetch(invalidFilters) + // Expect the subgraph query not to be called + expect(subgraphMock.query).not.toHaveBeenCalled() + // Expect the result to be an empty array + expect(result).toEqual([]) + }) + }) + }) + + describe('and the shouldFetch function is provided', () => { + describe('and it returns false', () => { + it('should return an empty array', async () => { + // Mock the shouldFetch function to return false + const shouldFetchMock = jest.fn().mockReturnValue(false) + + // Create the sales component with the shouldFetch function + salesComponent = createSalesComponent({ + subgraph: subgraphMock, + network: Network.ETHEREUM, + chainId: 1, + shouldFetch: shouldFetchMock, + }) + + // Define invalid filters that won't trigger a fetch + const invalidFilters = { + network: Network.ETHEREUM, + } + + // Call the fetch method with invalid filters + const result = await salesComponent.fetch(invalidFilters) + + // Expect the subgraph query not to be called + expect(subgraphMock.query).not.toHaveBeenCalled() + // Expect the result to be an empty array + expect(result).toEqual([]) + }) + }) + describe('and it returns true', () => { + it('should fetch the sales', async () => { + // Mock the shouldFetch function to return true + const shouldFetchMock = jest.fn().mockReturnValue(true) + + // Create the sales component with the shouldFetch function + salesComponent = createSalesComponent({ + subgraph: subgraphMock, + network: Network.ETHEREUM, + chainId: 1, + shouldFetch: shouldFetchMock, + }) + ;(subgraphMock.query as jest.Mock).mockResolvedValue({ + sales: mockSalesFragments, + }) + + const filters = { + contractAddress: '0x1234567890', + } + + const result = await salesComponent.fetch(filters) + + // Expect the subgraph query to be called with the correct arguments + expect(subgraphMock.query).toHaveBeenCalledWith( + expect.stringContaining('searchContractAddress') + ) + + // Expect the result to match the mocked sales + expect(result).toEqual( + mockSalesFragments.map((fragment) => + fromSaleFragment(fragment, Network.ETHEREUM, 1) + ) + ) + }) + }) + }) + + describe('and useLegacySchema is true', () => { + fit('should not include searchItemId field in the query', async () => { + // Create the sales component with the shouldFetch function + salesComponent = createSalesComponent({ + subgraph: subgraphMock, + network: Network.ETHEREUM, + chainId: 1, + useLegacySchema: true, + }) + ;(subgraphMock.query as jest.Mock).mockResolvedValue({ + sales: mockSalesFragments, + }) + + const filters = { + contractAddress: '0x1234567890', + } + + await salesComponent.fetch(filters) + // Expect the subgraph query to be called with the correct arguments + expect(subgraphMock.query).toHaveBeenCalledWith( + expect.not.stringContaining('searchItemId') + ) + }) + }) + + describe('and useLegacySchema is false', () => { + fit('should include searchItemId field in the query', async () => { + // Create the sales component with the shouldFetch function + salesComponent = createSalesComponent({ + subgraph: subgraphMock, + network: Network.ETHEREUM, + chainId: 1, + }) + ;(subgraphMock.query as jest.Mock).mockResolvedValue({ + sales: mockSalesFragments, + }) + + const filters = { + contractAddress: '0x1234567890', + } + + await salesComponent.fetch(filters) + // Expect the subgraph query to be called with the correct arguments + expect(subgraphMock.query).toHaveBeenCalledWith( + expect.stringContaining('searchItemId') + ) + }) + }) + + describe('and the request is valid and shouldFetch is not provided', () => { + it('should fetch the sales', async () => { + ;(subgraphMock.query as jest.Mock).mockResolvedValue({ + sales: mockSalesFragments, + }) + + const filters = { + contractAddress: '0x1234567890', + } + + const result = await salesComponent.fetch(filters) + // Expect the result to match the mocked sales + expect(result).toEqual( + mockSalesFragments.map((fragment) => + fromSaleFragment(fragment, Network.ETHEREUM, 1) + ) + ) + }) + }) +})