mppx 0.8.7 → 0.8.9
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 +13 -0
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +3 -2
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +10 -9
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +5 -3
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +3 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +281 -146
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +4 -1
- package/dist/tempo/internal/fee-payer.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 +4 -0
- 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/validation/core.d.ts +4 -8
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +37 -24
- package/dist/validation/core.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/plugins/stripe.ts +3 -2
- package/src/cli/validate/challenge.ts +13 -8
- package/src/cli/validate/index.ts +5 -3
- package/src/cli/validate/payment.ts +354 -168
- package/src/cli/validate.test.ts +94 -0
- package/src/tempo/internal/fee-payer.test.ts +4 -1
- package/src/tempo/internal/fee-payer.ts +4 -1
- package/src/tempo/session/precompile/Chain.test.ts +42 -0
- package/src/tempo/session/precompile/Chain.ts +12 -7
- package/src/tempo/session/server/CredentialVerification.ts +3 -3
- package/src/tempo/session/server/RequestState.ts +4 -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/validation/core.ts +67 -41
package/src/cli/validate.test.ts
CHANGED
|
@@ -677,3 +677,97 @@ describe('validate: multi-challenge', () => {
|
|
|
677
677
|
expect(output).toContain('Amount is valid integer string (Got: 1.50)')
|
|
678
678
|
})
|
|
679
679
|
})
|
|
680
|
+
|
|
681
|
+
describe('validate: payment methods coverage', () => {
|
|
682
|
+
test(
|
|
683
|
+
'every method in Constants.Methods produces a payment result',
|
|
684
|
+
{ timeout: 15_000 },
|
|
685
|
+
async () => {
|
|
686
|
+
const methods = Object.values(Constants.Methods)
|
|
687
|
+
const challenges = methods.map((method) => ({
|
|
688
|
+
id: `${method}-id`,
|
|
689
|
+
realm: 'localhost',
|
|
690
|
+
method,
|
|
691
|
+
intent: 'charge',
|
|
692
|
+
request: { amount: '100', currency: 'usd', methodDetails: {} },
|
|
693
|
+
expires: new Date(Date.now() + 300_000).toISOString(),
|
|
694
|
+
})) as Challenge.Challenge[]
|
|
695
|
+
|
|
696
|
+
const header = challenges.map((c) => Challenge.serialize(c)).join(', ')
|
|
697
|
+
const server = await testServer((req, res) => {
|
|
698
|
+
const url = new URL(req.url!, 'http://localhost')
|
|
699
|
+
if (url.pathname === '/openapi.json') {
|
|
700
|
+
res.setHeader('Content-Type', 'application/json')
|
|
701
|
+
res.end(
|
|
702
|
+
JSON.stringify({
|
|
703
|
+
openapi: '3.1.0',
|
|
704
|
+
info: { title: 'T', version: '1' },
|
|
705
|
+
paths: {
|
|
706
|
+
'/api/test': {
|
|
707
|
+
post: { 'x-payment-info': { amount: '100' }, responses: { '402': {} } },
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
}),
|
|
711
|
+
)
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
res.writeHead(402, { [Constants.Headers.wwwAuthenticate]: header })
|
|
715
|
+
res.end()
|
|
716
|
+
})
|
|
717
|
+
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
718
|
+
const jsonStart = output.indexOf('{')
|
|
719
|
+
const jsonEnd = output.lastIndexOf('}')
|
|
720
|
+
const result = JSON.parse(output.slice(jsonStart, jsonEnd + 1))
|
|
721
|
+
const paymentLabels = result.endpoints[0].payment.map((r: { label: string }) => r.label)
|
|
722
|
+
for (const method of methods) {
|
|
723
|
+
expect(paymentLabels.some((l: string) => l.includes(`[${method}]`))).toBe(true)
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
describe('validate: JSON mode', () => {
|
|
730
|
+
function mainnetChallenge() {
|
|
731
|
+
return makeChallenge({
|
|
732
|
+
request: {
|
|
733
|
+
amount: '10000',
|
|
734
|
+
currency: '0x20c0000000000000000000000000000000000000',
|
|
735
|
+
recipient: '0x1234567890123456789012345678901234567890',
|
|
736
|
+
methodDetails: { chainId: 4217 },
|
|
737
|
+
},
|
|
738
|
+
} as Partial<Challenge.Challenge>)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function parseJson(output: string) {
|
|
742
|
+
const jsonStart = output.indexOf('{')
|
|
743
|
+
const jsonEnd = output.lastIndexOf('}')
|
|
744
|
+
return JSON.parse(output.slice(jsonStart, jsonEnd + 1))
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
test('does not prompt (non-interactive)', { timeout: 15_000 }, async () => {
|
|
748
|
+
const server = await mppServer(mainnetChallenge())
|
|
749
|
+
const { output } = await serve(['validate', server.url, '--outputJson'])
|
|
750
|
+
const result = parseJson(output)
|
|
751
|
+
const allPayment = result.endpoints.flatMap((ep: { payment: unknown[] }) => ep.payment)
|
|
752
|
+
expect(allPayment.length).toBeGreaterThan(0)
|
|
753
|
+
expect(allPayment.every((r: { severity: string }) => r.severity === 'skip')).toBe(true)
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
test('no console leaks before JSON', { timeout: 15_000 }, async () => {
|
|
757
|
+
const server = await mppServer(mainnetChallenge())
|
|
758
|
+
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
759
|
+
const jsonStart = output.indexOf('{')
|
|
760
|
+
expect(jsonStart).toBeGreaterThanOrEqual(0)
|
|
761
|
+
const beforeJson = output.slice(0, jsonStart)
|
|
762
|
+
expect(beforeJson).not.toContain('Using wallet')
|
|
763
|
+
expect(beforeJson).not.toContain('Auto-approved')
|
|
764
|
+
expect(beforeJson).not.toContain('Attempting')
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
test('includes suggestions', { timeout: 15_000 }, async () => {
|
|
768
|
+
const server = await mppServer(mainnetChallenge())
|
|
769
|
+
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
770
|
+
const result = parseJson(output)
|
|
771
|
+
expect(Array.isArray(result.suggestions)).toBe(true)
|
|
772
|
+
})
|
|
773
|
+
})
|
|
@@ -565,7 +565,10 @@ describe('fillHostedFeePayerTransaction', () => {
|
|
|
565
565
|
},
|
|
566
566
|
},
|
|
567
567
|
},
|
|
568
|
-
(
|
|
568
|
+
(key, value) => {
|
|
569
|
+
if (key === 'yParity') return toHex(value)
|
|
570
|
+
return typeof value === 'bigint' ? toHex(value) : value
|
|
571
|
+
},
|
|
569
572
|
),
|
|
570
573
|
)
|
|
571
574
|
})
|
|
@@ -224,7 +224,10 @@ export async function fillHostedFeePayerTransaction(parameters: {
|
|
|
224
224
|
|
|
225
225
|
assertAllowedFeeToken({ feeToken: filled.feeToken }, allowedFeeTokens)
|
|
226
226
|
|
|
227
|
-
const feePayerSignature =
|
|
227
|
+
const feePayerSignature = {
|
|
228
|
+
...(filled.feePayerSignature as Record<string, unknown>),
|
|
229
|
+
yParity: Number((filled.feePayerSignature as { yParity?: unknown }).yParity),
|
|
230
|
+
}
|
|
228
231
|
const feeToken = filled.feeToken
|
|
229
232
|
|
|
230
233
|
// Recover the concrete sponsor address so the simulation can use a concrete
|
|
@@ -613,6 +613,48 @@ describe('precompile broadcastOpenTransaction', () => {
|
|
|
613
613
|
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
614
614
|
})
|
|
615
615
|
|
|
616
|
+
test('hosted fee-payer relays a sender-signed open without local co-signing', async () => {
|
|
617
|
+
const rpcMethods: string[] = []
|
|
618
|
+
const serializedTransaction = await createOpenTransaction({ signed: true })
|
|
619
|
+
const transaction = Transaction.deserialize(
|
|
620
|
+
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
621
|
+
)
|
|
622
|
+
const payer = transaction.from!
|
|
623
|
+
const expiringNonceHash = Channel.computeExpiringNonceHash(
|
|
624
|
+
Channel.transactionForExpiringNonceHash({ feePayer: true, transaction }),
|
|
625
|
+
{ sender: payer },
|
|
626
|
+
)
|
|
627
|
+
const expectedDescriptor = { ...descriptor, payer, expiringNonceHash }
|
|
628
|
+
const channelId = Channel.computeId({
|
|
629
|
+
...expectedDescriptor,
|
|
630
|
+
chainId,
|
|
631
|
+
escrow: tip20ChannelEscrow,
|
|
632
|
+
})
|
|
633
|
+
const state = { settled: 0n, deposit, closeRequestedAt: 0 }
|
|
634
|
+
|
|
635
|
+
await Chain.broadcastOpenTransaction({
|
|
636
|
+
chainId,
|
|
637
|
+
client: createMockClient({
|
|
638
|
+
channel: { descriptor: expectedDescriptor, state },
|
|
639
|
+
receipt: receipt([openedLog({ channelId, expiringNonceHash })]),
|
|
640
|
+
rpcMethods,
|
|
641
|
+
}),
|
|
642
|
+
escrowContract: tip20ChannelEscrow,
|
|
643
|
+
expectedAuthorizedSigner: descriptor.authorizedSigner,
|
|
644
|
+
expectedChannelId: channelId,
|
|
645
|
+
expectedCurrency: descriptor.token,
|
|
646
|
+
expectedExpiringNonceHash: expiringNonceHash,
|
|
647
|
+
expectedOperator: descriptor.operator,
|
|
648
|
+
expectedPayee: descriptor.payee,
|
|
649
|
+
expectedPayer: payer,
|
|
650
|
+
feePayer: true,
|
|
651
|
+
serializedTransaction,
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
expect(rpcMethods).toContain('eth_sendRawTransaction')
|
|
655
|
+
expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
|
|
656
|
+
})
|
|
657
|
+
|
|
616
658
|
test('rejects expiring nonce hash mismatches before broadcasting', async () => {
|
|
617
659
|
const serializedTransaction = await createOpenTransaction()
|
|
618
660
|
|
|
@@ -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 {
|
|
@@ -668,8 +668,8 @@ export type SendCredentialTransactionParameters = {
|
|
|
668
668
|
allowedFeeTokens?: readonly Address[] | undefined
|
|
669
669
|
/** Human-readable transaction details used by fee-payer policy hooks. */
|
|
670
670
|
details: Record<string, string>
|
|
671
|
-
/** Fee-payer account
|
|
672
|
-
feePayer?: Account | undefined
|
|
671
|
+
/** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
|
|
672
|
+
feePayer?: Account | true | undefined
|
|
673
673
|
/** Optional fee-payer policy enforced before co-signing. */
|
|
674
674
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
675
675
|
/** Management transaction kind, used for validation errors. */
|
|
@@ -704,6 +704,11 @@ export async function sendCredentialTransaction(parameters: SendCredentialTransa
|
|
|
704
704
|
throw new BadRequestError({ reason: 'Only Tempo (0x76/0x78) transactions are supported' })
|
|
705
705
|
assertSenderSigned(transaction)
|
|
706
706
|
|
|
707
|
+
if (feePayer === true) {
|
|
708
|
+
const txHash = await sendTransaction(client, serializedTransaction)
|
|
709
|
+
return waitForSuccessfulReceipt(client, txHash)
|
|
710
|
+
}
|
|
711
|
+
|
|
707
712
|
await simulateTempoTransaction(client, transaction)
|
|
708
713
|
|
|
709
714
|
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
@@ -771,8 +776,8 @@ export type BroadcastOpenTransactionParameters = {
|
|
|
771
776
|
expectedPayee: Address
|
|
772
777
|
/** Payer expected to have signed the open transaction. */
|
|
773
778
|
expectedPayer: Address
|
|
774
|
-
/** Fee-payer account
|
|
775
|
-
feePayer?: Account | undefined
|
|
779
|
+
/** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
|
|
780
|
+
feePayer?: Account | true | undefined
|
|
776
781
|
/** Optional fee-payer policy enforced before co-signing. */
|
|
777
782
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
778
783
|
/** Client-signed serialized open transaction. */
|
|
@@ -895,8 +900,8 @@ export type BroadcastTopUpTransactionParameters = {
|
|
|
895
900
|
expectedChannelId: Hex
|
|
896
901
|
/** Payment token expected for sponsored transaction fee token checks. */
|
|
897
902
|
expectedCurrency: Address
|
|
898
|
-
/** Fee-payer account
|
|
899
|
-
feePayer?: Account | undefined
|
|
903
|
+
/** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
|
|
904
|
+
feePayer?: Account | true | undefined
|
|
900
905
|
/** Optional fee-payer policy enforced before co-signing. */
|
|
901
906
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
902
907
|
/** 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
|
}
|
|
@@ -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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { Chain } from 'viem'
|
|
2
|
+
import * as viemChains from 'viem/chains'
|
|
3
|
+
|
|
1
4
|
import { validateChallenge, validateErrorHandling } from '../cli/validate/challenge.js'
|
|
2
5
|
import {
|
|
3
6
|
buildUrl,
|
|
@@ -25,6 +28,7 @@ export type ValidateOptions = {
|
|
|
25
28
|
verbose?: boolean | undefined
|
|
26
29
|
yes?: boolean | undefined
|
|
27
30
|
skipPayment?: boolean | undefined
|
|
31
|
+
interactive?: boolean | undefined
|
|
28
32
|
discoveryPath?: string | undefined
|
|
29
33
|
onPaymentResults?: (results: CheckResult[]) => void
|
|
30
34
|
}
|
|
@@ -50,14 +54,7 @@ export type ValidateResult = {
|
|
|
50
54
|
discovery: DiscoveryResult
|
|
51
55
|
endpoints: EndpointValidationResult[]
|
|
52
56
|
summary: { passed: number; failed: number; warnings: number; skipped: number }
|
|
53
|
-
|
|
54
|
-
sawMppEndpoint: boolean
|
|
55
|
-
sawNonMppPaymentEndpoint: boolean
|
|
56
|
-
sawMalformedChallenge: boolean
|
|
57
|
-
sawTestnet: boolean
|
|
58
|
-
sawMainnet: boolean
|
|
59
|
-
paymentSucceeded: boolean
|
|
60
|
-
}
|
|
57
|
+
suggestions: string[]
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
export type ValidateEvent =
|
|
@@ -69,8 +66,10 @@ export type ValidateEvent =
|
|
|
69
66
|
results: CheckResult[]
|
|
70
67
|
isMpp: boolean
|
|
71
68
|
isTestnet: boolean
|
|
69
|
+
isMainnet: boolean
|
|
72
70
|
isNonMppPayment: boolean
|
|
73
71
|
isMalformedChallenge: boolean
|
|
72
|
+
methods: string[]
|
|
74
73
|
}
|
|
75
74
|
| { phase: 'errorHandling'; endpoint: EndpointSpec; results: CheckResult[] }
|
|
76
75
|
| {
|
|
@@ -102,26 +101,25 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
|
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
const {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
104
|
+
const {
|
|
105
|
+
results: challengeResults,
|
|
106
|
+
resolvedBody,
|
|
107
|
+
challenges: parsedChallenges,
|
|
108
|
+
} = await validateChallenge(baseUrl, endpoint, verbose, {
|
|
109
|
+
body,
|
|
110
|
+
query: options.query,
|
|
111
|
+
discoveryDoc: discovery.doc ?? undefined,
|
|
112
|
+
})
|
|
115
113
|
const effectiveBody = resolvedBody ?? body
|
|
116
114
|
const isMpp = challengeResults.some(
|
|
117
115
|
(r) => r.severity === 'pass' && r.label === 'Challenge parseable',
|
|
118
116
|
)
|
|
119
|
-
const
|
|
120
|
-
(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
117
|
+
const chainIds = (parsedChallenges ?? [])
|
|
118
|
+
.filter((ch) => ch.method !== 'stripe')
|
|
119
|
+
.map((ch) => ((ch.request as Record<string, unknown>).methodDetails as any)?.chainId)
|
|
120
|
+
.filter((id): id is number => typeof id === 'number')
|
|
121
|
+
const isTestnet = chainIds.some(chainIsTestnet)
|
|
122
|
+
const isMainnet = chainIds.some((id) => !chainIsTestnet(id))
|
|
125
123
|
const isNonMppPayment =
|
|
126
124
|
!isMpp && challengeResults.some((r) => r.label === 'Not an MPP endpoint')
|
|
127
125
|
const hasPaymentScheme = challengeResults.some(
|
|
@@ -132,14 +130,18 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
|
|
|
132
130
|
)
|
|
133
131
|
const isMalformedChallenge = !isMpp && hasPaymentScheme && challengeFailed
|
|
134
132
|
|
|
133
|
+
const methods = parsedChallenges?.map((c) => c.method) ?? []
|
|
134
|
+
|
|
135
135
|
yield {
|
|
136
136
|
phase: 'challenge',
|
|
137
137
|
endpoint,
|
|
138
138
|
results: challengeResults,
|
|
139
139
|
isMpp,
|
|
140
140
|
isTestnet,
|
|
141
|
+
isMainnet,
|
|
141
142
|
isNonMppPayment,
|
|
142
143
|
isMalformedChallenge,
|
|
144
|
+
methods,
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
if (isMpp) {
|
|
@@ -155,6 +157,8 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
|
|
|
155
157
|
body: effectiveBody,
|
|
156
158
|
query: options.query,
|
|
157
159
|
yes: options.yes,
|
|
160
|
+
silent: !onResults,
|
|
161
|
+
interactive: options.interactive,
|
|
158
162
|
...(onResults && { onResults }),
|
|
159
163
|
})
|
|
160
164
|
const succeeded = paymentResults.some(
|
|
@@ -175,10 +179,6 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
|
|
|
175
179
|
|
|
176
180
|
let sawTestnet = false
|
|
177
181
|
let sawMainnet = false
|
|
178
|
-
let paymentSucceeded = false
|
|
179
|
-
let sawMppEndpoint = false
|
|
180
|
-
let sawNonMppPaymentEndpoint = false
|
|
181
|
-
let sawMalformedChallenge = false
|
|
182
182
|
|
|
183
183
|
for await (const event of validateStream(options)) {
|
|
184
184
|
switch (event.phase) {
|
|
@@ -198,19 +198,15 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
|
|
|
198
198
|
case 'challenge':
|
|
199
199
|
if (current) current.challenge = event.results
|
|
200
200
|
if (event.isMpp) {
|
|
201
|
-
sawMppEndpoint = true
|
|
202
201
|
if (event.isTestnet) sawTestnet = true
|
|
203
|
-
|
|
202
|
+
if (event.isMainnet) sawMainnet = true
|
|
204
203
|
}
|
|
205
|
-
if (event.isNonMppPayment) sawNonMppPaymentEndpoint = true
|
|
206
|
-
if (event.isMalformedChallenge) sawMalformedChallenge = true
|
|
207
204
|
break
|
|
208
205
|
case 'errorHandling':
|
|
209
206
|
if (current) current.errorHandling = event.results
|
|
210
207
|
break
|
|
211
208
|
case 'payment':
|
|
212
209
|
if (current) current.payment = event.results
|
|
213
|
-
if (event.succeeded) paymentSucceeded = true
|
|
214
210
|
break
|
|
215
211
|
}
|
|
216
212
|
}
|
|
@@ -235,20 +231,50 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
|
|
|
235
231
|
|
|
236
232
|
if (!discovery) throw new Error('Discovery phase did not complete')
|
|
237
233
|
|
|
234
|
+
const suggestions = computeSuggestions(endpointResults, { sawTestnet, sawMainnet })
|
|
235
|
+
|
|
238
236
|
return {
|
|
239
237
|
url: baseUrl,
|
|
240
238
|
discovery,
|
|
241
239
|
endpoints: endpointResults,
|
|
242
240
|
summary,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
241
|
+
suggestions,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function computeSuggestions(
|
|
246
|
+
endpoints: EndpointValidationResult[],
|
|
247
|
+
flags: { sawTestnet: boolean; sawMainnet: boolean },
|
|
248
|
+
): string[] {
|
|
249
|
+
const steps: string[] = []
|
|
250
|
+
const allResults = endpoints.flatMap((ep) => [...ep.challenge, ...ep.payment])
|
|
251
|
+
|
|
252
|
+
const sawCryptoMainnet = flags.sawMainnet
|
|
253
|
+
const sawCryptoTestnet = flags.sawTestnet
|
|
254
|
+
const sawStripeLive = allResults.some(
|
|
255
|
+
(r) => r.label.includes('[stripe]') && r.label.includes('livemode'),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if (sawCryptoMainnet && !sawCryptoTestnet) {
|
|
259
|
+
steps.push(
|
|
260
|
+
'Validate your testnet server to test without real funds (Tempo wallets are automatically provisioned).',
|
|
261
|
+
)
|
|
262
|
+
} else if (sawCryptoTestnet && !sawCryptoMainnet) {
|
|
263
|
+
steps.push('Validate your mainnet server to confirm real payments work end-to-end.')
|
|
251
264
|
}
|
|
265
|
+
if (sawStripeLive) {
|
|
266
|
+
steps.push(
|
|
267
|
+
'Validate your testmode Stripe server to test card payments end-to-end (auto-pays with test card).',
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return steps
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const allChains = Object.values(viemChains) as Chain[]
|
|
275
|
+
|
|
276
|
+
function chainIsTestnet(chainId: number): boolean {
|
|
277
|
+
return allChains.find((c) => c.id === chainId)?.testnet === true
|
|
252
278
|
}
|
|
253
279
|
|
|
254
280
|
async function runDiscovery(baseUrl: string, options: ValidateOptions): Promise<DiscoveryResult> {
|