mppx 0.8.8 → 0.8.11
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/CHANGELOG.md +24 -0
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +21 -9
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +50 -19
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +11 -0
- package/dist/cli/validate/helpers.js.map +1 -1
- package/dist/cli/validate/index.d.ts +1 -0
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +4 -0
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +2 -2
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +39 -2
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +49 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/fee-token.d.ts +8 -0
- package/dist/tempo/internal/fee-token.d.ts.map +1 -1
- package/dist/tempo/internal/fee-token.js +19 -6
- package/dist/tempo/internal/fee-token.js.map +1 -1
- package/dist/tempo/legacy/session/Chain.d.ts.map +1 -1
- package/dist/tempo/legacy/session/Chain.js +53 -50
- package/dist/tempo/legacy/session/Chain.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +8 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +49 -45
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +7 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +24 -16
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +3 -0
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +1 -0
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +20 -0
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +23 -11
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts +6 -6
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +29 -19
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +2 -2
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +1 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/RequestState.d.ts +4 -4
- package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +6 -3
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/tempo/session/server/Settlement.d.ts +5 -3
- package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
- package/dist/tempo/session/server/Settlement.js +9 -2
- package/dist/tempo/session/server/Settlement.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +2 -0
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/session/server/index.js +2 -0
- package/dist/tempo/session/server/index.js.map +1 -1
- package/dist/validation/core.d.ts +1 -0
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +5 -0
- package/dist/validation/core.js.map +1 -1
- package/dist/x402/Header.js +1 -1
- package/dist/x402/Header.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/validate/challenge.ts +20 -8
- package/src/cli/validate/discovery.test.ts +128 -0
- package/src/cli/validate/discovery.ts +50 -15
- package/src/cli/validate/helpers.ts +10 -0
- package/src/cli/validate/index.ts +4 -0
- package/src/cli/validate/payment.ts +4 -1
- package/src/cli/validate.test.ts +23 -2
- package/src/client/Mppx.test-d.ts +3 -1
- package/src/tempo/PublicExports.test-d.ts +5 -0
- package/src/tempo/internal/fee-payer.test.ts +97 -1
- package/src/tempo/internal/fee-payer.ts +68 -3
- package/src/tempo/internal/fee-token.test.ts +37 -0
- package/src/tempo/internal/fee-token.ts +26 -5
- package/src/tempo/legacy/session/Chain.test.ts +78 -2
- package/src/tempo/legacy/session/Chain.ts +56 -52
- package/src/tempo/server/Charge.test.ts +121 -6
- package/src/tempo/server/Charge.ts +61 -48
- package/src/tempo/server/Methods.ts +6 -0
- package/src/tempo/server/Subscription.test.ts +79 -4
- package/src/tempo/server/Subscription.ts +24 -16
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/SessionManager.test.ts +12 -1
- package/src/tempo/session/client/SessionManager.ts +3 -0
- package/src/tempo/session/client/Transports.test.ts +34 -0
- package/src/tempo/session/client/Transports.ts +47 -20
- package/src/tempo/session/precompile/Chain.test.ts +51 -0
- package/src/tempo/session/precompile/Chain.ts +39 -26
- package/src/tempo/session/server/CredentialVerification.ts +3 -3
- package/src/tempo/session/server/RequestState.ts +4 -3
- package/src/tempo/session/server/Session.test.ts +23 -3
- package/src/tempo/session/server/Session.ts +6 -3
- package/src/tempo/session/server/Settlement.test.ts +16 -0
- package/src/tempo/session/server/Settlement.ts +16 -5
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +6 -0
- package/src/x402/Header.ts +1 -1
|
@@ -329,9 +329,13 @@ describe('Session', () => {
|
|
|
329
329
|
expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
|
|
330
330
|
})
|
|
331
331
|
|
|
332
|
-
test('seeds a same-route HEAD snapshot
|
|
332
|
+
test('seeds a same-route HEAD snapshot and resolves the account when resuming it', async () => {
|
|
333
333
|
const { store, set } = makeChannelStore()
|
|
334
334
|
const posted: SessionCredentialPayload[] = []
|
|
335
|
+
const resolveAccount = vi.fn(
|
|
336
|
+
(info: Parameters<NonNullable<sessionManager.Parameters['resolveAccount']>>[0]) =>
|
|
337
|
+
info.account,
|
|
338
|
+
)
|
|
335
339
|
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
336
340
|
const headers = new Headers(init?.headers)
|
|
337
341
|
if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization)) {
|
|
@@ -377,6 +381,7 @@ describe('Session', () => {
|
|
|
377
381
|
client,
|
|
378
382
|
fetch: mockFetch as typeof globalThis.fetch,
|
|
379
383
|
channelStore: store,
|
|
384
|
+
resolveAccount,
|
|
380
385
|
})
|
|
381
386
|
|
|
382
387
|
const response = await s.fetch('https://api.example.com/data')
|
|
@@ -384,6 +389,12 @@ describe('Session', () => {
|
|
|
384
389
|
expect(response.status).toBe(200)
|
|
385
390
|
expect(set).toHaveBeenCalledWith(expect.objectContaining({ channelId: storedChannelId }))
|
|
386
391
|
expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
|
|
392
|
+
expect(resolveAccount).toHaveBeenCalledOnce()
|
|
393
|
+
expect(resolveAccount).toHaveBeenCalledWith({
|
|
394
|
+
account,
|
|
395
|
+
chainId: 4217,
|
|
396
|
+
operation: { authority: account.address, kind: 'authorizePaymentChannel' },
|
|
397
|
+
})
|
|
387
398
|
const contentCall = mockFetch.mock.calls.find((call) => call[1]?.method !== 'HEAD')
|
|
388
399
|
expect(new Headers(contentCall?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
389
400
|
})
|
|
@@ -274,6 +274,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
274
274
|
const method = sessionPlugin({
|
|
275
275
|
account: parameters.account,
|
|
276
276
|
getClient: parameters.client ? () => parameters.client! : parameters.getClient,
|
|
277
|
+
resolveAccount: parameters.resolveAccount,
|
|
277
278
|
escrow: parameters.escrow,
|
|
278
279
|
decimals: config.decimals,
|
|
279
280
|
maxDeposit: parameters.maxDeposit,
|
|
@@ -787,6 +788,8 @@ export namespace sessionManager {
|
|
|
787
788
|
fetch?: typeof globalThis.fetch | undefined
|
|
788
789
|
/** Maximum deposit in human-readable units (e.g. `'10'` for 10 tokens). Converted to raw units via `decimals`. */
|
|
789
790
|
maxDeposit?: string | undefined
|
|
791
|
+
/** Selects the account that signs session credentials. */
|
|
792
|
+
resolveAccount?: sessionPlugin.ResolveAccount | undefined
|
|
790
793
|
/** Store for reusable session channels. Defaults to in-memory. */
|
|
791
794
|
channelStore?: ChannelStore | undefined
|
|
792
795
|
/** Optional websocket constructor for runtimes without a global WebSocket. */
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
applyTopUpResult,
|
|
20
20
|
closeHttpSession,
|
|
21
21
|
createActiveSocketSession,
|
|
22
|
+
driveSseResponse,
|
|
22
23
|
isExpectedSocketReceipt,
|
|
23
24
|
managementInput,
|
|
24
25
|
postTopUp,
|
|
@@ -640,6 +641,39 @@ describe('VoucherManagement', () => {
|
|
|
640
641
|
})
|
|
641
642
|
})
|
|
642
643
|
|
|
644
|
+
describe('SseDriver', () => {
|
|
645
|
+
test('preserves application frames and consumes payment receipts', async () => {
|
|
646
|
+
const receipt = createSessionReceipt({
|
|
647
|
+
acceptedCumulative: 2n,
|
|
648
|
+
challengeId: 'challenge-1',
|
|
649
|
+
channelId: `0x${'01'.repeat(32)}`,
|
|
650
|
+
spent: 2n,
|
|
651
|
+
})
|
|
652
|
+
const acceptReceipt = vi.fn()
|
|
653
|
+
const response = new Response(
|
|
654
|
+
[
|
|
655
|
+
': keepalive\n\n',
|
|
656
|
+
'event: custom\ndata: hello\n\n',
|
|
657
|
+
`event: payment-receipt\ndata: ${JSON.stringify(receipt)}\n\n`,
|
|
658
|
+
].join(''),
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
const frames = []
|
|
662
|
+
for await (const frame of driveSseResponse({
|
|
663
|
+
async onNeedVoucher() {},
|
|
664
|
+
onReceipt: acceptReceipt,
|
|
665
|
+
response,
|
|
666
|
+
}))
|
|
667
|
+
frames.push(frame)
|
|
668
|
+
|
|
669
|
+
expect(frames).toEqual([
|
|
670
|
+
{ raw: ': keepalive\n\n' },
|
|
671
|
+
{ data: 'hello', raw: 'event: custom\ndata: hello\n\n' },
|
|
672
|
+
])
|
|
673
|
+
expect(acceptReceipt).toHaveBeenCalledWith(receipt)
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
|
|
643
677
|
describe('WsDriver', () => {
|
|
644
678
|
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex.Hex
|
|
645
679
|
const challenge: TempoSessionChallenge = {
|
|
@@ -777,29 +777,49 @@ export async function openSseSession(
|
|
|
777
777
|
if (!isEventStream(response)) throw new Error('SSE response is not an event stream.')
|
|
778
778
|
if (!response.body) throw new Error('Response has no body.')
|
|
779
779
|
|
|
780
|
-
return
|
|
781
|
-
challenge,
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
780
|
+
return iterateSseMessages({
|
|
781
|
+
onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
|
|
782
|
+
onReceipt(receipt) {
|
|
783
|
+
driver.acceptReceipt(receipt)
|
|
784
|
+
onReceipt?.(receipt)
|
|
785
|
+
},
|
|
785
786
|
response,
|
|
786
787
|
signal,
|
|
787
788
|
})
|
|
788
789
|
}
|
|
789
790
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
791
|
+
/** Application SSE frame emitted after session payment events are handled. */
|
|
792
|
+
export type SseResponseFrame = {
|
|
793
|
+
/** Parsed application data, when present. */
|
|
794
|
+
data?: string | undefined
|
|
795
|
+
/** Original frame text, including its separator. */
|
|
796
|
+
raw: string
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Inputs for driving an open paid SSE response. */
|
|
800
|
+
export type DriveSseResponseParameters = {
|
|
801
|
+
/** Handles a request for the stream's next cumulative voucher. */
|
|
802
|
+
onNeedVoucher(event: NeedVoucherEvent): Promise<void>
|
|
803
|
+
/** Handles a payment receipt emitted by the stream. */
|
|
804
|
+
onReceipt(receipt: SessionReceipt): void
|
|
805
|
+
/** Open SSE response to drive. */
|
|
795
806
|
response: Response
|
|
807
|
+
/** Abort signal used to stop reading the response. */
|
|
796
808
|
signal?: AbortSignal | undefined
|
|
797
809
|
}
|
|
798
810
|
|
|
799
|
-
async function*
|
|
800
|
-
parameters
|
|
801
|
-
)
|
|
802
|
-
|
|
811
|
+
async function* iterateSseMessages(parameters: DriveSseResponseParameters): AsyncGenerator<string> {
|
|
812
|
+
for await (const frame of driveSseResponse(parameters)) {
|
|
813
|
+
if (frame.data !== undefined) yield frame.data
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Handles session payment events and yields application SSE frames unchanged. */
|
|
818
|
+
export async function* driveSseResponse(
|
|
819
|
+
parameters: DriveSseResponseParameters,
|
|
820
|
+
): AsyncGenerator<SseResponseFrame> {
|
|
821
|
+
if (!parameters.response.body) throw new Error('Response has no body.')
|
|
822
|
+
const reader = parameters.response.body.getReader()
|
|
803
823
|
const decoder = new TextDecoder()
|
|
804
824
|
let buffer = ''
|
|
805
825
|
|
|
@@ -817,18 +837,21 @@ async function* iterateSseResponse(
|
|
|
817
837
|
for (const part of parts) {
|
|
818
838
|
if (!part.trim()) continue
|
|
819
839
|
const event = parseEvent(part)
|
|
820
|
-
|
|
840
|
+
const raw = `${part}\n\n`
|
|
841
|
+
if (!event) {
|
|
842
|
+
yield { raw }
|
|
843
|
+
continue
|
|
844
|
+
}
|
|
821
845
|
|
|
822
846
|
switch (event.type) {
|
|
823
847
|
case 'message':
|
|
824
|
-
yield event.data
|
|
848
|
+
yield { data: event.data, raw }
|
|
825
849
|
break
|
|
826
850
|
case 'payment-need-voucher':
|
|
827
|
-
await
|
|
851
|
+
await parameters.onNeedVoucher(event.data)
|
|
828
852
|
break
|
|
829
853
|
case 'payment-receipt':
|
|
830
|
-
parameters.
|
|
831
|
-
parameters.onReceipt?.(event.data)
|
|
854
|
+
parameters.onReceipt(event.data)
|
|
832
855
|
break
|
|
833
856
|
}
|
|
834
857
|
}
|
|
@@ -839,7 +862,11 @@ async function* iterateSseResponse(
|
|
|
839
862
|
}
|
|
840
863
|
|
|
841
864
|
async function handleSseNeedVoucher(
|
|
842
|
-
parameters:
|
|
865
|
+
parameters: {
|
|
866
|
+
challenge: TempoSessionChallenge | null
|
|
867
|
+
driver: OpenSseSessionParameters
|
|
868
|
+
input: RequestInfo | URL
|
|
869
|
+
},
|
|
843
870
|
event: NeedVoucherEvent,
|
|
844
871
|
) {
|
|
845
872
|
const channel = parameters.driver.getChannel()
|
|
@@ -611,6 +611,54 @@ describe('precompile broadcastOpenTransaction', () => {
|
|
|
611
611
|
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
612
612
|
expect(simulationIndex).toBeGreaterThan(-1)
|
|
613
613
|
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
614
|
+
expect(
|
|
615
|
+
rpcMethods.slice(0, broadcastIndex).filter((method) => method === 'eth_call'),
|
|
616
|
+
).toHaveLength(2)
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
test('hosted fee-payer relays a sender-signed open without local co-signing', async () => {
|
|
620
|
+
const rpcMethods: string[] = []
|
|
621
|
+
const serializedTransaction = await createOpenTransaction({ signed: true })
|
|
622
|
+
const transaction = Transaction.deserialize(
|
|
623
|
+
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
624
|
+
)
|
|
625
|
+
const payer = transaction.from!
|
|
626
|
+
const expiringNonceHash = Channel.computeExpiringNonceHash(
|
|
627
|
+
Channel.transactionForExpiringNonceHash({ feePayer: true, transaction }),
|
|
628
|
+
{ sender: payer },
|
|
629
|
+
)
|
|
630
|
+
const expectedDescriptor = { ...descriptor, payer, expiringNonceHash }
|
|
631
|
+
const channelId = Channel.computeId({
|
|
632
|
+
...expectedDescriptor,
|
|
633
|
+
chainId,
|
|
634
|
+
escrow: tip20ChannelEscrow,
|
|
635
|
+
})
|
|
636
|
+
const state = { settled: 0n, deposit, closeRequestedAt: 0 }
|
|
637
|
+
|
|
638
|
+
await Chain.broadcastOpenTransaction({
|
|
639
|
+
chainId,
|
|
640
|
+
client: createMockClient({
|
|
641
|
+
channel: { descriptor: expectedDescriptor, state },
|
|
642
|
+
receipt: receipt([openedLog({ channelId, expiringNonceHash })]),
|
|
643
|
+
rpcMethods,
|
|
644
|
+
}),
|
|
645
|
+
escrowContract: tip20ChannelEscrow,
|
|
646
|
+
expectedAuthorizedSigner: descriptor.authorizedSigner,
|
|
647
|
+
expectedChannelId: channelId,
|
|
648
|
+
expectedCurrency: descriptor.token,
|
|
649
|
+
expectedExpiringNonceHash: expiringNonceHash,
|
|
650
|
+
expectedOperator: descriptor.operator,
|
|
651
|
+
expectedPayee: descriptor.payee,
|
|
652
|
+
expectedPayer: payer,
|
|
653
|
+
feePayer: true,
|
|
654
|
+
serializedTransaction,
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
expect(rpcMethods).toContain('eth_sendRawTransaction')
|
|
658
|
+
expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
|
|
659
|
+
expect(rpcMethods.indexOf('eth_call')).toBeLessThan(
|
|
660
|
+
rpcMethods.indexOf('eth_sendRawTransaction'),
|
|
661
|
+
)
|
|
614
662
|
})
|
|
615
663
|
|
|
616
664
|
test('rejects expiring nonce hash mismatches before broadcasting', async () => {
|
|
@@ -929,6 +977,9 @@ describe('precompile broadcastTopUpTransaction', () => {
|
|
|
929
977
|
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
930
978
|
expect(simulationIndex).toBeGreaterThan(-1)
|
|
931
979
|
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
980
|
+
expect(
|
|
981
|
+
rpcMethods.slice(0, broadcastIndex).filter((method) => method === 'eth_call'),
|
|
982
|
+
).toHaveLength(2)
|
|
932
983
|
})
|
|
933
984
|
|
|
934
985
|
test('rejects top-up calldata amount mismatches before broadcasting', async () => {
|
|
@@ -406,7 +406,7 @@ type ParsedPrecompileCredentialTransaction = {
|
|
|
406
406
|
|
|
407
407
|
function parsePrecompileCredentialTransaction(parameters: {
|
|
408
408
|
escrowContract: Address
|
|
409
|
-
feePayer?: Account | undefined
|
|
409
|
+
feePayer?: Account | true | undefined
|
|
410
410
|
label: 'open' | 'topUp'
|
|
411
411
|
serializedTransaction: Hex
|
|
412
412
|
}): ParsedPrecompileCredentialTransaction {
|
|
@@ -434,15 +434,10 @@ function parsePrecompileCredentialTransaction(parameters: {
|
|
|
434
434
|
return { transaction, call: { ...call, data: call.data, to: call.to } }
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
async function simulateTempoTransaction(client: Client,
|
|
437
|
+
async function simulateTempoTransaction(client: Client, request: unknown) {
|
|
438
438
|
// viem's public `call` type does not yet model Tempo's multi-call and
|
|
439
439
|
// fee-payer fields together. Keep that compatibility cast in one place.
|
|
440
|
-
await call(client,
|
|
441
|
-
...transaction,
|
|
442
|
-
account: transaction.from,
|
|
443
|
-
calls: transaction.calls ?? [],
|
|
444
|
-
feePayerSignature: undefined,
|
|
445
|
-
} as never)
|
|
440
|
+
await call(client, request as never)
|
|
446
441
|
}
|
|
447
442
|
|
|
448
443
|
async function signTempoTransaction(client: Client, transaction: unknown): Promise<Hex> {
|
|
@@ -668,8 +663,8 @@ export type SendCredentialTransactionParameters = {
|
|
|
668
663
|
allowedFeeTokens?: readonly Address[] | undefined
|
|
669
664
|
/** Human-readable transaction details used by fee-payer policy hooks. */
|
|
670
665
|
details: Record<string, string>
|
|
671
|
-
/** Fee-payer account
|
|
672
|
-
feePayer?: Account | undefined
|
|
666
|
+
/** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
|
|
667
|
+
feePayer?: Account | true | undefined
|
|
673
668
|
/** Optional fee-payer policy enforced before co-signing. */
|
|
674
669
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
675
670
|
/** Management transaction kind, used for validation errors. */
|
|
@@ -704,21 +699,39 @@ export async function sendCredentialTransaction(parameters: SendCredentialTransa
|
|
|
704
699
|
throw new BadRequestError({ reason: 'Only Tempo (0x76/0x78) transactions are supported' })
|
|
705
700
|
assertSenderSigned(transaction)
|
|
706
701
|
|
|
707
|
-
|
|
702
|
+
if (feePayer === true) {
|
|
703
|
+
// The transport owns hosted completion, so mppx can only preflight call
|
|
704
|
+
// execution as the sender. It intentionally omits fee fields here; the
|
|
705
|
+
// transport is responsible for validating its final sponsored envelope.
|
|
706
|
+
await simulateTempoTransaction(
|
|
707
|
+
client,
|
|
708
|
+
FeePayer.simulationTransaction(transaction, { feePayer: true }),
|
|
709
|
+
)
|
|
710
|
+
const txHash = await sendTransaction(client, serializedTransaction)
|
|
711
|
+
return waitForSuccessfulReceipt(client, txHash)
|
|
712
|
+
}
|
|
708
713
|
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
allowedFeeTokens,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
714
|
+
const sponsorshipTransaction = {
|
|
715
|
+
...transaction,
|
|
716
|
+
...(allowedFeeTokens?.[0] ? { feeToken: transaction.feeToken ?? allowedFeeTokens[0] } : {}),
|
|
717
|
+
}
|
|
718
|
+
const completed = await FeePayer.preflightSponsorship({
|
|
719
|
+
transaction: sponsorshipTransaction,
|
|
720
|
+
simulate: (request) => simulateTempoTransaction(client, request),
|
|
721
|
+
async complete() {
|
|
722
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
723
|
+
account: feePayer,
|
|
724
|
+
allowedFeeTokens,
|
|
725
|
+
challengeExpires,
|
|
726
|
+
chainId,
|
|
727
|
+
details,
|
|
728
|
+
policy: feePayerPolicy,
|
|
729
|
+
transaction: sponsorshipTransaction,
|
|
730
|
+
})
|
|
731
|
+
return { feePayer: feePayer.address, transaction: sponsored }
|
|
719
732
|
},
|
|
720
733
|
})
|
|
721
|
-
const serialized = await signTempoTransaction(client,
|
|
734
|
+
const serialized = await signTempoTransaction(client, completed.transaction)
|
|
722
735
|
const receipt = await sendRawTransactionSync(client, {
|
|
723
736
|
serializedTransaction: serialized as Transaction.TransactionSerializedTempo,
|
|
724
737
|
})
|
|
@@ -771,8 +784,8 @@ export type BroadcastOpenTransactionParameters = {
|
|
|
771
784
|
expectedPayee: Address
|
|
772
785
|
/** Payer expected to have signed the open transaction. */
|
|
773
786
|
expectedPayer: Address
|
|
774
|
-
/** Fee-payer account
|
|
775
|
-
feePayer?: Account | undefined
|
|
787
|
+
/** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
|
|
788
|
+
feePayer?: Account | true | undefined
|
|
776
789
|
/** Optional fee-payer policy enforced before co-signing. */
|
|
777
790
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
778
791
|
/** Client-signed serialized open transaction. */
|
|
@@ -895,8 +908,8 @@ export type BroadcastTopUpTransactionParameters = {
|
|
|
895
908
|
expectedChannelId: Hex
|
|
896
909
|
/** Payment token expected for sponsored transaction fee token checks. */
|
|
897
910
|
expectedCurrency: Address
|
|
898
|
-
/** Fee-payer account
|
|
899
|
-
feePayer?: Account | undefined
|
|
911
|
+
/** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
|
|
912
|
+
feePayer?: Account | true | undefined
|
|
900
913
|
/** Optional fee-payer policy enforced before co-signing. */
|
|
901
914
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
902
915
|
/** Client-signed serialized top-up transaction. */
|
|
@@ -334,8 +334,8 @@ export type VerifyCredentialPayloadParameters = {
|
|
|
334
334
|
escrow: Address
|
|
335
335
|
/** Operator address advertised in the HMAC-bound challenge details. */
|
|
336
336
|
expectedOperator?: Address | undefined
|
|
337
|
-
/**
|
|
338
|
-
feePayer?: viem_Account | undefined
|
|
337
|
+
/** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
|
|
338
|
+
feePayer?: viem_Account | true | undefined
|
|
339
339
|
/** Optional policy for fee-sponsored close/open/top-up transactions. */
|
|
340
340
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
341
341
|
/** Optional fee token override for close transactions. */
|
|
@@ -714,7 +714,7 @@ async function handleCloseCredential(
|
|
|
714
714
|
account
|
|
715
715
|
? {
|
|
716
716
|
account,
|
|
717
|
-
...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
|
|
717
|
+
...(typeof parameters.feePayer === 'object' ? { feePayer: parameters.feePayer } : {}),
|
|
718
718
|
...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
|
|
719
719
|
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
720
720
|
candidateFeeTokens: [channel.token],
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
resolveCredentialFeePayer,
|
|
28
28
|
resolveRequestFeePayer,
|
|
29
29
|
type ParameterFeePayer,
|
|
30
|
+
type ResolvedFeePayer,
|
|
30
31
|
} from './Settlement.js'
|
|
31
32
|
|
|
32
33
|
/** Inputs used to build server bootstrap hints for a reusable session channel. */
|
|
@@ -267,8 +268,8 @@ export type ResolveSessionPaymentRequestParameters = {
|
|
|
267
268
|
|
|
268
269
|
/** Inputs used to resolve shared context for credential verification. */
|
|
269
270
|
export type ResolveCredentialVerificationContextParameters = {
|
|
270
|
-
/**
|
|
271
|
-
feePayer?:
|
|
271
|
+
/** Configured local fee payer, hosted relay URL, or sponsorship flag. */
|
|
272
|
+
feePayer?: ParameterFeePayer
|
|
272
273
|
/** Resolves a viem client for the challenge chain ID. */
|
|
273
274
|
getClient(parameters: {
|
|
274
275
|
chainId: number
|
|
@@ -294,7 +295,7 @@ export type CredentialVerificationContext = {
|
|
|
294
295
|
/** Client for precompile reads and transaction broadcasts. */
|
|
295
296
|
client: PrecompileChain.TransactionClient
|
|
296
297
|
/** Fee payer authorized for this credential, when allowed. */
|
|
297
|
-
feePayer?:
|
|
298
|
+
feePayer?: ResolvedFeePayer
|
|
298
299
|
/** Minimum allowed voucher delta in raw token units. */
|
|
299
300
|
minVoucherDelta: bigint
|
|
300
301
|
}
|
|
@@ -2257,6 +2257,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2257
2257
|
action: SessionCredentialPayload['action']
|
|
2258
2258
|
contentLength: string | null
|
|
2259
2259
|
hasBody: boolean
|
|
2260
|
+
hasReceipt: boolean
|
|
2260
2261
|
status: number
|
|
2261
2262
|
}> = []
|
|
2262
2263
|
const app = new Hono()
|
|
@@ -2271,6 +2272,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2271
2272
|
action: payload.action,
|
|
2272
2273
|
contentLength: request.headers.get('content-length'),
|
|
2273
2274
|
hasBody: request.body !== null,
|
|
2275
|
+
hasReceipt: response.headers.has(Constants.Headers.paymentReceipt),
|
|
2274
2276
|
status: response.status,
|
|
2275
2277
|
})
|
|
2276
2278
|
return response
|
|
@@ -2302,9 +2304,27 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2302
2304
|
expect(closeReceipt?.spent).toBe('3')
|
|
2303
2305
|
expect(managementPosts).toEqual(
|
|
2304
2306
|
expect.arrayContaining([
|
|
2305
|
-
{
|
|
2306
|
-
|
|
2307
|
-
|
|
2307
|
+
{
|
|
2308
|
+
action: 'topUp',
|
|
2309
|
+
contentLength: '0',
|
|
2310
|
+
hasBody: true,
|
|
2311
|
+
hasReceipt: true,
|
|
2312
|
+
status: 204,
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
action: 'voucher',
|
|
2316
|
+
contentLength: '0',
|
|
2317
|
+
hasBody: true,
|
|
2318
|
+
hasReceipt: true,
|
|
2319
|
+
status: 204,
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
action: 'close',
|
|
2323
|
+
contentLength: '0',
|
|
2324
|
+
hasBody: true,
|
|
2325
|
+
hasReceipt: true,
|
|
2326
|
+
status: 204,
|
|
2327
|
+
},
|
|
2308
2328
|
]),
|
|
2309
2329
|
)
|
|
2310
2330
|
expect(managementPosts.every(({ status }) => status === 204)).toBe(true)
|
|
@@ -315,9 +315,11 @@ export function session<const parameters extends session.Parameters>(
|
|
|
315
315
|
|
|
316
316
|
const store = ChannelStore.fromStore(rawStore)
|
|
317
317
|
const lastOnChainVerified = new Map<Hex, number>()
|
|
318
|
-
const { account, feePayer, recipient } = Account.resolve(parameters)
|
|
318
|
+
const { account, feePayer, feePayerUrl, recipient } = Account.resolve(parameters)
|
|
319
|
+
const configuredFeePayer = feePayer ?? feePayerUrl
|
|
319
320
|
const getClient = Client.getResolver({
|
|
320
321
|
chain: tempo_chain,
|
|
322
|
+
feePayerUrl,
|
|
321
323
|
getClient: parameters.getClient,
|
|
322
324
|
rpcUrl: defaults.rpcUrl,
|
|
323
325
|
})
|
|
@@ -326,6 +328,7 @@ export function session<const parameters extends session.Parameters>(
|
|
|
326
328
|
chainId: parameters.chainId,
|
|
327
329
|
currency,
|
|
328
330
|
decimals,
|
|
331
|
+
feePayer: parameters.feePayer,
|
|
329
332
|
getClient: parameters.getClient,
|
|
330
333
|
recipient,
|
|
331
334
|
store: rawStore,
|
|
@@ -401,7 +404,7 @@ export function session<const parameters extends session.Parameters>(
|
|
|
401
404
|
const payload = requireSessionCredentialPayload(credential.payload)
|
|
402
405
|
const context = await resolveCredentialVerificationContext({
|
|
403
406
|
decimals,
|
|
404
|
-
feePayer,
|
|
407
|
+
feePayer: configuredFeePayer,
|
|
405
408
|
getClient,
|
|
406
409
|
minVoucherDelta: parameters.minVoucherDelta,
|
|
407
410
|
request,
|
|
@@ -438,7 +441,7 @@ export function session<const parameters extends session.Parameters>(
|
|
|
438
441
|
maybeSettleScheduled({
|
|
439
442
|
account,
|
|
440
443
|
client: context.client,
|
|
441
|
-
feePayer: context.feePayer,
|
|
444
|
+
...(typeof context.feePayer === 'object' ? { feePayer: context.feePayer } : {}),
|
|
442
445
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
443
446
|
feeToken: parameters.feeToken,
|
|
444
447
|
schedule: settlementSchedule,
|
|
@@ -23,6 +23,7 @@ describe('FeePayerResolution', () => {
|
|
|
23
23
|
const requestFeePayer = privateKeyToAccount(
|
|
24
24
|
'0x5de4111a56d1f1ad3c74c8a3be6fba32114d0f9f8e9e4b0d4d4f5a7833f1b6c9',
|
|
25
25
|
)
|
|
26
|
+
const feePayerUrl = 'https://fee-payer.example/relay'
|
|
26
27
|
|
|
27
28
|
function credential(): Credential.Credential {
|
|
28
29
|
return {
|
|
@@ -60,6 +61,13 @@ describe('FeePayerResolution', () => {
|
|
|
60
61
|
parameterFeePayer: true,
|
|
61
62
|
}),
|
|
62
63
|
).toBe(true)
|
|
64
|
+
|
|
65
|
+
expect(
|
|
66
|
+
resolveRequestFeePayer({
|
|
67
|
+
credential: null,
|
|
68
|
+
parameterFeePayer: feePayerUrl,
|
|
69
|
+
}),
|
|
70
|
+
).toBe(true)
|
|
63
71
|
})
|
|
64
72
|
|
|
65
73
|
test('does not advertise fee-payer support when request disables it', () => {
|
|
@@ -106,6 +114,14 @@ describe('FeePayerResolution', () => {
|
|
|
106
114
|
}),
|
|
107
115
|
).toBe(defaultFeePayer)
|
|
108
116
|
|
|
117
|
+
expect(
|
|
118
|
+
resolveCredentialFeePayer({
|
|
119
|
+
feePayer: feePayerUrl,
|
|
120
|
+
methodDetails: { feePayer: true },
|
|
121
|
+
request: { feePayer: true },
|
|
122
|
+
}),
|
|
123
|
+
).toBe(true)
|
|
124
|
+
|
|
109
125
|
expect(
|
|
110
126
|
resolveCredentialFeePayer({
|
|
111
127
|
feePayer: defaultFeePayer,
|
|
@@ -32,6 +32,9 @@ import * as ChannelStore from './ChannelStore.js'
|
|
|
32
32
|
/** Fee-payer parameter accepted by the server session method. */
|
|
33
33
|
export type ParameterFeePayer = viem_Account | string | true | undefined
|
|
34
34
|
|
|
35
|
+
/** Resolved fee-payer mode for credential-time transaction submission. */
|
|
36
|
+
export type ResolvedFeePayer = viem_Account | true | undefined
|
|
37
|
+
|
|
35
38
|
/** Minimum method details needed to decide credential-time fee sponsorship. */
|
|
36
39
|
export type CredentialFeePayerMethodDetails = {
|
|
37
40
|
/** Whether the challenge advertised fee-payer support. */
|
|
@@ -56,8 +59,8 @@ export type ResolveCredentialFeePayerParameters = {
|
|
|
56
59
|
request: unknown
|
|
57
60
|
/** Challenge method details echoed by the credential. */
|
|
58
61
|
methodDetails: CredentialFeePayerMethodDetails
|
|
59
|
-
/**
|
|
60
|
-
feePayer?:
|
|
62
|
+
/** Configured local fee payer, hosted relay URL, or sponsorship flag. */
|
|
63
|
+
feePayer?: ParameterFeePayer
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
/** Fee-payer value read from an untrusted credential challenge request. */
|
|
@@ -89,20 +92,28 @@ export function resolveRequestFeePayer(
|
|
|
89
92
|
|
|
90
93
|
const account = typeof requestFeePayer === 'object' ? requestFeePayer : defaultFeePayer
|
|
91
94
|
if (credential) return account ?? undefined
|
|
92
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
account ||
|
|
97
|
+
defaultFeePayer ||
|
|
98
|
+
parameterFeePayer === true ||
|
|
99
|
+
typeof parameterFeePayer === 'string'
|
|
100
|
+
)
|
|
101
|
+
return true
|
|
93
102
|
return undefined
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
/** Resolves the fee-payer account allowed for an incoming credential. */
|
|
97
106
|
export function resolveCredentialFeePayer(
|
|
98
107
|
parameters: ResolveCredentialFeePayerParameters,
|
|
99
|
-
):
|
|
108
|
+
): ResolvedFeePayer {
|
|
100
109
|
const { feePayer, methodDetails, request } = parameters
|
|
101
110
|
const requestFeePayer = readRequestFeePayer(request)
|
|
102
111
|
const requestAllowsFeePayer =
|
|
103
112
|
requestFeePayer === undefined || requestFeePayer === true || typeof requestFeePayer === 'object'
|
|
104
113
|
if (methodDetails.feePayer !== true || !requestAllowsFeePayer) return undefined
|
|
105
|
-
|
|
114
|
+
if (typeof requestFeePayer === 'object') return requestFeePayer
|
|
115
|
+
if (typeof feePayer === 'object') return feePayer
|
|
116
|
+
return typeof feePayer === 'string' ? true : undefined
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
/** Declarative server-side settlement cadence for automatic session settlement. */
|
package/src/validation/core.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type ValidateOptions = {
|
|
|
25
25
|
endpoint?: string | undefined
|
|
26
26
|
body?: string | undefined
|
|
27
27
|
query?: string[] | undefined
|
|
28
|
+
headers?: string[] | undefined
|
|
28
29
|
verbose?: boolean | undefined
|
|
29
30
|
yes?: boolean | undefined
|
|
30
31
|
skipPayment?: boolean | undefined
|
|
@@ -92,12 +93,14 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
|
|
|
92
93
|
yield { phase: 'endpoint', endpoint }
|
|
93
94
|
|
|
94
95
|
let body: string | undefined
|
|
96
|
+
let bodyIsAutogenerated = false
|
|
95
97
|
if (options.endpoint) {
|
|
96
98
|
body = options.body
|
|
97
99
|
} else {
|
|
98
100
|
body = resolveBodyForEndpoint(options.body, endpoint.path)
|
|
99
101
|
if (!body && discovery.doc) {
|
|
100
102
|
body = extractRequestBodyFromDiscovery(discovery.doc, endpoint)
|
|
103
|
+
bodyIsAutogenerated = !!body
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
|
|
@@ -107,7 +110,9 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
|
|
|
107
110
|
challenges: parsedChallenges,
|
|
108
111
|
} = await validateChallenge(baseUrl, endpoint, verbose, {
|
|
109
112
|
body,
|
|
113
|
+
bodyIsAutogenerated,
|
|
110
114
|
query: options.query,
|
|
115
|
+
extraHeaders: options.headers,
|
|
111
116
|
discoveryDoc: discovery.doc ?? undefined,
|
|
112
117
|
})
|
|
113
118
|
const effectiveBody = resolvedBody ?? body
|
|
@@ -156,6 +161,7 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
|
|
|
156
161
|
const paymentResults = await validatePaymentFlow(baseUrl, endpoint, verbose, {
|
|
157
162
|
body: effectiveBody,
|
|
158
163
|
query: options.query,
|
|
164
|
+
extraHeaders: options.headers,
|
|
159
165
|
yes: options.yes,
|
|
160
166
|
silent: !onResults,
|
|
161
167
|
interactive: options.interactive,
|
package/src/x402/Header.ts
CHANGED
|
@@ -68,7 +68,7 @@ const parsePaymentRequiredEnvelope = (value: unknown): PaymentRequiredEnvelope =
|
|
|
68
68
|
|
|
69
69
|
const resource = ResourceInfoSchema.parse(record.resource)
|
|
70
70
|
const extensions =
|
|
71
|
-
record.extensions
|
|
71
|
+
record.extensions !== undefined ? ExtensionsSchema.safeParse(record.extensions).data : undefined
|
|
72
72
|
|
|
73
73
|
return {
|
|
74
74
|
accepts: record.accepts,
|