• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

decentraland / lamb2 / 18720247021

22 Oct 2025 02:50PM UTC coverage: 80.33% (-3.3%) from 83.615%
18720247021

push

github

web-flow
feat: consume new marketplace-api endpoints + support isSmartWearable filter (#419)

* feat: consume new marketplace-api endpoints

* feat: bump @dcl/catalyst-api-specs

* feat: refactor to use cache and more transparent implementation

* feat: update .env.default

* feat: fix tests

* feat: add MARKETPLACE_API_URL default

* feat: use base types

* fix: individual data

* feat: Add smart wearables filters to explorer-handler (#436)

* feat: add itemType support for elements fetching

* feat: add support for isSmartWearable parameter in explorer handler tests

* feat: enhance itemType handling by introducing ItemType type and updating related filters

---------

Co-authored-by: Andrés Morelos <6563162+AndresMorelos@users.noreply.github.com>

537 of 755 branches covered (71.13%)

Branch coverage included in aggregate %.

306 of 396 new or added lines in 24 files covered. (77.27%)

1 existing line in 1 file now uncovered.

1603 of 1909 relevant lines covered (83.97%)

36.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.44
/src/logic/fetch-elements/fetch-items.ts
1
import { EmoteCategory, WearableCategory } from '@dcl/schemas'
2
import { Item, OnChainEmote, OnChainWearable, Pagination } from '../../types'
3
import { MarketplaceApiParams } from '../../adapters/marketplace-api-fetcher'
4
import { ElementsFilters, ElementsFetcherDependencies, ItemType } from '../../adapters/elements-fetcher'
5

6
import { fetchNFTsPaginated, createItemQueryBuilder } from './graph-pagination'
19✔
7
import { compareByRarity } from '../sorting'
19✔
8
import { fetchWithMarketplaceFallback } from '../api-with-fallback'
19✔
9

10
export function buildMarketplaceApiParams(
19✔
11
  filters?: ElementsFilters,
12
  pagination?: Pick<Pagination, 'pageNum' | 'pageSize'>
13
): MarketplaceApiParams {
14
  const params: MarketplaceApiParams = {}
56✔
15

16
  // Pagination
17
  if (pagination) {
56✔
18
    params.limit = pagination.pageSize
56✔
19
    params.offset = (pagination.pageNum - 1) * pagination.pageSize
56✔
20
  }
21

22
  // Filtering
23
  if (filters?.category) {
56✔
24
    params.category = filters.category
4✔
25
  }
26
  if (filters?.rarity) {
56✔
27
    params.rarity = filters.rarity
2✔
28
  }
29
  if (filters?.name) {
56✔
30
    params.name = filters.name
3✔
31
  }
32

33
  // Sorting
34
  if (filters?.orderBy) {
56✔
35
    params.orderBy = filters.orderBy
6✔
36
  }
37
  if (filters?.direction) {
56✔
38
    params.direction = filters.direction
6✔
39
  }
40

41
  // Item type
42
  if (filters?.itemType) {
56!
NEW
43
    params.itemType = filters.itemType
×
44
  }
45

46
  return params
56✔
47
}
48

49
function groupItemsByURN<
50
  T extends WearableFromQuery | EmoteFromQuery,
51
  E extends WearableFromQuery['metadata']['wearable'] | EmoteFromQuery['metadata']['emote']
52
>(items: T[], getMetadata: (item: T) => E): Item<E['category']>[] {
53
  const itemsByURN = new Map<string, Item<E['category']>>()
32✔
54

55
  items.forEach((itemFromQuery) => {
32✔
56
    const individualData = {
73✔
57
      id: itemFromQuery.urn + ':' + itemFromQuery.tokenId,
58
      tokenId: itemFromQuery.tokenId,
59
      transferredAt: itemFromQuery.transferredAt,
60
      price: itemFromQuery.item.price
61
    }
62

63
    if (itemsByURN.has(itemFromQuery.urn)) {
73✔
64
      const itemFromMap = itemsByURN.get(itemFromQuery.urn)!
6✔
65
      itemFromMap.individualData.push(individualData)
6✔
66
      itemFromMap.amount = itemFromMap.amount + 1
6✔
67
      itemFromMap.minTransferredAt = Math.min(itemFromQuery.transferredAt, itemFromMap.minTransferredAt)
6✔
68
      itemFromMap.maxTransferredAt = Math.max(itemFromQuery.transferredAt, itemFromMap.maxTransferredAt)
6✔
69
    } else {
70
      itemsByURN.set(itemFromQuery.urn, {
67✔
71
        urn: itemFromQuery.urn,
72
        individualData: [individualData],
73
        rarity: itemFromQuery.item.rarity,
74
        amount: 1,
75
        name: getMetadata(itemFromQuery).name,
76
        category: getMetadata(itemFromQuery).category,
77
        minTransferredAt: itemFromQuery.transferredAt,
78
        maxTransferredAt: itemFromQuery.transferredAt
79
      })
80
    }
81
  })
82

83
  return Array.from(itemsByURN.values())
32✔
84
}
85

86
type ItemCategory = 'wearable' | 'emote'
87

88
type ItemFromQuery = {
89
  urn: string
90
  id: string
91
  tokenId: string
92
  transferredAt: number
93
  item: {
94
    rarity: string
95
    price: number
96
  }
97
  category: ItemCategory
98
}
99

100
export type WearableFromQuery = ItemFromQuery & {
101
  category: 'wearable'
102
  metadata: {
103
    wearable: {
104
      name: string
105
      category: WearableCategory
106
    }
107
  }
108
}
109

110
export type EmoteFromQuery = ItemFromQuery & {
111
  category: 'emote'
112
  metadata: {
113
    emote: {
114
      name: string
115
      category: EmoteCategory
116
    }
117
  }
118
}
119

120
export async function fetchEmotes(
19✔
121
  dependencies: ElementsFetcherDependencies,
122
  owner: string,
123
  pagination?: { pageSize: number; pageNum: number },
124
  filters?: ElementsFilters
125
): Promise<{ elements: OnChainEmote[]; totalAmount: number }> {
126
  const { marketplaceApiFetcher, theGraph, logs } = dependencies
32✔
127

128
  // Build marketplace API params from filters if available, otherwise just pagination
129
  const apiParams: MarketplaceApiParams | undefined =
130
    filters || pagination ? buildMarketplaceApiParams(filters, pagination) : undefined
32✔
131

132
  return fetchWithMarketplaceFallback(
32✔
133
    { marketplaceApiFetcher, theGraph, logs },
134
    'emotes',
135
    async () => {
136
      const { emotes, total } = await marketplaceApiFetcher!.fetchUserEmotes(owner, apiParams)
28✔
137
      const sortedEmotes = emotes.sort(compareByRarity)
24✔
138

139
      return {
24✔
140
        elements: sortedEmotes,
141
        totalAmount: total || sortedEmotes.length
30✔
142
      }
143
    },
144
    async () => {
145
      // TheGraph fallback implementation
146
      // There are no emotes on Ethereum, only on Polygon
147
      const emoteQueryBuilder = createItemQueryBuilder('emote')
8✔
148

149
      const maticResult = await fetchNFTsPaginated<EmoteFromQuery>(
8✔
150
        theGraph.maticCollectionsSubgraph,
151
        emoteQueryBuilder,
152
        owner,
153
        pagination,
154
        filters
155
      )
156

157
      const emotesGrouped = groupItemsByURN(maticResult.elements, (item) => item.metadata.emote)
14✔
158

159
      return {
7✔
160
        elements: emotesGrouped,
161
        totalAmount: maticResult.totalAmount
162
      }
163
    }
164
  )
165
}
166

167
export async function fetchWearables(
19✔
168
  dependencies: ElementsFetcherDependencies,
169
  owner: string,
170
  pagination?: { pageSize: number; pageNum: number },
171
  filters?: ElementsFilters
172
): Promise<{ elements: OnChainWearable[]; totalAmount: number }> {
173
  const { marketplaceApiFetcher, theGraph, logs } = dependencies
39✔
174

175
  // Build marketplace API params from filters if available, otherwise just pagination
176
  const apiParams: MarketplaceApiParams | undefined =
177
    filters || pagination ? buildMarketplaceApiParams(filters, pagination) : undefined
39✔
178

179
  return fetchWithMarketplaceFallback(
39✔
180
    { marketplaceApiFetcher, theGraph, logs },
181
    'wearables',
182
    async () => {
183
      const { wearables, total } = await marketplaceApiFetcher!.fetchUserWearables(owner, apiParams)
27✔
184
      const sortedWearables = wearables.sort(compareByRarity)
13✔
185

186
      return {
13✔
187
        elements: sortedWearables,
188
        totalAmount: total || sortedWearables.length
14✔
189
      }
190
    },
191
    async () => {
192
      // TheGraph fallback implementation
193
      const wearableQueryBuilder = createItemQueryBuilder((filters?.itemType || 'wearable') as ItemType)
26✔
194

195
      const [ethereumResult, maticResult] = await Promise.all([
26✔
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
        )
210
      ])
211

212
      const allWearables = [...ethereumResult.elements, ...maticResult.elements]
25✔
213
      const wearables = groupItemsByURN(allWearables, (item) => item.metadata.wearable)
120✔
214

215
      return {
25✔
216
        elements: wearables,
217
        totalAmount: ethereumResult.totalAmount + maticResult.totalAmount
218
      }
219
    }
220
  )
221
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc