@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,888 @@
1
+ // Core data model implementation
2
+ import { batch, effect, type Signal, signal } from '@preact/signals-core'
3
+ import type { Address, Hex } from 'viem'
4
+ import { hexToBigInt, isHex, size, slice } from 'viem'
5
+ import type {
6
+ AddressState,
7
+ DataKey,
8
+ DataModelOptions,
9
+ DecoderResult,
10
+ EnhancedInfo,
11
+ IDataModel,
12
+ IDataModelConsumer,
13
+ TransactionState,
14
+ } from '../types'
15
+ import { collectDataKeys } from './addressCollector'
16
+
17
+ // Helper type for deep freezing - kept internal to this module
18
+ type DeepReadonly<T> = T extends (infer R)[]
19
+ ? DeepReadonlyArray<R>
20
+ : T extends (...args: unknown[]) => unknown
21
+ ? T
22
+ : T extends object
23
+ ? DeepReadonlyObject<T>
24
+ : T
25
+
26
+ interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
27
+
28
+ type DeepReadonlyObject<T> = {
29
+ readonly [P in keyof T]: DeepReadonly<T[P]>
30
+ }
31
+
32
+ /**
33
+ * Deep freeze an object to prevent any modifications
34
+ * Returns the same object if it's already frozen
35
+ */
36
+ export function deepFreeze<T>(obj: T): T {
37
+ // Primitives and already frozen objects don't need processing
38
+ if (obj === null || typeof obj !== 'object' || Object.isFrozen(obj)) {
39
+ return obj
40
+ }
41
+
42
+ // Freeze the object itself
43
+ Object.freeze(obj)
44
+
45
+ // Recursively freeze all properties
46
+ for (const prop of Object.getOwnPropertyNames(obj)) {
47
+ const value = (obj as Record<string, unknown>)[prop]
48
+ if (value !== null && typeof value === 'object') {
49
+ deepFreeze(value)
50
+ }
51
+ }
52
+
53
+ return obj
54
+ }
55
+
56
+ export interface TransactionKey {
57
+ hash: Hex
58
+ decoderIndex?: number // For batch transactions
59
+ }
60
+
61
+ export class DataModel implements IDataModel {
62
+ // Nested map structure: Address -> (TokenId | null) -> Signal
63
+ private dataSignals = new Map<Address, Signal<AddressState>>()
64
+
65
+ // Transaction signals: Hash -> (DecoderIndex | null) -> Signal
66
+ private transactionSignals = new Map<
67
+ Hex,
68
+ Map<number | null, Signal<TransactionState>>
69
+ >()
70
+
71
+ // Keep track of unique transaction hashes in order
72
+ private transactionOrder: Array<Hex> = []
73
+
74
+ private options: DataModelOptions
75
+
76
+ // Track pending resolved data updates
77
+ private pendingResolvedUpdate = false
78
+
79
+ constructor(options: DataModelOptions = {}) {
80
+ this.options = options
81
+ }
82
+
83
+ /**
84
+ * Extract a 20-byte address from a potentially 32-byte hex value
85
+ * If the input is 32 bytes, validates it's zero-padded and takes the rightmost 20 bytes
86
+ * If the input is 20 bytes, returns it as-is
87
+ */
88
+ private extractAddress(hex: Hex): Address {
89
+ if (!isHex(hex)) {
90
+ throw new Error(`Invalid hex value: ${hex}`)
91
+ }
92
+
93
+ const bytes = size(hex)
94
+
95
+ if (bytes === 32) {
96
+ // Check if the first 12 bytes are zeros (valid padding)
97
+ const first12Bytes = slice(hex, 0, 12)
98
+ if (hexToBigInt(first12Bytes) !== 0n) {
99
+ throw new Error(
100
+ `Invalid 32-byte address: first 12 bytes must be zero for a padded address, got ${first12Bytes}`
101
+ )
102
+ }
103
+ // 32 bytes with valid padding - extract the rightmost 20 bytes
104
+ return slice(hex, 12, 32) as Address
105
+ }
106
+ if (bytes === 20) {
107
+ // 20 bytes - already a proper address
108
+ return hex as Address
109
+ }
110
+ throw new Error(`Invalid address length: ${bytes} bytes`)
111
+ }
112
+
113
+ /**
114
+ * Get or create a signal for a specific key
115
+ */
116
+ private getOrCreateSignal(key: DataKey): Signal<AddressState> {
117
+ let sig = this.dataSignals.get(key)
118
+ if (!sig) {
119
+ sig = signal<AddressState>({
120
+ loading: false,
121
+ data: undefined,
122
+ error: undefined,
123
+ lastUpdated: undefined,
124
+ })
125
+
126
+ this.dataSignals.set(key, sig)
127
+
128
+ // Notify about missing keys (for external fetching)
129
+ if (this.options.onMissingKeys) {
130
+ // Debounce notifications
131
+ setTimeout(() => {
132
+ const missing = this.getMissingKeys()
133
+ if (missing.length > 0) {
134
+ this.options.onMissingKeys?.(missing)
135
+ }
136
+ }, 0)
137
+ }
138
+ }
139
+
140
+ return sig
141
+ }
142
+
143
+ /**
144
+ * Extract transaction key from transaction JSON
145
+ */
146
+ private getTransactionKey(transaction: unknown): TransactionKey {
147
+ if (!transaction || typeof transaction !== 'object') {
148
+ throw new Error('Transaction must be an object')
149
+ }
150
+
151
+ const tx = transaction as Record<string, unknown>
152
+ if (!tx.hash && !tx.transactionHash) {
153
+ throw new Error('Transaction must have hash or transactionHash')
154
+ }
155
+
156
+ return {
157
+ hash: (tx.hash || tx.transactionHash) as Hex,
158
+ decoderIndex: tx.decoderIndex ? (tx.decoderIndex as number) : undefined,
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get or create the inner map for a transaction hash
164
+ */
165
+ private getOrCreateTransactionMap(
166
+ hash: Hex
167
+ ): Map<number | null, Signal<TransactionState>> {
168
+ const normalizedHash = hash.toLowerCase() as Hex
169
+ let map = this.transactionSignals.get(normalizedHash)
170
+ if (!map) {
171
+ map = new Map()
172
+ this.transactionSignals.set(normalizedHash, map)
173
+ }
174
+ return map
175
+ }
176
+
177
+ /**
178
+ * Add one or more transactions to the model
179
+ * Collects all addresses from all transactions and marks them as loading
180
+ */
181
+ addTransactions<T extends unknown | unknown[]>(
182
+ jsonTransactions: T
183
+ ): T extends unknown[]
184
+ ? Array<Signal<TransactionState>>
185
+ : Signal<TransactionState> {
186
+ // Normalize input to array
187
+ const transactions = Array.isArray(jsonTransactions)
188
+ ? jsonTransactions
189
+ : [jsonTransactions]
190
+ const isSingle = !Array.isArray(jsonTransactions)
191
+ // First, collect all addresses from all transactions
192
+ const allAddresses = new Set<string>()
193
+ const transactionAddresses: DataKey[][] = []
194
+
195
+ for (const tx of transactions) {
196
+ const addresses = collectDataKeys(tx)
197
+ transactionAddresses.push(addresses)
198
+
199
+ // Add to set for deduplication
200
+ for (const addr of addresses) {
201
+ allAddresses.add(addr)
202
+ }
203
+ }
204
+
205
+ // Convert back to DataKey array
206
+ const uniqueAddresses = Array.from(allAddresses) as DataKey[]
207
+
208
+ // Mark all addresses as loading at once
209
+ if (uniqueAddresses.length > 0) {
210
+ this.setLoading(uniqueAddresses)
211
+ }
212
+
213
+ // Now add each transaction
214
+ const results = transactions.map((tx, index) => {
215
+ const key = this.getTransactionKey(tx)
216
+ const normalizedHash = key.hash.toLowerCase() as Hex
217
+ const decoderIndex = key.decoderIndex || null
218
+
219
+ const isNewHash = !this.transactionSignals.has(normalizedHash)
220
+ const txMap = this.getOrCreateTransactionMap(normalizedHash)
221
+
222
+ if (txMap.has(decoderIndex)) {
223
+ throw new Error(
224
+ `Transaction already exists: ${normalizedHash}:${decoderIndex || 0}`
225
+ )
226
+ }
227
+
228
+ // Create frozen data once
229
+ const frozenData = deepFreeze(
230
+ structuredClone(tx)
231
+ ) as unknown as DeepReadonly<DecoderResult>
232
+
233
+ // Create reactive transaction with frozen data
234
+ const sig = signal<TransactionState>({
235
+ data: frozenData,
236
+ loading: false,
237
+ addresses: Object.freeze([...transactionAddresses[index]]) as DataKey[],
238
+ error: undefined,
239
+ lastUpdated: Date.now(),
240
+ resolvedData: frozenData, // Start with the same frozen data
241
+ addressesResolved: false,
242
+ })
243
+
244
+ txMap.set(decoderIndex, sig)
245
+
246
+ // Track transaction order (only for new hashes)
247
+ if (isNewHash) {
248
+ this.transactionOrder.push(normalizedHash)
249
+ }
250
+
251
+ // Notify about new transaction
252
+ if (this.options.onNewTransaction) {
253
+ this.options.onNewTransaction(tx)
254
+ }
255
+
256
+ return sig
257
+ })
258
+
259
+ // Return single signal if input was single, otherwise return array
260
+ return (isSingle ? results[0] : results) as T extends unknown[]
261
+ ? Array<Signal<TransactionState>>
262
+ : Signal<TransactionState>
263
+ }
264
+
265
+ /**
266
+ * Get an existing transaction signal (returns undefined if not found)
267
+ */
268
+ getTransaction(
269
+ jsonTransaction: unknown
270
+ ): Signal<TransactionState> | undefined {
271
+ const key = this.getTransactionKey(jsonTransaction)
272
+ const normalizedHash = key.hash.toLowerCase() as Hex
273
+ const decoderIndex = key.decoderIndex || null
274
+
275
+ const txMap = this.transactionSignals.get(normalizedHash)
276
+ if (!txMap) {
277
+ return undefined
278
+ }
279
+
280
+ return txMap.get(decoderIndex)
281
+ }
282
+
283
+ /**
284
+ * Get transaction by hash and decoder index
285
+ */
286
+ getTransactionByKey(
287
+ hash: Hex,
288
+ decoderIndex?: number
289
+ ): Signal<TransactionState> | undefined {
290
+ const normalizedHash = hash.toLowerCase() as Hex
291
+ const txMap = this.transactionSignals.get(normalizedHash)
292
+ if (!txMap) {
293
+ return undefined
294
+ }
295
+
296
+ return txMap.get(decoderIndex || null)
297
+ }
298
+
299
+ /**
300
+ * Update a transaction's data and re-collect addresses
301
+ * Useful after async decoding completes
302
+ */
303
+ updateTransactionData(
304
+ hash: Hex,
305
+ newData: DecoderResult,
306
+ decoderIndex?: number
307
+ ): void {
308
+ const normalizedHash = hash.toLowerCase() as Hex
309
+ const txMap = this.transactionSignals.get(normalizedHash)
310
+ if (!txMap) return
311
+
312
+ const sig = txMap.get(decoderIndex ?? null)
313
+ if (!sig) return
314
+
315
+ // Re-collect addresses with the new decoded data
316
+ const newAddresses = collectDataKeys(newData, true, [
317
+ ...sig.value.addresses,
318
+ ])
319
+
320
+ // Create frozen data once
321
+ const frozenData = deepFreeze(structuredClone(newData))
322
+
323
+ // Update the signal with frozen data
324
+ sig.value = {
325
+ ...sig.value,
326
+ data: frozenData,
327
+ addresses: Object.freeze([...newAddresses]) as DataKey[],
328
+ lastUpdated: Date.now(),
329
+ // Start with the current data, will be enhanced when addresses load
330
+ resolvedData: frozenData,
331
+ addressesResolved: false,
332
+ }
333
+
334
+ // Mark any new addresses as loading
335
+ this.setLoading(newAddresses)
336
+
337
+ // Check if we can create resolved data immediately
338
+ queueMicrotask(() => {
339
+ this.updateResolvedTransactionData()
340
+ })
341
+ }
342
+
343
+ /**
344
+ * Get all decoded transactions for a hash by index (insertion order)
345
+ */
346
+ getTransactionsByIndex(
347
+ index: number
348
+ ): Map<number | null, Signal<TransactionState>> | undefined {
349
+ if (index < 0 || index >= this.transactionOrder.length) {
350
+ return undefined
351
+ }
352
+
353
+ const hash = this.transactionOrder[index]
354
+ return this.transactionSignals.get(hash)
355
+ }
356
+
357
+ /**
358
+ * Get transaction hash by index
359
+ */
360
+ getTransactionHashByIndex(index: number): Hex | undefined {
361
+ return this.transactionOrder[index]
362
+ }
363
+
364
+ /**
365
+ * Get number of unique transaction hashes
366
+ */
367
+ getTransactionCount(): number {
368
+ return this.transactionOrder.length
369
+ }
370
+
371
+ /**
372
+ * Get number of decoded transactions for a specific hash
373
+ */
374
+ getDecodedCount(hash: Hex): number {
375
+ const normalizedHash = hash.toLowerCase() as Hex
376
+ const txMap = this.transactionSignals.get(normalizedHash)
377
+ return txMap ? txMap.size : 0
378
+ }
379
+
380
+ /**
381
+ * Get all decoded transactions for a specific hash
382
+ */
383
+ getDecodedTransactions(hash: Hex): Array<Signal<TransactionState>> {
384
+ const normalizedHash = hash.toLowerCase() as Hex
385
+ const txMap = this.transactionSignals.get(normalizedHash)
386
+ return txMap ? Array.from(txMap.values()) : []
387
+ }
388
+
389
+ /**
390
+ * Get address signal (alias for getSignal for clearer API)
391
+ */
392
+ getAddress(address: DataKey): Signal<AddressState> {
393
+ return this.getSignal(address)
394
+ }
395
+
396
+ /**
397
+ * Inject data into the model
398
+ * Can be called multiple times to progressively add/update data
399
+ */
400
+ injectData(dataList: EnhancedInfo[]): void {
401
+ batch(() => {
402
+ for (const data of dataList) {
403
+ const key: DataKey = data.tokenId
404
+ ? `${data.address as Hex}:${data.tokenId}`
405
+ : (data.address as Hex)
406
+
407
+ const sig = this.getOrCreateSignal(key)
408
+
409
+ // Update the signal with frozen data
410
+ sig.value = {
411
+ loading: false,
412
+ data: deepFreeze(structuredClone(data)),
413
+ error: undefined,
414
+ lastUpdated: Date.now(),
415
+ }
416
+ }
417
+ })
418
+
419
+ // After the batch, check if any transactions need their resolved data updated
420
+ this.updateResolvedTransactionData()
421
+ }
422
+
423
+ /**
424
+ * Update a single item
425
+ */
426
+ updateData(data: EnhancedInfo): void {
427
+ const key: DataKey = data.tokenId
428
+ ? `${data.address as Hex}:${data.tokenId}`
429
+ : (data.address as Hex)
430
+
431
+ const sig = this.getOrCreateSignal(key)
432
+
433
+ sig.value = {
434
+ loading: false,
435
+ data: deepFreeze(structuredClone(data)),
436
+ error: undefined,
437
+ lastUpdated: Date.now(),
438
+ }
439
+
440
+ // Debounce the resolved data update using a microtask
441
+ if (!this.pendingResolvedUpdate) {
442
+ this.pendingResolvedUpdate = true
443
+ queueMicrotask(() => {
444
+ this.pendingResolvedUpdate = false
445
+ this.updateResolvedTransactionData()
446
+ })
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Mark keys as loading
452
+ */
453
+ setLoading(keys: DataKey[]): void {
454
+ batch(() => {
455
+ for (const key of keys) {
456
+ const sig = this.getOrCreateSignal(key)
457
+ sig.value = { ...sig.value, loading: true }
458
+ }
459
+ })
460
+ }
461
+
462
+ /**
463
+ * Mark a key as errored
464
+ */
465
+ setError(key: DataKey, error: string): void {
466
+ const sig = this.getOrCreateSignal(key)
467
+ sig.value = {
468
+ loading: false,
469
+ data: undefined,
470
+ error,
471
+ lastUpdated: Date.now(),
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Get signal for a key (creates one if doesn't exist)
477
+ */
478
+ getSignal(key: DataKey): Signal<AddressState> {
479
+ return this.getOrCreateSignal(key)
480
+ }
481
+
482
+ /**
483
+ * Subscribe to key updates
484
+ */
485
+ subscribe(key: DataKey, callback: (state: AddressState) => void): () => void {
486
+ const sig = this.getSignal(key)
487
+ return effect(() => callback(sig.value))
488
+ }
489
+
490
+ /**
491
+ * Get current state of a key
492
+ */
493
+ getState(key: DataKey): AddressState {
494
+ return this.getSignal(key).value
495
+ }
496
+
497
+ /**
498
+ * Get all keys that have no data
499
+ */
500
+ getMissingKeys(): DataKey[] {
501
+ const missingKeys: DataKey[] = []
502
+
503
+ for (const [key] of this.dataSignals) {
504
+ missingKeys.push(key as Hex)
505
+ }
506
+
507
+ return missingKeys
508
+ }
509
+
510
+ /**
511
+ * Get all keys currently loading
512
+ */
513
+ getLoadingKeys(): DataKey[] {
514
+ const loadingKeys: DataKey[] = []
515
+
516
+ for (const [key, sig] of this.dataSignals) {
517
+ if (sig.value.loading) {
518
+ loadingKeys.push(key as Hex)
519
+ }
520
+ }
521
+
522
+ return loadingKeys
523
+ }
524
+
525
+ /**
526
+ * Check if we have data for a key
527
+ */
528
+ hasData(key: DataKey): boolean {
529
+ const sig = this.dataSignals.get(key)
530
+ return sig ? sig.value.data !== undefined : false
531
+ }
532
+
533
+ /**
534
+ * Clear all data
535
+ */
536
+ clear(): void {
537
+ batch(() => {
538
+ // Clear address data
539
+ for (const sig of this.dataSignals.values()) {
540
+ sig.value = {
541
+ loading: false,
542
+ data: undefined,
543
+ error: undefined,
544
+ lastUpdated: undefined,
545
+ }
546
+ }
547
+
548
+ // Clear transaction data
549
+ for (const txMap of this.transactionSignals.values()) {
550
+ for (const sig of txMap.values()) {
551
+ sig.value = {
552
+ ...sig.value,
553
+ loading: false,
554
+ error: undefined,
555
+ }
556
+ }
557
+ }
558
+ })
559
+ }
560
+
561
+ /**
562
+ * Remove specific keys from cache
563
+ */
564
+ remove(keys: DataKey[]): void {
565
+ for (const key of keys) {
566
+ this.dataSignals.delete(key)
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Get all cached keys
572
+ */
573
+ getAllKeys(): ReadonlyArray<DataKey> {
574
+ const keys: DataKey[] = []
575
+
576
+ for (const [key] of this.dataSignals) {
577
+ keys.push(key as Hex)
578
+ }
579
+
580
+ return Object.freeze(keys)
581
+ }
582
+
583
+ /**
584
+ * Get all data as a plain object (for debugging/serialization)
585
+ */
586
+ getAllData(): Readonly<Record<string, AddressState>> {
587
+ const result: Record<string, AddressState> = {}
588
+
589
+ for (const [key, sig] of this.dataSignals) {
590
+ result[key] = sig.value
591
+ }
592
+
593
+ return Object.freeze(result)
594
+ }
595
+
596
+ /**
597
+ * Get all transaction data (for debugging)
598
+ */
599
+ getAllTransactions(): Readonly<Record<string, TransactionState>> {
600
+ const result: Record<string, TransactionState> = {}
601
+
602
+ for (const [hash, txMap] of this.transactionSignals) {
603
+ for (const [decoderIndex, sig] of txMap) {
604
+ const key = decoderIndex === null ? hash : `${hash}:${decoderIndex}`
605
+ result[key] = sig.value
606
+ }
607
+ }
608
+
609
+ return Object.freeze(result)
610
+ }
611
+
612
+ /**
613
+ * Get all transactions in order (returns all decoded transactions grouped by hash)
614
+ */
615
+ getTransactionsInOrder(): Array<{
616
+ hash: Hex
617
+ transactions: Array<Signal<TransactionState>>
618
+ }> {
619
+ const result: Array<{
620
+ hash: Hex
621
+ transactions: Array<Signal<TransactionState>>
622
+ }> = []
623
+
624
+ for (const hash of this.transactionOrder) {
625
+ const txMap = this.transactionSignals.get(hash)
626
+ if (txMap) {
627
+ result.push({
628
+ hash,
629
+ transactions: Array.from(txMap.values()),
630
+ })
631
+ }
632
+ }
633
+
634
+ return result
635
+ }
636
+
637
+ /**
638
+ * Create a resolved version of transaction data with all addresses populated
639
+ */
640
+ createResolvedTransactionData(
641
+ transactionData: DecoderResult,
642
+ addresses: ReadonlyArray<DataKey>
643
+ ): DecoderResult {
644
+ // Clone the transaction data to avoid modifying the original
645
+ const resolved = structuredClone(transactionData)
646
+
647
+ // Create a map of address data for quick lookup
648
+ const addressMap = new Map<string, EnhancedInfo>()
649
+
650
+ for (const key of addresses) {
651
+ const addressData = this.getData(key)
652
+ if (addressData) {
653
+ addressMap.set(key.toLowerCase(), addressData)
654
+ }
655
+ }
656
+
657
+ // Use the path-based approach to replace addresses
658
+ this.replaceAddressesWithPaths(resolved, addressMap)
659
+
660
+ // Deep freeze the result
661
+ return deepFreeze(resolved)
662
+ }
663
+
664
+ /**
665
+ * Replace addresses using path information (similar to collectAddressesWithPaths)
666
+ */
667
+ private replaceAddressesWithPaths(
668
+ data: unknown,
669
+ addressMap: Map<string, EnhancedInfo>
670
+ ): void {
671
+ function traverse(
672
+ obj: unknown,
673
+ path: Array<string | number> = [],
674
+ parent?: Record<string, unknown> | unknown[]
675
+ ): void {
676
+ // Handle string case first
677
+ if (typeof obj === 'string' && obj.startsWith('0x')) {
678
+ // Use regex to detect zero-padded addresses
679
+ const isPaddedAddress = /^0x0*([a-fA-F0-9]{40})$/.test(obj)
680
+
681
+ if (isPaddedAddress || (isHex(obj) && size(obj as Hex) === 20)) {
682
+ // Extract the actual address (remove padding if needed)
683
+ const address = isPaddedAddress
684
+ ? (obj.replace(/^0x0*([a-fA-F0-9]{40})$/, '0x$1') as Address)
685
+ : obj
686
+
687
+ // Skip if too many zeros (likely not an address)
688
+ if (
689
+ address
690
+ .slice(2)
691
+ .split('')
692
+ .filter((c: string) => c === '0').length > 10
693
+ ) {
694
+ return
695
+ }
696
+
697
+ // Check for tokenId in parent
698
+ const currentKey = path[path.length - 1]
699
+ const tokenId =
700
+ parent &&
701
+ !Array.isArray(parent) &&
702
+ 'tokenId' in parent &&
703
+ parent.tokenId
704
+ ? (parent.tokenId as Hex)
705
+ : undefined
706
+
707
+ // Look up the address data
708
+ let addressData: EnhancedInfo | undefined
709
+ if (tokenId) {
710
+ const compositeKey = `${address.toLowerCase()}_${tokenId.toLowerCase()}`
711
+ addressData = addressMap.get(compositeKey)
712
+ } else {
713
+ addressData = addressMap.get(address.toLowerCase())
714
+ }
715
+
716
+ // Replace the address with the data if found
717
+ if (addressData && parent) {
718
+ if (Array.isArray(parent) && typeof currentKey === 'number') {
719
+ parent[currentKey] = addressData
720
+ } else if (
721
+ !Array.isArray(parent) &&
722
+ typeof currentKey === 'string'
723
+ ) {
724
+ parent[currentKey] = addressData
725
+ }
726
+ }
727
+ }
728
+ return
729
+ }
730
+
731
+ if (!obj || typeof obj !== 'object') return
732
+
733
+ if (Array.isArray(obj)) {
734
+ for (let index = 0; index < obj.length; index++) {
735
+ traverse(obj[index], path.concat([index]), obj)
736
+ }
737
+ } else {
738
+ const record = obj as Record<string, unknown>
739
+ for (const [key, value] of Object.entries(record)) {
740
+ traverse(value, path.concat([key]), record)
741
+ }
742
+ }
743
+ }
744
+
745
+ traverse(data)
746
+ }
747
+
748
+ /**
749
+ * Update resolved transaction data for all transactions where addresses are loaded
750
+ */
751
+ private updateResolvedTransactionData(): void {
752
+ // Go through all transactions
753
+ for (const [_hash, txMap] of this.transactionSignals) {
754
+ for (const [_decoderIndex, signal] of txMap) {
755
+ const currentState = signal.value
756
+
757
+ // Skip if already resolved or if no addresses
758
+ if (
759
+ currentState.addressesResolved ||
760
+ currentState.addresses.length === 0
761
+ ) {
762
+ continue
763
+ }
764
+
765
+ // Check if all addresses are loaded
766
+ let allAddressesLoaded = true
767
+ for (const addressKey of currentState.addresses) {
768
+ const addressState = this.getState(addressKey)
769
+ if (addressState.loading || !addressState.data) {
770
+ allAddressesLoaded = false
771
+ break
772
+ }
773
+ }
774
+
775
+ // If all addresses are loaded, create resolved data
776
+ if (allAddressesLoaded) {
777
+ const resolvedData = this.createResolvedTransactionData(
778
+ currentState.data as DecoderResult,
779
+ currentState.addresses
780
+ )
781
+
782
+ // Update the transaction state
783
+ signal.value = {
784
+ ...currentState,
785
+ resolvedData: resolvedData as DeepReadonly<DecoderResult>,
786
+ addressesResolved: true,
787
+ lastUpdated: Date.now(),
788
+ }
789
+ }
790
+ }
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Get data by key - convenience method that handles both 20 and 32 byte addresses
796
+ */
797
+ getData(key: DataKey): DeepReadonly<EnhancedInfo> | undefined {
798
+ return this.getState(key).data
799
+ }
800
+
801
+ /**
802
+ * Check if a key is loading
803
+ */
804
+ isLoading(key: DataKey): boolean {
805
+ return this.getState(key).loading
806
+ }
807
+
808
+ /**
809
+ * Get error for a key
810
+ */
811
+ getError(key: DataKey): string | undefined {
812
+ return this.getState(key).error
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Create a read-only consumer proxy that only exposes safe read methods
818
+ */
819
+ export function createConsumerProxy(model: DataModel): IDataModelConsumer {
820
+ const consumerMethods = [
821
+ // Transaction read methods
822
+ 'getTransaction',
823
+ 'getTransactionByKey',
824
+ 'getTransactionsByIndex',
825
+ 'getTransactionHashByIndex',
826
+ 'getTransactionCount',
827
+ 'getDecodedCount',
828
+ 'getDecodedTransactions',
829
+ 'getTransactionsInOrder',
830
+ // Address read methods
831
+ 'getAddress',
832
+ 'getSignal',
833
+ 'subscribe',
834
+ 'getState',
835
+ 'hasData',
836
+ 'getData',
837
+ 'isLoading',
838
+ 'getError',
839
+ // Collection read methods
840
+ 'getAllKeys',
841
+ 'getAllData',
842
+ 'getAllTransactions',
843
+ ]
844
+
845
+ const proxy = new Proxy(model, {
846
+ get(target, prop, receiver) {
847
+ if (typeof prop === 'string' && consumerMethods.includes(prop)) {
848
+ const value = Reflect.get(target, prop, receiver)
849
+ if (typeof value === 'function') {
850
+ return value.bind(target)
851
+ }
852
+ return value
853
+ }
854
+ // Block access to all other properties and methods
855
+ return undefined
856
+ },
857
+ set() {
858
+ return false // Prevent modifications
859
+ },
860
+ deleteProperty() {
861
+ return false
862
+ },
863
+ defineProperty() {
864
+ return false
865
+ },
866
+ setPrototypeOf() {
867
+ return false
868
+ },
869
+ }) as IDataModelConsumer
870
+
871
+ return proxy
872
+ }
873
+
874
+ /**
875
+ * Create a secure DataModel instance
876
+ * Freezes the prototype to prevent prototype pollution attacks
877
+ */
878
+ export function createDataModel(options?: DataModelOptions): IDataModel {
879
+ const model = new DataModel(options)
880
+
881
+ // Freeze the prototype to prevent modification
882
+ Object.freeze(DataModel.prototype)
883
+
884
+ // Freeze the constructor
885
+ Object.freeze(DataModel)
886
+
887
+ return model
888
+ }