@lukso/transaction-decoder 1.0.1-dev.0f1bea5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +486 -0
  3. package/dist/browser.cjs +6912 -0
  4. package/dist/browser.cjs.map +1 -0
  5. package/dist/browser.d.cts +6 -0
  6. package/dist/browser.d.ts +6 -0
  7. package/dist/browser.js +131 -0
  8. package/dist/browser.js.map +1 -0
  9. package/dist/cdn/transaction-decoder.global.js +296 -0
  10. package/dist/cdn/transaction-decoder.global.js.map +1 -0
  11. package/dist/chunk-GGBHTWJL.js +437 -0
  12. package/dist/chunk-GGBHTWJL.js.map +1 -0
  13. package/dist/chunk-GXZOF3QY.js +839 -0
  14. package/dist/chunk-GXZOF3QY.js.map +1 -0
  15. package/dist/chunk-LJ6ES5XF.js +776 -0
  16. package/dist/chunk-LJ6ES5XF.js.map +1 -0
  17. package/dist/chunk-XVHJWV5U.js +4925 -0
  18. package/dist/chunk-XVHJWV5U.js.map +1 -0
  19. package/dist/data.cjs +5518 -0
  20. package/dist/data.cjs.map +1 -0
  21. package/dist/data.d.cts +43 -0
  22. package/dist/data.d.ts +43 -0
  23. package/dist/data.js +55 -0
  24. package/dist/data.js.map +1 -0
  25. package/dist/index-BzXh7poJ.d.cts +524 -0
  26. package/dist/index-BzXh7poJ.d.ts +524 -0
  27. package/dist/index.cjs +6912 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +756 -0
  30. package/dist/index.d.ts +756 -0
  31. package/dist/index.js +131 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/server.cjs +5644 -0
  34. package/dist/server.cjs.map +1 -0
  35. package/dist/server.d.cts +217 -0
  36. package/dist/server.d.ts +217 -0
  37. package/dist/server.js +644 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/utils-CBAkjQh3.d.cts +108 -0
  40. package/dist/utils-xT9-km0r.d.ts +108 -0
  41. package/package.json +101 -0
  42. package/src/browser.ts +13 -0
  43. package/src/client/resolveAddresses.ts +157 -0
  44. package/src/core/addressCollector.ts +153 -0
  45. package/src/core/addressResolver.ts +135 -0
  46. package/src/core/dataModel.ts +888 -0
  47. package/src/core/instance.ts +33 -0
  48. package/src/core/integrateDecoder.ts +325 -0
  49. package/src/data.ts +70 -0
  50. package/src/decoder/GENERATOR_PROPOSAL.md +182 -0
  51. package/src/decoder/THREE_PHASE_EXAMPLE.md +108 -0
  52. package/src/decoder/aggregation.ts +218 -0
  53. package/src/decoder/browserCache.ts +237 -0
  54. package/src/decoder/cache/README.md +126 -0
  55. package/src/decoder/cache/index.ts +44 -0
  56. package/src/decoder/cache.ts +139 -0
  57. package/src/decoder/constants.ts +125 -0
  58. package/src/decoder/decodeTransaction.ts +292 -0
  59. package/src/decoder/errors.ts +95 -0
  60. package/src/decoder/events.ts +192 -0
  61. package/src/decoder/functionSignature.ts +344 -0
  62. package/src/decoder/getDataFromExternalSources.ts +248 -0
  63. package/src/decoder/graphqlWS.ts +22 -0
  64. package/src/decoder/interfaces.ts +185 -0
  65. package/src/decoder/keyValue.ts +5 -0
  66. package/src/decoder/kvCache.ts +241 -0
  67. package/src/decoder/lruCache.ts +184 -0
  68. package/src/decoder/lsp7Mint.test.ts +179 -0
  69. package/src/decoder/lsp7TransferBatch.test.ts +105 -0
  70. package/src/decoder/plugins/RegistryAbi.ts +562 -0
  71. package/src/decoder/plugins/enhanceBurntPix.ts +132 -0
  72. package/src/decoder/plugins/enhanceGraffiti.ts +70 -0
  73. package/src/decoder/plugins/enhanceLSP0ERC725Account.ts +179 -0
  74. package/src/decoder/plugins/enhanceLSP26FollowerSystem.ts +88 -0
  75. package/src/decoder/plugins/enhanceLSP6KeyManager.ts +231 -0
  76. package/src/decoder/plugins/enhanceLSP7DigitalAsset.ts +165 -0
  77. package/src/decoder/plugins/enhanceLSP8IdentifiableDigitalAsset.ts +170 -0
  78. package/src/decoder/plugins/enhanceLSP9Vault.ts +57 -0
  79. package/src/decoder/plugins/enhanceRetrieveAbi.ts +85 -0
  80. package/src/decoder/plugins/enhanceSetData.ts +135 -0
  81. package/src/decoder/plugins/index.ts +99 -0
  82. package/src/decoder/plugins/schemaDefault.ts +318 -0
  83. package/src/decoder/plugins/standardPlugin.ts +202 -0
  84. package/src/decoder/registry.ts +322 -0
  85. package/src/decoder/singleGQL.ts +293 -0
  86. package/src/decoder/transaction.ts +198 -0
  87. package/src/decoder/types.ts +465 -0
  88. package/src/decoder/utils.ts +212 -0
  89. package/src/example/usage.ts +172 -0
  90. package/src/index.ts +174 -0
  91. package/src/server/addressResolver.ts +68 -0
  92. package/src/server/caches.ts +209 -0
  93. package/src/server/decodeTransactionSync.ts +156 -0
  94. package/src/server/decodeTransactionsBatch.ts +207 -0
  95. package/src/server/finishDecoding.ts +116 -0
  96. package/src/server/index.ts +81 -0
  97. package/src/server/lsp23Resolver.test.ts +46 -0
  98. package/src/server/lsp23Resolver.ts +419 -0
  99. package/src/server/types.ts +168 -0
  100. package/src/server.ts +22 -0
  101. package/src/shared/addressResolver.ts +651 -0
  102. package/src/shared/cache.ts +144 -0
  103. package/src/shared/constants.ts +21 -0
  104. package/src/stubs/tty.ts +13 -0
  105. package/src/stubs/util.ts +42 -0
  106. package/src/types/index.ts +154 -0
  107. package/src/types/provider.ts +46 -0
  108. package/src/umd.ts +13 -0
  109. package/src/utils/debug.ts +49 -0
  110. package/src/utils/json-bigint.ts +47 -0
@@ -0,0 +1,651 @@
1
+ import request, { gql } from 'graphql-request'
2
+ import type { Address, Chain, Hex } from 'viem'
3
+ import { lukso } from 'viem/chains'
4
+ import type { DataKey, EnhancedInfo } from '../types'
5
+ import type { AddressIdentityCache } from './cache'
6
+
7
+ /**
8
+ * GraphQL queries for address resolution
9
+ * Using the exact schema from addressGQL.ts
10
+ */
11
+ const addressesGql = gql`
12
+ query AddressQuery($profiles: [String!], $assets: [String!], $tokens: [String!]) {
13
+ Profile(
14
+ where: {id: {_in: $profiles}}
15
+ ) {
16
+ fullName
17
+ id
18
+ name
19
+ tags
20
+ description
21
+ standard
22
+ controllers {
23
+ address
24
+ tags
25
+ permissions
26
+ }
27
+ owner {
28
+ id
29
+ }
30
+ profileImages(where: {error: {_is_null: true}}, order_by: {width: asc}) {
31
+ width
32
+ height
33
+ src
34
+ verified
35
+ }
36
+ backgroundImages(where: {error: {_is_null: true}}, order_by: {width: asc}) {
37
+ width
38
+ height
39
+ src
40
+ verified
41
+ }
42
+ avatars(where: {error: {_is_null: true}}, order_by: {width: asc}) {
43
+ width
44
+ height
45
+ src
46
+ verified
47
+ }
48
+ }
49
+ Token(where: {id: {_in: $tokens}}) {
50
+ id
51
+ baseAsset {
52
+ id
53
+ description
54
+ icons {
55
+ src
56
+ verified
57
+ width
58
+ height
59
+ }
60
+ isCollection
61
+ isLSP7
62
+ isUnknown
63
+ name
64
+ standard
65
+ interfaces
66
+ decimals
67
+ owner_id
68
+ lsp4Creators {
69
+ profile_id
70
+ }
71
+ }
72
+ formattedTokenId
73
+ icons {
74
+ src
75
+ verified
76
+ width
77
+ height
78
+ }
79
+ images(where: {index: {_eq: 0}}) {
80
+ src
81
+ verified
82
+ width
83
+ height
84
+ }
85
+ lsp4TokenName
86
+ lsp4TokenSymbol
87
+ lsp4TokenType
88
+ lsp8TokenIdFormat
89
+ tokenId
90
+ name
91
+ description
92
+ lsp4Creators {
93
+ profile_id
94
+ }
95
+ }
96
+ Asset(where: {id: {_in: $assets}}) {
97
+ id
98
+ images(where: {index: {_eq: 0}}) {
99
+ src
100
+ width
101
+ verified
102
+ height
103
+ }
104
+ interfaces
105
+ isCollection
106
+ isLSP7
107
+ isUnknown
108
+ lsp4TokenName
109
+ lsp4TokenSymbol
110
+ lsp4TokenType
111
+ method
112
+ name
113
+ icons {
114
+ src
115
+ width
116
+ height
117
+ }
118
+ description
119
+ decimals
120
+ standard
121
+ owner_id
122
+ lsp4Creators {
123
+ profile_id
124
+ }
125
+ }
126
+ }
127
+ `
128
+
129
+ // Image data structure from GraphQL
130
+ export interface ImageData {
131
+ width?: number
132
+ height?: number
133
+ url?: string // Raw URL (may be ipfs://)
134
+ src?: string // Resolved HTTPS URL (fallback to url if not present)
135
+ verified?: boolean
136
+ }
137
+
138
+ // Link data structure from GraphQL
139
+ export interface LinkData {
140
+ url: string
141
+ title?: string
142
+ }
143
+
144
+ // Profile data from GraphQL
145
+ export interface ProfileData {
146
+ id: Hex
147
+ name?: string
148
+ fullName?: string
149
+ tags?: string[]
150
+ description?: string
151
+ links?: LinkData[]
152
+ standard?: string
153
+ controllers?: Array<{
154
+ address: string
155
+ tags?: string[]
156
+ permissions?: Hex
157
+ }>
158
+ owner?: { id: string }
159
+ profileImages?: ImageData[]
160
+ backgroundImages?: ImageData[]
161
+ avatars?: ImageData[]
162
+ }
163
+
164
+ // Asset data from GraphQL
165
+ export interface AssetData {
166
+ id: Hex
167
+ name?: string
168
+ standard?: string
169
+ lsp4TokenName?: string
170
+ lsp4TokenSymbol?: string
171
+ lsp4TokenType?: string
172
+ method?: string
173
+ description?: string
174
+ links?: LinkData[]
175
+ decimals?: number
176
+ interfaces?: string[]
177
+ isCollection?: boolean
178
+ isLSP7?: boolean
179
+ isUnknown?: boolean
180
+ icons?: ImageData[]
181
+ images?: ImageData[]
182
+ }
183
+
184
+ // Token data from GraphQL
185
+ export interface TokenData {
186
+ id: Hex
187
+ tokenId?: Hex
188
+ name?: string
189
+ description?: string
190
+ links?: LinkData[]
191
+ lsp4TokenName?: string
192
+ lsp4TokenSymbol?: string
193
+ lsp4TokenType?: string
194
+ lsp8TokenIdFormat?: string
195
+ formattedTokenId?: string
196
+ baseAsset?: {
197
+ id: Hex
198
+ description?: string
199
+ icons?: ImageData[]
200
+ isCollection?: boolean
201
+ isLSP7?: boolean
202
+ isUnknown?: boolean
203
+ name?: string
204
+ standard?: string
205
+ interfaces?: string[]
206
+ decimals?: number
207
+ }
208
+ icons?: ImageData[]
209
+ images?: ImageData[]
210
+ }
211
+
212
+ export type TFetchAddressData = {
213
+ Profile?: ProfileData[]
214
+ Asset?: AssetData[]
215
+ Token?: TokenData[]
216
+ }
217
+
218
+ /**
219
+ * Fetch multiple addresses from GraphQL and return them as a Map
220
+ * This is the shared implementation that both server and core can use
221
+ */
222
+ export async function fetchMultipleAddresses(
223
+ addresses: readonly DataKey[],
224
+ graphqlEndpoint: string,
225
+ cache?: AddressIdentityCache
226
+ ): Promise<Map<DataKey, EnhancedInfo>> {
227
+ const results = new Map<DataKey, EnhancedInfo>()
228
+
229
+ if (addresses.length === 0) {
230
+ return results
231
+ }
232
+
233
+ // Check cache first if provided
234
+ const uncachedAddresses: DataKey[] = []
235
+
236
+ if (cache) {
237
+ // Try to get many at once if supported
238
+ if (cache.getMany) {
239
+ const cachedResults = await cache.getMany(addresses)
240
+ for (const [key, value] of cachedResults) {
241
+ results.set(key, value)
242
+ }
243
+ // Find which ones we still need to fetch
244
+ for (const addr of addresses) {
245
+ if (!results.has(addr)) {
246
+ uncachedAddresses.push(addr)
247
+ }
248
+ }
249
+ } else {
250
+ // Fall back to individual lookups
251
+ for (const addr of addresses) {
252
+ const cached = await cache.get(addr)
253
+ if (cached) {
254
+ results.set(addr, cached)
255
+ } else {
256
+ uncachedAddresses.push(addr)
257
+ }
258
+ }
259
+ }
260
+
261
+ // If all were cached, return early
262
+ if (uncachedAddresses.length === 0) {
263
+ return results
264
+ }
265
+ } else {
266
+ // No cache, fetch all
267
+ uncachedAddresses.push(...addresses)
268
+ }
269
+
270
+ // Parse addresses and tokens
271
+ const profiles: string[] = []
272
+ const assets: string[] = []
273
+ const tokens: string[] = []
274
+ const tokenMapping = new Map<string, { address: Address; tokenId: string }>()
275
+
276
+ for (const key of uncachedAddresses) {
277
+ const hasDash = key.includes('-')
278
+ const hasColon = key.includes(':')
279
+
280
+ if (hasColon || hasDash) {
281
+ const normalizedKey = key.replace(/[-:]/, '-')
282
+ tokens.push(normalizedKey)
283
+ const [address, tokenId] = key.split(/[-:]/)
284
+ tokenMapping.set(key, { address: address as Address, tokenId })
285
+ } else {
286
+ // Regular addresses - could be profiles or assets
287
+ profiles.push(key)
288
+ assets.push(key)
289
+ }
290
+ }
291
+
292
+ try {
293
+ // Fetch data from GraphQL
294
+ const data = (await request(graphqlEndpoint, addressesGql, {
295
+ profiles,
296
+ assets,
297
+ tokens,
298
+ })) as TFetchAddressData
299
+
300
+ // Process profiles
301
+ if (data.Profile) {
302
+ for (const profile of data.Profile) {
303
+ if (profile.standard !== 'LSP0ERC725Account') {
304
+ continue
305
+ }
306
+ const enhancedInfo: EnhancedInfo = {
307
+ address: profile.id as Address,
308
+ __gqltype: 'Profile',
309
+ name: profile.name || profile.fullName,
310
+ fullName: profile.fullName,
311
+ standard: profile.standard,
312
+ tags: profile.tags,
313
+ description: profile.description,
314
+ owner: profile.owner,
315
+ controllers: profile.controllers,
316
+ profileImages: profile.profileImages,
317
+ backgroundImages: profile.backgroundImages,
318
+ avatars: profile.avatars,
319
+ }
320
+ results.set(profile.id as Address, enhancedInfo)
321
+ }
322
+ }
323
+
324
+ // Process tokens (must be before assets per addressGQL.ts logic)
325
+ if (data.Token) {
326
+ for (const token of data.Token) {
327
+ const { id, baseAsset, tokenId, ...rest } = token
328
+
329
+ // Tokens have a composite ID format and baseAsset reference
330
+ if (baseAsset?.id && tokenId) {
331
+ const enhancedInfo: EnhancedInfo = {
332
+ address: baseAsset.id as Address,
333
+ tokenId: tokenId as Address,
334
+ __gqltype: 'Token',
335
+ ...rest,
336
+ baseAsset,
337
+ }
338
+ // Store with both dash and colon separators to handle both formats
339
+ const keyWithColon = `${baseAsset.id}:${tokenId}` as DataKey
340
+ const keyWithDash = `${baseAsset.id}-${tokenId}` as DataKey
341
+ results.set(keyWithColon, enhancedInfo)
342
+ results.set(keyWithDash, enhancedInfo)
343
+ }
344
+ }
345
+ }
346
+
347
+ // Process assets
348
+ if (data.Asset) {
349
+ for (const asset of data.Asset) {
350
+ if (asset.standard === 'LSP0ERC725Account') {
351
+ // Skip profiles - already processed
352
+ continue
353
+ }
354
+ const enhancedInfo: EnhancedInfo = {
355
+ address: asset.id as Address,
356
+ __gqltype: 'Asset',
357
+ ...asset,
358
+ }
359
+ if (!results.has(asset.id as Address)) {
360
+ results.set(asset.id as Address, enhancedInfo)
361
+ }
362
+ }
363
+ }
364
+
365
+ // Cache the newly fetched results
366
+ if (cache) {
367
+ const newEntries: Array<[DataKey, EnhancedInfo]> = []
368
+
369
+ // Collect all new entries
370
+ for (const [key, value] of results) {
371
+ // Only cache entries that were fetched (not from cache)
372
+ const isCached = addresses.some(
373
+ (addr) => addr.toLowerCase() === key.toLowerCase()
374
+ )
375
+
376
+ if (
377
+ isCached &&
378
+ uncachedAddresses.some(
379
+ (addr) => addr.toLowerCase() === key.toLowerCase()
380
+ )
381
+ ) {
382
+ newEntries.push([key, value])
383
+ }
384
+ }
385
+
386
+ // Use batch update if available
387
+ if (cache.setMany && newEntries.length > 0) {
388
+ await cache.setMany(newEntries)
389
+ } else {
390
+ // Fall back to individual updates
391
+ for (const [key, value] of newEntries) {
392
+ await cache.set(key, value)
393
+ }
394
+ }
395
+ }
396
+
397
+ return results
398
+ } catch (error) {
399
+ console.error('Failed to fetch addresses:', error)
400
+ return results
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Get GraphQL endpoint for a given chain
406
+ */
407
+ export function getGraphQLEndpoint(chain: Chain): string {
408
+ return chain.id === lukso.id
409
+ ? 'https://envio.lukso-mainnet.universal.tech/v1/graphql'
410
+ : 'https://envio.lukso-testnet.universal.tech/v1/graphql'
411
+ }
412
+
413
+ /**
414
+ * GraphQL query to find profiles controlled by specific addresses
415
+ */
416
+ const profilesByControllerGql = gql`
417
+ query ProfilesByController($controllerAddresses: [String!]) {
418
+ Profile(
419
+ where: {
420
+ controllers: {
421
+ address: { _in: $controllerAddresses }
422
+ tags: {
423
+ _contains: [
424
+ "ADDCONTROLLER"
425
+ "EDITPERMISSIONS"
426
+ "SUPER_TRANSFERVALUE"
427
+ "TRANSFERVALUE"
428
+ "SUPER_CALL"
429
+ "CALL"
430
+ "SUPER_STATICCALL"
431
+ "STATICCALL"
432
+ "DEPLOY"
433
+ "SUPER_SETDATA"
434
+ "SETDATA"
435
+ "ENCRYPT"
436
+ "DECRYPT"
437
+ "SIGN"
438
+ "EXECUTE_RELAY_CALL"
439
+ ]
440
+ }
441
+ }
442
+ }
443
+ order_by: { blockNumber: asc }
444
+ ) {
445
+ fullName
446
+ id
447
+ name
448
+ tags
449
+ description
450
+ standard
451
+ controllers {
452
+ address
453
+ tags
454
+ permissions
455
+ }
456
+ owner {
457
+ id
458
+ }
459
+ profileImages(where: {error: {_is_null: true}}, order_by: {width: asc}) {
460
+ width
461
+ height
462
+ src
463
+ verified
464
+ }
465
+ backgroundImages(where: {error: {_is_null: true}}, order_by: {width: asc}) {
466
+ width
467
+ height
468
+ src
469
+ verified
470
+ }
471
+ avatars(where: {error: {_is_null: true}}, order_by: {width: asc}) {
472
+ width
473
+ height
474
+ src
475
+ verified
476
+ }
477
+ }
478
+ }
479
+ `
480
+
481
+ /**
482
+ * Find profiles controlled by specific controller addresses
483
+ * @param controllerAddresses - Array of controller addresses to search for
484
+ * @param chain - Which chain to query
485
+ * @returns Array of profile data
486
+ */
487
+ export async function fetchProfilesByControllers(
488
+ controllerAddresses: readonly Address[],
489
+ chain: Chain
490
+ ): Promise<ProfileData[]> {
491
+ const graphqlEndpoint = getGraphQLEndpoint(chain)
492
+
493
+ try {
494
+ const data = (await request(graphqlEndpoint, profilesByControllerGql, {
495
+ controllerAddresses: controllerAddresses.map((addr) =>
496
+ addr.toLowerCase()
497
+ ),
498
+ })) as { Profile?: ProfileData[] }
499
+
500
+ return data.Profile || []
501
+ } catch (error) {
502
+ console.error(
503
+ `Failed to fetch profiles by controllers on ${chain.name}:`,
504
+ error
505
+ )
506
+ return []
507
+ }
508
+ }
509
+
510
+ export type ImageURL = {
511
+ url?: string
512
+ data?: string
513
+ src?: string
514
+ width: number
515
+ height: number
516
+ index?: number
517
+ verified?: string
518
+ }
519
+
520
+ /**
521
+ * Assuming the images are sorted ascending by width and/or height pick the first
522
+ * image that's larger than the requested size * dpr and process that one.
523
+ * By default this will assume the data came from the indexer and therefore
524
+ * we do not need to verify the existence of all the images (i.e. HEAD requests for
525
+ * each image) Setting ignoreHead to false is important during RPC mode so that
526
+ * we can skip images that are not really available.
527
+ *
528
+ * @param images - array of images
529
+ * @param options - specify arguments for search/processing
530
+ * @returns { width, height, src } - the image that fits the criteria
531
+ */
532
+ export async function getImage(
533
+ images: ImageURL[],
534
+ options: {
535
+ width: number
536
+ height?: number
537
+ index?: number
538
+ ignoreVerification?: boolean
539
+ forcePng?: boolean
540
+ ignoreHead?: boolean
541
+ fallback?: string
542
+ dpr?: number
543
+ }
544
+ ): Promise<{ width: number; height: number; src?: string } | undefined> {
545
+ const {
546
+ width,
547
+ height: _height,
548
+ index,
549
+ ignoreVerification,
550
+ ignoreHead = true, // By default we don't want to do HEAD requests for all images.
551
+ dpr = 1,
552
+ } = options
553
+ const boost = dpr === 1 ? 1.5 : 1
554
+ const height = _height || width
555
+ const { src: _src, verified } =
556
+ (images || []).find(
557
+ (i) =>
558
+ i.width >= width * boost * dpr &&
559
+ i.height &&
560
+ i.height >= height * boost * dpr &&
561
+ (index ? i.index === index : i.index === undefined)
562
+ ) ||
563
+ images?.at(-1) ||
564
+ {}
565
+ // We're talking about images here, so we should be using /image/ instead of /ipfs/
566
+ const url = _src?.startsWith('https://api.universalprofile.cloud/ipfs/')
567
+ ? _src?.replace(/\/ipfs\//, '/image/')
568
+ : _src
569
+ if (!url) {
570
+ return options.fallback
571
+ ? { src: options.fallback, width: 128, height: 128 }
572
+ : undefined
573
+ }
574
+ try {
575
+ let isImage: string | undefined = undefined
576
+ if (typeof caches !== 'undefined' && !ignoreHead) {
577
+ const cache = await caches.open('image-types')
578
+ const cached = await cache.match(url)
579
+ if (cached) {
580
+ const response = await cached.json()
581
+ if (response) {
582
+ isImage = response
583
+ }
584
+ }
585
+ }
586
+ if (!isImage && !ignoreHead) {
587
+ // HEAD will return 200, 404 and so including the 'content-type'
588
+ // We use this to determine what type of images we have.
589
+ // If the images came from the indexer then this test
590
+ // has already been done on the indexer and we can skip it here.
591
+ const now = Date.now()
592
+ isImage = await fetch(url, { method: 'HEAD' })
593
+ .then((response) => {
594
+ if (response.ok) {
595
+ const mime = response.headers.get('content-type') || ''
596
+ response.body?.cancel()
597
+ return /^image\//.test(mime) ? mime : undefined
598
+ }
599
+ response.body?.cancel()
600
+ return undefined
601
+ })
602
+ .catch((error) => {
603
+ console.error((error as Error).stack)
604
+ return undefined
605
+ })
606
+ if (isImage != null && typeof caches !== 'undefined') {
607
+ const cache = await caches.open('image-types')
608
+ await cache
609
+ .put(url, new Response(JSON.stringify(isImage)))
610
+ .catch((error) => {
611
+ console.error((error as Error).stack, url)
612
+ })
613
+ }
614
+ }
615
+ if (isImage || ignoreHead) {
616
+ const finalUrl = new URL(url)
617
+ const { searchParams } = finalUrl
618
+ const qs = new URLSearchParams(searchParams)
619
+ if (width) {
620
+ qs.set('width', `${width * boost}`)
621
+ }
622
+ if (width || height) {
623
+ qs.set('height', `${(height || width) * boost}`)
624
+ }
625
+ qs.set('fit', 'cover')
626
+ qs.set('dpr', dpr.toString())
627
+ if (
628
+ !options.ignoreHead &&
629
+ !/\/svg/.test(isImage || '') &&
630
+ options.forcePng
631
+ ) {
632
+ qs.set('format', 'png')
633
+ }
634
+ finalUrl.search = qs.toString()
635
+ const image = finalUrl.toString()
636
+ return {
637
+ width,
638
+ height: height || width,
639
+ src:
640
+ verified === 'INVALID' && !ignoreVerification
641
+ ? `${image}&blur=30`
642
+ : image,
643
+ }
644
+ }
645
+ } catch (error) {
646
+ console.error((error as Error).stack)
647
+ }
648
+ return options.fallback
649
+ ? { src: options.fallback, width: 128, height: 128 }
650
+ : undefined
651
+ }