mppx 0.3.3 → 0.3.5

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 (148) hide show
  1. package/README.md +0 -52
  2. package/dist/Challenge.d.ts +8 -0
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +20 -4
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Errors.d.ts +7 -7
  7. package/dist/Errors.d.ts.map +1 -1
  8. package/dist/Errors.js +7 -7
  9. package/dist/Errors.js.map +1 -1
  10. package/dist/cli.js +280 -119
  11. package/dist/cli.js.map +1 -1
  12. package/dist/internal/env.js +2 -2
  13. package/dist/internal/env.js.map +1 -1
  14. package/dist/server/Mppx.d.ts +2 -0
  15. package/dist/server/Mppx.d.ts.map +1 -1
  16. package/dist/server/Mppx.js +4 -3
  17. package/dist/server/Mppx.js.map +1 -1
  18. package/dist/tempo/client/ChannelOps.d.ts +5 -5
  19. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  20. package/dist/tempo/client/ChannelOps.js +3 -3
  21. package/dist/tempo/client/ChannelOps.js.map +1 -1
  22. package/dist/tempo/client/Session.d.ts +2 -2
  23. package/dist/tempo/client/Session.d.ts.map +1 -1
  24. package/dist/tempo/client/Session.js +3 -3
  25. package/dist/tempo/client/Session.js.map +1 -1
  26. package/dist/tempo/client/SessionManager.d.ts +4 -4
  27. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  28. package/dist/tempo/client/SessionManager.js +4 -4
  29. package/dist/tempo/client/SessionManager.js.map +1 -1
  30. package/dist/tempo/index.d.ts +1 -1
  31. package/dist/tempo/index.d.ts.map +1 -1
  32. package/dist/tempo/index.js +1 -1
  33. package/dist/tempo/index.js.map +1 -1
  34. package/dist/tempo/server/Charge.js +1 -1
  35. package/dist/tempo/server/Charge.js.map +1 -1
  36. package/dist/tempo/server/Methods.d.ts +1 -1
  37. package/dist/tempo/server/Methods.d.ts.map +1 -1
  38. package/dist/tempo/server/Session.d.ts +8 -8
  39. package/dist/tempo/server/Session.d.ts.map +1 -1
  40. package/dist/tempo/server/Session.js +24 -24
  41. package/dist/tempo/server/Session.js.map +1 -1
  42. package/dist/tempo/server/index.d.ts +2 -2
  43. package/dist/tempo/server/index.d.ts.map +1 -1
  44. package/dist/tempo/server/index.js +2 -2
  45. package/dist/tempo/server/index.js.map +1 -1
  46. package/dist/tempo/server/internal/transport.d.ts +4 -4
  47. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/transport.js +3 -3
  49. package/dist/tempo/server/internal/transport.js.map +1 -1
  50. package/dist/tempo/session/Chain.d.ts.map +1 -0
  51. package/dist/tempo/session/Chain.js.map +1 -0
  52. package/dist/tempo/session/Channel.d.ts.map +1 -0
  53. package/dist/tempo/session/Channel.js.map +1 -0
  54. package/dist/tempo/session/ChannelStore.d.ts.map +1 -0
  55. package/dist/tempo/session/ChannelStore.js.map +1 -0
  56. package/dist/tempo/session/Receipt.d.ts +22 -0
  57. package/dist/tempo/session/Receipt.d.ts.map +1 -0
  58. package/dist/tempo/{stream → session}/Receipt.js +6 -6
  59. package/dist/tempo/session/Receipt.js.map +1 -0
  60. package/dist/tempo/{stream → session}/Sse.d.ts +7 -7
  61. package/dist/tempo/session/Sse.d.ts.map +1 -0
  62. package/dist/tempo/{stream → session}/Sse.js +4 -4
  63. package/dist/tempo/session/Sse.js.map +1 -0
  64. package/dist/tempo/{stream → session}/Types.d.ts +4 -4
  65. package/dist/tempo/session/Types.d.ts.map +1 -0
  66. package/dist/tempo/{stream → session}/Types.js.map +1 -1
  67. package/dist/tempo/session/Voucher.d.ts.map +1 -0
  68. package/dist/tempo/session/Voucher.js.map +1 -0
  69. package/dist/tempo/{stream → session}/escrow.abi.d.ts.map +1 -1
  70. package/dist/tempo/session/escrow.abi.js.map +1 -0
  71. package/dist/tempo/session/index.d.ts.map +1 -0
  72. package/dist/tempo/session/index.js.map +1 -0
  73. package/package.json +1 -1
  74. package/src/Challenge.test.ts +201 -11
  75. package/src/Challenge.ts +34 -4
  76. package/src/Errors.test.ts +10 -10
  77. package/src/Errors.ts +7 -7
  78. package/src/Store.test.ts +93 -0
  79. package/src/cli.test.ts +234 -38
  80. package/src/cli.ts +340 -135
  81. package/src/client/Transport.test.ts +4 -4
  82. package/src/internal/env.test.ts +42 -0
  83. package/src/internal/env.ts +2 -2
  84. package/src/middlewares/express.test.ts +1 -1
  85. package/src/middlewares/hono.test.ts +1 -1
  86. package/src/middlewares/nextjs.test.ts +1 -1
  87. package/src/server/Mppx.test.ts +173 -0
  88. package/src/server/Mppx.ts +6 -3
  89. package/src/server/Transport.test.ts +6 -6
  90. package/src/tempo/client/ChannelOps.test.ts +2 -2
  91. package/src/tempo/client/ChannelOps.ts +8 -8
  92. package/src/tempo/client/Session.test.ts +3 -3
  93. package/src/tempo/client/Session.ts +9 -9
  94. package/src/tempo/client/SessionManager.test.ts +3 -3
  95. package/src/tempo/client/SessionManager.ts +9 -9
  96. package/src/tempo/index.ts +1 -1
  97. package/src/tempo/server/Charge.ts +1 -1
  98. package/src/tempo/server/Session.test.ts +61 -9
  99. package/src/tempo/server/Session.ts +47 -47
  100. package/src/tempo/server/Sse.test.ts +3 -3
  101. package/src/tempo/server/index.ts +2 -2
  102. package/src/tempo/server/internal/transport.test.ts +285 -0
  103. package/src/tempo/server/internal/transport.ts +6 -6
  104. package/src/tempo/{stream → session}/Chain.test.ts +1 -1
  105. package/src/tempo/{stream → session}/Receipt.test.ts +16 -12
  106. package/src/tempo/{stream → session}/Receipt.ts +9 -9
  107. package/src/tempo/{stream → session}/Sse.test.ts +5 -5
  108. package/src/tempo/{stream → session}/Sse.ts +11 -11
  109. package/src/tempo/{stream → session}/Types.ts +4 -4
  110. package/dist/tempo/stream/Chain.d.ts.map +0 -1
  111. package/dist/tempo/stream/Chain.js.map +0 -1
  112. package/dist/tempo/stream/Channel.d.ts.map +0 -1
  113. package/dist/tempo/stream/Channel.js.map +0 -1
  114. package/dist/tempo/stream/ChannelStore.d.ts.map +0 -1
  115. package/dist/tempo/stream/ChannelStore.js.map +0 -1
  116. package/dist/tempo/stream/Receipt.d.ts +0 -22
  117. package/dist/tempo/stream/Receipt.d.ts.map +0 -1
  118. package/dist/tempo/stream/Receipt.js.map +0 -1
  119. package/dist/tempo/stream/Sse.d.ts.map +0 -1
  120. package/dist/tempo/stream/Sse.js.map +0 -1
  121. package/dist/tempo/stream/Types.d.ts.map +0 -1
  122. package/dist/tempo/stream/Voucher.d.ts.map +0 -1
  123. package/dist/tempo/stream/Voucher.js.map +0 -1
  124. package/dist/tempo/stream/escrow.abi.js.map +0 -1
  125. package/dist/tempo/stream/index.d.ts.map +0 -1
  126. package/dist/tempo/stream/index.js.map +0 -1
  127. /package/dist/tempo/{stream → session}/Chain.d.ts +0 -0
  128. /package/dist/tempo/{stream → session}/Chain.js +0 -0
  129. /package/dist/tempo/{stream → session}/Channel.d.ts +0 -0
  130. /package/dist/tempo/{stream → session}/Channel.js +0 -0
  131. /package/dist/tempo/{stream → session}/ChannelStore.d.ts +0 -0
  132. /package/dist/tempo/{stream → session}/ChannelStore.js +0 -0
  133. /package/dist/tempo/{stream → session}/Types.js +0 -0
  134. /package/dist/tempo/{stream → session}/Voucher.d.ts +0 -0
  135. /package/dist/tempo/{stream → session}/Voucher.js +0 -0
  136. /package/dist/tempo/{stream → session}/escrow.abi.d.ts +0 -0
  137. /package/dist/tempo/{stream → session}/escrow.abi.js +0 -0
  138. /package/dist/tempo/{stream → session}/index.d.ts +0 -0
  139. /package/dist/tempo/{stream → session}/index.js +0 -0
  140. /package/src/tempo/{stream → session}/Chain.ts +0 -0
  141. /package/src/tempo/{stream → session}/Channel.test.ts +0 -0
  142. /package/src/tempo/{stream → session}/Channel.ts +0 -0
  143. /package/src/tempo/{stream → session}/ChannelStore.test.ts +0 -0
  144. /package/src/tempo/{stream → session}/ChannelStore.ts +0 -0
  145. /package/src/tempo/{stream → session}/Voucher.test.ts +0 -0
  146. /package/src/tempo/{stream → session}/Voucher.ts +0 -0
  147. /package/src/tempo/{stream → session}/escrow.abi.ts +0 -0
  148. /package/src/tempo/{stream → session}/index.ts +0 -0
package/dist/cli.js CHANGED
@@ -14,8 +14,9 @@ import { z } from 'zod/mini';
14
14
  import * as Challenge from './Challenge.js';
15
15
  import * as Credential from './Credential.js';
16
16
  import * as Mppx from './client/Mppx.js';
17
+ import { stripe } from './stripe/client/index.js';
17
18
  import { tempo } from './tempo/client/index.js';
18
- import { signVoucher } from './tempo/stream/Voucher.js';
19
+ import { signVoucher } from './tempo/session/Voucher.js';
19
20
  const require = createRequire(import.meta.url);
20
21
  const { name, version } = require('../package.json');
21
22
  const cli = cac(name);
@@ -33,19 +34,16 @@ cli
33
34
  .option('-H, --header <header>', 'Add header (repeatable)')
34
35
  .option('-L, --location', 'Follow redirects')
35
36
  .option('-X, --method <method>', 'HTTP method')
36
- .option('--channel <id>', 'Reuse existing stream channel ID')
37
+ .option('-M, --method-opt <opt>', 'Method-specific option (key=value, repeatable)')
37
38
  .option('--confirm', 'Show confirmation prompts')
38
- .option('--deposit <amount>', 'Deposit amount for stream payments (human-readable units)')
39
39
  .option('--json <json>', 'Send JSON body (sets Content-Type and Accept, implies POST)')
40
40
  .example(`${name} example.com/content`)
41
41
  .example(`${name} example.com/api --json '{"key":"value"}'`)
42
42
  .action(async (rawUrl, rawOptions) => {
43
43
  const options = parseOptions(z.object({
44
44
  account: z.optional(z.string()),
45
- channel: z.optional(z.coerce.string()),
46
45
  confirm: z.optional(z.boolean()),
47
46
  data: z.optional(z.string()),
48
- deposit: z.optional(z.union([z.string(), z.number()])),
49
47
  fail: z.optional(z.boolean()),
50
48
  header: z.optional(z.union([z.string(), z.array(z.string())])),
51
49
  include: z.optional(z.boolean()),
@@ -53,11 +51,13 @@ cli
53
51
  json: z.optional(z.string()),
54
52
  location: z.optional(z.boolean()),
55
53
  method: z.optional(z.string()),
54
+ methodOpt: z.optional(z.union([z.string(), z.array(z.string())])),
56
55
  rpcUrl: z.optional(z.string()),
57
56
  silent: z.optional(z.boolean()),
58
57
  userAgent: z.optional(z.string()),
59
58
  verbose: z.optional(z.boolean()),
60
59
  }), rawOptions);
60
+ const methodOpts = parseMethodOpts(options.methodOpt);
61
61
  if (!rawUrl) {
62
62
  cli.outputHelp();
63
63
  return;
@@ -67,14 +67,6 @@ cli
67
67
  if (silent)
68
68
  options.confirm = false;
69
69
  const accountName = resolveAccountName(options.account);
70
- const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get());
71
- if (!privateKey) {
72
- if (options.account)
73
- console.log(`Account "${accountName}" not found.`);
74
- else
75
- console.log(`No account found.`);
76
- process.exit(1);
77
- }
78
70
  const headers = {};
79
71
  if (options.header) {
80
72
  const headerList = Array.isArray(options.header) ? options.header : [options.header];
@@ -148,22 +140,39 @@ cli
148
140
  console.log((await challengeResponse.text()).replace(/\n+$/, ''));
149
141
  return;
150
142
  }
151
- const account = privateKeyToAccount(privateKey);
152
- const rpcUrl = options.rpcUrl ?? process.env.MPPX_RPC_URL;
153
- const client = createClient({
154
- chain: await resolveChain({ ...options, rpcUrl }),
155
- transport: http(rpcUrl),
156
- });
157
143
  const challenge = Challenge.fromResponse(challengeResponse);
158
- const explorerUrl = client.chain?.blockExplorers?.default?.url;
159
- const shownKeys = new Set();
160
144
  const challengeRequest = challenge.request;
161
145
  const currency = challengeRequest.currency;
162
- const tokenInfo = currency
163
- ? await fetchTokenInfo(client, currency, account.address).catch(() => undefined)
164
- : undefined;
165
- const tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
166
- const tokenDecimals = tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
146
+ const shownKeys = new Set();
147
+ let tokenSymbol = challenge.method === 'stripe' ? (currency?.toUpperCase() ?? '') : (currency ?? '');
148
+ let tokenDecimals = challengeRequest.decimals ?? (challenge.method === 'stripe' ? 2 : 6);
149
+ let explorerUrl;
150
+ // Tempo-specific setup (private key, viem account/client, token info)
151
+ let account;
152
+ let client;
153
+ if (challenge.method === 'tempo') {
154
+ const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get());
155
+ if (!privateKey) {
156
+ if (options.account)
157
+ console.error(`Account "${accountName}" not found.`);
158
+ else
159
+ console.error(`No account found.`);
160
+ process.exit(1);
161
+ }
162
+ account = privateKeyToAccount(privateKey);
163
+ const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
164
+ client = createClient({
165
+ chain: await resolveChain({ ...options, rpcUrl }),
166
+ transport: http(rpcUrl),
167
+ });
168
+ explorerUrl = client.chain?.blockExplorers?.default?.url;
169
+ const tokenInfo = currency
170
+ ? await fetchTokenInfo(client, currency, account.address).catch(() => undefined)
171
+ : undefined;
172
+ tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
173
+ tokenDecimals =
174
+ tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
175
+ }
167
176
  {
168
177
  printResponseHeaders(challengeResponse);
169
178
  const request = challengeRequest;
@@ -266,55 +275,149 @@ cli
266
275
  }
267
276
  }
268
277
  }
269
- const mppx = Mppx.create({
270
- methods: tempo({
271
- account,
272
- getClient: () => client,
273
- deposit: (() => {
274
- if (challenge.intent !== 'session')
275
- return undefined;
276
- const suggestedDeposit = challenge.request
277
- .suggestedDeposit;
278
- const cliDeposit = options.deposit !== undefined ? String(options.deposit) : undefined;
279
- const resolved = suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain) ? '10' : undefined);
280
- if (!resolved) {
281
- console.error('Stream payment requires a deposit. Use --deposit <amount> or connect to testnet.');
282
- process.exit(1);
283
- }
284
- return resolved;
285
- })(),
286
- }),
287
- polyfill: false,
288
- });
289
- const credential = await mppx.createCredential(challengeResponse, (() => {
290
- if (!options.channel)
291
- return undefined;
292
- const idx = process.argv.indexOf('--channel');
293
- const channelId = idx !== -1 ? process.argv[idx + 1] : String(options.channel);
294
- const saved = readChannelCumulative(channelId);
295
- return {
296
- channelId,
297
- ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
298
- };
299
- })());
300
- const streamMd = challenge.request.methodDetails;
301
- let streamChannelId;
302
- let streamEscrowContract;
303
- let streamChainId = 0;
304
- let streamCumulativeAmount = 0n;
278
+ let credential;
279
+ if (challenge.method === 'tempo') {
280
+ if (!account || !client) {
281
+ console.error('Tempo requires a configured account.');
282
+ process.exit(1);
283
+ }
284
+ const tempoOpts = parseOptions(z.object({
285
+ channel: z.optional(z.coerce.string()),
286
+ deposit: z.optional(z.union([z.string(), z.number()])),
287
+ }), methodOpts);
288
+ const mppx = Mppx.create({
289
+ methods: tempo({
290
+ account,
291
+ getClient: () => client,
292
+ deposit: (() => {
293
+ if (challenge.intent !== 'session')
294
+ return undefined;
295
+ const suggestedDeposit = challenge.request
296
+ .suggestedDeposit;
297
+ const cliDeposit = tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined;
298
+ const resolved = suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain) ? '10' : undefined);
299
+ if (!resolved) {
300
+ console.error('Session payment requires a deposit. Use -M deposit=<amount> or connect to testnet.');
301
+ process.exit(1);
302
+ }
303
+ return resolved;
304
+ })(),
305
+ }),
306
+ polyfill: false,
307
+ });
308
+ credential = await mppx.createCredential(challengeResponse, (() => {
309
+ if (!tempoOpts.channel)
310
+ return undefined;
311
+ const channelId = tempoOpts.channel;
312
+ const saved = readChannelCumulative(channelId);
313
+ return {
314
+ channelId,
315
+ ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
316
+ };
317
+ })());
318
+ }
319
+ else if (challenge.method === 'stripe') {
320
+ const stripeOpts = parseOptions(z.object({
321
+ paymentMethod: z.string(),
322
+ }), methodOpts);
323
+ const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY;
324
+ if (!stripeSecretKey) {
325
+ console.error('\nMPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.');
326
+ process.exit(1);
327
+ }
328
+ if (!stripeSecretKey.startsWith('sk_test_')) {
329
+ console.error('\nStripe CLI payments are currently only supported in test mode (sk_test_... keys).');
330
+ process.exit(1);
331
+ }
332
+ const mppx = Mppx.create({
333
+ methods: [
334
+ stripe.charge({
335
+ paymentMethod: stripeOpts.paymentMethod,
336
+ createToken: async ({ paymentMethod, amount, currency, networkId, expiresAt, metadata, }) => {
337
+ const body = new URLSearchParams({
338
+ payment_method: paymentMethod,
339
+ 'usage_limits[currency]': currency,
340
+ 'usage_limits[max_amount]': amount,
341
+ 'usage_limits[expires_at]': expiresAt.toString(),
342
+ });
343
+ if (networkId)
344
+ body.set('seller_details[network_id]', networkId);
345
+ if (metadata) {
346
+ for (const [key, value] of Object.entries(metadata)) {
347
+ body.set(`metadata[${key}]`, value);
348
+ }
349
+ }
350
+ const sptUrl = process.env.MPPX_STRIPE_SPT_URL ??
351
+ 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens';
352
+ const sptHeaders = {
353
+ Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
354
+ 'Content-Type': 'application/x-www-form-urlencoded',
355
+ };
356
+ let response = await globalThis.fetch(sptUrl, {
357
+ method: 'POST',
358
+ headers: sptHeaders,
359
+ body,
360
+ });
361
+ if (!response.ok) {
362
+ const errorBody = (await response.json());
363
+ if ((metadata || networkId) &&
364
+ errorBody.error.message.includes('Received unknown parameter')) {
365
+ const fallbackBody = new URLSearchParams({
366
+ payment_method: paymentMethod,
367
+ 'usage_limits[currency]': currency,
368
+ 'usage_limits[max_amount]': amount,
369
+ 'usage_limits[expires_at]': expiresAt.toString(),
370
+ });
371
+ response = await globalThis.fetch(sptUrl, {
372
+ method: 'POST',
373
+ headers: sptHeaders,
374
+ body: fallbackBody,
375
+ });
376
+ if (!response.ok) {
377
+ const fallbackError = (await response.json());
378
+ throw new Error(`Failed to create SPT: ${fallbackError.error.message}`);
379
+ }
380
+ }
381
+ else {
382
+ throw new Error(`Failed to create SPT: ${errorBody.error.message}`);
383
+ }
384
+ }
385
+ const { id } = (await response.json());
386
+ return id;
387
+ },
388
+ }),
389
+ ],
390
+ polyfill: false,
391
+ });
392
+ credential = await mppx.createCredential(challengeResponse);
393
+ }
394
+ else {
395
+ console.error(`Unsupported payment method: ${challenge.method}`);
396
+ process.exit(1);
397
+ }
398
+ const sessionMd = challenge.request.methodDetails;
399
+ let sessionChannelId;
400
+ let sessionEscrowContract;
401
+ let sessionChainId = 0;
402
+ let sessionCumulativeAmount = 0n;
305
403
  if (challenge.intent === 'session') {
306
404
  const parsed = Credential.deserialize(credential);
307
- streamChannelId = parsed.payload.channelId;
308
- streamChainId = streamMd?.chainId ?? client.chain?.id ?? 0;
309
- streamEscrowContract = streamMd?.escrowContract;
405
+ sessionChannelId = parsed.payload.channelId;
406
+ sessionChainId = sessionMd?.chainId ?? client?.chain?.id ?? 0;
407
+ sessionEscrowContract = sessionMd?.escrowContract;
310
408
  if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
311
- streamCumulativeAmount = BigInt(parsed.payload.cumulativeAmount);
409
+ sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount);
312
410
  if (parsed.payload.action === 'open') {
313
- const depositRaw = challengeRequest.suggestedDeposit ?? options.deposit;
411
+ const depositRaw = challengeRequest.suggestedDeposit;
314
412
  const depositDisplay = depositRaw
315
413
  ? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
316
414
  : '';
317
- info(`\n${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`);
415
+ const prefix = options.confirm ? '' : '\n';
416
+ info(`${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`);
417
+ }
418
+ else {
419
+ const prefix = options.confirm ? '' : '\n';
420
+ info(`${prefix}${pc.dim(`Channel reused ${parsed.payload.channelId}`)}\n`);
318
421
  }
319
422
  }
320
423
  const credentialFetchInit = {
@@ -355,9 +458,9 @@ cli
355
458
  const receiptJson = JSON.parse(Base64.toString(receiptHeader));
356
459
  if (typeof receiptJson.acceptedCumulative === 'string' &&
357
460
  receiptJson.acceptedCumulative) {
358
- streamCumulativeAmount = BigInt(receiptJson.acceptedCumulative);
359
- if (streamChannelId)
360
- writeChannelCumulative(streamChannelId, streamCumulativeAmount);
461
+ sessionCumulativeAmount = BigInt(receiptJson.acceptedCumulative);
462
+ if (sessionChannelId)
463
+ writeChannelCumulative(sessionChannelId, sessionCumulativeAmount);
361
464
  }
362
465
  info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`);
363
466
  const rows = [];
@@ -381,6 +484,14 @@ cli
381
484
  explorerUrl) {
382
485
  rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)]);
383
486
  }
487
+ else if (key === 'reference' &&
488
+ typeof value === 'string' &&
489
+ challenge.method === 'stripe' &&
490
+ value.startsWith('pi_')) {
491
+ const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_');
492
+ const dashboardUrl = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`;
493
+ rows.push([key, pc.link(dashboardUrl, value)]);
494
+ }
384
495
  else
385
496
  rows.push([key, String(value)]);
386
497
  }
@@ -402,17 +513,17 @@ cli
402
513
  const decoder = new TextDecoder();
403
514
  let buffer = '';
404
515
  let currentEvent = '';
405
- const streamCred = challenge.intent === 'session'
516
+ const sessionCred = challenge.intent === 'session'
406
517
  ? Credential.deserialize(credential)
407
518
  : undefined;
408
- const channelId = streamCred?.payload.channelId;
519
+ const channelId = sessionCred?.payload.channelId;
409
520
  const md = challenge.request.methodDetails;
410
- const streamChainId = md?.chainId ?? client.chain?.id ?? 0;
521
+ const sessionChainId = md?.chainId ?? client?.chain?.id ?? 0;
411
522
  const escrowContract = md?.escrowContract;
412
- let cumulativeAmount = streamCred?.payload &&
413
- 'cumulativeAmount' in streamCred.payload &&
414
- streamCred.payload.cumulativeAmount
415
- ? BigInt(streamCred.payload.cumulativeAmount)
523
+ let cumulativeAmount = sessionCred?.payload &&
524
+ 'cumulativeAmount' in sessionCred.payload &&
525
+ sessionCred.payload.cumulativeAmount
526
+ ? BigInt(sessionCred.payload.cumulativeAmount)
416
527
  : 0n;
417
528
  let _voucherSeq = 0;
418
529
  const termBg = verbose ? await detectTerminalBg() : undefined;
@@ -458,12 +569,12 @@ cli
458
569
  if (currentEvent === 'payment-need-voucher' &&
459
570
  channelId &&
460
571
  escrowContract &&
461
- streamChainId) {
572
+ sessionChainId) {
462
573
  try {
463
574
  const event = JSON.parse(data);
464
575
  const required = BigInt(event.requiredCumulative);
465
576
  cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required;
466
- const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, streamChainId);
577
+ const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
467
578
  const voucherCred = Credential.serialize({
468
579
  challenge,
469
580
  payload: {
@@ -472,7 +583,7 @@ cli
472
583
  cumulativeAmount: cumulativeAmount.toString(),
473
584
  signature,
474
585
  },
475
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
586
+ source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
476
587
  });
477
588
  await globalThis.fetch(url, {
478
589
  method: 'POST',
@@ -549,8 +660,8 @@ cli
549
660
  }
550
661
  if (buffer.trim())
551
662
  await processLines([buffer]);
552
- if (channelId && escrowContract && streamChainId) {
553
- const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, streamChainId);
663
+ if (channelId && escrowContract && sessionChainId) {
664
+ const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
554
665
  const closePayload = {
555
666
  action: 'close',
556
667
  channelId,
@@ -560,7 +671,7 @@ cli
560
671
  const closeCred = Credential.serialize({
561
672
  challenge,
562
673
  payload: closePayload,
563
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
674
+ source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
564
675
  });
565
676
  const closeRes = await globalThis.fetch(url, {
566
677
  method: 'POST',
@@ -592,9 +703,9 @@ cli
592
703
  console.log(body);
593
704
  const shouldClose = challenge.intent === 'session' &&
594
705
  credentialResponse.ok &&
595
- streamChannelId &&
596
- streamEscrowContract &&
597
- streamChainId;
706
+ sessionChannelId &&
707
+ sessionEscrowContract &&
708
+ sessionChainId;
598
709
  if (shouldClose && options.confirm) {
599
710
  info('\n');
600
711
  }
@@ -602,17 +713,17 @@ cli
602
713
  info(`${pc.dim('Kept channel open.')}\n`);
603
714
  }
604
715
  else if (shouldClose) {
605
- const signature = await signVoucher(client, account, { channelId: streamChannelId, cumulativeAmount: streamCumulativeAmount }, streamEscrowContract, streamChainId);
716
+ const signature = await signVoucher(client, account, { channelId: sessionChannelId, cumulativeAmount: sessionCumulativeAmount }, sessionEscrowContract, sessionChainId);
606
717
  const closePayload = {
607
718
  action: 'close',
608
- channelId: streamChannelId,
609
- cumulativeAmount: streamCumulativeAmount.toString(),
719
+ channelId: sessionChannelId,
720
+ cumulativeAmount: sessionCumulativeAmount.toString(),
610
721
  signature,
611
722
  };
612
723
  const closeCred = Credential.serialize({
613
724
  challenge,
614
725
  payload: closePayload,
615
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
726
+ source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
616
727
  });
617
728
  const closeRes = await globalThis.fetch(url, {
618
729
  ...fetchInit,
@@ -622,7 +733,7 @@ cli
622
733
  },
623
734
  });
624
735
  if (closeRes.ok) {
625
- deleteChannelState(streamChannelId);
736
+ deleteChannelState(sessionChannelId);
626
737
  const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
627
738
  let closeTxHash;
628
739
  if (closeReceiptHeader) {
@@ -636,16 +747,17 @@ cli
636
747
  const txInfo = closeTxHash && explorerUrl
637
748
  ? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
638
749
  : '';
639
- info(`\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(streamCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
750
+ const closePrefix = options.confirm ? '' : '\n';
751
+ info(`${closePrefix}${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(sessionCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
640
752
  }
641
753
  else {
642
754
  const closeBody = await closeRes.text().catch(() => '');
643
- info(`${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
644
- info(`${pc.dim(` channelId: ${streamChannelId}`)}\n` +
645
- `${pc.dim(` cumulativeAmount: ${streamCumulativeAmount}`)}\n` +
646
- `${pc.dim(` escrowContract: ${streamEscrowContract}`)}\n` +
647
- `${pc.dim(` chainId: ${streamChainId}`)}\n` +
648
- `${pc.dim(` account: ${account.address}`)}\n` +
755
+ info(`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
756
+ info(`${pc.dim(` channelId: ${sessionChannelId}`)}\n` +
757
+ `${pc.dim(` cumulativeAmount: ${sessionCumulativeAmount}`)}\n` +
758
+ `${pc.dim(` escrowContract: ${sessionEscrowContract}`)}\n` +
759
+ `${pc.dim(` chainId: ${sessionChainId}`)}\n` +
760
+ `${pc.dim(` account: ${account?.address}`)}\n` +
649
761
  `${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`);
650
762
  }
651
763
  }
@@ -726,7 +838,11 @@ cli
726
838
  if (accounts.length === 1)
727
839
  createDefaultStore().set(resolvedName);
728
840
  console.log(`Account "${resolvedName}" saved to keychain.`);
729
- console.log(pc.dim(`Address ${account.address}`));
841
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
842
+ const addrDisplay = explorerUrl
843
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
844
+ : account.address;
845
+ console.log(pc.dim(`Address ${addrDisplay}`));
730
846
  resolveChain(options)
731
847
  .then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
732
848
  .then((client) => import('viem/tempo').then(({ Actions }) => Actions.faucet.fund(client, { account }).catch(() => { })));
@@ -761,8 +877,12 @@ cli
761
877
  const account = privateKeyToAccount(key);
762
878
  const balanceLines = await fetchBalanceLines(account.address, { includeTestnet: false });
763
879
  if (!options.yes) {
880
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
881
+ const addrDisplay = explorerUrl
882
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
883
+ : account.address;
764
884
  process.stderr.write(pc.dim(`Delete account "${options.account}"\n`));
765
- process.stderr.write(pc.dim(` Address ${account.address}\n`));
885
+ process.stderr.write(pc.dim(` Address ${addrDisplay}\n`));
766
886
  for (let i = 0; i < balanceLines.length; i++)
767
887
  process.stderr.write(pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`));
768
888
  process.stderr.write(pc.dim('This action cannot be undone\n\n'));
@@ -836,12 +956,16 @@ cli
836
956
  };
837
957
  }));
838
958
  const resolved = entries.filter((e) => e !== undefined);
959
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
839
960
  const maxWidth = Math.max(...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)));
840
961
  for (const entry of resolved) {
841
962
  const isDefault = entry.name === currentDefault;
842
963
  const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name;
843
964
  const width = entry.name.length + (isDefault ? 1 : 0);
844
- console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(entry.address)}`);
965
+ const addrDisplay = explorerUrl
966
+ ? pc.link(`${explorerUrl}/address/${entry.address}`, entry.address)
967
+ : entry.address;
968
+ console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(addrDisplay)}`);
845
969
  }
846
970
  return;
847
971
  }
@@ -857,9 +981,13 @@ cli
857
981
  process.exit(1);
858
982
  }
859
983
  const account = privateKeyToAccount(key);
860
- console.log(`${pc.dim('Address')} ${account.address}`);
861
- const rpcUrl = options.rpcUrl ?? process.env.MPPX_RPC_URL;
862
- const chain = rpcUrl ? await resolveChain({ rpcUrl }) : undefined;
984
+ const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined);
985
+ const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet;
986
+ const explorerUrl = chain.blockExplorers?.default?.url;
987
+ const addrDisplay = explorerUrl
988
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
989
+ : account.address;
990
+ console.log(`${pc.dim('Address')} ${addrDisplay}`);
863
991
  const balanceLines = await fetchBalanceLines(account.address, chain && rpcUrl ? { chain, rpcUrl } : undefined);
864
992
  for (let i = 0; i < balanceLines.length; i++)
865
993
  console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`);
@@ -903,6 +1031,21 @@ catch (err) {
903
1031
  process.exit(1);
904
1032
  }
905
1033
  /////////////////////////////////////////////////////////////////////////////////////////////////
1034
+ function parseMethodOpts(raw) {
1035
+ if (!raw)
1036
+ return {};
1037
+ const list = Array.isArray(raw) ? raw : [raw];
1038
+ const result = {};
1039
+ for (const item of list) {
1040
+ const idx = item.indexOf('=');
1041
+ if (idx === -1) {
1042
+ console.error(`Invalid method option format: ${item} (expected key=value)`);
1043
+ process.exit(1);
1044
+ }
1045
+ result[item.slice(0, idx)] = item.slice(idx + 1);
1046
+ }
1047
+ return result;
1048
+ }
906
1049
  function parseOptions(schema, rawOptions) {
907
1050
  const result = schema.safeParse(rawOptions ?? {});
908
1051
  if (result.success)
@@ -971,7 +1114,7 @@ function createDefaultStore() {
971
1114
  function resolveAccountName(explicit) {
972
1115
  if (explicit)
973
1116
  return explicit;
974
- if (process.env.MPPX_ACCOUNT)
1117
+ if (process.env.MPPX_ACCOUNT?.trim())
975
1118
  return process.env.MPPX_ACCOUNT;
976
1119
  return createDefaultStore().get();
977
1120
  }
@@ -1154,10 +1297,10 @@ const pc = (() => {
1154
1297
  bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
1155
1298
  bgCyanBright: f('\x1b[106m', '\x1b[49m'),
1156
1299
  bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
1157
- link(url, text) {
1300
+ link(url, text, noUnderline) {
1158
1301
  if (!isColorSupported)
1159
1302
  return text;
1160
- return `\x1b]8;;${url}\x07${pc.underline(text)}\x1b]8;;\x07`;
1303
+ return `\x1b]8;;${url}\x07${noUnderline ? text : pc.underline(text)}\x1b]8;;\x07`;
1161
1304
  },
1162
1305
  };
1163
1306
  })();
@@ -1181,17 +1324,22 @@ function chainName(chain) {
1181
1324
  return chainNames[chain.id] ?? chain.name;
1182
1325
  }
1183
1326
  const pathUsd = '0x20c0000000000000000000000000000000000000';
1327
+ const usdc = '0x20C000000000000000000000b9537d11c60E8b50';
1328
+ const mainnetTokens = [pathUsd, usdc];
1184
1329
  const testnetTokens = [
1185
1330
  '0x20c0000000000000000000000000000000000000',
1186
1331
  '0x20c0000000000000000000000000000000000001',
1187
1332
  '0x20c0000000000000000000000000000000000002',
1188
1333
  '0x20c0000000000000000000000000000000000003',
1189
1334
  ];
1190
- function fmtBalance(b, symbol, decimals = 6) {
1335
+ function fmtBalance(b, symbol, decimals = 6, opts) {
1191
1336
  const value = Number(b) / 10 ** decimals;
1192
1337
  const [int, dec] = value.toString().split('.');
1193
1338
  const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, '_');
1194
- return `${dec ? `${formatted}.${dec}` : formatted} ${pc.dim(symbol)}`;
1339
+ const sym = opts?.explorerUrl && opts.token
1340
+ ? pc.dim(pc.link(`${opts.explorerUrl}/token/${opts.token}`, symbol, true))
1341
+ : pc.dim(symbol);
1342
+ return `${dec ? `${formatted}.${dec}` : formatted} ${sym}`;
1195
1343
  }
1196
1344
  function isTestnet(chain) {
1197
1345
  return chain.id !== tempoMainnet.id;
@@ -1202,9 +1350,13 @@ async function fetchTokenInfo(client, token, account) {
1202
1350
  Actions.token.getBalance(client, { account, token }).catch(() => 0n),
1203
1351
  Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token })),
1204
1352
  ]);
1205
- const symbol = token === pathUsd ? 'PathUSD' : metadata.symbol;
1353
+ const knownSymbols = {
1354
+ [pathUsd]: 'PathUSD',
1355
+ [usdc]: 'USDC',
1356
+ };
1357
+ const symbol = knownSymbols[token] ?? metadata.symbol;
1206
1358
  const decimals = 'decimals' in metadata ? metadata.decimals : 6;
1207
- return { balance, symbol, decimals };
1359
+ return { balance, symbol, decimals, token };
1208
1360
  }
1209
1361
  function detectTerminalBg(timeoutMs = 100) {
1210
1362
  if (!process.stdin.isTTY || !process.stdout.isTTY)
@@ -1242,25 +1394,34 @@ function detectTerminalBg(timeoutMs = 100) {
1242
1394
  async function fetchBalanceLines(address, opts) {
1243
1395
  if (opts?.chain) {
1244
1396
  const client = createClient({ chain: opts.chain, transport: http(opts.rpcUrl) });
1397
+ const explorerUrl = opts.chain.blockExplorers?.default?.url;
1245
1398
  const label = pc.dim(`(${chainName(opts.chain)})`);
1246
1399
  if (isTestnet(opts.chain)) {
1247
1400
  const results = await Promise.all(testnetTokens.map((token) => fetchTokenInfo(client, token, address)));
1248
1401
  return results
1249
1402
  .filter((t) => t.balance > 0n)
1250
- .map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals)} ${label}`);
1403
+ .map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`);
1251
1404
  }
1252
- const { balance, symbol, decimals } = await fetchTokenInfo(client, pathUsd, address);
1253
- return [`${fmtBalance(balance, symbol, decimals)} ${label}`];
1405
+ const results = await Promise.all(mainnetTokens.map((token) => fetchTokenInfo(client, token, address)));
1406
+ return results.map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`);
1254
1407
  }
1255
- const mainnetClient = createClient({ chain: tempoMainnet, transport: http() });
1256
- const mainnetInfo = await fetchTokenInfo(mainnetClient, pathUsd, address);
1257
- const lines = [fmtBalance(mainnetInfo.balance, mainnetInfo.symbol, mainnetInfo.decimals)];
1408
+ const mainnetClient = createClient({
1409
+ chain: tempoMainnet,
1410
+ transport: http(process.env.MPPX_RPC_URL || undefined),
1411
+ });
1412
+ const mainnetExplorerUrl = tempoMainnet.blockExplorers?.default?.url;
1413
+ const mainnetResults = await Promise.all(mainnetTokens.map((token) => fetchTokenInfo(mainnetClient, token, address)));
1414
+ const lines = mainnetResults.map((t) => fmtBalance(t.balance, t.symbol, t.decimals, {
1415
+ explorerUrl: mainnetExplorerUrl,
1416
+ token: t.token,
1417
+ }));
1258
1418
  if (opts?.includeTestnet !== false) {
1259
1419
  const testnetClient = createClient({ chain: tempoModerato, transport: http() });
1420
+ const testnetExplorerUrl = tempoModerato.blockExplorers?.default?.url;
1260
1421
  const testnetResults = await Promise.all(testnetTokens.map((token) => fetchTokenInfo(testnetClient, token, address)));
1261
1422
  for (const t of testnetResults) {
1262
1423
  if (t.balance > 0n)
1263
- lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals)} ${pc.dim('(testnet)')}`);
1424
+ lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl: testnetExplorerUrl, token: t.token })} ${pc.dim('(testnet)')}`);
1264
1425
  }
1265
1426
  }
1266
1427
  return lines;