@dvmkit/sdk 0.0.0 → 0.1.0-rc.2
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/NOTICE +2 -0
- package/README.md +38 -2
- package/dist/chunk-27V2ILSR.js +291 -0
- package/dist/chunk-365P52XQ.js +4121 -0
- package/dist/chunk-5GFED3GJ.js +955 -0
- package/dist/chunk-6JZIX5WW.js +1155 -0
- package/dist/chunk-7IH5SG2A.js +1038 -0
- package/dist/chunk-AT6V3SY7.js +102 -0
- package/dist/chunk-DCNT4PJS.js +733 -0
- package/dist/chunk-DMNLFNTW.js +135 -0
- package/dist/chunk-FROTD5XQ.js +70 -0
- package/dist/chunk-H25M54MI.js +149 -0
- package/dist/chunk-KQAJVVZT.js +712 -0
- package/dist/chunk-KXWROQGK.js +74 -0
- package/dist/chunk-L4OYF4DQ.js +67 -0
- package/dist/chunk-OJ5WFIB2.js +1266 -0
- package/dist/chunk-S3XAHZQY.js +63 -0
- package/dist/chunk-YG7G4DPZ.js +25 -0
- package/dist/credit-ledger-RO4FGSHG.js +28 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +303 -0
- package/dist/job-store-6gR4pZRP.d.ts +5350 -0
- package/dist/memory-credit-ledger-I2G64DDK.js +9 -0
- package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
- package/dist/mpp-setup-MOBWGTWJ.js +30 -0
- package/dist/payout-reporter-4TNWRS5F.js +753 -0
- package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
- package/dist/postgres-job-store-J5F4GUWU.js +7 -0
- package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
- package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
- package/dist/pricing-4CEB34RM.js +48 -0
- package/dist/processed-payment-store-HAA4SFNK.js +11 -0
- package/dist/revenue-reporter-GB4WKLDC.js +510 -0
- package/dist/server/index.d.ts +4168 -0
- package/dist/server/index.js +22716 -0
- package/dist/ssrf-DZi-xJyn.d.ts +325 -0
- package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
- package/dist/tempo-session-store-FTEEGZXA.js +467 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +151 -0
- package/dist/x402-35VLYFKZ.js +1272 -0
- package/package.json +89 -6
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
// src/sdk/server/mpp-setup.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { Errors as MppxErrors, PaymentRequest } from "mppx";
|
|
4
|
+
import { Mppx, tempo } from "mppx/server";
|
|
5
|
+
import { Session as TempoSession } from "mppx/tempo";
|
|
6
|
+
|
|
7
|
+
// src/sdk/server/tempo-settlement-readiness.ts
|
|
8
|
+
import { BaseError, InsufficientFundsError } from "viem";
|
|
9
|
+
var TEMPO_SETTLEMENT_DEFAULT_LOW_BALANCE_MICRO = 10000n;
|
|
10
|
+
var TEMPO_SETTLEMENT_BALANCE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
11
|
+
var TempoSettlementReadiness = class {
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.opts = opts;
|
|
14
|
+
this.lowBalanceMicro = opts.lowBalanceMicro ?? TEMPO_SETTLEMENT_DEFAULT_LOW_BALANCE_MICRO;
|
|
15
|
+
this.intervalMs = opts.balanceCheckIntervalMs ?? TEMPO_SETTLEMENT_BALANCE_INTERVAL_MS;
|
|
16
|
+
this.now = opts.now ?? Date.now;
|
|
17
|
+
this.failureStateKnown = !opts.stateStore;
|
|
18
|
+
if (this.lowBalanceMicro <= 0n) {
|
|
19
|
+
throw new Error("Tempo settlement lowBalanceMicro must be positive");
|
|
20
|
+
}
|
|
21
|
+
if (!Number.isFinite(this.intervalMs) || this.intervalMs <= 0) {
|
|
22
|
+
throw new Error("Tempo settlement balanceCheckIntervalMs must be positive");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
opts;
|
|
26
|
+
lowBalanceMicro;
|
|
27
|
+
intervalMs;
|
|
28
|
+
now;
|
|
29
|
+
listeners = /* @__PURE__ */ new Set();
|
|
30
|
+
timer;
|
|
31
|
+
inFlight;
|
|
32
|
+
lastBalanceMicro;
|
|
33
|
+
failureBalanceMicro;
|
|
34
|
+
failureVersion;
|
|
35
|
+
failureLatched = false;
|
|
36
|
+
failureStateKnown;
|
|
37
|
+
localFailureGeneration = 0;
|
|
38
|
+
pendingFailureGeneration = 0;
|
|
39
|
+
failurePersistence;
|
|
40
|
+
/** Whether new Tempo session channels may be offered at this instant. */
|
|
41
|
+
available() {
|
|
42
|
+
return !this.failureLatched && this.failureStateKnown && this.lastBalanceMicro !== void 0 && this.lastBalanceMicro >= this.lowBalanceMicro;
|
|
43
|
+
}
|
|
44
|
+
/** Refresh shared failure state and hold the first balance read before gating. */
|
|
45
|
+
async ensureFresh() {
|
|
46
|
+
if (this.lastBalanceMicro === void 0) {
|
|
47
|
+
await this.checkBalance().catch(() => void 0);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const state = await this.refreshFailureState().catch((error) => {
|
|
51
|
+
logStateFailure(error);
|
|
52
|
+
return void 0;
|
|
53
|
+
});
|
|
54
|
+
if (state?.latched && state.balanceMicro === void 0) {
|
|
55
|
+
await this.checkBalance().catch(() => void 0);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Add a reporting listener; the disposer removes only that listener. */
|
|
59
|
+
subscribe(listener) {
|
|
60
|
+
this.listeners.add(listener);
|
|
61
|
+
return () => this.listeners.delete(listener);
|
|
62
|
+
}
|
|
63
|
+
/** Start the boot reading and in-process cadence. Idempotent. */
|
|
64
|
+
start() {
|
|
65
|
+
if (this.timer) return;
|
|
66
|
+
void this.checkBalance().catch(logBalanceFailure);
|
|
67
|
+
this.timer = setInterval(
|
|
68
|
+
() => void this.checkBalance().catch(logBalanceFailure),
|
|
69
|
+
this.intervalMs
|
|
70
|
+
);
|
|
71
|
+
this.timer.unref();
|
|
72
|
+
}
|
|
73
|
+
/** Stop the cadence without affecting any in-flight reading. */
|
|
74
|
+
stop() {
|
|
75
|
+
if (this.timer) clearInterval(this.timer);
|
|
76
|
+
this.timer = void 0;
|
|
77
|
+
}
|
|
78
|
+
/** Read and report the configured fee-token balance now. */
|
|
79
|
+
async checkBalance(opts) {
|
|
80
|
+
if (this.inFlight) {
|
|
81
|
+
const current = await this.inFlight;
|
|
82
|
+
if (!opts?.successfulTransaction) return current;
|
|
83
|
+
}
|
|
84
|
+
const check = this.checkBalanceOnce(opts).finally(() => {
|
|
85
|
+
if (this.inFlight === check) this.inFlight = void 0;
|
|
86
|
+
});
|
|
87
|
+
this.inFlight = check;
|
|
88
|
+
return check;
|
|
89
|
+
}
|
|
90
|
+
/** Latch fleet-wide and report every redacted fee-funding rejection. */
|
|
91
|
+
async recordInsufficientFunds(failure) {
|
|
92
|
+
this.failureLatched = true;
|
|
93
|
+
this.failureStateKnown = true;
|
|
94
|
+
this.failureBalanceMicro = void 0;
|
|
95
|
+
this.failureVersion = void 0;
|
|
96
|
+
let sharedLatchWritten = false;
|
|
97
|
+
if (this.opts.stateStore) {
|
|
98
|
+
this.localFailureGeneration += 1;
|
|
99
|
+
this.pendingFailureGeneration = this.localFailureGeneration;
|
|
100
|
+
try {
|
|
101
|
+
await this.persistPendingFailure();
|
|
102
|
+
sharedLatchWritten = this.pendingFailureGeneration === 0;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logStateFailure(error);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
this.failureBalanceMicro ??= this.lastBalanceMicro;
|
|
108
|
+
}
|
|
109
|
+
const observation = {
|
|
110
|
+
address: this.opts.address,
|
|
111
|
+
network: this.opts.network,
|
|
112
|
+
token: this.opts.token,
|
|
113
|
+
operation: failure.operation,
|
|
114
|
+
errorClass: "InsufficientFundsError",
|
|
115
|
+
checkedAt: this.now(),
|
|
116
|
+
...this.lastBalanceMicro !== void 0 ? { balanceMicro: this.lastBalanceMicro } : {},
|
|
117
|
+
...failure.channelId ? { channelId: failure.channelId } : {},
|
|
118
|
+
...failure.creditId ? { creditId: failure.creditId } : {},
|
|
119
|
+
...failure.drainId ? { drainId: failure.drainId } : {}
|
|
120
|
+
};
|
|
121
|
+
console.warn(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
level: "warn",
|
|
124
|
+
event: "tempo_settlement_insufficient_funds",
|
|
125
|
+
address: observation.address,
|
|
126
|
+
network: observation.network,
|
|
127
|
+
token: observation.token,
|
|
128
|
+
operation: observation.operation,
|
|
129
|
+
error_class: observation.errorClass,
|
|
130
|
+
...observation.channelId ? { channel_id: observation.channelId } : {},
|
|
131
|
+
...observation.creditId ? { credit_id: observation.creditId } : {},
|
|
132
|
+
...observation.drainId ? { drain_id: observation.drainId } : {}
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
await this.notify((listener) => listener.onFailure?.(observation));
|
|
136
|
+
if (sharedLatchWritten && this.failureBalanceMicro === void 0) {
|
|
137
|
+
await this.captureFailureBalance().catch(logBalanceFailure);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Snapshot the failure version an operator-funded transaction may recover. */
|
|
141
|
+
async captureRecoveryVersion() {
|
|
142
|
+
const state = await this.refreshFailureState().catch((error) => {
|
|
143
|
+
logStateFailure(error);
|
|
144
|
+
return void 0;
|
|
145
|
+
});
|
|
146
|
+
return state?.latched ? state.version : void 0;
|
|
147
|
+
}
|
|
148
|
+
/** Re-check after a landed transaction and recover only its preflight version. */
|
|
149
|
+
async recordSuccessfulTransaction(recoveryVersion) {
|
|
150
|
+
await this.checkBalance({
|
|
151
|
+
successfulTransaction: true,
|
|
152
|
+
...recoveryVersion ? { recoveryVersion } : {}
|
|
153
|
+
}).catch(logBalanceFailure);
|
|
154
|
+
}
|
|
155
|
+
/** Exposed for timer-lifecycle assertions without reaching into Node internals. */
|
|
156
|
+
timerHasRef() {
|
|
157
|
+
return this.timer?.hasRef();
|
|
158
|
+
}
|
|
159
|
+
async checkBalanceOnce(opts) {
|
|
160
|
+
const state = await this.refreshFailureState().catch((error) => {
|
|
161
|
+
logStateFailure(error);
|
|
162
|
+
return this.currentFailureState();
|
|
163
|
+
});
|
|
164
|
+
const balanceMicro = await this.opts.readBalance();
|
|
165
|
+
if (this.opts.stateStore && state?.latched && state.version) {
|
|
166
|
+
if (opts?.successfulTransaction && opts.recoveryVersion && balanceMicro >= this.lowBalanceMicro) {
|
|
167
|
+
await this.opts.stateStore.clearFailureAfterSuccessfulTransaction(opts.recoveryVersion);
|
|
168
|
+
}
|
|
169
|
+
if (state.balanceMicro === void 0) {
|
|
170
|
+
await this.opts.stateStore.captureFailureBalance(state.version, balanceMicro);
|
|
171
|
+
} else {
|
|
172
|
+
await this.opts.stateStore.clearFailureIfRefilled(
|
|
173
|
+
state.version,
|
|
174
|
+
balanceMicro,
|
|
175
|
+
this.lowBalanceMicro
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
await this.refreshFailureState().catch(logStateFailure);
|
|
179
|
+
} else if (!this.opts.stateStore) {
|
|
180
|
+
const refilled = this.failureLatched && this.failureBalanceMicro !== void 0 && balanceMicro > this.failureBalanceMicro;
|
|
181
|
+
if (this.failureLatched && balanceMicro >= this.lowBalanceMicro && (opts?.successfulTransaction || refilled)) {
|
|
182
|
+
this.failureLatched = false;
|
|
183
|
+
this.failureBalanceMicro = void 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
this.lastBalanceMicro = balanceMicro;
|
|
187
|
+
return this.reportBalance(balanceMicro);
|
|
188
|
+
}
|
|
189
|
+
async captureFailureBalance() {
|
|
190
|
+
if (!this.opts.stateStore || !this.failureVersion) return;
|
|
191
|
+
const version = this.failureVersion;
|
|
192
|
+
const balanceMicro = await this.opts.readBalance();
|
|
193
|
+
await this.opts.stateStore.captureFailureBalance(version, balanceMicro);
|
|
194
|
+
await this.refreshFailureState();
|
|
195
|
+
this.lastBalanceMicro = balanceMicro;
|
|
196
|
+
await this.reportBalance(balanceMicro);
|
|
197
|
+
}
|
|
198
|
+
async refreshFailureState() {
|
|
199
|
+
if (!this.opts.stateStore) return void 0;
|
|
200
|
+
try {
|
|
201
|
+
await this.persistPendingFailure();
|
|
202
|
+
const generation = this.localFailureGeneration;
|
|
203
|
+
const state = await this.opts.stateStore.readFailure();
|
|
204
|
+
if (generation !== this.localFailureGeneration || this.pendingFailureGeneration !== 0) {
|
|
205
|
+
return this.currentFailureState();
|
|
206
|
+
}
|
|
207
|
+
this.applyFailureState(state);
|
|
208
|
+
return state;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (this.pendingFailureGeneration !== 0) {
|
|
211
|
+
this.failureLatched = true;
|
|
212
|
+
this.failureStateKnown = true;
|
|
213
|
+
} else {
|
|
214
|
+
this.failureStateKnown = false;
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async persistPendingFailure() {
|
|
220
|
+
if (!this.opts.stateStore) return;
|
|
221
|
+
while (this.pendingFailureGeneration !== 0) {
|
|
222
|
+
if (this.failurePersistence) {
|
|
223
|
+
await this.failurePersistence;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const generation = this.pendingFailureGeneration;
|
|
227
|
+
const persistence = this.opts.stateStore.latchFailure().then((state) => {
|
|
228
|
+
if (this.pendingFailureGeneration !== generation) return;
|
|
229
|
+
this.pendingFailureGeneration = 0;
|
|
230
|
+
this.applyFailureState(state);
|
|
231
|
+
});
|
|
232
|
+
this.failurePersistence = persistence;
|
|
233
|
+
try {
|
|
234
|
+
await persistence;
|
|
235
|
+
} finally {
|
|
236
|
+
if (this.failurePersistence === persistence) this.failurePersistence = void 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
applyFailureState(state) {
|
|
241
|
+
this.failureLatched = state.latched;
|
|
242
|
+
this.failureVersion = state.latched ? state.version : void 0;
|
|
243
|
+
this.failureBalanceMicro = state.latched ? state.balanceMicro : void 0;
|
|
244
|
+
this.failureStateKnown = true;
|
|
245
|
+
}
|
|
246
|
+
currentFailureState() {
|
|
247
|
+
return {
|
|
248
|
+
latched: this.failureLatched,
|
|
249
|
+
...this.failureVersion ? { version: this.failureVersion } : {},
|
|
250
|
+
...this.failureBalanceMicro !== void 0 ? { balanceMicro: this.failureBalanceMicro } : {}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async reportBalance(balanceMicro) {
|
|
254
|
+
const observation = {
|
|
255
|
+
address: this.opts.address,
|
|
256
|
+
network: this.opts.network,
|
|
257
|
+
token: this.opts.token,
|
|
258
|
+
balanceMicro,
|
|
259
|
+
lowBalanceMicro: this.lowBalanceMicro,
|
|
260
|
+
checkedAt: this.now(),
|
|
261
|
+
ready: this.available()
|
|
262
|
+
};
|
|
263
|
+
console.info(
|
|
264
|
+
JSON.stringify({
|
|
265
|
+
level: "info",
|
|
266
|
+
event: "tempo_settlement_fee_balance",
|
|
267
|
+
address: observation.address,
|
|
268
|
+
network: observation.network,
|
|
269
|
+
token: observation.token,
|
|
270
|
+
balance_micro: observation.balanceMicro.toString(),
|
|
271
|
+
low_balance_micro: observation.lowBalanceMicro.toString(),
|
|
272
|
+
ready: observation.ready
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
await this.notify((listener) => listener.onBalance?.(observation));
|
|
276
|
+
return observation;
|
|
277
|
+
}
|
|
278
|
+
async notify(invoke) {
|
|
279
|
+
await Promise.all(
|
|
280
|
+
[...this.listeners].map(
|
|
281
|
+
(listener) => Promise.resolve(invoke(listener)).catch(() => void 0)
|
|
282
|
+
)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
function isTempoInsufficientFundsError(error) {
|
|
287
|
+
if (error instanceof InsufficientFundsError) return true;
|
|
288
|
+
return error instanceof BaseError && error.walk((cause) => cause instanceof InsufficientFundsError) !== null;
|
|
289
|
+
}
|
|
290
|
+
function logBalanceFailure(error) {
|
|
291
|
+
console.warn(
|
|
292
|
+
JSON.stringify({
|
|
293
|
+
level: "warn",
|
|
294
|
+
event: "tempo_settlement_fee_balance_failed",
|
|
295
|
+
error_name: error instanceof Error ? error.name : "unknown"
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
function logStateFailure(error) {
|
|
300
|
+
console.warn(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
level: "warn",
|
|
303
|
+
event: "tempo_settlement_readiness_state_failed",
|
|
304
|
+
error_name: error instanceof Error ? error.name : "unknown"
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/sdk/server/mpp-setup.ts
|
|
310
|
+
var MPPX_HMAC_MISMATCH_REASON = "challenge was not issued by this server";
|
|
311
|
+
var TEMPO_SETTLEMENT_LEASE_FIELD = "settlementLeaseUntil";
|
|
312
|
+
var TEMPO_SETTLEMENT_LEASE_OWNER_FIELD = "settlementLeaseOwner";
|
|
313
|
+
var TEMPO_SETTLEMENT_LEASE_MS = 3e5;
|
|
314
|
+
var TEMPO_SETTLEMENT_IN_FLIGHT = "settlement already in flight";
|
|
315
|
+
var TEMPO_USDC_MAINNET = "0x20C000000000000000000000b9537d11c60E8b50";
|
|
316
|
+
var HEX_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
317
|
+
function challengeMeta(challenge) {
|
|
318
|
+
if (challenge.opaque) {
|
|
319
|
+
try {
|
|
320
|
+
return PaymentRequest.deserialize(challenge.opaque);
|
|
321
|
+
} catch {
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return challenge.meta;
|
|
326
|
+
}
|
|
327
|
+
function createMppFromOpts(opts = {}, env = process.env) {
|
|
328
|
+
const built = buildMppMethods(opts, env);
|
|
329
|
+
if (!built) return void 0;
|
|
330
|
+
const secretKey = env.DVMKIT_TEMPO_SECRET_KEY;
|
|
331
|
+
if (!secretKey) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
"Tempo requires DVMKIT_TEMPO_SECRET_KEY env var. Generate one with 'openssl rand -hex 32'."
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return Object.assign(
|
|
337
|
+
attachTempoSessionRuntime(
|
|
338
|
+
wrapMppx(
|
|
339
|
+
Mppx.create({
|
|
340
|
+
methods: built.methods,
|
|
341
|
+
secretKey,
|
|
342
|
+
...opts.realm && { realm: opts.realm }
|
|
343
|
+
})
|
|
344
|
+
),
|
|
345
|
+
opts.tempoSession
|
|
346
|
+
),
|
|
347
|
+
{ tempoConfiguration: built.tempoConfiguration }
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
function createDualKeyMppxFromOpts(opts, env, stateLoader) {
|
|
351
|
+
const built = buildMppMethods(opts, env);
|
|
352
|
+
if (!built) return void 0;
|
|
353
|
+
const activeSecret = env.DVMKIT_TEMPO_SECRET_KEY;
|
|
354
|
+
if (!activeSecret) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
"Tempo requires DVMKIT_TEMPO_SECRET_KEY env var. Generate one with 'openssl rand -hex 32'."
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const activeMppx = Object.assign(
|
|
360
|
+
attachTempoSessionRuntime(
|
|
361
|
+
wrapMppx(
|
|
362
|
+
Mppx.create({
|
|
363
|
+
methods: built.methods,
|
|
364
|
+
secretKey: activeSecret,
|
|
365
|
+
...opts.realm && { realm: opts.realm }
|
|
366
|
+
})
|
|
367
|
+
),
|
|
368
|
+
opts.tempoSession
|
|
369
|
+
),
|
|
370
|
+
{ tempoConfiguration: built.tempoConfiguration }
|
|
371
|
+
);
|
|
372
|
+
const activeVerify = activeMppx.broadcastCredential.bind(activeMppx);
|
|
373
|
+
const verifyWithDualKey = async (credential, verifyOpts) => {
|
|
374
|
+
try {
|
|
375
|
+
return await activeVerify(credential, verifyOpts);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (!isHmacMismatch(err)) throw err;
|
|
378
|
+
const state = await stateLoader();
|
|
379
|
+
const previous = state?.previous;
|
|
380
|
+
if (!previous) {
|
|
381
|
+
if (state?.rapidRotationAtMs != null) {
|
|
382
|
+
throw decorateRapidRotation(err, state.rapidRotationAtMs);
|
|
383
|
+
}
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
const previousMppx = wrapMppx(
|
|
387
|
+
Mppx.create({
|
|
388
|
+
methods: built.methods,
|
|
389
|
+
secretKey: previous.secret,
|
|
390
|
+
...opts.realm && { realm: opts.realm }
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
try {
|
|
394
|
+
return await previousMppx.broadcastCredential(credential, verifyOpts);
|
|
395
|
+
} catch (retryErr) {
|
|
396
|
+
if (state.rapidRotationAtMs != null) {
|
|
397
|
+
throw decorateRapidRotation(retryErr, state.rapidRotationAtMs);
|
|
398
|
+
}
|
|
399
|
+
throw retryErr;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
return Object.assign(activeMppx, {
|
|
404
|
+
broadcastCredential: verifyWithDualKey,
|
|
405
|
+
verifyCredential: verifyWithDualKey
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function buildMppMethods(opts, env) {
|
|
409
|
+
if (!opts.tempoRecipient) return void 0;
|
|
410
|
+
const allowlist = opts.methodsAllowlist;
|
|
411
|
+
const isAllowed = (name) => !allowlist || allowlist.length === 0 || allowlist.includes(name);
|
|
412
|
+
const methods = [];
|
|
413
|
+
let tempoConfiguration;
|
|
414
|
+
if (isAllowed("tempo")) {
|
|
415
|
+
if (!HEX_ADDRESS_RE.test(opts.tempoRecipient)) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`DVMKIT_TEMPO_RECIPIENT must be a 0x-prefixed 40-char hex address; got '${opts.tempoRecipient}'.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const tempoCurrency = env.DVMKIT_TEMPO_CURRENCY ?? TEMPO_USDC_MAINNET;
|
|
421
|
+
if (!HEX_ADDRESS_RE.test(tempoCurrency)) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`DVMKIT_TEMPO_CURRENCY must be a 0x-prefixed 40-char hex address; got '${tempoCurrency}'.`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
tempoConfiguration = {
|
|
427
|
+
recipient: opts.tempoRecipient,
|
|
428
|
+
paymentToken: tempoCurrency,
|
|
429
|
+
...opts.tempoSession && {
|
|
430
|
+
operator: opts.tempoSession.account.address,
|
|
431
|
+
feeToken: tempoCurrency
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
methods.push(
|
|
435
|
+
tempo.charge({
|
|
436
|
+
recipient: opts.tempoRecipient,
|
|
437
|
+
currency: tempoCurrency,
|
|
438
|
+
decimals: 6,
|
|
439
|
+
// Without this mppx installs `Store.memory()`, so the consumed-hash
|
|
440
|
+
// guard is per-process and a credential replayed onto a sibling machine
|
|
441
|
+
// (or after a restart) reaches a broadcast the chain then rejects
|
|
442
|
+
// instead of a cheap refusal (internal-review).
|
|
443
|
+
...opts.tempoCharge && { store: opts.tempoCharge.store }
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
if (opts.tempoSession) {
|
|
447
|
+
methods.push(
|
|
448
|
+
tempo.session({
|
|
449
|
+
recipient: opts.tempoRecipient,
|
|
450
|
+
account: opts.tempoSession.account,
|
|
451
|
+
operator: opts.tempoSession.account.address,
|
|
452
|
+
currency: tempoCurrency,
|
|
453
|
+
// A server-driven settle/close must spend fees from the same TIP-20
|
|
454
|
+
// token the channel carries. Leaving this implicit lets an account's
|
|
455
|
+
// stale fee preference win even when that token has too little left
|
|
456
|
+
// to broadcast, while the channel token is funded (internal-review).
|
|
457
|
+
feeToken: tempoCurrency,
|
|
458
|
+
decimals: 6,
|
|
459
|
+
unitType: "request",
|
|
460
|
+
store: opts.tempoSession.store,
|
|
461
|
+
channelStateTtl: 5e3,
|
|
462
|
+
// dvmkit's watcher owns scheduled settlement because it gates on
|
|
463
|
+
// terminal ledger consumption. mppx's in-request scheduler settles
|
|
464
|
+
// the full accepted voucher and can capture caller-owned balance.
|
|
465
|
+
...opts.tempoSession.chainId !== void 0 && {
|
|
466
|
+
chainId: opts.tempoSession.chainId
|
|
467
|
+
},
|
|
468
|
+
...opts.tempoSession.getClient && { getClient: opts.tempoSession.getClient },
|
|
469
|
+
...opts.tempoSession.onSessionSettlement && {
|
|
470
|
+
onSessionSettlement: opts.tempoSession.onSessionSettlement
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (methods.length === 0 || !tempoConfiguration) return void 0;
|
|
477
|
+
return { methods, tempoConfiguration };
|
|
478
|
+
}
|
|
479
|
+
function isHmacMismatch(err) {
|
|
480
|
+
if (!(err instanceof MppxErrors.InvalidChallengeError)) return false;
|
|
481
|
+
return typeof err.message === "string" && err.message.includes(MPPX_HMAC_MISMATCH_REASON);
|
|
482
|
+
}
|
|
483
|
+
function decorateRapidRotation(original, rapidRotationAtMs) {
|
|
484
|
+
const base = original instanceof Error ? original : new Error(String(original));
|
|
485
|
+
const decorated = new Error(
|
|
486
|
+
`${base.message} (rapid_rotation=true; last_rotation_at_ms=${rapidRotationAtMs})`
|
|
487
|
+
);
|
|
488
|
+
decorated.name = base.name;
|
|
489
|
+
decorated.stack = base.stack;
|
|
490
|
+
return decorated;
|
|
491
|
+
}
|
|
492
|
+
function wrapMppx(mppx) {
|
|
493
|
+
const dispatcher = mppx.challenge;
|
|
494
|
+
const issueChallenge = (method, intent, o) => {
|
|
495
|
+
const handler = dispatcher[method][intent];
|
|
496
|
+
if (!handler) {
|
|
497
|
+
throw new Error(`Mppx: no challenge handler registered for ${method}/${intent}`);
|
|
498
|
+
}
|
|
499
|
+
return handler(o);
|
|
500
|
+
};
|
|
501
|
+
return Object.assign(mppx, { issueChallenge });
|
|
502
|
+
}
|
|
503
|
+
var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
|
|
504
|
+
getChannelStatesBatch: TempoSession.Precompile.Chain.getChannelStatesBatch,
|
|
505
|
+
settle: TempoSession.Server.settle,
|
|
506
|
+
readChannelClose: async (client, channelId, txHash) => TempoSession.Precompile.Chain.readChannelClosedReceiptFields(
|
|
507
|
+
TempoSession.Precompile.Chain.getChannelEvent(
|
|
508
|
+
await TempoSession.Precompile.Chain.waitForSuccessfulReceipt(client, txHash),
|
|
509
|
+
"ChannelClosed",
|
|
510
|
+
channelId
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
};
|
|
514
|
+
function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
515
|
+
let creditLedger;
|
|
516
|
+
Object.assign(mppx, {
|
|
517
|
+
useTempoCreditLedger: (ledger) => {
|
|
518
|
+
creditLedger = ledger;
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
const chainOps = {
|
|
522
|
+
...DEFAULT_TEMPO_SESSION_CHAIN_OPS,
|
|
523
|
+
...chainOpOverrides
|
|
524
|
+
};
|
|
525
|
+
const store = config?.store;
|
|
526
|
+
if (store?.withVoucherAcceptance) {
|
|
527
|
+
Object.assign(mppx, {
|
|
528
|
+
withTempoSessionAcceptance: store.withVoucherAcceptance.bind(store)
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (store) {
|
|
532
|
+
Object.assign(mppx, {
|
|
533
|
+
getTempoSessionSpent: async (channelId) => {
|
|
534
|
+
const state = readTempoSessionState(await store.get(channelId.toLowerCase()));
|
|
535
|
+
if (!state) throw new Error(`Tempo session channel ${channelId} not found`);
|
|
536
|
+
return state.spent;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const getClient = config?.getClient;
|
|
541
|
+
const listActive = store?.listActive;
|
|
542
|
+
if (!config || !getClient || !store) return mppx;
|
|
543
|
+
const terminalizeLostBacking = async (channel, chainState) => {
|
|
544
|
+
if (chainState.deposit !== 0n) return;
|
|
545
|
+
if (!creditLedger) return;
|
|
546
|
+
const ledger = creditLedger;
|
|
547
|
+
const retire = async (lifecycle, tx) => {
|
|
548
|
+
const settledOnChain = maxBigInt(lifecycle.settledOnChain, chainState.settled);
|
|
549
|
+
if (lifecycle.highestVoucherAmount <= settledOnChain) return;
|
|
550
|
+
await ledger.terminalizeTempoCredits(
|
|
551
|
+
{
|
|
552
|
+
channelId: channel.channelId,
|
|
553
|
+
settledOnChainNative: settledOnChain,
|
|
554
|
+
highestVoucherNative: lifecycle.highestVoucherAmount,
|
|
555
|
+
consumedNative: lifecycle.spent
|
|
556
|
+
},
|
|
557
|
+
tx
|
|
558
|
+
);
|
|
559
|
+
};
|
|
560
|
+
if (store.withFinalizedCreditLoss) {
|
|
561
|
+
await store.withFinalizedCreditLoss(
|
|
562
|
+
channel.channelId,
|
|
563
|
+
(tx, lifecycle) => retire(lifecycle, tx)
|
|
564
|
+
);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
await retire(channel);
|
|
568
|
+
};
|
|
569
|
+
const getTempoSessionDrainState = async (channelId) => {
|
|
570
|
+
const normalized = channelId.toLowerCase();
|
|
571
|
+
const current = readTempoSessionState(await store.get(normalized));
|
|
572
|
+
if (!current) throw new Error(`Tempo session channel ${channelId} not found`);
|
|
573
|
+
const client = await getClient({ chainId: current.chainId });
|
|
574
|
+
const [chainState] = await chainOps.getChannelStatesBatch(
|
|
575
|
+
client,
|
|
576
|
+
[current.channelId],
|
|
577
|
+
current.escrowContract
|
|
578
|
+
);
|
|
579
|
+
await terminalizeLostBacking(current, chainState);
|
|
580
|
+
const chainFinalized = chainState.deposit === 0n;
|
|
581
|
+
await store.update(normalized, (value) => {
|
|
582
|
+
const active = readTempoSessionState(value);
|
|
583
|
+
if (!active) return { op: "noop", result: void 0 };
|
|
584
|
+
return {
|
|
585
|
+
op: "set",
|
|
586
|
+
value: {
|
|
587
|
+
...value,
|
|
588
|
+
finalized: active.finalized || chainFinalized,
|
|
589
|
+
closeRequestedAt: chainFinalized ? 0n : BigInt(chainState.closeRequestedAt),
|
|
590
|
+
deposit: chainFinalized ? 0n : maxBigInt(active.deposit, chainState.deposit),
|
|
591
|
+
settledOnChain: maxBigInt(active.settledOnChain, chainState.settled)
|
|
592
|
+
},
|
|
593
|
+
result: void 0
|
|
594
|
+
};
|
|
595
|
+
});
|
|
596
|
+
const reconciled = readTempoSessionState(await store.get(normalized));
|
|
597
|
+
if (!reconciled) throw new Error(`Tempo session channel ${channelId} disappeared`);
|
|
598
|
+
return {
|
|
599
|
+
spent: reconciled.spent,
|
|
600
|
+
settledOnChain: reconciled.settledOnChain,
|
|
601
|
+
closeRequestedAt: reconciled.closeRequestedAt,
|
|
602
|
+
deposit: reconciled.deposit,
|
|
603
|
+
finalized: reconciled.finalized
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
Object.assign(mppx, { getTempoSessionDrainState });
|
|
607
|
+
const writeBackChainState = async (channel, chainState) => {
|
|
608
|
+
const chainFinalized = chainState.deposit === 0n;
|
|
609
|
+
await store.update(channel.channelId, (current) => {
|
|
610
|
+
const active = readTempoSessionState(current);
|
|
611
|
+
if (!active) return { op: "noop", result: void 0 };
|
|
612
|
+
return {
|
|
613
|
+
op: "set",
|
|
614
|
+
value: {
|
|
615
|
+
...current,
|
|
616
|
+
finalized: active.finalized || chainFinalized,
|
|
617
|
+
closeRequestedAt: chainFinalized ? 0n : BigInt(chainState.closeRequestedAt),
|
|
618
|
+
deposit: chainFinalized ? 0n : maxBigInt(active.deposit, chainState.deposit),
|
|
619
|
+
settledOnChain: maxBigInt(active.settledOnChain, chainState.settled)
|
|
620
|
+
},
|
|
621
|
+
result: void 0
|
|
622
|
+
};
|
|
623
|
+
});
|
|
624
|
+
return chainFinalized;
|
|
625
|
+
};
|
|
626
|
+
const settlingHere = /* @__PURE__ */ new Set();
|
|
627
|
+
const leaseMs = config.settlementSchedule?.leaseMs ?? TEMPO_SETTLEMENT_LEASE_MS;
|
|
628
|
+
const claimSettlement = async (channelId) => {
|
|
629
|
+
if (settlingHere.has(channelId)) return false;
|
|
630
|
+
settlingHere.add(channelId);
|
|
631
|
+
const leaseOwner = randomUUID();
|
|
632
|
+
const leaseUntil = Date.now() + leaseMs;
|
|
633
|
+
try {
|
|
634
|
+
const claimed = await store.update(channelId, (current) => {
|
|
635
|
+
if (!isStateRecord(current)) return { op: "noop", result: false };
|
|
636
|
+
const until = current[TEMPO_SETTLEMENT_LEASE_FIELD];
|
|
637
|
+
const owner = current[TEMPO_SETTLEMENT_LEASE_OWNER_FIELD];
|
|
638
|
+
if (typeof until === "number" && until > Date.now() && owner !== leaseOwner) {
|
|
639
|
+
return { op: "noop", result: false };
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
op: "set",
|
|
643
|
+
value: {
|
|
644
|
+
...current,
|
|
645
|
+
[TEMPO_SETTLEMENT_LEASE_FIELD]: leaseUntil,
|
|
646
|
+
[TEMPO_SETTLEMENT_LEASE_OWNER_FIELD]: leaseOwner
|
|
647
|
+
},
|
|
648
|
+
result: true
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
if (!claimed) settlingHere.delete(channelId);
|
|
652
|
+
return claimed;
|
|
653
|
+
} catch (err) {
|
|
654
|
+
settlingHere.delete(channelId);
|
|
655
|
+
throw err;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const releaseSettlement = async (channelId) => {
|
|
659
|
+
try {
|
|
660
|
+
await store.update(channelId, (current) => {
|
|
661
|
+
if (!isStateRecord(current)) return { op: "noop", result: void 0 };
|
|
662
|
+
const next = Object.fromEntries(
|
|
663
|
+
Object.entries(current).filter(
|
|
664
|
+
([field]) => field !== TEMPO_SETTLEMENT_LEASE_FIELD && field !== TEMPO_SETTLEMENT_LEASE_OWNER_FIELD
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
return { op: "set", value: next, result: void 0 };
|
|
668
|
+
});
|
|
669
|
+
} catch {
|
|
670
|
+
} finally {
|
|
671
|
+
settlingHere.delete(channelId);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
const settleChannel = async (channel, client) => {
|
|
675
|
+
if (!await claimSettlement(channel.channelId)) return false;
|
|
676
|
+
try {
|
|
677
|
+
const recoveryVersion = await config.beforeSessionSettlement?.();
|
|
678
|
+
await chainOps.settle(store, client, channel.channelId, {
|
|
679
|
+
account: config.account,
|
|
680
|
+
escrowContract: channel.escrowContract,
|
|
681
|
+
// The session method pins the channel token for credential-driven
|
|
682
|
+
// cooperative closes. This explicit observer/sweep path calls mppx's
|
|
683
|
+
// public settle helper directly, so it must carry the same fee-token
|
|
684
|
+
// choice itself. Leaving it implicit makes Tempo prepare the operator
|
|
685
|
+
// transaction without a spendable fee token and fail before broadcast.
|
|
686
|
+
feeToken: channel.token,
|
|
687
|
+
...config.onSessionSettlement && {
|
|
688
|
+
onSessionSettlement: (context) => config.onSessionSettlement?.({
|
|
689
|
+
...context,
|
|
690
|
+
...recoveryVersion ? { recoveryVersion } : {}
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
} catch (err) {
|
|
695
|
+
settlingHere.delete(channel.channelId);
|
|
696
|
+
if (isTempoInsufficientFundsError(err)) {
|
|
697
|
+
await config.onInsufficientFunds?.({
|
|
698
|
+
operation: "settle",
|
|
699
|
+
channelId: channel.channelId
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
throw err;
|
|
703
|
+
}
|
|
704
|
+
await releaseSettlement(channel.channelId);
|
|
705
|
+
return true;
|
|
706
|
+
};
|
|
707
|
+
const settleTempoSessionChannel = async (channelId) => {
|
|
708
|
+
const normalized = channelId.toLowerCase();
|
|
709
|
+
const channel = readTempoSessionState(await store.get(normalized));
|
|
710
|
+
if (!channel) return { outcome: "unknown_channel", channelId: normalized };
|
|
711
|
+
try {
|
|
712
|
+
const client = await getClient({ chainId: channel.chainId });
|
|
713
|
+
const [chainState] = await chainOps.getChannelStatesBatch(
|
|
714
|
+
client,
|
|
715
|
+
[channel.channelId],
|
|
716
|
+
channel.escrowContract
|
|
717
|
+
);
|
|
718
|
+
await terminalizeLostBacking(channel, chainState);
|
|
719
|
+
const chainFinalized = await writeBackChainState(channel, chainState);
|
|
720
|
+
const settledOnChain = maxBigInt(channel.settledOnChain, chainState.settled);
|
|
721
|
+
if (chainFinalized) {
|
|
722
|
+
const owed = channel.highestVoucherAmount - settledOnChain;
|
|
723
|
+
return owed > 0n ? {
|
|
724
|
+
outcome: "finalized_before_protection",
|
|
725
|
+
channelId: normalized,
|
|
726
|
+
owed,
|
|
727
|
+
settledOnChain
|
|
728
|
+
} : { outcome: "already_safe", channelId: normalized, settledOnChain };
|
|
729
|
+
}
|
|
730
|
+
const forcedClose = BigInt(chainState.closeRequestedAt) !== 0n;
|
|
731
|
+
if (!forcedClose) {
|
|
732
|
+
return {
|
|
733
|
+
outcome: "retryable_failure",
|
|
734
|
+
channelId: normalized,
|
|
735
|
+
error: "chain reports no close request yet"
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (!tempoSettlementDue({
|
|
739
|
+
channel,
|
|
740
|
+
chainSettled: chainState.settled,
|
|
741
|
+
forcedClose,
|
|
742
|
+
intervalMs: config.settlementSchedule?.intervalMs
|
|
743
|
+
})) {
|
|
744
|
+
return { outcome: "already_safe", channelId: normalized, settledOnChain };
|
|
745
|
+
}
|
|
746
|
+
if (!await settleChannel(channel, client)) {
|
|
747
|
+
return {
|
|
748
|
+
outcome: "retryable_failure",
|
|
749
|
+
channelId: normalized,
|
|
750
|
+
error: TEMPO_SETTLEMENT_IN_FLIGHT
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
outcome: "settled",
|
|
755
|
+
channelId: normalized,
|
|
756
|
+
settledAmount: channel.highestVoucherAmount
|
|
757
|
+
};
|
|
758
|
+
} catch (err) {
|
|
759
|
+
return {
|
|
760
|
+
outcome: "retryable_failure",
|
|
761
|
+
channelId: normalized,
|
|
762
|
+
error: err instanceof Error ? err.message : String(err)
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
const getTempoSessionBinding = async (channelId) => {
|
|
767
|
+
const stored = await store.get(channelId.toLowerCase());
|
|
768
|
+
const channel = readTempoSessionState(stored);
|
|
769
|
+
if (!channel) return null;
|
|
770
|
+
const payee = stored?.payee;
|
|
771
|
+
return {
|
|
772
|
+
chainId: channel.chainId,
|
|
773
|
+
escrowContract: channel.escrowContract,
|
|
774
|
+
...typeof payee === "string" && { payee }
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
const getTempoReceipt = async ({
|
|
778
|
+
chainId,
|
|
779
|
+
txHash
|
|
780
|
+
}) => {
|
|
781
|
+
const { getTransactionReceipt } = await import("viem/actions");
|
|
782
|
+
const client = await getClient({ chainId });
|
|
783
|
+
return getTransactionReceipt(client, { hash: txHash });
|
|
784
|
+
};
|
|
785
|
+
const getTempoSessionCloseAmounts = async ({ channelId, txHash }) => {
|
|
786
|
+
const current = readTempoSessionState(await store.get(channelId.toLowerCase()));
|
|
787
|
+
if (!current) return null;
|
|
788
|
+
const client = await getClient({ chainId: current.chainId });
|
|
789
|
+
return chainOps.readChannelClose(client, current.channelId, txHash);
|
|
790
|
+
};
|
|
791
|
+
Object.assign(mppx, {
|
|
792
|
+
settleTempoSessionChannel,
|
|
793
|
+
getTempoSessionBinding,
|
|
794
|
+
getTempoReceipt,
|
|
795
|
+
getTempoSessionCloseAmounts
|
|
796
|
+
});
|
|
797
|
+
if (!listActive) return mppx;
|
|
798
|
+
const sweepTempoSessions = async () => {
|
|
799
|
+
const rows = [];
|
|
800
|
+
let cursor;
|
|
801
|
+
do {
|
|
802
|
+
const page = await listActive.call(store, 100, cursor);
|
|
803
|
+
rows.push(...page);
|
|
804
|
+
const last = page.at(-1);
|
|
805
|
+
cursor = last ? { updatedAt: last.updatedAt, key: last.key } : void 0;
|
|
806
|
+
if (page.length < 100) break;
|
|
807
|
+
} while (cursor);
|
|
808
|
+
let closeRequested = 0;
|
|
809
|
+
let settled = 0;
|
|
810
|
+
let inFlight = 0;
|
|
811
|
+
const groups = /* @__PURE__ */ new Map();
|
|
812
|
+
for (const row of rows) {
|
|
813
|
+
const state = readTempoSessionState(row.state);
|
|
814
|
+
if (!state) continue;
|
|
815
|
+
const groupKey = `${state.chainId}:${state.escrowContract.toLowerCase()}`;
|
|
816
|
+
const group = groups.get(groupKey) ?? [];
|
|
817
|
+
group.push(state);
|
|
818
|
+
groups.set(groupKey, group);
|
|
819
|
+
}
|
|
820
|
+
for (const groupedChannels of groups.values()) {
|
|
821
|
+
for (let offset = 0; offset < groupedChannels.length; offset += 100) {
|
|
822
|
+
const channels = groupedChannels.slice(offset, offset + 100);
|
|
823
|
+
const first = channels[0];
|
|
824
|
+
const client = await getClient({ chainId: first.chainId });
|
|
825
|
+
const chainStates = await chainOps.getChannelStatesBatch(
|
|
826
|
+
client,
|
|
827
|
+
channels.map((channel) => channel.channelId),
|
|
828
|
+
first.escrowContract
|
|
829
|
+
);
|
|
830
|
+
for (const [index, channel] of channels.entries()) {
|
|
831
|
+
const chainState = chainStates[index];
|
|
832
|
+
await terminalizeLostBacking(channel, chainState);
|
|
833
|
+
const chainFinalized = await writeBackChainState(channel, chainState);
|
|
834
|
+
const forcedClose = !chainFinalized && BigInt(chainState.closeRequestedAt) !== 0n;
|
|
835
|
+
if (forcedClose) closeRequested += 1;
|
|
836
|
+
if (!tempoSettlementDue({
|
|
837
|
+
channel,
|
|
838
|
+
chainSettled: chainState.settled,
|
|
839
|
+
forcedClose,
|
|
840
|
+
intervalMs: config.settlementSchedule?.intervalMs
|
|
841
|
+
})) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (await settleChannel(channel, client)) settled += 1;
|
|
845
|
+
else inFlight += 1;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return { scanned: rows.length, closeRequested, settled, inFlight };
|
|
850
|
+
};
|
|
851
|
+
const recoverTempoSessionClose = async (channelId, drainId) => {
|
|
852
|
+
if (!store.getClose || !store.confirmClose) return null;
|
|
853
|
+
const intent = await store.getClose(channelId);
|
|
854
|
+
if (intent?.drainId !== drainId) return null;
|
|
855
|
+
if (intent.reference) return intent.reference;
|
|
856
|
+
const current = readTempoSessionState(await store.get(channelId));
|
|
857
|
+
if (!current) throw new Error(`Tempo session channel ${channelId} not found`);
|
|
858
|
+
let finalized = current.finalized;
|
|
859
|
+
if (!finalized) {
|
|
860
|
+
const client = await getClient({ chainId: current.chainId });
|
|
861
|
+
const [chainState] = await chainOps.getChannelStatesBatch(
|
|
862
|
+
client,
|
|
863
|
+
[current.channelId],
|
|
864
|
+
current.escrowContract
|
|
865
|
+
);
|
|
866
|
+
await terminalizeLostBacking(current, chainState);
|
|
867
|
+
finalized = chainState.deposit === 0n;
|
|
868
|
+
}
|
|
869
|
+
if (!finalized) return null;
|
|
870
|
+
const recovered = await store.confirmClose(channelId, drainId, `channel:${channelId}`);
|
|
871
|
+
return recovered.reference;
|
|
872
|
+
};
|
|
873
|
+
const getClose = store.getClose?.bind(store) ?? (() => Promise.resolve(null));
|
|
874
|
+
const beginClose = store.beginClose?.bind(store);
|
|
875
|
+
const withCloseIntent = store.withCloseIntent?.bind(store);
|
|
876
|
+
const resolveCloseForecast = store.resolveCloseForecast?.bind(store);
|
|
877
|
+
const releaseCloseIntent = store.releaseCloseIntent?.bind(store);
|
|
878
|
+
const confirmClose = store.confirmClose?.bind(store);
|
|
879
|
+
return Object.assign(mppx, {
|
|
880
|
+
sweepTempoSessions,
|
|
881
|
+
...beginClose && {
|
|
882
|
+
beginTempoSessionClose: async (channelId, drainId) => {
|
|
883
|
+
await beginClose(channelId, drainId);
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
...withCloseIntent && { withTempoSessionCloseIntent: withCloseIntent },
|
|
887
|
+
...resolveCloseForecast && {
|
|
888
|
+
resolveTempoSessionCloseForecast: resolveCloseForecast
|
|
889
|
+
},
|
|
890
|
+
...releaseCloseIntent && { releaseTempoSessionCloseIntent: releaseCloseIntent },
|
|
891
|
+
...confirmClose && {
|
|
892
|
+
confirmTempoSessionClose: async (channelId, drainId, reference) => (await confirmClose(channelId, drainId, reference)).reference ?? reference
|
|
893
|
+
},
|
|
894
|
+
...store.getClose && store.confirmClose && { recoverTempoSessionClose },
|
|
895
|
+
...store.getClose && {
|
|
896
|
+
getTempoSessionCloseIntent: (channelId) => getClose(channelId.toLowerCase())
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
function readTempoSessionState(value) {
|
|
901
|
+
if (!value || typeof value !== "object") return void 0;
|
|
902
|
+
const state = value;
|
|
903
|
+
if (typeof state.channelId !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(state.channelId) || typeof state.chainId !== "number" || typeof state.escrowContract !== "string" || typeof state.token !== "string" || !HEX_ADDRESS_RE.test(state.token) || typeof state.closeRequestedAt !== "bigint" || typeof state.deposit !== "bigint" || typeof state.highestVoucherAmount !== "bigint" || typeof state.spent !== "bigint" || typeof state.settledOnChain !== "bigint") {
|
|
904
|
+
return void 0;
|
|
905
|
+
}
|
|
906
|
+
return state;
|
|
907
|
+
}
|
|
908
|
+
function maxBigInt(left, right) {
|
|
909
|
+
return left > right ? left : right;
|
|
910
|
+
}
|
|
911
|
+
function isStateRecord(value) {
|
|
912
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
913
|
+
}
|
|
914
|
+
function tempoSettlementDue(args) {
|
|
915
|
+
const { channel, chainSettled, forcedClose, intervalMs } = args;
|
|
916
|
+
const settlementBoundary = Date.parse(channel.lastSettlementAt ?? channel.createdAt ?? "");
|
|
917
|
+
const scheduled = intervalMs !== void 0 && Number.isFinite(settlementBoundary) && Date.now() - settlementBoundary >= intervalMs;
|
|
918
|
+
if (!forcedClose && !scheduled) return false;
|
|
919
|
+
if (channel.highestVoucherAmount <= chainSettled) return false;
|
|
920
|
+
if (!forcedClose && channel.highestVoucherAmount !== channel.spent) return false;
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
function parseMppMethodsAllowlist(raw) {
|
|
924
|
+
if (!raw) return void 0;
|
|
925
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
926
|
+
return parts.length > 0 ? parts : void 0;
|
|
927
|
+
}
|
|
928
|
+
function advertisedMethods(mppx) {
|
|
929
|
+
return mppx.methods.map((m) => {
|
|
930
|
+
const method = m;
|
|
931
|
+
return { method: method.name, intent: method.intent };
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
var _testing = {
|
|
935
|
+
attachTempoSessionRuntime
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
export {
|
|
939
|
+
TEMPO_SETTLEMENT_DEFAULT_LOW_BALANCE_MICRO,
|
|
940
|
+
TempoSettlementReadiness,
|
|
941
|
+
isTempoInsufficientFundsError,
|
|
942
|
+
MPPX_HMAC_MISMATCH_REASON,
|
|
943
|
+
TEMPO_SETTLEMENT_LEASE_FIELD,
|
|
944
|
+
TEMPO_SETTLEMENT_LEASE_OWNER_FIELD,
|
|
945
|
+
TEMPO_SETTLEMENT_LEASE_MS,
|
|
946
|
+
TEMPO_SETTLEMENT_IN_FLIGHT,
|
|
947
|
+
TEMPO_USDC_MAINNET,
|
|
948
|
+
challengeMeta,
|
|
949
|
+
createMppFromOpts,
|
|
950
|
+
createDualKeyMppxFromOpts,
|
|
951
|
+
wrapMppx,
|
|
952
|
+
parseMppMethodsAllowlist,
|
|
953
|
+
advertisedMethods,
|
|
954
|
+
_testing
|
|
955
|
+
};
|