mppx 0.8.11 → 0.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +16 -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 +35 -6
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodResponse.d.ts +23 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -0
- package/dist/client/internal/MethodResponse.js +15 -0
- package/dist/client/internal/MethodResponse.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/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 +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 +2 -0
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +4 -0
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/legacy/client/Session.d.ts +2 -2
- package/dist/tempo/legacy/client/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/client/Session.js +7 -0
- package/dist/tempo/legacy/client/Session.js.map +1 -1
- 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 +3 -2
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +2 -23
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +2 -2
- package/dist/tempo/server/Methods.js.map +1 -1
- 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.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +11 -2
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +21 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +66 -1
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +1 -1
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +53 -1
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +2 -2
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +130 -30
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +21 -2
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +129 -18
- 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.js +13 -4
- package/dist/tempo/session/precompile/Chain.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 +10 -4
- 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/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/internal/Fetch.test.ts +29 -0
- package/src/client/internal/Fetch.ts +51 -6
- package/src/client/internal/MethodResponse.ts +39 -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 +42 -0
- 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/internal/types.ts +13 -2
- package/src/tempo/legacy/client/ChannelOps.test.ts +21 -0
- package/src/tempo/legacy/client/ChannelOps.ts +7 -0
- package/src/tempo/legacy/client/Session.test.ts +102 -1
- package/src/tempo/legacy/client/Session.ts +10 -0
- 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.ts +6 -4
- package/src/tempo/server/Methods.ts +5 -4
- 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 +40 -0
- package/src/tempo/session/client/ChannelOps.ts +12 -2
- package/src/tempo/session/client/CredentialState.test.ts +2 -2
- package/src/tempo/session/client/CredentialState.ts +103 -1
- package/src/tempo/session/client/Session.test.ts +86 -4
- package/src/tempo/session/client/Session.ts +73 -1
- package/src/tempo/session/client/SessionManager.test.ts +137 -4
- package/src/tempo/session/client/SessionManager.ts +141 -33
- package/src/tempo/session/client/Transports.test.ts +225 -8
- package/src/tempo/session/client/Transports.ts +156 -21
- package/src/tempo/session/client/internal/SessionManager.ts +36 -0
- package/src/tempo/session/precompile/Chain.test.ts +26 -0
- package/src/tempo/session/precompile/Chain.ts +17 -4
- 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 +39 -3
- package/src/tempo/session/server/RequestState.ts +13 -6
- 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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Tempo Sessions design
|
|
2
|
+
|
|
3
|
+
Tempo Sessions implements TIP-1034 payment channels for repeated HTTP and
|
|
4
|
+
streaming payments. A session is a channel, not an application login session.
|
|
5
|
+
|
|
6
|
+
A payer authorizes a cumulative amount with a signed voucher. The server
|
|
7
|
+
accepts that authorization, records delivered spend, and settles on-chain under
|
|
8
|
+
server-owned policy.
|
|
9
|
+
|
|
10
|
+
## Design principles
|
|
11
|
+
|
|
12
|
+
1. A channel has distinct on-chain, server, and client state. No copy replaces
|
|
13
|
+
another authority.
|
|
14
|
+
2. Vouchers authorize value; server accounting records delivered value; chain
|
|
15
|
+
settlement captures value. These are separate operations.
|
|
16
|
+
3. The server atomically accepts vouchers and records charges. Multiple server
|
|
17
|
+
instances share one linearizable channel store.
|
|
18
|
+
4. Client persistence and server snapshots accelerate recovery. Neither is
|
|
19
|
+
proof of a reusable channel until cryptographic and chain validation pass.
|
|
20
|
+
5. Management credentials never invoke application content handlers.
|
|
21
|
+
6. Settlement policy belongs to the server. A client authorizes value but does
|
|
22
|
+
not choose when it is settled.
|
|
23
|
+
|
|
24
|
+
```mermaid
|
|
25
|
+
flowchart LR
|
|
26
|
+
client["Client"]
|
|
27
|
+
cache["Client ChannelStore\nrecovery cache"]
|
|
28
|
+
server["Server session method\nverification and accounting"]
|
|
29
|
+
ledger["Server AtomicStore\naccepted vouchers and spend"]
|
|
30
|
+
chain["Tempo escrow precompile\ndeposit, settlement, close state"]
|
|
31
|
+
|
|
32
|
+
client <--> cache
|
|
33
|
+
client -->|"transactions and vouchers"| server
|
|
34
|
+
client -->|"channel state reads"| chain
|
|
35
|
+
server <--> ledger
|
|
36
|
+
server -->|"validates, funds, settles"| chain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authority model
|
|
40
|
+
|
|
41
|
+
| State | Meaning | Authority |
|
|
42
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
43
|
+
| Channel descriptor and `channelId` | Immutable payer, payee, token, signer, operator, nonce, and salt identity. | Derived from descriptor, chain ID, and escrow address. Every boundary validates it. |
|
|
44
|
+
| On-chain channel state | Deposit, settled amount, and close state. | Tempo escrow precompile. |
|
|
45
|
+
| Signed voucher | Payer authorization of a cumulative amount for one channel. | Client signer; server verifies before accepting. |
|
|
46
|
+
| Server channel record | Highest accepted signed voucher, delivered `spent`, `units`, and settlement progress. | Server `AtomicStore`. |
|
|
47
|
+
| Client channel entry | Latest locally usable channel descriptor, deposit, and cumulative authorization. | Client `ChannelStore`; a cache. |
|
|
48
|
+
| `SessionSnapshot` | Server-provided recovery hint. | Untrusted until client validation. |
|
|
49
|
+
|
|
50
|
+
The amounts below have intentionally different meanings.
|
|
51
|
+
|
|
52
|
+
| Amount | Meaning | Rule |
|
|
53
|
+
| --------------------------------------------- | -------------------------------------------- | -------------------------------------------------------- |
|
|
54
|
+
| `acceptedCumulative` / `highestVoucherAmount` | Highest voucher the server accepted. | Never decreases. |
|
|
55
|
+
| `spent` | Value charged for content. | Never decreases; does not exceed accepted authorization. |
|
|
56
|
+
| `settled` / `settledOnChain` | Value captured by escrow. | Never decreases; chain-authoritative. |
|
|
57
|
+
| `requiredCumulative` | Authorization required for the next request. | It is a boundary, not a signed voucher. |
|
|
58
|
+
|
|
59
|
+
## Request lifecycle
|
|
60
|
+
|
|
61
|
+
The session method owns payment control flow. The route handler owns content.
|
|
62
|
+
|
|
63
|
+
```mermaid
|
|
64
|
+
sequenceDiagram
|
|
65
|
+
participant C as Client
|
|
66
|
+
participant S as Server session
|
|
67
|
+
participant L as AtomicStore
|
|
68
|
+
participant T as Tempo chain
|
|
69
|
+
participant H as Route handler
|
|
70
|
+
|
|
71
|
+
C->>S: protected request
|
|
72
|
+
S-->>C: 402 session challenge
|
|
73
|
+
C->>S: open or voucher credential
|
|
74
|
+
S->>T: validate channel state; broadcast management transaction when needed
|
|
75
|
+
S->>L: atomically accept voucher and record channel state
|
|
76
|
+
S->>L: charge billable HTTP content
|
|
77
|
+
S->>H: invoke only for a content credential
|
|
78
|
+
H-->>C: response with Payment-Receipt
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`open` and `voucher` credentials may pay for a billable request. `topUp` and
|
|
82
|
+
`close` are management credentials: they return `204` and never invoke the
|
|
83
|
+
route handler. `open` and `voucher` used on non-billable requests are also
|
|
84
|
+
management updates.
|
|
85
|
+
|
|
86
|
+
The current HTTP contract is deliberately pre-handler: voucher acceptance and
|
|
87
|
+
default request charging occur before the handler runs. A handler failure can
|
|
88
|
+
therefore follow an accepted voucher and recorded `spent`. Sessions does not
|
|
89
|
+
promise an atomic “charge only after a successful handler” transaction.
|
|
90
|
+
|
|
91
|
+
Applications with irreversible work define their own idempotency and failure
|
|
92
|
+
behavior. SDK changes must preserve this boundary or introduce an explicit
|
|
93
|
+
prepare/commit contract rather than changing it implicitly.
|
|
94
|
+
|
|
95
|
+
## Recovery lifecycle
|
|
96
|
+
|
|
97
|
+
The application owns durable mapping from its authenticated request identity to
|
|
98
|
+
a channel ID. The SDK owns channel lookup, snapshot construction, and recovery
|
|
99
|
+
validation.
|
|
100
|
+
|
|
101
|
+
`resolveChannelId` is the only application-defined recovery boundary. It maps a
|
|
102
|
+
verified request identity to an existing channel ID. It does not authorize a
|
|
103
|
+
channel, recreate accounting, or trust a client-provided channel ID.
|
|
104
|
+
|
|
105
|
+
### Bootstrap configuration
|
|
106
|
+
|
|
107
|
+
Enable `bootstrap` when a client should recover an existing channel before its
|
|
108
|
+
first paid request:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
tempo.session({
|
|
112
|
+
bootstrap: true,
|
|
113
|
+
resolveChannelId({ source, paymentRequest }) {
|
|
114
|
+
return db.findChannelId({
|
|
115
|
+
payer: parseSource(source),
|
|
116
|
+
payee: paymentRequest.recipient,
|
|
117
|
+
token: paymentRequest.currency,
|
|
118
|
+
// Include chain and escrow when the application supports more than one.
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The hook resolves application identity and payment scope. The channel store
|
|
125
|
+
loads the returned primary key only; MPPx does not scan the store or define a
|
|
126
|
+
secondary-index format. If the request supplies a channel ID, MPPx uses it and
|
|
127
|
+
does not call the hook.
|
|
128
|
+
|
|
129
|
+
```mermaid
|
|
130
|
+
sequenceDiagram
|
|
131
|
+
participant C as Cold client
|
|
132
|
+
participant S as Server session
|
|
133
|
+
participant R as resolveChannelId
|
|
134
|
+
participant L as AtomicStore
|
|
135
|
+
participant T as Tempo chain
|
|
136
|
+
|
|
137
|
+
C->>S: HEAD protected resource
|
|
138
|
+
S-->>C: 402 zero-amount identity challenge
|
|
139
|
+
C->>S: HEAD with signed proof
|
|
140
|
+
S->>S: verify proof and recover source
|
|
141
|
+
S->>R: resolve source and payment scope to channelId
|
|
142
|
+
R-->>S: channelId or no result
|
|
143
|
+
S->>L: load compatible channel record
|
|
144
|
+
S->>S: validate chain, token, escrow, payee, and signed voucher
|
|
145
|
+
S-->>C: 204 with session snapshot when available
|
|
146
|
+
C->>T: read live channel state
|
|
147
|
+
C->>C: validate descriptor, signer, voucher, and state
|
|
148
|
+
C->>C: hydrate Client ChannelStore
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A snapshot is never authorization. Before caching it, the client validates the
|
|
152
|
+
descriptor-derived ID, payer, authorized signer, signed voucher when present,
|
|
153
|
+
and live on-chain state. A snapshot that lacks a usable signed voucher cannot
|
|
154
|
+
create higher client authorization; the client reuses persisted state or opens
|
|
155
|
+
a channel instead.
|
|
156
|
+
|
|
157
|
+
## Streaming lifecycle
|
|
158
|
+
|
|
159
|
+
SSE and WebSocket are stream-metered. They do not reuse normal HTTP response
|
|
160
|
+
accounting. The server emits a voucher boundary as content is consumed; the
|
|
161
|
+
client submits the next signed cumulative voucher; the server accepts it and
|
|
162
|
+
emits a receipt before content continues.
|
|
163
|
+
|
|
164
|
+
```mermaid
|
|
165
|
+
sequenceDiagram
|
|
166
|
+
participant S as Metered server stream
|
|
167
|
+
participant C as Client transport driver
|
|
168
|
+
participant L as AtomicStore
|
|
169
|
+
|
|
170
|
+
S->>C: content
|
|
171
|
+
S->>C: need-voucher(requiredCumulative)
|
|
172
|
+
C->>C: enforce max deposit; top up when required
|
|
173
|
+
C->>S: signed voucher
|
|
174
|
+
S->>L: atomically accept voucher
|
|
175
|
+
S->>C: receipt
|
|
176
|
+
S->>C: continue content
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The initial SSE POST is excluded from default HTTP charging to prevent double
|
|
180
|
+
counting. Stream accounting remains owned by the transport driver.
|
|
181
|
+
|
|
182
|
+
## Extension boundaries
|
|
183
|
+
|
|
184
|
+
| Change | Preserve |
|
|
185
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
186
|
+
| Credential action or payload | Descriptor validation, source binding, action gate, receipt semantics, and atomic store update. |
|
|
187
|
+
| Voucher or accounting rule | Monotonic accepted/spent/settled values and linearizable updates. |
|
|
188
|
+
| Client planning or persistence | Channel scope key, recovery validation, max-deposit enforcement, and manual context behavior. |
|
|
189
|
+
| Bootstrap or snapshot | `resolveChannelId` ownership and client-side validation before cache hydration. |
|
|
190
|
+
| SSE or WebSocket transport | Transport-owned metering and receipt coordination; no HTTP double charge. |
|
|
191
|
+
|
|
192
|
+
The module boundaries implement these contracts:
|
|
193
|
+
|
|
194
|
+
| Module | Responsibility |
|
|
195
|
+
| --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
196
|
+
| `precompile/` | Channel identity, chain calls, vouchers, and wire primitives. |
|
|
197
|
+
| `client/CredentialState.ts` and `client/ChannelOps.ts` | Credential planning, recovery validation, and client cache updates. |
|
|
198
|
+
| `server/CredentialVerification.ts` and `server/ChannelStore.ts` | Credential verification and authoritative atomic channel state. |
|
|
199
|
+
| `server/Settlement.ts` | Content accounting and server-owned settlement cadence. |
|
|
200
|
+
| `server/RequestState.ts` | Challenge context, bootstrap identity lookup, snapshots, and response gating. |
|
|
201
|
+
| `client/Transports.ts` and `server/Transports.ts` | SSE and WebSocket payment control messages. |
|
|
@@ -22,6 +22,17 @@ export type SessionSnapshot = {
|
|
|
22
22
|
escrow: Address
|
|
23
23
|
/** Minimum cumulative authorization needed for the challenged request or stream continuation. */
|
|
24
24
|
requiredCumulative: RawAmountString
|
|
25
|
+
/** Highest client-signed voucher accepted by the server. */
|
|
26
|
+
highestVoucher?:
|
|
27
|
+
| {
|
|
28
|
+
/** Channel identifier bound into the voucher signature. */
|
|
29
|
+
channelId: Hex
|
|
30
|
+
/** Cumulative authorization bound into the voucher signature. */
|
|
31
|
+
cumulativeAmount: RawAmountString
|
|
32
|
+
/** Original client signature proving the accepted cumulative authorization. */
|
|
33
|
+
signature: Hex
|
|
34
|
+
}
|
|
35
|
+
| undefined
|
|
25
36
|
/** Amount already settled on-chain. */
|
|
26
37
|
settled: RawAmountString
|
|
27
38
|
/** Amount consumed by delivered content according to server accounting. */
|
|
@@ -36,6 +47,9 @@ const addressSchema = z.custom<Address>(
|
|
|
36
47
|
const hashSchema = z.custom<Hex>(
|
|
37
48
|
(value) => typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value),
|
|
38
49
|
)
|
|
50
|
+
const hexSchema = z.custom<Hex>(
|
|
51
|
+
(value) => typeof value === 'string' && /^0x[0-9a-fA-F]+$/.test(value),
|
|
52
|
+
)
|
|
39
53
|
|
|
40
54
|
const channelDescriptorSchema = z.object({
|
|
41
55
|
authorizedSigner: addressSchema,
|
|
@@ -55,6 +69,13 @@ const sessionSnapshotSchema = z.object({
|
|
|
55
69
|
deposit: z.string(),
|
|
56
70
|
descriptor: channelDescriptorSchema,
|
|
57
71
|
escrow: addressSchema,
|
|
72
|
+
highestVoucher: z.optional(
|
|
73
|
+
z.object({
|
|
74
|
+
channelId: hashSchema,
|
|
75
|
+
cumulativeAmount: z.string(),
|
|
76
|
+
signature: hexSchema,
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
58
79
|
requiredCumulative: z.string(),
|
|
59
80
|
settled: z.string(),
|
|
60
81
|
spent: z.string(),
|
|
@@ -9,6 +9,17 @@ import * as Types from '../precompile/Protocol.js'
|
|
|
9
9
|
import * as Voucher from '../precompile/Voucher.js'
|
|
10
10
|
import * as ChannelOps from './ChannelOps.js'
|
|
11
11
|
|
|
12
|
+
const mocks = vi.hoisted(() => ({
|
|
13
|
+
prepareTransactionRequest: vi.fn(async (_client: unknown, request: unknown) => request),
|
|
14
|
+
signTransaction: vi.fn(async () => '0x1234'),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
vi.mock('viem/actions', async (importOriginal) => ({
|
|
18
|
+
...(await importOriginal<typeof import('viem/actions')>()),
|
|
19
|
+
prepareTransactionRequest: mocks.prepareTransactionRequest,
|
|
20
|
+
signTransaction: mocks.signTransaction,
|
|
21
|
+
}))
|
|
22
|
+
|
|
12
23
|
const account = privateKeyToAccount(
|
|
13
24
|
'0xac0974bec39a17e36ba6a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
14
25
|
)
|
|
@@ -160,4 +171,33 @@ describe('precompile client ChannelOps credential builders', () => {
|
|
|
160
171
|
),
|
|
161
172
|
).toBe(true)
|
|
162
173
|
})
|
|
174
|
+
|
|
175
|
+
test('distinguishes otherwise-identical fee-sponsored management transactions', async () => {
|
|
176
|
+
mocks.prepareTransactionRequest.mockClear()
|
|
177
|
+
let randomValue = 1
|
|
178
|
+
const random = vi.spyOn(globalThis.crypto, 'getRandomValues').mockImplementation((array) => {
|
|
179
|
+
if (!array) return array
|
|
180
|
+
new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(randomValue++)
|
|
181
|
+
return array
|
|
182
|
+
})
|
|
183
|
+
try {
|
|
184
|
+
await ChannelOps.createTopUpPayload(client, account, descriptor, 10n, chainId, true)
|
|
185
|
+
await ChannelOps.createTopUpPayload(client, account, descriptor, 10n, chainId, true)
|
|
186
|
+
} finally {
|
|
187
|
+
random.mockRestore()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const requests = mocks.prepareTransactionRequest.mock.calls.map(([, request]) => request)
|
|
191
|
+
const validAfter = requests.map((request) =>
|
|
192
|
+
Number((request as { validAfter?: number }).validAfter),
|
|
193
|
+
)
|
|
194
|
+
const now = Math.floor(Date.now() / 1_000)
|
|
195
|
+
|
|
196
|
+
expect(requests).toMatchObject([
|
|
197
|
+
{ feePayer: true, validAfter: expect.any(Number) },
|
|
198
|
+
{ feePayer: true, validAfter: expect.any(Number) },
|
|
199
|
+
])
|
|
200
|
+
expect(new Set(validAfter).size).toBe(2)
|
|
201
|
+
expect(validAfter.every((value) => value >= 0 && value < now)).toBe(true)
|
|
202
|
+
})
|
|
163
203
|
})
|
|
@@ -67,6 +67,14 @@ function readAccessKeyAddress(account: Account): Address | undefined {
|
|
|
67
67
|
return readOptionalAddress((account as AccountWithAccessKey).accessKeyAddress)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** Returns random past seconds to distinguish otherwise-identical expiring transactions. */
|
|
71
|
+
function randomValidAfter(): number {
|
|
72
|
+
const now = BigInt(Math.floor(Date.now() / 1_000))
|
|
73
|
+
const latest = now - 60n
|
|
74
|
+
if (latest <= 0n) return 0
|
|
75
|
+
return Number(BigInt(Hex.random(8)) % latest)
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
/** Resolves the voucher authority address for a client account. */
|
|
71
79
|
export function resolveAuthorizedSigner(account: Account): Address {
|
|
72
80
|
return readAccessKeyAddress(account) ?? account.address
|
|
@@ -79,16 +87,17 @@ async function prepareTempoChannelTransaction(
|
|
|
79
87
|
calls: readonly TempoChannelCall[]
|
|
80
88
|
feePayer?: boolean | undefined
|
|
81
89
|
feeToken: Address
|
|
90
|
+
validAfter?: number | undefined
|
|
82
91
|
},
|
|
83
92
|
) {
|
|
84
|
-
const { account, calls, feePayer, feeToken } = parameters
|
|
93
|
+
const { account, calls, feePayer, feeToken, validAfter } = parameters
|
|
85
94
|
// viem's stable transaction request type does not yet expose Tempo's
|
|
86
95
|
// `calls`, `feePayer`, and `feeToken` fields together. Keep the cast at
|
|
87
96
|
// this boundary so session credential builders stay typed.
|
|
88
97
|
return prepareTransactionRequest(client, {
|
|
89
98
|
account,
|
|
90
99
|
calls,
|
|
91
|
-
...(feePayer ? { feePayer: true } : {}),
|
|
100
|
+
...(feePayer ? { feePayer: true, ...(validAfter !== undefined ? { validAfter } : {}) } : {}),
|
|
92
101
|
feeToken,
|
|
93
102
|
} as never)
|
|
94
103
|
}
|
|
@@ -316,6 +325,7 @@ export async function createTopUpPayload(
|
|
|
316
325
|
],
|
|
317
326
|
feePayer,
|
|
318
327
|
feeToken: descriptor.token,
|
|
328
|
+
...(feePayer ? { validAfter: randomValidAfter() } : {}),
|
|
319
329
|
})
|
|
320
330
|
const transaction = await signPreparedTempoTransaction(client, prepared)
|
|
321
331
|
|
|
@@ -443,7 +443,7 @@ describe('CredentialPlan', () => {
|
|
|
443
443
|
})
|
|
444
444
|
})
|
|
445
445
|
|
|
446
|
-
test('recovery cumulative
|
|
446
|
+
test('recovery cumulative never decreases below the server-accepted voucher', () => {
|
|
447
447
|
expect(
|
|
448
448
|
resolveRecoveredCumulative({
|
|
449
449
|
context: { descriptor, channelId },
|
|
@@ -456,7 +456,7 @@ describe('CredentialPlan', () => {
|
|
|
456
456
|
spent: '10',
|
|
457
457
|
}),
|
|
458
458
|
}),
|
|
459
|
-
).toBe(
|
|
459
|
+
).toBe(1_000_000n)
|
|
460
460
|
})
|
|
461
461
|
|
|
462
462
|
test('plans manual credentials only when an explicit action includes descriptor', () => {
|
|
@@ -14,6 +14,7 @@ import * as z from '../../../zod.js'
|
|
|
14
14
|
import * as Chain from '../precompile/Chain.js'
|
|
15
15
|
import * as Channel from '../precompile/Channel.js'
|
|
16
16
|
import type { SessionCredentialPayload } from '../precompile/Protocol.js'
|
|
17
|
+
import * as Voucher from '../precompile/Voucher.js'
|
|
17
18
|
import type { SessionSnapshot } from '../Snapshot.js'
|
|
18
19
|
import {
|
|
19
20
|
createClosePayload,
|
|
@@ -309,6 +310,26 @@ export type ResolveRecoveredCumulativeParameters = {
|
|
|
309
310
|
settled: bigint
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
/** Inputs used to validate and hydrate a server-provided session snapshot. */
|
|
314
|
+
export type HydrateSessionSnapshotParameters = {
|
|
315
|
+
/** Account expected to own the channel and its voucher authority. */
|
|
316
|
+
account: ViemAccount
|
|
317
|
+
/** Viem client used to read authoritative TIP-1034 channel state. */
|
|
318
|
+
client: Client
|
|
319
|
+
/** Snapshot returned by an MPP server bootstrap. */
|
|
320
|
+
snapshot: SessionSnapshot
|
|
321
|
+
/** Optional state reader for deterministic tests. */
|
|
322
|
+
readChannelState?: ReadReusableChannelState | undefined
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Validated client channel state reconstructed without local persistence. */
|
|
326
|
+
export type HydratedSessionSnapshot = {
|
|
327
|
+
/** Reusable channel entry safe to cache locally. */
|
|
328
|
+
entry: ChannelEntry
|
|
329
|
+
/** Server-accounted delivered spend at bootstrap time. */
|
|
330
|
+
spent: bigint
|
|
331
|
+
}
|
|
332
|
+
|
|
312
333
|
/** Data-first description of the next credential operation the client should execute. */
|
|
313
334
|
export type CredentialPlan =
|
|
314
335
|
/** No reusable channel is available, so create an open transaction and initial voucher. */
|
|
@@ -501,7 +522,12 @@ export function resolveRecoveredCumulative(
|
|
|
501
522
|
const { context, decimals, requestAmount, snapshot, settled } = parameters
|
|
502
523
|
|
|
503
524
|
if (snapshot) {
|
|
504
|
-
return
|
|
525
|
+
return [
|
|
526
|
+
BigInt(snapshot.acceptedCumulative),
|
|
527
|
+
BigInt(snapshot.requiredCumulative),
|
|
528
|
+
BigInt(snapshot.spent) + requestAmount,
|
|
529
|
+
settled,
|
|
530
|
+
].reduce((highest, value) => (value > highest ? value : highest), 0n)
|
|
505
531
|
}
|
|
506
532
|
|
|
507
533
|
const contextCumulative = parseOptionalContextAmount(context, decimals, 'cumulativeAmount')
|
|
@@ -509,6 +535,82 @@ export function resolveRecoveredCumulative(
|
|
|
509
535
|
return settled + requestAmount
|
|
510
536
|
}
|
|
511
537
|
|
|
538
|
+
/** Checks a server snapshot against its signed voucher and current TIP-1034 state. */
|
|
539
|
+
export async function hydrateSessionSnapshot(
|
|
540
|
+
parameters: HydrateSessionSnapshotParameters,
|
|
541
|
+
): Promise<HydratedSessionSnapshot> {
|
|
542
|
+
const { account, client, readChannelState, snapshot } = parameters
|
|
543
|
+
const signed = snapshot.highestVoucher
|
|
544
|
+
if (!signed) throw new Error('session snapshot is missing its highest signed voucher')
|
|
545
|
+
if (signed.channelId.toLowerCase() !== snapshot.channelId.toLowerCase())
|
|
546
|
+
throw new Error('session snapshot voucher channelId does not match snapshot channelId')
|
|
547
|
+
|
|
548
|
+
const acceptedCumulative = BigInt(snapshot.acceptedCumulative)
|
|
549
|
+
const voucherCumulative = BigInt(signed.cumulativeAmount)
|
|
550
|
+
const requiredCumulative = BigInt(snapshot.requiredCumulative)
|
|
551
|
+
const snapshotDeposit = BigInt(snapshot.deposit)
|
|
552
|
+
const snapshotSettled = BigInt(snapshot.settled)
|
|
553
|
+
const spent = BigInt(snapshot.spent)
|
|
554
|
+
if (voucherCumulative !== acceptedCumulative)
|
|
555
|
+
throw new Error('session snapshot voucher amount does not match acceptedCumulative')
|
|
556
|
+
if (spent > acceptedCumulative)
|
|
557
|
+
throw new Error('session snapshot spent exceeds acceptedCumulative')
|
|
558
|
+
if (snapshotSettled > snapshotDeposit)
|
|
559
|
+
throw new Error('session snapshot settled amount exceeds deposit')
|
|
560
|
+
|
|
561
|
+
const authorizedSigner =
|
|
562
|
+
BigInt(snapshot.descriptor.authorizedSigner) === 0n
|
|
563
|
+
? snapshot.descriptor.payer
|
|
564
|
+
: snapshot.descriptor.authorizedSigner
|
|
565
|
+
if (
|
|
566
|
+
!Voucher.verifyVoucher(
|
|
567
|
+
snapshot.escrow,
|
|
568
|
+
snapshot.chainId,
|
|
569
|
+
{
|
|
570
|
+
channelId: signed.channelId,
|
|
571
|
+
cumulativeAmount: voucherCumulative,
|
|
572
|
+
signature: signed.signature,
|
|
573
|
+
},
|
|
574
|
+
authorizedSigner,
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
throw new Error('session snapshot highest voucher signature is invalid')
|
|
578
|
+
|
|
579
|
+
const reusable = await resolveReusableChannel({
|
|
580
|
+
channelId: snapshot.channelId,
|
|
581
|
+
client,
|
|
582
|
+
descriptor: snapshot.descriptor,
|
|
583
|
+
expected: {
|
|
584
|
+
chainId: snapshot.chainId,
|
|
585
|
+
escrow: snapshot.escrow,
|
|
586
|
+
payee: snapshot.descriptor.payee,
|
|
587
|
+
payer: account.address,
|
|
588
|
+
authorizedSigner: resolveAuthorizedSigner(account),
|
|
589
|
+
token: snapshot.descriptor.token,
|
|
590
|
+
},
|
|
591
|
+
readChannelState,
|
|
592
|
+
})
|
|
593
|
+
const cumulativeAmount = [acceptedCumulative, requiredCumulative, reusable.state.settled].reduce(
|
|
594
|
+
(highest, value) => (value > highest ? value : highest),
|
|
595
|
+
0n,
|
|
596
|
+
)
|
|
597
|
+
if (cumulativeAmount > reusable.state.deposit)
|
|
598
|
+
throw new Error('recovered session cumulative amount exceeds on-chain channel deposit')
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
entry: {
|
|
602
|
+
channelId: reusable.channelId,
|
|
603
|
+
cumulativeAmount,
|
|
604
|
+
deposit: reusable.state.deposit,
|
|
605
|
+
descriptor: snapshot.descriptor,
|
|
606
|
+
escrow: snapshot.escrow,
|
|
607
|
+
chainId: snapshot.chainId,
|
|
608
|
+
opened: true,
|
|
609
|
+
},
|
|
610
|
+
spent,
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
512
614
|
/** Returns whether `account` can satisfy the descriptor's voucher authority. */
|
|
513
615
|
export function canSignDescriptor(
|
|
514
616
|
account: ViemAccount,
|
|
@@ -3,7 +3,8 @@ import { privateKeyToAccount } from 'viem/accounts'
|
|
|
3
3
|
import { Account as TempoAccount, Secp256k1, Transaction } from 'viem/tempo'
|
|
4
4
|
import { describe, expect, test } from 'vp/test'
|
|
5
5
|
|
|
6
|
-
import type
|
|
6
|
+
import { serialize as serializeChallenge, type Challenge } from '../../../Challenge.js'
|
|
7
|
+
import * as Fetch from '../../../client/internal/Fetch.js'
|
|
7
8
|
import * as Constants from '../../../Constants.js'
|
|
8
9
|
import * as Credential from '../../../Credential.js'
|
|
9
10
|
import * as z from '../../../zod.js'
|
|
@@ -13,6 +14,7 @@ import { escrowAbi } from '../precompile/escrow.abi.js'
|
|
|
13
14
|
import { tip20ChannelEscrow } from '../precompile/Protocol.js'
|
|
14
15
|
import * as Types from '../precompile/Protocol.js'
|
|
15
16
|
import * as Voucher from '../precompile/Voucher.js'
|
|
17
|
+
import { createChannelStore } from './ChannelStore.js'
|
|
16
18
|
import { session } from './Session.js'
|
|
17
19
|
|
|
18
20
|
const account = privateKeyToAccount(
|
|
@@ -126,6 +128,86 @@ describe('precompile client session', () => {
|
|
|
126
128
|
).toBe(false)
|
|
127
129
|
})
|
|
128
130
|
|
|
131
|
+
test('drives paid SSE responses with a supplied credential', async () => {
|
|
132
|
+
const challenge = makeChallenge({
|
|
133
|
+
suggestedDeposit: '100',
|
|
134
|
+
methodDetails: {
|
|
135
|
+
chainId,
|
|
136
|
+
escrowContract: tip20ChannelEscrow,
|
|
137
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
const channelStore = createChannelStore()
|
|
141
|
+
const parameters = {
|
|
142
|
+
account,
|
|
143
|
+
channelStore,
|
|
144
|
+
decimals: 0,
|
|
145
|
+
getClient: () => client,
|
|
146
|
+
maxDeposit: '300',
|
|
147
|
+
} as const
|
|
148
|
+
// The response handler must follow the credential selected by `onChallenge`.
|
|
149
|
+
const externalCredential = await session(parameters).createCredential({
|
|
150
|
+
challenge,
|
|
151
|
+
context: {},
|
|
152
|
+
})
|
|
153
|
+
let useExternalCredential = true
|
|
154
|
+
const actions: Types.SessionCredentialPayload['action'][] = []
|
|
155
|
+
const rawFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
156
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
157
|
+
if (!authorization)
|
|
158
|
+
return new Response(null, {
|
|
159
|
+
status: 402,
|
|
160
|
+
headers: { [Constants.Headers.wwwAuthenticate]: serializeChallenge(challenge) },
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const payload = deserialize(authorization)
|
|
164
|
+
actions.push(payload.action)
|
|
165
|
+
if (init?.method === 'POST' || payload.action === 'open')
|
|
166
|
+
return new Response(null, { status: 204 })
|
|
167
|
+
if (payload.action !== 'voucher') throw new Error('expected voucher')
|
|
168
|
+
|
|
169
|
+
const receipt = Types.createSessionReceipt({
|
|
170
|
+
acceptedCumulative: 200n,
|
|
171
|
+
challengeId: challenge.id,
|
|
172
|
+
channelId: payload.channelId,
|
|
173
|
+
spent: 200n,
|
|
174
|
+
})
|
|
175
|
+
return new Response(
|
|
176
|
+
[
|
|
177
|
+
Types.formatMessageEvent('first'),
|
|
178
|
+
Types.formatNeedVoucherEvent({
|
|
179
|
+
acceptedCumulative: '100',
|
|
180
|
+
channelId: payload.channelId,
|
|
181
|
+
deposit: '100',
|
|
182
|
+
requiredCumulative: '200',
|
|
183
|
+
}),
|
|
184
|
+
Types.formatMessageEvent('second'),
|
|
185
|
+
Types.formatReceiptEvent(receipt),
|
|
186
|
+
].join(''),
|
|
187
|
+
{ headers: { 'content-type': 'text/event-stream' } },
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
const fetch = Fetch.from({
|
|
191
|
+
fetch: rawFetch,
|
|
192
|
+
methods: [session(parameters)],
|
|
193
|
+
async onChallenge() {
|
|
194
|
+
if (!useExternalCredential) return undefined
|
|
195
|
+
useExternalCredential = false
|
|
196
|
+
return externalCredential
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const response = await fetch('https://example.com/stream', {
|
|
201
|
+
headers: { accept: 'text/event-stream' },
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
expect(response.headers.get('content-type')).toBe('text/event-stream')
|
|
205
|
+
expect(await response.text()).toBe(
|
|
206
|
+
`${Types.formatMessageEvent('first')}${Types.formatMessageEvent('second')}`,
|
|
207
|
+
)
|
|
208
|
+
expect(actions).toEqual(['open', 'voucher', 'topUp', 'voucher'])
|
|
209
|
+
})
|
|
210
|
+
|
|
129
211
|
test('opens for the current amount without client deposit configuration', async () => {
|
|
130
212
|
const method = session({ account, getClient: () => client })
|
|
131
213
|
const payload = deserialize(
|
|
@@ -424,7 +506,7 @@ describe('precompile client session', () => {
|
|
|
424
506
|
expect(payload.cumulativeAmount).toBe('400')
|
|
425
507
|
})
|
|
426
508
|
|
|
427
|
-
test('
|
|
509
|
+
test('does not decrease recovered authorization below accepted snapshot headroom', async () => {
|
|
428
510
|
const method = session({ account, decimals: 0, maxDeposit: '1000', getClient: () => client })
|
|
429
511
|
const channelId = Channel.computeId({ ...descriptor, chainId, escrow: tip20ChannelEscrow })
|
|
430
512
|
const payload = deserialize(
|
|
@@ -453,7 +535,7 @@ describe('precompile client session', () => {
|
|
|
453
535
|
|
|
454
536
|
expect(payload.action).toBe('voucher')
|
|
455
537
|
if (payload.action !== 'voucher') throw new Error('expected voucher')
|
|
456
|
-
expect(payload.cumulativeAmount).toBe('
|
|
538
|
+
expect(payload.cumulativeAmount).toBe('500')
|
|
457
539
|
})
|
|
458
540
|
|
|
459
541
|
test('caps recovered voucher authorization by maxDeposit', async () => {
|
|
@@ -511,7 +593,7 @@ describe('precompile client session', () => {
|
|
|
511
593
|
}),
|
|
512
594
|
context: {},
|
|
513
595
|
}),
|
|
514
|
-
).rejects.toThrow('requested voucher amount
|
|
596
|
+
).rejects.toThrow('requested voucher amount 1000 exceeds local maxDeposit 300')
|
|
515
597
|
})
|
|
516
598
|
|
|
517
599
|
test('rejects descriptor recovery for closed or missing channels', async () => {
|