@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,209 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache'
|
|
2
|
+
import type { Abi } from 'viem'
|
|
3
|
+
import type { DecoderResult, Info } from '../decoder/types'
|
|
4
|
+
import type { DataKey, EnhancedInfo } from '../types'
|
|
5
|
+
import type { CacheStats } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Multi-level caching system for server-side decoder
|
|
9
|
+
*/
|
|
10
|
+
export class ServerDecoderCaches {
|
|
11
|
+
// Level 1: Decoded transaction cache (keyed by transaction hash)
|
|
12
|
+
private transactionCache: LRUCache<string, DecoderResult>
|
|
13
|
+
private transactionStats = { hits: 0, misses: 0 }
|
|
14
|
+
|
|
15
|
+
// Level 2: Address metadata cache (keyed by address or address:tokenId)
|
|
16
|
+
private addressCache: LRUCache<string, EnhancedInfo>
|
|
17
|
+
private addressStats = { hits: 0, misses: 0 }
|
|
18
|
+
|
|
19
|
+
// Level 3: Contract ABI cache (keyed by address)
|
|
20
|
+
private abiCache: LRUCache<string, Abi>
|
|
21
|
+
private abiStats = { hits: 0, misses: 0 }
|
|
22
|
+
|
|
23
|
+
// Level 4: Schema decode cache (keyed by key:value)
|
|
24
|
+
private schemaCache: LRUCache<string, Info>
|
|
25
|
+
private schemaStats = { hits: 0, misses: 0 }
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
options: {
|
|
29
|
+
transactionCacheSize?: number
|
|
30
|
+
addressCacheSize?: number
|
|
31
|
+
abiCacheSize?: number
|
|
32
|
+
schemaCacheSize?: number
|
|
33
|
+
} = {}
|
|
34
|
+
) {
|
|
35
|
+
// Transaction cache - store complete decoded results
|
|
36
|
+
this.transactionCache = new LRUCache<string, DecoderResult>({
|
|
37
|
+
max: options.transactionCacheSize || 10000,
|
|
38
|
+
ttl: 1000 * 60 * 60, // 1 hour TTL
|
|
39
|
+
updateAgeOnGet: true,
|
|
40
|
+
updateAgeOnHas: true,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Address cache - store profile/asset metadata
|
|
44
|
+
this.addressCache = new LRUCache<string, EnhancedInfo>({
|
|
45
|
+
max: options.addressCacheSize || 50000,
|
|
46
|
+
ttl: 1000 * 60 * 60 * 24, // 24 hour TTL
|
|
47
|
+
updateAgeOnGet: true,
|
|
48
|
+
updateAgeOnHas: true,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// ABI cache - store contract ABIs
|
|
52
|
+
this.abiCache = new LRUCache<string, Abi>({
|
|
53
|
+
max: options.abiCacheSize || 5000,
|
|
54
|
+
ttl: 1000 * 60 * 60 * 24 * 7, // 7 day TTL (ABIs rarely change)
|
|
55
|
+
updateAgeOnGet: true,
|
|
56
|
+
updateAgeOnHas: true,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Schema cache - store decoded schema values
|
|
60
|
+
this.schemaCache = new LRUCache<string, Info>({
|
|
61
|
+
max: options.schemaCacheSize || 10000,
|
|
62
|
+
ttl: 1000 * 60 * 60, // 1 hour TTL
|
|
63
|
+
updateAgeOnGet: true,
|
|
64
|
+
updateAgeOnHas: true,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get cache key for a data key (address or address:tokenId)
|
|
70
|
+
*/
|
|
71
|
+
private getDataKeyCacheKey(key: DataKey): string {
|
|
72
|
+
return key.toLowerCase()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Transaction cache methods
|
|
76
|
+
getTransaction(hash: string): DecoderResult | undefined {
|
|
77
|
+
const result = this.transactionCache.get(hash.toLowerCase())
|
|
78
|
+
if (result) {
|
|
79
|
+
this.transactionStats.hits++
|
|
80
|
+
return { ...result, cached: true }
|
|
81
|
+
}
|
|
82
|
+
this.transactionStats.misses++
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setTransaction(hash: string, result: DecoderResult): void {
|
|
87
|
+
this.transactionCache.set(hash.toLowerCase(), result)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Address cache methods
|
|
91
|
+
getAddress(key: DataKey): EnhancedInfo | undefined {
|
|
92
|
+
const cacheKey = this.getDataKeyCacheKey(key)
|
|
93
|
+
const result = this.addressCache.get(cacheKey)
|
|
94
|
+
if (result) {
|
|
95
|
+
this.addressStats.hits++
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
this.addressStats.misses++
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setAddress(key: DataKey, data: EnhancedInfo): void {
|
|
103
|
+
const cacheKey = this.getDataKeyCacheKey(key)
|
|
104
|
+
this.addressCache.set(cacheKey, data)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
hasAddress(key: DataKey): boolean {
|
|
108
|
+
return this.addressCache.has(this.getDataKeyCacheKey(key))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getAddresses(keys: DataKey[]): Map<DataKey, EnhancedInfo> {
|
|
112
|
+
const results = new Map<DataKey, EnhancedInfo>()
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
const data = this.getAddress(key)
|
|
115
|
+
if (data) {
|
|
116
|
+
results.set(key, data)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return results
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ABI cache methods
|
|
123
|
+
getAbi(address: string): Abi | undefined {
|
|
124
|
+
const result = this.abiCache.get(address.toLowerCase())
|
|
125
|
+
if (result) {
|
|
126
|
+
this.abiStats.hits++
|
|
127
|
+
return result
|
|
128
|
+
}
|
|
129
|
+
this.abiStats.misses++
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setAbi(address: string, abi: Abi): void {
|
|
134
|
+
this.abiCache.set(address.toLowerCase(), abi)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
hasAbi(address: string): boolean {
|
|
138
|
+
return this.abiCache.has(address.toLowerCase())
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Schema cache methods
|
|
142
|
+
getSchema(key: string, value: string): Info | undefined {
|
|
143
|
+
const cacheKey = `${key}:${value}`
|
|
144
|
+
const result = this.schemaCache.get(cacheKey)
|
|
145
|
+
if (result) {
|
|
146
|
+
this.schemaStats.hits++
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
this.schemaStats.misses++
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setSchema(key: string, value: string, info: Info): void {
|
|
154
|
+
const cacheKey = `${key}:${value}`
|
|
155
|
+
this.schemaCache.set(cacheKey, info)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Cache management
|
|
159
|
+
clear(): void {
|
|
160
|
+
this.transactionCache.clear()
|
|
161
|
+
this.addressCache.clear()
|
|
162
|
+
this.abiCache.clear()
|
|
163
|
+
this.schemaCache.clear()
|
|
164
|
+
|
|
165
|
+
// Reset stats
|
|
166
|
+
this.transactionStats = { hits: 0, misses: 0 }
|
|
167
|
+
this.addressStats = { hits: 0, misses: 0 }
|
|
168
|
+
this.abiStats = { hits: 0, misses: 0 }
|
|
169
|
+
this.schemaStats = { hits: 0, misses: 0 }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getStats(): CacheStats {
|
|
173
|
+
const calculateHitRate = (stats: { hits: number; misses: number }) => {
|
|
174
|
+
const total = stats.hits + stats.misses
|
|
175
|
+
return total > 0 ? stats.hits / total : 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
transactions: {
|
|
180
|
+
size: this.transactionCache.size,
|
|
181
|
+
maxSize: this.transactionCache.max,
|
|
182
|
+
hits: this.transactionStats.hits,
|
|
183
|
+
misses: this.transactionStats.misses,
|
|
184
|
+
hitRate: calculateHitRate(this.transactionStats),
|
|
185
|
+
},
|
|
186
|
+
addresses: {
|
|
187
|
+
size: this.addressCache.size,
|
|
188
|
+
maxSize: this.addressCache.max,
|
|
189
|
+
hits: this.addressStats.hits,
|
|
190
|
+
misses: this.addressStats.misses,
|
|
191
|
+
hitRate: calculateHitRate(this.addressStats),
|
|
192
|
+
},
|
|
193
|
+
abis: {
|
|
194
|
+
size: this.abiCache.size,
|
|
195
|
+
maxSize: this.abiCache.max,
|
|
196
|
+
hits: this.abiStats.hits,
|
|
197
|
+
misses: this.abiStats.misses,
|
|
198
|
+
hitRate: calculateHitRate(this.abiStats),
|
|
199
|
+
},
|
|
200
|
+
schemas: {
|
|
201
|
+
size: this.schemaCache.size,
|
|
202
|
+
maxSize: this.schemaCache.max,
|
|
203
|
+
hits: this.schemaStats.hits,
|
|
204
|
+
misses: this.schemaStats.misses,
|
|
205
|
+
hitRate: calculateHitRate(this.schemaStats),
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Transaction } from 'viem'
|
|
2
|
+
import { collectDataKeys } from '../core/addressCollector'
|
|
3
|
+
import { pluginRegistry } from '../decoder/registry'
|
|
4
|
+
import { decodeTransaction as decodeTransactionCore } from '../decoder/transaction'
|
|
5
|
+
import type { DecoderResult } from '../decoder/types'
|
|
6
|
+
import { needsEnhancement } from '../decoder/utils'
|
|
7
|
+
import type { ServerDecoderCaches } from './caches'
|
|
8
|
+
import type { ServerDecoderOptions } from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decode a single transaction with time-bounded enhancement
|
|
12
|
+
*/
|
|
13
|
+
export async function decodeTransactionSync(
|
|
14
|
+
transaction: DecoderResult,
|
|
15
|
+
options: ServerDecoderOptions,
|
|
16
|
+
caches: ServerDecoderCaches
|
|
17
|
+
): Promise<DecoderResult> {
|
|
18
|
+
const startTime = Date.now()
|
|
19
|
+
const { timeoutMs = 800, enableEnhancement = true, chain } = options
|
|
20
|
+
|
|
21
|
+
// Check transaction cache first
|
|
22
|
+
if (transaction.hash) {
|
|
23
|
+
const cached = caches.getTransaction(transaction.hash)
|
|
24
|
+
if (cached) {
|
|
25
|
+
return cached
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Phase 1: Immediate decode (sync plugins only)
|
|
31
|
+
const phase1Start = Date.now()
|
|
32
|
+
const plugins = await pluginRegistry.getAll({ syncOnly: true })
|
|
33
|
+
const schemaPlugins = pluginRegistry.getAllSchema()
|
|
34
|
+
|
|
35
|
+
const immediateOptions = {
|
|
36
|
+
chain,
|
|
37
|
+
plugins,
|
|
38
|
+
schemaPlugins,
|
|
39
|
+
wrappers: [],
|
|
40
|
+
async: false,
|
|
41
|
+
}
|
|
42
|
+
const immediateResult = await decodeTransactionCore(
|
|
43
|
+
transaction,
|
|
44
|
+
immediateOptions
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (!immediateResult) {
|
|
48
|
+
return {
|
|
49
|
+
...createErrorResult(transaction),
|
|
50
|
+
phase: 'immediate',
|
|
51
|
+
cached: false,
|
|
52
|
+
timeTaken: Date.now() - startTime,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let currentResult: DecoderResult = immediateResult
|
|
57
|
+
let phase: 'immediate' | 'enhanced' | 'complete' = 'immediate'
|
|
58
|
+
|
|
59
|
+
// Transfer collected wrappers if any
|
|
60
|
+
if (immediateOptions.wrappers.length > 0) {
|
|
61
|
+
;(currentResult as any).wrappers = immediateOptions.wrappers
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Collect addresses from the decoded result
|
|
65
|
+
const addresses = collectDataKeys(currentResult)
|
|
66
|
+
currentResult.addresses = addresses
|
|
67
|
+
|
|
68
|
+
// Check if we have time for enhancement
|
|
69
|
+
const phase1Time = Date.now() - phase1Start
|
|
70
|
+
if (enableEnhancement && phase1Time < timeoutMs * 0.6) {
|
|
71
|
+
// Check if enhancement is actually needed
|
|
72
|
+
if (!needsEnhancement(currentResult)) {
|
|
73
|
+
// Already fully decoded during immediate phase
|
|
74
|
+
phase = 'enhanced'
|
|
75
|
+
} else {
|
|
76
|
+
// Phase 2: Enhancement with async plugins
|
|
77
|
+
try {
|
|
78
|
+
const allPlugins = await pluginRegistry.getAll()
|
|
79
|
+
const enhancedOptions = {
|
|
80
|
+
chain,
|
|
81
|
+
plugins: allPlugins,
|
|
82
|
+
schemaPlugins,
|
|
83
|
+
wrappers: [],
|
|
84
|
+
async: true,
|
|
85
|
+
}
|
|
86
|
+
const enhancedResult = await Promise.race([
|
|
87
|
+
decodeTransactionCore(transaction, enhancedOptions),
|
|
88
|
+
// Timeout for phase 2
|
|
89
|
+
new Promise<null>((resolve) =>
|
|
90
|
+
setTimeout(() => resolve(null), timeoutMs * 0.6 - phase1Time)
|
|
91
|
+
),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
if (enhancedResult) {
|
|
95
|
+
currentResult = enhancedResult
|
|
96
|
+
phase = 'enhanced'
|
|
97
|
+
|
|
98
|
+
// Merge wrappers from both phases (immediate + enhanced)
|
|
99
|
+
const allWrappers = [
|
|
100
|
+
...((immediateOptions.wrappers || []) as any[]),
|
|
101
|
+
...(enhancedOptions.wrappers || []),
|
|
102
|
+
].filter(
|
|
103
|
+
(w, i, arr) =>
|
|
104
|
+
// Deduplicate by checking if this is the first occurrence
|
|
105
|
+
arr.findIndex(
|
|
106
|
+
(x) => JSON.stringify(x) === JSON.stringify(w)
|
|
107
|
+
) === i
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if (allWrappers.length > 0) {
|
|
111
|
+
;(currentResult as any).wrappers = allWrappers
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Re-collect addresses from enhanced result
|
|
115
|
+
currentResult.addresses = collectDataKeys(currentResult)
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Enhancement failed:', error)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result: DecoderResult = {
|
|
124
|
+
...currentResult,
|
|
125
|
+
phase,
|
|
126
|
+
cached: false,
|
|
127
|
+
timeTaken: Date.now() - startTime,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Cache the result if we have a hash
|
|
131
|
+
if (transaction.hash) {
|
|
132
|
+
caches.setTransaction(transaction.hash, result)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Failed to decode transaction:', error)
|
|
138
|
+
return {
|
|
139
|
+
...createErrorResult(transaction),
|
|
140
|
+
phase: 'immediate',
|
|
141
|
+
cached: false,
|
|
142
|
+
timeTaken: Date.now() - startTime,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createErrorResult(transaction: DecoderResult): DecoderResult {
|
|
148
|
+
return {
|
|
149
|
+
...transaction,
|
|
150
|
+
isDecoded: false,
|
|
151
|
+
resultType: 'error',
|
|
152
|
+
errorType: 'UNKNOWN',
|
|
153
|
+
sig: '0x' as `0x${string}`,
|
|
154
|
+
addresses: [],
|
|
155
|
+
} as DecoderResult
|
|
156
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { collectDataKeys } from '../core/addressCollector'
|
|
2
|
+
import { pluginRegistry } from '../decoder/registry'
|
|
3
|
+
import { decodeTransaction as decodeTransactionCore } from '../decoder/transaction'
|
|
4
|
+
import type { DecoderResult, ResultShared } from '../decoder/types'
|
|
5
|
+
import { needsEnhancement } from '../decoder/utils'
|
|
6
|
+
import type { ServerDecoderCaches } from './caches'
|
|
7
|
+
import type { BatchDecodeResult, ServerDecoderOptions } from './types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Decode multiple transactions with batching and time budget
|
|
11
|
+
*/
|
|
12
|
+
export async function decodeTransactionsBatch(
|
|
13
|
+
transactions: DecoderResult[],
|
|
14
|
+
options: ServerDecoderOptions,
|
|
15
|
+
caches: ServerDecoderCaches
|
|
16
|
+
): Promise<BatchDecodeResult> {
|
|
17
|
+
const startTime = Date.now()
|
|
18
|
+
const { timeoutMs = 800, enableEnhancement = true, chain } = options
|
|
19
|
+
|
|
20
|
+
// Step 1: Check cache for all transactions
|
|
21
|
+
const results: (DecoderResult | null)[] = transactions.map((tx) => {
|
|
22
|
+
if (tx.transactionHash) {
|
|
23
|
+
const cached = caches.getTransaction(tx.transactionHash)
|
|
24
|
+
if (cached) return cached
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Find uncached transactions
|
|
30
|
+
const uncachedIndexes: number[] = []
|
|
31
|
+
const uncachedTransactions: DecoderResult[] = []
|
|
32
|
+
results.forEach((result, index) => {
|
|
33
|
+
if (!result) {
|
|
34
|
+
uncachedIndexes.push(index)
|
|
35
|
+
uncachedTransactions.push(transactions[index])
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// If all are cached, return early
|
|
40
|
+
if (uncachedTransactions.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
results: results as DecoderResult[],
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Get plugins once for all transactions
|
|
48
|
+
const syncPlugins = await pluginRegistry.getAll({ syncOnly: true })
|
|
49
|
+
const allPlugins = await pluginRegistry.getAll()
|
|
50
|
+
const schemaPlugins = pluginRegistry.getAllSchema()
|
|
51
|
+
|
|
52
|
+
// Phase 1: Immediate decode for all uncached transactions
|
|
53
|
+
const phase1Start = Date.now()
|
|
54
|
+
const immediateOptionsArray = uncachedTransactions.map(() => ({
|
|
55
|
+
chain,
|
|
56
|
+
plugins: syncPlugins,
|
|
57
|
+
schemaPlugins,
|
|
58
|
+
wrappers: [],
|
|
59
|
+
async: false,
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
const immediateResults: DecoderResult[] = (
|
|
63
|
+
await Promise.all(
|
|
64
|
+
uncachedTransactions.map(async (tx, i) => {
|
|
65
|
+
return await decodeTransactionCore(tx, immediateOptionsArray[i])
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
).filter(Boolean) as DecoderResult[]
|
|
69
|
+
|
|
70
|
+
// Create result objects and collect addresses
|
|
71
|
+
const newResults: DecoderResult[] = immediateResults
|
|
72
|
+
.map((result, i) => {
|
|
73
|
+
const data = result || createErrorResult(uncachedTransactions[i])
|
|
74
|
+
|
|
75
|
+
// Transfer wrappers from immediate phase options
|
|
76
|
+
if (immediateOptionsArray[i].wrappers.length > 0) {
|
|
77
|
+
;(data as any).wrappers = immediateOptionsArray[i].wrappers
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Collect addresses from the decoded result
|
|
81
|
+
data.addresses = collectDataKeys(data)
|
|
82
|
+
;(data as ResultShared).timeTaken = Date.now() - startTime
|
|
83
|
+
return data
|
|
84
|
+
})
|
|
85
|
+
.filter(Boolean) as DecoderResult[]
|
|
86
|
+
|
|
87
|
+
// Check if we have time for enhancement
|
|
88
|
+
const phase1Time = Date.now() - phase1Start
|
|
89
|
+
if (enableEnhancement && phase1Time < timeoutMs * 0.6) {
|
|
90
|
+
// Phase 2: Enhancement with async plugins (batch all transactions)
|
|
91
|
+
try {
|
|
92
|
+
const enhancedOptionsArray = uncachedTransactions.map(() => ({
|
|
93
|
+
chain,
|
|
94
|
+
plugins: allPlugins,
|
|
95
|
+
schemaPlugins,
|
|
96
|
+
wrappers: [],
|
|
97
|
+
async: true,
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
const enhancePromises = uncachedTransactions.map((tx, i) => {
|
|
101
|
+
// Only enhance if immediate decode succeeded
|
|
102
|
+
if (!immediateResults[i]) return null
|
|
103
|
+
|
|
104
|
+
// Check if enhancement is actually needed
|
|
105
|
+
if (!needsEnhancement(immediateResults[i])) {
|
|
106
|
+
// Already fully decoded during immediate phase
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return decodeTransactionCore(tx, enhancedOptionsArray[i])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Wait for all enhancements with remaining time budget
|
|
114
|
+
const remainingTime = timeoutMs * 0.6 - phase1Time
|
|
115
|
+
const enhancedResults = await Promise.all(enhancePromises)
|
|
116
|
+
|
|
117
|
+
// Update results with enhanced data
|
|
118
|
+
enhancedResults.forEach((enhanced, i) => {
|
|
119
|
+
if (enhanced) {
|
|
120
|
+
// Merge wrappers from both phases (immediate + enhanced)
|
|
121
|
+
const immediateWrappers = immediateOptionsArray[i].wrappers || []
|
|
122
|
+
const enhancedWrappers = enhancedOptionsArray[i].wrappers || []
|
|
123
|
+
const allWrappers = [
|
|
124
|
+
...immediateWrappers,
|
|
125
|
+
...enhancedWrappers,
|
|
126
|
+
].filter(
|
|
127
|
+
(w, idx, arr) =>
|
|
128
|
+
// Deduplicate by checking if this is the first occurrence
|
|
129
|
+
arr.findIndex(
|
|
130
|
+
(x) => JSON.stringify(x) === JSON.stringify(w)
|
|
131
|
+
) === idx
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// Enhancement returned a new result
|
|
135
|
+
newResults[i] = enhanced
|
|
136
|
+
|
|
137
|
+
// Add merged wrappers if any
|
|
138
|
+
if (allWrappers.length > 0) {
|
|
139
|
+
;(newResults[i] as any).wrappers = allWrappers
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Re-collect addresses from enhanced result
|
|
143
|
+
newResults[i].addresses = collectDataKeys(enhanced)
|
|
144
|
+
newResults[i].phase = 'enhanced'
|
|
145
|
+
} else if (
|
|
146
|
+
immediateResults[i] &&
|
|
147
|
+
!needsEnhancement(immediateResults[i])
|
|
148
|
+
) {
|
|
149
|
+
// Enhancement was skipped because already fully decoded
|
|
150
|
+
newResults[i].phase = 'enhanced'
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Batch enhancement failed:', error)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Update timings
|
|
159
|
+
const totalTime = Date.now() - startTime
|
|
160
|
+
newResults.forEach((result) => {
|
|
161
|
+
result.timeTaken = totalTime
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Cache all new results
|
|
165
|
+
newResults.forEach((result, i) => {
|
|
166
|
+
const tx = uncachedTransactions[i]
|
|
167
|
+
if (tx.hash) {
|
|
168
|
+
caches.setTransaction(tx.hash, result)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// Merge cached and new results
|
|
173
|
+
uncachedIndexes.forEach((originalIndex, i) => {
|
|
174
|
+
results[originalIndex] = newResults[i]
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
results: results as DecoderResult[],
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Batch decode failed:', error)
|
|
182
|
+
// Return error results for uncached transactions
|
|
183
|
+
uncachedIndexes.forEach((originalIndex) => {
|
|
184
|
+
results[originalIndex] = {
|
|
185
|
+
...createErrorResult(transactions[originalIndex]),
|
|
186
|
+
phase: 'immediate',
|
|
187
|
+
cached: false,
|
|
188
|
+
timeTaken: Date.now() - startTime,
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
results: results as DecoderResult[],
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function createErrorResult(transaction: DecoderResult): DecoderResult {
|
|
199
|
+
return {
|
|
200
|
+
...transaction,
|
|
201
|
+
isDecoded: false,
|
|
202
|
+
resultType: 'error',
|
|
203
|
+
errorType: 'UNKNOWN',
|
|
204
|
+
sig: '0x' as `0x${string}`,
|
|
205
|
+
addresses: [],
|
|
206
|
+
} as DecoderResult
|
|
207
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { collectDataKeys } from '../core/addressCollector'
|
|
2
|
+
import { pluginRegistry } from '../decoder/registry'
|
|
3
|
+
import { decodeTransaction as decodeTransactionCore } from '../decoder/transaction'
|
|
4
|
+
import type { DecoderResult, ResultShared } from '../decoder/types'
|
|
5
|
+
import { createDebug } from '../utils/debug'
|
|
6
|
+
import type { ServerDecoderCaches } from './caches'
|
|
7
|
+
import type { ServerDecoderOptions } from './types'
|
|
8
|
+
|
|
9
|
+
const debug = createDebug('decoder:finishDecoding')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Finish decoding a transaction by running enhancement phase
|
|
13
|
+
* This only does phase 2 (async plugins) - address resolution should be done separately
|
|
14
|
+
*/
|
|
15
|
+
export async function finishDecoding(
|
|
16
|
+
transaction: DecoderResult,
|
|
17
|
+
options: ServerDecoderOptions,
|
|
18
|
+
caches: ServerDecoderCaches
|
|
19
|
+
): Promise<DecoderResult> {
|
|
20
|
+
const startTime = Date.now()
|
|
21
|
+
const {
|
|
22
|
+
timeoutMs = 5000, // Give more time for finishing
|
|
23
|
+
chain,
|
|
24
|
+
} = options
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Check cache first
|
|
28
|
+
const txHash = transaction.hash || transaction.transactionHash
|
|
29
|
+
if (txHash) {
|
|
30
|
+
const cached = caches.getTransaction(txHash)
|
|
31
|
+
if (cached && cached.enhancementAttempted) {
|
|
32
|
+
return {
|
|
33
|
+
...cached,
|
|
34
|
+
cached: true,
|
|
35
|
+
timeTaken: 0,
|
|
36
|
+
} as DecoderResult
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get all plugins
|
|
41
|
+
const allPlugins = await pluginRegistry.getAll()
|
|
42
|
+
const schemaPlugins = pluginRegistry.getAllSchema()
|
|
43
|
+
|
|
44
|
+
const enhancedOptions = {
|
|
45
|
+
chain,
|
|
46
|
+
plugins: allPlugins,
|
|
47
|
+
schemaPlugins,
|
|
48
|
+
wrappers: [],
|
|
49
|
+
async: true,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Wrappers and children must be removed when finishing decoding, because otherwise
|
|
53
|
+
// the decoder will build hierarchical structures which are infinite.
|
|
54
|
+
const {
|
|
55
|
+
wrappers,
|
|
56
|
+
children: _children,
|
|
57
|
+
addresses: _addresses,
|
|
58
|
+
...transactionWithoutWrappers
|
|
59
|
+
} = transaction as DecoderResult & {
|
|
60
|
+
children?: unknown[]
|
|
61
|
+
wrappers?: unknown[]
|
|
62
|
+
addresses?: unknown[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Simply pass the entire transaction hierarchy to decodeTransactionCore
|
|
66
|
+
// It will handle children properly and return a fully enhanced copy
|
|
67
|
+
const enhancedResult = await decodeTransactionCore(
|
|
68
|
+
transactionWithoutWrappers as DecoderResult,
|
|
69
|
+
enhancedOptions
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if (!enhancedResult) {
|
|
73
|
+
if (txHash) {
|
|
74
|
+
caches.setTransaction(txHash, {
|
|
75
|
+
...transaction,
|
|
76
|
+
enhancementAttempted: true,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return transaction
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = {
|
|
83
|
+
...enhancedResult,
|
|
84
|
+
// Only add wrappers if there are any
|
|
85
|
+
...((wrappers?.length || 0) > 0 ? { wrappers } : {}),
|
|
86
|
+
cached: false,
|
|
87
|
+
timeTaken: Date.now() - startTime,
|
|
88
|
+
enhancementAttempted: true,
|
|
89
|
+
} as DecoderResult
|
|
90
|
+
|
|
91
|
+
// Always update cache with the enhanced result
|
|
92
|
+
if (txHash) {
|
|
93
|
+
caches.setTransaction(txHash, result)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Failed to finish decoding transaction:', error)
|
|
99
|
+
return {
|
|
100
|
+
...createErrorResult(transaction),
|
|
101
|
+
phase: 'immediate',
|
|
102
|
+
cached: false,
|
|
103
|
+
timeTaken: Date.now() - startTime,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createErrorResult(transaction: DecoderResult): DecoderResult {
|
|
109
|
+
return {
|
|
110
|
+
...transaction,
|
|
111
|
+
resultType: 'error',
|
|
112
|
+
errorType: 'UNKNOWN',
|
|
113
|
+
sig: '0x' as `0x${string}`,
|
|
114
|
+
addresses: [],
|
|
115
|
+
} as DecoderResult
|
|
116
|
+
}
|