mppx 0.4.1 → 0.4.2

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 (58) hide show
  1. package/CHANGELOG.md +260 -0
  2. package/dist/bin.js +2 -2
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli/account.d.ts +53 -0
  5. package/dist/cli/account.d.ts.map +1 -0
  6. package/dist/cli/account.js +156 -0
  7. package/dist/cli/account.js.map +1 -0
  8. package/dist/{cli.d.ts → cli/cli.d.ts} +4 -3
  9. package/dist/cli/cli.d.ts.map +1 -0
  10. package/dist/cli/cli.js +852 -0
  11. package/dist/cli/cli.js.map +1 -0
  12. package/dist/cli/config.d.ts +39 -0
  13. package/dist/cli/config.d.ts.map +1 -0
  14. package/dist/cli/config.js +30 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/internal.d.ts +16 -0
  17. package/dist/cli/internal.d.ts.map +1 -0
  18. package/dist/cli/internal.js +58 -0
  19. package/dist/cli/internal.js.map +1 -0
  20. package/dist/cli/plugins/index.d.ts +4 -0
  21. package/dist/cli/plugins/index.d.ts.map +1 -0
  22. package/dist/cli/plugins/index.js +4 -0
  23. package/dist/cli/plugins/index.js.map +1 -0
  24. package/dist/cli/plugins/plugin.d.ts +68 -0
  25. package/dist/cli/plugins/plugin.d.ts.map +1 -0
  26. package/dist/cli/plugins/plugin.js +4 -0
  27. package/dist/cli/plugins/plugin.js.map +1 -0
  28. package/dist/cli/plugins/stripe.d.ts +2 -0
  29. package/dist/cli/plugins/stripe.d.ts.map +1 -0
  30. package/dist/cli/plugins/stripe.js +118 -0
  31. package/dist/cli/plugins/stripe.js.map +1 -0
  32. package/dist/cli/plugins/tempo.d.ts +11 -0
  33. package/dist/cli/plugins/tempo.d.ts.map +1 -0
  34. package/dist/cli/plugins/tempo.js +706 -0
  35. package/dist/cli/plugins/tempo.js.map +1 -0
  36. package/dist/cli/utils.d.ts +93 -0
  37. package/dist/cli/utils.d.ts.map +1 -0
  38. package/dist/cli/utils.js +274 -0
  39. package/dist/cli/utils.js.map +1 -0
  40. package/dist/tempo/client/Methods.d.ts +1 -1
  41. package/dist/tempo/client/Session.d.ts +2 -2
  42. package/package.json +12 -1
  43. package/src/bin.ts +2 -2
  44. package/src/cli/account.ts +157 -0
  45. package/src/{cli.test.ts → cli/cli.test.ts} +107 -51
  46. package/src/cli/cli.ts +907 -0
  47. package/src/cli/config.test.ts +82 -0
  48. package/src/cli/config.ts +44 -0
  49. package/src/cli/internal.ts +72 -0
  50. package/src/cli/plugins/index.ts +3 -0
  51. package/src/cli/plugins/plugin.ts +73 -0
  52. package/src/cli/plugins/stripe.ts +143 -0
  53. package/src/cli/plugins/tempo.ts +842 -0
  54. package/src/cli/utils.ts +336 -0
  55. package/dist/cli.d.ts.map +0 -1
  56. package/dist/cli.js +0 -1992
  57. package/dist/cli.js.map +0 -1
  58. package/src/cli.ts +0 -2178
package/dist/cli.js DELETED
@@ -1,1992 +0,0 @@
1
- import * as child from 'node:child_process';
2
- import * as fs from 'node:fs';
3
- import { createRequire } from 'node:module';
4
- import * as os from 'node:os';
5
- import * as path from 'node:path';
6
- import * as readline from 'node:readline';
7
- import { Cli, z } from 'incur';
8
- import { Base64 } from 'ox';
9
- import { createClient, http } from 'viem';
10
- import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
11
- import { tempo as tempoMainnet, tempoModerato } from 'viem/chains';
12
- import * as Challenge from './Challenge.js';
13
- import * as Credential from './Credential.js';
14
- import * as Mppx from './client/Mppx.js';
15
- import { stripe } from './stripe/client/index.js';
16
- import { tempo } from './tempo/client/index.js';
17
- import { signVoucher } from './tempo/session/Voucher.js';
18
- function readStdin() {
19
- return new Promise((resolve, reject) => {
20
- let data = '';
21
- process.stdin.setEncoding('utf-8');
22
- process.stdin.on('data', (chunk) => {
23
- data += chunk;
24
- });
25
- process.stdin.on('end', () => resolve(data.trim()));
26
- process.stdin.on('error', reject);
27
- });
28
- }
29
- const require = createRequire(import.meta.url);
30
- const { name, version } = require('../package.json');
31
- const cli = Cli.create('mppx', {
32
- version,
33
- description: 'Make HTTP requests with automatic payment',
34
- usage: [{ suffix: '<url> [options]' }],
35
- args: z.object({
36
- url: z.string().describe('URL to make payment request to'),
37
- }),
38
- options: z.object({
39
- account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
40
- confirm: z.boolean().optional().describe('Show confirmation prompts'),
41
- data: z.string().optional().describe('Send request body (implies POST unless -X is set)'),
42
- fail: z.boolean().optional().describe('Fail silently on HTTP errors (exit 22)'),
43
- header: z.array(z.string()).optional().describe('Add header (repeatable)'),
44
- include: z.boolean().optional().describe('Include response headers in output'),
45
- insecure: z
46
- .boolean()
47
- .optional()
48
- .describe('Skip TLS certificate verification (true for localhost/.local)'),
49
- jsonBody: z
50
- .string()
51
- .optional()
52
- .describe('Send JSON body (sets Content-Type and Accept, implies POST)'),
53
- location: z.boolean().optional().describe('Follow redirects'),
54
- method: z.string().optional().describe('HTTP method'),
55
- methodOpt: z
56
- .array(z.string())
57
- .optional()
58
- .describe('Method-specific option (key=value, repeatable)'),
59
- rpcUrl: z
60
- .string()
61
- .optional()
62
- .describe('RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)'),
63
- silent: z.boolean().optional().describe('Silent mode (suppress progress and info)'),
64
- userAgent: z.string().optional().describe('Set User-Agent header'),
65
- verbose: z
66
- .number()
67
- .default(0)
68
- .meta({ count: true })
69
- .describe('Verbosity (-v details, -vv headers)'),
70
- }),
71
- alias: {
72
- account: 'a',
73
- data: 'd',
74
- fail: 'f',
75
- header: 'H',
76
- include: 'i',
77
- insecure: 'k',
78
- jsonBody: 'J',
79
- location: 'L',
80
- method: 'X',
81
- methodOpt: 'M',
82
- rpcUrl: 'r',
83
- silent: 's',
84
- userAgent: 'A',
85
- verbose: 'v',
86
- },
87
- examples: [
88
- { args: { url: 'example.com/content' }, description: 'Make a payment request' },
89
- {
90
- args: { url: 'example.com/api' },
91
- options: { jsonBody: '{"key":"value"}' },
92
- description: 'POST JSON with payment',
93
- },
94
- ],
95
- async run({ args, options, error }) {
96
- const methodOpts = parseMethodOpts(options.methodOpt);
97
- const silent = options.silent ?? false;
98
- const info = silent ? (_msg) => { } : (msg) => process.stderr.write(msg);
99
- let confirmEnabled = options.confirm ?? false;
100
- if (silent)
101
- confirmEnabled = false;
102
- const accountName = resolveAccountName(options.account);
103
- const headers = {};
104
- if (options.header) {
105
- for (const header of options.header) {
106
- const index = header.indexOf(':');
107
- if (index === -1) {
108
- return error({
109
- code: 'INVALID_HEADER',
110
- message: `Invalid header format: ${header}`,
111
- exitCode: 2,
112
- });
113
- }
114
- headers[header.slice(0, index).trim()] = header.slice(index + 1).trim();
115
- }
116
- }
117
- headers['User-Agent'] = options.userAgent ?? `${name}/${version}`;
118
- const rawUrl = args.url;
119
- const url = (() => {
120
- const hasProtocol = /^https?:\/\//.test(rawUrl);
121
- const isLocal = /^(localhost|.*\.localhost|127\.0\.0\.1|\[::1\])(:\d+)?/.test(rawUrl);
122
- return hasProtocol ? rawUrl : `${isLocal ? 'http' : 'https'}://${rawUrl}`;
123
- })();
124
- const { hostname } = new URL(url);
125
- if (options.insecure ||
126
- hostname === 'localhost' ||
127
- hostname.endsWith('.localhost') ||
128
- hostname.endsWith('.local')) {
129
- process.removeAllListeners('warning');
130
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
131
- }
132
- // Node.js doesn't resolve *.localhost subdomains to loopback (unlike
133
- // browsers per RFC 6761). Rewrite the URL to 127.0.0.1 and set the
134
- // Host header so reverse proxies can route correctly.
135
- const isSubLocalhost = hostname.endsWith('.localhost') && hostname !== 'localhost';
136
- const fetchUrl = isSubLocalhost ? url.replace(hostname, '127.0.0.1') : url;
137
- if (isSubLocalhost) {
138
- const { host } = new URL(url);
139
- headers.Host = host;
140
- }
141
- try {
142
- const fetchInit = { redirect: options.location ? 'follow' : 'manual' };
143
- if (options.jsonBody) {
144
- fetchInit.body = options.jsonBody;
145
- headers['Content-Type'] ??= 'application/json';
146
- headers.Accept ??= 'application/json';
147
- }
148
- else if (options.data) {
149
- fetchInit.body = options.data;
150
- }
151
- if (options.method)
152
- fetchInit.method = options.method.toUpperCase();
153
- else if (fetchInit.body)
154
- fetchInit.method = 'POST';
155
- if (Object.keys(headers).length > 0)
156
- fetchInit.headers = headers;
157
- const verbose = options.verbose;
158
- const printRequestHeaders = (reqUrl, init) => {
159
- if (verbose < 2)
160
- return;
161
- const { pathname, host } = new URL(reqUrl);
162
- const method = (init.method ?? 'GET').toUpperCase();
163
- info(`> ${method} ${pathname} HTTP/1.1\n`);
164
- info(`> Host: ${host}\n`);
165
- for (const [k, v] of Object.entries((init.headers ?? {})))
166
- info(`> ${k}: ${v}\n`);
167
- info('>\n');
168
- };
169
- const printResponseHeaders = (res) => {
170
- if (!options.include && verbose < 2)
171
- return;
172
- if (silent)
173
- return;
174
- const status = `HTTP/1.1 ${res.status} ${res.statusText}`;
175
- const out = verbose >= 2 ? process.stderr : process.stdout;
176
- const prefix = verbose >= 2 ? '< ' : '';
177
- out.write(`${prefix}${status}\n`);
178
- for (const [k, v] of res.headers)
179
- out.write(`${prefix}${k}: ${v}\n`);
180
- out.write(verbose >= 2 ? '<\n' : '\n');
181
- };
182
- printRequestHeaders(url, fetchInit);
183
- const challengeResponse = await globalThis.fetch(fetchUrl, fetchInit);
184
- if (challengeResponse.status !== 402) {
185
- if (options.fail && challengeResponse.status >= 400)
186
- return error({
187
- code: 'HTTP_ERROR',
188
- message: `HTTP error ${challengeResponse.status}`,
189
- exitCode: 22,
190
- });
191
- printResponseHeaders(challengeResponse);
192
- console.log((await challengeResponse.text()).replace(/\n+$/, ''));
193
- return;
194
- }
195
- const challenge = Challenge.fromResponse(challengeResponse);
196
- const challengeRequest = challenge.request;
197
- const currency = challengeRequest.currency;
198
- const shownKeys = new Set();
199
- let tokenSymbol = challenge.method === 'stripe' ? (currency?.toUpperCase() ?? '') : (currency ?? '');
200
- let tokenDecimals = challengeRequest.decimals ?? (challenge.method === 'stripe' ? 2 : 6);
201
- let explorerUrl;
202
- // Tempo-specific setup (private key, viem account/client, token info)
203
- let account;
204
- let client;
205
- let useTempoCliSign = false;
206
- if (challenge.method === 'tempo') {
207
- const privateKey = process.env.MPPX_PRIVATE_KEY?.trim() ||
208
- (isTempoAccount(accountName) ? undefined : await createKeychain(accountName).get());
209
- if (!privateKey && isTempoAccount(accountName) && hasTempoCliSync()) {
210
- useTempoCliSign = true;
211
- // Resolve wallet address from keys.toml for display/balance
212
- const tempoEntry = resolveTempoAccount(accountName);
213
- if (tempoEntry) {
214
- const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
215
- client = createClient({
216
- chain: await resolveChain({ ...options, rpcUrl }),
217
- transport: http(rpcUrl),
218
- });
219
- explorerUrl = client.chain?.blockExplorers?.default?.url;
220
- const tokenInfo = currency
221
- ? await fetchTokenInfo(client, currency, tempoEntry.wallet_address).catch(() => undefined)
222
- : undefined;
223
- tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
224
- tokenDecimals =
225
- tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
226
- }
227
- }
228
- else if (!privateKey) {
229
- // tempo CLI not available — try silent fallback
230
- const fallback = fallbackFromTempo();
231
- if (fallback) {
232
- const fallbackKey = await createKeychain(fallback).get();
233
- if (fallbackKey) {
234
- account = privateKeyToAccount(fallbackKey);
235
- }
236
- }
237
- if (!account) {
238
- if (options.account)
239
- return error({
240
- code: 'ACCOUNT_NOT_FOUND',
241
- message: `Account "${accountName}" not found.`,
242
- exitCode: 69,
243
- });
244
- else
245
- return error({
246
- code: 'ACCOUNT_NOT_FOUND',
247
- message: 'No account found.',
248
- exitCode: 69,
249
- });
250
- }
251
- }
252
- else {
253
- account = privateKeyToAccount(privateKey);
254
- }
255
- if (!useTempoCliSign && account) {
256
- const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
257
- client = createClient({
258
- chain: await resolveChain({ ...options, rpcUrl }),
259
- transport: http(rpcUrl),
260
- });
261
- explorerUrl = client.chain?.blockExplorers?.default?.url;
262
- const tokenInfo = currency
263
- ? await fetchTokenInfo(client, currency, account.address).catch(() => undefined)
264
- : undefined;
265
- tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
266
- tokenDecimals =
267
- tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
268
- }
269
- }
270
- {
271
- printResponseHeaders(challengeResponse);
272
- const request = challengeRequest;
273
- const balanceKeys = new Set(['amount', 'suggestedDeposit', 'minVoucherDelta']);
274
- const skipKeys = new Set(['decimals', 'currency', 'methodDetails']);
275
- const fmtRequestValue = (key, value) => {
276
- if (balanceKeys.has(key) && typeof value === 'string') {
277
- return `${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`;
278
- }
279
- if (key === 'chainId' && typeof value === 'number') {
280
- const name = chainName({ id: value, name: '' });
281
- return name ? `${value} ${pc.dim(`(${name})`)}` : String(value);
282
- }
283
- if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value))
284
- return explorerUrl ? pc.link(`${explorerUrl}/address/${value}`, value) : value;
285
- if (typeof value === 'string' && /^https?:\/\//.test(value))
286
- return pc.link(value, value);
287
- return String(value);
288
- };
289
- const decodeMemo = (hex) => {
290
- try {
291
- const stripped = hex.replace(/^0x0*/, '');
292
- if (!stripped)
293
- return undefined;
294
- const bytes = Uint8Array.from(stripped.match(/.{1,2}/g).map((b) => Number.parseInt(b, 16)));
295
- const decoded = new TextDecoder().decode(bytes);
296
- return /^[\x20-\x7e]+$/.test(decoded) ? decoded : undefined;
297
- }
298
- catch {
299
- return undefined;
300
- }
301
- };
302
- const skipChallengeKeys = new Set(['id', 'request']);
303
- const fmtChallengeValue = (key, value) => {
304
- if (key === 'realm' && typeof value === 'string') {
305
- try {
306
- const realmUrl = new URL(value.includes('://') ? value : `https://${value}`);
307
- return pc.link(realmUrl.href, value);
308
- }
309
- catch { }
310
- }
311
- return String(value);
312
- };
313
- const challengeRows = [];
314
- for (const [key, value] of Object.entries(challenge)) {
315
- if (skipChallengeKeys.has(key) || value === undefined)
316
- continue;
317
- challengeRows.push([key, fmtChallengeValue(key, value)]);
318
- }
319
- challengeRows.sort(([a], [b]) => a.localeCompare(b));
320
- const requestRows = [];
321
- for (const [key, value] of Object.entries(request)) {
322
- if (skipKeys.has(key) || value === undefined)
323
- continue;
324
- requestRows.push([key, fmtRequestValue(key, value)]);
325
- }
326
- requestRows.sort(([a], [b]) => a.localeCompare(b));
327
- const detailRows = [];
328
- const methodDetails = request.methodDetails;
329
- if (methodDetails) {
330
- for (const [key, value] of Object.entries(methodDetails)) {
331
- if (value === undefined)
332
- continue;
333
- if (key === 'memo' && typeof value === 'string') {
334
- const decoded = decodeMemo(value);
335
- detailRows.push([key, decoded ? `${decoded}\n${pc.dim(value)}` : value]);
336
- }
337
- else {
338
- detailRows.push([key, fmtRequestValue(key, value)]);
339
- }
340
- }
341
- detailRows.sort(([a], [b]) => a.localeCompare(b));
342
- }
343
- const sections = [
344
- ['Challenge', challengeRows],
345
- ['Request', requestRows],
346
- ...(detailRows.length ? [['Details', detailRows]] : []),
347
- ];
348
- for (const [, rows] of sections)
349
- for (const [key] of rows)
350
- shownKeys.add(key);
351
- const pad = Math.max(...sections.flatMap(([, rows]) => rows.map(([k]) => k.length)));
352
- const indent = ` ${''.padEnd(pad)} `;
353
- if (verbose >= 1 || confirmEnabled) {
354
- info(`${pc.bold(pc.yellow('Payment Required'))}\n`);
355
- for (const [title, rows] of sections) {
356
- info(`${pc.bold(title)}\n`);
357
- for (const [label, value] of rows) {
358
- const [first, ...rest] = value.split('\n');
359
- info(` ${pc.dim(label.padEnd(pad))} ${first}\n`);
360
- for (const line of rest)
361
- info(`${indent}${line}\n`);
362
- }
363
- }
364
- }
365
- if (confirmEnabled) {
366
- info('\n');
367
- const ok = await confirm(`Proceed with ${challenge.intent}?`, true);
368
- if (!ok) {
369
- info('Aborted.\n');
370
- return;
371
- }
372
- }
373
- }
374
- let credential;
375
- if (challenge.method === 'tempo' && useTempoCliSign) {
376
- const wwwAuth = challengeResponse.headers.get('www-authenticate');
377
- if (!wwwAuth) {
378
- return error({
379
- code: 'MISSING_CHALLENGE',
380
- message: 'No WWW-Authenticate header in 402 response.',
381
- exitCode: 2,
382
- });
383
- }
384
- credential = await tempoCliSign(wwwAuth);
385
- }
386
- else if (challenge.method === 'tempo') {
387
- if (!account || !client) {
388
- return error({
389
- code: 'ACCOUNT_NOT_FOUND',
390
- message: 'Tempo requires a configured account.',
391
- exitCode: 69,
392
- });
393
- }
394
- const tempoOpts = parseOptions(z.object({
395
- channel: z.optional(z.coerce.string()),
396
- deposit: z.optional(z.union([z.string(), z.number()])),
397
- }), methodOpts);
398
- const mppx = Mppx.create({
399
- methods: tempo({
400
- account,
401
- getClient: () => client,
402
- deposit: (() => {
403
- if (challenge.intent !== 'session')
404
- return undefined;
405
- const suggestedDeposit = challenge.request
406
- .suggestedDeposit;
407
- const cliDeposit = tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined;
408
- const resolved = suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain) ? '10' : undefined);
409
- if (!resolved) {
410
- return error({
411
- code: 'MISSING_DEPOSIT',
412
- message: 'Session payment requires a deposit. Use -M deposit=<amount> or connect to testnet.',
413
- exitCode: 2,
414
- });
415
- }
416
- return resolved;
417
- })(),
418
- }),
419
- polyfill: false,
420
- });
421
- credential = await mppx.createCredential(challengeResponse, (() => {
422
- if (!tempoOpts.channel)
423
- return undefined;
424
- const channelId = tempoOpts.channel;
425
- const saved = readChannelCumulative(channelId);
426
- return {
427
- channelId,
428
- ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
429
- };
430
- })());
431
- }
432
- else if (challenge.method === 'stripe') {
433
- const stripeOpts = parseOptions(z.object({
434
- paymentMethod: z.string(),
435
- }), methodOpts);
436
- const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY;
437
- if (!stripeSecretKey) {
438
- return error({
439
- code: 'MISSING_ENV',
440
- message: 'MPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
441
- exitCode: 2,
442
- });
443
- }
444
- if (!stripeSecretKey.startsWith('sk_test_')) {
445
- return error({
446
- code: 'UNSUPPORTED_MODE',
447
- message: 'Stripe CLI payments are currently only supported in test mode (sk_test_... keys).',
448
- exitCode: 2,
449
- });
450
- }
451
- const mppx = Mppx.create({
452
- methods: [
453
- stripe.charge({
454
- paymentMethod: stripeOpts.paymentMethod,
455
- createToken: async ({ paymentMethod, amount, currency, networkId, expiresAt, metadata, }) => {
456
- const body = new URLSearchParams({
457
- payment_method: paymentMethod,
458
- 'usage_limits[currency]': currency,
459
- 'usage_limits[max_amount]': amount,
460
- 'usage_limits[expires_at]': expiresAt.toString(),
461
- });
462
- if (networkId)
463
- body.set('seller_details[network_id]', networkId);
464
- if (metadata) {
465
- for (const [key, value] of Object.entries(metadata)) {
466
- body.set(`metadata[${key}]`, value);
467
- }
468
- }
469
- const sptUrl = process.env.MPPX_STRIPE_SPT_URL ??
470
- 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens';
471
- const sptHeaders = {
472
- Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
473
- 'Content-Type': 'application/x-www-form-urlencoded',
474
- };
475
- let response = await globalThis.fetch(sptUrl, {
476
- method: 'POST',
477
- headers: sptHeaders,
478
- body,
479
- });
480
- if (!response.ok) {
481
- const errorBody = (await response.json());
482
- if ((metadata || networkId) &&
483
- errorBody.error.message.includes('Received unknown parameter')) {
484
- const fallbackBody = new URLSearchParams({
485
- payment_method: paymentMethod,
486
- 'usage_limits[currency]': currency,
487
- 'usage_limits[max_amount]': amount,
488
- 'usage_limits[expires_at]': expiresAt.toString(),
489
- });
490
- response = await globalThis.fetch(sptUrl, {
491
- method: 'POST',
492
- headers: sptHeaders,
493
- body: fallbackBody,
494
- });
495
- if (!response.ok) {
496
- const fallbackError = (await response.json());
497
- return error({
498
- code: 'STRIPE_ERROR',
499
- message: `Failed to create SPT: ${fallbackError.error.message}`,
500
- exitCode: 77,
501
- });
502
- }
503
- }
504
- else {
505
- return error({
506
- code: 'STRIPE_ERROR',
507
- message: `Failed to create SPT: ${errorBody.error.message}`,
508
- exitCode: 77,
509
- });
510
- }
511
- }
512
- const { id } = (await response.json());
513
- return id;
514
- },
515
- }),
516
- ],
517
- polyfill: false,
518
- });
519
- credential = await mppx.createCredential(challengeResponse);
520
- }
521
- else {
522
- return error({
523
- code: 'UNSUPPORTED_METHOD',
524
- message: `Unsupported payment method: ${challenge.method}`,
525
- exitCode: 2,
526
- });
527
- }
528
- const sessionMd = challenge.request.methodDetails;
529
- let sessionChannelId;
530
- let sessionEscrowContract;
531
- let sessionChainId = 0;
532
- let sessionCumulativeAmount = 0n;
533
- if (challenge.intent === 'session') {
534
- const parsed = Credential.deserialize(credential);
535
- sessionChannelId = parsed.payload.channelId;
536
- sessionChainId = sessionMd?.chainId ?? client?.chain?.id ?? 0;
537
- sessionEscrowContract = sessionMd?.escrowContract;
538
- if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
539
- sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount);
540
- if (verbose >= 1) {
541
- if (parsed.payload.action === 'open') {
542
- const depositRaw = challengeRequest.suggestedDeposit;
543
- const depositDisplay = depositRaw
544
- ? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
545
- : '';
546
- const prefix = confirmEnabled ? '' : '\n';
547
- info(`${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`);
548
- }
549
- else {
550
- const prefix = confirmEnabled ? '' : '\n';
551
- info(`${prefix}${pc.dim(`Channel reused ${parsed.payload.channelId}`)}\n`);
552
- }
553
- }
554
- }
555
- const credentialFetchInit = {
556
- ...fetchInit,
557
- headers: {
558
- ...fetchInit.headers,
559
- ...(challenge.intent === 'session' ? { Accept: 'text/event-stream' } : {}),
560
- Authorization: credential,
561
- },
562
- };
563
- printRequestHeaders(url, credentialFetchInit);
564
- let credentialResponse = await globalThis.fetch(fetchUrl, credentialFetchInit);
565
- if (challenge.intent === 'session' &&
566
- credentialResponse.ok &&
567
- !credentialResponse.headers.get('Content-Type')?.includes('text/event-stream')) {
568
- const parsed = Credential.deserialize(credential);
569
- if (parsed.payload.action === 'open' && 'cumulativeAmount' in parsed.payload) {
570
- const tickAmount = BigInt(challenge.request.amount);
571
- sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount) + tickAmount;
572
- if (sessionEscrowContract && account && client) {
573
- const signature = await signVoucher(client, account, { channelId: sessionChannelId, cumulativeAmount: sessionCumulativeAmount }, sessionEscrowContract, sessionChainId);
574
- const voucherPayload = {
575
- action: 'voucher',
576
- channelId: sessionChannelId,
577
- cumulativeAmount: sessionCumulativeAmount.toString(),
578
- signature,
579
- };
580
- const voucherCred = Credential.serialize({
581
- challenge,
582
- payload: voucherPayload,
583
- source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
584
- });
585
- credentialResponse = await globalThis.fetch(fetchUrl, {
586
- ...fetchInit,
587
- headers: {
588
- ...fetchInit.headers,
589
- Accept: 'text/event-stream',
590
- Authorization: voucherCred,
591
- },
592
- });
593
- }
594
- }
595
- }
596
- if (options.fail && credentialResponse.status >= 400)
597
- return error({
598
- code: 'HTTP_ERROR',
599
- message: `HTTP error ${credentialResponse.status}`,
600
- exitCode: 22,
601
- });
602
- if (credentialResponse.status === 402) {
603
- const body = await credentialResponse.text();
604
- info(`${pc.bold(pc.red('Payment Rejected'))}\n`);
605
- try {
606
- const problem = JSON.parse(body);
607
- const rows = [];
608
- for (const [key, value] of Object.entries(problem)) {
609
- if (value === undefined)
610
- continue;
611
- rows.push([key, String(value)]);
612
- }
613
- rows.sort(([a], [b]) => a.localeCompare(b));
614
- const pad = Math.max(...rows.map(([k]) => k.length));
615
- for (const [label, value] of rows)
616
- info(` ${pc.dim(label.padEnd(pad))} ${value}\n`);
617
- }
618
- catch {
619
- if (body)
620
- info(` ${body}\n`);
621
- }
622
- return error({ code: 'PAYMENT_REJECTED', message: 'Payment rejected', exitCode: 75 });
623
- }
624
- else {
625
- printResponseHeaders(credentialResponse);
626
- const receiptHeader = credentialResponse.headers.get('Payment-Receipt');
627
- if (receiptHeader) {
628
- try {
629
- const receiptJson = JSON.parse(Base64.toString(receiptHeader));
630
- if (typeof receiptJson.acceptedCumulative === 'string' &&
631
- receiptJson.acceptedCumulative) {
632
- sessionCumulativeAmount = BigInt(receiptJson.acceptedCumulative);
633
- if (sessionChannelId)
634
- writeChannelCumulative(sessionChannelId, sessionCumulativeAmount);
635
- }
636
- if (verbose >= 1) {
637
- info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`);
638
- const rows = [];
639
- const channelId = receiptJson.channelId;
640
- const reference = receiptJson.reference;
641
- const skipReference = channelId && reference && channelId === reference;
642
- const receiptBalanceKeys = new Set(['acceptedCumulative', 'spent']);
643
- for (const [key, value] of Object.entries(receiptJson)) {
644
- if (value === undefined || shownKeys.has(key))
645
- continue;
646
- if (key === 'reference' && skipReference)
647
- continue;
648
- if (receiptBalanceKeys.has(key) && typeof value === 'string') {
649
- rows.push([
650
- key,
651
- `${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`,
652
- ]);
653
- }
654
- else if ((key === 'reference' || key === 'txHash') &&
655
- typeof value === 'string' &&
656
- explorerUrl) {
657
- rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)]);
658
- }
659
- else if (key === 'reference' &&
660
- typeof value === 'string' &&
661
- challenge.method === 'stripe' &&
662
- value.startsWith('pi_')) {
663
- const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_');
664
- const dashboardUrl = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`;
665
- rows.push([key, pc.link(dashboardUrl, value)]);
666
- }
667
- else
668
- rows.push([key, String(value)]);
669
- }
670
- rows.sort(([a], [b]) => a.localeCompare(b));
671
- const pad = Math.max(...rows.map(([k]) => k.length));
672
- for (const [label, value] of rows)
673
- info(` ${pc.dim(label.padEnd(pad))} ${value}\n`);
674
- info('\n');
675
- }
676
- }
677
- catch { }
678
- }
679
- const contentType = credentialResponse.headers.get('Content-Type') ?? '';
680
- if (contentType.includes('text/event-stream')) {
681
- const reader = credentialResponse.body?.getReader();
682
- if (!reader) {
683
- return error({ code: 'NO_RESPONSE_BODY', message: 'No response body' });
684
- }
685
- const decoder = new TextDecoder();
686
- let buffer = '';
687
- let currentEvent = '';
688
- const sessionCred = challenge.intent === 'session'
689
- ? Credential.deserialize(credential)
690
- : undefined;
691
- const channelId = sessionCred?.payload.channelId;
692
- const md = challenge.request.methodDetails;
693
- const sessionChainId = md?.chainId ?? client?.chain?.id ?? 0;
694
- const escrowContract = md?.escrowContract;
695
- let cumulativeAmount = sessionCred?.payload &&
696
- 'cumulativeAmount' in sessionCred.payload &&
697
- sessionCred.payload.cumulativeAmount
698
- ? BigInt(sessionCred.payload.cumulativeAmount)
699
- : 0n;
700
- let _voucherSeq = 0;
701
- const termBg = verbose ? await detectTerminalBg() : undefined;
702
- const chunkBgs = (() => {
703
- if (!termBg || !pc.isColorSupported)
704
- return undefined;
705
- const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
706
- const isDark = 0.299 * termBg.r + 0.587 * termBg.g + 0.114 * termBg.b < 128;
707
- const offset = isDark ? 1 : -1;
708
- const bgRgb = (d) => (s) => {
709
- const r = clamp(termBg.r + d * offset);
710
- const g = clamp(termBg.g + d * offset);
711
- const b = clamp(termBg.b + d * offset);
712
- return `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
713
- };
714
- return [bgRgb(12), bgRgb(24)];
715
- })();
716
- let chunkIdx = 0;
717
- const writeContent = (chunk) => {
718
- if (chunkBgs) {
719
- const bgFn = chunkBgs[chunkIdx % chunkBgs.length];
720
- process.stdout.write(chunk.replace(/[^\n]+/g, (m) => bgFn(m)));
721
- chunkIdx++;
722
- }
723
- else {
724
- process.stdout.write(chunk);
725
- }
726
- };
727
- const processLines = async (lines) => {
728
- for (const line of lines) {
729
- if (line.startsWith('event: ')) {
730
- currentEvent = line.slice(7).trim();
731
- continue;
732
- }
733
- if (!line.startsWith('data: ')) {
734
- if (line === '')
735
- currentEvent = '';
736
- continue;
737
- }
738
- const data = line.slice(6);
739
- if (data.trim() === '[DONE]')
740
- continue;
741
- if (currentEvent === 'payment-need-voucher' &&
742
- channelId &&
743
- escrowContract &&
744
- sessionChainId) {
745
- try {
746
- const event = JSON.parse(data);
747
- const required = BigInt(event.requiredCumulative);
748
- cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required;
749
- const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
750
- const voucherCred = Credential.serialize({
751
- challenge,
752
- payload: {
753
- action: 'voucher',
754
- channelId,
755
- cumulativeAmount: cumulativeAmount.toString(),
756
- signature,
757
- },
758
- source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
759
- });
760
- await globalThis.fetch(fetchUrl, {
761
- method: 'POST',
762
- headers: { Authorization: voucherCred },
763
- });
764
- _voucherSeq++;
765
- }
766
- catch (e) {
767
- info(pc.dim(pc.yellow(` [voucher failed: ${e instanceof Error ? e.message : e}]`)));
768
- }
769
- currentEvent = '';
770
- continue;
771
- }
772
- if (currentEvent === 'payment-receipt') {
773
- if (verbose >= 1) {
774
- try {
775
- const receipt = JSON.parse(data);
776
- info(`\n\n${pc.bold(pc.green('Payment Receipt'))}\n`);
777
- const rows = [];
778
- const skipRef = receipt.channelId &&
779
- receipt.reference &&
780
- receipt.channelId === receipt.reference;
781
- for (const [key, value] of Object.entries(receipt)) {
782
- if (value === undefined || shownKeys.has(key))
783
- continue;
784
- if (key === 'reference' && skipRef)
785
- continue;
786
- const receiptBalanceKeys = ['acceptedCumulative', 'spent'];
787
- if (receiptBalanceKeys.includes(key) && typeof value === 'string') {
788
- rows.push([
789
- key,
790
- `${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`,
791
- ]);
792
- }
793
- else if ((key === 'reference' || key === 'txHash') &&
794
- typeof value === 'string' &&
795
- explorerUrl) {
796
- rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)]);
797
- }
798
- else
799
- rows.push([key, String(value)]);
800
- }
801
- rows.sort(([a], [b]) => a.localeCompare(b));
802
- const rpad = Math.max(...rows.map(([k]) => k.length));
803
- for (const [label, value] of rows)
804
- info(` ${pc.dim(label.padEnd(rpad))} ${value}\n`);
805
- }
806
- catch { }
807
- }
808
- currentEvent = '';
809
- continue;
810
- }
811
- if (data.length === 0) {
812
- writeContent('\n');
813
- }
814
- else {
815
- try {
816
- const parsed = JSON.parse(data);
817
- writeContent(parsed.token ?? parsed.choices?.[0]?.delta?.content ?? data);
818
- }
819
- catch {
820
- writeContent(data);
821
- }
822
- }
823
- currentEvent = '';
824
- }
825
- };
826
- while (true) {
827
- const { done, value } = await reader.read();
828
- if (done)
829
- break;
830
- buffer += decoder.decode(value, { stream: true });
831
- const lines = buffer.split('\n');
832
- buffer = lines.pop();
833
- await processLines(lines);
834
- }
835
- if (buffer.trim())
836
- await processLines([buffer]);
837
- if (channelId && escrowContract && sessionChainId) {
838
- const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
839
- const closePayload = {
840
- action: 'close',
841
- channelId,
842
- cumulativeAmount: cumulativeAmount.toString(),
843
- signature,
844
- };
845
- const closeCred = Credential.serialize({
846
- challenge,
847
- payload: closePayload,
848
- source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
849
- });
850
- const closeRes = await globalThis.fetch(fetchUrl, {
851
- method: 'POST',
852
- headers: { Authorization: closeCred },
853
- });
854
- if (closeRes.ok) {
855
- if (verbose >= 1) {
856
- const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
857
- let closeTxHash;
858
- if (closeReceiptHeader) {
859
- try {
860
- const r = JSON.parse(Base64.toString(closeReceiptHeader));
861
- if (typeof r.txHash === 'string')
862
- closeTxHash = r.txHash;
863
- }
864
- catch { }
865
- }
866
- const txInfo = closeTxHash && explorerUrl
867
- ? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
868
- : '';
869
- info(`\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(cumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
870
- }
871
- }
872
- else {
873
- info(`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
874
- }
875
- }
876
- }
877
- else {
878
- const body = (await credentialResponse.text()).replace(/\n+$/, '');
879
- console.log(body);
880
- const shouldClose = challenge.intent === 'session' &&
881
- credentialResponse.ok &&
882
- sessionChannelId &&
883
- sessionEscrowContract &&
884
- sessionChainId;
885
- if (shouldClose && confirmEnabled) {
886
- info('\n');
887
- }
888
- if (shouldClose && confirmEnabled && !(await confirm('Close channel?', true))) {
889
- if (verbose >= 1)
890
- info(`${pc.dim('Kept channel open.')}\n`);
891
- }
892
- else if (shouldClose) {
893
- const signature = await signVoucher(client, account, { channelId: sessionChannelId, cumulativeAmount: sessionCumulativeAmount }, sessionEscrowContract, sessionChainId);
894
- const closePayload = {
895
- action: 'close',
896
- channelId: sessionChannelId,
897
- cumulativeAmount: sessionCumulativeAmount.toString(),
898
- signature,
899
- };
900
- const closeCred = Credential.serialize({
901
- challenge,
902
- payload: closePayload,
903
- source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
904
- });
905
- const closeRes = await globalThis.fetch(fetchUrl, {
906
- ...fetchInit,
907
- headers: {
908
- ...fetchInit.headers,
909
- Authorization: closeCred,
910
- },
911
- });
912
- if (closeRes.ok) {
913
- deleteChannelState(sessionChannelId);
914
- if (verbose >= 1) {
915
- const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
916
- let closeTxHash;
917
- if (closeReceiptHeader) {
918
- try {
919
- const r = JSON.parse(Base64.toString(closeReceiptHeader));
920
- if (typeof r.txHash === 'string')
921
- closeTxHash = r.txHash;
922
- }
923
- catch { }
924
- }
925
- const txInfo = closeTxHash && explorerUrl
926
- ? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
927
- : '';
928
- const closePrefix = confirmEnabled ? '' : '\n';
929
- info(`${closePrefix}${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(sessionCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
930
- }
931
- }
932
- else {
933
- const closeBody = await closeRes.text().catch(() => '');
934
- info(`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
935
- info(`${pc.dim(` channelId: ${sessionChannelId}`)}\n` +
936
- `${pc.dim(` cumulativeAmount: ${sessionCumulativeAmount}`)}\n` +
937
- `${pc.dim(` escrowContract: ${sessionEscrowContract}`)}\n` +
938
- `${pc.dim(` chainId: ${sessionChainId}`)}\n` +
939
- `${pc.dim(` account: ${account?.address}`)}\n` +
940
- `${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`);
941
- }
942
- }
943
- }
944
- }
945
- }
946
- catch (err) {
947
- // TODO: revert cast when https://github.com/wevm/zile/pull/26 is merged
948
- const errCause = err instanceof Error ? err.cause : undefined;
949
- const cause = errCause instanceof Error ? errCause : undefined;
950
- if (cause && 'code' in cause) {
951
- const code = cause.code;
952
- if (code === 'ENOTFOUND')
953
- return error({
954
- code: 'DNS_ERROR',
955
- message: `Could not resolve host "${hostname}". Check the URL and try again.`,
956
- exitCode: 6,
957
- });
958
- else if (code === 'ECONNREFUSED')
959
- return error({
960
- code: 'CONNECTION_REFUSED',
961
- message: `Connection refused by "${hostname}". Is the server running?`,
962
- retryable: true,
963
- exitCode: 7,
964
- });
965
- else if (code === 'ECONNRESET')
966
- return error({
967
- code: 'CONNECTION_RESET',
968
- message: `Connection to "${hostname}" was reset.`,
969
- retryable: true,
970
- exitCode: 56,
971
- });
972
- else if (code === 'ETIMEDOUT')
973
- return error({
974
- code: 'CONNECTION_TIMEOUT',
975
- message: `Connection to "${hostname}" timed out.`,
976
- retryable: true,
977
- exitCode: 28,
978
- });
979
- else if (code === 'CERT_HAS_EXPIRED' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')
980
- return error({
981
- code: 'TLS_ERROR',
982
- message: `TLS certificate error for "${hostname}". Use --insecure to skip verification.`,
983
- exitCode: 60,
984
- });
985
- else
986
- return error({
987
- code: 'REQUEST_FAILED',
988
- message: `Request to "${hostname}" failed: ${cause.message}`,
989
- });
990
- }
991
- else {
992
- const msg = err instanceof Error ? err.message : String(err);
993
- return error({
994
- code: 'REQUEST_FAILED',
995
- message: cause
996
- ? `Request failed: ${msg} (Cause: ${cause.message})`
997
- : `Request failed: ${msg}`,
998
- });
999
- }
1000
- }
1001
- },
1002
- });
1003
- const account = Cli.create('account', {
1004
- description: 'Manage accounts (create, default, delete, fund, list, view)',
1005
- });
1006
- account.command('create', {
1007
- description: 'Create new account',
1008
- options: z.object({
1009
- account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
1010
- rpcUrl: z.string().optional().describe('RPC endpoint (env: MPPX_RPC_URL)'),
1011
- }),
1012
- alias: { account: 'a', rpcUrl: 'r' },
1013
- async run({ options }) {
1014
- let resolvedName = options.account;
1015
- if (!resolvedName) {
1016
- const existing = await createKeychain().list();
1017
- if (existing.length === 0)
1018
- resolvedName = 'main';
1019
- else {
1020
- const input = await prompt('Account name');
1021
- if (!input)
1022
- return;
1023
- resolvedName = input;
1024
- }
1025
- }
1026
- let keychain = createKeychain(resolvedName);
1027
- while (await keychain.get()) {
1028
- process.stderr.write(`${pc.dim(`Account "${resolvedName}" already exists.`)}\n\n`);
1029
- const input = await prompt('Enter different name');
1030
- if (!input)
1031
- return;
1032
- resolvedName = input;
1033
- keychain = createKeychain(resolvedName);
1034
- }
1035
- const privateKey = generatePrivateKey();
1036
- const acct = privateKeyToAccount(privateKey);
1037
- await keychain.set(privateKey);
1038
- const accounts = await createKeychain().list();
1039
- if (accounts.length === 1)
1040
- createDefaultStore().set(resolvedName);
1041
- console.log(`Account "${resolvedName}" saved to keychain.`);
1042
- const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
1043
- const addrDisplay = explorerUrl
1044
- ? pc.link(`${explorerUrl}/address/${acct.address}`, acct.address)
1045
- : acct.address;
1046
- console.log(pc.dim(`Address ${addrDisplay}`));
1047
- resolveChain(options)
1048
- .then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
1049
- .then((client) => import('viem/tempo').then(({ Actions }) => Actions.faucet.fund(client, { account: acct }).catch(() => { })));
1050
- },
1051
- });
1052
- account.command('default', {
1053
- description: 'Set default account',
1054
- options: z.object({
1055
- account: z.string().describe('Account name'),
1056
- }),
1057
- alias: { account: 'a' },
1058
- async run({ options, error }) {
1059
- const accountName = options.account;
1060
- if (isTempoAccount(accountName)) {
1061
- const tempoEntry = resolveTempoAccount(accountName);
1062
- if (!tempoEntry) {
1063
- return error({
1064
- code: 'ACCOUNT_NOT_FOUND',
1065
- message: `Account "${accountName}" not found. Is Tempo wallet configured?`,
1066
- exitCode: 69,
1067
- });
1068
- }
1069
- createDefaultStore().set(accountName);
1070
- console.log(`Default account set to "${accountName}"`);
1071
- return;
1072
- }
1073
- const key = await createKeychain(accountName).get();
1074
- if (!key) {
1075
- return error({
1076
- code: 'ACCOUNT_NOT_FOUND',
1077
- message: `Account "${accountName}" not found.`,
1078
- exitCode: 69,
1079
- });
1080
- }
1081
- createDefaultStore().set(accountName);
1082
- console.log(`Default account set to "${accountName}"`);
1083
- },
1084
- });
1085
- account.command('delete', {
1086
- description: 'Delete account',
1087
- options: z.object({
1088
- account: z.string().describe('Account name'),
1089
- yes: z.boolean().optional().describe('DANGER!! Skip confirmation prompts'),
1090
- }),
1091
- alias: { account: 'a' },
1092
- async run({ options, error }) {
1093
- const keychain = createKeychain(options.account);
1094
- const key = await keychain.get();
1095
- if (!key) {
1096
- return error({
1097
- code: 'ACCOUNT_NOT_FOUND',
1098
- message: `Account "${options.account}" not found.`,
1099
- exitCode: 69,
1100
- });
1101
- }
1102
- const acct = privateKeyToAccount(key);
1103
- const balanceLines = await fetchBalanceLines(acct.address, { includeTestnet: false });
1104
- if (!options.yes) {
1105
- const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
1106
- const addrDisplay = explorerUrl
1107
- ? pc.link(`${explorerUrl}/address/${acct.address}`, acct.address)
1108
- : acct.address;
1109
- process.stderr.write(pc.dim(`Delete account "${options.account}"\n`));
1110
- process.stderr.write(pc.dim(` Address ${addrDisplay}\n`));
1111
- for (let i = 0; i < balanceLines.length; i++)
1112
- process.stderr.write(pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`));
1113
- process.stderr.write(pc.dim('This action cannot be undone\n\n'));
1114
- const confirmed = await confirm('Confirm delete?');
1115
- if (!confirmed) {
1116
- console.log('Canceled');
1117
- return;
1118
- }
1119
- }
1120
- await keychain.delete();
1121
- const currentDefault = createDefaultStore().get();
1122
- if (currentDefault === options.account) {
1123
- const remaining = await createKeychain().list();
1124
- if (remaining.length > 0) {
1125
- createDefaultStore().set(remaining[0]);
1126
- console.log(`Default account set to "${remaining[0]}"`);
1127
- }
1128
- else {
1129
- createDefaultStore().clear();
1130
- }
1131
- }
1132
- console.log(`Account "${options.account}" deleted`);
1133
- },
1134
- });
1135
- account.command('fund', {
1136
- description: 'Fund account with testnet tokens',
1137
- options: z.object({
1138
- account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
1139
- rpcUrl: z.string().optional().describe('RPC endpoint (env: MPPX_RPC_URL)'),
1140
- }),
1141
- alias: { account: 'a', rpcUrl: 'r' },
1142
- async run({ options, error }) {
1143
- const accountName = resolveAccountName(options.account);
1144
- const keychain = createKeychain(accountName);
1145
- const key = await keychain.get();
1146
- if (!key) {
1147
- if (options.account)
1148
- return error({
1149
- code: 'ACCOUNT_NOT_FOUND',
1150
- message: `Account "${accountName}" not found.`,
1151
- exitCode: 69,
1152
- });
1153
- else
1154
- return error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 });
1155
- }
1156
- const acct = privateKeyToAccount(key);
1157
- const chain = await resolveChain(options);
1158
- const client = createClient({ chain, transport: http(options.rpcUrl) });
1159
- console.log(`Funding "${accountName}" on ${chainName(chain)}`);
1160
- try {
1161
- const { Actions } = await import('viem/tempo');
1162
- const hashes = await Actions.faucet.fund(client, { account: acct });
1163
- const explorerUrl = chain.blockExplorers?.default?.url;
1164
- for (const hash of hashes) {
1165
- const label = explorerUrl ? pc.link(`${explorerUrl}/tx/${hash}`, pc.gray(hash)) : hash;
1166
- console.log(` ${label}`);
1167
- }
1168
- const { waitForTransactionReceipt } = await import('viem/actions');
1169
- await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })));
1170
- console.log('Funded successfully');
1171
- }
1172
- catch (err) {
1173
- console.error('Funding failed:', err instanceof Error ? err.message : err);
1174
- }
1175
- },
1176
- });
1177
- account.command('list', {
1178
- description: 'List all accounts',
1179
- async run() {
1180
- const currentDefault = createDefaultStore().get();
1181
- const accounts = (await createKeychain().list()).sort();
1182
- const resolved = [];
1183
- for (const accountName of accounts) {
1184
- const key = await createKeychain(accountName).get();
1185
- if (!key)
1186
- continue;
1187
- resolved.push({
1188
- name: accountName,
1189
- address: privateKeyToAccount(key).address,
1190
- });
1191
- }
1192
- const tempoEntries = readTempoKeystore();
1193
- for (let i = 0; i < tempoEntries.length; i++) {
1194
- const entry = tempoEntries[i];
1195
- const tempoName = i === 0 ? 'tempo:default' : `tempo:${i}`;
1196
- if (entry.wallet_address)
1197
- resolved.push({ name: tempoName, address: entry.wallet_address, source: 'tempo wallet' });
1198
- }
1199
- if (resolved.length === 0) {
1200
- console.log(`No accounts found.`);
1201
- return;
1202
- }
1203
- const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
1204
- const maxWidth = Math.max(...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)));
1205
- for (const entry of resolved) {
1206
- const isDefault = entry.name === currentDefault;
1207
- const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name;
1208
- const width = entry.name.length + (isDefault ? 1 : 0);
1209
- const addrDisplay = explorerUrl
1210
- ? pc.link(`${explorerUrl}/address/${entry.address}`, entry.address)
1211
- : entry.address;
1212
- const sourceLabel = entry.source ? ` ${pc.dim(`(${entry.source})`)}` : '';
1213
- console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(addrDisplay)}${sourceLabel}`);
1214
- }
1215
- },
1216
- });
1217
- account.command('view', {
1218
- description: 'View account address',
1219
- options: z.object({
1220
- account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
1221
- rpcUrl: z.string().optional().describe('RPC endpoint (env: MPPX_RPC_URL)'),
1222
- }),
1223
- alias: { account: 'a', rpcUrl: 'r' },
1224
- async run({ options, error }) {
1225
- const accountName = resolveAccountName(options.account);
1226
- if (isTempoAccount(accountName)) {
1227
- const tempoEntry = resolveTempoAccount(accountName);
1228
- if (!tempoEntry) {
1229
- return error({
1230
- code: 'ACCOUNT_NOT_FOUND',
1231
- message: `Account "${accountName}" not found. Is Tempo wallet configured?`,
1232
- exitCode: 69,
1233
- });
1234
- }
1235
- const address = tempoEntry.wallet_address;
1236
- const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined);
1237
- const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet;
1238
- const explorerUrl = chain.blockExplorers?.default?.url;
1239
- const addrDisplay = explorerUrl
1240
- ? pc.link(`${explorerUrl}/address/${address}`, address)
1241
- : address;
1242
- console.log(`${pc.dim('Address')} ${addrDisplay}`);
1243
- const balanceLines = await fetchBalanceLines(address, chain && rpcUrl ? { chain, rpcUrl } : undefined);
1244
- for (let i = 0; i < balanceLines.length; i++)
1245
- console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`);
1246
- console.log(`${pc.dim('Name')} ${accountName}`);
1247
- console.log(`${pc.dim('Type')} ${tempoEntry.wallet_type} ${pc.dim('(tempo wallet)')}`);
1248
- return;
1249
- }
1250
- const keychain = createKeychain(accountName);
1251
- const key = await keychain.get();
1252
- if (!key) {
1253
- if (options.account)
1254
- return error({
1255
- code: 'ACCOUNT_NOT_FOUND',
1256
- message: `Account "${accountName}" not found.`,
1257
- exitCode: 69,
1258
- });
1259
- else
1260
- return error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 });
1261
- }
1262
- const acct = privateKeyToAccount(key);
1263
- const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined);
1264
- const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet;
1265
- const explorerUrl = chain.blockExplorers?.default?.url;
1266
- const addrDisplay = explorerUrl
1267
- ? pc.link(`${explorerUrl}/address/${acct.address}`, acct.address)
1268
- : acct.address;
1269
- console.log(`${pc.dim('Address')} ${addrDisplay}`);
1270
- const balanceLines = await fetchBalanceLines(acct.address, chain && rpcUrl ? { chain, rpcUrl } : undefined);
1271
- for (let i = 0; i < balanceLines.length; i++)
1272
- console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`);
1273
- console.log(`${pc.dim('Name')} ${accountName}`);
1274
- },
1275
- });
1276
- cli.command(account);
1277
- const sign = Cli.create('sign', {
1278
- description: 'Sign a payment challenge and output the Authorization header',
1279
- usage: [
1280
- { suffix: '--challenge <value> [options]' },
1281
- { prefix: 'echo <challenge> |', suffix: '[options]' },
1282
- ],
1283
- options: z.object({
1284
- account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
1285
- challenge: z.string().optional().describe('WWW-Authenticate challenge value'),
1286
- dryRun: z.boolean().optional().describe('Validate and parse the challenge without signing'),
1287
- methodOpt: z
1288
- .array(z.string())
1289
- .optional()
1290
- .describe('Method-specific option (key=value, repeatable)'),
1291
- rpcUrl: z
1292
- .string()
1293
- .optional()
1294
- .describe('RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)'),
1295
- }),
1296
- alias: {
1297
- account: 'a',
1298
- challenge: 'c',
1299
- methodOpt: 'M',
1300
- rpcUrl: 'r',
1301
- },
1302
- async run({ options, format, error }) {
1303
- const raw = options.challenge || (process.stdin.isTTY === false ? await readStdin() : undefined);
1304
- if (!raw) {
1305
- return error({
1306
- code: 'NO_CHALLENGE',
1307
- message: 'No challenge provided. Use --challenge or pipe via stdin.',
1308
- exitCode: 2,
1309
- });
1310
- }
1311
- let challenge;
1312
- try {
1313
- challenge = Challenge.deserialize(raw);
1314
- }
1315
- catch (err) {
1316
- return error({
1317
- code: 'INVALID_CHALLENGE',
1318
- message: `Failed to parse challenge: ${err instanceof Error ? err.message : err}`,
1319
- exitCode: 2,
1320
- });
1321
- }
1322
- if (options.dryRun) {
1323
- process.stderr.write('Challenge is valid.\n');
1324
- return;
1325
- }
1326
- if (challenge.method === 'tempo') {
1327
- const accountName = resolveAccountName(options.account);
1328
- // Delegate to tempo CLI for tempo wallet accounts
1329
- if (isTempoAccount(accountName) && hasTempoCliSync()) {
1330
- const wwwAuth = Challenge.serialize(challenge);
1331
- const result = await tempoCliSign(wwwAuth);
1332
- if (format === 'json') {
1333
- const tempoEntry = resolveTempoAccount(accountName);
1334
- console.log(JSON.stringify({ authorization: result, from: tempoEntry?.wallet_address }));
1335
- }
1336
- else {
1337
- console.log(result);
1338
- }
1339
- return;
1340
- }
1341
- let privateKey = process.env.MPPX_PRIVATE_KEY?.trim() ||
1342
- (isTempoAccount(accountName) ? undefined : await createKeychain(accountName).get());
1343
- if (!privateKey) {
1344
- const fallback = fallbackFromTempo();
1345
- if (fallback)
1346
- privateKey = await createKeychain(fallback).get();
1347
- }
1348
- if (!privateKey) {
1349
- if (options.account)
1350
- return error({
1351
- code: 'ACCOUNT_NOT_FOUND',
1352
- message: `Account "${accountName}" not found.`,
1353
- exitCode: 69,
1354
- });
1355
- else
1356
- return error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 });
1357
- }
1358
- const account = privateKeyToAccount(privateKey);
1359
- const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
1360
- const client = createClient({
1361
- chain: await resolveChain({ rpcUrl }),
1362
- transport: http(rpcUrl),
1363
- });
1364
- const methodOpts = parseMethodOpts(options.methodOpt);
1365
- const tempoOpts = parseOptions(z.object({
1366
- channel: z.optional(z.coerce.string()),
1367
- deposit: z.optional(z.union([z.string(), z.number()])),
1368
- }), methodOpts);
1369
- const mppx = Mppx.create({
1370
- methods: tempo({
1371
- account,
1372
- getClient: () => client,
1373
- deposit: (() => {
1374
- if (challenge.intent !== 'session')
1375
- return undefined;
1376
- const suggestedDeposit = challenge.request
1377
- .suggestedDeposit;
1378
- const cliDeposit = tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined;
1379
- return suggestedDeposit ?? cliDeposit;
1380
- })(),
1381
- }),
1382
- polyfill: false,
1383
- });
1384
- const wwwAuth = Challenge.serialize(challenge);
1385
- const fakeResponse = new Response(null, {
1386
- status: 402,
1387
- headers: { 'WWW-Authenticate': wwwAuth },
1388
- });
1389
- const credential = await mppx.createCredential(fakeResponse, (() => {
1390
- if (!tempoOpts.channel)
1391
- return undefined;
1392
- const channelId = tempoOpts.channel;
1393
- const saved = readChannelCumulative(channelId);
1394
- return {
1395
- channelId,
1396
- ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
1397
- };
1398
- })());
1399
- if (format === 'json') {
1400
- console.log(JSON.stringify({ authorization: credential, from: account.address }));
1401
- }
1402
- else {
1403
- console.log(credential);
1404
- }
1405
- }
1406
- else if (challenge.method === 'stripe') {
1407
- const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY;
1408
- if (!stripeSecretKey) {
1409
- return error({
1410
- code: 'MISSING_ENV',
1411
- message: 'MPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
1412
- exitCode: 2,
1413
- });
1414
- }
1415
- const methodOpts = parseMethodOpts(options.methodOpt);
1416
- const stripeOpts = parseOptions(z.object({ paymentMethod: z.string() }), methodOpts);
1417
- const mppx = Mppx.create({
1418
- methods: [
1419
- stripe.charge({
1420
- paymentMethod: stripeOpts.paymentMethod,
1421
- createToken: async ({ paymentMethod, amount, currency, networkId, expiresAt, metadata, }) => {
1422
- const body = new URLSearchParams({
1423
- payment_method: paymentMethod,
1424
- 'usage_limits[currency]': currency,
1425
- 'usage_limits[max_amount]': amount,
1426
- 'usage_limits[expires_at]': expiresAt.toString(),
1427
- });
1428
- if (networkId)
1429
- body.set('seller_details[network_id]', networkId);
1430
- if (metadata) {
1431
- for (const [key, value] of Object.entries(metadata))
1432
- body.set(`metadata[${key}]`, value);
1433
- }
1434
- const sptUrl = process.env.MPPX_STRIPE_SPT_URL ??
1435
- 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens';
1436
- const response = await globalThis.fetch(sptUrl, {
1437
- method: 'POST',
1438
- headers: {
1439
- Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
1440
- 'Content-Type': 'application/x-www-form-urlencoded',
1441
- },
1442
- body,
1443
- });
1444
- if (!response.ok) {
1445
- const errorBody = (await response.json());
1446
- return error({
1447
- code: 'STRIPE_ERROR',
1448
- message: `Failed to create SPT: ${errorBody.error.message}`,
1449
- exitCode: 77,
1450
- });
1451
- }
1452
- const { id } = (await response.json());
1453
- return id;
1454
- },
1455
- }),
1456
- ],
1457
- polyfill: false,
1458
- });
1459
- const wwwAuth = Challenge.serialize(challenge);
1460
- const fakeResponse = new Response(null, {
1461
- status: 402,
1462
- headers: { 'WWW-Authenticate': wwwAuth },
1463
- });
1464
- const credential = await mppx.createCredential(fakeResponse);
1465
- if (format === 'json') {
1466
- console.log(JSON.stringify({ authorization: credential }));
1467
- }
1468
- else {
1469
- console.log(credential);
1470
- }
1471
- }
1472
- else {
1473
- return error({
1474
- code: 'UNSUPPORTED_METHOD',
1475
- message: `Unsupported payment method: ${challenge.method}`,
1476
- exitCode: 2,
1477
- });
1478
- }
1479
- },
1480
- });
1481
- cli.command(sign);
1482
- export default cli;
1483
- /////////////////////////////////////////////////////////////////////////////////////////////////
1484
- function parseMethodOpts(raw) {
1485
- if (!raw)
1486
- return {};
1487
- const list = Array.isArray(raw) ? raw : [raw];
1488
- const result = {};
1489
- for (const item of list) {
1490
- const idx = item.indexOf('=');
1491
- if (idx === -1) {
1492
- throw new Error(`Invalid method option format: ${item} (expected key=value)`);
1493
- }
1494
- result[item.slice(0, idx)] = item.slice(idx + 1);
1495
- }
1496
- return result;
1497
- }
1498
- function parseOptions(schema, rawOptions) {
1499
- const result = schema.safeParse(rawOptions ?? {});
1500
- if (result.success)
1501
- return result.data;
1502
- const summary = result.error.issues
1503
- .map((issue) => {
1504
- const path = issue.path.length ? issue.path.join('.') : 'options';
1505
- return `${path}: ${issue.message}`;
1506
- })
1507
- .join(', ');
1508
- throw new Error(`Invalid CLI options (${summary})`);
1509
- }
1510
- function execCommand(command, args) {
1511
- return new Promise((resolve) => {
1512
- child.execFile(command, args, (error, stdout, stderr) => {
1513
- resolve({ stdout: stdout.trim(), stderr: stderr.trim(), error });
1514
- });
1515
- });
1516
- }
1517
- function channelStateDir() {
1518
- return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'mppx', 'channels');
1519
- }
1520
- function readChannelCumulative(channelId) {
1521
- try {
1522
- const raw = fs.readFileSync(path.join(channelStateDir(), channelId), 'utf-8').trim();
1523
- return raw ? BigInt(raw) : undefined;
1524
- }
1525
- catch {
1526
- return undefined;
1527
- }
1528
- }
1529
- function writeChannelCumulative(channelId, cumulative) {
1530
- const dir = channelStateDir();
1531
- fs.mkdirSync(dir, { recursive: true });
1532
- fs.writeFileSync(path.join(dir, channelId), cumulative.toString(), 'utf-8');
1533
- }
1534
- function deleteChannelState(channelId) {
1535
- try {
1536
- fs.unlinkSync(path.join(channelStateDir(), channelId));
1537
- }
1538
- catch { }
1539
- }
1540
- function createDefaultStore() {
1541
- const configPath = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'mppx', 'default');
1542
- return {
1543
- get() {
1544
- try {
1545
- return fs.readFileSync(configPath, 'utf-8').trim() || 'main';
1546
- }
1547
- catch {
1548
- return 'main';
1549
- }
1550
- },
1551
- set(value) {
1552
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
1553
- fs.writeFileSync(configPath, value, 'utf-8');
1554
- },
1555
- clear() {
1556
- try {
1557
- fs.unlinkSync(configPath);
1558
- }
1559
- catch { }
1560
- },
1561
- };
1562
- }
1563
- function resolveAccountName(explicit) {
1564
- if (explicit)
1565
- return explicit;
1566
- if (process.env.MPPX_ACCOUNT?.trim())
1567
- return process.env.MPPX_ACCOUNT;
1568
- return createDefaultStore().get();
1569
- }
1570
- function isTempoAccount(accountName) {
1571
- return accountName.startsWith('tempo:');
1572
- }
1573
- function tempoKeystorePath() {
1574
- const platform = os.platform();
1575
- if (platform === 'darwin')
1576
- return path.join(os.homedir(), 'Library', 'Application Support', 'tempo', 'wallet', 'keys.toml');
1577
- return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'tempo', 'wallet', 'keys.toml');
1578
- }
1579
- function readTempoKeystore() {
1580
- try {
1581
- const raw = fs.readFileSync(tempoKeystorePath(), 'utf-8');
1582
- const entries = [];
1583
- let current;
1584
- for (const line of raw.split('\n')) {
1585
- const trimmed = line.trim();
1586
- if (trimmed === '[[keys]]') {
1587
- if (current?.wallet_address)
1588
- entries.push(current);
1589
- current = { wallet_type: 'local', wallet_address: '', chain_id: 0 };
1590
- continue;
1591
- }
1592
- if (!current)
1593
- continue;
1594
- const m = trimmed.match(/^(\w+)\s*=\s*"?([^"]*)"?$/);
1595
- if (!m)
1596
- continue;
1597
- const [, key, value] = m;
1598
- if (key === 'wallet_type')
1599
- current.wallet_type = value;
1600
- else if (key === 'wallet_address')
1601
- current.wallet_address = value;
1602
- else if (key === 'chain_id')
1603
- current.chain_id = Number.parseInt(value, 10);
1604
- }
1605
- if (current?.wallet_address)
1606
- entries.push(current);
1607
- return entries;
1608
- }
1609
- catch {
1610
- return [];
1611
- }
1612
- }
1613
- function resolveTempoAccount(accountName) {
1614
- const entries = readTempoKeystore();
1615
- if (entries.length === 0)
1616
- return undefined;
1617
- const suffix = accountName.slice('tempo:'.length);
1618
- if (suffix === 'default' || suffix === '')
1619
- return entries[0];
1620
- const idx = Number.parseInt(suffix, 10);
1621
- if (!Number.isNaN(idx) && idx >= 0 && idx < entries.length)
1622
- return entries[idx];
1623
- return undefined;
1624
- }
1625
- let _tempoCliAvailable;
1626
- function hasTempoCliSync() {
1627
- if (_tempoCliAvailable !== undefined)
1628
- return _tempoCliAvailable;
1629
- try {
1630
- child.execFileSync('which', ['tempo'], { stdio: 'ignore' });
1631
- _tempoCliAvailable = true;
1632
- }
1633
- catch {
1634
- _tempoCliAvailable = false;
1635
- }
1636
- return _tempoCliAvailable;
1637
- }
1638
- async function tempoCliSign(wwwAuth) {
1639
- return new Promise((resolve, reject) => {
1640
- child.execFile('tempo', ['mpp', 'sign', '--challenge', wwwAuth], (error, stdout, stderr) => {
1641
- if (error) {
1642
- const msg = stderr?.trim() || error.message;
1643
- reject(new Error(`tempo mpp sign failed: ${msg}`));
1644
- return;
1645
- }
1646
- const trimmed = stdout.trim();
1647
- if (!trimmed) {
1648
- reject(new Error('tempo mpp sign returned empty output'));
1649
- return;
1650
- }
1651
- resolve(trimmed);
1652
- });
1653
- });
1654
- }
1655
- function fallbackFromTempo() {
1656
- const store = createDefaultStore();
1657
- const currentDefault = store.get();
1658
- if (!isTempoAccount(currentDefault))
1659
- return undefined;
1660
- if (hasTempoCliSync())
1661
- return undefined;
1662
- // tempo CLI not installed, fall back to first mppx account
1663
- // (sync list via security dump-keychain to avoid async in hot path)
1664
- const platform = os.platform();
1665
- if (platform === 'darwin') {
1666
- try {
1667
- const stdout = child.execFileSync('security', ['dump-keychain'], { encoding: 'utf-8' });
1668
- const mppxAccounts = [];
1669
- for (const block of stdout.split('keychain:')) {
1670
- const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/);
1671
- const accountMatch = block.match(/"acct"<blob>="([^"]*)"/);
1672
- if (serviceMatch?.[1] === name && accountMatch?.[1])
1673
- mppxAccounts.push(accountMatch[1]);
1674
- }
1675
- if (mppxAccounts.length > 0) {
1676
- store.set(mppxAccounts[0]);
1677
- return mppxAccounts[0];
1678
- }
1679
- }
1680
- catch { }
1681
- }
1682
- return undefined;
1683
- }
1684
- // biome-ignore format: compact shell commands
1685
- function createKeychain(account = 'main') {
1686
- const service = name;
1687
- return {
1688
- async list() {
1689
- const platform = os.platform();
1690
- if (platform === 'darwin') {
1691
- const { stdout, error } = await execCommand('security', ['dump-keychain']);
1692
- if (error)
1693
- return [];
1694
- const accounts = [];
1695
- const blocks = stdout.split('keychain:');
1696
- for (const block of blocks) {
1697
- const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/);
1698
- const accountMatch = block.match(/"acct"<blob>="([^"]*)"/);
1699
- if (serviceMatch?.[1] === service && accountMatch?.[1])
1700
- accounts.push(accountMatch[1]);
1701
- }
1702
- return accounts;
1703
- }
1704
- if (platform === 'linux') {
1705
- const { stdout, stderr, error } = await execCommand('secret-tool', ['search', '--all', '--unlock', 'service', service]);
1706
- if (error)
1707
- return [];
1708
- const combined = `${stdout}\n${stderr}`;
1709
- const accounts = [];
1710
- const matches = combined.matchAll(/\baccount = (.+)/g);
1711
- for (const match of matches)
1712
- if (match[1])
1713
- accounts.push(match[1]);
1714
- return accounts;
1715
- }
1716
- throw new Error(`Unsupported platform: ${platform}`);
1717
- },
1718
- async get() {
1719
- const platform = os.platform();
1720
- if (platform === 'darwin') {
1721
- const { stdout, error } = await execCommand('security', ['find-generic-password', '-s', service, '-a', account, '-w']);
1722
- return error ? undefined : stdout;
1723
- }
1724
- if (platform === 'linux') {
1725
- const { stdout, error } = await execCommand('secret-tool', ['lookup', 'service', service, 'account', account]);
1726
- return error ? undefined : stdout || undefined;
1727
- }
1728
- throw new Error(`Unsupported platform: ${platform}`);
1729
- },
1730
- async set(value) {
1731
- const platform = os.platform();
1732
- if (platform === 'darwin') {
1733
- await execCommand('security', ['delete-generic-password', '-s', service, '-a', account]);
1734
- const { error } = await execCommand('security', ['add-generic-password', '-s', service, '-a', account, '-w', value]);
1735
- if (error)
1736
- throw error;
1737
- return;
1738
- }
1739
- if (platform === 'linux') {
1740
- const proc = child.execFile('secret-tool', ['store', '--label', `${service} ${account}`, 'service', service, 'account', account]);
1741
- proc.stdin?.write(value);
1742
- proc.stdin?.end();
1743
- return new Promise((resolve, reject) => {
1744
- proc.on('close', (code) => {
1745
- if (code === 0)
1746
- resolve();
1747
- else
1748
- reject(new Error(`secret-tool exited with code ${code}`));
1749
- });
1750
- proc.on('error', reject);
1751
- });
1752
- }
1753
- throw new Error(`Unsupported platform: ${platform}`);
1754
- },
1755
- async delete() {
1756
- const platform = os.platform();
1757
- if (platform === 'darwin') {
1758
- await execCommand('security', ['delete-generic-password', '-s', service, '-a', account]);
1759
- return;
1760
- }
1761
- if (platform === 'linux') {
1762
- await execCommand('secret-tool', ['clear', 'service', service, 'account', account]);
1763
- return;
1764
- }
1765
- throw new Error(`Unsupported platform: ${platform}`);
1766
- },
1767
- };
1768
- }
1769
- function prompt(message) {
1770
- const reader = readline.createInterface({ input: process.stdin, output: process.stderr });
1771
- return new Promise((resolve) => {
1772
- reader.on('close', () => resolve(undefined));
1773
- reader.question(`${pc.bold(`▸ ${message}:`)} `, (answer) => {
1774
- reader.close();
1775
- const value = answer.trim();
1776
- resolve(value || undefined);
1777
- });
1778
- });
1779
- }
1780
- function confirm(prompt, defaultYes = false) {
1781
- const reader = readline.createInterface({ input: process.stdin, output: process.stderr });
1782
- return new Promise((resolve) => {
1783
- const hint = defaultYes ? '(Y/n)' : '(y/N)';
1784
- reader.question(`${pc.bold(`▸ ${prompt}`)} ${pc.dim(hint)} `, (answer) => {
1785
- reader.close();
1786
- const trimmed = answer.trim().toLowerCase();
1787
- resolve(trimmed === '' ? defaultYes : trimmed === 'y');
1788
- });
1789
- });
1790
- }
1791
- // Inlined from https://github.com/alexeyraspopov/picocolors (ISC License)
1792
- const pc = (() => {
1793
- const p = process || {};
1794
- const argv = p.argv || [];
1795
- const env = p.env || {};
1796
- const isColorSupported = !(!!env.NO_COLOR || argv.includes('--no-color')) &&
1797
- (!!env.FORCE_COLOR ||
1798
- argv.includes('--color') ||
1799
- ((p.stdout || {}).isTTY && env.TERM !== 'dumb') ||
1800
- !!env.CI);
1801
- const replaceClose = (string, close, replace, index) => {
1802
- let result = '';
1803
- let cursor = 0;
1804
- let i = index;
1805
- do {
1806
- result += string.substring(cursor, i) + replace;
1807
- cursor = i + close.length;
1808
- i = string.indexOf(close, cursor);
1809
- } while (~i);
1810
- return result + string.substring(cursor);
1811
- };
1812
- const formatter = (open, close, replace = open) => (input) => {
1813
- const string = `${input}`;
1814
- const index = string.indexOf(close, open.length);
1815
- return ~index
1816
- ? open + replaceClose(string, close, replace, index) + close
1817
- : open + string + close;
1818
- };
1819
- const f = isColorSupported ? formatter : () => String;
1820
- return {
1821
- isColorSupported,
1822
- reset: f('\x1b[0m', '\x1b[0m'),
1823
- bold: f('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m'),
1824
- dim: f('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m'),
1825
- italic: f('\x1b[3m', '\x1b[23m'),
1826
- underline: f('\x1b[4m', '\x1b[24m'),
1827
- inverse: f('\x1b[7m', '\x1b[27m'),
1828
- hidden: f('\x1b[8m', '\x1b[28m'),
1829
- strikethrough: f('\x1b[9m', '\x1b[29m'),
1830
- black: f('\x1b[30m', '\x1b[39m'),
1831
- red: f('\x1b[31m', '\x1b[39m'),
1832
- green: f('\x1b[32m', '\x1b[39m'),
1833
- yellow: f('\x1b[33m', '\x1b[39m'),
1834
- blue: f('\x1b[34m', '\x1b[39m'),
1835
- magenta: f('\x1b[35m', '\x1b[39m'),
1836
- cyan: f('\x1b[36m', '\x1b[39m'),
1837
- white: f('\x1b[37m', '\x1b[39m'),
1838
- gray: f('\x1b[90m', '\x1b[39m'),
1839
- bgBlack: f('\x1b[40m', '\x1b[49m'),
1840
- bgRed: f('\x1b[41m', '\x1b[49m'),
1841
- bgGreen: f('\x1b[42m', '\x1b[49m'),
1842
- bgYellow: f('\x1b[43m', '\x1b[49m'),
1843
- bgBlue: f('\x1b[44m', '\x1b[49m'),
1844
- bgMagenta: f('\x1b[45m', '\x1b[49m'),
1845
- bgCyan: f('\x1b[46m', '\x1b[49m'),
1846
- bgWhite: f('\x1b[47m', '\x1b[49m'),
1847
- blackBright: f('\x1b[90m', '\x1b[39m'),
1848
- redBright: f('\x1b[91m', '\x1b[39m'),
1849
- greenBright: f('\x1b[92m', '\x1b[39m'),
1850
- yellowBright: f('\x1b[93m', '\x1b[39m'),
1851
- blueBright: f('\x1b[94m', '\x1b[39m'),
1852
- magentaBright: f('\x1b[95m', '\x1b[39m'),
1853
- cyanBright: f('\x1b[96m', '\x1b[39m'),
1854
- whiteBright: f('\x1b[97m', '\x1b[39m'),
1855
- bgBlackBright: f('\x1b[100m', '\x1b[49m'),
1856
- bgRedBright: f('\x1b[101m', '\x1b[49m'),
1857
- bgGreenBright: f('\x1b[102m', '\x1b[49m'),
1858
- bgYellowBright: f('\x1b[103m', '\x1b[49m'),
1859
- bgBlueBright: f('\x1b[104m', '\x1b[49m'),
1860
- bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
1861
- bgCyanBright: f('\x1b[106m', '\x1b[49m'),
1862
- bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
1863
- link(url, text, noUnderline) {
1864
- if (!isColorSupported)
1865
- return text;
1866
- return `\x1b]8;;${url}\x07${noUnderline ? text : pc.underline(text)}\x1b]8;;\x07`;
1867
- },
1868
- };
1869
- })();
1870
- async function resolveChain(opts = {}) {
1871
- if (!opts.rpcUrl)
1872
- return tempoModerato;
1873
- const { getChainId } = await import('viem/actions');
1874
- const chainId = await getChainId(createClient({ transport: http(opts.rpcUrl) }));
1875
- const allExports = Object.values(await import('viem/chains'));
1876
- const candidates = allExports.filter((c) => typeof c === 'object' && c !== null && 'id' in c && c.id === chainId);
1877
- const found = candidates.find((c) => 'serializers' in c && c.serializers) ?? candidates[0];
1878
- if (!found)
1879
- throw new Error(`Unknown chain ID ${chainId} from RPC ${opts.rpcUrl}`);
1880
- return found;
1881
- }
1882
- function chainName(chain) {
1883
- const chainNames = {
1884
- [tempoMainnet.id]: 'mainnet',
1885
- [tempoModerato.id]: 'testnet',
1886
- };
1887
- return chainNames[chain.id] ?? chain.name;
1888
- }
1889
- const pathUsd = '0x20c0000000000000000000000000000000000000';
1890
- const usdc = '0x20C000000000000000000000b9537d11c60E8b50';
1891
- const mainnetTokens = [pathUsd, usdc];
1892
- const testnetTokens = [
1893
- '0x20c0000000000000000000000000000000000000',
1894
- '0x20c0000000000000000000000000000000000001',
1895
- '0x20c0000000000000000000000000000000000002',
1896
- '0x20c0000000000000000000000000000000000003',
1897
- ];
1898
- function fmtBalance(b, symbol, decimals = 6, opts) {
1899
- const value = Number(b) / 10 ** decimals;
1900
- const [int, dec] = value.toString().split('.');
1901
- const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, '_');
1902
- const sym = opts?.explorerUrl && opts.token
1903
- ? pc.dim(pc.link(`${opts.explorerUrl}/token/${opts.token}`, symbol, true))
1904
- : pc.dim(symbol);
1905
- return `${dec ? `${formatted}.${dec}` : formatted} ${sym}`;
1906
- }
1907
- function isTestnet(chain) {
1908
- return chain.id !== tempoMainnet.id;
1909
- }
1910
- async function fetchTokenInfo(client, token, account) {
1911
- const { Actions } = await import('viem/tempo');
1912
- const [balance, metadata] = await Promise.all([
1913
- Actions.token.getBalance(client, { account, token }).catch(() => 0n),
1914
- Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token })),
1915
- ]);
1916
- const knownSymbols = {
1917
- [pathUsd]: 'PathUSD',
1918
- [usdc]: 'USDC',
1919
- };
1920
- const symbol = knownSymbols[token] ?? metadata.symbol;
1921
- const decimals = 'decimals' in metadata ? metadata.decimals : 6;
1922
- return { balance, symbol, decimals, token };
1923
- }
1924
- function detectTerminalBg(timeoutMs = 100) {
1925
- if (!process.stdin.isTTY || !process.stdout.isTTY)
1926
- return Promise.resolve(undefined);
1927
- return new Promise((resolve) => {
1928
- const wasRaw = process.stdin.isRaw;
1929
- let buf = '';
1930
- const cleanup = () => {
1931
- clearTimeout(timer);
1932
- process.stdin.removeListener('data', onData);
1933
- if (process.stdin.isTTY)
1934
- process.stdin.setRawMode(wasRaw ?? false);
1935
- process.stdin.pause();
1936
- };
1937
- const timer = setTimeout(() => {
1938
- cleanup();
1939
- resolve(undefined);
1940
- }, timeoutMs);
1941
- const onData = (data) => {
1942
- buf += data.toString();
1943
- // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence for terminal background detection
1944
- const match = buf.match(/\x1b\]11;rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
1945
- if (!match)
1946
- return;
1947
- cleanup();
1948
- const parse = (hex) => Number.parseInt(hex.slice(0, 2), 16);
1949
- resolve({ r: parse(match[1]), g: parse(match[2]), b: parse(match[3]) });
1950
- };
1951
- process.stdin.setRawMode(true);
1952
- process.stdin.resume();
1953
- process.stdin.on('data', onData);
1954
- process.stdout.write('\x1b]11;?\x07');
1955
- });
1956
- }
1957
- async function fetchBalanceLines(address, opts) {
1958
- if (opts?.chain) {
1959
- const client = createClient({ chain: opts.chain, transport: http(opts.rpcUrl) });
1960
- const explorerUrl = opts.chain.blockExplorers?.default?.url;
1961
- const label = pc.dim(`(${chainName(opts.chain)})`);
1962
- if (isTestnet(opts.chain)) {
1963
- const results = await Promise.all(testnetTokens.map((token) => fetchTokenInfo(client, token, address)));
1964
- return results
1965
- .filter((t) => t.balance > 0n)
1966
- .map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`);
1967
- }
1968
- const results = await Promise.all(mainnetTokens.map((token) => fetchTokenInfo(client, token, address)));
1969
- return results.map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`);
1970
- }
1971
- const mainnetClient = createClient({
1972
- chain: tempoMainnet,
1973
- transport: http(process.env.MPPX_RPC_URL || undefined),
1974
- });
1975
- const mainnetExplorerUrl = tempoMainnet.blockExplorers?.default?.url;
1976
- const mainnetResults = await Promise.all(mainnetTokens.map((token) => fetchTokenInfo(mainnetClient, token, address)));
1977
- const lines = mainnetResults.map((t) => fmtBalance(t.balance, t.symbol, t.decimals, {
1978
- explorerUrl: mainnetExplorerUrl,
1979
- token: t.token,
1980
- }));
1981
- if (opts?.includeTestnet !== false) {
1982
- const testnetClient = createClient({ chain: tempoModerato, transport: http() });
1983
- const testnetExplorerUrl = tempoModerato.blockExplorers?.default?.url;
1984
- const testnetResults = await Promise.all(testnetTokens.map((token) => fetchTokenInfo(testnetClient, token, address)));
1985
- for (const t of testnetResults) {
1986
- if (t.balance > 0n)
1987
- lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl: testnetExplorerUrl, token: t.token })} ${pc.dim('(testnet)')}`);
1988
- }
1989
- }
1990
- return lines;
1991
- }
1992
- //# sourceMappingURL=cli.js.map