@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,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
+ }