mppx 0.8.6 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cli/internal.js +2 -2
  3. package/dist/cli/internal.js.map +1 -1
  4. package/dist/cli/plugins/evm.d.ts +2 -0
  5. package/dist/cli/plugins/evm.d.ts.map +1 -0
  6. package/dist/cli/plugins/evm.js +51 -0
  7. package/dist/cli/plugins/evm.js.map +1 -0
  8. package/dist/cli/plugins/index.d.ts +1 -0
  9. package/dist/cli/plugins/index.d.ts.map +1 -1
  10. package/dist/cli/plugins/index.js +1 -0
  11. package/dist/cli/plugins/index.js.map +1 -1
  12. package/dist/cli/validate/challenge.d.ts +2 -0
  13. package/dist/cli/validate/challenge.d.ts.map +1 -1
  14. package/dist/cli/validate/challenge.js +184 -78
  15. package/dist/cli/validate/challenge.js.map +1 -1
  16. package/dist/cli/validate/discovery.d.ts +1 -0
  17. package/dist/cli/validate/discovery.d.ts.map +1 -1
  18. package/dist/cli/validate/discovery.js +99 -35
  19. package/dist/cli/validate/discovery.js.map +1 -1
  20. package/dist/cli/validate/helpers.d.ts +14 -1
  21. package/dist/cli/validate/helpers.d.ts.map +1 -1
  22. package/dist/cli/validate/helpers.js +0 -13
  23. package/dist/cli/validate/helpers.js.map +1 -1
  24. package/dist/cli/validate/index.d.ts +1 -0
  25. package/dist/cli/validate/index.d.ts.map +1 -1
  26. package/dist/cli/validate/index.js +98 -151
  27. package/dist/cli/validate/index.js.map +1 -1
  28. package/dist/cli/validate/payment.d.ts +1 -0
  29. package/dist/cli/validate/payment.d.ts.map +1 -1
  30. package/dist/cli/validate/payment.js +215 -144
  31. package/dist/cli/validate/payment.js.map +1 -1
  32. package/dist/evm/Chains.d.ts +4 -0
  33. package/dist/evm/Chains.d.ts.map +1 -1
  34. package/dist/evm/Chains.js +4 -0
  35. package/dist/evm/Chains.js.map +1 -1
  36. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  37. package/dist/internal/AcceptPayment.js +7 -1
  38. package/dist/internal/AcceptPayment.js.map +1 -1
  39. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  40. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  41. package/dist/stripe/server/internal/html.gen.js +1 -1
  42. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  43. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  44. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  45. package/dist/tempo/server/internal/html.gen.js +1 -1
  46. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  47. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/request-body.js +2 -0
  49. package/dist/tempo/server/internal/request-body.js.map +1 -1
  50. package/dist/tempo/session/server/ChannelStore.js +4 -4
  51. package/dist/tempo/session/server/ChannelStore.js.map +1 -1
  52. package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
  53. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  54. package/dist/tempo/session/server/CredentialVerification.js +21 -1
  55. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  56. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  57. package/dist/tempo/session/server/Session.js +1 -0
  58. package/dist/tempo/session/server/Session.js.map +1 -1
  59. package/dist/validation/core.d.ts +79 -0
  60. package/dist/validation/core.d.ts.map +1 -0
  61. package/dist/validation/core.js +256 -0
  62. package/dist/validation/core.js.map +1 -0
  63. package/dist/validation/index.d.ts +3 -0
  64. package/dist/validation/index.d.ts.map +1 -0
  65. package/dist/validation/index.js +2 -0
  66. package/dist/validation/index.js.map +1 -0
  67. package/dist/x402/Assets.d.ts +9 -0
  68. package/dist/x402/Assets.d.ts.map +1 -1
  69. package/dist/x402/Assets.js +38 -0
  70. package/dist/x402/Assets.js.map +1 -1
  71. package/package.json +9 -4
  72. package/src/cli/internal.ts +2 -2
  73. package/src/cli/plugins/evm.ts +55 -0
  74. package/src/cli/plugins/index.ts +1 -0
  75. package/src/cli/validate/challenge.ts +334 -121
  76. package/src/cli/validate/discovery.test.ts +307 -0
  77. package/src/cli/validate/discovery.ts +104 -38
  78. package/src/cli/validate/helpers.ts +15 -13
  79. package/src/cli/validate/index.ts +117 -240
  80. package/src/cli/validate/payment.ts +261 -178
  81. package/src/cli/validate.test.ts +143 -3
  82. package/src/client/internal/Fetch.test.ts +142 -2
  83. package/src/evm/Chains.ts +6 -0
  84. package/src/evm/PublicInterface.test-d.ts +7 -0
  85. package/src/internal/AcceptPayment.test.ts +45 -0
  86. package/src/internal/AcceptPayment.ts +11 -1
  87. package/src/stripe/server/internal/html/package.json +1 -1
  88. package/src/stripe/server/internal/html.gen.ts +1 -1
  89. package/src/tempo/server/internal/html/package.json +1 -1
  90. package/src/tempo/server/internal/html.gen.ts +1 -1
  91. package/src/tempo/server/internal/request-body.test.ts +9 -0
  92. package/src/tempo/server/internal/request-body.ts +2 -0
  93. package/src/tempo/session/server/ChannelStore.test.ts +17 -0
  94. package/src/tempo/session/server/ChannelStore.ts +4 -4
  95. package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
  96. package/src/tempo/session/server/CredentialVerification.ts +30 -0
  97. package/src/tempo/session/server/Session.integration.test.ts +5 -0
  98. package/src/tempo/session/server/Session.test.ts +119 -53
  99. package/src/tempo/session/server/Session.ts +1 -0
  100. package/src/validation/core.ts +348 -0
  101. package/src/validation/index.ts +11 -0
  102. package/src/x402/Assets.test.ts +42 -0
  103. package/src/x402/Assets.ts +40 -0
  104. package/src/x402/Exact.localnet.test.ts +475 -0
@@ -1,6 +1,7 @@
1
- import { createClient, http } from 'viem'
1
+ import { type Address, type Chain, createClient, erc20Abi, http } from 'viem'
2
2
  import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
3
- import { waitForTransactionReceipt } from 'viem/actions'
3
+ import { readContract, waitForTransactionReceipt } from 'viem/actions'
4
+ import * as viemChains from 'viem/chains'
4
5
  import { Actions } from 'viem/tempo'
5
6
  import { tempoModerato, tempo as tempoMainnetChain } from 'viem/tempo/chains'
6
7
 
@@ -11,10 +12,19 @@ import * as Receipt from '../../Receipt.js'
11
12
  import { tempo as tempoMethods } from '../../tempo/client/index.js'
12
13
  import { chainId as tempoChainIds } from '../../tempo/internal/defaults.js'
13
14
  import { resolveAccount, resolveAccountName } from '../account.js'
14
- import { loadConfig, selectChallenge } from '../internal.js'
15
+ import { loadConfig, resolvePlugin } from '../internal.js'
15
16
  import { fetchTokenInfo, confirm, pc } from '../utils.js'
17
+ import { buildUrl } from './discovery.js'
16
18
  import type { CheckResult, EndpointSpec } from './helpers.js'
17
- import { buildUrl, check, fail, fetchWithTimeout, formatBytes, skip, warn } from './helpers.js'
19
+ import {
20
+ check,
21
+ fail,
22
+ fetchWithTimeout,
23
+ formatBytes,
24
+ isValidIntegerAmount,
25
+ skip,
26
+ warn,
27
+ } from './helpers.js'
18
28
 
19
29
  async function provisionAndPayTestnet(
20
30
  challenge: Challenge.Challenge,
@@ -54,11 +64,52 @@ async function resolveWalletAddress(): Promise<string | undefined> {
54
64
  }
55
65
  }
56
66
 
67
+ async function fetchEvmTokenInfo(
68
+ chain: Chain,
69
+ token: Address,
70
+ account: Address,
71
+ ): Promise<{ balance: bigint; symbol: string | undefined }> {
72
+ const client = createClient({ chain, transport: http() })
73
+ const [balance, symbol] = await Promise.all([
74
+ readContract(client, {
75
+ address: token,
76
+ abi: erc20Abi,
77
+ functionName: 'balanceOf',
78
+ args: [account],
79
+ }),
80
+ readContract(client, { address: token, abi: erc20Abi, functionName: 'symbol' }).catch(
81
+ () => undefined,
82
+ ),
83
+ ])
84
+ return { balance, symbol: symbol ?? undefined }
85
+ }
86
+
87
+ function resolveEvmChain(chainId: number): Chain | undefined {
88
+ const all = Object.values(viemChains) as Chain[]
89
+ return all.find((c) => c.id === chainId)
90
+ }
91
+
92
+ function methodSetupHint(challenge: Challenge.Challenge): string {
93
+ switch (challenge.method) {
94
+ case Constants.Methods.evm:
95
+ return 'no EVM payment plugin yet'
96
+ case 'solana':
97
+ return 'no Solana payment plugin yet'
98
+ default:
99
+ return `no plugin for ${challenge.method}/${challenge.intent}`
100
+ }
101
+ }
102
+
57
103
  export async function validatePaymentFlow(
58
104
  baseUrl: string,
59
105
  endpoint: EndpointSpec,
60
106
  verbose: boolean,
61
- options?: { body?: string | undefined; query?: string[] | undefined; yes?: boolean | undefined },
107
+ options?: {
108
+ body?: string | undefined
109
+ query?: string[] | undefined
110
+ yes?: boolean | undefined
111
+ onResults?: (results: CheckResult[]) => void
112
+ },
62
113
  ): Promise<CheckResult[]> {
63
114
  const results: CheckResult[] = []
64
115
  const url = buildUrl(baseUrl, endpoint, options?.query)
@@ -87,33 +138,40 @@ export async function validatePaymentFlow(
87
138
  return results
88
139
  }
89
140
 
90
- // Parse challenge
91
- let challenge: Challenge.Challenge
141
+ // Parse all challenges
142
+ let challenges: Challenge.Challenge[]
92
143
  try {
93
- challenge = Challenge.fromResponse(challengeResponse)
144
+ challenges = Challenge.fromResponseList(challengeResponse)
94
145
  } catch (error) {
95
146
  results.push(fail('Payment: parse challenge', (error as Error).message))
96
147
  return results
97
148
  }
98
-
99
- // Detect network
100
- const request = challenge.request as Record<string, unknown>
101
- const methodDetails = request.methodDetails as Record<string, unknown> | undefined
102
- const isTestnet =
103
- typeof methodDetails?.chainId === 'number' && methodDetails.chainId !== tempoChainIds.mainnet
149
+ if (challenges.length === 0) {
150
+ results.push(fail('Payment: parse challenge', 'No Payment challenges in response'))
151
+ return results
152
+ }
104
153
 
105
154
  // Testnet Tempo: always use ephemeral wallet (zero-setup, free money)
106
- if (isTestnet && challenge.method === Constants.Methods.tempo) {
107
- const provisioned = await provisionAndPayTestnet(challenge, verbose)
155
+ const tempoTestnetChallenge = challenges.find((ch) => {
156
+ if (ch.method !== Constants.Methods.tempo) return false
157
+ const req = ch.request as Record<string, unknown>
158
+ const md = req.methodDetails as Record<string, unknown> | undefined
159
+ return typeof md?.chainId === 'number' && md.chainId !== tempoChainIds.mainnet
160
+ })
161
+
162
+ if (tempoTestnetChallenge) {
163
+ const provisioned = await provisionAndPayTestnet(tempoTestnetChallenge, verbose)
108
164
  if (provisioned) {
109
165
  const fakeResp = new Response(null, {
110
166
  status: 402,
111
- headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
167
+ headers: {
168
+ [Constants.Headers.wwwAuthenticate]: Challenge.serialize(tempoTestnetChallenge),
169
+ },
112
170
  })
113
171
  try {
114
172
  const mppx = Mppx.create({ methods: provisioned.methods, polyfill: false })
115
173
  const cred = await mppx.createCredential(fakeResp)
116
- results.push(check('Payment: credential created', 'ephemeral testnet wallet'))
174
+ results.push(check('Payment: submitted', 'ephemeral testnet wallet'))
117
175
  return await sendAndValidateResponse(
118
176
  results,
119
177
  url,
@@ -122,7 +180,7 @@ export async function validatePaymentFlow(
122
180
  fetchHeaders,
123
181
  fetchBody,
124
182
  verbose,
125
- true,
183
+ tempoModerato,
126
184
  )
127
185
  } catch (error) {
128
186
  results.push(fail('Payment: create credential', (error as Error).message))
@@ -136,177 +194,202 @@ export async function validatePaymentFlow(
136
194
  }
137
195
  }
138
196
 
139
- // Mainnet: use configured wallet
197
+ // Try each challenge method (skip testnet challenge already handled above)
140
198
  const loaded = await loadConfig().catch(() => undefined)
141
- const selected = selectChallenge([challenge], loaded?.config)
142
-
143
- if (!selected) {
144
- results.push(
145
- skip(
146
- 'Payment: no configured method',
147
- `Need ${challenge.method}/${challenge.intent}`,
148
- 'Run "mppx account create" to create a local wallet for payment testing. The wallet is stored on your machine and only used by mppx.',
149
- ),
150
- )
151
- return results
152
- }
199
+ const isInteractive = process.stdin.isTTY ?? false
200
+ const supportedPaymentMethods: Set<string> = new Set([
201
+ Constants.Methods.tempo,
202
+ Constants.Methods.evm,
203
+ ])
204
+
205
+ for (const challenge of challenges) {
206
+ if (challenge === tempoTestnetChallenge) continue
207
+ if (!supportedPaymentMethods.has(challenge.method)) continue
208
+
209
+ const flushStart = results.length
210
+ const flush = () => {
211
+ if (options?.onResults && results.length > flushStart)
212
+ options.onResults(results.slice(flushStart))
213
+ }
214
+ const tag = `Payment [${challenge.method}]`
153
215
 
154
- const { plugin, method: directMethod } = selected
216
+ try {
217
+ const request = challenge.request as Record<string, unknown>
218
+ const requiredAmount = isValidIntegerAmount(request.amount)
219
+ ? BigInt(request.amount as string)
220
+ : undefined
221
+ const methodDetails = request.methodDetails as Record<string, unknown> | undefined
222
+ const decimals =
223
+ (methodDetails?.decimals as number | undefined) ??
224
+ (request.decimals as number | undefined) ??
225
+ 6
226
+ const currency = request.currency as string | undefined
227
+
228
+ // Resolve wallet
229
+ let walletAddress: string | undefined
230
+ try {
231
+ walletAddress = await resolveWalletAddress()
232
+ } catch {}
155
233
 
156
- if (!plugin && !directMethod) {
157
- results.push(skip('Payment: no plugin available', `${challenge.method}/${challenge.intent}`))
158
- return results
159
- }
234
+ if (!walletAddress) {
235
+ results.push(skip(tag, 'no wallet configured. Run "mppx account create" to create one.'))
236
+ continue
237
+ }
160
238
 
161
- const requiredAmount = request.amount ? BigInt(request.amount as string) : undefined
162
- // Display only -- defaults to 6 (standard for USDC/PathUSD tokens)
163
- const decimals = (request.decimals as number | undefined) ?? 6
164
- const currency = request.currency as string | undefined
239
+ // Pre-flight balance check and chain resolution
240
+ let insufficientBalance = false
241
+ let tokenSymbol: string | undefined
242
+ let paymentChain: Chain | undefined
243
+ if (challenge.method === Constants.Methods.tempo) {
244
+ paymentChain = tempoMainnetChain
245
+ } else if (challenge.method === Constants.Methods.evm) {
246
+ const chainId = methodDetails?.chainId as number | undefined
247
+ if (chainId) paymentChain = resolveEvmChain(chainId)
248
+ }
165
249
 
166
- // Pre-flight balance check
167
- let walletAddress: string | undefined
168
- if (challenge.method === Constants.Methods.tempo && requiredAmount && currency) {
169
- try {
170
- walletAddress = await resolveWalletAddress()
171
- if (walletAddress) {
172
- const client = createClient({ chain: tempoMainnetChain, transport: http() })
173
- const tokenInfo = await fetchTokenInfo(
174
- client,
175
- currency as `0x${string}`,
176
- walletAddress as `0x${string}`,
250
+ if (requiredAmount && currency && paymentChain) {
251
+ try {
252
+ let balance: bigint
253
+ if (challenge.method === Constants.Methods.tempo) {
254
+ const client = createClient({ chain: tempoMainnetChain, transport: http() })
255
+ const info = await fetchTokenInfo(client, currency as Address, walletAddress as Address)
256
+ balance = info.balance
257
+ tokenSymbol = info.symbol
258
+ } else {
259
+ const info = await fetchEvmTokenInfo(
260
+ paymentChain,
261
+ currency as Address,
262
+ walletAddress as Address,
263
+ )
264
+ balance = info.balance
265
+ tokenSymbol = info.symbol
266
+ }
267
+ if (balance < requiredAmount) {
268
+ const requiredDisplay = `$${(Number(requiredAmount) / 10 ** decimals).toFixed(2)}`
269
+ const balanceDisplay = `$${(Number(balance) / 10 ** decimals).toFixed(2)}`
270
+ const symbol = tokenSymbol ?? 'tokens'
271
+ results.push(
272
+ skip(
273
+ tag,
274
+ `insufficient balance (have ${balanceDisplay}, need ${requiredDisplay} ${symbol} on ${paymentChain.name})`,
275
+ `Fund wallet ${walletAddress} with at least ${requiredDisplay} ${symbol} on ${paymentChain.name}.`,
276
+ ),
277
+ )
278
+ insufficientBalance = true
279
+ }
280
+ } catch (e) {
281
+ if (verbose) console.log(pc.dim(` Balance check skipped: ${(e as Error).message}`))
282
+ }
283
+ }
284
+ if (insufficientBalance) continue
285
+
286
+ // Prompt
287
+ const amountDisplay = requiredAmount
288
+ ? `$${(Number(requiredAmount) / 10 ** decimals).toFixed(2)}`
289
+ : 'unknown amount'
290
+ const tokenDisplay = tokenSymbol ?? (currency?.length === 3 ? currency.toUpperCase() : '')
291
+ const chainName = paymentChain?.name
292
+
293
+ if (walletAddress) console.log(pc.dim(` Using wallet: ${walletAddress}`))
294
+
295
+ if (paymentChain?.testnet) {
296
+ console.log(
297
+ pc.dim(
298
+ ` Auto-approved: ${amountDisplay}${tokenDisplay ? ` ${tokenDisplay}` : ''}${chainName ? ` on ${chainName}` : ''} (testnet)`,
299
+ ),
300
+ )
301
+ } else if (!options?.yes && !isInteractive) {
302
+ results.push(skip(tag, 'non-interactive mode, use --yes to approve'))
303
+ continue
304
+ } else if (!options?.yes) {
305
+ console.log('')
306
+ const ok = await confirm(
307
+ ` ${pc.yellow('Pay')} ${amountDisplay}${tokenDisplay ? ` ${tokenDisplay}` : ''}${chainName ? ` on ${chainName}` : ''}. Continue?`,
308
+ false,
177
309
  )
178
- const requiredDisplay = `$${(Number(requiredAmount) / 10 ** decimals).toFixed(2)}`
179
- const balanceDisplay = `$${(Number(tokenInfo.balance) / 10 ** tokenInfo.decimals).toFixed(2)}`
180
-
181
- if (tokenInfo.balance < requiredAmount) {
182
- console.log(pc.dim(` Wallet: ${walletAddress}`))
183
- console.log(pc.dim(` Balance: ${balanceDisplay} ${tokenInfo.symbol}`))
184
- const hint = `Wallet ${walletAddress} has ${balanceDisplay} but endpoint requires ${requiredDisplay}. Fund this wallet to run payment tests, or use a testnet server.`
185
- results.push(
186
- skip(
187
- 'Payment: insufficient balance',
188
- `Have ${balanceDisplay}, need ${requiredDisplay}`,
189
- hint,
190
- ),
191
- )
192
- return results
310
+ if (!ok) {
311
+ results.push(skip(tag, 'declined'))
312
+ continue
193
313
  }
314
+ } else {
315
+ console.log(
316
+ pc.dim(
317
+ ` Auto-approved: ${amountDisplay}${tokenDisplay ? ` ${tokenDisplay}` : ''}${chainName ? ` on ${chainName}` : ''}`,
318
+ ),
319
+ )
194
320
  }
195
- } catch (e) {
196
- if (verbose) console.log(pc.dim(` Balance check skipped: ${(e as Error).message}`))
197
- }
198
- }
199
-
200
- // Prompt before paying on mainnet (unless --yes or non-interactive)
201
- const amountDisplay = requiredAmount
202
- ? `$${(Number(requiredAmount) / 10 ** decimals).toFixed(2)}`
203
- : 'unknown amount'
204
- if (walletAddress) console.log(pc.dim(` Using wallet: ${walletAddress}`))
205
- const isInteractive = process.stdin.isTTY ?? false
206
- if (!options?.yes && !isInteractive) {
207
- results.push(
208
- skip('Payment: skipped', 'Non-interactive mode. Use --yes to approve mainnet payments.'),
209
- )
210
- return results
211
- }
212
- if (!options?.yes) {
213
- console.log('')
214
- const ok = await confirm(
215
- ` ${pc.yellow('Mainnet payment:')} Will transfer ${amountDisplay} to ${String(request.recipient).slice(0, 10)}... Continue?`,
216
- false,
217
- )
218
- if (!ok) {
219
- results.push(skip('Payment: skipped by user', 'mainnet payment declined'))
220
- return results
221
- }
222
- } else {
223
- console.log(
224
- pc.dim(` Auto-approved: ${amountDisplay} to ${String(request.recipient).slice(0, 10)}...`),
225
- )
226
- }
227
321
 
228
- // Setup plugin or use direct method
229
- let methods: import('../../Method.js').AnyClient[]
230
- let createCredentialFn: ((response: Response) => Promise<string>) | undefined
322
+ // Resolve payment methods
323
+ let methods: import('../../Method.js').AnyClient[]
324
+ let createCredentialFn: ((response: Response) => Promise<string>) | undefined
325
+ let plugin: import('../plugins/plugin.js').Plugin | undefined
326
+
327
+ const resolved = resolvePlugin(challenge, loaded?.config)
328
+ plugin = resolved.plugin
329
+ const directMethod = resolved.method
330
+ if (!plugin && !directMethod) {
331
+ results.push(skip(tag, methodSetupHint(challenge)))
332
+ continue
333
+ }
334
+ if (plugin) {
335
+ try {
336
+ const pluginResult = await plugin.setup({
337
+ challenge,
338
+ options: { network: 'mainnet' },
339
+ methodOpts: {},
340
+ })
341
+ methods = pluginResult.methods
342
+ createCredentialFn = pluginResult.createCredential
343
+ } catch (error) {
344
+ results.push(skip(tag, (error as Error).message))
345
+ continue
346
+ }
347
+ } else {
348
+ methods = [directMethod!]
349
+ }
231
350
 
232
- if (plugin) {
233
- try {
234
- const pluginResult = await plugin.setup({
235
- challenge,
236
- options: { network: 'mainnet' },
237
- methodOpts: {},
351
+ // Create credential and send
352
+ let credential: string
353
+ const fakeResponse = new Response(null, {
354
+ status: 402,
355
+ headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
238
356
  })
239
- methods = pluginResult.methods
240
- createCredentialFn = pluginResult.createCredential
241
- } catch (error) {
242
- const msg = (error as Error).message
243
- if (msg.includes('No account found') || msg.includes('not found')) {
244
- results.push(skip('Payment: no wallet configured', msg))
245
- } else {
246
- results.push(fail('Payment: wallet setup', msg))
357
+ try {
358
+ if (createCredentialFn) {
359
+ credential = await createCredentialFn(fakeResponse)
360
+ } else {
361
+ const mppx = Mppx.create({ methods, polyfill: false })
362
+ credential = await mppx.createCredential(fakeResponse)
363
+ }
364
+ } catch (error) {
365
+ const msg = (error as Error).message
366
+ if (msg.toLowerCase().includes('insufficient')) {
367
+ results.push(skip(tag, `insufficient balance: ${msg}`))
368
+ continue
369
+ }
370
+ results.push(fail(tag, msg))
371
+ continue
247
372
  }
248
- return results
249
- }
250
- } else {
251
- methods = [directMethod!]
252
- }
253
373
 
254
- // Create credential
255
- let credential: string
256
- const fakeResponse = new Response(null, {
257
- status: 402,
258
- headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
259
- })
260
- try {
261
- if (createCredentialFn) {
262
- credential = await createCredentialFn(fakeResponse)
263
- } else {
264
- const mppx = Mppx.create({ methods, polyfill: false })
265
- credential = await mppx.createCredential(fakeResponse)
266
- }
267
- } catch (error) {
268
- const msg = (error as Error).message
269
- const isInsufficientBalance =
270
- msg.toLowerCase().includes('insufficientbalance') ||
271
- msg.toLowerCase().includes('insufficient')
272
- if (isInsufficientBalance) {
273
- const match = msg.match(/available:\s*(\d+),\s*required:\s*(\d+)/)
274
- const available = match ? BigInt(match[1]!) : undefined
275
- const required = match ? BigInt(match[2]!) : requiredAmount
276
- const fromMatch = msg.match(/from:\s*(0x[0-9a-fA-F]{40})/)
277
- const fromAddr = fromMatch?.[1]
278
- const requiredDisplay = required
279
- ? `$${(Number(required) / 10 ** decimals).toFixed(2)}`
280
- : 'unknown'
281
- const availableDisplay =
282
- available !== undefined ? `$${(Number(available) / 10 ** decimals).toFixed(2)}` : undefined
283
- const detail = availableDisplay
284
- ? `Have ${availableDisplay}, need ${requiredDisplay}`
285
- : `Endpoint requires ${requiredDisplay}`
286
- const hint = fromAddr
287
- ? `Wallet ${fromAddr} needs at least ${requiredDisplay}. This is a local wallet created by "mppx account create". Fund it to run payment tests, or point at a testnet server for free validation.`
288
- : `Fund your mppx wallet with at least ${requiredDisplay} to run payment tests, or use a testnet server.`
289
- results.push(skip('Payment: insufficient balance', detail, hint))
290
- return results
374
+ results.push(check(`${tag}: submitted`))
375
+
376
+ plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders })
377
+ await sendAndValidateResponse(
378
+ results,
379
+ url,
380
+ endpoint,
381
+ credential,
382
+ fetchHeaders,
383
+ fetchBody,
384
+ verbose,
385
+ paymentChain,
386
+ )
387
+ } finally {
388
+ flush()
291
389
  }
292
- results.push(fail('Payment: create credential', msg))
293
- return results
294
390
  }
295
391
 
296
- results.push(check('Payment: credential created'))
297
-
298
- // Prepare and send
299
- plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders })
300
- return await sendAndValidateResponse(
301
- results,
302
- url,
303
- endpoint,
304
- credential,
305
- fetchHeaders,
306
- fetchBody,
307
- verbose,
308
- isTestnet,
309
- )
392
+ return results
310
393
  }
311
394
 
312
395
  async function sendAndValidateResponse(
@@ -317,7 +400,7 @@ async function sendAndValidateResponse(
317
400
  baseHeaders: Record<string, string>,
318
401
  fetchBody: string | undefined,
319
402
  verbose: boolean,
320
- isTestnet?: boolean,
403
+ explorerChain?: Chain | undefined,
321
404
  ): Promise<CheckResult[]> {
322
405
  let paymentResponse: Response
323
406
  try {
@@ -475,10 +558,10 @@ async function sendAndValidateResponse(
475
558
  try {
476
559
  const receipt = Receipt.deserialize(receiptHeader)
477
560
  if (receipt.reference && /^0x[0-9a-fA-F]{64}$/.test(receipt.reference)) {
478
- const chain = isTestnet ? tempoModerato : tempoMainnetChain
479
- const explorerUrl = chain.blockExplorers?.default?.url
561
+ const explorerUrl = explorerChain?.blockExplorers?.default?.url
480
562
  if (explorerUrl) {
481
- results.push(check('On-chain transaction', `${explorerUrl}/receipt/${receipt.reference}`))
563
+ const path = explorerUrl.includes('tempo') ? 'receipt' : 'tx'
564
+ results.push(check('On-chain transaction', `${explorerUrl}/${path}/${receipt.reference}`))
482
565
  }
483
566
  }
484
567
  } catch {}