mppx 0.8.13 → 0.8.15

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 (156) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/Errors.d.ts +10 -0
  3. package/dist/Errors.d.ts.map +1 -1
  4. package/dist/Errors.js +11 -2
  5. package/dist/Errors.js.map +1 -1
  6. package/dist/Method.d.ts +105 -3
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +102 -2
  9. package/dist/Method.js.map +1 -1
  10. package/dist/cli/validate/payment.d.ts.map +1 -1
  11. package/dist/cli/validate/payment.js +18 -4
  12. package/dist/cli/validate/payment.js.map +1 -1
  13. package/dist/client/internal/Fetch.d.ts.map +1 -1
  14. package/dist/client/internal/Fetch.js +98 -5
  15. package/dist/client/internal/Fetch.js.map +1 -1
  16. package/dist/client/internal/MethodChallenge.d.ts +19 -0
  17. package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
  18. package/dist/client/internal/MethodChallenge.js +15 -0
  19. package/dist/client/internal/MethodChallenge.js.map +1 -0
  20. package/dist/client/internal/MethodResponse.d.ts +15 -0
  21. package/dist/client/internal/MethodResponse.d.ts.map +1 -1
  22. package/dist/client/internal/MethodResponse.js +10 -0
  23. package/dist/client/internal/MethodResponse.js.map +1 -1
  24. package/dist/server/Mppx.d.ts +23 -4
  25. package/dist/server/Mppx.d.ts.map +1 -1
  26. package/dist/server/Mppx.js +91 -82
  27. package/dist/server/Mppx.js.map +1 -1
  28. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  29. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  30. package/dist/stripe/server/internal/html.gen.js +1 -1
  31. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  32. package/dist/tempo/client/Methods.d.ts +1 -0
  33. package/dist/tempo/client/Methods.d.ts.map +1 -1
  34. package/dist/tempo/internal/fee-payer.d.ts +1 -0
  35. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  36. package/dist/tempo/internal/fee-payer.js +7 -2
  37. package/dist/tempo/internal/fee-payer.js.map +1 -1
  38. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  39. package/dist/tempo/legacy/client/ChannelOps.js +2 -1
  40. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  41. package/dist/tempo/server/Charge.d.ts +46 -77
  42. package/dist/tempo/server/Charge.d.ts.map +1 -1
  43. package/dist/tempo/server/Charge.js +289 -209
  44. package/dist/tempo/server/Charge.js.map +1 -1
  45. package/dist/tempo/server/Methods.d.ts +12 -1
  46. package/dist/tempo/server/Methods.d.ts.map +1 -1
  47. package/dist/tempo/server/Methods.js +4 -0
  48. package/dist/tempo/server/Methods.js.map +1 -1
  49. package/dist/tempo/server/Relay.d.ts +57 -0
  50. package/dist/tempo/server/Relay.d.ts.map +1 -0
  51. package/dist/tempo/server/Relay.js +179 -0
  52. package/dist/tempo/server/Relay.js.map +1 -0
  53. package/dist/tempo/server/SponsorBudget.d.ts +59 -0
  54. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
  55. package/dist/tempo/server/SponsorBudget.js +150 -0
  56. package/dist/tempo/server/SponsorBudget.js.map +1 -0
  57. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  58. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  59. package/dist/tempo/server/internal/html.gen.js +1 -1
  60. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  61. package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
  62. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  63. package/dist/tempo/session/client/ChannelOps.js +9 -4
  64. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  65. package/dist/tempo/session/client/CredentialState.d.ts +5 -1
  66. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  67. package/dist/tempo/session/client/CredentialState.js +37 -19
  68. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  69. package/dist/tempo/session/client/Runtime.d.ts +8 -0
  70. package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
  71. package/dist/tempo/session/client/Runtime.js +17 -0
  72. package/dist/tempo/session/client/Runtime.js.map +1 -1
  73. package/dist/tempo/session/client/Session.d.ts +15 -5
  74. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  75. package/dist/tempo/session/client/Session.js +216 -45
  76. package/dist/tempo/session/client/Session.js.map +1 -1
  77. package/dist/tempo/session/client/SessionManager.d.ts +8 -1
  78. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  79. package/dist/tempo/session/client/SessionManager.js +114 -44
  80. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  81. package/dist/tempo/session/client/Transports.d.ts +16 -6
  82. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  83. package/dist/tempo/session/client/Transports.js +84 -37
  84. package/dist/tempo/session/client/Transports.js.map +1 -1
  85. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  86. package/dist/tempo/session/precompile/Chain.js +96 -8
  87. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  88. package/dist/tempo/session/precompile/Channel.d.ts +2 -0
  89. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
  90. package/dist/tempo/session/precompile/Channel.js +4 -0
  91. package/dist/tempo/session/precompile/Channel.js.map +1 -1
  92. package/dist/tempo/session/server/RequestState.js +3 -1
  93. package/dist/tempo/session/server/RequestState.js.map +1 -1
  94. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  95. package/dist/tempo/session/server/Ws.js +3 -0
  96. package/dist/tempo/session/server/Ws.js.map +1 -1
  97. package/dist/tempo/subscription/KeyAuthorization.d.ts +1 -829
  98. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  99. package/dist/tempo/subscription/KeyAuthorization.js +14 -3
  100. package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
  101. package/dist/viem/Client.d.ts.map +1 -1
  102. package/dist/viem/Client.js +6 -1
  103. package/dist/viem/Client.js.map +1 -1
  104. package/package.json +2 -2
  105. package/src/Errors.test.ts +23 -0
  106. package/src/Errors.ts +21 -2
  107. package/src/Method.test.ts +102 -1
  108. package/src/Method.ts +241 -5
  109. package/src/cli/validate/payment.ts +18 -4
  110. package/src/cli/validate.test.ts +16 -0
  111. package/src/client/Mppx.test-d.ts +3 -3
  112. package/src/client/internal/Fetch.test.ts +77 -0
  113. package/src/client/internal/Fetch.ts +122 -11
  114. package/src/client/internal/MethodChallenge.ts +35 -0
  115. package/src/client/internal/MethodResponse.ts +28 -0
  116. package/src/server/Mppx.test-d.ts +2 -0
  117. package/src/server/Mppx.test.ts +205 -0
  118. package/src/server/Mppx.ts +167 -103
  119. package/src/stripe/server/internal/html.gen.ts +1 -1
  120. package/src/tempo/PublicExports.test-d.ts +20 -0
  121. package/src/tempo/internal/fee-payer.ts +7 -3
  122. package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
  123. package/src/tempo/legacy/client/ChannelOps.ts +2 -1
  124. package/src/tempo/server/Charge.test.ts +337 -63
  125. package/src/tempo/server/Charge.ts +443 -267
  126. package/src/tempo/server/Methods.ts +11 -0
  127. package/src/tempo/server/Relay.test.ts +677 -0
  128. package/src/tempo/server/Relay.ts +297 -0
  129. package/src/tempo/server/SponsorBudget.test.ts +154 -0
  130. package/src/tempo/server/SponsorBudget.ts +219 -0
  131. package/src/tempo/server/internal/html.gen.ts +1 -1
  132. package/src/tempo/session/README.md +7 -0
  133. package/src/tempo/session/client/ChannelOps.test.ts +17 -2
  134. package/src/tempo/session/client/ChannelOps.ts +12 -4
  135. package/src/tempo/session/client/CredentialState.ts +61 -20
  136. package/src/tempo/session/client/Runtime.test.ts +12 -0
  137. package/src/tempo/session/client/Runtime.ts +22 -0
  138. package/src/tempo/session/client/Session.test.ts +506 -7
  139. package/src/tempo/session/client/Session.ts +279 -54
  140. package/src/tempo/session/client/SessionManager.test.ts +318 -13
  141. package/src/tempo/session/client/SessionManager.ts +132 -42
  142. package/src/tempo/session/client/Transports.test.ts +54 -1
  143. package/src/tempo/session/client/Transports.ts +137 -57
  144. package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
  145. package/src/tempo/session/precompile/Chain.test.ts +197 -10
  146. package/src/tempo/session/precompile/Chain.ts +102 -8
  147. package/src/tempo/session/precompile/Channel.test.ts +9 -0
  148. package/src/tempo/session/precompile/Channel.ts +5 -0
  149. package/src/tempo/session/server/RequestState.test.ts +8 -1
  150. package/src/tempo/session/server/RequestState.ts +2 -2
  151. package/src/tempo/session/server/Session.test.ts +66 -4
  152. package/src/tempo/session/server/Ws.ts +3 -0
  153. package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
  154. package/src/tempo/subscription/KeyAuthorization.ts +14 -3
  155. package/src/viem/Client.test.ts +18 -1
  156. package/src/viem/Client.ts +7 -1
@@ -4,6 +4,7 @@ import { tempo as tempo_chain } from 'viem/chains'
4
4
 
5
5
  import * as Challenge from '../../../Challenge.js'
6
6
  import * as Fetch from '../../../client/internal/Fetch.js'
7
+ import * as MethodChallenge from '../../../client/internal/MethodChallenge.js'
7
8
  import * as MethodResponse from '../../../client/internal/MethodResponse.js'
8
9
  import * as Constants from '../../../Constants.js'
9
10
  import * as Account from '../../../viem/Account.js'
@@ -16,7 +17,7 @@ import { hydrateSessionSnapshot, type SessionContext } from '../client/Credentia
16
17
  import { session as sessionPlugin } from '../client/Session.js'
17
18
  import * as Channel from '../precompile/Channel.js'
18
19
  import { deserializeSessionReceipt } from '../precompile/Protocol.js'
19
- import { readSessionChallengeAmount, type SessionReceipt } from '../precompile/Protocol.js'
20
+ import type { SessionReceipt } from '../precompile/Protocol.js'
20
21
  import {
21
22
  deserializeSnapshot as deserializeSessionSnapshot,
22
23
  serializeSnapshot as serializeSessionSnapshot,
@@ -34,6 +35,7 @@ import {
34
35
  dispatchSessionEvent,
35
36
  restoreCumulativeAuthorization,
36
37
  restoreRuntimeSnapshot as restoreRuntimeStateSnapshot,
38
+ resolveAutomaticTopUp,
37
39
  type RuntimeSnapshot,
38
40
  } from './Runtime.js'
39
41
  import { closeSocketSession } from './Runtime.js'
@@ -41,7 +43,6 @@ import {
41
43
  closeHttpSession,
42
44
  getSessionSnapshot,
43
45
  isTempoSessionChallenge,
44
- managementInput,
45
46
  postTopUp,
46
47
  retryHttpPaymentRequired,
47
48
  type TempoSessionChallenge,
@@ -57,6 +58,7 @@ import { applyTopUpResult, resolveManualTopUp, type TopUpRequirement } from './T
57
58
  import {
58
59
  openWebSocketSession,
59
60
  prepareWebSocketSession,
61
+ probeWebSocketSession,
60
62
  type WebSocketDriverOptions,
61
63
  } from './Transports.js'
62
64
 
@@ -111,6 +113,8 @@ type SessionManagerConfig = {
111
113
  fetch: typeof globalThis.fetch
112
114
  /** Local maximum cumulative voucher authorization, or null when uncapped. */
113
115
  maxVoucherCumulative: bigint | null
116
+ /** Preferred top-up, or null to use a bounded server suggestion and then the exact shortfall. */
117
+ topUpAmount: bigint | null
114
118
  /** WebSocket constructor available in the current runtime, when configured. */
115
119
  WebSocket: WebSocketConstructor | undefined
116
120
  }
@@ -168,6 +172,8 @@ function resolveSessionManagerConfig(parameters: sessionManager.Parameters): Ses
168
172
  fetch: parameters.fetch ?? globalThis.fetch.bind(globalThis),
169
173
  maxVoucherCumulative:
170
174
  parameters.maxDeposit !== undefined ? parseUnits(parameters.maxDeposit, decimals) : null,
175
+ topUpAmount:
176
+ parameters.topUpAmount !== undefined ? parseUnits(parameters.topUpAmount, decimals) : null,
171
177
  WebSocket,
172
178
  }
173
179
  }
@@ -201,6 +207,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
201
207
  // Tracks one fetch's channel reuse so stale stored entries can be evicted once.
202
208
  type ChannelUse = {
203
209
  challengesReceived: number
210
+ committed: RuntimeSnapshot | undefined
204
211
  created: Map<string, ChannelEntry>
205
212
  seenExisting: Set<string>
206
213
  previous: RuntimeSnapshot
@@ -221,7 +228,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
221
228
  const entry = await getReusable(key)
222
229
  if (entry && channelUse) {
223
230
  channelUse.seenExisting.add(key)
224
- if (!channelUse.created.has(key)) channelUse.resumed ??= entry
231
+ if (!channelUse.committed && !channelUse.created.has(key)) channelUse.resumed ??= entry
225
232
  }
226
233
  return entry
227
234
  },
@@ -237,11 +244,14 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
237
244
 
238
245
  /** Removes a failed channel from candidacy for the rest of this manager's life. */
239
246
  async function ignoreChannel(entry: ChannelEntry) {
247
+ const key = entryKey(entry)
240
248
  ignoredChannelIds.add(entry.channelId)
241
- await Promise.resolve(backing.delete(entryKey(entry))).catch(() => undefined)
249
+ channelUse?.seenExisting.delete(key)
250
+ if (channelUse?.resumed === entry) channelUse.resumed = undefined
251
+ await Promise.resolve(backing.delete(key)).catch(() => undefined)
242
252
  }
243
253
 
244
- async function rollbackCreatedChannelsForRetry() {
254
+ async function rollbackCreatedChannels() {
245
255
  const use = channelUse
246
256
  if (!use?.created.size) return
247
257
  for (const [key, entry] of use.created) {
@@ -249,19 +259,31 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
249
259
  await Promise.resolve(backing.delete(key)).catch(() => undefined)
250
260
  }
251
261
  use.created.clear()
252
- restoreRuntime(use.previous)
262
+ await restoreRuntime(use.committed ?? use.previous)
253
263
  }
254
264
 
255
- function referencesCreatedChannel(challenge: Challenge.Challenge): boolean {
265
+ /** Commits a newly created channel once an authoritative server snapshot references it. */
266
+ function commitReferencedChannel(challenge: Challenge.Challenge): boolean {
256
267
  const snapshot = Constants.getMethodDetail<{ channelId?: unknown }>(
257
268
  challenge.request.methodDetails,
258
269
  Constants.MethodDetailKeys.sessionSnapshot,
259
270
  )
260
271
  if (typeof snapshot?.channelId !== 'string') return false
261
272
  const use = channelUse
262
- if (!use?.created.size) return false
263
- for (const entry of use.created.values()) {
264
- if (entry.channelId.toLowerCase() === snapshot.channelId.toLowerCase()) return true
273
+ if (!use?.created.size || !runtime.channel) return false
274
+ for (const [key, entry] of use.created) {
275
+ if (
276
+ entry.channelId.toLowerCase() !== snapshot.channelId.toLowerCase() ||
277
+ runtime.channel.channelId.toLowerCase() !== snapshot.channelId.toLowerCase()
278
+ )
279
+ continue
280
+ use.created.delete(key)
281
+ use.committed = captureRuntimeStateSnapshot({
282
+ channel: runtime.channel,
283
+ spent: runtime.spent,
284
+ state: runtime.state,
285
+ })
286
+ return true
265
287
  }
266
288
  return false
267
289
  }
@@ -270,13 +292,40 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
270
292
  return dispatchSessionEvent(runtime, event)
271
293
  }
272
294
 
295
+ function activeUnits() {
296
+ const state =
297
+ runtime.state.status === 'active'
298
+ ? runtime.state
299
+ : (channelUse?.committed?.state ?? channelUse?.previous.state)
300
+ return state?.status === 'active' ? state.units : 0
301
+ }
302
+
303
+ function commitDurableTopUp(entry: ChannelEntry) {
304
+ const use = channelUse
305
+ const baseline = use?.committed?.channel?.entry ?? use?.resumed
306
+ if (
307
+ !use ||
308
+ baseline?.channelId.toLowerCase() !== entry.channelId.toLowerCase() ||
309
+ entry.deposit <= baseline.deposit
310
+ )
311
+ return
312
+ use.resumed = undefined
313
+ use.committed = captureRuntimeStateSnapshot({
314
+ channel: runtime.channel,
315
+ spent: runtime.spent,
316
+ state: runtime.state,
317
+ })
318
+ }
319
+
273
320
  const method = sessionPlugin({
274
321
  account: parameters.account,
322
+ autoSwap: parameters.autoSwap,
275
323
  getClient: parameters.client ? () => parameters.client! : parameters.getClient,
276
324
  resolveAccount: parameters.resolveAccount,
277
325
  escrow: parameters.escrow,
278
326
  decimals: config.decimals,
279
327
  maxDeposit: parameters.maxDeposit,
328
+ topUpAmount: parameters.topUpAmount,
280
329
  channelStore: store,
281
330
  onChannelUpdate(entry) {
282
331
  if (entry.channelId !== runtime.channel?.channelId) runtime.spent = 0n
@@ -287,9 +336,10 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
287
336
  challengeId: runtime.lastChallenge.id,
288
337
  entry,
289
338
  spent: runtime.spent.toString(),
290
- units: 0,
339
+ units: activeUnits(),
291
340
  })
292
341
  }
342
+ commitDurableTopUp(entry)
293
343
  },
294
344
  })
295
345
  MethodResponse.unregister(method)
@@ -306,24 +356,17 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
306
356
  const use = channelUse
307
357
  const isRepeatedRetryChallenge =
308
358
  use && use.challengesReceived > 0 && challenge.id === runtime.lastChallenge?.id
309
- if (!referencesCreatedChannel(challenge)) {
310
- if (use?.created.size) await rollbackCreatedChannelsForRetry()
311
- else if (isRepeatedRetryChallenge) restoreRuntime(use.previous)
312
- }
359
+ const isSameRetryChallenge =
360
+ isRepeatedRetryChallenge &&
361
+ Challenge.serialize(challenge) === Challenge.serialize(runtime.lastChallenge!)
313
362
  if (use) use.challengesReceived++
363
+ if (isSameRetryChallenge) return undefined
364
+ if (!commitReferencedChannel(challenge)) {
365
+ if (use?.created.size) await rollbackCreatedChannels()
366
+ else if (isRepeatedRetryChallenge) await restoreRuntime(use.committed ?? use.previous)
367
+ }
314
368
  runtime.lastChallenge = challenge
315
369
  dispatch({ type: 'challengeReceived', challengeId: challenge.id })
316
- if (runtime.channel?.opened && runtime.lastUrl) {
317
- const requiredCumulative =
318
- runtime.channel.cumulativeAmount + readSessionChallengeAmount(challenge)
319
- await topUpIfNeeded({
320
- challenge,
321
- input: runtime.lastUrl,
322
- channelId: runtime.channel.channelId,
323
- deposit: runtime.channel.deposit,
324
- requiredCumulative,
325
- })
326
- }
327
370
  return undefined
328
371
  },
329
372
  })
@@ -366,16 +409,12 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
366
409
  const snapshot = deserializeSessionSnapshot(header)
367
410
  const client = await getClient({ chainId: snapshot.chainId })
368
411
  const defaultAccount = getAccount(client)
369
- const authority =
370
- BigInt(snapshot.descriptor.authorizedSigner) === 0n
371
- ? snapshot.descriptor.payer
372
- : snapshot.descriptor.authorizedSigner
373
412
  const account =
374
413
  (await parameters.resolveAccount?.({
375
414
  account: defaultAccount,
376
415
  chainId: snapshot.chainId,
377
416
  operation: {
378
- authority,
417
+ authority: Channel.resolveAuthorizedSigner(snapshot.descriptor),
379
418
  kind: 'authorizePaymentChannel',
380
419
  },
381
420
  })) ?? defaultAccount
@@ -520,9 +559,11 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
520
559
  challenge: TempoSessionChallenge
521
560
  channelId: Hex.Hex
522
561
  input: RequestInfo | URL
562
+ knownDeposit?: bigint | undefined
523
563
  }) {
564
+ const { knownDeposit, ...topUp } = parameters
524
565
  const receipt = await postTopUp({
525
- ...parameters,
566
+ ...topUp,
526
567
  channel: runtime.channel,
527
568
  createSessionCredential,
528
569
  fetch: config.fetch,
@@ -534,6 +575,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
534
575
  channelId: parameters.channelId,
535
576
  challengeId: runtime.lastChallenge?.id,
536
577
  currentState: runtime.state,
578
+ knownDeposit,
537
579
  receipt,
538
580
  spent: runtime.spent,
539
581
  })
@@ -544,24 +586,38 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
544
586
  challengeId: runtime.lastChallenge.id,
545
587
  entry: applied.channel,
546
588
  spent: runtime.spent.toString(),
547
- units: runtime.state.status === 'active' ? runtime.state.units : 0,
589
+ units: activeUnits(),
548
590
  })
591
+ commitDurableTopUp(applied.channel)
549
592
  }
550
593
  return receipt
551
594
  }
552
595
 
553
596
  async function topUpIfNeeded(parameters: TopUpRequirement) {
554
- if (parameters.requiredCumulative <= parameters.deposit) return
555
- assertVoucherWithinLocalLimit(parameters.requiredCumulative)
597
+ const channelDeposit =
598
+ runtime.channel?.channelId === parameters.channelId ? runtime.channel.deposit : 0n
599
+ const deposit = channelDeposit > parameters.deposit ? channelDeposit : parameters.deposit
600
+ const additionalDeposit = resolveAutomaticTopUp({
601
+ deposit,
602
+ maxDeposit: config.maxVoucherCumulative,
603
+ requiredCumulative: parameters.requiredCumulative,
604
+ suggestedDeposit:
605
+ parameters.challenge.request.suggestedDeposit === undefined
606
+ ? undefined
607
+ : BigInt(parameters.challenge.request.suggestedDeposit),
608
+ topUpAmount: config.topUpAmount,
609
+ })
610
+ if (additionalDeposit === 0n) return
556
611
  await postTopUpAndApply({
557
612
  challenge: parameters.challenge,
558
613
  input: parameters.input,
559
614
  channelId: parameters.channelId,
560
- additionalDeposit: parameters.requiredCumulative - parameters.deposit,
615
+ additionalDeposit,
616
+ knownDeposit: deposit,
561
617
  })
562
618
  }
563
619
 
564
- function restoreCumulative(channelId: Hex.Hex, cumulativeAmount: bigint) {
620
+ async function restoreCumulative(channelId: Hex.Hex, cumulativeAmount: bigint) {
565
621
  const restored = restoreCumulativeAuthorization({
566
622
  channel: runtime.channel,
567
623
  channelId,
@@ -578,14 +634,16 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
578
634
  spent: restored.spent,
579
635
  units: restored.units,
580
636
  })
637
+ await backing.set(runtime.channel)
581
638
  }
582
639
  }
583
640
 
584
- function restoreRuntime(snapshot: RuntimeSnapshot) {
641
+ async function restoreRuntime(snapshot: RuntimeSnapshot) {
585
642
  const restored = restoreRuntimeStateSnapshot(snapshot, runtime.channel)
586
643
  runtime.channel = restored.channel
587
644
  runtime.spent = restored.spent
588
645
  runtime.state = restored.state
646
+ if (runtime.channel?.opened) await backing.set(runtime.channel)
589
647
  }
590
648
 
591
649
  function toPaymentResponse(response: Response): PaymentResponse {
@@ -617,6 +675,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
617
675
  })
618
676
  const use: ChannelUse = {
619
677
  challengesReceived: 0,
678
+ committed: undefined,
620
679
  created: new Map(),
621
680
  previous,
622
681
  seenExisting: new Set(),
@@ -650,7 +709,8 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
650
709
  try {
651
710
  response = await wrappedFetch(input, effectiveInit)
652
711
  } catch (error) {
653
- restoreRuntime(previous)
712
+ if (use.created.size) await rollbackCreatedChannels()
713
+ else await restoreRuntime(use.committed ?? previous)
654
714
  if (await retryWithoutResumed()) continue
655
715
  throw error
656
716
  }
@@ -677,7 +737,8 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
677
737
  }
678
738
  }
679
739
  if (!attemptedHttpManagement && !paymentResponse.ok && !paymentResponse.receipt) {
680
- restoreRuntime(previous)
740
+ if (use.created.size) await rollbackCreatedChannels()
741
+ else await restoreRuntime(use.committed ?? previous)
681
742
  if (await retryWithoutResumed()) continue
682
743
  return paymentResponse
683
744
  }
@@ -719,7 +780,6 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
719
780
  getChannel: () => runtime.channel,
720
781
  getChallenge: () => runtime.lastChallenge,
721
782
  assertVoucherWithinLocalLimit,
722
- managementInput,
723
783
  acceptReceipt(receipt: SessionReceipt) {
724
784
  updateSpentFromReceipt(receipt)
725
785
  },
@@ -777,7 +837,16 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
777
837
  const liveHint = runtime.channel?.opened ? runtime.channel.channelId : undefined
778
838
 
779
839
  const prepared = await prepareWebSocketSession({
780
- createSessionCredential,
840
+ async createSessionCredential(challenge, context) {
841
+ runtime.lastChallenge = challenge
842
+ await MethodChallenge.handle(method, {
843
+ challenge,
844
+ context,
845
+ fetch: config.fetch,
846
+ input: probeUrl,
847
+ })
848
+ return createSessionCredential(challenge, context)
849
+ },
781
850
  fetch: config.fetch,
782
851
  input,
783
852
  onProbeUrl(httpUrl) {
@@ -801,6 +870,20 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
801
870
  setSocketSession(session) {
802
871
  runtime.socketSession = session
803
872
  },
873
+ async refreshChallenge() {
874
+ const refreshed = await probeWebSocketSession({
875
+ fetch: config.fetch,
876
+ input: httpUrl,
877
+ probeInit: requestInitWithSessionHint(
878
+ httpUrl,
879
+ signalInit,
880
+ runtime.channel?.opened ? runtime.channel.channelId : undefined,
881
+ ),
882
+ signal: init?.signal,
883
+ })
884
+ runtime.lastChallenge = refreshed.challenge
885
+ return refreshed.challenge
886
+ },
804
887
  assertVoucherWithinLocalLimit,
805
888
  acceptReceipt: updateSpentFromReceipt,
806
889
  rejectCloseReady: receipts.rejectCloseReady,
@@ -884,6 +967,8 @@ export namespace sessionManager {
884
967
 
885
968
  export type Parameters = Account.getResolver.Parameters &
886
969
  Client.getResolver.Parameters & {
970
+ /** Automatically acquire the session currency from fallback stablecoins before open/top-up. */
971
+ autoSwap?: sessionPlugin.Parameters['autoSwap']
887
972
  /** Enables same-route HEAD bootstrap from a server session snapshot before opening a new channel. */
888
973
  bootstrap?: boolean | undefined
889
974
  /** Viem client instance. Shorthand for `getClient: () => client`. */
@@ -896,6 +981,11 @@ export namespace sessionManager {
896
981
  fetch?: typeof globalThis.fetch | undefined
897
982
  /** Maximum deposit in human-readable units (e.g. `'10'` for 10 tokens). Converted to raw units via `decimals`. */
898
983
  maxDeposit?: string | undefined
984
+ /**
985
+ * Preferred automatic top-up size in human-readable units. When omitted,
986
+ * a bounded server `suggestedDeposit` is preferred, then the exact shortfall.
987
+ */
988
+ topUpAmount?: string | undefined
899
989
  /** Selects the account that signs session credentials. */
900
990
  resolveAccount?: sessionPlugin.ResolveAccount | undefined
901
991
  /** Store for reusable session channels. Defaults to in-memory. */
@@ -85,6 +85,7 @@ describe('HttpManagement', () => {
85
85
  methodDetails: {
86
86
  chainId: 4217,
87
87
  escrowContract: undefined,
88
+ sessionProtocol: Constants.SessionProtocols.v2,
88
89
  ...(snapshot && { [Constants.MethodDetailKeys.sessionSnapshot]: snapshot }),
89
90
  },
90
91
  },
@@ -220,6 +221,36 @@ describe('HttpManagement', () => {
220
221
  expect(fetch).toHaveBeenCalledOnce()
221
222
  })
222
223
 
224
+ test('postTopUp retries once with the management route challenge', async () => {
225
+ const routeChallenge = { ...challenge(), id: 'management-challenge' } as TempoSessionChallenge
226
+ const createSessionCredential = vi.fn(async (challenge_) => `top-up-${challenge_.id}`)
227
+ const fetch = vi
228
+ .fn()
229
+ .mockResolvedValueOnce(response402(routeChallenge))
230
+ .mockResolvedValueOnce(
231
+ new Response(null, {
232
+ status: 204,
233
+ headers: {
234
+ [Constants.Headers.paymentReceipt]: receiptHeader(8n, 5n, routeChallenge.id),
235
+ },
236
+ }),
237
+ )
238
+
239
+ const receipt = await postTopUp({
240
+ additionalDeposit: 3n,
241
+ challenge: challenge(),
242
+ channel: channel(),
243
+ channelId,
244
+ createSessionCredential,
245
+ fetch,
246
+ input: 'https://example.test/resource',
247
+ })
248
+
249
+ expect(receipt?.challengeId).toBe(routeChallenge.id)
250
+ expect(createSessionCredential).toHaveBeenCalledTimes(2)
251
+ expect(authorizationHeader(fetch.mock.calls[1]?.[1])).toBe(`top-up-${routeChallenge.id}`)
252
+ })
253
+
223
254
  test('retryHttpPaymentRequired signs the server-required cumulative voucher', async () => {
224
255
  let entry = channel({ cumulativeAmount: 5n, deposit: 6n })
225
256
  const topUpIfNeeded = vi.fn(async () => {
@@ -279,6 +310,28 @@ describe('HttpManagement', () => {
279
310
  expect(restoreCumulative).toHaveBeenCalledWith(channelId, 5n)
280
311
  })
281
312
 
313
+ test('retryHttpPaymentRequired restores cumulative authorization when retry throws', async () => {
314
+ const entry = channel({ cumulativeAmount: 5n })
315
+ const restoreCumulative = vi.fn()
316
+
317
+ await expect(
318
+ retryHttpPaymentRequired({
319
+ createSessionCredential: async () => 'voucher-credential',
320
+ fetch: async () => {
321
+ throw new Error('network failed')
322
+ },
323
+ getChannel: () => entry,
324
+ input: 'https://example.test/resource',
325
+ response: response402(challenge(snapshot())),
326
+ restoreCumulative,
327
+ setChallenge() {},
328
+ topUpIfNeeded: async () => {},
329
+ }),
330
+ ).rejects.toThrow('network failed')
331
+
332
+ expect(restoreCumulative).toHaveBeenCalledWith(channelId, 5n)
333
+ })
334
+
282
335
  test('closeHttpSession posts a close credential and parses the receipt', async () => {
283
336
  const entry = channel()
284
337
  const createSessionCredential = vi.fn(async (_challenge, context: SessionContext) => {
@@ -874,7 +927,6 @@ describe('SseDriver', () => {
874
927
  fetch: vi.fn(async () => new Response(null, { status: 204 })),
875
928
  getChallenge: () => currentChallenge,
876
929
  getChannel: () => channel,
877
- managementInput: (input) => input,
878
930
  async topUpIfNeeded() {},
879
931
  })
880
932
 
@@ -1011,6 +1063,7 @@ describe('WsDriver', () => {
1011
1063
  methodDetails: {
1012
1064
  chainId: 4217,
1013
1065
  escrowContract: tip20ChannelEscrow,
1066
+ sessionProtocol: Constants.SessionProtocols.v2,
1014
1067
  },
1015
1068
  recipient: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00',
1016
1069
  },