@lukso/transaction-view-headless 0.2.2-dev.a8c9315

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 (76) hide show
  1. package/.turbo/turbo-build.log +51 -0
  2. package/CHANGELOG.md +88 -0
  3. package/COMPONENT_ARCHITECTURE.md +190 -0
  4. package/COMPONENT_CONVERSION_ANALYSIS.md +371 -0
  5. package/CONTEXT_VARIANT_GUIDE.md +425 -0
  6. package/LAZY_LOADING_GUIDE.md +534 -0
  7. package/LICENSE +201 -0
  8. package/PLAYBACK_EXAMPLES.md +658 -0
  9. package/PLAYBACK_GUIDE.md +402 -0
  10. package/README.md +43 -0
  11. package/REGISTRY_SUMMARY.md +638 -0
  12. package/SUFFERING_ECONOMICS.md +346 -0
  13. package/SUFFERING_ECONOMICS.zip +0 -0
  14. package/VIEW_REGISTRATION_GUIDE.md +795 -0
  15. package/dist/composables/index.cjs +1321 -0
  16. package/dist/composables/index.cjs.map +1 -0
  17. package/dist/composables/index.d.cts +6 -0
  18. package/dist/composables/index.d.ts +6 -0
  19. package/dist/composables/index.js +1276 -0
  20. package/dist/composables/index.js.map +1 -0
  21. package/dist/index-CMLU1hKL.d.ts +374 -0
  22. package/dist/index-CSDjhiKV.d.cts +374 -0
  23. package/dist/index.cjs +1534 -0
  24. package/dist/index.cjs.map +1 -0
  25. package/dist/index.d.cts +93 -0
  26. package/dist/index.d.ts +93 -0
  27. package/dist/index.js +1463 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/registry/index.cjs +522 -0
  30. package/dist/registry/index.cjs.map +1 -0
  31. package/dist/registry/index.d.cts +46 -0
  32. package/dist/registry/index.d.ts +46 -0
  33. package/dist/registry/index.js +487 -0
  34. package/dist/registry/index.js.map +1 -0
  35. package/dist/types/index.cjs +19 -0
  36. package/dist/types/index.cjs.map +1 -0
  37. package/dist/types/index.d.cts +203 -0
  38. package/dist/types/index.d.ts +203 -0
  39. package/dist/types/index.js +1 -0
  40. package/dist/types/index.js.map +1 -0
  41. package/dist/utils/index.cjs +217 -0
  42. package/dist/utils/index.cjs.map +1 -0
  43. package/dist/utils/index.d.cts +47 -0
  44. package/dist/utils/index.d.ts +47 -0
  45. package/dist/utils/index.js +188 -0
  46. package/dist/utils/index.js.map +1 -0
  47. package/dist/view-loader-dD86QAlQ.d.cts +121 -0
  48. package/dist/view-loader-ii8AxiQR.d.ts +121 -0
  49. package/package.json +64 -0
  50. package/publish.log +0 -0
  51. package/src/composables/index.ts +59 -0
  52. package/src/composables/use-image-loader.ts +315 -0
  53. package/src/composables/use-transaction-playback.ts +396 -0
  54. package/src/composables/use-transaction-view.ts +309 -0
  55. package/src/config/lukso-config.ts +171 -0
  56. package/src/examples/context-aware-views.ts +300 -0
  57. package/src/examples/example-views.ts +197 -0
  58. package/src/examples/lit-plus-rn-strategy.ts +448 -0
  59. package/src/examples/lsp7-custom-matcher-examples.ts +174 -0
  60. package/src/examples/opt-in-context-examples.ts +372 -0
  61. package/src/examples/optimized-view-definitions.ts +408 -0
  62. package/src/examples/vue-to-lit-compilation.ts +358 -0
  63. package/src/hooks/useAddressQuery.ts +135 -0
  64. package/src/hooks/useAddressResolution.ts +437 -0
  65. package/src/index.ts +36 -0
  66. package/src/registry/index.ts +13 -0
  67. package/src/registry/view-loader.ts +305 -0
  68. package/src/registry/view-registry.ts +135 -0
  69. package/src/types/index.ts +16 -0
  70. package/src/types/view-contexts.ts +72 -0
  71. package/src/types/view-matching.ts +173 -0
  72. package/src/utils/context-matcher.ts +165 -0
  73. package/src/utils/index.ts +5 -0
  74. package/src/utils/view-matcher.ts +338 -0
  75. package/tsconfig.json +20 -0
  76. package/tsup.config.ts +34 -0
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Framework-agnostic address resolution hook
3
+ * Can be adapted for Vue (composable), React (hook), or Lit (controller)
4
+ * Uses batching mechanism inspired by the decoder package
5
+ */
6
+
7
+ import type {
8
+ AssetData,
9
+ EnhancedInfo,
10
+ ImageData,
11
+ LinkData,
12
+ ProfileData,
13
+ TokenData,
14
+ } from '@lukso/transaction-decoder'
15
+ import type { Chain } from 'viem'
16
+ import {
17
+ cacheAddress,
18
+ currentChainSignal,
19
+ getCachedAddress,
20
+ getLuksoConfig,
21
+ initializeChainSignal,
22
+ isAddressResolutionEnabled,
23
+ } from '../config/lukso-config'
24
+
25
+ /**
26
+ * Union type of all possible resolved address data
27
+ * Combines Profile, Asset, and Token data structures
28
+ */
29
+ export type ResolvedAddress = {
30
+ address: string
31
+ hasProfile: boolean
32
+ truncatedAddress: string
33
+ __gqltype?: 'Profile' | 'Asset' | 'Token' | 'Contract'
34
+ } & Partial<ProfileData> &
35
+ Partial<AssetData> &
36
+ Partial<TokenData>
37
+
38
+ /**
39
+ * Truncate address to 0x{first5}...{last4} format
40
+ */
41
+ function truncateAddress(
42
+ address: string,
43
+ prefixLength = 5,
44
+ suffixLength = 4
45
+ ): string {
46
+ if (!address || address.length <= prefixLength + suffixLength + 2) {
47
+ return address
48
+ }
49
+
50
+ const prefix = address.slice(0, 2 + prefixLength) // 0x + first 5
51
+ const suffix = address.slice(-suffixLength) // last 4
52
+
53
+ return `${prefix}...${suffix}`
54
+ }
55
+
56
+ // Per-chain batching state
57
+ interface ChainBatchState {
58
+ batchTimeout: NodeJS.Timeout | null
59
+ batchQueue: Set<string>
60
+ batchResolvers: Map<string, (result: ResolvedAddress) => void>
61
+ pendingResolutions: Map<string, Promise<ResolvedAddress>>
62
+ isProcessing: boolean // Prevent concurrent batch processing
63
+ }
64
+
65
+ const chainBatchStates = new Map<number, ChainBatchState>()
66
+ const BATCH_DELAY = 0 // Just wait for next tick to batch addresses from the same render cycle
67
+ const BATCH_SIZE = 50
68
+
69
+ /**
70
+ * Get or create batch state for a specific chain
71
+ */
72
+ function getBatchState(chainId: number): ChainBatchState {
73
+ let state = chainBatchStates.get(chainId)
74
+ if (!state) {
75
+ state = {
76
+ batchTimeout: null,
77
+ batchQueue: new Set<string>(),
78
+ batchResolvers: new Map<string, (result: ResolvedAddress) => void>(),
79
+ pendingResolutions: new Map<string, Promise<ResolvedAddress>>(),
80
+ isProcessing: false,
81
+ }
82
+ chainBatchStates.set(chainId, state)
83
+ }
84
+ return state
85
+ }
86
+
87
+ /**
88
+ * Process a batch of addresses using decoder package's GraphQL resolver
89
+ */
90
+ async function processBatch(chainId: number): Promise<void> {
91
+ const state = getBatchState(chainId)
92
+ if (state.batchQueue.size === 0 || state.isProcessing) return
93
+
94
+ // Prevent concurrent processing for this chain
95
+ state.isProcessing = true
96
+
97
+ const addresses = Array.from(state.batchQueue)
98
+ state.batchQueue.clear()
99
+
100
+ try {
101
+ // Use decoder package's GraphQL resolver
102
+ // @ts-ignore - Declaration files may not be ready during development
103
+ const { fetchMultipleAddresses, getGraphQLEndpoint } = await import(
104
+ '@lukso/transaction-decoder'
105
+ )
106
+ const { lukso, luksoTestnet } = await import('viem/chains')
107
+
108
+ // Get the appropriate chain
109
+ const chain = chainId === 42 ? lukso : luksoTestnet
110
+ const graphqlEndpoint = getGraphQLEndpoint(chain)
111
+
112
+ // Convert addresses to DataKey format expected by decoder
113
+ const dataKeys = addresses as any[] // DataKey is a hex string type
114
+
115
+ // Fetch using decoder package
116
+ const results = await fetchMultipleAddresses(dataKeys, graphqlEndpoint)
117
+
118
+ // Process results and resolve promises
119
+ for (const address of addresses) {
120
+ const normalizedAddress = address.toLowerCase() as any
121
+ const enhanced: EnhancedInfo | undefined = results.get(normalizedAddress)
122
+
123
+ let result: ResolvedAddress
124
+
125
+ if (enhanced) {
126
+ // Just pass through the enhanced data as-is - no need to restructure
127
+ const profileImages = (enhanced as Record<string, any>).profileImages
128
+ const resolvedData = {
129
+ ...enhanced,
130
+ hasProfile: !!(enhanced.name || profileImages?.length),
131
+ }
132
+
133
+ // Cache the result with chainId
134
+ cacheAddressForChain(address, resolvedData, chainId)
135
+
136
+ result = {
137
+ ...resolvedData,
138
+ address,
139
+ truncatedAddress: truncateAddress(address),
140
+ }
141
+ } else {
142
+ // No profile found
143
+ result = {
144
+ address,
145
+ truncatedAddress: truncateAddress(address),
146
+ hasProfile: false,
147
+ }
148
+
149
+ // Cache negative result with chainId
150
+ cacheAddressForChain(address, { hasProfile: false }, chainId)
151
+ }
152
+
153
+ // Resolve the promise
154
+ const resolver = state.batchResolvers.get(normalizedAddress)
155
+ if (resolver) {
156
+ resolver(result)
157
+ state.batchResolvers.delete(normalizedAddress)
158
+ }
159
+ }
160
+ } catch (error) {
161
+ console.warn(
162
+ 'Batch address resolution failed (falling back to API):',
163
+ error
164
+ )
165
+
166
+ // Fallback to original API approach if decoder package fails
167
+ try {
168
+ const response = await fetch('/api/resolveAddresses', {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/json' },
171
+ body: JSON.stringify({
172
+ addresses,
173
+ chainId: chainId,
174
+ }),
175
+ })
176
+
177
+ if (!response.ok) {
178
+ throw new Error(`API fallback failed: ${response.status}`)
179
+ }
180
+
181
+ const data = await response.json()
182
+
183
+ // Process API results
184
+ for (const address of addresses) {
185
+ const normalizedAddress = address.toLowerCase()
186
+ const resolved = data.resolved?.[normalizedAddress]
187
+
188
+ let result: ResolvedAddress
189
+
190
+ if (resolved) {
191
+ const resolvedData = {
192
+ name: resolved.name,
193
+ profileImage: resolved.profileImage,
194
+ hasProfile: !!(resolved.name || resolved.profileImage),
195
+ }
196
+
197
+ cacheAddressForChain(address, resolvedData, chainId)
198
+
199
+ result = {
200
+ address,
201
+ truncatedAddress: truncateAddress(address),
202
+ ...resolvedData,
203
+ }
204
+ } else {
205
+ result = {
206
+ address,
207
+ truncatedAddress: truncateAddress(address),
208
+ hasProfile: false,
209
+ }
210
+ cacheAddressForChain(address, { hasProfile: false }, chainId)
211
+ }
212
+
213
+ const resolver = state.batchResolvers.get(normalizedAddress)
214
+ if (resolver) {
215
+ resolver(result)
216
+ state.batchResolvers.delete(normalizedAddress)
217
+ }
218
+ }
219
+ } catch (fallbackError) {
220
+ console.warn('Both GraphQL and API resolution failed:', fallbackError)
221
+
222
+ // Return fallbacks for all addresses
223
+ for (const address of addresses) {
224
+ const fallback: ResolvedAddress = {
225
+ address,
226
+ truncatedAddress: truncateAddress(address),
227
+ hasProfile: false,
228
+ }
229
+
230
+ cacheAddressForChain(address, { hasProfile: false }, chainId)
231
+
232
+ const resolver = state.batchResolvers.get(address.toLowerCase())
233
+ if (resolver) {
234
+ resolver(fallback)
235
+ state.batchResolvers.delete(address.toLowerCase())
236
+ }
237
+ }
238
+ }
239
+ } finally {
240
+ // Always reset processing flag
241
+ state.isProcessing = false
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Chain-aware address cache functions
247
+ */
248
+ function cacheAddressForChain(
249
+ address: string,
250
+ data: any,
251
+ chainId: number
252
+ ): void {
253
+ const key = `${address.toLowerCase()}:${chainId}`
254
+ cacheAddress(key, data)
255
+ }
256
+
257
+ function getCachedAddressForChain(
258
+ address: string,
259
+ chainId: number
260
+ ): any | null {
261
+ const key = `${address.toLowerCase()}:${chainId}`
262
+ return getCachedAddress(key)
263
+ }
264
+
265
+ /**
266
+ * Get effective chain ID from parameter or current signal
267
+ */
268
+ async function getEffectiveChainId(chainId?: number): Promise<number> {
269
+ if (chainId !== undefined) return chainId
270
+
271
+ // Initialize chain signal if needed
272
+ await initializeChainSignal()
273
+
274
+ // Get from signal or fall back to global config
275
+ const currentChain = currentChainSignal.value
276
+ if (currentChain) {
277
+ return currentChain.id
278
+ }
279
+
280
+ // Final fallback to global config
281
+ const config = getLuksoConfig()
282
+ return config.chainId
283
+ }
284
+
285
+ /**
286
+ * Resolve a single address using batched resolution
287
+ * Returns immediately with cached data or fallback, then updates with resolved data
288
+ * Handles both plain addresses and address-tokenId composite format (with - separator)
289
+ */
290
+ export async function resolveAddress(
291
+ address: string,
292
+ chainId?: number
293
+ ): Promise<ResolvedAddress> {
294
+ const effectiveChainId = await getEffectiveChainId(chainId)
295
+ const normalizedAddress = address.toLowerCase()
296
+
297
+ // Always provide truncated fallback using the original address
298
+ const fallback: ResolvedAddress = {
299
+ address,
300
+ truncatedAddress: truncateAddress(address),
301
+ hasProfile: false,
302
+ }
303
+
304
+ // Return fallback immediately if resolution disabled
305
+ if (!isAddressResolutionEnabled()) {
306
+ return fallback
307
+ }
308
+
309
+ // Check cache first (chain-aware)
310
+ const cached = getCachedAddressForChain(address, effectiveChainId)
311
+ if (cached) {
312
+ return {
313
+ ...fallback,
314
+ ...cached,
315
+ hasProfile: !!(cached.name || cached.profileImage),
316
+ }
317
+ }
318
+
319
+ const state = getBatchState(effectiveChainId)
320
+
321
+ // Check if resolution is already pending for this chain
322
+ const existingPromise = state.pendingResolutions.get(normalizedAddress)
323
+ if (existingPromise) {
324
+ return existingPromise
325
+ }
326
+
327
+ // Safety check: if we already have this address in the batch queue, return a fast fallback
328
+ if (state.batchQueue.has(normalizedAddress)) {
329
+ return Promise.resolve(fallback)
330
+ }
331
+
332
+ // Create new resolution promise
333
+ const resolutionPromise = new Promise<ResolvedAddress>((resolve) => {
334
+ // Add full address to batch queue (supports address-tokenId format with -)
335
+ state.batchQueue.add(normalizedAddress)
336
+ state.batchResolvers.set(normalizedAddress, resolve)
337
+
338
+ // Clear existing timeout
339
+ if (state.batchTimeout) {
340
+ clearTimeout(state.batchTimeout)
341
+ }
342
+
343
+ // Set new timeout for batch processing (only if not already processing)
344
+ if (!state.isProcessing) {
345
+ state.batchTimeout = setTimeout(() => {
346
+ state.batchTimeout = null
347
+ processBatch(effectiveChainId)
348
+ }, BATCH_DELAY)
349
+
350
+ // Process immediately if batch is full
351
+ if (state.batchQueue.size >= BATCH_SIZE) {
352
+ if (state.batchTimeout) {
353
+ clearTimeout(state.batchTimeout)
354
+ state.batchTimeout = null
355
+ }
356
+ processBatch(effectiveChainId)
357
+ }
358
+ }
359
+ })
360
+
361
+ // Store the promise to avoid duplicate requests
362
+ state.pendingResolutions.set(normalizedAddress, resolutionPromise)
363
+
364
+ // Clean up after resolution
365
+ resolutionPromise.finally(() => {
366
+ state.pendingResolutions.delete(normalizedAddress)
367
+ })
368
+
369
+ return resolutionPromise
370
+ }
371
+
372
+ /**
373
+ * Resolve multiple addresses
374
+ */
375
+ export async function resolveAddresses(
376
+ addresses: string[],
377
+ chainId?: number
378
+ ): Promise<Map<string, ResolvedAddress>> {
379
+ const results = new Map<string, ResolvedAddress>()
380
+
381
+ // Resolve all addresses in parallel using the batched resolver
382
+ const resolutionPromises = addresses.map(async (address) => {
383
+ const resolved = await resolveAddress(address, chainId)
384
+ results.set(address.toLowerCase(), resolved)
385
+ })
386
+
387
+ await Promise.all(resolutionPromises)
388
+ return results
389
+ }
390
+
391
+ /**
392
+ * Simple reactive state for frameworks to implement
393
+ * Each framework will wrap this with their reactivity system
394
+ */
395
+ export interface AddressResolutionState {
396
+ resolved: ResolvedAddress | null
397
+ loading: boolean
398
+ error: string | null
399
+ }
400
+
401
+ /**
402
+ * Create initial state
403
+ */
404
+ export function createAddressResolutionState(): AddressResolutionState {
405
+ return {
406
+ resolved: null,
407
+ loading: false,
408
+ error: null,
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Framework-agnostic resolution function that updates state
414
+ */
415
+ export async function resolveAddressWithState(
416
+ address: string,
417
+ _state: AddressResolutionState,
418
+ updateCallback: (newState: Partial<AddressResolutionState>) => void,
419
+ chainId?: number
420
+ ): Promise<void> {
421
+ // Start loading
422
+ updateCallback({ loading: true, error: null })
423
+
424
+ try {
425
+ const resolved = await resolveAddress(address, chainId)
426
+ updateCallback({
427
+ resolved,
428
+ loading: false,
429
+ error: null,
430
+ })
431
+ } catch (error) {
432
+ updateCallback({
433
+ loading: false,
434
+ error: error instanceof Error ? error.message : 'Resolution failed',
435
+ })
436
+ }
437
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Main exports for the decoder-headless package
2
+
3
+ export type {
4
+ AddressImageData,
5
+ AddressResolutionState,
6
+ ImageLoadOptions,
7
+ ImageResult,
8
+ LuksoConfig,
9
+ ResolvedAddress,
10
+ UseTransactionViewOptions,
11
+ UseTransactionViewResult,
12
+ } from './composables'
13
+ export * from './composables'
14
+ export {
15
+ findBestTransactionView,
16
+ useTransactionView,
17
+ } from './composables'
18
+ // Configuration and address resolution (framework-agnostic)
19
+ export * from './config/lukso-config'
20
+ export * from './hooks/useAddressQuery'
21
+ export * from './hooks/useAddressResolution'
22
+ export * from './registry'
23
+ // Convenience re-exports for common use cases
24
+ export {
25
+ getGlobalLoader,
26
+ getGlobalRegistry,
27
+ } from './registry'
28
+ export type {
29
+ DecoderRecordForSelection,
30
+ ViewContext,
31
+ ViewDefinition,
32
+ ViewMatch,
33
+ ViewMatchCriteria,
34
+ } from './types'
35
+ export * from './types'
36
+ export * from './utils'
@@ -0,0 +1,13 @@
1
+ export {
2
+ getGlobalLoader,
3
+ LoaderHelpers,
4
+ setGlobalLoader,
5
+ UniversalViewLoader,
6
+ ViewLoadingError,
7
+ } from './view-loader'
8
+ export {
9
+ DefaultViewRegistry,
10
+ getGlobalRegistry,
11
+ resetGlobalRegistry,
12
+ setGlobalRegistry,
13
+ } from './view-registry'