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

decentraland / lamb2 / 19104127051

05 Nov 2025 01:48PM UTC coverage: 80.237% (-0.09%) from 80.33%
19104127051

Pull #445

github

web-flow
Merge branch 'main' into feat/gifting-changes
Pull Request #445: feat: add support for polygon wearables in explorer and marketplace APIs

567 of 800 branches covered (70.88%)

Branch coverage included in aggregate %.

68 of 81 new or added lines in 6 files covered. (83.95%)

16 existing lines in 1 file now uncovered.

1666 of 1983 relevant lines covered (84.01%)

39.95 hits per line

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

91.4
/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'
20✔
7
import { compareByRarity } from '../sorting'
20✔
8
import { fetchWithMarketplaceFallback } from '../api-with-fallback'
20✔
9

10
export function buildMarketplaceApiParams(
20✔
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!
43
    params.itemType = filters.itemType
×
44
  }
45

46
  // Network
47
  if (filters?.network) {
56!
NEW
48
    params.network = filters.network
×
49
  }
50

51
  return params
56✔
52
}
53

54
function groupItemsByURN<
55
  T extends WearableFromQuery | EmoteFromQuery,
56
  E extends WearableFromQuery['metadata']['wearable'] | EmoteFromQuery['metadata']['emote']
57
>(items: T[], getMetadata: (item: T) => E): Item<E['category']>[] {
58
  const itemsByURN = new Map<string, Item<E['category']>>()
32✔
59

60
  items.forEach((itemFromQuery) => {
32✔
61
    const individualData = {
73✔
62
      id: itemFromQuery.urn + ':' + itemFromQuery.tokenId,
63
      tokenId: itemFromQuery.tokenId,
64
      transferredAt: itemFromQuery.transferredAt,
65
      price: itemFromQuery.item.price
66
    }
67

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

88
  return Array.from(itemsByURN.values())
32✔
89
}
90

91
type ItemCategory = 'wearable' | 'emote'
92

93
type ItemFromQuery = {
94
  urn: string
95
  id: string
96
  tokenId: string
97
  transferredAt: number
98
  item: {
99
    rarity: string
100
    price: number
101
  }
102
  category: ItemCategory
103
}
104

105
export type WearableFromQuery = ItemFromQuery & {
106
  category: 'wearable'
107
  metadata: {
108
    wearable: {
109
      name: string
110
      category: WearableCategory
111
    }
112
  }
113
}
114

115
export type EmoteFromQuery = ItemFromQuery & {
116
  category: 'emote'
117
  metadata: {
118
    emote: {
119
      name: string
120
      category: EmoteCategory
121
    }
122
  }
123
}
124

125
export async function fetchEmotes(
20✔
126
  dependencies: ElementsFetcherDependencies,
127
  owner: string,
128
  pagination?: { pageSize: number; pageNum: number },
129
  filters?: ElementsFilters
130
): Promise<{ elements: OnChainEmote[]; totalAmount: number }> {
131
  const { marketplaceApiFetcher, theGraph, logs } = dependencies
32✔
132

133
  // Build marketplace API params from filters if available, otherwise just pagination
134
  const apiParams: MarketplaceApiParams | undefined =
135
    filters || pagination ? buildMarketplaceApiParams(filters, pagination) : undefined
32✔
136

137
  return fetchWithMarketplaceFallback(
32✔
138
    { marketplaceApiFetcher, theGraph, logs },
139
    'emotes',
140
    async () => {
141
      const { emotes, total } = await marketplaceApiFetcher!.fetchUserEmotes(owner, apiParams)
28✔
142
      const sortedEmotes = emotes.sort(compareByRarity)
24✔
143

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

154
      const maticResult = await fetchNFTsPaginated<EmoteFromQuery>(
8✔
155
        theGraph.maticCollectionsSubgraph,
156
        emoteQueryBuilder,
157
        owner,
158
        pagination,
159
        filters
160
      )
161

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

164
      return {
7✔
165
        elements: emotesGrouped,
166
        totalAmount: maticResult.totalAmount
167
      }
168
    }
169
  )
170
}
171

172
export async function fetchWearables(
20✔
173
  dependencies: ElementsFetcherDependencies,
174
  owner: string,
175
  pagination?: { pageSize: number; pageNum: number },
176
  filters?: ElementsFilters
177
): Promise<{ elements: OnChainWearable[]; totalAmount: number }> {
178
  const { marketplaceApiFetcher, theGraph, logs } = dependencies
39✔
179

180
  // Build marketplace API params from filters if available, otherwise just pagination
181
  const apiParams: MarketplaceApiParams | undefined =
182
    filters || pagination ? buildMarketplaceApiParams(filters, pagination) : undefined
39✔
183

184
  return fetchWithMarketplaceFallback(
39✔
185
    { marketplaceApiFetcher, theGraph, logs },
186
    'wearables',
187
    async () => {
188
      const { wearables, total } = await marketplaceApiFetcher!.fetchUserWearables(owner, apiParams)
27✔
189
      const sortedWearables = wearables.sort(compareByRarity)
13✔
190

191
      return {
13✔
192
        elements: sortedWearables,
193
        totalAmount: total || sortedWearables.length
14✔
194
      }
195
    },
196
    async () => {
197
      // TheGraph fallback implementation
198
      const itemType = (filters?.itemType || 'wearable') as ItemType
26✔
199
      const network = filters?.network
26✔
200

201
      // Determine which subgraphs to query based on network filter
202
      const shouldQueryEthereum = !network || network === 'ethereum'
26!
203
      const shouldQueryMatic = !network || network === 'polygon'
26!
204

205
      const wearableQueryBuilder = createItemQueryBuilder(itemType, network)
26✔
206

207
      const [ethereumResult, maticResult] = await Promise.all([
26✔
208
        shouldQueryEthereum
26!
209
          ? fetchNFTsPaginated<WearableFromQuery>(
210
              theGraph.ethereumCollectionsSubgraph,
211
              wearableQueryBuilder,
212
              owner,
213
              pagination,
214
              filters
215
            )
216
          : Promise.resolve({ elements: [], totalAmount: 0 }),
217
        shouldQueryMatic
26!
218
          ? fetchNFTsPaginated<WearableFromQuery>(
219
              theGraph.maticCollectionsSubgraph,
220
              wearableQueryBuilder,
221
              owner,
222
              pagination,
223
              filters
224
            )
225
          : Promise.resolve({ elements: [], totalAmount: 0 })
226
      ])
227

228
      const allWearables = [...ethereumResult.elements, ...maticResult.elements]
25✔
229
      const wearables = groupItemsByURN(allWearables, (item) => item.metadata.wearable)
120✔
230

231
      return {
25✔
232
        elements: wearables,
233
        totalAmount: ethereumResult.totalAmount + maticResult.totalAmount
234
      }
235
    }
236
  )
237
}
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