@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.
- package/dist/{chunk-F2L6KIMD.js → chunk-5URG56JJ.js} +5 -143
- package/dist/{chunk-EDU6COY2.js → chunk-7AKBC4PW.js} +754 -150
- package/dist/chunk-ANFX5HEG.js +186 -0
- package/dist/{chunk-4UVRXDNY.js → chunk-CMDI3ENK.js} +342 -176
- package/dist/{chunk-GJD7PVWY.js → chunk-FJDCFHW5.js} +26 -12
- package/dist/{chunk-AAJNGQMC.js → chunk-JGGI65I3.js} +1 -1
- package/dist/{chunk-2K6UXDAN.js → chunk-KSQEFVFJ.js} +623 -691
- package/dist/{chunk-XYTSDAPH.js → chunk-LDTWX7JW.js} +1 -1
- package/dist/chunk-MKI6OVW4.js +194 -0
- package/dist/{chunk-AZBXSXQT.js → chunk-P4RUVDU7.js} +1 -0
- package/dist/chunk-TSWKITGR.js +772 -0
- package/dist/{credit-menu-ClA1JyYW.d.ts → credit-menu-CYkETVM4.d.ts} +22 -3
- package/dist/{fx-ptKVFOwq.d.ts → fx-Pf4Ey4f_.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/internal/index.d.ts +4974 -85
- package/dist/internal/index.js +3742 -167
- package/dist/{job-store-DxFqDPYq.d.ts → job-store-BtCaLnvJ.d.ts} +23 -1
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +8 -21
- package/dist/tempo-lifecycle-SQL3KLEZ.js +13 -0
- package/dist/tempo-wallet-QOLEIPCV.js +31 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/{usd-Cha_j80I.d.ts → usd-D7fW2S7I.d.ts} +1 -1
- package/dist/{x402-XXFQQAAD.js → x402-FTG2GRAQ.js} +6 -4
- package/package.json +1 -1
package/dist/internal/index.js
CHANGED
|
@@ -1,36 +1,73 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
131
|
+
json,
|
|
132
|
+
listCredits,
|
|
133
|
+
listFundingMenuSnapshots,
|
|
134
|
+
listImplicitCredits,
|
|
83
135
|
listPendingFundings,
|
|
84
|
-
|
|
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
|
-
|
|
159
|
+
recordRefusedDrawBalance,
|
|
160
|
+
setOutputTraceId,
|
|
161
|
+
settleDrainEvidence,
|
|
96
162
|
withinAdvertisedTolerance
|
|
97
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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 (!
|
|
1433
|
+
if (!existsSync3(RECEIPTS_FILE)) return [];
|
|
1114
1434
|
try {
|
|
1115
|
-
return
|
|
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
|
|
1386
|
-
if (!
|
|
1387
|
-
const parsed = Number(
|
|
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
|
|
1465
|
-
return
|
|
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/
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
|
|
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
|
|
1618
|
-
const parsed =
|
|
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/
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
|
1711
|
-
|
|
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
|
|
1716
|
-
return
|
|
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
|
|
1722
|
-
|
|
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
|
|
1725
|
-
if (
|
|
1726
|
-
if (
|
|
1727
|
-
return
|
|
2781
|
+
function fallbackKindFor(reason) {
|
|
2782
|
+
if (reason === "minting_disabled") return "capability";
|
|
2783
|
+
if (isBoundsRefusal(reason)) return "bounds";
|
|
2784
|
+
return "health";
|
|
1728
2785
|
}
|
|
1729
|
-
function
|
|
1730
|
-
|
|
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
|
|
1733
|
-
return
|
|
2799
|
+
async function probeMintHealthOnce(mintUrl) {
|
|
2800
|
+
return checkMintHealth(mintUrl, { retries: 1, backoffMs: [0] });
|
|
1734
2801
|
}
|
|
1735
|
-
function
|
|
1736
|
-
|
|
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
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
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(
|
|
2891
|
-
const payload =
|
|
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
|
|
3263
|
-
if (!
|
|
3264
|
-
return
|
|
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
|
};
|