mppx 0.3.15 → 0.4.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.
- package/README.md +1 -0
- package/dist/Challenge.d.ts +38 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +62 -0
- package/dist/Challenge.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +16 -4
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +6 -1
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +6 -1
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/proxy/Service.js +1 -1
- package/dist/proxy/Service.js.map +1 -1
- package/dist/server/Mppx.d.ts +92 -2
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +155 -10
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js +1 -0
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +6 -4
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +13 -6
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.ts +72 -0
- package/src/client/internal/Fetch.test.ts +1 -1
- package/src/client/internal/Fetch.ts +18 -6
- package/src/middlewares/internal/mppx.test.ts +152 -0
- package/src/middlewares/internal/mppx.ts +27 -4
- package/src/proxy/Service.ts +2 -1
- package/src/server/Mppx.test-d.ts +94 -299
- package/src/server/Mppx.test.ts +694 -0
- package/src/server/Mppx.ts +256 -14
- package/src/tempo/client/ChannelOps.ts +1 -0
- package/src/tempo/server/Charge.ts +9 -4
- package/src/tempo/session/Chain.ts +15 -5
- package/dist/tempo/internal/simulate.d.ts +0 -21
- package/dist/tempo/internal/simulate.d.ts.map +0 -1
- package/dist/tempo/internal/simulate.js +0 -31
- package/dist/tempo/internal/simulate.js.map +0 -1
- package/src/tempo/internal/simulate.ts +0 -49
package/src/server/Mppx.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
2
|
import * as Challenge from '../Challenge.js'
|
|
3
|
-
import
|
|
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'
|
|
7
7
|
import type * as Method from '../Method.js'
|
|
8
|
+
import * as PaymentRequest from '../PaymentRequest.js'
|
|
8
9
|
import type * as Receipt from '../Receipt.js'
|
|
9
10
|
import type * as z from '../zod.js'
|
|
10
11
|
import * as NodeListener from './NodeListener.js'
|
|
@@ -26,7 +27,45 @@ export type Mppx<
|
|
|
26
27
|
realm: string
|
|
27
28
|
/** The transport used. */
|
|
28
29
|
transport: transport
|
|
29
|
-
} &
|
|
30
|
+
} & (transport extends Transport.Http
|
|
31
|
+
? {
|
|
32
|
+
/**
|
|
33
|
+
* Combines multiple method handlers into a single route handler that presents
|
|
34
|
+
* all methods to the client via multiple `WWW-Authenticate` headers.
|
|
35
|
+
*
|
|
36
|
+
* Each entry is a `[method, options]` tuple where `method` is one of the
|
|
37
|
+
* server methods passed to `Mppx.create()`, looked up by `name`+`intent`.
|
|
38
|
+
*
|
|
39
|
+
* Only available on HTTP transports.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { Mppx, tempo, stripe } from 'mppx/server'
|
|
44
|
+
*
|
|
45
|
+
* const mppx = Mppx.create({
|
|
46
|
+
* methods: [
|
|
47
|
+
* tempo.charge({ currency: USDC, recipient: '0x...' }),
|
|
48
|
+
* stripe.charge({ currency: 'usd' }),
|
|
49
|
+
* ],
|
|
50
|
+
* secretKey,
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* app.get('/api/resource', async (req) => {
|
|
54
|
+
* const result = await mppx.compose(
|
|
55
|
+
* mppx.tempo.charge({ amount: '100' }),
|
|
56
|
+
* mppx.stripe.charge({ amount: '100' }),
|
|
57
|
+
* )(req)
|
|
58
|
+
* if (result.status === 402) return result.challenge
|
|
59
|
+
* return result.withReceipt(new Response('OK'))
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
compose(
|
|
64
|
+
...entries: ComposeEntry<FlattenMethods<methods>>[]
|
|
65
|
+
): (input: Request) => Promise<MethodFn.Response<Transport.Http>>
|
|
66
|
+
}
|
|
67
|
+
: {}) &
|
|
68
|
+
Handlers<FlattenMethods<methods>, transport>
|
|
30
69
|
|
|
31
70
|
/** Extracts the transport override from a method, if any. */
|
|
32
71
|
type TransportOverrideOf<mi> = mi extends { transport?: infer transport }
|
|
@@ -68,6 +107,20 @@ type UniqueIntentHandlers<
|
|
|
68
107
|
>
|
|
69
108
|
}
|
|
70
109
|
|
|
110
|
+
/** Nested handlers: `mppx.tempo.charge(...)`, grouped by method name then intent. */
|
|
111
|
+
type NestedHandlers<
|
|
112
|
+
methods extends readonly Method.AnyServer[],
|
|
113
|
+
transport extends Transport.AnyTransport,
|
|
114
|
+
> = {
|
|
115
|
+
[name in methods[number]['name']]: {
|
|
116
|
+
[mi in Extract<methods[number], { name: name }> as mi['intent']]: MethodFn<
|
|
117
|
+
mi,
|
|
118
|
+
EffectiveTransportOf<mi, transport>,
|
|
119
|
+
NonNullable<mi['defaults']>
|
|
120
|
+
> & { _method: mi }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
type Handlers<
|
|
72
125
|
methods extends readonly Method.AnyServer[],
|
|
73
126
|
transport extends Transport.AnyTransport,
|
|
@@ -77,7 +130,8 @@ type Handlers<
|
|
|
77
130
|
EffectiveTransportOf<mi, transport>,
|
|
78
131
|
NonNullable<mi['defaults']>
|
|
79
132
|
>
|
|
80
|
-
} & UniqueIntentHandlers<methods, transport>
|
|
133
|
+
} & UniqueIntentHandlers<methods, transport> &
|
|
134
|
+
NestedHandlers<methods, transport>
|
|
81
135
|
|
|
82
136
|
/**
|
|
83
137
|
* Creates a server-side payment handler from methods.
|
|
@@ -135,7 +189,44 @@ export function create<
|
|
|
135
189
|
if (intentCount[mi.intent] === 1) handlers[mi.intent] = handlers[`${mi.name}/${mi.intent}`]
|
|
136
190
|
}
|
|
137
191
|
|
|
138
|
-
|
|
192
|
+
// Build nested handlers: mppx.tempo.charge(...)
|
|
193
|
+
for (const mi of methods) {
|
|
194
|
+
if (!handlers[mi.name]) handlers[mi.name] = {}
|
|
195
|
+
const fn = handlers[`${mi.name}/${mi.intent}`] as AnyMethodFn & { _method?: Method.AnyServer }
|
|
196
|
+
fn._method = mi
|
|
197
|
+
;(handlers[mi.name] as Record<string, unknown>)[mi.intent] = fn
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function composeFn(
|
|
201
|
+
...entries: readonly [
|
|
202
|
+
Method.AnyServer | AnyMethodFnWithMethod | string,
|
|
203
|
+
Record<string, unknown>,
|
|
204
|
+
][]
|
|
205
|
+
) {
|
|
206
|
+
if (transport.name !== 'http') throw new Error('compose() only supports HTTP transport')
|
|
207
|
+
if (entries.length === 0) throw new Error('compose() requires at least one entry')
|
|
208
|
+
const configured = entries.map(([methodOrKey, options]) => {
|
|
209
|
+
const key =
|
|
210
|
+
typeof methodOrKey === 'string'
|
|
211
|
+
? methodOrKey
|
|
212
|
+
: typeof methodOrKey === 'function' && '_method' in methodOrKey
|
|
213
|
+
? `${(methodOrKey._method as Method.AnyServer).name}/${(methodOrKey._method as Method.AnyServer).intent}`
|
|
214
|
+
: `${(methodOrKey as Method.AnyServer).name}/${(methodOrKey as Method.AnyServer).intent}`
|
|
215
|
+
const handlerFn = handlers[key] as AnyMethodFn | undefined
|
|
216
|
+
if (!handlerFn)
|
|
217
|
+
throw new Error(`No handler for "${key}". Is this method in your methods array?`)
|
|
218
|
+
return handlerFn(options)
|
|
219
|
+
})
|
|
220
|
+
return compose(...(configured as ConfiguredHandler[]))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
methods,
|
|
225
|
+
compose: composeFn,
|
|
226
|
+
realm: realm as string,
|
|
227
|
+
transport,
|
|
228
|
+
...handlers,
|
|
229
|
+
} as never
|
|
139
230
|
}
|
|
140
231
|
|
|
141
232
|
export declare namespace create {
|
|
@@ -166,20 +257,14 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
166
257
|
const { defaults, method, realm, respond, secretKey, transport, verify } = parameters
|
|
167
258
|
|
|
168
259
|
return (options) => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
...options,
|
|
173
|
-
}
|
|
260
|
+
const { description, meta, ...rest } = options
|
|
261
|
+
const merged = { ...defaults, ...rest }
|
|
262
|
+
|
|
174
263
|
return Object.assign(
|
|
175
264
|
async (input: Transport.InputOf): Promise<MethodFn.Response> => {
|
|
176
|
-
const { description, meta, ...rest } = options
|
|
177
265
|
const expires =
|
|
178
266
|
'expires' in options ? (options.expires as string | undefined) : Expires.minutes(5)
|
|
179
267
|
|
|
180
|
-
// Merge defaults with per-request options
|
|
181
|
-
const merged = { ...defaults, ...rest }
|
|
182
|
-
|
|
183
268
|
// Extract credential once — getCredential may have side effects (e.g. SSE transports).
|
|
184
269
|
const [credential, credentialError] = (() => {
|
|
185
270
|
try {
|
|
@@ -248,6 +333,9 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
248
333
|
// Verify the credential's challenge matches this route's configured
|
|
249
334
|
// request. Prevents cross-route scope confusion where a credential
|
|
250
335
|
// issued for a cheap route is presented at an expensive route.
|
|
336
|
+
// Note: we compare specific payment parameters rather than the full
|
|
337
|
+
// request because the `request` hook may produce credential-dependent
|
|
338
|
+
// output (e.g. `feePayer` differs between 402 and credential calls).
|
|
251
339
|
{
|
|
252
340
|
const routeReq = challenge.request as Record<string, unknown>
|
|
253
341
|
const echoedReq = credential.challenge.request as Record<string, unknown>
|
|
@@ -339,7 +427,16 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
339
427
|
},
|
|
340
428
|
}
|
|
341
429
|
},
|
|
342
|
-
{
|
|
430
|
+
{
|
|
431
|
+
_internal: {
|
|
432
|
+
...method,
|
|
433
|
+
...defaults,
|
|
434
|
+
...options,
|
|
435
|
+
name: method.name,
|
|
436
|
+
intent: method.intent,
|
|
437
|
+
_canonicalRequest: PaymentRequest.fromMethod(method, merged),
|
|
438
|
+
},
|
|
439
|
+
},
|
|
343
440
|
)
|
|
344
441
|
}
|
|
345
442
|
}
|
|
@@ -376,6 +473,8 @@ export type MethodFn<
|
|
|
376
473
|
) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>
|
|
377
474
|
/** @internal */
|
|
378
475
|
export type AnyMethodFn = (options: any) => (input: any) => Promise<any>
|
|
476
|
+
/** A MethodFn tagged with its source Method (set by `create()`). @internal */
|
|
477
|
+
type AnyMethodFnWithMethod = AnyMethodFn & { _method: Method.AnyServer }
|
|
379
478
|
|
|
380
479
|
/** @internal */
|
|
381
480
|
declare namespace MethodFn {
|
|
@@ -402,6 +501,149 @@ declare namespace MethodFn {
|
|
|
402
501
|
}
|
|
403
502
|
}
|
|
404
503
|
|
|
504
|
+
/** A configured handler — the return value of e.g. `mppx.charge({ ... })`. @internal */
|
|
505
|
+
type ConfiguredHandler = ((input: Request) => Promise<MethodFn.Response<Transport.Http>>) & {
|
|
506
|
+
_internal: {
|
|
507
|
+
name: string
|
|
508
|
+
intent: string
|
|
509
|
+
_canonicalRequest: Record<string, unknown>
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** An entry for `compose()`: a method reference, handler function ref, or string key paired with its options. */
|
|
514
|
+
type ComposeEntry<methods extends readonly Method.AnyServer[]> =
|
|
515
|
+
| {
|
|
516
|
+
[i in keyof methods]: readonly [
|
|
517
|
+
methods[i],
|
|
518
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>,
|
|
519
|
+
]
|
|
520
|
+
}[number]
|
|
521
|
+
| {
|
|
522
|
+
[i in keyof methods]: readonly [
|
|
523
|
+
`${methods[i]['name']}/${methods[i]['intent']}`,
|
|
524
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>,
|
|
525
|
+
]
|
|
526
|
+
}[number]
|
|
527
|
+
| {
|
|
528
|
+
[i in keyof methods]: readonly [
|
|
529
|
+
MethodFn<methods[i], any, any> & { _method: methods[i] },
|
|
530
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>,
|
|
531
|
+
]
|
|
532
|
+
}[number]
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Combines multiple configured payment handlers into a single route handler
|
|
536
|
+
* that presents all methods to the client via multiple `WWW-Authenticate` headers.
|
|
537
|
+
*
|
|
538
|
+
* When no credential is present, all handlers are called and their challenges
|
|
539
|
+
* are merged into a single 402 response. When a credential is present, it is
|
|
540
|
+
* dispatched to the handler matching the credential's `method`+`intent`.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```ts
|
|
544
|
+
* import { Mppx, tempo, stripe } from 'mppx/server'
|
|
545
|
+
*
|
|
546
|
+
* const mppx = Mppx.create({
|
|
547
|
+
* methods: [tempo(), stripe()],
|
|
548
|
+
* secretKey: process.env.PAYMENT_SECRET_KEY,
|
|
549
|
+
* })
|
|
550
|
+
*
|
|
551
|
+
* app.get('/api/resource', async (req) => {
|
|
552
|
+
* const result = await Mppx.compose(
|
|
553
|
+
* mppx['tempo/charge']({ amount: '100', currency: USDC, recipient: '0x...' }),
|
|
554
|
+
* mppx['stripe/charge']({ amount: '100', currency: 'usd' }),
|
|
555
|
+
* )(req)
|
|
556
|
+
* if (result.status === 402) return result.challenge
|
|
557
|
+
* return result.withReceipt(new Response('OK'))
|
|
558
|
+
* })
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
export function compose(
|
|
562
|
+
...handlers: readonly ((input: Request) => Promise<MethodFn.Response<Transport.Http>>)[]
|
|
563
|
+
): (input: Request) => Promise<MethodFn.Response<Transport.Http>> {
|
|
564
|
+
if (handlers.length === 0) throw new Error('compose() requires at least one handler')
|
|
565
|
+
|
|
566
|
+
return async (input: Request) => {
|
|
567
|
+
// Try to extract a Payment credential to decide whether to dispatch or challenge.
|
|
568
|
+
// Only gate on the Payment scheme — other auth schemes (Bearer, Basic, etc.)
|
|
569
|
+
// should fall through to the merged-402 path so all offers are presented.
|
|
570
|
+
const header = input.headers.get('Authorization')
|
|
571
|
+
const paymentHeader = header ? Credential.extractPaymentScheme(header) : null
|
|
572
|
+
|
|
573
|
+
if (paymentHeader) {
|
|
574
|
+
// Parse the credential to find method+intent for dispatch.
|
|
575
|
+
let credential: Credential.Credential | undefined
|
|
576
|
+
try {
|
|
577
|
+
credential = Credential.deserialize(paymentHeader)
|
|
578
|
+
} catch {}
|
|
579
|
+
|
|
580
|
+
if (credential) {
|
|
581
|
+
const { method: credMethod, intent: credIntent } = credential.challenge
|
|
582
|
+
const credReq = credential.challenge.request as Record<string, unknown>
|
|
583
|
+
|
|
584
|
+
// Filter by name+intent, then narrow by comparing stable request fields
|
|
585
|
+
// from the echoed challenge against each handler's canonical request.
|
|
586
|
+
// Uses the schema-parsed canonical form (not raw options) so that
|
|
587
|
+
// transformed fields (e.g. amount with decimals) match correctly.
|
|
588
|
+
const candidates = handlers.filter((h) => {
|
|
589
|
+
const meta = (h as ConfiguredHandler)._internal
|
|
590
|
+
if (!meta || meta.name !== credMethod || meta.intent !== credIntent) return false
|
|
591
|
+
const canonical = meta._canonicalRequest
|
|
592
|
+
if (!canonical) return true
|
|
593
|
+
for (const field of ['amount', 'currency', 'recipient', 'chainId'] as const) {
|
|
594
|
+
const canonicalVal = canonical[field]
|
|
595
|
+
if (
|
|
596
|
+
canonicalVal !== undefined &&
|
|
597
|
+
credReq[field] !== undefined &&
|
|
598
|
+
String(canonicalVal) !== String(credReq[field])
|
|
599
|
+
)
|
|
600
|
+
return false
|
|
601
|
+
}
|
|
602
|
+
return true
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
const match =
|
|
606
|
+
candidates[0] ??
|
|
607
|
+
handlers.find((h) => {
|
|
608
|
+
const meta = (h as ConfiguredHandler)._internal
|
|
609
|
+
return meta?.name === credMethod && meta?.intent === credIntent
|
|
610
|
+
})
|
|
611
|
+
if (match) return match(input)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Payment credential present but no matching handler — dispatch to first
|
|
615
|
+
// handler which will reject with an appropriate error (invalid challenge, etc.).
|
|
616
|
+
return handlers[0]!(input)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// No credential — call all handlers and merge 402 challenges.
|
|
620
|
+
const results = await Promise.all(handlers.map((h) => h(input)))
|
|
621
|
+
|
|
622
|
+
// Merge WWW-Authenticate headers from all 402 responses.
|
|
623
|
+
const mergedHeaders = new Headers()
|
|
624
|
+
mergedHeaders.set('Cache-Control', 'no-store')
|
|
625
|
+
|
|
626
|
+
let body: string | null = null
|
|
627
|
+
for (const result of results) {
|
|
628
|
+
if (result.status !== 402) continue
|
|
629
|
+
const response = result.challenge as Response
|
|
630
|
+
const wwwAuth = response.headers.get('WWW-Authenticate')
|
|
631
|
+
if (wwwAuth) mergedHeaders.append('WWW-Authenticate', wwwAuth)
|
|
632
|
+
// Use the first handler's body for the problem details response.
|
|
633
|
+
if (!body) {
|
|
634
|
+
const contentType = response.headers.get('Content-Type')
|
|
635
|
+
if (contentType) mergedHeaders.set('Content-Type', contentType)
|
|
636
|
+
body = await response.text()
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
status: 402,
|
|
642
|
+
challenge: new Response(body, { status: 402, headers: mergedHeaders }),
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
405
647
|
/**
|
|
406
648
|
* Wraps a payment handler to create a Node.js HTTP listener.
|
|
407
649
|
*
|
|
@@ -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
|
|
|
@@ -238,12 +238,16 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
238
238
|
if (feePayer && methodDetails?.feePayer !== false)
|
|
239
239
|
FeePayer.validateCalls(calls, { amount, currency, recipient })
|
|
240
240
|
|
|
241
|
+
const resolvedFeeToken =
|
|
242
|
+
transaction.feeToken ?? defaults.currency[chainId as keyof typeof defaults.currency]
|
|
243
|
+
|
|
241
244
|
const serializedTransaction_final = await (async () => {
|
|
242
245
|
if (feePayer && methodDetails?.feePayer !== false) {
|
|
243
246
|
return signTransaction(client, {
|
|
244
247
|
...transaction,
|
|
245
248
|
account: feePayer,
|
|
246
249
|
feePayer,
|
|
250
|
+
feeToken: resolvedFeeToken,
|
|
247
251
|
} as never)
|
|
248
252
|
}
|
|
249
253
|
return serializedTransaction
|
|
@@ -258,11 +262,12 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
258
262
|
// Optimistic path: simulate to catch obvious reverts, then broadcast
|
|
259
263
|
// without waiting for on-chain confirmation. The returned receipt
|
|
260
264
|
// assumes success — callers opt into this risk via waitForConfirmation: false.
|
|
261
|
-
await
|
|
265
|
+
await viem_call(client, {
|
|
262
266
|
...transaction,
|
|
263
|
-
|
|
267
|
+
account: transaction.from,
|
|
268
|
+
feeToken: resolvedFeeToken,
|
|
264
269
|
calls,
|
|
265
|
-
})
|
|
270
|
+
} as never)
|
|
266
271
|
const hash = await sendRawTransaction(client, {
|
|
267
272
|
serializedTransaction: serializedTransaction_final,
|
|
268
273
|
})
|
|
@@ -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
|
|
|
@@ -277,20 +276,28 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
277
276
|
})
|
|
278
277
|
}
|
|
279
278
|
|
|
279
|
+
const resolvedFeeToken =
|
|
280
|
+
transaction.feeToken ?? defaults.currency[client.chain?.id as keyof typeof defaults.currency]
|
|
281
|
+
|
|
280
282
|
const serializedTransaction_final = await (async () => {
|
|
281
283
|
if (feePayer) {
|
|
282
284
|
return signTransaction(client, {
|
|
283
285
|
...transaction,
|
|
284
286
|
account: feePayer,
|
|
285
287
|
feePayer,
|
|
288
|
+
feeToken: resolvedFeeToken,
|
|
286
289
|
} as never)
|
|
287
290
|
}
|
|
288
291
|
return serializedTransaction
|
|
289
292
|
})()
|
|
290
293
|
|
|
291
294
|
if (!waitForConfirmation) {
|
|
292
|
-
|
|
293
|
-
|
|
295
|
+
await call(client, {
|
|
296
|
+
...transaction,
|
|
297
|
+
account: transaction.from,
|
|
298
|
+
feeToken: resolvedFeeToken,
|
|
299
|
+
calls,
|
|
300
|
+
} as never)
|
|
294
301
|
const txHash = await sendRawTransaction(client, {
|
|
295
302
|
serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
|
|
296
303
|
})
|
|
@@ -298,7 +305,7 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
298
305
|
return {
|
|
299
306
|
txHash,
|
|
300
307
|
onChain: {
|
|
301
|
-
payer: from,
|
|
308
|
+
payer: transaction.from,
|
|
302
309
|
payee,
|
|
303
310
|
token,
|
|
304
311
|
authorizedSigner,
|
|
@@ -412,6 +419,9 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
412
419
|
...transaction,
|
|
413
420
|
account: feePayer,
|
|
414
421
|
feePayer,
|
|
422
|
+
feeToken:
|
|
423
|
+
transaction.feeToken ??
|
|
424
|
+
defaults.currency[client.chain?.id as keyof typeof defaults.currency],
|
|
415
425
|
} as never)
|
|
416
426
|
}
|
|
417
427
|
return serializedTransaction
|
|
@@ -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
|
-
}
|