@dvmkit/sdk 0.5.0-rc.1 → 0.7.0-rc.1

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 (54) hide show
  1. package/README.md +100 -0
  2. package/dist/{chunk-JH3SX3LY.js → chunk-6XF3RVVX.js} +1 -1
  3. package/dist/chunk-AFV77S4C.js +258 -0
  4. package/dist/{chunk-RLTSCKRI.js → chunk-BPYMEMQX.js} +50 -3
  5. package/dist/chunk-BQJIFRAZ.js +36 -0
  6. package/dist/{chunk-IWQHUSMO.js → chunk-CXCQLOOK.js} +143 -2
  7. package/dist/{chunk-PB4FYZET.js → chunk-EQLA4FFY.js} +54 -4
  8. package/dist/{chunk-LE74UEV7.js → chunk-EZXAJ6EI.js} +64 -53
  9. package/dist/{chunk-FI4CST2D.js → chunk-FWGWLPXF.js} +4 -6
  10. package/dist/{chunk-EYTHFFFU.js → chunk-GY66THYI.js} +179 -56
  11. package/dist/{chunk-TZ2WKCDD.js → chunk-HCLVYZUZ.js} +2 -2
  12. package/dist/{chunk-O5AT6S5T.js → chunk-IV6Q7NRW.js} +25 -195
  13. package/dist/{chunk-UOCZXO2Z.js → chunk-JEVLNAO3.js} +10 -5
  14. package/dist/{chunk-TUA7PYH7.js → chunk-KMED3H3A.js} +134 -130
  15. package/dist/{chunk-Z4KLECAC.js → chunk-N5RZ5HVB.js} +1 -1
  16. package/dist/{chunk-PAIM66YA.js → chunk-NFMOPYBA.js} +47 -37
  17. package/dist/{chunk-B2IROPQF.js → chunk-O2OTZ5GP.js} +40 -15
  18. package/dist/{chunk-FYRR2UEZ.js → chunk-OML32F6I.js} +1 -1
  19. package/dist/{chunk-6DOGTAHA.js → chunk-QR2EF2NI.js} +9 -43
  20. package/dist/{chunk-7ZUKL4IV.js → chunk-RA2WIX7G.js} +5 -5
  21. package/dist/{chunk-A4YFWZ7R.js → chunk-RCM3P2HE.js} +1 -1
  22. package/dist/{chunk-PLG4XER6.js → chunk-S3OY533W.js} +1 -1
  23. package/dist/{chunk-GFY5FRX3.js → chunk-SZVXOQ75.js} +5 -12
  24. package/dist/{chunk-MZRJR2CW.js → chunk-WBOKPC37.js} +22 -3
  25. package/dist/content-retention-DHyLdA6b.d.ts +78 -0
  26. package/dist/{credit-ledger-MI2H7NEC.js → credit-ledger-MCUXYKSJ.js} +2 -3
  27. package/dist/{credit-menu-CAfqe8IY.d.ts → credit-menu-DjnkeEUU.d.ts} +98 -5
  28. package/dist/{fx-D9nsN3n4.d.ts → fx-zMWFzfrU.d.ts} +1 -1
  29. package/dist/index.d.ts +5 -5
  30. package/dist/index.js +3 -3
  31. package/dist/internal/caller.d.ts +1369 -860
  32. package/dist/internal/caller.js +1079 -991
  33. package/dist/internal/index.js +1 -1
  34. package/dist/internal/server.d.ts +8 -99
  35. package/dist/internal/server.js +19 -18
  36. package/dist/{job-store-Bx-VFRjg.d.ts → job-store-DdLXBpol.d.ts} +1 -1
  37. package/dist/{lightning-backend-BAx2Y8pP.d.ts → lightning-backend-Bw-qtAbt.d.ts} +4 -2
  38. package/dist/{memory-credit-ledger-W2BAUYJP.js → memory-credit-ledger-SBSMGMDX.js} +3 -4
  39. package/dist/{postgres-consumed-credential-store-CUQJKRSL.js → postgres-consumed-credential-store-BUVCAPLS.js} +13 -0
  40. package/dist/{postgres-job-store-VLXLWXUQ.js → postgres-job-store-VHLQBJZC.js} +3 -1
  41. package/dist/server/index.d.ts +28 -10
  42. package/dist/server/index.js +34 -24
  43. package/dist/{step-cache-QG_h5-3G.d.ts → step-cache-2MkvenJn.d.ts} +3 -3
  44. package/dist/tempo-lifecycle-GJ77YKVY.js +15 -0
  45. package/dist/{tempo-wallet-B5XXV3YH.js → tempo-wallet-X2GGAZ6X.js} +4 -4
  46. package/dist/testing/index.d.ts +9 -2
  47. package/dist/testing/index.js +6 -3
  48. package/dist/{usd-EwRqCPF-.d.ts → usd-DI3d3gTK.d.ts} +1 -1
  49. package/dist/{x402-R6HPBY5D.js → x402-FT4S4JS4.js} +8 -10
  50. package/package.json +5 -3
  51. package/recipes/postgres/financial-export-v1.sql +137 -0
  52. package/dist/chunk-CZULM42S.js +0 -143
  53. package/dist/chunk-LIRAHP7B.js +0 -53
  54. package/dist/tempo-lifecycle-ZBBLFBMH.js +0 -15
@@ -8,6 +8,7 @@ import {
8
8
  assertTempoSessionVoucherWithinAsk,
9
9
  assertTempoWalletIdle,
10
10
  clearPendingTempoChargeFunding,
11
+ createTempoChannelStore,
11
12
  createTempoCloseCredential,
12
13
  createTempoCreditSession,
13
14
  createTempoSessionPaymentFetch,
@@ -22,6 +23,7 @@ import {
22
23
  loadIdentities,
23
24
  loadResumableTempoChargeFunding,
24
25
  loadTempoChannelAssociation,
26
+ loadTempoChannelAssociationByKey,
25
27
  loadTempoChannelController,
26
28
  loadTempoChannelEntry,
27
29
  nwcRejectionDisplay,
@@ -35,12 +37,15 @@ import {
35
37
  reconcileTempoChannelOnChain,
36
38
  recordPendingTempoChargeFunding,
37
39
  resolveSigningIdentity,
40
+ retireTempoChannel,
41
+ retireTempoChannelScope,
38
42
  rpcEndpointLine,
39
43
  rpcEndpointSuffix,
40
44
  saveIdentities,
41
45
  shellQuoteArg,
42
46
  tempoChannelControlledBy,
43
47
  tempoChargeCredentialSettleable,
48
+ tempoDescriptorController,
44
49
  tempoKeyImportRoutes,
45
50
  tempoSessionAssociationByKey,
46
51
  tempoSessionChallenge,
@@ -55,7 +60,7 @@ import {
55
60
  withIdentitiesLock,
56
61
  withTempoChannelLock,
57
62
  withholdsTempoCreditSession
58
- } from "../chunk-UOCZXO2Z.js";
63
+ } from "../chunk-JEVLNAO3.js";
59
64
  import {
60
65
  TEMPO_CHAIN_ID,
61
66
  TEMPO_MODERATO_CHAIN_ID,
@@ -71,13 +76,14 @@ import {
71
76
  tempoTokenReference,
72
77
  tempoWrongAssetHint,
73
78
  tryResolveTempoAsset
74
- } from "../chunk-GFY5FRX3.js";
79
+ } from "../chunk-SZVXOQ75.js";
75
80
  import {
76
81
  DvmX402ChannelStorage,
77
82
  X402_EXACT_AUTHORIZATION_TTL_SECONDS,
78
83
  X402_PENDING_STALE_MS,
79
84
  X402_REFUSAL_TRANSACTION_DISCOVERY_WINDOW_MS,
80
85
  acknowledgeX402ChannelOwner,
86
+ assertNoRivalX402Voucher,
81
87
  clearPendingX402ExactFunding,
82
88
  clearPendingX402Funding,
83
89
  connectedX402Payer,
@@ -121,8 +127,9 @@ import {
121
127
  x402PendingRetired,
122
128
  x402PendingStale,
123
129
  x402PendingUnsettled,
130
+ x402VoucherPendingState,
124
131
  x402WrongAssetHint
125
- } from "../chunk-LE74UEV7.js";
132
+ } from "../chunk-EZXAJ6EI.js";
126
133
  import {
127
134
  RAIL_FLAGS,
128
135
  SUPPORTED_PROTOCOL_VERSION,
@@ -164,7 +171,7 @@ import {
164
171
  recordRefusedDrawBalance,
165
172
  settleDrainEvidence,
166
173
  withinAdvertisedTolerance
167
- } from "../chunk-B2IROPQF.js";
174
+ } from "../chunk-O2OTZ5GP.js";
168
175
  import {
169
176
  CREDIT_POSTURES,
170
177
  FEEDBACK_POSTURES,
@@ -181,11 +188,13 @@ import {
181
188
  previewConfigRollback,
182
189
  retireConfigBackup,
183
190
  rollbackConfig,
184
- updateConfig
185
- } from "../chunk-MZRJR2CW.js";
191
+ tempoAccountFromConfig,
192
+ updateConfig,
193
+ x402WalletFromConfig
194
+ } from "../chunk-WBOKPC37.js";
186
195
  import {
187
196
  CAPABILITY_NAME_RE
188
- } from "../chunk-TZ2WKCDD.js";
197
+ } from "../chunk-HCLVYZUZ.js";
189
198
  import {
190
199
  DbTransport,
191
200
  NwcBackend,
@@ -210,7 +219,7 @@ import {
210
219
  sha256hex,
211
220
  validateX402Env,
212
221
  withNwcPool
213
- } from "../chunk-FI4CST2D.js";
222
+ } from "../chunk-FWGWLPXF.js";
214
223
  import {
215
224
  MemoryKVStore
216
225
  } from "../chunk-SDK6KDJN.js";
@@ -256,6 +265,7 @@ import {
256
265
  isX402SettlementReconciliationReason,
257
266
  loadAgentWallet,
258
267
  loadIdentitySecret,
268
+ loadWalletConfiguredMints,
259
269
  looksLikeMnemonic,
260
270
  meltAmountBounds,
261
271
  mnemonicFingerprint,
@@ -280,31 +290,7 @@ import {
280
290
  withWalletLock,
281
291
  writeAgentMnemonic,
282
292
  writeIdentity
283
- } from "../chunk-TUA7PYH7.js";
284
- import {
285
- X402_BATCH_SETTLEMENT_SCHEME,
286
- X402_DEFAULT_FACILITATOR,
287
- X402_DEFAULT_NETWORK,
288
- X402_EXACT_SCHEME,
289
- X402_V1_VERSION,
290
- X402_VERSION,
291
- buildPaymentRequiredV2,
292
- buildPaymentRequirements,
293
- caip2ToX402Network,
294
- chainIdFromCaip2,
295
- decodePayment,
296
- decodePaymentRequiredHeader,
297
- encodePayment,
298
- encodePaymentRequiredHeader,
299
- encodeSettleResponseHeader,
300
- exactEvmAuthorization,
301
- paymentRequiredV2FromV1,
302
- usdcContractByCaip2,
303
- usdcContractFor,
304
- usdcDomainNameFor,
305
- usdcDomainVersionFor,
306
- x402NetworkToCaip2
307
- } from "../chunk-IWQHUSMO.js";
293
+ } from "../chunk-KMED3H3A.js";
308
294
  import {
309
295
  COINGECKO_SIMPLE_PRICE_URL,
310
296
  assertValidBudget,
@@ -324,7 +310,9 @@ import {
324
310
  isDrainReceipt,
325
311
  isFundingReceipt,
326
312
  isSignedJobReceipt,
313
+ microToSats,
327
314
  monetaryJson,
315
+ msatsToMicro,
328
316
  msatsToUsd,
329
317
  msatsToUsdCents,
330
318
  msatsToUsdc,
@@ -337,7 +325,7 @@ import {
337
325
  verifyDrainReceipt,
338
326
  verifyFundingReceipt,
339
327
  verifyReceipt
340
- } from "../chunk-PAIM66YA.js";
328
+ } from "../chunk-NFMOPYBA.js";
341
329
  import {
342
330
  AGENT_MNEMONIC_FILE,
343
331
  BTC_RATE_FILE,
@@ -359,13 +347,35 @@ import {
359
347
  REQUEST_INTENTS_DIR,
360
348
  TEMPO_CHANNELS_FILE,
361
349
  WALLET_FILE,
350
+ X402_BATCH_SETTLEMENT_SCHEME,
362
351
  X402_CHANNELS_FILE,
352
+ X402_DEFAULT_FACILITATOR,
353
+ X402_DEFAULT_NETWORK,
354
+ X402_EXACT_SCHEME,
355
+ X402_V1_VERSION,
356
+ X402_VERSION,
357
+ buildPaymentRequiredV2,
358
+ buildPaymentRequirements,
359
+ caip2ToX402Network,
360
+ chainIdFromCaip2,
361
+ decodePayment,
362
+ decodePaymentRequiredHeader,
363
+ encodePayment,
364
+ encodePaymentRequiredHeader,
365
+ encodeSettleResponseHeader,
363
366
  ensureConfigDir,
364
367
  ensureConfigSubdir,
368
+ exactEvmAuthorization,
369
+ paymentRequiredV2FromV1,
365
370
  repairConfigDirMode,
366
371
  resolveRpcHttpTransportOptions,
367
- resolveRpcOverride
368
- } from "../chunk-CZULM42S.js";
372
+ resolveRpcOverride,
373
+ usdcContractByCaip2,
374
+ usdcContractFor,
375
+ usdcDomainNameFor,
376
+ usdcDomainVersionFor,
377
+ x402NetworkToCaip2
378
+ } from "../chunk-CXCQLOOK.js";
369
379
  import {
370
380
  RAIL_REFUNDABLE,
371
381
  SIGNED_REQUEST_AUTH_ID,
@@ -373,14 +383,13 @@ import {
373
383
  canonicalSignedRequestStatement,
374
384
  signedRequestStatementHeader,
375
385
  testOnlySignedRequestDomain
376
- } from "../chunk-A4YFWZ7R.js";
386
+ } from "../chunk-RCM3P2HE.js";
377
387
  import {
378
388
  canonicaliseForSigning,
379
389
  canonicalize,
380
390
  verifyCanonicalSignedRequest
381
391
  } from "../chunk-OFVV37XC.js";
382
392
  import {
383
- StepCache,
384
393
  createNoopLogger,
385
394
  createStdoutLogger,
386
395
  currentContext,
@@ -390,12 +399,15 @@ import {
390
399
  randomTraceId,
391
400
  setEmitter,
392
401
  withTraceContext
393
- } from "../chunk-6DOGTAHA.js";
402
+ } from "../chunk-QR2EF2NI.js";
394
403
  import "../chunk-66HGCPBU.js";
395
404
  import {
396
405
  redactUrl,
397
406
  redactUrlsInText
398
407
  } from "../chunk-FUJ36YDV.js";
408
+ import {
409
+ StepCache
410
+ } from "../chunk-BQJIFRAZ.js";
399
411
  import {
400
412
  FX_CACHE_TTL_MS,
401
413
  FX_RETRY_COUNT,
@@ -410,20 +422,18 @@ import {
410
422
  withInitRetry
411
423
  } from "../chunk-P6QON7ER.js";
412
424
  import {
425
+ DvmError,
413
426
  X402_NETWORKS,
414
427
  assertSupportedX402Network,
428
+ isAbortError,
429
+ providerProseSources,
430
+ transportErrorCode,
415
431
  x402NetworkByCaip2,
416
432
  x402NetworkBySlug,
417
433
  x402NetworkLabel,
418
434
  x402NetworkListForCopy,
419
435
  x402SupportedNetworksHint
420
- } from "../chunk-RLTSCKRI.js";
421
- import {
422
- DvmError,
423
- isAbortError,
424
- providerProseSources,
425
- transportErrorCode
426
- } from "../chunk-LIRAHP7B.js";
436
+ } from "../chunk-BPYMEMQX.js";
427
437
  import {
428
438
  DIAGNOSTIC_CATALOGUE_VERSION,
429
439
  bindDiagnosticCapabilityAuthority,
@@ -435,40 +445,9 @@ import {
435
445
  projectSpanRow
436
446
  } from "../chunk-ETXDTUJO.js";
437
447
 
438
- // src/lib/budget-ledger.ts
439
- import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
440
- function loadLedger() {
441
- if (!existsSync(BUDGET_FILE)) return { caps: {}, entries: [] };
442
- try {
443
- const data = JSON.parse(readFileSync(BUDGET_FILE, "utf-8"));
444
- return { caps: data.caps ?? {}, entries: data.entries ?? [] };
445
- } catch {
446
- return { caps: {}, entries: [] };
447
- }
448
- }
449
- function saveLedger(ledger, _now = /* @__PURE__ */ new Date()) {
450
- ensureConfigDir();
451
- const tmp = `${BUDGET_FILE}.tmp`;
452
- writeFileSync(tmp, JSON.stringify(ledger, null, 2) + "\n");
453
- renameSync(tmp, BUDGET_FILE);
454
- }
455
- function recordSpend(opts) {
456
- appendSpend(loadLedger(), opts);
457
- }
458
- function recordSpendOnce(opts) {
459
- const ledger = loadLedger();
460
- const kind = opts.kind ?? "job";
461
- if (ledger.entries.some(
462
- (entry) => entryKind(entry) === kind && entry.jobId === opts.jobId && entry.providerEndpoint === opts.providerEndpoint
463
- )) {
464
- return false;
465
- }
466
- appendSpend(ledger, opts);
467
- return true;
468
- }
469
- function checkBudget(opts) {
448
+ // src/lib/budget-core.ts
449
+ function checkBudgetForLedger(ledger, opts) {
470
450
  const now = opts.now ?? /* @__PURE__ */ new Date();
471
- const ledger = loadLedger();
472
451
  const plannedUsd = msatsToUsd(opts.plannedMsats, opts.btcRate);
473
452
  const windows = ["daily", "weekly", "monthly"];
474
453
  for (const window of windows) {
@@ -489,62 +468,6 @@ function checkBudget(opts) {
489
468
  }
490
469
  return { ok: true };
491
470
  }
492
- async function enforceGlobalBudget(opts) {
493
- const ledger = loadLedger();
494
- const hasCaps = ledger.caps.dailyUsd !== void 0 || ledger.caps.weeklyUsd !== void 0 || ledger.caps.monthlyUsd !== void 0;
495
- if (!hasCaps) {
496
- if (opts.btcRate !== void 0 && opts.btcRate > 0) return opts.btcRate;
497
- try {
498
- return (await resolveBtcUsdRate("payment")).rate;
499
- } catch {
500
- return 0;
501
- }
502
- }
503
- const rate = opts.btcRate && opts.btcRate > 0 ? opts.btcRate : (await resolveBtcUsdRate("payment")).rate;
504
- const check = checkBudget({ plannedMsats: opts.plannedMsats, btcRate: rate });
505
- if (!check.ok) {
506
- const windowName = check.window.charAt(0).toUpperCase() + check.window.slice(1);
507
- const suffix = check.window === "daily" ? "day" : check.window === "weekly" ? "week" : "month";
508
- const suggestedCap = Math.max(1, Math.ceil(check.limitUsd * 2));
509
- throw new DvmError(
510
- "budget_exceeded",
511
- `${windowName} budget would be exceeded by this payment.`,
512
- `${windowName} budget exhausted. The window resets at ${new Date(check.resetsAt).toISOString()}. Ask the user if they'd like to raise the cap (e.g. 'dvm budget set --${check.window} "$${suggestedCap}"' \u2014 substitute any USD amount).`,
513
- {
514
- window: check.window,
515
- spent: {
516
- display: `$${formatUsd(check.spentUsd)}`,
517
- // the cap counts both what the agent paid DVMs and what it
518
- // moved out of the Lightning float, so name the split rather than
519
- // leaving the user to wonder why the number is higher than their jobs.
520
- jobs: `$${formatUsd(check.breakdown.jobsUsd)}`,
521
- float_fundings: `$${formatUsd(check.breakdown.floatFundingsUsd)}`,
522
- credit_fundings: `$${formatUsd(check.breakdown.creditFundingsUsd)}`
523
- },
524
- limit: { display: `$${formatUsd(check.limitUsd)}/${suffix}` },
525
- resets_at: new Date(check.resetsAt).toISOString()
526
- }
527
- );
528
- }
529
- return rate;
530
- }
531
- function statusSnapshot(now = /* @__PURE__ */ new Date()) {
532
- const ledger = loadLedger();
533
- const windows = ["daily", "weekly", "monthly"];
534
- return {
535
- caps: ledger.caps,
536
- windows: windows.map((window) => {
537
- const breakdown = breakdownSince(ledger.entries, windowStart(window, now).getTime());
538
- return {
539
- window,
540
- spentUsd: breakdown.totalUsd,
541
- limitUsd: capFor(ledger.caps, window),
542
- resetsAt: windowEnd(window, now).getTime(),
543
- breakdown
544
- };
545
- })
546
- };
547
- }
548
471
  function entryKind(entry) {
549
472
  return entry.kind ?? "job";
550
473
  }
@@ -569,7 +492,7 @@ function windowEnd(window, now) {
569
492
  }
570
493
  return new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, 1));
571
494
  }
572
- function appendSpend(ledger, opts) {
495
+ function spendEntry(opts) {
573
496
  const usd = msatsToUsd(opts.msats, opts.btcRate);
574
497
  const entry = {
575
498
  timestamp: (opts.now ?? /* @__PURE__ */ new Date()).getTime(),
@@ -589,8 +512,7 @@ function appendSpend(ledger, opts) {
589
512
  if (opts.walletDebitMsats !== void 0) entry.walletDebitMsats = opts.walletDebitMsats;
590
513
  if (opts.label !== void 0) entry.label = opts.label;
591
514
  if (opts.labelSource !== void 0) entry.labelSource = opts.labelSource;
592
- ledger.entries.push(entry);
593
- saveLedger(ledger);
515
+ return entry;
594
516
  }
595
517
  function capFor(caps, window) {
596
518
  if (window === "daily") return caps.dailyUsd;
@@ -615,6 +537,110 @@ function breakdownSince(entries, sinceMs) {
615
537
  return out;
616
538
  }
617
539
 
540
+ // src/lib/budget-store.ts
541
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
542
+ import { dirname } from "path";
543
+ function loadBudgetLedger(file) {
544
+ if (!existsSync(file)) return { caps: {}, entries: [] };
545
+ try {
546
+ const data = JSON.parse(readFileSync(file, "utf-8"));
547
+ return { caps: data.caps ?? {}, entries: data.entries ?? [] };
548
+ } catch {
549
+ return { caps: {}, entries: [] };
550
+ }
551
+ }
552
+ function saveBudgetLedger(file, ledger) {
553
+ mkdirSync(dirname(file), { recursive: true, mode: 448 });
554
+ const tmp = `${file}.tmp`;
555
+ writeFileSync(tmp, JSON.stringify(ledger, null, 2) + "\n");
556
+ renameSync(tmp, file);
557
+ }
558
+ function appendBudgetSpend(file, opts, once = false) {
559
+ const ledger = loadBudgetLedger(file);
560
+ const kind = opts.kind ?? "job";
561
+ if (once && ledger.entries.some(
562
+ (entry) => entryKind(entry) === kind && entry.jobId === opts.jobId && entry.providerEndpoint === opts.providerEndpoint
563
+ )) {
564
+ return false;
565
+ }
566
+ ledger.entries.push(spendEntry(opts));
567
+ saveBudgetLedger(file, ledger);
568
+ return true;
569
+ }
570
+
571
+ // src/lib/budget-ledger.ts
572
+ function loadLedger() {
573
+ return loadBudgetLedger(BUDGET_FILE);
574
+ }
575
+ function saveLedger(ledger, _now = /* @__PURE__ */ new Date()) {
576
+ saveBudgetLedger(BUDGET_FILE, ledger);
577
+ }
578
+ function recordSpend(opts) {
579
+ appendBudgetSpend(BUDGET_FILE, opts);
580
+ }
581
+ function recordSpendOnce(opts) {
582
+ return appendBudgetSpend(BUDGET_FILE, opts, true);
583
+ }
584
+ function checkBudget(opts) {
585
+ return checkBudgetForLedger(loadLedger(), opts);
586
+ }
587
+ async function enforceGlobalBudget(opts) {
588
+ const ledger = loadLedger();
589
+ const hasCaps = ledger.caps.dailyUsd !== void 0 || ledger.caps.weeklyUsd !== void 0 || ledger.caps.monthlyUsd !== void 0;
590
+ if (!hasCaps) {
591
+ if (opts.btcRate !== void 0 && opts.btcRate > 0) return opts.btcRate;
592
+ try {
593
+ return (await resolveBtcUsdRate("payment")).rate;
594
+ } catch {
595
+ return 0;
596
+ }
597
+ }
598
+ const rate = opts.btcRate && opts.btcRate > 0 ? opts.btcRate : (await resolveBtcUsdRate("payment")).rate;
599
+ const check = checkBudget({ plannedMsats: opts.plannedMsats, btcRate: rate });
600
+ if (!check.ok) {
601
+ const windowName = check.window.charAt(0).toUpperCase() + check.window.slice(1);
602
+ const suffix = check.window === "daily" ? "day" : check.window === "weekly" ? "week" : "month";
603
+ const suggestedCap = Math.max(1, Math.ceil(check.limitUsd * 2));
604
+ throw new DvmError(
605
+ "budget_exceeded",
606
+ `${windowName} budget would be exceeded by this payment.`,
607
+ `${windowName} budget exhausted. The window resets at ${new Date(check.resetsAt).toISOString()}. Ask the user if they'd like to raise the cap (e.g. 'dvm budget set --${check.window} "$${suggestedCap}"' \u2014 substitute any USD amount).`,
608
+ {
609
+ window: check.window,
610
+ spent: {
611
+ display: `$${formatUsd(check.spentUsd)}`,
612
+ // the cap counts both what the agent paid DVMs and what it
613
+ // moved out of the Lightning float, so name the split rather than
614
+ // leaving the user to wonder why the number is higher than their jobs.
615
+ jobs: `$${formatUsd(check.breakdown.jobsUsd)}`,
616
+ float_fundings: `$${formatUsd(check.breakdown.floatFundingsUsd)}`,
617
+ credit_fundings: `$${formatUsd(check.breakdown.creditFundingsUsd)}`
618
+ },
619
+ limit: { display: `$${formatUsd(check.limitUsd)}/${suffix}` },
620
+ resets_at: new Date(check.resetsAt).toISOString()
621
+ }
622
+ );
623
+ }
624
+ return rate;
625
+ }
626
+ function statusSnapshot(now = /* @__PURE__ */ new Date()) {
627
+ const ledger = loadLedger();
628
+ const windows = ["daily", "weekly", "monthly"];
629
+ return {
630
+ caps: ledger.caps,
631
+ windows: windows.map((window) => {
632
+ const breakdown = breakdownSince(ledger.entries, windowStart(window, now).getTime());
633
+ return {
634
+ window,
635
+ spentUsd: breakdown.totalUsd,
636
+ limitUsd: capFor(ledger.caps, window),
637
+ resetsAt: windowEnd(window, now).getTime(),
638
+ breakdown
639
+ };
640
+ })
641
+ };
642
+ }
643
+
618
644
  // src/lib/cashu/builder-subkey.ts
619
645
  import { createHash } from "crypto";
620
646
  import { HDKey } from "@scure/bip32";
@@ -779,7 +805,7 @@ function randomNonce() {
779
805
  }
780
806
 
781
807
  // src/lib/credit-fund.ts
782
- import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
808
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
783
809
  import { Challenge, Credential } from "mppx";
784
810
  import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
785
811
 
@@ -3298,716 +3324,293 @@ function deriveAgentRefundSubkeyPubkey(mnemonic, canonicalDvmId) {
3298
3324
  return deriveAgentRefundSubkey(mnemonic, canonicalDvmId).pubkey;
3299
3325
  }
3300
3326
 
3301
- // src/lib/credit-policy.ts
3302
- import { randomUUID } from "crypto";
3303
-
3304
- // src/lib/x402/menu.ts
3305
- function offersX402CreditFunding(menu, network) {
3306
- return matchesNetwork(x402CreditFundingNetworks(menu), network);
3307
- }
3308
- function offersX402CreditChannel(menu, network) {
3309
- return matchesNetwork(x402CreditChannelNetworks(menu), network);
3327
+ // src/lib/credit-funding-contracts.ts
3328
+ var CREDIT_FUND_RAILS = ["cashu", "lightning", "x402", "tempo"];
3329
+ var CREDIT_DRAIN_RAILS = ["cashu", "x402", "tempo"];
3330
+ function isCreditFundRail(value) {
3331
+ return typeof value === "string" && CREDIT_FUND_RAILS.includes(value);
3310
3332
  }
3311
- function x402CreditFlavour(menu, network) {
3312
- if (offersX402CreditChannel(menu, network)) return X402_BATCH_SETTLEMENT_SCHEME;
3313
- if (matchesNetwork(x402CreditExactNetworks(menu), network)) return X402_EXACT_SCHEME;
3314
- return void 0;
3333
+ var STABLECOIN_FUND_RAILS = ["x402", "tempo"];
3334
+ function isStablecoinFundRail(rail) {
3335
+ return STABLECOIN_FUND_RAILS.includes(rail);
3315
3336
  }
3316
- function offersX402CreditFlavour(menu, network, flavour) {
3317
- return matchesNetwork(
3318
- flavour === X402_BATCH_SETTLEMENT_SCHEME ? x402CreditChannelNetworks(menu) : x402CreditExactNetworks(menu),
3319
- network
3320
- );
3337
+ var CREDIT_FUNDING_MODES = ["one-shot", "reusable"];
3338
+ function isCreditFundingMode(value) {
3339
+ return typeof value === "string" && CREDIT_FUNDING_MODES.includes(value);
3321
3340
  }
3322
- function x402InstrumentForFlavour(flavour) {
3323
- return flavour === X402_BATCH_SETTLEMENT_SCHEME ? "x402_channel" : "x402_exact";
3341
+ function fundingModeForTempoIntent(intent) {
3342
+ return intent === "session" ? "reusable" : "one-shot";
3324
3343
  }
3325
- function x402FlavourForInstrument(instrument) {
3326
- if (instrument === "x402_channel") return X402_BATCH_SETTLEMENT_SCHEME;
3327
- if (instrument === "x402_exact") return X402_EXACT_SCHEME;
3328
- return void 0;
3344
+ function fundingModeForX402Flavour(flavour) {
3345
+ return flavour === X402_BATCH_SETTLEMENT_SCHEME ? "reusable" : "one-shot";
3329
3346
  }
3330
- function x402CreditChannelNetworks(menu) {
3331
- return schemeNetworks(menu, X402_BATCH_SETTLEMENT_SCHEME);
3347
+ function fundWireAmountMicro(priceMicroServer, fundMsats, requiredMsats) {
3348
+ if (fundMsats === requiredMsats) return priceMicroServer;
3349
+ return Math.round(priceMicroServer * fundMsats / requiredMsats);
3332
3350
  }
3333
- function x402CreditExactNetworks(menu) {
3334
- return schemeNetworks(menu, X402_EXACT_SCHEME);
3351
+ function readFundValuation(fields) {
3352
+ const structured = usableMicro(fields.expectedMicro);
3353
+ if (structured !== void 0) return { kind: "figure", micro: structured };
3354
+ const match = /quoted price \((\d+)\)/.exec(fields.message ?? "");
3355
+ const stated = match ? usableMicro(Number(match[1])) : void 0;
3356
+ if (stated !== void 0) return { kind: "figure", micro: stated };
3357
+ const providerStatedSomething = fields.expectedMicro !== void 0 || match !== null;
3358
+ return providerStatedSomething ? { kind: "malformed" } : { kind: "absent" };
3335
3359
  }
3336
- function x402CreditFundingNetworks(menu) {
3337
- return [.../* @__PURE__ */ new Set([...x402CreditChannelNetworks(menu), ...x402CreditExactNetworks(menu)])];
3360
+ function usableMicro(value) {
3361
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
3338
3362
  }
3339
- function schemeNetworks(menu, scheme) {
3340
- return (menu?.x402?.schemes ?? []).filter((entry) => entry.scheme === scheme).map((entry) => entry.network);
3363
+
3364
+ // src/lib/router-feedback-claim.ts
3365
+ import { createHmac, timingSafeEqual } from "crypto";
3366
+ var ROUTER_FEEDBACK_CLAIM_HEADER = "x-dvmkit-router-feedback-claim";
3367
+ var ROUTER_FEEDBACK_CLAIM_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
3368
+ function normalizeFeedbackEndpointHost(endpointOrHost) {
3369
+ try {
3370
+ const url = new URL(
3371
+ endpointOrHost.includes("://") ? endpointOrHost : `https://${endpointOrHost}`
3372
+ );
3373
+ return url.host === "" ? null : url.host.toLowerCase();
3374
+ } catch {
3375
+ return null;
3376
+ }
3341
3377
  }
3342
- function matchesNetwork(networks, network) {
3343
- return networks.some((candidate) => network === void 0 || candidate === network);
3378
+ function issueRouterFeedbackClaim(secret, attribution, now = Date.now()) {
3379
+ const body = {
3380
+ v: 2,
3381
+ dvm_id: attribution.dvmId,
3382
+ job_id: attribution.jobId,
3383
+ endpoint_host: attribution.endpointHost,
3384
+ exp: now + ROUTER_FEEDBACK_CLAIM_TTL_MS
3385
+ };
3386
+ const encoded = Buffer.from(JSON.stringify(body), "utf8").toString("base64url");
3387
+ const signature = createHmac("sha256", secret).update(encoded).digest("base64url");
3388
+ return `${encoded}.${signature}`;
3389
+ }
3390
+ function verifyRouterFeedbackClaim(secret, token, now = Date.now()) {
3391
+ const dot = token.indexOf(".");
3392
+ if (dot <= 0 || dot === token.length - 1) return null;
3393
+ const encoded = token.slice(0, dot);
3394
+ const presentedSignature = token.slice(dot + 1);
3395
+ const expectedSignature = createHmac("sha256", secret).update(encoded).digest("base64url");
3396
+ if (!safeEqual(expectedSignature, presentedSignature)) return null;
3397
+ let body;
3398
+ try {
3399
+ body = JSON.parse(
3400
+ Buffer.from(encoded, "base64url").toString("utf8")
3401
+ );
3402
+ } catch {
3403
+ return null;
3404
+ }
3405
+ if (body.v !== 2 || typeof body.dvm_id !== "string" || body.dvm_id.length === 0 || typeof body.job_id !== "string" || body.job_id.length === 0 || typeof body.endpoint_host !== "string" || body.endpoint_host.length === 0 || typeof body.exp !== "number" || !Number.isFinite(body.exp) || body.exp <= now) {
3406
+ return null;
3407
+ }
3408
+ return {
3409
+ dvmId: body.dvm_id,
3410
+ jobId: body.job_id,
3411
+ endpointHost: body.endpoint_host,
3412
+ expiresAt: body.exp
3413
+ };
3414
+ }
3415
+ function safeEqual(expected, presented) {
3416
+ const expectedBuffer = Buffer.from(expected);
3417
+ const presentedBuffer = Buffer.from(presented);
3418
+ return expectedBuffer.length === presentedBuffer.length && timingSafeEqual(expectedBuffer, presentedBuffer);
3344
3419
  }
3345
3420
 
3346
- // src/lib/credit-policy.ts
3347
- var TIER0_CAP_MICRO = 5e5;
3348
- var TIER1_CAP_MICRO = 2e6;
3349
- var TIER1_MIN_RECEIPTS = 3;
3350
- var TIER2_MIN_RECEIPTS = 15;
3351
- var TIER2_MIN_SPAN_MS = 7 * 24 * 60 * 60 * 1e3;
3352
- var OPEN_TARGET_JOBS = 20;
3353
- var CASHU_REFILL_TARGET_JOBS = 40;
3354
- var TOPUP_THRESHOLD_FRACTION = 0.25;
3355
- var DEFAULT_CREDIT_POSTURE = "suggest";
3356
- var POCKET_TARGET_MICRO = 5e6;
3357
- function trustTierFor(args) {
3358
- const ladder = trustLadderFor(args);
3359
- return { tier: ladder.tier, capMicro: ladder.capMicro ?? args.menu.max_micro };
3421
+ // src/lib/wallet-agent/spend.ts
3422
+ import {
3423
+ getEncodedToken,
3424
+ MintOperationError
3425
+ } from "@cashu/cashu-ts";
3426
+ import { randomUUID } from "crypto";
3427
+
3428
+ // src/lib/wallet-agent/select.ts
3429
+ function selectProofsForSpend(args) {
3430
+ if (args.amountSats <= 0) {
3431
+ return {
3432
+ error: {
3433
+ code: "insufficient_balance",
3434
+ message: `amountSats must be positive; got ${args.amountSats}.`,
3435
+ hint: "Pass a positive sat amount."
3436
+ }
3437
+ };
3438
+ }
3439
+ if (args.mintAllowlist.length === 0) {
3440
+ return { error: noAcceptedMintsError() };
3441
+ }
3442
+ const reserveSats = args.swapReserveSats ?? (() => 0);
3443
+ let largestEligibleBalance = 0;
3444
+ for (const mint of args.mintAllowlist) {
3445
+ const candidates = eligibleProofsAt(args.proofs, mint);
3446
+ const balance = candidates.reduce((sum, p) => sum + p.amount, 0);
3447
+ largestEligibleBalance = Math.max(largestEligibleBalance, balance);
3448
+ if (balance < args.amountSats) continue;
3449
+ const selected = [];
3450
+ let total = 0;
3451
+ for (const p of candidates) {
3452
+ selected.push(p);
3453
+ total += p.amount;
3454
+ const required = args.amountSats + reserveSats(selected);
3455
+ if (total >= required) {
3456
+ return { selected, mint, totalSats: total };
3457
+ }
3458
+ }
3459
+ }
3460
+ return { error: noCoverError(largestEligibleBalance, args.amountSats) };
3360
3461
  }
3361
- function trustLadderFor(args) {
3362
- const successes = completedReceiptsFor(
3363
- args.receipts,
3364
- args.endpoint,
3365
- args.signingPubkey,
3366
- args.identity
3367
- );
3368
- const tier0Cap = args.tier0CapMicro ?? TIER0_CAP_MICRO;
3369
- const tier1Cap = args.tier1CapMicro ?? TIER1_CAP_MICRO;
3370
- const builderMax = args.menu?.max_micro;
3371
- const issued = successes.map((r) => r.receipt.issued_at * 1e3);
3372
- const spanMs = issued.length > 1 ? Math.max(...issued) - Math.min(...issued) : 0;
3373
- const count = successes.length;
3374
- if (count >= TIER2_MIN_RECEIPTS && spanMs >= TIER2_MIN_SPAN_MS) {
3462
+ function coveringMintsForSpend(args) {
3463
+ if (args.amountSats <= 0) {
3375
3464
  return {
3376
- tier: 2,
3377
- ...builderMax !== void 0 ? { capMicro: builderMax } : {},
3378
- receipts: count,
3379
- spanMs
3465
+ error: {
3466
+ code: "insufficient_balance",
3467
+ message: `amountSats must be positive; got ${args.amountSats}.`,
3468
+ hint: "Pass a positive sat amount."
3469
+ }
3380
3470
  };
3381
3471
  }
3382
- const toTier2 = {
3383
- tier: 2,
3384
- ...builderMax !== void 0 ? { capMicro: builderMax } : {},
3385
- receiptsNeeded: Math.max(TIER2_MIN_RECEIPTS - count, 0),
3386
- spanMs: TIER2_MIN_SPAN_MS
3387
- };
3388
- if (count >= TIER1_MIN_RECEIPTS) {
3389
- return { tier: 1, capMicro: tier1Cap, receipts: count, spanMs, next: toTier2 };
3472
+ if (args.mintAllowlist.length === 0) {
3473
+ return { error: noAcceptedMintsError() };
3474
+ }
3475
+ const mints = [];
3476
+ let largestEligibleBalance = 0;
3477
+ for (const mint of args.mintAllowlist) {
3478
+ const balance = eligibleProofsAt(args.proofs, mint).reduce((sum, p) => sum + p.amount, 0);
3479
+ largestEligibleBalance = Math.max(largestEligibleBalance, balance);
3480
+ if (balance >= args.amountSats && !mints.includes(mint)) mints.push(mint);
3390
3481
  }
3482
+ if (mints.length === 0) {
3483
+ return { error: noCoverError(largestEligibleBalance, args.amountSats) };
3484
+ }
3485
+ return { mints };
3486
+ }
3487
+ function eligibleProofsAt(proofs, mint) {
3488
+ return proofs.filter((p) => p.mint === mint && p.status === "valid" && p.reserved_for === void 0).slice().sort((a, b) => b.amount - a.amount);
3489
+ }
3490
+ function noAcceptedMintsError() {
3391
3491
  return {
3392
- tier: 0,
3393
- capMicro: tier0Cap,
3394
- receipts: count,
3395
- spanMs,
3396
- next: { tier: 1, capMicro: tier1Cap, receiptsNeeded: TIER1_MIN_RECEIPTS - count }
3492
+ code: "no_eligible_mint",
3493
+ message: "DVM advertises no Cashu mints for the agent-wallet path.",
3494
+ hint: "Use a different DVM, or fall back to the legacy --auto-pay flow."
3397
3495
  };
3398
3496
  }
3399
- function effectiveCreditPosture(config, endpoint, identity) {
3400
- const postures = perDvmEntriesFor(config, endpoint, identity).map(([, per2]) => per2.posture).filter(isCreditPosture);
3401
- const per = postures.reduce(
3402
- (strictest, posture) => strictest === void 0 || POSTURE_STRICTNESS[posture] > POSTURE_STRICTNESS[strictest] ? posture : strictest,
3403
- void 0
3404
- );
3405
- if (per !== void 0) return { posture: per, source: "per_dvm" };
3406
- const global = config?.posture;
3407
- if (isCreditPosture(global)) return { posture: global, source: "global" };
3408
- return { posture: DEFAULT_CREDIT_POSTURE, source: "default" };
3409
- }
3410
- function effectiveFundingRail(config, endpoint, identity) {
3411
- let conflict;
3412
- if (endpoint !== void 0) {
3413
- const pinned = perDvmEntriesFor(config, endpoint, identity).flatMap(
3414
- ([origin, per]) => isCreditFundRail(per.fundingRail) ? [[origin, per.fundingRail]] : []
3415
- );
3416
- const rails = [...new Set(pinned.map(([, rail]) => rail))];
3417
- if (rails.length > 1) conflict = { rails, endpoints: pinned.map(([origin]) => origin) };
3418
- else if (rails.length === 1) return { rail: rails[0], source: "per_dvm" };
3497
+ function noCoverError(largestEligibleBalance, amountSats) {
3498
+ if (largestEligibleBalance === 0) {
3499
+ return {
3500
+ code: "no_eligible_mint",
3501
+ message: "Agent wallet has no proofs at any of the DVM's accepted mints.",
3502
+ hint: "Fund the wallet at one of the DVM's accepted mints with: dvm wallet fund <amount>"
3503
+ };
3419
3504
  }
3420
- const global = config?.fundingRail;
3421
- if (isCreditFundRail(global))
3422
- return { rail: global, source: "global", ...conflict ? { conflict } : {} };
3423
- return { source: "default", ...conflict ? { conflict } : {} };
3505
+ return {
3506
+ code: "insufficient_balance",
3507
+ message: `Agent wallet has ${largestEligibleBalance} sats at the DVM's mints; need ${amountSats} plus the per-call mint swap fee.`,
3508
+ hint: "Top the wallet up with: dvm wallet fund <amount>"
3509
+ };
3424
3510
  }
3425
- function automaticCreditBoundRail(stored) {
3426
- if (stored?.status !== "active" || stored.reconciliationPending) return void 0;
3427
- return isCreditFundRail(stored.rail) ? stored.rail : void 0;
3511
+
3512
+ // src/lib/wallet-agent/submission-binding.ts
3513
+ import { createHash as createHash2 } from "crypto";
3514
+ function cashuSubmissionBindingFingerprint(binding) {
3515
+ return createHash2("sha256").update(canonicaliseForSigning(binding)).digest("hex");
3428
3516
  }
3429
- function trustOverrideCapMicro(args) {
3430
- const raw2 = args.builderPubkey ? lookupOverride(args.config?.trustOverrides, args.builderPubkey) : void 0;
3431
- if (!raw2) return void 0;
3432
- if (raw2.trim().toLowerCase() === "builder-max") {
3433
- return args.menu?.max_micro ?? Number.POSITIVE_INFINITY;
3517
+ function cashuSubmissionBindingFromContext(context) {
3518
+ if (context.strict_per_call !== true || context.route === "credit" || typeof context.dvm_id !== "string" || context.dvm_id.length === 0 || typeof context.capability !== "string" || context.capability.length === 0 || typeof context.pinned_mint !== "string" || context.pinned_mint.length === 0 || !context.policy) {
3519
+ return null;
3434
3520
  }
3435
- return parseCapMicro(raw2);
3436
- }
3437
- function tierCapOverrides(config) {
3438
- const out = {};
3439
- const tier0 = parseCapMicro(config?.tier0Cap);
3440
- const tier1 = parseCapMicro(config?.tier1Cap);
3441
- if (tier0 !== void 0) out.tier0CapMicro = tier0;
3442
- if (tier1 !== void 0) out.tier1CapMicro = tier1;
3443
- return out;
3444
- }
3445
- function sizeCreditMicro(args) {
3446
- const target = (args.targetJobs ?? OPEN_TARGET_JOBS) * args.priceMicro;
3447
- let size = Math.max(args.menu.min_micro, Math.min(target, args.capMicro));
3448
- const residualCeiling = args.menu.max_micro + args.priceMicro;
3449
- size = Math.min(size, residualCeiling);
3450
- return Math.max(size, args.menu.min_micro);
3451
- }
3452
- function refillTargetMicro(args) {
3453
- const target = (args.targetJobs ?? CASHU_REFILL_TARGET_JOBS) * args.priceMicro;
3454
- return Math.max(args.menu.min_micro, Math.min(target, args.capMicro, args.menu.max_micro));
3521
+ return {
3522
+ dvmId: context.dvm_id,
3523
+ capability: context.capability,
3524
+ requestBody: context.submit_body,
3525
+ mint: context.pinned_mint,
3526
+ signer: context.signing_pubkey ?? null,
3527
+ policy: context.policy
3528
+ };
3455
3529
  }
3456
- function planCreditForQuote(args) {
3457
- const none = { mode: "none" };
3458
- const menu = args.menu;
3459
- if (!menu || !args.signingPubkey) return none;
3460
- const posture = effectiveCreditPosture(args.config, args.endpoint, args.identity).posture;
3461
- if (posture === "off") return none;
3462
- if (menu.currency.toLowerCase() !== "usd") return none;
3463
- const canCashu = menu.funding.includes("cashu");
3464
- const canFloat = menu.funding.includes("lightning") && args.floatConnected === true;
3465
- const canX402 = menu.funding.includes("x402") && args.x402Connected === true && offersX402CreditFunding(menu, args.x402Network);
3466
- const canMpp = menu.funding.includes("tempo") && args.tempoConnected === true && offersTempoCreditFunding(menu);
3467
- if (!canCashu && !canFloat && !canX402 && !canMpp) return none;
3468
- if (!Number.isFinite(args.priceMsats) || args.priceMsats <= 0) return none;
3469
- const stored = args.stored;
3470
- if (stored?.reconciliationPending) return none;
3471
- const refilling = stored?.status === "active" && stored.currency === menu.currency;
3472
- const rateUnavailable = !Number.isFinite(args.btcRate) || args.btcRate <= 0;
3473
- if (!refilling) {
3474
- const completed = completedReceiptsFor(
3475
- args.receipts,
3476
- args.endpoint,
3477
- args.signingPubkey,
3478
- args.identity
3479
- );
3480
- if (completed.length === 0) return none;
3481
- const identity = args.identity;
3482
- if (rateUnavailable && identity && !completed.some((entry) => {
3483
- const attested = attestedDvmIdentity(entry.builder);
3484
- return attested?.dvmId === identity.dvmId && attested.builderPubkey === identity.builderPubkey;
3485
- })) {
3486
- return none;
3487
- }
3488
- }
3489
- if (rateUnavailable) {
3490
- return posture === "auto" ? {
3491
- mode: "none",
3492
- fundingSkipped: {
3493
- reason: "btc_rate_unavailable",
3494
- operation: refilling ? "refill" : "open"
3530
+
3531
+ // src/lib/wallet-agent/submission-context.ts
3532
+ import { createHash as createHash3 } from "crypto";
3533
+ function bindCashuTokenToSubmissionContext(context, cashuToken) {
3534
+ if (!context) return void 0;
3535
+ const commitment = createHash3("sha256").update(cashuToken).digest("hex");
3536
+ if (context.route === "credit") {
3537
+ const fund2 = context.credit_body?.fund;
3538
+ if (!fund2) return context;
3539
+ return {
3540
+ ...context,
3541
+ credit_body: {
3542
+ ...context.credit_body,
3543
+ fund: { ...fund2, commitment }
3495
3544
  }
3496
- } : none;
3545
+ };
3497
3546
  }
3498
- const priceMicro = msatsToMicro(args.priceMsats, args.btcRate);
3499
- if (priceMicro <= 0) return none;
3500
- const tier = trustTierFor({
3501
- receipts: args.receipts,
3502
- endpoint: args.endpoint,
3503
- signingPubkey: args.signingPubkey,
3504
- ...args.identity ? { identity: args.identity } : {},
3505
- menu,
3506
- ...tierCapOverrides(args.config)
3507
- });
3508
- const targetJobs = targetJobsFor(args.config, args.endpoint, args.identity);
3509
- const pin = effectiveFundingRail(args.config, args.endpoint, args.identity);
3510
- const eligible = [
3511
- ...canCashu ? ["cashu"] : [],
3512
- ...canFloat ? ["lightning"] : [],
3513
- ...canX402 ? ["x402"] : [],
3514
- ...canMpp ? ["tempo"] : []
3515
- ];
3516
- const routeFor = (fundMicro2, boundRail2) => {
3517
- const forced = boundRail2 ?? pin.rail;
3518
- const pick = pickCreditFundRail({
3519
- rails: eligible,
3520
- amountMicro: fundMicro2,
3521
- menu,
3522
- ...args.x402Network ? { x402Network: args.x402Network } : {},
3523
- ...forced ? { forced } : {}
3524
- });
3525
- const head = forced ?? pick.order[0];
3526
- return { route: head === "cashu" ? "cashu" : "deferred", reason: pick.reason };
3527
- };
3528
- const pinned = pin.rail && pin.source !== "default" ? { rail: pin.rail, source: pin.source } : void 0;
3529
- const defer = (reason, creditId, fundMicro2, targetMicro2, maxDepositMicro, forced = pinned) => ({
3530
- mode: "none",
3531
- deferred: {
3532
- reason,
3533
- creditId,
3534
- fundMicro: fundMicro2,
3535
- targetMicro: targetMicro2,
3536
- maxDepositMicro,
3537
- currency: menu.currency,
3538
- menu,
3539
- ...forced ? { pinnedRail: forced.rail, pinnedRailSource: forced.source } : {}
3540
- }
3541
- });
3542
- if (refilling) {
3543
- const targetMicro2 = refillTargetMicro({
3544
- menu,
3545
- priceMicro,
3546
- capMicro: tier.capMicro,
3547
- ...targetJobs !== void 0 ? { targetJobs } : {}
3548
- });
3549
- const boundRail2 = isCreditFundRail(stored.rail) ? stored.rail : void 0;
3550
- const forcedRefill = boundRail2 ? { rail: boundRail2, source: "credit_bound" } : pinned;
3551
- const railFloorMicro = forcedRefill?.rail === "lightning" && menu.lightning_min_micro !== void 0 ? Math.max(menu.min_micro, menu.lightning_min_micro) : menu.min_micro;
3552
- const fundMicro2 = Math.min(
3553
- Math.max(railFloorMicro, targetMicro2 - Math.max(stored.remainingMicro, 0) + priceMicro),
3554
- menu.max_micro
3555
- );
3556
- if (posture === "suggest") {
3557
- return {
3558
- mode: "none",
3559
- suppressed: { reason: "refill", fundMicro: fundMicro2, targetMicro: targetMicro2, currency: menu.currency }
3560
- };
3561
- }
3562
- const refillRoute = routeFor(fundMicro2, boundRail2);
3563
- if (refillRoute.route === "deferred") {
3564
- return defer("refill", stored.creditId, fundMicro2, targetMicro2, fundMicro2, forcedRefill);
3547
+ const data = context.submit_body.data;
3548
+ const fund = data?.fund;
3549
+ if (!data || !fund) return context;
3550
+ return {
3551
+ ...context,
3552
+ submit_body: {
3553
+ ...context.submit_body,
3554
+ data: { ...data, fund: { ...fund, commitment } }
3565
3555
  }
3566
- if (!canCashu) return none;
3556
+ };
3557
+ }
3558
+
3559
+ // src/lib/wallet-agent/spend.ts
3560
+ async function prepareAgentSpend(args) {
3561
+ if (args.maxCashuPrincipalSats !== void 0 && (!Number.isSafeInteger(args.maxCashuPrincipalSats) || args.maxCashuPrincipalSats < 0)) {
3567
3562
  return {
3568
- mode: "topup",
3569
- creditId: stored.creditId,
3570
- drawId: randomUUID(),
3571
- ...fundAmounts(fundMicro2, args.btcRate),
3572
- currency: menu.currency,
3573
- targetMicro: targetMicro2,
3574
- railReason: refillRoute.reason
3563
+ error: {
3564
+ code: "invalid_max_cashu_principal",
3565
+ message: "The Cashu principal limit must be an exact non-negative integer number of sats.",
3566
+ hint: "Pass --max-cashu-principal-sats with a value such as 20 or 100."
3567
+ }
3575
3568
  };
3576
3569
  }
3577
- const fundMicro = sizeCreditMicro({
3578
- menu,
3579
- priceMicro,
3580
- capMicro: tier.capMicro,
3581
- ...targetJobs !== void 0 ? { targetJobs: Math.min(targetJobs, OPEN_TARGET_JOBS) } : {}
3582
- });
3583
- const targetMicro = refillTargetMicro({
3584
- menu,
3585
- priceMicro,
3586
- capMicro: tier.capMicro,
3587
- ...targetJobs !== void 0 ? { targetJobs } : {}
3588
- });
3589
- if (posture === "suggest") {
3570
+ if (args.maxCashuPrincipalSats !== void 0 && args.amountSats > args.maxCashuPrincipalSats) {
3571
+ return principalCapFailure(args.amountSats, args.maxCashuPrincipalSats);
3572
+ }
3573
+ if (args.maxCashuFeeSats !== void 0 && (!Number.isSafeInteger(args.maxCashuFeeSats) || args.maxCashuFeeSats < 0)) {
3590
3574
  return {
3591
- mode: "none",
3592
- suppressed: { reason: "open", fundMicro, targetMicro, currency: menu.currency }
3575
+ error: {
3576
+ code: "invalid_max_cashu_fee",
3577
+ message: "The Cashu fee limit must be an exact non-negative integer number of sats.",
3578
+ hint: "Pass --max-cashu-fee-sats with a value such as 1 or 2."
3579
+ }
3593
3580
  };
3594
3581
  }
3595
- const openRoute = routeFor(fundMicro);
3596
- if (openRoute.route === "deferred") {
3597
- return defer("open", randomUUID(), fundMicro, targetMicro, fundMicro);
3582
+ const strict = args.mode === "strict_per_call";
3583
+ let submitContext = args.submitContext;
3584
+ if (strict) {
3585
+ const strictError = strictSpendInputError(args);
3586
+ if (strictError) return strictError;
3587
+ if (!submitContext)
3588
+ return strictSpendFailure("The strict Cashu request has no recovery context.");
3589
+ const binding = cashuSubmissionBindingFromContext(submitContext);
3590
+ if (!binding)
3591
+ return strictSpendFailure("The strict Cashu request has no complete recovery binding.");
3592
+ const bindingFingerprint = cashuSubmissionBindingFingerprint(binding);
3593
+ if (submitContext.binding_fingerprint !== bindingFingerprint)
3594
+ return strictSpendFailure(
3595
+ "The strict Cashu request binding does not match its request body."
3596
+ );
3597
+ submitContext = { ...submitContext, binding_fingerprint: bindingFingerprint };
3598
3598
  }
3599
- if (!canCashu) return none;
3600
- return {
3601
- mode: "open",
3602
- creditId: randomUUID(),
3603
- drawId: randomUUID(),
3604
- ...fundAmounts(fundMicro, args.btcRate),
3605
- currency: menu.currency,
3606
- targetMicro,
3607
- railReason: openRoute.reason
3608
- };
3609
- }
3610
- function planProbeDraw(args) {
3611
- const now = args.now ?? Date.now();
3612
- const implicit = [...args.implicit ?? []].filter(
3613
- (credit) => credit.status === "active" && credit.remainingMicro > 0 && credit.expiryMs > now
3614
- ).sort((a, b) => a.expiryMs - b.expiryMs).at(0);
3615
- if (implicit) {
3616
- if (effectiveCreditPosture(args.config, implicit.endpoint, args.identity).posture === "off") {
3599
+ const now = args.now ?? (() => Date.now());
3600
+ const uuid = args.uuid ?? (() => randomUUID());
3601
+ const timeoutSec = args.inFlightTimeoutSec ?? inFlightTimeoutSeconds();
3602
+ const getWalletForMint = args.getWalletForMint ?? getWallet;
3603
+ const refundClause = resolveRefundClause(args.cashuP2pk, args.locktimeSeconds, now(), {
3604
+ readMnemonic: args.readMnemonic
3605
+ });
3606
+ const lock1 = await withWalletLock(() => {
3607
+ const initial = loadAgentWallet();
3608
+ if (!initial) {
3617
3609
  return {
3618
- mode: "none",
3619
- drawSkipped: {
3620
- reason: "posture_off",
3621
- creditId: implicit.creditId,
3622
- remainingMicro: implicit.remainingMicro
3623
- }
3624
- };
3625
- }
3626
- return {
3627
- mode: "draw",
3628
- creditId: implicit.creditId,
3629
- drawId: randomUUID(),
3630
- source: "implicit"
3631
- };
3632
- }
3633
- const stored = args.stored;
3634
- if (!stored) return { mode: "none" };
3635
- const skip = (reason, extra) => ({
3636
- mode: "none",
3637
- drawSkipped: {
3638
- reason,
3639
- creditId: stored.creditId,
3640
- remainingMicro: stored.remainingMicro,
3641
- ...extra
3642
- }
3643
- });
3644
- if (effectiveCreditPosture(args.config, stored.endpoint, args.identity).posture === "off") {
3645
- return skip("posture_off");
3646
- }
3647
- if (stored.status !== "active") return skip("not_active");
3648
- if (stored.reconciliationPending) return skip("reconciliation_pending");
3649
- if (stored.remainingMicro <= 0 && !stored.provisional) return skip("exhausted");
3650
- if (stored.expiryMs > 0 && stored.expiryMs <= now) return skip("expired");
3651
- if (stored.lastPriceMicro !== void 0 && args.btcRate > 0) {
3652
- const ceilingMicro = msatsToMicro(args.drawCeilingMsats, args.btcRate);
3653
- if (ceilingMicro > 0 && stored.lastPriceMicro > ceilingMicro) {
3654
- return skip("price_above_ceiling", {
3655
- ceilingMicro,
3656
- lastPriceMicro: stored.lastPriceMicro,
3657
- ceilingSource: args.drawCeilingSource
3658
- });
3659
- }
3660
- }
3661
- return { mode: "draw", creditId: stored.creditId, drawId: randomUUID(), source: "prepaid" };
3662
- }
3663
- function needsTopup(balanceMicro, targetMicro) {
3664
- return targetMicro > 0 && balanceMicro < targetMicro * TOPUP_THRESHOLD_FRACTION;
3665
- }
3666
- function pocketTargetMicro(config) {
3667
- return parseCapMicro(config?.pocketTarget) ?? POCKET_TARGET_MICRO;
3668
- }
3669
- function needsPocketRefill(balanceMicro, targetMicro) {
3670
- return needsTopup(balanceMicro, targetMicro);
3671
- }
3672
- function fundWireAmountMicro(priceMicroServer, fundMsats, requiredMsats) {
3673
- if (fundMsats === requiredMsats) return priceMicroServer;
3674
- return Math.round(priceMicroServer * fundMsats / requiredMsats);
3675
- }
3676
- function readFundValuation(fields) {
3677
- const structured = usableMicro(fields.expectedMicro);
3678
- if (structured !== void 0) return { kind: "figure", micro: structured };
3679
- const match = /quoted price \((\d+)\)/.exec(fields.message ?? "");
3680
- const stated = match ? usableMicro(Number(match[1])) : void 0;
3681
- if (stated !== void 0) return { kind: "figure", micro: stated };
3682
- const providerStatedSomething = fields.expectedMicro !== void 0 || match !== null;
3683
- return providerStatedSomething ? { kind: "malformed" } : { kind: "absent" };
3684
- }
3685
- function msatsToMicro(msats, btcRate) {
3686
- return Math.round(msatsToUsd(msats, btcRate) * 1e6);
3687
- }
3688
- function microToSats(micro, btcRate) {
3689
- return Math.ceil(usdToMsats(micro / 1e6, btcRate) / 1e3);
3690
- }
3691
- function perDvmEntriesFor(config, endpoint, identity) {
3692
- const perDvm = config?.perDvm;
3693
- if (!perDvm) return [];
3694
- const key = normalizeCreditEndpoint(endpoint);
3695
- return Object.entries(perDvm).filter(
3696
- ([origin, per]) => normalizeCreditEndpoint(origin) === key || identity !== void 0 && per.dvmId === identity.dvmId && per.builderPubkey?.trim().toLowerCase() === identity.builderPubkey
3697
- );
3698
- }
3699
- var POSTURE_STRICTNESS = { auto: 0, suggest: 1, off: 2 };
3700
- function usableMicro(value) {
3701
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
3702
- }
3703
- function fundAmounts(fundMicro, btcRate) {
3704
- const fundSats = microToSats(fundMicro, btcRate);
3705
- return { fundMicro, fundSats, fundMsats: fundSats * 1e3 };
3706
- }
3707
- function completedReceiptsFor(receipts, endpoint, signingPubkey, identity) {
3708
- const key = normalizeCreditEndpoint(endpoint);
3709
- const requester = signingPubkey?.trim().toLowerCase();
3710
- if (!requester) return [];
3711
- const mine = (entry) => entry.receipt.requester_pubkey?.trim().toLowerCase() === requester && entry.receipt.outcome === "completed";
3712
- const here = [];
3713
- let anchor = identity;
3714
- for (const entry of receipts) {
3715
- if (normalizeCreditEndpoint(entry.endpoint) !== key || !mine(entry)) continue;
3716
- const verdict = verifyJobReceipt({ receipt: entry.receipt, builder: entry.builder });
3717
- if (verdict.status !== "verified" && verdict.status !== "verified_unattested") continue;
3718
- if (identity !== void 0) {
3719
- const attested = verdict.checks.attestation === "ok" ? attestedDvmIdentity(entry.builder) : void 0;
3720
- if (attested?.dvmId !== identity.dvmId || attested.builderPubkey !== identity.builderPubkey) {
3721
- continue;
3722
- }
3723
- }
3724
- here.push(entry);
3725
- if (anchor === void 0 && verdict.checks.attestation === "ok") {
3726
- anchor = attestedDvmIdentity(entry.builder);
3727
- }
3728
- }
3729
- if (!anchor) return here;
3730
- const elsewhere = receipts.filter((entry) => {
3731
- if (normalizeCreditEndpoint(entry.endpoint) === key || !mine(entry)) return false;
3732
- if (entry.receipt.dvm_id !== anchor.dvmId) return false;
3733
- const verdict = verifyJobReceipt({ receipt: entry.receipt, builder: entry.builder });
3734
- if (verdict.checks.attestation !== "ok") return false;
3735
- const attested = attestedDvmIdentity(entry.builder);
3736
- return attested?.dvmId === anchor.dvmId && attested.builderPubkey === anchor.builderPubkey;
3737
- });
3738
- return [...here, ...elsewhere];
3739
- }
3740
- function targetJobsFor(config, endpoint, identity) {
3741
- const targets = perDvmEntriesFor(config, endpoint, identity).map(([, per]) => per.targetJobs).filter((jobs) => typeof jobs === "number" && Number.isFinite(jobs));
3742
- if (targets.length > 0) return Math.min(...targets);
3743
- return config?.targetJobs;
3744
- }
3745
- function lookupOverride(overrides, builderPubkey) {
3746
- if (!overrides) return void 0;
3747
- const key = builderPubkey.trim().toLowerCase();
3748
- for (const [pubkey, cap] of Object.entries(overrides)) {
3749
- if (pubkey.trim().toLowerCase() === key) return cap;
3750
- }
3751
- return void 0;
3752
- }
3753
- function parseCapMicro(cap) {
3754
- if (!cap) return void 0;
3755
- const match = /^\$?(\d+(?:\.\d{1,6})?)$/.exec(cap.trim());
3756
- if (!match) return void 0;
3757
- const micro = Math.round(Number(match[1]) * 1e6);
3758
- return micro > 0 ? micro : void 0;
3759
- }
3760
-
3761
- // src/lib/router-feedback-claim.ts
3762
- import { createHmac, timingSafeEqual } from "crypto";
3763
- var ROUTER_FEEDBACK_CLAIM_HEADER = "x-dvmkit-router-feedback-claim";
3764
- var ROUTER_FEEDBACK_CLAIM_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
3765
- function normalizeFeedbackEndpointHost(endpointOrHost) {
3766
- try {
3767
- const url = new URL(
3768
- endpointOrHost.includes("://") ? endpointOrHost : `https://${endpointOrHost}`
3769
- );
3770
- return url.host === "" ? null : url.host.toLowerCase();
3771
- } catch {
3772
- return null;
3773
- }
3774
- }
3775
- function issueRouterFeedbackClaim(secret, attribution, now = Date.now()) {
3776
- const body = {
3777
- v: 2,
3778
- dvm_id: attribution.dvmId,
3779
- job_id: attribution.jobId,
3780
- endpoint_host: attribution.endpointHost,
3781
- exp: now + ROUTER_FEEDBACK_CLAIM_TTL_MS
3782
- };
3783
- const encoded = Buffer.from(JSON.stringify(body), "utf8").toString("base64url");
3784
- const signature = createHmac("sha256", secret).update(encoded).digest("base64url");
3785
- return `${encoded}.${signature}`;
3786
- }
3787
- function verifyRouterFeedbackClaim(secret, token, now = Date.now()) {
3788
- const dot = token.indexOf(".");
3789
- if (dot <= 0 || dot === token.length - 1) return null;
3790
- const encoded = token.slice(0, dot);
3791
- const presentedSignature = token.slice(dot + 1);
3792
- const expectedSignature = createHmac("sha256", secret).update(encoded).digest("base64url");
3793
- if (!safeEqual(expectedSignature, presentedSignature)) return null;
3794
- let body;
3795
- try {
3796
- body = JSON.parse(
3797
- Buffer.from(encoded, "base64url").toString("utf8")
3798
- );
3799
- } catch {
3800
- return null;
3801
- }
3802
- if (body.v !== 2 || typeof body.dvm_id !== "string" || body.dvm_id.length === 0 || typeof body.job_id !== "string" || body.job_id.length === 0 || typeof body.endpoint_host !== "string" || body.endpoint_host.length === 0 || typeof body.exp !== "number" || !Number.isFinite(body.exp) || body.exp <= now) {
3803
- return null;
3804
- }
3805
- return {
3806
- dvmId: body.dvm_id,
3807
- jobId: body.job_id,
3808
- endpointHost: body.endpoint_host,
3809
- expiresAt: body.exp
3810
- };
3811
- }
3812
- function safeEqual(expected, presented) {
3813
- const expectedBuffer = Buffer.from(expected);
3814
- const presentedBuffer = Buffer.from(presented);
3815
- return expectedBuffer.length === presentedBuffer.length && timingSafeEqual(expectedBuffer, presentedBuffer);
3816
- }
3817
-
3818
- // src/lib/wallet-agent/spend.ts
3819
- import {
3820
- getEncodedToken,
3821
- MintOperationError
3822
- } from "@cashu/cashu-ts";
3823
- import { randomUUID as randomUUID2 } from "crypto";
3824
-
3825
- // src/lib/wallet-agent/select.ts
3826
- function selectProofsForSpend(args) {
3827
- if (args.amountSats <= 0) {
3828
- return {
3829
- error: {
3830
- code: "insufficient_balance",
3831
- message: `amountSats must be positive; got ${args.amountSats}.`,
3832
- hint: "Pass a positive sat amount."
3833
- }
3834
- };
3835
- }
3836
- if (args.mintAllowlist.length === 0) {
3837
- return { error: noAcceptedMintsError() };
3838
- }
3839
- const reserveSats = args.swapReserveSats ?? (() => 0);
3840
- let largestEligibleBalance = 0;
3841
- for (const mint of args.mintAllowlist) {
3842
- const candidates = eligibleProofsAt(args.proofs, mint);
3843
- const balance = candidates.reduce((sum, p) => sum + p.amount, 0);
3844
- largestEligibleBalance = Math.max(largestEligibleBalance, balance);
3845
- if (balance < args.amountSats) continue;
3846
- const selected = [];
3847
- let total = 0;
3848
- for (const p of candidates) {
3849
- selected.push(p);
3850
- total += p.amount;
3851
- const required = args.amountSats + reserveSats(selected);
3852
- if (total >= required) {
3853
- return { selected, mint, totalSats: total };
3854
- }
3855
- }
3856
- }
3857
- return { error: noCoverError(largestEligibleBalance, args.amountSats) };
3858
- }
3859
- function coveringMintsForSpend(args) {
3860
- if (args.amountSats <= 0) {
3861
- return {
3862
- error: {
3863
- code: "insufficient_balance",
3864
- message: `amountSats must be positive; got ${args.amountSats}.`,
3865
- hint: "Pass a positive sat amount."
3866
- }
3867
- };
3868
- }
3869
- if (args.mintAllowlist.length === 0) {
3870
- return { error: noAcceptedMintsError() };
3871
- }
3872
- const mints = [];
3873
- let largestEligibleBalance = 0;
3874
- for (const mint of args.mintAllowlist) {
3875
- const balance = eligibleProofsAt(args.proofs, mint).reduce((sum, p) => sum + p.amount, 0);
3876
- largestEligibleBalance = Math.max(largestEligibleBalance, balance);
3877
- if (balance >= args.amountSats && !mints.includes(mint)) mints.push(mint);
3878
- }
3879
- if (mints.length === 0) {
3880
- return { error: noCoverError(largestEligibleBalance, args.amountSats) };
3881
- }
3882
- return { mints };
3883
- }
3884
- function eligibleProofsAt(proofs, mint) {
3885
- return proofs.filter((p) => p.mint === mint && p.status === "valid" && p.reserved_for === void 0).slice().sort((a, b) => b.amount - a.amount);
3886
- }
3887
- function noAcceptedMintsError() {
3888
- return {
3889
- code: "no_eligible_mint",
3890
- message: "DVM advertises no Cashu mints for the agent-wallet path.",
3891
- hint: "Use a different DVM, or fall back to the legacy --auto-pay flow."
3892
- };
3893
- }
3894
- function noCoverError(largestEligibleBalance, amountSats) {
3895
- if (largestEligibleBalance === 0) {
3896
- return {
3897
- code: "no_eligible_mint",
3898
- message: "Agent wallet has no proofs at any of the DVM's accepted mints.",
3899
- hint: "Fund the wallet at one of the DVM's accepted mints with: dvm wallet fund <amount>"
3900
- };
3901
- }
3902
- return {
3903
- code: "insufficient_balance",
3904
- message: `Agent wallet has ${largestEligibleBalance} sats at the DVM's mints; need ${amountSats} plus the per-call mint swap fee.`,
3905
- hint: "Top the wallet up with: dvm wallet fund <amount>"
3906
- };
3907
- }
3908
-
3909
- // src/lib/wallet-agent/submission-binding.ts
3910
- import { createHash as createHash2 } from "crypto";
3911
- function cashuSubmissionBindingFingerprint(binding) {
3912
- return createHash2("sha256").update(canonicaliseForSigning(binding)).digest("hex");
3913
- }
3914
- function cashuSubmissionBindingFromContext(context) {
3915
- if (context.strict_per_call !== true || context.route === "credit" || typeof context.dvm_id !== "string" || context.dvm_id.length === 0 || typeof context.capability !== "string" || context.capability.length === 0 || typeof context.pinned_mint !== "string" || context.pinned_mint.length === 0 || !context.policy) {
3916
- return null;
3917
- }
3918
- return {
3919
- dvmId: context.dvm_id,
3920
- capability: context.capability,
3921
- requestBody: context.submit_body,
3922
- mint: context.pinned_mint,
3923
- signer: context.signing_pubkey ?? null,
3924
- policy: context.policy
3925
- };
3926
- }
3927
-
3928
- // src/lib/wallet-agent/submission-context.ts
3929
- import { createHash as createHash3 } from "crypto";
3930
- function bindCashuTokenToSubmissionContext(context, cashuToken) {
3931
- if (!context) return void 0;
3932
- const commitment = createHash3("sha256").update(cashuToken).digest("hex");
3933
- if (context.route === "credit") {
3934
- const fund2 = context.credit_body?.fund;
3935
- if (!fund2) return context;
3936
- return {
3937
- ...context,
3938
- credit_body: {
3939
- ...context.credit_body,
3940
- fund: { ...fund2, commitment }
3941
- }
3942
- };
3943
- }
3944
- const data = context.submit_body.data;
3945
- const fund = data?.fund;
3946
- if (!data || !fund) return context;
3947
- return {
3948
- ...context,
3949
- submit_body: {
3950
- ...context.submit_body,
3951
- data: { ...data, fund: { ...fund, commitment } }
3952
- }
3953
- };
3954
- }
3955
-
3956
- // src/lib/wallet-agent/spend.ts
3957
- async function prepareAgentSpend(args) {
3958
- if (args.maxCashuPrincipalSats !== void 0 && (!Number.isSafeInteger(args.maxCashuPrincipalSats) || args.maxCashuPrincipalSats < 0)) {
3959
- return {
3960
- error: {
3961
- code: "invalid_max_cashu_principal",
3962
- message: "The Cashu principal limit must be an exact non-negative integer number of sats.",
3963
- hint: "Pass --max-cashu-principal-sats with a value such as 20 or 100."
3964
- }
3965
- };
3966
- }
3967
- if (args.maxCashuPrincipalSats !== void 0 && args.amountSats > args.maxCashuPrincipalSats) {
3968
- return principalCapFailure(args.amountSats, args.maxCashuPrincipalSats);
3969
- }
3970
- if (args.maxCashuFeeSats !== void 0 && (!Number.isSafeInteger(args.maxCashuFeeSats) || args.maxCashuFeeSats < 0)) {
3971
- return {
3972
- error: {
3973
- code: "invalid_max_cashu_fee",
3974
- message: "The Cashu fee limit must be an exact non-negative integer number of sats.",
3975
- hint: "Pass --max-cashu-fee-sats with a value such as 1 or 2."
3976
- }
3977
- };
3978
- }
3979
- const strict = args.mode === "strict_per_call";
3980
- let submitContext = args.submitContext;
3981
- if (strict) {
3982
- const strictError = strictSpendInputError(args);
3983
- if (strictError) return strictError;
3984
- if (!submitContext)
3985
- return strictSpendFailure("The strict Cashu request has no recovery context.");
3986
- const binding = cashuSubmissionBindingFromContext(submitContext);
3987
- if (!binding)
3988
- return strictSpendFailure("The strict Cashu request has no complete recovery binding.");
3989
- const bindingFingerprint = cashuSubmissionBindingFingerprint(binding);
3990
- if (submitContext.binding_fingerprint !== bindingFingerprint)
3991
- return strictSpendFailure(
3992
- "The strict Cashu request binding does not match its request body."
3993
- );
3994
- submitContext = { ...submitContext, binding_fingerprint: bindingFingerprint };
3995
- }
3996
- const now = args.now ?? (() => Date.now());
3997
- const uuid = args.uuid ?? (() => randomUUID2());
3998
- const timeoutSec = args.inFlightTimeoutSec ?? inFlightTimeoutSeconds();
3999
- const getWalletForMint = args.getWalletForMint ?? getWallet;
4000
- const refundClause = resolveRefundClause(args.cashuP2pk, args.locktimeSeconds, now(), {
4001
- readMnemonic: args.readMnemonic
4002
- });
4003
- const lock1 = await withWalletLock(() => {
4004
- const initial = loadAgentWallet();
4005
- if (!initial) {
4006
- return {
4007
- error: {
4008
- code: "no_agent_wallet",
4009
- message: "No agent wallet found at ~/.dvm/wallet.json.",
4010
- hint: "Run: dvm wallet init"
3610
+ error: {
3611
+ code: "no_agent_wallet",
3612
+ message: "No agent wallet found at ~/.dvm/wallet.json.",
3613
+ hint: "Run: dvm wallet init"
4011
3614
  }
4012
3615
  };
4013
3616
  }
@@ -5966,13 +5569,16 @@ async function createX402BatchPayment(args) {
5966
5569
  };
5967
5570
  }
5968
5571
  function x402VoucherPendingRefusal(blocking, data = {}) {
5572
+ return formatX402VoucherPendingRefusal(x402VoucherPendingState(blocking), data);
5573
+ }
5574
+ function formatX402VoucherPendingRefusal(blocking, data = {}) {
5969
5575
  const base = {
5970
5576
  credit_id: blocking.creditId,
5971
5577
  fund_id: blocking.fundId,
5972
5578
  channel_id: blocking.channelId,
5973
5579
  ...data
5974
5580
  };
5975
- if (!x402PendingRetired(blocking)) {
5581
+ if (!blocking.retired) {
5976
5582
  return new DvmError(
5977
5583
  "x402_voucher_pending",
5978
5584
  `A signed x402 voucher for credit ${blocking.creditId} is still waiting for its acknowledgement, so this channel cannot sign another.`,
@@ -5980,8 +5586,8 @@ function x402VoucherPendingRefusal(blocking, data = {}) {
5980
5586
  base
5981
5587
  );
5982
5588
  }
5983
- const until = new Date(x402PendingBlockExpiresAt(blocking)).toISOString();
5984
- const signedDeadline = x402PendingDepositExpiresAt(blocking) !== void 0;
5589
+ const until = new Date(blocking.blockedUntil).toISOString();
5590
+ const signedDeadline = blocking.depositExpiresAt !== void 0;
5985
5591
  return new DvmError(
5986
5592
  "x402_voucher_pending",
5987
5593
  signedDeadline ? `The deposit this channel signed for credit ${blocking.creditId} was refused by the DVM but can still be submitted, so the channel cannot sign a second one yet.` : `The funding this channel signed for credit ${blocking.creditId} was refused by the DVM, and it names no deadline after which it stops being submittable, so the channel cannot sign a second one yet.`,
@@ -6383,12 +5989,15 @@ function x402RefundError(err, chain) {
6383
5989
  );
6384
5990
  }
6385
5991
  function assertNoRivalVoucher(channelId, creditId) {
6386
- const blocking = listPendingX402FundingsForChannel(channelId).filter(
6387
- (entry) => x402PendingUnsettled(entry)
6388
- );
6389
- const rival = blocking.find((entry) => entry.creditId !== creditId && !x402PendingRetired(entry)) ?? blocking.find((entry) => entry.creditId !== creditId || x402PendingRetired(entry));
6390
- if (!rival) return;
6391
- throw x402VoucherPendingRefusal(rival, { channel_id: channelId });
5992
+ const pending = listPendingX402FundingsForChannel(channelId);
5993
+ try {
5994
+ assertNoRivalX402Voucher(channelId, creditId, pending);
5995
+ } catch (state) {
5996
+ if (typeof state !== "object" || state === null || !("code" in state) || state.code !== "x402_voucher_pending" || !("creditId" in state) || typeof state.creditId !== "string" || !("fundId" in state) || typeof state.fundId !== "string" || !("channelId" in state) || typeof state.channelId !== "string" || !("endpoint" in state) || typeof state.endpoint !== "string" || !("retired" in state) || typeof state.retired !== "boolean" || !("blockedUntil" in state) || typeof state.blockedUntil !== "number") {
5997
+ throw state;
5998
+ }
5999
+ throw formatX402VoucherPendingRefusal(state);
6000
+ }
6392
6001
  }
6393
6002
  function selectBatchRequirement(required, network, recorded) {
6394
6003
  const accepted = required.accepts.find(
@@ -6424,26 +6033,49 @@ function normalizeEndpoint(endpoint) {
6424
6033
  }
6425
6034
  }
6426
6035
 
6427
- // src/lib/credit-fund.ts
6428
- var CREDIT_FUND_RAILS = ["cashu", "lightning", "x402", "tempo"];
6429
- var CREDIT_DRAIN_RAILS = ["cashu", "x402", "tempo"];
6430
- function isCreditFundRail(value) {
6431
- return typeof value === "string" && CREDIT_FUND_RAILS.includes(value);
6036
+ // src/lib/x402/menu.ts
6037
+ function offersX402CreditFunding(menu, network) {
6038
+ return matchesNetwork(x402CreditFundingNetworks(menu), network);
6432
6039
  }
6433
- var STABLECOIN_FUND_RAILS = ["x402", "tempo"];
6434
- function isStablecoinFundRail(rail) {
6435
- return STABLECOIN_FUND_RAILS.includes(rail);
6040
+ function offersX402CreditChannel(menu, network) {
6041
+ return matchesNetwork(x402CreditChannelNetworks(menu), network);
6436
6042
  }
6437
- var CREDIT_FUNDING_MODES = ["one-shot", "reusable"];
6438
- function isCreditFundingMode(value) {
6439
- return typeof value === "string" && CREDIT_FUNDING_MODES.includes(value);
6043
+ function x402CreditFlavour(menu, network) {
6044
+ if (offersX402CreditChannel(menu, network)) return X402_BATCH_SETTLEMENT_SCHEME;
6045
+ if (matchesNetwork(x402CreditExactNetworks(menu), network)) return X402_EXACT_SCHEME;
6046
+ return void 0;
6440
6047
  }
6441
- function fundingModeForTempoIntent(intent) {
6442
- return intent === "session" ? "reusable" : "one-shot";
6048
+ function offersX402CreditFlavour(menu, network, flavour) {
6049
+ return matchesNetwork(
6050
+ flavour === X402_BATCH_SETTLEMENT_SCHEME ? x402CreditChannelNetworks(menu) : x402CreditExactNetworks(menu),
6051
+ network
6052
+ );
6443
6053
  }
6444
- function fundingModeForX402Flavour(flavour) {
6445
- return flavour === X402_BATCH_SETTLEMENT_SCHEME ? "reusable" : "one-shot";
6054
+ function x402InstrumentForFlavour(flavour) {
6055
+ return flavour === X402_BATCH_SETTLEMENT_SCHEME ? "x402_channel" : "x402_exact";
6056
+ }
6057
+ function x402FlavourForInstrument(instrument) {
6058
+ if (instrument === "x402_channel") return X402_BATCH_SETTLEMENT_SCHEME;
6059
+ if (instrument === "x402_exact") return X402_EXACT_SCHEME;
6060
+ return void 0;
6061
+ }
6062
+ function x402CreditChannelNetworks(menu) {
6063
+ return schemeNetworks(menu, X402_BATCH_SETTLEMENT_SCHEME);
6064
+ }
6065
+ function x402CreditExactNetworks(menu) {
6066
+ return schemeNetworks(menu, X402_EXACT_SCHEME);
6067
+ }
6068
+ function x402CreditFundingNetworks(menu) {
6069
+ return [.../* @__PURE__ */ new Set([...x402CreditChannelNetworks(menu), ...x402CreditExactNetworks(menu)])];
6070
+ }
6071
+ function schemeNetworks(menu, scheme) {
6072
+ return (menu?.x402?.schemes ?? []).filter((entry) => entry.scheme === scheme).map((entry) => entry.network);
6073
+ }
6074
+ function matchesNetwork(networks, network) {
6075
+ return networks.some((candidate) => network === void 0 || candidate === network);
6446
6076
  }
6077
+
6078
+ // src/lib/credit-fund.ts
6447
6079
  var CREDIT_POST_TIMEOUT_MS = 15e3;
6448
6080
  var CREDIT_PERMANENT_REFUSALS = /* @__PURE__ */ new Set([
6449
6081
  "credit_over_max",
@@ -6503,7 +6135,7 @@ async function runCreditFunding(args) {
6503
6135
  }
6504
6136
  clearPending(pending);
6505
6137
  }
6506
- const fundId = args.fundId ?? randomUUID3();
6138
+ const fundId = args.fundId ?? randomUUID2();
6507
6139
  const base = { creditId: args.creditId, fundId };
6508
6140
  const eligible = args.fundingMode ? availableRails(args).filter(isStablecoinFundRail) : availableRails(args);
6509
6141
  const x402Network = loadX402Wallet()?.network;
@@ -7627,7 +7259,7 @@ async function fundOverCashu(args, fundId) {
7627
7259
  display: "The bitcoin exchange rate couldn't be read, so the funding amount can't be sized."
7628
7260
  };
7629
7261
  }
7630
- const fundSats = microToSats2(args.amountMicro, rate);
7262
+ const fundSats = microToSats(args.amountMicro, rate);
7631
7263
  if (args.maxCashuPrincipalSats !== void 0 && fundSats > args.maxCashuPrincipalSats) {
7632
7264
  return cashuFundingCapRefusal(base, {
7633
7265
  code: "cashu_principal_cap_exceeded",
@@ -7837,7 +7469,7 @@ async function fundOverMppCharge(args, fundId, resumed, locked = false) {
7837
7469
  display: "The bitcoin exchange rate couldn't be read, so the funding can't be checked against the global budget."
7838
7470
  };
7839
7471
  }
7840
- const plannedMsats = microToSats2(args.amountMicro, rate) * 1e3;
7472
+ const plannedMsats = microToSats(args.amountMicro, rate) * 1e3;
7841
7473
  try {
7842
7474
  await enforceGlobalBudget({ plannedMsats, btcRate: rate });
7843
7475
  } catch (err) {
@@ -8082,7 +7714,7 @@ async function fundOverMpp(args, fundId, resumed, locked = false) {
8082
7714
  display: "The bitcoin exchange rate couldn't be read, so the funding can't be checked against the global budget."
8083
7715
  };
8084
7716
  }
8085
- const plannedMsats = microToSats2(args.amountMicro, rate) * 1e3;
7717
+ const plannedMsats = microToSats(args.amountMicro, rate) * 1e3;
8086
7718
  let budgetBtcRate;
8087
7719
  try {
8088
7720
  budgetBtcRate = await enforceGlobalBudget({ plannedMsats, btcRate: rate });
@@ -8301,7 +7933,7 @@ async function fundOverMpp(args, fundId, resumed, locked = false) {
8301
7933
  const bookedMicro = current.authorizedMicro ?? current.amountMicro;
8302
7934
  if (current.budgetBtcRate) {
8303
7935
  recordSpendOnce({
8304
- msats: microToSats2(bookedMicro, current.budgetBtcRate) * 1e3,
7936
+ msats: microToSats(bookedMicro, current.budgetBtcRate) * 1e3,
8305
7937
  btcRate: current.budgetBtcRate,
8306
7938
  kind: "credit_fund",
8307
7939
  jobId: `fund:${current.creditId}:${fundId}`,
@@ -8442,7 +8074,7 @@ async function fundOverLightning(args, fundId, resumed) {
8442
8074
  };
8443
8075
  }
8444
8076
  try {
8445
- await assertFloatBudget(microToSats2(args.amountMicro, rate) * 1e3);
8077
+ await assertFloatBudget(microToSats(args.amountMicro, rate) * 1e3);
8446
8078
  } catch (err) {
8447
8079
  return budgetRefusal(base, err);
8448
8080
  }
@@ -8873,184 +8505,575 @@ function clearPending(pending) {
8873
8505
  function shouldClearPending(err) {
8874
8506
  return isPermanentRefusal(err) || err instanceof DvmError && err.code === "lightning_unavailable";
8875
8507
  }
8876
- function expiredFunding(base) {
8877
- const error = new DvmError(
8878
- "lightning_invoice_expired",
8879
- "The original Lightning funding invoice expired before it was confirmed.",
8880
- "Re-run the funding to start a new intent with a new funding id."
8508
+ function expiredFunding(base) {
8509
+ const error = new DvmError(
8510
+ "lightning_invoice_expired",
8511
+ "The original Lightning funding invoice expired before it was confirmed.",
8512
+ "Re-run the funding to start a new intent with a new funding id."
8513
+ );
8514
+ return {
8515
+ ...base,
8516
+ status: "refused",
8517
+ error,
8518
+ display: "The original Lightning invoice expired before it was funded. It was cleared and will not be polled again.",
8519
+ hint: error.hint
8520
+ };
8521
+ }
8522
+ function lightningOffer(err) {
8523
+ if (!isSettlementPending(err)) return void 0;
8524
+ const offer = err.data?.lightning;
8525
+ return offer;
8526
+ }
8527
+ function isSettlementPending(err) {
8528
+ return err instanceof DvmError && err.code === "lightning_settlement_pending" && isRecord(err.data?.lightning);
8529
+ }
8530
+ function isPermanentRefusal(err) {
8531
+ if (!(err instanceof DvmError)) return false;
8532
+ if (err.data?.retryable === false) return true;
8533
+ return CREDIT_PERMANENT_REFUSALS.has(err.code) || boundRail(err) !== void 0;
8534
+ }
8535
+ function boundRail(err) {
8536
+ if (!(err instanceof DvmError)) return void 0;
8537
+ const rail = err.data?.credit_rail;
8538
+ return isCreditFundRail(rail) ? rail : void 0;
8539
+ }
8540
+ function boundInstrument(err) {
8541
+ if (!(err instanceof DvmError)) return void 0;
8542
+ return typeof err.data?.credit_instrument === "string" ? err.data.credit_instrument : void 0;
8543
+ }
8544
+ function boundChannelId(err) {
8545
+ if (!(err instanceof DvmError)) return void 0;
8546
+ return typeof err.data?.credit_channel_id === "string" ? err.data.credit_channel_id : void 0;
8547
+ }
8548
+ function boundCreditId(err) {
8549
+ if (!(err instanceof DvmError)) return void 0;
8550
+ return typeof err.data?.bound_credit_id === "string" ? err.data.bound_credit_id : void 0;
8551
+ }
8552
+ function isBindingRefusal(err) {
8553
+ return boundInstrument(err) !== void 0 || boundRail(err) !== void 0;
8554
+ }
8555
+ function lightningRefusal(base, err) {
8556
+ const mapped = err instanceof DvmError ? err : void 0;
8557
+ const required = boundRail(err);
8558
+ if (required) {
8559
+ return {
8560
+ ...base,
8561
+ status: "unavailable",
8562
+ requiredRail: required,
8563
+ error: mapped,
8564
+ display: `This credit was opened over ${required}, so a Lightning top-up can't be added to it.`
8565
+ };
8566
+ }
8567
+ if (mapped?.code === "below_rail_minimum") {
8568
+ const minMicro = typeof mapped.data?.min_micro === "number" ? mapped.data.min_micro : void 0;
8569
+ return {
8570
+ ...base,
8571
+ status: "unavailable",
8572
+ error: mapped,
8573
+ display: typeof mapped.data?.display === "string" ? mapped.data.display : "This amount is below the minimum this DVM can receive over Lightning, so no invoice was issued.",
8574
+ hint: "Refill the agent's ecash pocket with 'dvm wallet fund' and retry \u2014 ecash funds any amount here." + (minMicro !== void 0 ? ` Or fund at least $${formatUsd(minMicro / 1e6)} to use Lightning.` : "")
8575
+ };
8576
+ }
8577
+ if (mapped?.code === "lightning_unavailable") {
8578
+ return {
8579
+ ...base,
8580
+ status: "unavailable",
8581
+ error: mapped,
8582
+ display: typeof mapped.data?.display === "string" ? mapped.data.display : "This DVM can't issue a Lightning invoice right now.",
8583
+ hint: mapped.hint
8584
+ };
8585
+ }
8586
+ return {
8587
+ ...base,
8588
+ status: "refused",
8589
+ error: mapped,
8590
+ // The DVM's own `display` when it wrote one; otherwise its message, so the
8591
+ // composed prose is never thinner than the error it was built from —
8592
+ // callers relay `display`, not `error.message`.
8593
+ display: typeof mapped?.data?.display === "string" ? mapped.data.display : `The credit funding was refused${mapped ? ` (${mapped.code}). ${mapped.message}` : "."}`,
8594
+ hint: mapped?.hint
8595
+ };
8596
+ }
8597
+ function budgetRefusal(base, err) {
8598
+ const mapped = err instanceof DvmError ? err : void 0;
8599
+ const display = typeof mapped?.data?.display === "string" ? mapped.data.display : mapped?.message ?? "";
8600
+ return {
8601
+ ...base,
8602
+ status: "refused",
8603
+ error: mapped,
8604
+ display: `${display} Nothing was charged, and any prepaid balance already at this DVM keeps working.`.trim(),
8605
+ hint: mapped?.hint
8606
+ };
8607
+ }
8608
+ function forcedRailHint(args, rail) {
8609
+ const release = releaseForcedRailHint(args);
8610
+ const advertised = args.menu?.funding;
8611
+ if (advertised && !advertised.includes(rail)) {
8612
+ return `This DVM advertises credit funding over ${advertised.join(", ") || "no rail at all"}, not ${rail}. ${release.text}`;
8613
+ }
8614
+ if (rail === "lightning") {
8615
+ const connect = `No Lightning wallet is connected here, so there is nothing to pay an invoice with. Connect one with 'dvm wallet connect', piping the connection string in`;
8616
+ return release.instruction ? `${connect}, or ${lowerFirst(release.text)}` : `${connect}. ${release.text}`;
8617
+ }
8618
+ if (rail === "x402") {
8619
+ const wallet = loadX402Wallet();
8620
+ if (!wallet) {
8621
+ return `No x402 wallet is connected here, so there is nothing to sign a funding payment with. Run 'dvm wallet x402-connect' to create one, or add --key-file <path> to import a key you already hold. ${release.text}`;
8622
+ }
8623
+ const networks = x402CreditFundingNetworks(args.menu);
8624
+ if (networks.length > 0) {
8625
+ return `This DVM takes x402 credit funding on ${networks.map(x402NetworkLabel).join(", ")}, and the connected wallet is pinned to ${x402NetworkLabel(wallet.network)}. Re-pin it without handling its key: 'dvm wallet x402-network ${networks[0]}'. ${release.text}`;
8626
+ }
8627
+ if (args.menu) {
8628
+ return `This DVM's funding menu names no x402 scheme, so there is no requirement to sign a payment against. ${release.text}`;
8629
+ }
8630
+ return `This DVM's funding menu hasn't been read here, so no x402 scheme is known to sign a payment against. ${release.text}`;
8631
+ }
8632
+ if (rail === "tempo") {
8633
+ if (!hasConnectedTempoWallet(args)) {
8634
+ const connect = `No Tempo wallet is connected here, so there is nothing to sign a funding payment with. Connect one with 'dvm wallet tempo-connect'`;
8635
+ return release.instruction ? `${connect}, or ${lowerFirst(release.text)}` : `${connect}. ${release.text}`;
8636
+ }
8637
+ if (args.menu) {
8638
+ return `This DVM advertises Tempo credit funding, but its funding menu names no supported Tempo charge or session intent, so this caller has no payment to build. ${release.text}`;
8639
+ }
8640
+ return `This DVM's funding menu hasn't been read here, so no Tempo charge or session intent is known to build a payment against. ${release.text}`;
8641
+ }
8642
+ if (!args.providerInfo) {
8643
+ return `This DVM's '/v1/info' hasn't been read here, so no Cashu offer is known to attach a proof to. ${release.text}`;
8644
+ }
8645
+ return `This DVM publishes no Cashu offer for a proof to be locked to, so ecash can't fund it. ${release.text}`;
8646
+ }
8647
+ function releaseForcedRailHint(args) {
8648
+ switch (args.railSource) {
8649
+ case "per_dvm":
8650
+ return {
8651
+ text: `Run 'dvm credit rail --clear --dvm ${args.ref.endpoint}' to let the global pin or amount rule decide.`,
8652
+ instruction: true
8653
+ };
8654
+ case "global":
8655
+ return {
8656
+ text: "Run 'dvm credit rail --clear' to let the amount rule decide.",
8657
+ instruction: true
8658
+ };
8659
+ case "credit_bound":
8660
+ return {
8661
+ text: "This credit is bound to that rail by the DVM, so it can only be topped up over it \u2014 drain it and open a new credit to change rails.",
8662
+ instruction: false
8663
+ };
8664
+ default:
8665
+ return { text: "Drop --rail to let the CLI pick.", instruction: true };
8666
+ }
8667
+ }
8668
+ function lowerFirst(value) {
8669
+ return value.charAt(0).toLowerCase() + value.slice(1);
8670
+ }
8671
+ function unavailableDisplay(args, rail) {
8672
+ const what = rail === "lightning" ? "over Lightning" : rail === "cashu" ? "with ecash" : rail === "x402" ? "over x402" : "";
8673
+ return `No way to fund a prepaid credit at ${args.ref.endpoint}${what ? ` ${what}` : ""} right now.`;
8674
+ }
8675
+ function sha256Hex(value) {
8676
+ return createHash4("sha256").update(value, "utf8").digest("hex");
8677
+ }
8678
+ function isRecord(value) {
8679
+ return !!value && typeof value === "object" && !Array.isArray(value);
8680
+ }
8681
+ function sleep(ms) {
8682
+ return new Promise((resolve) => setTimeout(resolve, ms));
8683
+ }
8684
+
8685
+ // src/lib/credit-policy.ts
8686
+ import { randomUUID as randomUUID3 } from "crypto";
8687
+ var TIER0_CAP_MICRO = 5e5;
8688
+ var TIER1_CAP_MICRO = 2e6;
8689
+ var TIER1_MIN_RECEIPTS = 3;
8690
+ var TIER2_MIN_RECEIPTS = 15;
8691
+ var TIER2_MIN_SPAN_MS = 7 * 24 * 60 * 60 * 1e3;
8692
+ var OPEN_TARGET_JOBS = 20;
8693
+ var CASHU_REFILL_TARGET_JOBS = 40;
8694
+ var TOPUP_THRESHOLD_FRACTION = 0.25;
8695
+ var DEFAULT_CREDIT_POSTURE = "suggest";
8696
+ var POCKET_TARGET_MICRO = 5e6;
8697
+ function trustTierFor(args) {
8698
+ const ladder = trustLadderFor(args);
8699
+ return { tier: ladder.tier, capMicro: ladder.capMicro ?? args.menu.max_micro };
8700
+ }
8701
+ function trustLadderFor(args) {
8702
+ const successes = completedReceiptsFor(
8703
+ args.receipts,
8704
+ args.endpoint,
8705
+ args.signingPubkey,
8706
+ args.identity
8881
8707
  );
8708
+ const tier0Cap = args.tier0CapMicro ?? TIER0_CAP_MICRO;
8709
+ const tier1Cap = args.tier1CapMicro ?? TIER1_CAP_MICRO;
8710
+ const builderMax = args.menu?.max_micro;
8711
+ const issued = successes.map((r) => r.receipt.issued_at * 1e3);
8712
+ const spanMs = issued.length > 1 ? Math.max(...issued) - Math.min(...issued) : 0;
8713
+ const count = successes.length;
8714
+ if (count >= TIER2_MIN_RECEIPTS && spanMs >= TIER2_MIN_SPAN_MS) {
8715
+ return {
8716
+ tier: 2,
8717
+ ...builderMax !== void 0 ? { capMicro: builderMax } : {},
8718
+ receipts: count,
8719
+ spanMs
8720
+ };
8721
+ }
8722
+ const toTier2 = {
8723
+ tier: 2,
8724
+ ...builderMax !== void 0 ? { capMicro: builderMax } : {},
8725
+ receiptsNeeded: Math.max(TIER2_MIN_RECEIPTS - count, 0),
8726
+ spanMs: TIER2_MIN_SPAN_MS
8727
+ };
8728
+ if (count >= TIER1_MIN_RECEIPTS) {
8729
+ return { tier: 1, capMicro: tier1Cap, receipts: count, spanMs, next: toTier2 };
8730
+ }
8882
8731
  return {
8883
- ...base,
8884
- status: "refused",
8885
- error,
8886
- display: "The original Lightning invoice expired before it was funded. It was cleared and will not be polled again.",
8887
- hint: error.hint
8732
+ tier: 0,
8733
+ capMicro: tier0Cap,
8734
+ receipts: count,
8735
+ spanMs,
8736
+ next: { tier: 1, capMicro: tier1Cap, receiptsNeeded: TIER1_MIN_RECEIPTS - count }
8888
8737
  };
8889
8738
  }
8890
- function lightningOffer(err) {
8891
- if (!isSettlementPending(err)) return void 0;
8892
- const offer = err.data?.lightning;
8893
- return offer;
8894
- }
8895
- function isSettlementPending(err) {
8896
- return err instanceof DvmError && err.code === "lightning_settlement_pending" && isRecord(err.data?.lightning);
8739
+ function effectiveCreditPosture(config, endpoint, identity) {
8740
+ const postures = perDvmEntriesFor(config, endpoint, identity).map(([, per2]) => per2.posture).filter(isCreditPosture);
8741
+ const per = postures.reduce(
8742
+ (strictest, posture) => strictest === void 0 || POSTURE_STRICTNESS[posture] > POSTURE_STRICTNESS[strictest] ? posture : strictest,
8743
+ void 0
8744
+ );
8745
+ if (per !== void 0) return { posture: per, source: "per_dvm" };
8746
+ const global = config?.posture;
8747
+ if (isCreditPosture(global)) return { posture: global, source: "global" };
8748
+ return { posture: DEFAULT_CREDIT_POSTURE, source: "default" };
8897
8749
  }
8898
- function isPermanentRefusal(err) {
8899
- if (!(err instanceof DvmError)) return false;
8900
- if (err.data?.retryable === false) return true;
8901
- return CREDIT_PERMANENT_REFUSALS.has(err.code) || boundRail(err) !== void 0;
8750
+ function effectiveFundingRail(config, endpoint, identity) {
8751
+ let conflict;
8752
+ if (endpoint !== void 0) {
8753
+ const pinned = perDvmEntriesFor(config, endpoint, identity).flatMap(
8754
+ ([origin, per]) => isCreditFundRail(per.fundingRail) ? [[origin, per.fundingRail]] : []
8755
+ );
8756
+ const rails = [...new Set(pinned.map(([, rail]) => rail))];
8757
+ if (rails.length > 1) conflict = { rails, endpoints: pinned.map(([origin]) => origin) };
8758
+ else if (rails.length === 1) return { rail: rails[0], source: "per_dvm" };
8759
+ }
8760
+ const global = config?.fundingRail;
8761
+ if (isCreditFundRail(global))
8762
+ return { rail: global, source: "global", ...conflict ? { conflict } : {} };
8763
+ return { source: "default", ...conflict ? { conflict } : {} };
8902
8764
  }
8903
- function boundRail(err) {
8904
- if (!(err instanceof DvmError)) return void 0;
8905
- const rail = err.data?.credit_rail;
8906
- return isCreditFundRail(rail) ? rail : void 0;
8765
+ function automaticCreditBoundRail(stored) {
8766
+ if (stored?.status !== "active" || stored.reconciliationPending) return void 0;
8767
+ return isCreditFundRail(stored.rail) ? stored.rail : void 0;
8907
8768
  }
8908
- function boundInstrument(err) {
8909
- if (!(err instanceof DvmError)) return void 0;
8910
- return typeof err.data?.credit_instrument === "string" ? err.data.credit_instrument : void 0;
8769
+ function trustOverrideCapMicro(args) {
8770
+ const raw2 = args.builderPubkey ? lookupOverride(args.config?.trustOverrides, args.builderPubkey) : void 0;
8771
+ if (!raw2) return void 0;
8772
+ if (raw2.trim().toLowerCase() === "builder-max") {
8773
+ return args.menu?.max_micro ?? Number.POSITIVE_INFINITY;
8774
+ }
8775
+ return parseCapMicro(raw2);
8911
8776
  }
8912
- function boundChannelId(err) {
8913
- if (!(err instanceof DvmError)) return void 0;
8914
- return typeof err.data?.credit_channel_id === "string" ? err.data.credit_channel_id : void 0;
8777
+ function tierCapOverrides(config) {
8778
+ const out = {};
8779
+ const tier0 = parseCapMicro(config?.tier0Cap);
8780
+ const tier1 = parseCapMicro(config?.tier1Cap);
8781
+ if (tier0 !== void 0) out.tier0CapMicro = tier0;
8782
+ if (tier1 !== void 0) out.tier1CapMicro = tier1;
8783
+ return out;
8915
8784
  }
8916
- function boundCreditId(err) {
8917
- if (!(err instanceof DvmError)) return void 0;
8918
- return typeof err.data?.bound_credit_id === "string" ? err.data.bound_credit_id : void 0;
8785
+ function sizeCreditMicro(args) {
8786
+ const target = (args.targetJobs ?? OPEN_TARGET_JOBS) * args.priceMicro;
8787
+ let size = Math.max(args.menu.min_micro, Math.min(target, args.capMicro));
8788
+ const residualCeiling = args.menu.max_micro + args.priceMicro;
8789
+ size = Math.min(size, residualCeiling);
8790
+ return Math.max(size, args.menu.min_micro);
8919
8791
  }
8920
- function isBindingRefusal(err) {
8921
- return boundInstrument(err) !== void 0 || boundRail(err) !== void 0;
8792
+ function refillTargetMicro(args) {
8793
+ const target = (args.targetJobs ?? CASHU_REFILL_TARGET_JOBS) * args.priceMicro;
8794
+ return Math.max(args.menu.min_micro, Math.min(target, args.capMicro, args.menu.max_micro));
8922
8795
  }
8923
- function lightningRefusal(base, err) {
8924
- const mapped = err instanceof DvmError ? err : void 0;
8925
- const required = boundRail(err);
8926
- if (required) {
8927
- return {
8928
- ...base,
8929
- status: "unavailable",
8930
- requiredRail: required,
8931
- error: mapped,
8932
- display: `This credit was opened over ${required}, so a Lightning top-up can't be added to it.`
8933
- };
8796
+ function planCreditForQuote(args) {
8797
+ const none = { mode: "none" };
8798
+ const menu = args.menu;
8799
+ if (!menu || !args.signingPubkey) return none;
8800
+ const posture = effectiveCreditPosture(args.config, args.endpoint, args.identity).posture;
8801
+ if (posture === "off") return none;
8802
+ if (menu.currency.toLowerCase() !== "usd") return none;
8803
+ const canCashu = menu.funding.includes("cashu");
8804
+ const canFloat = menu.funding.includes("lightning") && args.floatConnected === true;
8805
+ const canX402 = menu.funding.includes("x402") && args.x402Connected === true && offersX402CreditFunding(menu, args.x402Network);
8806
+ const canMpp = menu.funding.includes("tempo") && args.tempoConnected === true && offersTempoCreditFunding(menu);
8807
+ if (!canCashu && !canFloat && !canX402 && !canMpp) return none;
8808
+ if (!Number.isFinite(args.priceMsats) || args.priceMsats <= 0) return none;
8809
+ const stored = args.stored;
8810
+ if (stored?.reconciliationPending) return none;
8811
+ const refilling = stored?.status === "active" && stored.currency === menu.currency;
8812
+ const rateUnavailable = !Number.isFinite(args.btcRate) || args.btcRate <= 0;
8813
+ if (!refilling) {
8814
+ const completed = completedReceiptsFor(
8815
+ args.receipts,
8816
+ args.endpoint,
8817
+ args.signingPubkey,
8818
+ args.identity
8819
+ );
8820
+ if (completed.length === 0) return none;
8821
+ const identity = args.identity;
8822
+ if (rateUnavailable && identity && !completed.some((entry) => {
8823
+ const attested = attestedDvmIdentity(entry.builder);
8824
+ return attested?.dvmId === identity.dvmId && attested.builderPubkey === identity.builderPubkey;
8825
+ })) {
8826
+ return none;
8827
+ }
8934
8828
  }
8935
- if (mapped?.code === "below_rail_minimum") {
8936
- const minMicro = typeof mapped.data?.min_micro === "number" ? mapped.data.min_micro : void 0;
8829
+ if (rateUnavailable) {
8830
+ return posture === "auto" ? {
8831
+ mode: "none",
8832
+ fundingSkipped: {
8833
+ reason: "btc_rate_unavailable",
8834
+ operation: refilling ? "refill" : "open"
8835
+ }
8836
+ } : none;
8837
+ }
8838
+ const priceMicro = msatsToMicro(args.priceMsats, args.btcRate);
8839
+ if (priceMicro <= 0) return none;
8840
+ const tier = trustTierFor({
8841
+ receipts: args.receipts,
8842
+ endpoint: args.endpoint,
8843
+ signingPubkey: args.signingPubkey,
8844
+ ...args.identity ? { identity: args.identity } : {},
8845
+ menu,
8846
+ ...tierCapOverrides(args.config)
8847
+ });
8848
+ const targetJobs = targetJobsFor(args.config, args.endpoint, args.identity);
8849
+ const pin = effectiveFundingRail(args.config, args.endpoint, args.identity);
8850
+ const eligible = [
8851
+ ...canCashu ? ["cashu"] : [],
8852
+ ...canFloat ? ["lightning"] : [],
8853
+ ...canX402 ? ["x402"] : [],
8854
+ ...canMpp ? ["tempo"] : []
8855
+ ];
8856
+ const routeFor = (fundMicro2, boundRail2) => {
8857
+ const forced = boundRail2 ?? pin.rail;
8858
+ const pick = pickCreditFundRail({
8859
+ rails: eligible,
8860
+ amountMicro: fundMicro2,
8861
+ menu,
8862
+ ...args.x402Network ? { x402Network: args.x402Network } : {},
8863
+ ...forced ? { forced } : {}
8864
+ });
8865
+ const head = forced ?? pick.order[0];
8866
+ return { route: head === "cashu" ? "cashu" : "deferred", reason: pick.reason };
8867
+ };
8868
+ const pinned = pin.rail && pin.source !== "default" ? { rail: pin.rail, source: pin.source } : void 0;
8869
+ const defer = (reason, creditId, fundMicro2, targetMicro2, maxDepositMicro, forced = pinned) => ({
8870
+ mode: "none",
8871
+ deferred: {
8872
+ reason,
8873
+ creditId,
8874
+ fundMicro: fundMicro2,
8875
+ targetMicro: targetMicro2,
8876
+ maxDepositMicro,
8877
+ currency: menu.currency,
8878
+ menu,
8879
+ ...forced ? { pinnedRail: forced.rail, pinnedRailSource: forced.source } : {}
8880
+ }
8881
+ });
8882
+ if (refilling) {
8883
+ const targetMicro2 = refillTargetMicro({
8884
+ menu,
8885
+ priceMicro,
8886
+ capMicro: tier.capMicro,
8887
+ ...targetJobs !== void 0 ? { targetJobs } : {}
8888
+ });
8889
+ const boundRail2 = isCreditFundRail(stored.rail) ? stored.rail : void 0;
8890
+ const forcedRefill = boundRail2 ? { rail: boundRail2, source: "credit_bound" } : pinned;
8891
+ const railFloorMicro = forcedRefill?.rail === "lightning" && menu.lightning_min_micro !== void 0 ? Math.max(menu.min_micro, menu.lightning_min_micro) : menu.min_micro;
8892
+ const fundMicro2 = Math.min(
8893
+ Math.max(railFloorMicro, targetMicro2 - Math.max(stored.remainingMicro, 0) + priceMicro),
8894
+ menu.max_micro
8895
+ );
8896
+ if (posture === "suggest") {
8897
+ return {
8898
+ mode: "none",
8899
+ suppressed: { reason: "refill", fundMicro: fundMicro2, targetMicro: targetMicro2, currency: menu.currency }
8900
+ };
8901
+ }
8902
+ const refillRoute = routeFor(fundMicro2, boundRail2);
8903
+ if (refillRoute.route === "deferred") {
8904
+ return defer("refill", stored.creditId, fundMicro2, targetMicro2, fundMicro2, forcedRefill);
8905
+ }
8906
+ if (!canCashu) return none;
8937
8907
  return {
8938
- ...base,
8939
- status: "unavailable",
8940
- error: mapped,
8941
- display: typeof mapped.data?.display === "string" ? mapped.data.display : "This amount is below the minimum this DVM can receive over Lightning, so no invoice was issued.",
8942
- hint: "Refill the agent's ecash pocket with 'dvm wallet fund' and retry \u2014 ecash funds any amount here." + (minMicro !== void 0 ? ` Or fund at least $${formatUsd(minMicro / 1e6)} to use Lightning.` : "")
8908
+ mode: "topup",
8909
+ creditId: stored.creditId,
8910
+ drawId: randomUUID3(),
8911
+ ...fundAmounts(fundMicro2, args.btcRate),
8912
+ currency: menu.currency,
8913
+ targetMicro: targetMicro2,
8914
+ railReason: refillRoute.reason
8943
8915
  };
8944
8916
  }
8945
- if (mapped?.code === "lightning_unavailable") {
8917
+ const fundMicro = sizeCreditMicro({
8918
+ menu,
8919
+ priceMicro,
8920
+ capMicro: tier.capMicro,
8921
+ ...targetJobs !== void 0 ? { targetJobs: Math.min(targetJobs, OPEN_TARGET_JOBS) } : {}
8922
+ });
8923
+ const targetMicro = refillTargetMicro({
8924
+ menu,
8925
+ priceMicro,
8926
+ capMicro: tier.capMicro,
8927
+ ...targetJobs !== void 0 ? { targetJobs } : {}
8928
+ });
8929
+ if (posture === "suggest") {
8946
8930
  return {
8947
- ...base,
8948
- status: "unavailable",
8949
- error: mapped,
8950
- display: typeof mapped.data?.display === "string" ? mapped.data.display : "This DVM can't issue a Lightning invoice right now.",
8951
- hint: mapped.hint
8931
+ mode: "none",
8932
+ suppressed: { reason: "open", fundMicro, targetMicro, currency: menu.currency }
8952
8933
  };
8953
8934
  }
8935
+ const openRoute = routeFor(fundMicro);
8936
+ if (openRoute.route === "deferred") {
8937
+ return defer("open", randomUUID3(), fundMicro, targetMicro, fundMicro);
8938
+ }
8939
+ if (!canCashu) return none;
8954
8940
  return {
8955
- ...base,
8956
- status: "refused",
8957
- error: mapped,
8958
- // The DVM's own `display` when it wrote one; otherwise its message, so the
8959
- // composed prose is never thinner than the error it was built from —
8960
- // callers relay `display`, not `error.message`.
8961
- display: typeof mapped?.data?.display === "string" ? mapped.data.display : `The credit funding was refused${mapped ? ` (${mapped.code}). ${mapped.message}` : "."}`,
8962
- hint: mapped?.hint
8963
- };
8964
- }
8965
- function budgetRefusal(base, err) {
8966
- const mapped = err instanceof DvmError ? err : void 0;
8967
- const display = typeof mapped?.data?.display === "string" ? mapped.data.display : mapped?.message ?? "";
8968
- return {
8969
- ...base,
8970
- status: "refused",
8971
- error: mapped,
8972
- display: `${display} Nothing was charged, and any prepaid balance already at this DVM keeps working.`.trim(),
8973
- hint: mapped?.hint
8941
+ mode: "open",
8942
+ creditId: randomUUID3(),
8943
+ drawId: randomUUID3(),
8944
+ ...fundAmounts(fundMicro, args.btcRate),
8945
+ currency: menu.currency,
8946
+ targetMicro,
8947
+ railReason: openRoute.reason
8974
8948
  };
8975
8949
  }
8976
- function forcedRailHint(args, rail) {
8977
- const release = releaseForcedRailHint(args);
8978
- const advertised = args.menu?.funding;
8979
- if (advertised && !advertised.includes(rail)) {
8980
- return `This DVM advertises credit funding over ${advertised.join(", ") || "no rail at all"}, not ${rail}. ${release.text}`;
8981
- }
8982
- if (rail === "lightning") {
8983
- const connect = `No Lightning wallet is connected here, so there is nothing to pay an invoice with. Connect one with 'dvm wallet connect', piping the connection string in`;
8984
- return release.instruction ? `${connect}, or ${lowerFirst(release.text)}` : `${connect}. ${release.text}`;
8985
- }
8986
- if (rail === "x402") {
8987
- const wallet = loadX402Wallet();
8988
- if (!wallet) {
8989
- return `No x402 wallet is connected here, so there is nothing to sign a funding payment with. Run 'dvm wallet x402-connect' to create one, or add --key-file <path> to import a key you already hold. ${release.text}`;
8990
- }
8991
- const networks = x402CreditFundingNetworks(args.menu);
8992
- if (networks.length > 0) {
8993
- return `This DVM takes x402 credit funding on ${networks.map(x402NetworkLabel).join(", ")}, and the connected wallet is pinned to ${x402NetworkLabel(wallet.network)}. Re-pin it without handling its key: 'dvm wallet x402-network ${networks[0]}'. ${release.text}`;
8994
- }
8995
- if (args.menu) {
8996
- return `This DVM's funding menu names no x402 scheme, so there is no requirement to sign a payment against. ${release.text}`;
8950
+ function planProbeDraw(args) {
8951
+ const now = args.now ?? Date.now();
8952
+ const implicit = [...args.implicit ?? []].filter(
8953
+ (credit) => credit.status === "active" && credit.remainingMicro > 0 && credit.expiryMs > now
8954
+ ).sort((a, b) => a.expiryMs - b.expiryMs).at(0);
8955
+ if (implicit) {
8956
+ if (effectiveCreditPosture(args.config, implicit.endpoint, args.identity).posture === "off") {
8957
+ return {
8958
+ mode: "none",
8959
+ drawSkipped: {
8960
+ reason: "posture_off",
8961
+ creditId: implicit.creditId,
8962
+ remainingMicro: implicit.remainingMicro
8963
+ }
8964
+ };
8997
8965
  }
8998
- return `This DVM's funding menu hasn't been read here, so no x402 scheme is known to sign a payment against. ${release.text}`;
8966
+ return {
8967
+ mode: "draw",
8968
+ creditId: implicit.creditId,
8969
+ drawId: randomUUID3(),
8970
+ source: "implicit"
8971
+ };
8999
8972
  }
9000
- if (rail === "tempo") {
9001
- if (!hasConnectedTempoWallet(args)) {
9002
- const connect = `No Tempo wallet is connected here, so there is nothing to sign a funding payment with. Connect one with 'dvm wallet tempo-connect'`;
9003
- return release.instruction ? `${connect}, or ${lowerFirst(release.text)}` : `${connect}. ${release.text}`;
9004
- }
9005
- if (args.menu) {
9006
- return `This DVM advertises Tempo credit funding, but its funding menu names no supported Tempo charge or session intent, so this caller has no payment to build. ${release.text}`;
8973
+ const stored = args.stored;
8974
+ if (!stored) return { mode: "none" };
8975
+ const skip = (reason, extra) => ({
8976
+ mode: "none",
8977
+ drawSkipped: {
8978
+ reason,
8979
+ creditId: stored.creditId,
8980
+ remainingMicro: stored.remainingMicro,
8981
+ ...extra
9007
8982
  }
9008
- return `This DVM's funding menu hasn't been read here, so no Tempo charge or session intent is known to build a payment against. ${release.text}`;
8983
+ });
8984
+ if (effectiveCreditPosture(args.config, stored.endpoint, args.identity).posture === "off") {
8985
+ return skip("posture_off");
9009
8986
  }
9010
- if (!args.providerInfo) {
9011
- return `This DVM's '/v1/info' hasn't been read here, so no Cashu offer is known to attach a proof to. ${release.text}`;
8987
+ if (stored.status !== "active") return skip("not_active");
8988
+ if (stored.reconciliationPending) return skip("reconciliation_pending");
8989
+ if (stored.remainingMicro <= 0 && !stored.provisional) return skip("exhausted");
8990
+ if (stored.expiryMs > 0 && stored.expiryMs <= now) return skip("expired");
8991
+ if (stored.lastPriceMicro !== void 0 && args.btcRate > 0) {
8992
+ const ceilingMicro = msatsToMicro(args.drawCeilingMsats, args.btcRate);
8993
+ if (ceilingMicro > 0 && stored.lastPriceMicro > ceilingMicro) {
8994
+ return skip("price_above_ceiling", {
8995
+ ceilingMicro,
8996
+ lastPriceMicro: stored.lastPriceMicro,
8997
+ ceilingSource: args.drawCeilingSource
8998
+ });
8999
+ }
9012
9000
  }
9013
- return `This DVM publishes no Cashu offer for a proof to be locked to, so ecash can't fund it. ${release.text}`;
9001
+ return { mode: "draw", creditId: stored.creditId, drawId: randomUUID3(), source: "prepaid" };
9014
9002
  }
9015
- function releaseForcedRailHint(args) {
9016
- switch (args.railSource) {
9017
- case "per_dvm":
9018
- return {
9019
- text: `Run 'dvm credit rail --clear --dvm ${args.ref.endpoint}' to let the global pin or amount rule decide.`,
9020
- instruction: true
9021
- };
9022
- case "global":
9023
- return {
9024
- text: "Run 'dvm credit rail --clear' to let the amount rule decide.",
9025
- instruction: true
9026
- };
9027
- case "credit_bound":
9028
- return {
9029
- text: "This credit is bound to that rail by the DVM, so it can only be topped up over it \u2014 drain it and open a new credit to change rails.",
9030
- instruction: false
9031
- };
9032
- default:
9033
- return { text: "Drop --rail to let the CLI pick.", instruction: true };
9034
- }
9003
+ function needsTopup(balanceMicro, targetMicro) {
9004
+ return targetMicro > 0 && balanceMicro < targetMicro * TOPUP_THRESHOLD_FRACTION;
9035
9005
  }
9036
- function lowerFirst(value) {
9037
- return value.charAt(0).toLowerCase() + value.slice(1);
9006
+ function pocketTargetMicro(config) {
9007
+ return parseCapMicro(config?.pocketTarget) ?? POCKET_TARGET_MICRO;
9038
9008
  }
9039
- function unavailableDisplay(args, rail) {
9040
- const what = rail === "lightning" ? "over Lightning" : rail === "cashu" ? "with ecash" : rail === "x402" ? "over x402" : "";
9041
- return `No way to fund a prepaid credit at ${args.ref.endpoint}${what ? ` ${what}` : ""} right now.`;
9009
+ function needsPocketRefill(balanceMicro, targetMicro) {
9010
+ return needsTopup(balanceMicro, targetMicro);
9042
9011
  }
9043
- function microToSats2(micro, btcRate) {
9044
- return Math.ceil(usdToMsats(micro / 1e6, btcRate) / 1e3);
9012
+ function perDvmEntriesFor(config, endpoint, identity) {
9013
+ const perDvm = config?.perDvm;
9014
+ if (!perDvm) return [];
9015
+ const key = normalizeCreditEndpoint(endpoint);
9016
+ return Object.entries(perDvm).filter(
9017
+ ([origin, per]) => normalizeCreditEndpoint(origin) === key || identity !== void 0 && per.dvmId === identity.dvmId && per.builderPubkey?.trim().toLowerCase() === identity.builderPubkey
9018
+ );
9045
9019
  }
9046
- function sha256Hex(value) {
9047
- return createHash4("sha256").update(value, "utf8").digest("hex");
9020
+ var POSTURE_STRICTNESS = { auto: 0, suggest: 1, off: 2 };
9021
+ function fundAmounts(fundMicro, btcRate) {
9022
+ const fundSats = microToSats(fundMicro, btcRate);
9023
+ return { fundMicro, fundSats, fundMsats: fundSats * 1e3 };
9048
9024
  }
9049
- function isRecord(value) {
9050
- return !!value && typeof value === "object" && !Array.isArray(value);
9025
+ function completedReceiptsFor(receipts, endpoint, signingPubkey, identity) {
9026
+ const key = normalizeCreditEndpoint(endpoint);
9027
+ const requester = signingPubkey?.trim().toLowerCase();
9028
+ if (!requester) return [];
9029
+ const mine = (entry) => entry.receipt.requester_pubkey?.trim().toLowerCase() === requester && entry.receipt.outcome === "completed";
9030
+ const here = [];
9031
+ let anchor = identity;
9032
+ for (const entry of receipts) {
9033
+ if (normalizeCreditEndpoint(entry.endpoint) !== key || !mine(entry)) continue;
9034
+ const verdict = verifyJobReceipt({ receipt: entry.receipt, builder: entry.builder });
9035
+ if (verdict.status !== "verified" && verdict.status !== "verified_unattested") continue;
9036
+ if (identity !== void 0) {
9037
+ const attested = verdict.checks.attestation === "ok" ? attestedDvmIdentity(entry.builder) : void 0;
9038
+ if (attested?.dvmId !== identity.dvmId || attested.builderPubkey !== identity.builderPubkey) {
9039
+ continue;
9040
+ }
9041
+ }
9042
+ here.push(entry);
9043
+ if (anchor === void 0 && verdict.checks.attestation === "ok") {
9044
+ anchor = attestedDvmIdentity(entry.builder);
9045
+ }
9046
+ }
9047
+ if (!anchor) return here;
9048
+ const elsewhere = receipts.filter((entry) => {
9049
+ if (normalizeCreditEndpoint(entry.endpoint) === key || !mine(entry)) return false;
9050
+ if (entry.receipt.dvm_id !== anchor.dvmId) return false;
9051
+ const verdict = verifyJobReceipt({ receipt: entry.receipt, builder: entry.builder });
9052
+ if (verdict.checks.attestation !== "ok") return false;
9053
+ const attested = attestedDvmIdentity(entry.builder);
9054
+ return attested?.dvmId === anchor.dvmId && attested.builderPubkey === anchor.builderPubkey;
9055
+ });
9056
+ return [...here, ...elsewhere];
9051
9057
  }
9052
- function sleep(ms) {
9053
- return new Promise((resolve) => setTimeout(resolve, ms));
9058
+ function targetJobsFor(config, endpoint, identity) {
9059
+ const targets = perDvmEntriesFor(config, endpoint, identity).map(([, per]) => per.targetJobs).filter((jobs) => typeof jobs === "number" && Number.isFinite(jobs));
9060
+ if (targets.length > 0) return Math.min(...targets);
9061
+ return config?.targetJobs;
9062
+ }
9063
+ function lookupOverride(overrides, builderPubkey) {
9064
+ if (!overrides) return void 0;
9065
+ const key = builderPubkey.trim().toLowerCase();
9066
+ for (const [pubkey, cap] of Object.entries(overrides)) {
9067
+ if (pubkey.trim().toLowerCase() === key) return cap;
9068
+ }
9069
+ return void 0;
9070
+ }
9071
+ function parseCapMicro(cap) {
9072
+ if (!cap) return void 0;
9073
+ const match = /^\$?(\d+(?:\.\d{1,6})?)$/.exec(cap.trim());
9074
+ if (!match) return void 0;
9075
+ const micro = Math.round(Number(match[1]) * 1e6);
9076
+ return micro > 0 ? micro : void 0;
9054
9077
  }
9055
9078
 
9056
9079
  // src/lib/messages/types.ts
@@ -9436,7 +9459,7 @@ async function probeX402(x402, timeoutMs) {
9436
9459
  let network;
9437
9460
  let address;
9438
9461
  try {
9439
- const { loadX402Wallet: loadX402Wallet2, getX402Balance: getX402Balance2, ensureAddress: ensureAddress2, usdcContractByCaip2: usdcContractByCaip22 } = await import("../x402-R6HPBY5D.js");
9462
+ const { loadX402Wallet: loadX402Wallet2, getX402Balance: getX402Balance2, ensureAddress: ensureAddress2, usdcContractByCaip2: usdcContractByCaip22 } = await import("../x402-FT4S4JS4.js");
9440
9463
  const wallet = loadX402Wallet2();
9441
9464
  if (!wallet) {
9442
9465
  const storedKey = x402.privateKey;
@@ -9488,7 +9511,7 @@ async function probeMpp(mpp, timeoutMs) {
9488
9511
  if (!mpp) return null;
9489
9512
  if (mpp.method === "tempo") {
9490
9513
  try {
9491
- const { connectedTempoAccount: connectedTempoAccount2, getTempoUsdcBalance: getTempoUsdcBalance2, resolveTempoNetwork: resolveTempoNetwork2 } = await import("../tempo-wallet-B5XXV3YH.js");
9514
+ const { connectedTempoAccount: connectedTempoAccount2, getTempoUsdcBalance: getTempoUsdcBalance2, resolveTempoNetwork: resolveTempoNetwork2 } = await import("../tempo-wallet-X2GGAZ6X.js");
9492
9515
  const account = await connectedTempoAccount2();
9493
9516
  if (!account) {
9494
9517
  return {
@@ -9633,10 +9656,12 @@ export {
9633
9656
  StepCache,
9634
9657
  TEMPO_CHAIN_ID,
9635
9658
  TEMPO_CHANNELS_FILE,
9659
+ TEMPO_CHARGE_PENDING_STALE_MS,
9636
9660
  TEMPO_EXIT_KEY_MISSING_HINT,
9637
9661
  TEMPO_MODERATO_CHAIN_ID,
9638
9662
  TEMPO_MODERATO_RPC,
9639
9663
  TEMPO_PATH_USD_MODERATO,
9664
+ TEMPO_SESSION_UPDATED_REASON,
9640
9665
  WALLET_FILE,
9641
9666
  WALLET_VERSION,
9642
9667
  X402EnvValidationError,
@@ -9662,6 +9687,9 @@ export {
9662
9687
  addPayment,
9663
9688
  advanceTempoChannelExit,
9664
9689
  amountBoundsVerdict,
9690
+ appendBudgetSpend,
9691
+ appendFundingReceipt,
9692
+ appendReceipt,
9665
9693
  applyImplicitReceiptCredit,
9666
9694
  applyReceiptCredit,
9667
9695
  applyRetryToPool,
@@ -9669,8 +9697,12 @@ export {
9669
9697
  asProviderTextOpt,
9670
9698
  assertAgentMnemonicReplaceable,
9671
9699
  assertFloatBudget,
9700
+ assertNoRivalX402Voucher,
9672
9701
  assertNutSupport,
9673
9702
  assertSupportedX402Network,
9703
+ assertTempoSessionChallengeWithinAsk,
9704
+ assertTempoSessionDepositWithinCap,
9705
+ assertTempoSessionVoucherWithinAsk,
9674
9706
  assertTempoWalletIdle,
9675
9707
  assertValidBudget,
9676
9708
  assertX402ChannelPayer,
@@ -9683,8 +9715,10 @@ export {
9683
9715
  availableRails,
9684
9716
  backfillTrustAnchor,
9685
9717
  backupSecretFile,
9718
+ bindCashuTokenToSubmissionContext,
9686
9719
  bindDiagnosticCapabilityAuthority,
9687
9720
  bindDiagnosticIdentityAuthority,
9721
+ breakdownSince,
9688
9722
  buildAdminAuthHeader,
9689
9723
  buildAdminChallenge,
9690
9724
  buildAttestation,
@@ -9701,16 +9735,21 @@ export {
9701
9735
  canonicalRequestPath,
9702
9736
  canonicaliseForSigning,
9703
9737
  canonicalize,
9738
+ capFor,
9704
9739
  captureFundingReceipt,
9705
9740
  captureTransportReceipt,
9706
9741
  cashOutFromMint,
9707
9742
  cashuSubmissionBindingFingerprint,
9743
+ cashuSubmissionBindingFromContext,
9708
9744
  chainIdFromCaip2,
9745
+ checkBudgetForLedger,
9709
9746
  checkMeltQuoteState,
9710
9747
  checkMintHealth,
9711
9748
  checkReceiptAttestation,
9712
9749
  clearCreditSelection,
9713
9750
  clearPendingDrain,
9751
+ clearPendingFunding,
9752
+ clearPendingTempoChargeFunding,
9714
9753
  clearPendingX402ExactFunding,
9715
9754
  clearPendingX402Funding,
9716
9755
  clearRefillRailUnavailable,
@@ -9728,16 +9767,22 @@ export {
9728
9767
  costAnchor,
9729
9768
  coveringMintsForSpend,
9730
9769
  createDiagnosticCapabilityAuthority,
9770
+ createMintQuoteRetryState,
9731
9771
  createMonotonicClock,
9732
9772
  createNoopLogger,
9733
9773
  createStdoutLogger,
9774
+ createTempoChannelStore,
9734
9775
  createTempoCloseCredential,
9776
+ createTempoCreditSession,
9777
+ createTempoSessionPaymentFetch,
9735
9778
  createX402BatchPayment,
9736
9779
  currentContext,
9737
9780
  decodePayment,
9738
9781
  decodePaymentRequiredHeader,
9739
9782
  deleteAgentMnemonic,
9740
9783
  deriveAgentMasterKeypair,
9784
+ deriveAgentRefundSubkey,
9785
+ deriveAgentRefundSubkeyPubkey,
9741
9786
  deriveBuilderLockKeypair,
9742
9787
  deriveBuilderLockPubkey,
9743
9788
  deriveIdentityFromPrivkey,
@@ -9832,6 +9877,7 @@ export {
9832
9877
  isPromptMessage,
9833
9878
  isPublicRequestId,
9834
9879
  isResumable,
9880
+ isSignableExactRequirement,
9835
9881
  isSignedJobReceipt,
9836
9882
  isStablecoinFundRail,
9837
9883
  isTerminalJobStatus,
@@ -9854,9 +9900,11 @@ export {
9854
9900
  listX402Channels,
9855
9901
  loadAgentWallet,
9856
9902
  loadAllJobs,
9903
+ loadBudgetLedger,
9857
9904
  loadConfig,
9858
9905
  loadConfiguredMints,
9859
9906
  loadCredit,
9907
+ loadCreditX402Binding,
9860
9908
  loadFloat,
9861
9909
  loadIdentities,
9862
9910
  loadIdentitySecret,
@@ -9866,11 +9914,14 @@ export {
9866
9914
  loadPendingFunding,
9867
9915
  loadReceipt,
9868
9916
  loadReceipts,
9917
+ loadResumableTempoChargeFunding,
9869
9918
  loadResumableX402ExactFunding,
9870
9919
  loadResumableX402Funding,
9871
9920
  loadTempoChannelAssociation,
9921
+ loadTempoChannelAssociationByKey,
9872
9922
  loadTempoChannelController,
9873
9923
  loadTempoChannelEntry,
9924
+ loadWalletConfiguredMints,
9874
9925
  loadX402Wallet,
9875
9926
  loggers,
9876
9927
  looksLikeMnemonic,
@@ -9881,7 +9932,9 @@ export {
9881
9932
  markJobTerminal,
9882
9933
  markRefillDrainNoticed,
9883
9934
  markRefillRailUnavailable,
9935
+ maxAdvertisedMicro,
9884
9936
  maxKeysetDenomination,
9937
+ meltAmountBounds,
9885
9938
  microToSats,
9886
9939
  missingDleqRecoveryDiagnostic,
9887
9940
  mnemonicFingerprint,
@@ -9898,7 +9951,11 @@ export {
9898
9951
  nwcDedupWindowMs,
9899
9952
  nwcRejectionDisplay,
9900
9953
  nwcTimeoutMs,
9954
+ offersTempoCreditCharge,
9901
9955
  offersTempoCreditFunding,
9956
+ offersTempoCreditSession,
9957
+ offersX402CreditChannel,
9958
+ offersX402CreditFlavour,
9902
9959
  offersX402CreditFunding,
9903
9960
  parseBudget,
9904
9961
  parseCreditMenu,
@@ -9915,6 +9972,7 @@ export {
9915
9972
  paymentRequiredV2FromV1,
9916
9973
  pendingDrainById,
9917
9974
  pendingDrainFor,
9975
+ persistTempoOpenCredential,
9918
9976
  pickCreditFundRail,
9919
9977
  pinConfigSnapshot,
9920
9978
  pinDimension,
@@ -9935,6 +9993,7 @@ export {
9935
9993
  processX402BatchCorrection,
9936
9994
  processX402BatchResponse,
9937
9995
  processX402BatchResponseForWallet,
9996
+ progress,
9938
9997
  projectDiagnosticFields,
9939
9998
  projectEventName,
9940
9999
  projectSpanRow,
@@ -9943,6 +10002,7 @@ export {
9943
10002
  railLabelAtSentenceStart,
9944
10003
  randomTraceId,
9945
10004
  raw,
10005
+ readAgentMnemonic,
9946
10006
  readCompletedCreditHistory,
9947
10007
  readFundValuation,
9948
10008
  readX402ChannelOnChain,
@@ -9964,6 +10024,8 @@ export {
9964
10024
  recordFundResponse,
9965
10025
  recordFundingMenuSnapshot,
9966
10026
  recordPendingDrain,
10027
+ recordPendingFunding,
10028
+ recordPendingTempoChargeFunding,
9967
10029
  recordPendingX402ExactFunding,
9968
10030
  recordPendingX402Funding,
9969
10031
  recordRefusedDrawBalance,
@@ -9974,6 +10036,7 @@ export {
9974
10036
  redactUrlsInText,
9975
10037
  refFromJob,
9976
10038
  refundX402BatchChannel,
10039
+ rehydrateOutputData,
9977
10040
  releaseAgentReserve,
9978
10041
  removeX402Wallet,
9979
10042
  renderReceiptHuman,
@@ -9994,10 +10057,14 @@ export {
9994
10057
  restoreProofsFromOutputs,
9995
10058
  retireConfigBackup,
9996
10059
  retirePendingX402Funding,
10060
+ retireTempoChannel,
10061
+ retireTempoChannelScope,
10062
+ retryMintQuoteOperation,
9997
10063
  rollbackConfig,
9998
10064
  rpcEndpointLine,
9999
10065
  rpcEndpointSuffix,
10000
10066
  saveAgentWallet,
10067
+ saveBudgetLedger,
10001
10068
  saveIdentities,
10002
10069
  saveJob,
10003
10070
  saveLedger,
@@ -10007,6 +10074,7 @@ export {
10007
10074
  setEmitter,
10008
10075
  setOutputTraceId,
10009
10076
  settleDrainEvidence,
10077
+ sha256hex,
10010
10078
  shellQuoteArg,
10011
10079
  signAttestation,
10012
10080
  signDrainReceipt,
@@ -10015,6 +10083,7 @@ export {
10015
10083
  signReceipt,
10016
10084
  signRequestData,
10017
10085
  signedRequestDomain,
10086
+ spendEntry,
10018
10087
  spendableAt,
10019
10088
  staleRateNotice,
10020
10089
  statusSnapshot,
@@ -10024,16 +10093,27 @@ export {
10024
10093
  sweepPendingMints,
10025
10094
  sweepPendingSubmissions,
10026
10095
  sweepStaleInFlight,
10096
+ tempoAccountFromConfig,
10027
10097
  tempoBalanceCheckHint,
10028
10098
  tempoChannelControlledBy,
10099
+ tempoChargeCredentialSettleable,
10029
10100
  tempoCredentialAmountMicro,
10101
+ tempoCredentialValidBefore,
10102
+ tempoDescriptorController,
10030
10103
  tempoKeyImportRoutes,
10104
+ tempoSessionAssociationByKey,
10031
10105
  tempoSessionChallenge,
10106
+ tempoSessionChallengeChannelScope,
10032
10107
  tempoSessionChallengeMicro,
10108
+ tempoSessionCredentialChannelScope,
10109
+ tempoSessionCredentialCumulativeMicro,
10110
+ tempoSessionCumulativeCapMicro,
10111
+ tempoSessionPriorCumulativeMicro,
10033
10112
  tempoTokenReference,
10034
10113
  tempoWrongAssetHint,
10035
10114
  tierCapOverrides,
10036
10115
  toAgentProof,
10116
+ toCashuProofs,
10037
10117
  toFloatError,
10038
10118
  toLockPubkey,
10039
10119
  transportErrorCode,
@@ -10070,6 +10150,7 @@ export {
10070
10150
  walletExists,
10071
10151
  warmFxSnapshot,
10072
10152
  weakestReceiptVerdict,
10153
+ windowEnd,
10073
10154
  windowStart,
10074
10155
  withIdentitiesLock,
10075
10156
  withInitRetry,
@@ -10078,6 +10159,8 @@ export {
10078
10159
  withTraceContext,
10079
10160
  withWalletLock,
10080
10161
  withX402ChannelLock,
10162
+ withholdsTempoCreditSession,
10163
+ withinAdvertisedTolerance,
10081
10164
  writeAgentMnemonic,
10082
10165
  writeIdentity,
10083
10166
  x402ChannelBalanceUnreconciledAt,
@@ -10085,8 +10168,11 @@ export {
10085
10168
  x402ChannelReservation,
10086
10169
  x402CreditChannelNetworks,
10087
10170
  x402CreditExactNetworks,
10171
+ x402CreditFlavour,
10172
+ x402CreditFundingNetworks,
10088
10173
  x402ExactFundingDead,
10089
10174
  x402FlavourForInstrument,
10175
+ x402InstrumentForFlavour,
10090
10176
  x402NetworkByCaip2,
10091
10177
  x402NetworkBySlug,
10092
10178
  x402NetworkLabel,
@@ -10102,5 +10188,7 @@ export {
10102
10188
  x402ReservationRelease,
10103
10189
  x402SupportedNetworksHint,
10104
10190
  x402VoucherPendingRefusal,
10191
+ x402VoucherPendingState,
10192
+ x402WalletFromConfig,
10105
10193
  x402WrongAssetHint
10106
10194
  };