mppx 0.3.2 → 0.3.4

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 (140) hide show
  1. package/dist/Errors.d.ts +7 -7
  2. package/dist/Errors.d.ts.map +1 -1
  3. package/dist/Errors.js +7 -7
  4. package/dist/Errors.js.map +1 -1
  5. package/dist/cli.js +98 -64
  6. package/dist/cli.js.map +1 -1
  7. package/dist/internal/env.d.ts +19 -0
  8. package/dist/internal/env.d.ts.map +1 -0
  9. package/dist/internal/env.js +55 -0
  10. package/dist/internal/env.js.map +1 -0
  11. package/dist/server/Mppx.d.ts +2 -2
  12. package/dist/server/Mppx.d.ts.map +1 -1
  13. package/dist/server/Mppx.js +2 -1
  14. package/dist/server/Mppx.js.map +1 -1
  15. package/dist/tempo/client/ChannelOps.d.ts +5 -5
  16. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  17. package/dist/tempo/client/ChannelOps.js +3 -3
  18. package/dist/tempo/client/ChannelOps.js.map +1 -1
  19. package/dist/tempo/client/Session.d.ts +2 -2
  20. package/dist/tempo/client/Session.d.ts.map +1 -1
  21. package/dist/tempo/client/Session.js +3 -3
  22. package/dist/tempo/client/Session.js.map +1 -1
  23. package/dist/tempo/client/SessionManager.d.ts +4 -4
  24. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  25. package/dist/tempo/client/SessionManager.js +4 -4
  26. package/dist/tempo/client/SessionManager.js.map +1 -1
  27. package/dist/tempo/index.d.ts +1 -1
  28. package/dist/tempo/index.d.ts.map +1 -1
  29. package/dist/tempo/index.js +1 -1
  30. package/dist/tempo/index.js.map +1 -1
  31. package/dist/tempo/server/Charge.js +1 -1
  32. package/dist/tempo/server/Charge.js.map +1 -1
  33. package/dist/tempo/server/Methods.d.ts +1 -1
  34. package/dist/tempo/server/Methods.d.ts.map +1 -1
  35. package/dist/tempo/server/Session.d.ts +8 -8
  36. package/dist/tempo/server/Session.d.ts.map +1 -1
  37. package/dist/tempo/server/Session.js +24 -24
  38. package/dist/tempo/server/Session.js.map +1 -1
  39. package/dist/tempo/server/index.d.ts +2 -2
  40. package/dist/tempo/server/index.d.ts.map +1 -1
  41. package/dist/tempo/server/index.js +2 -2
  42. package/dist/tempo/server/index.js.map +1 -1
  43. package/dist/tempo/server/internal/transport.d.ts +4 -4
  44. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  45. package/dist/tempo/server/internal/transport.js +3 -3
  46. package/dist/tempo/server/internal/transport.js.map +1 -1
  47. package/dist/tempo/session/Chain.d.ts.map +1 -0
  48. package/dist/tempo/session/Chain.js.map +1 -0
  49. package/dist/tempo/session/Channel.d.ts.map +1 -0
  50. package/dist/tempo/session/Channel.js.map +1 -0
  51. package/dist/tempo/session/ChannelStore.d.ts.map +1 -0
  52. package/dist/tempo/session/ChannelStore.js.map +1 -0
  53. package/dist/tempo/session/Receipt.d.ts +22 -0
  54. package/dist/tempo/session/Receipt.d.ts.map +1 -0
  55. package/dist/tempo/{stream → session}/Receipt.js +6 -6
  56. package/dist/tempo/session/Receipt.js.map +1 -0
  57. package/dist/tempo/{stream → session}/Sse.d.ts +7 -7
  58. package/dist/tempo/session/Sse.d.ts.map +1 -0
  59. package/dist/tempo/{stream → session}/Sse.js +4 -4
  60. package/dist/tempo/session/Sse.js.map +1 -0
  61. package/dist/tempo/{stream → session}/Types.d.ts +4 -4
  62. package/dist/tempo/session/Types.d.ts.map +1 -0
  63. package/dist/tempo/{stream → session}/Types.js.map +1 -1
  64. package/dist/tempo/session/Voucher.d.ts.map +1 -0
  65. package/dist/tempo/session/Voucher.js.map +1 -0
  66. package/dist/tempo/{stream → session}/escrow.abi.d.ts.map +1 -1
  67. package/dist/tempo/session/escrow.abi.js.map +1 -0
  68. package/dist/tempo/session/index.d.ts.map +1 -0
  69. package/dist/tempo/session/index.js.map +1 -0
  70. package/package.json +1 -1
  71. package/src/Errors.test.ts +10 -10
  72. package/src/Errors.ts +7 -7
  73. package/src/Expires.test.ts +111 -0
  74. package/src/cli.test.ts +3 -3
  75. package/src/cli.ts +125 -70
  76. package/src/internal/env.ts +54 -0
  77. package/src/mcp-sdk/server/Transport.test.ts +171 -0
  78. package/src/middlewares/express.test.ts +1 -1
  79. package/src/middlewares/hono.test.ts +1 -1
  80. package/src/middlewares/nextjs.test.ts +1 -1
  81. package/src/server/Mppx.ts +5 -4
  82. package/src/server/Transport.test.ts +1 -1
  83. package/src/tempo/client/ChannelOps.test.ts +290 -0
  84. package/src/tempo/client/ChannelOps.ts +8 -8
  85. package/src/tempo/client/Session.test.ts +467 -0
  86. package/src/tempo/client/Session.ts +9 -9
  87. package/src/tempo/client/SessionManager.test.ts +3 -3
  88. package/src/tempo/client/SessionManager.ts +9 -9
  89. package/src/tempo/index.ts +1 -1
  90. package/src/tempo/server/Charge.ts +1 -1
  91. package/src/tempo/server/Session.test.ts +9 -9
  92. package/src/tempo/server/Session.ts +47 -47
  93. package/src/tempo/server/Sse.test.ts +3 -3
  94. package/src/tempo/server/index.ts +2 -2
  95. package/src/tempo/server/internal/transport.ts +6 -6
  96. package/src/tempo/session/Chain.test.ts +511 -0
  97. package/src/tempo/session/Channel.test.ts +108 -0
  98. package/src/tempo/{stream → session}/Receipt.test.ts +16 -12
  99. package/src/tempo/{stream → session}/Receipt.ts +9 -9
  100. package/src/tempo/{stream → session}/Sse.test.ts +5 -5
  101. package/src/tempo/{stream → session}/Sse.ts +11 -11
  102. package/src/tempo/{stream → session}/Types.ts +4 -4
  103. package/dist/tempo/stream/Chain.d.ts.map +0 -1
  104. package/dist/tempo/stream/Chain.js.map +0 -1
  105. package/dist/tempo/stream/Channel.d.ts.map +0 -1
  106. package/dist/tempo/stream/Channel.js.map +0 -1
  107. package/dist/tempo/stream/ChannelStore.d.ts.map +0 -1
  108. package/dist/tempo/stream/ChannelStore.js.map +0 -1
  109. package/dist/tempo/stream/Receipt.d.ts +0 -22
  110. package/dist/tempo/stream/Receipt.d.ts.map +0 -1
  111. package/dist/tempo/stream/Receipt.js.map +0 -1
  112. package/dist/tempo/stream/Sse.d.ts.map +0 -1
  113. package/dist/tempo/stream/Sse.js.map +0 -1
  114. package/dist/tempo/stream/Types.d.ts.map +0 -1
  115. package/dist/tempo/stream/Voucher.d.ts.map +0 -1
  116. package/dist/tempo/stream/Voucher.js.map +0 -1
  117. package/dist/tempo/stream/escrow.abi.js.map +0 -1
  118. package/dist/tempo/stream/index.d.ts.map +0 -1
  119. package/dist/tempo/stream/index.js.map +0 -1
  120. /package/dist/tempo/{stream → session}/Chain.d.ts +0 -0
  121. /package/dist/tempo/{stream → session}/Chain.js +0 -0
  122. /package/dist/tempo/{stream → session}/Channel.d.ts +0 -0
  123. /package/dist/tempo/{stream → session}/Channel.js +0 -0
  124. /package/dist/tempo/{stream → session}/ChannelStore.d.ts +0 -0
  125. /package/dist/tempo/{stream → session}/ChannelStore.js +0 -0
  126. /package/dist/tempo/{stream → session}/Types.js +0 -0
  127. /package/dist/tempo/{stream → session}/Voucher.d.ts +0 -0
  128. /package/dist/tempo/{stream → session}/Voucher.js +0 -0
  129. /package/dist/tempo/{stream → session}/escrow.abi.d.ts +0 -0
  130. /package/dist/tempo/{stream → session}/escrow.abi.js +0 -0
  131. /package/dist/tempo/{stream → session}/index.d.ts +0 -0
  132. /package/dist/tempo/{stream → session}/index.js +0 -0
  133. /package/src/tempo/{stream → session}/Chain.ts +0 -0
  134. /package/src/tempo/{stream → session}/Channel.ts +0 -0
  135. /package/src/tempo/{stream → session}/ChannelStore.test.ts +0 -0
  136. /package/src/tempo/{stream → session}/ChannelStore.ts +0 -0
  137. /package/src/tempo/{stream → session}/Voucher.test.ts +0 -0
  138. /package/src/tempo/{stream → session}/Voucher.ts +0 -0
  139. /package/src/tempo/{stream → session}/escrow.abi.ts +0 -0
  140. /package/src/tempo/{stream → session}/index.ts +0 -0
@@ -0,0 +1,111 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
2
+ import * as Expires from './Expires.js'
3
+
4
+ const FIXED_NOW = new Date('2025-06-15T12:00:00.000Z').getTime()
5
+
6
+ beforeEach(() => {
7
+ vi.useFakeTimers()
8
+ vi.setSystemTime(FIXED_NOW)
9
+ })
10
+
11
+ afterEach(() => {
12
+ vi.useRealTimers()
13
+ })
14
+
15
+ describe('seconds', () => {
16
+ test('returns ISO string n seconds from now', () => {
17
+ expect(Expires.seconds(30)).toBe('2025-06-15T12:00:30.000Z')
18
+ })
19
+
20
+ test('0 returns current time', () => {
21
+ expect(Expires.seconds(0)).toBe('2025-06-15T12:00:00.000Z')
22
+ })
23
+
24
+ test('negative n returns past time', () => {
25
+ expect(Expires.seconds(-60)).toBe('2025-06-15T11:59:00.000Z')
26
+ })
27
+ })
28
+
29
+ describe('minutes', () => {
30
+ test('returns ISO string n minutes from now', () => {
31
+ expect(Expires.minutes(5)).toBe('2025-06-15T12:05:00.000Z')
32
+ })
33
+
34
+ test('0 returns current time', () => {
35
+ expect(Expires.minutes(0)).toBe('2025-06-15T12:00:00.000Z')
36
+ })
37
+
38
+ test('negative n returns past time', () => {
39
+ expect(Expires.minutes(-10)).toBe('2025-06-15T11:50:00.000Z')
40
+ })
41
+ })
42
+
43
+ describe('hours', () => {
44
+ test('returns ISO string n hours from now', () => {
45
+ expect(Expires.hours(2)).toBe('2025-06-15T14:00:00.000Z')
46
+ })
47
+
48
+ test('0 returns current time', () => {
49
+ expect(Expires.hours(0)).toBe('2025-06-15T12:00:00.000Z')
50
+ })
51
+
52
+ test('negative n returns past time', () => {
53
+ expect(Expires.hours(-3)).toBe('2025-06-15T09:00:00.000Z')
54
+ })
55
+ })
56
+
57
+ describe('days', () => {
58
+ test('returns ISO string n days from now', () => {
59
+ expect(Expires.days(1)).toBe('2025-06-16T12:00:00.000Z')
60
+ })
61
+
62
+ test('0 returns current time', () => {
63
+ expect(Expires.days(0)).toBe('2025-06-15T12:00:00.000Z')
64
+ })
65
+
66
+ test('negative n returns past time', () => {
67
+ expect(Expires.days(-2)).toBe('2025-06-13T12:00:00.000Z')
68
+ })
69
+ })
70
+
71
+ describe('weeks', () => {
72
+ test('returns ISO string n weeks from now', () => {
73
+ expect(Expires.weeks(1)).toBe('2025-06-22T12:00:00.000Z')
74
+ })
75
+
76
+ test('0 returns current time', () => {
77
+ expect(Expires.weeks(0)).toBe('2025-06-15T12:00:00.000Z')
78
+ })
79
+
80
+ test('negative n returns past time', () => {
81
+ expect(Expires.weeks(-1)).toBe('2025-06-08T12:00:00.000Z')
82
+ })
83
+ })
84
+
85
+ describe('months', () => {
86
+ test('returns ISO string n months (30 days) from now', () => {
87
+ expect(Expires.months(1)).toBe('2025-07-15T12:00:00.000Z')
88
+ })
89
+
90
+ test('0 returns current time', () => {
91
+ expect(Expires.months(0)).toBe('2025-06-15T12:00:00.000Z')
92
+ })
93
+
94
+ test('negative n returns past time', () => {
95
+ expect(Expires.months(-1)).toBe('2025-05-16T12:00:00.000Z')
96
+ })
97
+ })
98
+
99
+ describe('years', () => {
100
+ test('returns ISO string n years (365 days) from now', () => {
101
+ expect(Expires.years(1)).toBe('2026-06-15T12:00:00.000Z')
102
+ })
103
+
104
+ test('0 returns current time', () => {
105
+ expect(Expires.years(0)).toBe('2025-06-15T12:00:00.000Z')
106
+ })
107
+
108
+ test('negative n returns past time', () => {
109
+ expect(Expires.years(-1)).toBe('2024-06-15T12:00:00.000Z')
110
+ })
111
+ })
package/src/cli.test.ts CHANGED
@@ -6,7 +6,7 @@ import { Addresses } from 'viem/tempo'
6
6
  import { afterAll, describe, expect, test } from 'vitest'
7
7
  import * as Http from '~test/Http.js'
8
8
  import { rpcUrl } from '~test/tempo/prool.js'
9
- import { deployEscrow } from '~test/tempo/stream.js'
9
+ import { deployEscrow } from '~test/tempo/session.js'
10
10
  import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
11
11
  import * as Store from './Store.js'
12
12
  import * as Mppx_server from './server/Mppx.js'
@@ -517,9 +517,9 @@ test('mppx --help', () => {
517
517
  -H, --header <header> Add header (repeatable)
518
518
  -L, --location Follow redirects
519
519
  -X, --method <method> HTTP method
520
- --channel <id> Reuse existing stream channel ID
520
+ --channel <id> Reuse existing session channel ID
521
521
  --confirm Show confirmation prompts
522
- --deposit <amount> Deposit amount for stream payments (human-readable units)
522
+ --deposit <amount> Deposit amount for session payments (human-readable units)
523
523
  --json <json> Send JSON body (sets Content-Type and Accept, implies POST)
524
524
  -V, --version Display version number
525
525
  -h, --help Display this message
package/src/cli.ts CHANGED
@@ -16,8 +16,8 @@ import * as Challenge from './Challenge.js'
16
16
  import * as Credential from './Credential.js'
17
17
  import * as Mppx from './client/Mppx.js'
18
18
  import { tempo } from './tempo/client/index.js'
19
- import type { StreamCredentialPayload } from './tempo/stream/Types.js'
20
- import { signVoucher } from './tempo/stream/Voucher.js'
19
+ import type { SessionCredentialPayload } from './tempo/session/Types.js'
20
+ import { signVoucher } from './tempo/session/Voucher.js'
21
21
 
22
22
  const require = createRequire(import.meta.url)
23
23
  const { name, version } = require('../package.json') as { name: string; version: string }
@@ -41,9 +41,9 @@ cli
41
41
  .option('-H, --header <header>', 'Add header (repeatable)')
42
42
  .option('-L, --location', 'Follow redirects')
43
43
  .option('-X, --method <method>', 'HTTP method')
44
- .option('--channel <id>', 'Reuse existing stream channel ID')
44
+ .option('--channel <id>', 'Reuse existing session channel ID')
45
45
  .option('--confirm', 'Show confirmation prompts')
46
- .option('--deposit <amount>', 'Deposit amount for stream payments (human-readable units)')
46
+ .option('--deposit <amount>', 'Deposit amount for session 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"}'`)
@@ -79,7 +79,7 @@ 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())
82
+ const privateKey = process.env.MPPX_PRIVATE_KEY || (await createKeychain(accountName).get())
83
83
  if (!privateKey) {
84
84
  if (options.account) console.log(`Account "${accountName}" not found.`)
85
85
  else console.log(`No account found.`)
@@ -158,7 +158,7 @@ cli
158
158
  }
159
159
 
160
160
  const account = privateKeyToAccount(privateKey as `0x${string}`)
161
- const rpcUrl = options.rpcUrl ?? process.env.MPPX_RPC_URL
161
+ const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined)
162
162
  const client = createClient({
163
163
  chain: await resolveChain({ ...options, rpcUrl }),
164
164
  transport: http(rpcUrl),
@@ -289,7 +289,7 @@ cli
289
289
  suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain!) ? '10' : undefined)
290
290
  if (!resolved) {
291
291
  console.error(
292
- 'Stream payment requires a deposit. Use --deposit <amount> or connect to testnet.',
292
+ 'Session payment requires a deposit. Use --deposit <amount> or connect to testnet.',
293
293
  )
294
294
  process.exit(1)
295
295
  }
@@ -313,21 +313,21 @@ cli
313
313
  })(),
314
314
  )
315
315
 
316
- const streamMd = challenge.request.methodDetails as
316
+ const sessionMd = challenge.request.methodDetails as
317
317
  | { escrowContract?: string; chainId?: number }
318
318
  | undefined
319
- let streamChannelId: `0x${string}` | undefined
320
- let streamEscrowContract: Address | undefined
321
- let streamChainId = 0
322
- let streamCumulativeAmount = 0n
319
+ let sessionChannelId: `0x${string}` | undefined
320
+ let sessionEscrowContract: Address | undefined
321
+ let sessionChainId = 0
322
+ let sessionCumulativeAmount = 0n
323
323
 
324
324
  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
325
+ const parsed = Credential.deserialize<SessionCredentialPayload>(credential)
326
+ sessionChannelId = parsed.payload.channelId
327
+ sessionChainId = sessionMd?.chainId ?? client.chain?.id ?? 0
328
+ sessionEscrowContract = sessionMd?.escrowContract as Address | undefined
329
329
  if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
330
- streamCumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
330
+ sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
331
331
 
332
332
  if (parsed.payload.action === 'open') {
333
333
  const depositRaw =
@@ -379,8 +379,9 @@ cli
379
379
  typeof receiptJson.acceptedCumulative === 'string' &&
380
380
  receiptJson.acceptedCumulative
381
381
  ) {
382
- streamCumulativeAmount = BigInt(receiptJson.acceptedCumulative)
383
- if (streamChannelId) writeChannelCumulative(streamChannelId, streamCumulativeAmount)
382
+ sessionCumulativeAmount = BigInt(receiptJson.acceptedCumulative)
383
+ if (sessionChannelId)
384
+ writeChannelCumulative(sessionChannelId, sessionCumulativeAmount)
384
385
  }
385
386
  info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`)
386
387
  const rows: [string, string][] = []
@@ -421,21 +422,21 @@ cli
421
422
  let buffer = ''
422
423
  let currentEvent = ''
423
424
 
424
- const streamCred =
425
+ const sessionCred =
425
426
  challenge.intent === 'session'
426
- ? Credential.deserialize<StreamCredentialPayload>(credential)
427
+ ? Credential.deserialize<SessionCredentialPayload>(credential)
427
428
  : undefined
428
- const channelId = streamCred?.payload.channelId
429
+ const channelId = sessionCred?.payload.channelId
429
430
  const md = challenge.request.methodDetails as
430
431
  | { escrowContract?: string; chainId?: number }
431
432
  | undefined
432
- const streamChainId = md?.chainId ?? client.chain?.id ?? 0
433
+ const sessionChainId = md?.chainId ?? client.chain?.id ?? 0
433
434
  const escrowContract = md?.escrowContract as Address | undefined
434
435
  let cumulativeAmount =
435
- streamCred?.payload &&
436
- 'cumulativeAmount' in streamCred.payload &&
437
- streamCred.payload.cumulativeAmount
438
- ? BigInt(streamCred.payload.cumulativeAmount)
436
+ sessionCred?.payload &&
437
+ 'cumulativeAmount' in sessionCred.payload &&
438
+ sessionCred.payload.cumulativeAmount
439
+ ? BigInt(sessionCred.payload.cumulativeAmount)
439
440
  : 0n
440
441
  let _voucherSeq = 0
441
442
 
@@ -481,7 +482,7 @@ cli
481
482
  currentEvent === 'payment-need-voucher' &&
482
483
  channelId &&
483
484
  escrowContract &&
484
- streamChainId
485
+ sessionChainId
485
486
  ) {
486
487
  try {
487
488
  const event = JSON.parse(data) as {
@@ -496,7 +497,7 @@ cli
496
497
  account,
497
498
  { channelId, cumulativeAmount },
498
499
  escrowContract,
499
- streamChainId,
500
+ sessionChainId,
500
501
  )
501
502
  const voucherCred = Credential.serialize({
502
503
  challenge,
@@ -506,7 +507,7 @@ cli
506
507
  cumulativeAmount: cumulativeAmount.toString(),
507
508
  signature,
508
509
  },
509
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
510
+ source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
510
511
  })
511
512
  await globalThis.fetch(url, {
512
513
  method: 'POST',
@@ -583,15 +584,15 @@ cli
583
584
  }
584
585
  if (buffer.trim()) await processLines([buffer])
585
586
 
586
- if (channelId && escrowContract && streamChainId) {
587
+ if (channelId && escrowContract && sessionChainId) {
587
588
  const signature = await signVoucher(
588
589
  client,
589
590
  account,
590
591
  { channelId, cumulativeAmount },
591
592
  escrowContract,
592
- streamChainId,
593
+ sessionChainId,
593
594
  )
594
- const closePayload: StreamCredentialPayload = {
595
+ const closePayload: SessionCredentialPayload = {
595
596
  action: 'close',
596
597
  channelId,
597
598
  cumulativeAmount: cumulativeAmount.toString(),
@@ -600,7 +601,7 @@ cli
600
601
  const closeCred = Credential.serialize({
601
602
  challenge,
602
603
  payload: closePayload,
603
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
604
+ source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
604
605
  })
605
606
  const closeRes = await globalThis.fetch(url, {
606
607
  method: 'POST',
@@ -638,9 +639,9 @@ cli
638
639
  const shouldClose =
639
640
  challenge.intent === 'session' &&
640
641
  credentialResponse.ok &&
641
- streamChannelId &&
642
- streamEscrowContract &&
643
- streamChainId
642
+ sessionChannelId &&
643
+ sessionEscrowContract &&
644
+ sessionChainId
644
645
  if (shouldClose && options.confirm) {
645
646
  info('\n')
646
647
  }
@@ -650,20 +651,20 @@ cli
650
651
  const signature = await signVoucher(
651
652
  client,
652
653
  account,
653
- { channelId: streamChannelId!, cumulativeAmount: streamCumulativeAmount },
654
- streamEscrowContract!,
655
- streamChainId,
654
+ { channelId: sessionChannelId!, cumulativeAmount: sessionCumulativeAmount },
655
+ sessionEscrowContract!,
656
+ sessionChainId,
656
657
  )
657
- const closePayload: StreamCredentialPayload = {
658
+ const closePayload: SessionCredentialPayload = {
658
659
  action: 'close',
659
- channelId: streamChannelId!,
660
- cumulativeAmount: streamCumulativeAmount.toString(),
660
+ channelId: sessionChannelId!,
661
+ cumulativeAmount: sessionCumulativeAmount.toString(),
661
662
  signature,
662
663
  }
663
664
  const closeCred = Credential.serialize({
664
665
  challenge,
665
666
  payload: closePayload,
666
- source: `did:pkh:eip155:${streamChainId}:${account.address}`,
667
+ source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
667
668
  })
668
669
  const closeRes = await globalThis.fetch(url, {
669
670
  ...fetchInit,
@@ -673,7 +674,7 @@ cli
673
674
  },
674
675
  })
675
676
  if (closeRes.ok) {
676
- deleteChannelState(streamChannelId!)
677
+ deleteChannelState(sessionChannelId!)
677
678
  const closeReceiptHeader = closeRes.headers.get('Payment-Receipt')
678
679
  let closeTxHash: string | undefined
679
680
  if (closeReceiptHeader) {
@@ -690,7 +691,7 @@ cli
690
691
  ? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
691
692
  : ''
692
693
  info(
693
- `\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(streamCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`,
694
+ `\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(sessionCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`,
694
695
  )
695
696
  } else {
696
697
  const closeBody = await closeRes.text().catch(() => '')
@@ -698,10 +699,10 @@ cli
698
699
  `${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
699
700
  )
700
701
  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` +
702
+ `${pc.dim(` channelId: ${sessionChannelId}`)}\n` +
703
+ `${pc.dim(` cumulativeAmount: ${sessionCumulativeAmount}`)}\n` +
704
+ `${pc.dim(` escrowContract: ${sessionEscrowContract}`)}\n` +
705
+ `${pc.dim(` chainId: ${sessionChainId}`)}\n` +
705
706
  `${pc.dim(` account: ${account.address}`)}\n` +
706
707
  `${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`,
707
708
  )
@@ -784,7 +785,11 @@ cli
784
785
  const accounts = await createKeychain().list()
785
786
  if (accounts.length === 1) createDefaultStore().set(resolvedName)
786
787
  console.log(`Account "${resolvedName}" saved to keychain.`)
787
- console.log(pc.dim(`Address ${account.address}`))
788
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url
789
+ const addrDisplay = explorerUrl
790
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
791
+ : account.address
792
+ console.log(pc.dim(`Address ${addrDisplay}`))
788
793
  resolveChain(options)
789
794
  .then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
790
795
  .then((client) =>
@@ -823,8 +828,12 @@ cli
823
828
  const account = privateKeyToAccount(key as `0x${string}`)
824
829
  const balanceLines = await fetchBalanceLines(account.address, { includeTestnet: false })
825
830
  if (!options.yes) {
831
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url
832
+ const addrDisplay = explorerUrl
833
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
834
+ : account.address
826
835
  process.stderr.write(pc.dim(`Delete account "${options.account}"\n`))
827
- process.stderr.write(pc.dim(` Address ${account.address}\n`))
836
+ process.stderr.write(pc.dim(` Address ${addrDisplay}\n`))
828
837
  for (let i = 0; i < balanceLines.length; i++)
829
838
  process.stderr.write(
830
839
  pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`),
@@ -897,6 +906,7 @@ cli
897
906
  }),
898
907
  )
899
908
  const resolved = entries.filter((e) => e !== undefined)
909
+ const explorerUrl = tempoMainnet.blockExplorers?.default?.url
900
910
  const maxWidth = Math.max(
901
911
  ...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)),
902
912
  )
@@ -904,7 +914,10 @@ cli
904
914
  const isDefault = entry.name === currentDefault
905
915
  const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name
906
916
  const width = entry.name.length + (isDefault ? 1 : 0)
907
- console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(entry.address)}`)
917
+ const addrDisplay = explorerUrl
918
+ ? pc.link(`${explorerUrl}/address/${entry.address}`, entry.address)
919
+ : entry.address
920
+ console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(addrDisplay)}`)
908
921
  }
909
922
  return
910
923
  }
@@ -918,10 +931,14 @@ cli
918
931
  process.exit(1)
919
932
  }
920
933
  const account = privateKeyToAccount(key as `0x${string}`)
921
- console.log(`${pc.dim('Address')} ${account.address}`)
934
+ const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined)
935
+ const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet
936
+ const explorerUrl = chain.blockExplorers?.default?.url
937
+ const addrDisplay = explorerUrl
938
+ ? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
939
+ : account.address
940
+ console.log(`${pc.dim('Address')} ${addrDisplay}`)
922
941
 
923
- const rpcUrl = options.rpcUrl ?? process.env.MPPX_RPC_URL
924
- const chain = rpcUrl ? await resolveChain({ rpcUrl }) : undefined
925
942
  const balanceLines = await fetchBalanceLines(
926
943
  account.address,
927
944
  chain && rpcUrl ? { chain, rpcUrl } : undefined,
@@ -1054,7 +1071,7 @@ function createDefaultStore() {
1054
1071
 
1055
1072
  function resolveAccountName(explicit?: string): string {
1056
1073
  if (explicit) return explicit
1057
- if (process.env.MPPX_ACCOUNT) return process.env.MPPX_ACCOUNT
1074
+ if (process.env.MPPX_ACCOUNT?.trim()) return process.env.MPPX_ACCOUNT
1058
1075
  return createDefaultStore().get()
1059
1076
  }
1060
1077
 
@@ -1238,9 +1255,9 @@ const pc = (() => {
1238
1255
  bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
1239
1256
  bgCyanBright: f('\x1b[106m', '\x1b[49m'),
1240
1257
  bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
1241
- link(url: string, text: string) {
1258
+ link(url: string, text: string, noUnderline?: boolean) {
1242
1259
  if (!isColorSupported) return text
1243
- return `\x1b]8;;${url}\x07${pc.underline(text)}\x1b]8;;\x07`
1260
+ return `\x1b]8;;${url}\x07${noUnderline ? text : pc.underline(text)}\x1b]8;;\x07`
1244
1261
  },
1245
1262
  }
1246
1263
  })()
@@ -1268,6 +1285,8 @@ function chainName(chain: { id: number; name: string }) {
1268
1285
  }
1269
1286
 
1270
1287
  const pathUsd = '0x20c0000000000000000000000000000000000000' as Address
1288
+ const usdcE = '0x20C000000000000000000000b9537d11c60E8b50' as Address
1289
+ const mainnetTokens = [pathUsd, usdcE] as const
1271
1290
  const testnetTokens = [
1272
1291
  '0x20c0000000000000000000000000000000000000',
1273
1292
  '0x20c0000000000000000000000000000000000001',
@@ -1275,11 +1294,20 @@ const testnetTokens = [
1275
1294
  '0x20c0000000000000000000000000000000000003',
1276
1295
  ] as const
1277
1296
 
1278
- function fmtBalance(b: bigint, symbol: string, decimals = 6) {
1297
+ function fmtBalance(
1298
+ b: bigint,
1299
+ symbol: string,
1300
+ decimals = 6,
1301
+ opts?: { explorerUrl?: string | undefined; token?: string | undefined },
1302
+ ) {
1279
1303
  const value = Number(b) / 10 ** decimals
1280
1304
  const [int, dec] = value.toString().split('.')
1281
1305
  const formatted = int!.replace(/\B(?=(\d{3})+(?!\d))/g, '_')
1282
- return `${dec ? `${formatted}.${dec}` : formatted} ${pc.dim(symbol)}`
1306
+ const sym =
1307
+ opts?.explorerUrl && opts.token
1308
+ ? pc.dim(pc.link(`${opts.explorerUrl}/token/${opts.token}`, symbol, true))
1309
+ : pc.dim(symbol)
1310
+ return `${dec ? `${formatted}.${dec}` : formatted} ${sym}`
1283
1311
  }
1284
1312
 
1285
1313
  function isTestnet(chain: Chain) {
@@ -1296,9 +1324,13 @@ async function fetchTokenInfo(
1296
1324
  Actions.token.getBalance(client, { account, token }).catch(() => 0n),
1297
1325
  Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token as string })),
1298
1326
  ])
1299
- const symbol = token === pathUsd ? 'PathUSD' : metadata.symbol
1327
+ const knownSymbols: Record<string, string> = {
1328
+ [pathUsd]: 'PathUSD',
1329
+ [usdcE]: 'USDC.e',
1330
+ }
1331
+ const symbol = knownSymbols[token] ?? metadata.symbol
1300
1332
  const decimals = 'decimals' in metadata ? metadata.decimals : 6
1301
- return { balance, symbol, decimals }
1333
+ return { balance, symbol, decimals, token }
1302
1334
  }
1303
1335
 
1304
1336
  function detectTerminalBg(
@@ -1340,6 +1372,7 @@ async function fetchBalanceLines(
1340
1372
  ): Promise<string[]> {
1341
1373
  if (opts?.chain) {
1342
1374
  const client = createClient({ chain: opts.chain, transport: http(opts.rpcUrl) })
1375
+ const explorerUrl = opts.chain.blockExplorers?.default?.url
1343
1376
  const label = pc.dim(`(${chainName(opts.chain)})`)
1344
1377
  if (isTestnet(opts.chain)) {
1345
1378
  const results = await Promise.all(
@@ -1347,24 +1380,46 @@ async function fetchBalanceLines(
1347
1380
  )
1348
1381
  return results
1349
1382
  .filter((t) => t.balance > 0n)
1350
- .map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals)} ${label}`)
1383
+ .map(
1384
+ (t) =>
1385
+ `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`,
1386
+ )
1351
1387
  }
1352
- const { balance, symbol, decimals } = await fetchTokenInfo(client, pathUsd, address)
1353
- return [`${fmtBalance(balance, symbol, decimals)} ${label}`]
1388
+ const results = await Promise.all(
1389
+ mainnetTokens.map((token) => fetchTokenInfo(client, token, address)),
1390
+ )
1391
+ return results.map(
1392
+ (t) =>
1393
+ `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`,
1394
+ )
1354
1395
  }
1355
1396
 
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)]
1397
+ const mainnetClient = createClient({
1398
+ chain: tempoMainnet,
1399
+ transport: http(process.env.MPPX_RPC_URL || undefined),
1400
+ })
1401
+ const mainnetExplorerUrl = tempoMainnet.blockExplorers?.default?.url
1402
+ const mainnetResults = await Promise.all(
1403
+ mainnetTokens.map((token) => fetchTokenInfo(mainnetClient, token, address)),
1404
+ )
1405
+ const lines = mainnetResults.map((t) =>
1406
+ fmtBalance(t.balance, t.symbol, t.decimals, {
1407
+ explorerUrl: mainnetExplorerUrl,
1408
+ token: t.token,
1409
+ }),
1410
+ )
1359
1411
 
1360
1412
  if (opts?.includeTestnet !== false) {
1361
1413
  const testnetClient = createClient({ chain: tempoModerato, transport: http() })
1414
+ const testnetExplorerUrl = tempoModerato.blockExplorers?.default?.url
1362
1415
  const testnetResults = await Promise.all(
1363
1416
  testnetTokens.map((token) => fetchTokenInfo(testnetClient, token, address)),
1364
1417
  )
1365
1418
  for (const t of testnetResults) {
1366
1419
  if (t.balance > 0n)
1367
- lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals)} ${pc.dim('(testnet)')}`)
1420
+ lines.push(
1421
+ `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl: testnetExplorerUrl, token: t.token })} ${pc.dim('(testnet)')}`,
1422
+ )
1368
1423
  }
1369
1424
  }
1370
1425
 
@@ -0,0 +1,54 @@
1
+ /** Map of configuration keys to environment variable names, checked in order. */
2
+ const variables = {
3
+ realm: [
4
+ 'FLY_APP_NAME',
5
+ 'HEROKU_APP_NAME',
6
+ 'HOST',
7
+ 'HOSTNAME',
8
+ 'MPP_REALM',
9
+ 'RAILWAY_PUBLIC_DOMAIN',
10
+ 'RENDER_EXTERNAL_HOSTNAME',
11
+ 'VERCEL_URL',
12
+ 'WEBSITE_HOSTNAME',
13
+ ],
14
+ secretKey: ['MPP_SECRET_KEY'],
15
+ } as const satisfies Record<string, readonly string[]>
16
+
17
+ /** Fallback values when no environment variable is set. */
18
+ const defaults = {
19
+ realm: 'MPP Payment',
20
+ secretKey: 'tmp',
21
+ } as const satisfies Record<keyof typeof variables, string>
22
+
23
+ /**
24
+ * Resolves a configuration value from environment variables.
25
+ *
26
+ * Checks platform-specific env vars in order, falling back to a default.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * Env.get('realm') // e.g. "my-app.vercel.app"
31
+ * Env.get('secretKey') // e.g. value of MPP_SECRET_KEY
32
+ * ```
33
+ */
34
+ export function get(key: keyof typeof variables): string {
35
+ for (const name of variables[key]) {
36
+ const value = read(name)
37
+ if (value) return value
38
+ }
39
+ return defaults[key]
40
+ }
41
+
42
+ /** Reads a single environment variable, probing available runtime APIs. */
43
+ function read(name: string): string | undefined {
44
+ try {
45
+ if (typeof process !== 'undefined' && process?.env) return process.env[name] || undefined
46
+ } catch {}
47
+
48
+ try {
49
+ const deno = (globalThis as any).Deno
50
+ if (deno?.env?.get) return deno.env.get(name) || undefined
51
+ } catch {}
52
+
53
+ return undefined
54
+ }