@dvmkit/sdk 0.1.0-rc.4 → 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-EDU6COY2.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,29 +111,74 @@ import {
72
111
  x402PendingFundingJson,
73
112
  x402PendingRetired,
74
113
  x402PendingStale,
75
- x402PendingUnsettled
76
- } from "../chunk-GJD7PVWY.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,
123
+ clearPendingDrain,
79
124
  clearPendingFunding,
125
+ clearRefillRailUnavailable,
126
+ dvmLocationIdentity,
127
+ ensureProvisionalCredit,
128
+ forgetCredit,
129
+ getOutputTraceId,
80
130
  info,
81
- isCreditPosture,
82
- isValidEvmPrivateKey,
131
+ json,
132
+ listCredits,
133
+ listFundingMenuSnapshots,
134
+ listImplicitCredits,
83
135
  listPendingFundings,
84
- loadConfig,
136
+ loadCredit,
85
137
  loadCreditX402Binding,
138
+ loadImplicitCredits,
86
139
  loadPendingFunding,
140
+ markCredit,
141
+ markImplicitCreditLost,
142
+ markRefillDrainNoticed,
143
+ markRefillRailUnavailable,
87
144
  maxAdvertisedMicro,
88
145
  normalizeCreditEndpoint,
89
146
  parseCreditMenu,
90
147
  parseCreditTerms,
148
+ pendingDrainById,
149
+ pendingDrainFor,
91
150
  progress,
151
+ raw,
152
+ recordBalanceResponse,
92
153
  recordCreditX402Instrument,
154
+ recordDrainReceipts,
93
155
  recordFundResponse,
156
+ recordFundingMenuSnapshot,
157
+ recordPendingDrain,
94
158
  recordPendingFunding,
95
- updateConfig,
159
+ recordRefusedDrawBalance,
160
+ setOutputTraceId,
161
+ settleDrainEvidence,
96
162
  withinAdvertisedTolerance
97
- } from "../chunk-2K6UXDAN.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";
98
182
  import {
99
183
  RevenueReporter
100
184
  } from "../chunk-2ABMGUDS.js";
@@ -110,22 +194,27 @@ import {
110
194
  import "../chunk-YG7G4DPZ.js";
111
195
  import {
112
196
  CAPABILITY_NAME_RE
113
- } from "../chunk-XYTSDAPH.js";
197
+ } from "../chunk-LDTWX7JW.js";
114
198
  import {
115
199
  ADMIN_STATE_UNAVAILABLE,
200
+ DEFAULT_AGENT_MINT_URL,
116
201
  DEFAULT_LOCK_PUBKEY_GRACE_SECONDS,
117
202
  DbTransport,
118
203
  MintHealthTracker,
119
204
  NwcBackend,
120
205
  NwcError,
121
206
  PAYMENT_PROOF_KEYS,
207
+ PRIVKEY_HEX_RE,
208
+ PersistentCounterSource,
122
209
  RECENT_SPENDS_CAP,
210
+ RECOMMENDED_MINTS,
123
211
  SPANS_INSERTED_CHANNEL,
124
212
  SpanWriter,
125
213
  TEMPO_CLOSE_DB_RECOVERY_BUDGET_MS,
126
214
  TEMPO_CLOSE_REQUESTED_TOPIC,
127
215
  TEMPO_SESSION_UPDATED_REASON,
128
216
  TempoSessionChannelMismatchError,
217
+ WALLET_VERSION,
129
218
  X402EnvValidationError,
130
219
  X402FacilitatorHealth,
131
220
  X402_BATCH_CHANNEL_ABI,
@@ -135,7 +224,10 @@ import {
135
224
  X402_RPC_URL_KEY,
136
225
  X402_SELF_RELAY_KEY,
137
226
  activePubkeySet,
227
+ amountBoundsVerdict,
138
228
  applyPaymentInfoToJob,
229
+ assertAgentMnemonicReplaceable,
230
+ assertNutSupport,
139
231
  assertRevenueReporterReady,
140
232
  attestedDvmIdentity,
141
233
  authenticatedBuilderPubkey,
@@ -153,14 +245,20 @@ import {
153
245
  createDVMServer,
154
246
  createMonotonicClock,
155
247
  creditDepositPayload,
248
+ deleteAgentMnemonic,
249
+ deriveAgentMasterKeypair,
250
+ derivePubkeyFromPrivkey,
156
251
  deriveReceiptKeypair,
157
252
  deriveReceiptSecret,
158
253
  derivedFundCreditId,
159
254
  devModeSkipsPaymentVerification,
160
255
  drainReceiptsDisplay,
161
256
  drainReceiptsHint,
257
+ effectiveFundingMint,
258
+ effectiveMints,
162
259
  epochMsToIso,
163
260
  fundedMicroFor,
261
+ generateAgentMnemonic,
164
262
  generateEphemeralReceiptKey,
165
263
  generateIdentity,
166
264
  getBalance,
@@ -178,6 +276,7 @@ import {
178
276
  isDerivedCreditId,
179
277
  isDrainReceipt,
180
278
  isFundingReceipt,
279
+ isPublicRequestId,
181
280
  isSignedJobReceipt,
182
281
  isX402SettlementReconciliationReason,
183
282
  issueUpfrontChallenges,
@@ -185,12 +284,16 @@ import {
185
284
  loadAgentWallet,
186
285
  loadIdentitySecret,
187
286
  loadLockPubkeyState,
287
+ looksLikeMnemonic,
188
288
  lookupInvoice,
189
289
  makeInvoice,
290
+ meltAmountBounds,
291
+ mnemonicFingerprint,
190
292
  msatsToFiatMicro,
191
293
  nwcDedupWindowMs,
192
294
  nwcTimeoutMs,
193
295
  parseNwcUri,
296
+ parseTraceparent,
194
297
  payInvoiceReconciling,
195
298
  paymentErrorBody,
196
299
  pinAskFiat,
@@ -218,6 +321,7 @@ import {
218
321
  toCreditTerms,
219
322
  toLockPubkey,
220
323
  unknownRouteNotFound,
324
+ validateAgentMnemonic,
221
325
  validateX402Env,
222
326
  verifyAttestation,
223
327
  verifyChallenge,
@@ -230,13 +334,15 @@ import {
230
334
  verifyReceipt,
231
335
  verifyTempoSessionManagementCredential,
232
336
  verifyUpfrontPayment,
337
+ walletExists,
233
338
  weakestReceiptVerdict,
234
339
  withNwcPool,
235
340
  withWalletLock,
341
+ writeAgentMnemonic,
236
342
  writeIdentity,
237
343
  x402RequiredUsdcMicro,
238
344
  x402SettledShare
239
- } from "../chunk-4UVRXDNY.js";
345
+ } from "../chunk-CMDI3ENK.js";
240
346
  import {
241
347
  X402_BATCH_SETTLEMENT_SCHEME,
242
348
  X402_DEFAULT_FACILITATOR,
@@ -269,15 +375,11 @@ import {
269
375
  x402NetworkListForCopy,
270
376
  x402NetworkToCaip2,
271
377
  x402SupportedNetworksHint
272
- } from "../chunk-AAJNGQMC.js";
378
+ } from "../chunk-JGGI65I3.js";
273
379
  import {
274
- BUDGET_FILE,
275
380
  COINGECKO_SIMPLE_PRICE_URL,
276
- DvmError,
277
- RECEIPTS_FILE,
278
381
  assertValidBudget,
279
382
  costAnchor,
280
- ensureConfigDir,
281
383
  fetchBtcUsdRate,
282
384
  fetchCoinGeckoBtcUsd,
283
385
  fetchCoinGeckoSpot,
@@ -289,19 +391,46 @@ import {
289
391
  formatUsd,
290
392
  formatUsdcMicrounits,
291
393
  getLastKnownBtcRate,
292
- isAbortError,
293
394
  monetaryJson,
294
395
  msatsToUsd,
295
396
  msatsToUsdCents,
296
397
  msatsToUsdc,
297
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,
298
428
  providerProseSources,
429
+ repairConfigDirMode,
299
430
  resolveRpcHttpTransportOptions,
300
431
  resolveRpcOverride,
301
- transportErrorCode,
302
- usdToMsats,
303
- usdcToMsats
304
- } from "../chunk-F2L6KIMD.js";
432
+ transportErrorCode
433
+ } from "../chunk-MKI6OVW4.js";
305
434
  import "../chunk-EXHBXA4U.js";
306
435
  import {
307
436
  RAIL_REFUNDABLE,
@@ -315,8 +444,9 @@ import {
315
444
  createDefaultReplayStore,
316
445
  humanizeZodError,
317
446
  signedRequestStatementHeader,
447
+ testOnlySignedRequestDomain,
318
448
  verifyCanonicalSignedRequest
319
- } from "../chunk-AZBXSXQT.js";
449
+ } from "../chunk-P4RUVDU7.js";
320
450
  import {
321
451
  JobCancelledError,
322
452
  MemoryKVStore,
@@ -341,6 +471,7 @@ import {
341
471
  } from "../chunk-TKA6ZP4M.js";
342
472
  import "../chunk-66HGCPBU.js";
343
473
  import {
474
+ redactUrl,
344
475
  redactUrlsInText
345
476
  } from "../chunk-FUJ36YDV.js";
346
477
  import {
@@ -472,6 +603,23 @@ async function enforceGlobalBudget(opts) {
472
603
  }
473
604
  return rate;
474
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
+ }
475
623
  function entryKind(entry) {
476
624
  return entry.kind ?? "job";
477
625
  }
@@ -673,6 +821,14 @@ function signDvmRequestData(privkeyHex, body, domain, opts = {}) {
673
821
  );
674
822
  return { ...input, auth_statement: authStatement, signature };
675
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
+ }
676
832
  function bytesToHex(bytes) {
677
833
  let s = "";
678
834
  for (const b of bytes) s += b.toString(16).padStart(2, "0");
@@ -699,6 +855,14 @@ import { Challenge, Credential } from "mppx";
699
855
  import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
700
856
 
701
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
+ }
702
866
  function floatPaymentDisplay(payment) {
703
867
  const principal = formatSats2(payment.principalMsats);
704
868
  if (payment.routingFeeMsats === null || payment.walletDebitMsats === null) {
@@ -729,6 +893,23 @@ function emitFloatBudgetWarning(args) {
729
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."
730
894
  });
731
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
+ };
732
913
  function loadFloat(config) {
733
914
  const cfg = config === void 0 ? loadConfig() : config;
734
915
  if (!cfg?.nwcUri) return null;
@@ -816,6 +997,47 @@ async function fundViaFloat(params) {
816
997
  });
817
998
  return { ...payment, btcRate };
818
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
+ }
819
1041
  function backendFor(float, opts) {
820
1042
  return new NwcBackend(float.conn, opts);
821
1043
  }
@@ -823,6 +1045,43 @@ function formatSats2(msats) {
823
1045
  const sats = msats / 1e3;
824
1046
  return `${Number.isInteger(sats) ? sats.toLocaleString("en-US") : String(sats)} sats`;
825
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
+ }
826
1085
  function toFloatError(err) {
827
1086
  if (err instanceof DvmError) return err;
828
1087
  if (!(err instanceof NwcError)) {
@@ -1005,6 +1264,10 @@ function describeNwcMethod(method) {
1005
1264
  }
1006
1265
  }
1007
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
+ }
1008
1271
 
1009
1272
  // src/lib/payment-rails/tempo-credential.ts
1010
1273
  async function buildTempoCredential(args) {
@@ -1098,8 +1361,65 @@ function fail(mode, refusal) {
1098
1361
  return null;
1099
1362
  }
1100
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
+
1101
1416
  // src/lib/receipt-store.ts
1102
- 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
+ }
1103
1423
  function appendFundingReceipt(entry) {
1104
1424
  if (loadFundingReceipts().some(
1105
1425
  (held) => held.receipt.credit_id === entry.receipt.credit_id && held.receipt.fund_id === entry.receipt.fund_id
@@ -1110,9 +1430,9 @@ function appendFundingReceipt(entry) {
1110
1430
  appendFileSync(RECEIPTS_FILE, JSON.stringify(entry) + "\n");
1111
1431
  }
1112
1432
  function loadFundingReceipts() {
1113
- if (!existsSync2(RECEIPTS_FILE)) return [];
1433
+ if (!existsSync3(RECEIPTS_FILE)) return [];
1114
1434
  try {
1115
- return readFileSync2(RECEIPTS_FILE, "utf-8").split("\n").flatMap((line) => {
1435
+ return readFileSync3(RECEIPTS_FILE, "utf-8").split("\n").flatMap((line) => {
1116
1436
  if (!line.trim()) return [];
1117
1437
  try {
1118
1438
  const parsed = JSON.parse(line);
@@ -1125,10 +1445,37 @@ function loadFundingReceipts() {
1125
1445
  return [];
1126
1446
  }
1127
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
+ }
1128
1472
  function isStoredFundingReceipt(value) {
1129
1473
  if (!value || typeof value !== "object") return false;
1130
1474
  return isFundingReceipt(value.receipt);
1131
1475
  }
1476
+ function loadReceipt(jobId) {
1477
+ return loadReceipts().find((e) => e.receipt.job_id === jobId) ?? null;
1478
+ }
1132
1479
 
1133
1480
  // src/lib/receipt-capture.ts
1134
1481
  function captureFundingReceipt(args) {
@@ -1162,6 +1509,139 @@ function captureFundingReceipt(args) {
1162
1509
  }
1163
1510
  return result.status;
1164
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
+ }
1165
1645
 
1166
1646
  // src/lib/signed-request-target.ts
1167
1647
  function signedRequestAudience(provider, endpoint) {
@@ -1211,6 +1691,19 @@ function isLoopbackEndpoint(endpoint) {
1211
1691
 
1212
1692
  // src/lib/wallet-agent/builder-handle.ts
1213
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
+ }
1214
1707
  function reservedForMarker(builderHandle) {
1215
1708
  return `${BUILDER_RESERVE_PREFIX}${builderHandle}`;
1216
1709
  }
@@ -1382,14 +1875,25 @@ async function validateLnAddress(address) {
1382
1875
  var DEFAULT_INFLIGHT_PROOF_TIMEOUT_SEC = 60;
1383
1876
  var INFLIGHT_TIMEOUT_ENV = "DVM_INFLIGHT_PROOF_TIMEOUT_SEC";
1384
1877
  function inFlightTimeoutSeconds() {
1385
- const raw = process.env[INFLIGHT_TIMEOUT_ENV];
1386
- if (!raw) return DEFAULT_INFLIGHT_PROOF_TIMEOUT_SEC;
1387
- 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);
1388
1881
  if (!Number.isFinite(parsed) || parsed <= 0) {
1389
1882
  return DEFAULT_INFLIGHT_PROOF_TIMEOUT_SEC;
1390
1883
  }
1391
1884
  return parsed;
1392
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
+ }
1393
1897
  function proofKey(p) {
1394
1898
  return `${p.mint}\0${p.secret}`;
1395
1899
  }
@@ -1461,8 +1965,8 @@ function rehydrateOutputData(persisted) {
1461
1965
  }
1462
1966
  function bigintToHex(n) {
1463
1967
  if (n < 0n) throw new Error(`blinding factor must be non-negative, got ${n.toString()}`);
1464
- const raw = n.toString(16);
1465
- return raw.length % 2 === 0 ? raw : "0" + raw;
1968
+ const raw2 = n.toString(16);
1969
+ return raw2.length % 2 === 0 ? raw2 : "0" + raw2;
1466
1970
  }
1467
1971
  function hexToBigint(hex) {
1468
1972
  if (!/^[0-9a-fA-F]*$/.test(hex)) throw new Error(`invalid hex: ${hex}`);
@@ -1537,59 +2041,500 @@ function toCashuProofs(proofs) {
1537
2041
  return proofs.map(toCashuProof);
1538
2042
  }
1539
2043
 
1540
- // src/lib/wallet-agent/denominations.ts
1541
- var TINY_AMOUNT_THRESHOLD_SATS = 8;
1542
- function computeSmartDenominations(amountSats, opts = {}) {
1543
- if (!Number.isFinite(amountSats) || amountSats <= 0) return [];
1544
- const cap = normalizeCap(opts.maxDenomination);
1545
- const withTail = opts.includeTinyTail !== false;
1546
- if (!withTail || amountSats < TINY_AMOUNT_THRESHOLD_SATS) {
1547
- 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
+ );
1548
2057
  }
1549
- const total = Math.floor(amountSats);
1550
- const small1 = Math.floor(total * 0.05);
1551
- const small2 = Math.floor(total * 0.1 / 2) * 2;
1552
- const small4 = Math.floor(total * 0.2 / 4) * 4;
1553
- const remainder = total - small1 - small2 - small4;
1554
- const out = [];
1555
- for (let i = 0; i < small1; i++) out.push(1);
1556
- for (let i = 0; i < small2 / 2; i++) out.push(2);
1557
- for (let i = 0; i < small4 / 4; i++) out.push(4);
1558
- out.push(...greedyPowerOfTwo(remainder, cap));
1559
- return out;
1560
- }
1561
- function greedyPowerOfTwo(n, cap) {
1562
- const out = [];
1563
- let r = n;
1564
- if (cap !== void 0 && r >= cap) {
1565
- const capCount = Math.floor(r / cap);
1566
- for (let i = 0; i < capCount; i++) out.push(cap);
1567
- 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
+ };
1568
2066
  }
1569
- let bit = 1;
1570
- while (r > 0) {
1571
- if (r & 1) out.push(bit);
1572
- r = Math.floor(r / 2);
1573
- 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
+ }
1574
2082
  }
1575
- return out;
1576
- }
1577
- function normalizeCap(raw) {
1578
- if (raw === void 0) return void 0;
1579
- if (!Number.isFinite(raw) || raw <= 0) return void 0;
1580
- const floored = Math.floor(raw);
1581
- let pow = 1;
1582
- while (pow * 2 <= floored) pow *= 2;
1583
- return pow;
1584
- }
1585
-
1586
- // src/lib/wallet-agent/fund.ts
1587
- import { MintQuoteState } from "@cashu/cashu-ts";
1588
- async function prepareP2PKMint(cashuWallet, sats, quote, lockPrivkey, lockPubkey, denominations, extras = {}) {
1589
- const options = {
1590
- 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
1591
2196
  };
1592
- 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;
1593
2538
  if (extras.refundKeys && extras.refundKeys.length > 0) options.refundKeys = extras.refundKeys;
1594
2539
  return cashuWallet.prepareMint(
1595
2540
  "bolt11",
@@ -1599,6 +2544,17 @@ async function prepareP2PKMint(cashuWallet, sats, quote, lockPrivkey, lockPubkey
1599
2544
  { type: "p2pk", options, denominations }
1600
2545
  );
1601
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
+ }
1602
2558
  function maxKeysetDenomination(cashuWallet) {
1603
2559
  const keysets = cashuWallet.keyChain.getKeysets();
1604
2560
  if (keysets.length === 0) return void 0;
@@ -1608,14 +2564,35 @@ function maxKeysetDenomination(cashuWallet) {
1608
2564
  if (amounts.length === 0) return void 0;
1609
2565
  return Math.max(...amounts);
1610
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
+ }
1611
2588
 
1612
2589
  // src/lib/wallet-agent/locktime.ts
1613
2590
  var DEFAULT_RESERVE_LOCKTIME_SECONDS = 30 * 24 * 60 * 60;
1614
2591
  var DEFAULT_PERCALL_LOCKTIME_SECONDS = 48 * 60 * 60;
1615
2592
  var PERCALL_LOCKTIME_ENV = "DVM_PERCALL_LOCKTIME_SEC";
1616
2593
  function perCallLocktimeSeconds(minLocktimeSeconds) {
1617
- const raw = process.env[PERCALL_LOCKTIME_ENV];
1618
- const parsed = raw ? Number(raw) : NaN;
2594
+ const raw2 = process.env[PERCALL_LOCKTIME_ENV];
2595
+ const parsed = raw2 ? Number(raw2) : NaN;
1619
2596
  const base = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_PERCALL_LOCKTIME_SECONDS;
1620
2597
  const floor = typeof minLocktimeSeconds === "number" && Number.isFinite(minLocktimeSeconds) ? Math.max(0, Math.floor(minLocktimeSeconds)) : 0;
1621
2598
  return Math.max(base, floor);
@@ -1626,6 +2603,35 @@ var SUFFIXES = {
1626
2603
  h: 60 * 60,
1627
2604
  d: 24 * 60 * 60
1628
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
+ }
1629
2635
 
1630
2636
  // src/lib/wallet-agent/mint-inventory.ts
1631
2637
  async function loadConfiguredMints() {
@@ -1648,6 +2654,29 @@ async function loadConfiguredMints() {
1648
2654
  return merged;
1649
2655
  });
1650
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
+ }
1651
2680
  function dedupe(values) {
1652
2681
  const seen = /* @__PURE__ */ new Set();
1653
2682
  const out = [];
@@ -1659,84 +2688,492 @@ function dedupe(values) {
1659
2688
  return out;
1660
2689
  }
1661
2690
 
1662
- // src/lib/wallet-agent/receive.ts
1663
- import { Amount as Amount5, getP2PKExpectedWitnessPubkeys, getTokenMetadata } from "@cashu/cashu-ts";
1664
-
1665
- // src/lib/wallet-agent/recovery.ts
1666
- import { MintQuoteState as MintQuoteState2 } from "@cashu/cashu-ts";
1667
-
1668
- // src/lib/wallet-agent/recovery-submissions.ts
1669
- import {
1670
- Amount as Amount6,
1671
- CheckStateEnum,
1672
- getEncodedToken as getEncodedToken2
1673
- } from "@cashu/cashu-ts";
1674
-
1675
- // src/lib/cashu/agent-refund-subkey.ts
1676
- import { HDKey as HDKey2 } from "@scure/bip32";
1677
- import { mnemonicToSeedSync as mnemonicToSeedSync2 } from "@scure/bip39";
1678
- var PURPOSE_INDEX2 = 1789;
1679
- var AGENT_REFUND_ROLE_INDEX = 2;
1680
- function agentRefundSubkeyPath(canonicalDvmId) {
1681
- const dvmIndex = hashedCanonicalDvmId(canonicalDvmId);
1682
- return `m/${PURPOSE_INDEX2}'/${AGENT_REFUND_ROLE_INDEX}'/${dvmIndex}'`;
1683
- }
1684
- function deriveAgentRefundSubkey(mnemonic, canonicalDvmId) {
1685
- const seed = mnemonicToSeedSync2(mnemonic);
1686
- const hd = HDKey2.fromMasterSeed(seed);
1687
- const child = hd.derive(agentRefundSubkeyPath(canonicalDvmId));
1688
- if (!child.privateKey || !child.publicKey) {
1689
- 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
+ );
1690
2706
  }
1691
- return {
1692
- privkey: Buffer.from(child.privateKey).toString("hex"),
1693
- 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
+ });
1694
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
+ );
1695
2760
  }
1696
- function deriveAgentRefundSubkeyPubkey(mnemonic, canonicalDvmId) {
1697
- return deriveAgentRefundSubkey(mnemonic, canonicalDvmId).pubkey;
1698
- }
1699
-
1700
- // src/lib/credit-policy.ts
1701
- import { randomUUID } from "crypto";
1702
-
1703
- // src/lib/x402/menu.ts
1704
- function offersX402CreditFunding(menu, network) {
1705
- return matchesNetwork(x402CreditFundingNetworks(menu), network);
1706
- }
1707
- function offersX402CreditChannel(menu, network) {
1708
- 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";
1709
2768
  }
1710
- function x402CreditFlavour(menu, network) {
1711
- if (offersX402CreditChannel(menu, network)) return X402_BATCH_SETTLEMENT_SCHEME;
1712
- if (matchesNetwork(x402CreditExactNetworks(menu), network)) return X402_EXACT_SCHEME;
1713
- return void 0;
2769
+ function isBoundsRefusal(reason) {
2770
+ return reason === "amount_below_min" || reason === "amount_above_max";
1714
2771
  }
1715
- function offersX402CreditFlavour(menu, network, flavour) {
1716
- return matchesNetwork(
1717
- flavour === X402_BATCH_SETTLEMENT_SCHEME ? x402CreditChannelNetworks(menu) : x402CreditExactNetworks(menu),
1718
- network
1719
- );
2772
+ function canFund(probe, sats) {
2773
+ return probe.ok && probe.mintingEnabled !== false && amountBoundsVerdict(sats, probe.mintAmountBounds) === "ok";
1720
2774
  }
1721
- function x402InstrumentForFlavour(flavour) {
1722
- 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";
1723
2780
  }
1724
- function x402FlavourForInstrument(instrument) {
1725
- if (instrument === "x402_channel") return X402_BATCH_SETTLEMENT_SCHEME;
1726
- if (instrument === "x402_exact") return X402_EXACT_SCHEME;
1727
- return void 0;
2781
+ function fallbackKindFor(reason) {
2782
+ if (reason === "minting_disabled") return "capability";
2783
+ if (isBoundsRefusal(reason)) return "bounds";
2784
+ return "health";
1728
2785
  }
1729
- function x402CreditChannelNetworks(menu) {
1730
- 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.";
1731
2798
  }
1732
- function x402CreditExactNetworks(menu) {
1733
- return schemeNetworks(menu, X402_EXACT_SCHEME);
2799
+ async function probeMintHealthOnce(mintUrl) {
2800
+ return checkMintHealth(mintUrl, { retries: 1, backoffMs: [0] });
1734
2801
  }
1735
- function x402CreditFundingNetworks(menu) {
1736
- 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;
1737
2810
  }
1738
- function schemeNetworks(menu, scheme) {
1739
- 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);
1740
3177
  }
1741
3178
  function matchesNetwork(networks, network) {
1742
3179
  return networks.some((candidate) => network === void 0 || candidate === network);
@@ -1750,7 +3187,9 @@ var TIER2_MIN_RECEIPTS = 15;
1750
3187
  var TIER2_MIN_SPAN_MS = 7 * 24 * 60 * 60 * 1e3;
1751
3188
  var OPEN_TARGET_JOBS = 20;
1752
3189
  var CASHU_REFILL_TARGET_JOBS = 40;
3190
+ var TOPUP_THRESHOLD_FRACTION = 0.25;
1753
3191
  var DEFAULT_CREDIT_POSTURE = "suggest";
3192
+ var POCKET_TARGET_MICRO = 5e6;
1754
3193
  function trustTierFor(args) {
1755
3194
  const ladder = trustLadderFor(args);
1756
3195
  return { tier: ladder.tier, capMicro: ladder.capMicro ?? args.menu.max_micro };
@@ -1819,6 +3258,18 @@ function effectiveFundingRail(config, endpoint, identity) {
1819
3258
  return { rail: global, source: "global", ...conflict ? { conflict } : {} };
1820
3259
  return { source: "default", ...conflict ? { conflict } : {} };
1821
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
+ }
1822
3273
  function tierCapOverrides(config) {
1823
3274
  const out = {};
1824
3275
  const tier0 = parseCapMicro(config?.tier0Cap);
@@ -1968,6 +3419,81 @@ function planCreditForQuote(args) {
1968
3419
  railReason: openRoute.reason
1969
3420
  };
1970
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
+ }
1971
3497
  function msatsToMicro(msats, btcRate) {
1972
3498
  return Math.round(msatsToUsd(msats, btcRate) * 1e6);
1973
3499
  }
@@ -1983,6 +3509,9 @@ function perDvmEntriesFor(config, endpoint, identity) {
1983
3509
  );
1984
3510
  }
1985
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
+ }
1986
3515
  function fundAmounts(fundMicro, btcRate) {
1987
3516
  const fundSats = microToSats(fundMicro, btcRate);
1988
3517
  return { fundMicro, fundSats, fundMsats: fundSats * 1e3 };
@@ -2019,6 +3548,14 @@ function targetJobsFor(config, endpoint, identity) {
2019
3548
  if (targets.length > 0) return Math.min(...targets);
2020
3549
  return config?.targetJobs;
2021
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
+ }
2022
3559
  function parseCapMicro(cap) {
2023
3560
  if (!cap) return void 0;
2024
3561
  const match = /^\$?(\d+(?:\.\d{1,6})?)$/.exec(cap.trim());
@@ -2679,6 +4216,32 @@ async function commitAgentSpend(args) {
2679
4216
  saveAgentWallet(next);
2680
4217
  });
2681
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
+ }
2682
4245
  async function abandonPendingSubmission(args) {
2683
4246
  await withWalletLock(() => {
2684
4247
  const wallet = loadAgentWallet();
@@ -2820,20 +4383,1085 @@ function minOvershootSubset(proofs, amountSats) {
2820
4383
  reachable.set(next, [...indices2, i]);
2821
4384
  }
2822
4385
  }
2823
- let bestTarget = -1;
2824
- for (const t of reachable.keys()) {
2825
- 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
+ );
2826
5409
  }
2827
- if (bestTarget === -1) return null;
2828
- const indices = reachable.get(bestTarget) ?? [];
2829
- 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] };
2830
5464
  }
2831
-
2832
- // src/lib/wallet-agent/recovery-submissions.ts
2833
- var RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
2834
-
2835
- // src/lib/wallet-agent/reserve.ts
2836
- import { MintQuoteState as MintQuoteState3 } from "@cashu/cashu-ts";
2837
5465
 
2838
5466
  // src/lib/x402/batch-client.ts
2839
5467
  import {
@@ -2868,6 +5496,29 @@ function resolveX402ChannelRpc(channelNetwork, configuredNetwork, bundled) {
2868
5496
 
2869
5497
  // src/lib/x402/lifecycle.ts
2870
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
+ }
2871
5522
  function assertX402ChannelPayer(channel, address) {
2872
5523
  if (channel.config.payer.toLowerCase() === address.toLowerCase()) return;
2873
5524
  throw new DvmError(
@@ -2884,11 +5535,162 @@ function assertX402ChannelPayer(channel, address) {
2884
5535
  }
2885
5536
  );
2886
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
+ }
2887
5689
 
2888
5690
  // src/lib/x402/voucher-cap.ts
2889
5691
  import { decodePaymentSignatureHeader } from "@x402/core/http";
2890
- function x402VoucherFactsFromPayload(raw) {
2891
- const payload = raw;
5692
+ function x402VoucherFactsFromPayload(raw2) {
5693
+ const payload = raw2;
2892
5694
  const maxClaimableMicro = payload.voucher?.maxClaimableAmount;
2893
5695
  if (maxClaimableMicro === void 0) return void 0;
2894
5696
  if (payload.type !== "deposit") return { maxClaimableMicro };
@@ -2905,6 +5707,10 @@ function x402ChannelReservation(journalled, totalClaimed) {
2905
5707
  totalClaimed
2906
5708
  );
2907
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
+ }
2908
5714
  function x402VoucherCap(pending) {
2909
5715
  const recorded = pending.maxClaimableMicro ?? x402VoucherFacts(pending.paymentHeader)?.maxClaimableMicro;
2910
5716
  if (recorded === void 0) return X402_MAX_UINT128;
@@ -3259,9 +6065,9 @@ async function refundX402BatchChannelLocked(args) {
3259
6065
  return { result, paymentResponse: encoded, atRiskMicro, pendingFundings: journalled };
3260
6066
  }
3261
6067
  function x402RequiredFromError(err) {
3262
- const raw = err.data?.x402_payment_required;
3263
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
3264
- 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;
3265
6071
  }
3266
6072
  function assertX402RequirementDescribesChannel(args) {
3267
6073
  assertX402ChannelPayer(
@@ -6053,40 +8859,587 @@ function isArtifactMessage(msg) {
6053
8859
  function isPaymentRequestMessage(msg) {
6054
8860
  return msg.type === "payment-request";
6055
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
+ }
6056
8896
 
6057
8897
  // src/lib/provenance.ts
6058
8898
  function asProviderText(text) {
6059
8899
  return { _source: "provider", text };
6060
8900
  }
8901
+ function asProviderTextOpt(text) {
8902
+ return text === void 0 ? void 0 : asProviderText(text);
8903
+ }
6061
8904
  function providerTextToString(value) {
6062
8905
  return typeof value === "string" ? value : value.text;
6063
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
+ }
6064
9386
  export {
6065
9387
  ADMIN_STATE_UNAVAILABLE,
9388
+ AGENT_MNEMONIC_FILE,
9389
+ BTC_RATE_FILE,
9390
+ BUDGET_FILE,
6066
9391
  CAPABILITY_NAME_RE,
6067
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,
6068
9399
  CREDIT_DRAIN_RAILS,
6069
9400
  CREDIT_FUNDING_MODES,
6070
9401
  CREDIT_FUND_RAILS,
9402
+ CREDIT_POSTURES,
9403
+ DEFAULT_AGENT_MINT_URL,
9404
+ DEFAULT_CREDIT_POSTURE,
6071
9405
  DEFAULT_LOCK_PUBKEY_GRACE_SECONDS,
9406
+ DEFAULT_RESERVE_LOCKTIME_SECONDS,
6072
9407
  DbTransport,
6073
9408
  DvmError,
6074
9409
  DvmX402ChannelStorage,
9410
+ FAVORITES_FILE,
9411
+ FEEDBACK_NUDGES_FILE,
9412
+ FEEDBACK_POSTURES,
6075
9413
  FX_CACHE_TTL_MS,
6076
9414
  FX_RETRY_COUNT,
9415
+ IDENTITIES_FILE,
9416
+ IDENTITIES_VERSION,
6077
9417
  InvalidUsdPriceError,
9418
+ JOBS_FILE,
6078
9419
  JobCancelledError,
9420
+ MESSAGES_DIR,
6079
9421
  MemoryKVStore,
6080
9422
  MintHealthTracker,
6081
9423
  NON_CHANNEL_BITCOIN_RAILS,
9424
+ NWC_FUNDING_FLOAT_ROLE,
6082
9425
  NwcBackend,
6083
9426
  NwcError,
9427
+ PAIRINGS_DIR,
6084
9428
  PAYMENT_PROOF_KEYS,
9429
+ PRIVKEY_HEX_RE,
6085
9430
  PayoutReporter,
9431
+ PersistentCounterSource,
6086
9432
  PostgresTempoChargeStore,
6087
9433
  PostgresTempoSessionStore,
9434
+ RAIL_FLAGS,
6088
9435
  RAIL_REFUNDABLE,
9436
+ RECEIPTS_FILE,
6089
9437
  RECENT_SPENDS_CAP,
9438
+ RECOMMENDED_MINTS,
9439
+ RECOVERY_MNEMONIC_FILE,
9440
+ REQUEST_INTENTS_DIR,
9441
+ RESUME_MAX_AGE_MS,
9442
+ RESUME_MAX_ATTEMPTS,
6090
9443
  ROUTER_FEEDBACK_CLAIM_HEADER,
6091
9444
  ROUTER_FEEDBACK_CLAIM_TTL_MS,
6092
9445
  RevenueReporter,
@@ -6098,15 +9451,20 @@ export {
6098
9451
  SpanWriter,
6099
9452
  StepCache,
6100
9453
  TEMPO_CHAIN_ID,
9454
+ TEMPO_CHANNELS_FILE,
6101
9455
  TEMPO_CLOSE_DB_RECOVERY_BUDGET_MS,
6102
9456
  TEMPO_CLOSE_REQUESTED_TOPIC,
9457
+ TEMPO_EXIT_KEY_MISSING_HINT,
6103
9458
  TempoSessionChannelMismatchError,
9459
+ WALLET_FILE,
9460
+ WALLET_VERSION,
6104
9461
  X402EnvValidationError,
6105
9462
  X402FacilitatorHealth,
6106
9463
  X402_BATCH_CHANNEL_ABI,
6107
9464
  X402_BATCH_SETTLEMENT_KEY,
6108
9465
  X402_BATCH_SETTLEMENT_MAINNET_NETWORK,
6109
9466
  X402_BATCH_SETTLEMENT_SCHEME,
9467
+ X402_CHANNELS_FILE,
6110
9468
  X402_DEFAULT_FACILITATOR,
6111
9469
  X402_DEFAULT_NETWORK,
6112
9470
  X402_EXACT_AUTHORIZATION_TTL_SECONDS,
@@ -6120,46 +9478,81 @@ export {
6120
9478
  X402_V1_VERSION,
6121
9479
  X402_VERSION,
6122
9480
  _testing,
9481
+ abandonPendingSubmission,
6123
9482
  abortJob,
6124
9483
  acknowledgeReplayedX402Payment,
6125
9484
  acknowledgeX402ChannelOwner,
6126
9485
  activePubkeySet,
9486
+ addPayment,
9487
+ advanceTempoChannelExit,
9488
+ amountBoundsVerdict,
9489
+ applyImplicitReceiptCredit,
6127
9490
  applyPaymentInfoToJob,
9491
+ applyReceiptCredit,
6128
9492
  applyRetryToPool,
6129
9493
  asProviderText,
9494
+ asProviderTextOpt,
9495
+ assertAgentMnemonicReplaceable,
9496
+ assertFloatBudget,
9497
+ assertNutSupport,
6130
9498
  assertRevenueReporterReady,
6131
9499
  assertSupportedX402Network,
9500
+ assertTempoWalletIdle,
6132
9501
  assertValidBudget,
9502
+ assertX402ChannelPayer,
6133
9503
  assertX402RequirementDescribesChannel,
9504
+ assertX402WalletIdle,
6134
9505
  attestedDvmIdentity,
6135
9506
  authenticatedBuilderPubkey,
9507
+ authorizeProtectedRemoval,
9508
+ automaticCreditBoundRail,
6136
9509
  availableRails,
9510
+ backfillTrustAnchor,
6137
9511
  backupSecretFile,
6138
9512
  buildAdminAuthHeader,
6139
9513
  buildAdminChallenge,
6140
9514
  buildAttestation,
9515
+ buildCompletionHintFields,
9516
+ buildDisplayMintInventory,
6141
9517
  buildPaymentErrorResponse,
6142
9518
  buildPaymentRequiredV2,
6143
9519
  buildPaymentRequirements,
9520
+ buildPolicy,
6144
9521
  buildTempoCredential,
6145
9522
  builderLockPath,
6146
9523
  caip2ToX402Network,
6147
9524
  canReplaceExpiredX402Deposit,
6148
9525
  canSwap,
9526
+ canonicalRequestPath,
6149
9527
  canonicaliseForSigning,
6150
9528
  canonicalize,
9529
+ captureFundingReceipt,
9530
+ captureTransportReceipt,
9531
+ cashOutFromMint,
6151
9532
  cashuSubmissionBindingFingerprint,
6152
9533
  chainIdFromCaip2,
6153
9534
  checkMeltQuoteState,
6154
9535
  checkMintHealth,
6155
9536
  checkReceiptAttestation,
6156
9537
  clearAccumulatorForDvm,
9538
+ clearCreditSelection,
9539
+ clearPendingDrain,
6157
9540
  clearPendingX402ExactFunding,
6158
9541
  clearPendingX402Funding,
9542
+ clearRefillRailUnavailable,
9543
+ generateIdentity2 as cliIdentityStoreGenerateIdentity,
9544
+ commitAgentSpend,
9545
+ completionFromMessages,
9546
+ completionSnapshot,
9547
+ computeNextAction,
6159
9548
  computeResultHash,
6160
9549
  computeSmartDenominations,
9550
+ configSnapshotPath,
9551
+ connectedTempoAccount,
6161
9552
  connectedX402Payer,
9553
+ correctPendingFundAmount,
6162
9554
  costAnchor,
9555
+ coveringMintsForSpend,
6163
9556
  createAdminCashuNonceStore,
6164
9557
  createDVMServer,
6165
9558
  createDefaultReplayStore,
@@ -6167,54 +9560,90 @@ export {
6167
9560
  createMonotonicClock,
6168
9561
  createNoopLogger,
6169
9562
  createStdoutLogger,
9563
+ createTempoCloseCredential,
6170
9564
  createX402BatchPayment,
6171
9565
  creditDepositPayload,
6172
9566
  currentContext,
6173
9567
  decodePayment,
6174
9568
  decodePaymentRequiredHeader,
9569
+ deleteAgentMnemonic,
6175
9570
  depleteLots,
9571
+ deriveAgentMasterKeypair,
6176
9572
  deriveBuilderLockKeypair,
6177
9573
  deriveBuilderLockPubkey,
9574
+ deriveIdentityFromPrivkey,
9575
+ derivePubkeyFromPrivkey,
6178
9576
  deriveReceiptKeypair,
6179
9577
  deriveReceiptSecret,
9578
+ deriveTempoAddress,
6180
9579
  derivedFundCreditId,
9580
+ describeConfigBackup,
9581
+ describeMintBounds,
6181
9582
  devModeSkipsPaymentVerification,
6182
9583
  drainReceiptsDisplay,
6183
9584
  drainReceiptsHint,
9585
+ dvmLocationIdentity,
9586
+ effectiveCreditPosture,
9587
+ effectiveFundingMint,
9588
+ effectiveFundingRail,
9589
+ effectiveMints,
9590
+ emitFloatBudgetWarning,
6184
9591
  encodePayment,
6185
9592
  encodePaymentRequiredHeader,
6186
9593
  encodeSettleResponseHeader,
6187
9594
  enforceGlobalBudget,
6188
9595
  ensureAddress,
9596
+ ensureConfig,
9597
+ ensureConfigDir,
9598
+ ensureConfigSubdir,
9599
+ ensureProvisionalCredit,
9600
+ entryKind,
6189
9601
  epochMsToIso,
6190
9602
  exactEvmAuthorization,
9603
+ extractBuilderHandle,
6191
9604
  fetchBtcUsdRate,
6192
9605
  fetchCoinGeckoBtcUsd,
6193
9606
  fetchCoinGeckoSpot,
6194
9607
  fetchLnurlPayInvoice,
6195
9608
  fetchMeltQuote,
9609
+ fetchMissingJobReceipt,
9610
+ fetchTrustAnchor,
6196
9611
  fifoOrder,
9612
+ findSigningIdentityByPubkey,
6197
9613
  findX402Channel,
9614
+ floatBudgetJson,
9615
+ floatPaymentDisplay,
9616
+ floatPaymentJson,
9617
+ forgetCredit,
6198
9618
  formatAmount,
6199
9619
  formatAmountBestEffort,
9620
+ formatLocktime,
6200
9621
  formatPaymentAmount,
6201
9622
  formatSats,
6202
9623
  formatStaleness,
6203
9624
  formatUsd,
6204
9625
  formatUsdcMicrounits,
6205
9626
  fromJobRecord,
9627
+ fundAgentWallet,
6206
9628
  fundCredit,
9629
+ fundWireAmountMicro,
6207
9630
  fundedMicroFor,
6208
9631
  fundingModeForTempoIntent,
6209
9632
  fundingModeForX402Flavour,
6210
9633
  fundingRailLabel,
9634
+ generateAgentMnemonic,
6211
9635
  generateEphemeralReceiptKey,
6212
9636
  generateIdentity,
9637
+ generateTempoKey,
6213
9638
  generateX402PrivateKey,
6214
9639
  getBalance,
9640
+ getBalanceByMint,
9641
+ getFundingGuidance,
6215
9642
  getInfo,
6216
9643
  getInputFeesForProofs,
6217
9644
  getLastKnownBtcRate,
9645
+ getOutputTraceId,
9646
+ getTempoUsdcBalance,
6218
9647
  getWallet,
6219
9648
  getX402Balance,
6220
9649
  hasPaymentProof,
@@ -6222,8 +9651,10 @@ export {
6222
9651
  hashLockKey,
6223
9652
  hashedCanonicalDvmId,
6224
9653
  humanizeZodError,
9654
+ identitiesFileExists,
6225
9655
  implicitCreditId,
6226
9656
  inKindDrawMsats,
9657
+ info,
6227
9658
  initLockPubkeyTable,
6228
9659
  initLoggers,
6229
9660
  initWalletAccumulatorTable,
@@ -6232,72 +9663,134 @@ export {
6232
9663
  installAdminCashuRoutes,
6233
9664
  isAbortError,
6234
9665
  isArtifactMessage,
9666
+ isCancelMessage,
9667
+ isCompleteMessage,
6235
9668
  isCreditFundRail,
6236
9669
  isCreditFundingMode,
9670
+ isCreditPosture,
6237
9671
  isDerivedCreditId,
6238
9672
  isDrainReceipt,
9673
+ isFeedbackPosture,
6239
9674
  isFundingReceipt,
6240
9675
  isInKindDepletion,
6241
9676
  isNonChannelBitcoinRail,
9677
+ isPaymentRailKey,
6242
9678
  isPaymentRequestMessage,
6243
9679
  isPromptMessage,
9680
+ isPublicRequestId,
9681
+ isResumable,
6244
9682
  isSignedJobReceipt,
6245
9683
  isStablecoinFundRail,
6246
9684
  isStaleJobReapable,
6247
9685
  isTerminal,
9686
+ isTerminalJobStatus,
9687
+ isTextMessage,
9688
+ isValidEvmPrivateKey,
9689
+ isWorkingMessage,
6248
9690
  isYieldMessage,
6249
9691
  issueRouterFeedbackClaim,
6250
9692
  issueUpfrontChallenges,
6251
9693
  jobCredentialGate,
9694
+ json,
9695
+ listCredits,
9696
+ listFundingMenuSnapshots,
9697
+ listImplicitCredits,
9698
+ listPendingFundings,
6252
9699
  listPendingX402Fundings,
6253
9700
  listPendingX402FundingsForChannel,
9701
+ listResumableTempoChargeFundings,
6254
9702
  listResumableX402ExactFundings,
6255
9703
  listResumableX402Fundings,
6256
9704
  listTempoChannelAssociations,
6257
9705
  listX402Channels,
6258
9706
  loadAgentWallet,
9707
+ loadAllJobs,
9708
+ loadConfig,
6259
9709
  loadConfiguredMints,
9710
+ loadCredit,
9711
+ loadFloat,
9712
+ loadIdentities,
6260
9713
  loadIdentitySecret,
9714
+ loadImplicitCredits,
9715
+ loadJob,
6261
9716
  loadLedger,
6262
9717
  loadLockPubkeyState,
9718
+ loadPendingFunding,
9719
+ loadReceipt,
9720
+ loadReceipts,
6263
9721
  loadResumableX402ExactFunding,
6264
9722
  loadResumableX402Funding,
9723
+ loadTempoChannelAssociation,
9724
+ loadTempoChannelController,
6265
9725
  loadTempoChannelEntry,
6266
9726
  loadX402Wallet,
6267
9727
  toLockPubkey as lockPubkeyStoreToLockPubkey,
6268
9728
  loggers,
9729
+ looksLikeMnemonic,
6269
9730
  lookupInvoice,
6270
9731
  lotOwedSats,
6271
9732
  makeInvoice,
9733
+ markCredit,
9734
+ markImplicitCreditLost,
9735
+ markJobTerminal,
9736
+ markRefillDrainNoticed,
9737
+ markRefillRailUnavailable,
6272
9738
  maxKeysetDenomination,
9739
+ microToSats,
6273
9740
  missingDleqRecoveryDiagnostic,
9741
+ mnemonicFingerprint,
6274
9742
  monetaryJson,
6275
9743
  msatsToFiatMicro,
9744
+ msatsToMicro,
6276
9745
  msatsToUsd,
6277
9746
  msatsToUsdCents,
6278
9747
  msatsToUsdc,
9748
+ needsPocketRefill,
9749
+ needsTopup,
6279
9750
  netOwedSats,
9751
+ nextStepHint,
9752
+ normalizeCreditEndpoint,
6280
9753
  normalizeFeedbackEndpointHost,
6281
9754
  nwcDedupWindowMs,
9755
+ nwcRejectionDisplay,
6282
9756
  nwcTimeoutMs,
9757
+ offersTempoCreditFunding,
6283
9758
  offersX402CreditFunding,
6284
9759
  parseBudget,
6285
9760
  parseCreditMenu,
6286
9761
  parseCreditTerms,
6287
9762
  parseLnAddress,
9763
+ parseLocktime,
6288
9764
  parseNwcUri,
9765
+ parseStatedBudget,
9766
+ parseTraceparent,
6289
9767
  parseUsdPrice,
6290
9768
  payInvoiceReconciling,
9769
+ payViaFloat,
6291
9770
  paymentErrorBody,
9771
+ paymentRailLabel,
6292
9772
  paymentRequiredV2FromV1,
9773
+ pendingDrainById,
9774
+ pendingDrainFor,
6293
9775
  pickCreditFundRail,
6294
9776
  pinAskFiat,
9777
+ pinConfigSnapshot,
6295
9778
  pinDimension,
9779
+ planConfigReset,
6296
9780
  planCreditForQuote,
9781
+ planProbeDraw,
9782
+ pocketTargetMicro,
9783
+ pollNextAction,
6297
9784
  pollPendingCreditFundings,
6298
9785
  preferLightningFirst,
9786
+ prepareAgentSpend,
6299
9787
  prepareP2PKMint,
9788
+ previewConfigRollback,
6300
9789
  priceFiatMicro,
9790
+ printFundingGuidance,
9791
+ probeFloat,
9792
+ probeRailLabel,
9793
+ probeWallets,
6301
9794
  processIncomingPayment,
6302
9795
  processX402BatchCorrection,
6303
9796
  processX402BatchResponse,
@@ -6306,37 +9799,78 @@ export {
6306
9799
  providerProseSources,
6307
9800
  providerTextToString,
6308
9801
  pruneRetired,
9802
+ railLabelAtSentenceStart,
6309
9803
  randomTraceId,
6310
9804
  rateUnavailablePaymentError,
9805
+ raw,
9806
+ readFundValuation,
9807
+ readX402ChannelOnChain,
6311
9808
  receiptDisplay,
6312
9809
  receiptFailureDetail,
6313
9810
  receiptHint,
6314
9811
  receiptIssuedAtIso,
9812
+ receiptJsonFields,
6315
9813
  receiptPubkeyFromSecret,
9814
+ receiptTrustFields,
9815
+ receiveToAgentWallet,
9816
+ reclaimAtMs,
6316
9817
  reconcileRefusedX402Deposits,
9818
+ reconcileTempoChannelOnChain,
9819
+ recordBalanceResponse,
9820
+ recordCreditX402Instrument,
9821
+ recordDrainReceipts,
9822
+ recordFloatSpend,
9823
+ recordFundResponse,
9824
+ recordFundingMenuSnapshot,
9825
+ recordPendingDrain,
6317
9826
  recordPendingX402ExactFunding,
6318
9827
  recordPendingX402Funding,
9828
+ recordRefusedDrawBalance,
6319
9829
  recordSpend,
6320
9830
  recordX402ChannelMetadata,
9831
+ recoverPendingSubmission,
9832
+ redactUrl,
9833
+ redactUrlsInText,
9834
+ refFromJob,
6321
9835
  refundX402BatchChannel,
9836
+ releaseAgentReserve,
6322
9837
  removeX402Wallet,
9838
+ renderReceiptHuman,
9839
+ repairConfigDirMode,
6323
9840
  repairX402ExactSettlementEffect,
6324
9841
  repinX402WalletNetwork,
9842
+ replayStoredReceipt,
9843
+ reserveAgentWallet,
9844
+ resolveBtcUsdRate,
9845
+ resolveFundMint,
6325
9846
  resolveFxSnapshot,
6326
9847
  resolveLnurlPayMetadataUrl,
6327
9848
  resolvePriceFiat,
6328
9849
  resolveRpcHttpTransportOptions,
6329
9850
  resolveRpcOverride,
9851
+ resolveSigningIdentity,
9852
+ resolveTempoAsset,
9853
+ resolveX402Asset,
9854
+ resolveX402ChannelRpc,
6330
9855
  restoreProofsFromOutputs,
9856
+ retireConfigBackup,
6331
9857
  retirePendingX402Funding,
6332
9858
  revenueReporterBannerState,
9859
+ rollbackConfig,
6333
9860
  rotateLockPubkey,
9861
+ rpcEndpointLine,
9862
+ rpcEndpointSuffix,
6334
9863
  saveAgentWallet,
9864
+ saveIdentities,
9865
+ saveJob,
9866
+ saveLedger,
6335
9867
  saveX402Wallet,
6336
9868
  seedLockPubkeyState,
6337
9869
  selectProofsForSpend,
6338
9870
  serializeOutputData,
6339
9871
  setEmitter,
9872
+ setOutputTraceId,
9873
+ settleDrainEvidence,
6340
9874
  settleWithFacilitator,
6341
9875
  shellQuoteArg,
6342
9876
  signAttestation,
@@ -6346,20 +9880,49 @@ export {
6346
9880
  signReceipt,
6347
9881
  signRequestData,
6348
9882
  signedRequestDomain,
9883
+ spendableAt,
9884
+ staleRateNotice,
9885
+ statusSnapshot,
6349
9886
  swapFeeForProofs,
6350
9887
  swapSendReserve,
9888
+ sweepPendingMelts,
9889
+ sweepPendingMints,
9890
+ sweepPendingSubmissions,
9891
+ sweepStaleInFlight,
9892
+ tempoBalanceCheckHint,
9893
+ tempoChannelControlledBy,
6351
9894
  tempoCredentialAmountMicro,
9895
+ tempoKeyImportRoutes,
9896
+ tempoSessionChallenge,
9897
+ tempoSessionChallengeMicro,
9898
+ tempoTokenReference,
9899
+ tempoWrongAssetHint,
9900
+ tierCapOverrides,
9901
+ toAgentProof,
6352
9902
  toCreditTerms,
9903
+ toFloatError,
6353
9904
  toJobRecord,
6354
9905
  toLockPubkey,
6355
9906
  transportErrorCode,
9907
+ trustLadderFor,
9908
+ trustOverrideCapMicro,
9909
+ tryResolveBtcUsdRate,
9910
+ tryResolveTempoAsset,
9911
+ tryX402Payment,
6356
9912
  unknownRouteNotFound,
9913
+ unreachableRailDisplay,
9914
+ unreachableRailHint,
9915
+ unrecordedFloatBudgetWarning,
9916
+ unsignRequestData,
9917
+ updateConfig,
9918
+ updateJob,
6357
9919
  usdToMsats,
6358
9920
  usdcContractByCaip2,
6359
9921
  usdcContractFor,
6360
9922
  usdcDomainNameFor,
6361
9923
  usdcDomainVersionFor,
6362
9924
  usdcToMsats,
9925
+ validateAgentMnemonic,
6363
9926
  validateLnAddress,
6364
9927
  validateX402Env,
6365
9928
  verifyAttestation,
@@ -6377,17 +9940,27 @@ export {
6377
9940
  verifyUpfrontPayment,
6378
9941
  verifyWithFacilitator,
6379
9942
  verifyX402Payment,
9943
+ walletExists,
6380
9944
  warmFxSnapshot,
6381
9945
  weakestReceiptVerdict,
9946
+ windowStart,
9947
+ withIdentitiesLock,
6382
9948
  withInitRetry,
6383
9949
  withNwcPool,
9950
+ withTempoChannelLock,
6384
9951
  withTraceContext,
6385
9952
  withWalletLock,
6386
9953
  withX402ChannelLock,
6387
9954
  wrapMppx,
9955
+ writeAgentMnemonic,
6388
9956
  writeIdentity,
6389
9957
  x402ChannelBalanceUnreconciledAt,
9958
+ x402ChannelControlledBy,
9959
+ x402ChannelReservation,
9960
+ x402CreditChannelNetworks,
9961
+ x402CreditExactNetworks,
6390
9962
  x402ExactFundingDead,
9963
+ x402FlavourForInstrument,
6391
9964
  x402NetworkByCaip2,
6392
9965
  x402NetworkBySlug,
6393
9966
  x402NetworkLabel,
@@ -6401,7 +9974,9 @@ export {
6401
9974
  x402PendingUnsettled,
6402
9975
  x402RequiredFromError,
6403
9976
  x402RequiredUsdcMicro,
9977
+ x402ReservationRelease,
6404
9978
  x402SettledShare,
6405
9979
  x402SupportedNetworksHint,
6406
- x402VoucherPendingRefusal
9980
+ x402VoucherPendingRefusal,
9981
+ x402WrongAssetHint
6407
9982
  };