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/src/cli.ts CHANGED
@@ -15,9 +15,10 @@ import { type ZodMiniType, z } from 'zod/mini'
15
15
  import * as Challenge from './Challenge.js'
16
16
  import * as Credential from './Credential.js'
17
17
  import * as Mppx from './client/Mppx.js'
18
+ import { stripe } from './stripe/client/index.js'
18
19
  import { tempo } from './tempo/client/index.js'
19
- import type { StreamCredentialPayload } from './tempo/stream/Types.js'
20
- import { signVoucher } from './tempo/stream/Voucher.js'
20
+ import type { SessionCredentialPayload } from './tempo/session/Types.js'
21
+ import { signVoucher } from './tempo/session/Voucher.js'
21
22
 
22
23
  const require = createRequire(import.meta.url)
23
24
  const { name, version } = require('../package.json') as { name: string; version: string }
@@ -41,9 +42,8 @@ cli
41
42
  .option('-H, --header <header>', 'Add header (repeatable)')
42
43
  .option('-L, --location', 'Follow redirects')
43
44
  .option('-X, --method <method>', 'HTTP method')
44
- .option('--channel <id>', 'Reuse existing stream channel ID')
45
+ .option('-M, --method-opt <opt>', 'Method-specific option (key=value, repeatable)')
45
46
  .option('--confirm', 'Show confirmation prompts')
46
- .option('--deposit <amount>', 'Deposit amount for stream payments (human-readable units)')
47
47
  .option('--json <json>', 'Send JSON body (sets Content-Type and Accept, implies POST)')
48
48
  .example(`${name} example.com/content`)
49
49
  .example(`${name} example.com/api --json '{"key":"value"}'`)
@@ -51,10 +51,8 @@ cli
51
51
  const options = parseOptions(
52
52
  z.object({
53
53
  account: z.optional(z.string()),
54
- channel: z.optional(z.coerce.string()),
55
54
  confirm: z.optional(z.boolean()),
56
55
  data: z.optional(z.string()),
57
- deposit: z.optional(z.union([z.string(), z.number()])),
58
56
  fail: z.optional(z.boolean()),
59
57
  header: z.optional(z.union([z.string(), z.array(z.string())])),
60
58
  include: z.optional(z.boolean()),
@@ -62,6 +60,7 @@ cli
62
60
  json: z.optional(z.string()),
63
61
  location: z.optional(z.boolean()),
64
62
  method: z.optional(z.string()),
63
+ methodOpt: z.optional(z.union([z.string(), z.array(z.string())])),
65
64
  rpcUrl: z.optional(z.string()),
66
65
  silent: z.optional(z.boolean()),
67
66
  userAgent: z.optional(z.string()),
@@ -69,6 +68,7 @@ cli
69
68
  }),
70
69
  rawOptions,
71
70
  )
71
+ const methodOpts = parseMethodOpts(options.methodOpt)
72
72
  if (!rawUrl) {
73
73
  cli.outputHelp()
74
74
  return
@@ -79,12 +79,6 @@ cli
79
79
  if (silent) options.confirm = false
80
80
 
81
81
  const accountName = resolveAccountName(options.account)
82
- const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get())
83
- if (!privateKey) {
84
- if (options.account) console.log(`Account "${accountName}" not found.`)
85
- else console.log(`No account found.`)
86
- process.exit(1)
87
- }
88
82
 
89
83
  const headers: Record<string, string> = {}
90
84
  if (options.header) {
@@ -157,24 +151,43 @@ cli
157
151
  return
158
152
  }
159
153
 
160
- const account = privateKeyToAccount(privateKey as `0x${string}`)
161
- const rpcUrl = options.rpcUrl ?? process.env.MPPX_RPC_URL
162
- const client = createClient({
163
- chain: await resolveChain({ ...options, rpcUrl }),
164
- transport: http(rpcUrl),
165
- })
166
-
167
154
  const challenge = Challenge.fromResponse(challengeResponse)
168
- const explorerUrl = client.chain?.blockExplorers?.default?.url
169
- const shownKeys = new Set<string>()
170
155
  const challengeRequest = challenge.request as Record<string, unknown>
171
156
  const currency = challengeRequest.currency as string | undefined
172
- const tokenInfo = currency
173
- ? await fetchTokenInfo(client, currency as Address, account.address).catch(() => undefined)
174
- : undefined
175
- const tokenSymbol = tokenInfo?.symbol ?? currency ?? ''
176
- const tokenDecimals =
177
- tokenInfo?.decimals ?? (challengeRequest.decimals as number | undefined) ?? 6
157
+ const shownKeys = new Set<string>()
158
+
159
+ let tokenSymbol =
160
+ challenge.method === 'stripe' ? (currency?.toUpperCase() ?? '') : (currency ?? '')
161
+ let tokenDecimals =
162
+ (challengeRequest.decimals as number | undefined) ?? (challenge.method === 'stripe' ? 2 : 6)
163
+ let explorerUrl: string | undefined
164
+
165
+ // Tempo-specific setup (private key, viem account/client, token info)
166
+ let account: ReturnType<typeof privateKeyToAccount> | undefined
167
+ let client: ReturnType<typeof createClient> | undefined
168
+ if (challenge.method === 'tempo') {
169
+ const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get())
170
+ if (!privateKey) {
171
+ if (options.account) console.error(`Account "${accountName}" not found.`)
172
+ else console.error(`No account found.`)
173
+ process.exit(1)
174
+ }
175
+ account = privateKeyToAccount(privateKey as `0x${string}`)
176
+ const rpcUrl = options.rpcUrl ?? process.env.RPC_URL
177
+ client = createClient({
178
+ chain: await resolveChain({ ...options, rpcUrl }),
179
+ transport: http(rpcUrl),
180
+ })
181
+ explorerUrl = client.chain?.blockExplorers?.default?.url
182
+ const tokenInfo = currency
183
+ ? await fetchTokenInfo(client, currency as Address, account.address).catch(
184
+ () => undefined,
185
+ )
186
+ : undefined
187
+ tokenSymbol = tokenInfo?.symbol ?? currency ?? ''
188
+ tokenDecimals =
189
+ tokenInfo?.decimals ?? (challengeRequest.decimals as number | undefined) ?? 6
190
+ }
178
191
 
179
192
  {
180
193
  printResponseHeaders(challengeResponse)
@@ -276,66 +289,178 @@ cli
276
289
  }
277
290
  }
278
291
 
279
- const mppx = Mppx.create({
280
- methods: tempo({
281
- account,
282
- getClient: () => client,
283
- deposit: (() => {
284
- if (challenge.intent !== 'session') return undefined
285
- const suggestedDeposit = (challenge.request as Record<string, unknown>)
286
- .suggestedDeposit as string | undefined
287
- const cliDeposit = options.deposit !== undefined ? String(options.deposit) : undefined
288
- const resolved =
289
- suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain!) ? '10' : undefined)
290
- if (!resolved) {
291
- console.error(
292
- 'Stream payment requires a deposit. Use --deposit <amount> or connect to testnet.',
293
- )
294
- process.exit(1)
292
+ let credential: string
293
+ if (challenge.method === 'tempo') {
294
+ if (!account || !client) {
295
+ console.error('Tempo requires a configured account.')
296
+ process.exit(1)
297
+ }
298
+ const tempoOpts = parseOptions(
299
+ z.object({
300
+ channel: z.optional(z.coerce.string()),
301
+ deposit: z.optional(z.union([z.string(), z.number()])),
302
+ }),
303
+ methodOpts,
304
+ )
305
+ const mppx = Mppx.create({
306
+ methods: tempo({
307
+ account,
308
+ getClient: () => client!,
309
+ deposit: (() => {
310
+ if (challenge.intent !== 'session') return undefined
311
+ const suggestedDeposit = (challenge.request as Record<string, unknown>)
312
+ .suggestedDeposit as string | undefined
313
+ const cliDeposit =
314
+ tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined
315
+ const resolved =
316
+ suggestedDeposit ?? cliDeposit ?? (isTestnet(client!.chain!) ? '10' : undefined)
317
+ if (!resolved) {
318
+ console.error(
319
+ 'Session payment requires a deposit. Use -M deposit=<amount> or connect to testnet.',
320
+ )
321
+ process.exit(1)
322
+ }
323
+ return resolved
324
+ })(),
325
+ }),
326
+ polyfill: false,
327
+ })
328
+ credential = await mppx.createCredential(
329
+ challengeResponse,
330
+ (() => {
331
+ if (!tempoOpts.channel) return undefined
332
+ const channelId = tempoOpts.channel
333
+ const saved = readChannelCumulative(channelId)
334
+ return {
335
+ channelId,
336
+ ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
295
337
  }
296
- return resolved
297
338
  })(),
298
- }),
299
- polyfill: false,
300
- })
301
-
302
- const credential = await mppx.createCredential(
303
- challengeResponse,
304
- (() => {
305
- if (!options.channel) return undefined
306
- const idx = process.argv.indexOf('--channel')
307
- const channelId = idx !== -1 ? process.argv[idx + 1]! : String(options.channel)
308
- const saved = readChannelCumulative(channelId)
309
- return {
310
- channelId,
311
- ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
312
- }
313
- })(),
314
- )
339
+ )
340
+ } else if (challenge.method === 'stripe') {
341
+ const stripeOpts = parseOptions(
342
+ z.object({
343
+ paymentMethod: z.string(),
344
+ }),
345
+ methodOpts,
346
+ )
347
+ const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY
348
+ if (!stripeSecretKey) {
349
+ console.error(
350
+ '\nMPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
351
+ )
352
+ process.exit(1)
353
+ }
354
+ if (!stripeSecretKey.startsWith('sk_test_')) {
355
+ console.error(
356
+ '\nStripe CLI payments are currently only supported in test mode (sk_test_... keys).',
357
+ )
358
+ process.exit(1)
359
+ }
360
+ const mppx = Mppx.create({
361
+ methods: [
362
+ stripe.charge({
363
+ paymentMethod: stripeOpts.paymentMethod,
364
+ createToken: async ({
365
+ paymentMethod,
366
+ amount,
367
+ currency,
368
+ networkId,
369
+ expiresAt,
370
+ metadata,
371
+ }) => {
372
+ const body = new URLSearchParams({
373
+ payment_method: paymentMethod!,
374
+ 'usage_limits[currency]': currency,
375
+ 'usage_limits[max_amount]': amount,
376
+ 'usage_limits[expires_at]': expiresAt.toString(),
377
+ })
378
+ if (networkId) body.set('seller_details[network_id]', networkId)
379
+ if (metadata) {
380
+ for (const [key, value] of Object.entries(metadata)) {
381
+ body.set(`metadata[${key}]`, value)
382
+ }
383
+ }
384
+ const sptUrl =
385
+ process.env.MPPX_STRIPE_SPT_URL ??
386
+ 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens'
387
+ const sptHeaders = {
388
+ Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
389
+ 'Content-Type': 'application/x-www-form-urlencoded',
390
+ }
391
+ let response = await globalThis.fetch(sptUrl, {
392
+ method: 'POST',
393
+ headers: sptHeaders,
394
+ body,
395
+ })
396
+ if (!response.ok) {
397
+ const errorBody = (await response.json()) as { error: { message: string } }
398
+ if (
399
+ (metadata || networkId) &&
400
+ errorBody.error.message.includes('Received unknown parameter')
401
+ ) {
402
+ const fallbackBody = new URLSearchParams({
403
+ payment_method: paymentMethod!,
404
+ 'usage_limits[currency]': currency,
405
+ 'usage_limits[max_amount]': amount,
406
+ 'usage_limits[expires_at]': expiresAt.toString(),
407
+ })
408
+ response = await globalThis.fetch(sptUrl, {
409
+ method: 'POST',
410
+ headers: sptHeaders,
411
+ body: fallbackBody,
412
+ })
413
+ if (!response.ok) {
414
+ const fallbackError = (await response.json()) as {
415
+ error: { message: string }
416
+ }
417
+ throw new Error(`Failed to create SPT: ${fallbackError.error.message}`)
418
+ }
419
+ } else {
420
+ throw new Error(`Failed to create SPT: ${errorBody.error.message}`)
421
+ }
422
+ }
423
+ const { id } = (await response.json()) as { id: string }
424
+ return id
425
+ },
426
+ }),
427
+ ],
428
+ polyfill: false,
429
+ })
430
+ credential = await mppx.createCredential(challengeResponse)
431
+ } else {
432
+ console.error(`Unsupported payment method: ${challenge.method}`)
433
+ process.exit(1)
434
+ }
315
435
 
316
- const streamMd = challenge.request.methodDetails as
436
+ const sessionMd = challenge.request.methodDetails as
317
437
  | { escrowContract?: string; chainId?: number }
318
438
  | undefined
319
- let streamChannelId: `0x${string}` | undefined
320
- let streamEscrowContract: Address | undefined
321
- let streamChainId = 0
322
- let streamCumulativeAmount = 0n
439
+ let sessionChannelId: `0x${string}` | undefined
440
+ let sessionEscrowContract: Address | undefined
441
+ let sessionChainId = 0
442
+ let sessionCumulativeAmount = 0n
323
443
 
324
444
  if (challenge.intent === 'session') {
325
- const parsed = Credential.deserialize<StreamCredentialPayload>(credential)
326
- streamChannelId = parsed.payload.channelId
327
- streamChainId = streamMd?.chainId ?? client.chain?.id ?? 0
328
- streamEscrowContract = streamMd?.escrowContract as Address | undefined
445
+ const parsed = Credential.deserialize<SessionCredentialPayload>(credential)
446
+ sessionChannelId = parsed.payload.channelId
447
+ sessionChainId = sessionMd?.chainId ?? client?.chain?.id ?? 0
448
+ sessionEscrowContract = sessionMd?.escrowContract as Address | undefined
329
449
  if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
330
- streamCumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
450
+ sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
331
451
 
332
452
  if (parsed.payload.action === 'open') {
333
- const depositRaw =
334
- (challengeRequest.suggestedDeposit as string | undefined) ?? options.deposit
453
+ const depositRaw = challengeRequest.suggestedDeposit as string | undefined
335
454
  const depositDisplay = depositRaw
336
455
  ? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
337
456
  : ''
338
- info(`\n${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`)
457
+ const prefix = options.confirm ? '' : '\n'
458
+ info(
459
+ `${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`,
460
+ )
461
+ } else {
462
+ const prefix = options.confirm ? '' : '\n'
463
+ info(`${prefix}${pc.dim(`Channel reused ${parsed.payload.channelId}`)}\n`)
339
464
  }
340
465
  }
341
466
 
@@ -379,8 +504,9 @@ cli
379
504
  typeof receiptJson.acceptedCumulative === 'string' &&
380
505
  receiptJson.acceptedCumulative
381
506
  ) {
382
- streamCumulativeAmount = BigInt(receiptJson.acceptedCumulative)
383
- if (streamChannelId) writeChannelCumulative(streamChannelId, streamCumulativeAmount)
507
+ sessionCumulativeAmount = BigInt(receiptJson.acceptedCumulative)
508
+ if (sessionChannelId)
509
+ writeChannelCumulative(sessionChannelId, sessionCumulativeAmount)
384
510
  }
385
511
  info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`)
386
512
  const rows: [string, string][] = []
@@ -402,6 +528,15 @@ cli
402
528
  explorerUrl
403
529
  ) {
404
530
  rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)])
531
+ } else if (
532
+ key === 'reference' &&
533
+ typeof value === 'string' &&
534
+ challenge.method === 'stripe' &&
535
+ value.startsWith('pi_')
536
+ ) {
537
+ const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_')
538
+ const dashboardUrl = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`
539
+ rows.push([key, pc.link(dashboardUrl, value)])
405
540
  } else rows.push([key, String(value)])
406
541
  }
407
542
  rows.sort(([a], [b]) => a.localeCompare(b))
@@ -421,21 +556,21 @@ cli
421
556
  let buffer = ''
422
557
  let currentEvent = ''
423
558
 
424
- const streamCred =
559
+ const sessionCred =
425
560
  challenge.intent === 'session'
426
- ? Credential.deserialize<StreamCredentialPayload>(credential)
561
+ ? Credential.deserialize<SessionCredentialPayload>(credential)
427
562
  : undefined
428
- const channelId = streamCred?.payload.channelId
563
+ const channelId = sessionCred?.payload.channelId
429
564
  const md = challenge.request.methodDetails as
430
565
  | { escrowContract?: string; chainId?: number }
431
566
  | undefined
432
- const streamChainId = md?.chainId ?? client.chain?.id ?? 0
567
+ const sessionChainId = md?.chainId ?? client?.chain?.id ?? 0
433
568
  const escrowContract = md?.escrowContract as Address | undefined
434
569
  let cumulativeAmount =
435
- streamCred?.payload &&
436
- 'cumulativeAmount' in streamCred.payload &&
437
- streamCred.payload.cumulativeAmount
438
- ? BigInt(streamCred.payload.cumulativeAmount)
570
+ sessionCred?.payload &&
571
+ 'cumulativeAmount' in sessionCred.payload &&
572
+ sessionCred.payload.cumulativeAmount
573
+ ? BigInt(sessionCred.payload.cumulativeAmount)
439
574
  : 0n
440
575
  let _voucherSeq = 0
441
576
 
@@ -481,7 +616,7 @@ cli
481
616
  currentEvent === 'payment-need-voucher' &&
482
617
  channelId &&
483
618
  escrowContract &&
484
- streamChainId
619
+ sessionChainId
485
620
  ) {
486
621
  try {
487
622
  const event = JSON.parse(data) as {
@@ -492,11 +627,11 @@ cli
492
627
  cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required
493
628
 
494
629
  const signature = await signVoucher(
495
- client,
496
- account,
630
+ client!,
631
+ account!,
497
632
  { channelId, cumulativeAmount },
498
633
  escrowContract,
499
- streamChainId,
634
+ sessionChainId,
500
635
  )
501
636
  const voucherCred = Credential.serialize({
502
637
  challenge,
@@ -506,7 +641,7 @@ cli
506
641
  cumulativeAmount: cumulativeAmount.toString(),
507
642
  signature,
508
643
  },
509
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
644
+ source: `did:pkh:eip155:${sessionChainId}:${account!.address}`,
510
645
  })
511
646
  await globalThis.fetch(url, {
512
647
  method: 'POST',
@@ -583,15 +718,15 @@ cli
583
718
  }
584
719
  if (buffer.trim()) await processLines([buffer])
585
720
 
586
- if (channelId && escrowContract && streamChainId) {
721
+ if (channelId && escrowContract && sessionChainId) {
587
722
  const signature = await signVoucher(
588
- client,
589
- account,
723
+ client!,
724
+ account!,
590
725
  { channelId, cumulativeAmount },
591
726
  escrowContract,
592
- streamChainId,
727
+ sessionChainId,
593
728
  )
594
- const closePayload: StreamCredentialPayload = {
729
+ const closePayload: SessionCredentialPayload = {
595
730
  action: 'close',
596
731
  channelId,
597
732
  cumulativeAmount: cumulativeAmount.toString(),
@@ -600,7 +735,7 @@ cli
600
735
  const closeCred = Credential.serialize({
601
736
  challenge,
602
737
  payload: closePayload,
603
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
738
+ source: `did:pkh:eip155:${sessionChainId}:${account!.address}`,
604
739
  })
605
740
  const closeRes = await globalThis.fetch(url, {
606
741
  method: 'POST',
@@ -638,9 +773,9 @@ cli
638
773
  const shouldClose =
639
774
  challenge.intent === 'session' &&
640
775
  credentialResponse.ok &&
641
- streamChannelId &&
642
- streamEscrowContract &&
643
- streamChainId
776
+ sessionChannelId &&
777
+ sessionEscrowContract &&
778
+ sessionChainId
644
779
  if (shouldClose && options.confirm) {
645
780
  info('\n')
646
781
  }
@@ -648,22 +783,22 @@ cli
648
783
  info(`${pc.dim('Kept channel open.')}\n`)
649
784
  } else if (shouldClose) {
650
785
  const signature = await signVoucher(
651
- client,
652
- account,
653
- { channelId: streamChannelId!, cumulativeAmount: streamCumulativeAmount },
654
- streamEscrowContract!,
655
- streamChainId,
786
+ client!,
787
+ account!,
788
+ { channelId: sessionChannelId!, cumulativeAmount: sessionCumulativeAmount },
789
+ sessionEscrowContract!,
790
+ sessionChainId,
656
791
  )
657
- const closePayload: StreamCredentialPayload = {
792
+ const closePayload: SessionCredentialPayload = {
658
793
  action: 'close',
659
- channelId: streamChannelId!,
660
- cumulativeAmount: streamCumulativeAmount.toString(),
794
+ channelId: sessionChannelId!,
795
+ cumulativeAmount: sessionCumulativeAmount.toString(),
661
796
  signature,
662
797
  }
663
798
  const closeCred = Credential.serialize({
664
799
  challenge,
665
800
  payload: closePayload,
666
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
801
+ source: `did:pkh:eip155:${sessionChainId}:${account!.address}`,
667
802
  })
668
803
  const closeRes = await globalThis.fetch(url, {
669
804
  ...fetchInit,
@@ -673,7 +808,7 @@ cli
673
808
  },
674
809
  })
675
810
  if (closeRes.ok) {
676
- deleteChannelState(streamChannelId!)
811
+ deleteChannelState(sessionChannelId!)
677
812
  const closeReceiptHeader = closeRes.headers.get('Payment-Receipt')
678
813
  let closeTxHash: string | undefined
679
814
  if (closeReceiptHeader) {
@@ -689,20 +824,21 @@ cli
689
824
  closeTxHash && explorerUrl
690
825
  ? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
691
826
  : ''
827
+ const closePrefix = options.confirm ? '' : '\n'
692
828
  info(
693
- `\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(streamCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`,
829
+ `${closePrefix}${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(sessionCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`,
694
830
  )
695
831
  } else {
696
832
  const closeBody = await closeRes.text().catch(() => '')
697
833
  info(
698
- `${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
834
+ `\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
699
835
  )
700
836
  info(
701
- `${pc.dim(` channelId: ${streamChannelId}`)}\n` +
702
- `${pc.dim(` cumulativeAmount: ${streamCumulativeAmount}`)}\n` +
703
- `${pc.dim(` escrowContract: ${streamEscrowContract}`)}\n` +
704
- `${pc.dim(` chainId: ${streamChainId}`)}\n` +
705
- `${pc.dim(` account: ${account.address}`)}\n` +
837
+ `${pc.dim(` channelId: ${sessionChannelId}`)}\n` +
838
+ `${pc.dim(` cumulativeAmount: ${sessionCumulativeAmount}`)}\n` +
839
+ `${pc.dim(` escrowContract: ${sessionEscrowContract}`)}\n` +
840
+ `${pc.dim(` chainId: ${sessionChainId}`)}\n` +
841
+ `${pc.dim(` account: ${account?.address}`)}\n` +
706
842
  `${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`,
707
843
  )
708
844
  }
@@ -784,7 +920,11 @@ cli
784
920
  const accounts = await createKeychain().list()
785
921
  if (accounts.length === 1) createDefaultStore().set(resolvedName)
786
922
  console.log(`Account "${resolvedName}" saved to keychain.`)
787
- console.log(pc.dim(`Address ${account.address}`))
923
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url
924
+ const addrDisplay = explorerUrl
925
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
926
+ : account.address
927
+ console.log(pc.dim(`Address ${addrDisplay}`))
788
928
  resolveChain(options)
789
929
  .then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
790
930
  .then((client) =>
@@ -823,8 +963,12 @@ cli
823
963
  const account = privateKeyToAccount(key as `0x${string}`)
824
964
  const balanceLines = await fetchBalanceLines(account.address, { includeTestnet: false })
825
965
  if (!options.yes) {
966
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url
967
+ const addrDisplay = explorerUrl
968
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
969
+ : account.address
826
970
  process.stderr.write(pc.dim(`Delete account "${options.account}"\n`))
827
- process.stderr.write(pc.dim(` Address ${account.address}\n`))
971
+ process.stderr.write(pc.dim(` Address ${addrDisplay}\n`))
828
972
  for (let i = 0; i < balanceLines.length; i++)
829
973
  process.stderr.write(
830
974
  pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`),
@@ -897,6 +1041,7 @@ cli
897
1041
  }),
898
1042
  )
899
1043
  const resolved = entries.filter((e) => e !== undefined)
1044
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url
900
1045
  const maxWidth = Math.max(
901
1046
  ...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)),
902
1047
  )
@@ -904,7 +1049,10 @@ cli
904
1049
  const isDefault = entry.name === currentDefault
905
1050
  const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name
906
1051
  const width = entry.name.length + (isDefault ? 1 : 0)
907
- console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(entry.address)}`)
1052
+ const addrDisplay = explorerUrl
1053
+ ? pc.link(`${explorerUrl}/address/${entry.address}`, entry.address)
1054
+ : entry.address
1055
+ console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(addrDisplay)}`)
908
1056
  }
909
1057
  return
910
1058
  }
@@ -918,10 +1066,14 @@ cli
918
1066
  process.exit(1)
919
1067
  }
920
1068
  const account = privateKeyToAccount(key as `0x${string}`)
921
- console.log(`${pc.dim('Address')} ${account.address}`)
1069
+ const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined)
1070
+ const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet
1071
+ const explorerUrl = chain.blockExplorers?.default?.url
1072
+ const addrDisplay = explorerUrl
1073
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
1074
+ : account.address
1075
+ console.log(`${pc.dim('Address')} ${addrDisplay}`)
922
1076
 
923
- const rpcUrl = options.rpcUrl ?? process.env.MPPX_RPC_URL
924
- const chain = rpcUrl ? await resolveChain({ rpcUrl }) : undefined
925
1077
  const balanceLines = await fetchBalanceLines(
926
1078
  account.address,
927
1079
  chain && rpcUrl ? { chain, rpcUrl } : undefined,
@@ -971,6 +1123,21 @@ try {
971
1123
 
972
1124
  /////////////////////////////////////////////////////////////////////////////////////////////////
973
1125
 
1126
+ function parseMethodOpts(raw: string | string[] | undefined): Record<string, string> {
1127
+ if (!raw) return {}
1128
+ const list = Array.isArray(raw) ? raw : [raw]
1129
+ const result: Record<string, string> = {}
1130
+ for (const item of list) {
1131
+ const idx = item.indexOf('=')
1132
+ if (idx === -1) {
1133
+ console.error(`Invalid method option format: ${item} (expected key=value)`)
1134
+ process.exit(1)
1135
+ }
1136
+ result[item.slice(0, idx)] = item.slice(idx + 1)
1137
+ }
1138
+ return result
1139
+ }
1140
+
974
1141
  function parseOptions<const schema extends ZodMiniType>(
975
1142
  schema: schema,
976
1143
  rawOptions: unknown,
@@ -1054,7 +1221,7 @@ function createDefaultStore() {
1054
1221
 
1055
1222
  function resolveAccountName(explicit?: string): string {
1056
1223
  if (explicit) return explicit
1057
- if (process.env.MPPX_ACCOUNT) return process.env.MPPX_ACCOUNT
1224
+ if (process.env.MPPX_ACCOUNT?.trim()) return process.env.MPPX_ACCOUNT
1058
1225
  return createDefaultStore().get()
1059
1226
  }
1060
1227
 
@@ -1238,9 +1405,9 @@ const pc = (() => {
1238
1405
  bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
1239
1406
  bgCyanBright: f('\x1b[106m', '\x1b[49m'),
1240
1407
  bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
1241
- link(url: string, text: string) {
1408
+ link(url: string, text: string, noUnderline?: boolean) {
1242
1409
  if (!isColorSupported) return text
1243
- return `\x1b]8;;${url}\x07${pc.underline(text)}\x1b]8;;\x07`
1410
+ return `\x1b]8;;${url}\x07${noUnderline ? text : pc.underline(text)}\x1b]8;;\x07`
1244
1411
  },
1245
1412
  }
1246
1413
  })()
@@ -1268,6 +1435,8 @@ function chainName(chain: { id: number; name: string }) {
1268
1435
  }
1269
1436
 
1270
1437
  const pathUsd = '0x20c0000000000000000000000000000000000000' as Address
1438
+ const usdc = '0x20C000000000000000000000b9537d11c60E8b50' as Address
1439
+ const mainnetTokens = [pathUsd, usdc] as const
1271
1440
  const testnetTokens = [
1272
1441
  '0x20c0000000000000000000000000000000000000',
1273
1442
  '0x20c0000000000000000000000000000000000001',
@@ -1275,11 +1444,20 @@ const testnetTokens = [
1275
1444
  '0x20c0000000000000000000000000000000000003',
1276
1445
  ] as const
1277
1446
 
1278
- function fmtBalance(b: bigint, symbol: string, decimals = 6) {
1447
+ function fmtBalance(
1448
+ b: bigint,
1449
+ symbol: string,
1450
+ decimals = 6,
1451
+ opts?: { explorerUrl?: string | undefined; token?: string | undefined },
1452
+ ) {
1279
1453
  const value = Number(b) / 10 ** decimals
1280
1454
  const [int, dec] = value.toString().split('.')
1281
1455
  const formatted = int!.replace(/\B(?=(\d{3})+(?!\d))/g, '_')
1282
- return `${dec ? `${formatted}.${dec}` : formatted} ${pc.dim(symbol)}`
1456
+ const sym =
1457
+ opts?.explorerUrl && opts.token
1458
+ ? pc.dim(pc.link(`${opts.explorerUrl}/token/${opts.token}`, symbol, true))
1459
+ : pc.dim(symbol)
1460
+ return `${dec ? `${formatted}.${dec}` : formatted} ${sym}`
1283
1461
  }
1284
1462
 
1285
1463
  function isTestnet(chain: Chain) {
@@ -1296,9 +1474,13 @@ async function fetchTokenInfo(
1296
1474
  Actions.token.getBalance(client, { account, token }).catch(() => 0n),
1297
1475
  Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token as string })),
1298
1476
  ])
1299
- const symbol = token === pathUsd ? 'PathUSD' : metadata.symbol
1477
+ const knownSymbols: Record<string, string> = {
1478
+ [pathUsd]: 'PathUSD',
1479
+ [usdc]: 'USDC',
1480
+ }
1481
+ const symbol = knownSymbols[token] ?? metadata.symbol
1300
1482
  const decimals = 'decimals' in metadata ? metadata.decimals : 6
1301
- return { balance, symbol, decimals }
1483
+ return { balance, symbol, decimals, token }
1302
1484
  }
1303
1485
 
1304
1486
  function detectTerminalBg(
@@ -1340,6 +1522,7 @@ async function fetchBalanceLines(
1340
1522
  ): Promise<string[]> {
1341
1523
  if (opts?.chain) {
1342
1524
  const client = createClient({ chain: opts.chain, transport: http(opts.rpcUrl) })
1525
+ const explorerUrl = opts.chain.blockExplorers?.default?.url
1343
1526
  const label = pc.dim(`(${chainName(opts.chain)})`)
1344
1527
  if (isTestnet(opts.chain)) {
1345
1528
  const results = await Promise.all(
@@ -1347,24 +1530,46 @@ async function fetchBalanceLines(
1347
1530
  )
1348
1531
  return results
1349
1532
  .filter((t) => t.balance > 0n)
1350
- .map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals)} ${label}`)
1533
+ .map(
1534
+ (t) =>
1535
+ `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`,
1536
+ )
1351
1537
  }
1352
- const { balance, symbol, decimals } = await fetchTokenInfo(client, pathUsd, address)
1353
- return [`${fmtBalance(balance, symbol, decimals)} ${label}`]
1538
+ const results = await Promise.all(
1539
+ mainnetTokens.map((token) => fetchTokenInfo(client, token, address)),
1540
+ )
1541
+ return results.map(
1542
+ (t) =>
1543
+ `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`,
1544
+ )
1354
1545
  }
1355
1546
 
1356
- const mainnetClient = createClient({ chain: tempoMainnet, transport: http() })
1357
- const mainnetInfo = await fetchTokenInfo(mainnetClient, pathUsd, address)
1358
- const lines = [fmtBalance(mainnetInfo.balance, mainnetInfo.symbol, mainnetInfo.decimals)]
1547
+ const mainnetClient = createClient({
1548
+ chain: tempoMainnet,
1549
+ transport: http(process.env.MPPX_RPC_URL || undefined),
1550
+ })
1551
+ const mainnetExplorerUrl = tempoMainnet.blockExplorers?.default?.url
1552
+ const mainnetResults = await Promise.all(
1553
+ mainnetTokens.map((token) => fetchTokenInfo(mainnetClient, token, address)),
1554
+ )
1555
+ const lines = mainnetResults.map((t) =>
1556
+ fmtBalance(t.balance, t.symbol, t.decimals, {
1557
+ explorerUrl: mainnetExplorerUrl,
1558
+ token: t.token,
1559
+ }),
1560
+ )
1359
1561
 
1360
1562
  if (opts?.includeTestnet !== false) {
1361
1563
  const testnetClient = createClient({ chain: tempoModerato, transport: http() })
1564
+ const testnetExplorerUrl = tempoModerato.blockExplorers?.default?.url
1362
1565
  const testnetResults = await Promise.all(
1363
1566
  testnetTokens.map((token) => fetchTokenInfo(testnetClient, token, address)),
1364
1567
  )
1365
1568
  for (const t of testnetResults) {
1366
1569
  if (t.balance > 0n)
1367
- lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals)} ${pc.dim('(testnet)')}`)
1570
+ lines.push(
1571
+ `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl: testnetExplorerUrl, token: t.token })} ${pc.dim('(testnet)')}`,
1572
+ )
1368
1573
  }
1369
1574
  }
1370
1575