@dvmkit/sdk 0.1.0-rc.5 → 0.1.0-rc.6

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.
@@ -1,36 +1,73 @@
1
1
  import {
2
- TEMPO_CHAIN_ID,
2
+ IDENTITIES_VERSION,
3
3
  TEMPO_CHARGE_PENDING_STALE_MS,
4
+ TEMPO_EXIT_KEY_MISSING_HINT,
5
+ advanceTempoChannelExit,
4
6
  assertTempoSessionChallengeWithinAsk,
5
7
  assertTempoSessionDepositWithinCap,
6
8
  assertTempoSessionVoucherWithinAsk,
9
+ assertTempoWalletIdle,
7
10
  clearPendingTempoChargeFunding,
8
- connectedTempoAccount,
11
+ createTempoCloseCredential,
9
12
  createTempoCreditSession,
10
13
  createTempoSessionPaymentFetch,
14
+ deriveIdentityFromPrivkey,
15
+ findSigningIdentityByPubkey,
11
16
  fundingRailLabel,
17
+ generateIdentity as generateIdentity2,
18
+ identitiesFileExists,
19
+ isPaymentRailKey,
12
20
  listResumableTempoChargeFundings,
13
21
  listTempoChannelAssociations,
22
+ loadIdentities,
14
23
  loadResumableTempoChargeFunding,
15
24
  loadTempoChannelAssociation,
25
+ loadTempoChannelController,
16
26
  loadTempoChannelEntry,
27
+ nwcRejectionDisplay,
17
28
  offersTempoCreditCharge,
18
29
  offersTempoCreditFunding,
19
30
  offersTempoCreditSession,
31
+ paymentRailLabel,
20
32
  persistTempoOpenCredential,
33
+ probeRailLabel,
34
+ railLabelAtSentenceStart,
35
+ reconcileTempoChannelOnChain,
21
36
  recordPendingTempoChargeFunding,
37
+ resolveSigningIdentity,
38
+ rpcEndpointLine,
39
+ rpcEndpointSuffix,
40
+ saveIdentities,
22
41
  shellQuoteArg,
42
+ tempoChannelControlledBy,
23
43
  tempoChargeCredentialSettleable,
44
+ tempoKeyImportRoutes,
24
45
  tempoSessionAssociationByKey,
25
46
  tempoSessionChallenge,
26
47
  tempoSessionChallengeChannelScope,
48
+ tempoSessionChallengeMicro,
27
49
  tempoSessionCredentialChannelScope,
28
50
  tempoSessionCredentialCumulativeMicro,
29
51
  tempoSessionCumulativeCapMicro,
30
52
  tempoSessionPriorCumulativeMicro,
53
+ unreachableRailDisplay,
54
+ unreachableRailHint,
55
+ withIdentitiesLock,
31
56
  withTempoChannelLock,
32
57
  withholdsTempoCreditSession
33
- } from "../chunk-ZMZTZFRC.js";
58
+ } from "../chunk-7AKBC4PW.js";
59
+ import {
60
+ TEMPO_CHAIN_ID,
61
+ connectedTempoAccount,
62
+ deriveTempoAddress,
63
+ generateTempoKey,
64
+ getTempoUsdcBalance,
65
+ resolveTempoAsset,
66
+ tempoBalanceCheckHint,
67
+ tempoTokenReference,
68
+ tempoWrongAssetHint,
69
+ tryResolveTempoAsset
70
+ } from "../chunk-ANFX5HEG.js";
34
71
  import {
35
72
  DvmX402ChannelStorage,
36
73
  X402_EXACT_AUTHORIZATION_TTL_SECONDS,
@@ -59,8 +96,10 @@ import {
59
96
  removeX402Wallet,
60
97
  repinX402WalletNetwork,
61
98
  resolveBtcUsdRate,
99
+ resolveX402Asset,
62
100
  retirePendingX402Funding,
63
101
  saveX402Wallet,
102
+ staleRateNotice,
64
103
  tryResolveBtcUsdRate,
65
104
  tryX402Payment,
66
105
  verifyX402Payment,
@@ -72,34 +111,74 @@ import {
72
111
  x402PendingFundingJson,
73
112
  x402PendingRetired,
74
113
  x402PendingStale,
75
- x402PendingUnsettled
76
- } from "../chunk-AB3L6B52.js";
114
+ x402PendingUnsettled,
115
+ x402WrongAssetHint
116
+ } from "../chunk-FJDCFHW5.js";
77
117
  import {
118
+ RAIL_FLAGS,
78
119
  SUPPORTED_PROTOCOL_VERSION,
120
+ applyImplicitReceiptCredit,
121
+ applyReceiptCredit,
122
+ clearCreditSelection,
79
123
  clearPendingDrain,
80
124
  clearPendingFunding,
125
+ clearRefillRailUnavailable,
126
+ dvmLocationIdentity,
81
127
  ensureProvisionalCredit,
128
+ forgetCredit,
129
+ getOutputTraceId,
82
130
  info,
83
- isCreditPosture,
84
- isValidEvmPrivateKey,
131
+ json,
132
+ listCredits,
133
+ listFundingMenuSnapshots,
134
+ listImplicitCredits,
85
135
  listPendingFundings,
86
- loadConfig,
87
136
  loadCredit,
88
137
  loadCreditX402Binding,
138
+ loadImplicitCredits,
89
139
  loadPendingFunding,
140
+ markCredit,
141
+ markImplicitCreditLost,
142
+ markRefillDrainNoticed,
143
+ markRefillRailUnavailable,
90
144
  maxAdvertisedMicro,
91
145
  normalizeCreditEndpoint,
92
146
  parseCreditMenu,
93
147
  parseCreditTerms,
148
+ pendingDrainById,
94
149
  pendingDrainFor,
95
150
  progress,
151
+ raw,
152
+ recordBalanceResponse,
96
153
  recordCreditX402Instrument,
154
+ recordDrainReceipts,
97
155
  recordFundResponse,
156
+ recordFundingMenuSnapshot,
98
157
  recordPendingDrain,
99
158
  recordPendingFunding,
100
- updateConfig,
159
+ recordRefusedDrawBalance,
160
+ setOutputTraceId,
161
+ settleDrainEvidence,
101
162
  withinAdvertisedTolerance
102
- } from "../chunk-4KB3LTOS.js";
163
+ } from "../chunk-KSQEFVFJ.js";
164
+ import {
165
+ CREDIT_POSTURES,
166
+ FEEDBACK_POSTURES,
167
+ authorizeProtectedRemoval,
168
+ configSnapshotPath,
169
+ describeConfigBackup,
170
+ ensureConfig,
171
+ isCreditPosture,
172
+ isFeedbackPosture,
173
+ isValidEvmPrivateKey,
174
+ loadConfig,
175
+ pinConfigSnapshot,
176
+ planConfigReset,
177
+ previewConfigRollback,
178
+ retireConfigBackup,
179
+ rollbackConfig,
180
+ updateConfig
181
+ } from "../chunk-TSWKITGR.js";
103
182
  import {
104
183
  RevenueReporter
105
184
  } from "../chunk-2ABMGUDS.js";
@@ -115,22 +194,27 @@ import {
115
194
  import "../chunk-YG7G4DPZ.js";
116
195
  import {
117
196
  CAPABILITY_NAME_RE
118
- } from "../chunk-XYTSDAPH.js";
197
+ } from "../chunk-LDTWX7JW.js";
119
198
  import {
120
199
  ADMIN_STATE_UNAVAILABLE,
200
+ DEFAULT_AGENT_MINT_URL,
121
201
  DEFAULT_LOCK_PUBKEY_GRACE_SECONDS,
122
202
  DbTransport,
123
203
  MintHealthTracker,
124
204
  NwcBackend,
125
205
  NwcError,
126
206
  PAYMENT_PROOF_KEYS,
207
+ PRIVKEY_HEX_RE,
208
+ PersistentCounterSource,
127
209
  RECENT_SPENDS_CAP,
210
+ RECOMMENDED_MINTS,
128
211
  SPANS_INSERTED_CHANNEL,
129
212
  SpanWriter,
130
213
  TEMPO_CLOSE_DB_RECOVERY_BUDGET_MS,
131
214
  TEMPO_CLOSE_REQUESTED_TOPIC,
132
215
  TEMPO_SESSION_UPDATED_REASON,
133
216
  TempoSessionChannelMismatchError,
217
+ WALLET_VERSION,
134
218
  X402EnvValidationError,
135
219
  X402FacilitatorHealth,
136
220
  X402_BATCH_CHANNEL_ABI,
@@ -140,7 +224,10 @@ import {
140
224
  X402_RPC_URL_KEY,
141
225
  X402_SELF_RELAY_KEY,
142
226
  activePubkeySet,
227
+ amountBoundsVerdict,
143
228
  applyPaymentInfoToJob,
229
+ assertAgentMnemonicReplaceable,
230
+ assertNutSupport,
144
231
  assertRevenueReporterReady,
145
232
  attestedDvmIdentity,
146
233
  authenticatedBuilderPubkey,
@@ -158,14 +245,20 @@ import {
158
245
  createDVMServer,
159
246
  createMonotonicClock,
160
247
  creditDepositPayload,
248
+ deleteAgentMnemonic,
249
+ deriveAgentMasterKeypair,
250
+ derivePubkeyFromPrivkey,
161
251
  deriveReceiptKeypair,
162
252
  deriveReceiptSecret,
163
253
  derivedFundCreditId,
164
254
  devModeSkipsPaymentVerification,
165
255
  drainReceiptsDisplay,
166
256
  drainReceiptsHint,
257
+ effectiveFundingMint,
258
+ effectiveMints,
167
259
  epochMsToIso,
168
260
  fundedMicroFor,
261
+ generateAgentMnemonic,
169
262
  generateEphemeralReceiptKey,
170
263
  generateIdentity,
171
264
  getBalance,
@@ -183,6 +276,7 @@ import {
183
276
  isDerivedCreditId,
184
277
  isDrainReceipt,
185
278
  isFundingReceipt,
279
+ isPublicRequestId,
186
280
  isSignedJobReceipt,
187
281
  isX402SettlementReconciliationReason,
188
282
  issueUpfrontChallenges,
@@ -190,12 +284,16 @@ import {
190
284
  loadAgentWallet,
191
285
  loadIdentitySecret,
192
286
  loadLockPubkeyState,
287
+ looksLikeMnemonic,
193
288
  lookupInvoice,
194
289
  makeInvoice,
290
+ meltAmountBounds,
291
+ mnemonicFingerprint,
195
292
  msatsToFiatMicro,
196
293
  nwcDedupWindowMs,
197
294
  nwcTimeoutMs,
198
295
  parseNwcUri,
296
+ parseTraceparent,
199
297
  payInvoiceReconciling,
200
298
  paymentErrorBody,
201
299
  pinAskFiat,
@@ -223,6 +321,7 @@ import {
223
321
  toCreditTerms,
224
322
  toLockPubkey,
225
323
  unknownRouteNotFound,
324
+ validateAgentMnemonic,
226
325
  validateX402Env,
227
326
  verifyAttestation,
228
327
  verifyChallenge,
@@ -235,13 +334,15 @@ import {
235
334
  verifyReceipt,
236
335
  verifyTempoSessionManagementCredential,
237
336
  verifyUpfrontPayment,
337
+ walletExists,
238
338
  weakestReceiptVerdict,
239
339
  withNwcPool,
240
340
  withWalletLock,
341
+ writeAgentMnemonic,
241
342
  writeIdentity,
242
343
  x402RequiredUsdcMicro,
243
344
  x402SettledShare
244
- } from "../chunk-ATQIII6K.js";
345
+ } from "../chunk-CMDI3ENK.js";
245
346
  import {
246
347
  X402_BATCH_SETTLEMENT_SCHEME,
247
348
  X402_DEFAULT_FACILITATOR,
@@ -274,15 +375,11 @@ import {
274
375
  x402NetworkListForCopy,
275
376
  x402NetworkToCaip2,
276
377
  x402SupportedNetworksHint
277
- } from "../chunk-AAJNGQMC.js";
378
+ } from "../chunk-JGGI65I3.js";
278
379
  import {
279
- BUDGET_FILE,
280
380
  COINGECKO_SIMPLE_PRICE_URL,
281
- DvmError,
282
- RECEIPTS_FILE,
283
381
  assertValidBudget,
284
382
  costAnchor,
285
- ensureConfigDir,
286
383
  fetchBtcUsdRate,
287
384
  fetchCoinGeckoBtcUsd,
288
385
  fetchCoinGeckoSpot,
@@ -294,19 +391,46 @@ import {
294
391
  formatUsd,
295
392
  formatUsdcMicrounits,
296
393
  getLastKnownBtcRate,
297
- isAbortError,
298
394
  monetaryJson,
299
395
  msatsToUsd,
300
396
  msatsToUsdCents,
301
397
  msatsToUsdc,
302
398
  parseBudget,
399
+ usdToMsats,
400
+ usdcToMsats
401
+ } from "../chunk-5URG56JJ.js";
402
+ import {
403
+ AGENT_MNEMONIC_FILE,
404
+ BTC_RATE_FILE,
405
+ BUDGET_FILE,
406
+ CONFIG_AUDIT_FILE,
407
+ CONFIG_BACKUP_FILE,
408
+ CONFIG_DIR,
409
+ CONFIG_FILE,
410
+ CONFIG_PINNED_FILE,
411
+ CREDITS_FILE,
412
+ DvmError,
413
+ FAVORITES_FILE,
414
+ FEEDBACK_NUDGES_FILE,
415
+ IDENTITIES_FILE,
416
+ JOBS_FILE,
417
+ MESSAGES_DIR,
418
+ PAIRINGS_DIR,
419
+ RECEIPTS_FILE,
420
+ RECOVERY_MNEMONIC_FILE,
421
+ REQUEST_INTENTS_DIR,
422
+ TEMPO_CHANNELS_FILE,
423
+ WALLET_FILE,
424
+ X402_CHANNELS_FILE,
425
+ ensureConfigDir,
426
+ ensureConfigSubdir,
427
+ isAbortError,
303
428
  providerProseSources,
429
+ repairConfigDirMode,
304
430
  resolveRpcHttpTransportOptions,
305
431
  resolveRpcOverride,
306
- transportErrorCode,
307
- usdToMsats,
308
- usdcToMsats
309
- } from "../chunk-F2L6KIMD.js";
432
+ transportErrorCode
433
+ } from "../chunk-MKI6OVW4.js";
310
434
  import "../chunk-EXHBXA4U.js";
311
435
  import {
312
436
  RAIL_REFUNDABLE,
@@ -320,8 +444,9 @@ import {
320
444
  createDefaultReplayStore,
321
445
  humanizeZodError,
322
446
  signedRequestStatementHeader,
447
+ testOnlySignedRequestDomain,
323
448
  verifyCanonicalSignedRequest
324
- } from "../chunk-AZBXSXQT.js";
449
+ } from "../chunk-P4RUVDU7.js";
325
450
  import {
326
451
  JobCancelledError,
327
452
  MemoryKVStore,
@@ -346,6 +471,7 @@ import {
346
471
  } from "../chunk-TKA6ZP4M.js";
347
472
  import "../chunk-66HGCPBU.js";
348
473
  import {
474
+ redactUrl,
349
475
  redactUrlsInText
350
476
  } from "../chunk-FUJ36YDV.js";
351
477
  import {
@@ -477,6 +603,23 @@ async function enforceGlobalBudget(opts) {
477
603
  }
478
604
  return rate;
479
605
  }
606
+ function statusSnapshot(now = /* @__PURE__ */ new Date()) {
607
+ const ledger = loadLedger();
608
+ const windows = ["daily", "weekly", "monthly"];
609
+ return {
610
+ caps: ledger.caps,
611
+ windows: windows.map((window) => {
612
+ const breakdown = breakdownSince(ledger.entries, windowStart(window, now).getTime());
613
+ return {
614
+ window,
615
+ spentUsd: breakdown.totalUsd,
616
+ limitUsd: capFor(ledger.caps, window),
617
+ resetsAt: windowEnd(window, now).getTime(),
618
+ breakdown
619
+ };
620
+ })
621
+ };
622
+ }
480
623
  function entryKind(entry) {
481
624
  return entry.kind ?? "job";
482
625
  }
@@ -678,6 +821,14 @@ function signDvmRequestData(privkeyHex, body, domain, opts = {}) {
678
821
  );
679
822
  return { ...input, auth_statement: authStatement, signature };
680
823
  }
824
+ function unsignRequestData(body) {
825
+ if (!body) return null;
826
+ const { pubkey, timestamp, nonce, signature, auth_statement: _authStatement, ...rest } = body;
827
+ if (typeof pubkey !== "string" || typeof timestamp !== "number" || typeof nonce !== "string" || typeof signature !== "string") {
828
+ return null;
829
+ }
830
+ return { body: rest, pubkey };
831
+ }
681
832
  function bytesToHex(bytes) {
682
833
  let s = "";
683
834
  for (const b of bytes) s += b.toString(16).padStart(2, "0");
@@ -704,6 +855,14 @@ import { Challenge, Credential } from "mppx";
704
855
  import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
705
856
 
706
857
  // src/lib/float.ts
858
+ function floatPaymentJson(payment) {
859
+ return {
860
+ outcome: payment.outcome,
861
+ principal_sats: payment.principalMsats / 1e3,
862
+ routing_fee_sats: payment.routingFeeMsats === null ? null : payment.routingFeeMsats / 1e3,
863
+ wallet_debit_sats: payment.walletDebitMsats === null ? null : payment.walletDebitMsats / 1e3
864
+ };
865
+ }
707
866
  function floatPaymentDisplay(payment) {
708
867
  const principal = formatSats2(payment.principalMsats);
709
868
  if (payment.routingFeeMsats === null || payment.walletDebitMsats === null) {
@@ -734,6 +893,23 @@ function emitFloatBudgetWarning(args) {
734
893
  hint: "Set a connection policy in the wallet app, confirm whether it counts routing fees, then reconnect with --budget to record it for dvm."
735
894
  });
736
895
  }
896
+ var NWC_FUNDING_FLOAT_ROLE = {
897
+ role: "nwc_funding_float",
898
+ uses: ["cashu_wallet_topups", "eligible_prepaid_credit_funding"],
899
+ direct_request_rail: false,
900
+ display: "This connected Lightning wallet is an NWC funding float: it funds ecash wallet top-ups and eligible prepaid credits directly, not direct dvm request payments.",
901
+ hint: "dvm request uses ecash, Tempo stablecoin, or x402 stablecoin. Prepaid-credit funding is selected automatically: ecash below a DVM's advertised Lightning floor, this wallet directly when eligible. dvm checks Lightning invoice principal against local caps; the wallet chooses the route and routing fee."
902
+ };
903
+ var BUDGET_PERIODS = {
904
+ day: "day",
905
+ daily: "day",
906
+ week: "week",
907
+ weekly: "week",
908
+ month: "month",
909
+ monthly: "month",
910
+ year: "year",
911
+ yearly: "year"
912
+ };
737
913
  function loadFloat(config) {
738
914
  const cfg = config === void 0 ? loadConfig() : config;
739
915
  if (!cfg?.nwcUri) return null;
@@ -821,6 +997,47 @@ async function fundViaFloat(params) {
821
997
  });
822
998
  return { ...payment, btcRate };
823
999
  }
1000
+ async function probeFloat(params) {
1001
+ const { float, timeoutMs } = params;
1002
+ return withNwcPool(float.conn, (opts) => probeOnPool(float, opts), timeoutMs);
1003
+ }
1004
+ async function probeOnPool(float, opts) {
1005
+ const backend = backendFor(float, opts);
1006
+ let info2;
1007
+ try {
1008
+ info2 = await backend.getInfo();
1009
+ } catch (err) {
1010
+ const mapped = toFloatError(err);
1011
+ return {
1012
+ reachable: false,
1013
+ methods: [],
1014
+ canPay: false,
1015
+ canInvoice: false,
1016
+ canReconcile: false,
1017
+ error: {
1018
+ code: mapped.code,
1019
+ message: mapped.message,
1020
+ display: typeof mapped.data?.display === "string" ? mapped.data.display : mapped.message,
1021
+ hint: mapped.hint,
1022
+ ...mapped.data ? { data: mapped.data } : {}
1023
+ }
1024
+ };
1025
+ }
1026
+ const probe = {
1027
+ reachable: true,
1028
+ alias: info2.alias,
1029
+ methods: info2.methods,
1030
+ canPay: supportsMethod(info2.methods, "pay_invoice"),
1031
+ canInvoice: supportsMethod(info2.methods, "make_invoice"),
1032
+ canReconcile: supportsMethod(info2.methods, "lookup_invoice")
1033
+ };
1034
+ if (info2.walletReportedBudget) probe.walletReportedBudget = info2.walletReportedBudget;
1035
+ try {
1036
+ probe.balanceMsats = await backend.getBalance();
1037
+ } catch {
1038
+ }
1039
+ return probe;
1040
+ }
824
1041
  function backendFor(float, opts) {
825
1042
  return new NwcBackend(float.conn, opts);
826
1043
  }
@@ -828,6 +1045,43 @@ function formatSats2(msats) {
828
1045
  const sats = msats / 1e3;
829
1046
  return `${Number.isInteger(sats) ? sats.toLocaleString("en-US") : String(sats)} sats`;
830
1047
  }
1048
+ function parseStatedBudget(raw2) {
1049
+ const trimmed = raw2.trim();
1050
+ if (!trimmed || trimmed.length > 64) {
1051
+ throw new DvmError(
1052
+ "invalid_budget",
1053
+ `Invalid budget: "${raw2}".`,
1054
+ "Pass the cap you set on the wallet, e.g. --budget '$20/month' or --budget '50000 sats/week'.",
1055
+ { display: "That budget didn't look like an amount \u2014 try something like '$20/month'." }
1056
+ );
1057
+ }
1058
+ const slash = trimmed.lastIndexOf("/");
1059
+ if (slash === -1) return { raw: trimmed, display: trimmed };
1060
+ const suffix = trimmed.slice(slash + 1).trim().toLowerCase();
1061
+ const period = BUDGET_PERIODS[suffix];
1062
+ if (!period) {
1063
+ throw new DvmError(
1064
+ "invalid_budget",
1065
+ `Unknown budget period: "${suffix}".`,
1066
+ "Use one of day, week, month, or year \u2014 e.g. --budget '$20/month'.",
1067
+ {
1068
+ display: `"${suffix}" isn't a period we recognise. Use day, week, month, or year.`
1069
+ }
1070
+ );
1071
+ }
1072
+ return { raw: trimmed, period, display: trimmed };
1073
+ }
1074
+ function floatBudgetJson(float, walletReported) {
1075
+ const stated = float.statedBudget;
1076
+ return {
1077
+ stated: stated ?? null,
1078
+ source: "user_stated",
1079
+ routing_fee_inclusion: "unknown",
1080
+ ...walletReported ? { wallet_reported: { _source: "wallet", ...walletReported } } : {},
1081
+ display: stated ? `Wallet connection budget: ${stated} (as you reported it \u2014 not a live wallet reading)` : "No wallet budget recorded. Set one in your wallet app, then re-connect with --budget to note it here.",
1082
+ hint: "The wallet enforces its connection policy, but dvm does not know whether that policy counts routing fees. dvm checks invoice principal against its own local caps; NIP-47 cannot set a per-payment routing-fee ceiling."
1083
+ };
1084
+ }
831
1085
  function toFloatError(err) {
832
1086
  if (err instanceof DvmError) return err;
833
1087
  if (!(err instanceof NwcError)) {
@@ -1010,6 +1264,10 @@ function describeNwcMethod(method) {
1010
1264
  }
1011
1265
  }
1012
1266
  var BUDGET_WORDING = /\b(budget\w*|quota\w*|limit\w*|caps?|capped|allowance\w*|exceed\w*)\b/i;
1267
+ function supportsMethod(methods, method) {
1268
+ if (methods.length === 0) return true;
1269
+ return methods.some((m) => m === method || m.endsWith(`.${method}`));
1270
+ }
1013
1271
 
1014
1272
  // src/lib/payment-rails/tempo-credential.ts
1015
1273
  async function buildTempoCredential(args) {
@@ -1103,8 +1361,65 @@ function fail(mode, refusal) {
1103
1361
  return null;
1104
1362
  }
1105
1363
 
1364
+ // src/lib/job-store.ts
1365
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1366
+ function saveJob(job) {
1367
+ const jobs = loadAllJobs();
1368
+ jobs.push(job);
1369
+ writeJobs(jobs);
1370
+ }
1371
+ function updateJob(id, update) {
1372
+ const jobs = loadAllJobs();
1373
+ const idx = jobs.findIndex((j) => j.id === id);
1374
+ if (idx === -1) return;
1375
+ jobs[idx] = { ...jobs[idx], ...update };
1376
+ writeJobs(jobs);
1377
+ }
1378
+ function markJobTerminal(id, status, completion, extra) {
1379
+ const existing = loadJob(id);
1380
+ if (!existing) return;
1381
+ updateJob(id, {
1382
+ ...extra,
1383
+ status,
1384
+ completedAt: existing.completedAt ?? Date.now(),
1385
+ completion
1386
+ });
1387
+ }
1388
+ function loadJob(id) {
1389
+ const jobs = loadAllJobs();
1390
+ return jobs.find((j) => j.id === id) ?? null;
1391
+ }
1392
+ function loadAllJobs() {
1393
+ if (!existsSync2(JOBS_FILE)) return [];
1394
+ try {
1395
+ return JSON.parse(readFileSync2(JOBS_FILE, "utf-8"));
1396
+ } catch {
1397
+ return [];
1398
+ }
1399
+ }
1400
+ function addPayment(jobId, amountMsats) {
1401
+ const job = loadJob(jobId);
1402
+ if (!job) return;
1403
+ updateJob(jobId, {
1404
+ totalPaidMsats: (job.totalPaidMsats ?? 0) + amountMsats,
1405
+ paymentsHonoured: (job.paymentsHonoured ?? 0) + 1
1406
+ });
1407
+ }
1408
+ function refFromJob(job) {
1409
+ return { endpoint: job.endpoint };
1410
+ }
1411
+ function writeJobs(jobs) {
1412
+ ensureConfigDir();
1413
+ writeFileSync2(JOBS_FILE, JSON.stringify(jobs, null, 2) + "\n");
1414
+ }
1415
+
1106
1416
  // src/lib/receipt-store.ts
1107
- import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1417
+ import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1418
+ function appendReceipt(entry) {
1419
+ if (loadReceipt(entry.receipt.job_id)) return;
1420
+ ensureConfigDir();
1421
+ appendFileSync(RECEIPTS_FILE, JSON.stringify(entry) + "\n");
1422
+ }
1108
1423
  function appendFundingReceipt(entry) {
1109
1424
  if (loadFundingReceipts().some(
1110
1425
  (held) => held.receipt.credit_id === entry.receipt.credit_id && held.receipt.fund_id === entry.receipt.fund_id
@@ -1115,9 +1430,9 @@ function appendFundingReceipt(entry) {
1115
1430
  appendFileSync(RECEIPTS_FILE, JSON.stringify(entry) + "\n");
1116
1431
  }
1117
1432
  function loadFundingReceipts() {
1118
- if (!existsSync2(RECEIPTS_FILE)) return [];
1433
+ if (!existsSync3(RECEIPTS_FILE)) return [];
1119
1434
  try {
1120
- return readFileSync2(RECEIPTS_FILE, "utf-8").split("\n").flatMap((line) => {
1435
+ return readFileSync3(RECEIPTS_FILE, "utf-8").split("\n").flatMap((line) => {
1121
1436
  if (!line.trim()) return [];
1122
1437
  try {
1123
1438
  const parsed = JSON.parse(line);
@@ -1130,10 +1445,37 @@ function loadFundingReceipts() {
1130
1445
  return [];
1131
1446
  }
1132
1447
  }
1448
+ function loadReceipts() {
1449
+ if (!existsSync3(RECEIPTS_FILE)) return [];
1450
+ let content;
1451
+ try {
1452
+ content = readFileSync3(RECEIPTS_FILE, "utf-8").trim();
1453
+ } catch {
1454
+ return [];
1455
+ }
1456
+ if (!content) return [];
1457
+ const entries = [];
1458
+ for (const line of content.split("\n")) {
1459
+ if (!line.trim()) continue;
1460
+ try {
1461
+ const parsed = JSON.parse(line);
1462
+ if (isStoredReceipt(parsed)) entries.push(parsed);
1463
+ } catch {
1464
+ }
1465
+ }
1466
+ return entries;
1467
+ }
1468
+ function isStoredReceipt(value) {
1469
+ if (!value || typeof value !== "object") return false;
1470
+ return isSignedJobReceipt(value.receipt);
1471
+ }
1133
1472
  function isStoredFundingReceipt(value) {
1134
1473
  if (!value || typeof value !== "object") return false;
1135
1474
  return isFundingReceipt(value.receipt);
1136
1475
  }
1476
+ function loadReceipt(jobId) {
1477
+ return loadReceipts().find((e) => e.receipt.job_id === jobId) ?? null;
1478
+ }
1137
1479
 
1138
1480
  // src/lib/receipt-capture.ts
1139
1481
  function captureFundingReceipt(args) {
@@ -1167,6 +1509,139 @@ function captureFundingReceipt(args) {
1167
1509
  }
1168
1510
  return result.status;
1169
1511
  }
1512
+ function captureJobReceipt(jobId, receipt, parseError) {
1513
+ if (!receipt && parseError) {
1514
+ persistParseError(jobId, parseError);
1515
+ const capture = unparseableCapture(parseError);
1516
+ info(`Warning: this job's receipt could not be parsed (${parseError}). ${capture.display}`);
1517
+ return capture;
1518
+ }
1519
+ if (!receipt) return absentCapture();
1520
+ const job = loadJob(jobId);
1521
+ const result = verifyJobReceipt({ receipt, builder: job?.builderAnchor, expectedJobId: jobId });
1522
+ if (result.reason !== "job_id_mismatch") persist(jobId, receipt, result.status, job);
1523
+ if (result.status === "invalid") {
1524
+ info(
1525
+ `Warning: this job's receipt did not verify. ${receiptFailureDetail(result.reason ?? "signature_invalid")}`
1526
+ );
1527
+ }
1528
+ return toCapture(receipt, result);
1529
+ }
1530
+ function captureTransportReceipt(jobId, response) {
1531
+ return captureJobReceipt(jobId, response.receipt, response.receiptParseError);
1532
+ }
1533
+ function receiptJsonFields(capture) {
1534
+ const out = { receipt_verified: capture.status };
1535
+ if (capture.status !== "absent") out.receipt_display = capture.display;
1536
+ if (capture.receipt) {
1537
+ out.receipt = capture.receipt;
1538
+ if (capture.checks) out.receipt_checks = capture.checks;
1539
+ }
1540
+ if (capture.reason) out.receipt_error = capture.reason;
1541
+ return out;
1542
+ }
1543
+ function renderReceiptHuman(capture) {
1544
+ if (capture.status === "absent") return;
1545
+ info(`\u{1F9FE} ${capture.display}`);
1546
+ }
1547
+ function receiptTrustFields(provider) {
1548
+ return {
1549
+ ...provider.receipts === true ? { receiptsSupported: true } : {},
1550
+ ...provider.builder ? { builderAnchor: provider.builder } : {}
1551
+ };
1552
+ }
1553
+ async function fetchTrustAnchor(transport, ref) {
1554
+ try {
1555
+ const { builderAnchor } = receiptTrustFields(await transport.describe(ref));
1556
+ return { reachable: true, ...builderAnchor ? { anchor: builderAnchor } : {} };
1557
+ } catch {
1558
+ return { reachable: false };
1559
+ }
1560
+ }
1561
+ async function fetchMissingJobReceipt(jobId, transport, ref) {
1562
+ const job = loadJob(jobId);
1563
+ if (!job?.receiptsSupported) return {};
1564
+ const held = recordedReceipt(job);
1565
+ if (held) return { receipt: held };
1566
+ try {
1567
+ const status = await transport.status(jobId, ref, { jobToken: job.jobToken });
1568
+ return {
1569
+ ...status.receipt ? { receipt: status.receipt } : {},
1570
+ ...status.receiptParseError ? { receiptParseError: status.receiptParseError } : {}
1571
+ };
1572
+ } catch {
1573
+ return {};
1574
+ }
1575
+ }
1576
+ async function backfillTrustAnchor(jobId, transport, ref) {
1577
+ try {
1578
+ const provider = await transport.describe(ref);
1579
+ const fields = receiptTrustFields(provider);
1580
+ if (Object.keys(fields).length > 0) updateJob(jobId, fields);
1581
+ return fields.builderAnchor;
1582
+ } catch {
1583
+ return void 0;
1584
+ }
1585
+ }
1586
+ function replayStoredReceipt(job) {
1587
+ const receipt = recordedReceipt(job);
1588
+ if (!receipt) {
1589
+ return job.receiptParseError === "shape_invalid" ? unparseableCapture(job.receiptParseError) : absentCapture();
1590
+ }
1591
+ return toCapture(
1592
+ receipt,
1593
+ verifyJobReceipt({
1594
+ receipt,
1595
+ builder: job.builderAnchor,
1596
+ expectedJobId: job.id
1597
+ })
1598
+ );
1599
+ }
1600
+ function recordedReceipt(job) {
1601
+ const receipt = job.receipt;
1602
+ return isSignedJobReceipt(receipt) ? receipt : void 0;
1603
+ }
1604
+ function absentCapture() {
1605
+ return { status: "absent", display: receiptHint("absent") };
1606
+ }
1607
+ function unparseableCapture(reason) {
1608
+ const display = "UNREADABLE receipt: this DVM returned a receipt block that does not match the signed receipt protocol shape. The job itself is unaffected.";
1609
+ return { status: "unparseable", reason, display };
1610
+ }
1611
+ function toCapture(receipt, result) {
1612
+ return {
1613
+ status: result.status,
1614
+ receipt,
1615
+ checks: result.checks,
1616
+ ...result.reason ? { reason: result.reason } : {},
1617
+ display: receiptDisplay(receipt, result.status, getLastKnownBtcRate()?.rate ?? 0)
1618
+ };
1619
+ }
1620
+ function persist(jobId, receipt, verification, job) {
1621
+ try {
1622
+ updateJob(jobId, { receipt });
1623
+ appendReceipt({
1624
+ receipt,
1625
+ endpoint: job?.endpoint ?? "",
1626
+ ...job?.builderAnchor ? { builder: job.builderAnchor } : {},
1627
+ verification,
1628
+ verified_at: Date.now()
1629
+ });
1630
+ } catch (err) {
1631
+ info(
1632
+ `Warning: could not store job receipt: ${err instanceof Error ? err.message : String(err)}`
1633
+ );
1634
+ }
1635
+ }
1636
+ function persistParseError(jobId, receiptParseError) {
1637
+ try {
1638
+ updateJob(jobId, { receiptParseError });
1639
+ } catch (err) {
1640
+ info(
1641
+ `Warning: could not store job receipt diagnostic: ${err instanceof Error ? err.message : String(err)}`
1642
+ );
1643
+ }
1644
+ }
1170
1645
 
1171
1646
  // src/lib/signed-request-target.ts
1172
1647
  function signedRequestAudience(provider, endpoint) {
@@ -1216,6 +1691,19 @@ function isLoopbackEndpoint(endpoint) {
1216
1691
 
1217
1692
  // src/lib/wallet-agent/builder-handle.ts
1218
1693
  var BUILDER_RESERVE_PREFIX = "builder/";
1694
+ function extractBuilderHandle(endpoint) {
1695
+ let url;
1696
+ try {
1697
+ url = new URL(endpoint);
1698
+ } catch {
1699
+ throw new DvmError(
1700
+ "invalid_endpoint",
1701
+ `Could not parse "${endpoint}" as a URL.`,
1702
+ "Pass a full HTTPS URL like `https://scrape.dvmkit.ai`, or a favorite alias like `@scrape`."
1703
+ );
1704
+ }
1705
+ return url.hostname.toLowerCase();
1706
+ }
1219
1707
  function reservedForMarker(builderHandle) {
1220
1708
  return `${BUILDER_RESERVE_PREFIX}${builderHandle}`;
1221
1709
  }
@@ -1387,14 +1875,25 @@ async function validateLnAddress(address) {
1387
1875
  var DEFAULT_INFLIGHT_PROOF_TIMEOUT_SEC = 60;
1388
1876
  var INFLIGHT_TIMEOUT_ENV = "DVM_INFLIGHT_PROOF_TIMEOUT_SEC";
1389
1877
  function inFlightTimeoutSeconds() {
1390
- const raw = process.env[INFLIGHT_TIMEOUT_ENV];
1391
- if (!raw) return DEFAULT_INFLIGHT_PROOF_TIMEOUT_SEC;
1392
- const parsed = Number(raw);
1878
+ const raw2 = process.env[INFLIGHT_TIMEOUT_ENV];
1879
+ if (!raw2) return DEFAULT_INFLIGHT_PROOF_TIMEOUT_SEC;
1880
+ const parsed = Number(raw2);
1393
1881
  if (!Number.isFinite(parsed) || parsed <= 0) {
1394
1882
  return DEFAULT_INFLIGHT_PROOF_TIMEOUT_SEC;
1395
1883
  }
1396
1884
  return parsed;
1397
1885
  }
1886
+ var DEFAULT_MELT_INFLIGHT_TIMEOUT_SEC = 900;
1887
+ var MELT_INFLIGHT_TIMEOUT_ENV = "DVM_MELT_INFLIGHT_TIMEOUT_SEC";
1888
+ function meltInFlightTimeoutSeconds() {
1889
+ const raw2 = process.env[MELT_INFLIGHT_TIMEOUT_ENV];
1890
+ if (!raw2) return DEFAULT_MELT_INFLIGHT_TIMEOUT_SEC;
1891
+ const parsed = Number(raw2);
1892
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1893
+ return DEFAULT_MELT_INFLIGHT_TIMEOUT_SEC;
1894
+ }
1895
+ return parsed;
1896
+ }
1398
1897
  function proofKey(p) {
1399
1898
  return `${p.mint}\0${p.secret}`;
1400
1899
  }
@@ -1466,8 +1965,8 @@ function rehydrateOutputData(persisted) {
1466
1965
  }
1467
1966
  function bigintToHex(n) {
1468
1967
  if (n < 0n) throw new Error(`blinding factor must be non-negative, got ${n.toString()}`);
1469
- const raw = n.toString(16);
1470
- return raw.length % 2 === 0 ? raw : "0" + raw;
1968
+ const raw2 = n.toString(16);
1969
+ return raw2.length % 2 === 0 ? raw2 : "0" + raw2;
1471
1970
  }
1472
1971
  function hexToBigint(hex) {
1473
1972
  if (!/^[0-9a-fA-F]*$/.test(hex)) throw new Error(`invalid hex: ${hex}`);
@@ -1542,59 +2041,500 @@ function toCashuProofs(proofs) {
1542
2041
  return proofs.map(toCashuProof);
1543
2042
  }
1544
2043
 
1545
- // src/lib/wallet-agent/denominations.ts
1546
- var TINY_AMOUNT_THRESHOLD_SATS = 8;
1547
- function computeSmartDenominations(amountSats, opts = {}) {
1548
- if (!Number.isFinite(amountSats) || amountSats <= 0) return [];
1549
- const cap = normalizeCap(opts.maxDenomination);
1550
- const withTail = opts.includeTinyTail !== false;
1551
- if (!withTail || amountSats < TINY_AMOUNT_THRESHOLD_SATS) {
1552
- return greedyPowerOfTwo(Math.floor(amountSats), cap);
2044
+ // src/lib/wallet-agent/cash-out.ts
2045
+ async function cashOutFromMint(opts) {
2046
+ const mintUrl = opts.mintUrl;
2047
+ const now = opts.now ?? (() => Date.now());
2048
+ const getWalletForMint = opts.getWalletForMint ?? getWallet;
2049
+ const fetchInvoice = opts.fetchInvoice ?? fetchLnurlPayInvoice;
2050
+ const initial = loadAgentWallet();
2051
+ if (!initial) {
2052
+ throw new DvmError(
2053
+ "wallet_missing",
2054
+ "No agent wallet found.",
2055
+ "Run `dvm wallet init` first to generate a lock keypair."
2056
+ );
1553
2057
  }
1554
- const total = Math.floor(amountSats);
1555
- const small1 = Math.floor(total * 0.05);
1556
- const small2 = Math.floor(total * 0.1 / 2) * 2;
1557
- const small4 = Math.floor(total * 0.2 / 4) * 4;
1558
- const remainder = total - small1 - small2 - small4;
1559
- const out = [];
1560
- for (let i = 0; i < small1; i++) out.push(1);
1561
- for (let i = 0; i < small2 / 2; i++) out.push(2);
1562
- for (let i = 0; i < small4 / 4; i++) out.push(4);
1563
- out.push(...greedyPowerOfTwo(remainder, cap));
1564
- return out;
1565
- }
1566
- function greedyPowerOfTwo(n, cap) {
1567
- const out = [];
1568
- let r = n;
1569
- if (cap !== void 0 && r >= cap) {
1570
- const capCount = Math.floor(r / cap);
1571
- for (let i = 0; i < capCount; i++) out.push(cap);
1572
- r -= capCount * cap;
2058
+ const candidates = spendableAt(initial.proofs, mintUrl);
2059
+ if (candidates.length === 0) {
2060
+ return {
2061
+ kind: "skipped",
2062
+ mint: mintUrl,
2063
+ reason: "no_balance",
2064
+ message: `No spendable proofs at ${mintUrl}.`
2065
+ };
1573
2066
  }
1574
- let bit = 1;
1575
- while (r > 0) {
1576
- if (r & 1) out.push(bit);
1577
- r = Math.floor(r / 2);
1578
- bit *= 2;
2067
+ let cashuWallet;
2068
+ try {
2069
+ cashuWallet = await getWalletForMint(mintUrl);
2070
+ } catch (err) {
2071
+ return { kind: "failed", mint: mintUrl, message: errorMessage(err), settlement: "reverted" };
2072
+ }
2073
+ const meltBounds = meltBoundsOf(cashuWallet);
2074
+ let target = opts.target;
2075
+ let cappedByMintMax;
2076
+ if (meltBounds.maxSats !== null) {
2077
+ const wanted = opts.target.kind === "all" ? sumSats(candidates) : opts.target.sats;
2078
+ if (wanted > meltBounds.maxSats) {
2079
+ target = { kind: "exact", sats: meltBounds.maxSats };
2080
+ cappedByMintMax = meltBounds.maxSats;
2081
+ }
1579
2082
  }
1580
- return out;
1581
- }
1582
- function normalizeCap(raw) {
1583
- if (raw === void 0) return void 0;
1584
- if (!Number.isFinite(raw) || raw <= 0) return void 0;
1585
- const floored = Math.floor(raw);
1586
- let pow = 1;
1587
- while (pow * 2 <= floored) pow *= 2;
1588
- return pow;
1589
- }
1590
-
1591
- // src/lib/wallet-agent/fund.ts
1592
- import { MintQuoteState } from "@cashu/cashu-ts";
1593
- async function prepareP2PKMint(cashuWallet, sats, quote, lockPrivkey, lockPubkey, denominations, extras = {}) {
1594
- const options = {
1595
- pubkey: lockPubkey
2083
+ let selected = selectInputs(candidates, target);
2084
+ let inputFees;
2085
+ try {
2086
+ inputFees = await getInputFeesForProofs(mintUrl, toCashuProofs(selected));
2087
+ } catch (err) {
2088
+ return { kind: "failed", mint: mintUrl, message: errorMessage(err), settlement: "reverted" };
2089
+ }
2090
+ let selectedTotal = sumSats(selected);
2091
+ if (target.kind === "exact" && selectedTotal - inputFees < target.sats) {
2092
+ const widened = widenSelection(candidates, selected, target.sats, inputFees);
2093
+ if (widened) {
2094
+ selected = widened.selected;
2095
+ selectedTotal = sumSats(selected);
2096
+ try {
2097
+ inputFees = await getInputFeesForProofs(mintUrl, toCashuProofs(selected));
2098
+ } catch (err) {
2099
+ return {
2100
+ kind: "failed",
2101
+ mint: mintUrl,
2102
+ message: errorMessage(err),
2103
+ settlement: "reverted"
2104
+ };
2105
+ }
2106
+ }
2107
+ }
2108
+ const desired = target.kind === "all" ? selectedTotal - inputFees : Math.min(target.sats, selectedTotal - inputFees);
2109
+ if (desired <= 0) {
2110
+ return {
2111
+ kind: "skipped",
2112
+ mint: mintUrl,
2113
+ reason: "fees_exceed_balance",
2114
+ message: `The mint's per-input fee (${inputFees} sats) meets or exceeds the ${selectedTotal} sats available at ${mintUrl}.`
2115
+ };
2116
+ }
2117
+ if (meltBounds.minSats !== null && desired < meltBounds.minSats) {
2118
+ return {
2119
+ kind: "skipped",
2120
+ mint: mintUrl,
2121
+ reason: "amount_out_of_range",
2122
+ message: `${mintUrl} won't pay out less than ${meltBounds.minSats} sats in one go, and only ${desired} sats can be sent from here after fees.`
2123
+ };
2124
+ }
2125
+ if (opts.maxFeeSats !== void 0 && inputFees > opts.maxFeeSats) {
2126
+ return {
2127
+ kind: "skipped",
2128
+ mint: mintUrl,
2129
+ reason: "fee_cap_exceeded",
2130
+ message: `The per-input fee at ${mintUrl} is ${inputFees} sats on its own, over the ${opts.maxFeeSats} sat cap.`
2131
+ };
2132
+ }
2133
+ let invoice;
2134
+ let quote;
2135
+ try {
2136
+ invoice = await fetchInvoice(opts.destination, desired * 1e3);
2137
+ quote = await fetchMeltQuote(mintUrl, invoice);
2138
+ } catch (err) {
2139
+ return classifyPreCommitError(mintUrl, err);
2140
+ }
2141
+ if (selectedTotal < quote.amountSats + quote.feeReserveSats + inputFees) {
2142
+ const widened = widenSelection(
2143
+ candidates,
2144
+ selected,
2145
+ quote.amountSats + quote.feeReserveSats,
2146
+ inputFees
2147
+ );
2148
+ if (widened) {
2149
+ selected = widened.selected;
2150
+ selectedTotal = sumSats(selected);
2151
+ try {
2152
+ inputFees = await getInputFeesForProofs(mintUrl, toCashuProofs(selected));
2153
+ } catch (err) {
2154
+ return {
2155
+ kind: "failed",
2156
+ mint: mintUrl,
2157
+ message: errorMessage(err),
2158
+ settlement: "reverted"
2159
+ };
2160
+ }
2161
+ }
2162
+ if (selectedTotal < quote.amountSats + quote.feeReserveSats + inputFees) {
2163
+ const target2 = selectedTotal - quote.feeReserveSats - inputFees;
2164
+ if (target2 <= 0) {
2165
+ return {
2166
+ kind: "skipped",
2167
+ mint: mintUrl,
2168
+ reason: "fees_exceed_balance",
2169
+ message: `Fees at ${mintUrl} (${quote.feeReserveSats} sats routing reserve + ${inputFees} sats input fee) meet or exceed the ${selectedTotal} sats available.`
2170
+ };
2171
+ }
2172
+ try {
2173
+ invoice = await fetchInvoice(opts.destination, target2 * 1e3);
2174
+ quote = await fetchMeltQuote(mintUrl, invoice);
2175
+ } catch (err) {
2176
+ return classifyPreCommitError(mintUrl, err);
2177
+ }
2178
+ if (selectedTotal < quote.amountSats + quote.feeReserveSats + inputFees) {
2179
+ return {
2180
+ kind: "skipped",
2181
+ mint: mintUrl,
2182
+ reason: "fees_exceed_balance",
2183
+ message: `${mintUrl} quoted a routing reserve (${quote.feeReserveSats} sats) the ${selectedTotal} sats available can't cover.`
2184
+ };
2185
+ }
2186
+ }
2187
+ }
2188
+ const quoteView = {
2189
+ mint: mintUrl,
2190
+ quote_id: quote.rawQuote.quote,
2191
+ amount_sats: quote.amountSats,
2192
+ fee_reserve_sats: quote.feeReserveSats,
2193
+ input_fee_sats: inputFees,
2194
+ debit_sats: selectedTotal,
2195
+ expires_at: typeof quote.rawQuote.expiry === "number" ? quote.rawQuote.expiry : null
1596
2196
  };
1597
- if (extras.locktimeUnixSec !== void 0) options.locktime = extras.locktimeUnixSec;
2197
+ const totalFees = quote.feeReserveSats + inputFees;
2198
+ if (opts.maxFeeSats !== void 0 && totalFees > opts.maxFeeSats) {
2199
+ return {
2200
+ kind: "skipped",
2201
+ mint: mintUrl,
2202
+ reason: "fee_cap_exceeded",
2203
+ message: `Fees at ${mintUrl} would be up to ${totalFees} sats, over the ${opts.maxFeeSats} sat cap.`,
2204
+ quote: quoteView
2205
+ };
2206
+ }
2207
+ opts.onQuote?.(quoteView);
2208
+ if (opts.dryRun) {
2209
+ return {
2210
+ kind: "quoted",
2211
+ mint: mintUrl,
2212
+ quote: quoteView,
2213
+ ...cappedByMintMax !== void 0 ? { mint_max_sats: cappedByMintMax } : {}
2214
+ };
2215
+ }
2216
+ let preview;
2217
+ try {
2218
+ preview = await cashuWallet.ops.meltBolt11(quote.rawQuote, toCashuProofs(selected)).asP2PK({ pubkey: initial.lock_pubkey }).privkey(initial.lock_privkey).prepare();
2219
+ } catch (err) {
2220
+ return {
2221
+ kind: "failed",
2222
+ mint: mintUrl,
2223
+ message: errorMessage(err),
2224
+ settlement: "reverted",
2225
+ quote: quoteView
2226
+ };
2227
+ }
2228
+ const untilMs = now() + (opts.inFlightTimeoutSec ?? meltInFlightTimeoutSeconds()) * 1e3;
2229
+ const selectedKeys = new Set(selected.map((p) => proofKey(p)));
2230
+ const entry = {
2231
+ mint: mintUrl,
2232
+ quote_id: quote.rawQuote.quote,
2233
+ bolt11: invoice,
2234
+ destination: opts.destination,
2235
+ amount_sats: quote.amountSats,
2236
+ fee_reserve_sats: quote.feeReserveSats,
2237
+ input_fee_sats: inputFees,
2238
+ input_proofs: selected,
2239
+ melting: {
2240
+ keyset_id: preview.keysetId,
2241
+ outputs: serializeOutputData(preview.outputData)
2242
+ },
2243
+ created_at: new Date(now()).toISOString(),
2244
+ ...quoteView.expires_at !== null ? { expires_at: quoteView.expires_at } : {}
2245
+ };
2246
+ const committed = await withWalletLock(() => {
2247
+ const fresh = loadAgentWallet();
2248
+ if (!fresh) return false;
2249
+ const swept = sweepStaleInFlight(fresh, now());
2250
+ const available = new Set(spendableAt(swept.wallet.proofs, mintUrl).map((p) => proofKey(p)));
2251
+ if (![...selectedKeys].every((k) => available.has(k))) {
2252
+ if (swept.reverted > 0) saveAgentWallet(swept.wallet);
2253
+ return false;
2254
+ }
2255
+ const marked = markProofsInFlight(swept.wallet, selectedKeys, untilMs);
2256
+ saveAgentWallet({ ...marked, pending_melts: [...marked.pending_melts, entry] });
2257
+ return true;
2258
+ });
2259
+ if (!committed) {
2260
+ return {
2261
+ kind: "failed",
2262
+ mint: mintUrl,
2263
+ message: `Proofs at ${mintUrl} changed underneath the melt (another dvm run may have spent them). Nothing was sent.`,
2264
+ settlement: "reverted",
2265
+ quote: quoteView
2266
+ };
2267
+ }
2268
+ let change;
2269
+ let preimage;
2270
+ try {
2271
+ const result = await cashuWallet.completeMelt(preview, initial.lock_privkey);
2272
+ change = result.change;
2273
+ preimage = result.quote.payment_preimage ?? null;
2274
+ } catch (err) {
2275
+ return resolveFailedMelt(cashuWallet, mintUrl, entry, selectedKeys, quoteView, err);
2276
+ }
2277
+ const changeProofs = change.map((p) => toAgentProof(p, mintUrl, initial.lock_pubkey));
2278
+ await withWalletLock(() => {
2279
+ const fresh = loadAgentWallet();
2280
+ if (!fresh) return;
2281
+ saveAgentWallet({
2282
+ ...fresh,
2283
+ proofs: [...fresh.proofs.filter((p) => !selectedKeys.has(proofKey(p))), ...changeProofs],
2284
+ pending_melts: fresh.pending_melts.filter((m) => m.quote_id !== entry.quote_id)
2285
+ });
2286
+ });
2287
+ return {
2288
+ kind: "sent",
2289
+ mint: mintUrl,
2290
+ quote: quoteView,
2291
+ preimage,
2292
+ change_sats: sumSats(changeProofs),
2293
+ ...cappedByMintMax !== void 0 ? { mint_max_sats: cappedByMintMax } : {}
2294
+ };
2295
+ }
2296
+ function meltBoundsOf(wallet) {
2297
+ const reader = wallet;
2298
+ try {
2299
+ const nuts = reader.getMintInfo?.().cache?.nuts;
2300
+ if (nuts === void 0 || nuts === null) return { minSats: null, maxSats: null };
2301
+ return meltAmountBounds(nuts);
2302
+ } catch {
2303
+ return { minSats: null, maxSats: null };
2304
+ }
2305
+ }
2306
+ function spendableAt(proofs, mintUrl) {
2307
+ return proofs.filter((p) => p.mint === mintUrl && p.status === "valid" && p.reserved_for === void 0).slice().sort((a, b) => b.amount - a.amount);
2308
+ }
2309
+ async function resolveFailedMelt(cashuWallet, mintUrl, entry, selectedKeys, quoteView, err) {
2310
+ let unpaid;
2311
+ try {
2312
+ const state = await cashuWallet.checkMeltQuoteBolt11(entry.quote_id);
2313
+ unpaid = state.state === "UNPAID";
2314
+ } catch {
2315
+ unpaid = false;
2316
+ }
2317
+ if (!unpaid) {
2318
+ return {
2319
+ kind: "failed",
2320
+ mint: mintUrl,
2321
+ message: `${errorMessage(err)} \u2014 ${entry.amount_sats} sats may still be in flight at ${mintUrl}. Run \`dvm wallet show\` to reconcile before retrying.`,
2322
+ settlement: "unresolved",
2323
+ quote: quoteView
2324
+ };
2325
+ }
2326
+ await withWalletLock(() => {
2327
+ const fresh = loadAgentWallet();
2328
+ if (!fresh) return;
2329
+ const reverted = revertInFlight(fresh, selectedKeys);
2330
+ saveAgentWallet({
2331
+ ...reverted,
2332
+ pending_melts: reverted.pending_melts.filter((m) => m.quote_id !== entry.quote_id)
2333
+ });
2334
+ });
2335
+ return {
2336
+ kind: "failed",
2337
+ mint: mintUrl,
2338
+ message: `${errorMessage(err)} \u2014 the mint confirms the invoice is unpaid, so the ${quoteView.debit_sats} sats are back in the wallet.`,
2339
+ settlement: "reverted",
2340
+ quote: quoteView
2341
+ };
2342
+ }
2343
+ function selectInputs(candidates, target) {
2344
+ if (target.kind === "all") return [...candidates];
2345
+ const selected = [];
2346
+ let total = 0;
2347
+ for (const p of candidates) {
2348
+ selected.push(p);
2349
+ total += p.amount;
2350
+ if (total >= target.sats) return selected;
2351
+ }
2352
+ return selected;
2353
+ }
2354
+ function widenSelection(candidates, selected, needed, currentInputFees) {
2355
+ const chosen = new Set(selected.map((p) => proofKey(p)));
2356
+ const rest = candidates.filter((p) => !chosen.has(proofKey(p)));
2357
+ if (rest.length === 0) return null;
2358
+ const next = [...selected];
2359
+ let total = sumSats(next);
2360
+ for (const p of rest) {
2361
+ if (total >= needed + currentInputFees) break;
2362
+ next.push(p);
2363
+ total += p.amount;
2364
+ }
2365
+ return next.length === selected.length ? null : { selected: next };
2366
+ }
2367
+ function classifyPreCommitError(mintUrl, err) {
2368
+ if (err instanceof DvmError && err.code === "lnurl_amount_out_of_range") {
2369
+ return {
2370
+ kind: "skipped",
2371
+ mint: mintUrl,
2372
+ reason: "amount_out_of_range",
2373
+ message: err.message
2374
+ };
2375
+ }
2376
+ return { kind: "failed", mint: mintUrl, message: errorMessage(err), settlement: "reverted" };
2377
+ }
2378
+ function sumSats(proofs) {
2379
+ return proofs.reduce((sum, p) => sum + p.amount, 0);
2380
+ }
2381
+ function errorMessage(err) {
2382
+ return err instanceof Error ? err.message : String(err);
2383
+ }
2384
+
2385
+ // src/lib/wallet-agent/denominations.ts
2386
+ var TINY_AMOUNT_THRESHOLD_SATS = 8;
2387
+ function computeSmartDenominations(amountSats, opts = {}) {
2388
+ if (!Number.isFinite(amountSats) || amountSats <= 0) return [];
2389
+ const cap = normalizeCap(opts.maxDenomination);
2390
+ const withTail = opts.includeTinyTail !== false;
2391
+ if (!withTail || amountSats < TINY_AMOUNT_THRESHOLD_SATS) {
2392
+ return greedyPowerOfTwo(Math.floor(amountSats), cap);
2393
+ }
2394
+ const total = Math.floor(amountSats);
2395
+ const small1 = Math.floor(total * 0.05);
2396
+ const small2 = Math.floor(total * 0.1 / 2) * 2;
2397
+ const small4 = Math.floor(total * 0.2 / 4) * 4;
2398
+ const remainder = total - small1 - small2 - small4;
2399
+ const out = [];
2400
+ for (let i = 0; i < small1; i++) out.push(1);
2401
+ for (let i = 0; i < small2 / 2; i++) out.push(2);
2402
+ for (let i = 0; i < small4 / 4; i++) out.push(4);
2403
+ out.push(...greedyPowerOfTwo(remainder, cap));
2404
+ return out;
2405
+ }
2406
+ function greedyPowerOfTwo(n, cap) {
2407
+ const out = [];
2408
+ let r = n;
2409
+ if (cap !== void 0 && r >= cap) {
2410
+ const capCount = Math.floor(r / cap);
2411
+ for (let i = 0; i < capCount; i++) out.push(cap);
2412
+ r -= capCount * cap;
2413
+ }
2414
+ let bit = 1;
2415
+ while (r > 0) {
2416
+ if (r & 1) out.push(bit);
2417
+ r = Math.floor(r / 2);
2418
+ bit *= 2;
2419
+ }
2420
+ return out;
2421
+ }
2422
+ function normalizeCap(raw2) {
2423
+ if (raw2 === void 0) return void 0;
2424
+ if (!Number.isFinite(raw2) || raw2 <= 0) return void 0;
2425
+ const floored = Math.floor(raw2);
2426
+ let pow = 1;
2427
+ while (pow * 2 <= floored) pow *= 2;
2428
+ return pow;
2429
+ }
2430
+
2431
+ // src/lib/wallet-agent/fund.ts
2432
+ import { MintQuoteState } from "@cashu/cashu-ts";
2433
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
2434
+ var DEFAULT_TIMEOUT_SEC = 600;
2435
+ async function fundAgentWallet(amountSats, opts) {
2436
+ if (!Number.isFinite(amountSats) || amountSats <= 0) {
2437
+ throw new DvmError(
2438
+ "invalid_amount",
2439
+ "Amount must be a positive integer number of sats.",
2440
+ "Pass a positive sats amount, e.g. `dvm wallet fund 1000`, or a USD amount, e.g. `dvm wallet fund $5`."
2441
+ );
2442
+ }
2443
+ const sats = Math.floor(amountSats);
2444
+ const initial = loadAgentWallet();
2445
+ if (!initial) {
2446
+ throw new DvmError(
2447
+ "wallet_missing",
2448
+ "No agent wallet found.",
2449
+ "Run `dvm wallet init` first to generate a lock keypair."
2450
+ );
2451
+ }
2452
+ const cashuWallet = await (opts.getWalletForMint ?? getWallet)(opts.mintUrl);
2453
+ const quote = await cashuWallet.createLockedMintQuote(sats, initial.lock_pubkey);
2454
+ const pending = {
2455
+ mint: opts.mintUrl,
2456
+ quote_id: quote.quote,
2457
+ amount_sats: sats,
2458
+ requested_at: (/* @__PURE__ */ new Date()).toISOString()
2459
+ };
2460
+ await withWalletLock(() => {
2461
+ const fresh = loadAgentWallet() ?? initial;
2462
+ saveAgentWallet({
2463
+ ...fresh,
2464
+ pending_mints: [...fresh.pending_mints, pending]
2465
+ });
2466
+ });
2467
+ let floatPayment;
2468
+ if (opts.testSkipLightning) {
2469
+ } else if (opts.payInvoice) {
2470
+ floatPayment = await opts.payInvoice(quote.request, quote);
2471
+ if (opts.onFloatPayment) await opts.onFloatPayment(floatPayment);
2472
+ } else if (opts.onInvoice) {
2473
+ await opts.onInvoice(quote.request, quote);
2474
+ }
2475
+ const finalState = await pollUntilPaidOrTimeout(cashuWallet, quote.quote, opts);
2476
+ if (finalState === MintQuoteState.ISSUED) {
2477
+ throw new DvmError(
2478
+ "fund_already_issued",
2479
+ `Mint quote ${quote.quote} was already collected by a concurrent run.`,
2480
+ "Inspect wallet state with `dvm wallet show` \u2014 the proofs should be persisted by the other instance."
2481
+ );
2482
+ }
2483
+ if (finalState !== MintQuoteState.PAID) {
2484
+ throw new DvmError(
2485
+ "fund_timeout",
2486
+ floatPayment ? `Your Lightning wallet paid mint quote ${quote.quote}, but the mint has not credited it yet.` : `Mint quote ${quote.quote} was not paid within the fund timeout.`,
2487
+ floatPayment ? "Run `dvm wallet show` \u2014 the pending mint is on disk and will be collected once the mint catches up." : "Pay the invoice from a Lightning wallet, then run `dvm wallet show` to recover the pending mint, or `dvm wallet fund <amount>` to start a new flow.",
2488
+ floatPayment ? {
2489
+ display: "Your Lightning wallet sent the payment but the mint hasn't credited it yet. The sats aren't lost \u2014 run 'dvm wallet show' and they'll be collected once the mint catches up.",
2490
+ float_payment: floatPaymentJson(floatPayment)
2491
+ } : void 0
2492
+ );
2493
+ }
2494
+ const denominations = computeSmartDenominations(sats, {
2495
+ maxDenomination: maxKeysetDenomination(cashuWallet),
2496
+ includeTinyTail: false
2497
+ });
2498
+ const preview = await prepareP2PKMint(
2499
+ cashuWallet,
2500
+ sats,
2501
+ quote,
2502
+ initial.lock_privkey,
2503
+ initial.lock_pubkey,
2504
+ denominations
2505
+ );
2506
+ const minting = {
2507
+ keyset_id: preview.keysetId,
2508
+ outputs: serializeOutputData(preview.outputData)
2509
+ };
2510
+ await withWalletLock(() => {
2511
+ const fresh = loadAgentWallet() ?? initial;
2512
+ saveAgentWallet(upsertPending(fresh, { ...pending, minting }));
2513
+ });
2514
+ const proofs = await cashuWallet.completeMint(preview);
2515
+ const agentProofs = proofs.map((p) => toAgentProof(p, opts.mintUrl, initial.lock_pubkey));
2516
+ await withWalletLock(() => {
2517
+ const post = loadAgentWallet() ?? initial;
2518
+ saveAgentWallet({
2519
+ ...post,
2520
+ proofs: [...post.proofs, ...agentProofs],
2521
+ pending_mints: post.pending_mints.filter((p) => p.quote_id !== quote.quote)
2522
+ });
2523
+ });
2524
+ return {
2525
+ mint: opts.mintUrl,
2526
+ amount_sats: sats,
2527
+ quote_id: quote.quote,
2528
+ denominations,
2529
+ proofs: agentProofs,
2530
+ ...floatPayment ? { float_payment: floatPayment } : {}
2531
+ };
2532
+ }
2533
+ async function prepareP2PKMint(cashuWallet, sats, quote, lockPrivkey, lockPubkey, denominations, extras = {}) {
2534
+ const options = {
2535
+ pubkey: lockPubkey
2536
+ };
2537
+ if (extras.locktimeUnixSec !== void 0) options.locktime = extras.locktimeUnixSec;
1598
2538
  if (extras.refundKeys && extras.refundKeys.length > 0) options.refundKeys = extras.refundKeys;
1599
2539
  return cashuWallet.prepareMint(
1600
2540
  "bolt11",
@@ -1604,6 +2544,17 @@ async function prepareP2PKMint(cashuWallet, sats, quote, lockPrivkey, lockPubkey
1604
2544
  { type: "p2pk", options, denominations }
1605
2545
  );
1606
2546
  }
2547
+ function fundTimeoutMs() {
2548
+ const raw2 = process.env.DVM_FUND_TIMEOUT_SEC;
2549
+ if (!raw2) return DEFAULT_TIMEOUT_SEC * 1e3;
2550
+ const n = Number(raw2);
2551
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_TIMEOUT_SEC * 1e3;
2552
+ return Math.floor(n) * 1e3;
2553
+ }
2554
+ function upsertPending(wallet, entry) {
2555
+ const others = wallet.pending_mints.filter((p) => p.quote_id !== entry.quote_id);
2556
+ return { ...wallet, pending_mints: [...others, entry] };
2557
+ }
1607
2558
  function maxKeysetDenomination(cashuWallet) {
1608
2559
  const keysets = cashuWallet.keyChain.getKeysets();
1609
2560
  if (keysets.length === 0) return void 0;
@@ -1613,14 +2564,35 @@ function maxKeysetDenomination(cashuWallet) {
1613
2564
  if (amounts.length === 0) return void 0;
1614
2565
  return Math.max(...amounts);
1615
2566
  }
2567
+ async function pollUntilPaidOrTimeout(cashuWallet, quoteId, opts) {
2568
+ const now = opts.now ?? (() => Date.now());
2569
+ const sleep2 = opts.sleep ?? defaultSleep;
2570
+ const intervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2571
+ const timeoutMs = opts.timeoutMs ?? fundTimeoutMs();
2572
+ const start = now();
2573
+ let lastState = MintQuoteState.UNPAID;
2574
+ while (now() - start < timeoutMs) {
2575
+ const checked = await cashuWallet.checkMintQuoteBolt11(quoteId);
2576
+ lastState = checked.state;
2577
+ if (opts.onPoll) opts.onPoll(lastState, now() - start);
2578
+ if (lastState === MintQuoteState.PAID || lastState === MintQuoteState.ISSUED) {
2579
+ return lastState;
2580
+ }
2581
+ await sleep2(intervalMs);
2582
+ }
2583
+ return lastState;
2584
+ }
2585
+ function defaultSleep(ms) {
2586
+ return new Promise((resolve) => setTimeout(resolve, ms));
2587
+ }
1616
2588
 
1617
2589
  // src/lib/wallet-agent/locktime.ts
1618
2590
  var DEFAULT_RESERVE_LOCKTIME_SECONDS = 30 * 24 * 60 * 60;
1619
2591
  var DEFAULT_PERCALL_LOCKTIME_SECONDS = 48 * 60 * 60;
1620
2592
  var PERCALL_LOCKTIME_ENV = "DVM_PERCALL_LOCKTIME_SEC";
1621
2593
  function perCallLocktimeSeconds(minLocktimeSeconds) {
1622
- const raw = process.env[PERCALL_LOCKTIME_ENV];
1623
- const parsed = raw ? Number(raw) : NaN;
2594
+ const raw2 = process.env[PERCALL_LOCKTIME_ENV];
2595
+ const parsed = raw2 ? Number(raw2) : NaN;
1624
2596
  const base = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_PERCALL_LOCKTIME_SECONDS;
1625
2597
  const floor = typeof minLocktimeSeconds === "number" && Number.isFinite(minLocktimeSeconds) ? Math.max(0, Math.floor(minLocktimeSeconds)) : 0;
1626
2598
  return Math.max(base, floor);
@@ -1631,6 +2603,35 @@ var SUFFIXES = {
1631
2603
  h: 60 * 60,
1632
2604
  d: 24 * 60 * 60
1633
2605
  };
2606
+ function parseLocktime(raw2) {
2607
+ const trimmed = raw2.trim().toLowerCase();
2608
+ const match = /^(\d+)([smhd])$/.exec(trimmed);
2609
+ if (!match) {
2610
+ throw new DvmError(
2611
+ "invalid_locktime",
2612
+ `Could not parse "${raw2}" as a locktime.`,
2613
+ "Pass a positive integer followed by `s`, `m`, `h`, or `d` \u2014 e.g. `30d`, `12h`, `90m`."
2614
+ );
2615
+ }
2616
+ const n = Number(match[1]);
2617
+ if (!Number.isFinite(n) || n <= 0) {
2618
+ throw new DvmError(
2619
+ "invalid_locktime",
2620
+ `Locktime must be a positive number; got "${raw2}".`,
2621
+ "Pass a positive integer followed by `s`, `m`, `h`, or `d` \u2014 e.g. `30d`, `12h`, `90m`."
2622
+ );
2623
+ }
2624
+ const suffix = match[2];
2625
+ const seconds = n * SUFFIXES[suffix];
2626
+ return seconds;
2627
+ }
2628
+ function formatLocktime(seconds) {
2629
+ if (seconds <= 0) return `${seconds}s`;
2630
+ if (seconds % SUFFIXES.d === 0) return `${seconds / SUFFIXES.d}d`;
2631
+ if (seconds % SUFFIXES.h === 0) return `${seconds / SUFFIXES.h}h`;
2632
+ if (seconds % SUFFIXES.m === 0) return `${seconds / SUFFIXES.m}m`;
2633
+ return `${seconds}s`;
2634
+ }
1634
2635
 
1635
2636
  // src/lib/wallet-agent/mint-inventory.ts
1636
2637
  async function loadConfiguredMints() {
@@ -1653,6 +2654,29 @@ async function loadConfiguredMints() {
1653
2654
  return merged;
1654
2655
  });
1655
2656
  }
2657
+ function buildDisplayMintInventory(wallet) {
2658
+ const entries = [];
2659
+ const seen = /* @__PURE__ */ new Set();
2660
+ for (const url of wallet.mints) {
2661
+ if (seen.has(url)) continue;
2662
+ seen.add(url);
2663
+ entries.push({ url, source: "configured" });
2664
+ }
2665
+ for (const proof of wallet.proofs) {
2666
+ if (seen.has(proof.mint)) continue;
2667
+ seen.add(proof.mint);
2668
+ entries.push({ url: proof.mint, source: "proofs" });
2669
+ }
2670
+ for (const pending of wallet.pending_mints) {
2671
+ if (seen.has(pending.mint)) continue;
2672
+ seen.add(pending.mint);
2673
+ entries.push({ url: pending.mint, source: "pending" });
2674
+ }
2675
+ if (entries.length === 0) {
2676
+ entries.push({ url: DEFAULT_AGENT_MINT_URL, source: "default" });
2677
+ }
2678
+ return entries;
2679
+ }
1656
2680
  function dedupe(values) {
1657
2681
  const seen = /* @__PURE__ */ new Set();
1658
2682
  const out = [];
@@ -1664,84 +2688,492 @@ function dedupe(values) {
1664
2688
  return out;
1665
2689
  }
1666
2690
 
1667
- // src/lib/wallet-agent/receive.ts
1668
- import { Amount as Amount5, getP2PKExpectedWitnessPubkeys, getTokenMetadata } from "@cashu/cashu-ts";
1669
-
1670
- // src/lib/wallet-agent/recovery.ts
1671
- import { MintQuoteState as MintQuoteState2 } from "@cashu/cashu-ts";
1672
-
1673
- // src/lib/wallet-agent/recovery-submissions.ts
1674
- import {
1675
- Amount as Amount6,
1676
- CheckStateEnum,
1677
- getEncodedToken as getEncodedToken2
1678
- } from "@cashu/cashu-ts";
1679
-
1680
- // src/lib/cashu/agent-refund-subkey.ts
1681
- import { HDKey as HDKey2 } from "@scure/bip32";
1682
- import { mnemonicToSeedSync as mnemonicToSeedSync2 } from "@scure/bip39";
1683
- var PURPOSE_INDEX2 = 1789;
1684
- var AGENT_REFUND_ROLE_INDEX = 2;
1685
- function agentRefundSubkeyPath(canonicalDvmId) {
1686
- const dvmIndex = hashedCanonicalDvmId(canonicalDvmId);
1687
- return `m/${PURPOSE_INDEX2}'/${AGENT_REFUND_ROLE_INDEX}'/${dvmIndex}'`;
1688
- }
1689
- function deriveAgentRefundSubkey(mnemonic, canonicalDvmId) {
1690
- const seed = mnemonicToSeedSync2(mnemonic);
1691
- const hd = HDKey2.fromMasterSeed(seed);
1692
- const child = hd.derive(agentRefundSubkeyPath(canonicalDvmId));
1693
- if (!child.privateKey || !child.publicKey) {
1694
- throw new Error("Failed to derive agent refund subkey from mnemonic");
2691
+ // src/lib/wallet-agent/mint-selection.ts
2692
+ async function resolveFundMint(args) {
2693
+ const { preferred, explicit, configured, sats } = args;
2694
+ const probe = await probeMintHealthOnce(preferred);
2695
+ if (explicit) {
2696
+ if (probe.ok) {
2697
+ return { url: preferred, fallbackKind: null, preferredProbe: probe };
2698
+ }
2699
+ const healthy = await findFundableAlternatives(configured, preferred, sats);
2700
+ const hint = healthy.length > 0 ? `Pass --mint one of: ${healthy.join(", ")}, or drop --mint to auto-select a healthy mint.` : `No other wallet-configured mint can issue ${sats} sats right now \u2014 wait and retry, or add a new mint with \`dvm wallet mint-add\`.`;
2701
+ throw new DvmError(
2702
+ "mint_unhealthy",
2703
+ `Mint ${preferred} failed health probe (${probe.error?.code ?? "unknown"}: ${probe.error?.message ?? "\u2014"}).`,
2704
+ hint
2705
+ );
1695
2706
  }
1696
- return {
1697
- privkey: Buffer.from(child.privateKey).toString("hex"),
1698
- pubkey: Buffer.from(child.publicKey).toString("hex")
2707
+ if (canFund(probe, sats)) {
2708
+ return { url: preferred, fallbackKind: null, preferredProbe: probe };
2709
+ }
2710
+ const preferredReason = fundRefusalReason(probe, sats);
2711
+ const passedOver = [];
2712
+ const notePassedOver = (mint, candidate) => {
2713
+ passedOver.push({
2714
+ mint,
2715
+ reason: fundRefusalReason(candidate, sats),
2716
+ bounds: candidate.mintAmountBounds ?? { minSats: null, maxSats: null }
2717
+ });
1699
2718
  };
2719
+ notePassedOver(preferred, probe);
2720
+ for (const candidate of configured) {
2721
+ if (candidate === preferred) continue;
2722
+ const candidateProbe = await probeMintHealthOnce(candidate);
2723
+ if (canFund(candidateProbe, sats)) {
2724
+ const fallbackKind = fallbackKindFor(preferredReason);
2725
+ return {
2726
+ url: candidate,
2727
+ fallbackKind,
2728
+ preferredProbe: probe,
2729
+ fallback: { preferred, selected: candidate, reason: preferredReason }
2730
+ };
2731
+ }
2732
+ notePassedOver(candidate, candidateProbe);
2733
+ }
2734
+ const outOfRange = passedOver.filter((m) => isBoundsRefusal(m.reason));
2735
+ const unavailable = passedOver.filter((m) => !isBoundsRefusal(m.reason));
2736
+ if (outOfRange.length > 0) {
2737
+ const named = outOfRange.map((m) => `${m.mint} takes ${describeMintBounds(m.bounds)}`).join("; ");
2738
+ const alsoDown = unavailable.length > 0 ? ` ${unavailable.map((m) => `${m.mint} (${m.reason})`).join(", ")} couldn't be used at all right now.` : "";
2739
+ const retryNote = unavailable.length > 0 ? " Retrying later may also help \u2014 at least one mint is currently unavailable rather than limited." : "";
2740
+ throw new DvmError(
2741
+ "mint_amount_out_of_range",
2742
+ `No configured mint will issue ${sats} sats in one go: ${named}.${alsoDown}`,
2743
+ `${suggestInRangeAmount(outOfRange, sats)}${retryNote} Or add a mint with different limits using \`dvm wallet mint-add\`. Nothing was charged and no Lightning invoice was created.`,
2744
+ {
2745
+ amount_sats: sats,
2746
+ mint_bounds: outOfRange.map((m) => ({
2747
+ mint: m.mint,
2748
+ min_sats: m.bounds.minSats,
2749
+ max_sats: m.bounds.maxSats
2750
+ })),
2751
+ unavailable_mints: unavailable.map((m) => ({ mint: m.mint, reason: m.reason }))
2752
+ }
2753
+ );
2754
+ }
2755
+ throw new DvmError(
2756
+ "all_mints_unhealthy",
2757
+ `No wallet-configured mint can currently issue ecash (preferred: ${preferred}, ${preferredReason}).`,
2758
+ "Wait for at least one mint to recover, or add a new mint with `dvm wallet mint-add`. Existing balance is unaffected \u2014 `dvm wallet cash-out` still works against a mint that can only melt."
2759
+ );
1700
2760
  }
1701
- function deriveAgentRefundSubkeyPubkey(mnemonic, canonicalDvmId) {
1702
- return deriveAgentRefundSubkey(mnemonic, canonicalDvmId).pubkey;
1703
- }
1704
-
1705
- // src/lib/credit-policy.ts
1706
- import { randomUUID } from "crypto";
1707
-
1708
- // src/lib/x402/menu.ts
1709
- function offersX402CreditFunding(menu, network) {
1710
- return matchesNetwork(x402CreditFundingNetworks(menu), network);
1711
- }
1712
- function offersX402CreditChannel(menu, network) {
1713
- return matchesNetwork(x402CreditChannelNetworks(menu), network);
2761
+ function describeMintBounds(bounds) {
2762
+ const min = bounds?.minSats ?? null;
2763
+ const max = bounds?.maxSats ?? null;
2764
+ if (min !== null && max !== null) return `${min}\u2013${max} sats`;
2765
+ if (max !== null) return `up to ${max} sats`;
2766
+ if (min !== null) return `at least ${min} sats`;
2767
+ return "any amount";
1714
2768
  }
1715
- function x402CreditFlavour(menu, network) {
1716
- if (offersX402CreditChannel(menu, network)) return X402_BATCH_SETTLEMENT_SCHEME;
1717
- if (matchesNetwork(x402CreditExactNetworks(menu), network)) return X402_EXACT_SCHEME;
1718
- return void 0;
2769
+ function isBoundsRefusal(reason) {
2770
+ return reason === "amount_below_min" || reason === "amount_above_max";
1719
2771
  }
1720
- function offersX402CreditFlavour(menu, network, flavour) {
1721
- return matchesNetwork(
1722
- flavour === X402_BATCH_SETTLEMENT_SCHEME ? x402CreditChannelNetworks(menu) : x402CreditExactNetworks(menu),
1723
- network
1724
- );
2772
+ function canFund(probe, sats) {
2773
+ return probe.ok && probe.mintingEnabled !== false && amountBoundsVerdict(sats, probe.mintAmountBounds) === "ok";
1725
2774
  }
1726
- function x402InstrumentForFlavour(flavour) {
1727
- return flavour === X402_BATCH_SETTLEMENT_SCHEME ? "x402_channel" : "x402_exact";
2775
+ function fundRefusalReason(probe, sats) {
2776
+ if (!probe.ok) return probe.error?.code ?? "unhealthy";
2777
+ if (probe.mintingEnabled === false) return "minting_disabled";
2778
+ const verdict = amountBoundsVerdict(sats, probe.mintAmountBounds);
2779
+ return verdict === "below_min" ? "amount_below_min" : verdict === "above_max" ? "amount_above_max" : "unhealthy";
1728
2780
  }
1729
- function x402FlavourForInstrument(instrument) {
1730
- if (instrument === "x402_channel") return X402_BATCH_SETTLEMENT_SCHEME;
1731
- if (instrument === "x402_exact") return X402_EXACT_SCHEME;
1732
- return void 0;
2781
+ function fallbackKindFor(reason) {
2782
+ if (reason === "minting_disabled") return "capability";
2783
+ if (isBoundsRefusal(reason)) return "bounds";
2784
+ return "health";
1733
2785
  }
1734
- function x402CreditChannelNetworks(menu) {
1735
- return schemeNetworks(menu, X402_BATCH_SETTLEMENT_SCHEME);
2786
+ function suggestInRangeAmount(outOfRange, sats) {
2787
+ const maxima = outOfRange.map((m) => m.bounds.maxSats).filter((n) => n !== null);
2788
+ const largestMax = maxima.length > 0 ? Math.max(...maxima) : null;
2789
+ if (largestMax !== null && sats > largestMax) {
2790
+ return `Fund ${largestMax} sats or less, then repeat for the rest.`;
2791
+ }
2792
+ const minima = outOfRange.map((m) => m.bounds.minSats).filter((n) => n !== null);
2793
+ const smallestMin = minima.length > 0 ? Math.min(...minima) : null;
2794
+ if (smallestMin !== null && sats < smallestMin) {
2795
+ return `Fund at least ${smallestMin} sats.`;
2796
+ }
2797
+ return "Try a different amount.";
1736
2798
  }
1737
- function x402CreditExactNetworks(menu) {
1738
- return schemeNetworks(menu, X402_EXACT_SCHEME);
2799
+ async function probeMintHealthOnce(mintUrl) {
2800
+ return checkMintHealth(mintUrl, { retries: 1, backoffMs: [0] });
1739
2801
  }
1740
- function x402CreditFundingNetworks(menu) {
1741
- return [.../* @__PURE__ */ new Set([...x402CreditChannelNetworks(menu), ...x402CreditExactNetworks(menu)])];
2802
+ async function findFundableAlternatives(configured, skip, sats) {
2803
+ const healthy = [];
2804
+ for (const mint of configured) {
2805
+ if (mint === skip) continue;
2806
+ const probe = await probeMintHealthOnce(mint);
2807
+ if (canFund(probe, sats)) healthy.push(mint);
2808
+ }
2809
+ return healthy;
1742
2810
  }
1743
- function schemeNetworks(menu, scheme) {
1744
- return (menu?.x402?.schemes ?? []).filter((entry) => entry.scheme === scheme).map((entry) => entry.network);
2811
+
2812
+ // src/lib/wallet-agent/receive.ts
2813
+ import { Amount as Amount5, getP2PKExpectedWitnessPubkeys, getTokenMetadata } from "@cashu/cashu-ts";
2814
+ async function receiveToAgentWallet(token, opts = {}) {
2815
+ const initial = loadAgentWallet();
2816
+ if (!initial) {
2817
+ throw new DvmError(
2818
+ "wallet_missing",
2819
+ "No agent wallet found.",
2820
+ "Run `dvm wallet init` first to generate a lock keypair."
2821
+ );
2822
+ }
2823
+ let mintUrl;
2824
+ try {
2825
+ mintUrl = getTokenMetadata(token).mint;
2826
+ } catch {
2827
+ throw new DvmError(
2828
+ "invalid_token",
2829
+ "Could not decode the Cashu token.",
2830
+ "Pass a complete `cashu...` token string."
2831
+ );
2832
+ }
2833
+ const cashuWallet = await (opts.getWalletForMint ?? getWallet)(mintUrl);
2834
+ const ours = toLockPubkey(initial.lock_pubkey);
2835
+ for (const p of cashuWallet.decodeToken(token).proofs) {
2836
+ let allowed;
2837
+ try {
2838
+ allowed = getP2PKExpectedWitnessPubkeys(p.secret);
2839
+ } catch {
2840
+ continue;
2841
+ }
2842
+ if (allowed.length === 0) continue;
2843
+ if (!allowed.map(toLockPubkey).includes(ours)) {
2844
+ throw new DvmError(
2845
+ "cashu_not_claimable",
2846
+ "This token is P2PK-locked to a key that isn't your agent wallet's master lock pubkey.",
2847
+ "If it was issued to you as a per-DVM refund (NUT-11 `refund` tag), claim it via `dvm wallet release` instead \u2014 that path derives the matching per-DVM refund privkey. Otherwise it was issued to a third party; ask the sender for a token locked to your wallet, or one with no lock."
2848
+ );
2849
+ }
2850
+ }
2851
+ const proofs = await cashuWallet.receive(
2852
+ token,
2853
+ { privkey: initial.lock_privkey },
2854
+ { type: "p2pk", options: { pubkey: initial.lock_pubkey } }
2855
+ );
2856
+ const agentProofs = proofs.map((p) => toAgentProof(p, mintUrl, initial.lock_pubkey));
2857
+ const claimedSats = proofs.reduce((sum, p) => sum + Amount5.from(p.amount).toNumber(), 0);
2858
+ await withWalletLock(() => {
2859
+ const fresh = loadAgentWallet() ?? initial;
2860
+ const mints = fresh.mints.includes(mintUrl) ? fresh.mints : [...fresh.mints, mintUrl];
2861
+ saveAgentWallet({
2862
+ ...fresh,
2863
+ mints,
2864
+ proofs: [...fresh.proofs, ...agentProofs]
2865
+ });
2866
+ });
2867
+ return { mintUrl, claimedSats, proofCount: agentProofs.length };
2868
+ }
2869
+
2870
+ // src/lib/wallet-agent/recovery.ts
2871
+ import { MintQuoteState as MintQuoteState2 } from "@cashu/cashu-ts";
2872
+ async function sweepPendingMints(opts = {}) {
2873
+ const wallet = loadAgentWallet();
2874
+ if (!wallet || wallet.pending_mints.length === 0) {
2875
+ return { outcomes: [], recovered_proofs: [], still_pending: [], errors: [] };
2876
+ }
2877
+ const now = opts.now ?? (() => Date.now());
2878
+ const expirationMs = opts.expirationMs ?? fundTimeoutMs();
2879
+ const getWalletForMint = opts.getWalletForMint ?? getWallet;
2880
+ const outcomes = [];
2881
+ for (const entry of wallet.pending_mints) {
2882
+ const outcome = await processEntry(wallet, entry, now(), expirationMs, getWalletForMint);
2883
+ outcomes.push(outcome);
2884
+ }
2885
+ if (outcomes.some(
2886
+ (o) => o.kind === "minted" || o.kind === "restored" || o.kind === "already_issued" || o.kind === "expired"
2887
+ )) {
2888
+ await withWalletLock(() => {
2889
+ const fresh = loadAgentWallet() ?? wallet;
2890
+ const updated = outcomes.reduce(applyOutcome, fresh);
2891
+ saveAgentWallet(updated);
2892
+ });
2893
+ }
2894
+ return {
2895
+ outcomes,
2896
+ recovered_proofs: outcomes.flatMap(
2897
+ (o) => o.kind === "minted" || o.kind === "restored" ? o.proofs : []
2898
+ ),
2899
+ still_pending: outcomes.filter(
2900
+ (o) => o.kind === "still_pending"
2901
+ ).map((o) => o.entry),
2902
+ errors: outcomes.filter((o) => o.kind === "error").map((o) => ({ quote_id: o.entry.quote_id, error: o.error }))
2903
+ };
2904
+ }
2905
+ async function processEntry(wallet, entry, nowMs, expirationMs, getWalletForMint) {
2906
+ let cashuWallet;
2907
+ try {
2908
+ cashuWallet = await getWalletForMint(entry.mint);
2909
+ } catch (err) {
2910
+ return { kind: "error", entry, error: errorMessage2(err) };
2911
+ }
2912
+ let state;
2913
+ let quoteResponse;
2914
+ try {
2915
+ quoteResponse = await cashuWallet.checkMintQuoteBolt11(entry.quote_id);
2916
+ state = quoteResponse.state;
2917
+ } catch (err) {
2918
+ return { kind: "error", entry, error: errorMessage2(err) };
2919
+ }
2920
+ if (state === MintQuoteState2.ISSUED) {
2921
+ if (!entry.minting) {
2922
+ return { kind: "already_issued", entry };
2923
+ }
2924
+ try {
2925
+ const restoredProofs = await restoreFromMintingState(
2926
+ cashuWallet,
2927
+ entry.minting,
2928
+ wallet.lock_pubkey,
2929
+ entry.mint
2930
+ );
2931
+ if (restoredProofs.length === 0) {
2932
+ return { kind: "already_issued", entry };
2933
+ }
2934
+ return { kind: "restored", entry, proofs: restoredProofs };
2935
+ } catch (err) {
2936
+ return { kind: "error", entry, error: errorMessage2(err) };
2937
+ }
2938
+ }
2939
+ if (state === MintQuoteState2.PAID) {
2940
+ try {
2941
+ const denominations = computeSmartDenominations(entry.amount_sats, {
2942
+ maxDenomination: maxKeysetDenomination(cashuWallet),
2943
+ includeTinyTail: false
2944
+ });
2945
+ const preview = await prepareP2PKMint(
2946
+ cashuWallet,
2947
+ entry.amount_sats,
2948
+ quoteResponse,
2949
+ wallet.lock_privkey,
2950
+ wallet.lock_pubkey,
2951
+ denominations
2952
+ );
2953
+ const minting = {
2954
+ keyset_id: preview.keysetId,
2955
+ outputs: serializeOutputData(preview.outputData)
2956
+ };
2957
+ await withWalletLock(() => {
2958
+ const fresh = loadAgentWallet() ?? wallet;
2959
+ saveAgentWallet({
2960
+ ...fresh,
2961
+ pending_mints: fresh.pending_mints.map(
2962
+ (p) => p.quote_id === entry.quote_id ? { ...p, minting } : p
2963
+ )
2964
+ });
2965
+ });
2966
+ const proofs = await cashuWallet.completeMint(preview);
2967
+ const agentProofs = proofs.map((p) => toAgentProof(p, entry.mint, wallet.lock_pubkey));
2968
+ return { kind: "minted", entry, proofs: agentProofs };
2969
+ } catch (err) {
2970
+ return { kind: "error", entry, error: errorMessage2(err) };
2971
+ }
2972
+ }
2973
+ const requestedAtMs = Date.parse(entry.requested_at);
2974
+ const ageMs2 = Number.isFinite(requestedAtMs) ? nowMs - requestedAtMs : Infinity;
2975
+ if (ageMs2 > expirationMs) {
2976
+ return { kind: "expired", entry };
2977
+ }
2978
+ return { kind: "still_pending", entry };
2979
+ }
2980
+ async function restoreFromMintingState(cashuWallet, minting, lockPubkey, mintUrl) {
2981
+ const proofs = await restoreProofsFromOutputs(cashuWallet, minting.keyset_id, minting.outputs);
2982
+ return proofs.map((proof) => toAgentProof(proof, mintUrl, lockPubkey));
2983
+ }
2984
+ function applyOutcome(wallet, outcome) {
2985
+ if (outcome.kind === "still_pending" || outcome.kind === "error") {
2986
+ return wallet;
2987
+ }
2988
+ const next = {
2989
+ ...wallet,
2990
+ pending_mints: wallet.pending_mints.filter((p) => p.quote_id !== outcome.entry.quote_id)
2991
+ };
2992
+ if (outcome.kind === "minted" || outcome.kind === "restored") {
2993
+ next.proofs = [...wallet.proofs, ...outcome.proofs];
2994
+ }
2995
+ return next;
2996
+ }
2997
+ function errorMessage2(err) {
2998
+ return err instanceof Error ? err.message : String(err);
2999
+ }
3000
+
3001
+ // src/lib/wallet-agent/recovery-melt.ts
3002
+ async function sweepPendingMelts(opts = {}) {
3003
+ const wallet = loadAgentWallet();
3004
+ if (!wallet || wallet.pending_melts.length === 0) {
3005
+ return { outcomes: [], settled_sats: 0, returned_sats: 0, still_pending: [], errors: [] };
3006
+ }
3007
+ const now = opts.now ?? (() => Date.now());
3008
+ const abandonAfterMs = opts.abandonAfterMs ?? meltInFlightTimeoutSeconds() * 1e3;
3009
+ const getWalletForMint = opts.getWalletForMint ?? getWallet;
3010
+ const outcomes = [];
3011
+ for (const entry of wallet.pending_melts) {
3012
+ outcomes.push(await processEntry2(wallet, entry, now(), abandonAfterMs, getWalletForMint));
3013
+ }
3014
+ if (outcomes.some((o) => o.kind === "settled" || o.kind === "reverted")) {
3015
+ await withWalletLock(() => {
3016
+ const fresh = loadAgentWallet() ?? wallet;
3017
+ saveAgentWallet(outcomes.reduce(applyOutcome2, fresh));
3018
+ });
3019
+ }
3020
+ return {
3021
+ outcomes,
3022
+ settled_sats: outcomes.filter((o) => o.kind === "settled").reduce((sum, o) => sum + o.entry.amount_sats, 0),
3023
+ returned_sats: outcomes.filter((o) => o.kind === "reverted").reduce((sum, o) => sum + o.returned_sats, 0),
3024
+ still_pending: outcomes.filter(
3025
+ (o) => o.kind === "still_pending"
3026
+ ).map((o) => o.entry),
3027
+ errors: outcomes.filter((o) => o.kind === "error").map((o) => ({ quote_id: o.entry.quote_id, error: o.error }))
3028
+ };
3029
+ }
3030
+ async function processEntry2(wallet, entry, nowMs, abandonAfterMs, getWalletForMint) {
3031
+ let cashuWallet;
3032
+ try {
3033
+ cashuWallet = await getWalletForMint(entry.mint);
3034
+ } catch (err) {
3035
+ return { kind: "error", entry, error: errorMessage3(err) };
3036
+ }
3037
+ let state;
3038
+ try {
3039
+ const quote = await cashuWallet.checkMeltQuoteBolt11(entry.quote_id);
3040
+ state = quote.state;
3041
+ } catch (err) {
3042
+ return { kind: "error", entry, error: errorMessage3(err) };
3043
+ }
3044
+ if (state === "PAID") {
3045
+ let change;
3046
+ try {
3047
+ change = await restoreChange(cashuWallet, entry, wallet.lock_pubkey);
3048
+ } catch {
3049
+ change = [];
3050
+ }
3051
+ return { kind: "settled", entry, change };
3052
+ }
3053
+ if (state === "UNPAID") {
3054
+ const createdAtMs = Date.parse(entry.created_at);
3055
+ const ageMs2 = Number.isFinite(createdAtMs) ? nowMs - createdAtMs : Infinity;
3056
+ if (ageMs2 <= abandonAfterMs) return { kind: "still_pending", entry };
3057
+ return {
3058
+ kind: "reverted",
3059
+ entry,
3060
+ returned_sats: entry.input_proofs.reduce((sum, p) => sum + p.amount, 0)
3061
+ };
3062
+ }
3063
+ return { kind: "still_pending", entry };
3064
+ }
3065
+ async function restoreChange(cashuWallet, entry, lockPubkey) {
3066
+ if (!entry.melting || entry.melting.outputs.length === 0) return [];
3067
+ const data = rehydrateOutputData(entry.melting.outputs);
3068
+ const keyset = cashuWallet.keyChain.getKeyset(entry.melting.keyset_id);
3069
+ const restored = await cashuWallet.mint.restore({
3070
+ outputs: data.map((d) => d.blindedMessage)
3071
+ });
3072
+ if (restored.outputs.length !== restored.signatures.length) {
3073
+ throw new Error(
3074
+ `mint returned mismatched outputs (${restored.outputs.length}) and signatures (${restored.signatures.length}) \u2014 NUT-09 requires parallel arrays`
3075
+ );
3076
+ }
3077
+ const byB = new Map(data.map((d) => [d.blindedMessage.B_, d]));
3078
+ const proofs = [];
3079
+ for (let i = 0; i < restored.signatures.length; i++) {
3080
+ const owner = byB.get(restored.outputs[i].B_);
3081
+ if (!owner) continue;
3082
+ proofs.push(
3083
+ toAgentProof(owner.toProof(restored.signatures[i], keyset), entry.mint, lockPubkey)
3084
+ );
3085
+ }
3086
+ return proofs;
3087
+ }
3088
+ function applyOutcome2(wallet, outcome) {
3089
+ if (outcome.kind === "still_pending" || outcome.kind === "error") return wallet;
3090
+ const inputKeys = new Set(outcome.entry.input_proofs.map((p) => proofKey(p)));
3091
+ const next = {
3092
+ ...wallet,
3093
+ pending_melts: wallet.pending_melts.filter((m) => m.quote_id !== outcome.entry.quote_id)
3094
+ };
3095
+ if (outcome.kind === "settled") {
3096
+ next.proofs = [...next.proofs.filter((p) => !inputKeys.has(proofKey(p))), ...outcome.change];
3097
+ return next;
3098
+ }
3099
+ return revertInFlight(next, inputKeys);
3100
+ }
3101
+ function errorMessage3(err) {
3102
+ return err instanceof Error ? err.message : String(err);
3103
+ }
3104
+
3105
+ // src/lib/wallet-agent/recovery-submissions.ts
3106
+ import {
3107
+ Amount as Amount6,
3108
+ CheckStateEnum,
3109
+ getEncodedToken as getEncodedToken2
3110
+ } from "@cashu/cashu-ts";
3111
+
3112
+ // src/lib/cashu/agent-refund-subkey.ts
3113
+ import { HDKey as HDKey2 } from "@scure/bip32";
3114
+ import { mnemonicToSeedSync as mnemonicToSeedSync2 } from "@scure/bip39";
3115
+ var PURPOSE_INDEX2 = 1789;
3116
+ var AGENT_REFUND_ROLE_INDEX = 2;
3117
+ function agentRefundSubkeyPath(canonicalDvmId) {
3118
+ const dvmIndex = hashedCanonicalDvmId(canonicalDvmId);
3119
+ return `m/${PURPOSE_INDEX2}'/${AGENT_REFUND_ROLE_INDEX}'/${dvmIndex}'`;
3120
+ }
3121
+ function deriveAgentRefundSubkey(mnemonic, canonicalDvmId) {
3122
+ const seed = mnemonicToSeedSync2(mnemonic);
3123
+ const hd = HDKey2.fromMasterSeed(seed);
3124
+ const child = hd.derive(agentRefundSubkeyPath(canonicalDvmId));
3125
+ if (!child.privateKey || !child.publicKey) {
3126
+ throw new Error("Failed to derive agent refund subkey from mnemonic");
3127
+ }
3128
+ return {
3129
+ privkey: Buffer.from(child.privateKey).toString("hex"),
3130
+ pubkey: Buffer.from(child.publicKey).toString("hex")
3131
+ };
3132
+ }
3133
+ function deriveAgentRefundSubkeyPubkey(mnemonic, canonicalDvmId) {
3134
+ return deriveAgentRefundSubkey(mnemonic, canonicalDvmId).pubkey;
3135
+ }
3136
+
3137
+ // src/lib/credit-policy.ts
3138
+ import { randomUUID } from "crypto";
3139
+
3140
+ // src/lib/x402/menu.ts
3141
+ function offersX402CreditFunding(menu, network) {
3142
+ return matchesNetwork(x402CreditFundingNetworks(menu), network);
3143
+ }
3144
+ function offersX402CreditChannel(menu, network) {
3145
+ return matchesNetwork(x402CreditChannelNetworks(menu), network);
3146
+ }
3147
+ function x402CreditFlavour(menu, network) {
3148
+ if (offersX402CreditChannel(menu, network)) return X402_BATCH_SETTLEMENT_SCHEME;
3149
+ if (matchesNetwork(x402CreditExactNetworks(menu), network)) return X402_EXACT_SCHEME;
3150
+ return void 0;
3151
+ }
3152
+ function offersX402CreditFlavour(menu, network, flavour) {
3153
+ return matchesNetwork(
3154
+ flavour === X402_BATCH_SETTLEMENT_SCHEME ? x402CreditChannelNetworks(menu) : x402CreditExactNetworks(menu),
3155
+ network
3156
+ );
3157
+ }
3158
+ function x402InstrumentForFlavour(flavour) {
3159
+ return flavour === X402_BATCH_SETTLEMENT_SCHEME ? "x402_channel" : "x402_exact";
3160
+ }
3161
+ function x402FlavourForInstrument(instrument) {
3162
+ if (instrument === "x402_channel") return X402_BATCH_SETTLEMENT_SCHEME;
3163
+ if (instrument === "x402_exact") return X402_EXACT_SCHEME;
3164
+ return void 0;
3165
+ }
3166
+ function x402CreditChannelNetworks(menu) {
3167
+ return schemeNetworks(menu, X402_BATCH_SETTLEMENT_SCHEME);
3168
+ }
3169
+ function x402CreditExactNetworks(menu) {
3170
+ return schemeNetworks(menu, X402_EXACT_SCHEME);
3171
+ }
3172
+ function x402CreditFundingNetworks(menu) {
3173
+ return [.../* @__PURE__ */ new Set([...x402CreditChannelNetworks(menu), ...x402CreditExactNetworks(menu)])];
3174
+ }
3175
+ function schemeNetworks(menu, scheme) {
3176
+ return (menu?.x402?.schemes ?? []).filter((entry) => entry.scheme === scheme).map((entry) => entry.network);
1745
3177
  }
1746
3178
  function matchesNetwork(networks, network) {
1747
3179
  return networks.some((candidate) => network === void 0 || candidate === network);
@@ -1755,7 +3187,9 @@ var TIER2_MIN_RECEIPTS = 15;
1755
3187
  var TIER2_MIN_SPAN_MS = 7 * 24 * 60 * 60 * 1e3;
1756
3188
  var OPEN_TARGET_JOBS = 20;
1757
3189
  var CASHU_REFILL_TARGET_JOBS = 40;
3190
+ var TOPUP_THRESHOLD_FRACTION = 0.25;
1758
3191
  var DEFAULT_CREDIT_POSTURE = "suggest";
3192
+ var POCKET_TARGET_MICRO = 5e6;
1759
3193
  function trustTierFor(args) {
1760
3194
  const ladder = trustLadderFor(args);
1761
3195
  return { tier: ladder.tier, capMicro: ladder.capMicro ?? args.menu.max_micro };
@@ -1824,6 +3258,18 @@ function effectiveFundingRail(config, endpoint, identity) {
1824
3258
  return { rail: global, source: "global", ...conflict ? { conflict } : {} };
1825
3259
  return { source: "default", ...conflict ? { conflict } : {} };
1826
3260
  }
3261
+ function automaticCreditBoundRail(stored) {
3262
+ if (stored?.status !== "active" || stored.reconciliationPending) return void 0;
3263
+ return isCreditFundRail(stored.rail) ? stored.rail : void 0;
3264
+ }
3265
+ function trustOverrideCapMicro(args) {
3266
+ const raw2 = args.builderPubkey ? lookupOverride(args.config?.trustOverrides, args.builderPubkey) : void 0;
3267
+ if (!raw2) return void 0;
3268
+ if (raw2.trim().toLowerCase() === "builder-max") {
3269
+ return args.menu?.max_micro ?? Number.POSITIVE_INFINITY;
3270
+ }
3271
+ return parseCapMicro(raw2);
3272
+ }
1827
3273
  function tierCapOverrides(config) {
1828
3274
  const out = {};
1829
3275
  const tier0 = parseCapMicro(config?.tier0Cap);
@@ -1973,6 +3419,81 @@ function planCreditForQuote(args) {
1973
3419
  railReason: openRoute.reason
1974
3420
  };
1975
3421
  }
3422
+ function planProbeDraw(args) {
3423
+ const now = args.now ?? Date.now();
3424
+ const implicit = [...args.implicit ?? []].filter(
3425
+ (credit) => credit.status === "active" && credit.remainingMicro > 0 && credit.expiryMs > now
3426
+ ).sort((a, b) => a.expiryMs - b.expiryMs).at(0);
3427
+ if (implicit) {
3428
+ if (effectiveCreditPosture(args.config, implicit.endpoint, args.identity).posture === "off") {
3429
+ return {
3430
+ mode: "none",
3431
+ drawSkipped: {
3432
+ reason: "posture_off",
3433
+ creditId: implicit.creditId,
3434
+ remainingMicro: implicit.remainingMicro
3435
+ }
3436
+ };
3437
+ }
3438
+ return {
3439
+ mode: "draw",
3440
+ creditId: implicit.creditId,
3441
+ drawId: randomUUID(),
3442
+ source: "implicit"
3443
+ };
3444
+ }
3445
+ const stored = args.stored;
3446
+ if (!stored) return { mode: "none" };
3447
+ const skip = (reason, extra) => ({
3448
+ mode: "none",
3449
+ drawSkipped: {
3450
+ reason,
3451
+ creditId: stored.creditId,
3452
+ remainingMicro: stored.remainingMicro,
3453
+ ...extra
3454
+ }
3455
+ });
3456
+ if (effectiveCreditPosture(args.config, stored.endpoint, args.identity).posture === "off") {
3457
+ return skip("posture_off");
3458
+ }
3459
+ if (stored.status !== "active") return skip("not_active");
3460
+ if (stored.reconciliationPending) return skip("reconciliation_pending");
3461
+ if (stored.remainingMicro <= 0 && !stored.provisional) return skip("exhausted");
3462
+ if (stored.expiryMs > 0 && stored.expiryMs <= now) return skip("expired");
3463
+ if (stored.lastPriceMicro !== void 0 && args.btcRate > 0) {
3464
+ const ceilingMicro = msatsToMicro(args.drawCeilingMsats, args.btcRate);
3465
+ if (ceilingMicro > 0 && stored.lastPriceMicro > ceilingMicro) {
3466
+ return skip("price_above_ceiling", {
3467
+ ceilingMicro,
3468
+ lastPriceMicro: stored.lastPriceMicro,
3469
+ ceilingSource: args.drawCeilingSource
3470
+ });
3471
+ }
3472
+ }
3473
+ return { mode: "draw", creditId: stored.creditId, drawId: randomUUID(), source: "prepaid" };
3474
+ }
3475
+ function needsTopup(balanceMicro, targetMicro) {
3476
+ return targetMicro > 0 && balanceMicro < targetMicro * TOPUP_THRESHOLD_FRACTION;
3477
+ }
3478
+ function pocketTargetMicro(config) {
3479
+ return parseCapMicro(config?.pocketTarget) ?? POCKET_TARGET_MICRO;
3480
+ }
3481
+ function needsPocketRefill(balanceMicro, targetMicro) {
3482
+ return needsTopup(balanceMicro, targetMicro);
3483
+ }
3484
+ function fundWireAmountMicro(priceMicroServer, fundMsats, requiredMsats) {
3485
+ if (fundMsats === requiredMsats) return priceMicroServer;
3486
+ return Math.round(priceMicroServer * fundMsats / requiredMsats);
3487
+ }
3488
+ function readFundValuation(fields) {
3489
+ const structured = usableMicro(fields.expectedMicro);
3490
+ if (structured !== void 0) return { kind: "figure", micro: structured };
3491
+ const match = /quoted price \((\d+)\)/.exec(fields.message ?? "");
3492
+ const stated = match ? usableMicro(Number(match[1])) : void 0;
3493
+ if (stated !== void 0) return { kind: "figure", micro: stated };
3494
+ const providerStatedSomething = fields.expectedMicro !== void 0 || match !== null;
3495
+ return providerStatedSomething ? { kind: "malformed" } : { kind: "absent" };
3496
+ }
1976
3497
  function msatsToMicro(msats, btcRate) {
1977
3498
  return Math.round(msatsToUsd(msats, btcRate) * 1e6);
1978
3499
  }
@@ -1988,6 +3509,9 @@ function perDvmEntriesFor(config, endpoint, identity) {
1988
3509
  );
1989
3510
  }
1990
3511
  var POSTURE_STRICTNESS = { auto: 0, suggest: 1, off: 2 };
3512
+ function usableMicro(value) {
3513
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
3514
+ }
1991
3515
  function fundAmounts(fundMicro, btcRate) {
1992
3516
  const fundSats = microToSats(fundMicro, btcRate);
1993
3517
  return { fundMicro, fundSats, fundMsats: fundSats * 1e3 };
@@ -2024,6 +3548,14 @@ function targetJobsFor(config, endpoint, identity) {
2024
3548
  if (targets.length > 0) return Math.min(...targets);
2025
3549
  return config?.targetJobs;
2026
3550
  }
3551
+ function lookupOverride(overrides, builderPubkey) {
3552
+ if (!overrides) return void 0;
3553
+ const key = builderPubkey.trim().toLowerCase();
3554
+ for (const [pubkey, cap] of Object.entries(overrides)) {
3555
+ if (pubkey.trim().toLowerCase() === key) return cap;
3556
+ }
3557
+ return void 0;
3558
+ }
2027
3559
  function parseCapMicro(cap) {
2028
3560
  if (!cap) return void 0;
2029
3561
  const match = /^\$?(\d+(?:\.\d{1,6})?)$/.exec(cap.trim());
@@ -2684,6 +4216,32 @@ async function commitAgentSpend(args) {
2684
4216
  saveAgentWallet(next);
2685
4217
  });
2686
4218
  }
4219
+ async function correctPendingFundAmount(args) {
4220
+ await withWalletLock(() => {
4221
+ const wallet = loadAgentWallet();
4222
+ if (!wallet) return;
4223
+ const next = {
4224
+ ...wallet,
4225
+ pending_submissions: wallet.pending_submissions.map((s) => {
4226
+ if (s.request_id !== args.requestId || !s.context) return s;
4227
+ const data = s.context.submit_body.data;
4228
+ const fund = data?.fund;
4229
+ if (!fund) return s;
4230
+ return {
4231
+ ...s,
4232
+ context: {
4233
+ ...s.context,
4234
+ submit_body: {
4235
+ ...s.context.submit_body,
4236
+ data: { ...data, fund: { ...fund, amount_micro: args.amountMicro } }
4237
+ }
4238
+ }
4239
+ };
4240
+ })
4241
+ };
4242
+ saveAgentWallet(next);
4243
+ });
4244
+ }
2687
4245
  async function abandonPendingSubmission(args) {
2688
4246
  await withWalletLock(() => {
2689
4247
  const wallet = loadAgentWallet();
@@ -2825,20 +4383,1085 @@ function minOvershootSubset(proofs, amountSats) {
2825
4383
  reachable.set(next, [...indices2, i]);
2826
4384
  }
2827
4385
  }
2828
- let bestTarget = -1;
2829
- for (const t of reachable.keys()) {
2830
- if (t >= amountSats && (bestTarget === -1 || t < bestTarget)) bestTarget = t;
4386
+ let bestTarget = -1;
4387
+ for (const t of reachable.keys()) {
4388
+ if (t >= amountSats && (bestTarget === -1 || t < bestTarget)) bestTarget = t;
4389
+ }
4390
+ if (bestTarget === -1) return null;
4391
+ const indices = reachable.get(bestTarget) ?? [];
4392
+ return { selected: indices.map((i) => proofs[i]), totalSats: bestTarget };
4393
+ }
4394
+
4395
+ // src/lib/wallet-agent/recovery-submissions.ts
4396
+ var RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4397
+ var RESUME_MAX_ATTEMPTS = 5;
4398
+ async function sweepPendingSubmissions(opts = {}) {
4399
+ const wallet = loadAgentWallet();
4400
+ if (!wallet || wallet.pending_submissions.length === 0) {
4401
+ return { outcomes: [] };
4402
+ }
4403
+ const deps = {
4404
+ httpPost: opts.httpPost ?? createDefaultHttpPost(opts.fetchImpl ?? fetch),
4405
+ getWalletForMint: opts.getWalletForMint ?? getWallet,
4406
+ readMnemonic: opts.readMnemonic ?? readAgentMnemonic,
4407
+ lockPubkey: wallet.lock_pubkey
4408
+ };
4409
+ const now = opts.now ?? (() => Date.now());
4410
+ const outcomes = [];
4411
+ for (const entry of wallet.pending_submissions) {
4412
+ outcomes.push(await resolveEntry(entry, deps, now()));
4413
+ }
4414
+ if (outcomes.some(mutatesWallet)) {
4415
+ await withWalletLock(() => {
4416
+ const fresh = loadAgentWallet() ?? wallet;
4417
+ const next = outcomes.reduce(
4418
+ (acc, outcome) => applyOutcome3(acc, outcome, now),
4419
+ fresh
4420
+ );
4421
+ saveAgentWallet(next);
4422
+ });
4423
+ }
4424
+ return { outcomes };
4425
+ }
4426
+ async function recoverPendingSubmission(opts) {
4427
+ if (!UUID_V4_RE.test(opts.requestId))
4428
+ throw new DvmError("invalid_request_id", "The pending-submission request id must be a UUIDv4.");
4429
+ if (!SHA256_RE.test(opts.bindingFingerprint))
4430
+ throw new DvmError(
4431
+ "invalid_submission_binding",
4432
+ "The pending-submission binding fingerprint must be lowercase SHA-256 hex."
4433
+ );
4434
+ return withWalletLock(async () => {
4435
+ const wallet = loadAgentWallet();
4436
+ if (!wallet)
4437
+ throw new DvmError("wallet_missing", "No agent wallet found.", "Run: dvm wallet init");
4438
+ const matches = wallet.pending_submissions.filter(
4439
+ (entry2) => entry2.request_id === opts.requestId
4440
+ );
4441
+ if (matches.length !== 1)
4442
+ throw new DvmError(
4443
+ "submission_not_found",
4444
+ `No unique pending submission exists for request ${opts.requestId}.`,
4445
+ "Inspect the journal with: dvm wallet submissions"
4446
+ );
4447
+ const entry = matches[0];
4448
+ const binding = entry.context && cashuSubmissionBindingFromContext(entry.context);
4449
+ const recomputed = binding ? cashuSubmissionBindingFingerprint(binding) : null;
4450
+ if (entry.context?.binding_fingerprint !== opts.bindingFingerprint || recomputed !== opts.bindingFingerprint)
4451
+ throw new DvmError(
4452
+ "submission_binding_mismatch",
4453
+ "The saved submission does not match the approved strict request binding.",
4454
+ "Do not recover by endpoint, mint, or amount. Rebuild evidence for the exact saved request."
4455
+ );
4456
+ if (entry.output_proofs.length === 0 || entry.swapping)
4457
+ throw new DvmError(
4458
+ "submission_not_ready",
4459
+ "The exact submission has no completed payment output to recover.",
4460
+ "Use read-only NUT-09/NUT-07 reconciliation for the interrupted swap."
4461
+ );
4462
+ if ((entry.attempts ?? 0) !== 0)
4463
+ throw new DvmError(
4464
+ "submission_recovery_exhausted",
4465
+ "The exact strict submission already consumed its one recovery attempt.",
4466
+ "Use read-only receipt and job lookup; do not re-submit the payment."
4467
+ );
4468
+ const now = opts.now ?? (() => Date.now());
4469
+ const attempted = {
4470
+ ...entry,
4471
+ attempts: 1,
4472
+ last_attempt_at: new Date(now()).toISOString()
4473
+ };
4474
+ saveAgentWallet({
4475
+ ...wallet,
4476
+ pending_submissions: wallet.pending_submissions.map(
4477
+ (candidate) => candidate.request_id === opts.requestId ? attempted : candidate
4478
+ )
4479
+ });
4480
+ const outcome = await shipEntry(
4481
+ attempted,
4482
+ opts.httpPost ?? createDefaultHttpPost(opts.fetchImpl ?? fetch)
4483
+ );
4484
+ if (outcome.kind === "shipped" || outcome.kind === "already_accepted") {
4485
+ const current = loadAgentWallet() ?? wallet;
4486
+ saveAgentWallet(applyOutcome3(current, outcome, now));
4487
+ return {
4488
+ requestId: opts.requestId,
4489
+ outcome: outcome.kind,
4490
+ jobId: outcome.kind === "shipped" ? outcome.jobId ?? null : null,
4491
+ attemptCount: 1
4492
+ };
4493
+ }
4494
+ return { requestId: opts.requestId, outcome: "pending", jobId: null, attemptCount: 1 };
4495
+ });
4496
+ }
4497
+ function isResumable(entry) {
4498
+ return resumeBody(entry) !== null;
4499
+ }
4500
+ function reclaimAtMs(entry) {
4501
+ if (entry.output_proofs.length === 0) return null;
4502
+ if (!entry.canonical_id || typeof entry.t_expire !== "number") return null;
4503
+ return entry.t_expire;
4504
+ }
4505
+ async function resolveEntry(entry, deps, nowMs) {
4506
+ if (entry.output_proofs.length === 0) {
4507
+ const inFlightWindowMs = inFlightTimeoutSeconds() * 1e3;
4508
+ if (entry.swapping) {
4509
+ return ageMs(entry, nowMs) >= inFlightWindowMs ? recoverPreparedSwap(entry, deps) : { kind: "no_swap", entry };
4510
+ }
4511
+ return ageMs(entry, nowMs) >= inFlightWindowMs ? { kind: "pruned", entry } : { kind: "no_swap", entry };
4512
+ }
4513
+ const reclaimAt = reclaimAtMs(entry);
4514
+ if (reclaimAt !== null && nowMs >= reclaimAt) {
4515
+ return reclaimEntry(entry, deps);
4516
+ }
4517
+ if (!isResumable(entry)) {
4518
+ return { kind: "unresumable", entry };
4519
+ }
4520
+ if (ageMs(entry, nowMs) > RESUME_MAX_AGE_MS) {
4521
+ return { kind: "stale", entry, reason: "older than the resume window" };
4522
+ }
4523
+ if ((entry.attempts ?? 0) >= RESUME_MAX_ATTEMPTS) {
4524
+ return { kind: "stale", entry, reason: `no response after ${RESUME_MAX_ATTEMPTS} attempts` };
4525
+ }
4526
+ return shipEntry(entry, deps.httpPost);
4527
+ }
4528
+ async function recoverPreparedSwap(entry, deps) {
4529
+ const swapping = entry.swapping;
4530
+ if (!swapping) return { kind: "pruned", entry };
4531
+ let cashuWallet;
4532
+ try {
4533
+ cashuWallet = await deps.getWalletForMint(entry.mint);
4534
+ } catch (err) {
4535
+ return { kind: "swap_recovery_pending", entry, reason: errorText2(err) };
4536
+ }
4537
+ let sendData;
4538
+ let keepData;
4539
+ try {
4540
+ sendData = rehydrateOutputData(swapping.send_outputs);
4541
+ keepData = rehydrateOutputData(swapping.keep_outputs);
4542
+ } catch (err) {
4543
+ return { kind: "swap_recovery_pending", entry, reason: errorText2(err) };
4544
+ }
4545
+ const allData = [...sendData, ...keepData];
4546
+ let restoreError;
4547
+ try {
4548
+ const restored = await cashuWallet.mint.restore({
4549
+ outputs: allData.map((data) => data.blindedMessage)
4550
+ });
4551
+ if (restored.outputs.length !== restored.signatures.length) {
4552
+ return {
4553
+ kind: "swap_recovery_pending",
4554
+ entry,
4555
+ reason: "mint returned mismatched NUT-09 outputs and signatures"
4556
+ };
4557
+ }
4558
+ const signaturesByOutput = new Map(
4559
+ restored.outputs.map((output, index) => [output.B_, restored.signatures[index]])
4560
+ );
4561
+ if (allData.length > 0 && allData.every((data) => signaturesByOutput.has(data.blindedMessage.B_))) {
4562
+ const restoreGroup = (data) => data.map((output) => {
4563
+ const signature = signaturesByOutput.get(output.blindedMessage.B_);
4564
+ if (!signature) throw new Error("mint omitted a restored output");
4565
+ if (signature.id !== swapping.keyset_id) {
4566
+ throw new Error("mint restored an output under an unexpected keyset");
4567
+ }
4568
+ return output.toProof(signature, cashuWallet.keyChain.getKeyset(swapping.keyset_id));
4569
+ });
4570
+ const send = restoreGroup(sendData);
4571
+ const keep = restoreGroup(keepData);
4572
+ const cashuToken = getEncodedToken2({ mint: entry.mint, proofs: send });
4573
+ return {
4574
+ kind: "swap_restored",
4575
+ entry,
4576
+ outputs: send.map((proof) => toAgentProof(proof, entry.mint, entry.intended_lock_pubkey)),
4577
+ keep: keep.map((proof) => toAgentProof(proof, entry.mint, deps.lockPubkey)),
4578
+ cashuToken
4579
+ };
4580
+ }
4581
+ if (restored.outputs.length > 0) {
4582
+ return {
4583
+ kind: "swap_recovery_pending",
4584
+ entry,
4585
+ reason: "mint restored only part of the interrupted swap"
4586
+ };
4587
+ }
4588
+ } catch (err) {
4589
+ restoreError = errorText2(err);
4590
+ }
4591
+ try {
4592
+ const states = await cashuWallet.checkProofsStates(toCashuProofs(swapping.input_proofs));
4593
+ if (states.length === swapping.input_proofs.length && states.every((state) => state.state === CheckStateEnum.UNSPENT)) {
4594
+ return { kind: "swap_reverted", entry };
4595
+ }
4596
+ return {
4597
+ kind: "swap_recovery_pending",
4598
+ entry,
4599
+ reason: restoreError ? `NUT-09 restore failed (${restoreError}); mint still reports the interrupted swap inputs spent or pending` : "mint still reports the interrupted swap inputs spent or pending"
4600
+ };
4601
+ } catch (err) {
4602
+ return { kind: "swap_recovery_pending", entry, reason: errorText2(err) };
4603
+ }
4604
+ }
4605
+ async function reclaimEntry(entry, deps) {
4606
+ const skipped = (reason, terminal = false) => ({
4607
+ kind: "reclaim_skipped",
4608
+ entry,
4609
+ reason,
4610
+ terminal
4611
+ });
4612
+ const mnemonic = deps.readMnemonic();
4613
+ if (!mnemonic) {
4614
+ return skipped(
4615
+ "no mnemonic at ~/.dvm/wallet.mnemonic, so the refund key can't be derived \u2014 restore it with `dvm wallet recover`"
4616
+ );
4617
+ }
4618
+ let refundPrivkey;
4619
+ try {
4620
+ const refund = deriveAgentRefundSubkey(mnemonic, entry.canonical_id ?? "");
4621
+ if (entry.refund_pubkey && entry.refund_pubkey !== refund.pubkey) {
4622
+ return skipped(
4623
+ "this wallet's mnemonic does not derive the refund key the payment was locked to"
4624
+ );
4625
+ }
4626
+ refundPrivkey = refund.privkey;
4627
+ } catch (err) {
4628
+ return skipped(errorText2(err));
4629
+ }
4630
+ let cashuWallet;
4631
+ try {
4632
+ cashuWallet = await deps.getWalletForMint(entry.mint);
4633
+ } catch (err) {
4634
+ return skipped(errorText2(err));
4635
+ }
4636
+ let grouped;
4637
+ try {
4638
+ grouped = await cashuWallet.groupProofsByState(toCashuProofs(entry.output_proofs));
4639
+ } catch (err) {
4640
+ return skipped(errorText2(err));
4641
+ }
4642
+ const spentSecrets = new Set(grouped.spent.map((p) => p.secret));
4643
+ const pendingSecrets = new Set(grouped.pending.map((p) => p.secret));
4644
+ const spent = entry.output_proofs.filter((p) => spentSecrets.has(p.secret));
4645
+ const pending = entry.output_proofs.filter((p) => pendingSecrets.has(p.secret));
4646
+ const unspent = entry.output_proofs.filter(
4647
+ (p) => !spentSecrets.has(p.secret) && !pendingSecrets.has(p.secret)
4648
+ );
4649
+ if (unspent.length === 0) {
4650
+ return pending.length > 0 ? skipped("the mint still reports these proofs in flight") : { kind: "already_accepted", entry };
4651
+ }
4652
+ if (pending.length > 0) {
4653
+ return skipped("the mint still reports some of these proofs in flight");
4654
+ }
4655
+ const totalSats = unspent.reduce((sum, p) => sum + p.amount, 0);
4656
+ const inputs = toCashuProofs(unspent);
4657
+ let outputs;
4658
+ try {
4659
+ const swapFeeSats = swapFeeForProofs(cashuWallet, inputs);
4660
+ const allowance = reclaimFeeAllowance(entry);
4661
+ if (!allowance.ok) {
4662
+ return skipped(allowance.reason, true);
4663
+ }
4664
+ if (allowance.remainingSats !== null && swapFeeSats > allowance.remainingSats) {
4665
+ return skipped(
4666
+ `the initial swap used ${allowance.initialSats} sats of this job's ${allowance.maxSats}-sat Cashu fee ceiling, leaving a ${allowance.remainingSats}-sat allowance; the mint's ${swapFeeSats}-sat recovery swap fee exceeds that remainder`,
4667
+ true
4668
+ );
4669
+ }
4670
+ const reclaimSats = totalSats - swapFeeSats;
4671
+ if (reclaimSats <= 0) {
4672
+ return skipped(
4673
+ `worth ${totalSats} sats, which does not cover the mint's ${swapFeeSats}-sat swap fee`,
4674
+ true
4675
+ );
4676
+ }
4677
+ const result = await cashuWallet.ops.send(reclaimSats, inputs).privkey(refundPrivkey).asP2PK({ pubkey: deps.lockPubkey }).run();
4678
+ outputs = result.send;
4679
+ } catch (err) {
4680
+ const diagnostic = missingDleqRecoveryDiagnostic(inputs, err);
4681
+ return skipped(diagnostic ?? errorText2(err), diagnostic !== null);
4682
+ }
4683
+ return {
4684
+ kind: "reclaimed",
4685
+ entry,
4686
+ outputs: outputs.map((p) => toAgentProof(p, entry.mint, deps.lockPubkey)),
4687
+ reclaimed_sats: outputs.reduce((sum, p) => sum + Amount6.from(p.amount).toNumber(), 0),
4688
+ spent_sats: spent.reduce((sum, p) => sum + p.amount, 0)
4689
+ };
4690
+ }
4691
+ function reclaimFeeAllowance(entry) {
4692
+ const maxSats = entry.max_cashu_fee_sats;
4693
+ const contextMaxSats = entry.context?.max_cashu_fee_sats;
4694
+ const initialSats = entry.initial_cashu_fee_sats;
4695
+ const contextInitialSats = entry.context?.initial_cashu_fee_sats;
4696
+ if (maxSats === void 0) {
4697
+ if (contextMaxSats !== void 0) {
4698
+ return {
4699
+ ok: false,
4700
+ reason: "the saved submission's Cashu fee ceiling is missing from its money journal"
4701
+ };
4702
+ }
4703
+ if (initialSats !== void 0 && !isNonNegativeSafeInteger(initialSats) || contextInitialSats !== void 0 && !isNonNegativeSafeInteger(contextInitialSats) || initialSats !== void 0 && contextInitialSats !== void 0 && initialSats !== contextInitialSats) {
4704
+ return {
4705
+ ok: false,
4706
+ reason: "the saved submission's initial Cashu fee accounting is invalid"
4707
+ };
4708
+ }
4709
+ return {
4710
+ ok: true,
4711
+ maxSats: null,
4712
+ initialSats: initialSats ?? contextInitialSats ?? 0,
4713
+ remainingSats: null
4714
+ };
4715
+ }
4716
+ if (!isNonNegativeSafeInteger(maxSats)) {
4717
+ return { ok: false, reason: "the saved submission's Cashu fee ceiling is invalid" };
4718
+ }
4719
+ if (!isNonNegativeSafeInteger(initialSats)) {
4720
+ return {
4721
+ ok: false,
4722
+ reason: "the capped saved submission is missing valid initial Cashu fee accounting"
4723
+ };
4724
+ }
4725
+ if (entry.context && (contextMaxSats !== maxSats || contextInitialSats !== initialSats)) {
4726
+ return {
4727
+ ok: false,
4728
+ reason: "the saved submission's Cashu fee accounting disagrees with its request context"
4729
+ };
4730
+ }
4731
+ if (initialSats > maxSats) {
4732
+ return {
4733
+ ok: false,
4734
+ reason: "the saved submission's initial Cashu fee exceeds its job-wide ceiling"
4735
+ };
4736
+ }
4737
+ return {
4738
+ ok: true,
4739
+ maxSats,
4740
+ initialSats,
4741
+ remainingSats: maxSats - initialSats
4742
+ };
4743
+ }
4744
+ function isNonNegativeSafeInteger(value) {
4745
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
4746
+ }
4747
+ var UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4748
+ var SHA256_RE = /^[a-f0-9]{64}$/;
4749
+ async function shipEntry(entry, httpPost) {
4750
+ const token = { mint: entry.mint, proofs: toCashuProofs(entry.output_proofs) };
4751
+ const cashuToken = entry.cashu_token ?? getEncodedToken2(token);
4752
+ const headers = {
4753
+ "Content-Type": "application/json",
4754
+ "X-Cashu": cashuToken,
4755
+ "X-Cashu-Request-Id": entry.request_id
4756
+ };
4757
+ const route = entry.context?.route === "credit" ? "/v1/credit" : "/v1/job";
4758
+ let active = entry;
4759
+ let correctedOnce = false;
4760
+ for (; ; ) {
4761
+ const body = resumeBody(active);
4762
+ if (!body) return { kind: "unresumable", entry };
4763
+ let response;
4764
+ try {
4765
+ response = await httpPost({
4766
+ url: `${entry.endpoint}${route}`,
4767
+ headers,
4768
+ body: JSON.stringify(body)
4769
+ });
4770
+ } catch (err) {
4771
+ return {
4772
+ kind: "still_pending",
4773
+ entry,
4774
+ reason: `network error: ${err instanceof Error ? err.message : String(err)}`
4775
+ };
4776
+ }
4777
+ if (response.status >= 200 && response.status < 300) {
4778
+ if (entry.context?.route === "credit") {
4779
+ applyResumedCreditFund(entry, response.body);
4780
+ return { kind: "shipped", entry };
4781
+ }
4782
+ const jobId = typeof response.body?.job_id === "string" ? response.body.job_id : void 0;
4783
+ if (jobId && entry.context) {
4784
+ saveJob(jobRecordFromEntry(entry, jobId, response.body, response.feedbackClaim));
4785
+ }
4786
+ await reconcileResumedJobCredit(active, response.body, httpPost);
4787
+ return { kind: "shipped", entry, jobId };
4788
+ }
4789
+ if (response.status === 409 || response.body?.error === "replay_rejected") {
4790
+ await reconcileResumedJobCredit(active, response.body, httpPost);
4791
+ return { kind: "already_accepted", entry };
4792
+ }
4793
+ const errCode = typeof response.body?.error === "string" ? response.body.error : null;
4794
+ if (errCode === "funding_commitment_mismatch") {
4795
+ if (!correctedOnce) {
4796
+ const correction = await correctFundAmount(active, response.body);
4797
+ if (correction.kind === "corrected") {
4798
+ correctedOnce = true;
4799
+ active = correction.entry;
4800
+ continue;
4801
+ }
4802
+ if (correction.kind === "permanent") {
4803
+ return { kind: "abandoned", entry, reason: errCode };
4804
+ }
4805
+ }
4806
+ return { kind: "still_pending", entry, reason: errCode };
4807
+ }
4808
+ if (errCode === "wrong_lock_pubkey" || errCode === "not_p2pk_locked" || errCode === "payment_invalid" || errCode === "proof_not_unspent" || // Credit-route permanent refusals (internal-review): the fund can never land
4809
+ // under this signed body.
4810
+ errCode === "credit_id_unavailable" || errCode === "caller_mismatch" || errCode === "credit_not_supported") {
4811
+ return { kind: "abandoned", entry, reason: errCode };
4812
+ }
4813
+ return {
4814
+ kind: "still_pending",
4815
+ entry,
4816
+ reason: errCode ?? `http ${response.status}`
4817
+ };
4818
+ }
4819
+ }
4820
+ async function correctFundAmount(entry, body) {
4821
+ const ctx = entry.context;
4822
+ if (!ctx || ctx.route === "credit") return { kind: "permanent" };
4823
+ const data = ctx.submit_body.data;
4824
+ const fund = data?.fund;
4825
+ if (!data || !fund) return { kind: "permanent" };
4826
+ const valuation = readFundValuation({
4827
+ expectedMicro: body?.expected_micro,
4828
+ message: typeof body?.message === "string" ? body.message : void 0
4829
+ });
4830
+ if (valuation.kind === "absent") return { kind: "permanent" };
4831
+ if (valuation.kind === "malformed") return { kind: "hold" };
4832
+ const expected = valuation.micro;
4833
+ if (expected === fund.amount_micro) return { kind: "hold" };
4834
+ await correctPendingFundAmount({ requestId: entry.request_id, amountMicro: expected });
4835
+ return {
4836
+ kind: "corrected",
4837
+ entry: {
4838
+ ...entry,
4839
+ context: {
4840
+ ...ctx,
4841
+ submit_body: {
4842
+ ...ctx.submit_body,
4843
+ data: { ...data, fund: { ...fund, amount_micro: expected } }
4844
+ }
4845
+ }
4846
+ }
4847
+ };
4848
+ }
4849
+ function resumeBody(entry) {
4850
+ const ctx = entry.context;
4851
+ if (!ctx) return null;
4852
+ if (ctx.route === "credit") {
4853
+ const domain2 = ctx.auth_domain ?? testOnlySignedRequestDomain();
4854
+ if (!ctx.credit_body || !ctx.signing_pubkey || !domain2) return null;
4855
+ const signer2 = findSigningIdentityByPubkey(ctx.signing_pubkey);
4856
+ if (!signer2) return null;
4857
+ return signDvmRequestData(signer2.privkey, ctx.credit_body, domain2);
4858
+ }
4859
+ if (!ctx.signing_pubkey) return ctx.submit_body;
4860
+ const domain = ctx.auth_domain ?? testOnlySignedRequestDomain();
4861
+ if (!domain) return null;
4862
+ const signer = findSigningIdentityByPubkey(ctx.signing_pubkey);
4863
+ if (!signer) return null;
4864
+ const unsigned = ctx.submit_body.data ?? {};
4865
+ return { ...ctx.submit_body, data: signDvmRequestData(signer.privkey, unsigned, domain) };
4866
+ }
4867
+ function applyResumedCreditFund(entry, body) {
4868
+ const pubkey = entry.context?.signing_pubkey;
4869
+ const credit = isCreditWire(body?.credit) ? body.credit : void 0;
4870
+ const rawReceipt = body?.funding_receipt;
4871
+ const receipt = isFundingReceipt(rawReceipt) ? rawReceipt : void 0;
4872
+ if (!pubkey || !credit) return false;
4873
+ const ctx = entry.context;
4874
+ const request = fundingRequest(entry);
4875
+ if (!ctx || !request) return false;
4876
+ const mutableReplay = ctx.route === "credit" && body?.replayed === true;
4877
+ const verification = captureFundingReceipt({
4878
+ receipt,
4879
+ ...rawReceipt !== void 0 && !receipt ? { parseError: "shape_invalid" } : {},
4880
+ endpoint: entry.endpoint,
4881
+ ...ctx.builder ? { builder: ctx.builder } : {},
4882
+ expectedCreditId: credit.credit_id,
4883
+ expectedFundId: request.fundId,
4884
+ expectedFundedMicro: typeof body?.funded_micro === "number" ? body.funded_micro : request.amountMicro,
4885
+ ...mutableReplay ? {} : {
4886
+ expectedBalanceAfterMicro: credit.remaining_micro,
4887
+ expectedLedgerSeq: credit.ledger_seq
4888
+ },
4889
+ expectedCallerPubkey: pubkey
4890
+ });
4891
+ const sequenced = !mutableReplay && (verification === "verified" || verification === "verified_unattested");
4892
+ try {
4893
+ const stored = loadCredit(entry.endpoint, pubkey);
4894
+ recordFundResponse({
4895
+ endpoint: entry.endpoint,
4896
+ callerPubkey: pubkey,
4897
+ credit,
4898
+ targetMicro: stored?.targetMicro ?? credit.remaining_micro,
4899
+ sequenced
4900
+ });
4901
+ } catch {
4902
+ return false;
4903
+ }
4904
+ return true;
4905
+ }
4906
+ function fundingRequest(entry) {
4907
+ const ctx = entry.context;
4908
+ if (!ctx) return void 0;
4909
+ const data = ctx.route === "credit" ? ctx.credit_body : ctx.submit_body.data;
4910
+ const fund = data?.fund;
4911
+ if (!fund) return void 0;
4912
+ const fundId = ctx.route === "credit" ? fund.fund_id : entry.request_id;
4913
+ if (typeof fundId !== "string") return void 0;
4914
+ return {
4915
+ fundId,
4916
+ ...typeof fund.amount_micro === "number" ? { amountMicro: fund.amount_micro } : {}
4917
+ };
4918
+ }
4919
+ async function reconcileResumedJobCredit(entry, body, httpPost) {
4920
+ const ctx = entry.context;
4921
+ if (!ctx || ctx.route === "credit") return;
4922
+ const data = ctx.submit_body.data;
4923
+ if (!data || !data.fund && typeof data.credit_id !== "string") return;
4924
+ if (applyResumedCreditFund(entry, body)) return;
4925
+ const pubkey = ctx.signing_pubkey;
4926
+ if (!pubkey) return;
4927
+ try {
4928
+ if (loadCredit(entry.endpoint, pubkey)?.provisional !== true) return;
4929
+ } catch {
4930
+ return;
4931
+ }
4932
+ await readResumedCreditBalance(entry, httpPost).catch(() => void 0);
4933
+ }
4934
+ async function readResumedCreditBalance(entry, httpPost) {
4935
+ const pubkey = entry.context?.signing_pubkey;
4936
+ if (!pubkey) return;
4937
+ const signer = findSigningIdentityByPubkey(pubkey);
4938
+ const storedDomain = entry.context?.auth_domain ?? testOnlySignedRequestDomain();
4939
+ if (!signer || !storedDomain) return;
4940
+ const domain = creditOperationDomain(storedDomain);
4941
+ if (!domain) return;
4942
+ const response = await httpPost({
4943
+ url: `${entry.endpoint}/v1/credit`,
4944
+ headers: { "Content-Type": "application/json" },
4945
+ body: JSON.stringify(signDvmRequestData(signer.privkey, { op: "balance" }, domain))
4946
+ });
4947
+ if (response.status < 200 || response.status >= 300) return;
4948
+ const credits = (Array.isArray(response.body?.credits) ? response.body.credits : []).filter(
4949
+ isCreditWire
4950
+ );
4951
+ const primary = isCreditWire(response.body?.credit) ? response.body.credit : void 0;
4952
+ if (!primary && credits.length === 0) return;
4953
+ recordBalanceResponse({
4954
+ endpoint: entry.endpoint,
4955
+ callerPubkey: pubkey,
4956
+ credits,
4957
+ ...primary ? { primary } : {},
4958
+ sequenced: false
4959
+ });
4960
+ }
4961
+ function creditOperationDomain(domain) {
4962
+ const jobSuffix = "/v1/job";
4963
+ if (!domain.path.endsWith(jobSuffix)) return null;
4964
+ const mount = domain.path.slice(0, -jobSuffix.length);
4965
+ return {
4966
+ audience: domain.audience,
4967
+ method: "POST",
4968
+ path: `${mount}/v1/credit`,
4969
+ capability: null
4970
+ };
4971
+ }
4972
+ function isCreditWire(value) {
4973
+ if (typeof value !== "object" || value === null) return false;
4974
+ const credit = value;
4975
+ return typeof credit.credit_id === "string" && typeof credit.remaining_micro === "number";
4976
+ }
4977
+ function jobRecordFromEntry(entry, jobId, body, feedbackClaim) {
4978
+ const ctx = entry.context;
4979
+ return {
4980
+ id: jobId,
4981
+ endpoint: entry.endpoint,
4982
+ ...ctx?.dvm_id ? { dvmId: ctx.dvm_id } : {},
4983
+ ...feedbackClaim ? { feedbackClaim } : {},
4984
+ ...ctx?.capability ? { capability: ctx.capability } : {},
4985
+ status: "submitted",
4986
+ submittedAt: Date.parse(entry.created_at) || Date.now(),
4987
+ paymentMethod: "cashu",
4988
+ ...ctx?.pinned_mint ? { pinnedMint: ctx.pinned_mint } : {},
4989
+ ...ctx?.policy ? { policy: ctx.policy } : {},
4990
+ ...ctx?.strict_per_call ? { strictPerCall: true } : {},
4991
+ ...ctx?.max_cashu_fee_sats !== void 0 ? { maxCashuFeeSats: ctx.max_cashu_fee_sats } : {},
4992
+ // internal-review: the per-job token an anonymous caller needs on every later read.
4993
+ ...typeof body?.job_token === "string" ? { jobToken: body.job_token } : {}
4994
+ };
4995
+ }
4996
+ function mutatesWallet(outcome) {
4997
+ if (outcome.kind === "reclaim_skipped") {
4998
+ return outcome.entry.reclaim_error !== outcome.reason || (outcome.entry.reclaim_terminal ?? false) !== outcome.terminal;
4999
+ }
5000
+ return outcome.kind === "shipped" || outcome.kind === "already_accepted" || outcome.kind === "abandoned" || outcome.kind === "pruned" || outcome.kind === "swap_restored" || outcome.kind === "swap_reverted" || outcome.kind === "still_pending" || outcome.kind === "reclaimed";
5001
+ }
5002
+ function applyOutcome3(wallet, outcome, now) {
5003
+ const drop = () => ({
5004
+ ...wallet,
5005
+ pending_submissions: wallet.pending_submissions.filter(
5006
+ (s) => s.request_id !== outcome.entry.request_id
5007
+ )
5008
+ });
5009
+ if (outcome.kind === "shipped" || outcome.kind === "already_accepted") {
5010
+ const recentSpend = {
5011
+ request_id: outcome.entry.request_id,
5012
+ dvm_endpoint: outcome.entry.endpoint,
5013
+ mint: outcome.entry.mint,
5014
+ amount_sats: outcome.entry.amount_sats,
5015
+ signed_at_ms: now()
5016
+ };
5017
+ const dropped = drop();
5018
+ return {
5019
+ ...dropped,
5020
+ recent_spends: [...dropped.recent_spends, recentSpend].slice(-RECENT_SPENDS_CAP)
5021
+ };
5022
+ }
5023
+ if (outcome.kind === "abandoned" || outcome.kind === "pruned") {
5024
+ return drop();
5025
+ }
5026
+ if (outcome.kind === "swap_restored") {
5027
+ const swapping = outcome.entry.swapping;
5028
+ if (!swapping) return wallet;
5029
+ const trackedKeys = new Set(
5030
+ [...swapping.input_proofs, ...swapping.unselected_proofs].map((proof) => proofKey(proof))
5031
+ );
5032
+ const boundContext = bindCashuTokenToSubmissionContext(
5033
+ outcome.entry.context,
5034
+ outcome.cashuToken
5035
+ );
5036
+ return {
5037
+ ...wallet,
5038
+ proofs: [
5039
+ ...wallet.proofs.filter((proof) => !trackedKeys.has(proofKey(proof))),
5040
+ ...swapping.unselected_proofs.map((proof) => ({
5041
+ ...proof,
5042
+ status: "valid",
5043
+ in_flight_until_ms: void 0
5044
+ })),
5045
+ ...outcome.keep
5046
+ ],
5047
+ pending_submissions: wallet.pending_submissions.map(
5048
+ (submission) => submission.request_id === outcome.entry.request_id ? {
5049
+ ...submission,
5050
+ output_proofs: outcome.outputs,
5051
+ cashu_token: outcome.cashuToken,
5052
+ swapping: void 0,
5053
+ ...boundContext ? { context: boundContext } : {}
5054
+ } : submission
5055
+ )
5056
+ };
5057
+ }
5058
+ if (outcome.kind === "swap_reverted") {
5059
+ const swapping = outcome.entry.swapping;
5060
+ if (!swapping) return drop();
5061
+ const trackedKeys = new Set(
5062
+ [...swapping.input_proofs, ...swapping.unselected_proofs].map((proof) => proofKey(proof))
5063
+ );
5064
+ const reverted = revertInFlight(wallet, trackedKeys);
5065
+ return {
5066
+ ...reverted,
5067
+ pending_submissions: reverted.pending_submissions.filter(
5068
+ (submission) => submission.request_id !== outcome.entry.request_id
5069
+ )
5070
+ };
5071
+ }
5072
+ if (outcome.kind === "reclaimed") {
5073
+ const dropped = drop();
5074
+ const credited = { ...dropped, proofs: [...dropped.proofs, ...outcome.outputs] };
5075
+ if (outcome.spent_sats <= 0) return credited;
5076
+ const partialSpend = {
5077
+ request_id: outcome.entry.request_id,
5078
+ dvm_endpoint: outcome.entry.endpoint,
5079
+ mint: outcome.entry.mint,
5080
+ amount_sats: outcome.spent_sats,
5081
+ signed_at_ms: now()
5082
+ };
5083
+ return {
5084
+ ...credited,
5085
+ recent_spends: [...credited.recent_spends, partialSpend].slice(-RECENT_SPENDS_CAP)
5086
+ };
5087
+ }
5088
+ if (outcome.kind === "still_pending") {
5089
+ return {
5090
+ ...wallet,
5091
+ pending_submissions: wallet.pending_submissions.map(
5092
+ (s) => s.request_id === outcome.entry.request_id ? {
5093
+ ...s,
5094
+ attempts: (s.attempts ?? 0) + 1,
5095
+ last_attempt_at: new Date(now()).toISOString()
5096
+ } : s
5097
+ )
5098
+ };
5099
+ }
5100
+ if (outcome.kind === "reclaim_skipped") {
5101
+ return {
5102
+ ...wallet,
5103
+ pending_submissions: wallet.pending_submissions.map(
5104
+ (s) => s.request_id === outcome.entry.request_id ? { ...s, reclaim_error: outcome.reason, reclaim_terminal: outcome.terminal } : s
5105
+ )
5106
+ };
5107
+ }
5108
+ return wallet;
5109
+ }
5110
+ function ageMs(entry, nowMs) {
5111
+ const created = Date.parse(entry.created_at);
5112
+ if (Number.isNaN(created)) return Number.POSITIVE_INFINITY;
5113
+ return nowMs - created;
5114
+ }
5115
+ function errorText2(err) {
5116
+ return err instanceof Error ? err.message : String(err);
5117
+ }
5118
+ function createDefaultHttpPost(fetchImpl) {
5119
+ return async function defaultHttpPost(req) {
5120
+ let res;
5121
+ try {
5122
+ res = await fetchImpl(req.url, { method: "POST", headers: req.headers, body: req.body });
5123
+ } catch (err) {
5124
+ if (err instanceof DvmError) throw err;
5125
+ throw new DvmError(
5126
+ "provider_unreachable",
5127
+ `Could not reach ${req.url}: ${err instanceof Error ? err.message : String(err)}`,
5128
+ "Retry once the DVM is reachable again."
5129
+ );
5130
+ }
5131
+ let body;
5132
+ try {
5133
+ body = await res.json();
5134
+ } catch {
5135
+ body = null;
5136
+ }
5137
+ const feedbackClaim = res.headers.get(ROUTER_FEEDBACK_CLAIM_HEADER) ?? void 0;
5138
+ return { status: res.status, body, ...feedbackClaim ? { feedbackClaim } : {} };
5139
+ };
5140
+ }
5141
+
5142
+ // src/lib/wallet-agent/release.ts
5143
+ async function releaseAgentReserve(opts) {
5144
+ const cashu = opts.providerInfo.cashu;
5145
+ if (!cashu?.canonical_id) {
5146
+ throw new DvmError(
5147
+ "missing_canonical_id",
5148
+ "DVM advertises no `cashu.canonical_id` on /v1/info.",
5149
+ "Release needs canonical_id so the agent can derive the matching per-DVM refund subkey. Upgrade the builder SDK (internal-review+)."
5150
+ );
5151
+ }
5152
+ const outputMode = opts.outputMode ?? "locked";
5153
+ const initial = loadAgentWallet();
5154
+ if (!initial) {
5155
+ throw new DvmError("wallet_missing", "No agent wallet found.", "Run `dvm wallet init` first.");
5156
+ }
5157
+ const readMnemonic = opts.readMnemonic ?? readAgentMnemonic;
5158
+ const mnemonic = readMnemonic();
5159
+ if (!mnemonic) {
5160
+ throw new DvmError(
5161
+ "mnemonic_required",
5162
+ "Release requires a BIP-39 mnemonic at ~/.dvm/wallet.mnemonic.",
5163
+ "Restore the mnemonic with `dvm wallet recover --force --file <path>` (or pipe it via stdin) \u2014 it's the only way to derive the refund subkey privkey."
5164
+ );
5165
+ }
5166
+ const refund = deriveAgentRefundSubkey(mnemonic, cashu.canonical_id);
5167
+ const marker = reservedForMarker(opts.builderHandle);
5168
+ const now = (opts.now ?? Date.now)();
5169
+ const candidates = initial.proofs.filter(
5170
+ (p) => p.reserved_for === marker && p.status === "valid" && typeof p.t_expire === "number" && p.t_expire < now
5171
+ );
5172
+ if (candidates.length === 0) {
5173
+ return { builder_handle: opts.builderHandle, output_mode: outputMode, mints: [] };
5174
+ }
5175
+ const getWalletForMint = opts.getWalletForMint ?? getWallet;
5176
+ const byMint = /* @__PURE__ */ new Map();
5177
+ for (const p of candidates) {
5178
+ const bucket = byMint.get(p.mint) ?? [];
5179
+ bucket.push(p);
5180
+ byMint.set(p.mint, bucket);
5181
+ }
5182
+ const outcomes = [];
5183
+ for (const [mintUrl, mintProofs] of byMint) {
5184
+ outcomes.push(
5185
+ await releaseAtMint(
5186
+ mintUrl,
5187
+ mintProofs,
5188
+ refund.privkey,
5189
+ initial,
5190
+ outputMode,
5191
+ getWalletForMint
5192
+ )
5193
+ );
5194
+ }
5195
+ return { builder_handle: opts.builderHandle, output_mode: outputMode, mints: outcomes };
5196
+ }
5197
+ async function releaseAtMint(mintUrl, mintProofs, refundPrivkey, initial, outputMode, getWalletForMint) {
5198
+ let cashuWallet;
5199
+ try {
5200
+ cashuWallet = await getWalletForMint(mintUrl);
5201
+ } catch (err) {
5202
+ return {
5203
+ mint: mintUrl,
5204
+ alreadySpent: [],
5205
+ pending: [],
5206
+ reclaimedInputs: [],
5207
+ reclaimedOutputs: [],
5208
+ error: err instanceof Error ? err.message : String(err)
5209
+ };
5210
+ }
5211
+ let grouped;
5212
+ try {
5213
+ grouped = await cashuWallet.groupProofsByState(toCashuProofs(mintProofs));
5214
+ } catch (err) {
5215
+ return {
5216
+ mint: mintUrl,
5217
+ alreadySpent: [],
5218
+ pending: [],
5219
+ reclaimedInputs: [],
5220
+ reclaimedOutputs: [],
5221
+ error: err instanceof Error ? err.message : String(err)
5222
+ };
5223
+ }
5224
+ const spentSecrets = new Set(grouped.spent.map((p) => p.secret));
5225
+ const pendingSecrets = new Set(grouped.pending.map((p) => p.secret));
5226
+ const alreadySpent = mintProofs.filter((p) => spentSecrets.has(p.secret));
5227
+ const pending = mintProofs.filter((p) => pendingSecrets.has(p.secret));
5228
+ const unspent = mintProofs.filter(
5229
+ (p) => !spentSecrets.has(p.secret) && !pendingSecrets.has(p.secret)
5230
+ );
5231
+ if (alreadySpent.length > 0) {
5232
+ await withWalletLock(() => {
5233
+ const fresh = loadAgentWallet() ?? initial;
5234
+ const spentKeys = new Set(alreadySpent.map((p) => proofIdKey(p)));
5235
+ saveAgentWallet({
5236
+ ...fresh,
5237
+ proofs: fresh.proofs.filter((p) => !spentKeys.has(proofIdKey(p)))
5238
+ });
5239
+ });
5240
+ }
5241
+ if (unspent.length === 0) {
5242
+ return {
5243
+ mint: mintUrl,
5244
+ alreadySpent,
5245
+ pending,
5246
+ reclaimedInputs: [],
5247
+ reclaimedOutputs: [],
5248
+ error: null
5249
+ };
5250
+ }
5251
+ const totalSats = unspent.reduce((sum, p) => sum + p.amount, 0);
5252
+ const inputs = toCashuProofs(unspent);
5253
+ let outputs;
5254
+ try {
5255
+ const swapFeeSats = swapFeeForProofs(cashuWallet, inputs);
5256
+ const reclaimSats = totalSats - swapFeeSats;
5257
+ if (reclaimSats <= 0) {
5258
+ throw new Error(
5259
+ `Reserve at ${mintUrl} is ${totalSats} sats, which does not cover the mint's ${swapFeeSats}-sat swap fee.`
5260
+ );
5261
+ }
5262
+ const builder = cashuWallet.ops.send(reclaimSats, inputs).privkey(refundPrivkey);
5263
+ const result = outputMode === "locked" ? await builder.asP2PK({ pubkey: initial.lock_pubkey }).run() : await builder.asRandom().run();
5264
+ outputs = result.send;
5265
+ } catch (err) {
5266
+ return {
5267
+ mint: mintUrl,
5268
+ alreadySpent,
5269
+ pending,
5270
+ reclaimedInputs: [],
5271
+ reclaimedOutputs: [],
5272
+ error: err instanceof Error ? err.message : String(err)
5273
+ };
5274
+ }
5275
+ const reclaimedOutputs = outputs.map(
5276
+ (p) => toAgentProof(p, mintUrl, outputMode === "locked" ? initial.lock_pubkey : null)
5277
+ );
5278
+ await withWalletLock(() => {
5279
+ const fresh = loadAgentWallet() ?? initial;
5280
+ const inputKeys = new Set(unspent.map((p) => proofIdKey(p)));
5281
+ saveAgentWallet({
5282
+ ...fresh,
5283
+ proofs: [...fresh.proofs.filter((p) => !inputKeys.has(proofIdKey(p))), ...reclaimedOutputs]
5284
+ });
5285
+ });
5286
+ return {
5287
+ mint: mintUrl,
5288
+ alreadySpent,
5289
+ pending,
5290
+ reclaimedInputs: unspent,
5291
+ reclaimedOutputs,
5292
+ error: null
5293
+ };
5294
+ }
5295
+ function proofIdKey(p) {
5296
+ return `${p.secret}:${p.C}`;
5297
+ }
5298
+
5299
+ // src/lib/wallet-agent/reserve.ts
5300
+ import { MintQuoteState as MintQuoteState3 } from "@cashu/cashu-ts";
5301
+ async function reserveAgentWallet(amountSats, opts) {
5302
+ if (!Number.isFinite(amountSats) || amountSats <= 0) {
5303
+ throw new DvmError(
5304
+ "invalid_amount",
5305
+ "Amount must be a positive integer number of sats.",
5306
+ "Pass a positive sats amount, e.g. `dvm wallet reserve --builder scrape.dvmkit.ai 1000`, or a USD amount, e.g. `dvm wallet reserve --builder scrape.dvmkit.ai $5`."
5307
+ );
5308
+ }
5309
+ const sats = Math.floor(amountSats);
5310
+ const cashu = opts.providerInfo.cashu;
5311
+ if (!cashu) {
5312
+ throw new DvmError(
5313
+ "missing_cashu_info",
5314
+ "DVM does not advertise a `cashu` object on /v1/info.",
5315
+ "Reserve requires the builder to expose lock_pubkeys, mints, and canonical_id. Check the builder is running an SDK that supports pre-fund (internal-review+)."
5316
+ );
5317
+ }
5318
+ const lockPubkey = cashu.lock_pubkeys[0];
5319
+ if (!lockPubkey) {
5320
+ throw new DvmError(
5321
+ "missing_lock_pubkey",
5322
+ "DVM advertised no current lock pubkey.",
5323
+ "Ask the builder operator to deploy with DVMKIT_CASHU_LOCK_PUBKEY set."
5324
+ );
5325
+ }
5326
+ if (cashu.mints.length === 0) {
5327
+ throw new DvmError(
5328
+ "missing_dvm_mints",
5329
+ "DVM advertises no accepted Cashu mints.",
5330
+ "Ask the builder operator to set DVMKIT_CASHU_MINTS on the DVM machine."
5331
+ );
5332
+ }
5333
+ if (!cashu.canonical_id) {
5334
+ throw new DvmError(
5335
+ "missing_canonical_id",
5336
+ "DVM advertises no `cashu.canonical_id` on /v1/info.",
5337
+ "Pre-fund requires canonical_id so the agent can derive a per-DVM refund subkey. Upgrade the builder SDK to one that advertises it (internal-review+)."
5338
+ );
5339
+ }
5340
+ if (!cashu.mints.includes(opts.mintUrl)) {
5341
+ throw new DvmError(
5342
+ "mint_not_accepted",
5343
+ `Mint ${opts.mintUrl} is not in the builder's allowlist (${cashu.mints.join(", ")}).`,
5344
+ `Re-run with --mint set to one of the builder's accepted mints, or top up at one of them first.`
5345
+ );
5346
+ }
5347
+ const locktimeSeconds = opts.locktimeSeconds ?? DEFAULT_RESERVE_LOCKTIME_SECONDS;
5348
+ if (cashu.min_locktime && locktimeSeconds < cashu.min_locktime) {
5349
+ throw new DvmError(
5350
+ "locktime_too_short",
5351
+ `Requested locktime ${locktimeSeconds}s is below the builder's minimum ${cashu.min_locktime}s.`,
5352
+ `Re-run with --locktime at or above the builder's minimum (${cashu.min_locktime}s).`
5353
+ );
5354
+ }
5355
+ const initial = loadAgentWallet();
5356
+ if (!initial) {
5357
+ throw new DvmError(
5358
+ "wallet_missing",
5359
+ "No agent wallet found.",
5360
+ "Run `dvm wallet init` first to generate a wallet."
5361
+ );
5362
+ }
5363
+ const readMnemonic = opts.readMnemonic ?? readAgentMnemonic;
5364
+ const mnemonic = readMnemonic();
5365
+ if (!mnemonic) {
5366
+ throw new DvmError(
5367
+ "mnemonic_required",
5368
+ "Reserve requires a BIP-39 mnemonic at ~/.dvm/wallet.mnemonic.",
5369
+ "The per-DVM refund subkey is derived from the mnemonic, so `dvm wallet release` post-expire requires it. Run `dvm wallet init` (fresh wallet) or `dvm wallet recover --force --file <path>` (or pipe the mnemonic via stdin) to attach the mnemonic file."
5370
+ );
5371
+ }
5372
+ const refundPubkey = deriveAgentRefundSubkeyPubkey(mnemonic, cashu.canonical_id);
5373
+ const cashuWallet = await (opts.getWalletForMint ?? getWallet)(opts.mintUrl);
5374
+ const quote = await cashuWallet.createLockedMintQuote(sats, initial.lock_pubkey);
5375
+ const pending = {
5376
+ mint: opts.mintUrl,
5377
+ quote_id: quote.quote,
5378
+ amount_sats: sats,
5379
+ requested_at: (/* @__PURE__ */ new Date()).toISOString()
5380
+ };
5381
+ await withWalletLock(() => {
5382
+ const fresh = loadAgentWallet() ?? initial;
5383
+ saveAgentWallet({
5384
+ ...fresh,
5385
+ pending_mints: [...fresh.pending_mints, pending]
5386
+ });
5387
+ });
5388
+ if (opts.onInvoice) await opts.onInvoice(quote.request, quote);
5389
+ const finalState = await pollUntilPaidOrTimeout(cashuWallet, quote.quote, {
5390
+ timeoutMs: opts.timeoutMs ?? fundTimeoutMs(),
5391
+ pollIntervalMs: opts.pollIntervalMs,
5392
+ now: opts.now,
5393
+ sleep: opts.sleep,
5394
+ onPoll: opts.onPoll
5395
+ });
5396
+ if (finalState === MintQuoteState3.ISSUED) {
5397
+ throw new DvmError(
5398
+ "reserve_already_issued",
5399
+ `Mint quote ${quote.quote} was already collected by a concurrent run.`,
5400
+ "Inspect wallet state with `dvm wallet show` \u2014 the proofs should be persisted by the other instance."
5401
+ );
5402
+ }
5403
+ if (finalState !== MintQuoteState3.PAID) {
5404
+ throw new DvmError(
5405
+ "reserve_timeout",
5406
+ `Mint quote ${quote.quote} was not paid within the reserve timeout.`,
5407
+ "Pay the invoice from a Lightning wallet, then run `dvm wallet show` to recover the pending mint, or `dvm wallet reserve` to start a new flow."
5408
+ );
2831
5409
  }
2832
- if (bestTarget === -1) return null;
2833
- const indices = reachable.get(bestTarget) ?? [];
2834
- return { selected: indices.map((i) => proofs[i]), totalSats: bestTarget };
5410
+ const denominations = computeSmartDenominations(sats, {
5411
+ maxDenomination: maxKeysetDenomination(cashuWallet)
5412
+ });
5413
+ const nowMs = (opts.now ?? Date.now)();
5414
+ const tExpireSeconds = Math.floor(nowMs / 1e3) + locktimeSeconds;
5415
+ const tExpireMs = tExpireSeconds * 1e3;
5416
+ const preview = await prepareP2PKMint(
5417
+ cashuWallet,
5418
+ sats,
5419
+ quote,
5420
+ initial.lock_privkey,
5421
+ lockPubkey,
5422
+ denominations,
5423
+ { locktimeUnixSec: tExpireSeconds, refundKeys: [refundPubkey] }
5424
+ );
5425
+ const minting = {
5426
+ keyset_id: preview.keysetId,
5427
+ outputs: serializeOutputData(preview.outputData)
5428
+ };
5429
+ await withWalletLock(() => {
5430
+ const fresh = loadAgentWallet() ?? initial;
5431
+ saveAgentWallet(upsertPending2(fresh, { ...pending, minting }));
5432
+ });
5433
+ const reservedFor = reservedForMarker(opts.builderHandle);
5434
+ const proofs = await cashuWallet.completeMint(preview);
5435
+ const agentProofs = proofs.map(
5436
+ (p) => toAgentProof(p, opts.mintUrl, lockPubkey, {
5437
+ reservedFor,
5438
+ tExpireMs
5439
+ })
5440
+ );
5441
+ await withWalletLock(() => {
5442
+ const post = loadAgentWallet() ?? initial;
5443
+ saveAgentWallet({
5444
+ ...post,
5445
+ proofs: [...post.proofs, ...agentProofs],
5446
+ pending_mints: post.pending_mints.filter((p) => p.quote_id !== quote.quote)
5447
+ });
5448
+ });
5449
+ return {
5450
+ mint: opts.mintUrl,
5451
+ amount_sats: sats,
5452
+ quote_id: quote.quote,
5453
+ denominations,
5454
+ proofs: agentProofs,
5455
+ builder_handle: opts.builderHandle,
5456
+ t_expire_ms: tExpireMs,
5457
+ refund_pubkey: refundPubkey,
5458
+ lock_pubkey: lockPubkey
5459
+ };
5460
+ }
5461
+ function upsertPending2(wallet, entry) {
5462
+ const others = wallet.pending_mints.filter((p) => p.quote_id !== entry.quote_id);
5463
+ return { ...wallet, pending_mints: [...others, entry] };
2835
5464
  }
2836
-
2837
- // src/lib/wallet-agent/recovery-submissions.ts
2838
- var RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
2839
-
2840
- // src/lib/wallet-agent/reserve.ts
2841
- import { MintQuoteState as MintQuoteState3 } from "@cashu/cashu-ts";
2842
5465
 
2843
5466
  // src/lib/x402/batch-client.ts
2844
5467
  import {
@@ -2873,6 +5496,29 @@ function resolveX402ChannelRpc(channelNetwork, configuredNetwork, bundled) {
2873
5496
 
2874
5497
  // src/lib/x402/lifecycle.ts
2875
5498
  import { BATCH_SETTLEMENT_ADDRESS } from "@x402/evm";
5499
+ async function assertX402WalletIdle(wallet, intent) {
5500
+ const payerAddress = wallet.address;
5501
+ const owned = listX402Channels({ payer: payerAddress });
5502
+ const clients = /* @__PURE__ */ new Map();
5503
+ for (const channel of owned) {
5504
+ const state = await readChannelState(channel, wallet.network, clients, "guard");
5505
+ if (state.balance === 0n && state.pendingWithdrawal === 0n) continue;
5506
+ throw new DvmError(
5507
+ "x402_wallet_in_use",
5508
+ state.balance > 0n ? `The connected x402 wallet still holds ${formatUsdcMicrounits(state.balance)} USDC in an open channel with ${channel.endpoint}.` : `The connected x402 wallet has a timed withdrawal of ${formatUsdcMicrounits(state.pendingWithdrawal)} USDC still in flight on its channel with ${channel.endpoint}.`,
5509
+ `Only this key can move those funds. Empty the channel first \u2014 'dvm credit drain' for the cooperative exit, or 'dvm wallet x402-withdraw-initiate --channel-id ${channel.channelId}' and then finalize after the delay \u2014 and ${intentClause(intent)}.`,
5510
+ {
5511
+ anchor: "x402_channel_live",
5512
+ address: payerAddress,
5513
+ channel_id: channel.channelId,
5514
+ endpoint: channel.endpoint,
5515
+ network: channel.network,
5516
+ balance_micro: state.balance.toString(),
5517
+ pending_withdrawal_micro: state.pendingWithdrawal.toString()
5518
+ }
5519
+ );
5520
+ }
5521
+ }
2876
5522
  function assertX402ChannelPayer(channel, address) {
2877
5523
  if (channel.config.payer.toLowerCase() === address.toLowerCase()) return;
2878
5524
  throw new DvmError(
@@ -2889,11 +5535,162 @@ function assertX402ChannelPayer(channel, address) {
2889
5535
  }
2890
5536
  );
2891
5537
  }
5538
+ async function readX402ChannelOnChain(args) {
5539
+ const { channel } = args;
5540
+ const clients = args.clients ?? /* @__PURE__ */ new Map();
5541
+ let state;
5542
+ let error;
5543
+ try {
5544
+ state = await readChannelState(channel, args.configuredNetwork, clients, "report");
5545
+ } catch (err) {
5546
+ error = err instanceof DvmError ? err : new DvmError(
5547
+ "x402_chain_read_failed",
5548
+ `Reading the x402 channel with ${channel.endpoint} on ${channel.network} failed: ${redactUrlsInText(err instanceof Error ? err.message : String(err))}`
5549
+ );
5550
+ }
5551
+ let status = state ? classifyX402ChannelState(state) : "unreadable";
5552
+ if (state && status === "unconfirmed") {
5553
+ const refunds = await readChannelRefundNonce(channel, args.configuredNetwork, clients).catch(
5554
+ () => 0n
5555
+ );
5556
+ if (refunds > 0n) status = "closed";
5557
+ }
5558
+ const accounted = status === "unconfirmed" ? void 0 : state;
5559
+ const stored = accounted ? void 0 : await storedChannelBalance(channel.channelId);
5560
+ const depositMicro = accounted ? Number(accounted.balance) : stored?.balanceMicro;
5561
+ return {
5562
+ status,
5563
+ channelId: channel.channelId,
5564
+ ...depositMicro !== void 0 ? { depositMicro } : {},
5565
+ ...state ? {
5566
+ settledMicro: Number(state.totalClaimed),
5567
+ ...depositMicro !== void 0 ? { uncommittedMicro: Math.max(depositMicro - Number(state.totalClaimed), 0) } : {},
5568
+ pendingWithdrawalMicro: Number(state.pendingWithdrawal)
5569
+ } : {},
5570
+ ...depositMicro !== void 0 && stored?.unreconciledAt !== void 0 ? { balanceUnreconciledAt: stored.unreconciledAt } : {},
5571
+ ...error ? { error } : {}
5572
+ };
5573
+ }
5574
+ function x402ChannelControlledBy(channel, address) {
5575
+ if (!address) return false;
5576
+ return channel.config.payer.toLowerCase() === address.toLowerCase();
5577
+ }
5578
+ async function storedChannelBalance(channelId) {
5579
+ try {
5580
+ const balance = (await new DvmX402ChannelStorage().get(channelId))?.balance;
5581
+ const unreconciledAt = x402ChannelBalanceUnreconciledAt(channelId);
5582
+ return {
5583
+ ...balance !== void 0 ? { balanceMicro: Number(balance) } : {},
5584
+ ...unreconciledAt !== void 0 ? { unreconciledAt } : {}
5585
+ };
5586
+ } catch {
5587
+ return void 0;
5588
+ }
5589
+ }
5590
+ function classifyX402ChannelState(state) {
5591
+ if (state.pendingWithdrawal > 0n) return "closing";
5592
+ if (state.balance > 0n) return "open";
5593
+ return state.totalClaimed > 0n ? "closed" : "unconfirmed";
5594
+ }
5595
+ async function resolveChannelRead(channel, configuredNetwork, clients, posture) {
5596
+ const spec = x402NetworkByCaip2(channel.network);
5597
+ if (!spec) {
5598
+ throw new DvmError(
5599
+ "x402_unsupported_network",
5600
+ `The channel with ${channel.endpoint} is recorded on ${channel.network}, a chain this rail can't read.`,
5601
+ posture === "guard" ? `${x402SupportedNetworksHint()} The key stays connected: with no way to read that channel, there is no way to show it is empty.` : `${x402SupportedNetworksHint()} The local channel record is kept: with no way to read that chain, there is no way to show what the contract holds for it.`,
5602
+ { channel_id: channel.channelId, endpoint: channel.endpoint, network: channel.network }
5603
+ );
5604
+ }
5605
+ const chains2 = await import("viem/chains");
5606
+ const { createPublicClient: createPublicClient2, http: http2 } = await import("viem");
5607
+ const bundled = chains2[spec.viemChain]?.rpcUrls?.default.http[0];
5608
+ const { url, source, overrideOutOfScope } = resolveX402ChannelRpc(
5609
+ channel.network,
5610
+ configuredNetwork,
5611
+ bundled
5612
+ );
5613
+ let client = clients.get(channel.network);
5614
+ if (!client) {
5615
+ const transportOptions = resolveRpcHttpTransportOptions(
5616
+ source,
5617
+ process.env.DVM_X402_RPC_HEADERS_JSON
5618
+ );
5619
+ client = createPublicClient2({
5620
+ chain: chains2[spec.viemChain],
5621
+ transport: transportOptions ? http2(url, transportOptions) : http2(url)
5622
+ });
5623
+ clients.set(channel.network, client);
5624
+ }
5625
+ return { client, label: spec.label, url, source, overrideOutOfScope };
5626
+ }
5627
+ async function readChannelState(channel, configuredNetwork, clients, posture) {
5628
+ const read = await resolveChannelRead(channel, configuredNetwork, clients, posture);
5629
+ let onchain;
5630
+ let pending;
5631
+ try {
5632
+ [onchain, pending] = await Promise.all([
5633
+ read.client.readContract({
5634
+ address: BATCH_SETTLEMENT_ADDRESS,
5635
+ abi: X402_BATCH_CHANNEL_ABI,
5636
+ functionName: "channels",
5637
+ args: [channel.channelId]
5638
+ }),
5639
+ read.client.readContract({
5640
+ address: BATCH_SETTLEMENT_ADDRESS,
5641
+ abi: X402_BATCH_CHANNEL_ABI,
5642
+ functionName: "pendingWithdrawals",
5643
+ args: [channel.channelId]
5644
+ })
5645
+ ]);
5646
+ } catch (err) {
5647
+ throw rpcError(err, channel, read, posture);
5648
+ }
5649
+ await new DvmX402ChannelStorage().reconcileChainState(channel.channelId, {
5650
+ balance: onchain[0].toString(),
5651
+ totalClaimed: BigInt(onchain[1]).toString()
5652
+ }).catch(() => void 0);
5653
+ return {
5654
+ balance: onchain[0],
5655
+ totalClaimed: BigInt(onchain[1]),
5656
+ pendingWithdrawal: pending[0]
5657
+ };
5658
+ }
5659
+ async function readChannelRefundNonce(channel, configuredNetwork, clients) {
5660
+ const read = await resolveChannelRead(channel, configuredNetwork, clients, "report");
5661
+ try {
5662
+ return await read.client.readContract({
5663
+ address: BATCH_SETTLEMENT_ADDRESS,
5664
+ abi: X402_BATCH_CHANNEL_ABI,
5665
+ functionName: "refundNonce",
5666
+ args: [channel.channelId]
5667
+ });
5668
+ } catch (err) {
5669
+ throw rpcError(err, channel, read, "report");
5670
+ }
5671
+ }
5672
+ function rpcError(err, channel, read, posture) {
5673
+ const { label, url, source, overrideOutOfScope } = read;
5674
+ const cause = redactUrlsInText(err instanceof Error ? err.message : String(err));
5675
+ const opening = posture === "guard" ? "The x402 wallet is untouched. " : "Nothing was read or changed. ";
5676
+ const closing = posture === "guard" ? " then retry \u2014 until the channel reads as empty the key stays connected." : " then re-run 'dvm wallet balance'. The local channel record is kept until the chain can be read, because it is the only route back to any collateral the contract does hold.";
5677
+ const scopeClause = overrideOutOfScope ? `This channel is on ${label}, not the chain the wallet is pinned to, so DVM_X402_RPC_URL did not apply to it \u2014 it stays scoped to the pinned chain, because the settlement contract shares one address across chains and a cross-chain read would answer "empty" for a channel that isn't. Verify connectivity to ${label}'s bundled endpoint${url ? ` (${redactUrl(url)})` : ""},` : `Verify connectivity to ${label}'s bundled endpoint${url ? ` (${redactUrl(url)})` : ""}, or send the read somewhere else with DVM_X402_RPC_URL,`;
5678
+ const diagnosis = source === "override" ? `DVM_X402_RPC_URL sent this read to ${redactUrl(url ?? "")}. Correct it, or unset it to fall back to ${label}'s bundled endpoint,` : scopeClause;
5679
+ return new DvmError(
5680
+ "x402_rpc_error",
5681
+ `Reading the x402 channel with ${channel.endpoint} on ${label} failed: ${cause}`,
5682
+ `${opening}${diagnosis}${closing}`,
5683
+ { channel_id: channel.channelId, endpoint: channel.endpoint, network: channel.network }
5684
+ );
5685
+ }
5686
+ function intentClause(intent) {
5687
+ return intent === "replace" ? "retry the replacement" : "retry 'dvm wallet x402-disconnect'";
5688
+ }
2892
5689
 
2893
5690
  // src/lib/x402/voucher-cap.ts
2894
5691
  import { decodePaymentSignatureHeader } from "@x402/core/http";
2895
- function x402VoucherFactsFromPayload(raw) {
2896
- const payload = raw;
5692
+ function x402VoucherFactsFromPayload(raw2) {
5693
+ const payload = raw2;
2897
5694
  const maxClaimableMicro = payload.voucher?.maxClaimableAmount;
2898
5695
  if (maxClaimableMicro === void 0) return void 0;
2899
5696
  if (payload.type !== "deposit") return { maxClaimableMicro };
@@ -2910,6 +5707,10 @@ function x402ChannelReservation(journalled, totalClaimed) {
2910
5707
  totalClaimed
2911
5708
  );
2912
5709
  }
5710
+ function x402ReservationRelease(journalled) {
5711
+ const retriable = journalled.filter((entry) => !x402PendingRetired(entry));
5712
+ return retriable.length > 0 ? { entry: retriable[0], retriable: true } : { entry: journalled[0], retriable: false };
5713
+ }
2913
5714
  function x402VoucherCap(pending) {
2914
5715
  const recorded = pending.maxClaimableMicro ?? x402VoucherFacts(pending.paymentHeader)?.maxClaimableMicro;
2915
5716
  if (recorded === void 0) return X402_MAX_UINT128;
@@ -3264,9 +6065,9 @@ async function refundX402BatchChannelLocked(args) {
3264
6065
  return { result, paymentResponse: encoded, atRiskMicro, pendingFundings: journalled };
3265
6066
  }
3266
6067
  function x402RequiredFromError(err) {
3267
- const raw = err.data?.x402_payment_required;
3268
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
3269
- return raw;
6068
+ const raw2 = err.data?.x402_payment_required;
6069
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
6070
+ return raw2;
3270
6071
  }
3271
6072
  function assertX402RequirementDescribesChannel(args) {
3272
6073
  assertX402ChannelPayer(
@@ -6058,40 +8859,587 @@ function isArtifactMessage(msg) {
6058
8859
  function isPaymentRequestMessage(msg) {
6059
8860
  return msg.type === "payment-request";
6060
8861
  }
8862
+ function isTextMessage(msg) {
8863
+ return msg.type === "text";
8864
+ }
8865
+ function isCompleteMessage(msg) {
8866
+ return msg.type === "complete";
8867
+ }
8868
+ function isCancelMessage(msg) {
8869
+ return msg.type === "cancel";
8870
+ }
8871
+ function isWorkingMessage(msg) {
8872
+ return msg.type === "working";
8873
+ }
8874
+
8875
+ // src/lib/policy.ts
8876
+ function buildPolicy(opts, btcRate) {
8877
+ if (opts.maxPayments === void 0) {
8878
+ throw new DvmError(
8879
+ "missing_max_payments",
8880
+ "Cannot build a spending policy without a payment count cap.",
8881
+ "Pass --max-payments <n> on dvm request. Use 0 for free jobs, 1 for typical paid jobs, or higher for multi-turn flows that may require progressive payments."
8882
+ );
8883
+ }
8884
+ if (!Number.isInteger(opts.maxPayments) || opts.maxPayments < 0) {
8885
+ throw new DvmError(
8886
+ "invalid_max_payments",
8887
+ "Invalid value for --max-payments.",
8888
+ "Pass a non-negative integer (e.g. --max-payments 1)."
8889
+ );
8890
+ }
8891
+ const maxTotalMsats = opts.budget ? parseBudget(opts.budget, btcRate) : 0;
8892
+ const autoPayBelowMsats = opts.autoPayBelow ? parseBudget(opts.autoPayBelow, btcRate) : Math.floor(maxTotalMsats * 0.2);
8893
+ const maxIncrementMsats = opts.maxIncrement ? parseBudget(opts.maxIncrement, btcRate) : maxTotalMsats;
8894
+ return { maxTotalMsats, autoPayBelowMsats, maxIncrementMsats, maxPayments: opts.maxPayments };
8895
+ }
6061
8896
 
6062
8897
  // src/lib/provenance.ts
6063
8898
  function asProviderText(text) {
6064
8899
  return { _source: "provider", text };
6065
8900
  }
8901
+ function asProviderTextOpt(text) {
8902
+ return text === void 0 ? void 0 : asProviderText(text);
8903
+ }
6066
8904
  function providerTextToString(value) {
6067
8905
  return typeof value === "string" ? value : value.text;
6068
8906
  }
8907
+
8908
+ // src/lib/cashu/balance.ts
8909
+ function getBalance2(mintUrl) {
8910
+ const wallet = loadAgentWallet();
8911
+ if (!wallet) return 0;
8912
+ const proofs = wallet.proofs.filter(
8913
+ (p) => p.status === "valid" && (mintUrl === void 0 || p.mint === mintUrl)
8914
+ );
8915
+ return proofs.reduce((sum, p) => sum + p.amount, 0) * 1e3;
8916
+ }
8917
+ function getBalanceByMint() {
8918
+ const wallet = loadAgentWallet();
8919
+ if (!wallet) return /* @__PURE__ */ new Map();
8920
+ const map = /* @__PURE__ */ new Map();
8921
+ for (const p of wallet.proofs) {
8922
+ if (p.status !== "valid") continue;
8923
+ map.set(p.mint, (map.get(p.mint) ?? 0) + p.amount * 1e3);
8924
+ }
8925
+ return map;
8926
+ }
8927
+
8928
+ // src/lib/completion-hints.ts
8929
+ function buildCompletionHintFields(jobId, input) {
8930
+ const out = {};
8931
+ if (input.hasArtifacts !== void 0) out.has_artifacts = input.hasArtifacts;
8932
+ if (input.artifactCount !== void 0) out.artifact_count = input.artifactCount;
8933
+ if (input.payloadIn) out.payload_in = input.payloadIn;
8934
+ const hint = nextStepHint(jobId, input.payloadIn, input.artifactCount);
8935
+ if (hint) out.next_step_hint = hint;
8936
+ return out;
8937
+ }
8938
+ function nextStepHint(jobId, payloadIn, artifactCount) {
8939
+ if (payloadIn !== "messages") return void 0;
8940
+ const noun = artifactCount === 1 ? "artifact" : "artifacts";
8941
+ return `Run 'dvm messages ${jobId} --no-stream' to fetch the ${noun}.`;
8942
+ }
8943
+ function completionSnapshot(src) {
8944
+ const out = {};
8945
+ if (src.summary !== void 0) out.summary = src.summary;
8946
+ if (src.message !== void 0) out.message = src.message;
8947
+ if (src.hasArtifacts !== void 0) out.hasArtifacts = src.hasArtifacts;
8948
+ if (src.artifactCount !== void 0) out.artifactCount = src.artifactCount;
8949
+ if (src.payloadIn) out.payloadIn = src.payloadIn;
8950
+ return out;
8951
+ }
8952
+ function completionFromMessages(messages) {
8953
+ const complete = messages.findLast((m) => m.type === "complete");
8954
+ const summary = complete?.type === "complete" ? complete.content.summary : void 0;
8955
+ const artifactCount = messages.filter((m) => m.type === "artifact").length;
8956
+ const out = {
8957
+ hasArtifacts: artifactCount > 0,
8958
+ payloadIn: artifactCount > 0 ? "messages" : "summary"
8959
+ };
8960
+ if (summary !== void 0) out.summary = summary;
8961
+ if (artifactCount > 0) out.artifactCount = artifactCount;
8962
+ return out;
8963
+ }
8964
+
8965
+ // src/lib/funding.ts
8966
+ var SAFE_FUNDING_ADVISORY = {
8967
+ display: "Start small \u2014 fund a few days' worth and top up as confidence grows",
8968
+ description: "There's no need to fund a large amount up front. A few days of expected spend is enough to get going; top up once you've seen how your agent uses the wallet.",
8969
+ hint: "Funding lightly is the safest posture across all rails. A connected Lightning wallet lets the agent top itself up on its own \u2014 set a spending cap on the wallet (it's the one the agent can't change) and 'dvm budget set' caps here."
8970
+ };
8971
+ function detectWalletType(config, cashuMints) {
8972
+ if (!config && cashuMints.length === 0) return null;
8973
+ if (config?.nwcUri) return "nwc";
8974
+ if (cashuMints.length > 0) return "cashu";
8975
+ return null;
8976
+ }
8977
+ function getFundingGuidance(opts) {
8978
+ const fundingOptions = (opts.balanceMsats ?? 0) > 0 ? [] : opts.walletType === "nwc" ? nwcOptions() : cashuOptions(opts.cashuMints, opts.hasLightningBackend ?? false);
8979
+ return { fundingOptions, advisory: SAFE_FUNDING_ADVISORY };
8980
+ }
8981
+ function printFundingGuidance(result) {
8982
+ info(`
8983
+ ${result.advisory.display}`);
8984
+ info(` ${result.advisory.description}`);
8985
+ info(` ${result.advisory.hint}`);
8986
+ if (result.fundingOptions.length === 0) return;
8987
+ info("\nFund your wallet:");
8988
+ for (let i = 0; i < result.fundingOptions.length; i++) {
8989
+ const o = result.fundingOptions[i];
8990
+ info(` ${i + 1}. ${o.display}`);
8991
+ if (o.command) info(` ${o.command}`);
8992
+ info(` ${o.description}`);
8993
+ }
8994
+ }
8995
+ function nwcOptions() {
8996
+ return [
8997
+ {
8998
+ method: "external",
8999
+ display: "Add funds in your wallet app",
9000
+ description: "The connected Lightning wallet is the agent's float \u2014 the pot it tops itself up from. Money goes in through the wallet app itself (Alby Hub, Coinos, etc.); dvm spends from it but never fills it.",
9001
+ hint: "Receive a payment in your wallet app, then run 'dvm wallet fund <amount>' \u2014 the agent will move sats across on its own."
9002
+ }
9003
+ ];
9004
+ }
9005
+ function cashuOptions(mints, hasLightningBackend) {
9006
+ const exampleMint = mints && mints.length > 0 ? mints[0] : void 0;
9007
+ const hasMints = exampleMint !== void 0;
9008
+ const mintCommand = exampleMint ? `dvm wallet fund <sats> --mint ${exampleMint}` : "dvm wallet fund <sats> --mint <url>";
9009
+ const mintOption = {
9010
+ method: "cashu-mint",
9011
+ display: "Mint Cashu tokens via Lightning",
9012
+ command: mintCommand,
9013
+ description: hasMints ? "Pays a Lightning invoice from your Lightning wallet and mints matching P2PK-locked Cashu tokens into the agent wallet. Cashu mints are custodial \u2014 only deposit what you're willing to lose." : "After adding a mint above, pays a Lightning invoice from your Lightning wallet and mints matching P2PK-locked Cashu tokens into the agent wallet. Cashu mints are custodial \u2014 only deposit what you're willing to lose.",
9014
+ hint: hasLightningBackend ? "Requires a Lightning wallet \u2014 you have one configured." : "Requires a Lightning wallet \u2014 connect one with 'dvm wallet connect' first (pipe the connection string in, or pass --file <path>).",
9015
+ ...hasMints ? { mints } : {}
9016
+ };
9017
+ const receiveTokenOption = {
9018
+ method: "cashu-receive-token",
9019
+ display: "Receive an existing Cashu token",
9020
+ command: "dvm wallet receive-token <cashu-token>",
9021
+ description: "Claim a pre-minted Cashu token (e.g. from a friend or a faucet) into the agent wallet. The token is re-locked to your wallet's key so it's spendable.",
9022
+ hint: "Tokens are single-use; a token locked to a third party can't be claimed.",
9023
+ ...hasMints ? { mints } : {}
9024
+ };
9025
+ const options = [];
9026
+ if (!hasMints) {
9027
+ options.push({
9028
+ method: "cashu-add-mint",
9029
+ display: "Add a Cashu mint",
9030
+ command: "dvm wallet mint-add <url>",
9031
+ description: "Cashu mints hold your tokens. Add one before you can mint or receive tokens \u2014 'dvm wallet mint-list' shows recommended mints. Cashu mints are custodial \u2014 only deposit what you're willing to lose.",
9032
+ hint: "Recommended mints: see 'dvm wallet mint-list' for trusted options."
9033
+ });
9034
+ }
9035
+ if (hasLightningBackend) {
9036
+ options.push(mintOption, receiveTokenOption);
9037
+ } else {
9038
+ options.push(receiveTokenOption, mintOption);
9039
+ }
9040
+ return options;
9041
+ }
9042
+
9043
+ // src/lib/next-action.ts
9044
+ function isTerminalJobStatus(status) {
9045
+ return TERMINAL_STATUSES.has(status);
9046
+ }
9047
+ function pollNextAction(jobId, hint) {
9048
+ return {
9049
+ type: "poll",
9050
+ job_id: jobId,
9051
+ hint: hint ?? "Provider is working, check back later"
9052
+ };
9053
+ }
9054
+ var YIELD_TYPES = /* @__PURE__ */ new Set(["prompt", "payment-request", "working", "complete", "cancel"]);
9055
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
9056
+ "completed",
9057
+ "failed",
9058
+ "cancelled"
9059
+ ]);
9060
+ function computeNextAction(jobId, messages) {
9061
+ for (let i = messages.length - 1; i >= 0; i--) {
9062
+ const msg = messages[i];
9063
+ if (!YIELD_TYPES.has(msg.type)) continue;
9064
+ switch (msg.type) {
9065
+ case "complete":
9066
+ case "cancel":
9067
+ return null;
9068
+ case "prompt": {
9069
+ const c = msg.content;
9070
+ const action = {
9071
+ type: "respond",
9072
+ job_id: jobId,
9073
+ prompt_id: c.id,
9074
+ prompt: asProviderText(c.text),
9075
+ hint: "Provider is waiting for your input. Verify the question makes sense for your task before responding."
9076
+ };
9077
+ if (c.schema) action.schema = c.schema;
9078
+ return action;
9079
+ }
9080
+ case "payment-request": {
9081
+ const c = msg.content;
9082
+ const display = formatPaymentAmount(c.amount_msats, c.fiat_amount);
9083
+ return {
9084
+ type: "pay",
9085
+ job_id: jobId,
9086
+ amount_msats: c.amount_msats,
9087
+ // Mirror only a fiat figure the display actually led with: echoing a
9088
+ // zero amount would tell an agent doing budget math the charge is free.
9089
+ ...c.fiat_amount && c.fiat_amount.amount > 0 ? { fiat_amount: c.fiat_amount } : {},
9090
+ display,
9091
+ reason: asProviderText(c.reason),
9092
+ hint: `Payment required to continue: ${display}. Relay that amount to your human and get approval, then run 'dvm pay ${jobId}'. Provider's stated reason is wrapped as { _source: "provider", text: ... } \u2014 verify it matches the work you expect.`
9093
+ };
9094
+ }
9095
+ case "working": {
9096
+ const c = msg.content;
9097
+ return {
9098
+ type: "poll",
9099
+ job_id: jobId,
9100
+ estimate_seconds: c.estimate_seconds,
9101
+ hint: c.hint !== void 0 ? asProviderText(c.hint) : "Provider is working, check back later"
9102
+ };
9103
+ }
9104
+ }
9105
+ }
9106
+ return null;
9107
+ }
9108
+
9109
+ // src/lib/wallet-probe.ts
9110
+ async function probeWallets(opts = {}) {
9111
+ const timeoutMs = opts.timeoutMs ?? 5e3;
9112
+ const config = loadConfig();
9113
+ const cashuMints = await loadConfiguredMints();
9114
+ const walletType = detectWalletType(config, cashuMints);
9115
+ const rate = await fetchBtcUsdRate().catch(() => null);
9116
+ const probes = await Promise.allSettled([
9117
+ probeLightning(timeoutMs),
9118
+ probeX402(config?.x402, timeoutMs),
9119
+ probeMpp(config?.tempo, timeoutMs)
9120
+ ]);
9121
+ const rails = [];
9122
+ const cashuRow = probeCashu(cashuMints);
9123
+ if (cashuRow) rails.push(cashuRow);
9124
+ for (const result of probes) {
9125
+ if (result.status === "fulfilled" && result.value !== null) {
9126
+ rails.push(result.value);
9127
+ }
9128
+ }
9129
+ const totalLnAndCashuMsats = rails.filter((r) => r.rail === "lightning" || r.rail === "cashu").reduce((sum, r) => sum + (r.balanceMsats ?? 0), 0);
9130
+ return { walletType, rate, rails, totalLnAndCashuMsats, cashuMints };
9131
+ }
9132
+ async function probeLightning(timeoutMs) {
9133
+ let float;
9134
+ try {
9135
+ float = loadFloat();
9136
+ } catch (err) {
9137
+ const mapped = toFloatError(err);
9138
+ return {
9139
+ rail: "lightning",
9140
+ configured: true,
9141
+ reachable: false,
9142
+ error: {
9143
+ code: mapped.code,
9144
+ message: mapped.message,
9145
+ // The same drop fixed one branch down: `float_invalid_uri` knows to say
9146
+ // "reconnect with a URI copied whole", and the row was rendering the
9147
+ // generic rail hint over the top of it (internal-review).
9148
+ ...mapped.hint ? { hint: mapped.hint } : {},
9149
+ ...refusalFields(mapped.data)
9150
+ }
9151
+ };
9152
+ }
9153
+ if (!float) return null;
9154
+ const probe = await withTimeout(probeFloat({ float, timeoutMs }), timeoutMs, "lightning").catch(
9155
+ (err) => ({
9156
+ reachable: false,
9157
+ methods: [],
9158
+ canPay: false,
9159
+ canInvoice: false,
9160
+ canReconcile: false,
9161
+ error: {
9162
+ code: "float_wallet_offline",
9163
+ message: err instanceof Error ? err.message : String(err),
9164
+ display: "Your Lightning wallet didn't answer in time."
9165
+ }
9166
+ })
9167
+ );
9168
+ const details = {
9169
+ alias: probe.alias ?? null,
9170
+ methods: probe.methods,
9171
+ can_pay: probe.canPay,
9172
+ can_reconcile: probe.canReconcile,
9173
+ stated_budget: float.statedBudget ?? null
9174
+ };
9175
+ if (probe.walletReportedBudget) details.wallet_reported_budget = probe.walletReportedBudget;
9176
+ if (probe.reachable && probe.balanceMsats === void 0) details.balance_unknown = true;
9177
+ if (!probe.reachable) {
9178
+ return {
9179
+ rail: "lightning",
9180
+ configured: true,
9181
+ reachable: false,
9182
+ details,
9183
+ error: {
9184
+ code: probe.error?.code ?? "float_wallet_offline",
9185
+ message: probe.error?.message ?? "The Lightning wallet did not respond.",
9186
+ // Lightning was the one rail dropping the probe's own diagnosis here,
9187
+ // so every float failure rendered as the generic "re-connect" remedy —
9188
+ // the wrong move for a connection the wallet refused on a permission
9189
+ // (internal-review). x402 and Tempo already forward theirs via `classifyError`.
9190
+ ...probe.error?.hint ? { hint: probe.error.hint } : {},
9191
+ ...refusalFields(probe.error?.data)
9192
+ }
9193
+ };
9194
+ }
9195
+ return {
9196
+ rail: "lightning",
9197
+ configured: true,
9198
+ reachable: true,
9199
+ // Left absent, never coerced to 0 — see `details.balance_unknown` above.
9200
+ ...probe.balanceMsats === void 0 ? {} : { balanceMsats: probe.balanceMsats },
9201
+ details
9202
+ };
9203
+ }
9204
+ function refusalFields(data) {
9205
+ const text = (key) => {
9206
+ const value = data?.[key];
9207
+ return typeof value === "string" ? value : void 0;
9208
+ };
9209
+ const method = text("method");
9210
+ const restriction = text("restriction");
9211
+ const walletMessage = text("wallet_message");
9212
+ return {
9213
+ ...method ? { method } : {},
9214
+ ...restriction ? { restriction } : {},
9215
+ ...walletMessage ? { wallet_message: walletMessage } : {}
9216
+ };
9217
+ }
9218
+ function probeCashu(cashuMints) {
9219
+ const configured = (cashuMints?.length ?? 0) > 0;
9220
+ const balanceMsats = getBalance2();
9221
+ if (!configured && balanceMsats === 0) return null;
9222
+ const byMint = getBalanceByMint();
9223
+ const byMintObj = {};
9224
+ for (const url of cashuMints ?? []) {
9225
+ byMintObj[url] = byMint.get(url) ?? 0;
9226
+ }
9227
+ for (const [url, msats] of byMint) {
9228
+ if (!(url in byMintObj)) byMintObj[url] = msats;
9229
+ }
9230
+ return {
9231
+ rail: "cashu",
9232
+ configured,
9233
+ reachable: true,
9234
+ balanceMsats,
9235
+ details: { byMint: byMintObj }
9236
+ };
9237
+ }
9238
+ async function probeX402(x402, timeoutMs) {
9239
+ if (!x402) return null;
9240
+ let network;
9241
+ let address;
9242
+ try {
9243
+ const { loadX402Wallet: loadX402Wallet2, getX402Balance: getX402Balance2, ensureAddress: ensureAddress2, usdcContractByCaip2: usdcContractByCaip22 } = await import("../x402-FTG2GRAQ.js");
9244
+ const wallet = loadX402Wallet2();
9245
+ if (!wallet) {
9246
+ const storedKey = x402.privateKey;
9247
+ const missing = storedKey === void 0 || storedKey === "";
9248
+ return {
9249
+ rail: "x402",
9250
+ configured: true,
9251
+ reachable: false,
9252
+ ...x402.network ? { details: { network: x402.network } } : {},
9253
+ error: missing ? {
9254
+ code: "x402_key_missing",
9255
+ message: "x402 config is present but privateKey is missing or empty.",
9256
+ hint: "Run 'dvm wallet x402-connect' to create a wallet, or add --key-file <path> to import a key you already hold."
9257
+ } : {
9258
+ code: "x402_key_invalid",
9259
+ message: "x402 config contains a privateKey that is not a valid secp256k1 scalar.",
9260
+ hint: "Run 'dvm wallet x402-connect' to create a replacement wallet, or add --key-file <path> to import a valid key you already hold."
9261
+ }
9262
+ };
9263
+ }
9264
+ network = wallet.network;
9265
+ const w = await withTimeout(ensureAddress2(wallet), timeoutMs, "x402");
9266
+ address = w.address;
9267
+ const balance = await withTimeout(getX402Balance2(w), timeoutMs, "x402");
9268
+ return {
9269
+ rail: "x402",
9270
+ configured: true,
9271
+ reachable: true,
9272
+ balanceUsdc: balance.usdc,
9273
+ // internal-review: record the contract the balance was actually read against —
9274
+ // a zero here only means "none of *this* token, on *this* chain". Map
9275
+ // lookup off the network already resolved; no second RPC.
9276
+ details: {
9277
+ address: w.address,
9278
+ network: w.network,
9279
+ token: usdcContractByCaip22(w.network),
9280
+ // internal-review: and the endpoint that answered, which the caller can
9281
+ // point elsewhere — a wrong one reads as a plausible $0.00 otherwise.
9282
+ // Redacted here, at the boundary where the URL becomes display.
9283
+ ...rpcDetails(balance.rpcUrlRedacted, balance.rpcSource)
9284
+ }
9285
+ };
9286
+ } catch (err) {
9287
+ const details = { ...address ? { address } : {}, ...network ? { network } : {} };
9288
+ return {
9289
+ rail: "x402",
9290
+ configured: true,
9291
+ reachable: false,
9292
+ ...Object.keys(details).length > 0 ? { details } : {},
9293
+ error: classifyError("x402_unreachable", err)
9294
+ };
9295
+ }
9296
+ }
9297
+ async function probeMpp(mpp, timeoutMs) {
9298
+ if (!mpp) return null;
9299
+ if (mpp.method === "tempo") {
9300
+ try {
9301
+ const { connectedTempoAccount: connectedTempoAccount2, getTempoUsdcBalance: getTempoUsdcBalance2, TEMPO_CHAIN_ID: TEMPO_CHAIN_ID2 } = await import("../tempo-wallet-QOLEIPCV.js");
9302
+ const account = await connectedTempoAccount2();
9303
+ if (!account) {
9304
+ return {
9305
+ rail: "tempo",
9306
+ configured: true,
9307
+ reachable: false,
9308
+ error: {
9309
+ code: "tempo_key_missing",
9310
+ message: "Tempo config is present but apiKey is missing or not a private key.",
9311
+ hint: "Run 'dvm wallet tempo-connect' to generate a key, or add '--key-file <path>' to import one you already hold (DVM_TEMPO_KEY and piped stdin work too)."
9312
+ }
9313
+ };
9314
+ }
9315
+ try {
9316
+ const balance = await withTimeout(getTempoUsdcBalance2(account), timeoutMs, "tempo");
9317
+ return {
9318
+ rail: "tempo",
9319
+ configured: true,
9320
+ reachable: true,
9321
+ balanceUsdc: balance.usdc,
9322
+ // internal-review: record the contract the balance was actually read
9323
+ // against — a zero here only means "none of *this* token".
9324
+ details: {
9325
+ address: account.address,
9326
+ chain_id: TEMPO_CHAIN_ID2,
9327
+ token: balance.tokenAddress,
9328
+ // internal-review: as on x402 above — which endpoint the number came from.
9329
+ ...rpcDetails(balance.rpcUrlRedacted, balance.rpcSource)
9330
+ }
9331
+ };
9332
+ } catch (err) {
9333
+ return {
9334
+ rail: "tempo",
9335
+ configured: true,
9336
+ reachable: false,
9337
+ balanceUsdc: void 0,
9338
+ details: { address: account.address, chain_id: TEMPO_CHAIN_ID2, balance_unknown: true },
9339
+ error: classifyError("tempo_unreachable", err)
9340
+ };
9341
+ }
9342
+ } catch (err) {
9343
+ return {
9344
+ rail: "tempo",
9345
+ configured: true,
9346
+ reachable: false,
9347
+ error: classifyError("tempo_unavailable", err)
9348
+ };
9349
+ }
9350
+ }
9351
+ return {
9352
+ rail: "tempo",
9353
+ configured: true,
9354
+ reachable: false,
9355
+ error: { code: "tempo_unknown_method", message: `unknown Tempo method: ${mpp.method}` }
9356
+ };
9357
+ }
9358
+ function withTimeout(p, ms, rail) {
9359
+ return new Promise((resolve, reject) => {
9360
+ const t = setTimeout(() => {
9361
+ reject(new Error(`${rail} probe timed out after ${ms}ms`));
9362
+ }, ms);
9363
+ p.then(
9364
+ (v) => {
9365
+ clearTimeout(t);
9366
+ resolve(v);
9367
+ },
9368
+ (e) => {
9369
+ clearTimeout(t);
9370
+ reject(e instanceof Error ? e : new Error(String(e)));
9371
+ }
9372
+ );
9373
+ });
9374
+ }
9375
+ function rpcDetails(rpcUrlRedacted, rpcSource) {
9376
+ if (!rpcUrlRedacted) return {};
9377
+ return { rpc_url: rpcUrlRedacted, rpc_source: rpcSource };
9378
+ }
9379
+ function classifyError(code, err) {
9380
+ const message = err instanceof Error ? err.message : String(err);
9381
+ if (err instanceof DvmError) {
9382
+ return { code: err.code, message, ...err.hint ? { hint: err.hint } : {} };
9383
+ }
9384
+ return { code, message };
9385
+ }
6069
9386
  export {
6070
9387
  ADMIN_STATE_UNAVAILABLE,
9388
+ AGENT_MNEMONIC_FILE,
9389
+ BTC_RATE_FILE,
9390
+ BUDGET_FILE,
6071
9391
  CAPABILITY_NAME_RE,
6072
9392
  COINGECKO_SIMPLE_PRICE_URL,
9393
+ CONFIG_AUDIT_FILE,
9394
+ CONFIG_BACKUP_FILE,
9395
+ CONFIG_DIR,
9396
+ CONFIG_FILE,
9397
+ CONFIG_PINNED_FILE,
9398
+ CREDITS_FILE,
6073
9399
  CREDIT_DRAIN_RAILS,
6074
9400
  CREDIT_FUNDING_MODES,
6075
9401
  CREDIT_FUND_RAILS,
9402
+ CREDIT_POSTURES,
9403
+ DEFAULT_AGENT_MINT_URL,
9404
+ DEFAULT_CREDIT_POSTURE,
6076
9405
  DEFAULT_LOCK_PUBKEY_GRACE_SECONDS,
9406
+ DEFAULT_RESERVE_LOCKTIME_SECONDS,
6077
9407
  DbTransport,
6078
9408
  DvmError,
6079
9409
  DvmX402ChannelStorage,
9410
+ FAVORITES_FILE,
9411
+ FEEDBACK_NUDGES_FILE,
9412
+ FEEDBACK_POSTURES,
6080
9413
  FX_CACHE_TTL_MS,
6081
9414
  FX_RETRY_COUNT,
9415
+ IDENTITIES_FILE,
9416
+ IDENTITIES_VERSION,
6082
9417
  InvalidUsdPriceError,
9418
+ JOBS_FILE,
6083
9419
  JobCancelledError,
9420
+ MESSAGES_DIR,
6084
9421
  MemoryKVStore,
6085
9422
  MintHealthTracker,
6086
9423
  NON_CHANNEL_BITCOIN_RAILS,
9424
+ NWC_FUNDING_FLOAT_ROLE,
6087
9425
  NwcBackend,
6088
9426
  NwcError,
9427
+ PAIRINGS_DIR,
6089
9428
  PAYMENT_PROOF_KEYS,
9429
+ PRIVKEY_HEX_RE,
6090
9430
  PayoutReporter,
9431
+ PersistentCounterSource,
6091
9432
  PostgresTempoChargeStore,
6092
9433
  PostgresTempoSessionStore,
9434
+ RAIL_FLAGS,
6093
9435
  RAIL_REFUNDABLE,
9436
+ RECEIPTS_FILE,
6094
9437
  RECENT_SPENDS_CAP,
9438
+ RECOMMENDED_MINTS,
9439
+ RECOVERY_MNEMONIC_FILE,
9440
+ REQUEST_INTENTS_DIR,
9441
+ RESUME_MAX_AGE_MS,
9442
+ RESUME_MAX_ATTEMPTS,
6095
9443
  ROUTER_FEEDBACK_CLAIM_HEADER,
6096
9444
  ROUTER_FEEDBACK_CLAIM_TTL_MS,
6097
9445
  RevenueReporter,
@@ -6103,15 +9451,20 @@ export {
6103
9451
  SpanWriter,
6104
9452
  StepCache,
6105
9453
  TEMPO_CHAIN_ID,
9454
+ TEMPO_CHANNELS_FILE,
6106
9455
  TEMPO_CLOSE_DB_RECOVERY_BUDGET_MS,
6107
9456
  TEMPO_CLOSE_REQUESTED_TOPIC,
9457
+ TEMPO_EXIT_KEY_MISSING_HINT,
6108
9458
  TempoSessionChannelMismatchError,
9459
+ WALLET_FILE,
9460
+ WALLET_VERSION,
6109
9461
  X402EnvValidationError,
6110
9462
  X402FacilitatorHealth,
6111
9463
  X402_BATCH_CHANNEL_ABI,
6112
9464
  X402_BATCH_SETTLEMENT_KEY,
6113
9465
  X402_BATCH_SETTLEMENT_MAINNET_NETWORK,
6114
9466
  X402_BATCH_SETTLEMENT_SCHEME,
9467
+ X402_CHANNELS_FILE,
6115
9468
  X402_DEFAULT_FACILITATOR,
6116
9469
  X402_DEFAULT_NETWORK,
6117
9470
  X402_EXACT_AUTHORIZATION_TTL_SECONDS,
@@ -6125,47 +9478,81 @@ export {
6125
9478
  X402_V1_VERSION,
6126
9479
  X402_VERSION,
6127
9480
  _testing,
9481
+ abandonPendingSubmission,
6128
9482
  abortJob,
6129
9483
  acknowledgeReplayedX402Payment,
6130
9484
  acknowledgeX402ChannelOwner,
6131
9485
  activePubkeySet,
9486
+ addPayment,
9487
+ advanceTempoChannelExit,
9488
+ amountBoundsVerdict,
9489
+ applyImplicitReceiptCredit,
6132
9490
  applyPaymentInfoToJob,
9491
+ applyReceiptCredit,
6133
9492
  applyRetryToPool,
6134
9493
  asProviderText,
9494
+ asProviderTextOpt,
9495
+ assertAgentMnemonicReplaceable,
9496
+ assertFloatBudget,
9497
+ assertNutSupport,
6135
9498
  assertRevenueReporterReady,
6136
9499
  assertSupportedX402Network,
9500
+ assertTempoWalletIdle,
6137
9501
  assertValidBudget,
9502
+ assertX402ChannelPayer,
6138
9503
  assertX402RequirementDescribesChannel,
9504
+ assertX402WalletIdle,
6139
9505
  attestedDvmIdentity,
6140
9506
  authenticatedBuilderPubkey,
9507
+ authorizeProtectedRemoval,
9508
+ automaticCreditBoundRail,
6141
9509
  availableRails,
9510
+ backfillTrustAnchor,
6142
9511
  backupSecretFile,
6143
9512
  buildAdminAuthHeader,
6144
9513
  buildAdminChallenge,
6145
9514
  buildAttestation,
9515
+ buildCompletionHintFields,
9516
+ buildDisplayMintInventory,
6146
9517
  buildPaymentErrorResponse,
6147
9518
  buildPaymentRequiredV2,
6148
9519
  buildPaymentRequirements,
9520
+ buildPolicy,
6149
9521
  buildTempoCredential,
6150
9522
  builderLockPath,
6151
9523
  caip2ToX402Network,
6152
9524
  canReplaceExpiredX402Deposit,
6153
9525
  canSwap,
9526
+ canonicalRequestPath,
6154
9527
  canonicaliseForSigning,
6155
9528
  canonicalize,
9529
+ captureFundingReceipt,
9530
+ captureTransportReceipt,
9531
+ cashOutFromMint,
6156
9532
  cashuSubmissionBindingFingerprint,
6157
9533
  chainIdFromCaip2,
6158
9534
  checkMeltQuoteState,
6159
9535
  checkMintHealth,
6160
9536
  checkReceiptAttestation,
6161
9537
  clearAccumulatorForDvm,
9538
+ clearCreditSelection,
6162
9539
  clearPendingDrain,
6163
9540
  clearPendingX402ExactFunding,
6164
9541
  clearPendingX402Funding,
9542
+ clearRefillRailUnavailable,
9543
+ generateIdentity2 as cliIdentityStoreGenerateIdentity,
9544
+ commitAgentSpend,
9545
+ completionFromMessages,
9546
+ completionSnapshot,
9547
+ computeNextAction,
6165
9548
  computeResultHash,
6166
9549
  computeSmartDenominations,
9550
+ configSnapshotPath,
9551
+ connectedTempoAccount,
6167
9552
  connectedX402Payer,
9553
+ correctPendingFundAmount,
6168
9554
  costAnchor,
9555
+ coveringMintsForSpend,
6169
9556
  createAdminCashuNonceStore,
6170
9557
  createDVMServer,
6171
9558
  createDefaultReplayStore,
@@ -6173,55 +9560,90 @@ export {
6173
9560
  createMonotonicClock,
6174
9561
  createNoopLogger,
6175
9562
  createStdoutLogger,
9563
+ createTempoCloseCredential,
6176
9564
  createX402BatchPayment,
6177
9565
  creditDepositPayload,
6178
9566
  currentContext,
6179
9567
  decodePayment,
6180
9568
  decodePaymentRequiredHeader,
9569
+ deleteAgentMnemonic,
6181
9570
  depleteLots,
9571
+ deriveAgentMasterKeypair,
6182
9572
  deriveBuilderLockKeypair,
6183
9573
  deriveBuilderLockPubkey,
9574
+ deriveIdentityFromPrivkey,
9575
+ derivePubkeyFromPrivkey,
6184
9576
  deriveReceiptKeypair,
6185
9577
  deriveReceiptSecret,
9578
+ deriveTempoAddress,
6186
9579
  derivedFundCreditId,
9580
+ describeConfigBackup,
9581
+ describeMintBounds,
6187
9582
  devModeSkipsPaymentVerification,
6188
9583
  drainReceiptsDisplay,
6189
9584
  drainReceiptsHint,
9585
+ dvmLocationIdentity,
9586
+ effectiveCreditPosture,
9587
+ effectiveFundingMint,
9588
+ effectiveFundingRail,
9589
+ effectiveMints,
9590
+ emitFloatBudgetWarning,
6190
9591
  encodePayment,
6191
9592
  encodePaymentRequiredHeader,
6192
9593
  encodeSettleResponseHeader,
6193
9594
  enforceGlobalBudget,
6194
9595
  ensureAddress,
9596
+ ensureConfig,
9597
+ ensureConfigDir,
9598
+ ensureConfigSubdir,
6195
9599
  ensureProvisionalCredit,
9600
+ entryKind,
6196
9601
  epochMsToIso,
6197
9602
  exactEvmAuthorization,
9603
+ extractBuilderHandle,
6198
9604
  fetchBtcUsdRate,
6199
9605
  fetchCoinGeckoBtcUsd,
6200
9606
  fetchCoinGeckoSpot,
6201
9607
  fetchLnurlPayInvoice,
6202
9608
  fetchMeltQuote,
9609
+ fetchMissingJobReceipt,
9610
+ fetchTrustAnchor,
6203
9611
  fifoOrder,
9612
+ findSigningIdentityByPubkey,
6204
9613
  findX402Channel,
9614
+ floatBudgetJson,
9615
+ floatPaymentDisplay,
9616
+ floatPaymentJson,
9617
+ forgetCredit,
6205
9618
  formatAmount,
6206
9619
  formatAmountBestEffort,
9620
+ formatLocktime,
6207
9621
  formatPaymentAmount,
6208
9622
  formatSats,
6209
9623
  formatStaleness,
6210
9624
  formatUsd,
6211
9625
  formatUsdcMicrounits,
6212
9626
  fromJobRecord,
9627
+ fundAgentWallet,
6213
9628
  fundCredit,
9629
+ fundWireAmountMicro,
6214
9630
  fundedMicroFor,
6215
9631
  fundingModeForTempoIntent,
6216
9632
  fundingModeForX402Flavour,
6217
9633
  fundingRailLabel,
9634
+ generateAgentMnemonic,
6218
9635
  generateEphemeralReceiptKey,
6219
9636
  generateIdentity,
9637
+ generateTempoKey,
6220
9638
  generateX402PrivateKey,
6221
9639
  getBalance,
9640
+ getBalanceByMint,
9641
+ getFundingGuidance,
6222
9642
  getInfo,
6223
9643
  getInputFeesForProofs,
6224
9644
  getLastKnownBtcRate,
9645
+ getOutputTraceId,
9646
+ getTempoUsdcBalance,
6225
9647
  getWallet,
6226
9648
  getX402Balance,
6227
9649
  hasPaymentProof,
@@ -6229,8 +9651,10 @@ export {
6229
9651
  hashLockKey,
6230
9652
  hashedCanonicalDvmId,
6231
9653
  humanizeZodError,
9654
+ identitiesFileExists,
6232
9655
  implicitCreditId,
6233
9656
  inKindDrawMsats,
9657
+ info,
6234
9658
  initLockPubkeyTable,
6235
9659
  initLoggers,
6236
9660
  initWalletAccumulatorTable,
@@ -6239,74 +9663,134 @@ export {
6239
9663
  installAdminCashuRoutes,
6240
9664
  isAbortError,
6241
9665
  isArtifactMessage,
9666
+ isCancelMessage,
9667
+ isCompleteMessage,
6242
9668
  isCreditFundRail,
6243
9669
  isCreditFundingMode,
9670
+ isCreditPosture,
6244
9671
  isDerivedCreditId,
6245
9672
  isDrainReceipt,
9673
+ isFeedbackPosture,
6246
9674
  isFundingReceipt,
6247
9675
  isInKindDepletion,
6248
9676
  isNonChannelBitcoinRail,
9677
+ isPaymentRailKey,
6249
9678
  isPaymentRequestMessage,
6250
9679
  isPromptMessage,
9680
+ isPublicRequestId,
9681
+ isResumable,
6251
9682
  isSignedJobReceipt,
6252
9683
  isStablecoinFundRail,
6253
9684
  isStaleJobReapable,
6254
9685
  isTerminal,
9686
+ isTerminalJobStatus,
9687
+ isTextMessage,
9688
+ isValidEvmPrivateKey,
9689
+ isWorkingMessage,
6255
9690
  isYieldMessage,
6256
9691
  issueRouterFeedbackClaim,
6257
9692
  issueUpfrontChallenges,
6258
9693
  jobCredentialGate,
9694
+ json,
9695
+ listCredits,
9696
+ listFundingMenuSnapshots,
9697
+ listImplicitCredits,
9698
+ listPendingFundings,
6259
9699
  listPendingX402Fundings,
6260
9700
  listPendingX402FundingsForChannel,
9701
+ listResumableTempoChargeFundings,
6261
9702
  listResumableX402ExactFundings,
6262
9703
  listResumableX402Fundings,
6263
9704
  listTempoChannelAssociations,
6264
9705
  listX402Channels,
6265
9706
  loadAgentWallet,
9707
+ loadAllJobs,
9708
+ loadConfig,
6266
9709
  loadConfiguredMints,
6267
9710
  loadCredit,
9711
+ loadFloat,
9712
+ loadIdentities,
6268
9713
  loadIdentitySecret,
9714
+ loadImplicitCredits,
9715
+ loadJob,
6269
9716
  loadLedger,
6270
9717
  loadLockPubkeyState,
9718
+ loadPendingFunding,
9719
+ loadReceipt,
9720
+ loadReceipts,
6271
9721
  loadResumableX402ExactFunding,
6272
9722
  loadResumableX402Funding,
9723
+ loadTempoChannelAssociation,
9724
+ loadTempoChannelController,
6273
9725
  loadTempoChannelEntry,
6274
9726
  loadX402Wallet,
6275
9727
  toLockPubkey as lockPubkeyStoreToLockPubkey,
6276
9728
  loggers,
9729
+ looksLikeMnemonic,
6277
9730
  lookupInvoice,
6278
9731
  lotOwedSats,
6279
9732
  makeInvoice,
9733
+ markCredit,
9734
+ markImplicitCreditLost,
9735
+ markJobTerminal,
9736
+ markRefillDrainNoticed,
9737
+ markRefillRailUnavailable,
6280
9738
  maxKeysetDenomination,
9739
+ microToSats,
6281
9740
  missingDleqRecoveryDiagnostic,
9741
+ mnemonicFingerprint,
6282
9742
  monetaryJson,
6283
9743
  msatsToFiatMicro,
9744
+ msatsToMicro,
6284
9745
  msatsToUsd,
6285
9746
  msatsToUsdCents,
6286
9747
  msatsToUsdc,
9748
+ needsPocketRefill,
9749
+ needsTopup,
6287
9750
  netOwedSats,
9751
+ nextStepHint,
9752
+ normalizeCreditEndpoint,
6288
9753
  normalizeFeedbackEndpointHost,
6289
9754
  nwcDedupWindowMs,
9755
+ nwcRejectionDisplay,
6290
9756
  nwcTimeoutMs,
9757
+ offersTempoCreditFunding,
6291
9758
  offersX402CreditFunding,
6292
9759
  parseBudget,
6293
9760
  parseCreditMenu,
6294
9761
  parseCreditTerms,
6295
9762
  parseLnAddress,
9763
+ parseLocktime,
6296
9764
  parseNwcUri,
9765
+ parseStatedBudget,
9766
+ parseTraceparent,
6297
9767
  parseUsdPrice,
6298
9768
  payInvoiceReconciling,
9769
+ payViaFloat,
6299
9770
  paymentErrorBody,
9771
+ paymentRailLabel,
6300
9772
  paymentRequiredV2FromV1,
9773
+ pendingDrainById,
6301
9774
  pendingDrainFor,
6302
9775
  pickCreditFundRail,
6303
9776
  pinAskFiat,
9777
+ pinConfigSnapshot,
6304
9778
  pinDimension,
9779
+ planConfigReset,
6305
9780
  planCreditForQuote,
9781
+ planProbeDraw,
9782
+ pocketTargetMicro,
9783
+ pollNextAction,
6306
9784
  pollPendingCreditFundings,
6307
9785
  preferLightningFirst,
9786
+ prepareAgentSpend,
6308
9787
  prepareP2PKMint,
9788
+ previewConfigRollback,
6309
9789
  priceFiatMicro,
9790
+ printFundingGuidance,
9791
+ probeFloat,
9792
+ probeRailLabel,
9793
+ probeWallets,
6310
9794
  processIncomingPayment,
6311
9795
  processX402BatchCorrection,
6312
9796
  processX402BatchResponse,
@@ -6315,39 +9799,78 @@ export {
6315
9799
  providerProseSources,
6316
9800
  providerTextToString,
6317
9801
  pruneRetired,
9802
+ railLabelAtSentenceStart,
6318
9803
  randomTraceId,
6319
9804
  rateUnavailablePaymentError,
9805
+ raw,
9806
+ readFundValuation,
9807
+ readX402ChannelOnChain,
6320
9808
  receiptDisplay,
6321
9809
  receiptFailureDetail,
6322
9810
  receiptHint,
6323
9811
  receiptIssuedAtIso,
9812
+ receiptJsonFields,
6324
9813
  receiptPubkeyFromSecret,
9814
+ receiptTrustFields,
9815
+ receiveToAgentWallet,
9816
+ reclaimAtMs,
6325
9817
  reconcileRefusedX402Deposits,
9818
+ reconcileTempoChannelOnChain,
9819
+ recordBalanceResponse,
6326
9820
  recordCreditX402Instrument,
9821
+ recordDrainReceipts,
9822
+ recordFloatSpend,
9823
+ recordFundResponse,
9824
+ recordFundingMenuSnapshot,
6327
9825
  recordPendingDrain,
6328
9826
  recordPendingX402ExactFunding,
6329
9827
  recordPendingX402Funding,
9828
+ recordRefusedDrawBalance,
6330
9829
  recordSpend,
6331
9830
  recordX402ChannelMetadata,
9831
+ recoverPendingSubmission,
9832
+ redactUrl,
9833
+ redactUrlsInText,
9834
+ refFromJob,
6332
9835
  refundX402BatchChannel,
9836
+ releaseAgentReserve,
6333
9837
  removeX402Wallet,
9838
+ renderReceiptHuman,
9839
+ repairConfigDirMode,
6334
9840
  repairX402ExactSettlementEffect,
6335
9841
  repinX402WalletNetwork,
9842
+ replayStoredReceipt,
9843
+ reserveAgentWallet,
9844
+ resolveBtcUsdRate,
9845
+ resolveFundMint,
6336
9846
  resolveFxSnapshot,
6337
9847
  resolveLnurlPayMetadataUrl,
6338
9848
  resolvePriceFiat,
6339
9849
  resolveRpcHttpTransportOptions,
6340
9850
  resolveRpcOverride,
9851
+ resolveSigningIdentity,
9852
+ resolveTempoAsset,
9853
+ resolveX402Asset,
9854
+ resolveX402ChannelRpc,
6341
9855
  restoreProofsFromOutputs,
9856
+ retireConfigBackup,
6342
9857
  retirePendingX402Funding,
6343
9858
  revenueReporterBannerState,
9859
+ rollbackConfig,
6344
9860
  rotateLockPubkey,
9861
+ rpcEndpointLine,
9862
+ rpcEndpointSuffix,
6345
9863
  saveAgentWallet,
9864
+ saveIdentities,
9865
+ saveJob,
9866
+ saveLedger,
6346
9867
  saveX402Wallet,
6347
9868
  seedLockPubkeyState,
6348
9869
  selectProofsForSpend,
6349
9870
  serializeOutputData,
6350
9871
  setEmitter,
9872
+ setOutputTraceId,
9873
+ settleDrainEvidence,
6351
9874
  settleWithFacilitator,
6352
9875
  shellQuoteArg,
6353
9876
  signAttestation,
@@ -6357,20 +9880,49 @@ export {
6357
9880
  signReceipt,
6358
9881
  signRequestData,
6359
9882
  signedRequestDomain,
9883
+ spendableAt,
9884
+ staleRateNotice,
9885
+ statusSnapshot,
6360
9886
  swapFeeForProofs,
6361
9887
  swapSendReserve,
9888
+ sweepPendingMelts,
9889
+ sweepPendingMints,
9890
+ sweepPendingSubmissions,
9891
+ sweepStaleInFlight,
9892
+ tempoBalanceCheckHint,
9893
+ tempoChannelControlledBy,
6362
9894
  tempoCredentialAmountMicro,
9895
+ tempoKeyImportRoutes,
9896
+ tempoSessionChallenge,
9897
+ tempoSessionChallengeMicro,
9898
+ tempoTokenReference,
9899
+ tempoWrongAssetHint,
9900
+ tierCapOverrides,
9901
+ toAgentProof,
6363
9902
  toCreditTerms,
9903
+ toFloatError,
6364
9904
  toJobRecord,
6365
9905
  toLockPubkey,
6366
9906
  transportErrorCode,
9907
+ trustLadderFor,
9908
+ trustOverrideCapMicro,
9909
+ tryResolveBtcUsdRate,
9910
+ tryResolveTempoAsset,
9911
+ tryX402Payment,
6367
9912
  unknownRouteNotFound,
9913
+ unreachableRailDisplay,
9914
+ unreachableRailHint,
9915
+ unrecordedFloatBudgetWarning,
9916
+ unsignRequestData,
9917
+ updateConfig,
9918
+ updateJob,
6368
9919
  usdToMsats,
6369
9920
  usdcContractByCaip2,
6370
9921
  usdcContractFor,
6371
9922
  usdcDomainNameFor,
6372
9923
  usdcDomainVersionFor,
6373
9924
  usdcToMsats,
9925
+ validateAgentMnemonic,
6374
9926
  validateLnAddress,
6375
9927
  validateX402Env,
6376
9928
  verifyAttestation,
@@ -6388,17 +9940,27 @@ export {
6388
9940
  verifyUpfrontPayment,
6389
9941
  verifyWithFacilitator,
6390
9942
  verifyX402Payment,
9943
+ walletExists,
6391
9944
  warmFxSnapshot,
6392
9945
  weakestReceiptVerdict,
9946
+ windowStart,
9947
+ withIdentitiesLock,
6393
9948
  withInitRetry,
6394
9949
  withNwcPool,
9950
+ withTempoChannelLock,
6395
9951
  withTraceContext,
6396
9952
  withWalletLock,
6397
9953
  withX402ChannelLock,
6398
9954
  wrapMppx,
9955
+ writeAgentMnemonic,
6399
9956
  writeIdentity,
6400
9957
  x402ChannelBalanceUnreconciledAt,
9958
+ x402ChannelControlledBy,
9959
+ x402ChannelReservation,
9960
+ x402CreditChannelNetworks,
9961
+ x402CreditExactNetworks,
6401
9962
  x402ExactFundingDead,
9963
+ x402FlavourForInstrument,
6402
9964
  x402NetworkByCaip2,
6403
9965
  x402NetworkBySlug,
6404
9966
  x402NetworkLabel,
@@ -6412,7 +9974,9 @@ export {
6412
9974
  x402PendingUnsettled,
6413
9975
  x402RequiredFromError,
6414
9976
  x402RequiredUsdcMicro,
9977
+ x402ReservationRelease,
6415
9978
  x402SettledShare,
6416
9979
  x402SupportedNetworksHint,
6417
- x402VoucherPendingRefusal
9980
+ x402VoucherPendingRefusal,
9981
+ x402WrongAssetHint
6418
9982
  };