mppx 0.17.1 → 0.17.4
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 +65 -0
- package/dist/Errors.d.ts +94 -0
- package/dist/Errors.js +83 -0
- package/dist/Store.d.ts +413 -0
- package/dist/Store.js +837 -0
- package/dist/cli/cli.js +141 -136
- package/dist/cli/internal.js +28 -6
- package/dist/cli/plugins/index.js +16 -11
- package/dist/cli/sessions/Manager.js +12 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +6 -0
- package/dist/cli/validate/challenge.d.ts +4 -0
- package/dist/cli/validate/challenge.js +9 -6
- package/dist/client/Mppx.d.ts +1 -1
- package/dist/client/Mppx.js +7 -3
- package/dist/client/internal/Fetch.d.ts +1 -1
- package/dist/client/internal/Fetch.js +9 -0
- package/dist/client/internal/MethodResponse.d.ts +8 -0
- package/dist/client/internal/MethodResponse.js +9 -0
- package/dist/client/internal/ProofRealm.d.ts +3 -3
- package/dist/client/internal/ProofRealm.js +4 -4
- package/dist/client/internal/protocols/Mcp.d.ts +4 -0
- package/dist/client/internal/protocols/Mcp.js +27 -14
- package/dist/cloudflare/ChannelStore.d.ts +97 -0
- package/dist/cloudflare/ChannelStore.js +304 -0
- package/dist/cloudflare/Channels.d.ts +92 -0
- package/dist/cloudflare/Channels.js +325 -0
- package/dist/cloudflare/index.d.ts +5 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/identity/Client.js +7 -1
- package/dist/internal/sse.d.ts +21 -0
- package/dist/internal/sse.js +82 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.d.ts +2 -2
- package/dist/mcp/client/McpClient.js +5 -2
- package/dist/proxy/Proxy.d.ts +2 -0
- package/dist/proxy/Proxy.js +31 -6
- package/dist/proxy/internal/Replay.js +1 -1
- package/dist/server/Mppx.js +5 -4
- package/dist/server/Transport.js +1 -1
- package/dist/server/internal/html/config.d.ts +7 -1
- package/dist/server/internal/html/config.js +4 -0
- package/dist/stripe/internal/types.d.ts +82 -6
- package/dist/stripe/server/Charge.js +1 -2
- package/dist/stripe/server/Methods.d.ts +2 -0
- package/dist/stripe/server/Methods.js +2 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/record-payment.d.ts +49 -0
- package/dist/stripe/server/internal/record-payment.js +75 -18
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.js +18 -2
- package/dist/tempo/client/Charge.d.ts +32 -1
- package/dist/tempo/client/Charge.js +201 -3
- package/dist/tempo/client/Methods.d.ts +16 -0
- package/dist/tempo/internal/fee-payer.d.ts +64 -14
- package/dist/tempo/internal/fee-payer.js +231 -20
- package/dist/tempo/internal/zone.d.ts +79 -0
- package/dist/tempo/internal/zone.js +133 -0
- package/dist/tempo/legacy/session/Chain.js +2 -0
- package/dist/tempo/server/Charge.d.ts +7 -3
- package/dist/tempo/server/Charge.js +100 -6
- package/dist/tempo/server/Methods.d.ts +40 -0
- package/dist/tempo/server/Subscription.js +1 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +6 -0
- package/dist/tempo/server/internal/transport.js +21 -21
- package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/client/CredentialState.js +2 -0
- package/dist/tempo/session/client/Session.d.ts +11 -1
- package/dist/tempo/session/client/Session.js +39 -6
- package/dist/tempo/session/client/SessionManager.d.ts +1 -1
- package/dist/tempo/session/client/SessionManager.js +8 -4
- package/dist/tempo/session/client/Transports.d.ts +2 -0
- package/dist/tempo/session/client/Transports.js +4 -1
- package/dist/tempo/session/precompile/Chain.js +1 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +14 -0
- package/dist/tempo/session/server/ChannelStore.js +10 -3
- package/dist/tempo/session/server/CredentialVerification.js +1 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
- package/dist/tempo/session/server/MeteredStream.js +6 -1
- package/dist/tempo/session/server/Sse.d.ts +59 -0
- package/dist/tempo/session/server/Sse.js +175 -10
- package/dist/validation/core.js +2 -0
- package/dist/zod.d.ts +2 -1
- package/dist/zod.js +1 -1
- package/package.json +5 -3
- package/dist/stripe/server/internal/html/types.d.ts +0 -2
- package/dist/stripe/server/internal/html/types.js +0 -2
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Json } from 'ox';
|
|
2
|
+
import * as Store from '../Store.js';
|
|
3
|
+
import { pendingCloseClockSkewSeconds, pendingCloseValiditySeconds, resolveSettleableVoucher, } from '../tempo/session/server/ChannelStore.js';
|
|
4
|
+
/** Storage key of a channel object's settlement bookkeeping, beside the channel state. */
|
|
5
|
+
const settlementKey = 'mppx:channel-store:settlement';
|
|
6
|
+
/**
|
|
7
|
+
* Durable Object class that stores Tempo session channels, as {@link Store.durableObject}
|
|
8
|
+
* does, and settles them on-chain without a request in flight.
|
|
9
|
+
*
|
|
10
|
+
* Every channel lives in its own object. After each write of a channel state, the object
|
|
11
|
+
* checks how much a settlement could capture ({@link unsettledAmount}): from
|
|
12
|
+
* `max(10% of deposit, minUnsettledAmount)` it sends `{ channelId }` to the settlement
|
|
13
|
+
* queue at once; below that it sets an alarm that sends it once the channel has had no
|
|
14
|
+
* write for `staleAfterMs`. The Worker's `queue()` handler settles it with
|
|
15
|
+
* {@link settle}. After each send, a watchdog alarm sends it again while the channel
|
|
16
|
+
* stays unsettled (a dropped, dead-lettered or failed job), waiting twice as long each
|
|
17
|
+
* time, up to `maxAttempts` sends; settling more on-chain restarts the count. A close request,
|
|
18
|
+
* or a threshold's worth of new vouchers since the last send, sends it again at once and
|
|
19
|
+
* restarts the count, so a failed job never leaves them waiting on a long watchdog.
|
|
20
|
+
*
|
|
21
|
+
* Export a subclass from your Worker, bind it with a `new_sqlite_classes` migration and
|
|
22
|
+
* bind the queue as `MPPX_SETTLEMENT_QUEUE`, or override
|
|
23
|
+
* {@link ChannelStore.settlement} to change the queue or thresholds.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { DurableObject } from 'cloudflare:workers'
|
|
28
|
+
* import { ChannelStore } from 'mppx/cloudflare'
|
|
29
|
+
*
|
|
30
|
+
* export class MppxChannels extends ChannelStore.rpc(DurableObject) {}
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class ChannelStore extends Store.DurableObjectStore {
|
|
34
|
+
/**
|
|
35
|
+
* The settlement queue and thresholds. Reads the `MPPX_SETTLEMENT_QUEUE` binding by
|
|
36
|
+
* default; a subclass overrides it to use another binding or other thresholds.
|
|
37
|
+
*/
|
|
38
|
+
settlement() {
|
|
39
|
+
const env = this.env;
|
|
40
|
+
return { queue: env?.MPPX_SETTLEMENT_QUEUE };
|
|
41
|
+
}
|
|
42
|
+
/** Schedules settlement of a written channel state, and drops it for a closed or deleted one. */
|
|
43
|
+
async onWrite(key, _previous, next, transaction) {
|
|
44
|
+
// Writing through the store's transaction, when it has one, commits or rolls back with the write.
|
|
45
|
+
const storage = transaction ?? this.storage;
|
|
46
|
+
const pending = (await storage.get(settlementKey));
|
|
47
|
+
const settlement = resolveSettlement(this.settlement());
|
|
48
|
+
const due = isChannelState(next) ? settlementDue(next, settlement) : undefined;
|
|
49
|
+
if (!due || !isChannelState(next)) {
|
|
50
|
+
if (pending?.key === key)
|
|
51
|
+
await storage.delete(settlementKey);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// A job settled more on-chain since it was sent: the job is done, so restart the count.
|
|
55
|
+
const progressed = pending?.settledAtSend !== undefined && next.settledOnChain > pending.settledAtSend;
|
|
56
|
+
// Work the last send did not cover: a close request (once the escrow's grace period ends,
|
|
57
|
+
// the payer can withdraw what is unsettled) or a threshold's worth of new vouchers. Send it
|
|
58
|
+
// at once and restart the count instead of waiting for a watchdog that may be far off.
|
|
59
|
+
const renewed = !!pending?.sends &&
|
|
60
|
+
((closing(next) && closeRequest(next) !== pending.closeRequestAtSend) ||
|
|
61
|
+
(pending.capturedAtSend !== undefined &&
|
|
62
|
+
settleableAmount(next) - pending.capturedAtSend >= threshold(next, settlement)));
|
|
63
|
+
const sending = progressed || renewed ? undefined : pending?.sends ? pending : undefined;
|
|
64
|
+
const updated = {
|
|
65
|
+
channelId: next.channelId,
|
|
66
|
+
key,
|
|
67
|
+
sends: sending?.sends ?? 0,
|
|
68
|
+
updatedAt: Date.now(),
|
|
69
|
+
...(sending
|
|
70
|
+
? {
|
|
71
|
+
capturedAtSend: sending.capturedAtSend,
|
|
72
|
+
closeRequestAtSend: sending.closeRequestAtSend,
|
|
73
|
+
settledAtSend: sending.settledAtSend,
|
|
74
|
+
watchdogAt: sending.watchdogAt,
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
77
|
+
};
|
|
78
|
+
await storage.put(settlementKey, updated);
|
|
79
|
+
// A job is in flight: it settles the latest voucher, and its watchdog alarm stays, waking no
|
|
80
|
+
// later than the end of the server's close marker, after which a payer close request it hid shows.
|
|
81
|
+
if (sending)
|
|
82
|
+
return wakeBy(storage, pendingCloseEnd(next) ?? Infinity);
|
|
83
|
+
// Sent from an alarm that fires at once: a queue send here would let another write in before this one commits.
|
|
84
|
+
const at = due === 'threshold' || closing(next)
|
|
85
|
+
? Date.now()
|
|
86
|
+
: Math.min(updated.updatedAt + settlement.staleAfterMs, pendingCloseEnd(next) ?? Infinity);
|
|
87
|
+
await wakeBy(storage, at);
|
|
88
|
+
}
|
|
89
|
+
/** Sends a channel over the threshold or gone stale to the queue, or sends it again from the watchdog while unsettled. */
|
|
90
|
+
async alarm() {
|
|
91
|
+
await super.alarm();
|
|
92
|
+
let pending = (await this.storage.get(settlementKey));
|
|
93
|
+
if (!pending)
|
|
94
|
+
return;
|
|
95
|
+
const raw = await this.storage.get(pending.key);
|
|
96
|
+
// The store writes JSON text, or native values with `encoding: 'native'`.
|
|
97
|
+
const state = typeof raw === 'string' ? Json.parse(raw) : (raw ?? null);
|
|
98
|
+
const settlement = resolveSettlement(this.settlement());
|
|
99
|
+
const due = isChannelState(state) ? settlementDue(state, settlement) : undefined;
|
|
100
|
+
if (!due) {
|
|
101
|
+
await this.storage.delete(settlementKey);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// The channel had a write since this alarm was set: wait until it has been idle long enough.
|
|
105
|
+
const staleAt = pending.updatedAt + settlement.staleAfterMs;
|
|
106
|
+
if (!pending.sends && due === 'stale' && !closing(state) && Date.now() < staleAt)
|
|
107
|
+
return this.storage.setAlarm(Math.min(staleAt, pendingCloseEnd(state) ?? Infinity));
|
|
108
|
+
// A close request the last send did not cover, such as one the server's close marker hid until
|
|
109
|
+
// it expired, restarts the count as a write would.
|
|
110
|
+
if (pending.sends &&
|
|
111
|
+
closing(state) &&
|
|
112
|
+
closeRequest(state) !== pending.closeRequestAtSend) {
|
|
113
|
+
pending = { ...pending, sends: 0 };
|
|
114
|
+
await this.storage.put(settlementKey, pending);
|
|
115
|
+
}
|
|
116
|
+
// An alarm set for something that no longer needs a send, such as the end of a close marker a
|
|
117
|
+
// failed close then cleared: wait for the watchdog rather than spend an attempt early.
|
|
118
|
+
// A threshold's worth of vouchers the last send did not capture, such as ones written while
|
|
119
|
+
// it was in flight, still goes at once.
|
|
120
|
+
const uncaptured = pending.capturedAtSend !== undefined &&
|
|
121
|
+
settleableAmount(state) - pending.capturedAtSend >=
|
|
122
|
+
threshold(state, settlement);
|
|
123
|
+
if (pending.sends &&
|
|
124
|
+
!uncaptured &&
|
|
125
|
+
pending.watchdogAt !== undefined &&
|
|
126
|
+
Date.now() < pending.watchdogAt)
|
|
127
|
+
return this.storage.setAlarm(Math.min(pending.watchdogAt, pendingCloseEnd(state) ?? Infinity));
|
|
128
|
+
await this.send(pending, state, settlement, this.storage);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Sends the channel to the settlement queue and sets the watchdog alarm, or gives up after
|
|
132
|
+
* `maxAttempts` sends. A send that fails leaves the count alone and tries again from an
|
|
133
|
+
* alarm, since the write that scheduled it has already committed.
|
|
134
|
+
*/
|
|
135
|
+
async send(pending, state, settlement, storage) {
|
|
136
|
+
const sends = pending.sends + 1;
|
|
137
|
+
if (sends > settlement.maxAttempts) {
|
|
138
|
+
// A live close marker may hide a payer close request merged into it: wake when it ends,
|
|
139
|
+
// when a close request restarts the count, rather than giving up for good now.
|
|
140
|
+
const markerEnd = pendingCloseEnd(state);
|
|
141
|
+
if (markerEnd !== undefined)
|
|
142
|
+
return wakeBy(storage, markerEnd);
|
|
143
|
+
await storage.delete(settlementKey);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// A missing binding is a configuration error: fail the alarm so it shows in the Worker's
|
|
147
|
+
// logs and Cloudflare retries it with backoff, rather than waking every minute in silence.
|
|
148
|
+
if (!settlement.queue)
|
|
149
|
+
throw new Error('ChannelStore: no settlement queue (bind MPPX_SETTLEMENT_QUEUE).');
|
|
150
|
+
try {
|
|
151
|
+
await settlement.queue.send({ channelId: pending.channelId });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Retry from the alarm without waiting for the last watchdog. Keep a sooner alarm that a
|
|
155
|
+
// write during the send set, such as for a close request.
|
|
156
|
+
const current = (await storage.get(settlementKey));
|
|
157
|
+
if (current?.key === pending.key && current.watchdogAt !== undefined)
|
|
158
|
+
await storage.put(settlementKey, { ...current, watchdogAt: undefined });
|
|
159
|
+
await wakeBy(storage, Date.now() + settlement.retryMs);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// The send awaited the queue, so a channel write may have changed the bookkeeping since
|
|
163
|
+
// (such as restarting the count after progress): count this send against what is there now.
|
|
164
|
+
const current = (await storage.get(settlementKey));
|
|
165
|
+
if (current?.key !== pending.key)
|
|
166
|
+
return;
|
|
167
|
+
const counted = current.sends + 1;
|
|
168
|
+
const watchdog = settlement.watchdogMs * 2 ** (counted - 1);
|
|
169
|
+
// Once the escrow's grace period ends, the payer can withdraw what is unsettled: resend a
|
|
170
|
+
// closing channel often enough that every attempt fits inside it.
|
|
171
|
+
const watchdogAt = Date.now() + (closing(state) ? Math.min(watchdog, closingWatchdogMs) : watchdog);
|
|
172
|
+
await storage.put(settlementKey, {
|
|
173
|
+
...current,
|
|
174
|
+
capturedAtSend: settleableAmount(state),
|
|
175
|
+
closeRequestAtSend: closeRequest(state),
|
|
176
|
+
sends: counted,
|
|
177
|
+
settledAtSend: state.settledOnChain,
|
|
178
|
+
watchdogAt,
|
|
179
|
+
});
|
|
180
|
+
// A write during the send (such as a close request) may have set a sooner alarm from a newer
|
|
181
|
+
// state: keep it.
|
|
182
|
+
await wakeBy(storage, Math.min(watchdogAt, pendingCloseEnd(state) ?? Infinity));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Longest wait before sending a closing channel again: its default six sends then span 10
|
|
187
|
+
* minutes, inside the escrow's 15-minute close grace period.
|
|
188
|
+
*/
|
|
189
|
+
const closingWatchdogMs = 2 * 60_000;
|
|
190
|
+
/** Fills in the defaults of a {@link ChannelStore.Settlement}. */
|
|
191
|
+
function resolveSettlement(settlement) {
|
|
192
|
+
const resolved = {
|
|
193
|
+
maxAttempts: settlement.maxAttempts ?? 6,
|
|
194
|
+
minUnsettledAmount: settlement.minUnsettledAmount ?? 100000n,
|
|
195
|
+
queue: settlement.queue,
|
|
196
|
+
retryMs: settlement.retryMs ?? 60_000,
|
|
197
|
+
staleAfterMs: settlement.staleAfterMs ?? 300_000,
|
|
198
|
+
watchdogMs: settlement.watchdogMs ?? 5 * 60_000,
|
|
199
|
+
};
|
|
200
|
+
// A non-finite delay would schedule no alarm at all, silently never settling.
|
|
201
|
+
for (const name of ['retryMs', 'staleAfterMs', 'watchdogMs'])
|
|
202
|
+
if (!(Number.isFinite(resolved[name]) && resolved[name] >= 0))
|
|
203
|
+
throw new Error(`ChannelStore: \`${name}\` must be a finite, non-negative number.`);
|
|
204
|
+
if (!(Number.isSafeInteger(resolved.maxAttempts) && resolved.maxAttempts > 0))
|
|
205
|
+
throw new Error('ChannelStore: `maxAttempts` must be a positive integer.');
|
|
206
|
+
return resolved;
|
|
207
|
+
}
|
|
208
|
+
/** Whether a stored value is a precompile-backed session channel state. */
|
|
209
|
+
export function isChannelState(value) {
|
|
210
|
+
if (typeof value !== 'object' || value === null)
|
|
211
|
+
return false;
|
|
212
|
+
const state = value;
|
|
213
|
+
return (state.backend === 'precompile' &&
|
|
214
|
+
typeof state.channelId === 'string' &&
|
|
215
|
+
typeof state.chainId === 'number' &&
|
|
216
|
+
typeof state.deposit === 'bigint' &&
|
|
217
|
+
typeof state.settledOnChain === 'bigint' &&
|
|
218
|
+
typeof state.spent === 'bigint');
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The raw amount a settlement of the channel would capture now: the largest accepted voucher
|
|
222
|
+
* within `spent` (as mppx's `settle` picks it) above `settledOnChain`.
|
|
223
|
+
*/
|
|
224
|
+
export function unsettledAmount(state) {
|
|
225
|
+
return settleableAmount(state) - state.settledOnChain;
|
|
226
|
+
}
|
|
227
|
+
/** The cumulative amount a settlement of the channel would bring it to: its settleable voucher's, or `settledOnChain`. */
|
|
228
|
+
function settleableAmount(state) {
|
|
229
|
+
return resolveSettleableVoucher(state)?.cumulativeAmount ?? state.settledOnChain;
|
|
230
|
+
}
|
|
231
|
+
/** Whether the payer requested a close, after which the escrow's grace period runs. */
|
|
232
|
+
function closing(state) {
|
|
233
|
+
return closeRequest(state) !== 0n;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The payer's close request: `closeRequestedAt`, or `0n` without one. The server's own
|
|
237
|
+
* cooperative-close marker ({@link State.pendingCloseAt}) is not one while that close may still
|
|
238
|
+
* land ({@link pendingClose}): it captures the vouchers itself. An on-chain close request merged
|
|
239
|
+
* into the marker keeps its timestamp when the payer's is not later, so once the marker can no
|
|
240
|
+
* longer land, it counts as one.
|
|
241
|
+
*/
|
|
242
|
+
function closeRequest(state) {
|
|
243
|
+
if (pendingClose(state))
|
|
244
|
+
return 0n;
|
|
245
|
+
return BigInt(state.closeRequestedAt ?? 0n);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Unix milliseconds at which the server's close marker stops counting as {@link pendingClose},
|
|
249
|
+
* or `undefined` when it does not count now.
|
|
250
|
+
*/
|
|
251
|
+
function pendingCloseEnd(state) {
|
|
252
|
+
if (!pendingClose(state))
|
|
253
|
+
return undefined;
|
|
254
|
+
return ((Number(state.closeRequestedAt) +
|
|
255
|
+
pendingCloseValiditySeconds +
|
|
256
|
+
pendingCloseClockSkewSeconds +
|
|
257
|
+
1) *
|
|
258
|
+
1000);
|
|
259
|
+
}
|
|
260
|
+
/** Sets the alarm to `at`, unless it already fires sooner. */
|
|
261
|
+
async function wakeBy(storage, at) {
|
|
262
|
+
if (!Number.isFinite(at))
|
|
263
|
+
return;
|
|
264
|
+
const alarm = await storage.getAlarm();
|
|
265
|
+
if (alarm === null || at < alarm)
|
|
266
|
+
await storage.setAlarm(at);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Whether the server's own cooperative close may still land: its marker stands in
|
|
270
|
+
* `closeRequestedAt` and its transaction has not expired. That close captures every voucher a
|
|
271
|
+
* settlement could, so settling meanwhile only spends gas.
|
|
272
|
+
*/
|
|
273
|
+
export function pendingClose(state, now = Date.now()) {
|
|
274
|
+
if (state.pendingCloseAt === undefined || state.finalized)
|
|
275
|
+
return false;
|
|
276
|
+
const requestedAt = BigInt(state.closeRequestedAt ?? 0n);
|
|
277
|
+
if (requestedAt === 0n || requestedAt !== BigInt(state.pendingCloseAt))
|
|
278
|
+
return false;
|
|
279
|
+
const expiresAt = Number(requestedAt) + pendingCloseValiditySeconds + pendingCloseClockSkewSeconds;
|
|
280
|
+
return Math.floor(now / 1000) <= expiresAt;
|
|
281
|
+
}
|
|
282
|
+
/** The unsettled amount that sends a channel at once: `max(10% of deposit, minUnsettledAmount)`. */
|
|
283
|
+
function threshold(state, settlement) {
|
|
284
|
+
const minimum = settlement.minUnsettledAmount ?? 100000n;
|
|
285
|
+
const tenth = state.deposit / 10n;
|
|
286
|
+
const amount = tenth > minimum ? tenth : minimum;
|
|
287
|
+
// At least 1: a zero threshold would count every write as a threshold's worth of new vouchers
|
|
288
|
+
// and restart the send count forever, past `maxAttempts`.
|
|
289
|
+
return amount > 0n ? amount : 1n;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* When the channel needs settling: `threshold` (now) once the unsettled amount reaches
|
|
293
|
+
* `max(10% of deposit, minUnsettledAmount)`, `stale` (once idle) below it, or
|
|
294
|
+
* `undefined` when it is finalized or has nothing to settle.
|
|
295
|
+
*/
|
|
296
|
+
export function settlementDue(state, settlement = {}) {
|
|
297
|
+
if (state.finalized)
|
|
298
|
+
return undefined;
|
|
299
|
+
const unsettled = unsettledAmount(state);
|
|
300
|
+
if (unsettled <= 0n)
|
|
301
|
+
return undefined;
|
|
302
|
+
return unsettled >= threshold(state, settlement) ? 'threshold' : 'stale';
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=ChannelStore.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type Address } from 'viem';
|
|
2
|
+
import type { MaybePromise } from '../internal/types.js';
|
|
3
|
+
import * as Store from '../Store.js';
|
|
4
|
+
import type { TransactionClient } from '../tempo/session/precompile/Chain.js';
|
|
5
|
+
import type { ResolveSessionChannelId } from '../tempo/session/server/RequestState.js';
|
|
6
|
+
import { settle as settleChannel, type SettlementTransactionOptions } from '../tempo/session/server/Settlement.js';
|
|
7
|
+
/**
|
|
8
|
+
* Tempo session channel storage on a {@link ChannelStore} Durable Object namespace, to spread
|
|
9
|
+
* into `tempo.session()`: `store` keeps each channel in its own object, which schedules its
|
|
10
|
+
* settlement, and `resolveChannelId` finds the payer's open channel to the same payee, token,
|
|
11
|
+
* escrow, operator and chain, so a client that lost its state resumes it instead of opening another.
|
|
12
|
+
* The lookup needs the payer's identity, which only a `bootstrap` HEAD proof supplies: enable
|
|
13
|
+
* `bootstrap` on the session and in the client's session manager, or it never resolves.
|
|
14
|
+
*
|
|
15
|
+
* The store keeps that lookup current: after a write opens, closes or finalizes a channel,
|
|
16
|
+
* it adds or removes the channel under its payer and authorized signer. Each lookup entry is
|
|
17
|
+
* its own key holding the payer's open channels there, most recent first, so closing one leaves
|
|
18
|
+
* the others findable. The lookup returns the first whose channel still resumes, so a delayed
|
|
19
|
+
* write for a channel that has since closed cannot hide an open one, and mppx validates any
|
|
20
|
+
* channel it resolves before reusing it.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { channels } from 'mppx/cloudflare'
|
|
25
|
+
* import { tempo } from 'mppx/server'
|
|
26
|
+
*
|
|
27
|
+
* tempo.session({ account, bootstrap: true, currency, getClient, ...channels(env.MPPX_CHANNELS) })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function channels(namespace: channels.Namespace, options?: channels.Options): channels.ReturnType;
|
|
31
|
+
export declare namespace channels {
|
|
32
|
+
/** The Durable Object namespace binding of a {@link ChannelStore} class. */
|
|
33
|
+
type Namespace = Store.durableObject.Namespace;
|
|
34
|
+
/** Options of {@link channels}: those of {@link Store.durableObject}, and the escrow. */
|
|
35
|
+
type Options = Store.durableObject.Options & {
|
|
36
|
+
/** Escrow the lookup assumes when a challenge names none. @default the TIP20EscrowChannel precompile */
|
|
37
|
+
escrowContract?: Address | undefined;
|
|
38
|
+
};
|
|
39
|
+
/** What `tempo.session()` takes from {@link channels}. */
|
|
40
|
+
type ReturnType = {
|
|
41
|
+
/** Finds the payer's open channel for a challenge's payee, token, escrow and chain. */
|
|
42
|
+
resolveChannelId: ResolveSessionChannelId;
|
|
43
|
+
/** Channel store that schedules settlement and keeps the channel lookup current. */
|
|
44
|
+
store: Store.AtomicStore;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Settles the channels of a settlement queue batch on-chain, from the Worker's `queue()`
|
|
49
|
+
* handler. Each message is acknowledged once its channel settled or has nothing left to
|
|
50
|
+
* settle, and when settlement cannot succeed (the channel is gone or has no voucher); any
|
|
51
|
+
* other failure, such as a fee over the fee payer's cap, retries it after 30 s, doubling up
|
|
52
|
+
* to 10 minutes. The {@link ChannelStore} watchdog sends a channel again if its job never settles it.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { channels, settle } from 'mppx/cloudflare'
|
|
57
|
+
*
|
|
58
|
+
* export default {
|
|
59
|
+
* async queue(batch, env) {
|
|
60
|
+
* await settle(batch, { account, getClient, store: channels(env.MPPX_CHANNELS).store })
|
|
61
|
+
* },
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function settle(batch: settle.Batch, parameters: settle.Parameters): Promise<void>;
|
|
66
|
+
export declare namespace settle {
|
|
67
|
+
/** Parameters of {@link settle}: the channel store, a client, and the settlement transaction's options. */
|
|
68
|
+
type Parameters = SettlementTransactionOptions & {
|
|
69
|
+
/** Returns an account-bearing client for the channel's chain. */
|
|
70
|
+
getClient: (parameters: {
|
|
71
|
+
chainId: number;
|
|
72
|
+
}) => MaybePromise<TransactionClient>;
|
|
73
|
+
/** Settles one channel. @default mppx's `Session.Server.settle` */
|
|
74
|
+
settle?: typeof settleChannel | undefined;
|
|
75
|
+
/** The channel store, such as {@link channels}' `store`. */
|
|
76
|
+
store: Store.AtomicStore;
|
|
77
|
+
};
|
|
78
|
+
/** The subset of a Cloudflare `MessageBatch` that {@link settle} uses. */
|
|
79
|
+
type Batch = {
|
|
80
|
+
readonly messages: readonly Message[];
|
|
81
|
+
};
|
|
82
|
+
/** The subset of a Cloudflare queue `Message` that {@link settle} uses. */
|
|
83
|
+
type Message = {
|
|
84
|
+
readonly attempts: number;
|
|
85
|
+
readonly body: unknown;
|
|
86
|
+
ack(): void;
|
|
87
|
+
retry(options?: {
|
|
88
|
+
delaySeconds?: number;
|
|
89
|
+
}): void;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=Channels.d.ts.map
|