mppx 0.8.12 → 0.8.14

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 (266) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +16 -1
  3. package/dist/Errors.d.ts +10 -0
  4. package/dist/Errors.d.ts.map +1 -1
  5. package/dist/Errors.js +11 -2
  6. package/dist/Errors.js.map +1 -1
  7. package/dist/Method.d.ts +105 -3
  8. package/dist/Method.d.ts.map +1 -1
  9. package/dist/Method.js +102 -2
  10. package/dist/Method.js.map +1 -1
  11. package/dist/cli/account.d.ts +88 -0
  12. package/dist/cli/account.d.ts.map +1 -1
  13. package/dist/cli/account.js +39 -9
  14. package/dist/cli/account.js.map +1 -1
  15. package/dist/cli/cli.d.ts +2 -1
  16. package/dist/cli/cli.d.ts.map +1 -1
  17. package/dist/cli/cli.js +59 -2
  18. package/dist/cli/cli.js.map +1 -1
  19. package/dist/cli/sessions/Manager.d.ts +31 -0
  20. package/dist/cli/sessions/Manager.d.ts.map +1 -0
  21. package/dist/cli/sessions/Manager.js +56 -0
  22. package/dist/cli/sessions/Manager.js.map +1 -0
  23. package/dist/cli/sessions/commands.d.ts +36 -0
  24. package/dist/cli/sessions/commands.d.ts.map +1 -0
  25. package/dist/cli/sessions/commands.js +407 -0
  26. package/dist/cli/sessions/commands.js.map +1 -0
  27. package/dist/cli/sessions/request.d.ts +35 -0
  28. package/dist/cli/sessions/request.d.ts.map +1 -0
  29. package/dist/cli/sessions/request.js +271 -0
  30. package/dist/cli/sessions/request.js.map +1 -0
  31. package/dist/cli/sessions/store.d.ts +134 -0
  32. package/dist/cli/sessions/store.d.ts.map +1 -0
  33. package/dist/cli/sessions/store.js +723 -0
  34. package/dist/cli/sessions/store.js.map +1 -0
  35. package/dist/cli/utils.d.ts +2 -0
  36. package/dist/cli/utils.d.ts.map +1 -1
  37. package/dist/cli/utils.js +12 -0
  38. package/dist/cli/utils.js.map +1 -1
  39. package/dist/cli/validate/index.d.ts.map +1 -1
  40. package/dist/cli/validate/index.js +12 -6
  41. package/dist/cli/validate/index.js.map +1 -1
  42. package/dist/cli/validate/messages.d.ts +2 -0
  43. package/dist/cli/validate/messages.d.ts.map +1 -0
  44. package/dist/cli/validate/messages.js +8 -0
  45. package/dist/cli/validate/messages.js.map +1 -0
  46. package/dist/client/internal/Fetch.d.ts.map +1 -1
  47. package/dist/client/internal/Fetch.js +12 -2
  48. package/dist/client/internal/Fetch.js.map +1 -1
  49. package/dist/client/internal/MethodChallenge.d.ts +17 -0
  50. package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
  51. package/dist/client/internal/MethodChallenge.js +11 -0
  52. package/dist/client/internal/MethodChallenge.js.map +1 -0
  53. package/dist/client/node.d.ts +29 -0
  54. package/dist/client/node.d.ts.map +1 -0
  55. package/dist/client/node.js +177 -0
  56. package/dist/client/node.js.map +1 -0
  57. package/dist/internal/types.d.ts +24 -0
  58. package/dist/internal/types.d.ts.map +1 -1
  59. package/dist/server/Mppx.d.ts +23 -4
  60. package/dist/server/Mppx.d.ts.map +1 -1
  61. package/dist/server/Mppx.js +91 -82
  62. package/dist/server/Mppx.js.map +1 -1
  63. package/dist/stripe/server/Charge.d.ts +8 -4
  64. package/dist/stripe/server/Charge.d.ts.map +1 -1
  65. package/dist/stripe/server/Charge.js.map +1 -1
  66. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  67. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  68. package/dist/stripe/server/internal/html.gen.js +1 -1
  69. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  70. package/dist/tempo/client/Methods.d.ts +2 -1
  71. package/dist/tempo/client/Methods.d.ts.map +1 -1
  72. package/dist/tempo/internal/fee-payer.d.ts +1 -0
  73. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  74. package/dist/tempo/internal/fee-payer.js +7 -2
  75. package/dist/tempo/internal/fee-payer.js.map +1 -1
  76. package/dist/tempo/internal/types.d.ts +10 -1
  77. package/dist/tempo/internal/types.d.ts.map +1 -1
  78. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  79. package/dist/tempo/legacy/client/ChannelOps.js +2 -1
  80. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  81. package/dist/tempo/legacy/client/Session.d.ts +2 -2
  82. package/dist/tempo/legacy/server/Session.d.ts +3 -3
  83. package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
  84. package/dist/tempo/legacy/server/Session.js.map +1 -1
  85. package/dist/tempo/server/Charge.d.ts +40 -78
  86. package/dist/tempo/server/Charge.d.ts.map +1 -1
  87. package/dist/tempo/server/Charge.js +289 -209
  88. package/dist/tempo/server/Charge.js.map +1 -1
  89. package/dist/tempo/server/Methods.d.ts +13 -23
  90. package/dist/tempo/server/Methods.d.ts.map +1 -1
  91. package/dist/tempo/server/Methods.js +6 -2
  92. package/dist/tempo/server/Methods.js.map +1 -1
  93. package/dist/tempo/server/Relay.d.ts +48 -0
  94. package/dist/tempo/server/Relay.d.ts.map +1 -0
  95. package/dist/tempo/server/Relay.js +177 -0
  96. package/dist/tempo/server/Relay.js.map +1 -0
  97. package/dist/tempo/server/SponsorBudget.d.ts +59 -0
  98. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
  99. package/dist/tempo/server/SponsorBudget.js +144 -0
  100. package/dist/tempo/server/SponsorBudget.js.map +1 -0
  101. package/dist/tempo/server/Subscription.d.ts +3 -2
  102. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  103. package/dist/tempo/server/Subscription.js.map +1 -1
  104. package/dist/tempo/server/index.d.ts +1 -1
  105. package/dist/tempo/server/index.d.ts.map +1 -1
  106. package/dist/tempo/server/index.js.map +1 -1
  107. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  108. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  109. package/dist/tempo/server/internal/html.gen.js +1 -1
  110. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  111. package/dist/tempo/session/Snapshot.d.ts +9 -0
  112. package/dist/tempo/session/Snapshot.d.ts.map +1 -1
  113. package/dist/tempo/session/Snapshot.js +6 -0
  114. package/dist/tempo/session/Snapshot.js.map +1 -1
  115. package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
  116. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  117. package/dist/tempo/session/client/ChannelOps.js +17 -3
  118. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  119. package/dist/tempo/session/client/CredentialState.d.ts +26 -2
  120. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  121. package/dist/tempo/session/client/CredentialState.js +99 -16
  122. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  123. package/dist/tempo/session/client/Runtime.d.ts +8 -0
  124. package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
  125. package/dist/tempo/session/client/Runtime.js +17 -0
  126. package/dist/tempo/session/client/Runtime.js.map +1 -1
  127. package/dist/tempo/session/client/Session.d.ts +16 -6
  128. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  129. package/dist/tempo/session/client/Session.js +93 -36
  130. package/dist/tempo/session/client/Session.js.map +1 -1
  131. package/dist/tempo/session/client/SessionManager.d.ts +10 -3
  132. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  133. package/dist/tempo/session/client/SessionManager.js +176 -50
  134. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  135. package/dist/tempo/session/client/Transports.d.ts +18 -7
  136. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  137. package/dist/tempo/session/client/Transports.js +81 -33
  138. package/dist/tempo/session/client/Transports.js.map +1 -1
  139. package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
  140. package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
  141. package/dist/tempo/session/client/internal/SessionManager.js +13 -0
  142. package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
  143. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  144. package/dist/tempo/session/precompile/Chain.js +109 -12
  145. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  146. package/dist/tempo/session/precompile/Channel.d.ts +2 -0
  147. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
  148. package/dist/tempo/session/precompile/Channel.js +4 -0
  149. package/dist/tempo/session/precompile/Channel.js.map +1 -1
  150. package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
  151. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  152. package/dist/tempo/session/server/CredentialVerification.js +14 -0
  153. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  154. package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
  155. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  156. package/dist/tempo/session/server/MeteredStream.js +5 -4
  157. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  158. package/dist/tempo/session/server/RequestState.d.ts +5 -1
  159. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  160. package/dist/tempo/session/server/RequestState.js +13 -5
  161. package/dist/tempo/session/server/RequestState.js.map +1 -1
  162. package/dist/tempo/session/server/Session.d.ts +21 -6
  163. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  164. package/dist/tempo/session/server/Session.js +4 -1
  165. package/dist/tempo/session/server/Session.js.map +1 -1
  166. package/dist/tempo/session/server/Settlement.d.ts +20 -0
  167. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  168. package/dist/tempo/session/server/Settlement.js +20 -0
  169. package/dist/tempo/session/server/Settlement.js.map +1 -1
  170. package/dist/tempo/session/server/Ws.d.ts +2 -0
  171. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  172. package/dist/tempo/session/server/Ws.js.map +1 -1
  173. package/dist/tempo/session/server/index.d.ts +1 -1
  174. package/dist/tempo/session/server/index.d.ts.map +1 -1
  175. package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
  176. package/dist/validation/core.d.ts.map +1 -1
  177. package/dist/validation/core.js +6 -2
  178. package/dist/validation/core.js.map +1 -1
  179. package/dist/viem/Client.d.ts.map +1 -1
  180. package/dist/viem/Client.js +24 -17
  181. package/dist/viem/Client.js.map +1 -1
  182. package/package.json +6 -1
  183. package/src/Errors.test.ts +23 -0
  184. package/src/Errors.ts +21 -2
  185. package/src/Method.test.ts +102 -1
  186. package/src/Method.ts +241 -5
  187. package/src/cli/account.ts +45 -10
  188. package/src/cli/cli.test.ts +165 -70
  189. package/src/cli/cli.ts +67 -2
  190. package/src/cli/mcp.test.ts +11 -0
  191. package/src/cli/sessions/Manager.test.ts +249 -0
  192. package/src/cli/sessions/Manager.ts +93 -0
  193. package/src/cli/sessions/commands.ts +444 -0
  194. package/src/cli/sessions/request.test.ts +51 -0
  195. package/src/cli/sessions/request.ts +353 -0
  196. package/src/cli/sessions/store.test.ts +581 -0
  197. package/src/cli/sessions/store.ts +940 -0
  198. package/src/cli/utils.test.ts +23 -0
  199. package/src/cli/utils.ts +10 -0
  200. package/src/cli/validate/index.ts +12 -14
  201. package/src/cli/validate/messages.ts +7 -0
  202. package/src/cli/validate.test.ts +38 -5
  203. package/src/client/Mppx.test-d.ts +3 -3
  204. package/src/client/internal/Fetch.ts +16 -7
  205. package/src/client/internal/MethodChallenge.ts +30 -0
  206. package/src/client/node.test.ts +115 -0
  207. package/src/client/node.ts +247 -0
  208. package/src/internal/types.test-d.ts +21 -0
  209. package/src/internal/types.ts +33 -0
  210. package/src/server/Mppx.test-d.ts +44 -0
  211. package/src/server/Mppx.test.ts +205 -0
  212. package/src/server/Mppx.ts +167 -103
  213. package/src/stripe/Methods.test.ts +10 -0
  214. package/src/stripe/server/Charge.test-d.ts +66 -0
  215. package/src/stripe/server/Charge.ts +8 -5
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/PublicExports.test-d.ts +20 -0
  218. package/src/tempo/internal/fee-payer.ts +7 -3
  219. package/src/tempo/internal/types.ts +13 -2
  220. package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
  221. package/src/tempo/legacy/client/ChannelOps.ts +2 -1
  222. package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
  223. package/src/tempo/legacy/server/Session.ts +6 -5
  224. package/src/tempo/server/Charge.test.ts +337 -63
  225. package/src/tempo/server/Charge.ts +440 -270
  226. package/src/tempo/server/Methods.ts +16 -4
  227. package/src/tempo/server/Relay.test.ts +523 -0
  228. package/src/tempo/server/Relay.ts +288 -0
  229. package/src/tempo/server/SponsorBudget.test.ts +125 -0
  230. package/src/tempo/server/SponsorBudget.ts +213 -0
  231. package/src/tempo/server/Subscription.ts +6 -4
  232. package/src/tempo/server/index.ts +5 -1
  233. package/src/tempo/server/internal/html.gen.ts +1 -1
  234. package/src/tempo/session/README.md +201 -0
  235. package/src/tempo/session/Snapshot.ts +21 -0
  236. package/src/tempo/session/client/ChannelOps.test.ts +55 -0
  237. package/src/tempo/session/client/ChannelOps.ts +21 -3
  238. package/src/tempo/session/client/CredentialState.test.ts +2 -2
  239. package/src/tempo/session/client/CredentialState.ts +159 -16
  240. package/src/tempo/session/client/Runtime.test.ts +12 -0
  241. package/src/tempo/session/client/Runtime.ts +22 -0
  242. package/src/tempo/session/client/Session.test.ts +238 -5
  243. package/src/tempo/session/client/Session.ts +130 -41
  244. package/src/tempo/session/client/SessionManager.test.ts +239 -8
  245. package/src/tempo/session/client/SessionManager.ts +204 -54
  246. package/src/tempo/session/client/Transports.test.ts +181 -8
  247. package/src/tempo/session/client/Transports.ts +127 -45
  248. package/src/tempo/session/client/internal/SessionManager.ts +36 -0
  249. package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
  250. package/src/tempo/session/precompile/Chain.test.ts +223 -10
  251. package/src/tempo/session/precompile/Chain.ts +119 -12
  252. package/src/tempo/session/precompile/Channel.test.ts +9 -0
  253. package/src/tempo/session/precompile/Channel.ts +5 -0
  254. package/src/tempo/session/server/CredentialVerification.ts +18 -1
  255. package/src/tempo/session/server/MeteredStream.ts +12 -5
  256. package/src/tempo/session/server/RequestState.test.ts +47 -4
  257. package/src/tempo/session/server/RequestState.ts +15 -8
  258. package/src/tempo/session/server/Session.test.ts +461 -0
  259. package/src/tempo/session/server/Session.ts +31 -8
  260. package/src/tempo/session/server/Settlement.ts +45 -0
  261. package/src/tempo/session/server/Ws.test.ts +113 -0
  262. package/src/tempo/session/server/Ws.ts +2 -0
  263. package/src/tempo/session/server/index.ts +2 -0
  264. package/src/validation/core.ts +7 -1
  265. package/src/viem/Client.test.ts +44 -1
  266. package/src/viem/Client.ts +31 -17
@@ -316,7 +316,11 @@ describe('SessionSnapshotHints', () => {
316
316
  escrowContract,
317
317
  expiringNonceHash: descriptor.expiringNonceHash,
318
318
  finalized: false,
319
- highestVoucher: null,
319
+ highestVoucher: {
320
+ channelId,
321
+ cumulativeAmount: 500n,
322
+ signature: `0x${'44'.repeat(64)}`,
323
+ },
320
324
  highestVoucherAmount: 500n,
321
325
  operator: descriptor.operator,
322
326
  payee: descriptor.payee,
@@ -451,6 +455,11 @@ describe('SessionSnapshotHints', () => {
451
455
  deposit: '1000',
452
456
  descriptor,
453
457
  escrow: escrowContract,
458
+ highestVoucher: {
459
+ channelId,
460
+ cumulativeAmount: '500',
461
+ signature: `0x${'44'.repeat(64)}`,
462
+ },
454
463
  requiredCumulative: '350',
455
464
  settled: '100',
456
465
  spent: '300',
@@ -458,7 +467,7 @@ describe('SessionSnapshotHints', () => {
458
467
  })
459
468
  })
460
469
 
461
- test('raises accepted cumulative when next request exceeds current authorization', async () => {
470
+ test('keeps accepted cumulative at the signed voucher when the next request needs more', async () => {
462
471
  const snapshot = await resolveSessionSnapshot({
463
472
  amount: 300n,
464
473
  channelId,
@@ -466,7 +475,34 @@ describe('SessionSnapshotHints', () => {
466
475
  })
467
476
 
468
477
  expect(snapshot?.requiredCumulative).toBe('600')
469
- expect(snapshot?.acceptedCumulative).toBe('600')
478
+ expect(snapshot?.acceptedCumulative).toBe('500')
479
+ expect(snapshot?.highestVoucher?.cumulativeAmount).toBe('500')
480
+ })
481
+
482
+ test('omits reusable channel hints without a matching signed highest voucher', async () => {
483
+ await expect(
484
+ resolveSessionSnapshot({
485
+ amount: 50n,
486
+ channelId,
487
+ store: store(channel({ highestVoucher: null })),
488
+ }),
489
+ ).resolves.toBeUndefined()
490
+
491
+ await expect(
492
+ resolveSessionSnapshot({
493
+ amount: 50n,
494
+ channelId,
495
+ store: store(
496
+ channel({
497
+ highestVoucher: {
498
+ channelId,
499
+ cumulativeAmount: 499n,
500
+ signature: `0x${'44'.repeat(64)}`,
501
+ },
502
+ }),
503
+ ),
504
+ }),
505
+ ).resolves.toBeUndefined()
470
506
  })
471
507
 
472
508
  test('omits reusable channel hints when payment fields do not match', async () => {
@@ -508,10 +544,17 @@ describe('SessionSnapshotHints', () => {
508
544
  }
509
545
  })
510
546
 
511
- test('omits hints for missing, non-precompile, or finalized channels', async () => {
547
+ test('omits hints for missing, non-precompile, closing, or finalized channels', async () => {
512
548
  await expect(
513
549
  resolveSessionSnapshot({ amount: 1n, channelId, store: store(null) }),
514
550
  ).resolves.toBe(undefined)
551
+ await expect(
552
+ resolveSessionSnapshot({
553
+ amount: 1n,
554
+ channelId,
555
+ store: store(channel({ closeRequestedAt: 1n })),
556
+ }),
557
+ ).resolves.toBe(undefined)
515
558
  await expect(
516
559
  resolveSessionSnapshot({
517
560
  amount: 1n,
@@ -80,7 +80,11 @@ export type ResolveSessionChannelIdParameters = {
80
80
  store: ChannelStore.ChannelStore
81
81
  }
82
82
 
83
- /** Application hook for mapping request identity to an existing session channel. */
83
+ /**
84
+ * Application-owned lookup from authenticated identity and payment scope to an existing session
85
+ * channel. MPPx does not derive a storage key or scan the channel store; the returned channel ID is
86
+ * loaded and independently validated before its snapshot is advertised.
87
+ */
84
88
  export type ResolveSessionChannelId = (
85
89
  parameters: ResolveSessionChannelIdParameters,
86
90
  ) => Promise<string | null | undefined> | string | null | undefined
@@ -139,21 +143,24 @@ export async function resolveSessionSnapshot(
139
143
  const channel = await store.getChannel(ChannelStore.normalizeChannelId(channelId))
140
144
  if (!channel || !ChannelStore.isPrecompileState(channel)) return undefined
141
145
  if (channel.finalized) return undefined
146
+ if (channel.closeRequestedAt !== 0n) return undefined
147
+ if (!channel.highestVoucher) return undefined
148
+ if (channel.highestVoucher.cumulativeAmount !== channel.highestVoucherAmount) return undefined
142
149
  if (expected && !matchesSnapshotPaymentFields(channel, expected)) return undefined
143
150
  const requiredCumulative = channel.spent + amount
144
- const acceptedCumulative =
145
- channel.highestVoucherAmount > requiredCumulative
146
- ? channel.highestVoucherAmount
147
- : requiredCumulative
148
151
  return {
149
- acceptedCumulative: acceptedCumulative.toString(),
152
+ acceptedCumulative: channel.highestVoucherAmount.toString(),
150
153
  chainId: channel.chainId,
151
154
  channelId: channel.channelId,
152
- closeRequestedAt:
153
- channel.closeRequestedAt === 0n ? undefined : channel.closeRequestedAt.toString(),
155
+ closeRequestedAt: undefined,
154
156
  deposit: channel.deposit.toString(),
155
157
  descriptor: channel.descriptor,
156
158
  escrow: channel.escrowContract,
159
+ highestVoucher: {
160
+ channelId: channel.highestVoucher.channelId,
161
+ cumulativeAmount: channel.highestVoucher.cumulativeAmount.toString(),
162
+ signature: channel.highestVoucher.signature,
163
+ },
157
164
  requiredCumulative: requiredCumulative.toString(),
158
165
  settled: channel.settledOnChain.toString(),
159
166
  spent: channel.spent.toString(),
@@ -556,8 +556,21 @@ describe('precompile server session unit guardrails', () => {
556
556
  test('request returns a server session snapshot for a known precompile channel', async () => {
557
557
  const { method, store } = createServer()
558
558
  const openPayload = await createOpenPayload({ initialAmount: 100n })
559
+ const highestVoucher = await ClientOps.createVoucherPayload(
560
+ createSigningClient(),
561
+ payer,
562
+ openPayload.descriptor,
563
+ Types.uint96(250n),
564
+ chainId,
565
+ )
566
+ if (highestVoucher.action !== 'voucher') throw new Error('expected voucher payload')
559
567
  await persistPrecompileChannel(store, openPayload, {
560
568
  highestVoucherAmount: 250n,
569
+ highestVoucher: {
570
+ channelId: openPayload.channelId,
571
+ cumulativeAmount: 250n,
572
+ signature: highestVoucher.signature,
573
+ },
561
574
  spent: 200n,
562
575
  units: 2,
563
576
  })
@@ -585,6 +598,11 @@ describe('precompile server session unit guardrails', () => {
585
598
  deposit: '1000',
586
599
  descriptor: openPayload.descriptor,
587
600
  escrow: tip20ChannelEscrow,
601
+ highestVoucher: {
602
+ channelId: openPayload.channelId,
603
+ cumulativeAmount: '250',
604
+ signature: highestVoucher.signature,
605
+ },
588
606
  requiredCumulative: '201',
589
607
  settled: '0',
590
608
  spent: '200',
@@ -594,6 +612,14 @@ describe('precompile server session unit guardrails', () => {
594
612
 
595
613
  test('request can resolve a server session snapshot from request identity', async () => {
596
614
  const openPayload = await createOpenPayload({ initialAmount: 100n })
615
+ const highestVoucher = await ClientOps.createVoucherPayload(
616
+ createSigningClient(),
617
+ payer,
618
+ openPayload.descriptor,
619
+ Types.uint96(250n),
620
+ chainId,
621
+ )
622
+ if (highestVoucher.action !== 'voucher') throw new Error('expected voucher payload')
597
623
  const { method, store } = createServer({
598
624
  resolveChannelId({ request, credential, paymentRequest, store: hookStore }) {
599
625
  expect(request?.headers.get('cookie')).toBe('sid=session-1')
@@ -605,6 +631,11 @@ describe('precompile server session unit guardrails', () => {
605
631
  })
606
632
  await persistPrecompileChannel(store, openPayload, {
607
633
  highestVoucherAmount: 250n,
634
+ highestVoucher: {
635
+ channelId: openPayload.channelId,
636
+ cumulativeAmount: 250n,
637
+ signature: highestVoucher.signature,
638
+ },
608
639
  spent: 200n,
609
640
  units: 2,
610
641
  })
@@ -661,8 +692,21 @@ describe('precompile server session unit guardrails', () => {
661
692
  test('request uses zero effective amount for non-content snapshot hints', async () => {
662
693
  const { method, store } = createServer()
663
694
  const openPayload = await createOpenPayload({ initialAmount: 100n })
695
+ const highestVoucher = await ClientOps.createVoucherPayload(
696
+ createSigningClient(),
697
+ payer,
698
+ openPayload.descriptor,
699
+ Types.uint96(250n),
700
+ chainId,
701
+ )
702
+ if (highestVoucher.action !== 'voucher') throw new Error('expected voucher payload')
664
703
  await persistPrecompileChannel(store, openPayload, {
665
704
  highestVoucherAmount: 250n,
705
+ highestVoucher: {
706
+ channelId: openPayload.channelId,
707
+ cumulativeAmount: 250n,
708
+ signature: highestVoucher.signature,
709
+ },
666
710
  spent: 250n,
667
711
  units: 2,
668
712
  })
@@ -2032,8 +2076,21 @@ describe('precompile server session unit guardrails', () => {
2032
2076
  const rawStore = Store.memory()
2033
2077
  const store = channelStore(rawStore)
2034
2078
  const openPayload = await createOpenPayload({ initialAmount: 1n })
2079
+ const highestVoucher = await ClientOps.createVoucherPayload(
2080
+ createSigningClient(),
2081
+ payer,
2082
+ openPayload.descriptor,
2083
+ Types.uint96(5n),
2084
+ chainId,
2085
+ )
2086
+ if (highestVoucher.action !== 'voucher') throw new Error('expected voucher payload')
2035
2087
  await persistPrecompileChannel(store, openPayload, {
2036
2088
  highestVoucherAmount: 5n,
2089
+ highestVoucher: {
2090
+ channelId: openPayload.channelId,
2091
+ cumulativeAmount: 5n,
2092
+ signature: highestVoucher.signature,
2093
+ },
2037
2094
  spent: 3n,
2038
2095
  units: 3,
2039
2096
  })
@@ -2056,6 +2113,10 @@ describe('precompile server session unit guardrails', () => {
2056
2113
  expect(snapshot).toMatchObject({
2057
2114
  channelId: openPayload.channelId,
2058
2115
  acceptedCumulative: '5',
2116
+ highestVoucher: {
2117
+ channelId: openPayload.channelId,
2118
+ cumulativeAmount: '5',
2119
+ },
2059
2120
  requiredCumulative: '3',
2060
2121
  spent: '3',
2061
2122
  })
@@ -3337,3 +3398,403 @@ describe('precompile server session unit guardrails', () => {
3337
3398
  ).rejects.toThrow(/tx sender .* is not the channel payee/)
3338
3399
  })
3339
3400
  })
3401
+
3402
+ describe('onSessionSettlement', () => {
3403
+ function createSettleClient(channelId: Hex, settledAmount: bigint) {
3404
+ return createClient({
3405
+ account: payer,
3406
+ chain: testChain,
3407
+ transport: custom({
3408
+ async request(args) {
3409
+ if (args.method === 'eth_chainId') return `0x${chainId.toString(16)}`
3410
+ if (args.method === 'eth_getTransactionCount') return '0x0'
3411
+ if (args.method === 'eth_estimateGas') return '0x5208'
3412
+ if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
3413
+ if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
3414
+ if (args.method === 'eth_sendRawTransaction') return `0x${'cc'.repeat(32)}`
3415
+ if (args.method === 'eth_sendTransaction') return `0x${'cc'.repeat(32)}`
3416
+ if (args.method === 'eth_getTransactionReceipt')
3417
+ return transactionReceipt([settledLog(channelId, settledAmount)])
3418
+ if (args.method === 'eth_call') {
3419
+ return encodeFunctionResult({
3420
+ abi: escrowAbi,
3421
+ functionName: 'getChannelState',
3422
+ result: { settled: settledAmount, deposit: 1_000n, closeRequestedAt: 0 },
3423
+ })
3424
+ }
3425
+ throw new Error(`unexpected rpc request: ${args.method}`)
3426
+ },
3427
+ }),
3428
+ })
3429
+ }
3430
+
3431
+ test('fires onSessionSettlement for explicit settle() calls', async () => {
3432
+ const events: { trigger: string; txHash: string; amount: bigint; delta: bigint }[] = []
3433
+ const store = Store.memory()
3434
+ const openPayload = await createOpenPayload()
3435
+ await persistPrecompileChannel(channelStore(store), openPayload, {
3436
+ payee: payer.address,
3437
+ spent: 100n,
3438
+ highestVoucherAmount: 100n,
3439
+ highestVoucher: {
3440
+ channelId: openPayload.channelId,
3441
+ cumulativeAmount: 100n,
3442
+ signature: '0x1234',
3443
+ },
3444
+ })
3445
+
3446
+ const { settle } = await import('./Session.js')
3447
+ const client = createSettleClient(openPayload.channelId, 100n)
3448
+ await settle(store, client, openPayload.channelId, {
3449
+ onSessionSettlement: (ctx) => {
3450
+ events.push({
3451
+ trigger: ctx.trigger,
3452
+ txHash: ctx.txHash,
3453
+ amount: ctx.amount,
3454
+ delta: ctx.delta,
3455
+ })
3456
+ },
3457
+ })
3458
+
3459
+ expect(events).toHaveLength(1)
3460
+ expect(events[0]).toMatchObject({
3461
+ trigger: 'settle',
3462
+ txHash: `0x${'cc'.repeat(32)}`,
3463
+ amount: 100n,
3464
+ delta: 100n,
3465
+ })
3466
+ })
3467
+
3468
+ test('fires onSessionSettlement for auto-scheduled settlements', async () => {
3469
+ const events: { trigger: string; channelId: string; amount: bigint; delta: bigint }[] = []
3470
+ const rawStore = Store.memory()
3471
+ const openPayload = await createOpenPayload()
3472
+ const store = channelStore(rawStore)
3473
+ await persistPrecompileChannel(store, openPayload, {
3474
+ payee: payer.address,
3475
+ spent: 500n,
3476
+ units: 10,
3477
+ highestVoucherAmount: 500n,
3478
+ highestVoucher: {
3479
+ channelId: openPayload.channelId,
3480
+ cumulativeAmount: 500n,
3481
+ signature: '0x1234',
3482
+ },
3483
+ })
3484
+
3485
+ const { maybeSettleScheduled } = await import('./Settlement.js')
3486
+ const client = createSettleClient(openPayload.channelId, 500n)
3487
+ const channel = await store.getChannel(openPayload.channelId)
3488
+
3489
+ await maybeSettleScheduled({
3490
+ account: payer,
3491
+ channel: channel!,
3492
+ client,
3493
+ schedule: { units: 5 },
3494
+ store,
3495
+ onSessionSettlement: (ctx) => {
3496
+ events.push({
3497
+ trigger: ctx.trigger,
3498
+ channelId: ctx.channelId,
3499
+ amount: ctx.amount,
3500
+ delta: ctx.delta,
3501
+ })
3502
+ },
3503
+ })
3504
+
3505
+ expect(events).toHaveLength(1)
3506
+ expect(events[0]).toMatchObject({
3507
+ trigger: 'scheduled',
3508
+ channelId: openPayload.channelId,
3509
+ amount: 500n,
3510
+ delta: 500n,
3511
+ })
3512
+ })
3513
+
3514
+ test('delta reflects incremental scheduled settlement with prior on-chain settlement', async () => {
3515
+ const events: { trigger: string; amount: bigint; delta: bigint }[] = []
3516
+ const rawStore = Store.memory()
3517
+ const openPayload = await createOpenPayload()
3518
+ const store = channelStore(rawStore)
3519
+ await persistPrecompileChannel(store, openPayload, {
3520
+ payee: payer.address,
3521
+ spent: 500n,
3522
+ units: 10,
3523
+ settledOnChain: 200n,
3524
+ highestVoucherAmount: 500n,
3525
+ highestVoucher: {
3526
+ channelId: openPayload.channelId,
3527
+ cumulativeAmount: 500n,
3528
+ signature: '0x1234',
3529
+ },
3530
+ })
3531
+
3532
+ const { maybeSettleScheduled } = await import('./Settlement.js')
3533
+ const client = createSettleClient(openPayload.channelId, 500n)
3534
+ const channel = await store.getChannel(openPayload.channelId)
3535
+
3536
+ await maybeSettleScheduled({
3537
+ account: payer,
3538
+ channel: channel!,
3539
+ client,
3540
+ schedule: { units: 5 },
3541
+ store,
3542
+ onSessionSettlement: (ctx) => {
3543
+ events.push({ trigger: ctx.trigger, amount: ctx.amount, delta: ctx.delta })
3544
+ },
3545
+ })
3546
+
3547
+ expect(events).toHaveLength(1)
3548
+ expect(events[0]).toMatchObject({
3549
+ trigger: 'scheduled',
3550
+ amount: 500n,
3551
+ delta: 300n,
3552
+ })
3553
+ })
3554
+
3555
+ test('fires onSessionSettlement for cooperative close', async () => {
3556
+ const events: { trigger: string; txHash: string; amount: bigint; delta: bigint }[] = []
3557
+ const openPayload = await createOpenPayload()
3558
+ const closedAmount = 100n
3559
+ const refundedAmount = 900n
3560
+ const rawStore = Store.memory()
3561
+
3562
+ const closeClient = createClient({
3563
+ account: payer,
3564
+ chain: testChain,
3565
+ transport: custom({
3566
+ async request(args) {
3567
+ if (args.method === 'eth_chainId') return `0x${chainId.toString(16)}`
3568
+ if (args.method === 'eth_getTransactionCount') return '0x0'
3569
+ if (args.method === 'eth_estimateGas') return '0x5208'
3570
+ if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
3571
+ if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
3572
+ if (args.method === 'eth_sendRawTransaction') return `0x${'dd'.repeat(32)}`
3573
+ if (args.method === 'eth_sendTransaction') return `0x${'dd'.repeat(32)}`
3574
+ if (args.method === 'eth_getTransactionReceipt')
3575
+ return transactionReceipt([
3576
+ closedLog(openPayload.channelId, closedAmount, refundedAmount),
3577
+ ])
3578
+ if (args.method === 'eth_call') {
3579
+ return encodeFunctionResult({
3580
+ abi: escrowAbi,
3581
+ functionName: 'getChannelState',
3582
+ result: { settled: 0n, deposit: 1_000n, closeRequestedAt: 0 },
3583
+ })
3584
+ }
3585
+ throw new Error(`unexpected rpc request: ${args.method}`)
3586
+ },
3587
+ }),
3588
+ })
3589
+
3590
+ const method = session({
3591
+ amount: '1',
3592
+ chainId,
3593
+ currency: token,
3594
+ decimals: 0,
3595
+ recipient: payee,
3596
+ store: rawStore,
3597
+ unitType: 'request',
3598
+ getClient: () => closeClient,
3599
+ onSessionSettlement: (ctx) => {
3600
+ events.push({
3601
+ trigger: ctx.trigger,
3602
+ txHash: ctx.txHash,
3603
+ amount: ctx.amount,
3604
+ delta: ctx.delta,
3605
+ })
3606
+ },
3607
+ })
3608
+
3609
+ await persistPrecompileChannel(channelStore(rawStore), openPayload, {
3610
+ payee: payer.address,
3611
+ spent: 100n,
3612
+ highestVoucherAmount: 100n,
3613
+ highestVoucher: {
3614
+ channelId: openPayload.channelId,
3615
+ cumulativeAmount: 100n,
3616
+ signature: '0x1234',
3617
+ },
3618
+ })
3619
+
3620
+ const closeSignature = await Voucher.signVoucher(
3621
+ createSigningClient(),
3622
+ payer,
3623
+ { channelId: openPayload.channelId, cumulativeAmount: 100n },
3624
+ tip20ChannelEscrow,
3625
+ chainId,
3626
+ )
3627
+
3628
+ await method.verify({
3629
+ credential: {
3630
+ challenge: makeChallenge(openPayload.channelId),
3631
+ payload: {
3632
+ action: 'close',
3633
+ channelId: openPayload.channelId,
3634
+ descriptor: openPayload.descriptor,
3635
+ cumulativeAmount: '100',
3636
+ signature: closeSignature,
3637
+ },
3638
+ },
3639
+ request: verifyRequest(openPayload.channelId),
3640
+ })
3641
+
3642
+ expect(events).toHaveLength(1)
3643
+ expect(events[0]).toMatchObject({
3644
+ trigger: 'close',
3645
+ txHash: `0x${'dd'.repeat(32)}`,
3646
+ amount: closedAmount,
3647
+ delta: closedAmount,
3648
+ })
3649
+ })
3650
+
3651
+ test('delta reflects incremental settlement when channel has prior on-chain settlement', async () => {
3652
+ const events: { trigger: string; amount: bigint; delta: bigint }[] = []
3653
+ const store = Store.memory()
3654
+ const openPayload = await createOpenPayload()
3655
+ await persistPrecompileChannel(channelStore(store), openPayload, {
3656
+ payee: payer.address,
3657
+ spent: 300n,
3658
+ settledOnChain: 200n,
3659
+ highestVoucherAmount: 300n,
3660
+ highestVoucher: {
3661
+ channelId: openPayload.channelId,
3662
+ cumulativeAmount: 300n,
3663
+ signature: '0x1234',
3664
+ },
3665
+ })
3666
+
3667
+ const { settle } = await import('./Session.js')
3668
+ const client = createSettleClient(openPayload.channelId, 300n)
3669
+ await settle(store, client, openPayload.channelId, {
3670
+ onSessionSettlement: (ctx) => {
3671
+ events.push({ trigger: ctx.trigger, amount: ctx.amount, delta: ctx.delta })
3672
+ },
3673
+ })
3674
+
3675
+ expect(events).toHaveLength(1)
3676
+ expect(events[0]).toMatchObject({
3677
+ trigger: 'settle',
3678
+ amount: 300n,
3679
+ delta: 100n,
3680
+ })
3681
+ })
3682
+
3683
+ test('delta reflects incremental close when channel has prior on-chain settlement', async () => {
3684
+ const events: { trigger: string; amount: bigint; delta: bigint }[] = []
3685
+ const openPayload = await createOpenPayload()
3686
+ const priorSettled = 50n
3687
+ const closedAmount = 100n
3688
+ const refundedAmount = 900n
3689
+ const rawStore = Store.memory()
3690
+
3691
+ const closeClient = createClient({
3692
+ account: payer,
3693
+ chain: testChain,
3694
+ transport: custom({
3695
+ async request(args) {
3696
+ if (args.method === 'eth_chainId') return `0x${chainId.toString(16)}`
3697
+ if (args.method === 'eth_getTransactionCount') return '0x0'
3698
+ if (args.method === 'eth_estimateGas') return '0x5208'
3699
+ if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
3700
+ if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
3701
+ if (args.method === 'eth_sendRawTransaction') return `0x${'dd'.repeat(32)}`
3702
+ if (args.method === 'eth_sendTransaction') return `0x${'dd'.repeat(32)}`
3703
+ if (args.method === 'eth_getTransactionReceipt')
3704
+ return transactionReceipt([
3705
+ closedLog(openPayload.channelId, closedAmount, refundedAmount),
3706
+ ])
3707
+ if (args.method === 'eth_call') {
3708
+ return encodeFunctionResult({
3709
+ abi: escrowAbi,
3710
+ functionName: 'getChannelState',
3711
+ result: { settled: priorSettled, deposit: 1_000n, closeRequestedAt: 0 },
3712
+ })
3713
+ }
3714
+ throw new Error(`unexpected rpc request: ${args.method}`)
3715
+ },
3716
+ }),
3717
+ })
3718
+
3719
+ const method = session({
3720
+ amount: '1',
3721
+ chainId,
3722
+ currency: token,
3723
+ decimals: 0,
3724
+ recipient: payee,
3725
+ store: rawStore,
3726
+ unitType: 'request',
3727
+ getClient: () => closeClient,
3728
+ onSessionSettlement: (ctx) => {
3729
+ events.push({ trigger: ctx.trigger, amount: ctx.amount, delta: ctx.delta })
3730
+ },
3731
+ })
3732
+
3733
+ await persistPrecompileChannel(channelStore(rawStore), openPayload, {
3734
+ payee: payer.address,
3735
+ spent: 100n,
3736
+ settledOnChain: priorSettled,
3737
+ highestVoucherAmount: 100n,
3738
+ highestVoucher: {
3739
+ channelId: openPayload.channelId,
3740
+ cumulativeAmount: 100n,
3741
+ signature: '0x1234',
3742
+ },
3743
+ })
3744
+
3745
+ const closeSignature = await Voucher.signVoucher(
3746
+ createSigningClient(),
3747
+ payer,
3748
+ { channelId: openPayload.channelId, cumulativeAmount: 100n },
3749
+ tip20ChannelEscrow,
3750
+ chainId,
3751
+ )
3752
+
3753
+ await method.verify({
3754
+ credential: {
3755
+ challenge: makeChallenge(openPayload.channelId),
3756
+ payload: {
3757
+ action: 'close',
3758
+ channelId: openPayload.channelId,
3759
+ descriptor: openPayload.descriptor,
3760
+ cumulativeAmount: '100',
3761
+ signature: closeSignature,
3762
+ },
3763
+ },
3764
+ request: verifyRequest(openPayload.channelId),
3765
+ })
3766
+
3767
+ expect(events).toHaveLength(1)
3768
+ expect(events[0]).toMatchObject({
3769
+ trigger: 'close',
3770
+ amount: closedAmount,
3771
+ delta: closedAmount - priorSettled,
3772
+ })
3773
+ })
3774
+
3775
+ test('isolates errors in onSessionSettlement handler', async () => {
3776
+ const store = Store.memory()
3777
+ const openPayload = await createOpenPayload()
3778
+ await persistPrecompileChannel(channelStore(store), openPayload, {
3779
+ payee: payer.address,
3780
+ spent: 100n,
3781
+ highestVoucherAmount: 100n,
3782
+ highestVoucher: {
3783
+ channelId: openPayload.channelId,
3784
+ cumulativeAmount: 100n,
3785
+ signature: '0x1234',
3786
+ },
3787
+ })
3788
+
3789
+ const { settle } = await import('./Session.js')
3790
+ const client = createSettleClient(openPayload.channelId, 100n)
3791
+
3792
+ await expect(
3793
+ settle(store, client, openPayload.channelId, {
3794
+ onSessionSettlement: () => {
3795
+ throw new Error('observer exploded')
3796
+ },
3797
+ }),
3798
+ ).resolves.toBe(`0x${'cc'.repeat(32)}`)
3799
+ })
3800
+ })