viem 3.0.0-next.12 → 3.0.0-next.13

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 (185) hide show
  1. package/dist/chains/definitions/creditCoin3Devnet.d.ts +6 -0
  2. package/dist/chains/definitions/creditCoin3Devnet.d.ts.map +1 -1
  3. package/dist/chains/definitions/creditCoin3Devnet.js +6 -0
  4. package/dist/chains/definitions/creditCoin3Devnet.js.map +1 -1
  5. package/dist/chains/definitions/creditCoin3Mainnet.d.ts +5 -1
  6. package/dist/chains/definitions/creditCoin3Mainnet.d.ts.map +1 -1
  7. package/dist/chains/definitions/creditCoin3Mainnet.js +4 -0
  8. package/dist/chains/definitions/creditCoin3Mainnet.js.map +1 -1
  9. package/dist/chains/definitions/creditCoin3Testnet.d.ts +5 -1
  10. package/dist/chains/definitions/creditCoin3Testnet.d.ts.map +1 -1
  11. package/dist/chains/definitions/creditCoin3Testnet.js +4 -0
  12. package/dist/chains/definitions/creditCoin3Testnet.js.map +1 -1
  13. package/dist/chains/definitions/crymad.d.ts +25 -0
  14. package/dist/chains/definitions/crymad.d.ts.map +1 -0
  15. package/dist/chains/definitions/crymad.js +23 -0
  16. package/dist/chains/definitions/crymad.js.map +1 -0
  17. package/dist/chains/definitions/crymadL2.d.ts +26 -0
  18. package/dist/chains/definitions/crymadL2.d.ts.map +1 -0
  19. package/dist/chains/definitions/crymadL2.js +24 -0
  20. package/dist/chains/definitions/crymadL2.js.map +1 -0
  21. package/dist/chains/definitions/kortana.d.ts +18 -0
  22. package/dist/chains/definitions/kortana.d.ts.map +1 -0
  23. package/dist/chains/definitions/kortana.js +16 -0
  24. package/dist/chains/definitions/kortana.js.map +1 -0
  25. package/dist/chains/index.d.ts +3 -0
  26. package/dist/chains/index.d.ts.map +1 -1
  27. package/dist/chains/index.js +3 -0
  28. package/dist/chains/index.js.map +1 -1
  29. package/dist/core/actions/wallet/sendCalls.d.ts.map +1 -1
  30. package/dist/core/actions/wallet/sendCalls.js +1 -0
  31. package/dist/core/actions/wallet/sendCalls.js.map +1 -1
  32. package/dist/erc4337/actions/userOperation/waitForReceipt.js +1 -1
  33. package/dist/erc4337/actions/userOperation/waitForReceipt.js.map +1 -1
  34. package/dist/node/transports/ipc.d.ts.map +1 -1
  35. package/dist/node/transports/ipc.js +20 -2
  36. package/dist/node/transports/ipc.js.map +1 -1
  37. package/dist/tempo/Abis.d.ts +778 -174
  38. package/dist/tempo/Abis.d.ts.map +1 -1
  39. package/dist/tempo/Abis.js +178 -37
  40. package/dist/tempo/Abis.js.map +1 -1
  41. package/dist/tempo/Capabilities.d.ts +7 -13
  42. package/dist/tempo/Capabilities.d.ts.map +1 -1
  43. package/dist/tempo/Client.d.ts +0 -5
  44. package/dist/tempo/Client.d.ts.map +1 -1
  45. package/dist/tempo/Client.js +3 -10
  46. package/dist/tempo/Client.js.map +1 -1
  47. package/dist/tempo/ExecutionError.d.ts +81 -0
  48. package/dist/tempo/ExecutionError.d.ts.map +1 -0
  49. package/dist/tempo/ExecutionError.js +309 -0
  50. package/dist/tempo/ExecutionError.js.map +1 -0
  51. package/dist/tempo/Relay.d.ts +303 -0
  52. package/dist/tempo/Relay.d.ts.map +1 -0
  53. package/dist/tempo/Relay.js +170 -0
  54. package/dist/tempo/Relay.js.map +1 -0
  55. package/dist/tempo/Selectors.d.ts +16 -5
  56. package/dist/tempo/Selectors.d.ts.map +1 -1
  57. package/dist/tempo/Selectors.js +16 -5
  58. package/dist/tempo/Selectors.js.map +1 -1
  59. package/dist/tempo/Transport.d.ts +11 -11
  60. package/dist/tempo/Transport.d.ts.map +1 -1
  61. package/dist/tempo/Transport.js +22 -51
  62. package/dist/tempo/Transport.js.map +1 -1
  63. package/dist/tempo/actions/token/burn.d.ts +38 -9
  64. package/dist/tempo/actions/token/burn.d.ts.map +1 -1
  65. package/dist/tempo/actions/token/burn.js +12 -5
  66. package/dist/tempo/actions/token/burn.js.map +1 -1
  67. package/dist/tempo/actions/token/burnSync.d.ts +2 -0
  68. package/dist/tempo/actions/token/burnSync.d.ts.map +1 -1
  69. package/dist/tempo/chainConfig.d.ts +4 -5
  70. package/dist/tempo/chainConfig.d.ts.map +1 -1
  71. package/dist/tempo/chainConfig.js +6 -1
  72. package/dist/tempo/chainConfig.js.map +1 -1
  73. package/dist/tempo/index.d.ts +5 -3
  74. package/dist/tempo/index.d.ts.map +1 -1
  75. package/dist/tempo/index.js +5 -3
  76. package/dist/tempo/index.js.map +1 -1
  77. package/dist/tempo/internal/relay/cache.d.ts +9 -0
  78. package/dist/tempo/internal/relay/cache.d.ts.map +1 -0
  79. package/dist/tempo/internal/relay/cache.js +72 -0
  80. package/dist/tempo/internal/relay/cache.js.map +1 -0
  81. package/dist/tempo/internal/relay/deadline.d.ts +4 -0
  82. package/dist/tempo/internal/relay/deadline.d.ts.map +1 -0
  83. package/dist/tempo/internal/relay/deadline.js +34 -0
  84. package/dist/tempo/internal/relay/deadline.js.map +1 -0
  85. package/dist/tempo/internal/relay/error.d.ts +47 -0
  86. package/dist/tempo/internal/relay/error.d.ts.map +1 -0
  87. package/dist/tempo/internal/relay/error.js +101 -0
  88. package/dist/tempo/internal/relay/error.js.map +1 -0
  89. package/dist/tempo/internal/relay/feePayer.d.ts +70 -0
  90. package/dist/tempo/internal/relay/feePayer.d.ts.map +1 -0
  91. package/dist/tempo/internal/relay/feePayer.js +442 -0
  92. package/dist/tempo/internal/relay/feePayer.js.map +1 -0
  93. package/dist/tempo/internal/relay/feeToken.d.ts +38 -0
  94. package/dist/tempo/internal/relay/feeToken.d.ts.map +1 -0
  95. package/dist/tempo/internal/relay/feeToken.js +143 -0
  96. package/dist/tempo/internal/relay/feeToken.js.map +1 -0
  97. package/dist/tempo/internal/relay/multisig.d.ts +4 -0
  98. package/dist/tempo/internal/relay/multisig.d.ts.map +1 -0
  99. package/dist/tempo/{Multisig.js → internal/relay/multisig.js} +134 -126
  100. package/dist/tempo/internal/relay/multisig.js.map +1 -0
  101. package/dist/tempo/internal/relay/plugin.d.ts +5 -0
  102. package/dist/tempo/internal/relay/plugin.d.ts.map +1 -0
  103. package/dist/tempo/internal/relay/plugin.js +10 -0
  104. package/dist/tempo/internal/relay/plugin.js.map +1 -0
  105. package/dist/tempo/internal/relay/preflight.d.ts +17 -0
  106. package/dist/tempo/internal/relay/preflight.d.ts.map +1 -0
  107. package/dist/tempo/internal/relay/preflight.js +53 -0
  108. package/dist/tempo/internal/relay/preflight.js.map +1 -0
  109. package/dist/tempo/internal/relay/request.d.ts +16 -0
  110. package/dist/tempo/internal/relay/request.d.ts.map +1 -0
  111. package/dist/tempo/internal/relay/request.js +327 -0
  112. package/dist/tempo/internal/relay/request.js.map +1 -0
  113. package/dist/tempo/internal/relay/simulate.d.ts +62 -0
  114. package/dist/tempo/internal/relay/simulate.d.ts.map +1 -0
  115. package/dist/tempo/internal/relay/simulate.js +284 -0
  116. package/dist/tempo/internal/relay/simulate.js.map +1 -0
  117. package/dist/tempo/internal/relay/utils.d.ts +346 -0
  118. package/dist/tempo/internal/relay/utils.d.ts.map +1 -0
  119. package/dist/tempo/internal/relay/utils.js +185 -0
  120. package/dist/tempo/internal/relay/utils.js.map +1 -0
  121. package/dist/tempo/internal/relay/virtualAddress.d.ts +9 -0
  122. package/dist/tempo/internal/relay/virtualAddress.d.ts.map +1 -0
  123. package/dist/tempo/internal/relay/virtualAddress.js +73 -0
  124. package/dist/tempo/internal/relay/virtualAddress.js.map +1 -0
  125. package/dist/tempo/internal/relay.d.ts +4 -0
  126. package/dist/tempo/internal/relay.d.ts.map +1 -0
  127. package/dist/tempo/internal/relay.js +132 -0
  128. package/dist/tempo/internal/relay.js.map +1 -0
  129. package/dist/tokens/definitions/ousd.d.ts +26 -0
  130. package/dist/tokens/definitions/ousd.d.ts.map +1 -0
  131. package/dist/tokens/definitions/ousd.js +26 -0
  132. package/dist/tokens/definitions/ousd.js.map +1 -0
  133. package/dist/tokens/index.d.ts +2 -0
  134. package/dist/tokens/index.d.ts.map +1 -1
  135. package/dist/tokens/index.js +2 -0
  136. package/dist/tokens/index.js.map +1 -1
  137. package/dist/tokens/sets.d.ts +33 -0
  138. package/dist/tokens/sets.d.ts.map +1 -1
  139. package/dist/tokens/sets.js +4 -1
  140. package/dist/tokens/sets.js.map +1 -1
  141. package/dist/version.d.ts +1 -1
  142. package/dist/version.js +1 -1
  143. package/package.json +1 -1
  144. package/src/chains/definitions/creditCoin3Devnet.ts +6 -0
  145. package/src/chains/definitions/creditCoin3Mainnet.ts +4 -0
  146. package/src/chains/definitions/creditCoin3Testnet.ts +4 -0
  147. package/src/chains/definitions/crymad.ts +23 -0
  148. package/src/chains/definitions/crymadL2.ts +24 -0
  149. package/src/chains/definitions/kortana.ts +16 -0
  150. package/src/chains/index.ts +3 -0
  151. package/src/core/actions/wallet/sendCalls.ts +1 -0
  152. package/src/erc4337/actions/userOperation/waitForReceipt.ts +1 -1
  153. package/src/node/transports/ipc.ts +17 -2
  154. package/src/tempo/Abis.ts +179 -37
  155. package/src/tempo/Account.ts +1 -1
  156. package/src/tempo/Capabilities.ts +7 -15
  157. package/src/tempo/Client.ts +3 -23
  158. package/src/tempo/ExecutionError.ts +428 -0
  159. package/src/tempo/Relay.ts +430 -0
  160. package/src/tempo/Selectors.ts +25 -11
  161. package/src/tempo/Transport.ts +71 -41
  162. package/src/tempo/actions/token/burn.ts +21 -9
  163. package/src/tempo/actions/token/burnSync.ts +2 -0
  164. package/src/tempo/chainConfig.ts +9 -6
  165. package/src/tempo/index.ts +7 -4
  166. package/src/tempo/internal/relay/cache.ts +82 -0
  167. package/src/tempo/internal/relay/deadline.ts +45 -0
  168. package/src/tempo/internal/relay/error.ts +129 -0
  169. package/src/tempo/internal/relay/feePayer.ts +693 -0
  170. package/src/tempo/internal/relay/feeToken.ts +190 -0
  171. package/src/tempo/{Multisig.ts → internal/relay/multisig.ts} +165 -193
  172. package/src/tempo/internal/relay/plugin.ts +13 -0
  173. package/src/tempo/internal/relay/preflight.ts +83 -0
  174. package/src/tempo/internal/relay/request.ts +401 -0
  175. package/src/tempo/internal/relay/simulate.ts +394 -0
  176. package/src/tempo/internal/relay/utils.ts +213 -0
  177. package/src/tempo/internal/relay/virtualAddress.ts +95 -0
  178. package/src/tempo/internal/relay.ts +149 -0
  179. package/src/tokens/definitions/ousd.ts +26 -0
  180. package/src/tokens/index.ts +2 -0
  181. package/src/tokens/sets.ts +4 -1
  182. package/src/version.ts +1 -1
  183. package/dist/tempo/Multisig.d.ts +0 -39
  184. package/dist/tempo/Multisig.d.ts.map +0 -1
  185. package/dist/tempo/Multisig.js.map +0 -1
@@ -0,0 +1,394 @@
1
+ import type { Address } from 'abitype'
2
+ import { AbiEvent, AbiFunction, Hex, Log as Log_ } from 'ox'
3
+ import * as TransactionRequest from 'ox/TransactionRequest'
4
+ import type { Client } from '../../../core/Client.js'
5
+ import type { Call } from 'ox/tempo/TxEnvelopeTempo'
6
+ import type { Log } from 'ox/Log'
7
+ import { format as formatUnits } from 'ox/Value'
8
+ import * as Abis from '../../Abis.js'
9
+ import * as Actions from '../../actions/index.js'
10
+ import type * as Capabilities from '../../Capabilities.js'
11
+ import type * as Relay from '../../Relay.js'
12
+ import type * as Store from './cache.js'
13
+ import { formatError, isExecutionError } from './error.js'
14
+ import { resolveTokenMetadata } from './feeToken.js'
15
+ import * as Utils from './utils.js'
16
+ import { extractCalls } from './virtualAddress.js'
17
+
18
+ const zeroAddress = '0x0000000000000000000000000000000000000000'
19
+
20
+ export function create(options: Relay.simulate.Options): Relay.Plugin {
21
+ return {
22
+ async handleRequest(context, next) {
23
+ if (context.request.method !== 'eth_fillTransaction') return next()
24
+ try {
25
+ await next()
26
+ } catch (error) {
27
+ const parameters = context.request.params![0] as Record<string, unknown>
28
+ if (
29
+ isExecutionError(error) &&
30
+ (parameters.capabilities as Record<string, unknown> | undefined)
31
+ ?.errors === true
32
+ )
33
+ return formatError(
34
+ error,
35
+ parameters,
36
+ context.client,
37
+ context.getStore(options.store),
38
+ )
39
+ throw error
40
+ }
41
+ },
42
+ async afterFill(result, context) {
43
+ const parameters = context.request.params![0] as Record<string, unknown>
44
+ const store = context.getStore(options.store)
45
+ const signal = context.options.signal
46
+ const transaction = Utils.normalizeTempoTransaction(result.tx)
47
+ const feeToken = transaction.feeToken as Address | undefined
48
+ const simulation =
49
+ (parameters.capabilities as Record<string, unknown> | undefined)
50
+ ?.balanceDiffs !== false
51
+ ? await simulateAndParseDiffs(context.client, {
52
+ account: parameters.from as Address | undefined,
53
+ calls: extractCalls(transaction),
54
+ feeToken,
55
+ gas: transaction.gas,
56
+ maxFeePerGas: transaction.maxFeePerGas,
57
+ store,
58
+ signal,
59
+ })
60
+ : await computeFee(context.client, {
61
+ feeToken,
62
+ gas: transaction.gas,
63
+ maxFeePerGas: transaction.maxFeePerGas,
64
+ store,
65
+ signal,
66
+ }).then((fee) => ({ balanceDiffs: undefined, fee }))
67
+ return {
68
+ capabilities: {
69
+ balanceDiffs: simulation.balanceDiffs,
70
+ fee: simulation.fee,
71
+ },
72
+ }
73
+ },
74
+ }
75
+ }
76
+
77
+ export async function simulateAndParseDiffs(
78
+ client: Client,
79
+ options: simulateAndParseDiffs.Options,
80
+ ) {
81
+ const { account, calls, feeToken, gas, store, maxFeePerGas, signal } = options
82
+ signal?.throwIfAborted()
83
+
84
+ try {
85
+ // Including the fee token as a read target asks the node to return its metadata too.
86
+ const probe =
87
+ feeToken &&
88
+ !calls.some((call) => call.to?.toLowerCase() === feeToken.toLowerCase())
89
+ ? [
90
+ Actions.token.getBalance.call(client, {
91
+ account: account ?? zeroAddress,
92
+ token: feeToken,
93
+ }),
94
+ ]
95
+ : []
96
+ const simulation = (await client.request({
97
+ method: 'tempo_simulateV1',
98
+ params: [
99
+ {
100
+ blockStateCalls: [
101
+ {
102
+ calls: [...calls, ...probe].map((call) =>
103
+ TransactionRequest.toRpc({
104
+ ...call,
105
+ ...('abi' in call
106
+ ? {
107
+ to: call.address,
108
+ data: AbiFunction.encodeData(
109
+ AbiFunction.fromAbi(call.abi, call.functionName),
110
+ call.args,
111
+ ),
112
+ }
113
+ : {}),
114
+ from: account === zeroAddress ? undefined : account,
115
+ }),
116
+ ),
117
+ },
118
+ ],
119
+ traceTransfers: true,
120
+ },
121
+ 'latest',
122
+ ],
123
+ })) as {
124
+ blocks: readonly {
125
+ calls?:
126
+ | readonly { logs?: readonly Log_.Rpc[] | undefined }[]
127
+ | undefined
128
+ }[]
129
+ tokenMetadata?:
130
+ | Record<string, { name: string; symbol: string; currency: string }>
131
+ | undefined
132
+ }
133
+ const tokenMetadata = simulation.tokenMetadata ?? {}
134
+ signal?.throwIfAborted()
135
+ const logs = (simulation.blocks[0]?.calls ?? [])
136
+ .slice(0, calls.length)
137
+ .flatMap((result) => result.logs?.map((log) => Log_.fromRpc(log)) ?? [])
138
+
139
+ // Build per-token balance diffs relative to the sender.
140
+ const balanceDiffs = account
141
+ ? await buildBalanceDiffs(client, {
142
+ account,
143
+ store,
144
+ signal,
145
+ logs,
146
+ tokenMetadata: tokenMetadata as never,
147
+ })
148
+ : {}
149
+
150
+ // Compute fee breakdown.
151
+ const fee = await computeFee(client, {
152
+ feeToken,
153
+ gas,
154
+ store,
155
+ signal,
156
+ maxFeePerGas,
157
+ tokenMetadata: tokenMetadata as never,
158
+ })
159
+
160
+ return { balanceDiffs, fee, tokenMetadata }
161
+ } catch {
162
+ signal?.throwIfAborted()
163
+ // Simulation failures should not block the fill response —
164
+ // return empty diffs with fee computed from transaction fields.
165
+ const fee = await computeFee(client, {
166
+ feeToken,
167
+ gas,
168
+ store,
169
+ maxFeePerGas,
170
+ signal,
171
+ })
172
+ return { balanceDiffs: undefined, fee, tokenMetadata: undefined }
173
+ }
174
+ }
175
+
176
+ export declare namespace simulateAndParseDiffs {
177
+ type Options = {
178
+ account?: Address | undefined
179
+ calls: readonly Call[]
180
+ feeToken?: Address | undefined
181
+ gas?: bigint | undefined
182
+ store?: Store.Store | undefined
183
+ signal?: AbortSignal | undefined
184
+ maxFeePerGas?: bigint | undefined
185
+ }
186
+ }
187
+
188
+ /** Builds a complete preview within the metadata lookup budget. @internal */
189
+ export async function buildBalanceDiffs(
190
+ client: Client,
191
+ options: buildBalanceDiffs.Options,
192
+ ) {
193
+ const { account, store, logs, tokenMetadata, signal } = options
194
+ signal?.throwIfAborted()
195
+ const accountLower = account.toLowerCase()
196
+
197
+ const transferLogs = AbiEvent.extractLogs(
198
+ [AbiEvent.fromAbi(Abis.tip20, 'Transfer')],
199
+ logs,
200
+ { eventName: 'Transfer' },
201
+ )
202
+ const approvalLogs = AbiEvent.extractLogs(
203
+ [AbiEvent.fromAbi(Abis.tip20, 'Approval')],
204
+ logs,
205
+ { eventName: 'Approval' },
206
+ )
207
+
208
+ // Track net movement per token: incoming vs outgoing.
209
+ const tokenMap = new Map<
210
+ string,
211
+ {
212
+ incoming: bigint
213
+ outgoing: bigint
214
+ approved: bigint
215
+ recipients: Set<Address>
216
+ token: Address
217
+ }
218
+ >()
219
+
220
+ for (const log of transferLogs) {
221
+ const token = log.address.toLowerCase()
222
+ const fromLower = log.args.from.toLowerCase()
223
+ const toLower = log.args.to.toLowerCase()
224
+
225
+ const entry = tokenMap.get(token) ?? {
226
+ incoming: 0n,
227
+ outgoing: 0n,
228
+ approved: 0n,
229
+ recipients: new Set<Address>(),
230
+ token: log.address,
231
+ }
232
+ if (fromLower === accountLower) {
233
+ entry.outgoing += log.args.amount
234
+ entry.recipients.add(log.args.to)
235
+ }
236
+ if (toLower === accountLower) entry.incoming += log.args.amount
237
+ tokenMap.set(token, entry)
238
+ }
239
+
240
+ // Approvals replace allowances; retain the last event for each token and spender.
241
+ const approvals = new Map<string, (typeof approvalLogs)[number]>()
242
+ for (const log of approvalLogs) {
243
+ if (log.args.owner.toLowerCase() !== accountLower) continue
244
+ approvals.set(
245
+ `${log.address.toLowerCase()}:${log.args.spender.toLowerCase()}`,
246
+ log,
247
+ )
248
+ }
249
+
250
+ // Transfers do not identify allowance consumption, so retain final approval exposure.
251
+ for (const log of approvals.values()) {
252
+ if (log.args.amount === 0n) continue
253
+ const token = log.address.toLowerCase()
254
+
255
+ const entry = tokenMap.get(token) ?? {
256
+ incoming: 0n,
257
+ outgoing: 0n,
258
+ approved: 0n,
259
+ recipients: new Set<Address>(),
260
+ token: log.address,
261
+ }
262
+ entry.approved += log.args.amount
263
+ entry.recipients.add(log.args.spender)
264
+ tokenMap.set(token, entry)
265
+ }
266
+
267
+ // Collect unique tokens that need decimals.
268
+ const entries = [...tokenMap.values()].filter((e) => {
269
+ const net =
270
+ e.outgoing > e.incoming
271
+ ? e.outgoing - e.incoming
272
+ : e.incoming - e.outgoing
273
+ return net > 0n || e.approved > 0n
274
+ })
275
+ if (entries.length === 0) return {}
276
+ // Omit an unavailable preview rather than returning a partial set of movements.
277
+ if (entries.length > 100) return undefined
278
+
279
+ // Bound metadata work for a single fill independently of the RPC batch limit.
280
+ const metadataMap = new Map<
281
+ string,
282
+ { decimals: number; symbol: string; name: string }
283
+ >()
284
+ let index = 0
285
+ await Promise.all(
286
+ Array.from({ length: Math.min(10, entries.length) }, async () => {
287
+ while (index < entries.length) {
288
+ const entry = entries[index++]!
289
+ try {
290
+ const metadata = await resolveTokenMetadata(client, {
291
+ token: entry.token,
292
+ tokenMetadata,
293
+ store,
294
+ })
295
+ metadataMap.set(entry.token.toLowerCase(), metadata)
296
+ } catch {
297
+ signal?.throwIfAborted()
298
+ }
299
+ }
300
+ }),
301
+ )
302
+ signal?.throwIfAborted()
303
+ if (metadataMap.size !== entries.length) return undefined
304
+
305
+ // Build the diff array for this account.
306
+ const diffs: Capabilities.BalanceDiff[] = []
307
+ for (const entry of entries) {
308
+ const incoming =
309
+ entry.incoming > entry.outgoing ? entry.incoming - entry.outgoing : 0n
310
+ const outgoing =
311
+ (entry.outgoing > entry.incoming ? entry.outgoing - entry.incoming : 0n) +
312
+ entry.approved
313
+ const meta = metadataMap.get(entry.token.toLowerCase())!
314
+ const decimals = meta.decimals
315
+ const metadata = {
316
+ address: entry.token,
317
+ decimals,
318
+ name: meta.name,
319
+ symbol: meta.symbol,
320
+ }
321
+ if (incoming > 0n)
322
+ diffs.push({
323
+ ...metadata,
324
+ direction: 'incoming',
325
+ formatted: formatUnits(incoming, decimals),
326
+ recipients: [],
327
+ value: Hex.fromNumber(incoming),
328
+ })
329
+ if (outgoing > 0n)
330
+ diffs.push({
331
+ ...metadata,
332
+ direction: 'outgoing',
333
+ formatted: formatUnits(outgoing, decimals),
334
+ recipients: [...entry.recipients],
335
+ value: Hex.fromNumber(outgoing),
336
+ })
337
+ }
338
+
339
+ return { [account]: diffs }
340
+ }
341
+
342
+ export declare namespace buildBalanceDiffs {
343
+ type Options = {
344
+ account: Address
345
+ store?: Store.Store | undefined
346
+ signal?: AbortSignal | undefined
347
+ logs: readonly Log<boolean>[]
348
+ tokenMetadata: Record<
349
+ Address,
350
+ { name: string; symbol: string; currency: string }
351
+ >
352
+ }
353
+ }
354
+
355
+ // biome-ignore lint/correctness/noUnusedVariables: declaration merge
356
+ async function computeFee(client: Client, options: computeFee.Options) {
357
+ const { feeToken, gas, store, maxFeePerGas, tokenMetadata, signal } = options
358
+ signal?.throwIfAborted()
359
+ if (!feeToken || !gas || !maxFeePerGas) return undefined
360
+
361
+ try {
362
+ const metadata = await resolveTokenMetadata(client, {
363
+ token: feeToken,
364
+ tokenMetadata,
365
+ store,
366
+ })
367
+ signal?.throwIfAborted()
368
+ const raw = gas * maxFeePerGas
369
+ const scale = 10n ** BigInt(Math.max(0, 18 - metadata.decimals))
370
+ const amount = (raw + scale - 1n) / scale
371
+ return {
372
+ amount: Hex.fromNumber(amount) as `0x${string}`,
373
+ decimals: metadata.decimals,
374
+ formatted: formatUnits(amount, metadata.decimals),
375
+ symbol: metadata.symbol,
376
+ }
377
+ } catch {
378
+ signal?.throwIfAborted()
379
+ return undefined
380
+ }
381
+ }
382
+
383
+ declare namespace computeFee {
384
+ type Options = {
385
+ feeToken?: Address | undefined
386
+ gas?: bigint | undefined
387
+ store?: Store.Store | undefined
388
+ signal?: AbortSignal | undefined
389
+ maxFeePerGas?: bigint | undefined
390
+ tokenMetadata?:
391
+ | Record<Address, { name: string; symbol: string; currency: string }>
392
+ | undefined
393
+ }
394
+ }
@@ -0,0 +1,213 @@
1
+ import { Address, Hex, RpcResponse } from 'ox'
2
+ import {
3
+ Transaction as core_Transaction,
4
+ KeyAuthorization,
5
+ TxEnvelopeTempo,
6
+ } from 'ox/tempo'
7
+ import type { Client } from '../../../core/Client.js'
8
+ import { UnknownRpcError } from '../../../core/RpcError.js'
9
+
10
+ export function resolveChainId(value: unknown) {
11
+ if (typeof value === 'number') return value
12
+ if (typeof value === 'bigint') return Number(value)
13
+ if (typeof value === 'string') {
14
+ if (!/^(?:0x[0-9a-f]+|[0-9]+)$/i.test(value)) return undefined
15
+ const n = Number(value)
16
+ if (Number.isFinite(n)) return n
17
+ }
18
+ return undefined
19
+ }
20
+
21
+ export function formatFillTransactionRequest(
22
+ client: Client,
23
+ value: Record<string, unknown>,
24
+ ) {
25
+ if (value.type === '0x76') return { ...value }
26
+ const format = client.chain?.codecs?.transactionRequest?.toRpc
27
+ if (!format) return value
28
+ return {
29
+ ...(format({ ...value } as never) as Record<string, unknown>),
30
+ // Keep the selected token until the request leaves the plugin pipeline.
31
+ ...(value.feeToken !== undefined ? { feeToken: value.feeToken } : {}),
32
+ } as Record<string, unknown>
33
+ }
34
+
35
+ export function normalizeFillTransactionRequest(
36
+ tx: Record<string, unknown>,
37
+ ): Record<string, unknown> & { calls: unknown[] } {
38
+ const { to, data, value, ...rest } = tx
39
+ const keyAuthorization = normalizeKeyAuthorization(tx.keyAuthorization)
40
+ const withKeyAuthorization = keyAuthorization ? { keyAuthorization } : {}
41
+ if (Array.isArray(tx.calls))
42
+ return {
43
+ ...tx,
44
+ ...withKeyAuthorization,
45
+ calls: tx.calls.map((call) => {
46
+ if (!call || typeof call !== 'object' || Array.isArray(call))
47
+ throw new RpcResponse.InvalidParamsError({
48
+ message: 'Expected a transaction call object.',
49
+ })
50
+ assertCallTarget(call.to)
51
+ return { ...call, value: normalizeFillValue(call.value) }
52
+ }),
53
+ }
54
+ assertCallTarget(to)
55
+ const call = {
56
+ ...(typeof to !== 'undefined' ? { to } : {}),
57
+ ...(typeof data !== 'undefined' ? { data } : {}),
58
+ ...(typeof value !== 'undefined'
59
+ ? { value: normalizeFillValue(value) }
60
+ : {}),
61
+ }
62
+ return { ...rest, ...withKeyAuthorization, calls: [call] }
63
+ }
64
+
65
+ /**
66
+ * Forwards `keyAuthorization` to the chain in RPC shape. Pass-through
67
+ * when already RPC; convert via `KeyAuthorization.toRpc` when internal.
68
+ */
69
+ function normalizeKeyAuthorization(value: unknown) {
70
+ if (!value || typeof value !== 'object') return undefined
71
+ const ka = value as Record<string, unknown>
72
+ const signature = ka.signature as Record<string, unknown> | undefined
73
+ if (!signature || typeof signature !== 'object') return undefined
74
+ const isInternal =
75
+ typeof signature.signature === 'object' && signature.signature !== null
76
+ return isInternal ? KeyAuthorization.toRpc(value as never) : value
77
+ }
78
+
79
+ function assertCallTarget(to: unknown) {
80
+ if (to === undefined || to === null) return
81
+ if (typeof to !== 'string' || !Address.validate(to, { strict: false }))
82
+ throw new RpcResponse.InvalidParamsError({
83
+ message: 'Invalid transaction call target.',
84
+ })
85
+ }
86
+
87
+ function normalizeFillValue(value: unknown) {
88
+ if (typeof value !== 'string' || !value.startsWith('0x')) return value
89
+ if (!/^0x[\da-f]*$/i.test(value))
90
+ throw new RpcResponse.InvalidParamsError({
91
+ message: 'Invalid transaction value.',
92
+ })
93
+ return BigInt(value === '0x' ? '0x0' : value)
94
+ }
95
+
96
+ /** Returns whether a raw transaction uses a Tempo sender or fee-payer wire prefix. */
97
+ export function isSerializedTempoTransaction(
98
+ value: unknown,
99
+ ): value is `0x76${string}` | `0x78${string}` {
100
+ if (typeof value !== 'string') return false
101
+ // `0x78` is Tempo's fee-payer handoff magic, not a separate EIP-2718 type.
102
+ return (
103
+ value.startsWith(TxEnvelopeTempo.serializedType) ||
104
+ value.startsWith(TxEnvelopeTempo.feePayerMagic)
105
+ )
106
+ }
107
+
108
+ export function normalizeTempoTransaction(
109
+ value: Record<string, unknown> | undefined,
110
+ ) {
111
+ if (!value) throw new Error('Expected `tx` in eth_fillTransaction response.')
112
+ return core_Transaction.fromRpc({
113
+ type: '0x76',
114
+ ...value,
115
+ } as core_Transaction.Rpc)!
116
+ }
117
+
118
+ /** Preserves the nonce domain when serializing filled Tempo transactions. */
119
+ export function formatTempoTransaction(
120
+ transaction: core_Transaction.Transaction,
121
+ ) {
122
+ return {
123
+ ...core_Transaction.toRpc(transaction),
124
+ ...('nonceKey' in transaction && transaction.nonceKey !== undefined
125
+ ? { nonceKey: Hex.fromNumber(transaction.nonceKey) }
126
+ : {}),
127
+ }
128
+ }
129
+
130
+ /** Preserves upstream RPC errors and the relay's expired-transaction contract. */
131
+ export function toRpcError(error: unknown): RpcResponse.BaseError {
132
+ let current: unknown = error
133
+ let deepest: { code: number; message: string; data?: unknown } | undefined
134
+ const seen = new Set<unknown>()
135
+ while (current && typeof current === 'object' && !seen.has(current)) {
136
+ seen.add(current)
137
+ const candidate = current as Record<string, unknown>
138
+ if (
139
+ !(current instanceof UnknownRpcError) &&
140
+ typeof candidate.code === 'number' &&
141
+ typeof candidate.message === 'string'
142
+ )
143
+ deepest =
144
+ current instanceof RpcResponse.BaseError
145
+ ? current
146
+ : {
147
+ code: candidate.code,
148
+ message: candidate.message,
149
+ data: candidate.data,
150
+ }
151
+ // The RPC parser wraps unrecognized codes in an InternalError with the original error as data.
152
+ const data = candidate.data
153
+ current =
154
+ current instanceof RpcResponse.InternalError &&
155
+ data &&
156
+ typeof data === 'object' &&
157
+ 'code' in data &&
158
+ typeof data.code === 'number' &&
159
+ 'message' in data &&
160
+ data.message === candidate.message
161
+ ? data
162
+ : candidate.cause
163
+ }
164
+ if (!deepest)
165
+ return new RpcResponse.InternalError({
166
+ message: 'Internal error',
167
+ data: { code: 'internal_error' },
168
+ })
169
+ if (
170
+ deepest.code === -32603 &&
171
+ /^Revm error: transaction expired(?:\.|: .+)?$/.test(deepest.message)
172
+ )
173
+ return new RpcResponse.TransactionRejectedError({
174
+ message: 'Transaction expired.',
175
+ data: { code: 'transaction_expired' },
176
+ })
177
+ return deepest instanceof RpcResponse.BaseError
178
+ ? deepest
179
+ : new RpcResponse.BaseError(deepest)
180
+ }
181
+
182
+ /** Preserves envelope inputs omitted by the node, including calls and chain ID. Filled fields take precedence; legacy calls are normalized separately. */
183
+ export function mergeCallsFromRequest(
184
+ resultTx: Record<string, unknown>,
185
+ request: Record<string, unknown>,
186
+ ): Record<string, unknown> {
187
+ const merged: Record<string, unknown> = { ...request, ...resultTx }
188
+ const resultCalls = resultTx.calls
189
+ if (Array.isArray(resultCalls) && resultCalls.length > 0) return merged
190
+
191
+ const reqCalls = request.calls
192
+ if (Array.isArray(reqCalls)) {
193
+ merged.calls = reqCalls
194
+ return merged
195
+ }
196
+
197
+ const { to, data, value } = request
198
+ if (
199
+ typeof to === 'undefined' &&
200
+ typeof data === 'undefined' &&
201
+ typeof value === 'undefined'
202
+ )
203
+ return merged
204
+
205
+ merged.calls = [
206
+ {
207
+ ...(typeof to !== 'undefined' ? { to } : {}),
208
+ ...(typeof data !== 'undefined' ? { data } : {}),
209
+ ...(typeof value !== 'undefined' ? { value } : {}),
210
+ },
211
+ ]
212
+ return merged
213
+ }
@@ -0,0 +1,95 @@
1
+ import type { Address } from 'abitype'
2
+ import { type Hex, RpcResponse } from 'ox'
3
+ import * as VirtualAddress from 'ox/tempo/VirtualAddress'
4
+ import type { Client } from '../../../core/Client.js'
5
+ import type { Call } from 'ox/tempo/TxEnvelopeTempo'
6
+ import * as AbiFunction from 'ox/AbiFunction'
7
+ import { validate as isAddress } from 'ox/Address'
8
+ import * as Abis from '../../Abis.js'
9
+ import * as Preflight from './preflight.js'
10
+
11
+ export async function resolveVirtualAddresses(
12
+ client: Client,
13
+ options: { calls: readonly Call[] },
14
+ ): Promise<Record<Address, Address | null> | undefined> {
15
+ const targets = getVirtualAddressTargets(options.calls)
16
+ if (targets.length === 0) return undefined
17
+
18
+ const result = await Preflight.read(client, { targets })
19
+ return result.virtualAddresses
20
+ }
21
+
22
+ export function getVirtualAddressTargets(
23
+ calls: readonly Call[],
24
+ ): readonly Address[] {
25
+ const targets = new Set<Address>()
26
+ for (const call of calls) {
27
+ for (const address of [call.to, decodeTransferRecipient(call.data)]) {
28
+ if (!address || !isAddress(address) || !VirtualAddress.isVirtual(address))
29
+ continue
30
+ targets.add(address.toLowerCase() as Address)
31
+ if (targets.size > 100)
32
+ throw new RpcResponse.InvalidParamsError({
33
+ message: 'Virtual-address targets exceed the limit of 100 addresses.',
34
+ })
35
+ }
36
+ }
37
+
38
+ return [...targets]
39
+ }
40
+
41
+ function decodeTransferRecipient(data?: string): Address | undefined {
42
+ if (!data) return undefined
43
+
44
+ const selector = data.slice(0, 10).toLowerCase()
45
+ if (!transferSelectors.has(selector)) return undefined
46
+
47
+ try {
48
+ const fn = AbiFunction.fromAbi(Abis.tip20, data as Hex.Hex)
49
+ const args = AbiFunction.decodeData(fn, data as Hex.Hex)
50
+ const functionName = fn.name
51
+
52
+ if (
53
+ (functionName === 'transfer' || functionName === 'transferWithMemo') &&
54
+ typeof args[0] === 'string' &&
55
+ isAddress(args[0], { strict: false })
56
+ )
57
+ return args[0]
58
+
59
+ if (
60
+ (functionName === 'transferFrom' ||
61
+ functionName === 'transferFromWithMemo') &&
62
+ typeof args[1] === 'string'
63
+ )
64
+ return args[1]
65
+ } catch {}
66
+
67
+ return undefined
68
+ }
69
+
70
+ const transferSelectors = new Set([
71
+ '0xa9059cbb',
72
+ '0x95777d59',
73
+ '0x23b872dd',
74
+ '0x929c2539',
75
+ ])
76
+
77
+ export function extractCalls(
78
+ transaction: Record<string, unknown>,
79
+ ): readonly Call[] {
80
+ const calls = transaction.calls as readonly Call[] | undefined
81
+ if (calls && calls.length > 0)
82
+ return calls.map((c) => ({
83
+ ...(c.to ? { to: c.to } : {}),
84
+ ...(c.data ? { data: c.data } : {}),
85
+ ...(c.value ? { value: c.value } : {}),
86
+ })) as readonly Call[]
87
+
88
+ return [
89
+ {
90
+ ...(transaction.to ? { to: transaction.to as Address } : {}),
91
+ ...(transaction.data ? { data: transaction.data as `0x${string}` } : {}),
92
+ ...(transaction.value ? { value: transaction.value as bigint } : {}),
93
+ },
94
+ ] as readonly Call[]
95
+ }