mppx 0.8.5 → 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.
- package/CHANGELOG.md +25 -0
- package/dist/Receipt.d.ts +20 -2
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +19 -12
- package/dist/Receipt.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +6 -4
- package/dist/cli/cli.js.map +1 -1
- 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/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +4 -2
- package/dist/cli/plugins/tempo.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 +184 -78
- 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 +98 -151
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +215 -144
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +6 -1
- package/dist/client/internal/Fetch.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/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +6 -2
- package/dist/tempo/client/Charge.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/precompile/Chain.d.ts +26 -2
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +38 -5
- package/dist/tempo/session/precompile/Chain.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 +79 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +256 -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/Receipt.test.ts +53 -0
- package/src/Receipt.ts +26 -13
- package/src/cli/cli.test.ts +41 -5
- package/src/cli/cli.ts +7 -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/tempo.ts +6 -2
- package/src/cli/validate/challenge.ts +334 -121
- 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 +117 -240
- package/src/cli/validate/payment.ts +261 -178
- package/src/cli/validate.test.ts +143 -3
- package/src/client/internal/Fetch.test.ts +186 -2
- package/src/client/internal/Fetch.ts +5 -1
- 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/client/Charge.test.ts +61 -0
- package/src/tempo/client/Charge.ts +6 -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/precompile/Chain.test.ts +121 -2
- package/src/tempo/session/precompile/Chain.ts +56 -6
- 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 +348 -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,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,
|
|
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 {
|
|
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?: {
|
|
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
|
|
91
|
-
let
|
|
141
|
+
// Parse all challenges
|
|
142
|
+
let challenges: Challenge.Challenge[]
|
|
92
143
|
try {
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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: {
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
197
|
+
// Try each challenge method (skip testnet challenge already handled above)
|
|
140
198
|
const loaded = await loadConfig().catch(() => undefined)
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
479
|
-
const explorerUrl = chain.blockExplorers?.default?.url
|
|
561
|
+
const explorerUrl = explorerChain?.blockExplorers?.default?.url
|
|
480
562
|
if (explorerUrl) {
|
|
481
|
-
|
|
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 {}
|