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