Skip to content

Commit 45c5c18

Browse files
feat: add support for polygon wearables in explorer and marketplace APIs (#445)
* feat: add support for polygon wearables in explorer and marketplace APIs - Updated ItemType to include 'polygonWearables'. - Enhanced marketplace API fetcher to handle 'polygonWearables' item type, allowing for fetching both wearable_v2 and smart_wearable_v1. - Modified explorer handler to support isPolygonWearable filter, including amount in responses when requested. - Added tests to validate new functionality for polygon wearables and ensure correct behavior with existing filters. * feat: add explorer emotes handler and route - Introduced a new handler for fetching emotes in the explorer, supporting pagination and filtering. - Added a route to the router for accessing emotes by address. - Implemented tests to validate the functionality of the new emotes endpoint, including error handling and response structure. - Enhanced existing data generation utilities to support emote entities. * refactor: streamline itemType and network handling in fetchers and handlers - Removed 'polygonWearables' from ItemType and updated related logic to handle network-specific wearables. - Enhanced marketplace API fetcher to support network parameter for fetching wearables based on the selected network (Ethereum or Polygon). - Updated explorer handler to utilize network parameter instead of isPolygonWearable, simplifying the filters and response structure. - Refactored tests to validate new network-based filtering and ensure correct behavior across different scenarios. * refactor: update network handling to use enum values across fetchers and handlers - Replaced string literals for network parameters with the Network enum in elements-fetcher, marketplace-api-fetcher, explorer-handler, and related logic. - Adjusted tests to validate the new enum-based network handling, ensuring consistency and correctness in API responses. - Enhanced type safety and clarity in network-related code throughout the application. * refactor: validate network parameter in explorer handler and update tests - Enhanced the explorer handler to validate the network parameter, ensuring only valid Network enum values (ETHEREUM or MATIC) are accepted. - Updated integration tests to reflect changes in network handling, replacing instances of 'polygon' with 'MATIC' and ensuring consistency in test descriptions and expectations.
1 parent 1a34ad0 commit 45c5c18

File tree

10 files changed

+1513
-35
lines changed

10 files changed

+1513
-35
lines changed

src/adapters/elements-fetcher.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Network } from '@dcl/schemas'
12
import { AppComponents } from '../types'
23
import { IBaseComponent } from '@well-known-components/interfaces'
34
import { createLowerCaseKeysCache } from './lowercase-keys-cache'
@@ -50,6 +51,7 @@ export type ElementsFilters = {
5051
orderBy?: string
5152
direction?: string
5253
itemType?: ItemType
54+
network?: Network
5355
}
5456

5557
export type LegacyElementsFetcher<T> = IBaseComponent & {

src/adapters/marketplace-api-fetcher.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IBaseComponent } from '@well-known-components/interfaces'
2-
import { WearableCategory, EmoteCategory } from '@dcl/schemas'
2+
import { WearableCategory, EmoteCategory, Network } from '@dcl/schemas'
33
import { OnChainWearable, OnChainEmote, Name, AppComponents } from '../types'
44
import { ItemType } from './elements-fetcher'
55

@@ -84,6 +84,8 @@ export type MarketplaceApiParams = {
8484

8585
// Item type
8686
itemType?: ItemType
87+
// Network
88+
network?: Network
8789
}
8890

8991
/**
@@ -235,8 +237,22 @@ export async function createMarketplaceApiFetcher(
235237
if (params.direction) {
236238
queryParams.set('direction', params.direction)
237239
}
238-
if (params.itemType && params.itemType === 'smartWearable') {
239-
queryParams.set('itemType', 'smart_wearable_v1')
240+
241+
// Handle itemType and network parameters
242+
if (params.itemType) {
243+
if (params.itemType === 'smartWearable') {
244+
queryParams.append('itemType', 'smart_wearable_v1')
245+
} else if (params.itemType === 'wearable') {
246+
// Handle network-specific wearable types
247+
if (params.network === Network.MATIC) {
248+
// Polygon: wearable_v2 and smart_wearable_v1
249+
queryParams.append('itemType', 'wearable_v2')
250+
queryParams.append('itemType', 'smart_wearable_v1')
251+
} else if (params.network === Network.ETHEREUM) {
252+
// Ethereum: only wearable_v1
253+
queryParams.append('itemType', 'wearable_v1')
254+
}
255+
}
240256
}
241257

242258
const queryString = queryParams.toString()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Entity } from '@dcl/schemas'
2+
import { fetchAndPaginate, paginationObject } from '../../logic/pagination'
3+
import { createCombinedSorting } from '../../logic/sorting'
4+
import {
5+
AppComponents,
6+
HandlerContextWithPath,
7+
InvalidRequestError,
8+
OnChainEmote,
9+
PaginatedResponse
10+
} from '../../types'
11+
import { createFilters } from './items-commons'
12+
13+
export const ON_CHAIN = 'on-chain'
14+
15+
const VALID_COLLECTION_TYPES = ['on-chain']
16+
17+
export type MixedOnChainEmote = OnChainEmote & {
18+
type: typeof ON_CHAIN
19+
entity: Entity
20+
}
21+
22+
export type MixedEmote = MixedOnChainEmote
23+
24+
export type MixedEmoteResponse = Omit<MixedEmote, 'minTransferredAt' | 'maxTransferredAt'>
25+
26+
export type MixedEmoteTrimmedResponse = {
27+
entity: Entity
28+
amount?: number
29+
}
30+
31+
async function fetchCombinedElements(
32+
components: Pick<AppComponents, 'emotesFetcher' | 'entitiesFetcher'>,
33+
collectionTypes: string[],
34+
address: string
35+
): Promise<MixedEmote[]> {
36+
async function fetchOnChainEmotes(): Promise<MixedOnChainEmote[]> {
37+
const { elements } = await components.emotesFetcher.fetchOwnedElements(address)
38+
if (!elements.length) {
39+
return []
40+
}
41+
42+
const urns = elements.map((e) => e.urn)
43+
const entities = await components.entitiesFetcher.fetchEntities(urns)
44+
45+
return elements.reduce<MixedOnChainEmote[]>((acc, emote, i) => {
46+
const entity = entities[i]
47+
if (entity) {
48+
acc.push({
49+
type: ON_CHAIN,
50+
...emote,
51+
entity
52+
})
53+
}
54+
return acc
55+
}, [])
56+
}
57+
58+
const emotes = collectionTypes.includes(ON_CHAIN) ? await fetchOnChainEmotes() : []
59+
60+
return emotes
61+
}
62+
63+
export async function explorerEmotesHandler(
64+
context: HandlerContextWithPath<'emotesFetcher' | 'entitiesFetcher', '/explorer/:address/emotes'>
65+
): Promise<PaginatedResponse<MixedEmoteResponse> | PaginatedResponse<MixedEmoteTrimmedResponse>> {
66+
const { address } = context.params
67+
const pagination = paginationObject(context.url)
68+
const filter = createFilters(context.url)
69+
const sorting = createCombinedSorting(context.url)
70+
const collectionTypes = context.url.searchParams.has('collectionType')
71+
? context.url.searchParams.getAll('collectionType')
72+
: VALID_COLLECTION_TYPES
73+
const trimmedParam = context.url.searchParams.get('trimmed')
74+
const isTrimmed = trimmedParam === 'true' || trimmedParam === '1'
75+
const includeAmountParam = context.url.searchParams.get('includeAmount')
76+
const includeAmount = includeAmountParam === 'true' || includeAmountParam === '1'
77+
78+
if (collectionTypes.some((type) => !VALID_COLLECTION_TYPES.includes(type))) {
79+
throw new InvalidRequestError(`Invalid collection type. Valid types are: ${VALID_COLLECTION_TYPES.join(', ')}.`)
80+
}
81+
82+
const page = await fetchAndPaginate<MixedEmote>(
83+
() => fetchCombinedElements(context.components, collectionTypes, address),
84+
pagination,
85+
filter,
86+
sorting
87+
)
88+
89+
if (isTrimmed) {
90+
const results: MixedEmoteTrimmedResponse[] = page.elements.map((emote) => {
91+
const result: MixedEmoteTrimmedResponse = {
92+
entity: emote.entity
93+
}
94+
if (includeAmount) {
95+
result.amount = emote.individualData?.length || 0
96+
}
97+
return result
98+
})
99+
100+
return {
101+
status: 200,
102+
body: {
103+
...page,
104+
elements: results
105+
}
106+
}
107+
} else {
108+
const results: MixedEmoteResponse[] = []
109+
for (const emote of page.elements) {
110+
const { minTransferredAt, maxTransferredAt, ...clean } = emote
111+
results.push({ ...clean })
112+
}
113+
114+
return {
115+
status: 200,
116+
body: {
117+
...page,
118+
elements: results
119+
}
120+
}
121+
}
122+
}

src/controllers/handlers/explorer-handler.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Entity } from '@dcl/schemas'
1+
import { Entity, Network } from '@dcl/schemas'
22
import { fetchThirdPartyWearablesFromThirdPartyName } from '../../logic/fetch-elements/fetch-third-party-wearables'
33
import { fetchAndPaginate, paginationObject } from '../../logic/pagination'
44
import { createCombinedSorting } from '../../logic/sorting'
@@ -41,10 +41,12 @@ export type MixedWearableResponse = Omit<MixedWearable, 'minTransferredAt' | 'ma
4141

4242
export type MixedWearableTrimmedResponse = {
4343
entity: ExplorerWearableEntity
44+
amount?: number
4445
}
4546

4647
export type WearableFilters = {
4748
isSmartWearable: boolean
49+
network?: Network
4850
}
4951

5052
async function fetchCombinedElements(
@@ -85,9 +87,17 @@ async function fetchCombinedElements(
8587
}
8688

8789
async function fetchOnChainWearables(): Promise<MixedOnChainWearable[]> {
90+
let itemType: 'wearable' | 'smartWearable' = 'wearable'
91+
92+
if (filters.isSmartWearable) {
93+
itemType = 'smartWearable'
94+
}
95+
8896
const { elements } = await components.wearablesFetcher.fetchOwnedElements(address, undefined, {
89-
itemType: filters.isSmartWearable ? 'smartWearable' : 'wearable'
97+
itemType,
98+
network: filters.network
9099
})
100+
91101
if (!elements.length) {
92102
return []
93103
}
@@ -148,9 +158,13 @@ async function fetchCombinedElements(
148158
}
149159

150160
const [baseItems, nftItems, thirdPartyItems] = await Promise.all([
151-
filters.isSmartWearable ? [] : collectionTypes.includes(BASE_WEARABLE) ? fetchBaseWearables() : [],
161+
filters.isSmartWearable || filters.network
162+
? []
163+
: collectionTypes.includes(BASE_WEARABLE)
164+
? fetchBaseWearables()
165+
: [],
152166
collectionTypes.includes(ON_CHAIN) ? fetchOnChainWearables() : [],
153-
filters.isSmartWearable
167+
filters.isSmartWearable || filters.network
154168
? []
155169
: collectionTypes.includes(THIRD_PARTY)
156170
? fetchThirdPartyWearables(thirdPartyCollectionId)
@@ -185,23 +199,37 @@ export async function explorerHandler(
185199
const isTrimmed = trimmedParam === 'true' || trimmedParam === '1'
186200
const isSmartWearableParam = context.url.searchParams.get('isSmartWearable')
187201
const isSmartWearable = isSmartWearableParam === 'true' || isSmartWearableParam === '1'
202+
const networkParam = context.url.searchParams.get('network')
203+
// Validate network parameter - only allow valid Network enum values (ETHEREUM or MATIC)
204+
const network = networkParam === Network.ETHEREUM || networkParam === Network.MATIC ? networkParam : undefined
205+
const includeAmountParam = context.url.searchParams.get('includeAmount')
206+
const includeAmount = includeAmountParam === 'true' || includeAmountParam === '1'
188207

189208
if (collectionTypes.some((type) => !VALID_COLLECTION_TYPES.includes(type))) {
190209
throw new InvalidRequestError(`Invalid collection type. Valid types are: ${VALID_COLLECTION_TYPES.join(', ')}.`)
191210
}
192211

193212
const page = await fetchAndPaginate<MixedWearable>(
194213
() =>
195-
fetchCombinedElements(context.components, collectionTypes, thirdPartyCollectionIds, address, { isSmartWearable }),
214+
fetchCombinedElements(context.components, collectionTypes, thirdPartyCollectionIds, address, {
215+
isSmartWearable,
216+
network
217+
}),
196218
pagination,
197219
filter,
198220
sorting
199221
)
200222

201223
if (isTrimmed) {
202-
const results: MixedWearableTrimmedResponse[] = page.elements.map((wearable) => ({
203-
entity: buildTrimmedEntity(wearable.entity)
204-
}))
224+
const results: MixedWearableTrimmedResponse[] = page.elements.map((wearable) => {
225+
const result: MixedWearableTrimmedResponse = {
226+
entity: buildTrimmedEntity(wearable.entity)
227+
}
228+
if (includeAmount) {
229+
result.amount = wearable.individualData?.length || 0
230+
}
231+
return result
232+
})
205233

206234
return {
207235
status: 200,

src/controllers/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from './handlers/third-party-wearables-handler'
1414
import { wearablesHandler } from './handlers/wearables-handler'
1515
import { explorerHandler } from './handlers/explorer-handler'
16+
import { explorerEmotesHandler } from './handlers/explorer-emotes-handler'
1617
import { errorHandler } from './handlers/errorHandler'
1718
import { aboutHandler } from './handlers/about-handler'
1819
import { outfitsHandler } from './handlers/outfits-handler'
@@ -49,6 +50,7 @@ export async function setupRouter(_: GlobalContext): Promise<Router<GlobalContex
4950
router.get('/contracts/pois', getPOIsHandler)
5051
router.get('/contracts/denylisted-names', getNameDenylistHandler)
5152
router.get('/explorer/:address/wearables', explorerHandler)
53+
router.get('/explorer/:address/emotes', explorerEmotesHandler)
5254
router.get('/parcels/:x/:y/operators', parcelOperatorsHandler)
5355

5456
return router

src/logic/fetch-elements/fetch-items.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EmoteCategory, WearableCategory } from '@dcl/schemas'
1+
import { EmoteCategory, WearableCategory, Network } from '@dcl/schemas'
22
import { Item, OnChainEmote, OnChainWearable, Pagination } from '../../types'
33
import { MarketplaceApiParams } from '../../adapters/marketplace-api-fetcher'
44
import { ElementsFilters, ElementsFetcherDependencies, ItemType } from '../../adapters/elements-fetcher'
@@ -43,6 +43,11 @@ export function buildMarketplaceApiParams(
4343
params.itemType = filters.itemType
4444
}
4545

46+
// Network
47+
if (filters?.network) {
48+
params.network = filters.network
49+
}
50+
4651
return params
4752
}
4853

@@ -190,23 +195,34 @@ export async function fetchWearables(
190195
},
191196
async () => {
192197
// TheGraph fallback implementation
193-
const wearableQueryBuilder = createItemQueryBuilder((filters?.itemType || 'wearable') as ItemType)
198+
const itemType = (filters?.itemType || 'wearable') as ItemType
199+
const network = filters?.network
200+
201+
// Determine which subgraphs to query based on network filter
202+
const shouldQueryEthereum = !network || network === Network.ETHEREUM
203+
const shouldQueryMatic = !network || network === Network.MATIC
204+
205+
const wearableQueryBuilder = createItemQueryBuilder(itemType, network)
194206

195207
const [ethereumResult, maticResult] = await Promise.all([
196-
fetchNFTsPaginated<WearableFromQuery>(
197-
theGraph.ethereumCollectionsSubgraph,
198-
wearableQueryBuilder,
199-
owner,
200-
pagination,
201-
filters
202-
),
203-
fetchNFTsPaginated<WearableFromQuery>(
204-
theGraph.maticCollectionsSubgraph,
205-
wearableQueryBuilder,
206-
owner,
207-
pagination,
208-
filters
209-
)
208+
shouldQueryEthereum
209+
? fetchNFTsPaginated<WearableFromQuery>(
210+
theGraph.ethereumCollectionsSubgraph,
211+
wearableQueryBuilder,
212+
owner,
213+
pagination,
214+
filters
215+
)
216+
: Promise.resolve({ elements: [], totalAmount: 0 }),
217+
shouldQueryMatic
218+
? fetchNFTsPaginated<WearableFromQuery>(
219+
theGraph.maticCollectionsSubgraph,
220+
wearableQueryBuilder,
221+
owner,
222+
pagination,
223+
filters
224+
)
225+
: Promise.resolve({ elements: [], totalAmount: 0 })
210226
])
211227

212228
const allWearables = [...ethereumResult.elements, ...maticResult.elements]

src/logic/fetch-elements/graph-pagination.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ISubgraphComponent } from '@well-known-components/thegraph-component'
2+
import { Network } from '@dcl/schemas'
23
import { ElementsFilters, ItemType } from '../../adapters/elements-fetcher'
34

45
interface NFT {
@@ -188,15 +189,24 @@ async function fetchAllNFTsUpTo<E extends NFT>(
188189
/**
189190
* Creates a query builder for items (wearables/emotes)
190191
*/
191-
export function createItemQueryBuilder(category: ItemType) {
192+
export function createItemQueryBuilder(category: ItemType, network?: Network) {
192193
let itemTypeFilter: string
193194

194195
if (category === 'smartWearable') {
195196
itemTypeFilter = `itemType: smart_wearable_v1`
196197
} else if (category === 'emote') {
197198
itemTypeFilter = `itemType: emote_v1`
198199
} else if (category === 'wearable') {
199-
itemTypeFilter = `itemType_in: [wearable_v1, wearable_v2, smart_wearable_v1]`
200+
if (network === Network.MATIC) {
201+
// Polygon wearables: only wearable_v2 and smart_wearable_v1
202+
itemTypeFilter = `itemType_in: [wearable_v2, smart_wearable_v1]`
203+
} else if (network === Network.ETHEREUM) {
204+
// Ethereum wearables: only wearable_v1
205+
itemTypeFilter = `itemType: wearable_v1`
206+
} else {
207+
// No network filter: all wearable types
208+
itemTypeFilter = `itemType_in: [wearable_v1, wearable_v2, smart_wearable_v1]`
209+
}
200210
}
201211

202212
return (filters: string, orderBy: string, orderDirection: string, first: number) => `

0 commit comments

Comments
 (0)