mppx 0.3.14 → 0.3.16

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 (60) hide show
  1. package/README.md +1 -0
  2. package/dist/Challenge.d.ts +38 -0
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +62 -0
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/bin.d.ts +3 -0
  7. package/dist/bin.d.ts.map +1 -0
  8. package/dist/bin.js +4 -0
  9. package/dist/bin.js.map +1 -0
  10. package/dist/cli.d.ts +26 -2
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +1478 -915
  13. package/dist/cli.js.map +1 -1
  14. package/dist/client/Mppx.d.ts +2 -0
  15. package/dist/client/Mppx.d.ts.map +1 -1
  16. package/dist/client/Mppx.js +2 -0
  17. package/dist/client/Mppx.js.map +1 -1
  18. package/dist/client/internal/Fetch.d.ts.map +1 -1
  19. package/dist/client/internal/Fetch.js +16 -4
  20. package/dist/client/internal/Fetch.js.map +1 -1
  21. package/dist/middlewares/internal/mppx.d.ts +6 -1
  22. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  23. package/dist/middlewares/internal/mppx.js +4 -0
  24. package/dist/middlewares/internal/mppx.js.map +1 -1
  25. package/dist/server/Mppx.d.ts +79 -1
  26. package/dist/server/Mppx.d.ts.map +1 -1
  27. package/dist/server/Mppx.js +135 -7
  28. package/dist/server/Mppx.js.map +1 -1
  29. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  30. package/dist/tempo/client/ChannelOps.js +1 -0
  31. package/dist/tempo/client/ChannelOps.js.map +1 -1
  32. package/dist/tempo/server/Charge.d.ts.map +1 -1
  33. package/dist/tempo/server/Charge.js +4 -4
  34. package/dist/tempo/server/Charge.js.map +1 -1
  35. package/dist/tempo/session/Chain.d.ts.map +1 -1
  36. package/dist/tempo/session/Chain.js +9 -6
  37. package/dist/tempo/session/Chain.js.map +1 -1
  38. package/package.json +4 -4
  39. package/src/Challenge.ts +72 -0
  40. package/src/bin.ts +4 -0
  41. package/src/cli.test.ts +180 -252
  42. package/src/cli.ts +1085 -485
  43. package/src/client/Mppx.test-d.ts +9 -0
  44. package/src/client/Mppx.test.ts +78 -0
  45. package/src/client/Mppx.ts +5 -0
  46. package/src/client/internal/Fetch.test.ts +1 -1
  47. package/src/client/internal/Fetch.ts +18 -6
  48. package/src/middlewares/internal/mppx.test.ts +152 -0
  49. package/src/middlewares/internal/mppx.ts +22 -3
  50. package/src/server/Mppx.test-d.ts +94 -299
  51. package/src/server/Mppx.test.ts +650 -0
  52. package/src/server/Mppx.ts +213 -9
  53. package/src/tempo/client/ChannelOps.ts +1 -0
  54. package/src/tempo/server/Charge.ts +4 -3
  55. package/src/tempo/session/Chain.ts +8 -5
  56. package/dist/tempo/internal/simulate.d.ts +0 -21
  57. package/dist/tempo/internal/simulate.d.ts.map +0 -1
  58. package/dist/tempo/internal/simulate.js +0 -31
  59. package/dist/tempo/internal/simulate.js.map +0 -1
  60. package/src/tempo/internal/simulate.ts +0 -49
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
2
  import * as Challenge from '../Challenge.js'
3
- import type * as Credential from '../Credential.js'
3
+ import * as Credential from '../Credential.js'
4
4
  import * as Errors from '../Errors.js'
5
5
  import * as Expires from '../Expires.js'
6
6
  import * as Env from '../internal/env.js'
@@ -22,6 +22,38 @@ export type Mppx<
22
22
  > = {
23
23
  /** Methods to configure. */
24
24
  methods: FlattenMethods<methods>
25
+ /**
26
+ * Combines multiple method handlers into a single route handler that presents
27
+ * all methods to the client via multiple `WWW-Authenticate` headers.
28
+ *
29
+ * Each entry is a `[method, options]` tuple where `method` is one of the
30
+ * server methods passed to `Mppx.create()`, looked up by `name`+`intent`.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { Mppx, tempo, stripe } from 'mppx/server'
35
+ *
36
+ * const mppx = Mppx.create({
37
+ * methods: [
38
+ * tempo.charge({ currency: USDC, recipient: '0x...' }),
39
+ * stripe.charge({ currency: 'usd' }),
40
+ * ],
41
+ * secretKey,
42
+ * })
43
+ *
44
+ * app.get('/api/resource', async (req) => {
45
+ * const result = await mppx.compose(
46
+ * mppx.tempo.charge({ amount: '100' }),
47
+ * mppx.stripe.charge({ amount: '100' }),
48
+ * )(req)
49
+ * if (result.status === 402) return result.challenge
50
+ * return result.withReceipt(new Response('OK'))
51
+ * })
52
+ * ```
53
+ */
54
+ compose(
55
+ ...entries: ComposeEntry<FlattenMethods<methods>>[]
56
+ ): (input: Request) => Promise<MethodFn.Response<Transport.Http>>
25
57
  /** Server realm (e.g., hostname). */
26
58
  realm: string
27
59
  /** The transport used. */
@@ -68,6 +100,20 @@ type UniqueIntentHandlers<
68
100
  >
69
101
  }
70
102
 
103
+ /** Nested handlers: `mppx.tempo.charge(...)`, grouped by method name then intent. */
104
+ type NestedHandlers<
105
+ methods extends readonly Method.AnyServer[],
106
+ transport extends Transport.AnyTransport,
107
+ > = {
108
+ [name in methods[number]['name']]: {
109
+ [mi in Extract<methods[number], { name: name }> as mi['intent']]: MethodFn<
110
+ mi,
111
+ EffectiveTransportOf<mi, transport>,
112
+ NonNullable<mi['defaults']>
113
+ >
114
+ }
115
+ }
116
+
71
117
  type Handlers<
72
118
  methods extends readonly Method.AnyServer[],
73
119
  transport extends Transport.AnyTransport,
@@ -77,7 +123,8 @@ type Handlers<
77
123
  EffectiveTransportOf<mi, transport>,
78
124
  NonNullable<mi['defaults']>
79
125
  >
80
- } & UniqueIntentHandlers<methods, transport>
126
+ } & UniqueIntentHandlers<methods, transport> &
127
+ NestedHandlers<methods, transport>
81
128
 
82
129
  /**
83
130
  * Creates a server-side payment handler from methods.
@@ -135,7 +182,32 @@ export function create<
135
182
  if (intentCount[mi.intent] === 1) handlers[mi.intent] = handlers[`${mi.name}/${mi.intent}`]
136
183
  }
137
184
 
138
- return { methods, realm: realm as string, transport, ...handlers } as never
185
+ // Build nested handlers: mppx.tempo.charge(...)
186
+ for (const mi of methods) {
187
+ if (!handlers[mi.name]) handlers[mi.name] = {}
188
+ ;(handlers[mi.name] as Record<string, unknown>)[mi.intent] = handlers[`${mi.name}/${mi.intent}`]
189
+ }
190
+
191
+ function composeFn(...entries: readonly [Method.AnyServer | string, Record<string, unknown>][]) {
192
+ if (entries.length === 0) throw new Error('compose() requires at least one entry')
193
+ const configured = entries.map(([methodOrKey, options]) => {
194
+ const key =
195
+ typeof methodOrKey === 'string' ? methodOrKey : `${methodOrKey.name}/${methodOrKey.intent}`
196
+ const handlerFn = handlers[key] as AnyMethodFn | undefined
197
+ if (!handlerFn)
198
+ throw new Error(`No handler for "${key}". Is this method in your methods array?`)
199
+ return handlerFn(options)
200
+ })
201
+ return compose(...(configured as ConfiguredHandler[]))
202
+ }
203
+
204
+ return {
205
+ methods,
206
+ compose: composeFn,
207
+ realm: realm as string,
208
+ transport,
209
+ ...handlers,
210
+ } as never
139
211
  }
140
212
 
141
213
  export declare namespace create {
@@ -166,11 +238,6 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
166
238
  const { defaults, method, realm, respond, secretKey, transport, verify } = parameters
167
239
 
168
240
  return (options) => {
169
- const methodMeta = {
170
- ...method,
171
- ...defaults,
172
- ...options,
173
- }
174
241
  return Object.assign(
175
242
  async (input: Transport.InputOf): Promise<MethodFn.Response> => {
176
243
  const { description, meta, ...rest } = options
@@ -248,6 +315,9 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
248
315
  // Verify the credential's challenge matches this route's configured
249
316
  // request. Prevents cross-route scope confusion where a credential
250
317
  // issued for a cheap route is presented at an expensive route.
318
+ // Note: we compare specific payment parameters rather than the full
319
+ // request because the `request` hook may produce credential-dependent
320
+ // output (e.g. `feePayer` differs between 402 and credential calls).
251
321
  {
252
322
  const routeReq = challenge.request as Record<string, unknown>
253
323
  const echoedReq = credential.challenge.request as Record<string, unknown>
@@ -339,7 +409,9 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
339
409
  },
340
410
  }
341
411
  },
342
- { _internal: methodMeta },
412
+ {
413
+ _internal: { ...method, ...defaults, ...options, name: method.name, intent: method.intent },
414
+ },
343
415
  )
344
416
  }
345
417
  }
@@ -402,6 +474,138 @@ declare namespace MethodFn {
402
474
  }
403
475
  }
404
476
 
477
+ /** A configured handler — the return value of e.g. `mppx.charge({ ... })`. @internal */
478
+ type ConfiguredHandler = ((input: Request) => Promise<MethodFn.Response<Transport.Http>>) & {
479
+ _internal: { name: string; intent: string }
480
+ }
481
+
482
+ /** An entry for `compose()`: a method reference (or string key) paired with its options. */
483
+ type ComposeEntry<methods extends readonly Method.AnyServer[]> =
484
+ | {
485
+ [i in keyof methods]: readonly [
486
+ methods[i],
487
+ MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>,
488
+ ]
489
+ }[number]
490
+ | {
491
+ [i in keyof methods]: readonly [
492
+ `${methods[i]['name']}/${methods[i]['intent']}`,
493
+ MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>,
494
+ ]
495
+ }[number]
496
+
497
+ /**
498
+ * Combines multiple configured payment handlers into a single route handler
499
+ * that presents all methods to the client via multiple `WWW-Authenticate` headers.
500
+ *
501
+ * When no credential is present, all handlers are called and their challenges
502
+ * are merged into a single 402 response. When a credential is present, it is
503
+ * dispatched to the handler matching the credential's `method`+`intent`.
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * import { Mppx, tempo, stripe } from 'mppx/server'
508
+ *
509
+ * const mppx = Mppx.create({
510
+ * methods: [tempo(), stripe()],
511
+ * secretKey: process.env.PAYMENT_SECRET_KEY,
512
+ * })
513
+ *
514
+ * app.get('/api/resource', async (req) => {
515
+ * const result = await Mppx.compose(
516
+ * mppx['tempo/charge']({ amount: '100', currency: USDC, recipient: '0x...' }),
517
+ * mppx['stripe/charge']({ amount: '100', currency: 'usd' }),
518
+ * )(req)
519
+ * if (result.status === 402) return result.challenge
520
+ * return result.withReceipt(new Response('OK'))
521
+ * })
522
+ * ```
523
+ */
524
+ export function compose(
525
+ ...handlers: readonly ((input: Request) => Promise<MethodFn.Response<Transport.Http>>)[]
526
+ ): (input: Request) => Promise<MethodFn.Response<Transport.Http>> {
527
+ if (handlers.length === 0) throw new Error('compose() requires at least one handler')
528
+
529
+ return async (input: Request) => {
530
+ // Try to extract a Payment credential to decide whether to dispatch or challenge.
531
+ // Only gate on the Payment scheme — other auth schemes (Bearer, Basic, etc.)
532
+ // should fall through to the merged-402 path so all offers are presented.
533
+ const header = input.headers.get('Authorization')
534
+ const paymentHeader = header ? Credential.extractPaymentScheme(header) : null
535
+
536
+ if (paymentHeader) {
537
+ // Parse the credential to find method+intent for dispatch.
538
+ let credential: Credential.Credential | undefined
539
+ try {
540
+ credential = Credential.deserialize(paymentHeader)
541
+ } catch {}
542
+
543
+ if (credential) {
544
+ const { method: credMethod, intent: credIntent } = credential.challenge
545
+ const credReq = credential.challenge.request as Record<string, unknown>
546
+
547
+ // Filter by name+intent, then narrow by comparing stable request fields
548
+ // from the echoed challenge against each handler's configured options.
549
+ // This disambiguates handlers with the same name/intent but different
550
+ // amounts, currencies, recipients, etc. without invoking any handler.
551
+ const candidates = handlers.filter((h) => {
552
+ const meta = (h as ConfiguredHandler)._internal
553
+ if (!meta || meta.name !== credMethod || meta.intent !== credIntent) return false
554
+ // Compare stable fields that don't change between 402 and credential calls.
555
+ for (const field of ['amount', 'currency', 'recipient'] as const) {
556
+ const metaVal = (meta as Record<string, unknown>)[field]
557
+ if (
558
+ metaVal !== undefined &&
559
+ credReq[field] !== undefined &&
560
+ String(metaVal) !== String(credReq[field])
561
+ )
562
+ return false
563
+ }
564
+ return true
565
+ })
566
+
567
+ const match =
568
+ candidates[0] ??
569
+ handlers.find((h) => {
570
+ const meta = (h as ConfiguredHandler)._internal
571
+ return meta?.name === credMethod && meta?.intent === credIntent
572
+ })
573
+ if (match) return match(input)
574
+ }
575
+
576
+ // Payment credential present but no matching handler — dispatch to first
577
+ // handler which will reject with an appropriate error (invalid challenge, etc.).
578
+ return handlers[0]!(input)
579
+ }
580
+
581
+ // No credential — call all handlers and merge 402 challenges.
582
+ const results = await Promise.all(handlers.map((h) => h(input)))
583
+
584
+ // Merge WWW-Authenticate headers from all 402 responses.
585
+ const mergedHeaders = new Headers()
586
+ mergedHeaders.set('Cache-Control', 'no-store')
587
+
588
+ let body: string | null = null
589
+ for (const result of results) {
590
+ if (result.status !== 402) continue
591
+ const response = result.challenge as Response
592
+ const wwwAuth = response.headers.get('WWW-Authenticate')
593
+ if (wwwAuth) mergedHeaders.append('WWW-Authenticate', wwwAuth)
594
+ // Use the first handler's body for the problem details response.
595
+ if (!body) {
596
+ const contentType = response.headers.get('Content-Type')
597
+ if (contentType) mergedHeaders.set('Content-Type', contentType)
598
+ body = await response.text()
599
+ }
600
+ }
601
+
602
+ return {
603
+ status: 402,
604
+ challenge: new Response(body, { status: 402, headers: mergedHeaders }),
605
+ }
606
+ }
607
+ }
608
+
405
609
  /**
406
610
  * Wraps a payment handler to create a Node.js HTTP listener.
407
611
  *
@@ -164,6 +164,7 @@ export async function createOpenPayload(
164
164
  { to: escrowContract, data: openData },
165
165
  ],
166
166
  ...(feePayer && { feePayer: true }),
167
+ feeToken: currency,
167
168
  } as never)
168
169
  prepared.gas = prepared.gas! + 5_000n
169
170
  const transaction = (await signTransaction(client, prepared as never)) as Hex.Hex
@@ -4,6 +4,7 @@ import {
4
4
  sendRawTransaction,
5
5
  sendRawTransactionSync,
6
6
  signTransaction,
7
+ call as viem_call,
7
8
  } from 'viem/actions'
8
9
  import { tempo as tempo_chain } from 'viem/chains'
9
10
  import { Abis, Transaction } from 'viem/tempo'
@@ -15,7 +16,6 @@ import * as Account from '../internal/account.js'
15
16
  import * as defaults from '../internal/defaults.js'
16
17
  import * as FeePayer from '../internal/fee-payer.js'
17
18
  import * as Selectors from '../internal/selectors.js'
18
- import { simulateTransaction } from '../internal/simulate.js'
19
19
  import type * as types from '../internal/types.js'
20
20
  import * as Methods from '../Methods.js'
21
21
 
@@ -258,9 +258,10 @@ export function charge<const parameters extends charge.Parameters>(
258
258
  // Optimistic path: simulate to catch obvious reverts, then broadcast
259
259
  // without waiting for on-chain confirmation. The returned receipt
260
260
  // assumes success — callers opt into this risk via waitForConfirmation: false.
261
- await simulateTransaction(client, {
261
+ await viem_call(client, {
262
262
  ...transaction,
263
- from: transaction.from as `0x${string}`,
263
+ account: transaction.from,
264
+ // @ts-expect-error
264
265
  calls,
265
266
  })
266
267
  const hash = await sendRawTransaction(client, {
@@ -5,13 +5,13 @@ import {
5
5
  decodeFunctionData,
6
6
  encodeFunctionData,
7
7
  getAbiItem,
8
- getAddress,
9
8
  type Hex,
10
9
  isAddressEqual,
11
10
  type ReadContractReturnType,
12
11
  toFunctionSelector,
13
12
  } from 'viem'
14
13
  import {
14
+ call,
15
15
  prepareTransactionRequest,
16
16
  readContract,
17
17
  sendRawTransaction,
@@ -22,7 +22,6 @@ import {
22
22
  import { Transaction } from 'viem/tempo'
23
23
  import { BadRequestError, ChannelClosedError, VerificationFailedError } from '../../Errors.js'
24
24
  import * as defaults from '../internal/defaults.js'
25
- import { simulateTransaction } from '../internal/simulate.js'
26
25
  import { escrowAbi } from './escrow.abi.js'
27
26
  import type { SignedVoucher } from './Types.js'
28
27
 
@@ -289,8 +288,12 @@ export async function broadcastOpenTransaction(parameters: {
289
288
  })()
290
289
 
291
290
  if (!waitForConfirmation) {
292
- const from = getAddress(transaction.from as Address)
293
- await simulateTransaction(client, { ...transaction, from, calls })
291
+ await call(client, {
292
+ ...transaction,
293
+ account: transaction.from,
294
+ // @ts-expect-error
295
+ calls,
296
+ })
294
297
  const txHash = await sendRawTransaction(client, {
295
298
  serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
296
299
  })
@@ -298,7 +301,7 @@ export async function broadcastOpenTransaction(parameters: {
298
301
  return {
299
302
  txHash,
300
303
  onChain: {
301
- payer: from,
304
+ payer: transaction.from,
302
305
  payee,
303
306
  token,
304
307
  authorizedSigner,
@@ -1,21 +0,0 @@
1
- import type { Address, Client } from 'viem';
2
- /**
3
- * Simulate a Tempo transaction via `eth_estimateGas` to catch reverts
4
- * (e.g. insufficient balance, invalid calls) before broadcasting.
5
- */
6
- export declare function simulateTransaction(client: Client, transaction: {
7
- from: Address;
8
- chainId: number;
9
- nonce?: number | bigint | undefined;
10
- maxFeePerGas?: bigint | undefined;
11
- maxPriorityFeePerGas?: bigint | undefined;
12
- feeToken?: string | bigint | undefined;
13
- nonceKey?: bigint | undefined;
14
- validBefore?: number | undefined;
15
- calls?: readonly {
16
- to?: string | undefined;
17
- value?: bigint | undefined;
18
- data?: string | undefined;
19
- }[];
20
- }): Promise<void>;
21
- //# sourceMappingURL=simulate.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"simulate.d.ts","sourceRoot":"","sources":["../../../src/tempo/internal/simulate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE3C;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE;IACX,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACnC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACzC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,KAAK,CAAC,EAAE,SAAS;QACf,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QACvB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAC1B,EAAE,CAAA;CACJ,GACA,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -1,31 +0,0 @@
1
- /**
2
- * Simulate a Tempo transaction via `eth_estimateGas` to catch reverts
3
- * (e.g. insufficient balance, invalid calls) before broadcasting.
4
- */
5
- export async function simulateTransaction(client, transaction) {
6
- const simCalls = (transaction.calls ?? []).map((c) => ({
7
- to: c.to,
8
- value: c.value ? `0x${c.value.toString(16)}` : '0x0',
9
- input: c.data ?? '0x',
10
- }));
11
- await client.request({
12
- method: 'eth_estimateGas',
13
- params: [
14
- {
15
- from: transaction.from,
16
- chainId: `0x${transaction.chainId.toString(16)}`,
17
- nonce: `0x${BigInt(transaction.nonce ?? 0).toString(16)}`,
18
- gas: '0x2dc6c0', // 3M cap
19
- maxFeePerGas: `0x${(transaction.maxFeePerGas ?? 0n).toString(16)}`,
20
- maxPriorityFeePerGas: `0x${(transaction.maxPriorityFeePerGas ?? 0n).toString(16)}`,
21
- feeToken: transaction.feeToken,
22
- nonceKey: `0x${(transaction.nonceKey ?? 0n).toString(16)}`,
23
- calls: simCalls,
24
- ...(transaction.validBefore
25
- ? { validBefore: `0x${transaction.validBefore.toString(16)}` }
26
- : {}),
27
- },
28
- ],
29
- });
30
- }
31
- //# sourceMappingURL=simulate.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"simulate.js","sourceRoot":"","sources":["../../../src/tempo/internal/simulate.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAc,EACd,WAcC;IAED,MAAM,QAAQ,GAAG,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACrD,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK;QACpD,KAAK,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI;KACtB,CAAC,CAAC,CAAA;IACH,MAAM,MAAM,CAAC,OAAO,CAAC;QACnB,MAAM,EAAE,iBAA0B;QAClC,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,WAAW,CAAC,IAAI;gBACtB,OAAO,EAAE,KAAK,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE;gBAChD,KAAK,EAAE,KAAK,MAAM,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE;gBACzD,GAAG,EAAE,UAAU,EAAE,SAAS;gBAC1B,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE;gBAClE,oBAAoB,EAAE,KAAK,CAAC,WAAW,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE;gBAClF,QAAQ,EAAE,WAAW,CAAC,QAAQ;gBAC9B,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE;gBAC1D,KAAK,EAAE,QAAQ;gBACf,GAAG,CAAC,WAAW,CAAC,WAAW;oBACzB,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,WAAW,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE;oBAC9D,CAAC,CAAC,EAAE,CAAC;aACR;SACO;KACX,CAAC,CAAA;AACJ,CAAC"}
@@ -1,49 +0,0 @@
1
- import type { Address, Client } from 'viem'
2
-
3
- /**
4
- * Simulate a Tempo transaction via `eth_estimateGas` to catch reverts
5
- * (e.g. insufficient balance, invalid calls) before broadcasting.
6
- */
7
- export async function simulateTransaction(
8
- client: Client,
9
- transaction: {
10
- from: Address
11
- chainId: number
12
- nonce?: number | bigint | undefined
13
- maxFeePerGas?: bigint | undefined
14
- maxPriorityFeePerGas?: bigint | undefined
15
- feeToken?: string | bigint | undefined
16
- nonceKey?: bigint | undefined
17
- validBefore?: number | undefined
18
- calls?: readonly {
19
- to?: string | undefined
20
- value?: bigint | undefined
21
- data?: string | undefined
22
- }[]
23
- },
24
- ): Promise<void> {
25
- const simCalls = (transaction.calls ?? []).map((c) => ({
26
- to: c.to,
27
- value: c.value ? `0x${c.value.toString(16)}` : '0x0',
28
- input: c.data ?? '0x',
29
- }))
30
- await client.request({
31
- method: 'eth_estimateGas' as never,
32
- params: [
33
- {
34
- from: transaction.from,
35
- chainId: `0x${transaction.chainId.toString(16)}`,
36
- nonce: `0x${BigInt(transaction.nonce ?? 0).toString(16)}`,
37
- gas: '0x2dc6c0', // 3M cap
38
- maxFeePerGas: `0x${(transaction.maxFeePerGas ?? 0n).toString(16)}`,
39
- maxPriorityFeePerGas: `0x${(transaction.maxPriorityFeePerGas ?? 0n).toString(16)}`,
40
- feeToken: transaction.feeToken,
41
- nonceKey: `0x${(transaction.nonceKey ?? 0n).toString(16)}`,
42
- calls: simCalls,
43
- ...(transaction.validBefore
44
- ? { validBefore: `0x${transaction.validBefore.toString(16)}` }
45
- : {}),
46
- },
47
- ] as never,
48
- })
49
- }