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