mppx 0.8.6 → 0.8.8
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 +19 -0
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/evm.d.ts +2 -0
- package/dist/cli/plugins/evm.d.ts.map +1 -0
- package/dist/cli/plugins/evm.js +51 -0
- package/dist/cli/plugins/evm.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.d.ts.map +1 -1
- package/dist/cli/plugins/index.js +1 -0
- package/dist/cli/plugins/index.js.map +1 -1
- 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 +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +191 -84
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +99 -35
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +14 -1
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +0 -13
- 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 +102 -153
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +4 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +312 -106
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/evm/Chains.d.ts +4 -0
- package/dist/evm/Chains.d.ts.map +1 -1
- package/dist/evm/Chains.js +4 -0
- package/dist/evm/Chains.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +7 -1
- package/dist/internal/AcceptPayment.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.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/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +2 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/session/server/ChannelStore.js +4 -4
- package/dist/tempo/session/server/ChannelStore.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +21 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +1 -0
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/validation/core.d.ts +75 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +269 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/x402/Assets.d.ts +9 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +38 -0
- package/dist/x402/Assets.js.map +1 -1
- package/package.json +9 -4
- package/src/cli/internal.ts +2 -2
- package/src/cli/plugins/evm.ts +55 -0
- package/src/cli/plugins/index.ts +1 -0
- package/src/cli/plugins/stripe.ts +3 -2
- package/src/cli/validate/challenge.ts +345 -127
- package/src/cli/validate/discovery.test.ts +307 -0
- package/src/cli/validate/discovery.ts +104 -38
- package/src/cli/validate/helpers.ts +15 -13
- package/src/cli/validate/index.ts +121 -242
- package/src/cli/validate/payment.ts +413 -144
- package/src/cli/validate.test.ts +237 -3
- package/src/client/internal/Fetch.test.ts +142 -2
- package/src/evm/Chains.ts +6 -0
- package/src/evm/PublicInterface.test-d.ts +7 -0
- package/src/internal/AcceptPayment.test.ts +45 -0
- package/src/internal/AcceptPayment.ts +11 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +9 -0
- package/src/tempo/server/internal/request-body.ts +2 -0
- package/src/tempo/session/server/ChannelStore.test.ts +17 -0
- package/src/tempo/session/server/ChannelStore.ts +4 -4
- package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
- package/src/tempo/session/server/CredentialVerification.ts +30 -0
- package/src/tempo/session/server/Session.integration.test.ts +5 -0
- package/src/tempo/session/server/Session.test.ts +119 -53
- package/src/tempo/session/server/Session.ts +1 -0
- package/src/validation/core.ts +374 -0
- package/src/validation/index.ts +11 -0
- package/src/x402/Assets.test.ts +42 -0
- package/src/x402/Assets.ts +40 -0
- package/src/x402/Exact.localnet.test.ts +475 -0
|
@@ -1,34 +1,49 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as childProcess from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { type Address, type Chain, createClient, erc20Abi, http } from 'viem'
|
|
2
4
|
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
|
|
3
|
-
import { waitForTransactionReceipt } from 'viem/actions'
|
|
5
|
+
import { readContract, waitForTransactionReceipt } from 'viem/actions'
|
|
6
|
+
import * as viemChains from 'viem/chains'
|
|
4
7
|
import { Actions } from 'viem/tempo'
|
|
5
8
|
import { tempoModerato, tempo as tempoMainnetChain } from 'viem/tempo/chains'
|
|
6
9
|
|
|
7
10
|
import * as Challenge from '../../Challenge.js'
|
|
8
11
|
import * as Mppx from '../../client/Mppx.js'
|
|
9
12
|
import * as Constants from '../../Constants.js'
|
|
13
|
+
import type { AnyClient } from '../../Method.js'
|
|
10
14
|
import * as Receipt from '../../Receipt.js'
|
|
11
15
|
import { tempo as tempoMethods } from '../../tempo/client/index.js'
|
|
12
16
|
import { chainId as tempoChainIds } from '../../tempo/internal/defaults.js'
|
|
13
17
|
import { resolveAccount, resolveAccountName } from '../account.js'
|
|
14
|
-
import {
|
|
18
|
+
import type { Config } from '../config.js'
|
|
19
|
+
import { loadConfig, resolvePlugin } from '../internal.js'
|
|
15
20
|
import { fetchTokenInfo, confirm, pc } from '../utils.js'
|
|
21
|
+
import { buildUrl } from './discovery.js'
|
|
16
22
|
import type { CheckResult, EndpointSpec } from './helpers.js'
|
|
17
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
check,
|
|
25
|
+
fail,
|
|
26
|
+
fetchWithTimeout,
|
|
27
|
+
formatBytes,
|
|
28
|
+
isValidIntegerAmount,
|
|
29
|
+
skip,
|
|
30
|
+
warn,
|
|
31
|
+
} from './helpers.js'
|
|
18
32
|
|
|
19
33
|
async function provisionAndPayTestnet(
|
|
20
34
|
challenge: Challenge.Challenge,
|
|
21
35
|
verbose: boolean,
|
|
22
|
-
|
|
36
|
+
silent?: boolean,
|
|
37
|
+
): Promise<{ methods: AnyClient[] } | undefined> {
|
|
23
38
|
try {
|
|
24
|
-
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...'))
|
|
25
40
|
const key = generatePrivateKey()
|
|
26
41
|
const account = privateKeyToAccount(key)
|
|
27
42
|
|
|
28
43
|
const client = createClient({ chain: tempoModerato, transport: http() })
|
|
29
44
|
const hashes = await Actions.faucet.fund(client, { account })
|
|
30
45
|
await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })))
|
|
31
|
-
console.log(pc.dim(` Using wallet: ${account.address}`))
|
|
46
|
+
if (!silent) console.log(pc.dim(` Using wallet: ${account.address}`))
|
|
32
47
|
|
|
33
48
|
const methods = [...tempoMethods({ account })]
|
|
34
49
|
return { methods }
|
|
@@ -54,17 +69,77 @@ async function resolveWalletAddress(): Promise<string | undefined> {
|
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
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
|
+
|
|
86
|
+
async function fetchEvmTokenInfo(
|
|
87
|
+
chain: Chain,
|
|
88
|
+
token: Address,
|
|
89
|
+
account: Address,
|
|
90
|
+
): Promise<{ balance: bigint; symbol: string | undefined }> {
|
|
91
|
+
const client = createClient({ chain, transport: http() })
|
|
92
|
+
const [balance, symbol] = await Promise.all([
|
|
93
|
+
readContract(client, {
|
|
94
|
+
address: token,
|
|
95
|
+
abi: erc20Abi,
|
|
96
|
+
functionName: 'balanceOf',
|
|
97
|
+
args: [account],
|
|
98
|
+
}),
|
|
99
|
+
readContract(client, { address: token, abi: erc20Abi, functionName: 'symbol' }).catch(
|
|
100
|
+
() => undefined,
|
|
101
|
+
),
|
|
102
|
+
])
|
|
103
|
+
return { balance, symbol: symbol ?? undefined }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveEvmChain(chainId: number): Chain | undefined {
|
|
107
|
+
const all = Object.values(viemChains) as Chain[]
|
|
108
|
+
return all.find((c) => c.id === chainId)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatAmount(amount: bigint, decimals: number): string {
|
|
112
|
+
return `$${(Number(amount) / 10 ** decimals).toFixed(2)}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function methodSetupHint(challenge: Challenge.Challenge): string {
|
|
116
|
+
switch (challenge.method) {
|
|
117
|
+
case Constants.Methods.evm:
|
|
118
|
+
return 'no EVM payment plugin yet'
|
|
119
|
+
case 'solana':
|
|
120
|
+
return 'no Solana payment plugin yet'
|
|
121
|
+
default:
|
|
122
|
+
return `no plugin for ${challenge.method}/${challenge.intent}`
|
|
123
|
+
}
|
|
124
|
+
}
|
|
57
125
|
export async function validatePaymentFlow(
|
|
58
126
|
baseUrl: string,
|
|
59
127
|
endpoint: EndpointSpec,
|
|
60
128
|
verbose: boolean,
|
|
61
|
-
options
|
|
129
|
+
options: {
|
|
130
|
+
body?: string | undefined
|
|
131
|
+
query?: string[] | undefined
|
|
132
|
+
yes?: boolean | undefined
|
|
133
|
+
silent?: boolean | undefined
|
|
134
|
+
interactive?: boolean | undefined
|
|
135
|
+
onResults?: (results: CheckResult[]) => void
|
|
136
|
+
},
|
|
62
137
|
): Promise<CheckResult[]> {
|
|
63
138
|
const results: CheckResult[] = []
|
|
64
|
-
const url = buildUrl(baseUrl, endpoint, options
|
|
139
|
+
const url = buildUrl(baseUrl, endpoint, options.query)
|
|
65
140
|
const fetchHeaders: Record<string, string> = {}
|
|
66
141
|
let fetchBody: string | undefined
|
|
67
|
-
if (options
|
|
142
|
+
if (options.body) {
|
|
68
143
|
fetchBody = options.body
|
|
69
144
|
fetchHeaders['content-type'] = 'application/json'
|
|
70
145
|
}
|
|
@@ -87,33 +162,40 @@ export async function validatePaymentFlow(
|
|
|
87
162
|
return results
|
|
88
163
|
}
|
|
89
164
|
|
|
90
|
-
// Parse
|
|
91
|
-
let
|
|
165
|
+
// Parse all challenges
|
|
166
|
+
let challenges: Challenge.Challenge[]
|
|
92
167
|
try {
|
|
93
|
-
|
|
168
|
+
challenges = Challenge.fromResponseList(challengeResponse)
|
|
94
169
|
} catch (error) {
|
|
95
170
|
results.push(fail('Payment: parse challenge', (error as Error).message))
|
|
96
171
|
return results
|
|
97
172
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const isTestnet =
|
|
103
|
-
typeof methodDetails?.chainId === 'number' && methodDetails.chainId !== tempoChainIds.mainnet
|
|
173
|
+
if (challenges.length === 0) {
|
|
174
|
+
results.push(fail('Payment: parse challenge', 'No Payment challenges in response'))
|
|
175
|
+
return results
|
|
176
|
+
}
|
|
104
177
|
|
|
105
178
|
// Testnet Tempo: always use ephemeral wallet (zero-setup, free money)
|
|
106
|
-
|
|
107
|
-
|
|
179
|
+
const tempoTestnetChallenge = challenges.find((ch) => {
|
|
180
|
+
if (ch.method !== Constants.Methods.tempo) return false
|
|
181
|
+
const req = ch.request as Record<string, unknown>
|
|
182
|
+
const md = req.methodDetails as Record<string, unknown> | undefined
|
|
183
|
+
return typeof md?.chainId === 'number' && md.chainId !== tempoChainIds.mainnet
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
if (tempoTestnetChallenge) {
|
|
187
|
+
const provisioned = await provisionAndPayTestnet(tempoTestnetChallenge, verbose, options.silent)
|
|
108
188
|
if (provisioned) {
|
|
109
189
|
const fakeResp = new Response(null, {
|
|
110
190
|
status: 402,
|
|
111
|
-
headers: {
|
|
191
|
+
headers: {
|
|
192
|
+
[Constants.Headers.wwwAuthenticate]: Challenge.serialize(tempoTestnetChallenge),
|
|
193
|
+
},
|
|
112
194
|
})
|
|
113
195
|
try {
|
|
114
196
|
const mppx = Mppx.create({ methods: provisioned.methods, polyfill: false })
|
|
115
197
|
const cred = await mppx.createCredential(fakeResp)
|
|
116
|
-
results.push(check('Payment:
|
|
198
|
+
results.push(check('Payment: submitted', 'ephemeral testnet wallet'))
|
|
117
199
|
return await sendAndValidateResponse(
|
|
118
200
|
results,
|
|
119
201
|
url,
|
|
@@ -122,7 +204,7 @@ export async function validatePaymentFlow(
|
|
|
122
204
|
fetchHeaders,
|
|
123
205
|
fetchBody,
|
|
124
206
|
verbose,
|
|
125
|
-
|
|
207
|
+
tempoModerato,
|
|
126
208
|
)
|
|
127
209
|
} catch (error) {
|
|
128
210
|
results.push(fail('Payment: create credential', (error as Error).message))
|
|
@@ -136,99 +218,191 @@ export async function validatePaymentFlow(
|
|
|
136
218
|
}
|
|
137
219
|
}
|
|
138
220
|
|
|
139
|
-
//
|
|
221
|
+
// Try each challenge method (skip testnet challenge already handled above)
|
|
140
222
|
const loaded = await loadConfig().catch(() => undefined)
|
|
141
|
-
const
|
|
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_')
|
|
226
|
+
const supportedPaymentMethods: Set<string> = new Set([
|
|
227
|
+
Constants.Methods.tempo,
|
|
228
|
+
Constants.Methods.evm,
|
|
229
|
+
Constants.Methods.stripe,
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
for (const challenge of challenges) {
|
|
233
|
+
if (challenge === tempoTestnetChallenge) continue
|
|
234
|
+
if (!supportedPaymentMethods.has(challenge.method)) continue
|
|
235
|
+
if (!options.silent) console.log('')
|
|
236
|
+
|
|
237
|
+
const flushStart = results.length
|
|
238
|
+
const flush = () => {
|
|
239
|
+
if (options.onResults && results.length > flushStart)
|
|
240
|
+
options.onResults(results.slice(flushStart))
|
|
241
|
+
}
|
|
242
|
+
const tag = `Payment [${challenge.method}]`
|
|
142
243
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
244
|
+
try {
|
|
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
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
flush()
|
|
278
|
+
}
|
|
152
279
|
}
|
|
153
280
|
|
|
154
|
-
|
|
281
|
+
return results
|
|
282
|
+
}
|
|
155
283
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
159
300
|
}
|
|
301
|
+
}
|
|
160
302
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
164
316
|
const currency = request.currency as string | undefined
|
|
165
317
|
|
|
166
|
-
//
|
|
318
|
+
// Resolve wallet
|
|
167
319
|
let walletAddress: string | undefined
|
|
168
|
-
|
|
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
|
+
}
|
|
327
|
+
|
|
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
|
+
}
|
|
335
|
+
|
|
336
|
+
let tokenSymbol: string | undefined
|
|
337
|
+
if (requiredAmount && currency && paymentChain) {
|
|
169
338
|
try {
|
|
170
|
-
|
|
171
|
-
if (
|
|
339
|
+
let balance: bigint
|
|
340
|
+
if (challenge.method === Constants.Methods.tempo) {
|
|
172
341
|
const client = createClient({ chain: tempoMainnetChain, transport: http() })
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
342
|
+
const info = await fetchTokenInfo(client, currency as Address, walletAddress as Address)
|
|
343
|
+
balance = info.balance
|
|
344
|
+
tokenSymbol = info.symbol
|
|
345
|
+
} else {
|
|
346
|
+
const info = await fetchEvmTokenInfo(
|
|
347
|
+
paymentChain,
|
|
348
|
+
currency as Address,
|
|
349
|
+
walletAddress as Address,
|
|
177
350
|
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
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}.`,
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
return
|
|
194
366
|
}
|
|
195
367
|
} catch (e) {
|
|
196
368
|
if (verbose) console.log(pc.dim(` Balance check skipped: ${(e as Error).message}`))
|
|
197
369
|
}
|
|
198
370
|
}
|
|
199
371
|
|
|
200
|
-
// Prompt
|
|
201
|
-
const amountDisplay = requiredAmount
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (!options
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const ok = await confirm(
|
|
215
|
-
` ${pc.yellow('Mainnet payment:')} Will transfer ${amountDisplay} to ${String(request.recipient).slice(0, 10)}... Continue?`,
|
|
216
|
-
false,
|
|
217
|
-
)
|
|
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)
|
|
218
387
|
if (!ok) {
|
|
219
|
-
results.push(skip(
|
|
220
|
-
return
|
|
388
|
+
results.push(skip(tag, 'declined'))
|
|
389
|
+
return
|
|
221
390
|
}
|
|
222
391
|
} else {
|
|
223
|
-
console.log(
|
|
224
|
-
pc.dim(` Auto-approved: ${amountDisplay} to ${String(request.recipient).slice(0, 10)}...`),
|
|
225
|
-
)
|
|
392
|
+
if (!options.silent) console.log(pc.dim(` Auto-approved: ${paymentDesc}`))
|
|
226
393
|
}
|
|
227
394
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
}
|
|
231
403
|
|
|
404
|
+
let methods: AnyClient[]
|
|
405
|
+
let createCredentialFn: ((response: Response) => Promise<string>) | undefined
|
|
232
406
|
if (plugin) {
|
|
233
407
|
try {
|
|
234
408
|
const pluginResult = await plugin.setup({
|
|
@@ -239,74 +413,169 @@ export async function validatePaymentFlow(
|
|
|
239
413
|
methods = pluginResult.methods
|
|
240
414
|
createCredentialFn = pluginResult.createCredential
|
|
241
415
|
} catch (error) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
results.push(skip('Payment: no wallet configured', msg))
|
|
245
|
-
} else {
|
|
246
|
-
results.push(fail('Payment: wallet setup', msg))
|
|
247
|
-
}
|
|
248
|
-
return results
|
|
416
|
+
results.push(skip(tag, (error as Error).message))
|
|
417
|
+
return
|
|
249
418
|
}
|
|
250
419
|
} else {
|
|
251
420
|
methods = [directMethod!]
|
|
252
421
|
}
|
|
253
422
|
|
|
254
|
-
|
|
255
|
-
|
|
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 })
|
|
511
|
+
|
|
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
|
+
),
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
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> {
|
|
256
556
|
const fakeResponse = new Response(null, {
|
|
257
557
|
status: 402,
|
|
258
558
|
headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
|
|
259
559
|
})
|
|
260
560
|
try {
|
|
561
|
+
let credential: string
|
|
261
562
|
if (createCredentialFn) {
|
|
262
563
|
credential = await createCredentialFn(fakeResponse)
|
|
263
564
|
} else {
|
|
264
565
|
const mppx = Mppx.create({ methods, polyfill: false })
|
|
265
566
|
credential = await mppx.createCredential(fakeResponse)
|
|
266
567
|
}
|
|
568
|
+
results.push(check(`${tag}: submitted`))
|
|
569
|
+
return credential
|
|
267
570
|
} catch (error) {
|
|
268
571
|
const msg = (error as Error).message
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
572
|
+
if (msg.toLowerCase().includes('insufficient')) {
|
|
573
|
+
results.push(skip(tag, `insufficient balance: ${msg}`))
|
|
574
|
+
} else {
|
|
575
|
+
results.push(fail(tag, msg))
|
|
291
576
|
}
|
|
292
|
-
|
|
293
|
-
return results
|
|
577
|
+
return undefined
|
|
294
578
|
}
|
|
295
|
-
|
|
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
|
-
)
|
|
310
579
|
}
|
|
311
580
|
|
|
312
581
|
async function sendAndValidateResponse(
|
|
@@ -317,7 +586,7 @@ async function sendAndValidateResponse(
|
|
|
317
586
|
baseHeaders: Record<string, string>,
|
|
318
587
|
fetchBody: string | undefined,
|
|
319
588
|
verbose: boolean,
|
|
320
|
-
|
|
589
|
+
explorerChain?: Chain | undefined,
|
|
321
590
|
): Promise<CheckResult[]> {
|
|
322
591
|
let paymentResponse: Response
|
|
323
592
|
try {
|
|
@@ -475,10 +744,10 @@ async function sendAndValidateResponse(
|
|
|
475
744
|
try {
|
|
476
745
|
const receipt = Receipt.deserialize(receiptHeader)
|
|
477
746
|
if (receipt.reference && /^0x[0-9a-fA-F]{64}$/.test(receipt.reference)) {
|
|
478
|
-
const
|
|
479
|
-
const explorerUrl = chain.blockExplorers?.default?.url
|
|
747
|
+
const explorerUrl = explorerChain?.blockExplorers?.default?.url
|
|
480
748
|
if (explorerUrl) {
|
|
481
|
-
|
|
749
|
+
const path = explorerUrl.includes('tempo') ? 'receipt' : 'tx'
|
|
750
|
+
results.push(check('On-chain transaction', `${explorerUrl}/${path}/${receipt.reference}`))
|
|
482
751
|
}
|
|
483
752
|
}
|
|
484
753
|
} catch {}
|