mppx 0.8.12 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +16 -1
- 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/account.d.ts +88 -0
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +39 -9
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +59 -2
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/sessions/Manager.d.ts +31 -0
- package/dist/cli/sessions/Manager.d.ts.map +1 -0
- package/dist/cli/sessions/Manager.js +56 -0
- package/dist/cli/sessions/Manager.js.map +1 -0
- package/dist/cli/sessions/commands.d.ts +36 -0
- package/dist/cli/sessions/commands.d.ts.map +1 -0
- package/dist/cli/sessions/commands.js +407 -0
- package/dist/cli/sessions/commands.js.map +1 -0
- package/dist/cli/sessions/request.d.ts +35 -0
- package/dist/cli/sessions/request.d.ts.map +1 -0
- package/dist/cli/sessions/request.js +271 -0
- package/dist/cli/sessions/request.js.map +1 -0
- package/dist/cli/sessions/store.d.ts +134 -0
- package/dist/cli/sessions/store.d.ts.map +1 -0
- package/dist/cli/sessions/store.js +723 -0
- package/dist/cli/sessions/store.js.map +1 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +12 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +12 -6
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/messages.d.ts +2 -0
- package/dist/cli/validate/messages.d.ts.map +1 -0
- package/dist/cli/validate/messages.js +8 -0
- package/dist/cli/validate/messages.js.map +1 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -2
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +17 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +11 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/client/node.d.ts +29 -0
- package/dist/client/node.d.ts.map +1 -0
- package/dist/client/node.js +177 -0
- package/dist/client/node.js.map +1 -0
- package/dist/internal/types.d.ts +24 -0
- package/dist/internal/types.d.ts.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/Charge.d.ts +8 -4
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.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 +2 -1
- 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/internal/types.d.ts +10 -1
- package/dist/tempo/internal/types.d.ts.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/legacy/client/Session.d.ts +2 -2
- package/dist/tempo/legacy/server/Session.d.ts +3 -3
- package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/server/Session.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +40 -78
- 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 +13 -23
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +6 -2
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +48 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +177 -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 +144 -0
- package/dist/tempo/server/SponsorBudget.js.map +1 -0
- package/dist/tempo/server/Subscription.d.ts +3 -2
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/index.d.ts +1 -1
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js.map +1 -1
- 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/Snapshot.d.ts +9 -0
- package/dist/tempo/session/Snapshot.d.ts.map +1 -1
- package/dist/tempo/session/Snapshot.js +6 -0
- package/dist/tempo/session/Snapshot.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 +17 -3
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +26 -2
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +99 -16
- 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 +16 -6
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +93 -36
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +10 -3
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +176 -50
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +18 -7
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +81 -33
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
- package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
- package/dist/tempo/session/client/internal/SessionManager.js +13 -0
- package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +109 -12
- 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/CredentialVerification.d.ts +3 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +14 -0
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
- package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
- package/dist/tempo/session/server/MeteredStream.js +5 -4
- package/dist/tempo/session/server/MeteredStream.js.map +1 -1
- package/dist/tempo/session/server/RequestState.d.ts +5 -1
- package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
- package/dist/tempo/session/server/RequestState.js +13 -5
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts +21 -6
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +4 -1
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/tempo/session/server/Settlement.d.ts +20 -0
- package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
- package/dist/tempo/session/server/Settlement.js +20 -0
- package/dist/tempo/session/server/Settlement.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts +2 -0
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +1 -1
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +6 -2
- package/dist/validation/core.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +24 -17
- package/dist/viem/Client.js.map +1 -1
- package/package.json +6 -1
- 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/account.ts +45 -10
- package/src/cli/cli.test.ts +165 -70
- package/src/cli/cli.ts +67 -2
- package/src/cli/mcp.test.ts +11 -0
- package/src/cli/sessions/Manager.test.ts +249 -0
- package/src/cli/sessions/Manager.ts +93 -0
- package/src/cli/sessions/commands.ts +444 -0
- package/src/cli/sessions/request.test.ts +51 -0
- package/src/cli/sessions/request.ts +353 -0
- package/src/cli/sessions/store.test.ts +581 -0
- package/src/cli/sessions/store.ts +940 -0
- package/src/cli/utils.test.ts +23 -0
- package/src/cli/utils.ts +10 -0
- package/src/cli/validate/index.ts +12 -14
- package/src/cli/validate/messages.ts +7 -0
- package/src/cli/validate.test.ts +38 -5
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.ts +16 -7
- package/src/client/internal/MethodChallenge.ts +30 -0
- package/src/client/node.test.ts +115 -0
- package/src/client/node.ts +247 -0
- package/src/internal/types.test-d.ts +21 -0
- package/src/internal/types.ts +33 -0
- package/src/server/Mppx.test-d.ts +44 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- package/src/stripe/Methods.test.ts +10 -0
- package/src/stripe/server/Charge.test-d.ts +66 -0
- package/src/stripe/server/Charge.ts +8 -5
- 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/internal/types.ts +13 -2
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
- package/src/tempo/legacy/server/Session.ts +6 -5
- package/src/tempo/server/Charge.test.ts +337 -63
- package/src/tempo/server/Charge.ts +440 -270
- package/src/tempo/server/Methods.ts +16 -4
- package/src/tempo/server/Relay.test.ts +523 -0
- package/src/tempo/server/Relay.ts +288 -0
- package/src/tempo/server/SponsorBudget.test.ts +125 -0
- package/src/tempo/server/SponsorBudget.ts +213 -0
- package/src/tempo/server/Subscription.ts +6 -4
- package/src/tempo/server/index.ts +5 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +201 -0
- package/src/tempo/session/Snapshot.ts +21 -0
- package/src/tempo/session/client/ChannelOps.test.ts +55 -0
- package/src/tempo/session/client/ChannelOps.ts +21 -3
- package/src/tempo/session/client/CredentialState.test.ts +2 -2
- package/src/tempo/session/client/CredentialState.ts +159 -16
- 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 +238 -5
- package/src/tempo/session/client/Session.ts +130 -41
- package/src/tempo/session/client/SessionManager.test.ts +239 -8
- package/src/tempo/session/client/SessionManager.ts +204 -54
- package/src/tempo/session/client/Transports.test.ts +181 -8
- package/src/tempo/session/client/Transports.ts +127 -45
- package/src/tempo/session/client/internal/SessionManager.ts +36 -0
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +223 -10
- package/src/tempo/session/precompile/Chain.ts +119 -12
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- package/src/tempo/session/server/CredentialVerification.ts +18 -1
- package/src/tempo/session/server/MeteredStream.ts +12 -5
- package/src/tempo/session/server/RequestState.test.ts +47 -4
- package/src/tempo/session/server/RequestState.ts +15 -8
- package/src/tempo/session/server/Session.test.ts +461 -0
- package/src/tempo/session/server/Session.ts +31 -8
- package/src/tempo/session/server/Settlement.ts +45 -0
- package/src/tempo/session/server/Ws.test.ts +113 -0
- package/src/tempo/session/server/Ws.ts +2 -0
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +7 -1
- package/src/viem/Client.test.ts +44 -1
- package/src/viem/Client.ts +31 -17
|
@@ -11,9 +11,11 @@ import type * as Challenge from '../../../Challenge.js'
|
|
|
11
11
|
import * as Constants from '../../../Constants.js'
|
|
12
12
|
import * as Account from '../../../viem/Account.js'
|
|
13
13
|
import * as z from '../../../zod.js'
|
|
14
|
+
import * as AutoSwap from '../../internal/auto-swap.js'
|
|
14
15
|
import * as Chain from '../precompile/Chain.js'
|
|
15
16
|
import * as Channel from '../precompile/Channel.js'
|
|
16
17
|
import type { SessionCredentialPayload } from '../precompile/Protocol.js'
|
|
18
|
+
import * as Voucher from '../precompile/Voucher.js'
|
|
17
19
|
import type { SessionSnapshot } from '../Snapshot.js'
|
|
18
20
|
import {
|
|
19
21
|
createClosePayload,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
resolveAuthorizedSigner,
|
|
25
27
|
resolveEscrow,
|
|
26
28
|
type ChannelEntry,
|
|
29
|
+
type TempoChannelCall,
|
|
27
30
|
} from './ChannelOps.js'
|
|
28
31
|
import { channelKey, entryKey, type ChannelSink } from './ChannelStore.js'
|
|
29
32
|
import { assertWithinMaxDeposit, resolveOpeningDeposit } from './Runtime.js'
|
|
@@ -87,6 +90,7 @@ const hashSchema = z.custom<Hex>(
|
|
|
87
90
|
/** Runtime schema for low-level TIP-1034 session credential context. */
|
|
88
91
|
export const sessionContextSchema = z.object({
|
|
89
92
|
account: z.optional(z.custom<Account.getResolver.Parameters['account']>()),
|
|
93
|
+
autoSwap: z.optional(z.custom<AutoSwap.resolve.Value>()),
|
|
90
94
|
action: z.optional(z.enum(['open', 'topUp', 'voucher', 'close'])),
|
|
91
95
|
channelId: z.optional(hashSchema),
|
|
92
96
|
cumulativeAmount: z.optional(z.amount()),
|
|
@@ -102,6 +106,8 @@ export const sessionContextSchema = z.object({
|
|
|
102
106
|
export type SessionContext = {
|
|
103
107
|
/** Optional account override used for this credential only. */
|
|
104
108
|
account?: Account.getResolver.Parameters['account'] | undefined
|
|
109
|
+
/** Automatically acquire the session currency from fallback stablecoins before open/top-up. */
|
|
110
|
+
autoSwap?: AutoSwap.resolve.Value | undefined
|
|
105
111
|
/** Manual credential action. Omit for automatic open/recover/voucher management. */
|
|
106
112
|
action?: 'open' | 'topUp' | 'voucher' | 'close' | undefined
|
|
107
113
|
/** Channel ID being reused or manually operated on. */
|
|
@@ -309,6 +315,26 @@ export type ResolveRecoveredCumulativeParameters = {
|
|
|
309
315
|
settled: bigint
|
|
310
316
|
}
|
|
311
317
|
|
|
318
|
+
/** Inputs used to validate and hydrate a server-provided session snapshot. */
|
|
319
|
+
export type HydrateSessionSnapshotParameters = {
|
|
320
|
+
/** Account expected to own the channel and its voucher authority. */
|
|
321
|
+
account: ViemAccount
|
|
322
|
+
/** Viem client used to read authoritative TIP-1034 channel state. */
|
|
323
|
+
client: Client
|
|
324
|
+
/** Snapshot returned by an MPP server bootstrap. */
|
|
325
|
+
snapshot: SessionSnapshot
|
|
326
|
+
/** Optional state reader for deterministic tests. */
|
|
327
|
+
readChannelState?: ReadReusableChannelState | undefined
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Validated client channel state reconstructed without local persistence. */
|
|
331
|
+
export type HydratedSessionSnapshot = {
|
|
332
|
+
/** Reusable channel entry safe to cache locally. */
|
|
333
|
+
entry: ChannelEntry
|
|
334
|
+
/** Server-accounted delivered spend at bootstrap time. */
|
|
335
|
+
spent: bigint
|
|
336
|
+
}
|
|
337
|
+
|
|
312
338
|
/** Data-first description of the next credential operation the client should execute. */
|
|
313
339
|
export type CredentialPlan =
|
|
314
340
|
/** No reusable channel is available, so create an open transaction and initial voucher. */
|
|
@@ -476,7 +502,7 @@ function assertReusableChannelDescriptor(parameters: {
|
|
|
476
502
|
throw new Error('context descriptor token does not match challenge')
|
|
477
503
|
if (!isSameAddress(descriptor.payer, payer))
|
|
478
504
|
throw new Error('context descriptor payer does not match account')
|
|
479
|
-
if (!isSameAddress(descriptor
|
|
505
|
+
if (!isSameAddress(Channel.resolveAuthorizedSigner(descriptor), authorizedSigner))
|
|
480
506
|
throw new Error('context descriptor authorizedSigner does not match account')
|
|
481
507
|
}
|
|
482
508
|
|
|
@@ -501,7 +527,12 @@ export function resolveRecoveredCumulative(
|
|
|
501
527
|
const { context, decimals, requestAmount, snapshot, settled } = parameters
|
|
502
528
|
|
|
503
529
|
if (snapshot) {
|
|
504
|
-
return
|
|
530
|
+
return [
|
|
531
|
+
BigInt(snapshot.acceptedCumulative),
|
|
532
|
+
BigInt(snapshot.requiredCumulative),
|
|
533
|
+
BigInt(snapshot.spent) + requestAmount,
|
|
534
|
+
settled,
|
|
535
|
+
].reduce((highest, value) => (value > highest ? value : highest), 0n)
|
|
505
536
|
}
|
|
506
537
|
|
|
507
538
|
const contextCumulative = parseOptionalContextAmount(context, decimals, 'cumulativeAmount')
|
|
@@ -509,6 +540,78 @@ export function resolveRecoveredCumulative(
|
|
|
509
540
|
return settled + requestAmount
|
|
510
541
|
}
|
|
511
542
|
|
|
543
|
+
/** Checks a server snapshot against its signed voucher and current TIP-1034 state. */
|
|
544
|
+
export async function hydrateSessionSnapshot(
|
|
545
|
+
parameters: HydrateSessionSnapshotParameters,
|
|
546
|
+
): Promise<HydratedSessionSnapshot> {
|
|
547
|
+
const { account, client, readChannelState, snapshot } = parameters
|
|
548
|
+
const signed = snapshot.highestVoucher
|
|
549
|
+
if (!signed) throw new Error('session snapshot is missing its highest signed voucher')
|
|
550
|
+
if (signed.channelId.toLowerCase() !== snapshot.channelId.toLowerCase())
|
|
551
|
+
throw new Error('session snapshot voucher channelId does not match snapshot channelId')
|
|
552
|
+
|
|
553
|
+
const acceptedCumulative = BigInt(snapshot.acceptedCumulative)
|
|
554
|
+
const voucherCumulative = BigInt(signed.cumulativeAmount)
|
|
555
|
+
const requiredCumulative = BigInt(snapshot.requiredCumulative)
|
|
556
|
+
const snapshotDeposit = BigInt(snapshot.deposit)
|
|
557
|
+
const snapshotSettled = BigInt(snapshot.settled)
|
|
558
|
+
const spent = BigInt(snapshot.spent)
|
|
559
|
+
if (voucherCumulative !== acceptedCumulative)
|
|
560
|
+
throw new Error('session snapshot voucher amount does not match acceptedCumulative')
|
|
561
|
+
if (spent > acceptedCumulative)
|
|
562
|
+
throw new Error('session snapshot spent exceeds acceptedCumulative')
|
|
563
|
+
if (snapshotSettled > snapshotDeposit)
|
|
564
|
+
throw new Error('session snapshot settled amount exceeds deposit')
|
|
565
|
+
|
|
566
|
+
if (
|
|
567
|
+
!Voucher.verifyVoucher(
|
|
568
|
+
snapshot.escrow,
|
|
569
|
+
snapshot.chainId,
|
|
570
|
+
{
|
|
571
|
+
channelId: signed.channelId,
|
|
572
|
+
cumulativeAmount: voucherCumulative,
|
|
573
|
+
signature: signed.signature,
|
|
574
|
+
},
|
|
575
|
+
Channel.resolveAuthorizedSigner(snapshot.descriptor),
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
throw new Error('session snapshot highest voucher signature is invalid')
|
|
579
|
+
|
|
580
|
+
const reusable = await resolveReusableChannel({
|
|
581
|
+
channelId: snapshot.channelId,
|
|
582
|
+
client,
|
|
583
|
+
descriptor: snapshot.descriptor,
|
|
584
|
+
expected: {
|
|
585
|
+
chainId: snapshot.chainId,
|
|
586
|
+
escrow: snapshot.escrow,
|
|
587
|
+
payee: snapshot.descriptor.payee,
|
|
588
|
+
payer: account.address,
|
|
589
|
+
authorizedSigner: resolveAuthorizedSigner(account),
|
|
590
|
+
token: snapshot.descriptor.token,
|
|
591
|
+
},
|
|
592
|
+
readChannelState,
|
|
593
|
+
})
|
|
594
|
+
const cumulativeAmount = [acceptedCumulative, requiredCumulative, reusable.state.settled].reduce(
|
|
595
|
+
(highest, value) => (value > highest ? value : highest),
|
|
596
|
+
0n,
|
|
597
|
+
)
|
|
598
|
+
if (cumulativeAmount > reusable.state.deposit)
|
|
599
|
+
throw new Error('recovered session cumulative amount exceeds on-chain channel deposit')
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
entry: {
|
|
603
|
+
channelId: reusable.channelId,
|
|
604
|
+
cumulativeAmount,
|
|
605
|
+
deposit: reusable.state.deposit,
|
|
606
|
+
descriptor: snapshot.descriptor,
|
|
607
|
+
escrow: snapshot.escrow,
|
|
608
|
+
chainId: snapshot.chainId,
|
|
609
|
+
opened: true,
|
|
610
|
+
},
|
|
611
|
+
spent,
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
512
615
|
/** Returns whether `account` can satisfy the descriptor's voucher authority. */
|
|
513
616
|
export function canSignDescriptor(
|
|
514
617
|
account: ViemAccount,
|
|
@@ -516,9 +619,10 @@ export function canSignDescriptor(
|
|
|
516
619
|
): boolean {
|
|
517
620
|
// Only the payer can deposit into and voucher against its own channel.
|
|
518
621
|
if (!isSameAddress(account.address, descriptor.payer)) return false
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
622
|
+
return isSameAddress(
|
|
623
|
+
resolveAuthorizedSigner(account),
|
|
624
|
+
Channel.resolveAuthorizedSigner(descriptor),
|
|
625
|
+
)
|
|
522
626
|
}
|
|
523
627
|
|
|
524
628
|
/** Chooses the next credential plan from local channel cache and optional caller context. */
|
|
@@ -559,22 +663,43 @@ export function planCredential(parameters: PlanCredentialParameters): Credential
|
|
|
559
663
|
export async function executeCredentialPlan(
|
|
560
664
|
plan: CredentialPlan,
|
|
561
665
|
sink: ChannelSink,
|
|
666
|
+
autoSwap: AutoSwap.resolve.Resolved | false = false,
|
|
562
667
|
): Promise<SessionCredentialPayload> {
|
|
563
668
|
switch (plan.type) {
|
|
564
669
|
case 'open':
|
|
565
|
-
return open(plan, sink)
|
|
670
|
+
return open(plan, sink, autoSwap)
|
|
566
671
|
case 'recover':
|
|
567
672
|
return recover(plan, sink)
|
|
568
673
|
case 'voucher':
|
|
569
674
|
return voucher(plan, sink)
|
|
570
675
|
case 'manual':
|
|
571
|
-
return manual(plan, sink)
|
|
676
|
+
return manual(plan, sink, autoSwap)
|
|
572
677
|
}
|
|
573
678
|
}
|
|
574
679
|
|
|
680
|
+
async function resolveAutoSwapCalls(
|
|
681
|
+
autoSwap: AutoSwap.resolve.Resolved | false,
|
|
682
|
+
parameters: {
|
|
683
|
+
account: Address
|
|
684
|
+
amountOut: bigint
|
|
685
|
+
client: Client
|
|
686
|
+
tokenOut: Address
|
|
687
|
+
},
|
|
688
|
+
): Promise<readonly TempoChannelCall[] | undefined> {
|
|
689
|
+
if (!autoSwap) return undefined
|
|
690
|
+
return (await AutoSwap.findCalls(parameters.client, {
|
|
691
|
+
account: parameters.account,
|
|
692
|
+
amountOut: parameters.amountOut,
|
|
693
|
+
tokenOut: parameters.tokenOut,
|
|
694
|
+
tokenIn: autoSwap.tokenIn,
|
|
695
|
+
slippage: autoSwap.slippage,
|
|
696
|
+
})) as readonly TempoChannelCall[] | undefined
|
|
697
|
+
}
|
|
698
|
+
|
|
575
699
|
async function open(
|
|
576
700
|
plan: Extract<CredentialPlan, { type: 'open' }>,
|
|
577
701
|
sink: ChannelSink,
|
|
702
|
+
autoSwap: AutoSwap.resolve.Resolved | false,
|
|
578
703
|
): Promise<SessionCredentialPayload> {
|
|
579
704
|
const { account, resolved } = plan
|
|
580
705
|
const deposit = resolveOpeningDeposit({
|
|
@@ -591,6 +716,12 @@ async function open(
|
|
|
591
716
|
initialAmount: resolved.amount,
|
|
592
717
|
operator: resolved.operator,
|
|
593
718
|
payee: resolved.payee,
|
|
719
|
+
prefixCalls: await resolveAutoSwapCalls(autoSwap, {
|
|
720
|
+
account: account.address,
|
|
721
|
+
amountOut: deposit,
|
|
722
|
+
client: resolved.client,
|
|
723
|
+
tokenOut: resolved.token,
|
|
724
|
+
}),
|
|
594
725
|
token: resolved.token,
|
|
595
726
|
})
|
|
596
727
|
await storeChannelEntry(sink, {
|
|
@@ -677,6 +808,7 @@ async function voucher(
|
|
|
677
808
|
async function manual(
|
|
678
809
|
plan: Extract<CredentialPlan, { type: 'manual' }>,
|
|
679
810
|
sink: ChannelSink,
|
|
811
|
+
autoSwap: AutoSwap.resolve.Resolved | false,
|
|
680
812
|
): Promise<SessionCredentialPayload> {
|
|
681
813
|
const { account, context, decimals, resolved } = plan
|
|
682
814
|
const { descriptor } = context
|
|
@@ -695,26 +827,30 @@ async function manual(
|
|
|
695
827
|
token: resolved.token,
|
|
696
828
|
})
|
|
697
829
|
|
|
698
|
-
const payload = await executeManualCredential(
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
830
|
+
const payload = await executeManualCredential(
|
|
831
|
+
{
|
|
832
|
+
account,
|
|
833
|
+
channelId,
|
|
834
|
+
context,
|
|
835
|
+
decimals,
|
|
836
|
+
descriptor,
|
|
837
|
+
resolved,
|
|
838
|
+
},
|
|
839
|
+
autoSwap,
|
|
840
|
+
)
|
|
706
841
|
await applyCumulative(sink, resolved.key, payload)
|
|
707
842
|
return payload
|
|
708
843
|
}
|
|
709
844
|
|
|
710
845
|
async function executeManualCredential(
|
|
711
846
|
parameters: ManualCredentialParameters,
|
|
847
|
+
autoSwap: AutoSwap.resolve.Resolved | false,
|
|
712
848
|
): Promise<SessionCredentialPayload> {
|
|
713
849
|
switch (parameters.context.action) {
|
|
714
850
|
case 'open':
|
|
715
851
|
return manualOpen(parameters)
|
|
716
852
|
case 'topUp':
|
|
717
|
-
return manualTopUp(parameters)
|
|
853
|
+
return manualTopUp(parameters, autoSwap)
|
|
718
854
|
case 'voucher':
|
|
719
855
|
return manualVoucher(parameters)
|
|
720
856
|
case 'close':
|
|
@@ -750,6 +886,7 @@ async function manualOpen(
|
|
|
750
886
|
|
|
751
887
|
async function manualTopUp(
|
|
752
888
|
parameters: ManualCredentialParameters,
|
|
889
|
+
autoSwap: AutoSwap.resolve.Resolved | false,
|
|
753
890
|
): Promise<SessionCredentialPayload> {
|
|
754
891
|
const { account, channelId, context, decimals, descriptor, resolved } = parameters
|
|
755
892
|
const additionalDeposit = requireContextAmount(context, decimals, 'additionalDeposit', 'topUp')
|
|
@@ -771,6 +908,12 @@ async function manualTopUp(
|
|
|
771
908
|
resolved.chainId,
|
|
772
909
|
resolved.feePayer,
|
|
773
910
|
resolved.escrow,
|
|
911
|
+
await resolveAutoSwapCalls(autoSwap, {
|
|
912
|
+
account: account.address,
|
|
913
|
+
amountOut: additionalDeposit,
|
|
914
|
+
client: resolved.client,
|
|
915
|
+
tokenOut: resolved.token,
|
|
916
|
+
}),
|
|
774
917
|
)
|
|
775
918
|
}
|
|
776
919
|
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
nextSpentFromReceipt,
|
|
31
31
|
parseManagerAmount,
|
|
32
32
|
reduce,
|
|
33
|
+
resolveAutomaticTopUp,
|
|
33
34
|
resolveCloseTarget,
|
|
34
35
|
resolveNeedVoucherTransition,
|
|
35
36
|
resolveOpeningDeposit,
|
|
@@ -925,6 +926,17 @@ describe('LocalAuthorization', () => {
|
|
|
925
926
|
)
|
|
926
927
|
})
|
|
927
928
|
|
|
929
|
+
test.each([
|
|
930
|
+
[{ topUpAmount: 80n, suggestedDeposit: 100n }, 80n],
|
|
931
|
+
[{ suggestedDeposit: 100n }, 100n],
|
|
932
|
+
[{}, 50n],
|
|
933
|
+
[{ maxDeposit: 180n, suggestedDeposit: 100n }, 80n],
|
|
934
|
+
])('resolves bounded automatic top-ups %#', (policy, expected) => {
|
|
935
|
+
expect(resolveAutomaticTopUp({ deposit: 100n, requiredCumulative: 150n, ...policy })).toBe(
|
|
936
|
+
expected,
|
|
937
|
+
)
|
|
938
|
+
})
|
|
939
|
+
|
|
928
940
|
test('enforces optional maxDeposit through the compatibility helper', () => {
|
|
929
941
|
expect(() => assertWithinMaxDeposit(101n, 100n)).toThrow(
|
|
930
942
|
'requested voucher amount 101 exceeds local maxDeposit 100',
|
|
@@ -533,6 +533,28 @@ export function resolveOpeningDeposit(parameters: ResolveOpeningDepositParameter
|
|
|
533
533
|
return proposed
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
/** Resolves a bounded automatic refill, preferring explicit policy over server headroom. */
|
|
537
|
+
export function resolveAutomaticTopUp(parameters: {
|
|
538
|
+
deposit: bigint
|
|
539
|
+
maxDeposit?: bigint | null | undefined
|
|
540
|
+
requiredCumulative: bigint
|
|
541
|
+
suggestedDeposit?: bigint | undefined
|
|
542
|
+
topUpAmount?: bigint | null | undefined
|
|
543
|
+
}): bigint {
|
|
544
|
+
const { deposit, maxDeposit, requiredCumulative, suggestedDeposit, topUpAmount } = parameters
|
|
545
|
+
if (requiredCumulative <= deposit) return 0n
|
|
546
|
+
assertVoucherWithinLocalLimit({
|
|
547
|
+
cumulativeAmount: requiredCumulative,
|
|
548
|
+
maxVoucherCumulative: maxDeposit ?? null,
|
|
549
|
+
})
|
|
550
|
+
const shortfall = requiredCumulative - deposit
|
|
551
|
+
const preferred = topUpAmount ?? suggestedDeposit ?? shortfall
|
|
552
|
+
const proposed = preferred > shortfall ? preferred : shortfall
|
|
553
|
+
if (maxDeposit === null || maxDeposit === undefined) return proposed
|
|
554
|
+
const remaining = maxDeposit - deposit
|
|
555
|
+
return proposed < remaining ? proposed : remaining
|
|
556
|
+
}
|
|
557
|
+
|
|
536
558
|
/** Enforces the optional client-side maximum cumulative voucher authorization. */
|
|
537
559
|
export function assertWithinMaxDeposit(
|
|
538
560
|
cumulativeAmount: bigint,
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { type Address, createClient, custom, decodeFunctionData, encodeFunctionResult } from 'viem'
|
|
2
2
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
3
|
import { Account as TempoAccount, Secp256k1, Transaction } from 'viem/tempo'
|
|
4
|
-
import { describe, expect, test } from 'vp/test'
|
|
4
|
+
import { describe, expect, test, vi } from 'vp/test'
|
|
5
5
|
|
|
6
6
|
import { serialize as serializeChallenge, type Challenge } from '../../../Challenge.js'
|
|
7
7
|
import * as Fetch from '../../../client/internal/Fetch.js'
|
|
8
|
+
import * as MethodChallenge from '../../../client/internal/MethodChallenge.js'
|
|
8
9
|
import * as Constants from '../../../Constants.js'
|
|
9
10
|
import * as Credential from '../../../Credential.js'
|
|
10
11
|
import * as z from '../../../zod.js'
|
|
12
|
+
import * as AutoSwap from '../../internal/auto-swap.js'
|
|
11
13
|
import * as Methods from '../../Methods.js'
|
|
12
14
|
import * as Channel from '../precompile/Channel.js'
|
|
13
15
|
import { escrowAbi } from '../precompile/escrow.abi.js'
|
|
@@ -99,6 +101,14 @@ function openDeposit(payload: Types.SessionCredentialPayload): bigint {
|
|
|
99
101
|
return openArgs(payload)[3]
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
function transactionCalls(payload: Types.SessionCredentialPayload) {
|
|
105
|
+
if (payload.action !== 'open' && payload.action !== 'topUp')
|
|
106
|
+
throw new Error('expected transaction payload')
|
|
107
|
+
const transaction = Transaction.deserialize(payload.transaction)
|
|
108
|
+
if (!('calls' in transaction)) throw new Error('expected tempo calls')
|
|
109
|
+
return transaction.calls as readonly { to?: Address; data?: `0x${string}` }[]
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
describe('precompile client session', () => {
|
|
103
113
|
test('handles only TIP-1034 session challenges', () => {
|
|
104
114
|
const method = session({ account, getClient: () => client })
|
|
@@ -205,7 +215,164 @@ describe('precompile client session', () => {
|
|
|
205
215
|
expect(await response.text()).toBe(
|
|
206
216
|
`${Types.formatMessageEvent('first')}${Types.formatMessageEvent('second')}`,
|
|
207
217
|
)
|
|
208
|
-
expect(actions).toEqual(['open', '
|
|
218
|
+
expect(actions).toEqual(['open', 'topUp', 'voucher', 'voucher'])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('tops up before signing plain HTTP vouchers through Fetch.from', async () => {
|
|
222
|
+
const challenge = makeChallenge({
|
|
223
|
+
suggestedDeposit: '100',
|
|
224
|
+
methodDetails: {
|
|
225
|
+
chainId,
|
|
226
|
+
escrowContract: tip20ChannelEscrow,
|
|
227
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
const actions: Types.SessionCredentialPayload['action'][] = []
|
|
231
|
+
const paidRequests: {
|
|
232
|
+
body: BodyInit | null
|
|
233
|
+
header: string | null
|
|
234
|
+
method: string | undefined
|
|
235
|
+
}[] = []
|
|
236
|
+
let opened: Extract<Types.SessionCredentialPayload, { action: 'open' }> | undefined
|
|
237
|
+
const rawFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
238
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
239
|
+
if (!authorization)
|
|
240
|
+
return new Response(null, {
|
|
241
|
+
status: 402,
|
|
242
|
+
headers: { [Constants.Headers.wwwAuthenticate]: serializeChallenge(challenge) },
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const payload = deserialize(authorization)
|
|
246
|
+
actions.push(payload.action)
|
|
247
|
+
if (payload.action === 'open') {
|
|
248
|
+
opened = payload
|
|
249
|
+
return new Response('opened')
|
|
250
|
+
}
|
|
251
|
+
if (payload.action === 'topUp') return new Response(null, { status: 204 })
|
|
252
|
+
if (payload.action !== 'voucher' || !opened) throw new Error('expected open voucher')
|
|
253
|
+
|
|
254
|
+
paidRequests.push({
|
|
255
|
+
body: init?.body ?? null,
|
|
256
|
+
header: new Headers(init?.headers).get('x-test'),
|
|
257
|
+
method: init?.method,
|
|
258
|
+
})
|
|
259
|
+
return new Response('paid')
|
|
260
|
+
}
|
|
261
|
+
const fetch = Fetch.from({
|
|
262
|
+
fetch: rawFetch,
|
|
263
|
+
methods: [
|
|
264
|
+
session({
|
|
265
|
+
account,
|
|
266
|
+
decimals: 0,
|
|
267
|
+
getClient: () => client,
|
|
268
|
+
maxDeposit: '300',
|
|
269
|
+
}),
|
|
270
|
+
],
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
expect(await (await fetch('https://example.com/resource')).text()).toBe('opened')
|
|
274
|
+
const response = await fetch('https://example.com/resource', {
|
|
275
|
+
body: 'request-body',
|
|
276
|
+
headers: { 'x-test': 'preserved' },
|
|
277
|
+
method: 'POST',
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
expect(await response.text()).toBe('paid')
|
|
281
|
+
expect(actions).toEqual(['open', 'topUp', 'voucher'])
|
|
282
|
+
expect(paidRequests).toEqual([{ body: 'request-body', header: 'preserved', method: 'POST' }])
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('does not top up a stored channel owned by another account', async () => {
|
|
286
|
+
const channelStore = createChannelStore()
|
|
287
|
+
const foreign = '0x0000000000000000000000000000000000000004' as Address
|
|
288
|
+
await channelStore.set({
|
|
289
|
+
chainId,
|
|
290
|
+
channelId: `0x${'44'.repeat(32)}`,
|
|
291
|
+
cumulativeAmount: 100n,
|
|
292
|
+
deposit: 100n,
|
|
293
|
+
descriptor: { ...descriptor, authorizedSigner: foreign, payer: foreign },
|
|
294
|
+
escrow: tip20ChannelEscrow,
|
|
295
|
+
opened: true,
|
|
296
|
+
})
|
|
297
|
+
const actions: Types.SessionCredentialPayload['action'][] = []
|
|
298
|
+
const challenge = makeChallenge({
|
|
299
|
+
methodDetails: {
|
|
300
|
+
chainId,
|
|
301
|
+
escrowContract: tip20ChannelEscrow,
|
|
302
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
const fetch = Fetch.from({
|
|
306
|
+
fetch: async (_input, init) => {
|
|
307
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
308
|
+
if (!authorization)
|
|
309
|
+
return new Response(null, {
|
|
310
|
+
status: 402,
|
|
311
|
+
headers: { [Constants.Headers.wwwAuthenticate]: serializeChallenge(challenge) },
|
|
312
|
+
})
|
|
313
|
+
actions.push(deserialize(authorization).action)
|
|
314
|
+
return new Response('paid')
|
|
315
|
+
},
|
|
316
|
+
methods: [session({ account, channelStore, decimals: 0, getClient: () => client })],
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
expect(await (await fetch('https://example.com/resource')).text()).toBe('paid')
|
|
320
|
+
expect(actions).toEqual(['open'])
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('uses a matching server snapshot deposit when checking headroom', async () => {
|
|
324
|
+
const channelStore = createChannelStore()
|
|
325
|
+
const channelId = Channel.computeId({ ...descriptor, chainId, escrow: tip20ChannelEscrow })
|
|
326
|
+
await channelStore.set({
|
|
327
|
+
chainId,
|
|
328
|
+
channelId,
|
|
329
|
+
cumulativeAmount: 100n,
|
|
330
|
+
deposit: 100n,
|
|
331
|
+
descriptor,
|
|
332
|
+
escrow: tip20ChannelEscrow,
|
|
333
|
+
opened: true,
|
|
334
|
+
})
|
|
335
|
+
const challenge = makeChallenge({
|
|
336
|
+
methodDetails: {
|
|
337
|
+
chainId,
|
|
338
|
+
escrowContract: tip20ChannelEscrow,
|
|
339
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
340
|
+
sessionSnapshot: {
|
|
341
|
+
acceptedCumulative: '100',
|
|
342
|
+
chainId,
|
|
343
|
+
channelId,
|
|
344
|
+
deposit: '200',
|
|
345
|
+
descriptor,
|
|
346
|
+
escrow: tip20ChannelEscrow,
|
|
347
|
+
requiredCumulative: '200',
|
|
348
|
+
settled: '0',
|
|
349
|
+
spent: '100',
|
|
350
|
+
units: 1,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
let updatedDeposit = 0n
|
|
355
|
+
const method = session({
|
|
356
|
+
account,
|
|
357
|
+
channelStore,
|
|
358
|
+
decimals: 0,
|
|
359
|
+
getClient: () => client,
|
|
360
|
+
onChannelUpdate: (entry) => {
|
|
361
|
+
updatedDeposit = entry.deposit
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
await expect(
|
|
366
|
+
MethodChallenge.handle(method, {
|
|
367
|
+
challenge,
|
|
368
|
+
context: {},
|
|
369
|
+
fetch: async () => {
|
|
370
|
+
throw new Error('unexpected top-up')
|
|
371
|
+
},
|
|
372
|
+
input: 'https://example.com/resource',
|
|
373
|
+
}),
|
|
374
|
+
).resolves.toBeUndefined()
|
|
375
|
+
expect(updatedDeposit).toBe(200n)
|
|
209
376
|
})
|
|
210
377
|
|
|
211
378
|
test('opens for the current amount without client deposit configuration', async () => {
|
|
@@ -218,6 +385,72 @@ describe('precompile client session', () => {
|
|
|
218
385
|
expect(openDeposit(payload)).toBe(100n)
|
|
219
386
|
})
|
|
220
387
|
|
|
388
|
+
test('atomically auto-swaps before opening a session channel', async () => {
|
|
389
|
+
const sourceToken = '0x0000000000000000000000000000000000000007' as Address
|
|
390
|
+
const swapCalls = [
|
|
391
|
+
{ to: sourceToken, data: '0x1234' as const },
|
|
392
|
+
{ to: '0x0000000000000000000000000000000000000008' as Address, data: '0x5678' as const },
|
|
393
|
+
]
|
|
394
|
+
const findCalls = vi.spyOn(AutoSwap, 'findCalls').mockResolvedValue(swapCalls)
|
|
395
|
+
try {
|
|
396
|
+
const method = session({
|
|
397
|
+
account,
|
|
398
|
+
autoSwap: { slippage: 2, tokenIn: [sourceToken] },
|
|
399
|
+
getClient: () => client,
|
|
400
|
+
})
|
|
401
|
+
const payload = deserialize(
|
|
402
|
+
await method.createCredential({ challenge: makeChallenge(), context: {} }),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
expect(findCalls).toHaveBeenCalledWith(expect.any(Object), {
|
|
406
|
+
account: account.address,
|
|
407
|
+
amountOut: 100n,
|
|
408
|
+
slippage: 2,
|
|
409
|
+
tokenIn: expect.arrayContaining([sourceToken]),
|
|
410
|
+
tokenOut: descriptor.token,
|
|
411
|
+
})
|
|
412
|
+
const calls = transactionCalls(payload)
|
|
413
|
+
expect(calls.slice(0, 2)).toEqual(swapCalls)
|
|
414
|
+
expect(calls[2]!.to?.toLowerCase()).toBe(tip20ChannelEscrow.toLowerCase())
|
|
415
|
+
expect(decodeFunctionData({ abi: escrowAbi, data: calls[2]!.data! }).functionName).toBe(
|
|
416
|
+
'open',
|
|
417
|
+
)
|
|
418
|
+
} finally {
|
|
419
|
+
findCalls.mockRestore()
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test('atomically auto-swaps before topping up a session channel', async () => {
|
|
424
|
+
const sourceToken = '0x0000000000000000000000000000000000000007' as Address
|
|
425
|
+
const swapCalls = [{ to: sourceToken, data: '0x1234' as const }]
|
|
426
|
+
const findCalls = vi.spyOn(AutoSwap, 'findCalls').mockResolvedValue(swapCalls)
|
|
427
|
+
try {
|
|
428
|
+
const method = session({ account, autoSwap: true, getClient: () => client })
|
|
429
|
+
const payload = deserialize(
|
|
430
|
+
await method.createCredential({
|
|
431
|
+
challenge: makeChallenge(),
|
|
432
|
+
context: { action: 'topUp', additionalDepositRaw: '250', descriptor },
|
|
433
|
+
}),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
expect(findCalls).toHaveBeenCalledWith(expect.any(Object), {
|
|
437
|
+
account: account.address,
|
|
438
|
+
amountOut: 250n,
|
|
439
|
+
slippage: 1,
|
|
440
|
+
tokenIn: AutoSwap.defaultCurrencies,
|
|
441
|
+
tokenOut: descriptor.token,
|
|
442
|
+
})
|
|
443
|
+
const calls = transactionCalls(payload)
|
|
444
|
+
expect(calls[0]).toEqual(swapCalls[0])
|
|
445
|
+
expect(calls[1]!.to?.toLowerCase()).toBe(tip20ChannelEscrow.toLowerCase())
|
|
446
|
+
expect(decodeFunctionData({ abi: escrowAbi, data: calls[1]!.data! }).functionName).toBe(
|
|
447
|
+
'topUp',
|
|
448
|
+
)
|
|
449
|
+
} finally {
|
|
450
|
+
findCalls.mockRestore()
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
|
|
221
454
|
test('uses client chain ID when the challenge omits chain ID', async () => {
|
|
222
455
|
const method = session({ account, getClient: () => client })
|
|
223
456
|
const credential = deserializeCredential(
|
|
@@ -506,7 +739,7 @@ describe('precompile client session', () => {
|
|
|
506
739
|
expect(payload.cumulativeAmount).toBe('400')
|
|
507
740
|
})
|
|
508
741
|
|
|
509
|
-
test('
|
|
742
|
+
test('does not decrease recovered authorization below accepted snapshot headroom', async () => {
|
|
510
743
|
const method = session({ account, decimals: 0, maxDeposit: '1000', getClient: () => client })
|
|
511
744
|
const channelId = Channel.computeId({ ...descriptor, chainId, escrow: tip20ChannelEscrow })
|
|
512
745
|
const payload = deserialize(
|
|
@@ -535,7 +768,7 @@ describe('precompile client session', () => {
|
|
|
535
768
|
|
|
536
769
|
expect(payload.action).toBe('voucher')
|
|
537
770
|
if (payload.action !== 'voucher') throw new Error('expected voucher')
|
|
538
|
-
expect(payload.cumulativeAmount).toBe('
|
|
771
|
+
expect(payload.cumulativeAmount).toBe('500')
|
|
539
772
|
})
|
|
540
773
|
|
|
541
774
|
test('caps recovered voucher authorization by maxDeposit', async () => {
|
|
@@ -593,7 +826,7 @@ describe('precompile client session', () => {
|
|
|
593
826
|
}),
|
|
594
827
|
context: {},
|
|
595
828
|
}),
|
|
596
|
-
).rejects.toThrow('requested voucher amount
|
|
829
|
+
).rejects.toThrow('requested voucher amount 1000 exceeds local maxDeposit 300')
|
|
597
830
|
})
|
|
598
831
|
|
|
599
832
|
test('rejects descriptor recovery for closed or missing channels', async () => {
|