@dvmkit/sdk 0.1.0-rc.2 → 0.1.0-rc.3

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 (42) hide show
  1. package/README.md +10 -2
  2. package/dist/{revenue-reporter-GB4WKLDC.js → chunk-2ABMGUDS.js} +1 -0
  3. package/dist/chunk-5SO7ZVOH.js +375 -0
  4. package/dist/chunk-66HGCPBU.js +25 -0
  5. package/dist/{chunk-KQAJVVZT.js → chunk-AZBXSXQT.js} +27 -287
  6. package/dist/chunk-C2DC4FKR.js +21424 -0
  7. package/dist/{chunk-H25M54MI.js → chunk-C3MTFLC6.js} +16 -0
  8. package/dist/{chunk-OJ5WFIB2.js → chunk-EXHBXA4U.js} +1 -1
  9. package/dist/chunk-FT7IM66W.js +1557 -0
  10. package/dist/{chunk-KXWROQGK.js → chunk-FUJ36YDV.js} +1 -24
  11. package/dist/{tempo-charge-store-6GJEMNUU.js → chunk-JZWELPFH.js} +1 -0
  12. package/dist/{chunk-365P52XQ.js → chunk-LWUR4CGG.js} +2 -1
  13. package/dist/chunk-RHP3BRTH.js +1090 -0
  14. package/dist/{tempo-session-store-FTEEGZXA.js → chunk-RU7SXHLO.js} +2 -1
  15. package/dist/{chunk-7IH5SG2A.js → chunk-TKA6ZP4M.js} +62 -41
  16. package/dist/chunk-TVI4V7GF.js +283 -0
  17. package/dist/{payout-reporter-4TNWRS5F.js → chunk-X3IKFWJA.js} +3 -2
  18. package/dist/{chunk-DCNT4PJS.js → chunk-XY5Y5REG.js} +6 -258
  19. package/dist/chunk-XYTSDAPH.js +232 -0
  20. package/dist/chunk-YD3TZNXV.js +1042 -0
  21. package/dist/{credit-ledger-RO4FGSHG.js → credit-ledger-ED6JXKVD.js} +2 -2
  22. package/dist/credit-menu-DONAtGVf.d.ts +5076 -0
  23. package/dist/{ssrf-DZi-xJyn.d.ts → fx-Bq4cvn16.d.ts} +37 -119
  24. package/dist/index.d.ts +8 -66
  25. package/dist/index.js +11 -219
  26. package/dist/internal/index.d.ts +5745 -0
  27. package/dist/internal/index.js +6379 -0
  28. package/dist/{job-store-6gR4pZRP.d.ts → job-store-m2pYmvbr.d.ts} +1493 -12
  29. package/dist/{memory-credit-ledger-I2G64DDK.js → memory-credit-ledger-XJ5VQEVP.js} +3 -3
  30. package/dist/payout-reporter-3UB5WRCV.js +13 -0
  31. package/dist/revenue-reporter-JIKUPXOK.js +7 -0
  32. package/dist/server/index.d.ts +14 -3590
  33. package/dist/server/index.js +309 -20796
  34. package/dist/ssrf-DbFkpDv0.d.ts +118 -0
  35. package/dist/tempo-charge-store-RIFTALZK.js +8 -0
  36. package/dist/tempo-session-store-DALMRIWN.js +11 -0
  37. package/dist/testing/index.d.ts +3 -2
  38. package/dist/testing/index.js +3 -2
  39. package/dist/usd-DjVAPMlf.d.ts +97 -0
  40. package/dist/x402-5EVIUSEP.js +81 -0
  41. package/package.json +6 -2
  42. package/dist/x402-35VLYFKZ.js +0 -1272
@@ -0,0 +1,1042 @@
1
+ import {
2
+ fundingReachableAt,
3
+ isValidEvmPrivateKey,
4
+ listCredits,
5
+ loadConfig,
6
+ maxAdvertisedMicro,
7
+ normalizeCreditEndpoint,
8
+ withinAdvertisedTolerance
9
+ } from "./chunk-FT7IM66W.js";
10
+ import {
11
+ DvmError,
12
+ IDENTITIES_FILE,
13
+ TEMPO_CHANNELS_FILE,
14
+ costAnchor,
15
+ ensureConfigDir,
16
+ formatUsd,
17
+ resolveRpcHttpTransportOptions,
18
+ resolveRpcOverride
19
+ } from "./chunk-5SO7ZVOH.js";
20
+ import {
21
+ redactUrl,
22
+ redactUrlsInText
23
+ } from "./chunk-FUJ36YDV.js";
24
+
25
+ // src/lib/payment-rails/tempo-channel-store.ts
26
+ import { AsyncLocalStorage } from "async_hooks";
27
+ import { randomUUID } from "crypto";
28
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
29
+ import { Session as TempoSession } from "mppx/tempo";
30
+ import lockfile from "proper-lockfile";
31
+ import { isAddress, zeroAddress } from "viem";
32
+ var TEMPO_CHARGE_PENDING_STALE_MS = 24 * 60 * 60 * 1e3;
33
+ var tempoLockOwner = new AsyncLocalStorage();
34
+ async function withTempoChannelLock(operation) {
35
+ if (tempoLockOwner.getStore()?.active) return operation();
36
+ ensureTempoChannelFile();
37
+ const release = await lockfile.lock(TEMPO_CHANNELS_FILE, {
38
+ realpath: false,
39
+ retries: { retries: 20, minTimeout: 25, maxTimeout: 250 }
40
+ });
41
+ const owner = { active: true };
42
+ try {
43
+ return await tempoLockOwner.run(owner, operation);
44
+ } finally {
45
+ owner.active = false;
46
+ await release();
47
+ }
48
+ }
49
+ function createTempoChannelStore(context) {
50
+ let operator = context.operator === void 0 ? void 0 : normalizeTempoChannelOperator(context.operator);
51
+ return TempoSession.Client.createJsonChannelStore({
52
+ get(key) {
53
+ if (operator === void 0) return void 0;
54
+ return loadFile().values[channelStorageKey(context.callerPubkey, operator, key)];
55
+ },
56
+ set(key, value) {
57
+ const file = loadFile();
58
+ const entry = JSON.parse(value);
59
+ const entryOperator = normalizeTempoChannelOperator(entry.descriptor?.operator);
60
+ if (operator !== void 0 && operator !== entryOperator) {
61
+ throw new DvmError(
62
+ "payment_invalid",
63
+ "The Tempo channel operator does not match the session challenge."
64
+ );
65
+ }
66
+ operator = entryOperator;
67
+ const storedKey = channelStorageKey(context.callerPubkey, operator, key);
68
+ if (Object.hasOwn(file.values, storedKey)) {
69
+ const occupiedValue = file.values[storedKey];
70
+ const occupied = JSON.parse(occupiedValue);
71
+ if (typeof occupied.channelId !== "string" || occupied.channelId.toLowerCase() !== entry.channelId.toLowerCase()) {
72
+ throw new DvmError(
73
+ "tempo_channel_scope_occupied",
74
+ "A different Tempo channel already occupies this payment scope.",
75
+ "Exit the existing Tempo channel before retrying this funding. Its local recovery descriptor was preserved.",
76
+ {
77
+ occupied_channel_id: typeof occupied.channelId === "string" ? occupied.channelId : void 0,
78
+ refused_channel_id: entry.channelId
79
+ },
80
+ "Funding stopped before broadcast because this Tempo payment scope still holds another recoverable channel."
81
+ );
82
+ }
83
+ }
84
+ file.values[storedKey] = value;
85
+ if (context.endpoint && context.creditId) {
86
+ const updatedAt = Date.now();
87
+ const association = {
88
+ endpoint: normalizeCreditEndpoint(context.endpoint),
89
+ callerPubkey: context.callerPubkey,
90
+ creditId: context.creditId,
91
+ channelKey: key.startsWith("chan:") ? key.slice(5) : key,
92
+ operator,
93
+ channelId: entry.channelId,
94
+ cumulativeAmount: entry.cumulativeAmount,
95
+ deposit: entry.deposit,
96
+ chainId: entry.chainId,
97
+ opened: entry.opened,
98
+ updatedAt
99
+ };
100
+ file.associations = file.associations.map(
101
+ (candidate) => candidate.callerPubkey === association.callerPubkey && candidate.operator === association.operator && candidate.channelKey === association.channelKey && candidate.channelId.toLowerCase() === association.channelId.toLowerCase() ? {
102
+ ...candidate,
103
+ cumulativeAmount: association.cumulativeAmount,
104
+ deposit: association.deposit,
105
+ chainId: association.chainId,
106
+ opened: association.opened,
107
+ updatedAt
108
+ } : candidate
109
+ );
110
+ const index = file.associations.findIndex(
111
+ (candidate) => sameCredit(
112
+ candidate,
113
+ association.endpoint,
114
+ association.callerPubkey,
115
+ association.creditId
116
+ )
117
+ );
118
+ if (index === -1) file.associations.push(association);
119
+ else file.associations[index] = association;
120
+ }
121
+ saveFile(file);
122
+ },
123
+ delete(key) {
124
+ if (operator === void 0) return;
125
+ const file = loadFile();
126
+ const removedKey = channelStorageKey(context.callerPubkey, operator, key);
127
+ const channelKey = key.startsWith("chan:") ? key.slice(5) : key;
128
+ const endpoint = context.endpoint ? normalizeCreditEndpoint(context.endpoint) : void 0;
129
+ file.associations = file.associations.filter((association) => {
130
+ if (association.callerPubkey !== context.callerPubkey || association.operator !== operator || association.channelKey !== channelKey) {
131
+ return true;
132
+ }
133
+ if (endpoint && context.creditId) {
134
+ return !sameCredit(association, endpoint, context.callerPubkey, context.creditId);
135
+ }
136
+ return false;
137
+ });
138
+ const shared = file.associations.some(
139
+ (association) => association.callerPubkey === context.callerPubkey && association.operator === operator && association.channelKey === channelKey
140
+ );
141
+ if (!shared) {
142
+ file.values = Object.fromEntries(
143
+ Object.entries(file.values).filter(([storedKey]) => storedKey !== removedKey)
144
+ );
145
+ }
146
+ saveFile(file);
147
+ }
148
+ });
149
+ }
150
+ function listTempoChannelAssociations() {
151
+ return [...loadFile().associations].sort((a, b) => b.updatedAt - a.updatedAt);
152
+ }
153
+ function loadTempoChannelAssociation(endpoint, callerPubkey, creditId) {
154
+ const normalized = normalizeCreditEndpoint(endpoint);
155
+ return loadFile().associations.find(
156
+ (candidate) => sameCredit(candidate, normalized, callerPubkey, creditId)
157
+ );
158
+ }
159
+ function loadTempoChannelAssociationByKey(callerPubkey, operator, channelKey) {
160
+ const normalizedOperator = normalizeTempoChannelOperator(operator);
161
+ return loadFile().associations.find(
162
+ (candidate) => candidate.callerPubkey === callerPubkey && candidate.operator === normalizedOperator && candidate.channelKey === channelKey
163
+ );
164
+ }
165
+ async function loadTempoChannelEntry(association) {
166
+ return createTempoChannelStore({
167
+ callerPubkey: association.callerPubkey,
168
+ endpoint: association.endpoint,
169
+ creditId: association.creditId,
170
+ operator: association.operator
171
+ }).get(association.channelKey);
172
+ }
173
+ async function loadTempoChannelController(association) {
174
+ const entry = await loadTempoChannelEntry(association);
175
+ return entry && tempoDescriptorController(entry.descriptor);
176
+ }
177
+ function tempoDescriptorController(descriptor) {
178
+ const payer = descriptor.payer;
179
+ if (typeof payer !== "string" || !isAddress(payer, { strict: false })) return void 0;
180
+ const signer = descriptor.authorizedSigner;
181
+ const authorizedSigner = TempoSession.Precompile.Channel.resolveAuthorizedSigner({
182
+ ...descriptor,
183
+ payer,
184
+ authorizedSigner: typeof signer === "string" && isAddress(signer, { strict: false }) ? signer : zeroAddress
185
+ });
186
+ return { payer: payer.toLowerCase(), authorizedSigner: authorizedSigner.toLowerCase() };
187
+ }
188
+ function tempoChannelControlledBy(controller, address) {
189
+ if (!controller) return true;
190
+ if (address === void 0) return false;
191
+ const candidate = address.toLowerCase();
192
+ return controller.payer.toLowerCase() === candidate || controller.authorizedSigner.toLowerCase() === candidate;
193
+ }
194
+ async function retireTempoChannelScope(association) {
195
+ await createTempoChannelStore({
196
+ callerPubkey: association.callerPubkey,
197
+ operator: association.operator
198
+ }).delete(association.channelKey);
199
+ }
200
+ function tempoChargeFundingStale(pending, now = Date.now()) {
201
+ return now - pending.requestedAt >= TEMPO_CHARGE_PENDING_STALE_MS;
202
+ }
203
+ function tempoChargeCredentialSettleable(pending, now = Date.now()) {
204
+ return pending.credentialValidBefore !== void 0 && now < pending.credentialValidBefore * 1e3;
205
+ }
206
+ function recordPendingTempoChargeFunding(pending) {
207
+ const { file, credits } = loadChargeFile();
208
+ const entry = { ...pending, endpoint: normalizeCreditEndpoint(pending.endpoint) };
209
+ file.charges = [
210
+ ...file.charges.filter((candidate) => !sameChargeCredit(candidate, entry, credits)),
211
+ entry
212
+ ];
213
+ saveFile(file);
214
+ }
215
+ function loadResumableTempoChargeFunding(args) {
216
+ const key = {
217
+ endpoint: normalizeCreditEndpoint(args.endpoint),
218
+ callerPubkey: args.callerPubkey,
219
+ creditId: args.creditId
220
+ };
221
+ const { file, credits } = loadChargeFile();
222
+ return file.charges.find(
223
+ (entry) => sameChargeCredit(entry, key, credits) && !tempoChargeFundingStale(entry, args.now)
224
+ );
225
+ }
226
+ function listResumableTempoChargeFundings(filter = {}) {
227
+ const endpoint = filter.endpoint ? normalizeCreditEndpoint(filter.endpoint) : void 0;
228
+ const { file, credits } = loadChargeFile();
229
+ return file.charges.filter(
230
+ (entry) => (!endpoint || fundingReachableAt(entry, endpoint, credits)) && (!filter.callerPubkey || entry.callerPubkey === filter.callerPubkey) && !tempoChargeFundingStale(entry, filter.now)
231
+ ).sort((a, b) => a.requestedAt - b.requestedAt);
232
+ }
233
+ function clearPendingTempoChargeFunding(pending) {
234
+ const { file, credits } = loadChargeFile();
235
+ const key = { ...pending, endpoint: normalizeCreditEndpoint(pending.endpoint) };
236
+ const remaining = file.charges.filter(
237
+ (entry) => !sameChargeCredit(entry, key, credits) || entry.fundId !== pending.fundId
238
+ );
239
+ if (remaining.length === file.charges.length) return;
240
+ file.charges = remaining;
241
+ saveFile(file);
242
+ }
243
+ function sameCredit(association, endpoint, callerPubkey, creditId) {
244
+ return association.endpoint === endpoint && association.callerPubkey === callerPubkey && association.creditId === creditId;
245
+ }
246
+ function sameChargeCredit(entry, key, credits) {
247
+ return entry.callerPubkey === key.callerPubkey && entry.creditId === key.creditId && fundingReachableAt(entry, normalizeCreditEndpoint(key.endpoint), credits);
248
+ }
249
+ function loadChargeFile() {
250
+ const snapshot = collapseChargeFile();
251
+ if (!snapshot.changed) return snapshot;
252
+ const persisted = tryWithTempoChannelLockSync(() => {
253
+ const current = collapseChargeFile();
254
+ if (current.changed) saveFile(current.file);
255
+ return current;
256
+ });
257
+ return persisted ?? snapshot;
258
+ }
259
+ function collapseChargeFile() {
260
+ const file = loadFile();
261
+ const credits = listCredits();
262
+ const charges = [];
263
+ let changed = false;
264
+ for (const entry of file.charges) {
265
+ const index = charges.findIndex((candidate) => sameChargeCredit(candidate, entry, credits));
266
+ if (index === -1) {
267
+ charges.push(entry);
268
+ continue;
269
+ }
270
+ const seenAt = Number.isFinite(charges[index].requestedAt) ? charges[index].requestedAt : 0;
271
+ const entryAt = Number.isFinite(entry.requestedAt) ? entry.requestedAt : 0;
272
+ if (entryAt > seenAt) charges[index] = entry;
273
+ changed = true;
274
+ }
275
+ if (changed) file.charges = charges;
276
+ return { file, credits, changed };
277
+ }
278
+ function tryWithTempoChannelLockSync(operation) {
279
+ if (tempoLockOwner.getStore()?.active) return operation();
280
+ ensureTempoChannelFile();
281
+ let release;
282
+ try {
283
+ release = lockfile.lockSync(TEMPO_CHANNELS_FILE, { realpath: false });
284
+ } catch (error) {
285
+ if (error.code === "ELOCKED") return void 0;
286
+ throw error;
287
+ }
288
+ const owner = { active: true };
289
+ try {
290
+ return tempoLockOwner.run(owner, operation);
291
+ } finally {
292
+ owner.active = false;
293
+ release();
294
+ }
295
+ }
296
+ function ensureTempoChannelFile() {
297
+ ensureConfigDir();
298
+ if (existsSync(TEMPO_CHANNELS_FILE)) return;
299
+ try {
300
+ writeFileSync(TEMPO_CHANNELS_FILE, `${JSON.stringify(emptyFile(), null, 2)}
301
+ `, {
302
+ mode: 384,
303
+ flag: "wx"
304
+ });
305
+ } catch {
306
+ }
307
+ }
308
+ function loadFile() {
309
+ if (!existsSync(TEMPO_CHANNELS_FILE)) return emptyFile();
310
+ try {
311
+ const parsed = JSON.parse(readFileSync(TEMPO_CHANNELS_FILE, "utf8"));
312
+ if (parsed.version === 1) return migrateLegacyFile(parsed);
313
+ if (parsed.version !== 2 || !isStringRecord(parsed.values) || !Array.isArray(parsed.associations) || !parsed.associations.every(isTempoChannelAssociation)) {
314
+ throw new Error("unsupported Tempo channel store shape");
315
+ }
316
+ return {
317
+ version: 2,
318
+ values: parsed.values,
319
+ associations: parsed.associations,
320
+ // Tolerated rather than required, unlike its two siblings: every file
321
+ // written before internal-review lacks the key, and failing closed on it would
322
+ // read as a corrupt journal on a store that is perfectly intact.
323
+ charges: Array.isArray(parsed.charges) ? parsed.charges.filter(isPendingTempoCharge) : []
324
+ };
325
+ } catch (cause) {
326
+ throw new DvmError(
327
+ "tempo_channel_store_invalid",
328
+ "The saved Tempo channel journal cannot be read safely.",
329
+ `Do not delete ${TEMPO_CHANNELS_FILE}; restore or repair it before signing another channel credential.`,
330
+ { cause: cause instanceof Error ? cause.message : String(cause) }
331
+ );
332
+ }
333
+ }
334
+ function saveFile(file) {
335
+ ensureConfigDir();
336
+ const tmp = `${TEMPO_CHANNELS_FILE}.${randomUUID()}.tmp`;
337
+ writeFileSync(tmp, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
338
+ renameSync(tmp, TEMPO_CHANNELS_FILE);
339
+ }
340
+ function emptyFile() {
341
+ return { version: 2, values: {}, associations: [], charges: [] };
342
+ }
343
+ function migrateLegacyFile(parsed) {
344
+ if (!isStringRecord(parsed.values) || !Array.isArray(parsed.associations) || !parsed.associations.every(isLegacyTempoChannelAssociation)) {
345
+ throw new Error("unsupported Tempo channel store shape");
346
+ }
347
+ const values = { ...parsed.values };
348
+ const consumed = /* @__PURE__ */ new Set();
349
+ const associations = parsed.associations.map((association) => {
350
+ const legacyKey = `${association.callerPubkey}:chan:${association.channelKey}`;
351
+ const serialized = parsed.values?.[legacyKey];
352
+ if (serialized === void 0) {
353
+ throw new Error(`Tempo channel ${association.channelId} has no stored descriptor`);
354
+ }
355
+ const operator = serializedChannelOperator(serialized);
356
+ values[channelStorageKey(association.callerPubkey, operator, `chan:${association.channelKey}`)] = serialized;
357
+ consumed.add(legacyKey);
358
+ return { ...association, operator };
359
+ });
360
+ return {
361
+ version: 2,
362
+ values: Object.fromEntries(Object.entries(values).filter(([key]) => !consumed.has(key))),
363
+ associations,
364
+ charges: Array.isArray(parsed.charges) ? parsed.charges.filter(isPendingTempoCharge) : []
365
+ };
366
+ }
367
+ function channelStorageKey(callerPubkey, operator, key) {
368
+ return `${callerPubkey}:${operator}:${key}`;
369
+ }
370
+ function serializedChannelOperator(value) {
371
+ const parsed = JSON.parse(value);
372
+ return normalizeTempoChannelOperator(parsed.descriptor?.operator);
373
+ }
374
+ function normalizeTempoChannelOperator(value) {
375
+ if (value === void 0) return zeroAddress;
376
+ if (typeof value !== "string" || !isAddress(value, { strict: false })) {
377
+ throw new Error("invalid Tempo channel operator");
378
+ }
379
+ return value.toLowerCase();
380
+ }
381
+ function isStringRecord(value) {
382
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
383
+ }
384
+ function isPendingTempoCharge(value) {
385
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
386
+ const entry = value;
387
+ 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");
388
+ }
389
+ function isTempoChannelAssociation(value) {
390
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
391
+ const association = value;
392
+ 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";
393
+ }
394
+ function isLegacyTempoChannelAssociation(value) {
395
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
396
+ const association = value;
397
+ 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";
398
+ }
399
+
400
+ // src/lib/shell-quote.ts
401
+ function shellQuoteArg(value) {
402
+ if (value === "") return "''";
403
+ if (SAFE_VALUE_RE.test(value)) return value;
404
+ return `'${value.replace(/'/g, "'\\''")}'`;
405
+ }
406
+ var SAFE_VALUE_RE = /^[a-zA-Z0-9_\-./@:=,+%]+$/;
407
+
408
+ // src/lib/cli-identity-store.ts
409
+ import { schnorr } from "@noble/curves/secp256k1.js";
410
+ import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
411
+ import lockfile2 from "proper-lockfile";
412
+ var IDENTITIES_VERSION = 1;
413
+ function loadIdentities() {
414
+ if (!existsSync2(IDENTITIES_FILE)) return null;
415
+ const raw = readFileSync2(IDENTITIES_FILE, "utf-8");
416
+ let parsed;
417
+ try {
418
+ parsed = JSON.parse(raw);
419
+ } catch (err) {
420
+ throw new DvmError(
421
+ "identities_corrupt",
422
+ `Identities file at ${IDENTITIES_FILE} is not valid JSON.`,
423
+ `Restore a backup or delete the file and run 'dvm init'. Underlying error: ${err instanceof Error ? err.message : String(err)}`
424
+ );
425
+ }
426
+ if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
427
+ throw new DvmError(
428
+ "identities_corrupt",
429
+ `Identities file at ${IDENTITIES_FILE} is missing a version field.`,
430
+ "Restore a backup or delete the file and run 'dvm init'."
431
+ );
432
+ }
433
+ const version = parsed.version;
434
+ if (version !== IDENTITIES_VERSION) {
435
+ throw new DvmError(
436
+ "identities_unknown_version",
437
+ `Unknown identities file version ${String(version)} (this CLI understands version ${String(IDENTITIES_VERSION)}).`,
438
+ "Upgrade dvm CLI: a newer version wrote this file."
439
+ );
440
+ }
441
+ const identities = parsed.identities;
442
+ if (typeof identities !== "object" || identities === null || Array.isArray(identities)) {
443
+ throw new DvmError(
444
+ "identities_corrupt",
445
+ `Identities file at ${IDENTITIES_FILE} has a malformed identities map.`,
446
+ "Restore a backup or delete the file and run 'dvm init'."
447
+ );
448
+ }
449
+ return {
450
+ version: IDENTITIES_VERSION,
451
+ identities
452
+ };
453
+ }
454
+ function deriveIdentityFromPrivkey(privkeyHex, now = Date.now()) {
455
+ if (!/^[0-9a-f]{64}$/.test(privkeyHex)) {
456
+ throw new DvmError(
457
+ "invalid_privkey",
458
+ "Private key must be 64 lowercase hex characters (32 bytes).",
459
+ "Generate a fresh one with 'dvm identity create <name>'."
460
+ );
461
+ }
462
+ const privkeyBytes = hexToBytes(privkeyHex);
463
+ const pubkeyBytes = schnorr.getPublicKey(privkeyBytes);
464
+ return {
465
+ privkey: privkeyHex,
466
+ pubkey: bytesToHex(pubkeyBytes),
467
+ createdAt: now
468
+ };
469
+ }
470
+ function bytesToHex(bytes) {
471
+ let s = "";
472
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
473
+ return s;
474
+ }
475
+ function hexToBytes(hex) {
476
+ const out = new Uint8Array(hex.length / 2);
477
+ for (let i = 0; i < out.length; i++) {
478
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
479
+ }
480
+ return out;
481
+ }
482
+
483
+ // src/lib/cli-identity-resolve.ts
484
+ function findSigningIdentityByPubkey(pubkey) {
485
+ const wanted = pubkey.toLowerCase();
486
+ const envKey = process.env.DVM_IDENTITY_KEY;
487
+ if (envKey) {
488
+ try {
489
+ const derived = deriveIdentityFromPrivkey(envKey.toLowerCase());
490
+ if (derived.pubkey === wanted) {
491
+ return { privkey: derived.privkey, pubkey: derived.pubkey, source: { kind: "env" } };
492
+ }
493
+ } catch {
494
+ }
495
+ }
496
+ const identities = loadIdentities()?.identities ?? {};
497
+ for (const [name, entry] of Object.entries(identities)) {
498
+ if (entry.pubkey.toLowerCase() === wanted) {
499
+ return { privkey: entry.privkey, pubkey: entry.pubkey, source: { kind: "pubkey", name } };
500
+ }
501
+ }
502
+ return null;
503
+ }
504
+
505
+ // src/lib/payment-rails/tempo-wallet.ts
506
+ var TEMPO_CHAIN_ID = 4217;
507
+ async function connectedTempoAccount() {
508
+ const config = loadConfig();
509
+ return tempoAccountFromConfig(config);
510
+ }
511
+ async function deriveTempoAddress(privateKey) {
512
+ if (!isValidEvmPrivateKey(privateKey)) throw invalidTempoPrivateKey();
513
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
514
+ return privateKeyToAccount2(privateKey).address.toLowerCase();
515
+ }
516
+ async function tempoAccountFromConfig(config) {
517
+ if (config?.tempo?.method !== "tempo") return null;
518
+ const apiKey = config.tempo.apiKey;
519
+ if (!apiKey || !isValidEvmPrivateKey(apiKey)) return null;
520
+ const privateKey = apiKey;
521
+ const address = await deriveTempoAddress(privateKey);
522
+ return { privateKey, address };
523
+ }
524
+ function invalidTempoPrivateKey() {
525
+ return new DvmError(
526
+ "invalid_api_key",
527
+ "A Tempo private key must be a valid 0x-prefixed 32-byte hex secp256k1 private key.",
528
+ "Generate one with `cast wallet new` or `openssl rand -hex 32` (and prepend 0x)."
529
+ );
530
+ }
531
+
532
+ // src/lib/payment-rails/tempo-session.ts
533
+ import { Credential } from "mppx";
534
+ import { Mppx } from "mppx/client";
535
+ import { Session as TempoSession2 } from "mppx/tempo";
536
+ import { decodeFunctionData, formatUnits, isAddress as isAddress2, zeroAddress as zeroAddress2 } from "viem";
537
+ import { privateKeyToAccount } from "viem/accounts";
538
+ import { Transaction } from "viem/tempo";
539
+
540
+ // src/lib/wallet-rail-copy.ts
541
+ function tempoKeyImportRoutes(opts) {
542
+ const force = opts.replacingConnectedKey ? " --force" : "";
543
+ return `'dvm wallet tempo-connect --key-file <path>${force}', or DVM_TEMPO_KEY=<key> dvm wallet tempo-connect${force}`;
544
+ }
545
+ var TEMPO_EXIT_KEY_MISSING_HINT = `Reconnect that exact key (${tempoKeyImportRoutes({ replacingConnectedKey: false })}) before attempting an on-chain exit.`;
546
+
547
+ // src/lib/payment-rails/tempo-session.ts
548
+ function offersTempoCreditSession(menu) {
549
+ return menu?.tempo?.methods.some(
550
+ (candidate) => candidate.method === "tempo" && candidate.intent === "session"
551
+ ) === true;
552
+ }
553
+ function offersTempoCreditCharge(menu) {
554
+ return menu?.tempo?.methods.some(
555
+ (candidate) => candidate.method === "tempo" && candidate.intent === "charge"
556
+ ) === true;
557
+ }
558
+ function withholdsTempoCreditSession(menu) {
559
+ return menu?.tempo?.withheld?.some(
560
+ (candidate) => candidate.method === "tempo" && candidate.intent === "session"
561
+ ) === true;
562
+ }
563
+ function offersTempoCreditFunding(menu) {
564
+ return offersTempoCreditSession(menu) || offersTempoCreditCharge(menu);
565
+ }
566
+ function createTempoCreditSession(args) {
567
+ return TempoSession2.Client.session({
568
+ account: privateKeyToAccount(args.account.privateKey),
569
+ channelStore: createTempoChannelStore({
570
+ endpoint: args.endpoint,
571
+ callerPubkey: args.callerPubkey,
572
+ creditId: args.creditId,
573
+ operator: args.operator ?? zeroAddress2
574
+ }),
575
+ maxDeposit: formatUnits(BigInt(args.maxDepositMicro), 6),
576
+ topUpAmount: formatUnits(BigInt(args.topUpAmountMicro), 6),
577
+ ...args.getClient ? { getClient: args.getClient } : {}
578
+ });
579
+ }
580
+ function createTempoSessionPaymentFetch(method, fetch) {
581
+ return Mppx.create({
582
+ methods: [method],
583
+ fetch,
584
+ maxPaymentRetries: 5,
585
+ polyfill: false
586
+ }).fetch;
587
+ }
588
+ function tempoSessionChallengeChannelScope(challenge) {
589
+ if (!challenge) return void 0;
590
+ const request = challenge.request;
591
+ if (typeof request?.recipient !== "string" || !isAddress2(request.recipient, { strict: false }) || typeof request.currency !== "string" || !isAddress2(request.currency, { strict: false })) {
592
+ return void 0;
593
+ }
594
+ const methodDetails = request.methodDetails && typeof request.methodDetails === "object" ? request.methodDetails : void 0;
595
+ const chainId = methodDetails?.chainId;
596
+ if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId <= 0) {
597
+ return void 0;
598
+ }
599
+ const challengeEscrow = [methodDetails?.escrowContract, methodDetails?.escrow].find(
600
+ (candidate) => typeof candidate === "string" && isAddress2(candidate, { strict: false })
601
+ );
602
+ const escrow = challengeEscrow ?? TempoSession2.Precompile.Constants.tip20ChannelEscrow;
603
+ const operator = methodDetails?.operator;
604
+ if (operator !== void 0 && (typeof operator !== "string" || !isAddress2(operator, { strict: false }))) {
605
+ return void 0;
606
+ }
607
+ return {
608
+ channelKey: `${request.recipient.toLowerCase()}:${request.currency.toLowerCase()}:${escrow.toLowerCase()}:${String(chainId)}`,
609
+ operator: operator?.toLowerCase() ?? zeroAddress2
610
+ };
611
+ }
612
+ function tempoSessionCredentialChannelScope(credential) {
613
+ try {
614
+ return tempoSessionChallengeChannelScope(
615
+ Credential.deserialize(credential).challenge
616
+ );
617
+ } catch {
618
+ return void 0;
619
+ }
620
+ }
621
+ function tempoSessionAssociationByKey(args) {
622
+ return args.channelScope ? loadTempoChannelAssociationByKey(
623
+ args.callerPubkey,
624
+ args.channelScope.operator,
625
+ args.channelScope.channelKey
626
+ ) : loadTempoChannelAssociation(args.endpoint, args.callerPubkey, args.creditId);
627
+ }
628
+ function tempoSessionPriorCumulativeMicro(args) {
629
+ const association = args.association ?? loadTempoChannelAssociation(args.endpoint, args.callerPubkey, args.creditId);
630
+ if (!association?.opened) return 0;
631
+ const prior = BigInt(association.cumulativeAmount);
632
+ if (prior < 0n || prior > BigInt(Number.MAX_SAFE_INTEGER)) {
633
+ throw new DvmError("payment_invalid", "The Tempo channel's cumulative amount is out of range.");
634
+ }
635
+ return Number(prior);
636
+ }
637
+ function tempoSessionCumulativeCapMicro(args) {
638
+ const prior = BigInt(tempoSessionPriorCumulativeMicro(args));
639
+ const amount = BigInt(Math.max(0, args.amountMicro));
640
+ const explicitAllowance = BigInt(Math.max(args.allowanceMicro, args.amountMicro));
641
+ const allowance = explicitAllowance > amount ? explicitAllowance : maxAdvertisedMicro(amount);
642
+ const cap = prior + allowance;
643
+ if (cap > BigInt(Number.MAX_SAFE_INTEGER)) {
644
+ throw new DvmError("payment_invalid", "The Tempo session deposit cap is out of range.");
645
+ }
646
+ return Number(cap);
647
+ }
648
+ function tempoSessionCredentialDepositMicro(credential) {
649
+ return readSessionDeposit(credential).depositMicro;
650
+ }
651
+ function assertTempoSessionDepositWithinCap(args) {
652
+ const { action, depositMicro } = readSessionDeposit(args.credential);
653
+ if (depositMicro === 0) return;
654
+ const priorMicro = action === "topUp" ? Number(
655
+ (args.channelScope ? loadTempoChannelAssociationByKey(
656
+ args.callerPubkey,
657
+ args.channelScope.operator,
658
+ args.channelScope.channelKey
659
+ ) : loadTempoChannelAssociation(args.endpoint, args.callerPubkey, args.creditId))?.deposit ?? 0
660
+ ) : 0;
661
+ const committedMicro = priorMicro + depositMicro;
662
+ if (committedMicro <= args.capMicro) return;
663
+ const committedUsd = committedMicro / 1e6;
664
+ const capUsd = args.capMicro / 1e6;
665
+ throw new DvmError(
666
+ "tempo_session_deposit_exceeds_limit",
667
+ `This Tempo session would lock $${formatUsd(committedUsd)} of channel collateral, above the $${formatUsd(capUsd)} this funding authorized.`,
668
+ "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.",
669
+ {
670
+ committed_usdc_microunits: String(committedMicro),
671
+ max_usdc_microunits: String(args.capMicro),
672
+ anchor: costAnchor(committedUsd),
673
+ display: `Funding refused: the DVM's Tempo session would lock $${formatUsd(committedUsd)} of on-chain collateral, over the $${formatUsd(capUsd)} authorized for this funding.`
674
+ }
675
+ );
676
+ }
677
+ function assertTempoSessionChallengeWithinAsk(args) {
678
+ const local = BigInt(Math.max(0, Math.round(args.basisMicro)));
679
+ for (const challenge of args.challenges) {
680
+ if (challenge.method !== "tempo" || challenge.intent !== "session") continue;
681
+ const advertised = BigInt(tempoSessionChallengeMicro(challenge));
682
+ if (withinAdvertisedTolerance(advertised, local)) continue;
683
+ const advertisedUsd = Number(advertised) / 1e6;
684
+ const localUsd = Number(local) / 1e6;
685
+ throw new DvmError(
686
+ "tempo_session_amount_exceeds_limit",
687
+ `The DVM's Tempo session asks this funding to authorize $${formatUsd(advertisedUsd)}, more than 10% above the $${formatUsd(localUsd)} of credit it is buying.`,
688
+ "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.",
689
+ {
690
+ advertised_usdc_microunits: advertised.toString(),
691
+ local_usdc_microunits: local.toString(),
692
+ max_usdc_microunits: maxAdvertisedMicro(local).toString(),
693
+ anchor: costAnchor(advertisedUsd),
694
+ display: `Funding refused: the DVM's Tempo session wants a $${formatUsd(advertisedUsd)} voucher for $${formatUsd(localUsd)} of credit, over the 10% safety margin.`
695
+ }
696
+ );
697
+ }
698
+ }
699
+ function tempoSessionCredentialCumulativeMicro(credential) {
700
+ let payload;
701
+ try {
702
+ payload = Credential.deserialize(credential).payload;
703
+ } catch {
704
+ return void 0;
705
+ }
706
+ const { cumulativeAmount } = payload;
707
+ if (typeof cumulativeAmount !== "string" || !/^\d+$/.test(cumulativeAmount)) return void 0;
708
+ const micro = BigInt(cumulativeAmount);
709
+ if (micro > BigInt(Number.MAX_SAFE_INTEGER)) {
710
+ throw new DvmError("payment_invalid", "The Tempo session voucher amount is out of range.");
711
+ }
712
+ return Number(micro);
713
+ }
714
+ function assertTempoSessionVoucherWithinAsk(args) {
715
+ const local = BigInt(Math.max(0, Math.round(args.basisMicro)));
716
+ const authorized = BigInt(args.authorizedMicro);
717
+ if (authorized <= 0n || withinAdvertisedTolerance(authorized, local)) return;
718
+ const authorizedUsd = Number(authorized) / 1e6;
719
+ const localUsd = Number(local) / 1e6;
720
+ throw new DvmError(
721
+ "tempo_session_amount_exceeds_limit",
722
+ `This Tempo session voucher would let the DVM capture $${formatUsd(authorizedUsd)}, above the $${formatUsd(localUsd)} of credit this funding is buying.`,
723
+ "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.",
724
+ {
725
+ advertised_usdc_microunits: authorized.toString(),
726
+ local_usdc_microunits: local.toString(),
727
+ max_usdc_microunits: maxAdvertisedMicro(local).toString(),
728
+ anchor: costAnchor(authorizedUsd),
729
+ display: `Funding refused: the DVM's Tempo session voucher would capture $${formatUsd(authorizedUsd)} for $${formatUsd(localUsd)} of credit, over the 10% safety margin.`
730
+ }
731
+ );
732
+ }
733
+ async function persistTempoOpenCredential(args) {
734
+ try {
735
+ const parsed = Credential.deserialize(args.credential);
736
+ const payload = parsed.payload;
737
+ if (payload.action !== "open" || typeof payload.channelId !== "string" || typeof payload.cumulativeAmount !== "string" || !payload.descriptor || typeof payload.descriptor !== "object") {
738
+ return false;
739
+ }
740
+ const methodDetails = parsed.challenge.request.methodDetails;
741
+ if (typeof methodDetails?.chainId !== "number") return false;
742
+ const escrow = typeof methodDetails.escrowContract === "string" && isAddress2(methodDetails.escrowContract, { strict: false }) ? methodDetails.escrowContract : TempoSession2.Precompile.Constants.tip20ChannelEscrow;
743
+ const descriptor = payload.descriptor;
744
+ const channelId = TempoSession2.Precompile.Channel.computeId({
745
+ ...descriptor,
746
+ chainId: methodDetails.chainId,
747
+ escrow
748
+ });
749
+ if (channelId.toLowerCase() !== payload.channelId.toLowerCase()) return false;
750
+ const depositMicro = tempoSessionCredentialDepositMicro(args.credential);
751
+ const existingAssociation = loadTempoChannelAssociation(
752
+ args.endpoint,
753
+ args.callerPubkey,
754
+ args.creditId
755
+ );
756
+ const existing = existingAssociation ? await loadTempoChannelEntry(existingAssociation) : void 0;
757
+ await createTempoChannelStore({
758
+ endpoint: args.endpoint,
759
+ callerPubkey: args.callerPubkey,
760
+ creditId: args.creditId,
761
+ operator: descriptor.operator
762
+ }).set({
763
+ channelId,
764
+ cumulativeAmount: BigInt(payload.cumulativeAmount),
765
+ deposit: BigInt(depositMicro),
766
+ descriptor,
767
+ escrow,
768
+ chainId: methodDetails.chainId,
769
+ opened: args.opened || existing?.opened === true && existing.channelId.toLowerCase() === channelId.toLowerCase()
770
+ });
771
+ return true;
772
+ } catch (err) {
773
+ if (err instanceof DvmError && err.code === "tempo_channel_scope_occupied") throw err;
774
+ return false;
775
+ }
776
+ }
777
+ function tempoSessionChallenge(err) {
778
+ if (!(err instanceof DvmError) || !Array.isArray(err.data?.tempo_challenges)) return void 0;
779
+ const challenge = err.data.tempo_challenges.find(
780
+ (candidate) => candidate.method === "tempo" && candidate.intent === "session"
781
+ );
782
+ return challenge;
783
+ }
784
+ function tempoSessionChallengeMicro(challenge) {
785
+ const advertised = challenge.request?.amount;
786
+ if (typeof advertised !== "string" || !/^\d+$/.test(advertised)) {
787
+ throw new DvmError(
788
+ "payment_invalid",
789
+ `The DVM advertised an unreadable Tempo session amount: '${String(advertised)}'.`,
790
+ "Do not pay this. Ask the DVM operator to return a whole number of USDC micro-units."
791
+ );
792
+ }
793
+ const micro = BigInt(advertised);
794
+ if (micro > BigInt(Number.MAX_SAFE_INTEGER)) {
795
+ throw new DvmError("payment_invalid", "The Tempo session challenge amount is out of range.");
796
+ }
797
+ return Number(micro);
798
+ }
799
+ async function reconcileTempoChannelOnChain(args) {
800
+ const { association } = args;
801
+ const unread = {
802
+ retired: false,
803
+ channelId: association.channelId,
804
+ depositMicro: Number(association.deposit),
805
+ settledMicro: 0
806
+ };
807
+ let entry;
808
+ let state;
809
+ try {
810
+ ({ entry, state } = await readTempoChannelState(args));
811
+ } catch (err) {
812
+ return { ...unread, status: "unreadable", error: asTempoReadError(err) };
813
+ }
814
+ const read = {
815
+ status: classifyTempoChannelState(entry, state),
816
+ channelId: entry.channelId,
817
+ depositMicro: Number(state.deposit),
818
+ settledMicro: Number(state.settled)
819
+ };
820
+ if (read.status !== "closed") return { ...read, retired: false };
821
+ if (args.observeOnly) return { ...read, retired: false };
822
+ try {
823
+ const retired = await withTempoChannelLock(async () => {
824
+ const current = loadTempoChannelAssociation(
825
+ association.endpoint,
826
+ association.callerPubkey,
827
+ association.creditId
828
+ );
829
+ if (!current) return true;
830
+ if (current.channelId.toLowerCase() !== association.channelId.toLowerCase()) return false;
831
+ await retireTempoChannelScope(current);
832
+ return true;
833
+ });
834
+ return { ...read, retired };
835
+ } catch (err) {
836
+ return { ...read, retired: false, error: asTempoReadError(err) };
837
+ }
838
+ }
839
+ async function readTempoChannelState(args) {
840
+ const entry = await loadTempoChannelEntry(args.association);
841
+ if (!entry) {
842
+ throw new DvmError(
843
+ "tempo_channel_not_found",
844
+ `The Tempo channel record for credit ${args.association.creditId} has no stored descriptor.`,
845
+ `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.`
846
+ );
847
+ }
848
+ const rpc = await resolveTempoExitRpc(entry.chainId);
849
+ const client = args.client ?? await tempoPublicClient(rpc);
850
+ const chain = args.chain ?? TempoSession2.Precompile.Chain;
851
+ const state = await guardTempoChannelRead(
852
+ rpc,
853
+ () => chain.getChannelState(client, entry.channelId, entry.escrow)
854
+ );
855
+ return { entry, state };
856
+ }
857
+ function asTempoReadError(err) {
858
+ if (err instanceof DvmError) return err;
859
+ return new DvmError(
860
+ "tempo_chain_read_failed",
861
+ `The Tempo channel state could not be read: ${redactUrlsInText(
862
+ err instanceof Error ? err.message : String(err)
863
+ )}`
864
+ );
865
+ }
866
+ function classifyTempoChannelState(entry, state) {
867
+ if (state.deposit > 0n) return state.closeRequestedAt === 0 ? "open" : "closing";
868
+ return entry.opened || state.settled > 0n || state.closeRequestedAt !== 0 ? "closed" : "unconfirmed";
869
+ }
870
+ async function guardTempoChannelRead(rpc, run) {
871
+ try {
872
+ return await run();
873
+ } catch (err) {
874
+ if (err instanceof DvmError) throw err;
875
+ const cause = redactUrlsInText(err instanceof Error ? err.message : String(err));
876
+ if (!isTransportFailure(err)) {
877
+ throw new DvmError(
878
+ "tempo_chain_read_failed",
879
+ `The Tempo channel state could not be read: ${cause}`,
880
+ "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."
881
+ );
882
+ }
883
+ throw new DvmError(
884
+ "tempo_rpc_error",
885
+ `The Tempo channel state could not be read: ${cause}`,
886
+ 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.`
887
+ );
888
+ }
889
+ }
890
+ async function resolveTempoExitRpc(chainId) {
891
+ const { tempo, tempoModerato } = await import("viem/chains");
892
+ const chain = chainId === tempo.id ? tempo : chainId === tempoModerato.id ? tempoModerato : null;
893
+ if (!chain) {
894
+ throw new DvmError(
895
+ "tempo_chain_unsupported",
896
+ `The recorded Tempo channel uses unsupported chain ${String(chainId)}.`
897
+ );
898
+ }
899
+ const { url, source } = resolveRpcOverride(
900
+ process.env.DVM_TEMPO_RPC_URL,
901
+ chain.rpcUrls.default.http[0]
902
+ );
903
+ return { chain, url, source };
904
+ }
905
+ async function tempoPublicClient(rpc) {
906
+ const { createPublicClient, http } = await import("viem");
907
+ const transportOptions = resolveRpcHttpTransportOptions(
908
+ rpc.source,
909
+ process.env.DVM_TEMPO_RPC_HEADERS_JSON
910
+ );
911
+ return createPublicClient({
912
+ chain: rpc.chain,
913
+ transport: transportOptions ? http(rpc.url, transportOptions) : http(rpc.url)
914
+ });
915
+ }
916
+ function readSessionDeposit(credential) {
917
+ let payload;
918
+ try {
919
+ payload = Credential.deserialize(credential).payload;
920
+ } catch {
921
+ return { depositMicro: 0 };
922
+ }
923
+ const action = typeof payload.action === "string" ? payload.action : void 0;
924
+ if (action === "topUp") {
925
+ if (typeof payload.additionalDeposit !== "string" || !/^\d+$/.test(payload.additionalDeposit)) {
926
+ throw unreadableDeposit("top-up");
927
+ }
928
+ return { action, depositMicro: depositAsMicro(BigInt(payload.additionalDeposit)) };
929
+ }
930
+ if (action !== "open") return { ...action ? { action } : {}, depositMicro: 0 };
931
+ if (typeof payload.transaction !== "string") throw unreadableDeposit("channel open");
932
+ let calls = [];
933
+ try {
934
+ const deserialized = Transaction.deserialize(payload.transaction);
935
+ if ("calls" in deserialized && Array.isArray(deserialized.calls)) calls = deserialized.calls;
936
+ } catch {
937
+ throw unreadableDeposit("channel open");
938
+ }
939
+ for (const call of calls) {
940
+ if (typeof call.data !== "string") continue;
941
+ try {
942
+ const decoded = decodeFunctionData({
943
+ abi: TempoSession2.Precompile.escrowAbi,
944
+ data: call.data
945
+ });
946
+ if (decoded.functionName !== "open") continue;
947
+ return { action, depositMicro: depositAsMicro(decoded.args[3]) };
948
+ } catch {
949
+ continue;
950
+ }
951
+ }
952
+ throw unreadableDeposit("channel open");
953
+ }
954
+ function unreadableDeposit(leg) {
955
+ return new DvmError(
956
+ "tempo_session_deposit_unreadable",
957
+ `The Tempo session ${leg} credential does not state the deposit it commits.`,
958
+ "Nothing was sent. Retry the funding; if it recurs, the DVM's session challenge is malformed and its operator should be told."
959
+ );
960
+ }
961
+ function depositAsMicro(deposit) {
962
+ if (deposit < 0n || deposit > BigInt(Number.MAX_SAFE_INTEGER)) {
963
+ throw new DvmError("payment_invalid", "The Tempo session channel deposit is out of range.");
964
+ }
965
+ return Number(deposit);
966
+ }
967
+ var TRANSPORT_ERROR_NAMES = /* @__PURE__ */ new Set([
968
+ "HttpRequestError",
969
+ "ResponseBodyTooLargeError",
970
+ "RpcRequestError",
971
+ "SocketClosedError",
972
+ "TimeoutError",
973
+ "UrlRequiredError",
974
+ "WebSocketRequestError"
975
+ ]);
976
+ var TRANSPORT_ERROR_CODES = /* @__PURE__ */ new Set([
977
+ "EAI_AGAIN",
978
+ "ECONNREFUSED",
979
+ "ECONNRESET",
980
+ "ENOTFOUND",
981
+ "EPIPE",
982
+ "ETIMEDOUT"
983
+ ]);
984
+ var REVERT_ERROR_NAMES = /* @__PURE__ */ new Set(["ContractFunctionRevertedError", "ExecutionRevertedError"]);
985
+ var EXECUTION_REVERTED_RPC_CODE = 3;
986
+ function isRevertLink(err) {
987
+ if (REVERT_ERROR_NAMES.has(err.name)) return true;
988
+ const { code } = err;
989
+ return code === EXECUTION_REVERTED_RPC_CODE;
990
+ }
991
+ function isTransportLink(err) {
992
+ if (TRANSPORT_ERROR_NAMES.has(err.name)) return true;
993
+ const { code } = err;
994
+ return typeof code === "string" && TRANSPORT_ERROR_CODES.has(code);
995
+ }
996
+ function isTransportFailure(err) {
997
+ let transport = false;
998
+ let cursor = err;
999
+ for (let depth = 0; cursor instanceof Error && depth < 8; depth += 1) {
1000
+ if (isRevertLink(cursor)) return false;
1001
+ transport ||= isTransportLink(cursor);
1002
+ cursor = cursor.cause;
1003
+ }
1004
+ return transport;
1005
+ }
1006
+
1007
+ export {
1008
+ TEMPO_CHARGE_PENDING_STALE_MS,
1009
+ withTempoChannelLock,
1010
+ listTempoChannelAssociations,
1011
+ loadTempoChannelAssociation,
1012
+ loadTempoChannelEntry,
1013
+ loadTempoChannelController,
1014
+ tempoChannelControlledBy,
1015
+ tempoChargeCredentialSettleable,
1016
+ recordPendingTempoChargeFunding,
1017
+ loadResumableTempoChargeFunding,
1018
+ listResumableTempoChargeFundings,
1019
+ clearPendingTempoChargeFunding,
1020
+ shellQuoteArg,
1021
+ findSigningIdentityByPubkey,
1022
+ TEMPO_CHAIN_ID,
1023
+ connectedTempoAccount,
1024
+ offersTempoCreditSession,
1025
+ offersTempoCreditCharge,
1026
+ withholdsTempoCreditSession,
1027
+ offersTempoCreditFunding,
1028
+ createTempoCreditSession,
1029
+ createTempoSessionPaymentFetch,
1030
+ tempoSessionChallengeChannelScope,
1031
+ tempoSessionCredentialChannelScope,
1032
+ tempoSessionAssociationByKey,
1033
+ tempoSessionPriorCumulativeMicro,
1034
+ tempoSessionCumulativeCapMicro,
1035
+ assertTempoSessionDepositWithinCap,
1036
+ assertTempoSessionChallengeWithinAsk,
1037
+ tempoSessionCredentialCumulativeMicro,
1038
+ assertTempoSessionVoucherWithinAsk,
1039
+ persistTempoOpenCredential,
1040
+ tempoSessionChallenge,
1041
+ reconcileTempoChannelOnChain
1042
+ };