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.
- package/CHANGELOG.md +37 -0
- package/dist/Errors.d.ts +10 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +11 -2
- package/dist/Errors.js.map +1 -1
- package/dist/Method.d.ts +105 -3
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +102 -2
- package/dist/Method.js.map +1 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +18 -4
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +98 -5
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +19 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +15 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/client/internal/MethodResponse.d.ts +15 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -1
- package/dist/client/internal/MethodResponse.js +10 -0
- package/dist/client/internal/MethodResponse.js.map +1 -1
- package/dist/server/Mppx.d.ts +23 -4
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +91 -82
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +2 -1
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +46 -77
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +289 -209
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +12 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +57 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +179 -0
- package/dist/tempo/server/Relay.js.map +1 -0
- package/dist/tempo/server/SponsorBudget.d.ts +59 -0
- package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
- package/dist/tempo/server/SponsorBudget.js +150 -0
- package/dist/tempo/server/SponsorBudget.js.map +1 -0
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +9 -4
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +5 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +37 -19
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +8 -0
- package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
- package/dist/tempo/session/client/Runtime.js +17 -0
- package/dist/tempo/session/client/Runtime.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +15 -5
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +216 -45
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +8 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +114 -44
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +16 -6
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +84 -37
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +96 -8
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/precompile/Channel.d.ts +2 -0
- package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Channel.js +4 -0
- package/dist/tempo/session/precompile/Channel.js.map +1 -1
- package/dist/tempo/session/server/RequestState.js +3 -1
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js +3 -0
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +1 -829
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.js +14 -3
- package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +6 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- package/src/Errors.test.ts +23 -0
- package/src/Errors.ts +21 -2
- package/src/Method.test.ts +102 -1
- package/src/Method.ts +241 -5
- package/src/cli/validate/payment.ts +18 -4
- package/src/cli/validate.test.ts +16 -0
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.test.ts +77 -0
- package/src/client/internal/Fetch.ts +122 -11
- package/src/client/internal/MethodChallenge.ts +35 -0
- package/src/client/internal/MethodResponse.ts +28 -0
- package/src/server/Mppx.test-d.ts +2 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/PublicExports.test-d.ts +20 -0
- package/src/tempo/internal/fee-payer.ts +7 -3
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- package/src/tempo/server/Charge.test.ts +337 -63
- package/src/tempo/server/Charge.ts +443 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +677 -0
- package/src/tempo/server/Relay.ts +297 -0
- package/src/tempo/server/SponsorBudget.test.ts +154 -0
- package/src/tempo/server/SponsorBudget.ts +219 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +7 -0
- package/src/tempo/session/client/ChannelOps.test.ts +17 -2
- package/src/tempo/session/client/ChannelOps.ts +12 -4
- package/src/tempo/session/client/CredentialState.ts +61 -20
- package/src/tempo/session/client/Runtime.test.ts +12 -0
- package/src/tempo/session/client/Runtime.ts +22 -0
- package/src/tempo/session/client/Session.test.ts +506 -7
- package/src/tempo/session/client/Session.ts +279 -54
- package/src/tempo/session/client/SessionManager.test.ts +318 -13
- package/src/tempo/session/client/SessionManager.ts +132 -42
- package/src/tempo/session/client/Transports.test.ts +54 -1
- package/src/tempo/session/client/Transports.ts +137 -57
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +197 -10
- package/src/tempo/session/precompile/Chain.ts +102 -8
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- package/src/tempo/session/server/RequestState.test.ts +8 -1
- package/src/tempo/session/server/RequestState.ts +2 -2
- package/src/tempo/session/server/Session.test.ts +66 -4
- package/src/tempo/session/server/Ws.ts +3 -0
- package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
- package/src/tempo/subscription/KeyAuthorization.ts +14 -3
- package/src/viem/Client.test.ts +18 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
264
|
-
if (
|
|
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:
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
...
|
|
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:
|
|
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
|
-
|
|
555
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|