@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.
- package/LICENSE +201 -0
- package/README.md +486 -0
- package/dist/browser.cjs +6912 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +6 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +131 -0
- package/dist/browser.js.map +1 -0
- package/dist/cdn/transaction-decoder.global.js +296 -0
- package/dist/cdn/transaction-decoder.global.js.map +1 -0
- package/dist/chunk-GGBHTWJL.js +437 -0
- package/dist/chunk-GGBHTWJL.js.map +1 -0
- package/dist/chunk-GXZOF3QY.js +839 -0
- package/dist/chunk-GXZOF3QY.js.map +1 -0
- package/dist/chunk-LJ6ES5XF.js +776 -0
- package/dist/chunk-LJ6ES5XF.js.map +1 -0
- package/dist/chunk-XVHJWV5U.js +4925 -0
- package/dist/chunk-XVHJWV5U.js.map +1 -0
- package/dist/data.cjs +5518 -0
- package/dist/data.cjs.map +1 -0
- package/dist/data.d.cts +43 -0
- package/dist/data.d.ts +43 -0
- package/dist/data.js +55 -0
- package/dist/data.js.map +1 -0
- package/dist/index-BzXh7poJ.d.cts +524 -0
- package/dist/index-BzXh7poJ.d.ts +524 -0
- package/dist/index.cjs +6912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +756 -0
- package/dist/index.d.ts +756 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +5644 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +217 -0
- package/dist/server.d.ts +217 -0
- package/dist/server.js +644 -0
- package/dist/server.js.map +1 -0
- package/dist/utils-CBAkjQh3.d.cts +108 -0
- package/dist/utils-xT9-km0r.d.ts +108 -0
- package/package.json +101 -0
- package/src/browser.ts +13 -0
- package/src/client/resolveAddresses.ts +157 -0
- package/src/core/addressCollector.ts +153 -0
- package/src/core/addressResolver.ts +135 -0
- package/src/core/dataModel.ts +888 -0
- package/src/core/instance.ts +33 -0
- package/src/core/integrateDecoder.ts +325 -0
- package/src/data.ts +70 -0
- package/src/decoder/GENERATOR_PROPOSAL.md +182 -0
- package/src/decoder/THREE_PHASE_EXAMPLE.md +108 -0
- package/src/decoder/aggregation.ts +218 -0
- package/src/decoder/browserCache.ts +237 -0
- package/src/decoder/cache/README.md +126 -0
- package/src/decoder/cache/index.ts +44 -0
- package/src/decoder/cache.ts +139 -0
- package/src/decoder/constants.ts +125 -0
- package/src/decoder/decodeTransaction.ts +292 -0
- package/src/decoder/errors.ts +95 -0
- package/src/decoder/events.ts +192 -0
- package/src/decoder/functionSignature.ts +344 -0
- package/src/decoder/getDataFromExternalSources.ts +248 -0
- package/src/decoder/graphqlWS.ts +22 -0
- package/src/decoder/interfaces.ts +185 -0
- package/src/decoder/keyValue.ts +5 -0
- package/src/decoder/kvCache.ts +241 -0
- package/src/decoder/lruCache.ts +184 -0
- package/src/decoder/lsp7Mint.test.ts +179 -0
- package/src/decoder/lsp7TransferBatch.test.ts +105 -0
- package/src/decoder/plugins/RegistryAbi.ts +562 -0
- package/src/decoder/plugins/enhanceBurntPix.ts +132 -0
- package/src/decoder/plugins/enhanceGraffiti.ts +70 -0
- package/src/decoder/plugins/enhanceLSP0ERC725Account.ts +179 -0
- package/src/decoder/plugins/enhanceLSP26FollowerSystem.ts +88 -0
- package/src/decoder/plugins/enhanceLSP6KeyManager.ts +231 -0
- package/src/decoder/plugins/enhanceLSP7DigitalAsset.ts +165 -0
- package/src/decoder/plugins/enhanceLSP8IdentifiableDigitalAsset.ts +170 -0
- package/src/decoder/plugins/enhanceLSP9Vault.ts +57 -0
- package/src/decoder/plugins/enhanceRetrieveAbi.ts +85 -0
- package/src/decoder/plugins/enhanceSetData.ts +135 -0
- package/src/decoder/plugins/index.ts +99 -0
- package/src/decoder/plugins/schemaDefault.ts +318 -0
- package/src/decoder/plugins/standardPlugin.ts +202 -0
- package/src/decoder/registry.ts +322 -0
- package/src/decoder/singleGQL.ts +293 -0
- package/src/decoder/transaction.ts +198 -0
- package/src/decoder/types.ts +465 -0
- package/src/decoder/utils.ts +212 -0
- package/src/example/usage.ts +172 -0
- package/src/index.ts +174 -0
- package/src/server/addressResolver.ts +68 -0
- package/src/server/caches.ts +209 -0
- package/src/server/decodeTransactionSync.ts +156 -0
- package/src/server/decodeTransactionsBatch.ts +207 -0
- package/src/server/finishDecoding.ts +116 -0
- package/src/server/index.ts +81 -0
- package/src/server/lsp23Resolver.test.ts +46 -0
- package/src/server/lsp23Resolver.ts +419 -0
- package/src/server/types.ts +168 -0
- package/src/server.ts +22 -0
- package/src/shared/addressResolver.ts +651 -0
- package/src/shared/cache.ts +144 -0
- package/src/shared/constants.ts +21 -0
- package/src/stubs/tty.ts +13 -0
- package/src/stubs/util.ts +42 -0
- package/src/types/index.ts +154 -0
- package/src/types/provider.ts +46 -0
- package/src/umd.ts +13 -0
- package/src/utils/debug.ts +49 -0
- 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
|