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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as childProcess from 'node:child_process'
|
|
2
|
+
|
|
1
3
|
import { type Address, type Chain, createClient, erc20Abi, http } from 'viem'
|
|
2
4
|
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
|
|
3
5
|
import { readContract, waitForTransactionReceipt } from 'viem/actions'
|
|
@@ -8,10 +10,12 @@ import { tempoModerato, tempo as tempoMainnetChain } from 'viem/tempo/chains'
|
|
|
8
10
|
import * as Challenge from '../../Challenge.js'
|
|
9
11
|
import * as Mppx from '../../client/Mppx.js'
|
|
10
12
|
import * as Constants from '../../Constants.js'
|
|
13
|
+
import type { AnyClient } from '../../Method.js'
|
|
11
14
|
import * as Receipt from '../../Receipt.js'
|
|
12
15
|
import { tempo as tempoMethods } from '../../tempo/client/index.js'
|
|
13
16
|
import { chainId as tempoChainIds } from '../../tempo/internal/defaults.js'
|
|
14
17
|
import { resolveAccount, resolveAccountName } from '../account.js'
|
|
18
|
+
import type { Config } from '../config.js'
|
|
15
19
|
import { loadConfig, resolvePlugin } from '../internal.js'
|
|
16
20
|
import { fetchTokenInfo, confirm, pc } from '../utils.js'
|
|
17
21
|
import { buildUrl } from './discovery.js'
|
|
@@ -29,16 +33,17 @@ import {
|
|
|
29
33
|
async function provisionAndPayTestnet(
|
|
30
34
|
challenge: Challenge.Challenge,
|
|
31
35
|
verbose: boolean,
|
|
32
|
-
|
|
36
|
+
silent?: boolean,
|
|
37
|
+
): Promise<{ methods: AnyClient[] } | undefined> {
|
|
33
38
|
try {
|
|
34
|
-
console.log(pc.dim(' Provisioning testnet wallet and funding via faucet...'))
|
|
39
|
+
if (!silent) console.log(pc.dim(' Provisioning testnet wallet and funding via faucet...'))
|
|
35
40
|
const key = generatePrivateKey()
|
|
36
41
|
const account = privateKeyToAccount(key)
|
|
37
42
|
|
|
38
43
|
const client = createClient({ chain: tempoModerato, transport: http() })
|
|
39
44
|
const hashes = await Actions.faucet.fund(client, { account })
|
|
40
45
|
await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })))
|
|
41
|
-
console.log(pc.dim(` Using wallet: ${account.address}`))
|
|
46
|
+
if (!silent) console.log(pc.dim(` Using wallet: ${account.address}`))
|
|
42
47
|
|
|
43
48
|
const methods = [...tempoMethods({ account })]
|
|
44
49
|
return { methods }
|
|
@@ -64,6 +69,20 @@ async function resolveWalletAddress(): Promise<string | undefined> {
|
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
// Auto-detect Stripe test key from the Stripe CLI if installed and logged in.
|
|
73
|
+
function resolveStripeKey(verbose: boolean): string | undefined {
|
|
74
|
+
try {
|
|
75
|
+
const { execSync } = childProcess
|
|
76
|
+
const output = execSync('stripe config --list', { encoding: 'utf8', timeout: 5000 })
|
|
77
|
+
const match = output.match(/test_mode_api_key\s*=\s*'([^']+)'/)
|
|
78
|
+
if (match?.[1]) {
|
|
79
|
+
if (verbose) console.log(pc.dim(' Using Stripe test key from stripe CLI'))
|
|
80
|
+
return match[1]
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
async function fetchEvmTokenInfo(
|
|
68
87
|
chain: Chain,
|
|
69
88
|
token: Address,
|
|
@@ -89,6 +108,10 @@ function resolveEvmChain(chainId: number): Chain | undefined {
|
|
|
89
108
|
return all.find((c) => c.id === chainId)
|
|
90
109
|
}
|
|
91
110
|
|
|
111
|
+
function formatAmount(amount: bigint, decimals: number): string {
|
|
112
|
+
return `$${(Number(amount) / 10 ** decimals).toFixed(2)}`
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
function methodSetupHint(challenge: Challenge.Challenge): string {
|
|
93
116
|
switch (challenge.method) {
|
|
94
117
|
case Constants.Methods.evm:
|
|
@@ -99,23 +122,24 @@ function methodSetupHint(challenge: Challenge.Challenge): string {
|
|
|
99
122
|
return `no plugin for ${challenge.method}/${challenge.intent}`
|
|
100
123
|
}
|
|
101
124
|
}
|
|
102
|
-
|
|
103
125
|
export async function validatePaymentFlow(
|
|
104
126
|
baseUrl: string,
|
|
105
127
|
endpoint: EndpointSpec,
|
|
106
128
|
verbose: boolean,
|
|
107
|
-
options
|
|
129
|
+
options: {
|
|
108
130
|
body?: string | undefined
|
|
109
131
|
query?: string[] | undefined
|
|
110
132
|
yes?: boolean | undefined
|
|
133
|
+
silent?: boolean | undefined
|
|
134
|
+
interactive?: boolean | undefined
|
|
111
135
|
onResults?: (results: CheckResult[]) => void
|
|
112
136
|
},
|
|
113
137
|
): Promise<CheckResult[]> {
|
|
114
138
|
const results: CheckResult[] = []
|
|
115
|
-
const url = buildUrl(baseUrl, endpoint, options
|
|
139
|
+
const url = buildUrl(baseUrl, endpoint, options.query)
|
|
116
140
|
const fetchHeaders: Record<string, string> = {}
|
|
117
141
|
let fetchBody: string | undefined
|
|
118
|
-
if (options
|
|
142
|
+
if (options.body) {
|
|
119
143
|
fetchBody = options.body
|
|
120
144
|
fetchHeaders['content-type'] = 'application/json'
|
|
121
145
|
}
|
|
@@ -160,7 +184,7 @@ export async function validatePaymentFlow(
|
|
|
160
184
|
})
|
|
161
185
|
|
|
162
186
|
if (tempoTestnetChallenge) {
|
|
163
|
-
const provisioned = await provisionAndPayTestnet(tempoTestnetChallenge, verbose)
|
|
187
|
+
const provisioned = await provisionAndPayTestnet(tempoTestnetChallenge, verbose, options.silent)
|
|
164
188
|
if (provisioned) {
|
|
165
189
|
const fakeResp = new Response(null, {
|
|
166
190
|
status: 402,
|
|
@@ -196,200 +220,362 @@ export async function validatePaymentFlow(
|
|
|
196
220
|
|
|
197
221
|
// Try each challenge method (skip testnet challenge already handled above)
|
|
198
222
|
const loaded = await loadConfig().catch(() => undefined)
|
|
199
|
-
const isInteractive =
|
|
223
|
+
const isInteractive = options.interactive
|
|
224
|
+
const stripeKey = process.env.MPPX_STRIPE_SECRET_KEY ?? resolveStripeKey(verbose)
|
|
225
|
+
const isStripeTestKey = stripeKey?.startsWith('sk_test_') || stripeKey?.startsWith('rk_test_')
|
|
200
226
|
const supportedPaymentMethods: Set<string> = new Set([
|
|
201
227
|
Constants.Methods.tempo,
|
|
202
228
|
Constants.Methods.evm,
|
|
229
|
+
Constants.Methods.stripe,
|
|
203
230
|
])
|
|
204
231
|
|
|
205
232
|
for (const challenge of challenges) {
|
|
206
233
|
if (challenge === tempoTestnetChallenge) continue
|
|
207
234
|
if (!supportedPaymentMethods.has(challenge.method)) continue
|
|
235
|
+
if (!options.silent) console.log('')
|
|
208
236
|
|
|
209
237
|
const flushStart = results.length
|
|
210
238
|
const flush = () => {
|
|
211
|
-
if (options
|
|
239
|
+
if (options.onResults && results.length > flushStart)
|
|
212
240
|
options.onResults(results.slice(flushStart))
|
|
213
241
|
}
|
|
214
242
|
const tag = `Payment [${challenge.method}]`
|
|
215
243
|
|
|
216
244
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
245
|
+
switch (challenge.method) {
|
|
246
|
+
case Constants.Methods.tempo:
|
|
247
|
+
case Constants.Methods.evm:
|
|
248
|
+
await attemptCryptoPayment(challenge, tag, {
|
|
249
|
+
results,
|
|
250
|
+
url,
|
|
251
|
+
endpoint,
|
|
252
|
+
fetchHeaders,
|
|
253
|
+
fetchBody,
|
|
254
|
+
verbose,
|
|
255
|
+
loaded,
|
|
256
|
+
isInteractive,
|
|
257
|
+
options,
|
|
258
|
+
})
|
|
259
|
+
break
|
|
260
|
+
case Constants.Methods.stripe:
|
|
261
|
+
await attemptStripePayment(challenge, tag, {
|
|
262
|
+
results,
|
|
263
|
+
url,
|
|
264
|
+
endpoint,
|
|
265
|
+
fetchHeaders,
|
|
266
|
+
fetchBody,
|
|
267
|
+
verbose,
|
|
268
|
+
loaded,
|
|
269
|
+
isInteractive,
|
|
270
|
+
isStripeTestKey: isStripeTestKey ?? false,
|
|
271
|
+
stripeKey,
|
|
272
|
+
options,
|
|
273
|
+
})
|
|
274
|
+
break
|
|
237
275
|
}
|
|
276
|
+
} finally {
|
|
277
|
+
flush()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
238
280
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
}
|
|
281
|
+
return results
|
|
282
|
+
}
|
|
249
283
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
284
|
+
type PaymentContext = {
|
|
285
|
+
results: CheckResult[]
|
|
286
|
+
url: string
|
|
287
|
+
endpoint: EndpointSpec
|
|
288
|
+
fetchHeaders: Record<string, string>
|
|
289
|
+
fetchBody: string | undefined
|
|
290
|
+
verbose: boolean
|
|
291
|
+
loaded: { config: Config; path: string } | undefined
|
|
292
|
+
isInteractive: boolean | undefined
|
|
293
|
+
options: {
|
|
294
|
+
body?: string | undefined
|
|
295
|
+
query?: string[] | undefined
|
|
296
|
+
yes?: boolean | undefined
|
|
297
|
+
silent?: boolean | undefined
|
|
298
|
+
interactive?: boolean | undefined
|
|
299
|
+
onResults?: ((results: CheckResult[]) => void) | undefined
|
|
300
|
+
}
|
|
301
|
+
}
|
|
285
302
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
303
|
+
async function attemptCryptoPayment(
|
|
304
|
+
challenge: Challenge.Challenge,
|
|
305
|
+
tag: string,
|
|
306
|
+
ctx: PaymentContext,
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
const { results, url, endpoint, fetchHeaders, fetchBody, verbose, loaded, options } = ctx
|
|
309
|
+
const request = challenge.request as Record<string, unknown>
|
|
310
|
+
const methodDetails = request.methodDetails as Record<string, unknown> | undefined
|
|
311
|
+
const requiredAmount = isValidIntegerAmount(request.amount)
|
|
312
|
+
? BigInt(request.amount as string)
|
|
313
|
+
: undefined
|
|
314
|
+
const decimals =
|
|
315
|
+
(methodDetails?.decimals as number | undefined) ?? (request.decimals as number | undefined) ?? 6
|
|
316
|
+
const currency = request.currency as string | undefined
|
|
317
|
+
|
|
318
|
+
// Resolve wallet
|
|
319
|
+
let walletAddress: string | undefined
|
|
320
|
+
try {
|
|
321
|
+
walletAddress = await resolveWalletAddress()
|
|
322
|
+
} catch {}
|
|
323
|
+
if (!walletAddress) {
|
|
324
|
+
results.push(skip(tag, 'no wallet configured. Run "mppx account create" to create one.'))
|
|
325
|
+
return
|
|
326
|
+
}
|
|
292
327
|
|
|
293
|
-
|
|
328
|
+
// Pre-flight balance check and chain resolution
|
|
329
|
+
let paymentChain: Chain | undefined
|
|
330
|
+
if (challenge.method === Constants.Methods.tempo) paymentChain = tempoMainnetChain
|
|
331
|
+
else if (challenge.method === Constants.Methods.evm) {
|
|
332
|
+
const chainId = methodDetails?.chainId as number | undefined
|
|
333
|
+
if (chainId) paymentChain = resolveEvmChain(chainId)
|
|
334
|
+
}
|
|
294
335
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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,
|
|
309
|
-
)
|
|
310
|
-
if (!ok) {
|
|
311
|
-
results.push(skip(tag, 'declined'))
|
|
312
|
-
continue
|
|
313
|
-
}
|
|
336
|
+
let tokenSymbol: string | undefined
|
|
337
|
+
if (requiredAmount && currency && paymentChain) {
|
|
338
|
+
try {
|
|
339
|
+
let balance: bigint
|
|
340
|
+
if (challenge.method === Constants.Methods.tempo) {
|
|
341
|
+
const client = createClient({ chain: tempoMainnetChain, transport: http() })
|
|
342
|
+
const info = await fetchTokenInfo(client, currency as Address, walletAddress as Address)
|
|
343
|
+
balance = info.balance
|
|
344
|
+
tokenSymbol = info.symbol
|
|
314
345
|
} else {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
346
|
+
const info = await fetchEvmTokenInfo(
|
|
347
|
+
paymentChain,
|
|
348
|
+
currency as Address,
|
|
349
|
+
walletAddress as Address,
|
|
350
|
+
)
|
|
351
|
+
balance = info.balance
|
|
352
|
+
tokenSymbol = info.symbol
|
|
353
|
+
}
|
|
354
|
+
if (balance < requiredAmount) {
|
|
355
|
+
const requiredDisplay = formatAmount(requiredAmount, decimals)
|
|
356
|
+
const balanceDisplay = formatAmount(balance, decimals)
|
|
357
|
+
const symbol = tokenSymbol ?? 'tokens'
|
|
358
|
+
results.push(
|
|
359
|
+
skip(
|
|
360
|
+
tag,
|
|
361
|
+
`insufficient balance (have ${balanceDisplay}, need ${requiredDisplay} ${symbol} on ${paymentChain.name})`,
|
|
362
|
+
`Fund wallet ${walletAddress} with at least ${requiredDisplay} ${symbol} on ${paymentChain.name}.`,
|
|
318
363
|
),
|
|
319
364
|
)
|
|
365
|
+
return
|
|
320
366
|
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
if (verbose) console.log(pc.dim(` Balance check skipped: ${(e as Error).message}`))
|
|
369
|
+
}
|
|
370
|
+
}
|
|
321
371
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
results.push(skip(tag, (error as Error).message))
|
|
345
|
-
continue
|
|
346
|
-
}
|
|
347
|
-
} else {
|
|
348
|
-
methods = [directMethod!]
|
|
349
|
-
}
|
|
372
|
+
// Prompt
|
|
373
|
+
const amountDisplay = requiredAmount ? formatAmount(requiredAmount, decimals) : 'unknown amount'
|
|
374
|
+
const tokenDisplay = tokenSymbol ?? (currency?.length === 3 ? currency.toUpperCase() : '')
|
|
375
|
+
const chainName = paymentChain?.name
|
|
376
|
+
const paymentDesc = `${amountDisplay}${tokenDisplay ? ` ${tokenDisplay}` : ''}${chainName ? ` on ${chainName}` : ''}`
|
|
377
|
+
|
|
378
|
+
if (!options.silent) console.log(pc.dim(` Attempting payment with wallet ${walletAddress}`))
|
|
379
|
+
|
|
380
|
+
if (paymentChain?.testnet) {
|
|
381
|
+
if (!options.silent) console.log(pc.dim(` Auto-approved: ${paymentDesc} (testnet)`))
|
|
382
|
+
} else if (!options.yes && !ctx.isInteractive) {
|
|
383
|
+
results.push(skip(tag, 'non-interactive mode, use --yes to approve'))
|
|
384
|
+
return
|
|
385
|
+
} else if (!options.yes) {
|
|
386
|
+
const ok = await confirm(` ${pc.yellow('Pay')} ${paymentDesc}. Continue?`, false)
|
|
387
|
+
if (!ok) {
|
|
388
|
+
results.push(skip(tag, 'declined'))
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
if (!options.silent) console.log(pc.dim(` Auto-approved: ${paymentDesc}`))
|
|
393
|
+
}
|
|
350
394
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
395
|
+
// Resolve plugin and pay
|
|
396
|
+
const resolved = resolvePlugin(challenge, loaded?.config)
|
|
397
|
+
const plugin = resolved.plugin
|
|
398
|
+
const directMethod = resolved.method
|
|
399
|
+
if (!plugin && !directMethod) {
|
|
400
|
+
results.push(skip(tag, methodSetupHint(challenge)))
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let methods: AnyClient[]
|
|
405
|
+
let createCredentialFn: ((response: Response) => Promise<string>) | undefined
|
|
406
|
+
if (plugin) {
|
|
407
|
+
try {
|
|
408
|
+
const pluginResult = await plugin.setup({
|
|
409
|
+
challenge,
|
|
410
|
+
options: { network: 'mainnet' },
|
|
411
|
+
methodOpts: {},
|
|
356
412
|
})
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
413
|
+
methods = pluginResult.methods
|
|
414
|
+
createCredentialFn = pluginResult.createCredential
|
|
415
|
+
} catch (error) {
|
|
416
|
+
results.push(skip(tag, (error as Error).message))
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
methods = [directMethod!]
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results)
|
|
424
|
+
if (!credential) return
|
|
425
|
+
|
|
426
|
+
plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders })
|
|
427
|
+
await sendAndValidateResponse(
|
|
428
|
+
results,
|
|
429
|
+
url,
|
|
430
|
+
endpoint,
|
|
431
|
+
credential,
|
|
432
|
+
fetchHeaders,
|
|
433
|
+
fetchBody,
|
|
434
|
+
verbose,
|
|
435
|
+
paymentChain,
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function attemptStripePayment(
|
|
440
|
+
challenge: Challenge.Challenge,
|
|
441
|
+
tag: string,
|
|
442
|
+
ctx: PaymentContext & { isStripeTestKey: boolean; stripeKey?: string | undefined },
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
const {
|
|
445
|
+
results,
|
|
446
|
+
url,
|
|
447
|
+
endpoint,
|
|
448
|
+
fetchHeaders,
|
|
449
|
+
fetchBody,
|
|
450
|
+
verbose,
|
|
451
|
+
loaded,
|
|
452
|
+
options,
|
|
453
|
+
isStripeTestKey,
|
|
454
|
+
stripeKey,
|
|
455
|
+
} = ctx
|
|
456
|
+
const request = challenge.request as Record<string, unknown>
|
|
457
|
+
const requiredAmount = isValidIntegerAmount(request.amount)
|
|
458
|
+
? BigInt(request.amount as string)
|
|
459
|
+
: undefined
|
|
460
|
+
const decimals = (request.decimals as number | undefined) ?? 2
|
|
461
|
+
const currency = request.currency as string | undefined
|
|
462
|
+
|
|
463
|
+
const amountDisplay = requiredAmount ? formatAmount(requiredAmount, decimals) : 'unknown amount'
|
|
464
|
+
const tokenDisplay = currency?.length === 3 ? currency.toUpperCase() : ''
|
|
465
|
+
const paymentDesc = `${amountDisplay}${tokenDisplay ? ` ${tokenDisplay}` : ''} via Stripe${isStripeTestKey ? ' (testmode)' : ''}`
|
|
466
|
+
|
|
467
|
+
if (isStripeTestKey) {
|
|
468
|
+
if (!options.silent) console.log(pc.dim(` Attempting: ${paymentDesc}`))
|
|
469
|
+
} else if (!options.yes && !ctx.isInteractive) {
|
|
470
|
+
results.push(skip(tag, 'non-interactive mode, use --yes to approve'))
|
|
471
|
+
return
|
|
472
|
+
} else if (!options.yes) {
|
|
473
|
+
const ok = await confirm(` ${pc.yellow('Pay')} ${paymentDesc}. Continue?`, false)
|
|
474
|
+
if (!ok) {
|
|
475
|
+
results.push(skip(tag, 'declined'))
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
if (!options.silent) console.log(pc.dim(` Auto-approved: ${paymentDesc}`))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Resolve plugin
|
|
483
|
+
const resolved = resolvePlugin(challenge, loaded?.config)
|
|
484
|
+
const plugin = resolved.plugin
|
|
485
|
+
if (!plugin) {
|
|
486
|
+
results.push(skip(tag, 'no Stripe plugin available'))
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let methods: AnyClient[]
|
|
491
|
+
let createCredentialFn: ((response: Response) => Promise<string>) | undefined
|
|
492
|
+
try {
|
|
493
|
+
const methodOpts: Record<string, string> = { paymentMethod: 'pm_card_visa' }
|
|
494
|
+
if (stripeKey) methodOpts.secretKey = stripeKey
|
|
495
|
+
const pluginResult = await plugin.setup({
|
|
496
|
+
challenge,
|
|
497
|
+
options: {},
|
|
498
|
+
methodOpts,
|
|
499
|
+
})
|
|
500
|
+
methods = pluginResult.methods
|
|
501
|
+
createCredentialFn = pluginResult.createCredential
|
|
502
|
+
} catch (error) {
|
|
503
|
+
results.push(skip(tag, (error as Error).message))
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results)
|
|
508
|
+
if (!credential) return
|
|
509
|
+
|
|
510
|
+
plugin.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders })
|
|
373
511
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
512
|
+
// Stripe testmode: detect livemode rejection gracefully
|
|
513
|
+
if (isStripeTestKey) {
|
|
514
|
+
const resp = await fetchWithTimeout(
|
|
515
|
+
url,
|
|
516
|
+
{
|
|
517
|
+
method: endpoint.method,
|
|
518
|
+
headers: { ...fetchHeaders, [Constants.Headers.authorization]: credential },
|
|
519
|
+
body: fetchBody ?? null,
|
|
520
|
+
},
|
|
521
|
+
30_000,
|
|
522
|
+
)
|
|
523
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
524
|
+
results.push(check(`${tag}: successful`, `HTTP ${resp.status}`))
|
|
525
|
+
} else {
|
|
526
|
+
results.push(
|
|
527
|
+
skip(
|
|
528
|
+
`${tag}: server is in livemode`,
|
|
529
|
+
undefined,
|
|
530
|
+
'Run your server with a Stripe test key to automatically validate Stripe payments in testmode.',
|
|
531
|
+
),
|
|
386
532
|
)
|
|
387
|
-
} finally {
|
|
388
|
-
flush()
|
|
389
533
|
}
|
|
534
|
+
return
|
|
390
535
|
}
|
|
391
536
|
|
|
392
|
-
|
|
537
|
+
await sendAndValidateResponse(
|
|
538
|
+
results,
|
|
539
|
+
url,
|
|
540
|
+
endpoint,
|
|
541
|
+
credential,
|
|
542
|
+
fetchHeaders,
|
|
543
|
+
fetchBody,
|
|
544
|
+
verbose,
|
|
545
|
+
undefined,
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function createAndSend(
|
|
550
|
+
challenge: Challenge.Challenge,
|
|
551
|
+
methods: AnyClient[],
|
|
552
|
+
createCredentialFn: ((response: Response) => Promise<string>) | undefined,
|
|
553
|
+
tag: string,
|
|
554
|
+
results: CheckResult[],
|
|
555
|
+
): Promise<string | undefined> {
|
|
556
|
+
const fakeResponse = new Response(null, {
|
|
557
|
+
status: 402,
|
|
558
|
+
headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
|
|
559
|
+
})
|
|
560
|
+
try {
|
|
561
|
+
let credential: string
|
|
562
|
+
if (createCredentialFn) {
|
|
563
|
+
credential = await createCredentialFn(fakeResponse)
|
|
564
|
+
} else {
|
|
565
|
+
const mppx = Mppx.create({ methods, polyfill: false })
|
|
566
|
+
credential = await mppx.createCredential(fakeResponse)
|
|
567
|
+
}
|
|
568
|
+
results.push(check(`${tag}: submitted`))
|
|
569
|
+
return credential
|
|
570
|
+
} catch (error) {
|
|
571
|
+
const msg = (error as Error).message
|
|
572
|
+
if (msg.toLowerCase().includes('insufficient')) {
|
|
573
|
+
results.push(skip(tag, `insufficient balance: ${msg}`))
|
|
574
|
+
} else {
|
|
575
|
+
results.push(fail(tag, msg))
|
|
576
|
+
}
|
|
577
|
+
return undefined
|
|
578
|
+
}
|
|
393
579
|
}
|
|
394
580
|
|
|
395
581
|
async function sendAndValidateResponse(
|