@dvmkit/sdk 0.6.0-rc.1 → 0.7.0-rc.10

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.
Files changed (73) hide show
  1. package/README.md +39 -2
  2. package/dist/{chunk-PLG4XER6.js → chunk-2NUFE2PG.js} +47 -5
  3. package/dist/{chunk-7ZUKL4IV.js → chunk-3E4C6W7W.js} +16 -5
  4. package/dist/{chunk-HHBDBMEO.js → chunk-53SNRGQQ.js} +375 -158
  5. package/dist/{chunk-IWQHUSMO.js → chunk-62FI3NJF.js} +78 -2
  6. package/dist/chunk-6HJEVCVI.js +17 -0
  7. package/dist/chunk-A262WVYX.js +6 -0
  8. package/dist/{chunk-RLTSCKRI.js → chunk-BPYMEMQX.js} +50 -3
  9. package/dist/{chunk-IV6Q7NRW.js → chunk-C5RIQWG4.js} +1 -1
  10. package/dist/{chunk-GAOAYHZA.js → chunk-FBQVOOYF.js} +37 -6
  11. package/dist/{chunk-Z4ZT5LNT.js → chunk-GEI4MAXE.js} +3 -3
  12. package/dist/{chunk-FYRR2UEZ.js → chunk-GLIAR7RN.js} +1 -1
  13. package/dist/{chunk-X7EZ5CLI.js → chunk-HCATFJDD.js} +3 -3
  14. package/dist/{chunk-BYCEKO65.js → chunk-HPCXF3DV.js} +220 -32
  15. package/dist/{chunk-JH3SX3LY.js → chunk-IRXIH775.js} +6 -6
  16. package/dist/{chunk-QR2EF2NI.js → chunk-KCQDXIJU.js} +1 -42
  17. package/dist/chunk-MDW7ZR7V.js +47 -0
  18. package/dist/{chunk-KA6VN5FK.js → chunk-MQYM4T66.js} +3 -3
  19. package/dist/{chunk-P6QON7ER.js → chunk-NV7Z7377.js} +1 -1
  20. package/dist/{chunk-OQA7IHI4.js → chunk-OBBVDKLE.js} +484 -234
  21. package/dist/{chunk-EQLA4FFY.js → chunk-OHUXCAOM.js} +17 -6
  22. package/dist/{chunk-FI4CST2D.js → chunk-PZ5IAWFD.js} +5 -7
  23. package/dist/{chunk-IXR5RFK7.js → chunk-R3TQSXWT.js} +191 -5
  24. package/dist/{chunk-VHNPVGSA.js → chunk-T5DIXOLR.js} +3 -3
  25. package/dist/{chunk-RCM3P2HE.js → chunk-UKD3R2KU.js} +55 -4
  26. package/dist/chunk-WMGXJQI3.js +120 -0
  27. package/dist/{chunk-N5RZ5HVB.js → chunk-X6S2CHFM.js} +2 -2
  28. package/dist/{chunk-ETXDTUJO.js → chunk-ZCNPIILZ.js} +112 -1
  29. package/dist/{content-retention-BTGSQRfP.d.ts → content-retention-D2K4MKNO.d.ts} +1 -1
  30. package/dist/{credit-ledger-MI2H7NEC.js → credit-ledger-MFIIOSZH.js} +4 -5
  31. package/dist/{credit-menu-BAsrRu3z.d.ts → credit-menu-D2t1-Tf6.d.ts} +13 -6
  32. package/dist/{fx-BgDmyTdd.d.ts → fx-Ql4nvdbh.d.ts} +10 -3
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +248 -16
  35. package/dist/internal/caller.d.ts +4586 -7318
  36. package/dist/internal/caller.js +4431 -8127
  37. package/dist/internal/index.js +1 -1
  38. package/dist/internal/server.d.ts +7 -7
  39. package/dist/internal/server.js +30 -33
  40. package/dist/{job-store-BVeF5_Jq.d.ts → job-store-DCvJQUX0.d.ts} +1 -1
  41. package/dist/{lightning-backend-40fIzz1W.d.ts → lightning-backend-CY5ML3km.d.ts} +4 -2
  42. package/dist/memory-credit-ledger-44NE4PZC.js +11 -0
  43. package/dist/{mpp-setup-RF5FXGGV.js → mpp-setup-HRWUBULX.js} +4 -2
  44. package/dist/{payout-reporter-3Y2U55ZX.js → payout-reporter-7P66T77I.js} +6 -4
  45. package/dist/{postgres-consumed-credential-store-BUVCAPLS.js → postgres-consumed-credential-store-IDGPFKIL.js} +3 -3
  46. package/dist/{postgres-job-store-VHLQBJZC.js → postgres-job-store-UKNJ2F6Z.js} +2 -2
  47. package/dist/{postgres-replay-store-ARZSQ45X.js → postgres-replay-store-PMA4ADXN.js} +2 -2
  48. package/dist/{pricing-XT2IAPNE.js → pricing-52XJHJ6K.js} +12 -12
  49. package/dist/{revenue-reporter-MAERPYGK.js → revenue-reporter-HV7RMS54.js} +2 -2
  50. package/dist/server/index.d.ts +12 -11
  51. package/dist/server/index.js +49 -53
  52. package/dist/{step-cache-CPdH1hei.d.ts → step-cache-BOWAn_BH.d.ts} +84 -7
  53. package/dist/{tempo-charge-store-Q3D7KYMH.js → tempo-charge-store-P2Z4RMU7.js} +2 -2
  54. package/dist/{tempo-session-store-ZRNCOX7P.js → tempo-session-store-VSP7KLBS.js} +3 -3
  55. package/dist/testing/index.d.ts +3 -3
  56. package/dist/testing/index.js +7 -6
  57. package/dist/{usd-AWlrwlM5.d.ts → usd-MAtMp3DB.d.ts} +1 -1
  58. package/dist/{x402-R6HPBY5D.js → x402-3IQ3HXSG.js} +9 -29
  59. package/package.json +3 -2
  60. package/dist/chunk-AT6V3SY7.js +0 -102
  61. package/dist/chunk-B2IROPQF.js +0 -1844
  62. package/dist/chunk-CZULM42S.js +0 -143
  63. package/dist/chunk-GFY5FRX3.js +0 -218
  64. package/dist/chunk-HCLVYZUZ.js +0 -244
  65. package/dist/chunk-LE74UEV7.js +0 -1071
  66. package/dist/chunk-LIRAHP7B.js +0 -53
  67. package/dist/chunk-MZRJR2CW.js +0 -776
  68. package/dist/chunk-OFVV37XC.js +0 -57
  69. package/dist/chunk-PAIM66YA.js +0 -347
  70. package/dist/chunk-UOCZXO2Z.js +0 -1663
  71. package/dist/memory-credit-ledger-W2BAUYJP.js +0 -12
  72. package/dist/tempo-lifecycle-ZBBLFBMH.js +0 -15
  73. package/dist/tempo-wallet-B5XXV3YH.js +0 -40
@@ -1,1663 +0,0 @@
1
- import {
2
- connectedTempoAccount,
3
- tempoBalanceCheckHint,
4
- tempoTokenReference
5
- } from "./chunk-GFY5FRX3.js";
6
- import {
7
- fundingReachableAt,
8
- listCredits,
9
- maxAdvertisedMicro,
10
- normalizeCreditEndpoint,
11
- withinAdvertisedTolerance
12
- } from "./chunk-B2IROPQF.js";
13
- import {
14
- costAnchor,
15
- formatUsd,
16
- formatUsdcMicrounits
17
- } from "./chunk-PAIM66YA.js";
18
- import {
19
- IDENTITIES_FILE,
20
- TEMPO_CHANNELS_FILE,
21
- ensureConfigDir,
22
- resolveRpcHttpTransportOptions,
23
- resolveRpcOverride
24
- } from "./chunk-CZULM42S.js";
25
- import {
26
- redactUrl,
27
- redactUrlsInText
28
- } from "./chunk-FUJ36YDV.js";
29
- import {
30
- DvmError
31
- } from "./chunk-LIRAHP7B.js";
32
-
33
- // src/lib/cli-identity-store.ts
34
- import { schnorr } from "@noble/curves/secp256k1.js";
35
- import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
36
- import lockfile from "proper-lockfile";
37
- var DEFAULT_LOCK_STALE_MS = 3e4;
38
- var DEFAULT_LOCK_RETRIES = 30;
39
- var DEFAULT_LOCK_RETRY_INTERVAL_MS = 1e3;
40
- var IDENTITIES_VERSION = 1;
41
- function identitiesFileExists() {
42
- return existsSync(IDENTITIES_FILE);
43
- }
44
- function loadIdentities() {
45
- if (!existsSync(IDENTITIES_FILE)) return null;
46
- const raw = readFileSync(IDENTITIES_FILE, "utf-8");
47
- let parsed;
48
- try {
49
- parsed = JSON.parse(raw);
50
- } catch (err) {
51
- throw new DvmError(
52
- "identities_corrupt",
53
- `Identities file at ${IDENTITIES_FILE} is not valid JSON.`,
54
- `Restore a backup or delete the file and run 'dvm init'. Underlying error: ${err instanceof Error ? err.message : String(err)}`
55
- );
56
- }
57
- if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
58
- throw new DvmError(
59
- "identities_corrupt",
60
- `Identities file at ${IDENTITIES_FILE} is missing a version field.`,
61
- "Restore a backup or delete the file and run 'dvm init'."
62
- );
63
- }
64
- const version = parsed.version;
65
- if (version !== IDENTITIES_VERSION) {
66
- throw new DvmError(
67
- "identities_unknown_version",
68
- `Unknown identities file version ${String(version)} (this CLI understands version ${String(IDENTITIES_VERSION)}).`,
69
- "Upgrade dvm CLI: a newer version wrote this file."
70
- );
71
- }
72
- const identities = parsed.identities;
73
- if (typeof identities !== "object" || identities === null || Array.isArray(identities)) {
74
- throw new DvmError(
75
- "identities_corrupt",
76
- `Identities file at ${IDENTITIES_FILE} has a malformed identities map.`,
77
- "Restore a backup or delete the file and run 'dvm init'."
78
- );
79
- }
80
- return {
81
- version: IDENTITIES_VERSION,
82
- identities
83
- };
84
- }
85
- function saveIdentities(file) {
86
- ensureConfigDir();
87
- const tmp = `${IDENTITIES_FILE}.tmp`;
88
- const data = JSON.stringify(file, null, 2) + "\n";
89
- writeFileSync(tmp, data, { mode: 384 });
90
- try {
91
- renameSync(tmp, IDENTITIES_FILE);
92
- } catch (err) {
93
- try {
94
- unlinkSync(tmp);
95
- } catch {
96
- }
97
- throw err;
98
- }
99
- }
100
- async function withIdentitiesLock(fn, opts = {}) {
101
- ensureConfigDir();
102
- const lockPath = `${IDENTITIES_FILE}.lock`;
103
- const stale = opts.stale ?? DEFAULT_LOCK_STALE_MS;
104
- const retries = opts.retries ?? DEFAULT_LOCK_RETRIES;
105
- const interval = opts.retryIntervalMs ?? DEFAULT_LOCK_RETRY_INTERVAL_MS;
106
- let release;
107
- try {
108
- release = await lockfile.lock(IDENTITIES_FILE, {
109
- lockfilePath: lockPath,
110
- stale,
111
- realpath: false,
112
- retries: { retries, factor: 1, minTimeout: interval, maxTimeout: interval }
113
- });
114
- } catch (err) {
115
- throw new DvmError(
116
- "identities_locked",
117
- `Could not acquire the identities lock at ${lockPath} within the retry budget.`,
118
- `Another dvm CLI may be writing to ${IDENTITIES_FILE}. Wait a moment and retry. If no other dvm CLI is running, remove ${lockPath} and try again.`,
119
- { lock_file: lockPath, underlying: err instanceof Error ? err.message : String(err) }
120
- );
121
- }
122
- try {
123
- return await fn();
124
- } finally {
125
- try {
126
- await release();
127
- } catch {
128
- }
129
- }
130
- }
131
- function generateIdentity(now = Date.now()) {
132
- const { secretKey, publicKey } = schnorr.keygen();
133
- return {
134
- privkey: bytesToHex(secretKey),
135
- pubkey: bytesToHex(publicKey),
136
- createdAt: now
137
- };
138
- }
139
- function deriveIdentityFromPrivkey(privkeyHex, now = Date.now()) {
140
- if (!/^[0-9a-f]{64}$/.test(privkeyHex)) {
141
- throw new DvmError(
142
- "invalid_privkey",
143
- "Private key must be 64 lowercase hex characters (32 bytes).",
144
- "Generate a fresh one with 'dvm identity create <name>'."
145
- );
146
- }
147
- const privkeyBytes = hexToBytes(privkeyHex);
148
- const pubkeyBytes = schnorr.getPublicKey(privkeyBytes);
149
- return {
150
- privkey: privkeyHex,
151
- pubkey: bytesToHex(pubkeyBytes),
152
- createdAt: now
153
- };
154
- }
155
- function bytesToHex(bytes) {
156
- let s = "";
157
- for (const b of bytes) s += b.toString(16).padStart(2, "0");
158
- return s;
159
- }
160
- function hexToBytes(hex) {
161
- const out = new Uint8Array(hex.length / 2);
162
- for (let i = 0; i < out.length; i++) {
163
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
164
- }
165
- return out;
166
- }
167
-
168
- // src/lib/cli-identity-resolve.ts
169
- function resolveSigningIdentity(opts) {
170
- if (opts.as !== void 0) {
171
- const file2 = loadIdentities();
172
- const entry = file2?.identities[opts.as];
173
- if (!entry) {
174
- throw new DvmError(
175
- "auth_required",
176
- `No identity named '${opts.as}' is configured.`,
177
- availableHint(file2?.identities, "create one with 'dvm identity create <name>'.")
178
- );
179
- }
180
- return {
181
- privkey: entry.privkey,
182
- pubkey: entry.pubkey,
183
- source: { kind: "flag", name: opts.as }
184
- };
185
- }
186
- const envKey = process.env.DVM_IDENTITY_KEY;
187
- if (envKey) {
188
- const derived = deriveIdentityFromPrivkey(envKey.toLowerCase());
189
- return { privkey: derived.privkey, pubkey: derived.pubkey, source: { kind: "env" } };
190
- }
191
- const file = loadIdentities();
192
- const identities = file?.identities ?? {};
193
- const names = Object.keys(identities);
194
- const configuredDefault = opts.config?.defaultIdentity;
195
- if (configuredDefault !== void 0) {
196
- if (!Object.hasOwn(identities, configuredDefault)) {
197
- throw new DvmError(
198
- "auth_required",
199
- `Configured default identity '${configuredDefault}' no longer exists.`,
200
- availableHint(identities, "set a new default with 'dvm identity use <name>'.")
201
- );
202
- }
203
- const entry = identities[configuredDefault];
204
- return {
205
- privkey: entry.privkey,
206
- pubkey: entry.pubkey,
207
- source: { kind: "default", name: configuredDefault }
208
- };
209
- }
210
- if (names.length === 1) {
211
- const onlyName = names[0];
212
- const entry = identities[onlyName];
213
- return {
214
- privkey: entry.privkey,
215
- pubkey: entry.pubkey,
216
- source: { kind: "sole", name: onlyName }
217
- };
218
- }
219
- if (names.length === 0) {
220
- throw new DvmError(
221
- "auth_required",
222
- "This DVM requires a signed request but no caller identity is configured.",
223
- "Run 'dvm identity create <name>' (or 'dvm init') to generate one, or pass DVM_IDENTITY_KEY=<hex>."
224
- );
225
- }
226
- throw new DvmError(
227
- "auth_required",
228
- "Multiple identities are configured but none is selected.",
229
- `Pass --as <name>, set a default with 'dvm identity use <name>', or export DVM_IDENTITY_KEY=<hex>. Available: ${names.join(", ")}.`
230
- );
231
- }
232
- function findSigningIdentityByPubkey(pubkey) {
233
- const wanted = pubkey.toLowerCase();
234
- const envKey = process.env.DVM_IDENTITY_KEY;
235
- if (envKey) {
236
- try {
237
- const derived = deriveIdentityFromPrivkey(envKey.toLowerCase());
238
- if (derived.pubkey === wanted) {
239
- return { privkey: derived.privkey, pubkey: derived.pubkey, source: { kind: "env" } };
240
- }
241
- } catch {
242
- }
243
- }
244
- const identities = loadIdentities()?.identities ?? {};
245
- for (const [name, entry] of Object.entries(identities)) {
246
- if (entry.pubkey.toLowerCase() === wanted) {
247
- return { privkey: entry.privkey, pubkey: entry.pubkey, source: { kind: "pubkey", name } };
248
- }
249
- }
250
- return null;
251
- }
252
- function availableHint(identities, suffix) {
253
- const names = identities ? Object.keys(identities) : [];
254
- if (names.length === 0) return `No identities are configured \u2014 ${suffix}`;
255
- return `Available identities: ${names.join(", ")}. ${suffix}`;
256
- }
257
-
258
- // src/lib/shell-quote.ts
259
- function shellQuoteArg(value) {
260
- if (value === "") return "''";
261
- if (SAFE_VALUE_RE.test(value)) return value;
262
- return `'${value.replace(/'/g, "'\\''")}'`;
263
- }
264
- var SAFE_VALUE_RE = /^[a-zA-Z0-9_\-./@:=,+%]+$/;
265
-
266
- // src/lib/payment-rails/tempo-channel-store.ts
267
- import { AsyncLocalStorage } from "async_hooks";
268
- import { randomUUID } from "crypto";
269
- import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
270
- import { Session as TempoSession } from "mppx/tempo";
271
- import lockfile2 from "proper-lockfile";
272
- import { isAddress, zeroAddress } from "viem";
273
- var TEMPO_CHARGE_PENDING_STALE_MS = 24 * 60 * 60 * 1e3;
274
- var tempoLockOwner = new AsyncLocalStorage();
275
- async function withTempoChannelLock(operation) {
276
- if (tempoLockOwner.getStore()?.active) return operation();
277
- ensureTempoChannelFile();
278
- const release = await lockfile2.lock(TEMPO_CHANNELS_FILE, {
279
- realpath: false,
280
- retries: { retries: 20, minTimeout: 25, maxTimeout: 250 }
281
- });
282
- const owner = { active: true };
283
- try {
284
- return await tempoLockOwner.run(owner, operation);
285
- } finally {
286
- owner.active = false;
287
- await release();
288
- }
289
- }
290
- function createTempoChannelStore(context) {
291
- let operator = context.operator === void 0 ? void 0 : normalizeTempoChannelOperator(context.operator);
292
- return TempoSession.Client.createJsonChannelStore({
293
- get(key) {
294
- if (operator === void 0) return void 0;
295
- return loadFile().values[channelStorageKey(context.callerPubkey, operator, key)];
296
- },
297
- set(key, value) {
298
- const file = loadFile();
299
- const entry = JSON.parse(value);
300
- const entryOperator = normalizeTempoChannelOperator(entry.descriptor?.operator);
301
- if (operator !== void 0 && operator !== entryOperator) {
302
- throw new DvmError(
303
- "payment_invalid",
304
- "The Tempo channel operator does not match the session challenge."
305
- );
306
- }
307
- operator = entryOperator;
308
- const storedKey = channelStorageKey(context.callerPubkey, operator, key);
309
- if (Object.hasOwn(file.values, storedKey)) {
310
- const occupiedValue = file.values[storedKey];
311
- const occupied = JSON.parse(occupiedValue);
312
- if (typeof occupied.channelId !== "string" || occupied.channelId.toLowerCase() !== entry.channelId.toLowerCase()) {
313
- throw new DvmError(
314
- "tempo_channel_scope_occupied",
315
- "A different Tempo channel already occupies this payment scope.",
316
- "Exit the existing Tempo channel before retrying this funding. Its local recovery descriptor was preserved.",
317
- {
318
- occupied_channel_id: typeof occupied.channelId === "string" ? occupied.channelId : void 0,
319
- refused_channel_id: entry.channelId
320
- },
321
- "Funding stopped before broadcast because this Tempo payment scope still holds another recoverable channel."
322
- );
323
- }
324
- }
325
- file.values[storedKey] = value;
326
- if (context.endpoint && context.creditId) {
327
- const updatedAt = Date.now();
328
- const association = {
329
- endpoint: normalizeCreditEndpoint(context.endpoint),
330
- callerPubkey: context.callerPubkey,
331
- creditId: context.creditId,
332
- channelKey: key.startsWith("chan:") ? key.slice(5) : key,
333
- operator,
334
- channelId: entry.channelId,
335
- cumulativeAmount: entry.cumulativeAmount,
336
- deposit: entry.deposit,
337
- chainId: entry.chainId,
338
- opened: entry.opened,
339
- updatedAt
340
- };
341
- file.associations = file.associations.map(
342
- (candidate) => candidate.callerPubkey === association.callerPubkey && candidate.operator === association.operator && candidate.channelKey === association.channelKey && candidate.channelId.toLowerCase() === association.channelId.toLowerCase() ? {
343
- ...candidate,
344
- cumulativeAmount: association.cumulativeAmount,
345
- deposit: association.deposit,
346
- chainId: association.chainId,
347
- opened: association.opened,
348
- updatedAt
349
- } : candidate
350
- );
351
- const index = file.associations.findIndex(
352
- (candidate) => sameCredit(
353
- candidate,
354
- association.endpoint,
355
- association.callerPubkey,
356
- association.creditId
357
- )
358
- );
359
- if (index === -1) file.associations.push(association);
360
- else file.associations[index] = association;
361
- }
362
- saveFile(file);
363
- },
364
- delete(key) {
365
- if (operator === void 0) return;
366
- const file = loadFile();
367
- const removedKey = channelStorageKey(context.callerPubkey, operator, key);
368
- const channelKey = key.startsWith("chan:") ? key.slice(5) : key;
369
- const endpoint = context.endpoint ? normalizeCreditEndpoint(context.endpoint) : void 0;
370
- file.associations = file.associations.filter((association) => {
371
- if (association.callerPubkey !== context.callerPubkey || association.operator !== operator || association.channelKey !== channelKey) {
372
- return true;
373
- }
374
- if (endpoint && context.creditId) {
375
- return !sameCredit(association, endpoint, context.callerPubkey, context.creditId);
376
- }
377
- return false;
378
- });
379
- const shared = file.associations.some(
380
- (association) => association.callerPubkey === context.callerPubkey && association.operator === operator && association.channelKey === channelKey
381
- );
382
- if (!shared) {
383
- file.values = Object.fromEntries(
384
- Object.entries(file.values).filter(([storedKey]) => storedKey !== removedKey)
385
- );
386
- }
387
- saveFile(file);
388
- }
389
- });
390
- }
391
- function listTempoChannelAssociations() {
392
- return [...loadFile().associations].sort((a, b) => b.updatedAt - a.updatedAt);
393
- }
394
- function loadTempoChannelAssociation(endpoint, callerPubkey, creditId) {
395
- const normalized = normalizeCreditEndpoint(endpoint);
396
- return loadFile().associations.find(
397
- (candidate) => sameCredit(candidate, normalized, callerPubkey, creditId)
398
- );
399
- }
400
- function loadTempoChannelAssociationByKey(callerPubkey, operator, channelKey) {
401
- const normalizedOperator = normalizeTempoChannelOperator(operator);
402
- return loadFile().associations.find(
403
- (candidate) => candidate.callerPubkey === callerPubkey && candidate.operator === normalizedOperator && candidate.channelKey === channelKey
404
- );
405
- }
406
- async function loadTempoChannelEntry(association) {
407
- return createTempoChannelStore({
408
- callerPubkey: association.callerPubkey,
409
- endpoint: association.endpoint,
410
- creditId: association.creditId,
411
- operator: association.operator
412
- }).get(association.channelKey);
413
- }
414
- async function loadTempoChannelController(association) {
415
- const entry = await loadTempoChannelEntry(association);
416
- return entry && tempoDescriptorController(entry.descriptor);
417
- }
418
- function tempoDescriptorController(descriptor) {
419
- const payer = descriptor.payer;
420
- if (typeof payer !== "string" || !isAddress(payer, { strict: false })) return void 0;
421
- const signer = descriptor.authorizedSigner;
422
- const authorizedSigner = TempoSession.Precompile.Channel.resolveAuthorizedSigner({
423
- ...descriptor,
424
- payer,
425
- authorizedSigner: typeof signer === "string" && isAddress(signer, { strict: false }) ? signer : zeroAddress
426
- });
427
- return { payer: payer.toLowerCase(), authorizedSigner: authorizedSigner.toLowerCase() };
428
- }
429
- function tempoChannelControlledBy(controller, address) {
430
- if (!controller) return true;
431
- if (address === void 0) return false;
432
- const candidate = address.toLowerCase();
433
- return controller.payer.toLowerCase() === candidate || controller.authorizedSigner.toLowerCase() === candidate;
434
- }
435
- async function retireTempoChannel(association) {
436
- await createTempoChannelStore({
437
- callerPubkey: association.callerPubkey,
438
- endpoint: association.endpoint,
439
- creditId: association.creditId,
440
- operator: association.operator
441
- }).delete(association.channelKey);
442
- }
443
- async function retireTempoChannelScope(association) {
444
- await createTempoChannelStore({
445
- callerPubkey: association.callerPubkey,
446
- operator: association.operator
447
- }).delete(association.channelKey);
448
- }
449
- function tempoChargeFundingStale(pending, now = Date.now()) {
450
- return now - pending.requestedAt >= TEMPO_CHARGE_PENDING_STALE_MS;
451
- }
452
- function tempoChargeCredentialSettleable(pending, now = Date.now()) {
453
- return pending.credentialValidBefore !== void 0 && now < pending.credentialValidBefore * 1e3;
454
- }
455
- function recordPendingTempoChargeFunding(pending) {
456
- const { file, credits } = loadChargeFile();
457
- const entry = { ...pending, endpoint: normalizeCreditEndpoint(pending.endpoint) };
458
- file.charges = [
459
- ...file.charges.filter((candidate) => !sameChargeCredit(candidate, entry, credits)),
460
- entry
461
- ];
462
- saveFile(file);
463
- }
464
- function loadResumableTempoChargeFunding(args) {
465
- const key = {
466
- endpoint: normalizeCreditEndpoint(args.endpoint),
467
- callerPubkey: args.callerPubkey,
468
- creditId: args.creditId
469
- };
470
- const { file, credits } = loadChargeFile();
471
- return file.charges.find(
472
- (entry) => sameChargeCredit(entry, key, credits) && !tempoChargeFundingStale(entry, args.now)
473
- );
474
- }
475
- function listResumableTempoChargeFundings(filter = {}) {
476
- const endpoint = filter.endpoint ? normalizeCreditEndpoint(filter.endpoint) : void 0;
477
- const { file, credits } = loadChargeFile();
478
- return file.charges.filter(
479
- (entry) => (!endpoint || fundingReachableAt(entry, endpoint, credits)) && (!filter.callerPubkey || entry.callerPubkey === filter.callerPubkey) && !tempoChargeFundingStale(entry, filter.now)
480
- ).sort((a, b) => a.requestedAt - b.requestedAt);
481
- }
482
- function clearPendingTempoChargeFunding(pending) {
483
- const { file, credits } = loadChargeFile();
484
- const key = { ...pending, endpoint: normalizeCreditEndpoint(pending.endpoint) };
485
- const remaining = file.charges.filter(
486
- (entry) => !sameChargeCredit(entry, key, credits) || entry.fundId !== pending.fundId
487
- );
488
- if (remaining.length === file.charges.length) return;
489
- file.charges = remaining;
490
- saveFile(file);
491
- }
492
- function sameCredit(association, endpoint, callerPubkey, creditId) {
493
- return association.endpoint === endpoint && association.callerPubkey === callerPubkey && association.creditId === creditId;
494
- }
495
- function sameChargeCredit(entry, key, credits) {
496
- return entry.callerPubkey === key.callerPubkey && entry.creditId === key.creditId && fundingReachableAt(entry, normalizeCreditEndpoint(key.endpoint), credits);
497
- }
498
- function loadChargeFile() {
499
- const snapshot = collapseChargeFile();
500
- if (!snapshot.changed) return snapshot;
501
- const persisted = tryWithTempoChannelLockSync(() => {
502
- const current = collapseChargeFile();
503
- if (current.changed) saveFile(current.file);
504
- return current;
505
- });
506
- return persisted ?? snapshot;
507
- }
508
- function collapseChargeFile() {
509
- const file = loadFile();
510
- const credits = listCredits();
511
- const charges = [];
512
- let changed = false;
513
- for (const entry of file.charges) {
514
- const index = charges.findIndex((candidate) => sameChargeCredit(candidate, entry, credits));
515
- if (index === -1) {
516
- charges.push(entry);
517
- continue;
518
- }
519
- const seenAt = Number.isFinite(charges[index].requestedAt) ? charges[index].requestedAt : 0;
520
- const entryAt = Number.isFinite(entry.requestedAt) ? entry.requestedAt : 0;
521
- if (entryAt > seenAt) charges[index] = entry;
522
- changed = true;
523
- }
524
- if (changed) file.charges = charges;
525
- return { file, credits, changed };
526
- }
527
- function tryWithTempoChannelLockSync(operation) {
528
- if (tempoLockOwner.getStore()?.active) return operation();
529
- ensureTempoChannelFile();
530
- let release;
531
- try {
532
- release = lockfile2.lockSync(TEMPO_CHANNELS_FILE, { realpath: false });
533
- } catch (error) {
534
- if (error.code === "ELOCKED") return void 0;
535
- throw error;
536
- }
537
- const owner = { active: true };
538
- try {
539
- return tempoLockOwner.run(owner, operation);
540
- } finally {
541
- owner.active = false;
542
- release();
543
- }
544
- }
545
- function ensureTempoChannelFile() {
546
- ensureConfigDir();
547
- if (existsSync2(TEMPO_CHANNELS_FILE)) return;
548
- try {
549
- writeFileSync2(TEMPO_CHANNELS_FILE, `${JSON.stringify(emptyFile(), null, 2)}
550
- `, {
551
- mode: 384,
552
- flag: "wx"
553
- });
554
- } catch {
555
- }
556
- }
557
- function loadFile() {
558
- if (!existsSync2(TEMPO_CHANNELS_FILE)) return emptyFile();
559
- try {
560
- const parsed = JSON.parse(readFileSync2(TEMPO_CHANNELS_FILE, "utf8"));
561
- if (parsed.version === 1) return migrateLegacyFile(parsed);
562
- if (parsed.version !== 2 || !isStringRecord(parsed.values) || !Array.isArray(parsed.associations) || !parsed.associations.every(isTempoChannelAssociation)) {
563
- throw new Error("unsupported Tempo channel store shape");
564
- }
565
- return {
566
- version: 2,
567
- values: parsed.values,
568
- associations: parsed.associations,
569
- // Tolerated rather than required, unlike its two siblings: every file
570
- // written before lacks the key, and failing closed on it would
571
- // read as a corrupt journal on a store that is perfectly intact.
572
- charges: Array.isArray(parsed.charges) ? parsed.charges.filter(isPendingTempoCharge) : []
573
- };
574
- } catch (cause) {
575
- throw new DvmError(
576
- "tempo_channel_store_invalid",
577
- "The saved Tempo channel journal cannot be read safely.",
578
- `Do not delete ${TEMPO_CHANNELS_FILE}; restore or repair it before signing another channel credential.`,
579
- { cause: cause instanceof Error ? cause.message : String(cause) }
580
- );
581
- }
582
- }
583
- function saveFile(file) {
584
- ensureConfigDir();
585
- const tmp = `${TEMPO_CHANNELS_FILE}.${randomUUID()}.tmp`;
586
- writeFileSync2(tmp, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
587
- renameSync2(tmp, TEMPO_CHANNELS_FILE);
588
- }
589
- function emptyFile() {
590
- return { version: 2, values: {}, associations: [], charges: [] };
591
- }
592
- function migrateLegacyFile(parsed) {
593
- if (!isStringRecord(parsed.values) || !Array.isArray(parsed.associations) || !parsed.associations.every(isLegacyTempoChannelAssociation)) {
594
- throw new Error("unsupported Tempo channel store shape");
595
- }
596
- const values = { ...parsed.values };
597
- const consumed = /* @__PURE__ */ new Set();
598
- const associations = parsed.associations.map((association) => {
599
- const legacyKey = `${association.callerPubkey}:chan:${association.channelKey}`;
600
- const serialized = parsed.values?.[legacyKey];
601
- if (serialized === void 0) {
602
- throw new Error(`Tempo channel ${association.channelId} has no stored descriptor`);
603
- }
604
- const operator = serializedChannelOperator(serialized);
605
- values[channelStorageKey(association.callerPubkey, operator, `chan:${association.channelKey}`)] = serialized;
606
- consumed.add(legacyKey);
607
- return { ...association, operator };
608
- });
609
- return {
610
- version: 2,
611
- values: Object.fromEntries(Object.entries(values).filter(([key]) => !consumed.has(key))),
612
- associations,
613
- charges: Array.isArray(parsed.charges) ? parsed.charges.filter(isPendingTempoCharge) : []
614
- };
615
- }
616
- function channelStorageKey(callerPubkey, operator, key) {
617
- return `${callerPubkey}:${operator}:${key}`;
618
- }
619
- function serializedChannelOperator(value) {
620
- const parsed = JSON.parse(value);
621
- return normalizeTempoChannelOperator(parsed.descriptor?.operator);
622
- }
623
- function normalizeTempoChannelOperator(value) {
624
- if (value === void 0) return zeroAddress;
625
- if (typeof value !== "string" || !isAddress(value, { strict: false })) {
626
- throw new Error("invalid Tempo channel operator");
627
- }
628
- return value.toLowerCase();
629
- }
630
- function isStringRecord(value) {
631
- return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
632
- }
633
- function isPendingTempoCharge(value) {
634
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
635
- const entry = value;
636
- return typeof entry.endpoint === "string" && typeof entry.callerPubkey === "string" && typeof entry.creditId === "string" && typeof entry.fundId === "string" && typeof entry.amountMicro === "number" && typeof entry.targetMicro === "number" && typeof entry.credential === "string" && typeof entry.signedMicro === "number" && typeof entry.amountMsats === "number" && typeof entry.btcRate === "number" && typeof entry.requestedAt === "number" && (entry.credentialValidBefore === void 0 || typeof entry.credentialValidBefore === "number");
637
- }
638
- function isTempoChannelAssociation(value) {
639
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
640
- const association = value;
641
- return typeof association.endpoint === "string" && typeof association.callerPubkey === "string" && typeof association.creditId === "string" && typeof association.channelKey === "string" && typeof association.operator === "string" && isAddress(association.operator, { strict: false }) && typeof association.channelId === "string" && typeof association.cumulativeAmount === "string" && typeof association.deposit === "string" && typeof association.chainId === "number" && typeof association.opened === "boolean" && typeof association.updatedAt === "number";
642
- }
643
- function isLegacyTempoChannelAssociation(value) {
644
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
645
- const association = value;
646
- return typeof association.endpoint === "string" && typeof association.callerPubkey === "string" && typeof association.creditId === "string" && typeof association.channelKey === "string" && typeof association.channelId === "string" && typeof association.cumulativeAmount === "string" && typeof association.deposit === "string" && typeof association.chainId === "number" && typeof association.opened === "boolean" && typeof association.updatedAt === "number";
647
- }
648
-
649
- // src/lib/payment-rails/tempo-session.ts
650
- import { Credential } from "mppx";
651
- import { Mppx } from "mppx/client";
652
- import { Session as TempoSession2 } from "mppx/tempo";
653
- import { decodeFunctionData, formatUnits, isAddress as isAddress2, zeroAddress as zeroAddress2 } from "viem";
654
- import { privateKeyToAccount } from "viem/accounts";
655
- import { Transaction } from "viem/tempo";
656
-
657
- // src/lib/rail-labels.ts
658
- var PAYMENT_RAIL_KEYS = ["cashu", "cashu_p2pk", "x402", "tempo"];
659
- function isPaymentRailKey(value) {
660
- return typeof value === "string" && PAYMENT_RAIL_KEYS.includes(value);
661
- }
662
- function fundingRailLabel(rail) {
663
- switch (rail) {
664
- case "cashu":
665
- return "ecash";
666
- case "lightning":
667
- return "Lightning";
668
- case "x402":
669
- return "x402 stablecoin";
670
- case "tempo":
671
- return "Tempo stablecoin";
672
- }
673
- }
674
- function paymentRailLabel(rail) {
675
- switch (rail) {
676
- case "cashu":
677
- case "cashu_p2pk":
678
- return "ecash";
679
- case "x402":
680
- return "x402 stablecoin";
681
- case "tempo":
682
- return "Tempo stablecoin";
683
- }
684
- }
685
- function probeRailLabel(rail) {
686
- return rail === "lightning" ? "Lightning float" : fundingRailLabel(rail);
687
- }
688
- function railLabelAtSentenceStart(label) {
689
- return label === "ecash" ? "Ecash" : label;
690
- }
691
-
692
- // src/lib/wallet-rail-copy.ts
693
- function rpcEndpointSuffix(rail) {
694
- const url = rail.details?.rpc_url;
695
- return typeof url === "string" && url.length > 0 ? ` via ${url}` : "";
696
- }
697
- function rpcEndpointLine(rail) {
698
- const url = rail.details?.rpc_url;
699
- if (typeof url !== "string" || url.length === 0) return null;
700
- const source = rail.details?.rpc_source === "override" ? rpcOverrideEnvVar(rail.rail) : null;
701
- return `read from ${url} (${source ?? "bundled default"})`;
702
- }
703
- function unreachableRailDisplay(rail) {
704
- if (rail.error?.code === "x402_unsupported_network") {
705
- const network = rail.details?.network;
706
- return typeof network === "string" ? `x402 rail is configured for ${network}, a chain it can't settle on.` : "x402 rail is configured for a chain it can't settle on.";
707
- }
708
- return nwcRejectionDisplay(rail) ?? `${railLabelAtSentenceStart(probeRailLabel(rail.rail))} rail is configured but not reachable.`;
709
- }
710
- function nwcRejectionDisplay(rail) {
711
- switch (rail.error?.code) {
712
- case "float_unauthorized":
713
- return "The Lightning wallet no longer recognises this connection \u2014 it looks revoked or expired.";
714
- case "float_connection_restricted":
715
- return "The Lightning wallet recognises this connection but won't let it do what the agent needs.";
716
- default:
717
- return null;
718
- }
719
- }
720
- function unreachableRailHint(rail) {
721
- if (rail.error?.hint) return rail.error.hint;
722
- switch (rail.rail) {
723
- case "lightning":
724
- return "Re-run 'dvm wallet connect' with the connection string piped in.";
725
- case "cashu":
726
- return "Check mint health with 'dvm wallet mint-health'.";
727
- case "x402":
728
- return "Check DVM_X402_RPC_URL, or ensure viem is installed.";
729
- case "tempo":
730
- return "Check DVM_TEMPO_RPC_URL, or ensure viem is installed.";
731
- }
732
- }
733
- function tempoKeyImportRoutes(opts) {
734
- const force = opts.replacingConnectedKey ? " --force" : "";
735
- return `'dvm wallet tempo-connect --key-file <path>${force}', or DVM_TEMPO_KEY=<key> dvm wallet tempo-connect${force}`;
736
- }
737
- var TEMPO_EXIT_KEY_MISSING_HINT = `Reconnect that exact key (${tempoKeyImportRoutes({ replacingConnectedKey: false })}) before attempting an on-chain exit.`;
738
- function rpcOverrideEnvVar(rail) {
739
- switch (rail) {
740
- case "x402":
741
- return "DVM_X402_RPC_URL";
742
- case "tempo":
743
- return "DVM_TEMPO_RPC_URL";
744
- default:
745
- return null;
746
- }
747
- }
748
-
749
- // src/lib/payment-rails/tempo-session.ts
750
- function offersTempoCreditSession(menu) {
751
- return menu?.tempo?.methods.some(
752
- (candidate) => candidate.method === "tempo" && candidate.intent === "session"
753
- ) === true;
754
- }
755
- function offersTempoCreditCharge(menu) {
756
- return menu?.tempo?.methods.some(
757
- (candidate) => candidate.method === "tempo" && candidate.intent === "charge"
758
- ) === true;
759
- }
760
- function withholdsTempoCreditSession(menu) {
761
- return menu?.tempo?.withheld?.some(
762
- (candidate) => candidate.method === "tempo" && candidate.intent === "session"
763
- ) === true;
764
- }
765
- function offersTempoCreditFunding(menu) {
766
- return offersTempoCreditSession(menu) || offersTempoCreditCharge(menu);
767
- }
768
- function createTempoCreditSession(args) {
769
- return TempoSession2.Client.session({
770
- account: privateKeyToAccount(args.account.privateKey),
771
- channelStore: createTempoChannelStore({
772
- endpoint: args.endpoint,
773
- callerPubkey: args.callerPubkey,
774
- creditId: args.creditId,
775
- operator: args.operator ?? zeroAddress2
776
- }),
777
- maxDeposit: formatUnits(BigInt(args.maxDepositMicro), 6),
778
- topUpAmount: formatUnits(BigInt(args.topUpAmountMicro), 6),
779
- ...args.getClient ? { getClient: args.getClient } : {}
780
- });
781
- }
782
- function createTempoSessionPaymentFetch(method, fetch) {
783
- return Mppx.create({
784
- methods: [method],
785
- fetch,
786
- maxPaymentRetries: 5,
787
- polyfill: false
788
- }).fetch;
789
- }
790
- function tempoSessionChallengeChannelScope(challenge) {
791
- if (!challenge) return void 0;
792
- const request = challenge.request;
793
- if (typeof request?.recipient !== "string" || !isAddress2(request.recipient, { strict: false }) || typeof request.currency !== "string" || !isAddress2(request.currency, { strict: false })) {
794
- return void 0;
795
- }
796
- const methodDetails = request.methodDetails && typeof request.methodDetails === "object" ? request.methodDetails : void 0;
797
- const chainId = methodDetails?.chainId;
798
- if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId <= 0) {
799
- return void 0;
800
- }
801
- const challengeEscrow = [methodDetails?.escrowContract, methodDetails?.escrow].find(
802
- (candidate) => typeof candidate === "string" && isAddress2(candidate, { strict: false })
803
- );
804
- const escrow = challengeEscrow ?? TempoSession2.Precompile.Constants.tip20ChannelEscrow;
805
- const operator = methodDetails?.operator;
806
- if (operator !== void 0 && (typeof operator !== "string" || !isAddress2(operator, { strict: false }))) {
807
- return void 0;
808
- }
809
- return {
810
- channelKey: `${request.recipient.toLowerCase()}:${request.currency.toLowerCase()}:${escrow.toLowerCase()}:${String(chainId)}`,
811
- operator: operator?.toLowerCase() ?? zeroAddress2
812
- };
813
- }
814
- function tempoSessionCredentialChannelScope(credential) {
815
- try {
816
- return tempoSessionChallengeChannelScope(
817
- Credential.deserialize(credential).challenge
818
- );
819
- } catch {
820
- return void 0;
821
- }
822
- }
823
- function tempoSessionAssociationByKey(args) {
824
- return args.channelScope ? loadTempoChannelAssociationByKey(
825
- args.callerPubkey,
826
- args.channelScope.operator,
827
- args.channelScope.channelKey
828
- ) : loadTempoChannelAssociation(args.endpoint, args.callerPubkey, args.creditId);
829
- }
830
- function tempoSessionPriorCumulativeMicro(args) {
831
- const association = args.association ?? loadTempoChannelAssociation(args.endpoint, args.callerPubkey, args.creditId);
832
- if (!association?.opened) return 0;
833
- const prior = BigInt(association.cumulativeAmount);
834
- if (prior < 0n || prior > BigInt(Number.MAX_SAFE_INTEGER)) {
835
- throw new DvmError("payment_invalid", "The Tempo channel's cumulative amount is out of range.");
836
- }
837
- return Number(prior);
838
- }
839
- function tempoSessionCumulativeCapMicro(args) {
840
- const prior = BigInt(tempoSessionPriorCumulativeMicro(args));
841
- const amount = BigInt(Math.max(0, args.amountMicro));
842
- const explicitAllowance = BigInt(Math.max(args.allowanceMicro, args.amountMicro));
843
- const allowance = explicitAllowance > amount ? explicitAllowance : maxAdvertisedMicro(amount);
844
- const cap = prior + allowance;
845
- if (cap > BigInt(Number.MAX_SAFE_INTEGER)) {
846
- throw new DvmError("payment_invalid", "The Tempo session deposit cap is out of range.");
847
- }
848
- return Number(cap);
849
- }
850
- function tempoSessionCredentialDepositMicro(credential) {
851
- return readSessionDeposit(credential).depositMicro;
852
- }
853
- function assertTempoSessionDepositWithinCap(args) {
854
- const { action, depositMicro } = readSessionDeposit(args.credential);
855
- if (depositMicro === 0) return;
856
- const priorMicro = action === "topUp" ? Number(
857
- (args.channelScope ? loadTempoChannelAssociationByKey(
858
- args.callerPubkey,
859
- args.channelScope.operator,
860
- args.channelScope.channelKey
861
- ) : loadTempoChannelAssociation(args.endpoint, args.callerPubkey, args.creditId))?.deposit ?? 0
862
- ) : 0;
863
- const committedMicro = priorMicro + depositMicro;
864
- if (committedMicro <= args.capMicro) return;
865
- const committedUsd = committedMicro / 1e6;
866
- const capUsd = args.capMicro / 1e6;
867
- throw new DvmError(
868
- "tempo_session_deposit_exceeds_limit",
869
- `This Tempo session would lock $${formatUsd(committedUsd)} of channel collateral, above the $${formatUsd(capUsd)} this funding authorized.`,
870
- "Nothing was sent and no collateral moved. Fund a smaller amount, or ask the DVM operator why its session asks for a deposit this size.",
871
- {
872
- committed_usdc_microunits: String(committedMicro),
873
- max_usdc_microunits: String(args.capMicro),
874
- anchor: costAnchor(committedUsd),
875
- display: `Funding refused: the DVM's Tempo session would lock $${formatUsd(committedUsd)} of on-chain collateral, over the $${formatUsd(capUsd)} authorized for this funding.`
876
- }
877
- );
878
- }
879
- function assertTempoSessionChallengeWithinAsk(args) {
880
- const local = BigInt(Math.max(0, Math.round(args.basisMicro)));
881
- for (const challenge of args.challenges) {
882
- if (challenge.method !== "tempo" || challenge.intent !== "session") continue;
883
- const advertised = BigInt(tempoSessionChallengeMicro(challenge));
884
- if (withinAdvertisedTolerance(advertised, local)) continue;
885
- const advertisedUsd = Number(advertised) / 1e6;
886
- const localUsd = Number(local) / 1e6;
887
- throw new DvmError(
888
- "tempo_session_amount_exceeds_limit",
889
- `The DVM's Tempo session asks this funding to authorize $${formatUsd(advertisedUsd)}, more than 10% above the $${formatUsd(localUsd)} of credit it is buying.`,
890
- "No voucher was signed, so this DVM gained no authority to capture anything. If the funding had already opened or topped up its channel, that collateral is still yours \u2014 'dvm wallet balance' shows it and 'dvm wallet tempo-exit <dvm>' recovers it on-chain. Fund an amount this DVM prices, or ask its operator to check its BTC/USD rate.",
891
- {
892
- advertised_usdc_microunits: advertised.toString(),
893
- local_usdc_microunits: local.toString(),
894
- max_usdc_microunits: maxAdvertisedMicro(local).toString(),
895
- anchor: costAnchor(advertisedUsd),
896
- display: `Funding refused: the DVM's Tempo session wants a $${formatUsd(advertisedUsd)} voucher for $${formatUsd(localUsd)} of credit, over the 10% safety margin.`
897
- }
898
- );
899
- }
900
- }
901
- function tempoSessionCredentialCumulativeMicro(credential) {
902
- let payload;
903
- try {
904
- payload = Credential.deserialize(credential).payload;
905
- } catch {
906
- return void 0;
907
- }
908
- const { cumulativeAmount } = payload;
909
- if (typeof cumulativeAmount !== "string" || !/^\d+$/.test(cumulativeAmount)) return void 0;
910
- const micro = BigInt(cumulativeAmount);
911
- if (micro > BigInt(Number.MAX_SAFE_INTEGER)) {
912
- throw new DvmError("payment_invalid", "The Tempo session voucher amount is out of range.");
913
- }
914
- return Number(micro);
915
- }
916
- function assertTempoSessionVoucherWithinAsk(args) {
917
- const local = BigInt(Math.max(0, Math.round(args.basisMicro)));
918
- const authorized = BigInt(args.authorizedMicro);
919
- if (authorized <= 0n || withinAdvertisedTolerance(authorized, local)) return;
920
- const authorizedUsd = Number(authorized) / 1e6;
921
- const localUsd = Number(local) / 1e6;
922
- throw new DvmError(
923
- "tempo_session_amount_exceeds_limit",
924
- `This Tempo session voucher would let the DVM capture $${formatUsd(authorizedUsd)}, above the $${formatUsd(localUsd)} of credit this funding is buying.`,
925
- "The voucher never left this machine, so this DVM can capture nothing against it. Any channel collateral the funding already locked is still yours \u2014 'dvm wallet balance' shows it and 'dvm wallet tempo-exit <dvm>' recovers it on-chain. Fund an amount this DVM prices, or ask its operator why its session charges more than it quoted.",
926
- {
927
- advertised_usdc_microunits: authorized.toString(),
928
- local_usdc_microunits: local.toString(),
929
- max_usdc_microunits: maxAdvertisedMicro(local).toString(),
930
- anchor: costAnchor(authorizedUsd),
931
- display: `Funding refused: the DVM's Tempo session voucher would capture $${formatUsd(authorizedUsd)} for $${formatUsd(localUsd)} of credit, over the 10% safety margin.`
932
- }
933
- );
934
- }
935
- async function persistTempoOpenCredential(args) {
936
- try {
937
- const parsed = Credential.deserialize(args.credential);
938
- const payload = parsed.payload;
939
- if (payload.action !== "open" || typeof payload.channelId !== "string" || typeof payload.cumulativeAmount !== "string" || !payload.descriptor || typeof payload.descriptor !== "object") {
940
- return false;
941
- }
942
- const methodDetails = parsed.challenge.request.methodDetails;
943
- if (typeof methodDetails?.chainId !== "number") return false;
944
- const escrow = typeof methodDetails.escrowContract === "string" && isAddress2(methodDetails.escrowContract, { strict: false }) ? methodDetails.escrowContract : TempoSession2.Precompile.Constants.tip20ChannelEscrow;
945
- const descriptor = payload.descriptor;
946
- const channelId = TempoSession2.Precompile.Channel.computeId({
947
- ...descriptor,
948
- chainId: methodDetails.chainId,
949
- escrow
950
- });
951
- if (channelId.toLowerCase() !== payload.channelId.toLowerCase()) return false;
952
- const depositMicro = tempoSessionCredentialDepositMicro(args.credential);
953
- const existingAssociation = loadTempoChannelAssociation(
954
- args.endpoint,
955
- args.callerPubkey,
956
- args.creditId
957
- );
958
- const existing = existingAssociation ? await loadTempoChannelEntry(existingAssociation) : void 0;
959
- await createTempoChannelStore({
960
- endpoint: args.endpoint,
961
- callerPubkey: args.callerPubkey,
962
- creditId: args.creditId,
963
- operator: descriptor.operator
964
- }).set({
965
- channelId,
966
- cumulativeAmount: BigInt(payload.cumulativeAmount),
967
- deposit: BigInt(depositMicro),
968
- descriptor,
969
- escrow,
970
- chainId: methodDetails.chainId,
971
- opened: args.opened || existing?.opened === true && existing.channelId.toLowerCase() === channelId.toLowerCase()
972
- });
973
- return true;
974
- } catch (err) {
975
- if (err instanceof DvmError && err.code === "tempo_channel_scope_occupied") throw err;
976
- return false;
977
- }
978
- }
979
- function tempoSessionChallenge(err) {
980
- if (!(err instanceof DvmError) || !Array.isArray(err.data?.tempo_challenges)) return void 0;
981
- const challenge = err.data.tempo_challenges.find(
982
- (candidate) => candidate.method === "tempo" && candidate.intent === "session"
983
- );
984
- return challenge;
985
- }
986
- function tempoSessionChallengeMicro(challenge) {
987
- const advertised = challenge.request?.amount;
988
- if (typeof advertised !== "string" || !/^\d+$/.test(advertised)) {
989
- throw new DvmError(
990
- "payment_invalid",
991
- `The DVM advertised an unreadable Tempo session amount: '${String(advertised)}'.`,
992
- "Do not pay this. Ask the DVM operator to return a whole number of USDC micro-units."
993
- );
994
- }
995
- const micro = BigInt(advertised);
996
- if (micro > BigInt(Number.MAX_SAFE_INTEGER)) {
997
- throw new DvmError("payment_invalid", "The Tempo session challenge amount is out of range.");
998
- }
999
- return Number(micro);
1000
- }
1001
- async function createTempoCloseCredential(args) {
1002
- const entry = await loadTempoChannelEntry(args.association);
1003
- if (!entry) {
1004
- throw new DvmError(
1005
- "tempo_channel_not_found",
1006
- `The Tempo channel for credit ${args.association.creditId} is missing locally.`,
1007
- "Use 'dvm wallet tempo-exit <dvm>' only when the channel record is present; otherwise contact the DVM operator with the credit id."
1008
- );
1009
- }
1010
- if (BigInt(args.cumulativeAmountMicro) > entry.cumulativeAmount) {
1011
- throw new DvmError(
1012
- "tempo_close_amount_unauthorized",
1013
- `The DVM asked to close this Tempo channel at $${formatUsdcMicrounits(BigInt(args.cumulativeAmountMicro))}, above the $${formatUsdcMicrounits(entry.cumulativeAmount)} this wallet has authorized on it.`,
1014
- `No credential was signed and nothing is stranded: run 'dvm wallet tempo-exit <dvm>' to recover the channel's unsettled balance on-chain, and tell the DVM's operator that its close request exceeded the caller's authorization.`
1015
- );
1016
- }
1017
- const durableStore = createTempoChannelStore({
1018
- endpoint: args.association.endpoint,
1019
- callerPubkey: args.association.callerPubkey,
1020
- creditId: args.association.creditId,
1021
- operator: args.association.operator
1022
- });
1023
- const method = TempoSession2.Client.session({
1024
- account: privateKeyToAccount(args.account.privateKey),
1025
- // mppx normally deletes a channel as soon as it signs a manual close.
1026
- // The credential has not reached the DVM at that point, so keep the
1027
- // unilateral-exit record until wallet tempo-exit independently confirms the
1028
- // cooperative close on-chain and retires it.
1029
- channelStore: {
1030
- get: (key) => durableStore.get(key),
1031
- set: (next) => durableStore.set(next),
1032
- delete: () => void 0
1033
- },
1034
- ...args.getClient ? { getClient: args.getClient } : {}
1035
- });
1036
- return method.createCredential({
1037
- challenge: args.challenge,
1038
- context: {
1039
- action: "close",
1040
- descriptor: entry.descriptor,
1041
- cumulativeAmountRaw: String(args.cumulativeAmountMicro)
1042
- }
1043
- });
1044
- }
1045
- async function advanceTempoChannelExit(args) {
1046
- return withTempoChannelLock(() => advanceTempoChannelExitLocked(args));
1047
- }
1048
- async function reconcileTempoChannelOnChain(args) {
1049
- const { association } = args;
1050
- const unread = {
1051
- retired: false,
1052
- channelId: association.channelId,
1053
- depositMicro: Number(association.deposit),
1054
- settledMicro: 0
1055
- };
1056
- let entry;
1057
- let state;
1058
- try {
1059
- ({ entry, state } = await readTempoChannelState(args));
1060
- } catch (err) {
1061
- return { ...unread, status: "unreadable", error: asTempoReadError(err) };
1062
- }
1063
- const read = {
1064
- status: classifyTempoChannelState(entry, state),
1065
- channelId: entry.channelId,
1066
- depositMicro: Number(state.deposit),
1067
- settledMicro: Number(state.settled)
1068
- };
1069
- if (read.status !== "closed") return { ...read, retired: false };
1070
- if (args.observeOnly) return { ...read, retired: false };
1071
- try {
1072
- const retired = await withTempoChannelLock(async () => {
1073
- const current = loadTempoChannelAssociation(
1074
- association.endpoint,
1075
- association.callerPubkey,
1076
- association.creditId
1077
- );
1078
- if (!current) return true;
1079
- if (current.channelId.toLowerCase() !== association.channelId.toLowerCase()) return false;
1080
- await retireTempoChannelScope(current);
1081
- return true;
1082
- });
1083
- return { ...read, retired };
1084
- } catch (err) {
1085
- return { ...read, retired: false, error: asTempoReadError(err) };
1086
- }
1087
- }
1088
- async function advanceTempoChannelExitLocked(args) {
1089
- const association = loadTempoChannelAssociation(
1090
- args.association.endpoint,
1091
- args.association.callerPubkey,
1092
- args.association.creditId
1093
- );
1094
- if (association?.channelId.toLowerCase() !== args.association.channelId.toLowerCase()) {
1095
- throw new DvmError(
1096
- "tempo_channel_not_found",
1097
- `No local Tempo channel entry remains for credit ${args.association.creditId}.`
1098
- );
1099
- }
1100
- const entry = await loadTempoChannelEntry(association);
1101
- if (!entry) {
1102
- if (args.abandon) {
1103
- await retireTempoChannel(association);
1104
- return {
1105
- status: "abandoned",
1106
- descriptorMissing: true,
1107
- channelId: association.channelId
1108
- };
1109
- }
1110
- throw new DvmError(
1111
- "tempo_channel_not_found",
1112
- `The Tempo channel record for credit ${association.creditId} has no stored descriptor.`,
1113
- `Run 'dvm wallet tempo-exit ${association.endpoint} --abandon' to retire this unusable local record. Nothing will be broadcast, because there is no descriptor from which to address the escrow.`
1114
- );
1115
- }
1116
- const identity = { channelId: entry.channelId, channelToken: entry.descriptor.token };
1117
- const account = args.account;
1118
- const controller = tempoDescriptorController(entry.descriptor);
1119
- if (account && controller && !tempoChannelControlledBy(controller, account.address)) {
1120
- if (!args.abandon) throw foreignChannelExitError(association, controller, account);
1121
- await retireTempoChannelScope(association);
1122
- return {
1123
- status: "abandoned",
1124
- foreignPayer: controller.payer,
1125
- channelId: association.channelId
1126
- };
1127
- }
1128
- const rpc = await resolveTempoExitRpc(entry.chainId);
1129
- const client = args.client ?? (account ? await tempoWalletClient(account, rpc) : await tempoPublicClient(rpc));
1130
- const chain = redactingExitChain(
1131
- args.chain ?? TempoSession2.Precompile.Chain,
1132
- rpc,
1133
- entry.descriptor.token
1134
- );
1135
- let state = await chain.getChannelState(client, entry.channelId, entry.escrow);
1136
- const amounts = () => {
1137
- const authorized = entry.cumulativeAmount > state.settled ? entry.cumulativeAmount : state.settled;
1138
- return {
1139
- depositMicro: Number(state.deposit),
1140
- settledMicro: Number(state.settled),
1141
- guaranteedMicro: Number(state.deposit > authorized ? state.deposit - authorized : 0n),
1142
- // Clamped because the escrow zeroes a withdrawn channel's deposit while
1143
- // keeping its settled total, so a closed channel reads deposit 0 against
1144
- // a positive settled and the raw subtraction goes negative.
1145
- likelyMicro: Number(state.deposit > state.settled ? state.deposit - state.settled : 0n)
1146
- };
1147
- };
1148
- if (state.deposit === 0n) {
1149
- if (classifyTempoChannelState(entry, state) === "closed") {
1150
- await retireTempoChannelScope(association);
1151
- return { status: "already_closed", ...identity, ...amounts() };
1152
- }
1153
- if (args.abandon) {
1154
- await retireTempoChannel(association);
1155
- return { status: "abandoned", ...identity, ...amounts() };
1156
- }
1157
- return { status: "unconfirmed", ...identity, ...amounts() };
1158
- }
1159
- if (args.abandon) {
1160
- throw new DvmError(
1161
- "tempo_channel_on_chain",
1162
- `The Tempo channel for credit ${association.creditId} holds $${formatUsdcMicrounits(state.deposit)} on-chain, so its record cannot be abandoned.`,
1163
- account ? `Run 'dvm wallet tempo-exit' without --abandon to close it and recover the collateral.` : `No Tempo key is connected, so this record could not be checked against one. Reconnect the key that opened this channel (${tempoKeyImportRoutes({ replacingConnectedKey: false })}) and run 'dvm wallet tempo-exit' without --abandon to recover the collateral. Retiring the record is offered only once a connected key shows the channel is not this wallet's.`
1164
- );
1165
- }
1166
- if (!account) throw tempoExitKeyMissingError();
1167
- const exitAccount = privateKeyToAccount(account.privateKey);
1168
- const transactionOptions = {
1169
- account: exitAccount,
1170
- feePayer: exitAccount,
1171
- candidateFeeTokens: [entry.descriptor.token]
1172
- };
1173
- if (state.closeRequestedAt === 0) {
1174
- const hash2 = await chain.requestCloseOnChain(
1175
- client,
1176
- entry.descriptor,
1177
- entry.escrow,
1178
- transactionOptions
1179
- );
1180
- await chain.waitForSuccessfulReceipt(client, hash2);
1181
- state = await chain.getChannelState(client, entry.channelId, entry.escrow);
1182
- return {
1183
- status: "close_requested",
1184
- ...identity,
1185
- transactionHash: hash2,
1186
- withdrawAvailableAt: (state.closeRequestedAt + 900) * 1e3,
1187
- ...amounts()
1188
- };
1189
- }
1190
- const withdrawAvailableAt = state.closeRequestedAt + 900;
1191
- const now = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
1192
- if (now < withdrawAvailableAt) {
1193
- return {
1194
- status: "waiting",
1195
- ...identity,
1196
- withdrawAvailableAt: withdrawAvailableAt * 1e3,
1197
- ...amounts()
1198
- };
1199
- }
1200
- const withdrawnAmounts = amounts();
1201
- const hash = await chain.withdrawOnChain(
1202
- client,
1203
- entry.descriptor,
1204
- entry.escrow,
1205
- transactionOptions
1206
- );
1207
- await chain.waitForSuccessfulReceipt(client, hash);
1208
- state = await chain.getChannelState(client, entry.channelId, entry.escrow);
1209
- await retireTempoChannelScope(association);
1210
- return {
1211
- status: "withdrawn",
1212
- ...identity,
1213
- transactionHash: hash,
1214
- ...withdrawnAmounts
1215
- };
1216
- }
1217
- function foreignChannelExitError(association, controller, account) {
1218
- const abandon = [
1219
- "dvm wallet tempo-exit",
1220
- shellQuoteArg(association.endpoint),
1221
- "--credit-id",
1222
- shellQuoteArg(association.creditId),
1223
- "--abandon"
1224
- ].join(" ");
1225
- return new DvmError(
1226
- "tempo_wallet_mismatch",
1227
- `The connected Tempo key ${account.address} is not the wallet recorded for the channel with ${association.endpoint} \u2014 that channel was opened by ${controller.payer}.`,
1228
- // `--force` is live here and only here among the exit refusals: reaching
1229
- // this error took a connected `account`, so the reconnect is a replacement
1230
- // the import refuses without it.
1231
- `Nothing was signed or broadcast, and the channel's collateral is untouched \u2014 only ${controller.payer} can move it. Reconnect that exact key (${tempoKeyImportRoutes({ replacingConnectedKey: true })}) and retry, or run '${abandon}' to retire the local record. Retiring it discards this machine's only descriptor for that channel, so restoring the key afterwards would no longer recover the collateral; the record blocks nothing while it stands.`,
1232
- {
1233
- anchor: "tempo_channel_payer_mismatch",
1234
- endpoint: association.endpoint,
1235
- credit_id: association.creditId,
1236
- channel_id: association.channelId,
1237
- recorded_payer: controller.payer,
1238
- recorded_authorized_signer: controller.authorizedSigner,
1239
- connected_payer: account.address
1240
- }
1241
- );
1242
- }
1243
- function tempoExitKeyMissingError() {
1244
- return new DvmError(
1245
- "tempo_wallet_missing",
1246
- "The Tempo key that controls this channel is not connected.",
1247
- TEMPO_EXIT_KEY_MISSING_HINT
1248
- );
1249
- }
1250
- async function readTempoChannelState(args) {
1251
- const entry = await loadTempoChannelEntry(args.association);
1252
- if (!entry) {
1253
- throw new DvmError(
1254
- "tempo_channel_not_found",
1255
- `The Tempo channel record for credit ${args.association.creditId} has no stored descriptor.`,
1256
- `That channel can no longer be read or closed from here. Run 'dvm wallet tempo-exit ${args.association.endpoint} --abandon' to retire the unusable local record without broadcasting anything, then retry the wallet change.`
1257
- );
1258
- }
1259
- const rpc = await resolveTempoExitRpc(entry.chainId);
1260
- const client = args.client ?? await tempoPublicClient(rpc);
1261
- const chain = args.chain ?? TempoSession2.Precompile.Chain;
1262
- const state = await guardTempoChannelRead(
1263
- rpc,
1264
- () => chain.getChannelState(client, entry.channelId, entry.escrow)
1265
- );
1266
- return { entry, state };
1267
- }
1268
- function asTempoReadError(err) {
1269
- if (err instanceof DvmError) return err;
1270
- return new DvmError(
1271
- "tempo_chain_read_failed",
1272
- `The Tempo channel state could not be read: ${redactUrlsInText(
1273
- err instanceof Error ? err.message : String(err)
1274
- )}`
1275
- );
1276
- }
1277
- function classifyTempoChannelState(entry, state) {
1278
- if (state.deposit > 0n) return state.closeRequestedAt === 0 ? "open" : "closing";
1279
- return entry.opened || state.settled > 0n || state.closeRequestedAt !== 0 ? "closed" : "unconfirmed";
1280
- }
1281
- async function guardTempoChannelRead(rpc, run) {
1282
- try {
1283
- return await run();
1284
- } catch (err) {
1285
- if (err instanceof DvmError) throw err;
1286
- const cause = redactUrlsInText(err instanceof Error ? err.message : String(err));
1287
- if (!isTransportFailure(err)) {
1288
- throw new DvmError(
1289
- "tempo_chain_read_failed",
1290
- `The Tempo channel state could not be read: ${cause}`,
1291
- "This failed on-chain rather than in transit, so changing DVM_TEMPO_RPC_URL will not help. The local channel record was kept; 'dvm wallet tempo-exit <dvm>' re-reads the chain and reports where the channel actually is."
1292
- );
1293
- }
1294
- throw new DvmError(
1295
- "tempo_rpc_error",
1296
- `The Tempo channel state could not be read: ${cause}`,
1297
- rpc.source === "override" ? `DVM_TEMPO_RPC_URL sent this read to ${redactUrl(rpc.url)}. Correct it, or unset it to fall back to Tempo's bundled endpoint, then re-run \u2014 the local channel record was kept until the chain can be read.` : `Verify connectivity to Tempo's bundled endpoint (${redactUrl(rpc.url)}), or send the read somewhere else with DVM_TEMPO_RPC_URL, then re-run \u2014 the local channel record was kept until the chain can be read.`
1298
- );
1299
- }
1300
- }
1301
- async function resolveTempoExitRpc(chainId) {
1302
- const { tempo, tempoModerato } = await import("viem/chains");
1303
- const chain = chainId === tempo.id ? tempo : chainId === tempoModerato.id ? tempoModerato : null;
1304
- if (!chain) {
1305
- throw new DvmError(
1306
- "tempo_chain_unsupported",
1307
- `The recorded Tempo channel uses unsupported chain ${String(chainId)}.`
1308
- );
1309
- }
1310
- const { url, source } = resolveRpcOverride(
1311
- process.env.DVM_TEMPO_RPC_URL,
1312
- chain.rpcUrls.default.http[0]
1313
- );
1314
- return { chain, url, source };
1315
- }
1316
- async function tempoPublicClient(rpc) {
1317
- const { createPublicClient, http } = await import("viem");
1318
- const transportOptions = resolveRpcHttpTransportOptions(
1319
- rpc.source,
1320
- process.env.DVM_TEMPO_RPC_HEADERS_JSON
1321
- );
1322
- return createPublicClient({
1323
- chain: rpc.chain,
1324
- transport: transportOptions ? http(rpc.url, transportOptions) : http(rpc.url)
1325
- });
1326
- }
1327
- async function tempoWalletClient(account, rpc) {
1328
- const { createWalletClient, http } = await import("viem");
1329
- const transportOptions = resolveRpcHttpTransportOptions(
1330
- rpc.source,
1331
- process.env.DVM_TEMPO_RPC_HEADERS_JSON
1332
- );
1333
- return createWalletClient({
1334
- account: privateKeyToAccount(account.privateKey),
1335
- chain: rpc.chain,
1336
- transport: transportOptions ? http(rpc.url, transportOptions) : http(rpc.url)
1337
- });
1338
- }
1339
- function readSessionDeposit(credential) {
1340
- let payload;
1341
- try {
1342
- payload = Credential.deserialize(credential).payload;
1343
- } catch {
1344
- return { depositMicro: 0 };
1345
- }
1346
- const action = typeof payload.action === "string" ? payload.action : void 0;
1347
- if (action === "topUp") {
1348
- if (typeof payload.additionalDeposit !== "string" || !/^\d+$/.test(payload.additionalDeposit)) {
1349
- throw unreadableDeposit("top-up");
1350
- }
1351
- return { action, depositMicro: depositAsMicro(BigInt(payload.additionalDeposit)) };
1352
- }
1353
- if (action !== "open") return { ...action ? { action } : {}, depositMicro: 0 };
1354
- if (typeof payload.transaction !== "string") throw unreadableDeposit("channel open");
1355
- let calls = [];
1356
- try {
1357
- const deserialized = Transaction.deserialize(payload.transaction);
1358
- if ("calls" in deserialized && Array.isArray(deserialized.calls)) calls = deserialized.calls;
1359
- } catch {
1360
- throw unreadableDeposit("channel open");
1361
- }
1362
- for (const call of calls) {
1363
- if (typeof call.data !== "string") continue;
1364
- try {
1365
- const decoded = decodeFunctionData({
1366
- abi: TempoSession2.Precompile.escrowAbi,
1367
- data: call.data
1368
- });
1369
- if (decoded.functionName !== "open") continue;
1370
- return { action, depositMicro: depositAsMicro(decoded.args[3]) };
1371
- } catch {
1372
- continue;
1373
- }
1374
- }
1375
- throw unreadableDeposit("channel open");
1376
- }
1377
- function unreadableDeposit(leg) {
1378
- return new DvmError(
1379
- "tempo_session_deposit_unreadable",
1380
- `The Tempo session ${leg} credential does not state the deposit it commits.`,
1381
- "Nothing was sent. Retry the funding; if it recurs, the DVM's session challenge is malformed and its operator should be told."
1382
- );
1383
- }
1384
- function depositAsMicro(deposit) {
1385
- if (deposit < 0n || deposit > BigInt(Number.MAX_SAFE_INTEGER)) {
1386
- throw new DvmError("payment_invalid", "The Tempo session channel deposit is out of range.");
1387
- }
1388
- return Number(deposit);
1389
- }
1390
- var TRANSPORT_ERROR_NAMES = /* @__PURE__ */ new Set([
1391
- "HttpRequestError",
1392
- "ResponseBodyTooLargeError",
1393
- "RpcRequestError",
1394
- "SocketClosedError",
1395
- "TimeoutError",
1396
- "UrlRequiredError",
1397
- "WebSocketRequestError"
1398
- ]);
1399
- var TRANSPORT_ERROR_CODES = /* @__PURE__ */ new Set([
1400
- "EAI_AGAIN",
1401
- "ECONNREFUSED",
1402
- "ECONNRESET",
1403
- "ENOTFOUND",
1404
- "EPIPE",
1405
- "ETIMEDOUT"
1406
- ]);
1407
- var REVERT_ERROR_NAMES = /* @__PURE__ */ new Set(["ContractFunctionRevertedError", "ExecutionRevertedError"]);
1408
- var EXECUTION_REVERTED_RPC_CODE = 3;
1409
- function isRevertLink(err) {
1410
- if (REVERT_ERROR_NAMES.has(err.name)) return true;
1411
- const { code } = err;
1412
- return code === EXECUTION_REVERTED_RPC_CODE;
1413
- }
1414
- function isTransportLink(err) {
1415
- if (TRANSPORT_ERROR_NAMES.has(err.name)) return true;
1416
- const { code } = err;
1417
- return typeof code === "string" && TRANSPORT_ERROR_CODES.has(code);
1418
- }
1419
- function isTransportFailure(err) {
1420
- let transport = false;
1421
- let cursor = err;
1422
- for (let depth = 0; cursor instanceof Error && depth < 8; depth += 1) {
1423
- if (isRevertLink(cursor)) return false;
1424
- transport ||= isTransportLink(cursor);
1425
- cursor = cursor.cause;
1426
- }
1427
- return transport;
1428
- }
1429
- function redactingExitChain(chain, rpc, token) {
1430
- const guard = async (run) => {
1431
- try {
1432
- return await run();
1433
- } catch (err) {
1434
- if (err instanceof DvmError) throw err;
1435
- const cause = redactUrlsInText(err instanceof Error ? err.message : String(err));
1436
- if (!isTransportFailure(err)) {
1437
- throw new DvmError(
1438
- "tempo_exit_failed",
1439
- `Tempo channel exit failed: ${cause}`,
1440
- `This failed on-chain rather than in transit, so changing DVM_TEMPO_RPC_URL will not help \u2014 a transaction the escrow rejected reads like this, and so does one this wallet cannot pay for. The exit is caller-paid and Tempo charges its fees in a stablecoin rather than a separate gas token, so check that the wallet holds a spendable balance of ${tempoTokenReference(token)} outside the channel. ${tempoBalanceCheckHint(token)} Then re-run 'dvm wallet tempo-exit' \u2014 it re-reads the chain and resumes from wherever the channel actually is. If it fails the same way with the wallet funded, the escrow is refusing the exit and the channel needs a closer look.`
1441
- );
1442
- }
1443
- throw new DvmError(
1444
- "tempo_rpc_error",
1445
- `Tempo channel exit failed: ${cause}`,
1446
- // Which knob is the caller's to turn depends on whose endpoint this was.
1447
- rpc.source === "override" ? `DVM_TEMPO_RPC_URL sent this exit to ${redactUrl(rpc.url)}. Correct it, or unset it to fall back to Tempo's bundled endpoint, then re-run 'dvm wallet tempo-exit' \u2014 it re-reads the chain and resumes from wherever the channel actually is.` : `Verify connectivity to Tempo's bundled endpoint (${redactUrl(rpc.url)}), or send the exit somewhere else with DVM_TEMPO_RPC_URL, then re-run 'dvm wallet tempo-exit' \u2014 it re-reads the chain and resumes from wherever the channel actually is.`
1448
- );
1449
- }
1450
- };
1451
- return {
1452
- getChannelState: (...args) => guard(() => chain.getChannelState(...args)),
1453
- requestCloseOnChain: (...args) => guard(() => chain.requestCloseOnChain(...args)),
1454
- waitForSuccessfulReceipt: (...args) => guard(() => chain.waitForSuccessfulReceipt(...args)),
1455
- withdrawOnChain: (...args) => guard(() => chain.withdrawOnChain(...args))
1456
- };
1457
- }
1458
-
1459
- // src/lib/payment-rails/tempo-lifecycle.ts
1460
- async function assertTempoWalletIdle(intent) {
1461
- const connected = await connectedTempoAccount();
1462
- if (!connected) return;
1463
- const records = await ownedTempoChannelRecords(connected.address);
1464
- const verdicts = await Promise.all(
1465
- records.map(async (association) => ({
1466
- association,
1467
- chain: await reconcileTempoChannelOnChain({ association })
1468
- }))
1469
- );
1470
- let blocker;
1471
- for (const status of BLOCKING_ORDER) {
1472
- blocker = verdicts.find((verdict) => verdict.chain.status === status);
1473
- if (blocker) break;
1474
- }
1475
- if (!blocker) return;
1476
- const blocking = verdicts.filter((verdict) => verdict.chain.status !== "closed").length;
1477
- throw blocker.chain.status === "unreadable" ? blocker.chain.error?.code === "tempo_channel_not_found" ? descriptorlessChannelError(blocker.association, blocker.chain, intent, blocking) : unreadableChainError(blocker.association, blocker.chain, intent, blocking) : liveChannelError(blocker.association, blocker.chain, intent, blocking);
1478
- }
1479
- async function ownedTempoChannelRecords(address) {
1480
- const records = listTempoChannelAssociations();
1481
- const controlled = await Promise.all(
1482
- records.map(
1483
- async (association) => tempoChannelControlledBy(
1484
- await loadTempoChannelController(association).catch(() => void 0),
1485
- address
1486
- )
1487
- )
1488
- );
1489
- return records.filter((_, index) => controlled[index]);
1490
- }
1491
- var BLOCKING_ORDER = ["open", "closing", "unconfirmed", "unreadable"];
1492
- function liveChannelError(association, chain, intent, blocking) {
1493
- const depositMicro = chain.status === "unconfirmed" ? Number(association.deposit) : chain.depositMicro;
1494
- const amount = `$${formatUsdcMicrounits(BigInt(depositMicro))}`;
1495
- const message = chain.status === "open" ? `The connected Tempo key still holds ${amount} of collateral in an open channel with ${association.endpoint}.` : chain.status === "closing" ? `The connected Tempo key has ${amount} still escrowed on its channel with ${association.endpoint}, where a close has been requested on-chain and is in its 15-minute grace period.` : `The connected Tempo key signed an open committing ${amount} to a channel with ${association.endpoint} that the escrow has no record of, so that collateral may or may not be locked.`;
1496
- const caller = findSigningIdentityByPubkey(association.callerPubkey);
1497
- if (!caller) {
1498
- return new DvmError(
1499
- "tempo_channels_active",
1500
- message,
1501
- `Only this key can close that channel, but its caller identity (${association.callerPubkey}) is no longer configured. Restore that identity first, then retry the wallet ${intent === "replace" ? "replacement" : "disconnect"}; the key and channel record were left untouched.${remainingClause(blocking)}`,
1502
- channelData(association, chain, depositMicro, blocking)
1503
- );
1504
- }
1505
- const identityName = caller.source.kind === "env" ? void 0 : caller.source.name;
1506
- const selectors = { creditId: association.creditId, identityName };
1507
- const exit = tempoExitCommand(association.endpoint, selectors);
1508
- const abandon = tempoExitCommand(association.endpoint, selectors, true);
1509
- const fund = creditCommand("fund", association.endpoint, selectors);
1510
- const remedy = chain.status === "open" ? (
1511
- // The endpoint can hold several credits and caller identities. Both
1512
- // selectors keep every remedy pointed at the channel just named.
1513
- `Empty the channel first \u2014 '${creditCommand("drain", association.endpoint, selectors)}' for the cooperative exit, or '${exit}' if the DVM is unavailable`
1514
- ) : chain.status === "closing" ? `'${exit}' withdraws what remains once the grace period ends` : `'${exit}' reads the chain and says which it is, and '${fund}' resumes a funding still pending for it. Once that open is known never to have landed, '${abandon}' retires the record`;
1515
- return new DvmError(
1516
- "tempo_channels_active",
1517
- message,
1518
- `Only this key can close that channel. ${remedy} \u2014 and ${intentClause(intent)}.${remainingClause(blocking)}`,
1519
- channelData(association, chain, depositMicro, blocking)
1520
- );
1521
- }
1522
- function tempoExitCommand(endpoint, selectors, abandon = false) {
1523
- const parts = [
1524
- "dvm wallet tempo-exit",
1525
- shellQuoteArg(endpoint),
1526
- "--credit-id",
1527
- shellQuoteArg(selectors.creditId)
1528
- ];
1529
- if (selectors.identityName !== void 0) {
1530
- parts.push("--as", shellQuoteArg(selectors.identityName));
1531
- }
1532
- if (abandon) parts.push("--abandon");
1533
- return parts.join(" ");
1534
- }
1535
- function creditCommand(verb, endpoint, selectors) {
1536
- const parts = [
1537
- `dvm credit ${verb}`,
1538
- shellQuoteArg(endpoint),
1539
- "--credit-id",
1540
- shellQuoteArg(selectors.creditId)
1541
- ];
1542
- if (selectors.identityName !== void 0) {
1543
- parts.push("--as", shellQuoteArg(selectors.identityName));
1544
- }
1545
- return parts.join(" ");
1546
- }
1547
- function unreadableChainError(association, chain, intent, blocking) {
1548
- const cause = chain.error;
1549
- return new DvmError(
1550
- cause?.code ?? "tempo_chain_read_failed",
1551
- `The Tempo channel with ${association.endpoint} could not be read, so there is no way to show it is empty: ${cause?.message ?? "the chain read failed"}`,
1552
- `The Tempo key is untouched, and the local channel record was kept \u2014 it is the only route back to any collateral the escrow does hold. ${cause?.hint ? `${cause.hint} Then ${intentClause(intent)}.` : `Restore the chain read, then ${intentClause(intent)}.`}${remainingClause(blocking)}`,
1553
- channelData(association, chain, Number(association.deposit), blocking)
1554
- );
1555
- }
1556
- function descriptorlessChannelError(association, chain, intent, blocking) {
1557
- const message = `The saved Tempo channel with ${association.endpoint} has no stored descriptor, so its escrow state cannot be read or closed from this machine.`;
1558
- const data = {
1559
- ...channelData(association, chain, Number(association.deposit), blocking),
1560
- anchor: "tempo_channel_descriptor_missing"
1561
- };
1562
- const caller = findSigningIdentityByPubkey(association.callerPubkey);
1563
- if (!caller) {
1564
- return new DvmError(
1565
- "tempo_channel_not_found",
1566
- message,
1567
- `The caller identity that owns this record (${association.callerPubkey}) is no longer configured. Restore that identity first, then retire the descriptor-less record; the Tempo key and record were left untouched.${remainingClause(blocking)}`,
1568
- data
1569
- );
1570
- }
1571
- const identityName = caller.source.kind === "env" ? void 0 : caller.source.name;
1572
- const abandon = tempoExitCommand(
1573
- association.endpoint,
1574
- { creditId: association.creditId, identityName },
1575
- true
1576
- );
1577
- return new DvmError(
1578
- "tempo_channel_not_found",
1579
- message,
1580
- `Run '${abandon}' to retire this unusable local record without broadcasting anything, then ${intentClause(intent)}.${remainingClause(blocking)}`,
1581
- data
1582
- );
1583
- }
1584
- function channelData(association, chain, depositMicro, blocking) {
1585
- return {
1586
- anchor: "tempo_channel_live",
1587
- endpoint: association.endpoint,
1588
- credit_id: association.creditId,
1589
- channel_id: association.channelId,
1590
- chain_id: association.chainId,
1591
- chain_status: chain.status,
1592
- deposit_micro: depositMicro,
1593
- ...chain.status === "unreadable" ? {} : { settled_micro: chain.settledMicro },
1594
- // How many records are still in the way, so an agent knows whether clearing
1595
- // the one named above is the whole job.
1596
- blocking_channels: blocking
1597
- };
1598
- }
1599
- function remainingClause(blocking) {
1600
- return blocking > 1 ? ` ${blocking - 1} other recorded channel${blocking > 2 ? "s" : ""} still ${blocking > 2 ? "block" : "blocks"} this too \u2014 'dvm wallet balance' lists them all with what the escrow says about each.` : "";
1601
- }
1602
- function intentClause(intent) {
1603
- return intent === "replace" ? "retry the replacement" : "retry 'dvm wallet tempo-disconnect'";
1604
- }
1605
-
1606
- export {
1607
- TEMPO_CHARGE_PENDING_STALE_MS,
1608
- withTempoChannelLock,
1609
- listTempoChannelAssociations,
1610
- loadTempoChannelAssociation,
1611
- loadTempoChannelEntry,
1612
- loadTempoChannelController,
1613
- tempoChannelControlledBy,
1614
- tempoChargeCredentialSettleable,
1615
- recordPendingTempoChargeFunding,
1616
- loadResumableTempoChargeFunding,
1617
- listResumableTempoChargeFundings,
1618
- clearPendingTempoChargeFunding,
1619
- shellQuoteArg,
1620
- isPaymentRailKey,
1621
- fundingRailLabel,
1622
- paymentRailLabel,
1623
- probeRailLabel,
1624
- railLabelAtSentenceStart,
1625
- rpcEndpointSuffix,
1626
- rpcEndpointLine,
1627
- unreachableRailDisplay,
1628
- nwcRejectionDisplay,
1629
- unreachableRailHint,
1630
- tempoKeyImportRoutes,
1631
- TEMPO_EXIT_KEY_MISSING_HINT,
1632
- IDENTITIES_VERSION,
1633
- identitiesFileExists,
1634
- loadIdentities,
1635
- saveIdentities,
1636
- withIdentitiesLock,
1637
- generateIdentity,
1638
- deriveIdentityFromPrivkey,
1639
- resolveSigningIdentity,
1640
- findSigningIdentityByPubkey,
1641
- assertTempoWalletIdle,
1642
- offersTempoCreditSession,
1643
- offersTempoCreditCharge,
1644
- withholdsTempoCreditSession,
1645
- offersTempoCreditFunding,
1646
- createTempoCreditSession,
1647
- createTempoSessionPaymentFetch,
1648
- tempoSessionChallengeChannelScope,
1649
- tempoSessionCredentialChannelScope,
1650
- tempoSessionAssociationByKey,
1651
- tempoSessionPriorCumulativeMicro,
1652
- tempoSessionCumulativeCapMicro,
1653
- assertTempoSessionDepositWithinCap,
1654
- assertTempoSessionChallengeWithinAsk,
1655
- tempoSessionCredentialCumulativeMicro,
1656
- assertTempoSessionVoucherWithinAsk,
1657
- persistTempoOpenCredential,
1658
- tempoSessionChallenge,
1659
- tempoSessionChallengeMicro,
1660
- createTempoCloseCredential,
1661
- advanceTempoChannelExit,
1662
- reconcileTempoChannelOnChain
1663
- };