@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,33 @@
1
+ import type { IDataModel, IDataModelConsumer } from '../types'
2
+ import { createConsumerProxy, DataModel } from './dataModel'
3
+
4
+ // Create the singleton instance
5
+ const dataModelInstance = new DataModel()
6
+
7
+ // Freeze the prototype and constructor to prevent prototype pollution
8
+ Object.freeze(DataModel.prototype)
9
+ Object.freeze(DataModel)
10
+
11
+ // Export the full model for data providers to import
12
+ // Note: We don't freeze dataModel as data providers need to modify it
13
+ export const dataModel: IDataModel = dataModelInstance
14
+
15
+ // Create consumer-only proxy for public API (frozen)
16
+ export const consumerModel: IDataModelConsumer =
17
+ createConsumerProxy(dataModelInstance)
18
+
19
+ // Export a function to optionally create global instance
20
+ // This allows consumers to control when/if the global is created
21
+ export function createGlobalInstance(): void {
22
+ if (typeof window !== 'undefined') {
23
+ // Create consumer-only proxy for window
24
+ const consumerProxy = createConsumerProxy(dataModelInstance)
25
+
26
+ Object.defineProperty(window, 'TransactionDecoder', {
27
+ value: consumerProxy,
28
+ writable: false,
29
+ configurable: false,
30
+ enumerable: true,
31
+ })
32
+ }
33
+ }
@@ -0,0 +1,325 @@
1
+ import { effect } from '@preact/signals-core'
2
+ import type { Address, Transaction } from 'viem'
3
+ import { lukso } from 'viem/chains'
4
+ import { decodeTransaction } from '../decoder/decodeTransaction'
5
+ import { defaultPlugins, defaultSchemaPlugins } from '../decoder/plugins'
6
+ import type { DecoderOptions, DecoderResult } from '../decoder/types'
7
+ import type { DataKey, EnhancedInfo, IDataModel } from '../types'
8
+ import { collectDataKeys } from './addressCollector'
9
+
10
+ /**
11
+ * Configuration for decoder integration
12
+ */
13
+ export interface DecoderIntegrationConfig {
14
+ dataModel: IDataModel
15
+ decoderOptions?: Partial<DecoderOptions>
16
+ addressCache?: AddressCache
17
+ }
18
+
19
+ /**
20
+ * External cache interface for address data
21
+ */
22
+ export interface AddressCache {
23
+ get(key: DataKey): Promise<EnhancedInfo | undefined>
24
+ set(key: DataKey, value: EnhancedInfo): Promise<void>
25
+ has(key: DataKey): Promise<boolean>
26
+ delete(key: DataKey): Promise<void>
27
+ clear(): Promise<void>
28
+ }
29
+
30
+ /**
31
+ * Integrate the decoder with the data model
32
+ * Handles progressive transaction enhancement and address population
33
+ */
34
+ export class DecoderIntegration {
35
+ private dataModel: IDataModel
36
+ private decoderOptions: DecoderOptions
37
+ private addressCache?: AddressCache
38
+
39
+ constructor(config: DecoderIntegrationConfig) {
40
+ this.dataModel = config.dataModel
41
+ this.addressCache = config.addressCache
42
+
43
+ // Set up default decoder options
44
+ this.decoderOptions = {
45
+ plugins: defaultPlugins,
46
+ schemaPlugins: defaultSchemaPlugins,
47
+ wrappers: [],
48
+ chain: config.decoderOptions?.chain || lukso,
49
+ ...config.decoderOptions,
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Add and decode a transaction
55
+ * @param transaction - Raw transaction data
56
+ * @returns Transaction signal that updates as decoding progresses
57
+ */
58
+ async addAndDecodeTransaction(transaction: DecoderResult) {
59
+ // Add transaction to data model
60
+ const signal = this.dataModel.addTransactions(transaction)
61
+
62
+ // Create fresh wrappers array for this transaction
63
+ const transactionOptions: DecoderOptions = {
64
+ ...this.decoderOptions,
65
+ wrappers: [], // Fresh array for collecting wrappers in this decode hierarchy
66
+ }
67
+
68
+ // Start decoding process
69
+ const { signal: decodingSignal } = await decodeTransaction(
70
+ transaction,
71
+ transactionOptions
72
+ )
73
+
74
+ // Update transaction data as decoding progresses
75
+ effect(() => {
76
+ const state = decodingSignal.value
77
+ if (state.data) {
78
+ const decodedResult = state.data as DecoderResult
79
+
80
+ // Process any collected wrappers
81
+ if (transactionOptions.wrappers.length > 0) {
82
+ // Only certain result types support wrappers
83
+ if (
84
+ 'wrappers' in decodedResult &&
85
+ typeof decodedResult === 'object'
86
+ ) {
87
+ ;(decodedResult as Record<string, unknown>).wrappers =
88
+ transactionOptions.wrappers
89
+ }
90
+
91
+ // Extract addresses from wrappers
92
+ for (const wrapper of transactionOptions.wrappers) {
93
+ this.updateAddressesFromDecoded(wrapper as DecoderResult)
94
+ }
95
+ }
96
+
97
+ // Flatten nested batch transactions if configured
98
+ const processedResult = this.flattenBatchTransactions(decodedResult)
99
+
100
+ if (transaction.hash) {
101
+ this.dataModel.updateTransactionData(
102
+ transaction.hash,
103
+ processedResult,
104
+ 0 // decoder index
105
+ )
106
+ }
107
+
108
+ // Extract and update addresses from decoded data
109
+ this.updateAddressesFromDecoded(processedResult)
110
+ }
111
+ })
112
+
113
+ return signal
114
+ }
115
+
116
+ /**
117
+ * Add and decode multiple transactions
118
+ * @param transactions - Array of raw transactions
119
+ * @returns Array of transaction signals
120
+ */
121
+ async addAndDecodeTransactions(transactions: DecoderResult[]) {
122
+ // Add all transactions to data model
123
+ const signals = this.dataModel.addTransactions(transactions)
124
+
125
+ // Start decoding for each transaction
126
+ const decodingPromises = transactions.map(async (tx, index) => {
127
+ // Create fresh wrappers array for each transaction
128
+ const transactionOptions: DecoderOptions = {
129
+ ...this.decoderOptions,
130
+ wrappers: [], // Fresh array for collecting wrappers in this decode hierarchy
131
+ }
132
+
133
+ const { signal: decodingSignal } = await decodeTransaction(
134
+ tx,
135
+ transactionOptions
136
+ )
137
+
138
+ // Update transaction data as decoding progresses
139
+ effect(() => {
140
+ const state = decodingSignal.value
141
+ if (state.data) {
142
+ const decodedResult = state.data as DecoderResult
143
+
144
+ // Process any collected wrappers
145
+ if (transactionOptions.wrappers.length > 0) {
146
+ // Only certain result types support wrappers
147
+ if (
148
+ 'wrappers' in decodedResult &&
149
+ typeof decodedResult === 'object'
150
+ ) {
151
+ ;(decodedResult as Record<string, unknown>).wrappers =
152
+ transactionOptions.wrappers
153
+ }
154
+
155
+ // Extract addresses from wrappers
156
+ for (const wrapper of transactionOptions.wrappers) {
157
+ this.updateAddressesFromDecoded(wrapper as DecoderResult)
158
+ }
159
+ }
160
+
161
+ // Flatten nested batch transactions if configured
162
+ const processedResult = this.flattenBatchTransactions(decodedResult)
163
+
164
+ if (tx.hash) {
165
+ this.dataModel.updateTransactionData(
166
+ tx.hash,
167
+ processedResult,
168
+ 0 // decoder index
169
+ )
170
+ }
171
+
172
+ // Extract and update addresses from decoded data
173
+ this.updateAddressesFromDecoded(processedResult)
174
+ }
175
+ })
176
+
177
+ return decodingSignal
178
+ })
179
+
180
+ await Promise.all(decodingPromises)
181
+ return signals
182
+ }
183
+
184
+ /**
185
+ * Extract and register addresses from decoded transaction data
186
+ */
187
+ private async updateAddressesFromDecoded(decodedData: DecoderResult) {
188
+ const addresses = collectDataKeys(decodedData)
189
+
190
+ // Check cache and inject into data model
191
+ for (const address of addresses) {
192
+ // Check cache first
193
+ if (this.addressCache) {
194
+ const cached = await this.addressCache.get(address)
195
+ if (cached) {
196
+ // Inject into data model
197
+ this.dataModel.updateData(cached)
198
+ }
199
+ }
200
+
201
+ // Ensure the address signal exists (creates empty signal if not)
202
+ // This allows views to getAddress() without errors
203
+ this.dataModel.getAddress(address)
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Flatten nested batch transactions
209
+ * If a batch contains another batch as a child, flatten all sub-transactions into the parent's children
210
+ *
211
+ * Structure:
212
+ * - Wrappers: The "invisible" execute functions (execute, executeRelayCall, etc.) that wrap the actual transaction
213
+ * - Children: The actual transactions being executed
214
+ *
215
+ * Result:
216
+ * - Single transaction: { resultType: 'execute', wrappers: [...] }
217
+ * - Batch transaction: { resultType: 'executeBatch', children: [flat list], wrappers: [...] }
218
+ *
219
+ * This ensures only ONE level of batch with all transactions flattened
220
+ */
221
+ private flattenBatchTransactions(result: DecoderResult): DecoderResult {
222
+ // Only process batch result types
223
+ if (
224
+ result.resultType !== 'executeBatch' &&
225
+ result.resultType !== 'setDataBatch'
226
+ ) {
227
+ return result
228
+ }
229
+
230
+ // Type guard to ensure we have children property
231
+ if (!('children' in result) || !Array.isArray(result.children)) {
232
+ return result
233
+ }
234
+
235
+ // Flatten children - if any child is also a batch, merge its children up
236
+ const flattenedChildren: unknown[] = []
237
+
238
+ for (const child of result.children) {
239
+ if (typeof child === 'object' && child && 'resultType' in child) {
240
+ const childResult = child as Record<string, unknown>
241
+ if (
242
+ childResult.resultType === 'executeBatch' ||
243
+ childResult.resultType === 'setDataBatch'
244
+ ) {
245
+ // This child is also a batch - flatten its children into our array
246
+ if (
247
+ 'children' in childResult &&
248
+ Array.isArray(childResult.children)
249
+ ) {
250
+ flattenedChildren.push(...childResult.children)
251
+ }
252
+ } else {
253
+ // Regular child - keep as is
254
+ flattenedChildren.push(child)
255
+ }
256
+ }
257
+ }
258
+
259
+ // Return updated result with flattened children
260
+ return {
261
+ ...result,
262
+ children: flattenedChildren,
263
+ } as DecoderResult
264
+ }
265
+
266
+ /**
267
+ * Load missing address data
268
+ * This would be called by an external service that fetches address data
269
+ */
270
+ async loadAddressData(addressData: EnhancedInfo[]) {
271
+ // Update data model
272
+ this.dataModel.injectData(addressData)
273
+
274
+ // Update cache if available
275
+ if (this.addressCache) {
276
+ await Promise.all(
277
+ addressData.map((data) => {
278
+ const key: DataKey = data.tokenId
279
+ ? (`${data.address}:${data.tokenId}` as DataKey)
280
+ : (data.address as DataKey)
281
+ return this.addressCache?.set(key, data) ?? Promise.resolve()
282
+ })
283
+ )
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Get addresses that need to be fetched
289
+ */
290
+ getMissingAddresses(): DataKey[] {
291
+ return this.dataModel.getMissingKeys()
292
+ }
293
+
294
+ /**
295
+ * Get addresses currently being loaded
296
+ */
297
+ getLoadingAddresses(): DataKey[] {
298
+ return this.dataModel.getLoadingKeys()
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Create a simple in-memory address cache
304
+ */
305
+ export function createMemoryAddressCache(): AddressCache {
306
+ const cache = new Map<string, EnhancedInfo>()
307
+
308
+ return {
309
+ async get(key: DataKey) {
310
+ return cache.get(key)
311
+ },
312
+ async set(key: DataKey, value: EnhancedInfo) {
313
+ cache.set(key, value)
314
+ },
315
+ async has(key: DataKey) {
316
+ return cache.has(key)
317
+ },
318
+ async delete(key: DataKey) {
319
+ cache.delete(key)
320
+ },
321
+ async clear() {
322
+ cache.clear()
323
+ },
324
+ }
325
+ }
package/src/data.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @lukso/transaction-decoder/data - Data Provider API
3
+ *
4
+ * This module exports functions for data providers to manage transaction and address data.
5
+ * Consumers should use the main '@lukso/transaction-decoder' import instead.
6
+ */
7
+
8
+ import { dataModel } from './core/instance'
9
+
10
+ // Export provider-specific types
11
+ export type {
12
+ AddressState,
13
+ DataKey,
14
+ DataModelOptions,
15
+ DecoderResult,
16
+ EnhancedInfo,
17
+ IDataModel,
18
+ TransactionState,
19
+ } from './types'
20
+
21
+ // Export data management methods
22
+ export const addTransactions = dataModel.addTransactions.bind(dataModel)
23
+ export const updateTransactionData =
24
+ dataModel.updateTransactionData.bind(dataModel)
25
+
26
+ // Address data management
27
+ export const injectData = dataModel.injectData.bind(dataModel)
28
+ export const updateData = dataModel.updateData.bind(dataModel)
29
+ export const setLoading = dataModel.setLoading.bind(dataModel)
30
+ export const setError = dataModel.setError.bind(dataModel)
31
+
32
+ // Collection management
33
+ export const getMissingKeys = dataModel.getMissingKeys.bind(dataModel)
34
+ export const getLoadingKeys = dataModel.getLoadingKeys.bind(dataModel)
35
+
36
+ // Maintenance methods
37
+ export const clear = dataModel.clear.bind(dataModel)
38
+ export const remove = dataModel.remove.bind(dataModel)
39
+
40
+ // Also export the full data model for advanced usage
41
+ export { dataModel }
42
+
43
+ // Export factory function for creating new instances
44
+ export { createDataModel } from './core/dataModel'
45
+ // Export useful constants for plugin developers
46
+ export { defaultSchema, IPFS_GATEWAY } from './decoder/constants'
47
+
48
+ export { decodeTransaction } from './decoder/decodeTransaction'
49
+ export { decodeEvent } from './decoder/events'
50
+ export {
51
+ decodeKeyValue,
52
+ decodeKeyValueRaw,
53
+ standardSchemaPlugin,
54
+ } from './decoder/plugins'
55
+ // Export additional types for plugin development
56
+ export type {
57
+ ArrayArgs,
58
+ CustomDecodeFunctionDataReturn,
59
+ Info,
60
+ NamedArgs,
61
+ ResultShared,
62
+ ResultType,
63
+ } from './decoder/types'
64
+ // Export plugin development utilities
65
+ export {
66
+ createNamedArgs,
67
+ customDecodeFunctionData,
68
+ extractAddress,
69
+ getPublicClient,
70
+ } from './decoder/utils'
@@ -0,0 +1,182 @@
1
+ # Generator-Based Transaction Decoder Proposal
2
+
3
+ ## Current Limitations
4
+
5
+ 1. **State Updates**: Currently only yields state at two points:
6
+ - Initial sync decode (partial)
7
+ - Final async decode (complete)
8
+
9
+ 2. **Address Batching**: Single transactions typically yield 1-5 addresses, making batch resolution inefficient
10
+
11
+ ## Proposed Generator Approach
12
+
13
+ ```typescript
14
+ // Generator that yields intermediate states
15
+ async function* decodeTransactionGenerator(
16
+ transaction: Transaction,
17
+ options: DecoderOptions
18
+ ): AsyncGenerator<DecoderState, DecoderResult, unknown> {
19
+
20
+ // Phase 1: Initial sync decode
21
+ yield {
22
+ phase: 'initial',
23
+ status: 'decoding',
24
+ addresses: collectDataKeys(transaction),
25
+ }
26
+
27
+ const syncResult = await decodeSyncPlugins(transaction, options)
28
+
29
+ yield {
30
+ phase: 'sync-complete',
31
+ status: 'partial',
32
+ result: syncResult,
33
+ addresses: collectDataKeys(syncResult),
34
+ }
35
+
36
+ // Phase 2: Async plugins one by one
37
+ for (const plugin of options.plugins.filter(p => p.async)) {
38
+ yield {
39
+ phase: 'async-plugin',
40
+ status: 'enhancing',
41
+ pluginName: plugin.name,
42
+ currentResult: syncResult,
43
+ }
44
+
45
+ const enhanced = await plugin.enhance(syncResult, options)
46
+ if (enhanced) {
47
+ syncResult = enhanced
48
+ yield {
49
+ phase: 'async-plugin-complete',
50
+ status: 'partial',
51
+ result: enhanced,
52
+ addresses: collectDataKeys(enhanced),
53
+ }
54
+ }
55
+ }
56
+
57
+ // Phase 3: Event decoding
58
+ if (transaction.logs?.length) {
59
+ yield {
60
+ phase: 'events',
61
+ status: 'decoding-events',
62
+ eventCount: transaction.logs.length,
63
+ }
64
+
65
+ const decodedLogs = await decodeAllEvents(transaction.logs, options)
66
+ syncResult.logs = decodedLogs
67
+ }
68
+
69
+ return syncResult
70
+ }
71
+ ```
72
+
73
+ ## Batching Strategy for Address Resolution
74
+
75
+ ### Option 1: Transaction Accumulator
76
+ ```typescript
77
+ class TransactionAccumulator {
78
+ private pendingAddresses = new Set<DataKey>()
79
+ private addressCallbacks = new Map<DataKey, Array<(data: EnhancedInfo) => void>>()
80
+ private batchTimer?: NodeJS.Timeout
81
+
82
+ async addTransaction(addresses: DataKey[]): Promise<void> {
83
+ addresses.forEach(addr => this.pendingAddresses.add(addr))
84
+
85
+ // Start batch timer if not running
86
+ if (!this.batchTimer) {
87
+ this.batchTimer = setTimeout(() => this.flush(), 50) // 50ms debounce
88
+ }
89
+
90
+ // Force flush if batch is full
91
+ if (this.pendingAddresses.size >= 20) {
92
+ await this.flush()
93
+ }
94
+ }
95
+
96
+ private async flush() {
97
+ if (this.pendingAddresses.size === 0) return
98
+
99
+ const batch = Array.from(this.pendingAddresses)
100
+ this.pendingAddresses.clear()
101
+
102
+ const results = await fetchAddressBatch(batch)
103
+ // Notify callbacks...
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Option 2: Cross-Transaction Batching
109
+ ```typescript
110
+ // In DecoderIntegration
111
+ class DecoderIntegration {
112
+ private addressQueue = new BatchQueue<DataKey, EnhancedInfo>({
113
+ batchSize: 20,
114
+ batchDelay: 50,
115
+ fetchBatch: async (keys) => {
116
+ return this.addressResolver.resolveAddresses(keys)
117
+ }
118
+ })
119
+
120
+ async decodeTransactions(transactions: Transaction[]): Promise<Signal<TransactionState>[]> {
121
+ // Collect all addresses upfront
122
+ const allAddresses = transactions.flatMap(tx => collectDataKeys(tx))
123
+
124
+ // Pre-warm the cache
125
+ await this.addressQueue.addMany(allAddresses)
126
+
127
+ // Now decode with cached data available
128
+ return Promise.all(transactions.map(tx => this.decodeTransaction(tx)))
129
+ }
130
+ }
131
+ ```
132
+
133
+ ## Hybrid Approach Recommendation
134
+
135
+ 1. **Keep current two-phase approach** for single transactions
136
+ - Simpler API
137
+ - Less overhead for common case
138
+
139
+ 2. **Add batch decoder** for multiple transactions
140
+ ```typescript
141
+ export async function decodeTransactionBatch(
142
+ transactions: Transaction[],
143
+ options: DecoderOptions
144
+ ): Promise<TransactionDecoderResult[]> {
145
+ // Pre-collect all addresses
146
+ const allAddresses = new Set<DataKey>()
147
+ transactions.forEach(tx => {
148
+ collectDataKeys(tx).forEach(addr => allAddresses.add(addr))
149
+ })
150
+
151
+ // Pre-fetch in efficient batches
152
+ if (options.addressResolver) {
153
+ await options.addressResolver.preloadAddresses(Array.from(allAddresses))
154
+ }
155
+
156
+ // Decode all with cached data
157
+ return Promise.all(transactions.map(tx => decodeTransaction(tx, options)))
158
+ }
159
+ ```
160
+
161
+ 3. **Optional generator API** for advanced use cases
162
+ ```typescript
163
+ export async function* decodeTransactionStream(
164
+ transaction: Transaction,
165
+ options: DecoderOptions & { yieldInterval?: number }
166
+ ): AsyncGenerator<TransactionState, void, unknown> {
167
+ // Yield states as decoding progresses
168
+ }
169
+ ```
170
+
171
+ ## Benefits
172
+
173
+ 1. **Efficiency**: Batch API optimizes address resolution across transactions
174
+ 2. **Simplicity**: Current API remains simple for single transaction use case
175
+ 3. **Flexibility**: Generator API available for advanced real-time UI updates
176
+ 4. **Progressive**: Each API level builds on the previous
177
+
178
+ ## Implementation Priority
179
+
180
+ 1. First: Add `decodeTransactionBatch` for efficient multi-transaction decoding
181
+ 2. Second: Optimize `AddressResolver` to support pre-loading
182
+ 3. Third: Consider generator API based on real-world usage patterns