accounts 0.6.7 → 0.7.0

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 (50) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/ExecutionError.d.ts +25 -0
  3. package/dist/core/ExecutionError.d.ts.map +1 -0
  4. package/dist/core/ExecutionError.js +170 -0
  5. package/dist/core/ExecutionError.js.map +1 -0
  6. package/dist/core/Schema.d.ts +33 -7
  7. package/dist/core/Schema.d.ts.map +1 -1
  8. package/dist/core/zod/rpc.d.ts +14 -1
  9. package/dist/core/zod/rpc.d.ts.map +1 -1
  10. package/dist/core/zod/rpc.js +14 -1
  11. package/dist/core/zod/rpc.js.map +1 -1
  12. package/dist/server/CliAuth.d.ts +110 -43
  13. package/dist/server/CliAuth.d.ts.map +1 -1
  14. package/dist/server/CliAuth.js +243 -155
  15. package/dist/server/CliAuth.js.map +1 -1
  16. package/dist/server/Handler.d.ts +0 -1
  17. package/dist/server/Handler.d.ts.map +1 -1
  18. package/dist/server/Handler.js +0 -1
  19. package/dist/server/Handler.js.map +1 -1
  20. package/dist/server/internal/handlers/relay.d.ts +29 -12
  21. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  22. package/dist/server/internal/handlers/relay.js +180 -125
  23. package/dist/server/internal/handlers/relay.js.map +1 -1
  24. package/dist/server/internal/handlers/sponsorship.d.ts +77 -0
  25. package/dist/server/internal/handlers/sponsorship.d.ts.map +1 -0
  26. package/dist/server/internal/handlers/sponsorship.js +96 -0
  27. package/dist/server/internal/handlers/sponsorship.js.map +1 -0
  28. package/dist/server/internal/handlers/utils.d.ts +3 -1
  29. package/dist/server/internal/handlers/utils.d.ts.map +1 -1
  30. package/dist/server/internal/handlers/utils.js +15 -12
  31. package/dist/server/internal/handlers/utils.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/core/ExecutionError.test.ts +205 -0
  34. package/src/core/ExecutionError.ts +189 -0
  35. package/src/core/Provider.test.ts +4 -2
  36. package/src/core/zod/rpc.ts +18 -1
  37. package/src/server/CliAuth.test-d.ts +6 -0
  38. package/src/server/CliAuth.test.ts +83 -0
  39. package/src/server/CliAuth.ts +331 -208
  40. package/src/server/Handler.ts +0 -1
  41. package/src/server/internal/handlers/relay.test.ts +318 -108
  42. package/src/server/internal/handlers/relay.ts +243 -138
  43. package/src/server/internal/handlers/sponsorship.ts +172 -0
  44. package/src/server/internal/handlers/utils.ts +15 -10
  45. package/dist/server/internal/handlers/feePayer.d.ts +0 -73
  46. package/dist/server/internal/handlers/feePayer.d.ts.map +0 -1
  47. package/dist/server/internal/handlers/feePayer.js +0 -184
  48. package/dist/server/internal/handlers/feePayer.js.map +0 -1
  49. package/src/server/internal/handlers/feePayer.test.ts +0 -336
  50. package/src/server/internal/handlers/feePayer.ts +0 -271
@@ -6,21 +6,22 @@ import {
6
6
  type Chain,
7
7
  type Client,
8
8
  createClient,
9
- decodeErrorResult,
10
9
  formatUnits,
11
10
  http,
12
11
  type Log,
13
- parseAbi,
14
12
  parseEventLogs,
15
13
  type Transport,
14
+ zeroAddress,
16
15
  } from 'viem'
16
+ import type { LocalAccount } from 'viem/accounts'
17
17
  import { simulateCalls } from 'viem/actions'
18
18
  import { tempo, tempoLocalnet, tempoMainnet, tempoModerato } from 'viem/chains'
19
19
  import { Abis, Actions, Addresses, Capabilities, Transaction } from 'viem/tempo'
20
20
 
21
+ import * as ExecutionError from '../../../core/ExecutionError.js'
21
22
  import * as Schema from '../../../core/Schema.js'
22
23
  import { type Handler, from } from '../../Handler.js'
23
- import * as FeePayer from './feePayer.js'
24
+ import * as Sponsorship from './sponsorship.js'
24
25
  import * as Utils from './utils.js'
25
26
 
26
27
  /**
@@ -51,12 +52,9 @@ import * as Utils from './utils.js'
51
52
  * import { privateKeyToAccount } from 'viem/accounts'
52
53
  * import { Handler } from 'accounts/server'
53
54
  *
54
- * // With sponsorship — signs as fee payer when `validate` approves.
55
55
  * const handler = Handler.relay({
56
56
  * feePayer: {
57
57
  * account: privateKeyToAccount('0x...'),
58
- * // Optional: validate sponsorship approval.
59
- * // validate: (request) => request.from !== BLOCKED_ADDRESS,
60
58
  * },
61
59
  * })
62
60
  * ```
@@ -67,7 +65,6 @@ import * as Utils from './utils.js'
67
65
  export function relay(options: relay.Options = {}): Handler {
68
66
  const {
69
67
  chains = [tempo, tempoModerato],
70
- feePayer: feePayerOptions,
71
68
  onRequest,
72
69
  path = '/',
73
70
  resolveTokens = (chainId) =>
@@ -75,10 +72,21 @@ export function relay(options: relay.Options = {}): Handler {
75
72
  transports = {},
76
73
  ...rest
77
74
  } = options
75
+ const feePayerOptions = options.feePayer
76
+
77
+ const features = {
78
+ autoSwap: options.autoSwap ?? options.features === 'all',
79
+ feeTokenResolution: options.resolveTokens ?? options.features === 'all',
80
+ simulate: options.features === 'all',
81
+ }
78
82
 
79
83
  const autoSwap = (() => {
84
+ if (!features.autoSwap) return undefined
80
85
  if (options.autoSwap === false) return undefined
81
- return { slippage: options.autoSwap?.slippage ?? 0.05 }
86
+ return {
87
+ slippage:
88
+ (typeof options.autoSwap === 'object' ? options.autoSwap?.slippage : undefined) ?? 0.05,
89
+ }
82
90
  })()
83
91
 
84
92
  const clients = new Map<number, Client>()
@@ -99,92 +107,105 @@ export function relay(options: relay.Options = {}): Handler {
99
107
  return clients.get(chains[0]!.id)!
100
108
  }
101
109
 
102
- async function handleRequest(request: RpcRequest.RpcRequest<Schema.Ox>) {
110
+ async function handleRequest(
111
+ request: RpcRequest.RpcRequest<Schema.Ox>,
112
+ options?: { chainId?: number | undefined },
113
+ ) {
103
114
  await onRequest?.(request)
104
115
 
105
- // Resolve chainId + client from the first param object (if present).
116
+ // Resolve chainId from: 1) explicit option (URL path), 2) first param object, 3) default chain.
106
117
  const params = 'params' in request && Array.isArray(request.params) ? request.params : []
107
118
  const first =
108
119
  typeof params[0] === 'object' && params[0]
109
120
  ? (params[0] as Record<string, unknown>)
110
121
  : undefined
111
- const chainId = Utils.resolveChainId(first?.chainId) ?? chains[0]!.id
122
+ const chainId = Utils.resolveChainId(first?.chainId) ?? options?.chainId ?? chains[0]!.id
112
123
  const client = getClient(chainId)
113
124
 
114
125
  switch (request.method) {
115
126
  case 'eth_fillTransaction': {
116
127
  try {
117
- const parameters = request.params[0]
128
+ const parameters = params[0] as Record<string, unknown>
129
+ const from =
130
+ typeof parameters.from === 'string' ? (parameters.from as Address) : undefined
131
+ const requestFeeToken =
132
+ typeof parameters.feeToken === 'string' ? (parameters.feeToken as Address) : undefined
133
+ const requestsSponsorship = !!feePayerOptions && parameters.feePayer !== false
118
134
 
119
135
  // 1. Resolve fee token.
120
- const feeToken = await resolveFeeToken(client, {
121
- account: parameters.from,
122
- feeToken: parameters.feeToken,
123
- tokens: resolveTokens(chainId),
124
- })
136
+ const feeToken = features.feeTokenResolution
137
+ ? await resolveFeeToken(client, {
138
+ account: from,
139
+ feeToken: requestFeeToken,
140
+ tokens: resolveTokens(chainId),
141
+ })
142
+ : requestFeeToken
125
143
 
126
144
  // 2. Fill transaction via RPC node (with AMM resolution on InsufficientBalance).
127
145
  const normalized = Utils.normalizeFillTransactionRequest(parameters)
128
146
  const transaction = {
129
147
  ...normalized,
130
148
  ...(typeof chainId !== 'undefined' ? { chainId } : {}),
131
- ...(feePayerOptions ? { feePayer: true } : {}),
149
+ ...(requestsSponsorship ? { feePayer: true } : {}),
132
150
  ...(feeToken ? { feeToken } : {}),
133
151
  }
134
- let filled = await fill(client, { autoSwap, feeToken, transaction })
152
+ let filled = await (async () => {
153
+ if (requestsSponsorship && Sponsorship.isPreparedTransaction(transaction))
154
+ return { transaction: Utils.normalizeTempoTransaction(transaction) }
155
+ return await fill(client, { autoSwap, feeToken, transaction })
156
+ })()
135
157
 
136
158
  // 3. Check if the fee payer approves this transaction.
137
159
  const sponsored =
138
- feePayerOptions &&
139
- (!feePayerOptions.validate ||
140
- // @ts-expect-error - TODO: Convert to `TransactionRequest` properly.
141
- (await feePayerOptions.validate({
142
- ...filled.transaction,
143
- from: parameters.from,
144
- } as Transaction.TransactionRequest)))
160
+ requestsSponsorship && feePayerOptions
161
+ ? await Sponsorship.shouldSponsor({
162
+ sender: from,
163
+ transaction: filled.transaction,
164
+ validate: feePayerOptions.validate,
165
+ })
166
+ : false
145
167
 
146
168
  // Re-fill without feePayer when sponsorship is rejected so the
147
169
  // gas estimate and nonce are correct for a self-paid transaction.
148
- if (feePayerOptions && !sponsored) {
170
+ if (requestsSponsorship && !sponsored) {
149
171
  const { feePayer: _, ...tx } = transaction
150
172
  filled = await fill(client, { autoSwap, feeToken, transaction: tx })
151
173
  }
152
174
 
153
- const { transaction: transaction_filled, swap } = filled
175
+ const transaction_filled = filled.transaction
176
+ const swap = 'swap' in filled ? filled.swap : undefined
154
177
 
155
178
  // 4. Simulate and compute balance diffs + fee.
156
- const calls = extractCalls(transaction_filled)
157
- const { balanceDiffs, fee } = await simulateAndParseDiffs(client, {
158
- account: parameters.from,
159
- calls,
160
- swap,
161
- feeToken: transaction_filled.feeToken,
162
- gas: transaction_filled.gas,
163
- maxFeePerGas: transaction_filled.maxFeePerGas,
164
- })
165
-
166
- // 5. Sign as fee payer (if sponsored).
179
+ const { balanceDiffs, fee } = features.simulate
180
+ ? await simulateAndParseDiffs(client, {
181
+ account: from,
182
+ calls: extractCalls(transaction_filled),
183
+ swap,
184
+ feeToken: transaction_filled.feeToken,
185
+ gas: transaction_filled.gas,
186
+ maxFeePerGas: transaction_filled.maxFeePerGas,
187
+ })
188
+ : { balanceDiffs: undefined, fee: undefined }
189
+
190
+ // 5. Sign as fee payer (if sponsored and not already signed).
191
+ const alreadySigned =
192
+ 'feePayerSignature' in transaction_filled &&
193
+ transaction_filled.feePayerSignature != null
167
194
  const transaction_final = await (async () => {
168
- if (!sponsored) return transaction_filled
169
- return await FeePayer.sign({
195
+ if (!sponsored || !feePayerOptions || alreadySigned) return transaction_filled
196
+ return await Sponsorship.sign({
170
197
  account: feePayerOptions.account,
171
- sender: parameters.from,
198
+ sender: from,
172
199
  transaction: transaction_filled,
173
200
  })
174
201
  })()
175
202
 
176
- const sponsor = (() => {
177
- if (!sponsored) return undefined
178
- return {
179
- address: feePayerOptions.account.address,
180
- ...(feePayerOptions.name ? { name: feePayerOptions.name } : {}),
181
- ...(feePayerOptions.url ? { url: feePayerOptions.url } : {}),
182
- }
183
- })()
203
+ const sponsor =
204
+ sponsored && feePayerOptions ? Sponsorship.getSponsor(feePayerOptions) : undefined
184
205
 
185
206
  // 6. Resolve autoSwap metadata (when AMM path was taken).
186
207
  const autoSwap_ = await (async () => {
187
- if (!swap) return undefined
208
+ if (!autoSwap || !swap) return undefined
188
209
  const [inMeta, outMeta] = await Promise.all([
189
210
  resolveTokenMetadata(client, swap.tokenIn).catch(() => undefined),
190
211
  resolveTokenMetadata(client, swap.tokenOut).catch(() => undefined),
@@ -215,6 +236,7 @@ export function relay(options: relay.Options = {}): Handler {
215
236
  return RpcResponse.from(
216
237
  {
217
238
  result: {
239
+ ...(sponsor ? { sponsor } : {}),
218
240
  tx: core_Transaction.toRpc(transaction_final as core_Transaction.Transaction),
219
241
  capabilities: {
220
242
  balanceDiffs,
@@ -227,6 +249,111 @@ export function relay(options: relay.Options = {}): Handler {
227
249
  },
228
250
  { request },
229
251
  )
252
+ } catch (error) {
253
+ if (!(error instanceof Error)) return Utils.rpcErrorJson(request, error)
254
+
255
+ const revert = ExecutionError.parse(error)
256
+
257
+ const parameters = request.params[0]
258
+ const stub = {
259
+ from: parameters.from,
260
+ to: parameters.to ?? null,
261
+ gas: '0x0',
262
+ nonce: '0x0',
263
+ value: '0x0',
264
+ maxFeePerGas: '0x0',
265
+ maxPriorityFeePerGas: '0x0',
266
+ }
267
+
268
+ if (revert?.errorName === 'InsufficientBalance') {
269
+ const args = revert.args as [bigint, bigint, Address]
270
+ const [available, required, token] = args
271
+
272
+ const normalized = Utils.normalizeFillTransactionRequest(parameters)
273
+
274
+ // Simulate from zero address for optimistic balance diffs.
275
+ const optimisticCalls = normalized ? extractCalls(normalized) : undefined
276
+ const { balanceDiffs } = optimisticCalls
277
+ ? await simulateAndParseDiffs(client, {
278
+ account: zeroAddress,
279
+ calls: optimisticCalls,
280
+ })
281
+ : { balanceDiffs: undefined }
282
+
283
+ // Re-key balance diffs from zero address to the real sender.
284
+ const senderDiffs =
285
+ parameters.from && balanceDiffs
286
+ ? { [parameters.from]: balanceDiffs[zeroAddress] ?? [] }
287
+ : balanceDiffs
288
+
289
+ const metadata = await resolveTokenMetadata(client, token).catch(() => undefined)
290
+ const deficit = required - available
291
+ return RpcResponse.from(
292
+ {
293
+ result: {
294
+ tx: stub,
295
+ capabilities: {
296
+ balanceDiffs: senderDiffs,
297
+ error: ExecutionError.serialize(revert),
298
+ requireFunds: metadata
299
+ ? {
300
+ amount: Hex.fromNumber(deficit) as `0x${string}`,
301
+ decimals: metadata.decimals,
302
+ formatted: formatUnits(deficit, metadata.decimals),
303
+ token,
304
+ symbol: metadata.symbol,
305
+ }
306
+ : undefined,
307
+ sponsored: false,
308
+ },
309
+ },
310
+ },
311
+ { request },
312
+ )
313
+ }
314
+
315
+ return RpcResponse.from(
316
+ {
317
+ result: {
318
+ tx: stub,
319
+ capabilities: {
320
+ error: ExecutionError.serialize(revert),
321
+ sponsored: false,
322
+ },
323
+ },
324
+ },
325
+ { request },
326
+ )
327
+ }
328
+ }
329
+
330
+ // @ts-expect-error
331
+ case 'eth_signRawTransaction':
332
+ case 'eth_sendRawTransaction':
333
+ case 'eth_sendRawTransactionSync': {
334
+ try {
335
+ if (!feePayerOptions) {
336
+ const result = await client.request(request as never)
337
+ return RpcResponse.from({ result }, { request })
338
+ }
339
+
340
+ const serialized = params[0]
341
+ if (
342
+ typeof serialized !== 'string' ||
343
+ !Sponsorship.requestsRawSponsorship(serialized as `0x${string}`)
344
+ ) {
345
+ const result = await client.request(request as never)
346
+ return RpcResponse.from({ result }, { request })
347
+ }
348
+
349
+ const result = await Sponsorship.handleRawTransaction({
350
+ account: feePayerOptions.account,
351
+ getClient,
352
+ method: request.method as Sponsorship.handleRawTransaction.Options['method'],
353
+ request: { params: 'params' in request ? request.params : undefined },
354
+ validate: feePayerOptions.validate,
355
+ })
356
+ return RpcResponse.from({ result } as never, { request } as never)
230
357
  } catch (error) {
231
358
  return Utils.rpcErrorJson(request, error)
232
359
  }
@@ -241,22 +368,28 @@ export function relay(options: relay.Options = {}): Handler {
241
368
 
242
369
  const router = from(rest)
243
370
 
244
- router.post(path, async (c) => {
371
+ async function handlePost(c: { req: { raw: Request; param: (key: string) => string } }) {
372
+ const chainId = Utils.resolveChainId(c.req.param('chainId'))
245
373
  const body = await c.req.raw.json()
246
374
  const isBatch = Array.isArray(body)
247
375
 
248
376
  if (!isBatch) {
249
- const request = RpcRequest.from(body) as RpcRequest.RpcRequest<Schema.Ox>
250
- return Response.json(await handleRequest(request))
377
+ const request = RpcRequest.from(body as never) as RpcRequest.RpcRequest<Schema.Ox>
378
+ return Response.json(await handleRequest(request, { chainId }))
251
379
  }
252
380
 
253
381
  const responses = await Promise.all(
254
382
  (body as unknown[]).map((item) =>
255
- handleRequest(RpcRequest.from(item as never) as RpcRequest.RpcRequest<Schema.Ox>),
383
+ handleRequest(RpcRequest.from(item as never) as RpcRequest.RpcRequest<Schema.Ox>, {
384
+ chainId,
385
+ }),
256
386
  ),
257
387
  )
258
388
  return Response.json(responses)
259
- })
389
+ }
390
+
391
+ router.post(path, handlePost as never)
392
+ router.post(`${path === '/' ? '' : path}/:chainId`, handlePost as never)
260
393
 
261
394
  return router
262
395
  }
@@ -296,6 +429,16 @@ export namespace relay {
296
429
  } as const
297
430
 
298
431
  export type Options = from.Options & {
432
+ /**
433
+ * Auto-swap options.
434
+ */
435
+ autoSwap?:
436
+ | false
437
+ | {
438
+ /** Slippage tolerance (e.g. 0.05 = 5%). @default 0.05 */
439
+ slippage?: number | undefined
440
+ }
441
+ | undefined
299
442
  /**
300
443
  * Supported chains. The handler resolves the client based on the
301
444
  * `chainId` in the incoming transaction.
@@ -307,17 +450,20 @@ export namespace relay {
307
450
  * sign `feePayerSignature` on the filled transaction.
308
451
  */
309
452
  feePayer?:
310
- | Omit<FeePayer.feePayer.Options, 'chains' | 'transports' | 'path' | 'onRequest'>
311
- | undefined
312
- /**
313
- * AMM swap options for automatic insufficient balance resolution.
314
- * Set to `false` to disable. @default {}
315
- */
316
- autoSwap?:
317
- | false
318
453
  | {
319
- /** Slippage tolerance (e.g. 0.05 = 5%). @default 0.05 */
320
- slippage?: number | undefined
454
+ /** Account to use as the fee payer. */
455
+ account: LocalAccount
456
+ /**
457
+ * Validates whether to sponsor the transaction. When omitted, all
458
+ * transactions are sponsored. Return `false` to reject sponsorship.
459
+ */
460
+ validate?:
461
+ | ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>)
462
+ | undefined
463
+ /** Sponsor display name returned from `eth_fillTransaction`. */
464
+ name?: string | undefined
465
+ /** Sponsor URL returned from `eth_fillTransaction`. */
466
+ url?: string | undefined
321
467
  }
322
468
  | undefined
323
469
  /**
@@ -326,6 +472,14 @@ export namespace relay {
326
472
  * highest balance.
327
473
  */
328
474
  resolveTokens?: ((chainId?: number | undefined) => readonly Address[]) | undefined
475
+ /**
476
+ * Relay features.
477
+ *
478
+ * - `'all'` — enables fee token resolution, auto-swap,
479
+ * fee payer, and simulation (balance diffs + fee breakdown).
480
+ * - `undefined` (default) — only fee payers.
481
+ */
482
+ features?: 'all' | undefined
329
483
  /** Function to call before handling the request. */
330
484
  onRequest?: ((request: RpcRequest.RpcRequest) => Promise<void>) | undefined
331
485
  /** Path to use for the handler. @default "/" */
@@ -358,33 +512,27 @@ async function fill(
358
512
  return { transaction: Utils.normalizeTempoTransaction(result.tx) }
359
513
  } catch (error) {
360
514
  if (!(error instanceof Error)) throw error
361
- const parsed = parseInsufficientBalance(error)
362
- if (!parsed || !feeToken || !autoSwap) throw error
363
- if (parsed.token.toLowerCase() === feeToken.toLowerCase()) throw error
515
+ if (!feeToken || !autoSwap) throw error
516
+
517
+ const revert = ExecutionError.parse(error)
518
+ if (revert?.errorName !== 'InsufficientBalance') throw error
519
+
520
+ const [available, required, token] = revert.args
521
+ if (typeof available === 'undefined' || typeof required === 'undefined' || !token) throw error
364
522
 
365
- const deficit = parsed.required - parsed.available
523
+ if (token.toLowerCase() === feeToken.toLowerCase()) throw error
524
+
525
+ const deficit = required - available
366
526
  const maxAmountIn = deficit + (deficit * BigInt(Math.round(autoSwap.slippage * 1000))) / 1000n
367
- const swapCalls = buildSwapCalls(feeToken, parsed.token, deficit, maxAmountIn)
368
- const existingCalls = request.calls as Call[] | undefined
369
- // If the request was normalized to top-level to/data/value (single call),
370
- // convert back to a calls array so we can prepend swap calls.
371
- const originalCalls: Call[] = existingCalls
372
- ? [...existingCalls]
373
- : request.to
374
- ? [
375
- {
376
- to: request.to as Address,
377
- data: request.data as `0x${string}`,
378
- value: (request.value as bigint) ?? 0n,
379
- },
380
- ]
381
- : []
382
- const { to: _, data: __, value: ___, calls: ____, ...rest } = request
527
+
528
+ const originalCalls = (request.calls as Call[] | undefined) ?? []
529
+ const swapCalls = buildSwapCalls(feeToken, token, deficit, maxAmountIn)
530
+
383
531
  const result = await client.request({
384
532
  method: 'eth_fillTransaction',
385
533
  params: [
386
534
  format({
387
- ...rest,
535
+ ...request,
388
536
  calls: [...swapCalls, ...originalCalls],
389
537
  }) as never,
390
538
  ],
@@ -394,7 +542,7 @@ async function fill(
394
542
  swap: {
395
543
  calls: swapCalls,
396
544
  tokenIn: feeToken,
397
- tokenOut: parsed.token,
545
+ tokenOut: token,
398
546
  amountOut: deficit,
399
547
  maxAmountIn,
400
548
  },
@@ -517,7 +665,10 @@ async function simulateAndParseDiffs(
517
665
  const { account, calls, swap, feeToken, gas, maxFeePerGas } = options
518
666
 
519
667
  try {
520
- const { results, tokenMetadata } = await simulate(client, { account, calls })
668
+ const { results, tokenMetadata } = await simulate(client, {
669
+ account: account === zeroAddress ? undefined : account,
670
+ calls,
671
+ })
521
672
 
522
673
  // Collect all logs across all call results.
523
674
  const logs: (typeof results)[number]['logs'] = []
@@ -540,7 +691,7 @@ async function simulateAndParseDiffs(
540
691
  gas,
541
692
  maxFeePerGas,
542
693
  tokenMetadata: tokenMetadata as never,
543
- })
694
+ }).catch(() => undefined)
544
695
 
545
696
  return { balanceDiffs, fee }
546
697
  } catch {
@@ -704,7 +855,7 @@ async function computeFee(
704
855
  },
705
856
  ) {
706
857
  const { feeToken, gas, maxFeePerGas, tokenMetadata } = options
707
- if (!feeToken || !gas || !maxFeePerGas) return null
858
+ if (!feeToken || !gas || !maxFeePerGas) return undefined
708
859
 
709
860
  try {
710
861
  const metadata = await resolveTokenMetadata(client, feeToken, tokenMetadata)
@@ -717,54 +868,8 @@ async function computeFee(
717
868
  symbol: metadata.symbol,
718
869
  }
719
870
  } catch {
720
- return null
721
- }
722
- }
723
-
724
- const insufficientBalanceAbi = parseAbi([
725
- 'error InsufficientBalance(uint256 available, uint256 required, address token)',
726
- ])
727
- const insufficientBalancePattern =
728
- /InsufficientBalance\(\s*InsufficientBalance\s*\{\s*available:\s*(\d+),\s*required:\s*(\d+),\s*token:\s*(0x[0-9a-fA-F]+)\s*\}\s*\)/
729
-
730
- function parseInsufficientBalance(error: Error) {
731
- const message = (error as { shortMessage?: string }).shortMessage ?? error.message
732
-
733
- const match = insufficientBalancePattern.exec(message)
734
- if (match)
735
- return {
736
- available: BigInt(match[1]!),
737
- required: BigInt(match[2]!),
738
- token: match[3]! as Address,
739
- }
740
-
741
- const data = extractRevertData(error)
742
- if (!data) return null
743
- try {
744
- const decoded = decodeErrorResult({ abi: insufficientBalanceAbi, data })
745
- return {
746
- available: decoded.args[0],
747
- required: decoded.args[1],
748
- token: decoded.args[2] as Address,
749
- }
750
- } catch {
751
- return null
752
- }
753
- }
754
-
755
- function extractRevertData(error: unknown): `0x${string}` | null {
756
- if (!error || typeof error !== 'object') return null
757
- const e = error as Record<string, unknown>
758
- if (typeof e.data === 'string' && e.data.startsWith('0x')) return e.data as `0x${string}`
759
- if (e.cause) return extractRevertData(e.cause)
760
- if (e.error) return extractRevertData(e.error)
761
- if (typeof e.walk === 'function') {
762
- const inner = (e as { walk: (fn: (e: unknown) => boolean) => unknown }).walk(
763
- (e) => typeof (e as Record<string, unknown>).data === 'string',
764
- )
765
- if (inner) return extractRevertData(inner)
871
+ return undefined
766
872
  }
767
- return null
768
873
  }
769
874
 
770
875
  function buildSwapCalls(