Skip to content

Commit

Permalink
fix: fix the sales endpoint for L1 collections (#324)
Browse files Browse the repository at this point in the history
* fix: fix the sales endpoint for L1 collections

* refactor: use useLegacySchema to indicate whether to use the searchItemId field or not
  • Loading branch information
juanmahidalgo authored Jul 18, 2023
1 parent b8154ac commit 15be9e1
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 11 deletions.
20 changes: 19 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -496,6 +499,14 @@ async function initComponents(): Promise<AppComponents> {
subgraph: marketplaceSubgraph,
network: Network.ETHEREUM,
chainId: marketplaceChainId,
useLegacySchema: true,
shouldFetch: (filters: SaleFilters) => {
return (
!filters.contractAddress ||
(!!filters.contractAddress &&
!getIsEthereumCollection(filters.contractAddress, marketplaceChainId))
)
},
})

const collectionsSales = createSalesComponent({
Expand All @@ -504,10 +515,17 @@ async function initComponents(): Promise<AppComponents> {
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<Sale, SaleFilters, SaleSortBy>({
sources: [
createSalesSource(marketplaceSales),
createSalesSource(collectionsSales),
createSalesSource(collectionsEthereumSales),
],
defaultSortBy: SALE_DEFAULT_SORT_BY,
directions: {
Expand Down
14 changes: 14 additions & 0 deletions src/ports/collections/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CollectionSortBy,
Network,
} from '@dcl/schemas'
import { getMarketplaceContracts } from '../../logic/contracts'
import { SortDirection } from '../merger/types'
import { CollectionFragment } from './types'

Expand Down Expand Up @@ -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
}
18 changes: 13 additions & 5 deletions src/ports/sales/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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)
Expand All @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions src/ports/sales/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,7 +35,7 @@ export const getSaleFragment = (network: Network) => `
price
timestamp
txHash
${network === Network.MATIC ? 'searchItemId' : ''}
${useLegacySchema ? '' : 'searchItemId'}
searchTokenId
searchContractAddress
}
Expand All @@ -44,7 +44,7 @@ export const getSaleFragment = (network: Network) => `
export function getSalesQuery(
filters: SaleFilters,
isCount = false,
network: Network
useLegacySchema: boolean
) {
const {
first,
Expand All @@ -69,7 +69,7 @@ export function getSalesQuery(
where.push(`searchTokenId: "${tokenId}"`)
}

if (itemId && network === Network.MATIC) {
if (itemId && !useLegacySchema) {
where.push(`searchItemId: "${itemId}"`)
}

Expand Down Expand Up @@ -149,6 +149,6 @@ export function getSalesQuery(
})
{ ${isCount ? 'id' : `...saleFragment`} }
}
${isCount ? '' : getSaleFragment(network)}
${isCount ? '' : getSaleFragment(useLegacySchema)}
`
}
223 changes: 223 additions & 0 deletions src/tests/ports/sales.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
)
)
})
})
})

0 comments on commit 15be9e1

Please sign in to comment.