mppx 0.8.11 → 0.8.13

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 (218) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +16 -1
  3. package/dist/cli/account.d.ts +88 -0
  4. package/dist/cli/account.d.ts.map +1 -1
  5. package/dist/cli/account.js +39 -9
  6. package/dist/cli/account.js.map +1 -1
  7. package/dist/cli/cli.d.ts +2 -1
  8. package/dist/cli/cli.d.ts.map +1 -1
  9. package/dist/cli/cli.js +59 -2
  10. package/dist/cli/cli.js.map +1 -1
  11. package/dist/cli/sessions/Manager.d.ts +31 -0
  12. package/dist/cli/sessions/Manager.d.ts.map +1 -0
  13. package/dist/cli/sessions/Manager.js +56 -0
  14. package/dist/cli/sessions/Manager.js.map +1 -0
  15. package/dist/cli/sessions/commands.d.ts +36 -0
  16. package/dist/cli/sessions/commands.d.ts.map +1 -0
  17. package/dist/cli/sessions/commands.js +407 -0
  18. package/dist/cli/sessions/commands.js.map +1 -0
  19. package/dist/cli/sessions/request.d.ts +35 -0
  20. package/dist/cli/sessions/request.d.ts.map +1 -0
  21. package/dist/cli/sessions/request.js +271 -0
  22. package/dist/cli/sessions/request.js.map +1 -0
  23. package/dist/cli/sessions/store.d.ts +134 -0
  24. package/dist/cli/sessions/store.d.ts.map +1 -0
  25. package/dist/cli/sessions/store.js +723 -0
  26. package/dist/cli/sessions/store.js.map +1 -0
  27. package/dist/cli/utils.d.ts +2 -0
  28. package/dist/cli/utils.d.ts.map +1 -1
  29. package/dist/cli/utils.js +12 -0
  30. package/dist/cli/utils.js.map +1 -1
  31. package/dist/cli/validate/index.d.ts.map +1 -1
  32. package/dist/cli/validate/index.js +12 -6
  33. package/dist/cli/validate/index.js.map +1 -1
  34. package/dist/cli/validate/messages.d.ts +2 -0
  35. package/dist/cli/validate/messages.d.ts.map +1 -0
  36. package/dist/cli/validate/messages.js +8 -0
  37. package/dist/cli/validate/messages.js.map +1 -0
  38. package/dist/client/internal/Fetch.d.ts.map +1 -1
  39. package/dist/client/internal/Fetch.js +35 -6
  40. package/dist/client/internal/Fetch.js.map +1 -1
  41. package/dist/client/internal/MethodResponse.d.ts +23 -0
  42. package/dist/client/internal/MethodResponse.d.ts.map +1 -0
  43. package/dist/client/internal/MethodResponse.js +15 -0
  44. package/dist/client/internal/MethodResponse.js.map +1 -0
  45. package/dist/client/node.d.ts +29 -0
  46. package/dist/client/node.d.ts.map +1 -0
  47. package/dist/client/node.js +177 -0
  48. package/dist/client/node.js.map +1 -0
  49. package/dist/internal/types.d.ts +24 -0
  50. package/dist/internal/types.d.ts.map +1 -1
  51. package/dist/stripe/server/Charge.d.ts +8 -4
  52. package/dist/stripe/server/Charge.d.ts.map +1 -1
  53. package/dist/stripe/server/Charge.js.map +1 -1
  54. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  55. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  56. package/dist/stripe/server/internal/html.gen.js +1 -1
  57. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  58. package/dist/tempo/client/Methods.d.ts +1 -1
  59. package/dist/tempo/internal/types.d.ts +10 -1
  60. package/dist/tempo/internal/types.d.ts.map +1 -1
  61. package/dist/tempo/legacy/client/ChannelOps.d.ts +2 -0
  62. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  63. package/dist/tempo/legacy/client/ChannelOps.js +4 -0
  64. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  65. package/dist/tempo/legacy/client/Session.d.ts +2 -2
  66. package/dist/tempo/legacy/client/Session.d.ts.map +1 -1
  67. package/dist/tempo/legacy/client/Session.js +7 -0
  68. package/dist/tempo/legacy/client/Session.js.map +1 -1
  69. package/dist/tempo/legacy/server/Session.d.ts +3 -3
  70. package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
  71. package/dist/tempo/legacy/server/Session.js.map +1 -1
  72. package/dist/tempo/server/Charge.d.ts +3 -2
  73. package/dist/tempo/server/Charge.d.ts.map +1 -1
  74. package/dist/tempo/server/Charge.js.map +1 -1
  75. package/dist/tempo/server/Methods.d.ts +2 -23
  76. package/dist/tempo/server/Methods.d.ts.map +1 -1
  77. package/dist/tempo/server/Methods.js +2 -2
  78. package/dist/tempo/server/Methods.js.map +1 -1
  79. package/dist/tempo/server/Subscription.d.ts +3 -2
  80. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  81. package/dist/tempo/server/Subscription.js.map +1 -1
  82. package/dist/tempo/server/index.d.ts +1 -1
  83. package/dist/tempo/server/index.d.ts.map +1 -1
  84. package/dist/tempo/server/index.js.map +1 -1
  85. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  86. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  87. package/dist/tempo/server/internal/html.gen.js +1 -1
  88. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  89. package/dist/tempo/session/Snapshot.d.ts +9 -0
  90. package/dist/tempo/session/Snapshot.d.ts.map +1 -1
  91. package/dist/tempo/session/Snapshot.js +6 -0
  92. package/dist/tempo/session/Snapshot.js.map +1 -1
  93. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  94. package/dist/tempo/session/client/ChannelOps.js +11 -2
  95. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  96. package/dist/tempo/session/client/CredentialState.d.ts +21 -1
  97. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  98. package/dist/tempo/session/client/CredentialState.js +66 -1
  99. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  100. package/dist/tempo/session/client/Session.d.ts +1 -1
  101. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  102. package/dist/tempo/session/client/Session.js +53 -1
  103. package/dist/tempo/session/client/Session.js.map +1 -1
  104. package/dist/tempo/session/client/SessionManager.d.ts +2 -2
  105. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  106. package/dist/tempo/session/client/SessionManager.js +130 -30
  107. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  108. package/dist/tempo/session/client/Transports.d.ts +21 -2
  109. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  110. package/dist/tempo/session/client/Transports.js +129 -18
  111. package/dist/tempo/session/client/Transports.js.map +1 -1
  112. package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
  113. package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
  114. package/dist/tempo/session/client/internal/SessionManager.js +13 -0
  115. package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
  116. package/dist/tempo/session/precompile/Chain.js +13 -4
  117. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  118. package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
  119. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  120. package/dist/tempo/session/server/CredentialVerification.js +14 -0
  121. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  122. package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
  123. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  124. package/dist/tempo/session/server/MeteredStream.js +5 -4
  125. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  126. package/dist/tempo/session/server/RequestState.d.ts +5 -1
  127. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  128. package/dist/tempo/session/server/RequestState.js +10 -4
  129. package/dist/tempo/session/server/RequestState.js.map +1 -1
  130. package/dist/tempo/session/server/Session.d.ts +21 -6
  131. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  132. package/dist/tempo/session/server/Session.js +4 -1
  133. package/dist/tempo/session/server/Session.js.map +1 -1
  134. package/dist/tempo/session/server/Settlement.d.ts +20 -0
  135. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  136. package/dist/tempo/session/server/Settlement.js +20 -0
  137. package/dist/tempo/session/server/Settlement.js.map +1 -1
  138. package/dist/tempo/session/server/Ws.d.ts +2 -0
  139. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  140. package/dist/tempo/session/server/Ws.js.map +1 -1
  141. package/dist/tempo/session/server/index.d.ts +1 -1
  142. package/dist/tempo/session/server/index.d.ts.map +1 -1
  143. package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
  144. package/dist/validation/core.d.ts.map +1 -1
  145. package/dist/validation/core.js +6 -2
  146. package/dist/validation/core.js.map +1 -1
  147. package/dist/viem/Client.d.ts.map +1 -1
  148. package/dist/viem/Client.js +24 -17
  149. package/dist/viem/Client.js.map +1 -1
  150. package/package.json +6 -1
  151. package/src/cli/account.ts +45 -10
  152. package/src/cli/cli.test.ts +165 -70
  153. package/src/cli/cli.ts +67 -2
  154. package/src/cli/mcp.test.ts +11 -0
  155. package/src/cli/sessions/Manager.test.ts +249 -0
  156. package/src/cli/sessions/Manager.ts +93 -0
  157. package/src/cli/sessions/commands.ts +444 -0
  158. package/src/cli/sessions/request.test.ts +51 -0
  159. package/src/cli/sessions/request.ts +353 -0
  160. package/src/cli/sessions/store.test.ts +581 -0
  161. package/src/cli/sessions/store.ts +940 -0
  162. package/src/cli/utils.test.ts +23 -0
  163. package/src/cli/utils.ts +10 -0
  164. package/src/cli/validate/index.ts +12 -14
  165. package/src/cli/validate/messages.ts +7 -0
  166. package/src/cli/validate.test.ts +38 -5
  167. package/src/client/internal/Fetch.test.ts +29 -0
  168. package/src/client/internal/Fetch.ts +51 -6
  169. package/src/client/internal/MethodResponse.ts +39 -0
  170. package/src/client/node.test.ts +115 -0
  171. package/src/client/node.ts +247 -0
  172. package/src/internal/types.test-d.ts +21 -0
  173. package/src/internal/types.ts +33 -0
  174. package/src/server/Mppx.test-d.ts +42 -0
  175. package/src/stripe/Methods.test.ts +10 -0
  176. package/src/stripe/server/Charge.test-d.ts +66 -0
  177. package/src/stripe/server/Charge.ts +8 -5
  178. package/src/stripe/server/internal/html.gen.ts +1 -1
  179. package/src/tempo/internal/types.ts +13 -2
  180. package/src/tempo/legacy/client/ChannelOps.test.ts +21 -0
  181. package/src/tempo/legacy/client/ChannelOps.ts +7 -0
  182. package/src/tempo/legacy/client/Session.test.ts +102 -1
  183. package/src/tempo/legacy/client/Session.ts +10 -0
  184. package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
  185. package/src/tempo/legacy/server/Session.ts +6 -5
  186. package/src/tempo/server/Charge.ts +6 -4
  187. package/src/tempo/server/Methods.ts +5 -4
  188. package/src/tempo/server/Subscription.ts +6 -4
  189. package/src/tempo/server/index.ts +5 -1
  190. package/src/tempo/server/internal/html.gen.ts +1 -1
  191. package/src/tempo/session/README.md +201 -0
  192. package/src/tempo/session/Snapshot.ts +21 -0
  193. package/src/tempo/session/client/ChannelOps.test.ts +40 -0
  194. package/src/tempo/session/client/ChannelOps.ts +12 -2
  195. package/src/tempo/session/client/CredentialState.test.ts +2 -2
  196. package/src/tempo/session/client/CredentialState.ts +103 -1
  197. package/src/tempo/session/client/Session.test.ts +86 -4
  198. package/src/tempo/session/client/Session.ts +73 -1
  199. package/src/tempo/session/client/SessionManager.test.ts +137 -4
  200. package/src/tempo/session/client/SessionManager.ts +141 -33
  201. package/src/tempo/session/client/Transports.test.ts +225 -8
  202. package/src/tempo/session/client/Transports.ts +156 -21
  203. package/src/tempo/session/client/internal/SessionManager.ts +36 -0
  204. package/src/tempo/session/precompile/Chain.test.ts +26 -0
  205. package/src/tempo/session/precompile/Chain.ts +17 -4
  206. package/src/tempo/session/server/CredentialVerification.ts +18 -1
  207. package/src/tempo/session/server/MeteredStream.ts +12 -5
  208. package/src/tempo/session/server/RequestState.test.ts +39 -3
  209. package/src/tempo/session/server/RequestState.ts +13 -6
  210. package/src/tempo/session/server/Session.test.ts +461 -0
  211. package/src/tempo/session/server/Session.ts +31 -8
  212. package/src/tempo/session/server/Settlement.ts +45 -0
  213. package/src/tempo/session/server/Ws.test.ts +113 -0
  214. package/src/tempo/session/server/Ws.ts +2 -0
  215. package/src/tempo/session/server/index.ts +2 -0
  216. package/src/validation/core.ts +7 -1
  217. package/src/viem/Client.test.ts +44 -1
  218. package/src/viem/Client.ts +31 -17
@@ -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
+ })
@@ -44,10 +44,16 @@ import {
44
44
  import { respondToSessionCredential } from './RequestState.js'
45
45
  import { applyVerifiedHttpAccounting, chargeSessionChannel } from './Settlement.js'
46
46
  import { maybeSettleScheduled } from './Settlement.js'
47
- import { resolveSettlementSchedule, type SettlementSchedule } from './Settlement.js'
47
+ import {
48
+ resolveSettlementSchedule,
49
+ type OnSessionSettlement,
50
+ type SettlementSchedule,
51
+ } from './Settlement.js'
48
52
 
49
53
  /** Server-side automatic settlement schedule. */
50
54
  export type { SettlementSchedule } from './Settlement.js'
55
+ /** Server-side settlement event hook types. */
56
+ export type { OnSessionSettlement, SessionSettlementContext } from './Settlement.js'
51
57
  /** Server-side hook types for request-identity channel bootstrap. */
52
58
  export type {
53
59
  ResolveSessionChannelId,
@@ -312,6 +318,7 @@ export function session<const parameters extends session.Parameters>(
312
318
  unitType,
313
319
  } = parameters
314
320
  const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals)
321
+ const onSessionSettlement = parameters.onSessionSettlement
315
322
 
316
323
  const store = ChannelStore.fromStore(rawStore)
317
324
  const lastOnChainVerified = new Map<Hex, number>()
@@ -424,6 +431,7 @@ export function session<const parameters extends session.Parameters>(
424
431
  feeToken: parameters.feeToken,
425
432
  lastOnChainVerified,
426
433
  minVoucherDelta: context.minVoucherDelta,
434
+ onSessionSettlement,
427
435
  payload,
428
436
  store,
429
437
  })
@@ -444,6 +452,7 @@ export function session<const parameters extends session.Parameters>(
444
452
  ...(typeof context.feePayer === 'object' ? { feePayer: context.feePayer } : {}),
445
453
  feePayerPolicy: parameters.feePayerPolicy,
446
454
  feeToken: parameters.feeToken,
455
+ onSessionSettlement,
447
456
  schedule: settlementSchedule,
448
457
  store,
449
458
  channel,
@@ -492,9 +501,20 @@ export namespace session {
492
501
  feePayerPolicy?: FeePayerPolicy | undefined
493
502
  /** Minimum voucher delta to accept (numeric string, default: "0"). */
494
503
  minVoucherDelta?: string | undefined
495
- /** Resolve a reusable channel ID from request identity when no credential/request channel ID is present. */
504
+ /**
505
+ * Maps authenticated application identity and payment scope to an existing channel ID.
506
+ * Called only when the request does not already supply a channel ID. MPPx then loads that
507
+ * ID from `store` and validates the channel before including its snapshot in a challenge or
508
+ * bootstrap response. The store is not searched automatically and does not need to implement
509
+ * a secondary-index format.
510
+ */
496
511
  resolveChannelId?: ResolveSessionChannelId | undefined
497
- /** Enables same-route HEAD bootstrap using a zero-amount identity proof. */
512
+ /**
513
+ * Enables same-route `HEAD` recovery before a paid request. The server issues a zero-amount
514
+ * identity challenge, verifies the returned proof, passes its authenticated `source` to
515
+ * `resolveChannelId`, validates the loaded channel's payment scope, and returns a snapshot
516
+ * containing the signed highest voucher for client rehydration.
517
+ */
498
518
  bootstrap?: boolean | undefined
499
519
  /**
500
520
  * Atomic store backend for channel state.
@@ -510,6 +530,8 @@ export namespace session {
510
530
  chainId?: number | undefined
511
531
  /** TIP20EscrowChannel precompile address override. */
512
532
  escrowContract?: Address | undefined
533
+ /** Callback invoked after any on-chain settlement or close transaction is confirmed. */
534
+ onSessionSettlement?: OnSessionSettlement | undefined
513
535
  /** Server-owned automatic settlement cadence. Clients do not receive or control this schedule. */
514
536
  settlementSchedule?: SettlementSchedule | undefined
515
537
 
@@ -522,11 +544,12 @@ export namespace session {
522
544
  /** Defaults derived from `session()` parameters for handler type inference. */
523
545
  export type DeriveDefaults<parameters extends Parameters> = types.DeriveDefaults<
524
546
  parameters,
525
- Defaults
526
- > & {
527
- decimals: number
528
- escrowContract: Address
529
- }
547
+ Defaults,
548
+ {
549
+ currency: string
550
+ decimals: number
551
+ }
552
+ >
530
553
  }
531
554
 
532
555
  /**
@@ -16,6 +16,7 @@ import {
16
16
  InsufficientBalanceError,
17
17
  VerificationFailedError,
18
18
  } from '../../../Errors.js'
19
+ import type { MaybePromise } from '../../../internal/types.js'
19
20
  import type * as Method from '../../../Method.js'
20
21
  import * as Store from '../../../Store.js'
21
22
  import type * as FeePayer from '../../internal/fee-payer.js'
@@ -146,6 +147,23 @@ export type SettlementProgress = {
146
147
  units: number
147
148
  }
148
149
 
150
+ /** Context emitted when an on-chain settlement or close transaction is confirmed. */
151
+ export type SessionSettlementContext = Readonly<{
152
+ /** On-chain transaction hash (or signature on Solana). */
153
+ txHash: Hex
154
+ /** Channel ID that was settled. */
155
+ channelId: Hex
156
+ /** The trigger that caused settlement. */
157
+ trigger: 'settle' | 'close' | 'scheduled'
158
+ /** Cumulative amount settled on-chain to the payee (raw token units). */
159
+ amount: bigint
160
+ /** Incremental amount settled in this transaction (raw token units). */
161
+ delta: bigint
162
+ }>
163
+
164
+ /** Callback invoked after an on-chain settlement or close transaction is confirmed. */
165
+ export type OnSessionSettlement = (context: SessionSettlementContext) => MaybePromise<void>
166
+
149
167
  /** Inputs used to mark a channel after automatic scheduled settlement succeeds. */
150
168
  export type MarkSettlementCompleteParameters = {
151
169
  channelId: ChannelStore.State['channelId']
@@ -340,6 +358,8 @@ export type SettlementTransactionOptions = {
340
358
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
341
359
  /** Optional fee token override for settlement. */
342
360
  feeToken?: Address | undefined
361
+ /** Callback invoked after the settlement transaction is confirmed. */
362
+ onSessionSettlement?: OnSessionSettlement | undefined
343
363
  }
344
364
 
345
365
  /** Inputs for applying a server-owned automatic settlement schedule. */
@@ -356,6 +376,8 @@ export type MaybeSettleScheduledParameters = {
356
376
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
357
377
  /** Optional fee token override for settlement. */
358
378
  feeToken?: Address | undefined
379
+ /** Callback invoked after the scheduled settlement transaction is confirmed. */
380
+ onSessionSettlement?: OnSessionSettlement | undefined
359
381
  /** Resolved server-owned settlement cadence. */
360
382
  schedule: ResolvedSettlementSchedule | undefined
361
383
  /** Server-side channel store. */
@@ -400,6 +422,9 @@ export async function maybeSettleScheduled(
400
422
  ...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
401
423
  ...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
402
424
  ...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
425
+ onSessionSettlement: parameters.onSessionSettlement
426
+ ? (ctx) => parameters.onSessionSettlement!({ ...ctx, trigger: 'scheduled' })
427
+ : undefined,
403
428
  })
404
429
  await markSettlementComplete({ channelId: channel.channelId, store })
405
430
  return txHash
@@ -466,6 +491,15 @@ export async function settle(
466
491
  }
467
492
  : current,
468
493
  )
494
+ if (options?.onSessionSettlement) {
495
+ await emitSessionSettlement(options.onSessionSettlement, {
496
+ txHash,
497
+ channelId,
498
+ trigger: 'settle',
499
+ amount: newSettled,
500
+ delta: newSettled - channel.settledOnChain,
501
+ })
502
+ }
469
503
  return txHash
470
504
  }
471
505
 
@@ -480,3 +514,14 @@ export async function settleBatch(
480
514
  for (const channelId of channelIds) hashes.push(await settle(store, client, channelId, options))
481
515
  return hashes
482
516
  }
517
+
518
+ async function emitSessionSettlement(
519
+ onSessionSettlement: OnSessionSettlement,
520
+ context: SessionSettlementContext,
521
+ ): Promise<void> {
522
+ try {
523
+ await onSessionSettlement(Object.freeze(context))
524
+ } catch {
525
+ // Errors are isolated — observers cannot break the settlement flow.
526
+ }
527
+ }