@dvmkit/sdk 0.1.2-rc.7 → 0.1.4-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -8
- package/dist/chunk-BIFLRKMO.js +87 -0
- package/dist/chunk-BQ2NMWKE.js +160 -0
- package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
- package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
- package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
- package/dist/chunk-E4EVGPDX.js +391 -0
- package/dist/chunk-EDDYHZ6W.js +1010 -0
- package/dist/{chunk-2ABMGUDS.js → chunk-EVBK675R.js} +88 -10
- package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
- package/dist/chunk-KXZUCCEY.js +142 -0
- package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
- package/dist/{chunk-N4VTG3KH.js → chunk-QK3VJNCK.js} +930 -3011
- package/dist/chunk-RW5LP57K.js +44 -0
- package/dist/{chunk-6JZIX5WW.js → chunk-SSSZUVWM.js} +178 -8
- package/dist/{chunk-TKA6ZP4M.js → chunk-U6M3ATSG.js} +56 -426
- package/dist/chunk-VRQDX5P4.js +1742 -0
- package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
- package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
- package/dist/{credit-menu-BM4qCD5U.d.ts → credit-menu-C1ezIFlJ.d.ts} +1699 -1941
- package/dist/{fx-C-liI3oY.d.ts → fx-C6dl2LVI.d.ts} +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +8 -4
- package/dist/internal/caller.d.ts +10441 -0
- package/dist/internal/caller.js +10078 -0
- package/dist/internal/index.d.ts +2 -10816
- package/dist/internal/index.js +2 -10130
- package/dist/internal/server.d.ts +404 -0
- package/dist/internal/server.js +202 -0
- package/dist/job-store-DHnW4Cg_.d.ts +591 -0
- package/dist/lightning-backend-Ci1nogk_.d.ts +367 -0
- package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
- package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-3RAXMNSY.js} +1 -1
- package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-ASZ7SHHH.js} +1 -1
- package/dist/server/index.d.ts +41 -11
- package/dist/server/index.js +93 -69
- package/dist/{job-store-C53VQ5uu.d.ts → step-cache-BLPZNizw.d.ts} +176 -585
- package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
- package/dist/testing/index.d.ts +25 -3
- package/dist/testing/index.js +36 -3
- package/dist/{usd-gLcJB1ps.d.ts → usd-BgOfZlk6.d.ts} +1 -1
- package/dist/wallet-CJC8lwxx.d.ts +29 -0
- package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
- package/package.json +11 -5
- package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
|
@@ -1,3 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MemoryProcessedPaymentStore,
|
|
3
|
+
ProcessedPaymentReplayError
|
|
4
|
+
} from "./chunk-DMNLFNTW.js";
|
|
5
|
+
import {
|
|
6
|
+
createX402FacilitatorAuthHeaders,
|
|
7
|
+
createX402FacilitatorClient,
|
|
8
|
+
settleWithFacilitator,
|
|
9
|
+
verifyWithFacilitator
|
|
10
|
+
} from "./chunk-BQ2NMWKE.js";
|
|
11
|
+
import {
|
|
12
|
+
MemoryCreditLedger
|
|
13
|
+
} from "./chunk-DBCLBYHP.js";
|
|
14
|
+
import {
|
|
15
|
+
SIGNED_ENVELOPE_FIELDS,
|
|
16
|
+
SignedRequestError,
|
|
17
|
+
humanizeZodError,
|
|
18
|
+
humanizeZodIssues,
|
|
19
|
+
signedRequestInput,
|
|
20
|
+
zodIssuesOf
|
|
21
|
+
} from "./chunk-H2MEFVH6.js";
|
|
22
|
+
import {
|
|
23
|
+
JobCancelledError,
|
|
24
|
+
MemoryJobStore,
|
|
25
|
+
UNCAPPABLE_ASK_TOTAL,
|
|
26
|
+
abortJob,
|
|
27
|
+
accumulateAskedTopUp,
|
|
28
|
+
createInstrumentedFetch,
|
|
29
|
+
fromJobRecord,
|
|
30
|
+
isJobRetentionStore,
|
|
31
|
+
isReceiptIssuingStore,
|
|
32
|
+
isStreamableJobStore,
|
|
33
|
+
isTerminal,
|
|
34
|
+
isYieldMessage,
|
|
35
|
+
localMessage,
|
|
36
|
+
providerMessage,
|
|
37
|
+
toJobRecord
|
|
38
|
+
} from "./chunk-U6M3ATSG.js";
|
|
39
|
+
import {
|
|
40
|
+
ADMIN_AUTH_CLOCK_SKEW_SECONDS,
|
|
41
|
+
ADMIN_AUTH_NONCE_TTL_SECONDS,
|
|
42
|
+
ADMIN_STATE_UNAVAILABLE,
|
|
43
|
+
TEMPO_SESSION_UPDATED_REASON,
|
|
44
|
+
X402_BATCH_CHANNEL_ABI,
|
|
45
|
+
attestedDvmIdentity,
|
|
46
|
+
buildAdminChallenge,
|
|
47
|
+
checkMintHealth,
|
|
48
|
+
computeResultHash,
|
|
49
|
+
createMonotonicClock,
|
|
50
|
+
getWallet,
|
|
51
|
+
isPublicRequestId,
|
|
52
|
+
isX402SettlementReconciliationReason,
|
|
53
|
+
normaliseMintUrl,
|
|
54
|
+
parseTraceparent,
|
|
55
|
+
receiptPubkeyFromSecret,
|
|
56
|
+
signDrainReceipt,
|
|
57
|
+
signFundingReceipt,
|
|
58
|
+
signReceipt,
|
|
59
|
+
toLockPubkey,
|
|
60
|
+
verifyCashuToken,
|
|
61
|
+
verifyChallenge
|
|
62
|
+
} from "./chunk-VRQDX5P4.js";
|
|
1
63
|
import {
|
|
2
64
|
X402_BATCH_SETTLEMENT_SCHEME,
|
|
3
65
|
X402_DEFAULT_FACILITATOR,
|
|
@@ -6,78 +68,38 @@ import {
|
|
|
6
68
|
X402_VERSION,
|
|
7
69
|
buildPaymentRequiredV2,
|
|
8
70
|
buildPaymentRequirements,
|
|
9
|
-
createX402FacilitatorAuthHeaders,
|
|
10
|
-
createX402FacilitatorClient,
|
|
11
71
|
decodePayment,
|
|
12
72
|
encodePaymentRequiredHeader,
|
|
13
73
|
encodeSettleResponseHeader,
|
|
14
74
|
exactEvmAuthorization,
|
|
15
75
|
paymentRequiredV2FromV1,
|
|
16
|
-
settleWithFacilitator,
|
|
17
|
-
verifyWithFacilitator,
|
|
18
76
|
x402NetworkByCaip2,
|
|
19
77
|
x402NetworkToCaip2
|
|
20
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-Z4BNLUZF.js";
|
|
21
79
|
import {
|
|
22
80
|
IMPLICIT_CREDIT_TTL_MS,
|
|
23
|
-
costAnchor,
|
|
24
81
|
formatStaleness,
|
|
25
|
-
formatUsd,
|
|
26
82
|
msatsToUsd,
|
|
27
83
|
msatsToUsdc,
|
|
28
84
|
usdcToMsats
|
|
29
85
|
} from "./chunk-5URG56JJ.js";
|
|
30
86
|
import {
|
|
31
|
-
AGENT_MNEMONIC_FILE,
|
|
32
|
-
BUILDER_CONFIG_DIR,
|
|
33
|
-
BUILDER_FILE,
|
|
34
|
-
BUILDER_KEY_FILE,
|
|
35
|
-
DvmError,
|
|
36
|
-
WALLET_FILE,
|
|
37
|
-
ensureConfigDir,
|
|
38
87
|
resolveRpcOverride
|
|
39
88
|
} from "./chunk-MKI6OVW4.js";
|
|
40
|
-
import {
|
|
41
|
-
MemoryCreditLedger
|
|
42
|
-
} from "./chunk-EXHBXA4U.js";
|
|
43
89
|
import {
|
|
44
90
|
RAIL_REFUNDABLE,
|
|
45
|
-
SIGNED_ENVELOPE_FIELDS,
|
|
46
|
-
SignedRequestError,
|
|
47
91
|
canonicalRequestPath,
|
|
48
|
-
canonicaliseForSigning,
|
|
49
92
|
canonicalize,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
isZodSchema,
|
|
53
|
-
signedRequestInput,
|
|
54
|
-
zodIssuesOf
|
|
55
|
-
} from "./chunk-P4RUVDU7.js";
|
|
93
|
+
isZodSchema
|
|
94
|
+
} from "./chunk-KXZUCCEY.js";
|
|
56
95
|
import {
|
|
57
|
-
JobCancelledError,
|
|
58
|
-
MemoryJobStore,
|
|
59
96
|
StepCache,
|
|
60
|
-
UNCAPPABLE_ASK_TOTAL,
|
|
61
|
-
abortJob,
|
|
62
|
-
accumulateAskedTopUp,
|
|
63
97
|
createConsoleLogger,
|
|
64
|
-
createInstrumentedFetch,
|
|
65
|
-
fromJobRecord,
|
|
66
|
-
isReceiptIssuingStore,
|
|
67
|
-
isStreamableJobStore,
|
|
68
|
-
isTerminal,
|
|
69
|
-
isYieldMessage,
|
|
70
|
-
localMessage,
|
|
71
98
|
loggers,
|
|
72
99
|
pinDimension,
|
|
73
|
-
providerMessage,
|
|
74
|
-
toJobRecord,
|
|
75
100
|
withSpan,
|
|
76
101
|
withTraceContext
|
|
77
|
-
} from "./chunk-
|
|
78
|
-
import {
|
|
79
|
-
callerLoggers
|
|
80
|
-
} from "./chunk-66HGCPBU.js";
|
|
102
|
+
} from "./chunk-E4EVGPDX.js";
|
|
81
103
|
import {
|
|
82
104
|
FxRateUnavailableError,
|
|
83
105
|
InvalidCurrencyError,
|
|
@@ -107,7 +129,10 @@ import {
|
|
|
107
129
|
isNonChannelBitcoinRail,
|
|
108
130
|
netOwedSats,
|
|
109
131
|
x402SettlementPending
|
|
110
|
-
} from "./chunk-
|
|
132
|
+
} from "./chunk-MLRCSJYX.js";
|
|
133
|
+
import {
|
|
134
|
+
withSdkInitLock
|
|
135
|
+
} from "./chunk-S3XAHZQY.js";
|
|
111
136
|
import {
|
|
112
137
|
withPostgresRecoveryContext
|
|
113
138
|
} from "./chunk-C3MTFLC6.js";
|
|
@@ -116,18 +141,6 @@ import {
|
|
|
116
141
|
challengeMeta,
|
|
117
142
|
isTempoInsufficientFundsError
|
|
118
143
|
} from "./chunk-5GFED3GJ.js";
|
|
119
|
-
import {
|
|
120
|
-
MemoryProcessedPaymentStore,
|
|
121
|
-
ProcessedPaymentReplayError
|
|
122
|
-
} from "./chunk-DMNLFNTW.js";
|
|
123
|
-
import {
|
|
124
|
-
withSdkInitLock
|
|
125
|
-
} from "./chunk-S3XAHZQY.js";
|
|
126
|
-
|
|
127
|
-
// src/lib/cashu/lock-pubkey.ts
|
|
128
|
-
function toLockPubkey(raw) {
|
|
129
|
-
return raw.trim().toLowerCase();
|
|
130
|
-
}
|
|
131
144
|
|
|
132
145
|
// src/sdk/server/lock-pubkey-store.ts
|
|
133
146
|
async function initLockPubkeyTable(db) {
|
|
@@ -849,12 +862,12 @@ async function buildCreditMenu(args) {
|
|
|
849
862
|
method.intent === "session" ? "tempo_session" : "tempo_charge"
|
|
850
863
|
)
|
|
851
864
|
);
|
|
852
|
-
const x402Schemes = (args.x402Schemes ?? []).filter(
|
|
865
|
+
const x402Schemes = args.currency === "usd" ? (args.x402Schemes ?? []).filter(
|
|
853
866
|
(scheme) => stablecoinCreditInstrumentAllowed(
|
|
854
867
|
args.config.allowOneShotStablecoin,
|
|
855
868
|
scheme.scheme === "batch-settlement" ? "x402_batch_settlement" : "x402_exact"
|
|
856
869
|
)
|
|
857
|
-
);
|
|
870
|
+
) : [];
|
|
858
871
|
const funding = args.funding.filter((rail) => {
|
|
859
872
|
if (rail === "tempo") return tempoMethods.length > 0;
|
|
860
873
|
if (rail === "x402") return x402Schemes.length > 0;
|
|
@@ -999,11 +1012,6 @@ function warnOnce(event, message) {
|
|
|
999
1012
|
console.warn(JSON.stringify({ level: "warn", event, message }));
|
|
1000
1013
|
}
|
|
1001
1014
|
|
|
1002
|
-
// src/lib/x402/types.ts
|
|
1003
|
-
function isX402SettlementReconciliationReason(value) {
|
|
1004
|
-
return value === "settlement_not_on_chain" || value === "chain_unreachable" || value === "settlement_unbookmarked" || value === "facilitator_channel_state_unavailable";
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
1015
|
// src/sdk/server/funding.ts
|
|
1008
1016
|
import { createHash as createHash2 } from "crypto";
|
|
1009
1017
|
function resolvePriceFiat(args) {
|
|
@@ -1092,9 +1100,11 @@ function fundedMicroFor(priceFiat, paidMsats, requiredMsats) {
|
|
|
1092
1100
|
if (paidMsats === requiredMsats) return priceFiat.amountMicro;
|
|
1093
1101
|
return Math.round(priceFiat.amountMicro * paidMsats / requiredMsats);
|
|
1094
1102
|
}
|
|
1095
|
-
function x402RequiredUsdcMicro(priceFiat
|
|
1096
|
-
if (priceFiat
|
|
1097
|
-
|
|
1103
|
+
function x402RequiredUsdcMicro(priceFiat) {
|
|
1104
|
+
if (priceFiat.currency !== "usd") {
|
|
1105
|
+
throw new Error(`x402 is not offered for ${priceFiat.currency}-priced DVMs`);
|
|
1106
|
+
}
|
|
1107
|
+
return priceFiat.amountMicro;
|
|
1098
1108
|
}
|
|
1099
1109
|
function x402SettledShare(quoted, requiredUsdcMicro, settledUsdcMicro) {
|
|
1100
1110
|
if (requiredUsdcMicro <= 0 || settledUsdcMicro === requiredUsdcMicro) return quoted;
|
|
@@ -1229,889 +1239,67 @@ function rateUnavailable() {
|
|
|
1229
1239
|
);
|
|
1230
1240
|
}
|
|
1231
1241
|
|
|
1232
|
-
// src/
|
|
1233
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
1234
|
-
function backupSecretFile(file, nextContent) {
|
|
1235
|
-
const content = existsSync(file) ? readFileSync(file) : null;
|
|
1236
|
-
if (content === null) return null;
|
|
1237
|
-
if (content.toString("utf-8").trim() === nextContent?.trim()) return null;
|
|
1238
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1239
|
-
for (let n = 0; n < 100; n++) {
|
|
1240
|
-
const backup = n === 0 ? `${file}.${stamp}.bak` : `${file}.${stamp}-${String(n)}.bak`;
|
|
1241
|
-
try {
|
|
1242
|
-
writeFileSync(backup, content, { mode: 384, flag: "wx" });
|
|
1243
|
-
return backup;
|
|
1244
|
-
} catch (err) {
|
|
1245
|
-
if (err.code !== "EEXIST") throw err;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
throw new Error(`Could not create a backup of ${file}: 100 candidate paths already exist.`);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
// src/lib/wallet-agent/agent-mnemonic-file.ts
|
|
1252
|
-
import {
|
|
1253
|
-
chmodSync,
|
|
1254
|
-
existsSync as existsSync2,
|
|
1255
|
-
readFileSync as readFileSync2,
|
|
1256
|
-
renameSync,
|
|
1257
|
-
unlinkSync,
|
|
1258
|
-
writeFileSync as writeFileSync2
|
|
1259
|
-
} from "fs";
|
|
1260
|
-
function readAgentMnemonic() {
|
|
1261
|
-
if (!existsSync2(AGENT_MNEMONIC_FILE)) return null;
|
|
1262
|
-
const raw = readFileSync2(AGENT_MNEMONIC_FILE, "utf-8");
|
|
1263
|
-
return raw.trim().toLowerCase().split(/\s+/).join(" ");
|
|
1264
|
-
}
|
|
1265
|
-
function assertAgentMnemonicReplaceable(opts = {}) {
|
|
1266
|
-
if (existsSync2(AGENT_MNEMONIC_FILE) && !opts.force) {
|
|
1267
|
-
throw new DvmError(
|
|
1268
|
-
"agent_mnemonic_exists",
|
|
1269
|
-
`An agent wallet recovery mnemonic already exists at ${AGENT_MNEMONIC_FILE}.`,
|
|
1270
|
-
"Re-run with --force to replace it. WARNING: funds locked to keys derived from the current words become unrecoverable without a backup. The replaced mnemonic is copied to a timestamped .bak sibling."
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
function writeAgentMnemonic(mnemonic, opts = {}) {
|
|
1275
|
-
const normalised = mnemonic.trim().toLowerCase().split(/\s+/).join(" ");
|
|
1276
|
-
assertAgentMnemonicReplaceable(opts);
|
|
1277
|
-
ensureConfigDir();
|
|
1278
|
-
const backupFile = backupSecretFile(AGENT_MNEMONIC_FILE, normalised);
|
|
1279
|
-
const tmp = `${AGENT_MNEMONIC_FILE}.tmp`;
|
|
1280
|
-
writeFileSync2(tmp, normalised + "\n", { mode: 384 });
|
|
1281
|
-
try {
|
|
1282
|
-
renameSync(tmp, AGENT_MNEMONIC_FILE);
|
|
1283
|
-
} catch (err) {
|
|
1284
|
-
try {
|
|
1285
|
-
unlinkSync(tmp);
|
|
1286
|
-
} catch {
|
|
1287
|
-
}
|
|
1288
|
-
throw err;
|
|
1289
|
-
}
|
|
1290
|
-
try {
|
|
1291
|
-
chmodSync(AGENT_MNEMONIC_FILE, 384);
|
|
1292
|
-
} catch {
|
|
1293
|
-
}
|
|
1294
|
-
return { backupFile };
|
|
1295
|
-
}
|
|
1296
|
-
function deleteAgentMnemonic() {
|
|
1297
|
-
if (!existsSync2(AGENT_MNEMONIC_FILE)) return { backupFile: null };
|
|
1298
|
-
const backupFile = backupSecretFile(AGENT_MNEMONIC_FILE);
|
|
1299
|
-
unlinkSync(AGENT_MNEMONIC_FILE);
|
|
1300
|
-
return { backupFile };
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// src/lib/wallet-agent/mnemonic.ts
|
|
1242
|
+
// src/sdk/server/x402-batch-settlement.ts
|
|
1304
1243
|
import { createHash as createHash3 } from "crypto";
|
|
1305
|
-
import {
|
|
1244
|
+
import { isDeepStrictEqual } from "util";
|
|
1245
|
+
import { x402ResourceServer } from "@x402/core/server";
|
|
1306
1246
|
import {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
} from "@
|
|
1312
|
-
import {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
}
|
|
1319
|
-
function generateAgentMnemonic() {
|
|
1320
|
-
return generateMnemonic(wordlist, MNEMONIC_STRENGTH_BITS);
|
|
1321
|
-
}
|
|
1322
|
-
function deriveAgentMasterKeypair(mnemonic) {
|
|
1323
|
-
const seed = mnemonicToSeedSync(mnemonic);
|
|
1324
|
-
const hd = HDKey.fromMasterSeed(seed);
|
|
1325
|
-
const child = hd.derive(agentMasterKeypairPath());
|
|
1326
|
-
if (!child.privateKey || !child.publicKey) {
|
|
1327
|
-
throw new Error("Failed to derive agent master keypair from mnemonic");
|
|
1328
|
-
}
|
|
1329
|
-
return {
|
|
1330
|
-
privkey: Buffer.from(child.privateKey).toString("hex"),
|
|
1331
|
-
pubkey: Buffer.from(child.publicKey).toString("hex")
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
function validateAgentMnemonic(mnemonic) {
|
|
1335
|
-
const words = mnemonic.trim().toLowerCase().split(/\s+/);
|
|
1336
|
-
if (words.length !== 12) {
|
|
1337
|
-
return { ok: false, reason: "wrong_length", wordCount: words.length };
|
|
1338
|
-
}
|
|
1339
|
-
for (let i = 0; i < words.length; i++) {
|
|
1340
|
-
if (!wordlist.includes(words[i])) {
|
|
1341
|
-
return { ok: false, reason: "unknown_word", badWordIndex: i + 1, badWord: words[i] };
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
if (!validateMnemonic(words.join(" "), wordlist)) {
|
|
1345
|
-
return { ok: false, reason: "checksum" };
|
|
1346
|
-
}
|
|
1347
|
-
return { ok: true };
|
|
1348
|
-
}
|
|
1349
|
-
function looksLikeMnemonic(input) {
|
|
1350
|
-
return /\s/.test(input.trim());
|
|
1351
|
-
}
|
|
1352
|
-
function mnemonicFingerprint(mnemonic) {
|
|
1353
|
-
const normalised = mnemonic.trim().toLowerCase().split(/\s+/).join(" ");
|
|
1354
|
-
const entropy = mnemonicToEntropy(normalised, wordlist);
|
|
1355
|
-
return createHash3("sha256").update(entropy).digest("hex");
|
|
1356
|
-
}
|
|
1247
|
+
BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS2,
|
|
1248
|
+
isBatchSettlementDepositPayload as isBatchSettlementDepositPayload2,
|
|
1249
|
+
isBatchSettlementRefundPayload as isBatchSettlementRefundPayload2,
|
|
1250
|
+
isBatchSettlementVoucherPayload
|
|
1251
|
+
} from "@x402/evm";
|
|
1252
|
+
import {
|
|
1253
|
+
BatchSettlementEvmScheme as BatchSettlementEvmScheme2,
|
|
1254
|
+
InMemoryChannelStorage
|
|
1255
|
+
} from "@x402/evm/batch-settlement/server";
|
|
1256
|
+
import { createPublicClient as createPublicClient3, http as http3 } from "viem";
|
|
1257
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
1357
1258
|
|
|
1358
|
-
// src/
|
|
1359
|
-
import {
|
|
1360
|
-
import
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
var
|
|
1365
|
-
var
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const raw = readFileSync3(WALLET_FILE, "utf-8");
|
|
1371
|
-
let parsed;
|
|
1372
|
-
try {
|
|
1373
|
-
parsed = JSON.parse(raw);
|
|
1374
|
-
} catch (err) {
|
|
1375
|
-
throw new DvmError(
|
|
1376
|
-
"wallet_corrupt",
|
|
1377
|
-
`Wallet file at ${WALLET_FILE} is not valid JSON.`,
|
|
1378
|
-
`Restore a backup or delete the file. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
|
|
1382
|
-
throw new DvmError(
|
|
1383
|
-
"wallet_corrupt",
|
|
1384
|
-
`Wallet file at ${WALLET_FILE} is missing a version field.`,
|
|
1385
|
-
"Restore a backup or run: dvm wallet init --force"
|
|
1386
|
-
);
|
|
1387
|
-
}
|
|
1388
|
-
const version = parsed.version;
|
|
1389
|
-
if (version === 1) {
|
|
1390
|
-
return migrateV7ToV8(
|
|
1391
|
-
migrateV6ToV7(migrateV5ToV6(migrateV4ToV5(migrateV1ToV4(parsed))))
|
|
1392
|
-
);
|
|
1393
|
-
}
|
|
1394
|
-
if (version === 2) {
|
|
1395
|
-
return migrateV7ToV8(
|
|
1396
|
-
migrateV6ToV7(
|
|
1397
|
-
migrateV5ToV6(
|
|
1398
|
-
migrateV4ToV5(migrateV3ToV4(migrateV2ToV3(parsed)))
|
|
1399
|
-
)
|
|
1400
|
-
)
|
|
1401
|
-
);
|
|
1402
|
-
}
|
|
1403
|
-
if (version === 3) {
|
|
1404
|
-
return migrateV7ToV8(
|
|
1405
|
-
migrateV6ToV7(migrateV5ToV6(migrateV4ToV5(migrateV3ToV4(parsed))))
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
if (version === 4) {
|
|
1409
|
-
return migrateV7ToV8(
|
|
1410
|
-
migrateV6ToV7(migrateV5ToV6(migrateV4ToV5(parsed)))
|
|
1411
|
-
);
|
|
1412
|
-
}
|
|
1413
|
-
if (version === 5) {
|
|
1414
|
-
return migrateV7ToV8(migrateV6ToV7(migrateV5ToV6(parsed)));
|
|
1415
|
-
}
|
|
1416
|
-
if (version === 6) {
|
|
1417
|
-
return migrateV7ToV8(migrateV6ToV7(parsed));
|
|
1418
|
-
}
|
|
1419
|
-
if (version === 7) {
|
|
1420
|
-
return migrateV7ToV8(parsed);
|
|
1421
|
-
}
|
|
1422
|
-
if (version !== WALLET_VERSION) {
|
|
1423
|
-
throw new DvmError(
|
|
1424
|
-
"wallet_unknown_version",
|
|
1425
|
-
`Unknown wallet version ${String(version)} (this CLI understands version ${String(WALLET_VERSION)}).`,
|
|
1426
|
-
"Upgrade dvm CLI: a newer version wrote this wallet."
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
return parsed;
|
|
1430
|
-
}
|
|
1431
|
-
function saveAgentWallet(wallet) {
|
|
1432
|
-
ensureConfigDir();
|
|
1433
|
-
const tmp = `${WALLET_FILE}.tmp`;
|
|
1434
|
-
const data = JSON.stringify(wallet, null, 2) + "\n";
|
|
1435
|
-
writeFileSync3(tmp, data, { mode: 384 });
|
|
1436
|
-
try {
|
|
1437
|
-
renameSync2(tmp, WALLET_FILE);
|
|
1438
|
-
} catch (err) {
|
|
1439
|
-
try {
|
|
1440
|
-
unlinkSync2(tmp);
|
|
1441
|
-
} catch {
|
|
1442
|
-
}
|
|
1443
|
-
throw err;
|
|
1259
|
+
// src/sdk/server/x402-self-relay.ts
|
|
1260
|
+
import { x402Facilitator } from "@x402/core/facilitator";
|
|
1261
|
+
import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/facilitator";
|
|
1262
|
+
import { createPublicClient, createWalletClient, http } from "viem";
|
|
1263
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
1264
|
+
import { base } from "viem/chains";
|
|
1265
|
+
var X402_SELF_RELAY_DEFAULT_LOW_BALANCE_WEI = 1000000000000000n;
|
|
1266
|
+
var X402_SELF_RELAY_BALANCE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1267
|
+
function createX402SelfRelay(opts) {
|
|
1268
|
+
if (!opts.config.rpcUrl.trim()) throw new Error("x402 self-relay requires an explicit RPC URL");
|
|
1269
|
+
if (opts.config.lowBalanceWei !== void 0 && opts.config.lowBalanceWei <= 0n) {
|
|
1270
|
+
throw new Error("x402 self-relay lowBalanceWei must be positive");
|
|
1444
1271
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
return existsSync3(WALLET_FILE);
|
|
1448
|
-
}
|
|
1449
|
-
async function withWalletLock(fn, opts = {}) {
|
|
1450
|
-
ensureConfigDir();
|
|
1451
|
-
const stale = opts.stale ?? DEFAULT_LOCK_STALE_MS;
|
|
1452
|
-
const retries = opts.retries ?? DEFAULT_LOCK_RETRIES;
|
|
1453
|
-
const interval = opts.retryIntervalMs ?? DEFAULT_LOCK_RETRY_INTERVAL_MS;
|
|
1454
|
-
let release;
|
|
1455
|
-
try {
|
|
1456
|
-
release = await lockfile.lock(WALLET_FILE, {
|
|
1457
|
-
lockfilePath: LOCK_FILE,
|
|
1458
|
-
stale,
|
|
1459
|
-
realpath: false,
|
|
1460
|
-
retries: { retries, factor: 1, minTimeout: interval, maxTimeout: interval }
|
|
1461
|
-
});
|
|
1462
|
-
} catch (err) {
|
|
1463
|
-
throw new DvmError(
|
|
1464
|
-
"wallet_locked",
|
|
1465
|
-
`Could not acquire the agent wallet lock at ${LOCK_FILE} within the retry budget.`,
|
|
1466
|
-
`Another dvm CLI may be writing to ${WALLET_FILE}. Wait a moment and retry. If no other dvm CLI is running, remove ${LOCK_FILE} and try again.`,
|
|
1467
|
-
{ lock_file: LOCK_FILE, underlying: err instanceof Error ? err.message : String(err) }
|
|
1468
|
-
);
|
|
1272
|
+
if (opts.config.balanceCheckIntervalMs !== void 0 && (!Number.isFinite(opts.config.balanceCheckIntervalMs) || opts.config.balanceCheckIntervalMs <= 0)) {
|
|
1273
|
+
throw new Error("x402 self-relay balanceCheckIntervalMs must be positive");
|
|
1469
1274
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1275
|
+
const chain = relayChain(opts.network);
|
|
1276
|
+
const account = privateKeyToAccount(opts.config.privateKey);
|
|
1277
|
+
const transport = http(opts.config.rpcUrl);
|
|
1278
|
+
const publicClient = createPublicClient({ chain, transport });
|
|
1279
|
+
const walletClient = createWalletClient({ account, chain, transport });
|
|
1280
|
+
const observeRpcFailure = async (operation) => {
|
|
1473
1281
|
try {
|
|
1474
|
-
await
|
|
1475
|
-
} catch {
|
|
1282
|
+
return await operation();
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
const reason = classifyX402SelfRelayRpcFailure(error);
|
|
1285
|
+
if (reason) await reportRpcFailure(opts.config, account.address, opts.network, reason);
|
|
1286
|
+
throw error;
|
|
1476
1287
|
}
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
function effectiveMints(wallet) {
|
|
1480
|
-
return wallet.mints.length > 0 ? wallet.mints : [DEFAULT_AGENT_MINT_URL];
|
|
1481
|
-
}
|
|
1482
|
-
function effectiveFundingMint(mints) {
|
|
1483
|
-
return mints.length > 0 ? { url: mints[0], source: "configured" } : { url: DEFAULT_AGENT_MINT_URL, source: "fallback" };
|
|
1484
|
-
}
|
|
1485
|
-
function migrateV1ToV4(parsed) {
|
|
1486
|
-
const proofs = Array.isArray(parsed.proofs) ? parsed.proofs : [];
|
|
1487
|
-
const pending = Array.isArray(parsed.pending_mints) ? parsed.pending_mints : [];
|
|
1488
|
-
if (proofs.length > 0 || pending.length > 0) {
|
|
1489
|
-
throw new DvmError(
|
|
1490
|
-
"wallet_unknown_version",
|
|
1491
|
-
`Cannot migrate v1 wallet at ${WALLET_FILE}: contains ${proofs.length} proofs and ${pending.length} pending mints under the legacy schema.`,
|
|
1492
|
-
"v1 wallets predate the fund flow and are expected to be empty. If this wallet was somehow populated, restore a backup or run: dvm wallet init --force"
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
return {
|
|
1496
|
-
version: 4,
|
|
1497
|
-
lock_privkey: String(parsed.recovery_privkey ?? parsed.lock_privkey),
|
|
1498
|
-
lock_pubkey: String(parsed.recovery_pubkey ?? parsed.lock_pubkey),
|
|
1499
|
-
mints: Array.isArray(parsed.mints) ? parsed.mints : [],
|
|
1500
|
-
proofs: [],
|
|
1501
|
-
pending_mints: [],
|
|
1502
|
-
recent_spends: [],
|
|
1503
|
-
created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
function migrateV2ToV3(parsed) {
|
|
1507
|
-
return {
|
|
1508
|
-
...parsed,
|
|
1509
|
-
version: 3,
|
|
1510
|
-
recent_spends: []
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
function migrateV3ToV4(parsed) {
|
|
1514
|
-
return {
|
|
1515
|
-
version: 4,
|
|
1516
|
-
lock_privkey: String(parsed.recovery_privkey ?? parsed.lock_privkey),
|
|
1517
|
-
lock_pubkey: String(parsed.recovery_pubkey ?? parsed.lock_pubkey),
|
|
1518
|
-
mints: Array.isArray(parsed.mints) ? parsed.mints : [],
|
|
1519
|
-
proofs: Array.isArray(parsed.proofs) ? parsed.proofs : [],
|
|
1520
|
-
pending_mints: Array.isArray(parsed.pending_mints) ? parsed.pending_mints : [],
|
|
1521
|
-
recent_spends: Array.isArray(parsed.recent_spends) ? parsed.recent_spends : [],
|
|
1522
|
-
created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
function migrateV4ToV5(parsed) {
|
|
1526
|
-
return {
|
|
1527
|
-
version: 5,
|
|
1528
|
-
lock_privkey: String(parsed.lock_privkey),
|
|
1529
|
-
lock_pubkey: String(parsed.lock_pubkey),
|
|
1530
|
-
mints: Array.isArray(parsed.mints) ? parsed.mints : [],
|
|
1531
|
-
proofs: Array.isArray(parsed.proofs) ? parsed.proofs : [],
|
|
1532
|
-
pending_mints: Array.isArray(parsed.pending_mints) ? parsed.pending_mints : [],
|
|
1533
|
-
pending_submissions: Array.isArray(parsed.pending_submissions) ? parsed.pending_submissions : [],
|
|
1534
|
-
recent_spends: Array.isArray(parsed.recent_spends) ? parsed.recent_spends : [],
|
|
1535
|
-
created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
function migrateV5ToV6(parsed) {
|
|
1539
|
-
return {
|
|
1540
|
-
version: 6,
|
|
1541
|
-
lock_privkey: String(parsed.lock_privkey),
|
|
1542
|
-
lock_pubkey: String(parsed.lock_pubkey),
|
|
1543
|
-
mnemonic_fingerprint: typeof parsed.mnemonic_fingerprint === "string" ? parsed.mnemonic_fingerprint : null,
|
|
1544
|
-
mints: Array.isArray(parsed.mints) ? parsed.mints : [],
|
|
1545
|
-
proofs: Array.isArray(parsed.proofs) ? parsed.proofs : [],
|
|
1546
|
-
nut13_counters: isCounterMap(parsed.nut13_counters) ? parsed.nut13_counters : {},
|
|
1547
|
-
pending_mints: Array.isArray(parsed.pending_mints) ? parsed.pending_mints : [],
|
|
1548
|
-
pending_submissions: Array.isArray(parsed.pending_submissions) ? parsed.pending_submissions : [],
|
|
1549
|
-
recent_spends: Array.isArray(parsed.recent_spends) ? parsed.recent_spends : [],
|
|
1550
|
-
created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
function migrateV6ToV7(parsed) {
|
|
1554
|
-
return {
|
|
1555
|
-
version: 7,
|
|
1556
|
-
lock_privkey: String(parsed.lock_privkey),
|
|
1557
|
-
lock_pubkey: String(parsed.lock_pubkey),
|
|
1558
|
-
mnemonic_fingerprint: typeof parsed.mnemonic_fingerprint === "string" ? parsed.mnemonic_fingerprint : null,
|
|
1559
|
-
mints: Array.isArray(parsed.mints) ? parsed.mints : [],
|
|
1560
|
-
proofs: Array.isArray(parsed.proofs) ? parsed.proofs : [],
|
|
1561
|
-
nut13_counters: isCounterMap(parsed.nut13_counters) ? parsed.nut13_counters : {},
|
|
1562
|
-
pending_mints: Array.isArray(parsed.pending_mints) ? parsed.pending_mints : [],
|
|
1563
|
-
pending_submissions: Array.isArray(parsed.pending_submissions) ? parsed.pending_submissions : [],
|
|
1564
|
-
recent_spends: Array.isArray(parsed.recent_spends) ? parsed.recent_spends : [],
|
|
1565
|
-
created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
function migrateV7ToV8(parsed) {
|
|
1569
|
-
return {
|
|
1570
|
-
version: WALLET_VERSION,
|
|
1571
|
-
lock_privkey: String(parsed.lock_privkey),
|
|
1572
|
-
lock_pubkey: String(parsed.lock_pubkey),
|
|
1573
|
-
mnemonic_fingerprint: typeof parsed.mnemonic_fingerprint === "string" ? parsed.mnemonic_fingerprint : null,
|
|
1574
|
-
mints: Array.isArray(parsed.mints) ? parsed.mints : [],
|
|
1575
|
-
proofs: Array.isArray(parsed.proofs) ? parsed.proofs : [],
|
|
1576
|
-
nut13_counters: isCounterMap(parsed.nut13_counters) ? parsed.nut13_counters : {},
|
|
1577
|
-
pending_mints: Array.isArray(parsed.pending_mints) ? parsed.pending_mints : [],
|
|
1578
|
-
pending_melts: Array.isArray(parsed.pending_melts) ? parsed.pending_melts : [],
|
|
1579
|
-
pending_submissions: Array.isArray(parsed.pending_submissions) ? parsed.pending_submissions : [],
|
|
1580
|
-
recent_spends: Array.isArray(parsed.recent_spends) ? parsed.recent_spends : [],
|
|
1581
|
-
created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
1582
1288
|
};
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
constructor(mintUrl) {
|
|
1598
|
-
this.mintUrl = mintUrl;
|
|
1599
|
-
}
|
|
1600
|
-
mintUrl;
|
|
1601
|
-
/**
|
|
1602
|
-
* Reserve `n` counters for `keysetId`. Returns `{ start, count: n }`
|
|
1603
|
-
* and bumps the persisted `next` to `start + n` under the wallet lock.
|
|
1604
|
-
*
|
|
1605
|
-
* Per cashu-ts contract, `n === 0` MUST NOT mutate state and is a
|
|
1606
|
-
* pure peek — we honour that by returning `{ start, count: 0 }`
|
|
1607
|
-
* without acquiring the lock.
|
|
1608
|
-
*/
|
|
1609
|
-
async reserve(keysetId, n) {
|
|
1610
|
-
if (n === 0) {
|
|
1611
|
-
const wallet = loadAgentWallet();
|
|
1612
|
-
const current = wallet?.nut13_counters[this.mintUrl]?.[keysetId] ?? 0;
|
|
1613
|
-
return { start: current, count: 0 };
|
|
1614
|
-
}
|
|
1615
|
-
return withWalletLock(() => {
|
|
1616
|
-
const wallet = this.loadOrThrow();
|
|
1617
|
-
const current = wallet.nut13_counters[this.mintUrl]?.[keysetId] ?? 0;
|
|
1618
|
-
saveAgentWallet(this.withCounter(wallet, keysetId, current + n));
|
|
1619
|
-
return { start: current, count: n };
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Monotonic bump: ensure the persisted `next` for `keysetId` is at
|
|
1624
|
-
* least `minNext`. No-op if the wallet is already ahead. Used by
|
|
1625
|
-
* cashu-ts when the mint reports `outputs_already_signed` — the
|
|
1626
|
-
* losing wallet rolls forward past the colliding counter.
|
|
1627
|
-
*/
|
|
1628
|
-
async advanceToAtLeast(keysetId, minNext) {
|
|
1629
|
-
await withWalletLock(() => {
|
|
1630
|
-
const wallet = this.loadOrThrow();
|
|
1631
|
-
const current = wallet.nut13_counters[this.mintUrl]?.[keysetId] ?? 0;
|
|
1632
|
-
if (current >= minNext) return;
|
|
1633
|
-
saveAgentWallet(this.withCounter(wallet, keysetId, minNext));
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
/**
|
|
1637
|
-
* Snapshot the per-keyset `next` map for this mint. Useful for
|
|
1638
|
-
* `dvm wallet show` and test assertions; cashu-ts itself does not
|
|
1639
|
-
* depend on `snapshot` being implemented.
|
|
1640
|
-
*/
|
|
1641
|
-
snapshot() {
|
|
1642
|
-
const wallet = loadAgentWallet();
|
|
1643
|
-
const counters = wallet?.nut13_counters[this.mintUrl] ?? {};
|
|
1644
|
-
const out = {};
|
|
1645
|
-
for (const [keyset, next] of Object.entries(counters)) {
|
|
1646
|
-
if (typeof next === "number") out[keyset] = next;
|
|
1647
|
-
}
|
|
1648
|
-
return Promise.resolve(out);
|
|
1649
|
-
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Hard-set the persisted `next` for `keysetId`. Used by
|
|
1652
|
-
* `dvm wallet restore` to re-anchor counters after walking
|
|
1653
|
-
* `batchRestore` per keyset. cashu-ts also exposes this on
|
|
1654
|
-
* `wallet.counters.setNext`.
|
|
1655
|
-
*/
|
|
1656
|
-
async setNext(keysetId, next) {
|
|
1657
|
-
await withWalletLock(() => {
|
|
1658
|
-
const wallet = this.loadOrThrow();
|
|
1659
|
-
saveAgentWallet(this.withCounter(wallet, keysetId, next));
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
loadOrThrow() {
|
|
1663
|
-
const wallet = loadAgentWallet();
|
|
1664
|
-
if (!wallet) {
|
|
1665
|
-
throw new DvmError(
|
|
1666
|
-
"wallet_missing",
|
|
1667
|
-
"NUT-13 counter persistence requires an agent wallet at ~/.dvm/wallet.json.",
|
|
1668
|
-
"Run `dvm wallet init` or `dvm wallet recover --file <path>` (or pipe the mnemonic via stdin) first."
|
|
1669
|
-
);
|
|
1670
|
-
}
|
|
1671
|
-
return wallet;
|
|
1672
|
-
}
|
|
1673
|
-
withCounter(wallet, keysetId, next) {
|
|
1674
|
-
return {
|
|
1675
|
-
...wallet,
|
|
1676
|
-
nut13_counters: {
|
|
1677
|
-
...wallet.nut13_counters,
|
|
1678
|
-
[this.mintUrl]: {
|
|
1679
|
-
...wallet.nut13_counters[this.mintUrl] ?? {},
|
|
1680
|
-
[keysetId]: next
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
};
|
|
1686
|
-
|
|
1687
|
-
// src/lib/cashu/health.ts
|
|
1688
|
-
var DEFAULT_REQUIRED_NUTS = [4, 5, 7, 9, 10, 11, 20];
|
|
1689
|
-
async function checkMintHealth(mintUrl, opts) {
|
|
1690
|
-
const timeoutMs = opts?.timeoutMs ?? 5e3;
|
|
1691
|
-
const retries = opts?.retries ?? 3;
|
|
1692
|
-
const backoff = opts?.backoffMs ?? [200, 500, 1e3];
|
|
1693
|
-
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
1694
|
-
const url = mintUrl.replace(/\/+$/, "") + "/v1/info";
|
|
1695
|
-
let lastError;
|
|
1696
|
-
let lastLatency = 0;
|
|
1697
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
1698
|
-
if (attempt > 0) {
|
|
1699
|
-
const delay = backoff[Math.min(attempt - 1, backoff.length - 1)];
|
|
1700
|
-
await sleep(delay);
|
|
1701
|
-
}
|
|
1702
|
-
const startedAt = Date.now();
|
|
1703
|
-
try {
|
|
1704
|
-
const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
1705
|
-
lastLatency = Date.now() - startedAt;
|
|
1706
|
-
if (!res.ok) {
|
|
1707
|
-
lastError = { code: "http_error", message: `HTTP ${res.status}` };
|
|
1708
|
-
continue;
|
|
1709
|
-
}
|
|
1710
|
-
let info;
|
|
1711
|
-
try {
|
|
1712
|
-
info = await res.json();
|
|
1713
|
-
} catch (err) {
|
|
1714
|
-
lastError = {
|
|
1715
|
-
code: "parse_error",
|
|
1716
|
-
message: err instanceof Error ? err.message : String(err)
|
|
1717
|
-
};
|
|
1718
|
-
continue;
|
|
1719
|
-
}
|
|
1720
|
-
return parseInfo(mintUrl, info, lastLatency);
|
|
1721
|
-
} catch (err) {
|
|
1722
|
-
lastLatency = Date.now() - startedAt;
|
|
1723
|
-
lastError = classifyFetchError(err);
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
return {
|
|
1727
|
-
mintUrl,
|
|
1728
|
-
ok: false,
|
|
1729
|
-
latencyMs: lastLatency,
|
|
1730
|
-
error: lastError ?? { code: "network", message: "no attempts recorded" }
|
|
1731
|
-
};
|
|
1732
|
-
}
|
|
1733
|
-
function parseInfo(mintUrl, info, latencyMs) {
|
|
1734
|
-
if (!isRecord(info)) {
|
|
1735
|
-
return {
|
|
1736
|
-
mintUrl,
|
|
1737
|
-
ok: false,
|
|
1738
|
-
latencyMs,
|
|
1739
|
-
error: { code: "parse_error", message: "/v1/info response is not an object" }
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
const pubkey = typeof info.pubkey === "string" ? info.pubkey : void 0;
|
|
1743
|
-
const version = typeof info.version === "string" ? info.version : void 0;
|
|
1744
|
-
if (!pubkey || !version) {
|
|
1745
|
-
const missing = [];
|
|
1746
|
-
if (!pubkey) missing.push("pubkey");
|
|
1747
|
-
if (!version) missing.push("version");
|
|
1748
|
-
return {
|
|
1749
|
-
mintUrl,
|
|
1750
|
-
ok: false,
|
|
1751
|
-
latencyMs,
|
|
1752
|
-
pubkey,
|
|
1753
|
-
version,
|
|
1754
|
-
error: {
|
|
1755
|
-
code: "missing_fields",
|
|
1756
|
-
message: `NUT-06 missing required fields: ${missing.join(", ")}`
|
|
1757
|
-
}
|
|
1758
|
-
};
|
|
1759
|
-
}
|
|
1760
|
-
const nutsRaw = isRecord(info.nuts) ? info.nuts : {};
|
|
1761
|
-
const supportedNuts = Object.keys(nutsRaw).sort(byNutKey);
|
|
1762
|
-
const missingRequiredNuts = DEFAULT_REQUIRED_NUTS.map(String).filter((n) => !(n in nutsRaw));
|
|
1763
|
-
const bolt11SatMintSupported = hasBolt11Sat(nutsRaw["4"]);
|
|
1764
|
-
const bolt11SatMeltSupported = hasBolt11Sat(nutsRaw["5"]);
|
|
1765
|
-
return {
|
|
1766
|
-
mintUrl,
|
|
1767
|
-
ok: true,
|
|
1768
|
-
latencyMs,
|
|
1769
|
-
pubkey,
|
|
1770
|
-
version,
|
|
1771
|
-
supportedNuts,
|
|
1772
|
-
missingRequiredNuts,
|
|
1773
|
-
bolt11SatMintSupported,
|
|
1774
|
-
bolt11SatMeltSupported,
|
|
1775
|
-
mintingEnabled: canMintBolt11Sat(nutsRaw),
|
|
1776
|
-
meltingEnabled: canMeltBolt11Sat(nutsRaw),
|
|
1777
|
-
swapEnabled: canSwap(nutsRaw),
|
|
1778
|
-
mintAmountBounds: mintAmountBounds(nutsRaw),
|
|
1779
|
-
meltAmountBounds: meltAmountBounds(nutsRaw),
|
|
1780
|
-
nutCompliant: evaluateNutSupport(nutsRaw).ok,
|
|
1781
|
-
nutsRaw
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
function canMintBolt11Sat(nuts) {
|
|
1785
|
-
return isMethodEnabled(nuts["4"]);
|
|
1786
|
-
}
|
|
1787
|
-
function canMeltBolt11Sat(nuts) {
|
|
1788
|
-
return isMethodEnabled(nuts["5"]);
|
|
1789
|
-
}
|
|
1790
|
-
function canSwap(nuts) {
|
|
1791
|
-
const node = nuts["3"];
|
|
1792
|
-
if (!isRecord(node)) return true;
|
|
1793
|
-
if (node.disabled === true) return false;
|
|
1794
|
-
return node.supported !== false;
|
|
1795
|
-
}
|
|
1796
|
-
function isMethodEnabled(node) {
|
|
1797
|
-
if (!hasBolt11Sat(node)) return false;
|
|
1798
|
-
return node.disabled !== true;
|
|
1799
|
-
}
|
|
1800
|
-
function mintAmountBounds(nuts) {
|
|
1801
|
-
return boundsOf(nuts["4"]);
|
|
1802
|
-
}
|
|
1803
|
-
function meltAmountBounds(nuts) {
|
|
1804
|
-
return boundsOf(nuts["5"]);
|
|
1805
|
-
}
|
|
1806
|
-
function amountBoundsVerdict(sats, bounds) {
|
|
1807
|
-
if (!bounds) return "ok";
|
|
1808
|
-
if (bounds.minSats !== null && sats < bounds.minSats) return "below_min";
|
|
1809
|
-
if (bounds.maxSats !== null && sats > bounds.maxSats) return "above_max";
|
|
1810
|
-
return "ok";
|
|
1811
|
-
}
|
|
1812
|
-
function boundsOf(node) {
|
|
1813
|
-
const method = findBolt11Sat(node);
|
|
1814
|
-
return {
|
|
1815
|
-
minSats: method ? toBoundSats(method.min_amount) : null,
|
|
1816
|
-
maxSats: method ? toBoundSats(method.max_amount) : null
|
|
1817
|
-
};
|
|
1818
|
-
}
|
|
1819
|
-
function toBoundSats(value) {
|
|
1820
|
-
if (typeof value === "bigint") {
|
|
1821
|
-
return value >= 0n && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : null;
|
|
1822
|
-
}
|
|
1823
|
-
const n = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
1824
|
-
if (!Number.isFinite(n) || n < 0) return null;
|
|
1825
|
-
return Math.floor(n);
|
|
1826
|
-
}
|
|
1827
|
-
function evaluateNutSupport(nuts, requiredNuts = DEFAULT_REQUIRED_NUTS) {
|
|
1828
|
-
const missingNuts = requiredNuts.filter((n) => {
|
|
1829
|
-
const entry = nuts[String(n)];
|
|
1830
|
-
if (entry === void 0 || entry === null) return true;
|
|
1831
|
-
return typeof entry === "object" && entry.supported === false;
|
|
1832
|
-
});
|
|
1833
|
-
const missingBolt11 = [];
|
|
1834
|
-
if (!hasBolt11Sat(nuts["4"])) missingBolt11.push("mint");
|
|
1835
|
-
if (!hasBolt11Sat(nuts["5"])) missingBolt11.push("melt");
|
|
1836
|
-
return {
|
|
1837
|
-
ok: missingNuts.length === 0 && missingBolt11.length === 0,
|
|
1838
|
-
missingNuts: missingNuts.length > 0 ? [...missingNuts] : void 0,
|
|
1839
|
-
missingBolt11: missingBolt11.length > 0 ? missingBolt11 : void 0
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
function assertNutSupport(mintUrl, nuts, requiredNuts = DEFAULT_REQUIRED_NUTS) {
|
|
1843
|
-
const verdict = evaluateNutSupport(nuts, requiredNuts);
|
|
1844
|
-
if (verdict.missingNuts && verdict.missingNuts.length > 0) {
|
|
1845
|
-
throw new DvmError(
|
|
1846
|
-
"cashu_nut_support_missing",
|
|
1847
|
-
`Mint ${mintUrl} does not support required NUTs: ${verdict.missingNuts.join(", ")}. Required: ${requiredNuts.join(", ")}.`,
|
|
1848
|
-
"Choose a mint that supports all required NUT capabilities for P2PK-locked Cashu payments."
|
|
1849
|
-
);
|
|
1850
|
-
}
|
|
1851
|
-
if (verdict.missingBolt11?.includes("mint")) {
|
|
1852
|
-
throw new DvmError(
|
|
1853
|
-
"cashu_nut_support_missing",
|
|
1854
|
-
`Mint ${mintUrl} does not advertise bolt11/sat method on NUT-4 (mint quotes).`,
|
|
1855
|
-
"Choose a mint that advertises bolt11/sat on NUT-4 mint quotes."
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
|
-
if (verdict.missingBolt11?.includes("melt")) {
|
|
1859
|
-
throw new DvmError(
|
|
1860
|
-
"cashu_nut_support_missing",
|
|
1861
|
-
`Mint ${mintUrl} does not advertise bolt11/sat method on NUT-5 (melt quotes).`,
|
|
1862
|
-
"Choose a mint that advertises bolt11/sat on NUT-5 melt quotes."
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
function hasBolt11Sat(node) {
|
|
1867
|
-
return findBolt11Sat(node) !== void 0;
|
|
1868
|
-
}
|
|
1869
|
-
function findBolt11Sat(node) {
|
|
1870
|
-
if (!isRecord(node)) return void 0;
|
|
1871
|
-
const methods = node.methods;
|
|
1872
|
-
if (!Array.isArray(methods)) return void 0;
|
|
1873
|
-
return methods.find((m) => isRecord(m) && m.method === "bolt11" && m.unit === "sat");
|
|
1874
|
-
}
|
|
1875
|
-
function classifyFetchError(err) {
|
|
1876
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1877
|
-
if (err instanceof Error) {
|
|
1878
|
-
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
1879
|
-
return { code: "timeout", message };
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
return { code: "network", message };
|
|
1883
|
-
}
|
|
1884
|
-
function isRecord(value) {
|
|
1885
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1886
|
-
}
|
|
1887
|
-
function byNutKey(a, b) {
|
|
1888
|
-
const an = Number(a);
|
|
1889
|
-
const bn = Number(b);
|
|
1890
|
-
if (Number.isFinite(an) && Number.isFinite(bn)) return an - bn;
|
|
1891
|
-
return a.localeCompare(b);
|
|
1892
|
-
}
|
|
1893
|
-
function sleep(ms) {
|
|
1894
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
// src/lib/cashu/wallet.ts
|
|
1898
|
-
import { Wallet } from "@cashu/cashu-ts";
|
|
1899
|
-
import { mnemonicToSeedSync as mnemonicToSeedSync2 } from "@scure/bip39";
|
|
1900
|
-
var walletCache = /* @__PURE__ */ new Map();
|
|
1901
|
-
var keysetResidueRegistry = /* @__PURE__ */ new Map();
|
|
1902
|
-
var NUT13_MODULUS = BigInt(2 ** 31 - 1);
|
|
1903
|
-
function assertNoKeysetCollision(keysets, mintUrl) {
|
|
1904
|
-
for (const keyset of keysets) {
|
|
1905
|
-
let residue;
|
|
1906
|
-
try {
|
|
1907
|
-
residue = BigInt("0x" + keyset.id) % NUT13_MODULUS;
|
|
1908
|
-
} catch {
|
|
1909
|
-
throw new DvmError(
|
|
1910
|
-
"cashu_keyset_collision",
|
|
1911
|
-
`Mint ${mintUrl} has keyset ID "${keyset.id}" that is not valid hex. Cannot verify NUT-13 collision safety.`,
|
|
1912
|
-
"Contact the mint operator to ensure the mint returns standard keyset IDs."
|
|
1913
|
-
);
|
|
1914
|
-
}
|
|
1915
|
-
const existing = keysetResidueRegistry.get(residue);
|
|
1916
|
-
if (existing && existing.keysetId !== keyset.id) {
|
|
1917
|
-
throw new DvmError(
|
|
1918
|
-
"cashu_keyset_collision",
|
|
1919
|
-
`Keyset collision detected: mint ${mintUrl} keyset ${keyset.id} and mint ${existing.mintUrl} keyset ${existing.keysetId} share the same 31-bit NUT-13 residue. Cannot safely derive deterministic secrets.`,
|
|
1920
|
-
"Contact the affected mint operators to rotate their keyset IDs."
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
1923
|
-
keysetResidueRegistry.set(residue, { keysetId: keyset.id, mintUrl });
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
async function getWallet(mintUrl) {
|
|
1927
|
-
const cached = walletCache.get(mintUrl);
|
|
1928
|
-
if (cached) return cached;
|
|
1929
|
-
return callerLoggers.cashu.span("cashu.get_wallet", { mint_url: mintUrl }, async () => {
|
|
1930
|
-
let nut13 = loadNut13Config(mintUrl);
|
|
1931
|
-
let wallet = await buildAndLoadWallet(mintUrl, nut13);
|
|
1932
|
-
if (nut13) {
|
|
1933
|
-
if (wallet.getKeyset().id.startsWith("00")) {
|
|
1934
|
-
nut13 = null;
|
|
1935
|
-
wallet = await buildAndLoadWallet(mintUrl, null);
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
assertNutSupport(mintUrl, wallet.getMintInfo().cache.nuts);
|
|
1939
|
-
if (nut13) {
|
|
1940
|
-
const restoreKeysets = wallet.keyChain.getKeysets().filter((keyset) => keyset.unit === wallet.unit && !keyset.id.startsWith("00"));
|
|
1941
|
-
assertNoKeysetCollision(restoreKeysets, mintUrl);
|
|
1942
|
-
}
|
|
1943
|
-
walletCache.set(mintUrl, wallet);
|
|
1944
|
-
return wallet;
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
async function buildAndLoadWallet(mintUrl, nut13) {
|
|
1948
|
-
const wallet = nut13 ? new Wallet(mintUrl, nut13) : new Wallet(mintUrl);
|
|
1949
|
-
try {
|
|
1950
|
-
await wallet.loadMint();
|
|
1951
|
-
} catch {
|
|
1952
|
-
throw new DvmError(
|
|
1953
|
-
"cashu_mint_unreachable",
|
|
1954
|
-
`Cannot connect to mint: ${mintUrl}`,
|
|
1955
|
-
"The mint is not responding. Try again later."
|
|
1956
|
-
);
|
|
1957
|
-
}
|
|
1958
|
-
return wallet;
|
|
1959
|
-
}
|
|
1960
|
-
function loadNut13Config(mintUrl) {
|
|
1961
|
-
const wallet = loadAgentWallet();
|
|
1962
|
-
if (!wallet?.mnemonic_fingerprint) return null;
|
|
1963
|
-
const mnemonic = readAgentMnemonic();
|
|
1964
|
-
if (!mnemonic) return null;
|
|
1965
|
-
const actual = mnemonicFingerprint(mnemonic);
|
|
1966
|
-
if (actual !== wallet.mnemonic_fingerprint) {
|
|
1967
|
-
throw new DvmError(
|
|
1968
|
-
"wallet_mnemonic_mismatch",
|
|
1969
|
-
"The mnemonic at ~/.dvm/wallet.mnemonic does not match the fingerprint recorded on ~/.dvm/wallet.json.",
|
|
1970
|
-
"Either restore the matching mnemonic file from a backup, or run `dvm wallet restore --force --file <path>` (or pipe the mnemonic via stdin) with the wallet's actual mnemonic to rebuild from scratch."
|
|
1971
|
-
);
|
|
1972
|
-
}
|
|
1973
|
-
return {
|
|
1974
|
-
bip39seed: mnemonicToSeedSync2(mnemonic),
|
|
1975
|
-
counterSource: new PersistentCounterSource(mintUrl)
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
// src/lib/x402/batch-abi.ts
|
|
1980
|
-
var X402_CHANNEL_CONFIG_COMPONENTS = [
|
|
1981
|
-
{ name: "payer", type: "address" },
|
|
1982
|
-
{ name: "payerAuthorizer", type: "address" },
|
|
1983
|
-
{ name: "receiver", type: "address" },
|
|
1984
|
-
{ name: "receiverAuthorizer", type: "address" },
|
|
1985
|
-
{ name: "token", type: "address" },
|
|
1986
|
-
{ name: "withdrawDelay", type: "uint40" },
|
|
1987
|
-
{ name: "salt", type: "bytes32" }
|
|
1988
|
-
];
|
|
1989
|
-
var X402_BATCH_CHANNEL_ABI = [
|
|
1990
|
-
{
|
|
1991
|
-
type: "function",
|
|
1992
|
-
name: "initiateWithdraw",
|
|
1993
|
-
inputs: [
|
|
1994
|
-
{ name: "config", type: "tuple", components: X402_CHANNEL_CONFIG_COMPONENTS },
|
|
1995
|
-
{ name: "amount", type: "uint128" }
|
|
1996
|
-
],
|
|
1997
|
-
outputs: [],
|
|
1998
|
-
stateMutability: "nonpayable"
|
|
1999
|
-
},
|
|
2000
|
-
{
|
|
2001
|
-
type: "function",
|
|
2002
|
-
name: "finalizeWithdraw",
|
|
2003
|
-
inputs: [{ name: "config", type: "tuple", components: X402_CHANNEL_CONFIG_COMPONENTS }],
|
|
2004
|
-
outputs: [],
|
|
2005
|
-
stateMutability: "nonpayable"
|
|
2006
|
-
},
|
|
2007
|
-
{
|
|
2008
|
-
type: "function",
|
|
2009
|
-
name: "channels",
|
|
2010
|
-
inputs: [{ name: "channelId", type: "bytes32" }],
|
|
2011
|
-
outputs: [
|
|
2012
|
-
{ name: "balance", type: "uint128" },
|
|
2013
|
-
{ name: "totalClaimed", type: "uint128" }
|
|
2014
|
-
],
|
|
2015
|
-
stateMutability: "view"
|
|
2016
|
-
},
|
|
2017
|
-
{
|
|
2018
|
-
type: "function",
|
|
2019
|
-
name: "pendingWithdrawals",
|
|
2020
|
-
inputs: [{ name: "channelId", type: "bytes32" }],
|
|
2021
|
-
outputs: [
|
|
2022
|
-
{ name: "amount", type: "uint128" },
|
|
2023
|
-
{ name: "initiatedAt", type: "uint40" }
|
|
2024
|
-
],
|
|
2025
|
-
stateMutability: "view"
|
|
2026
|
-
},
|
|
2027
|
-
{
|
|
2028
|
-
type: "function",
|
|
2029
|
-
name: "refundNonce",
|
|
2030
|
-
inputs: [{ name: "channelId", type: "bytes32" }],
|
|
2031
|
-
outputs: [{ name: "", type: "uint256" }],
|
|
2032
|
-
stateMutability: "view"
|
|
2033
|
-
},
|
|
2034
|
-
{
|
|
2035
|
-
type: "event",
|
|
2036
|
-
name: "WithdrawInitiated",
|
|
2037
|
-
inputs: [
|
|
2038
|
-
{ name: "channelId", type: "bytes32", indexed: true },
|
|
2039
|
-
{ name: "amount", type: "uint128", indexed: false },
|
|
2040
|
-
{ name: "finalizeAfter", type: "uint40", indexed: false }
|
|
2041
|
-
]
|
|
2042
|
-
},
|
|
2043
|
-
{
|
|
2044
|
-
type: "event",
|
|
2045
|
-
name: "WithdrawFinalized",
|
|
2046
|
-
inputs: [
|
|
2047
|
-
{ name: "channelId", type: "bytes32", indexed: true },
|
|
2048
|
-
{ name: "sender", type: "address", indexed: true },
|
|
2049
|
-
{ name: "amount", type: "uint128", indexed: false }
|
|
2050
|
-
]
|
|
2051
|
-
}
|
|
2052
|
-
];
|
|
2053
|
-
|
|
2054
|
-
// src/sdk/server/x402-batch-settlement.ts
|
|
2055
|
-
import { createHash as createHash4 } from "crypto";
|
|
2056
|
-
import { isDeepStrictEqual } from "util";
|
|
2057
|
-
import { x402ResourceServer } from "@x402/core/server";
|
|
2058
|
-
import {
|
|
2059
|
-
BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS2,
|
|
2060
|
-
isBatchSettlementDepositPayload as isBatchSettlementDepositPayload2,
|
|
2061
|
-
isBatchSettlementRefundPayload as isBatchSettlementRefundPayload2,
|
|
2062
|
-
isBatchSettlementVoucherPayload
|
|
2063
|
-
} from "@x402/evm";
|
|
2064
|
-
import {
|
|
2065
|
-
BatchSettlementEvmScheme as BatchSettlementEvmScheme2,
|
|
2066
|
-
InMemoryChannelStorage
|
|
2067
|
-
} from "@x402/evm/batch-settlement/server";
|
|
2068
|
-
import { createPublicClient as createPublicClient3, http as http3 } from "viem";
|
|
2069
|
-
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
2070
|
-
|
|
2071
|
-
// src/sdk/server/x402-self-relay.ts
|
|
2072
|
-
import { x402Facilitator } from "@x402/core/facilitator";
|
|
2073
|
-
import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/facilitator";
|
|
2074
|
-
import { createPublicClient, createWalletClient, http } from "viem";
|
|
2075
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
2076
|
-
import { base } from "viem/chains";
|
|
2077
|
-
var X402_SELF_RELAY_DEFAULT_LOW_BALANCE_WEI = 1000000000000000n;
|
|
2078
|
-
var X402_SELF_RELAY_BALANCE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2079
|
-
function createX402SelfRelay(opts) {
|
|
2080
|
-
if (!opts.config.rpcUrl.trim()) throw new Error("x402 self-relay requires an explicit RPC URL");
|
|
2081
|
-
if (opts.config.lowBalanceWei !== void 0 && opts.config.lowBalanceWei <= 0n) {
|
|
2082
|
-
throw new Error("x402 self-relay lowBalanceWei must be positive");
|
|
2083
|
-
}
|
|
2084
|
-
if (opts.config.balanceCheckIntervalMs !== void 0 && (!Number.isFinite(opts.config.balanceCheckIntervalMs) || opts.config.balanceCheckIntervalMs <= 0)) {
|
|
2085
|
-
throw new Error("x402 self-relay balanceCheckIntervalMs must be positive");
|
|
2086
|
-
}
|
|
2087
|
-
const chain = relayChain(opts.network);
|
|
2088
|
-
const account = privateKeyToAccount(opts.config.privateKey);
|
|
2089
|
-
const transport = http(opts.config.rpcUrl);
|
|
2090
|
-
const publicClient = createPublicClient({ chain, transport });
|
|
2091
|
-
const walletClient = createWalletClient({ account, chain, transport });
|
|
2092
|
-
const observeRpcFailure = async (operation) => {
|
|
2093
|
-
try {
|
|
2094
|
-
return await operation();
|
|
2095
|
-
} catch (error) {
|
|
2096
|
-
const reason = classifyX402SelfRelayRpcFailure(error);
|
|
2097
|
-
if (reason) await reportRpcFailure(opts.config, account.address, opts.network, reason);
|
|
2098
|
-
throw error;
|
|
2099
|
-
}
|
|
2100
|
-
};
|
|
2101
|
-
const signer = createX402SelfRelaySigner({
|
|
2102
|
-
network: opts.network,
|
|
2103
|
-
...opts.submissionLock ? { submissionLock: opts.submissionLock } : {},
|
|
2104
|
-
backend: {
|
|
2105
|
-
address: account.address,
|
|
2106
|
-
readContract: (args) => observeRpcFailure(() => publicClient.readContract(args)),
|
|
2107
|
-
verifyTypedData: (args) => observeRpcFailure(() => publicClient.verifyTypedData(args)),
|
|
2108
|
-
writeContract: (args) => observeRpcFailure(() => walletClient.writeContract(args)),
|
|
2109
|
-
sendTransaction: (args) => observeRpcFailure(() => walletClient.sendTransaction(args)),
|
|
2110
|
-
waitForTransactionReceipt: async (args) => {
|
|
2111
|
-
const receipt = await observeRpcFailure(() => publicClient.waitForTransactionReceipt(args));
|
|
2112
|
-
return { status: receipt.status, logs: receipt.logs };
|
|
2113
|
-
},
|
|
2114
|
-
getCode: (args) => observeRpcFailure(() => publicClient.getCode(args))
|
|
1289
|
+
const signer = createX402SelfRelaySigner({
|
|
1290
|
+
network: opts.network,
|
|
1291
|
+
...opts.submissionLock ? { submissionLock: opts.submissionLock } : {},
|
|
1292
|
+
backend: {
|
|
1293
|
+
address: account.address,
|
|
1294
|
+
readContract: (args) => observeRpcFailure(() => publicClient.readContract(args)),
|
|
1295
|
+
verifyTypedData: (args) => observeRpcFailure(() => publicClient.verifyTypedData(args)),
|
|
1296
|
+
writeContract: (args) => observeRpcFailure(() => walletClient.writeContract(args)),
|
|
1297
|
+
sendTransaction: (args) => observeRpcFailure(() => walletClient.sendTransaction(args)),
|
|
1298
|
+
waitForTransactionReceipt: async (args) => {
|
|
1299
|
+
const receipt = await observeRpcFailure(() => publicClient.waitForTransactionReceipt(args));
|
|
1300
|
+
return { status: receipt.status, logs: receipt.logs };
|
|
1301
|
+
},
|
|
1302
|
+
getCode: (args) => observeRpcFailure(() => publicClient.getCode(args))
|
|
2115
1303
|
}
|
|
2116
1304
|
});
|
|
2117
1305
|
const local = new x402Facilitator().register(
|
|
@@ -3678,7 +2866,7 @@ async function applyRecoveredChannelSettlement(storage, payload, requirements, s
|
|
|
3678
2866
|
if (!isBatchSettlementDepositPayload2(raw) && !isBatchSettlementRefundPayload2(raw)) {
|
|
3679
2867
|
throw new Error("x402 recovery supports only irreversible deposit/refund settlements");
|
|
3680
2868
|
}
|
|
3681
|
-
const channelState =
|
|
2869
|
+
const channelState = isRecord(settlement.extra?.channelState) ? settlement.extra.channelState : {};
|
|
3682
2870
|
const result = await storage.updateChannel(raw.voucher.channelId, (current) => {
|
|
3683
2871
|
const currentMatchesOriginalAuthority = current?.chargedCumulativeAmount === channelBefore.chargedCumulativeAmount && current.signedMaxClaimable === channelBefore.signedMaxClaimable && current.signature === channelBefore.signature && isDeepStrictEqual(current.channelConfig, channelBefore.channelConfig);
|
|
3684
2872
|
const intentCreatedMissingChannel = current === void 0 && isBatchSettlementDepositPayload2(raw);
|
|
@@ -3728,12 +2916,12 @@ async function applyRecoveredChannelSettlement(storage, payload, requirements, s
|
|
|
3728
2916
|
}
|
|
3729
2917
|
}
|
|
3730
2918
|
function settlementIdentity(operation, effectId) {
|
|
3731
|
-
return
|
|
2919
|
+
return createHash3("sha256").update(operation).update("\0").update(effectId).digest("hex");
|
|
3732
2920
|
}
|
|
3733
2921
|
function depositAuthorizationPaymentId(payload) {
|
|
3734
|
-
const deposit =
|
|
3735
|
-
const wrapper =
|
|
3736
|
-
const authorization =
|
|
2922
|
+
const deposit = isRecord(payload.deposit) ? payload.deposit : void 0;
|
|
2923
|
+
const wrapper = isRecord(deposit?.authorization) ? deposit.authorization : void 0;
|
|
2924
|
+
const authorization = isRecord(wrapper?.erc3009Authorization) ? wrapper.erc3009Authorization : void 0;
|
|
3737
2925
|
const salt = authorization?.salt;
|
|
3738
2926
|
const voucher = batchVoucher(payload);
|
|
3739
2927
|
if (typeof salt !== "string" || !/^0x[0-9a-f]{64}$/i.test(salt)) {
|
|
@@ -3786,10 +2974,10 @@ function withCachedSupportedResponse(facilitator, supportedResponse) {
|
|
|
3786
2974
|
}
|
|
3787
2975
|
function decodeBatchPayment(header) {
|
|
3788
2976
|
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
|
|
3789
|
-
if (!
|
|
2977
|
+
if (!isRecord(parsed) || parsed.x402Version !== 2 || !isRecord(parsed.accepted)) {
|
|
3790
2978
|
throw new Error("invalid batch-settlement payment envelope");
|
|
3791
2979
|
}
|
|
3792
|
-
if (parsed.accepted.scheme !== X402_BATCH_SETTLEMENT_SCHEME || typeof parsed.accepted.network !== "string" || !
|
|
2980
|
+
if (parsed.accepted.scheme !== X402_BATCH_SETTLEMENT_SCHEME || typeof parsed.accepted.network !== "string" || !isRecord(parsed.payload)) {
|
|
3793
2981
|
throw new Error("invalid batch-settlement payment envelope");
|
|
3794
2982
|
}
|
|
3795
2983
|
if (!isBatchSettlementDepositPayload2(parsed.payload) && !isBatchSettlementVoucherPayload(parsed.payload) && !isBatchSettlementRefundPayload2(parsed.payload)) {
|
|
@@ -3839,7 +3027,7 @@ async function restoreRevokedVoucher(storage, channelId, pendingId, before) {
|
|
|
3839
3027
|
};
|
|
3840
3028
|
});
|
|
3841
3029
|
}
|
|
3842
|
-
function
|
|
3030
|
+
function isRecord(value) {
|
|
3843
3031
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3844
3032
|
}
|
|
3845
3033
|
|
|
@@ -3847,46 +3035,6 @@ function isRecord2(value) {
|
|
|
3847
3035
|
import { randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
3848
3036
|
import { Challenge } from "mppx";
|
|
3849
3037
|
|
|
3850
|
-
// src/lib/cashu/verify.ts
|
|
3851
|
-
import { getTokenMetadata } from "@cashu/cashu-ts";
|
|
3852
|
-
function verifyCashuToken(token, acceptedMints) {
|
|
3853
|
-
let mint, unit, totalMsats;
|
|
3854
|
-
try {
|
|
3855
|
-
const meta = getTokenMetadata(token);
|
|
3856
|
-
mint = meta.mint;
|
|
3857
|
-
unit = meta.unit;
|
|
3858
|
-
totalMsats = meta.amount.toNumber() * 1e3;
|
|
3859
|
-
} catch {
|
|
3860
|
-
throw new DvmError(
|
|
3861
|
-
"cashu_invalid_token",
|
|
3862
|
-
"The Cashu token could not be decoded.",
|
|
3863
|
-
"Ensure the token is a valid cashuA... or cashuB... string."
|
|
3864
|
-
);
|
|
3865
|
-
}
|
|
3866
|
-
const normalised = acceptedMints.map(normaliseMintUrl);
|
|
3867
|
-
if (!normalised.includes(normaliseMintUrl(mint))) {
|
|
3868
|
-
throw new DvmError(
|
|
3869
|
-
"cashu_mint_not_accepted",
|
|
3870
|
-
`Mint ${mint} is not accepted by this provider.`,
|
|
3871
|
-
`Accepted mints: ${acceptedMints.join(", ")}`
|
|
3872
|
-
);
|
|
3873
|
-
}
|
|
3874
|
-
return { mint, totalMsats, unit };
|
|
3875
|
-
}
|
|
3876
|
-
function normaliseMintUrl(url) {
|
|
3877
|
-
try {
|
|
3878
|
-
const u = new URL(url);
|
|
3879
|
-
u.hostname = u.hostname.toLowerCase();
|
|
3880
|
-
u.pathname = u.pathname.replace(/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
3881
|
-
return u.toString().replace(/\/$/, "");
|
|
3882
|
-
} catch {
|
|
3883
|
-
return url.toLowerCase().replace(/\/+$/, "");
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
|
|
3887
|
-
// src/lib/payment-rails/tempo-wire.ts
|
|
3888
|
-
var TEMPO_SESSION_UPDATED_REASON = "Tempo session updated; attach the next voucher to fund this credit.";
|
|
3889
|
-
|
|
3890
3038
|
// src/sdk/server/mpp-units.ts
|
|
3891
3039
|
function decimalsFor(methodName) {
|
|
3892
3040
|
switch (methodName) {
|
|
@@ -4158,21 +3306,19 @@ async function verifyUpfrontPayment(opts) {
|
|
|
4158
3306
|
} = opts;
|
|
4159
3307
|
const sessionOffered = opts.tempoSessionOffered ?? true;
|
|
4160
3308
|
const sessionUnavailable = tempoSessionUnavailableCopy(opts.tempoSessionUnavailableReason);
|
|
3309
|
+
const x402PriceFiat = opts.priceFiat?.currency === "usd" ? opts.priceFiat : void 0;
|
|
3310
|
+
if (x402Payment && x402Config && requiredMsats && !x402PriceFiat) {
|
|
3311
|
+
return x402NotOfferedResult(opts.priceFiat?.currency);
|
|
3312
|
+
}
|
|
4161
3313
|
const sessionIssuable = sessionOffered || await targetCreditHoldsTempoChannel(opts);
|
|
4162
3314
|
const x402ExactVersions = opts.x402ExactVersions ?? { v1: true, v2: true };
|
|
4163
|
-
const x402Advertised = Boolean(x402Config) && (x402Config?.dualServe ?? true) && (x402ExactVersions.v1 || x402ExactVersions.v2);
|
|
4164
|
-
const needsRate = Boolean(requiredMsats) &&
|
|
3315
|
+
const x402Advertised = Boolean(x402Config) && Boolean(x402PriceFiat) && (x402Config?.dualServe ?? true) && (x402ExactVersions.v1 || x402ExactVersions.v2);
|
|
3316
|
+
const needsRate = Boolean(requiredMsats) && Boolean(mpp);
|
|
4165
3317
|
const rate = needsRate ? await resolveRateOrNull(opts.fxFetcher) : null;
|
|
4166
3318
|
const rateOutage = needsRate && rate === null;
|
|
4167
|
-
const x402Requirements = x402Config && x402Advertised &&
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
x402Resource ?? "/v1/job",
|
|
4171
|
-
rate,
|
|
4172
|
-
opts.priceFiat
|
|
4173
|
-
) : void 0;
|
|
4174
|
-
const x402BatchRequirements = opts.fundOnly && opts.x402BatchSettlement && x402Advertised && requiredMsats && rate !== null ? await opts.x402BatchSettlement.buildRequirements(
|
|
4175
|
-
String(x402RequiredUsdcMicro(opts.priceFiat, requiredMsats, rate))
|
|
3319
|
+
const x402Requirements = x402Config && x402Advertised && x402PriceFiat && requiredMsats && (!opts.fundOnly || stablecoinCreditInstrumentAllowed(opts.fundOnly.allowOneShotStablecoin, "x402_exact")) ? buildX402Requirements(x402Config, x402Resource ?? "/v1/job", x402PriceFiat) : void 0;
|
|
3320
|
+
const x402BatchRequirements = opts.fundOnly && opts.x402BatchSettlement && x402Advertised && x402PriceFiat && requiredMsats ? await opts.x402BatchSettlement.buildRequirements(
|
|
3321
|
+
String(x402RequiredUsdcMicro(x402PriceFiat))
|
|
4176
3322
|
) : void 0;
|
|
4177
3323
|
const ledgerCtx = resolveLedgerContext(opts);
|
|
4178
3324
|
const creditEnv = opts.creditEnvelope;
|
|
@@ -4328,17 +3474,18 @@ async function verifyUpfrontPayment(opts) {
|
|
|
4328
3474
|
};
|
|
4329
3475
|
}
|
|
4330
3476
|
if (x402Payment && x402Config && requiredMsats && paymentScheme(x402Payment) === X402_BATCH_SETTLEMENT_SCHEME) {
|
|
3477
|
+
if (!x402PriceFiat) return x402NotOfferedResult(opts.priceFiat?.currency);
|
|
4331
3478
|
if (!opts.fundOnly || !opts.x402BatchSettlement) {
|
|
4332
3479
|
return creditRefusal(
|
|
4333
3480
|
"payment_invalid",
|
|
4334
3481
|
"Reusable x402 channels are accepted only by POST /v1/credit."
|
|
4335
3482
|
);
|
|
4336
3483
|
}
|
|
4337
|
-
if (
|
|
3484
|
+
if (rateOutage) return rateUnavailableResult();
|
|
4338
3485
|
if (!ledgerCtx?.fundOnly?.creditId) {
|
|
4339
3486
|
throw new Error("x402 batch settlement requires a named credit funding effect");
|
|
4340
3487
|
}
|
|
4341
|
-
const requiredUsdcMicro = x402RequiredUsdcMicro(
|
|
3488
|
+
const requiredUsdcMicro = x402RequiredUsdcMicro(x402PriceFiat);
|
|
4342
3489
|
try {
|
|
4343
3490
|
const accepted = await opts.x402BatchSettlement.accept({
|
|
4344
3491
|
paymentHeader: x402Payment,
|
|
@@ -4414,12 +3561,13 @@ async function verifyUpfrontPayment(opts) {
|
|
|
4414
3561
|
}
|
|
4415
3562
|
}
|
|
4416
3563
|
if (x402Payment && x402Config && requiredMsats) {
|
|
3564
|
+
if (!x402PriceFiat) return x402NotOfferedResult(opts.priceFiat?.currency);
|
|
4417
3565
|
if (opts.fundOnly && !stablecoinCreditInstrumentAllowed(opts.fundOnly.allowOneShotStablecoin, "x402_exact")) {
|
|
4418
3566
|
return oneShotCreditRefusal("x402 exact");
|
|
4419
3567
|
}
|
|
4420
|
-
if (
|
|
3568
|
+
if (rateOutage) return rateUnavailableResult();
|
|
4421
3569
|
try {
|
|
4422
|
-
const requiredUsdcMicro = BigInt(x402RequiredUsdcMicro(
|
|
3570
|
+
const requiredUsdcMicro = BigInt(x402RequiredUsdcMicro(x402PriceFiat));
|
|
4423
3571
|
if (creditEnv?.fund && ledgerCtx) {
|
|
4424
3572
|
const predicted = predictX402FundedMicro(
|
|
4425
3573
|
x402Payment,
|
|
@@ -4449,7 +3597,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
4449
3597
|
config: x402Config,
|
|
4450
3598
|
requiredUsdcMicro,
|
|
4451
3599
|
resource: x402Resource ?? "/v1/job",
|
|
4452
|
-
btcUsdRate: rate,
|
|
3600
|
+
btcUsdRate: rate ?? 0,
|
|
4453
3601
|
exactVersions: x402ExactVersions,
|
|
4454
3602
|
dvmId: opts.dvmId ?? "",
|
|
4455
3603
|
requiredMsats,
|
|
@@ -4457,13 +3605,13 @@ async function verifyUpfrontPayment(opts) {
|
|
|
4457
3605
|
x402Requirements
|
|
4458
3606
|
});
|
|
4459
3607
|
}
|
|
4460
|
-
const { verifyX402Payment } = await import("./x402-
|
|
3608
|
+
const { verifyX402Payment } = await import("./x402-T2C5MX3T.js");
|
|
4461
3609
|
const receipt = await verifyX402Payment(
|
|
4462
3610
|
x402Payment,
|
|
4463
3611
|
x402Config,
|
|
4464
3612
|
requiredUsdcMicro,
|
|
4465
3613
|
x402Resource ?? "/v1/job",
|
|
4466
|
-
rate,
|
|
3614
|
+
rate ?? 0,
|
|
4467
3615
|
x402ExactVersions
|
|
4468
3616
|
);
|
|
4469
3617
|
if (receipt.verified) {
|
|
@@ -4962,6 +4110,11 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4962
4110
|
hint: "Request a fresh quote and pay again \u2014 the previous payment can't be reused.",
|
|
4963
4111
|
retryable: false
|
|
4964
4112
|
},
|
|
4113
|
+
x402_not_offered: {
|
|
4114
|
+
display: "This service does not offer x402 payment for prices in its configured currency. Nothing was charged.",
|
|
4115
|
+
hint: "Use one of the payment methods advertised by this service and submit the request again.",
|
|
4116
|
+
retryable: false
|
|
4117
|
+
},
|
|
4965
4118
|
x402_settlement_ambiguous: {
|
|
4966
4119
|
display: "The stablecoin payment may have settled, but the service has not finished recording what it bought. Do not pay again.",
|
|
4967
4120
|
hint: "Retry the identical request with the same signed payment. If it remains pending, give settlement_intent_id to the service operator; do not request a fresh quote or sign another authorization.",
|
|
@@ -5359,6 +4512,9 @@ async function verifyIncomingPayment(body, opts, snapshot) {
|
|
|
5359
4512
|
};
|
|
5360
4513
|
}
|
|
5361
4514
|
if (body.content.x402_payment && opts.x402Config) {
|
|
4515
|
+
if ((opts.pricingCurrency ?? "usd") !== "usd") {
|
|
4516
|
+
return { error: x402NotOfferedPaymentError(opts.pricingCurrency) };
|
|
4517
|
+
}
|
|
5362
4518
|
const rate = await resolveRateOrNull(opts.fxFetcher);
|
|
5363
4519
|
if (rate === null) return { error: rateUnavailablePaymentError() };
|
|
5364
4520
|
try {
|
|
@@ -5377,7 +4533,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
|
|
|
5377
4533
|
snapshot
|
|
5378
4534
|
});
|
|
5379
4535
|
}
|
|
5380
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4536
|
+
const { verifyX402Payment } = await import("./x402-T2C5MX3T.js");
|
|
5381
4537
|
const receipt = await verifyX402Payment(
|
|
5382
4538
|
body.content.x402_payment,
|
|
5383
4539
|
opts.x402Config,
|
|
@@ -5782,12 +4938,15 @@ async function processIncomingPayment(job, body, opts) {
|
|
|
5782
4938
|
status: 402
|
|
5783
4939
|
};
|
|
5784
4940
|
} else if (body.content.x402_payment && opts.x402Config) {
|
|
4941
|
+
if ((opts.pricingCurrency ?? "usd") !== "usd") {
|
|
4942
|
+
return x402NotOfferedPaymentError(opts.pricingCurrency);
|
|
4943
|
+
}
|
|
5785
4944
|
const rate = await resolveRateOrNull(opts.fxFetcher);
|
|
5786
4945
|
if (rate === null) return rateUnavailablePaymentError();
|
|
5787
4946
|
try {
|
|
5788
4947
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5789
4948
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5790
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4949
|
+
const { verifyX402Payment } = await import("./x402-T2C5MX3T.js");
|
|
5791
4950
|
const receipt = await verifyX402Payment(
|
|
5792
4951
|
body.content.x402_payment,
|
|
5793
4952
|
opts.x402Config,
|
|
@@ -6090,8 +5249,8 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
6090
5249
|
credited
|
|
6091
5250
|
);
|
|
6092
5251
|
}
|
|
6093
|
-
function buildX402Requirements(config,
|
|
6094
|
-
const requiredUsdcMicro = BigInt(x402RequiredUsdcMicro(priceFiat
|
|
5252
|
+
function buildX402Requirements(config, resource, priceFiat) {
|
|
5253
|
+
const requiredUsdcMicro = BigInt(x402RequiredUsdcMicro(priceFiat));
|
|
6095
5254
|
return [
|
|
6096
5255
|
buildPaymentRequirements({
|
|
6097
5256
|
x402Config: config,
|
|
@@ -6403,11 +5562,15 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
6403
5562
|
if (createsDepositLiability && enqueue && (!tx || !ctx.dvmId)) {
|
|
6404
5563
|
throw new Error("deposit outbox enqueue requires the rail's open transaction and a DVM id");
|
|
6405
5564
|
}
|
|
6406
|
-
const commitDeposit = async (result) => {
|
|
5565
|
+
const commitDeposit = async (result, credit) => {
|
|
5566
|
+
const report = {
|
|
5567
|
+
...deposit,
|
|
5568
|
+
callerPubkey: credit.callerPubkey
|
|
5569
|
+
};
|
|
6407
5570
|
if (createsDepositLiability && enqueue && tx) {
|
|
6408
|
-
await enqueue(tx, creditDepositPayload(ctx.dvmId,
|
|
5571
|
+
await enqueue(tx, creditDepositPayload(ctx.dvmId, report));
|
|
6409
5572
|
}
|
|
6410
|
-
return result;
|
|
5573
|
+
return { ...result, deposit: report };
|
|
6411
5574
|
};
|
|
6412
5575
|
if (ctx.fundOnly) {
|
|
6413
5576
|
const { creditId: creditId2 } = ctx.fundOnly;
|
|
@@ -6435,11 +5598,11 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
6435
5598
|
ledgerSeq: credit.lastLedgerSeq,
|
|
6436
5599
|
tx
|
|
6437
5600
|
});
|
|
6438
|
-
return commitDeposit({ creditId: creditId2,
|
|
5601
|
+
return commitDeposit({ creditId: creditId2 }, credit);
|
|
6439
5602
|
}
|
|
6440
5603
|
if (ctx.topUp) {
|
|
6441
5604
|
const { creditId: creditId2, drawId: drawId2 } = ctx.topUp;
|
|
6442
|
-
await ctx.ledger.fund({
|
|
5605
|
+
const credit = await ctx.ledger.fund({
|
|
6443
5606
|
creditId: creditId2,
|
|
6444
5607
|
callerPubkey: ctx.callerId,
|
|
6445
5608
|
currency: ctx.priceFiat.currency,
|
|
@@ -6449,9 +5612,9 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
6449
5612
|
tx
|
|
6450
5613
|
});
|
|
6451
5614
|
if (ctx.askSatisfied) {
|
|
6452
|
-
return commitDeposit({ creditId: creditId2,
|
|
5615
|
+
return commitDeposit({ creditId: creditId2, unappliedMicro: fundedMicro }, credit);
|
|
6453
5616
|
}
|
|
6454
|
-
if (shortMidJob) return commitDeposit({ creditId: creditId2,
|
|
5617
|
+
if (shortMidJob) return commitDeposit({ creditId: creditId2 }, credit);
|
|
6455
5618
|
const grown = await ctx.ledger.growDraw({
|
|
6456
5619
|
creditId: creditId2,
|
|
6457
5620
|
drawId: drawId2,
|
|
@@ -6472,15 +5635,17 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
6472
5635
|
tx
|
|
6473
5636
|
});
|
|
6474
5637
|
if (grown.addedAmountMicro <= 0) {
|
|
6475
|
-
return commitDeposit({ creditId: creditId2,
|
|
5638
|
+
return commitDeposit({ creditId: creditId2, unappliedMicro: grown.cappedMicro }, credit);
|
|
6476
5639
|
}
|
|
6477
|
-
return commitDeposit(
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
5640
|
+
return commitDeposit(
|
|
5641
|
+
{
|
|
5642
|
+
creditId: creditId2,
|
|
5643
|
+
draw: grown,
|
|
5644
|
+
added: { amountMicro: grown.addedAmountMicro, ...grown.addedRailValue },
|
|
5645
|
+
...grown.cappedMicro > 0 && { unappliedMicro: grown.cappedMicro }
|
|
5646
|
+
},
|
|
5647
|
+
credit
|
|
5648
|
+
);
|
|
6484
5649
|
}
|
|
6485
5650
|
const creditId = deposit.creditId;
|
|
6486
5651
|
const drawId = env?.drawId ?? funding.paymentId;
|
|
@@ -6503,7 +5668,7 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
6503
5668
|
basis,
|
|
6504
5669
|
tx
|
|
6505
5670
|
});
|
|
6506
|
-
if (shortMidJob) return commitDeposit({ creditId,
|
|
5671
|
+
if (shortMidJob) return commitDeposit({ creditId }, fundedCredit);
|
|
6507
5672
|
let draw;
|
|
6508
5673
|
try {
|
|
6509
5674
|
draw = await ctx.ledger.draw({
|
|
@@ -6523,7 +5688,10 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
6523
5688
|
ledgerSeq: fundedCredit.lastLedgerSeq,
|
|
6524
5689
|
tx
|
|
6525
5690
|
});
|
|
6526
|
-
return commitDeposit(
|
|
5691
|
+
return commitDeposit(
|
|
5692
|
+
{ creditId, drawRefusal: err, fundingCredit: fundedCredit },
|
|
5693
|
+
fundedCredit
|
|
5694
|
+
);
|
|
6527
5695
|
}
|
|
6528
5696
|
throw err;
|
|
6529
5697
|
}
|
|
@@ -6538,17 +5706,20 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
6538
5706
|
tx
|
|
6539
5707
|
});
|
|
6540
5708
|
}
|
|
6541
|
-
return commitDeposit(
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
5709
|
+
return commitDeposit(
|
|
5710
|
+
{
|
|
5711
|
+
creditId,
|
|
5712
|
+
draw,
|
|
5713
|
+
...env ? { fundingCredit: fundedCredit } : {}
|
|
5714
|
+
},
|
|
5715
|
+
fundedCredit
|
|
5716
|
+
);
|
|
6547
5717
|
}
|
|
6548
5718
|
function creditDepositPayload(dvmId, funding) {
|
|
6549
5719
|
return {
|
|
6550
5720
|
dvmId,
|
|
6551
5721
|
creditId: funding.creditId,
|
|
5722
|
+
callerPubkey: funding.callerPubkey,
|
|
6552
5723
|
fundingId: funding.fundingId,
|
|
6553
5724
|
rail: funding.rail,
|
|
6554
5725
|
paidMsats: funding.paidMsats,
|
|
@@ -7119,6 +6290,21 @@ async function resolveRateOrNull(fetcher) {
|
|
|
7119
6290
|
function rateUnavailableResult() {
|
|
7120
6291
|
return { paidMsats: 0, receivedProofs: [], error: rateUnavailablePaymentError() };
|
|
7121
6292
|
}
|
|
6293
|
+
function x402NotOfferedPaymentError(currency) {
|
|
6294
|
+
return {
|
|
6295
|
+
body: paymentErrorBody("x402_not_offered", {
|
|
6296
|
+
message: `x402 is offered only by USD-priced DVMs; this DVM prices in ${currency ?? "an unknown currency"}.`
|
|
6297
|
+
}),
|
|
6298
|
+
status: 402
|
|
6299
|
+
};
|
|
6300
|
+
}
|
|
6301
|
+
function x402NotOfferedResult(currency) {
|
|
6302
|
+
return {
|
|
6303
|
+
paidMsats: 0,
|
|
6304
|
+
receivedProofs: [],
|
|
6305
|
+
error: x402NotOfferedPaymentError(currency)
|
|
6306
|
+
};
|
|
6307
|
+
}
|
|
7122
6308
|
function logCredentialRejected(rail, phase, err) {
|
|
7123
6309
|
console.warn(
|
|
7124
6310
|
JSON.stringify({
|
|
@@ -7174,8 +6360,8 @@ function authErrorBody(err, opts = {}) {
|
|
|
7174
6360
|
hint: "Regenerate the request with the current timestamp + a fresh nonce; sign with secp256k1+BIP-340 Schnorr over the canonical JSON form of the input."
|
|
7175
6361
|
};
|
|
7176
6362
|
}
|
|
7177
|
-
function infoSchemaUrl(requestUrl) {
|
|
7178
|
-
return `${new URL(requestUrl).origin}/v1/info`;
|
|
6363
|
+
function infoSchemaUrl(requestUrl, publicOrigin) {
|
|
6364
|
+
return `${publicOrigin ?? new URL(requestUrl).origin}/v1/info`;
|
|
7179
6365
|
}
|
|
7180
6366
|
function callerInputIssues(issues) {
|
|
7181
6367
|
if (!issues) return [];
|
|
@@ -7261,6 +6447,7 @@ function drainReport(dvmId, drain) {
|
|
|
7261
6447
|
return {
|
|
7262
6448
|
dvmId,
|
|
7263
6449
|
creditId: drain.creditId,
|
|
6450
|
+
callerPubkey: drain.callerPubkey,
|
|
7264
6451
|
drainId: drain.drainId,
|
|
7265
6452
|
method: drain.method,
|
|
7266
6453
|
amountMicro: drain.amountMicro,
|
|
@@ -9105,1239 +8292,107 @@ function x402SettlementPendingRefusal(args) {
|
|
|
9105
8292
|
...args.settlementId ? { settlement_id: args.settlementId } : {},
|
|
9106
8293
|
...args.settlementStatus ? { settlement_status: args.settlementStatus } : {},
|
|
9107
8294
|
...admitted ? { settlement_drain_id: admitted } : {}
|
|
9108
|
-
}
|
|
9109
|
-
});
|
|
9110
|
-
}
|
|
9111
|
-
function drainLedgerErrorResponse(error, creditId, drainId) {
|
|
9112
|
-
if (error.code === "nothing_to_drain") {
|
|
9113
|
-
return jsonPaymentError("nothing_to_drain", 409, {
|
|
9114
|
-
message: `credit ${creditId} has no available balance to drain.`,
|
|
9115
|
-
extra: {
|
|
9116
|
-
balance_micro: error.details.balanceMicro ?? 0,
|
|
9117
|
-
remaining_micro: Math.max(0, error.details.availableMicro ?? 0)
|
|
9118
|
-
}
|
|
9119
|
-
});
|
|
9120
|
-
}
|
|
9121
|
-
if (error.code === "drain_below_dust") {
|
|
9122
|
-
return jsonPaymentError("drain_below_dust", 409, {
|
|
9123
|
-
message: error.message,
|
|
9124
|
-
extra: {
|
|
9125
|
-
balance_micro: error.details.balanceMicro ?? 0,
|
|
9126
|
-
remaining_micro: Math.max(0, error.details.availableMicro ?? 0),
|
|
9127
|
-
owed_sats: 0
|
|
9128
|
-
}
|
|
9129
|
-
});
|
|
9130
|
-
}
|
|
9131
|
-
if (error.code === "drain_conflict") {
|
|
9132
|
-
const copy = error.details.drainConflict ? DRAIN_CONFLICT_COPY[error.details.drainConflict] : void 0;
|
|
9133
|
-
return jsonPaymentError("drain_conflict", 409, {
|
|
9134
|
-
message: error.message,
|
|
9135
|
-
display: copy?.display,
|
|
9136
|
-
hint: copy?.hint({ creditId, drainId }),
|
|
9137
|
-
// An unclassified conflict keeps the duplicate-payout copy's own
|
|
9138
|
-
// `retryable`, and the id it was never told anything about.
|
|
9139
|
-
retryable: copy?.retryable,
|
|
9140
|
-
extra: { drain_registered: copy?.drainRegistered ?? false }
|
|
9141
|
-
});
|
|
9142
|
-
}
|
|
9143
|
-
if (error.code === "settlement_pending") {
|
|
9144
|
-
return x402SettlementPendingRefusal({
|
|
9145
|
-
door: "drain",
|
|
9146
|
-
message: error.message,
|
|
9147
|
-
drainId,
|
|
9148
|
-
...error.details.settlementId ? { settlementId: error.details.settlementId } : {},
|
|
9149
|
-
...error.details.settlementStatus ? { settlementStatus: error.details.settlementStatus } : {},
|
|
9150
|
-
...error.details.blockingDrainId ? { blockingDrainId: error.details.blockingDrainId } : {}
|
|
9151
|
-
});
|
|
9152
|
-
}
|
|
9153
|
-
if (error.code === "caller_mismatch" || error.code === "credit_not_found") {
|
|
9154
|
-
return jsonPaymentError("credit_not_found", 404, {
|
|
9155
|
-
message: `No credit ${creditId} for this caller.`
|
|
9156
|
-
});
|
|
9157
|
-
}
|
|
9158
|
-
return jsonPaymentError(mapLedgerCode(error), 402, { message: error.message });
|
|
9159
|
-
}
|
|
9160
|
-
function isHttpResponse(value) {
|
|
9161
|
-
return value instanceof Response || typeof value === "object" && "headers" in value && typeof value.status === "number";
|
|
9162
|
-
}
|
|
9163
|
-
function mapLedgerCode(err) {
|
|
9164
|
-
switch (err.code) {
|
|
9165
|
-
case "insufficient_credit":
|
|
9166
|
-
case "credit_expired":
|
|
9167
|
-
case "credit_unbacked":
|
|
9168
|
-
case "credit_not_found":
|
|
9169
|
-
case "tempo_channel_closing":
|
|
9170
|
-
case "settlement_pending":
|
|
9171
|
-
case "nothing_to_drain":
|
|
9172
|
-
case "drain_below_dust":
|
|
9173
|
-
case "drain_not_found":
|
|
9174
|
-
case "drain_conflict":
|
|
9175
|
-
return err.code;
|
|
9176
|
-
default:
|
|
9177
|
-
return "payment_invalid";
|
|
9178
|
-
}
|
|
9179
|
-
}
|
|
9180
|
-
function creditBody(credit, reconciliationPending = false) {
|
|
9181
|
-
return {
|
|
9182
|
-
status: credit.status,
|
|
9183
|
-
credit_id: credit.creditId,
|
|
9184
|
-
currency: credit.currency,
|
|
9185
|
-
balance_micro: credit.balanceMicro,
|
|
9186
|
-
remaining_micro: credit.availableMicro,
|
|
9187
|
-
expiry_ms: credit.expiryMs,
|
|
9188
|
-
expired: credit.expired,
|
|
9189
|
-
// Reclaimable via op "drain" (internal-review) — true whenever spendable-or-
|
|
9190
|
-
// expired value remains. Expiry ends spending, never ownership.
|
|
9191
|
-
drainable: credit.status === "active" && credit.availableMicro > 0,
|
|
9192
|
-
...reconciliationPending && { reconciliation_pending: true },
|
|
9193
|
-
ledger_seq: credit.lastLedgerSeq
|
|
9194
|
-
};
|
|
9195
|
-
}
|
|
9196
|
-
function spendable(credit, currency) {
|
|
9197
|
-
return !credit.expired && credit.status === "active" && credit.currency === currency;
|
|
9198
|
-
}
|
|
9199
|
-
function pickPrimary(credits, currency) {
|
|
9200
|
-
let best;
|
|
9201
|
-
for (const credit of credits) {
|
|
9202
|
-
if (!spendable(credit, currency) || credit.availableMicro <= 0) continue;
|
|
9203
|
-
if (!best || credit.availableMicro > best.availableMicro || credit.availableMicro === best.availableMicro && credit.createdAt > best.createdAt) {
|
|
9204
|
-
best = credit;
|
|
9205
|
-
}
|
|
9206
|
-
}
|
|
9207
|
-
return best;
|
|
9208
|
-
}
|
|
9209
|
-
|
|
9210
|
-
// src/lib/mints.ts
|
|
9211
|
-
var RECOMMENDED_MINTS = [
|
|
9212
|
-
{
|
|
9213
|
-
name: "Testnut",
|
|
9214
|
-
description: "Cashu test mint \u2014 use for testing only, not real funds",
|
|
9215
|
-
url: "https://testnut.cashu.space"
|
|
9216
|
-
},
|
|
9217
|
-
{
|
|
9218
|
-
name: "Voltz Mint",
|
|
9219
|
-
description: "Production Cashu mint by the Voltz Wallet team",
|
|
9220
|
-
url: "https://mint.lnvoltz.com"
|
|
9221
|
-
}
|
|
9222
|
-
];
|
|
9223
|
-
function isTestMintUrl(url) {
|
|
9224
|
-
let host;
|
|
9225
|
-
try {
|
|
9226
|
-
host = new URL(url).hostname.toLowerCase();
|
|
9227
|
-
} catch {
|
|
9228
|
-
return false;
|
|
9229
|
-
}
|
|
9230
|
-
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
9231
|
-
if (LOOPBACK_HOSTS.has(host)) return true;
|
|
9232
|
-
if (host.endsWith(".localhost")) return true;
|
|
9233
|
-
return TEST_MINT_HOSTS.has(host);
|
|
9234
|
-
}
|
|
9235
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
|
|
9236
|
-
var TEST_MINT_HOSTS = /* @__PURE__ */ new Set(["testnut.cashu.space"]);
|
|
9237
|
-
|
|
9238
|
-
// src/lib/receipt-key.ts
|
|
9239
|
-
import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
|
|
9240
|
-
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
9241
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
9242
|
-
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
9243
|
-
var RECEIPT_KDF_SALT = "dvmkit/receipt/v1";
|
|
9244
|
-
function deriveReceiptSecret(builderSecretHex, dvmId) {
|
|
9245
|
-
const ikm = hexToBytes(builderSecretHex);
|
|
9246
|
-
const salt = new TextEncoder().encode(RECEIPT_KDF_SALT);
|
|
9247
|
-
for (let counter = 0; counter < 256; counter++) {
|
|
9248
|
-
const info = new TextEncoder().encode(
|
|
9249
|
-
counter === 0 ? `${RECEIPT_KDF_SALT}/${dvmId}` : `${RECEIPT_KDF_SALT}/${dvmId}#${String(counter)}`
|
|
9250
|
-
);
|
|
9251
|
-
const candidate = hkdf(sha256, ikm, salt, info, 32);
|
|
9252
|
-
if (secp256k1.utils.isValidSecretKey(candidate)) return bytesToHex(candidate);
|
|
9253
|
-
}
|
|
9254
|
-
throw new Error(`deriveReceiptSecret: no valid secp256k1 scalar for DVM ID "${dvmId}"`);
|
|
9255
|
-
}
|
|
9256
|
-
function receiptPubkeyFromSecret(secretHex) {
|
|
9257
|
-
return bytesToHex(schnorr.getPublicKey(hexToBytes(secretHex)));
|
|
9258
|
-
}
|
|
9259
|
-
function deriveReceiptKeypair(builderSecretHex, dvmId) {
|
|
9260
|
-
const secret = deriveReceiptSecret(builderSecretHex, dvmId);
|
|
9261
|
-
return { secret, pubkey: receiptPubkeyFromSecret(secret) };
|
|
9262
|
-
}
|
|
9263
|
-
function generateEphemeralReceiptKey() {
|
|
9264
|
-
const { secretKey, publicKey } = schnorr.keygen();
|
|
9265
|
-
return { secret: bytesToHex(secretKey), pubkey: bytesToHex(publicKey) };
|
|
9266
|
-
}
|
|
9267
|
-
|
|
9268
|
-
// src/lib/x402/env.ts
|
|
9269
|
-
import { isAddress } from "viem";
|
|
9270
|
-
import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
9271
|
-
var X402_SELF_RELAY_KEY = "DVMKIT_X402_SELF_RELAY_KEY";
|
|
9272
|
-
var X402_RECEIVER_AUTHORIZER_KEY = "DVMKIT_X402_RECEIVER_AUTHORIZER_KEY";
|
|
9273
|
-
var X402_BATCH_SETTLEMENT_KEY = "DVMKIT_X402_BATCH_SETTLEMENT";
|
|
9274
|
-
var X402_RPC_URL_KEY = "DVMKIT_X402_RPC_URL";
|
|
9275
|
-
var PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
9276
|
-
var X402EnvValidationError = class extends Error {
|
|
9277
|
-
constructor(code, variable, message, expectedAddress) {
|
|
9278
|
-
super(message);
|
|
9279
|
-
this.code = code;
|
|
9280
|
-
this.variable = variable;
|
|
9281
|
-
this.expectedAddress = expectedAddress;
|
|
9282
|
-
this.name = "X402EnvValidationError";
|
|
9283
|
-
}
|
|
9284
|
-
code;
|
|
9285
|
-
variable;
|
|
9286
|
-
expectedAddress;
|
|
9287
|
-
};
|
|
9288
|
-
function validateX402Env(env, opts = {}) {
|
|
9289
|
-
const relayKey = env[X402_SELF_RELAY_KEY];
|
|
9290
|
-
const authorizerKey = env[X402_RECEIVER_AUTHORIZER_KEY];
|
|
9291
|
-
const hasRelayKey = relayKey !== void 0;
|
|
9292
|
-
const hasAuthorizerKey = authorizerKey !== void 0;
|
|
9293
|
-
const relay = hasRelayKey ? derive(X402_SELF_RELAY_KEY, relayKey) : void 0;
|
|
9294
|
-
const authorizer = hasAuthorizerKey ? derive(X402_RECEIVER_AUTHORIZER_KEY, authorizerKey) : void 0;
|
|
9295
|
-
const completeWrite = opts.requireCompleteBundle === true && (hasRelayKey || hasAuthorizerKey);
|
|
9296
|
-
if (hasRelayKey || completeWrite) {
|
|
9297
|
-
if (env[X402_BATCH_SETTLEMENT_KEY] !== "true") {
|
|
9298
|
-
fail(
|
|
9299
|
-
"x402_bundle_incomplete",
|
|
9300
|
-
hasRelayKey ? X402_SELF_RELAY_KEY : X402_RECEIVER_AUTHORIZER_KEY,
|
|
9301
|
-
`Changing either x402 private key requires ${X402_BATCH_SETTLEMENT_KEY}=true in the same write.`
|
|
9302
|
-
);
|
|
9303
|
-
}
|
|
9304
|
-
if (!env[X402_RPC_URL_KEY]?.trim()) {
|
|
9305
|
-
fail(
|
|
9306
|
-
"x402_bundle_incomplete",
|
|
9307
|
-
hasRelayKey ? X402_SELF_RELAY_KEY : X402_RECEIVER_AUTHORIZER_KEY,
|
|
9308
|
-
`Changing either x402 private key requires an explicit ${X402_RPC_URL_KEY} in the same write.`
|
|
9309
|
-
);
|
|
9310
|
-
}
|
|
9311
|
-
if (!hasAuthorizerKey || completeWrite && !hasRelayKey) {
|
|
9312
|
-
if (!completeWrite && !hasAuthorizerKey) {
|
|
9313
|
-
fail(
|
|
9314
|
-
"x402_bundle_incomplete",
|
|
9315
|
-
X402_RECEIVER_AUTHORIZER_KEY,
|
|
9316
|
-
`${X402_SELF_RELAY_KEY} requires ${X402_RECEIVER_AUTHORIZER_KEY}.`
|
|
9317
|
-
);
|
|
9318
|
-
}
|
|
9319
|
-
fail(
|
|
9320
|
-
"x402_bundle_incomplete",
|
|
9321
|
-
hasRelayKey ? X402_RECEIVER_AUTHORIZER_KEY : X402_SELF_RELAY_KEY,
|
|
9322
|
-
`Changing either x402 private key requires both ${X402_SELF_RELAY_KEY} and ${X402_RECEIVER_AUTHORIZER_KEY} in the same write.`
|
|
9323
|
-
);
|
|
9324
|
-
}
|
|
9325
|
-
}
|
|
9326
|
-
if (hasRelayKey && relayKey.toLowerCase() === authorizerKey?.toLowerCase()) {
|
|
9327
|
-
fail(
|
|
9328
|
-
"x402_key_reuse",
|
|
9329
|
-
X402_SELF_RELAY_KEY,
|
|
9330
|
-
`${X402_SELF_RELAY_KEY} and ${X402_RECEIVER_AUTHORIZER_KEY} must use different private keys.`
|
|
9331
|
-
);
|
|
9332
|
-
}
|
|
9333
|
-
assertExpectation(X402_SELF_RELAY_KEY, relay, opts.expectedRelay);
|
|
9334
|
-
assertExpectation(X402_RECEIVER_AUTHORIZER_KEY, authorizer, opts.expectedAuthorizer);
|
|
9335
|
-
return { ...relay ? { relay } : {}, ...authorizer ? { authorizer } : {} };
|
|
9336
|
-
}
|
|
9337
|
-
function derive(variable, key) {
|
|
9338
|
-
if (!PRIVATE_KEY_RE.test(key)) {
|
|
9339
|
-
fail(
|
|
9340
|
-
"invalid_x402_private_key",
|
|
9341
|
-
variable,
|
|
9342
|
-
`${variable} must be a 0x-prefixed 32-byte hex secp256k1 private key.`
|
|
9343
|
-
);
|
|
9344
|
-
}
|
|
9345
|
-
try {
|
|
9346
|
-
return privateKeyToAccount3(key).address;
|
|
9347
|
-
} catch {
|
|
9348
|
-
fail(
|
|
9349
|
-
"invalid_x402_private_key",
|
|
9350
|
-
variable,
|
|
9351
|
-
`${variable} must be a valid secp256k1 private key.`
|
|
9352
|
-
);
|
|
9353
|
-
}
|
|
9354
|
-
}
|
|
9355
|
-
function assertExpectation(variable, actual, expected) {
|
|
9356
|
-
if (expected === void 0) return;
|
|
9357
|
-
if (!isAddress(expected, { strict: false })) {
|
|
9358
|
-
fail(
|
|
9359
|
-
"invalid_x402_expected_address",
|
|
9360
|
-
variable,
|
|
9361
|
-
`Expected address for ${variable} must be a 0x-prefixed 20-byte EVM address.`
|
|
9362
|
-
);
|
|
9363
|
-
}
|
|
9364
|
-
if (actual?.toLowerCase() !== expected.toLowerCase()) {
|
|
9365
|
-
fail(
|
|
9366
|
-
"x402_address_mismatch",
|
|
9367
|
-
variable,
|
|
9368
|
-
`${variable} derives to a different public address than expected ${expected}.`,
|
|
9369
|
-
expected
|
|
9370
|
-
);
|
|
9371
|
-
}
|
|
9372
|
-
}
|
|
9373
|
-
function fail(code, variable, message, expectedAddress) {
|
|
9374
|
-
throw new X402EnvValidationError(code, variable, message, expectedAddress);
|
|
9375
|
-
}
|
|
9376
|
-
|
|
9377
|
-
// src/observability/span-writer.ts
|
|
9378
|
-
var noop = () => {
|
|
9379
|
-
};
|
|
9380
|
-
var SPANS_INSERTED_CHANNEL = "spans_inserted";
|
|
9381
|
-
var MAX_BUFFERED_SPANS = 1e4;
|
|
9382
|
-
var MAX_ROW_JSONB_BYTES = 64 * 1024;
|
|
9383
|
-
var MAX_STRING_VALUE_BYTES = 4 * 1024;
|
|
9384
|
-
var FLUSH_THRESHOLD = 1e3;
|
|
9385
|
-
var MAX_BACKOFF_EXPONENT = 6;
|
|
9386
|
-
var SpanWriter = class {
|
|
9387
|
-
constructor(transport, flushIntervalMs = 1e4) {
|
|
9388
|
-
this.transport = transport;
|
|
9389
|
-
this.flushIntervalMs = flushIntervalMs;
|
|
9390
|
-
}
|
|
9391
|
-
transport;
|
|
9392
|
-
flushIntervalMs;
|
|
9393
|
-
buffer = [];
|
|
9394
|
-
flushTimer = null;
|
|
9395
|
-
flushing = false;
|
|
9396
|
-
dropCount = 0;
|
|
9397
|
-
truncatedSpanCount = 0;
|
|
9398
|
-
consecutiveFailures = 0;
|
|
9399
|
-
backoffTimer = null;
|
|
9400
|
-
/** Enqueue a span row. Non-blocking — truncates oversize JSONB and appends to buffer. */
|
|
9401
|
-
enqueue(row) {
|
|
9402
|
-
const originalBytes = JSON.stringify({
|
|
9403
|
-
attributes: row.attributes,
|
|
9404
|
-
dimensions: row.dimensions
|
|
9405
|
-
}).length;
|
|
9406
|
-
if (originalBytes > MAX_ROW_JSONB_BYTES) {
|
|
9407
|
-
row = this.truncateForSize(row, originalBytes);
|
|
9408
|
-
}
|
|
9409
|
-
if (this.buffer.length >= MAX_BUFFERED_SPANS) {
|
|
9410
|
-
this.dropCount++;
|
|
9411
|
-
if (this.dropCount === 1 || this.dropCount % 1e3 === 0) {
|
|
9412
|
-
console.error(
|
|
9413
|
-
JSON.stringify({
|
|
9414
|
-
event: "span_buffer_drop",
|
|
9415
|
-
reason: "buffer_full",
|
|
9416
|
-
total_dropped: this.dropCount,
|
|
9417
|
-
buffer_size: this.buffer.length
|
|
9418
|
-
})
|
|
9419
|
-
);
|
|
9420
|
-
}
|
|
9421
|
-
return;
|
|
9422
|
-
}
|
|
9423
|
-
this.buffer.push(row);
|
|
9424
|
-
if (this.buffer.length >= FLUSH_THRESHOLD && !this.flushing) {
|
|
9425
|
-
void this.flush().catch(noop);
|
|
9426
|
-
}
|
|
9427
|
-
}
|
|
9428
|
-
/** Start periodic flush to the transport. */
|
|
9429
|
-
startFlush() {
|
|
9430
|
-
this.flushTimer = setInterval(() => {
|
|
9431
|
-
void this.flush().catch(noop);
|
|
9432
|
-
}, this.flushIntervalMs);
|
|
9433
|
-
}
|
|
9434
|
-
/** Stop periodic flush. */
|
|
9435
|
-
stopFlush() {
|
|
9436
|
-
if (this.flushTimer) {
|
|
9437
|
-
clearInterval(this.flushTimer);
|
|
9438
|
-
this.flushTimer = null;
|
|
9439
|
-
}
|
|
9440
|
-
if (this.backoffTimer) {
|
|
9441
|
-
clearTimeout(this.backoffTimer);
|
|
9442
|
-
this.backoffTimer = null;
|
|
9443
|
-
}
|
|
9444
|
-
}
|
|
9445
|
-
/** Flush buffered spans. Single-flight: concurrent calls exit early. */
|
|
9446
|
-
async flush() {
|
|
9447
|
-
if (this.flushing) return;
|
|
9448
|
-
if (this.buffer.length === 0) return;
|
|
9449
|
-
this.flushing = true;
|
|
9450
|
-
const batch = this.buffer;
|
|
9451
|
-
this.buffer = [];
|
|
9452
|
-
try {
|
|
9453
|
-
await this.transport.send(batch);
|
|
9454
|
-
this.consecutiveFailures = 0;
|
|
9455
|
-
} catch (err) {
|
|
9456
|
-
this.consecutiveFailures++;
|
|
9457
|
-
console.error(
|
|
9458
|
-
JSON.stringify({
|
|
9459
|
-
event: "span_flush_failed",
|
|
9460
|
-
error: err instanceof Error ? err.message : String(err),
|
|
9461
|
-
batch_size: batch.length,
|
|
9462
|
-
consecutive_failures: this.consecutiveFailures
|
|
9463
|
-
})
|
|
9464
|
-
);
|
|
9465
|
-
const room = MAX_BUFFERED_SPANS - this.buffer.length;
|
|
9466
|
-
if (room > 0) {
|
|
9467
|
-
this.buffer.unshift(...batch.slice(0, room));
|
|
9468
|
-
}
|
|
9469
|
-
if (this.consecutiveFailures > 1) {
|
|
9470
|
-
const exponent = Math.min(this.consecutiveFailures - 1, MAX_BACKOFF_EXPONENT);
|
|
9471
|
-
const backoffMs = this.flushIntervalMs * Math.pow(2, exponent);
|
|
9472
|
-
this.stopFlush();
|
|
9473
|
-
this.backoffTimer = setTimeout(() => {
|
|
9474
|
-
this.backoffTimer = null;
|
|
9475
|
-
this.startFlush();
|
|
9476
|
-
}, backoffMs);
|
|
9477
|
-
}
|
|
9478
|
-
} finally {
|
|
9479
|
-
this.flushing = false;
|
|
9480
|
-
}
|
|
9481
|
-
}
|
|
9482
|
-
/** Flush remaining spans on shutdown. */
|
|
9483
|
-
async shutdown() {
|
|
9484
|
-
this.stopFlush();
|
|
9485
|
-
await this.flush();
|
|
9486
|
-
}
|
|
9487
|
-
/** Number of spans dropped due to buffer overflow. */
|
|
9488
|
-
getDropCount() {
|
|
9489
|
-
return this.dropCount;
|
|
9490
|
-
}
|
|
9491
|
-
/** Number of spans truncated due to oversized JSONB payload. */
|
|
9492
|
-
getTruncatedSpanCount() {
|
|
9493
|
-
return this.truncatedSpanCount;
|
|
9494
|
-
}
|
|
9495
|
-
/** Current buffer size. */
|
|
9496
|
-
getBufferSize() {
|
|
9497
|
-
return this.buffer.length;
|
|
9498
|
-
}
|
|
9499
|
-
/** Consecutive flush failures; resets to 0 on the next successful flush. */
|
|
9500
|
-
getConsecutiveFailures() {
|
|
9501
|
-
return this.consecutiveFailures;
|
|
9502
|
-
}
|
|
9503
|
-
/**
|
|
9504
|
-
* Two-pass truncation: cap individual string values, then fall back to
|
|
9505
|
-
* wholesale attribute replacement if the blob is still over the limit.
|
|
9506
|
-
*/
|
|
9507
|
-
truncateForSize(row, originalBytes) {
|
|
9508
|
-
const truncatedAttrs = {};
|
|
9509
|
-
for (const [key, value] of Object.entries(row.attributes)) {
|
|
9510
|
-
if (typeof value === "string" && value.length > MAX_STRING_VALUE_BYTES) {
|
|
9511
|
-
const omitted = value.length - MAX_STRING_VALUE_BYTES;
|
|
9512
|
-
truncatedAttrs[key] = `${value.slice(0, MAX_STRING_VALUE_BYTES)}\u2026 [truncated, ${omitted} chars omitted]`;
|
|
9513
|
-
} else {
|
|
9514
|
-
truncatedAttrs[key] = value;
|
|
9515
|
-
}
|
|
9516
|
-
}
|
|
9517
|
-
const afterStringPass = JSON.stringify({
|
|
9518
|
-
attributes: truncatedAttrs,
|
|
9519
|
-
dimensions: row.dimensions
|
|
9520
|
-
}).length;
|
|
9521
|
-
let mode;
|
|
9522
|
-
if (afterStringPass <= MAX_ROW_JSONB_BYTES) {
|
|
9523
|
-
row = { ...row, attributes: truncatedAttrs };
|
|
9524
|
-
mode = "string_pass";
|
|
9525
|
-
} else {
|
|
9526
|
-
const dimSize = JSON.stringify(row.dimensions).length;
|
|
9527
|
-
if (dimSize > MAX_ROW_JSONB_BYTES) {
|
|
9528
|
-
console.error(
|
|
9529
|
-
JSON.stringify({
|
|
9530
|
-
event: "span_dimension_oversized",
|
|
9531
|
-
name: row.name,
|
|
9532
|
-
dataset: row.dataset,
|
|
9533
|
-
dimension_bytes: dimSize
|
|
9534
|
-
})
|
|
9535
|
-
);
|
|
9536
|
-
}
|
|
9537
|
-
row = {
|
|
9538
|
-
...row,
|
|
9539
|
-
attributes: {
|
|
9540
|
-
_truncated: true,
|
|
9541
|
-
original_bytes: originalBytes,
|
|
9542
|
-
original_keys: Object.keys(row.attributes).sort()
|
|
9543
|
-
}
|
|
9544
|
-
};
|
|
9545
|
-
mode = "wholesale_fallback";
|
|
9546
|
-
}
|
|
9547
|
-
this.truncatedSpanCount++;
|
|
9548
|
-
if (this.truncatedSpanCount === 1 || this.truncatedSpanCount % 100 === 0) {
|
|
9549
|
-
console.error(
|
|
9550
|
-
JSON.stringify({
|
|
9551
|
-
event: "span_truncated",
|
|
9552
|
-
name: row.name,
|
|
9553
|
-
dataset: row.dataset,
|
|
9554
|
-
original_bytes: originalBytes,
|
|
9555
|
-
after_string_pass_bytes: afterStringPass,
|
|
9556
|
-
mode,
|
|
9557
|
-
total_truncated: this.truncatedSpanCount
|
|
9558
|
-
})
|
|
9559
|
-
);
|
|
9560
|
-
}
|
|
9561
|
-
return row;
|
|
9562
|
-
}
|
|
9563
|
-
};
|
|
9564
|
-
var SPAN_COLUMNS = [
|
|
9565
|
-
"span_id",
|
|
9566
|
-
"trace_id",
|
|
9567
|
-
"parent_span_id",
|
|
9568
|
-
"dataset",
|
|
9569
|
-
"name",
|
|
9570
|
-
"level",
|
|
9571
|
-
"status",
|
|
9572
|
-
"started_at_ms",
|
|
9573
|
-
"ended_at_ms",
|
|
9574
|
-
"service",
|
|
9575
|
-
"service_version",
|
|
9576
|
-
"host",
|
|
9577
|
-
"pid",
|
|
9578
|
-
"dimensions",
|
|
9579
|
-
"attributes",
|
|
9580
|
-
"error_message",
|
|
9581
|
-
"error_stack",
|
|
9582
|
-
"sample_rate"
|
|
9583
|
-
];
|
|
9584
|
-
async function insertSpanBatch(pool, rows) {
|
|
9585
|
-
if (rows.length === 0) return;
|
|
9586
|
-
const values = [];
|
|
9587
|
-
const placeholders = [];
|
|
9588
|
-
let idx = 1;
|
|
9589
|
-
for (const row of rows) {
|
|
9590
|
-
const rowPlaceholders = [];
|
|
9591
|
-
for (const col of SPAN_COLUMNS) {
|
|
9592
|
-
let val = row[col] ?? null;
|
|
9593
|
-
if (col === "sample_rate" && val == null) {
|
|
9594
|
-
val = 1;
|
|
9595
|
-
}
|
|
9596
|
-
if (col === "dimensions" || col === "attributes") {
|
|
9597
|
-
rowPlaceholders.push(`$${idx}::jsonb`);
|
|
9598
|
-
values.push(typeof val === "string" ? val : JSON.stringify(val ?? {}));
|
|
9599
|
-
} else {
|
|
9600
|
-
rowPlaceholders.push(`$${idx}`);
|
|
9601
|
-
values.push(val);
|
|
9602
|
-
}
|
|
9603
|
-
idx++;
|
|
9604
|
-
}
|
|
9605
|
-
placeholders.push(`(${rowPlaceholders.join(", ")})`);
|
|
9606
|
-
}
|
|
9607
|
-
await pool.query(
|
|
9608
|
-
`INSERT INTO observability.spans (${SPAN_COLUMNS.join(", ")})
|
|
9609
|
-
VALUES ${placeholders.join(",\n")}
|
|
9610
|
-
ON CONFLICT (dataset, span_id) DO NOTHING`,
|
|
9611
|
-
values
|
|
9612
|
-
);
|
|
9613
|
-
try {
|
|
9614
|
-
await pool.query(`SELECT pg_notify('${SPANS_INSERTED_CHANNEL}', '')`);
|
|
9615
|
-
} catch {
|
|
9616
|
-
}
|
|
9617
|
-
}
|
|
9618
|
-
var DbTransport = class {
|
|
9619
|
-
constructor(pool) {
|
|
9620
|
-
this.pool = pool;
|
|
9621
|
-
}
|
|
9622
|
-
pool;
|
|
9623
|
-
async send(rows) {
|
|
9624
|
-
await insertSpanBatch(this.pool, rows);
|
|
9625
|
-
}
|
|
9626
|
-
};
|
|
9627
|
-
var HttpTransport = class {
|
|
9628
|
-
constructor(platformUrl, serviceToken) {
|
|
9629
|
-
this.platformUrl = platformUrl;
|
|
9630
|
-
this.serviceToken = serviceToken;
|
|
9631
|
-
}
|
|
9632
|
-
platformUrl;
|
|
9633
|
-
serviceToken;
|
|
9634
|
-
async send(rows) {
|
|
9635
|
-
if (rows.length === 0) return;
|
|
9636
|
-
const resp = await fetch(`${this.platformUrl.replace(/\/$/, "")}/_internal/spans`, {
|
|
9637
|
-
method: "POST",
|
|
9638
|
-
headers: {
|
|
9639
|
-
"Content-Type": "application/json",
|
|
9640
|
-
Authorization: `Bearer ${this.serviceToken}`
|
|
9641
|
-
},
|
|
9642
|
-
body: JSON.stringify({ spans: rows })
|
|
9643
|
-
});
|
|
9644
|
-
if (!resp.ok) {
|
|
9645
|
-
const text = await resp.text().catch(() => "");
|
|
9646
|
-
throw new Error(`Span ingestion failed: ${resp.status} ${text}`);
|
|
9647
|
-
}
|
|
9648
|
-
}
|
|
9649
|
-
};
|
|
9650
|
-
|
|
9651
|
-
// src/lib/builder-identity.ts
|
|
9652
|
-
import {
|
|
9653
|
-
chmodSync as chmodSync3,
|
|
9654
|
-
existsSync as existsSync5,
|
|
9655
|
-
mkdirSync as mkdirSync2,
|
|
9656
|
-
readFileSync as readFileSync5,
|
|
9657
|
-
renameSync as renameSync4,
|
|
9658
|
-
statSync,
|
|
9659
|
-
unlinkSync as unlinkSync4,
|
|
9660
|
-
writeFileSync as writeFileSync5
|
|
9661
|
-
} from "fs";
|
|
9662
|
-
import { schnorr as schnorr2 } from "@noble/curves/secp256k1.js";
|
|
9663
|
-
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
9664
|
-
|
|
9665
|
-
// src/lib/builder-store.ts
|
|
9666
|
-
import {
|
|
9667
|
-
chmodSync as chmodSync2,
|
|
9668
|
-
existsSync as existsSync4,
|
|
9669
|
-
mkdirSync,
|
|
9670
|
-
readFileSync as readFileSync4,
|
|
9671
|
-
renameSync as renameSync3,
|
|
9672
|
-
unlinkSync as unlinkSync3,
|
|
9673
|
-
writeFileSync as writeFileSync4
|
|
9674
|
-
} from "fs";
|
|
9675
|
-
var CURRENT_VERSION = 2;
|
|
9676
|
-
function loadBuilderStore() {
|
|
9677
|
-
if (!existsSync4(BUILDER_FILE)) {
|
|
9678
|
-
return { version: CURRENT_VERSION, dvms: {} };
|
|
9679
|
-
}
|
|
9680
|
-
const raw = readFileSync4(BUILDER_FILE, "utf-8");
|
|
9681
|
-
let parsed;
|
|
9682
|
-
try {
|
|
9683
|
-
parsed = JSON.parse(raw);
|
|
9684
|
-
} catch (err) {
|
|
9685
|
-
throw new Error(
|
|
9686
|
-
`${BUILDER_FILE} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
9687
|
-
{ cause: err }
|
|
9688
|
-
);
|
|
9689
|
-
}
|
|
9690
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
9691
|
-
throw new Error(`${BUILDER_FILE} is not a JSON object`);
|
|
9692
|
-
}
|
|
9693
|
-
const values = parsed;
|
|
9694
|
-
const version = values.version;
|
|
9695
|
-
if (version === 1) {
|
|
9696
|
-
return migrateV1Store(parsed);
|
|
9697
|
-
}
|
|
9698
|
-
if (version !== CURRENT_VERSION) {
|
|
9699
|
-
throw new Error(
|
|
9700
|
-
`${BUILDER_FILE} has unknown version ${String(version)} (expected ${CURRENT_VERSION})`
|
|
9701
|
-
);
|
|
9702
|
-
}
|
|
9703
|
-
if (typeof values.dvms !== "object" || values.dvms === null) {
|
|
9704
|
-
throw new Error(`${BUILDER_FILE} is missing the dvms object`);
|
|
9705
|
-
}
|
|
9706
|
-
return {
|
|
9707
|
-
version: CURRENT_VERSION,
|
|
9708
|
-
...typeof values.identity_pubkey === "string" ? { identity_pubkey: values.identity_pubkey } : {},
|
|
9709
|
-
dvms: values.dvms
|
|
9710
|
-
};
|
|
9711
|
-
}
|
|
9712
|
-
function loadIdentityPubkey() {
|
|
9713
|
-
return loadBuilderStore().identity_pubkey ?? null;
|
|
9714
|
-
}
|
|
9715
|
-
function setIdentityPubkey(pubkey) {
|
|
9716
|
-
mkdirSync(BUILDER_CONFIG_DIR, { recursive: true, mode: 448 });
|
|
9717
|
-
const store = loadBuilderStore();
|
|
9718
|
-
store.identity_pubkey = pubkey;
|
|
9719
|
-
writeBuilderStore(store);
|
|
9720
|
-
}
|
|
9721
|
-
function writeBuilderStore(store) {
|
|
9722
|
-
const content = JSON.stringify(store, null, 2) + "\n";
|
|
9723
|
-
backupSecretFile(BUILDER_FILE, content);
|
|
9724
|
-
const tmp = `${BUILDER_FILE}.tmp`;
|
|
9725
|
-
try {
|
|
9726
|
-
writeFileSync4(tmp, content, { mode: 384 });
|
|
9727
|
-
renameSync3(tmp, BUILDER_FILE);
|
|
9728
|
-
} catch (err) {
|
|
9729
|
-
try {
|
|
9730
|
-
unlinkSync3(tmp);
|
|
9731
|
-
} catch {
|
|
9732
|
-
}
|
|
9733
|
-
throw err;
|
|
9734
|
-
}
|
|
9735
|
-
try {
|
|
9736
|
-
chmodSync2(BUILDER_FILE, 384);
|
|
9737
|
-
} catch {
|
|
9738
|
-
}
|
|
9739
|
-
}
|
|
9740
|
-
function migrateV1Store(parsed) {
|
|
9741
|
-
if (typeof parsed.dvms !== "object" || parsed.dvms === null) {
|
|
9742
|
-
throw new Error(`${BUILDER_FILE} is missing the dvms object`);
|
|
9743
|
-
}
|
|
9744
|
-
const dvms = {};
|
|
9745
|
-
for (const [handle, rawEntry] of Object.entries(parsed.dvms)) {
|
|
9746
|
-
if (typeof rawEntry !== "object" || rawEntry === null) {
|
|
9747
|
-
throw new Error(`${BUILDER_FILE} has an invalid DVM entry for ${handle}`);
|
|
9748
|
-
}
|
|
9749
|
-
const entry = rawEntry;
|
|
9750
|
-
const dvmId = entry.canonical_dvm_id;
|
|
9751
|
-
if (typeof dvmId !== "string") {
|
|
9752
|
-
throw new Error(`${BUILDER_FILE} has no canonical_dvm_id for ${handle}`);
|
|
9753
|
-
}
|
|
9754
|
-
if (Object.prototype.hasOwnProperty.call(dvms, dvmId)) {
|
|
9755
|
-
throw new Error(`${BUILDER_FILE} has multiple entries for canonical_dvm_id ${dvmId}`);
|
|
9756
|
-
}
|
|
9757
|
-
dvms[dvmId] = { ...entry, handle };
|
|
9758
|
-
}
|
|
9759
|
-
return {
|
|
9760
|
-
version: CURRENT_VERSION,
|
|
9761
|
-
...typeof parsed.identity_pubkey === "string" ? { identity_pubkey: parsed.identity_pubkey } : {},
|
|
9762
|
-
dvms
|
|
9763
|
-
};
|
|
9764
|
-
}
|
|
9765
|
-
|
|
9766
|
-
// src/lib/builder-identity.ts
|
|
9767
|
-
function generateIdentity() {
|
|
9768
|
-
const { secretKey, publicKey } = schnorr2.keygen();
|
|
9769
|
-
return { pubkey: bytesToHex2(publicKey), secret: bytesToHex2(secretKey) };
|
|
9770
|
-
}
|
|
9771
|
-
function loadIdentitySecret() {
|
|
9772
|
-
if (!existsSync5(BUILDER_KEY_FILE)) return null;
|
|
9773
|
-
warnAndTightenLooseKeyMode();
|
|
9774
|
-
const raw = readFileSync5(BUILDER_KEY_FILE, "utf-8").trim();
|
|
9775
|
-
if (!/^[0-9a-f]{64}$/.test(raw)) {
|
|
9776
|
-
throw new DvmError(
|
|
9777
|
-
"identity_secret_malformed",
|
|
9778
|
-
`${BUILDER_KEY_FILE} is not a 32-byte hex secret.`,
|
|
9779
|
-
"Delete the file and re-run `dvmctl identity create` (this rotates the key \u2014 existing DVM attestations need re-deploy)."
|
|
9780
|
-
);
|
|
9781
|
-
}
|
|
9782
|
-
return raw;
|
|
9783
|
-
}
|
|
9784
|
-
function writeIdentity(keypair, opts = {}) {
|
|
9785
|
-
const existing = loadIdentityPubkey();
|
|
9786
|
-
if (existing && !opts.force) {
|
|
9787
|
-
throw new DvmError(
|
|
9788
|
-
"identity_exists",
|
|
9789
|
-
"A builder identity already exists.",
|
|
9790
|
-
"Pass --force to overwrite. Any DVMs deployed under the old key will need to be re-deployed to refresh their /v1/info attestation."
|
|
9791
|
-
);
|
|
9792
|
-
}
|
|
9793
|
-
mkdirSync2(BUILDER_CONFIG_DIR, { recursive: true, mode: 448 });
|
|
9794
|
-
const tmp = `${BUILDER_KEY_FILE}.tmp`;
|
|
9795
|
-
writeFileSync5(tmp, keypair.secret + "\n", { mode: 384 });
|
|
9796
|
-
try {
|
|
9797
|
-
renameSync4(tmp, BUILDER_KEY_FILE);
|
|
9798
|
-
} catch (err) {
|
|
9799
|
-
try {
|
|
9800
|
-
unlinkSync4(tmp);
|
|
9801
|
-
} catch {
|
|
9802
|
-
}
|
|
9803
|
-
throw err;
|
|
9804
|
-
}
|
|
9805
|
-
try {
|
|
9806
|
-
chmodSync3(BUILDER_KEY_FILE, 384);
|
|
9807
|
-
} catch {
|
|
9808
|
-
}
|
|
9809
|
-
setIdentityPubkey(keypair.pubkey);
|
|
9810
|
-
}
|
|
9811
|
-
function hashCapabilities(capabilities) {
|
|
9812
|
-
return bytesToHex2(sha2562(canonicaliseForSigning(capabilities)));
|
|
9813
|
-
}
|
|
9814
|
-
function buildAttestation(args) {
|
|
9815
|
-
return {
|
|
9816
|
-
dvm_id: args.dvmId,
|
|
9817
|
-
slug: args.slug,
|
|
9818
|
-
capabilities_hash: hashCapabilities(args.capabilities),
|
|
9819
|
-
builder_pubkey: args.builderPubkey,
|
|
9820
|
-
...args.receiptPubkey ? { receipt_pubkey: args.receiptPubkey } : {},
|
|
9821
|
-
deployed_at: args.deployedAt ?? Math.floor(Date.now() / 1e3)
|
|
9822
|
-
};
|
|
9823
|
-
}
|
|
9824
|
-
function signAttestation(payload, secretHex) {
|
|
9825
|
-
const secretBytes = hexToBytes2(secretHex);
|
|
9826
|
-
const canonicalBytes = canonicaliseForSigning(payload);
|
|
9827
|
-
return bytesToHex2(schnorr2.sign(canonicalBytes, secretBytes));
|
|
9828
|
-
}
|
|
9829
|
-
function verifyAttestation(payload, signatureHex, pubkeyHex) {
|
|
9830
|
-
try {
|
|
9831
|
-
const canonicalBytes = canonicaliseForSigning(payload);
|
|
9832
|
-
return schnorr2.verify(hexToBytes2(signatureHex), canonicalBytes, hexToBytes2(pubkeyHex));
|
|
9833
|
-
} catch {
|
|
9834
|
-
return false;
|
|
9835
|
-
}
|
|
9836
|
-
}
|
|
9837
|
-
function warnAndTightenLooseKeyMode() {
|
|
9838
|
-
if (process.platform === "win32") return;
|
|
9839
|
-
try {
|
|
9840
|
-
const mode = statSync(BUILDER_KEY_FILE).mode & 511;
|
|
9841
|
-
if ((mode & 63) === 0) return;
|
|
9842
|
-
console.warn(
|
|
9843
|
-
JSON.stringify({
|
|
9844
|
-
level: "warn",
|
|
9845
|
-
event: "builder_identity_key_loose_mode",
|
|
9846
|
-
message: `${BUILDER_KEY_FILE} is group/other-readable (mode ${mode.toString(8).padStart(3, "0")}). The builder signing key should be owner-only \u2014 re-tightening to 0600. A cp/scp/git checkout can widen the mode; verify nobody else read it in the meantime.`,
|
|
9847
|
-
path: BUILDER_KEY_FILE,
|
|
9848
|
-
mode: mode.toString(8).padStart(3, "0")
|
|
9849
|
-
})
|
|
9850
|
-
);
|
|
9851
|
-
chmodSync3(BUILDER_KEY_FILE, 384);
|
|
9852
|
-
} catch {
|
|
9853
|
-
}
|
|
9854
|
-
}
|
|
9855
|
-
function bytesToHex2(bytes) {
|
|
9856
|
-
let s = "";
|
|
9857
|
-
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
9858
|
-
return s;
|
|
9859
|
-
}
|
|
9860
|
-
function hexToBytes2(hex) {
|
|
9861
|
-
if (hex.length % 2 !== 0) {
|
|
9862
|
-
throw new Error(`hex string has odd length: ${String(hex.length)}`);
|
|
9863
|
-
}
|
|
9864
|
-
const out = new Uint8Array(hex.length / 2);
|
|
9865
|
-
for (let i = 0; i < out.length; i++) {
|
|
9866
|
-
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
9867
|
-
}
|
|
9868
|
-
return out;
|
|
9869
|
-
}
|
|
9870
|
-
|
|
9871
|
-
// src/lib/receipt.ts
|
|
9872
|
-
import { schnorr as schnorr3 } from "@noble/curves/secp256k1.js";
|
|
9873
|
-
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
9874
|
-
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
9875
|
-
function computeResultHash(summary, artifactContents) {
|
|
9876
|
-
const artifactHashes = artifactContents.map(
|
|
9877
|
-
(content) => bytesToHex3(sha2563(canonicaliseForSigning(content)))
|
|
9878
|
-
);
|
|
9879
|
-
return bytesToHex3(
|
|
9880
|
-
sha2563(
|
|
9881
|
-
canonicaliseForSigning({
|
|
9882
|
-
summary: summary ?? null,
|
|
9883
|
-
artifact_hashes: artifactHashes
|
|
9884
|
-
})
|
|
9885
|
-
)
|
|
9886
|
-
);
|
|
9887
|
-
}
|
|
9888
|
-
function signReceipt(unsigned, secretHex) {
|
|
9889
|
-
const bytes = canonicaliseForSigning(unsigned);
|
|
9890
|
-
const signature = bytesToHex3(schnorr3.sign(bytes, hexToBytes3(secretHex)));
|
|
9891
|
-
return { ...unsigned, signature };
|
|
9892
|
-
}
|
|
9893
|
-
function verifyReceipt(receipt) {
|
|
9894
|
-
try {
|
|
9895
|
-
const { signature, ...unsigned } = receipt;
|
|
9896
|
-
const bytes = canonicaliseForSigning(unsigned);
|
|
9897
|
-
return schnorr3.verify(hexToBytes3(signature), bytes, hexToBytes3(receipt.receipt_pubkey));
|
|
9898
|
-
} catch {
|
|
9899
|
-
return false;
|
|
9900
|
-
}
|
|
9901
|
-
}
|
|
9902
|
-
function isSignedJobReceipt(value) {
|
|
9903
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
9904
|
-
const r = value;
|
|
9905
|
-
const paid = r.paid;
|
|
9906
|
-
return r.v === 1 && typeof r.job_id === "string" && typeof r.dvm_id === "string" && typeof r.dvm === "string" && typeof r.capability === "string" && (r.outcome === "completed" || r.outcome === "failed" || r.outcome === "cancelled") && typeof r.seq === "number" && typeof r.issued_at === "number" && typeof r.receipt_pubkey === "string" && typeof r.signature === "string" && !!paid && typeof paid === "object" && !Array.isArray(paid) && typeof paid.msats === "number";
|
|
9907
|
-
}
|
|
9908
|
-
function signDrainReceipt(unsigned, secretHex) {
|
|
9909
|
-
const bytes = canonicaliseForSigning(unsigned);
|
|
9910
|
-
const signature = bytesToHex3(schnorr3.sign(bytes, hexToBytes3(secretHex)));
|
|
9911
|
-
return { ...unsigned, signature };
|
|
9912
|
-
}
|
|
9913
|
-
function signFundingReceipt(unsigned, secretHex) {
|
|
9914
|
-
const bytes = canonicaliseForSigning(unsigned);
|
|
9915
|
-
const signature = bytesToHex3(schnorr3.sign(bytes, hexToBytes3(secretHex)));
|
|
9916
|
-
return { ...unsigned, signature };
|
|
9917
|
-
}
|
|
9918
|
-
function isFundingReceipt(value) {
|
|
9919
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
9920
|
-
const r = value;
|
|
9921
|
-
return r.v === 1 && r.kind === "credit_funding" && typeof r.credit_id === "string" && typeof r.fund_id === "string" && typeof r.funded_micro === "number" && typeof r.balance_after_micro === "number" && typeof r.ledger_seq === "number" && typeof r.issued_at === "number" && typeof r.caller_pubkey === "string" && typeof r.dvm_id === "string" && typeof r.dvm === "string" && typeof r.receipt_pubkey === "string" && typeof r.signature === "string";
|
|
9922
|
-
}
|
|
9923
|
-
function verifyFundingReceipt(receipt) {
|
|
9924
|
-
try {
|
|
9925
|
-
const { signature, ...unsigned } = receipt;
|
|
9926
|
-
const bytes = canonicaliseForSigning(unsigned);
|
|
9927
|
-
return schnorr3.verify(hexToBytes3(signature), bytes, hexToBytes3(receipt.receipt_pubkey));
|
|
9928
|
-
} catch {
|
|
9929
|
-
return false;
|
|
9930
|
-
}
|
|
9931
|
-
}
|
|
9932
|
-
function isDrainReceipt(value) {
|
|
9933
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
9934
|
-
const r = value;
|
|
9935
|
-
return r.v === 1 && r.kind === "credit_drain" && typeof r.credit_id === "string" && typeof r.drain_id === "string" && (r.event === "requested" || r.event === "parked" || r.event === "picked_up" || r.event === "sent" || r.event === "released") && typeof r.method === "string" && typeof r.amount_micro === "number" && typeof r.issued_at === "number" && typeof r.caller_pubkey === "string" && typeof r.dvm_id === "string" && typeof r.dvm === "string" && typeof r.receipt_pubkey === "string" && typeof r.signature === "string";
|
|
9936
|
-
}
|
|
9937
|
-
function verifyDrainReceipt(receipt) {
|
|
9938
|
-
try {
|
|
9939
|
-
const { signature, ...unsigned } = receipt;
|
|
9940
|
-
const bytes = canonicaliseForSigning(unsigned);
|
|
9941
|
-
return schnorr3.verify(hexToBytes3(signature), bytes, hexToBytes3(receipt.receipt_pubkey));
|
|
9942
|
-
} catch {
|
|
9943
|
-
return false;
|
|
9944
|
-
}
|
|
9945
|
-
}
|
|
9946
|
-
|
|
9947
|
-
// src/lib/receipt-verify.ts
|
|
9948
|
-
function attestedDvmIdentity(builder) {
|
|
9949
|
-
const builderPubkey = authenticatedBuilderPubkey(builder);
|
|
9950
|
-
if (!builderPubkey) return void 0;
|
|
9951
|
-
const dvmId = builder.attestation.dvm_id;
|
|
9952
|
-
if (typeof dvmId !== "string" || dvmId.length === 0) return void 0;
|
|
9953
|
-
return { dvmId, builderPubkey };
|
|
9954
|
-
}
|
|
9955
|
-
function authenticatedBuilderPubkey(builder) {
|
|
9956
|
-
if (!isTrustAnchor(builder)) return void 0;
|
|
9957
|
-
const pubkey = builder.pubkey.trim().toLowerCase();
|
|
9958
|
-
const attestedPubkey = builder.attestation.builder_pubkey;
|
|
9959
|
-
if (!/^[0-9a-f]{64}$/.test(pubkey) || typeof attestedPubkey !== "string" || attestedPubkey.trim().toLowerCase() !== pubkey) {
|
|
9960
|
-
return void 0;
|
|
9961
|
-
}
|
|
9962
|
-
const payload = builder.attestation;
|
|
9963
|
-
return verifyAttestation(payload, builder.signature, pubkey) ? pubkey : void 0;
|
|
9964
|
-
}
|
|
9965
|
-
function verifyJobReceipt(args) {
|
|
9966
|
-
const { receipt, builder, expectedJobId, delivered } = args;
|
|
9967
|
-
const checks = {
|
|
9968
|
-
job_id: checkJobId(receipt, expectedJobId),
|
|
9969
|
-
signature: verifyReceipt(receipt),
|
|
9970
|
-
attestation: "absent",
|
|
9971
|
-
result_hash: "unchecked"
|
|
9972
|
-
};
|
|
9973
|
-
if (checks.job_id === "mismatch") {
|
|
9974
|
-
return { status: "invalid", checks, reason: "job_id_mismatch" };
|
|
9975
|
-
}
|
|
9976
|
-
if (!checks.signature) {
|
|
9977
|
-
return { status: "invalid", checks, reason: "signature_invalid" };
|
|
9978
|
-
}
|
|
9979
|
-
checks.attestation = checkReceiptAttestation(receipt, builder);
|
|
9980
|
-
if (checks.attestation === "invalid") {
|
|
9981
|
-
return { status: "invalid", checks, reason: "attestation_invalid" };
|
|
9982
|
-
}
|
|
9983
|
-
if (checks.attestation === "identity_mismatch") {
|
|
9984
|
-
return { status: "invalid", checks, reason: "dvm_identity_mismatch" };
|
|
9985
|
-
}
|
|
9986
|
-
if (checks.attestation === "key_mismatch") {
|
|
9987
|
-
return { status: "invalid", checks, reason: "receipt_key_mismatch" };
|
|
9988
|
-
}
|
|
9989
|
-
checks.result_hash = checkResultHash(receipt, delivered);
|
|
9990
|
-
if (checks.result_hash === "mismatch") {
|
|
9991
|
-
return { status: "invalid", checks, reason: "result_hash_mismatch" };
|
|
9992
|
-
}
|
|
9993
|
-
return {
|
|
9994
|
-
status: checks.attestation === "ok" ? "verified" : "verified_unattested",
|
|
9995
|
-
checks
|
|
9996
|
-
};
|
|
9997
|
-
}
|
|
9998
|
-
function checkReceiptAttestation(receipt, builder) {
|
|
9999
|
-
if (!isTrustAnchor(builder)) return "absent";
|
|
10000
|
-
const payload = builder.attestation;
|
|
10001
|
-
if (!authenticatedBuilderPubkey(builder)) return "invalid";
|
|
10002
|
-
if (typeof payload.dvm_id !== "string" || payload.dvm_id !== receipt.dvm_id) {
|
|
10003
|
-
return "identity_mismatch";
|
|
10004
|
-
}
|
|
10005
|
-
const attested = builder.attestation.receipt_pubkey;
|
|
10006
|
-
if (typeof attested !== "string" || attested.length === 0) return "absent";
|
|
10007
|
-
return attested.toLowerCase() === receipt.receipt_pubkey.toLowerCase() ? "ok" : "key_mismatch";
|
|
10008
|
-
}
|
|
10009
|
-
function verifyDrainReceiptChain(args) {
|
|
10010
|
-
const { receipt, builder, expectedDrainId, expectedCreditId } = args;
|
|
10011
|
-
const named = isDrainReceipt(receipt) ? receipt : void 0;
|
|
10012
|
-
const checks = {
|
|
10013
|
-
drain_id: checkDrainField(named?.drain_id, expectedDrainId),
|
|
10014
|
-
credit_id: checkDrainField(named?.credit_id, expectedCreditId),
|
|
10015
|
-
signature: named !== void 0 && verifyDrainReceipt(named),
|
|
10016
|
-
attestation: "absent"
|
|
10017
|
-
};
|
|
10018
|
-
if (checks.drain_id === "mismatch") {
|
|
10019
|
-
return { status: "invalid", checks, reason: "drain_id_mismatch" };
|
|
10020
|
-
}
|
|
10021
|
-
if (checks.credit_id === "mismatch") {
|
|
10022
|
-
return { status: "invalid", checks, reason: "credit_id_mismatch" };
|
|
10023
|
-
}
|
|
10024
|
-
if (named === void 0 || !checks.signature) {
|
|
10025
|
-
return { status: "invalid", checks, reason: "signature_invalid" };
|
|
10026
|
-
}
|
|
10027
|
-
checks.attestation = checkReceiptAttestation(named, builder);
|
|
10028
|
-
if (checks.attestation === "invalid") {
|
|
10029
|
-
return { status: "invalid", checks, reason: "attestation_invalid" };
|
|
10030
|
-
}
|
|
10031
|
-
if (checks.attestation === "identity_mismatch") {
|
|
10032
|
-
return { status: "invalid", checks, reason: "dvm_identity_mismatch" };
|
|
10033
|
-
}
|
|
10034
|
-
if (checks.attestation === "key_mismatch") {
|
|
10035
|
-
return { status: "invalid", checks, reason: "receipt_key_mismatch" };
|
|
10036
|
-
}
|
|
10037
|
-
return {
|
|
10038
|
-
status: checks.attestation === "ok" ? "verified" : "verified_unattested",
|
|
10039
|
-
checks
|
|
10040
|
-
};
|
|
10041
|
-
}
|
|
10042
|
-
function verifyFundingReceiptChain(args) {
|
|
10043
|
-
const named = isFundingReceipt(args.receipt) ? args.receipt : void 0;
|
|
10044
|
-
const check = (actual, expected) => expected === void 0 ? "unchecked" : actual === expected ? "ok" : "mismatch";
|
|
10045
|
-
const checks = {
|
|
10046
|
-
credit_id: check(named?.credit_id, args.expectedCreditId),
|
|
10047
|
-
fund_id: check(named?.fund_id, args.expectedFundId),
|
|
10048
|
-
funded_micro: check(named?.funded_micro, args.expectedFundedMicro),
|
|
10049
|
-
balance_after_micro: check(named?.balance_after_micro, args.expectedBalanceAfterMicro),
|
|
10050
|
-
ledger_seq: check(named?.ledger_seq, args.expectedLedgerSeq),
|
|
10051
|
-
caller_pubkey: check(named?.caller_pubkey, args.expectedCallerPubkey),
|
|
10052
|
-
signature: named !== void 0 && verifyFundingReceipt(named),
|
|
10053
|
-
attestation: "absent"
|
|
10054
|
-
};
|
|
10055
|
-
const mismatch = [
|
|
10056
|
-
["credit_id", "credit_id_mismatch"],
|
|
10057
|
-
["fund_id", "fund_id_mismatch"],
|
|
10058
|
-
["funded_micro", "funding_amount_mismatch"],
|
|
10059
|
-
["balance_after_micro", "funding_balance_mismatch"],
|
|
10060
|
-
["ledger_seq", "funding_ledger_seq_mismatch"],
|
|
10061
|
-
["caller_pubkey", "caller_pubkey_mismatch"]
|
|
10062
|
-
];
|
|
10063
|
-
for (const [field, reason] of mismatch) {
|
|
10064
|
-
if (checks[field] === "mismatch") return { status: "invalid", checks, reason, receipt: named };
|
|
10065
|
-
}
|
|
10066
|
-
if (!named || !checks.signature) {
|
|
10067
|
-
return { status: "invalid", checks, reason: "signature_invalid", receipt: named };
|
|
10068
|
-
}
|
|
10069
|
-
checks.attestation = checkReceiptAttestation(named, args.builder);
|
|
10070
|
-
if (checks.attestation === "invalid") {
|
|
10071
|
-
return { status: "invalid", checks, reason: "attestation_invalid", receipt: named };
|
|
10072
|
-
}
|
|
10073
|
-
if (checks.attestation === "identity_mismatch") {
|
|
10074
|
-
return { status: "invalid", checks, reason: "dvm_identity_mismatch", receipt: named };
|
|
10075
|
-
}
|
|
10076
|
-
if (checks.attestation === "key_mismatch") {
|
|
10077
|
-
return { status: "invalid", checks, reason: "receipt_key_mismatch", receipt: named };
|
|
10078
|
-
}
|
|
10079
|
-
return {
|
|
10080
|
-
status: checks.attestation === "ok" ? "verified" : "verified_unattested",
|
|
10081
|
-
checks,
|
|
10082
|
-
receipt: named
|
|
10083
|
-
};
|
|
10084
|
-
}
|
|
10085
|
-
function weakestReceiptVerdict(statuses) {
|
|
10086
|
-
if (statuses.length === 0) return "absent";
|
|
10087
|
-
if (statuses.includes("invalid")) return "invalid";
|
|
10088
|
-
if (statuses.includes("absent")) return "absent";
|
|
10089
|
-
if (statuses.includes("verified_unattested")) return "verified_unattested";
|
|
10090
|
-
return "verified";
|
|
10091
|
-
}
|
|
10092
|
-
function drainReceiptsDisplay(status, sound, total) {
|
|
10093
|
-
const tally = `${String(sound)} of ${String(total)}`;
|
|
10094
|
-
switch (status) {
|
|
10095
|
-
case "verified":
|
|
10096
|
-
return `Verified reclaim receipts (${tally}): signed by the key this DVM's builder identity attests.`;
|
|
10097
|
-
case "verified_unattested":
|
|
10098
|
-
return `Reclaim receipts, signed but unattested (${tally}): the signature holds, but this DVM publishes no builder identity to vouch for the signing key.`;
|
|
10099
|
-
case "invalid":
|
|
10100
|
-
return `UNVERIFIED reclaim receipts: only ${tally} hold up \u2014 the refund is still owed, but this evidence is not sound.`;
|
|
10101
|
-
case "absent":
|
|
10102
|
-
return "No reclaim receipts: this DVM returned no signed evidence of the drain.";
|
|
10103
|
-
}
|
|
10104
|
-
}
|
|
10105
|
-
function drainReceiptsHint(status) {
|
|
10106
|
-
switch (status) {
|
|
10107
|
-
case "verified":
|
|
10108
|
-
return "The refund evidence chains to this DVM's published builder identity, and is stored locally with that identity pinned beside it \u2014 so an unhonoured refund stays provable against the builder, not against an anonymous key.";
|
|
10109
|
-
case "verified_unattested":
|
|
10110
|
-
return "The receipts are signed, but nothing vouches for the signing key \u2014 expected for a locally-run or development DVM, not for a deployed one. Stored either way; the refund is owed regardless.";
|
|
10111
|
-
case "invalid":
|
|
10112
|
-
return "The refund is unaffected \u2014 this is a problem with the proof, not the money. Keep this output, and treat the reclaim as unattested when deciding how much to leave on deposit at this DVM.";
|
|
10113
|
-
case "absent":
|
|
10114
|
-
return "This DVM countersigned nothing, so the only artifact behind this reclaim is the drain request the caller signed. Weaker evidence than a countersigned drain, and worth weighing before funding here again.";
|
|
10115
|
-
}
|
|
10116
|
-
}
|
|
10117
|
-
function receiptDisplay(receipt, status, rate = 0) {
|
|
10118
|
-
const what = `${receipt.dvm}/${receipt.capability} (DVM ID ${receipt.dvm_id})`;
|
|
10119
|
-
const when = receiptIssuedAtIso(receipt)?.slice(0, 10);
|
|
10120
|
-
return `${VERDICT_PREFIX[status]}: paid ${formatPaid(receipt.paid.msats, rate)} for ${what}, ${receipt.outcome}${when ? ` ${when}` : ""}.`;
|
|
10121
|
-
}
|
|
10122
|
-
function receiptIssuedAtIso(receipt) {
|
|
10123
|
-
const seconds = receipt.issued_at;
|
|
10124
|
-
return typeof seconds === "number" ? epochMsToIso(seconds * 1e3) : null;
|
|
10125
|
-
}
|
|
10126
|
-
function epochMsToIso(ms) {
|
|
10127
|
-
if (typeof ms !== "number" || !Number.isFinite(ms) || Math.abs(ms) > MAX_TIMESTAMP_MS) {
|
|
10128
|
-
return null;
|
|
10129
|
-
}
|
|
10130
|
-
return new Date(ms).toISOString();
|
|
10131
|
-
}
|
|
10132
|
-
function receiptHint(status, jobId) {
|
|
10133
|
-
const verifyCmd = jobId ? `'dvm receipts verify ${jobId}'` : "'dvm receipts verify <job-id>'";
|
|
10134
|
-
switch (status) {
|
|
10135
|
-
case "verified":
|
|
10136
|
-
return `Proof of purchase is stored locally. Re-check it any time with ${verifyCmd}, or hand over the raw bundle with 'dvm receipts export'.`;
|
|
10137
|
-
case "verified_unattested":
|
|
10138
|
-
return `The receipt's signature is valid, but this DVM publishes no builder identity to vouch for the signing key \u2014 expected for a locally-run or development DVM, not for a deployed one. Stored either way; inspect it with ${verifyCmd}.`;
|
|
10139
|
-
case "invalid":
|
|
10140
|
-
return `The job itself is unaffected \u2014 this is a problem with the proof, not the result. Run ${verifyCmd} to see which check failed before relying on this receipt as evidence.`;
|
|
10141
|
-
case "absent":
|
|
10142
|
-
return "This DVM does not issue signed receipts, so there is no proof of purchase to store. Nothing went wrong.";
|
|
10143
|
-
}
|
|
10144
|
-
}
|
|
10145
|
-
function receiptFailureDetail(reason) {
|
|
10146
|
-
switch (reason) {
|
|
10147
|
-
case "job_id_mismatch":
|
|
10148
|
-
return "The receipt names a different job than the one it was collected for \u2014 it is not proof of this job, whoever signed it.";
|
|
10149
|
-
case "drain_id_mismatch":
|
|
10150
|
-
return "The receipt names a different reclaim than the one it is filed under \u2014 it is not evidence of this drain, whoever signed it.";
|
|
10151
|
-
case "credit_id_mismatch":
|
|
10152
|
-
return "The receipt names a different credit than the one this reclaim was against \u2014 it is not evidence of this refund, whoever signed it.";
|
|
10153
|
-
case "fund_id_mismatch":
|
|
10154
|
-
return "The receipt names a different funding operation than the caller submitted.";
|
|
10155
|
-
case "funding_amount_mismatch":
|
|
10156
|
-
return "The signed funded amount differs from the provider's funding response.";
|
|
10157
|
-
case "funding_balance_mismatch":
|
|
10158
|
-
return "The signed post-funding balance differs from the provider's fresh balance response.";
|
|
10159
|
-
case "funding_ledger_seq_mismatch":
|
|
10160
|
-
return "The signed funding sequence differs from the provider's fresh balance response.";
|
|
10161
|
-
case "caller_pubkey_mismatch":
|
|
10162
|
-
return "The funding receipt names a different caller than the key that submitted the funding.";
|
|
10163
|
-
case "signature_invalid":
|
|
10164
|
-
return "The receipt's signature does not verify under the key it names \u2014 the bytes were altered, or it was never signed by that key.";
|
|
10165
|
-
case "attestation_invalid":
|
|
10166
|
-
return "The DVM's published builder attestation does not verify under its own builder pubkey.";
|
|
10167
|
-
case "dvm_identity_mismatch":
|
|
10168
|
-
return "The receipt names a different immutable DVM ID than the builder attestation, so it is not evidence for this provider.";
|
|
10169
|
-
case "receipt_key_mismatch":
|
|
10170
|
-
return "The receipt was signed by a different key than the one the DVM's builder attested.";
|
|
10171
|
-
case "result_hash_mismatch":
|
|
10172
|
-
return "The receipt attests a different result than the one delivered.";
|
|
10173
|
-
}
|
|
10174
|
-
}
|
|
10175
|
-
var MAX_TIMESTAMP_MS = 864e13;
|
|
10176
|
-
function checkJobId(receipt, expectedJobId) {
|
|
10177
|
-
if (expectedJobId === void 0) return "unchecked";
|
|
10178
|
-
return receipt.job_id === expectedJobId ? "ok" : "mismatch";
|
|
10179
|
-
}
|
|
10180
|
-
function checkDrainField(actual, expected) {
|
|
10181
|
-
if (expected === void 0 || actual === void 0) return "unchecked";
|
|
10182
|
-
return actual === expected ? "ok" : "mismatch";
|
|
10183
|
-
}
|
|
10184
|
-
var VERDICT_PREFIX = {
|
|
10185
|
-
verified: "Verified receipt",
|
|
10186
|
-
verified_unattested: "Receipt (signed, unattested)",
|
|
10187
|
-
invalid: "UNVERIFIED receipt",
|
|
10188
|
-
absent: "No receipt"
|
|
10189
|
-
};
|
|
10190
|
-
function checkResultHash(receipt, delivered) {
|
|
10191
|
-
if (!delivered) return "unchecked";
|
|
10192
|
-
if (receipt.result_hash === null) return "unchecked";
|
|
10193
|
-
const summary = receipt.outcome === "completed" ? delivered.summary : void 0;
|
|
10194
|
-
let recomputed;
|
|
10195
|
-
try {
|
|
10196
|
-
recomputed = computeResultHash(summary, delivered.artifactContents);
|
|
10197
|
-
} catch {
|
|
10198
|
-
return "unchecked";
|
|
10199
|
-
}
|
|
10200
|
-
return recomputed === receipt.result_hash ? "ok" : "mismatch";
|
|
10201
|
-
}
|
|
10202
|
-
function isTrustAnchor(value) {
|
|
10203
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
10204
|
-
const anchor = value;
|
|
10205
|
-
return typeof anchor.pubkey === "string" && typeof anchor.signature === "string" && !!anchor.attestation && typeof anchor.attestation === "object" && !Array.isArray(anchor.attestation);
|
|
10206
|
-
}
|
|
10207
|
-
function formatPaid(msats, rate) {
|
|
10208
|
-
if (msats === 0) return "nothing (free job)";
|
|
10209
|
-
const sats = msats % 1e3 === 0 ? msats / 1e3 : Math.round(msats / 1e3 * 100) / 100;
|
|
10210
|
-
const satsLabel = `${sats.toLocaleString("en-US")} sats`;
|
|
10211
|
-
if (rate <= 0) return satsLabel;
|
|
10212
|
-
const usd = msatsToUsd(msats, rate);
|
|
10213
|
-
return `~$${formatUsd(usd)} (${satsLabel}, ${costAnchor(usd)})`;
|
|
10214
|
-
}
|
|
10215
|
-
|
|
10216
|
-
// src/lib/request-id.ts
|
|
10217
|
-
var MAX_REQUEST_ID_LENGTH = 128;
|
|
10218
|
-
var REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
10219
|
-
function isPublicRequestId(value) {
|
|
10220
|
-
return typeof value === "string" && value.length > 0 && value.length <= MAX_REQUEST_ID_LENGTH && REQUEST_ID_PATTERN.test(value);
|
|
10221
|
-
}
|
|
10222
|
-
|
|
10223
|
-
// src/lib/traceparent.ts
|
|
10224
|
-
var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
10225
|
-
var ALL_ZERO_TRACE = "00000000000000000000000000000000";
|
|
10226
|
-
var ALL_ZERO_SPAN = "0000000000000000";
|
|
10227
|
-
function parseTraceparent(header) {
|
|
10228
|
-
if (!header) return null;
|
|
10229
|
-
const match = TRACEPARENT_RE.exec(header.trim());
|
|
10230
|
-
if (!match) return null;
|
|
10231
|
-
const [, traceId, parentSpanId, flagsHex] = match;
|
|
10232
|
-
if (traceId === ALL_ZERO_TRACE || parentSpanId === ALL_ZERO_SPAN) return null;
|
|
10233
|
-
return { traceId, parentSpanId, flags: parseInt(flagsHex, 16) };
|
|
8295
|
+
}
|
|
8296
|
+
});
|
|
10234
8297
|
}
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
"Provide a valid secp256k1 private key as a 64-char hex string."
|
|
10245
|
-
);
|
|
10246
|
-
}
|
|
10247
|
-
const bytes = hexToBytes4(privkey);
|
|
10248
|
-
if (!secp256k12.utils.isValidSecretKey(bytes)) {
|
|
10249
|
-
throw new DvmError(
|
|
10250
|
-
"invalid_privkey",
|
|
10251
|
-
"Privkey is not a valid secp256k1 scalar (zero or out of curve order).",
|
|
10252
|
-
"Generate a fresh key with: dvm wallet init"
|
|
10253
|
-
);
|
|
8298
|
+
function drainLedgerErrorResponse(error, creditId, drainId) {
|
|
8299
|
+
if (error.code === "nothing_to_drain") {
|
|
8300
|
+
return jsonPaymentError("nothing_to_drain", 409, {
|
|
8301
|
+
message: `credit ${creditId} has no available balance to drain.`,
|
|
8302
|
+
extra: {
|
|
8303
|
+
balance_micro: error.details.balanceMicro ?? 0,
|
|
8304
|
+
remaining_micro: Math.max(0, error.details.availableMicro ?? 0)
|
|
8305
|
+
}
|
|
8306
|
+
});
|
|
10254
8307
|
}
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
}
|
|
10264
|
-
|
|
10265
|
-
// src/lib/wallet-agent/sign.ts
|
|
10266
|
-
import { schnorr as schnorr4 } from "@noble/curves/secp256k1.js";
|
|
10267
|
-
var PUBKEY_COMPRESSED_HEX_RE = /^0[23][0-9a-f]{64}$/i;
|
|
10268
|
-
var PUBKEY_XONLY_HEX_RE = /^[0-9a-f]{64}$/i;
|
|
10269
|
-
var SIG_HEX_RE = /^[0-9a-f]{128}$/i;
|
|
10270
|
-
function signChallenge(privkeyHex, challenge) {
|
|
10271
|
-
if (!PRIVKEY_HEX_RE.test(privkeyHex)) {
|
|
10272
|
-
throw new DvmError(
|
|
10273
|
-
"invalid_privkey",
|
|
10274
|
-
"Privkey must be 64 hex characters (32 bytes).",
|
|
10275
|
-
"Provide a valid secp256k1 private key as a 64-char hex string."
|
|
10276
|
-
);
|
|
8308
|
+
if (error.code === "drain_below_dust") {
|
|
8309
|
+
return jsonPaymentError("drain_below_dust", 409, {
|
|
8310
|
+
message: error.message,
|
|
8311
|
+
extra: {
|
|
8312
|
+
balance_micro: error.details.balanceMicro ?? 0,
|
|
8313
|
+
remaining_micro: Math.max(0, error.details.availableMicro ?? 0),
|
|
8314
|
+
owed_sats: 0
|
|
8315
|
+
}
|
|
8316
|
+
});
|
|
10277
8317
|
}
|
|
10278
|
-
if (
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
8318
|
+
if (error.code === "drain_conflict") {
|
|
8319
|
+
const copy = error.details.drainConflict ? DRAIN_CONFLICT_COPY[error.details.drainConflict] : void 0;
|
|
8320
|
+
return jsonPaymentError("drain_conflict", 409, {
|
|
8321
|
+
message: error.message,
|
|
8322
|
+
display: copy?.display,
|
|
8323
|
+
hint: copy?.hint({ creditId, drainId }),
|
|
8324
|
+
// An unclassified conflict keeps the duplicate-payout copy's own
|
|
8325
|
+
// `retryable`, and the id it was never told anything about.
|
|
8326
|
+
retryable: copy?.retryable,
|
|
8327
|
+
extra: { drain_registered: copy?.drainRegistered ?? false }
|
|
8328
|
+
});
|
|
10284
8329
|
}
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
} else if (PUBKEY_XONLY_HEX_RE.test(pubkeyHex)) {
|
|
10295
|
-
xonly = hexToBytes5(pubkeyHex);
|
|
10296
|
-
} else {
|
|
10297
|
-
return false;
|
|
8330
|
+
if (error.code === "settlement_pending") {
|
|
8331
|
+
return x402SettlementPendingRefusal({
|
|
8332
|
+
door: "drain",
|
|
8333
|
+
message: error.message,
|
|
8334
|
+
drainId,
|
|
8335
|
+
...error.details.settlementId ? { settlementId: error.details.settlementId } : {},
|
|
8336
|
+
...error.details.settlementStatus ? { settlementStatus: error.details.settlementStatus } : {},
|
|
8337
|
+
...error.details.blockingDrainId ? { blockingDrainId: error.details.blockingDrainId } : {}
|
|
8338
|
+
});
|
|
10298
8339
|
}
|
|
10299
|
-
|
|
10300
|
-
return
|
|
10301
|
-
|
|
10302
|
-
|
|
8340
|
+
if (error.code === "caller_mismatch" || error.code === "credit_not_found") {
|
|
8341
|
+
return jsonPaymentError("credit_not_found", 404, {
|
|
8342
|
+
message: `No credit ${creditId} for this caller.`
|
|
8343
|
+
});
|
|
10303
8344
|
}
|
|
8345
|
+
return jsonPaymentError(mapLedgerCode(error), 402, { message: error.message });
|
|
10304
8346
|
}
|
|
10305
|
-
function
|
|
10306
|
-
return
|
|
8347
|
+
function isHttpResponse(value) {
|
|
8348
|
+
return value instanceof Response || typeof value === "object" && "headers" in value && typeof value.status === "number";
|
|
10307
8349
|
}
|
|
10308
|
-
function
|
|
10309
|
-
|
|
8350
|
+
function mapLedgerCode(err) {
|
|
8351
|
+
switch (err.code) {
|
|
8352
|
+
case "insufficient_credit":
|
|
8353
|
+
case "credit_expired":
|
|
8354
|
+
case "credit_unbacked":
|
|
8355
|
+
case "credit_not_found":
|
|
8356
|
+
case "tempo_channel_closing":
|
|
8357
|
+
case "settlement_pending":
|
|
8358
|
+
case "nothing_to_drain":
|
|
8359
|
+
case "drain_below_dust":
|
|
8360
|
+
case "drain_not_found":
|
|
8361
|
+
case "drain_conflict":
|
|
8362
|
+
return err.code;
|
|
8363
|
+
default:
|
|
8364
|
+
return "payment_invalid";
|
|
8365
|
+
}
|
|
10310
8366
|
}
|
|
10311
|
-
|
|
10312
|
-
// src/lib/cashu/admin-auth.ts
|
|
10313
|
-
import { randomBytes as randomBytes3 } from "crypto";
|
|
10314
|
-
import { sha256 as sha2564 } from "@noble/hashes/sha2.js";
|
|
10315
|
-
var ADMIN_AUTH_NONCE_TTL_SECONDS = 300;
|
|
10316
|
-
var ADMIN_AUTH_CLOCK_SKEW_SECONDS = 30;
|
|
10317
|
-
var ADMIN_STATE_UNAVAILABLE = "admin_state_unavailable";
|
|
10318
|
-
function buildAdminChallenge(args) {
|
|
10319
|
-
const msg = `dvmkit-admin:${args.method}:${args.path}:${args.dvmId}:${args.nonceHex}:${args.unixSeconds}`;
|
|
10320
|
-
return sha2564(new TextEncoder().encode(msg));
|
|
10321
|
-
}
|
|
10322
|
-
function buildAdminAuthHeader(args) {
|
|
10323
|
-
const nowMs = args.now ? args.now() : Date.now();
|
|
10324
|
-
const unixSeconds = Math.floor(nowMs / 1e3);
|
|
10325
|
-
const nonceBytes = args.randomNonce ? args.randomNonce() : Uint8Array.from(randomBytes3(16));
|
|
10326
|
-
const nonceHex = Buffer.from(nonceBytes).toString("hex");
|
|
10327
|
-
const challenge = buildAdminChallenge({
|
|
10328
|
-
method: args.method,
|
|
10329
|
-
path: args.path,
|
|
10330
|
-
dvmId: args.dvmId,
|
|
10331
|
-
nonceHex,
|
|
10332
|
-
unixSeconds
|
|
10333
|
-
});
|
|
10334
|
-
const sigHex = signChallenge(args.privkeyHex, challenge);
|
|
8367
|
+
function creditBody(credit, reconciliationPending = false) {
|
|
10335
8368
|
return {
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
8369
|
+
status: credit.status,
|
|
8370
|
+
credit_id: credit.creditId,
|
|
8371
|
+
currency: credit.currency,
|
|
8372
|
+
balance_micro: credit.balanceMicro,
|
|
8373
|
+
remaining_micro: credit.availableMicro,
|
|
8374
|
+
expiry_ms: credit.expiryMs,
|
|
8375
|
+
expired: credit.expired,
|
|
8376
|
+
// Reclaimable via op "drain" (internal-review) — true whenever spendable-or-
|
|
8377
|
+
// expired value remains. Expiry ends spending, never ownership.
|
|
8378
|
+
drainable: credit.status === "active" && credit.availableMicro > 0,
|
|
8379
|
+
...reconciliationPending && { reconciliation_pending: true },
|
|
8380
|
+
ledger_seq: credit.lastLedgerSeq
|
|
10339
8381
|
};
|
|
10340
8382
|
}
|
|
8383
|
+
function spendable(credit, currency) {
|
|
8384
|
+
return !credit.expired && credit.status === "active" && credit.currency === currency;
|
|
8385
|
+
}
|
|
8386
|
+
function pickPrimary(credits, currency) {
|
|
8387
|
+
let best;
|
|
8388
|
+
for (const credit of credits) {
|
|
8389
|
+
if (!spendable(credit, currency) || credit.availableMicro <= 0) continue;
|
|
8390
|
+
if (!best || credit.availableMicro > best.availableMicro || credit.availableMicro === best.availableMicro && credit.createdAt > best.createdAt) {
|
|
8391
|
+
best = credit;
|
|
8392
|
+
}
|
|
8393
|
+
}
|
|
8394
|
+
return best;
|
|
8395
|
+
}
|
|
10341
8396
|
|
|
10342
8397
|
// src/sdk/server/admin-cashu.ts
|
|
10343
8398
|
import { CheckStateEnum as CheckStateEnum2 } from "@cashu/cashu-ts";
|
|
@@ -10670,7 +8725,7 @@ async function handleRotateLockPubkey(c, opts) {
|
|
|
10670
8725
|
}
|
|
10671
8726
|
|
|
10672
8727
|
// src/sdk/server/x402-exact-settlement.ts
|
|
10673
|
-
import { createHash as
|
|
8728
|
+
import { createHash as createHash4 } from "crypto";
|
|
10674
8729
|
var X402ExactSettlementServer = class {
|
|
10675
8730
|
constructor(opts) {
|
|
10676
8731
|
this.opts = opts;
|
|
@@ -10998,7 +9053,7 @@ function settlementMayHaveMoved(response) {
|
|
|
10998
9053
|
return reason.includes("authorization_already_used") || reason.includes("authorization already used") || /facilitator_status_5\d\d/.test(reason);
|
|
10999
9054
|
}
|
|
11000
9055
|
function hash(parts) {
|
|
11001
|
-
return
|
|
9056
|
+
return createHash4("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
11002
9057
|
}
|
|
11003
9058
|
function safeError(err) {
|
|
11004
9059
|
if (err instanceof Error) return err.name;
|
|
@@ -11022,31 +9077,16 @@ function jobCredentialGate(opts) {
|
|
|
11022
9077
|
// (2026-06-29, paid jobs whose results were unretrievable behind an
|
|
11023
9078
|
// opaque 404) is only one of two ways to get here — a current client
|
|
11024
9079
|
// reading a job whose local state has no token gets the same refusal,
|
|
11025
|
-
// and
|
|
11026
|
-
// a human verbatim, so the conditional advice lives in `hint`, which
|
|
9080
|
+
// and an upgrade alone would be a dead end for them. `display` is relayed
|
|
9081
|
+
// to a human verbatim, so the conditional advice lives in `hint`, which
|
|
11027
9082
|
// is what the agent acts on.
|
|
11028
9083
|
display: "Couldn't retrieve your result \u2014 the request was missing its job credential.",
|
|
11029
|
-
hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/dvm-cli@0.
|
|
9084
|
+
hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/dvm-cli@0.2.0, then retry the same command with --endpoint <url> --job-token <hex> (or set DVM_JOB_ENDPOINT and DVM_JOB_TOKEN).`
|
|
11030
9085
|
};
|
|
11031
9086
|
}
|
|
11032
9087
|
};
|
|
11033
9088
|
}
|
|
11034
9089
|
|
|
11035
|
-
// src/lib/monotonic-clock.ts
|
|
11036
|
-
function createMonotonicClock() {
|
|
11037
|
-
let anchorWallMs = Date.now();
|
|
11038
|
-
let anchorMonoMs = performance.now();
|
|
11039
|
-
return () => {
|
|
11040
|
-
const wallMs = Date.now();
|
|
11041
|
-
const monoMs = performance.now();
|
|
11042
|
-
const projectedMs = anchorWallMs + (monoMs - anchorMonoMs);
|
|
11043
|
-
const nowMs = wallMs >= projectedMs ? wallMs : projectedMs;
|
|
11044
|
-
anchorWallMs = nowMs;
|
|
11045
|
-
anchorMonoMs = monoMs;
|
|
11046
|
-
return Math.floor(nowMs);
|
|
11047
|
-
};
|
|
11048
|
-
}
|
|
11049
|
-
|
|
11050
9090
|
// src/sdk/server/request-envelope.ts
|
|
11051
9091
|
var REQUEST_ID_FIELD = "request_id";
|
|
11052
9092
|
var AUTH_STATEMENT_FIELD = "auth_statement";
|
|
@@ -11055,7 +9095,7 @@ function protocolEnvelopeIgnoreFields(schema) {
|
|
|
11055
9095
|
}
|
|
11056
9096
|
function stripProtocolEnvelope(schema, data) {
|
|
11057
9097
|
const withoutCredit = stripCreditEnvelope(schema, data);
|
|
11058
|
-
if (!
|
|
9098
|
+
if (!isRecord2(withoutCredit)) return withoutCredit;
|
|
11059
9099
|
const {
|
|
11060
9100
|
[REQUEST_ID_FIELD]: _requestId,
|
|
11061
9101
|
[AUTH_STATEMENT_FIELD]: _authStatement,
|
|
@@ -11064,26 +9104,27 @@ function stripProtocolEnvelope(schema, data) {
|
|
|
11064
9104
|
return builderInput;
|
|
11065
9105
|
}
|
|
11066
9106
|
function requestIdFromEnvelope(data) {
|
|
11067
|
-
if (!
|
|
9107
|
+
if (!isRecord2(data) || !(REQUEST_ID_FIELD in data)) return void 0;
|
|
11068
9108
|
return isPublicRequestId(data[REQUEST_ID_FIELD]) ? data[REQUEST_ID_FIELD] : void 0;
|
|
11069
9109
|
}
|
|
11070
9110
|
function hasInvalidRequestId(data) {
|
|
11071
|
-
return
|
|
9111
|
+
return isRecord2(data) && REQUEST_ID_FIELD in data && !isPublicRequestId(data[REQUEST_ID_FIELD]);
|
|
11072
9112
|
}
|
|
11073
|
-
function
|
|
9113
|
+
function isRecord2(value) {
|
|
11074
9114
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11075
9115
|
}
|
|
11076
9116
|
|
|
11077
9117
|
// src/sdk/server/job-manager.ts
|
|
11078
|
-
import { randomBytes as
|
|
9118
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
11079
9119
|
|
|
11080
9120
|
// src/sdk/server/context.ts
|
|
11081
|
-
import {
|
|
9121
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
9122
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
11082
9123
|
|
|
11083
9124
|
// src/sdk/server/instrumented-kv.ts
|
|
11084
|
-
import { createHash as
|
|
9125
|
+
import { createHash as createHash5 } from "crypto";
|
|
11085
9126
|
function hashKey(key) {
|
|
11086
|
-
return
|
|
9127
|
+
return createHash5("sha256").update(key).digest("hex").slice(0, 16);
|
|
11087
9128
|
}
|
|
11088
9129
|
function instrumentKVStore(inner) {
|
|
11089
9130
|
return {
|
|
@@ -11120,6 +9161,7 @@ function instrumentKVStore(inner) {
|
|
|
11120
9161
|
}
|
|
11121
9162
|
|
|
11122
9163
|
// src/sdk/server/context.ts
|
|
9164
|
+
var stepCostScope = new AsyncLocalStorage();
|
|
11123
9165
|
function buildContext(job, opts) {
|
|
11124
9166
|
const log = createConsoleLogger(job.id);
|
|
11125
9167
|
const instrumentedStore = instrumentKVStore(opts.store);
|
|
@@ -11140,6 +9182,45 @@ function buildContext(job, opts) {
|
|
|
11140
9182
|
let creditedToHandlerMsats = 0;
|
|
11141
9183
|
let creditedToHandlerMicro = 0;
|
|
11142
9184
|
let poolBasis = false;
|
|
9185
|
+
let declaredCostAmount = 0;
|
|
9186
|
+
let declaredCostCurrency;
|
|
9187
|
+
let declaredCostRevision = 0;
|
|
9188
|
+
let costPoisoned = false;
|
|
9189
|
+
const stepCostOwner = {};
|
|
9190
|
+
const declareCost = (declared) => {
|
|
9191
|
+
if (terminated("cost")) return;
|
|
9192
|
+
const currency = validateCurrency(declared.currency);
|
|
9193
|
+
const total = declaredCostAmount + declared.amount;
|
|
9194
|
+
const micro = Math.round(total * 1e6);
|
|
9195
|
+
if (!Number.isFinite(declared.amount) || declared.amount < 0 || !Number.isSafeInteger(micro)) {
|
|
9196
|
+
throw new RangeError(
|
|
9197
|
+
`ctx.cost amounts must each be a finite, non-negative number of currency units, and must total no more than ${Math.floor(Number.MAX_SAFE_INTEGER / 1e6)}; got ${String(declared.amount)}`
|
|
9198
|
+
);
|
|
9199
|
+
}
|
|
9200
|
+
declaredCostRevision += 1;
|
|
9201
|
+
job.costRevision = declaredCostRevision;
|
|
9202
|
+
const replayable = { amount: declared.amount, currency };
|
|
9203
|
+
const scope = stepCostScope.getStore();
|
|
9204
|
+
if (scope?.owner === stepCostOwner) {
|
|
9205
|
+
for (const frame of scope.frames) frame.push(replayable);
|
|
9206
|
+
}
|
|
9207
|
+
if (declaredCostCurrency !== void 0 && declaredCostCurrency !== currency) {
|
|
9208
|
+
costPoisoned = true;
|
|
9209
|
+
log.error("ctx.cost() currency switched mid-job \u2014 this job reports no cost", {
|
|
9210
|
+
declared: currency,
|
|
9211
|
+
pinned: declaredCostCurrency
|
|
9212
|
+
});
|
|
9213
|
+
}
|
|
9214
|
+
declaredCostCurrency ??= currency;
|
|
9215
|
+
if (costPoisoned) {
|
|
9216
|
+
job.costAmountMicro = void 0;
|
|
9217
|
+
job.costCurrency = void 0;
|
|
9218
|
+
return;
|
|
9219
|
+
}
|
|
9220
|
+
declaredCostAmount = total;
|
|
9221
|
+
job.costAmountMicro = micro;
|
|
9222
|
+
job.costCurrency = currency;
|
|
9223
|
+
};
|
|
11143
9224
|
return {
|
|
11144
9225
|
jobId: job.id,
|
|
11145
9226
|
tags: job.tags,
|
|
@@ -11322,19 +9403,21 @@ function buildContext(job, opts) {
|
|
|
11322
9403
|
let x402Nonce;
|
|
11323
9404
|
let x402AmountUsdcMicro;
|
|
11324
9405
|
const hasExplicitX402 = Boolean(payOpts?.x402);
|
|
11325
|
-
if (
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
9406
|
+
if ((opts.pricingCurrency ?? "usd") === "usd") {
|
|
9407
|
+
if (payOpts?.x402) {
|
|
9408
|
+
paymentRequestContent.x402 = payOpts.x402;
|
|
9409
|
+
} else if (opts.paymentMethods?.includes("x402") && opts.x402 && (opts.x402FacilitatorHealth?.support().available ?? true)) {
|
|
9410
|
+
paymentRequestContent.x402 = {
|
|
9411
|
+
pay_to: opts.x402.payTo,
|
|
9412
|
+
network: opts.x402.network,
|
|
9413
|
+
asset: opts.x402.asset ?? "USDC",
|
|
9414
|
+
...opts.x402.facilitator && { facilitator: opts.x402.facilitator }
|
|
9415
|
+
};
|
|
9416
|
+
}
|
|
11334
9417
|
}
|
|
11335
9418
|
if (paymentRequestContent.x402) {
|
|
11336
9419
|
paymentRate = (await resolveServerBtcUsdRate(opts.fxFetcher)).rate;
|
|
11337
|
-
x402Nonce = `0x${
|
|
9420
|
+
x402Nonce = `0x${randomBytes3(32).toString("hex")}`;
|
|
11338
9421
|
x402AmountUsdcMicro = msatsToUsdc(amountMsats, paymentRate).toString();
|
|
11339
9422
|
const x402Request = paymentRequestContent.x402;
|
|
11340
9423
|
const challengeConfig = {
|
|
@@ -11530,10 +9613,21 @@ function buildContext(job, opts) {
|
|
|
11530
9613
|
providerMessage(job, "cancel", { reason: error });
|
|
11531
9614
|
runTerminal();
|
|
11532
9615
|
},
|
|
9616
|
+
cost: declareCost,
|
|
11533
9617
|
step: async (id, fn) => {
|
|
11534
|
-
if (job.stepCache.has(id))
|
|
11535
|
-
|
|
11536
|
-
|
|
9618
|
+
if (job.stepCache.has(id)) {
|
|
9619
|
+
const record = job.stepCache.getRecord(id);
|
|
9620
|
+
for (const declared of record.costs ?? []) declareCost(declared);
|
|
9621
|
+
return record.value;
|
|
9622
|
+
}
|
|
9623
|
+
const costs = [];
|
|
9624
|
+
const scope = stepCostScope.getStore();
|
|
9625
|
+
const parentFrames = scope?.owner === stepCostOwner ? scope.frames : [];
|
|
9626
|
+
const result = await stepCostScope.run(
|
|
9627
|
+
{ owner: stepCostOwner, frames: [...parentFrames, costs] },
|
|
9628
|
+
fn
|
|
9629
|
+
);
|
|
9630
|
+
job.stepCache.set(id, result, costs);
|
|
11537
9631
|
return result;
|
|
11538
9632
|
},
|
|
11539
9633
|
state: job.state,
|
|
@@ -11899,12 +9993,20 @@ async function reapExpiredKeysetRows(opts, db) {
|
|
|
11899
9993
|
// src/sdk/server/job-manager.ts
|
|
11900
9994
|
var STALE_JOB_WATCHDOG_HEADROOM_MS = 9e4;
|
|
11901
9995
|
var DEFAULT_PROCESSING_WATCHDOG_MS = 5 * 6e4;
|
|
9996
|
+
var TERMINAL_COST_RECOVERY_BATCH = 100;
|
|
9997
|
+
var TERMINAL_COST_RECOVERY_MAX_PAGES = 10;
|
|
9998
|
+
var TERMINAL_COST_RECOVERY_INTERVAL_MS = 3e4;
|
|
11902
9999
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
11903
10000
|
var DEFAULT_REACTIVATION_CLAIM_GRACE_MS = 150;
|
|
11904
10001
|
var REACTIVATION_CLAIM_POLL_MS = 25;
|
|
11905
10002
|
var DEFAULT_ORPHAN_DRAW_AGE_MS = 15 * 6e4;
|
|
11906
10003
|
var DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS = 30 * 6e4;
|
|
11907
10004
|
var DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
10005
|
+
var DEFAULT_JOB_RETENTION_SWEEP_INTERVAL_MS = 24 * 60 * 6e4;
|
|
10006
|
+
var DEFAULT_JOB_RETENTION_BOOT_DELAY_MS = 3e4;
|
|
10007
|
+
var JOB_RETENTION_SWEEP_BATCH = 50;
|
|
10008
|
+
var JOB_RETENTION_SWEEP_MAX_PAGES = 20;
|
|
10009
|
+
var DAY_MS = 24 * 60 * 6e4;
|
|
11908
10010
|
var ORPHAN_DRAW_SWEEP_BATCH = 50;
|
|
11909
10011
|
var ORPHAN_DRAW_SWEEP_MAX_PAGES = 20;
|
|
11910
10012
|
var ANY_DRAW_AGE_CUTOFF_MS = Number.MAX_SAFE_INTEGER;
|
|
@@ -11912,7 +10014,12 @@ var ORPHAN_SWEEP_CLAIMS = /* @__PURE__ */ new WeakSet();
|
|
|
11912
10014
|
var ATTACH_RAILS = ["cashu", "x402", "tempo"];
|
|
11913
10015
|
var CREDIT_INBOUND_ATTEMPTS = 3;
|
|
11914
10016
|
var CREDIT_INBOUND_BACKOFF_MS = 50;
|
|
10017
|
+
var TERMINAL_COST_HANDOFF_RETRY_INITIAL_MS = 100;
|
|
10018
|
+
var TERMINAL_COST_HANDOFF_RETRY_MAX_MS = 3e4;
|
|
11915
10019
|
var CREDIT_INBOUND_READ_ATTEMPTS = 2;
|
|
10020
|
+
function isTerminalCostRecoveryStore(store) {
|
|
10021
|
+
return "saveForDvm" in store && "listUnreportedTerminalCosts" in store && "markTerminalCostReported" in store;
|
|
10022
|
+
}
|
|
11916
10023
|
var JobManager = class {
|
|
11917
10024
|
descriptor;
|
|
11918
10025
|
opts;
|
|
@@ -11920,13 +10027,15 @@ var JobManager = class {
|
|
|
11920
10027
|
jobStore;
|
|
11921
10028
|
idleTimers = /* @__PURE__ */ new Map();
|
|
11922
10029
|
cleanupTimers = /* @__PURE__ */ new Map();
|
|
10030
|
+
/** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
|
|
10031
|
+
terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
|
|
11923
10032
|
/** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
|
|
11924
10033
|
statusWatchers = /* @__PURE__ */ new Map();
|
|
11925
10034
|
/** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
|
|
11926
10035
|
statusChecksInFlight = /* @__PURE__ */ new Set();
|
|
11927
10036
|
/** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
|
|
11928
10037
|
statusChecksQueued = /* @__PURE__ */ new Set();
|
|
11929
|
-
sessionId =
|
|
10038
|
+
sessionId = randomBytes4(3).toString("hex");
|
|
11930
10039
|
nextJobId = 1;
|
|
11931
10040
|
accumulatorMonitor;
|
|
11932
10041
|
staleJobTimeoutMs;
|
|
@@ -11943,10 +10052,14 @@ var JobManager = class {
|
|
|
11943
10052
|
staleSweepTimer;
|
|
11944
10053
|
heartbeatTimer;
|
|
11945
10054
|
orphanDrawSweepTimer;
|
|
10055
|
+
terminalCostRecoveryTimer;
|
|
10056
|
+
jobRetentionSweepTimer;
|
|
10057
|
+
jobRetentionBootTimer;
|
|
11946
10058
|
/** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
|
|
11947
10059
|
orphanSweepClaimed = false;
|
|
11948
10060
|
/** Where a page-capped tick left off; cleared once a tick reaches the end. */
|
|
11949
10061
|
orphanSweepResumeFrom;
|
|
10062
|
+
jobRetentionResumeFrom;
|
|
11950
10063
|
/**
|
|
11951
10064
|
* A `Date.now()` reading that cannot regress within this manager's lifetime
|
|
11952
10065
|
* (internal-review). Anchored at construction, so a manager already running when
|
|
@@ -11957,6 +10070,9 @@ var JobManager = class {
|
|
|
11957
10070
|
monotonicNowMs = createMonotonicClock();
|
|
11958
10071
|
sweepInFlight = false;
|
|
11959
10072
|
orphanSweepInFlight = false;
|
|
10073
|
+
terminalCostRecoveryInFlight = false;
|
|
10074
|
+
terminalCostRecoveryResumeAfter;
|
|
10075
|
+
jobRetentionSweepInFlight = false;
|
|
11960
10076
|
heartbeatInFlight = false;
|
|
11961
10077
|
/**
|
|
11962
10078
|
* The denomination of every credit this DVM opens (internal-review). Resolved once,
|
|
@@ -12055,6 +10171,45 @@ var JobManager = class {
|
|
|
12055
10171
|
}, orphanIntervalMs);
|
|
12056
10172
|
this.orphanDrawSweepTimer.unref();
|
|
12057
10173
|
}
|
|
10174
|
+
if (descriptor.jobRetentionDays > 0) {
|
|
10175
|
+
if (!isJobRetentionStore(this.jobStore)) {
|
|
10176
|
+
console.warn(
|
|
10177
|
+
JSON.stringify({
|
|
10178
|
+
level: "warn",
|
|
10179
|
+
event: "job_retention_store_unsupported",
|
|
10180
|
+
dvm: descriptor.name,
|
|
10181
|
+
retentionDays: descriptor.jobRetentionDays
|
|
10182
|
+
})
|
|
10183
|
+
);
|
|
10184
|
+
} else {
|
|
10185
|
+
const pass = () => {
|
|
10186
|
+
void this.sweepJobRetention().catch(() => void 0);
|
|
10187
|
+
};
|
|
10188
|
+
const bootDelayMs = opts.jobRetentionSweepBootDelayMs ?? DEFAULT_JOB_RETENTION_BOOT_DELAY_MS;
|
|
10189
|
+
if (bootDelayMs > 0) {
|
|
10190
|
+
this.jobRetentionBootTimer = setTimeout(pass, bootDelayMs);
|
|
10191
|
+
this.jobRetentionBootTimer.unref();
|
|
10192
|
+
}
|
|
10193
|
+
const intervalMs = opts.jobRetentionSweepIntervalMs ?? DEFAULT_JOB_RETENTION_SWEEP_INTERVAL_MS;
|
|
10194
|
+
if (intervalMs > 0) {
|
|
10195
|
+
this.jobRetentionSweepTimer = setInterval(pass, intervalMs);
|
|
10196
|
+
this.jobRetentionSweepTimer.unref();
|
|
10197
|
+
}
|
|
10198
|
+
}
|
|
10199
|
+
}
|
|
10200
|
+
if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10201
|
+
this.terminalCostRecoveryTimer = setInterval(() => {
|
|
10202
|
+
void this.recoverTerminalCosts().catch((err) => {
|
|
10203
|
+
console.error(
|
|
10204
|
+
JSON.stringify({
|
|
10205
|
+
level: "terminal_cost_recovery_failed",
|
|
10206
|
+
error: err instanceof Error ? err.message : String(err)
|
|
10207
|
+
})
|
|
10208
|
+
);
|
|
10209
|
+
});
|
|
10210
|
+
}, TERMINAL_COST_RECOVERY_INTERVAL_MS);
|
|
10211
|
+
this.terminalCostRecoveryTimer.unref();
|
|
10212
|
+
}
|
|
12058
10213
|
}
|
|
12059
10214
|
/** Active in-memory jobs. */
|
|
12060
10215
|
get activeJobs() {
|
|
@@ -12354,7 +10509,11 @@ var JobManager = class {
|
|
|
12354
10509
|
record.seq = Math.max(record.seq, highSeq);
|
|
12355
10510
|
}
|
|
12356
10511
|
}
|
|
12357
|
-
|
|
10512
|
+
if (this.opts.dvmId && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10513
|
+
await this.jobStore.saveForDvm(record, this.opts.dvmId);
|
|
10514
|
+
} else {
|
|
10515
|
+
await this.jobStore.save(record);
|
|
10516
|
+
}
|
|
12358
10517
|
}
|
|
12359
10518
|
/**
|
|
12360
10519
|
* Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
|
|
@@ -12395,6 +10554,7 @@ var JobManager = class {
|
|
|
12395
10554
|
job.pendingPrompts.clear();
|
|
12396
10555
|
job.pendingPayment = null;
|
|
12397
10556
|
abortJob(job, reason);
|
|
10557
|
+
const terminalCostPersisted = durable.status === "completed" ? true : await this.tryPersistAdoptedTerminalCost(job, 1);
|
|
12398
10558
|
if (durable.status !== "completed") {
|
|
12399
10559
|
localMessage(job, "provider", "cancel", { reason });
|
|
12400
10560
|
}
|
|
@@ -12409,8 +10569,78 @@ var JobManager = class {
|
|
|
12409
10569
|
this.clearIdleTimer(job.id);
|
|
12410
10570
|
void this.issueReceiptForStoredJob(job.id);
|
|
12411
10571
|
this.scheduleCleanup(job);
|
|
10572
|
+
if (durable.status !== "completed" && terminalCostPersisted) {
|
|
10573
|
+
await this.reportJobCost(job);
|
|
10574
|
+
}
|
|
12412
10575
|
return true;
|
|
12413
10576
|
}
|
|
10577
|
+
/**
|
|
10578
|
+
* Persist an adopted terminal cost, retaining the in-memory revision for a
|
|
10579
|
+
* later retry when the durable store is transiently unavailable.
|
|
10580
|
+
*/
|
|
10581
|
+
async tryPersistAdoptedTerminalCost(job, attempt) {
|
|
10582
|
+
try {
|
|
10583
|
+
await this.persistAdoptedTerminalCost(job);
|
|
10584
|
+
return true;
|
|
10585
|
+
} catch (err) {
|
|
10586
|
+
this.scheduleTerminalCostHandoffRetry(job, attempt, err);
|
|
10587
|
+
return false;
|
|
10588
|
+
}
|
|
10589
|
+
}
|
|
10590
|
+
/** Retry outside the status watcher, which correctly stops once the job is terminal. */
|
|
10591
|
+
scheduleTerminalCostHandoffRetry(job, attempt, err) {
|
|
10592
|
+
if (this.terminalCostHandoffRetryTimers.has(job.id)) return;
|
|
10593
|
+
const delayMs = Math.min(
|
|
10594
|
+
TERMINAL_COST_HANDOFF_RETRY_INITIAL_MS * 2 ** Math.min(attempt - 1, 8),
|
|
10595
|
+
TERMINAL_COST_HANDOFF_RETRY_MAX_MS
|
|
10596
|
+
);
|
|
10597
|
+
console.error(
|
|
10598
|
+
JSON.stringify({
|
|
10599
|
+
level: "terminal_cost_handoff_retry_scheduled",
|
|
10600
|
+
jobId: job.id,
|
|
10601
|
+
attempt,
|
|
10602
|
+
delayMs,
|
|
10603
|
+
error: err instanceof Error ? err.message : String(err)
|
|
10604
|
+
})
|
|
10605
|
+
);
|
|
10606
|
+
const timer = setTimeout(() => {
|
|
10607
|
+
this.terminalCostHandoffRetryTimers.delete(job.id);
|
|
10608
|
+
void this.retryTerminalCostHandoff(job, attempt + 1);
|
|
10609
|
+
}, delayMs);
|
|
10610
|
+
timer.unref();
|
|
10611
|
+
this.terminalCostHandoffRetryTimers.set(job.id, timer);
|
|
10612
|
+
}
|
|
10613
|
+
async retryTerminalCostHandoff(job, attempt) {
|
|
10614
|
+
if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
|
|
10615
|
+
await this.reportJobCost(job);
|
|
10616
|
+
}
|
|
10617
|
+
/** Durably merge a terminal handler owner's latest cost before reporting it. */
|
|
10618
|
+
async persistAdoptedTerminalCost(job) {
|
|
10619
|
+
const dvmId = this.opts.dvmId;
|
|
10620
|
+
const localRevision = job.costRevision;
|
|
10621
|
+
if (localRevision === void 0 || !dvmId || !isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10622
|
+
return;
|
|
10623
|
+
}
|
|
10624
|
+
const durable = await this.jobStore.get(job.id);
|
|
10625
|
+
if (!durable || durable.status !== job.status) {
|
|
10626
|
+
throw new Error(`Terminal cost merge lost durable job ${job.id}`);
|
|
10627
|
+
}
|
|
10628
|
+
if ((durable.costRevision ?? -1) > localRevision) {
|
|
10629
|
+
job.costAmountMicro = durable.costAmountMicro;
|
|
10630
|
+
job.costCurrency = durable.costCurrency;
|
|
10631
|
+
job.costRevision = durable.costRevision;
|
|
10632
|
+
return;
|
|
10633
|
+
}
|
|
10634
|
+
await this.jobStore.saveForDvm(
|
|
10635
|
+
{
|
|
10636
|
+
...durable,
|
|
10637
|
+
costAmountMicro: job.costAmountMicro,
|
|
10638
|
+
costCurrency: job.costCurrency,
|
|
10639
|
+
costRevision: localRevision
|
|
10640
|
+
},
|
|
10641
|
+
dvmId
|
|
10642
|
+
);
|
|
10643
|
+
}
|
|
12414
10644
|
/**
|
|
12415
10645
|
* Watch the durable status of a locally-active job (internal-review).
|
|
12416
10646
|
*
|
|
@@ -12686,7 +10916,7 @@ var JobManager = class {
|
|
|
12686
10916
|
);
|
|
12687
10917
|
if (claimed) {
|
|
12688
10918
|
swept++;
|
|
12689
|
-
await this.
|
|
10919
|
+
await this.finalizeStoredTerminal(record.id);
|
|
12690
10920
|
console.warn(
|
|
12691
10921
|
JSON.stringify({
|
|
12692
10922
|
level: awaiting ? "stale_job_cancelled" : "stale_job_failed",
|
|
@@ -12796,6 +11026,171 @@ var JobManager = class {
|
|
|
12796
11026
|
if (!heartbeated || this.heartbeatIntervalMs <= 0) return halfWindow;
|
|
12797
11027
|
return Math.max(0, Math.min(halfWindow, windowMs - 2 * this.heartbeatIntervalMs));
|
|
12798
11028
|
}
|
|
11029
|
+
// ── Terminal job content retention ───────────────────────────────────
|
|
11030
|
+
/**
|
|
11031
|
+
* Redact terminal job content beyond this DVM's configured window.
|
|
11032
|
+
*
|
|
11033
|
+
* The scan advances over every candidate, including rows that fail draw
|
|
11034
|
+
* resolution, so one money-path fault cannot starve everything behind it.
|
|
11035
|
+
* Any failed row remains unredacted and the next tick restarts from the head;
|
|
11036
|
+
* a page-capped clean pass instead retains its cursor and resumes at the
|
|
11037
|
+
* tail. Failure and recovery callbacks are awaited but never allowed to
|
|
11038
|
+
* change the sweep result.
|
|
11039
|
+
*/
|
|
11040
|
+
async sweepJobRetention() {
|
|
11041
|
+
const idle = { examined: 0, redacted: 0, complete: false };
|
|
11042
|
+
if (this.descriptor.jobRetentionDays <= 0 || !isJobRetentionStore(this.jobStore)) return idle;
|
|
11043
|
+
if (this.jobRetentionSweepInFlight) return idle;
|
|
11044
|
+
this.jobRetentionSweepInFlight = true;
|
|
11045
|
+
let examined = 0;
|
|
11046
|
+
let redacted = 0;
|
|
11047
|
+
let complete = false;
|
|
11048
|
+
let cursor = this.jobRetentionResumeFrom;
|
|
11049
|
+
const failures = [];
|
|
11050
|
+
const now = Date.now();
|
|
11051
|
+
const lastActivityBeforeMs = now - this.descriptor.jobRetentionDays * DAY_MS;
|
|
11052
|
+
try {
|
|
11053
|
+
for (let page = 0; page < JOB_RETENTION_SWEEP_MAX_PAGES; page++) {
|
|
11054
|
+
const candidates = await this.jobStore.listJobsForRetention({
|
|
11055
|
+
lastActivityBeforeMs,
|
|
11056
|
+
limit: JOB_RETENTION_SWEEP_BATCH,
|
|
11057
|
+
capabilities: this.capabilityNames(),
|
|
11058
|
+
after: cursor
|
|
11059
|
+
});
|
|
11060
|
+
for (const record of candidates) {
|
|
11061
|
+
examined++;
|
|
11062
|
+
cursor = { lastActivityAt: record.lastActivityAt, id: record.id };
|
|
11063
|
+
try {
|
|
11064
|
+
await this.prepareJobForRedaction(record, now);
|
|
11065
|
+
const changed = await this.jobStore.redactJob({
|
|
11066
|
+
jobId: record.id,
|
|
11067
|
+
expectedLastActivityAt: record.lastActivityAt,
|
|
11068
|
+
redactedAt: now
|
|
11069
|
+
});
|
|
11070
|
+
if (changed) redacted++;
|
|
11071
|
+
} catch (err) {
|
|
11072
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
11073
|
+
failures.push(failure);
|
|
11074
|
+
console.error(
|
|
11075
|
+
JSON.stringify({
|
|
11076
|
+
level: "error",
|
|
11077
|
+
event: "job_retention_candidate_failed",
|
|
11078
|
+
jobId: record.id,
|
|
11079
|
+
error: failure.message
|
|
11080
|
+
})
|
|
11081
|
+
);
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
if (candidates.length < JOB_RETENTION_SWEEP_BATCH) {
|
|
11085
|
+
complete = true;
|
|
11086
|
+
break;
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11089
|
+
if (failures.length > 0) {
|
|
11090
|
+
this.jobRetentionResumeFrom = void 0;
|
|
11091
|
+
throw new AggregateError(failures, `${failures.length} job retention candidate(s) failed`);
|
|
11092
|
+
}
|
|
11093
|
+
this.jobRetentionResumeFrom = complete ? void 0 : cursor;
|
|
11094
|
+
console.log(
|
|
11095
|
+
JSON.stringify({
|
|
11096
|
+
level: "info",
|
|
11097
|
+
event: "job_retention_swept",
|
|
11098
|
+
retentionDays: this.descriptor.jobRetentionDays,
|
|
11099
|
+
cutoffMs: lastActivityBeforeMs,
|
|
11100
|
+
examined,
|
|
11101
|
+
redacted,
|
|
11102
|
+
complete
|
|
11103
|
+
})
|
|
11104
|
+
);
|
|
11105
|
+
if (complete) {
|
|
11106
|
+
await this.notifyJobRetentionRecovered({ examined, redacted });
|
|
11107
|
+
}
|
|
11108
|
+
return { examined, redacted, complete };
|
|
11109
|
+
} catch (err) {
|
|
11110
|
+
this.jobRetentionResumeFrom = void 0;
|
|
11111
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
11112
|
+
console.error(
|
|
11113
|
+
JSON.stringify({
|
|
11114
|
+
level: "error",
|
|
11115
|
+
event: "job_retention_sweep_failed",
|
|
11116
|
+
retentionDays: this.descriptor.jobRetentionDays,
|
|
11117
|
+
examined,
|
|
11118
|
+
redacted,
|
|
11119
|
+
error
|
|
11120
|
+
})
|
|
11121
|
+
);
|
|
11122
|
+
await this.notifyJobRetentionFailed(error);
|
|
11123
|
+
throw err;
|
|
11124
|
+
} finally {
|
|
11125
|
+
this.jobRetentionSweepInFlight = false;
|
|
11126
|
+
}
|
|
11127
|
+
}
|
|
11128
|
+
/** Resolve every hold and persist any configured receipt before content disappears. */
|
|
11129
|
+
async prepareJobForRedaction(record, nowMs) {
|
|
11130
|
+
const ledger = this.opts.creditLedger;
|
|
11131
|
+
if (!ledger) {
|
|
11132
|
+
if (record.creditId || record.drawId) {
|
|
11133
|
+
throw new Error("job carries a credit draw but no ledger is configured");
|
|
11134
|
+
}
|
|
11135
|
+
} else {
|
|
11136
|
+
const draws = await ledger.listDrawsByJobId(record.id);
|
|
11137
|
+
for (const draw of draws) {
|
|
11138
|
+
if (draw.status !== "pending") continue;
|
|
11139
|
+
await this.reconcileTerminalDraw(ledger, draw, record, nowMs);
|
|
11140
|
+
}
|
|
11141
|
+
const stillPending = (await ledger.listDrawsByJobId(record.id)).filter(
|
|
11142
|
+
(draw) => draw.status === "pending"
|
|
11143
|
+
);
|
|
11144
|
+
if (stillPending.length > 0) {
|
|
11145
|
+
throw new Error(
|
|
11146
|
+
`job still has ${stillPending.length} pending credit draw(s): ${stillPending.map((draw) => draw.drawId).join(", ")}`
|
|
11147
|
+
);
|
|
11148
|
+
}
|
|
11149
|
+
}
|
|
11150
|
+
if (!record.receipt && this.opts.receiptIssuer && isReceiptIssuingStore(this.jobStore) && !await this.issueReceipt(record)) {
|
|
11151
|
+
throw new Error("signed receipt could not be persisted");
|
|
11152
|
+
}
|
|
11153
|
+
}
|
|
11154
|
+
async notifyJobRetentionFailed(error) {
|
|
11155
|
+
const callback = this.opts.onJobRetentionSweepFailed;
|
|
11156
|
+
if (!callback) return;
|
|
11157
|
+
try {
|
|
11158
|
+
await callback({
|
|
11159
|
+
dvmId: this.opts.dvmId ?? "",
|
|
11160
|
+
dvmName: this.descriptor.name,
|
|
11161
|
+
retentionDays: this.descriptor.jobRetentionDays,
|
|
11162
|
+
error
|
|
11163
|
+
});
|
|
11164
|
+
} catch (callbackError) {
|
|
11165
|
+
console.error(
|
|
11166
|
+
JSON.stringify({
|
|
11167
|
+
level: "error",
|
|
11168
|
+
event: "job_retention_alert_failed",
|
|
11169
|
+
error: callbackError instanceof Error ? callbackError.message : String(callbackError)
|
|
11170
|
+
})
|
|
11171
|
+
);
|
|
11172
|
+
}
|
|
11173
|
+
}
|
|
11174
|
+
async notifyJobRetentionRecovered(result) {
|
|
11175
|
+
const callback = this.opts.onJobRetentionSweepRecovered;
|
|
11176
|
+
if (!callback) return;
|
|
11177
|
+
try {
|
|
11178
|
+
await callback({
|
|
11179
|
+
dvmId: this.opts.dvmId ?? "",
|
|
11180
|
+
dvmName: this.descriptor.name,
|
|
11181
|
+
retentionDays: this.descriptor.jobRetentionDays,
|
|
11182
|
+
...result
|
|
11183
|
+
});
|
|
11184
|
+
} catch (callbackError) {
|
|
11185
|
+
console.error(
|
|
11186
|
+
JSON.stringify({
|
|
11187
|
+
level: "error",
|
|
11188
|
+
event: "job_retention_recovery_alert_failed",
|
|
11189
|
+
error: callbackError instanceof Error ? callbackError.message : String(callbackError)
|
|
11190
|
+
})
|
|
11191
|
+
);
|
|
11192
|
+
}
|
|
11193
|
+
}
|
|
12799
11194
|
// ── Orphan-draw sweeper (internal-review) + terminal reconciler (internal-review) ──
|
|
12800
11195
|
/**
|
|
12801
11196
|
* Resolve `pending` credit draws the terminal funnel can no longer reach.
|
|
@@ -13294,6 +11689,7 @@ var JobManager = class {
|
|
|
13294
11689
|
{
|
|
13295
11690
|
mints: this.opts.mints,
|
|
13296
11691
|
x402Config: this.opts.x402,
|
|
11692
|
+
pricingCurrency: this.pricingCurrency,
|
|
13297
11693
|
x402ExactSettlement: this.opts.x402ExactSettlement,
|
|
13298
11694
|
x402ExactVersions: this.opts.x402FacilitatorHealth?.support().exact,
|
|
13299
11695
|
mpp: this.opts.mpp,
|
|
@@ -13892,6 +12288,7 @@ var JobManager = class {
|
|
|
13892
12288
|
async finalizeTerminal(job) {
|
|
13893
12289
|
await this.attachReceipt(job);
|
|
13894
12290
|
await this.persistJob(job).catch(() => void 0);
|
|
12291
|
+
await this.reportJobCost(job);
|
|
13895
12292
|
}
|
|
13896
12293
|
/**
|
|
13897
12294
|
* Issue a receipt for a job this process doesn't hold in `activeJobs` — a
|
|
@@ -13908,13 +12305,117 @@ var JobManager = class {
|
|
|
13908
12305
|
if (active && receipt) active.receipt = receipt;
|
|
13909
12306
|
return receipt;
|
|
13910
12307
|
}
|
|
12308
|
+
/**
|
|
12309
|
+
* Finish accounting for a terminal written directly to the store — the
|
|
12310
|
+
* stale reaper and a cancel handled on a different machine (internal-review).
|
|
12311
|
+
*/
|
|
12312
|
+
async finalizeStoredTerminal(jobId) {
|
|
12313
|
+
const receipt = await this.issueReceiptForStoredJob(jobId);
|
|
12314
|
+
const record = await this.jobStore.get(jobId).catch(() => void 0);
|
|
12315
|
+
if (record && isTerminal(record.status)) await this.reportJobCost(record);
|
|
12316
|
+
return receipt;
|
|
12317
|
+
}
|
|
13911
12318
|
/** Handle job reaching terminal state (completed, failed, cancelled). */
|
|
13912
12319
|
async handleJobTerminal(job) {
|
|
13913
12320
|
this.clearIdleTimer(job.id);
|
|
13914
12321
|
await this.attachReceipt(job);
|
|
13915
|
-
await this.bookRevenue(job);
|
|
12322
|
+
const booked = await this.bookRevenue(job);
|
|
12323
|
+
if (!booked) await this.reportJobCost(job);
|
|
13916
12324
|
this.scheduleCleanup(job);
|
|
13917
12325
|
}
|
|
12326
|
+
/**
|
|
12327
|
+
* Re-enqueue terminal cost-only rows left between the job commit and outbox
|
|
12328
|
+
* insert. The marker advances only after `onJobCost` resolves, which for the
|
|
12329
|
+
* SDK reporter means the local durable insert has committed; a crash after
|
|
12330
|
+
* that insert but before the marker can duplicate a payload, but the
|
|
12331
|
+
* revisioned platform contract makes that replay harmless.
|
|
12332
|
+
*/
|
|
12333
|
+
async recoverTerminalCosts() {
|
|
12334
|
+
const store = this.jobStore;
|
|
12335
|
+
const dvmId = this.opts.dvmId;
|
|
12336
|
+
if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
|
|
12337
|
+
return { examined: 0, queued: 0, complete: true };
|
|
12338
|
+
}
|
|
12339
|
+
if (this.terminalCostRecoveryInFlight) {
|
|
12340
|
+
return { examined: 0, queued: 0, complete: false };
|
|
12341
|
+
}
|
|
12342
|
+
this.terminalCostRecoveryInFlight = true;
|
|
12343
|
+
let examined = 0;
|
|
12344
|
+
let queued = 0;
|
|
12345
|
+
let cursor = this.terminalCostRecoveryResumeAfter;
|
|
12346
|
+
try {
|
|
12347
|
+
for (let page = 0; page < TERMINAL_COST_RECOVERY_MAX_PAGES; page++) {
|
|
12348
|
+
const candidates = await store.listUnreportedTerminalCosts({
|
|
12349
|
+
dvmId,
|
|
12350
|
+
limit: TERMINAL_COST_RECOVERY_BATCH,
|
|
12351
|
+
...cursor ? { afterJobId: cursor } : {}
|
|
12352
|
+
});
|
|
12353
|
+
for (const record of candidates) {
|
|
12354
|
+
examined++;
|
|
12355
|
+
cursor = record.id;
|
|
12356
|
+
if (await this.reportJobCost(record)) queued++;
|
|
12357
|
+
}
|
|
12358
|
+
if (candidates.length < TERMINAL_COST_RECOVERY_BATCH) {
|
|
12359
|
+
this.terminalCostRecoveryResumeAfter = void 0;
|
|
12360
|
+
if (examined > 0) {
|
|
12361
|
+
console.log(
|
|
12362
|
+
JSON.stringify({
|
|
12363
|
+
level: "info",
|
|
12364
|
+
event: "terminal_cost_recovery_complete",
|
|
12365
|
+
dvmId,
|
|
12366
|
+
examined,
|
|
12367
|
+
queued
|
|
12368
|
+
})
|
|
12369
|
+
);
|
|
12370
|
+
}
|
|
12371
|
+
return { examined, queued, complete: true };
|
|
12372
|
+
}
|
|
12373
|
+
}
|
|
12374
|
+
this.terminalCostRecoveryResumeAfter = cursor;
|
|
12375
|
+
console.warn(
|
|
12376
|
+
JSON.stringify({
|
|
12377
|
+
level: "terminal_cost_recovery_truncated",
|
|
12378
|
+
dvmId,
|
|
12379
|
+
examined,
|
|
12380
|
+
queued,
|
|
12381
|
+
resumeAfterJobId: cursor
|
|
12382
|
+
})
|
|
12383
|
+
);
|
|
12384
|
+
return { examined, queued, complete: false };
|
|
12385
|
+
} finally {
|
|
12386
|
+
this.terminalCostRecoveryInFlight = false;
|
|
12387
|
+
}
|
|
12388
|
+
}
|
|
12389
|
+
/** Report the latest declared-cost state only when no revenue event carried it. */
|
|
12390
|
+
async reportJobCost(job) {
|
|
12391
|
+
const onJobCost = this.opts.onJobCost;
|
|
12392
|
+
if (!onJobCost || job.status !== "completed" && job.status !== "failed" && job.status !== "cancelled") {
|
|
12393
|
+
return false;
|
|
12394
|
+
}
|
|
12395
|
+
const costRevision = job.costRevision ?? (job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? 0 : void 0);
|
|
12396
|
+
if (costRevision === void 0) return false;
|
|
12397
|
+
const payload = {
|
|
12398
|
+
dvmId: this.opts.dvmId ?? "",
|
|
12399
|
+
jobId: job.id,
|
|
12400
|
+
...job.capability ? { capability: job.capability } : {},
|
|
12401
|
+
outcome: job.status,
|
|
12402
|
+
costRevision,
|
|
12403
|
+
cost: job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { amountMicro: job.costAmountMicro, currency: job.costCurrency } : null
|
|
12404
|
+
};
|
|
12405
|
+
try {
|
|
12406
|
+
await onJobCost(payload);
|
|
12407
|
+
if (isTerminalCostRecoveryStore(this.jobStore)) {
|
|
12408
|
+
const marked = await this.jobStore.markTerminalCostReported(job.id, costRevision);
|
|
12409
|
+
if (!marked) {
|
|
12410
|
+
return false;
|
|
12411
|
+
}
|
|
12412
|
+
}
|
|
12413
|
+
} catch (err) {
|
|
12414
|
+
console.error(`[${job.id}] Job cost reporting failed:`, err);
|
|
12415
|
+
return false;
|
|
12416
|
+
}
|
|
12417
|
+
return true;
|
|
12418
|
+
}
|
|
13918
12419
|
/**
|
|
13919
12420
|
* Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
|
|
13920
12421
|
* Fire-and-forget — the `RevenueReporter` owns persistence and retry.
|
|
@@ -14053,6 +12554,9 @@ var JobManager = class {
|
|
|
14053
12554
|
onJobCompleted({
|
|
14054
12555
|
dvmId: this.opts.dvmId ?? "",
|
|
14055
12556
|
jobId: job.id,
|
|
12557
|
+
// Legacy Postgres rows surface a missing capability as `""`. Keep
|
|
12558
|
+
// that absence distinct from every real capability name on the wire.
|
|
12559
|
+
...job.capability ? { capability: job.capability } : {},
|
|
14056
12560
|
paidMsats: bookedMsats,
|
|
14057
12561
|
paymentMint: job.paymentMint,
|
|
14058
12562
|
rail: job.paymentRail,
|
|
@@ -14060,6 +12564,11 @@ var JobManager = class {
|
|
|
14060
12564
|
nativeAmount: job.nativeAmount,
|
|
14061
12565
|
nativeAsset: job.nativeAsset,
|
|
14062
12566
|
cashuFlow: job.cashuFlow,
|
|
12567
|
+
// Absent unless the handler declared one, and absent is a fact: the
|
|
12568
|
+
// platform shows "no costs reported" rather than a zero cost and a
|
|
12569
|
+
// 100% margin (internal-review). Both halves or neither — a bare amount
|
|
12570
|
+
// in an unnamed currency is not a cost.
|
|
12571
|
+
...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
|
|
14063
12572
|
...credit
|
|
14064
12573
|
})
|
|
14065
12574
|
).catch((err) => {
|
|
@@ -14086,6 +12595,10 @@ var JobManager = class {
|
|
|
14086
12595
|
clearTimeout(timer);
|
|
14087
12596
|
this.cleanupTimers.delete(id);
|
|
14088
12597
|
}
|
|
12598
|
+
for (const [id, timer] of this.terminalCostHandoffRetryTimers) {
|
|
12599
|
+
clearTimeout(timer);
|
|
12600
|
+
this.terminalCostHandoffRetryTimers.delete(id);
|
|
12601
|
+
}
|
|
14089
12602
|
for (const id of [...this.statusWatchers.keys()]) {
|
|
14090
12603
|
this.unwatchDurableStatus(id);
|
|
14091
12604
|
}
|
|
@@ -14105,6 +12618,18 @@ var JobManager = class {
|
|
|
14105
12618
|
clearInterval(this.orphanDrawSweepTimer);
|
|
14106
12619
|
this.orphanDrawSweepTimer = void 0;
|
|
14107
12620
|
}
|
|
12621
|
+
if (this.terminalCostRecoveryTimer) {
|
|
12622
|
+
clearInterval(this.terminalCostRecoveryTimer);
|
|
12623
|
+
this.terminalCostRecoveryTimer = void 0;
|
|
12624
|
+
}
|
|
12625
|
+
if (this.jobRetentionBootTimer) {
|
|
12626
|
+
clearTimeout(this.jobRetentionBootTimer);
|
|
12627
|
+
this.jobRetentionBootTimer = void 0;
|
|
12628
|
+
}
|
|
12629
|
+
if (this.jobRetentionSweepTimer) {
|
|
12630
|
+
clearInterval(this.jobRetentionSweepTimer);
|
|
12631
|
+
this.jobRetentionSweepTimer = void 0;
|
|
12632
|
+
}
|
|
14108
12633
|
if (this.orphanSweepClaimed && this.opts.creditLedger) {
|
|
14109
12634
|
ORPHAN_SWEEP_CLAIMS.delete(this.opts.creditLedger);
|
|
14110
12635
|
this.orphanSweepClaimed = false;
|
|
@@ -15224,7 +13749,7 @@ function withWallClockDeadline(promise, timeoutMs) {
|
|
|
15224
13749
|
}
|
|
15225
13750
|
|
|
15226
13751
|
// src/sdk/server/app.ts
|
|
15227
|
-
import { createHash as
|
|
13752
|
+
import { createHash as createHash7, randomBytes as randomBytes5, randomUUID as randomUUID4 } from "crypto";
|
|
15228
13753
|
import { Hono } from "hono";
|
|
15229
13754
|
import { getCookie } from "hono/cookie";
|
|
15230
13755
|
import { cors } from "hono/cors";
|
|
@@ -15246,7 +13771,7 @@ import {
|
|
|
15246
13771
|
Amount as Amount3,
|
|
15247
13772
|
CheckStateEnum as CheckStateEnum3,
|
|
15248
13773
|
getP2PKExpectedWitnessPubkeys as getP2PKExpectedWitnessPubkeys2,
|
|
15249
|
-
getTokenMetadata
|
|
13774
|
+
getTokenMetadata
|
|
15250
13775
|
} from "@cashu/cashu-ts";
|
|
15251
13776
|
function installAdminCreditRoutes(app, opts) {
|
|
15252
13777
|
app.get("/admin/credit/pending-drains", (c) => handlePendingDrains(c, opts));
|
|
@@ -15522,6 +14047,15 @@ async function handleBitcoinLiability(c, opts) {
|
|
|
15522
14047
|
async function handleParkDrain(c, opts) {
|
|
15523
14048
|
const auth = await requireAuth(c, opts);
|
|
15524
14049
|
if ("response" in auth) return auth.response;
|
|
14050
|
+
if (!opts.accumulatorWired) {
|
|
14051
|
+
return c.json(
|
|
14052
|
+
{
|
|
14053
|
+
error: "accumulator_not_wired",
|
|
14054
|
+
message: "Cashu accumulator persistence is not configured on this DVM."
|
|
14055
|
+
},
|
|
14056
|
+
503
|
|
14057
|
+
);
|
|
14058
|
+
}
|
|
15525
14059
|
let body;
|
|
15526
14060
|
try {
|
|
15527
14061
|
body = await c.req.json();
|
|
@@ -15609,7 +14143,7 @@ async function handleParkDrain(c, opts) {
|
|
|
15609
14143
|
}
|
|
15610
14144
|
let parsed;
|
|
15611
14145
|
try {
|
|
15612
|
-
const meta =
|
|
14146
|
+
const meta = getTokenMetadata(token);
|
|
15613
14147
|
parsed = {
|
|
15614
14148
|
mint: meta.mint,
|
|
15615
14149
|
faceSats: Amount3.from(meta.amount).toNumber(),
|
|
@@ -15956,6 +14490,7 @@ async function handleReconcileInvoice(c, opts) {
|
|
|
15956
14490
|
payload: {
|
|
15957
14491
|
dvmId: opts.dvmId,
|
|
15958
14492
|
creditId: targetCreditId,
|
|
14493
|
+
callerPubkey: invoice.callerPubkey,
|
|
15959
14494
|
// The payment hash keys the deposit, as it does on the ordinary
|
|
15960
14495
|
// settlement path — so a redelivered report collides rather
|
|
15961
14496
|
// than double-counts.
|
|
@@ -17032,6 +15567,23 @@ function parseSerializedDleq(raw) {
|
|
|
17032
15567
|
return { e: dleq.e, s: dleq.s, ...dleq.r !== void 0 ? { r: dleq.r } : {} };
|
|
17033
15568
|
}
|
|
17034
15569
|
|
|
15570
|
+
// src/sdk/server/caller-facing-url.ts
|
|
15571
|
+
function resolvePublicOrigin(value) {
|
|
15572
|
+
if (!value) return void 0;
|
|
15573
|
+
try {
|
|
15574
|
+
const url = new URL(value);
|
|
15575
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
|
|
15576
|
+
return url.origin;
|
|
15577
|
+
} catch {
|
|
15578
|
+
return void 0;
|
|
15579
|
+
}
|
|
15580
|
+
}
|
|
15581
|
+
function callerFacingRequestUrl(requestUrl, publicOrigin) {
|
|
15582
|
+
if (!publicOrigin) return requestUrl;
|
|
15583
|
+
const request = new URL(requestUrl);
|
|
15584
|
+
return `${publicOrigin}${request.pathname}${request.search}`;
|
|
15585
|
+
}
|
|
15586
|
+
|
|
17035
15587
|
// src/sdk/server/credit-expiry-sweep.ts
|
|
17036
15588
|
function startCreditExpirySweep(ledger, opts = {}) {
|
|
17037
15589
|
const sweepExpiredCredits = ledger.sweepExpiredCredits?.bind(ledger);
|
|
@@ -18005,7 +16557,7 @@ initSchemaForm();
|
|
|
18005
16557
|
}
|
|
18006
16558
|
|
|
18007
16559
|
// src/sdk/server/request-fingerprint.ts
|
|
18008
|
-
import { createHash as
|
|
16560
|
+
import { createHash as createHash6 } from "crypto";
|
|
18009
16561
|
function requestFingerprint(req, opts) {
|
|
18010
16562
|
const canonical = canonicalize({
|
|
18011
16563
|
capability: req.capability,
|
|
@@ -18013,7 +16565,7 @@ function requestFingerprint(req, opts) {
|
|
|
18013
16565
|
params: req.params ?? {},
|
|
18014
16566
|
data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
|
|
18015
16567
|
});
|
|
18016
|
-
return
|
|
16568
|
+
return createHash6("sha256").update(canonical).digest("hex");
|
|
18017
16569
|
}
|
|
18018
16570
|
var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
|
|
18019
16571
|
function stripEnvelope(data) {
|
|
@@ -18062,7 +16614,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
18062
16614
|
let revenueReporter;
|
|
18063
16615
|
let payoutReporter;
|
|
18064
16616
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
18065
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
16617
|
+
const { RevenueReporter } = await import("./revenue-reporter-ASZ7SHHH.js");
|
|
18066
16618
|
const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
|
|
18067
16619
|
await reporter.init();
|
|
18068
16620
|
reporter.startRetryLoop();
|
|
@@ -18088,6 +16640,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
18088
16640
|
hasPgPool: !!opts.db
|
|
18089
16641
|
});
|
|
18090
16642
|
const effectiveOnJobCompleted = opts.onJobCompleted ?? (revenueReporter ? (info) => revenueReporter.report(info) : void 0);
|
|
16643
|
+
const effectiveOnJobCost = opts.onJobCost ?? (revenueReporter ? (info) => revenueReporter.reportCost(info) : void 0);
|
|
18091
16644
|
const effectiveOnPaidJobDeath = opts.onPaidJobDeath ?? (revenueReporter ? (info) => revenueReporter.reportPaidJobDeath(info) : void 0);
|
|
18092
16645
|
const effectiveOnRevenueSkippedNoRail = opts.onRevenueSkippedNoRail ?? (revenueReporter ? (info) => revenueReporter.reportRevenueSkippedNoRail(info) : void 0);
|
|
18093
16646
|
const enqueueCreditDeposit = !opts.onCreditFunded && revenueReporter ? revenueReporter.enqueueDeposit.bind(revenueReporter) : void 0;
|
|
@@ -18207,6 +16760,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
18207
16760
|
creditLedger,
|
|
18208
16761
|
processedPayments,
|
|
18209
16762
|
onJobCompleted: effectiveOnJobCompleted,
|
|
16763
|
+
onJobCost: effectiveOnJobCost,
|
|
18210
16764
|
onPaidJobDeath: effectiveOnPaidJobDeath,
|
|
18211
16765
|
onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
|
|
18212
16766
|
onCreditFunded: opts.onCreditFunded,
|
|
@@ -18218,6 +16772,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
18218
16772
|
x402ExactSettlement,
|
|
18219
16773
|
payoutReporter
|
|
18220
16774
|
});
|
|
16775
|
+
await server.jobManager.recoverTerminalCosts();
|
|
18221
16776
|
return {
|
|
18222
16777
|
app: server.app,
|
|
18223
16778
|
authAudience: server.authAudience,
|
|
@@ -18246,6 +16801,8 @@ var DVMServer = class {
|
|
|
18246
16801
|
mintHealthTracker;
|
|
18247
16802
|
descriptor;
|
|
18248
16803
|
opts;
|
|
16804
|
+
/** Validated `DVMKIT_PUBLIC_URL` origin, or request-derived URLs when absent. */
|
|
16805
|
+
publicOrigin;
|
|
18249
16806
|
consumedCredentialStore;
|
|
18250
16807
|
adminCashuNonceStore;
|
|
18251
16808
|
ownsMintHealthTracker;
|
|
@@ -18289,6 +16846,7 @@ var DVMServer = class {
|
|
|
18289
16846
|
this.descriptor = descriptor;
|
|
18290
16847
|
this.pricingCurrency = descriptor.currency ?? "usd";
|
|
18291
16848
|
this.opts = opts;
|
|
16849
|
+
this.publicOrigin = resolvePublicOrigin(opts.env.DVMKIT_PUBLIC_URL);
|
|
18292
16850
|
this.authAudience = this.resolveAuthAudience();
|
|
18293
16851
|
this.x402BatchSettlement = opts.x402BatchSettlement;
|
|
18294
16852
|
this.x402ExactSettlement = opts.x402ExactSettlement;
|
|
@@ -18345,7 +16903,9 @@ var DVMServer = class {
|
|
|
18345
16903
|
// needs-approval — reachable only from a rung that raises a real mid-job
|
|
18346
16904
|
// ask, which nothing above L1 did. Facilitator availability is still
|
|
18347
16905
|
// re-checked per ask in `context.ts`, so a static list is safe here.
|
|
18348
|
-
paymentMethods: opts.paymentMethods ?? this.defaultPaymentMethods()
|
|
16906
|
+
paymentMethods: (opts.paymentMethods ?? this.defaultPaymentMethods()).filter(
|
|
16907
|
+
(method) => method !== "x402" || this.x402CurrencySupported()
|
|
16908
|
+
),
|
|
18349
16909
|
x402: opts.x402,
|
|
18350
16910
|
x402ExactSettlement: this.x402ExactSettlement,
|
|
18351
16911
|
x402FacilitatorHealth: opts.x402FacilitatorHealth,
|
|
@@ -18368,8 +16928,11 @@ var DVMServer = class {
|
|
|
18368
16928
|
pricingCurrency: this.pricingCurrency,
|
|
18369
16929
|
onCreditFunded: opts.onCreditFunded,
|
|
18370
16930
|
onJobCompleted: opts.onJobCompleted,
|
|
16931
|
+
onJobCost: opts.onJobCost,
|
|
18371
16932
|
onPaidJobDeath: opts.onPaidJobDeath,
|
|
18372
16933
|
onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
|
|
16934
|
+
onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
|
|
16935
|
+
onJobRetentionSweepRecovered: opts.onJobRetentionSweepRecovered,
|
|
18373
16936
|
reactivationClaimGraceMs: opts.reactivationClaimGraceMs
|
|
18374
16937
|
});
|
|
18375
16938
|
this.app = new Hono();
|
|
@@ -18505,20 +17068,23 @@ var DVMServer = class {
|
|
|
18505
17068
|
this.app.post("/v1/job/:id/messages", (c) => this.handlePostMessage(c));
|
|
18506
17069
|
this.app.delete("/v1/job/:id", (c) => this.handleDeleteJob(c));
|
|
18507
17070
|
this.app.post("/v1/credit", (c) => this.handleCredit(c));
|
|
18508
|
-
if (this.opts.
|
|
17071
|
+
if (this.opts.db && this.opts.dvmId && this.opts.lockPubkey) {
|
|
18509
17072
|
const db = this.opts.db;
|
|
18510
17073
|
const dvmId = this.opts.dvmId;
|
|
18511
17074
|
const payoutReporter = this.opts.payoutReporter;
|
|
18512
|
-
|
|
18513
|
-
|
|
18514
|
-
|
|
18515
|
-
|
|
18516
|
-
|
|
18517
|
-
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
|
|
18521
|
-
|
|
17075
|
+
const accumulatorWired = this.opts.cashuMode === "p2pk-accumulator";
|
|
17076
|
+
if (accumulatorWired) {
|
|
17077
|
+
installAdminCashuRoutes(this.app, {
|
|
17078
|
+
db,
|
|
17079
|
+
dvmId,
|
|
17080
|
+
getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
|
|
17081
|
+
graceSeconds: this.graceSeconds(),
|
|
17082
|
+
nonceStore: this.adminCashuNonceStore,
|
|
17083
|
+
// internal-review: a completed melt is a landed payout, queued in the
|
|
17084
|
+
// transaction that marks it.
|
|
17085
|
+
...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
|
|
17086
|
+
});
|
|
17087
|
+
}
|
|
18522
17088
|
installAdminCreditRoutes(this.app, {
|
|
18523
17089
|
db,
|
|
18524
17090
|
dvmId,
|
|
@@ -18526,6 +17092,7 @@ var DVMServer = class {
|
|
|
18526
17092
|
fxFetcher: this.fxFetcher,
|
|
18527
17093
|
getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
|
|
18528
17094
|
nonceStore: this.adminCashuNonceStore,
|
|
17095
|
+
accumulatorWired,
|
|
18529
17096
|
receiptIssuer: this.receiptIssuer,
|
|
18530
17097
|
creditTtlMs: () => this.creditTtlMs(),
|
|
18531
17098
|
creditCurrency: () => this.creditCurrency(),
|
|
@@ -18843,7 +17410,7 @@ var DVMServer = class {
|
|
|
18843
17410
|
const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
|
|
18844
17411
|
return {
|
|
18845
17412
|
dvm_id: dvmId,
|
|
18846
|
-
builder_pubkey:
|
|
17413
|
+
builder_pubkey: createHash7("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
|
|
18847
17414
|
};
|
|
18848
17415
|
}
|
|
18849
17416
|
throw new AuthAudienceError();
|
|
@@ -18899,7 +17466,7 @@ var DVMServer = class {
|
|
|
18899
17466
|
const accumulatorReady = this.opts.cashuMode === "p2pk-accumulator" && !!this.opts.lockPubkey && !!this.opts.db;
|
|
18900
17467
|
if (accumulatorReady) methods.push("cashu");
|
|
18901
17468
|
}
|
|
18902
|
-
if (this.opts.x402) methods.push("x402");
|
|
17469
|
+
if (this.opts.x402 && this.x402CurrencySupported()) methods.push("x402");
|
|
18903
17470
|
if (this.opts.mpp) methods.push("tempo");
|
|
18904
17471
|
return methods;
|
|
18905
17472
|
}
|
|
@@ -18909,6 +17476,10 @@ var DVMServer = class {
|
|
|
18909
17476
|
if (!this.opts.x402.batchSettlement?.selfRelay) return support;
|
|
18910
17477
|
return { ...support, batchSettlement: true };
|
|
18911
17478
|
}
|
|
17479
|
+
/** Whether this DVM can state its price exactly in x402's USDC unit. */
|
|
17480
|
+
x402CurrencySupported() {
|
|
17481
|
+
return this.pricingCurrency === "usd";
|
|
17482
|
+
}
|
|
18912
17483
|
/**
|
|
18913
17484
|
* Settleable rails for this DVM, honouring an explicit `opts.paymentMethods`
|
|
18914
17485
|
* override and otherwise deriving from wired rails. Single source of truth so
|
|
@@ -18916,9 +17487,12 @@ var DVMServer = class {
|
|
|
18916
17487
|
*/
|
|
18917
17488
|
resolvePaymentMethods() {
|
|
18918
17489
|
const configured = this.opts.paymentMethods ?? this.defaultPaymentMethods();
|
|
18919
|
-
|
|
18920
|
-
|
|
18921
|
-
|
|
17490
|
+
const currencyFiltered = configured.filter(
|
|
17491
|
+
(method) => method !== "x402" || this.x402CurrencySupported()
|
|
17492
|
+
);
|
|
17493
|
+
if (!this.opts.x402) return currencyFiltered;
|
|
17494
|
+
const x402Available = this.x402CurrencySupported() && (this.x402Support()?.available ?? false);
|
|
17495
|
+
return currencyFiltered.filter((method) => method !== "x402" || x402Available);
|
|
18922
17496
|
}
|
|
18923
17497
|
/**
|
|
18924
17498
|
* Rails a **credit top-up** may arrive on — the funding menu's list
|
|
@@ -18936,7 +17510,9 @@ var DVMServer = class {
|
|
|
18936
17510
|
const methods = this.resolvePaymentMethods().filter(
|
|
18937
17511
|
(method) => method !== "tempo" || creditMpp
|
|
18938
17512
|
);
|
|
18939
|
-
if (this.x402BatchAvailable() && !methods.includes("x402"))
|
|
17513
|
+
if (this.x402CurrencySupported() && this.x402BatchAvailable() && !methods.includes("x402")) {
|
|
17514
|
+
methods.push("x402");
|
|
17515
|
+
}
|
|
18940
17516
|
if (this.opts.lightningReceive?.available()) methods.push("lightning");
|
|
18941
17517
|
return methods;
|
|
18942
17518
|
}
|
|
@@ -18984,7 +17560,7 @@ var DVMServer = class {
|
|
|
18984
17560
|
creditX402Schemes() {
|
|
18985
17561
|
const x402 = this.opts.x402;
|
|
18986
17562
|
const support = this.x402Support();
|
|
18987
|
-
if (!x402 || !support) return [];
|
|
17563
|
+
if (!this.x402CurrencySupported() || !x402 || !support) return [];
|
|
18988
17564
|
return [
|
|
18989
17565
|
...support.available ? [{ scheme: X402_EXACT_SCHEME, network: x402.network }] : [],
|
|
18990
17566
|
...this.x402BatchAvailable() ? [{ scheme: X402_BATCH_SETTLEMENT_SCHEME, network: x402.network }] : []
|
|
@@ -19026,6 +17602,7 @@ var DVMServer = class {
|
|
|
19026
17602
|
if (replayed || !funding || !credit) continue;
|
|
19027
17603
|
this.reportCreditDeposit({
|
|
19028
17604
|
creditId: invoice.creditId,
|
|
17605
|
+
callerPubkey: credit.callerPubkey,
|
|
19029
17606
|
// The payment hash is Lightning's own settlement reference, so it is
|
|
19030
17607
|
// what keys the deposit — a redelivered report collides rather than
|
|
19031
17608
|
// double-counts.
|
|
@@ -19078,7 +17655,7 @@ var DVMServer = class {
|
|
|
19078
17655
|
}
|
|
19079
17656
|
const x402Support = this.x402Support();
|
|
19080
17657
|
const x402BatchSettlement = await this.resolveX402BatchSettlement();
|
|
19081
|
-
const x402Payment = x402Support?.exact.v2 || x402Support?.batchSettlement ? c.req.header("PAYMENT-SIGNATURE") : void 0;
|
|
17658
|
+
const x402Payment = this.x402CurrencySupported() ? x402Support?.exact.v2 || x402Support?.batchSettlement ? c.req.header("PAYMENT-SIGNATURE") : void 0 : c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT");
|
|
19082
17659
|
return {
|
|
19083
17660
|
cashuToken: c.req.header("X-Cashu"),
|
|
19084
17661
|
mints: this.opts.mints,
|
|
@@ -19089,7 +17666,7 @@ var DVMServer = class {
|
|
|
19089
17666
|
x402ExactSettlement: this.x402ExactSettlement,
|
|
19090
17667
|
x402ExactVersions: x402Support?.exact,
|
|
19091
17668
|
x402BatchSettlement,
|
|
19092
|
-
x402Resource: c.req.url,
|
|
17669
|
+
x402Resource: callerFacingRequestUrl(c.req.url, this.publicOrigin),
|
|
19093
17670
|
mppCredential,
|
|
19094
17671
|
mpp: this.opts.mpp,
|
|
19095
17672
|
resourcePath: c.req.path,
|
|
@@ -19261,7 +17838,7 @@ var DVMServer = class {
|
|
|
19261
17838
|
if (this.opts.mints) info.mints = this.advertisedMints();
|
|
19262
17839
|
info.capabilities = this.renderCapabilities();
|
|
19263
17840
|
info.payment = { methods: this.resolvePaymentMethods() };
|
|
19264
|
-
if (this.opts.x402 && this.x402Support()?.available) {
|
|
17841
|
+
if (this.x402CurrencySupported() && this.opts.x402 && this.x402Support()?.available) {
|
|
19265
17842
|
info.x402 = {
|
|
19266
17843
|
pay_to: this.opts.x402.payTo,
|
|
19267
17844
|
network: this.opts.x402.network,
|
|
@@ -19419,7 +17996,8 @@ var DVMServer = class {
|
|
|
19419
17996
|
500
|
|
19420
17997
|
);
|
|
19421
17998
|
}
|
|
19422
|
-
if (err instanceof SignedRequestError)
|
|
17999
|
+
if (err instanceof SignedRequestError)
|
|
18000
|
+
return buildAuthErrorResponse(c, err, this.publicOrigin);
|
|
19423
18001
|
throw err;
|
|
19424
18002
|
}
|
|
19425
18003
|
}
|
|
@@ -19444,7 +18022,7 @@ var DVMServer = class {
|
|
|
19444
18022
|
{ onError: () => ({ "error.code": "invalid_quote_data" }) }
|
|
19445
18023
|
);
|
|
19446
18024
|
} catch (err) {
|
|
19447
|
-
const schemaUrl = infoSchemaUrl(c.req.url);
|
|
18025
|
+
const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
19448
18026
|
const { message, hint } = err instanceof z2.ZodError ? humanizeZodError(err, { schemaUrl }) : {
|
|
19449
18027
|
message: "Quote-input validation failed",
|
|
19450
18028
|
hint: `Inspect ${schemaUrl} for the expected input schema.`
|
|
@@ -19558,7 +18136,7 @@ var DVMServer = class {
|
|
|
19558
18136
|
*/
|
|
19559
18137
|
requireCapabilityFromBody(c, capability) {
|
|
19560
18138
|
const known = this.jobManager.capabilityNames();
|
|
19561
|
-
const infoUrl =
|
|
18139
|
+
const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
19562
18140
|
if (typeof capability !== "string" || capability.length === 0) {
|
|
19563
18141
|
return {
|
|
19564
18142
|
error: c.json(
|
|
@@ -19599,7 +18177,7 @@ var DVMServer = class {
|
|
|
19599
18177
|
try {
|
|
19600
18178
|
return { body: await c.req.json() };
|
|
19601
18179
|
} catch {
|
|
19602
|
-
const infoUrl =
|
|
18180
|
+
const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
19603
18181
|
return {
|
|
19604
18182
|
error: c.json(
|
|
19605
18183
|
{
|
|
@@ -19634,7 +18212,7 @@ var DVMServer = class {
|
|
|
19634
18212
|
* relayed to a human don't change with the status.
|
|
19635
18213
|
*/
|
|
19636
18214
|
invalidInputResponse(c, err) {
|
|
19637
|
-
const schemaUrl = infoSchemaUrl(c.req.url);
|
|
18215
|
+
const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
19638
18216
|
const issues = zodIssuesOf(err);
|
|
19639
18217
|
if (issues) {
|
|
19640
18218
|
const inputIssues = callerInputIssues(issues);
|
|
@@ -19748,7 +18326,8 @@ var DVMServer = class {
|
|
|
19748
18326
|
500
|
|
19749
18327
|
);
|
|
19750
18328
|
}
|
|
19751
|
-
if (error instanceof SignedRequestError)
|
|
18329
|
+
if (error instanceof SignedRequestError)
|
|
18330
|
+
return buildAuthErrorResponse(c, error, this.publicOrigin);
|
|
19752
18331
|
throw error;
|
|
19753
18332
|
}
|
|
19754
18333
|
const signedRequestId = requestIdFromEnvelope(body.data);
|
|
@@ -19802,6 +18381,7 @@ var DVMServer = class {
|
|
|
19802
18381
|
const capabilityResult = this.requireCapabilityFromBody(c, body.capability);
|
|
19803
18382
|
if ("error" in capabilityResult) return capabilityResult.error;
|
|
19804
18383
|
const capabilityName = capabilityResult.name;
|
|
18384
|
+
pinDimension("capability", capabilityName);
|
|
19805
18385
|
const cap = this.descriptor.capabilities[capabilityName];
|
|
19806
18386
|
let parsedInput;
|
|
19807
18387
|
let deferredInputError;
|
|
@@ -19866,7 +18446,8 @@ var DVMServer = class {
|
|
|
19866
18446
|
500
|
|
19867
18447
|
);
|
|
19868
18448
|
}
|
|
19869
|
-
if (err instanceof SignedRequestError)
|
|
18449
|
+
if (err instanceof SignedRequestError)
|
|
18450
|
+
return buildAuthErrorResponse(c, err, this.publicOrigin);
|
|
19870
18451
|
throw err;
|
|
19871
18452
|
}
|
|
19872
18453
|
}
|
|
@@ -19894,7 +18475,7 @@ var DVMServer = class {
|
|
|
19894
18475
|
requesterId: c.get("requesterId"),
|
|
19895
18476
|
requestFingerprint: fingerprint,
|
|
19896
18477
|
jobId: this.jobManager.allocateJobId(),
|
|
19897
|
-
claimToken:
|
|
18478
|
+
claimToken: randomBytes5(32).toString("hex")
|
|
19898
18479
|
};
|
|
19899
18480
|
publicRequestClaimResult = resumeOnly ? await recoveryStore.resumeRequestId(publicRequestClaim) : await recoveryStore.claimRequestId(publicRequestClaim);
|
|
19900
18481
|
if (!publicRequestClaimResult) {
|
|
@@ -20099,7 +18680,7 @@ var DVMServer = class {
|
|
|
20099
18680
|
} catch (err) {
|
|
20100
18681
|
if (err instanceof SignedRequestError && err.sub_reason === "replay_detected" && exactAdmission?.replayed) {
|
|
20101
18682
|
} else if (err instanceof SignedRequestError) {
|
|
20102
|
-
return buildAuthErrorResponse(c, err);
|
|
18683
|
+
return buildAuthErrorResponse(c, err, this.publicOrigin);
|
|
20103
18684
|
} else {
|
|
20104
18685
|
throw err;
|
|
20105
18686
|
}
|
|
@@ -20121,8 +18702,8 @@ var DVMServer = class {
|
|
|
20121
18702
|
}
|
|
20122
18703
|
}
|
|
20123
18704
|
const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
|
|
20124
|
-
const jobToken = isAnonymous ?
|
|
20125
|
-
const requesterTokenHash = jobToken ?
|
|
18705
|
+
const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
|
|
18706
|
+
const requesterTokenHash = jobToken ? createHash7("sha256").update(jobToken).digest("hex") : void 0;
|
|
20126
18707
|
const job = jm.createJob(
|
|
20127
18708
|
normalisedBody,
|
|
20128
18709
|
requesterId,
|
|
@@ -20657,8 +19238,7 @@ var DVMServer = class {
|
|
|
20657
19238
|
summary,
|
|
20658
19239
|
lastActivityAt: Date.now()
|
|
20659
19240
|
});
|
|
20660
|
-
const
|
|
20661
|
-
const infoUrl = `${baseUrl.origin}/v1/info`;
|
|
19241
|
+
const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
20662
19242
|
return c.json(
|
|
20663
19243
|
{
|
|
20664
19244
|
error: "unknown_capability",
|
|
@@ -20681,7 +19261,7 @@ var DVMServer = class {
|
|
|
20681
19261
|
summary: "Persisted auth envelope failed signature re-verification on reactivation",
|
|
20682
19262
|
lastActivityAt: Date.now()
|
|
20683
19263
|
}).catch(() => void 0);
|
|
20684
|
-
return buildAuthErrorResponse(c, err);
|
|
19264
|
+
return buildAuthErrorResponse(c, err, this.publicOrigin);
|
|
20685
19265
|
}
|
|
20686
19266
|
if (err instanceof ReactivationAuthSchemaError) {
|
|
20687
19267
|
return c.json(
|
|
@@ -20798,13 +19378,13 @@ var DVMServer = class {
|
|
|
20798
19378
|
if (isStreamableJobStore(store)) {
|
|
20799
19379
|
const cancelled = await store.cancelJob(id, reason ?? "Job cancelled");
|
|
20800
19380
|
if (!cancelled) return this.jobNotActiveResponse(c);
|
|
20801
|
-
await jm.
|
|
19381
|
+
await jm.finalizeStoredTerminal(id);
|
|
20802
19382
|
return c.json({ status: "cancelled" });
|
|
20803
19383
|
}
|
|
20804
19384
|
record.status = "cancelled";
|
|
20805
19385
|
record.lastActivityAt = Date.now();
|
|
20806
19386
|
await jm.store.save(record);
|
|
20807
|
-
await jm.
|
|
19387
|
+
await jm.finalizeStoredTerminal(id);
|
|
20808
19388
|
return c.json({ status: "cancelled" });
|
|
20809
19389
|
}
|
|
20810
19390
|
if (!isJobOwner(c, job)) return c.json({ error: "Job not found" }, 404);
|
|
@@ -20822,7 +19402,7 @@ function isJobOwner(c, job) {
|
|
|
20822
19402
|
if (job.requesterTokenHash) {
|
|
20823
19403
|
const token = c.req.header("X-Job-Token");
|
|
20824
19404
|
if (!token) return false;
|
|
20825
|
-
const provided =
|
|
19405
|
+
const provided = createHash7("sha256").update(token).digest("hex");
|
|
20826
19406
|
return timingSafeEqualHex(provided, job.requesterTokenHash);
|
|
20827
19407
|
}
|
|
20828
19408
|
return c.get("requesterId") === job.requesterId;
|
|
@@ -21071,8 +19651,8 @@ var PaymentValidationError = class extends Error {
|
|
|
21071
19651
|
}
|
|
21072
19652
|
detail;
|
|
21073
19653
|
};
|
|
21074
|
-
function buildAuthErrorResponse(c, err) {
|
|
21075
|
-
return c.json(authErrorBody(err, { schemaUrl: infoSchemaUrl(c.req.url) }), 401);
|
|
19654
|
+
function buildAuthErrorResponse(c, err, publicOrigin) {
|
|
19655
|
+
return c.json(authErrorBody(err, { schemaUrl: infoSchemaUrl(c.req.url, publicOrigin) }), 401);
|
|
21076
19656
|
}
|
|
21077
19657
|
function validateMessageContent(type, content, devMode) {
|
|
21078
19658
|
if (type === "response") {
|
|
@@ -21088,567 +19668,6 @@ function validateMessageContent(type, content, devMode) {
|
|
|
21088
19668
|
return null;
|
|
21089
19669
|
}
|
|
21090
19670
|
|
|
21091
|
-
// src/lib/nwc.ts
|
|
21092
|
-
import { decrypt, encrypt } from "nostr-tools/nip04";
|
|
21093
|
-
import { SimplePool, useWebSocketImplementation } from "nostr-tools/pool";
|
|
21094
|
-
import { finalizeEvent, getPublicKey } from "nostr-tools/pure";
|
|
21095
|
-
import WebSocket from "ws";
|
|
21096
|
-
|
|
21097
|
-
// src/lib/hash.ts
|
|
21098
|
-
import { createHash as createHash9 } from "crypto";
|
|
21099
|
-
function sha256hex(input) {
|
|
21100
|
-
return createHash9("sha256").update(input).digest("hex");
|
|
21101
|
-
}
|
|
21102
|
-
|
|
21103
|
-
// src/lib/nwc.ts
|
|
21104
|
-
useWebSocketImplementation(WebSocket);
|
|
21105
|
-
var NwcError = class extends Error {
|
|
21106
|
-
constructor(code, message, method) {
|
|
21107
|
-
super(message);
|
|
21108
|
-
this.code = code;
|
|
21109
|
-
this.method = method;
|
|
21110
|
-
this.name = "NwcError";
|
|
21111
|
-
}
|
|
21112
|
-
code;
|
|
21113
|
-
method;
|
|
21114
|
-
};
|
|
21115
|
-
function nwcTimeoutMs() {
|
|
21116
|
-
return positiveSecondsEnv("DVM_NWC_TIMEOUT_SEC", 30);
|
|
21117
|
-
}
|
|
21118
|
-
function nwcDedupWindowMs() {
|
|
21119
|
-
return positiveSecondsEnv("DVM_NWC_DEDUP_WINDOW_SEC", 240);
|
|
21120
|
-
}
|
|
21121
|
-
async function withNwcPool(conn, fn, timeoutMs) {
|
|
21122
|
-
return withPool(void 0, conn, (pool) => fn({ pool, timeoutMs }));
|
|
21123
|
-
}
|
|
21124
|
-
function parseNwcUri(uri) {
|
|
21125
|
-
if (!uri.startsWith("nostr+walletconnect://")) {
|
|
21126
|
-
throw new NwcError("invalid_uri", "NWC URI must start with nostr+walletconnect://");
|
|
21127
|
-
}
|
|
21128
|
-
const withoutScheme = uri.slice("nostr+walletconnect://".length);
|
|
21129
|
-
const [walletPubkey, queryString] = withoutScheme.split("?", 2);
|
|
21130
|
-
if (!walletPubkey || !queryString) {
|
|
21131
|
-
throw new NwcError("invalid_uri", "NWC URI missing pubkey or query params");
|
|
21132
|
-
}
|
|
21133
|
-
const params = new URLSearchParams(queryString);
|
|
21134
|
-
const relays = params.getAll("relay").filter((r) => r.length > 0);
|
|
21135
|
-
const secretHex = params.get("secret");
|
|
21136
|
-
if (relays.length === 0) throw new NwcError("invalid_uri", "NWC URI missing relay param");
|
|
21137
|
-
if (!secretHex) throw new NwcError("invalid_uri", "NWC URI missing secret param");
|
|
21138
|
-
if (!/^[0-9a-fA-F]{64}$/.test(secretHex)) {
|
|
21139
|
-
throw new NwcError("invalid_uri", "NWC URI secret must be 64 hex characters");
|
|
21140
|
-
}
|
|
21141
|
-
const secret = hexToBytes6(secretHex);
|
|
21142
|
-
const clientPubkey = getPublicKey(secret);
|
|
21143
|
-
return { walletPubkey, relays, secret, clientPubkey };
|
|
21144
|
-
}
|
|
21145
|
-
async function payInvoiceReconciling(conn, bolt11, opts = {}) {
|
|
21146
|
-
return callerLoggers.lightning.span(
|
|
21147
|
-
"lightning.pay_invoice",
|
|
21148
|
-
{
|
|
21149
|
-
relays: conn.relays.join(","),
|
|
21150
|
-
wallet_pubkey: conn.walletPubkey,
|
|
21151
|
-
bolt11_hash: sha256hex(bolt11).slice(0, 16)
|
|
21152
|
-
},
|
|
21153
|
-
async () => {
|
|
21154
|
-
const now = opts.now ?? (() => Date.now());
|
|
21155
|
-
const dedupWindowMs = opts.dedupWindowMs ?? nwcDedupWindowMs();
|
|
21156
|
-
return withPool(opts.pool, conn, async (pool) => {
|
|
21157
|
-
const method = "pay_invoice";
|
|
21158
|
-
const event = buildRequestEvent(conn, method, { invoice: bolt11 });
|
|
21159
|
-
const publishedAtMs = now();
|
|
21160
|
-
const first = await attemptPayment(conn, method, event, pool, opts.timeoutMs);
|
|
21161
|
-
if (first.kind === "settled") {
|
|
21162
|
-
return settledOutcome(first, "paid");
|
|
21163
|
-
}
|
|
21164
|
-
if (first.kind === "rejected") throw first.error;
|
|
21165
|
-
if (first.kind === "not_sent") {
|
|
21166
|
-
throw first.error;
|
|
21167
|
-
}
|
|
21168
|
-
const reconciled = await reconcile(conn, bolt11, pool, opts.timeoutMs);
|
|
21169
|
-
if (reconciled.kind === "settled") {
|
|
21170
|
-
callerLoggers.lightning.info("lightning.pay_invoice.reconciled", { republished: false });
|
|
21171
|
-
return settledOutcome(reconciled, "reconciled");
|
|
21172
|
-
}
|
|
21173
|
-
if (reconciled.kind === "unknown") {
|
|
21174
|
-
throw new NwcError(
|
|
21175
|
-
"payment_unknown",
|
|
21176
|
-
"The wallet never answered and could not confirm whether the payment went through."
|
|
21177
|
-
);
|
|
21178
|
-
}
|
|
21179
|
-
if (now() - publishedAtMs >= dedupWindowMs) {
|
|
21180
|
-
throw new NwcError(
|
|
21181
|
-
"payment_unknown",
|
|
21182
|
-
"The wallet never answered and too much time has passed to safely retry the payment."
|
|
21183
|
-
);
|
|
21184
|
-
}
|
|
21185
|
-
callerLoggers.lightning.info("lightning.pay_invoice.republish", { event_id: event.id });
|
|
21186
|
-
const second = await attemptPayment(conn, method, event, pool, opts.timeoutMs);
|
|
21187
|
-
if (second.kind === "settled") {
|
|
21188
|
-
return settledOutcome(second, "republished");
|
|
21189
|
-
}
|
|
21190
|
-
if (second.kind === "rejected") throw second.error;
|
|
21191
|
-
const final = await reconcile(conn, bolt11, pool, opts.timeoutMs);
|
|
21192
|
-
if (final.kind === "settled") {
|
|
21193
|
-
return settledOutcome(final, "republished");
|
|
21194
|
-
}
|
|
21195
|
-
throw new NwcError(
|
|
21196
|
-
"payment_unknown",
|
|
21197
|
-
"The wallet never answered and could not confirm whether the payment went through."
|
|
21198
|
-
);
|
|
21199
|
-
});
|
|
21200
|
-
}
|
|
21201
|
-
);
|
|
21202
|
-
}
|
|
21203
|
-
async function getBalance(conn, opts = {}) {
|
|
21204
|
-
return callerLoggers.lightning.span(
|
|
21205
|
-
"lightning.get_balance",
|
|
21206
|
-
{ relays: conn.relays.join(","), wallet_pubkey: conn.walletPubkey },
|
|
21207
|
-
async () => {
|
|
21208
|
-
const result = await sendNwcRequest(conn, "get_balance", {}, opts);
|
|
21209
|
-
const balance = result.balance ?? 0;
|
|
21210
|
-
callerLoggers.lightning.info("lightning.get_balance.result", { balance_msats: balance });
|
|
21211
|
-
return balance;
|
|
21212
|
-
}
|
|
21213
|
-
);
|
|
21214
|
-
}
|
|
21215
|
-
async function getInfo(conn, opts = {}) {
|
|
21216
|
-
return callerLoggers.lightning.span(
|
|
21217
|
-
"lightning.get_info",
|
|
21218
|
-
{ relays: conn.relays.join(","), wallet_pubkey: conn.walletPubkey },
|
|
21219
|
-
async () => {
|
|
21220
|
-
const result = await sendNwcRequest(conn, "get_info", {}, opts);
|
|
21221
|
-
const info = { alias: result.alias, methods: result.methods ?? [] };
|
|
21222
|
-
const volunteered = extractWalletReportedBudget(result);
|
|
21223
|
-
if (volunteered) info.walletReportedBudget = volunteered;
|
|
21224
|
-
callerLoggers.lightning.info("lightning.get_info.result", {
|
|
21225
|
-
alias: info.alias,
|
|
21226
|
-
methods: info.methods,
|
|
21227
|
-
wallet_reported_budget: volunteered !== void 0
|
|
21228
|
-
});
|
|
21229
|
-
return info;
|
|
21230
|
-
}
|
|
21231
|
-
);
|
|
21232
|
-
}
|
|
21233
|
-
async function makeInvoice(conn, params, opts = {}) {
|
|
21234
|
-
return callerLoggers.lightning.span(
|
|
21235
|
-
"lightning.make_invoice",
|
|
21236
|
-
{
|
|
21237
|
-
relays: conn.relays.join(","),
|
|
21238
|
-
wallet_pubkey: conn.walletPubkey,
|
|
21239
|
-
amount_msats: params.amountMsats
|
|
21240
|
-
},
|
|
21241
|
-
async () => {
|
|
21242
|
-
const body = { amount: params.amountMsats };
|
|
21243
|
-
if (params.description !== void 0) body.description = params.description;
|
|
21244
|
-
if (params.expirySeconds !== void 0) body.expiry = params.expirySeconds;
|
|
21245
|
-
const result = await sendNwcRequest(conn, "make_invoice", body, opts);
|
|
21246
|
-
const tx = toTransaction(result);
|
|
21247
|
-
if (!tx.invoice) {
|
|
21248
|
-
throw new NwcError("missing_invoice", "Wallet did not return a BOLT11 invoice");
|
|
21249
|
-
}
|
|
21250
|
-
if (!tx.paymentHash) {
|
|
21251
|
-
throw new NwcError("missing_payment_hash", "Wallet did not return a payment hash");
|
|
21252
|
-
}
|
|
21253
|
-
callerLoggers.lightning.info("lightning.make_invoice.result", {
|
|
21254
|
-
payment_hash_hash: sha256hex(tx.paymentHash).slice(0, 16)
|
|
21255
|
-
});
|
|
21256
|
-
return tx;
|
|
21257
|
-
}
|
|
21258
|
-
);
|
|
21259
|
-
}
|
|
21260
|
-
async function lookupInvoice(conn, params, opts = {}) {
|
|
21261
|
-
return callerLoggers.lightning.span(
|
|
21262
|
-
"lightning.lookup_invoice",
|
|
21263
|
-
{
|
|
21264
|
-
relays: conn.relays.join(","),
|
|
21265
|
-
wallet_pubkey: conn.walletPubkey,
|
|
21266
|
-
payment_hash_hash: params.paymentHash ? sha256hex(params.paymentHash).slice(0, 16) : void 0
|
|
21267
|
-
},
|
|
21268
|
-
async () => {
|
|
21269
|
-
const body = {};
|
|
21270
|
-
if (params.paymentHash) body.payment_hash = params.paymentHash;
|
|
21271
|
-
if (params.invoice) body.invoice = params.invoice;
|
|
21272
|
-
if (!body.payment_hash && !body.invoice) {
|
|
21273
|
-
throw new NwcError("bad_request", "lookupInvoice requires payment_hash or invoice");
|
|
21274
|
-
}
|
|
21275
|
-
const result = await sendNwcRequest(conn, "lookup_invoice", body, opts);
|
|
21276
|
-
return toTransaction(result);
|
|
21277
|
-
}
|
|
21278
|
-
);
|
|
21279
|
-
}
|
|
21280
|
-
async function listTransactions(conn, options, opts = {}) {
|
|
21281
|
-
return callerLoggers.lightning.span(
|
|
21282
|
-
"lightning.list_transactions",
|
|
21283
|
-
{
|
|
21284
|
-
relays: conn.relays.join(","),
|
|
21285
|
-
wallet_pubkey: conn.walletPubkey,
|
|
21286
|
-
from: options.from,
|
|
21287
|
-
limit: options.limit
|
|
21288
|
-
},
|
|
21289
|
-
async () => {
|
|
21290
|
-
const body = {};
|
|
21291
|
-
if (options.from !== void 0) body.from = options.from;
|
|
21292
|
-
if (options.limit !== void 0) body.limit = options.limit;
|
|
21293
|
-
const result = await sendNwcRequest(conn, "list_transactions", body, opts);
|
|
21294
|
-
if (!Array.isArray(result.transactions)) {
|
|
21295
|
-
throw new NwcError("invalid_response", "Wallet did not return a transaction list.");
|
|
21296
|
-
}
|
|
21297
|
-
const transactions = result.transactions.map(toTransactionSnapshot);
|
|
21298
|
-
callerLoggers.lightning.info("lightning.list_transactions.result", {
|
|
21299
|
-
transaction_count: transactions.length
|
|
21300
|
-
});
|
|
21301
|
-
return transactions;
|
|
21302
|
-
}
|
|
21303
|
-
);
|
|
21304
|
-
}
|
|
21305
|
-
var VOLUNTEERED_BUDGET_KEYS = [
|
|
21306
|
-
"budget",
|
|
21307
|
-
"budget_renewal",
|
|
21308
|
-
"budget_msats",
|
|
21309
|
-
"budget_amount",
|
|
21310
|
-
"max_amount",
|
|
21311
|
-
"remaining_budget",
|
|
21312
|
-
"budget_remaining",
|
|
21313
|
-
"renews_at"
|
|
21314
|
-
];
|
|
21315
|
-
function extractWalletReportedBudget(result) {
|
|
21316
|
-
const out = {};
|
|
21317
|
-
for (const key of VOLUNTEERED_BUDGET_KEYS) {
|
|
21318
|
-
const value = result[key];
|
|
21319
|
-
if (value !== void 0 && value !== null) out[key] = value;
|
|
21320
|
-
}
|
|
21321
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
21322
|
-
}
|
|
21323
|
-
async function attemptPayment(conn, method, event, pool, timeoutMs) {
|
|
21324
|
-
let result;
|
|
21325
|
-
try {
|
|
21326
|
-
result = await publishAndAwait(conn, method, event, pool, timeoutMs);
|
|
21327
|
-
} catch (err) {
|
|
21328
|
-
if (err instanceof NwcError && err.code === "relay_unreachable") {
|
|
21329
|
-
return { kind: "not_sent", error: err };
|
|
21330
|
-
}
|
|
21331
|
-
if (err instanceof NwcError && (err.code === "timeout" || err.code === "publish_rejected")) {
|
|
21332
|
-
return { kind: "unknown" };
|
|
21333
|
-
}
|
|
21334
|
-
if (err instanceof NwcError && err.code === "invalid_response") return { kind: "unknown" };
|
|
21335
|
-
if (err instanceof NwcError) return { kind: "rejected", error: err };
|
|
21336
|
-
throw err;
|
|
21337
|
-
}
|
|
21338
|
-
if (!result.preimage) {
|
|
21339
|
-
return {
|
|
21340
|
-
kind: "rejected",
|
|
21341
|
-
error: new NwcError("missing_preimage", "Wallet did not return a payment preimage.")
|
|
21342
|
-
};
|
|
21343
|
-
}
|
|
21344
|
-
const feesPaidMsats = optionalFeeMsats(result.fees_paid);
|
|
21345
|
-
return {
|
|
21346
|
-
kind: "settled",
|
|
21347
|
-
preimage: result.preimage,
|
|
21348
|
-
...feesPaidMsats !== void 0 ? { feesPaidMsats } : {}
|
|
21349
|
-
};
|
|
21350
|
-
}
|
|
21351
|
-
async function reconcile(conn, bolt11, pool, timeoutMs) {
|
|
21352
|
-
try {
|
|
21353
|
-
const tx = await lookupInvoice(conn, { invoice: bolt11 }, { pool, timeoutMs });
|
|
21354
|
-
if (tx.invoice !== bolt11) return { kind: "unknown" };
|
|
21355
|
-
if (tx.settledAt != null && tx.preimage) {
|
|
21356
|
-
return {
|
|
21357
|
-
kind: "settled",
|
|
21358
|
-
preimage: tx.preimage,
|
|
21359
|
-
...tx.feesPaidMsats !== void 0 ? { feesPaidMsats: tx.feesPaidMsats } : {}
|
|
21360
|
-
};
|
|
21361
|
-
}
|
|
21362
|
-
if (tx.settledAt != null) {
|
|
21363
|
-
return {
|
|
21364
|
-
kind: "settled",
|
|
21365
|
-
preimage: "",
|
|
21366
|
-
...tx.feesPaidMsats !== void 0 ? { feesPaidMsats: tx.feesPaidMsats } : {}
|
|
21367
|
-
};
|
|
21368
|
-
}
|
|
21369
|
-
return { kind: "unpaid" };
|
|
21370
|
-
} catch (err) {
|
|
21371
|
-
if (err instanceof NwcError && err.code.toUpperCase() === "NOT_FOUND") {
|
|
21372
|
-
return { kind: "unpaid" };
|
|
21373
|
-
}
|
|
21374
|
-
return { kind: "unknown" };
|
|
21375
|
-
}
|
|
21376
|
-
}
|
|
21377
|
-
async function sendNwcRequest(conn, method, params, opts) {
|
|
21378
|
-
return withPool(
|
|
21379
|
-
opts.pool,
|
|
21380
|
-
conn,
|
|
21381
|
-
(pool) => publishAndAwait(conn, method, buildRequestEvent(conn, method, params), pool, opts.timeoutMs)
|
|
21382
|
-
);
|
|
21383
|
-
}
|
|
21384
|
-
function buildRequestEvent(conn, method, params) {
|
|
21385
|
-
const content = JSON.stringify({ method, params });
|
|
21386
|
-
const encrypted = encrypt(conn.secret, conn.walletPubkey, content);
|
|
21387
|
-
return finalizeEvent(
|
|
21388
|
-
{
|
|
21389
|
-
kind: 23194,
|
|
21390
|
-
tags: [["p", conn.walletPubkey]],
|
|
21391
|
-
content: encrypted,
|
|
21392
|
-
created_at: Math.floor(Date.now() / 1e3)
|
|
21393
|
-
},
|
|
21394
|
-
conn.secret
|
|
21395
|
-
);
|
|
21396
|
-
}
|
|
21397
|
-
async function publishAndAwait(conn, method, event, pool, timeoutMs) {
|
|
21398
|
-
const relays = conn.relays;
|
|
21399
|
-
const budgetMs = timeoutMs ?? nwcTimeoutMs();
|
|
21400
|
-
const waiter = awaitResponse(pool, relays, conn.walletPubkey, event.id, budgetMs);
|
|
21401
|
-
try {
|
|
21402
|
-
await publishEvent(pool, relays, event);
|
|
21403
|
-
} catch (err) {
|
|
21404
|
-
waiter.cancel();
|
|
21405
|
-
if (err instanceof NwcError) throw err;
|
|
21406
|
-
throw new NwcError(
|
|
21407
|
-
"publish_rejected",
|
|
21408
|
-
`No relay accepted the request (${relays.join(", ")}): ${err instanceof Error ? err.message : String(err)}`
|
|
21409
|
-
);
|
|
21410
|
-
}
|
|
21411
|
-
const response = await waiter.promise;
|
|
21412
|
-
if (response === null) {
|
|
21413
|
-
throw new NwcError("timeout", `Wallet did not respond within ${Math.round(budgetMs / 1e3)}s`);
|
|
21414
|
-
}
|
|
21415
|
-
let decrypted;
|
|
21416
|
-
try {
|
|
21417
|
-
decrypted = decrypt(conn.secret, conn.walletPubkey, response);
|
|
21418
|
-
} catch {
|
|
21419
|
-
throw new NwcError("invalid_response", "Wallet response could not be decrypted.");
|
|
21420
|
-
}
|
|
21421
|
-
let parsed;
|
|
21422
|
-
try {
|
|
21423
|
-
parsed = JSON.parse(decrypted);
|
|
21424
|
-
} catch {
|
|
21425
|
-
throw new NwcError("invalid_response", "Wallet returned a malformed response.");
|
|
21426
|
-
}
|
|
21427
|
-
if (parsed.error) {
|
|
21428
|
-
throw new NwcError(
|
|
21429
|
-
parsed.error.code ?? "wallet_error",
|
|
21430
|
-
parsed.error.message ?? "Wallet error",
|
|
21431
|
-
method
|
|
21432
|
-
);
|
|
21433
|
-
}
|
|
21434
|
-
return parsed.result ?? {};
|
|
21435
|
-
}
|
|
21436
|
-
function awaitResponse(pool, relays, walletPubkey, eventId, timeoutMs) {
|
|
21437
|
-
let settle;
|
|
21438
|
-
const promise = new Promise((resolve) => {
|
|
21439
|
-
const done = (value) => {
|
|
21440
|
-
clearTimeout(timer);
|
|
21441
|
-
sub.close();
|
|
21442
|
-
resolve(value);
|
|
21443
|
-
};
|
|
21444
|
-
settle = done;
|
|
21445
|
-
const timer = setTimeout(() => {
|
|
21446
|
-
done(null);
|
|
21447
|
-
}, timeoutMs);
|
|
21448
|
-
const sub = pool.subscribeMany(
|
|
21449
|
-
relays,
|
|
21450
|
-
{
|
|
21451
|
-
kinds: [23195],
|
|
21452
|
-
authors: [walletPubkey],
|
|
21453
|
-
"#e": [eventId]
|
|
21454
|
-
},
|
|
21455
|
-
{
|
|
21456
|
-
onevent(ev) {
|
|
21457
|
-
done(ev.content);
|
|
21458
|
-
},
|
|
21459
|
-
// required by nostr-tools
|
|
21460
|
-
oneose() {
|
|
21461
|
-
}
|
|
21462
|
-
// eslint-disable-line @typescript-eslint/no-empty-function
|
|
21463
|
-
}
|
|
21464
|
-
);
|
|
21465
|
-
});
|
|
21466
|
-
return {
|
|
21467
|
-
promise,
|
|
21468
|
-
cancel: () => {
|
|
21469
|
-
settle?.(null);
|
|
21470
|
-
}
|
|
21471
|
-
};
|
|
21472
|
-
}
|
|
21473
|
-
async function withPool(external, conn, fn) {
|
|
21474
|
-
if (external) return fn(external);
|
|
21475
|
-
const pool = new SimplePool();
|
|
21476
|
-
try {
|
|
21477
|
-
return await fn(pool);
|
|
21478
|
-
} finally {
|
|
21479
|
-
pool.close(conn.relays);
|
|
21480
|
-
}
|
|
21481
|
-
}
|
|
21482
|
-
function toTransaction(raw) {
|
|
21483
|
-
const feesPaidMsats = optionalFeeMsats(raw.fees_paid);
|
|
21484
|
-
return {
|
|
21485
|
-
type: raw.type ?? "incoming",
|
|
21486
|
-
invoice: raw.invoice,
|
|
21487
|
-
description: raw.description,
|
|
21488
|
-
paymentHash: raw.payment_hash ?? "",
|
|
21489
|
-
preimage: raw.preimage,
|
|
21490
|
-
amountMsats: raw.amount ?? 0,
|
|
21491
|
-
...feesPaidMsats !== void 0 ? { feesPaidMsats } : {},
|
|
21492
|
-
createdAt: raw.created_at ?? Math.floor(Date.now() / 1e3),
|
|
21493
|
-
expiresAt: raw.expires_at,
|
|
21494
|
-
settledAt: raw.settled_at ?? void 0
|
|
21495
|
-
};
|
|
21496
|
-
}
|
|
21497
|
-
function toTransactionSnapshot(value) {
|
|
21498
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
21499
|
-
throw new NwcError("invalid_response", "Wallet returned a malformed transaction.");
|
|
21500
|
-
}
|
|
21501
|
-
const raw = value;
|
|
21502
|
-
if (raw.type !== "incoming" && raw.type !== "outgoing") {
|
|
21503
|
-
throw new NwcError("invalid_response", "Wallet returned an invalid transaction type.");
|
|
21504
|
-
}
|
|
21505
|
-
const amountMsats = requiredNonNegativeInteger(raw.amount, "amount");
|
|
21506
|
-
const createdAt = requiredNonNegativeInteger(raw.created_at, "creation time");
|
|
21507
|
-
const feesPaidMsats = optionalFeeMsats(raw.fees_paid);
|
|
21508
|
-
if (raw.fees_paid != null && feesPaidMsats === void 0) {
|
|
21509
|
-
throw new NwcError("invalid_response", "Wallet returned an invalid transaction fee.");
|
|
21510
|
-
}
|
|
21511
|
-
const settledAt = optionalNonNegativeInteger(raw.settled_at);
|
|
21512
|
-
if (raw.settled_at != null && settledAt === void 0) {
|
|
21513
|
-
throw new NwcError("invalid_response", "Wallet returned an invalid settlement time.");
|
|
21514
|
-
}
|
|
21515
|
-
const invoice = optionalPrivateString(raw.invoice, "invoice");
|
|
21516
|
-
const paymentHash = optionalPrivateString(raw.payment_hash, "payment hash");
|
|
21517
|
-
const reducedIdentity = {
|
|
21518
|
-
type: raw.type,
|
|
21519
|
-
invoice: invoice ?? null,
|
|
21520
|
-
paymentHash: paymentHash ?? null,
|
|
21521
|
-
amountMsats,
|
|
21522
|
-
feesPaidMsats: feesPaidMsats ?? null,
|
|
21523
|
-
createdAt,
|
|
21524
|
-
settledAt: settledAt ?? null
|
|
21525
|
-
};
|
|
21526
|
-
return {
|
|
21527
|
-
fingerprint: sha256hex(JSON.stringify(reducedIdentity)),
|
|
21528
|
-
...invoice ? { invoiceHash: sha256hex(invoice) } : {},
|
|
21529
|
-
type: raw.type,
|
|
21530
|
-
amountMsats,
|
|
21531
|
-
...feesPaidMsats !== void 0 ? { feesPaidMsats } : {},
|
|
21532
|
-
createdAt,
|
|
21533
|
-
...settledAt !== void 0 ? { settledAt } : {}
|
|
21534
|
-
};
|
|
21535
|
-
}
|
|
21536
|
-
function settledOutcome(payment, outcome) {
|
|
21537
|
-
return {
|
|
21538
|
-
preimage: payment.preimage,
|
|
21539
|
-
outcome,
|
|
21540
|
-
...payment.feesPaidMsats !== void 0 ? { feesPaidMsats: payment.feesPaidMsats } : {}
|
|
21541
|
-
};
|
|
21542
|
-
}
|
|
21543
|
-
function optionalFeeMsats(value) {
|
|
21544
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
21545
|
-
}
|
|
21546
|
-
function optionalNonNegativeInteger(value) {
|
|
21547
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
21548
|
-
}
|
|
21549
|
-
function requiredNonNegativeInteger(value, field) {
|
|
21550
|
-
const parsed = optionalNonNegativeInteger(value);
|
|
21551
|
-
if (parsed === void 0) {
|
|
21552
|
-
throw new NwcError("invalid_response", `Wallet returned an invalid transaction ${field}.`);
|
|
21553
|
-
}
|
|
21554
|
-
return parsed;
|
|
21555
|
-
}
|
|
21556
|
-
function optionalPrivateString(value, field) {
|
|
21557
|
-
if (value === void 0 || value === null) return void 0;
|
|
21558
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
21559
|
-
throw new NwcError("invalid_response", `Wallet returned an invalid transaction ${field}.`);
|
|
21560
|
-
}
|
|
21561
|
-
return value;
|
|
21562
|
-
}
|
|
21563
|
-
async function publishEvent(pool, relays, event) {
|
|
21564
|
-
const connections = await Promise.allSettled(relays.map((url) => pool.ensureRelay(url)));
|
|
21565
|
-
const live = connections.flatMap((c) => c.status === "fulfilled" ? [c.value] : []);
|
|
21566
|
-
if (live.length === 0) {
|
|
21567
|
-
throw new NwcError(
|
|
21568
|
-
"relay_unreachable",
|
|
21569
|
-
`Could not connect to any of the wallet's relays (${relays.join(", ")}): ${describeRejections(
|
|
21570
|
-
connections
|
|
21571
|
-
)}`
|
|
21572
|
-
);
|
|
21573
|
-
}
|
|
21574
|
-
try {
|
|
21575
|
-
await Promise.any(live.map((relay) => relay.publish(event)));
|
|
21576
|
-
} catch (err) {
|
|
21577
|
-
throw new NwcError(
|
|
21578
|
-
"publish_rejected",
|
|
21579
|
-
`No relay accepted the request (${relays.join(", ")}): ${describeAggregate(err)}`
|
|
21580
|
-
);
|
|
21581
|
-
}
|
|
21582
|
-
}
|
|
21583
|
-
function describeRejections(results) {
|
|
21584
|
-
const reasons = results.flatMap((r) => r.status === "rejected" ? [errorText(r.reason)] : []);
|
|
21585
|
-
return reasons.length > 0 ? reasons.join("; ") : "no relays configured";
|
|
21586
|
-
}
|
|
21587
|
-
function describeAggregate(err) {
|
|
21588
|
-
if (err instanceof AggregateError) {
|
|
21589
|
-
return err.errors.map((e) => errorText(e)).join("; ");
|
|
21590
|
-
}
|
|
21591
|
-
return errorText(err);
|
|
21592
|
-
}
|
|
21593
|
-
function errorText(err) {
|
|
21594
|
-
return err instanceof Error ? err.message : String(err);
|
|
21595
|
-
}
|
|
21596
|
-
function positiveSecondsEnv(name, fallbackSec) {
|
|
21597
|
-
const raw = process.env[name];
|
|
21598
|
-
if (!raw) return fallbackSec * 1e3;
|
|
21599
|
-
const n = Number(raw);
|
|
21600
|
-
if (!Number.isFinite(n) || n <= 0) return fallbackSec * 1e3;
|
|
21601
|
-
return Math.floor(n) * 1e3;
|
|
21602
|
-
}
|
|
21603
|
-
function hexToBytes6(hex) {
|
|
21604
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
21605
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
21606
|
-
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
21607
|
-
}
|
|
21608
|
-
return bytes;
|
|
21609
|
-
}
|
|
21610
|
-
|
|
21611
|
-
// src/lib/nwc-backend.ts
|
|
21612
|
-
var NwcBackend = class {
|
|
21613
|
-
constructor(conn, opts = {}) {
|
|
21614
|
-
this.conn = conn;
|
|
21615
|
-
this.opts = opts;
|
|
21616
|
-
}
|
|
21617
|
-
conn;
|
|
21618
|
-
opts;
|
|
21619
|
-
async payInvoice(bolt11, _amountMsats) {
|
|
21620
|
-
return payInvoiceReconciling(this.conn, bolt11, this.opts);
|
|
21621
|
-
}
|
|
21622
|
-
async getBalance() {
|
|
21623
|
-
return getBalance(this.conn, this.opts);
|
|
21624
|
-
}
|
|
21625
|
-
async getInfo() {
|
|
21626
|
-
return getInfo(this.conn, this.opts);
|
|
21627
|
-
}
|
|
21628
|
-
async createInvoice(params) {
|
|
21629
|
-
const tx = await makeInvoice(this.conn, params, this.opts);
|
|
21630
|
-
const expiresAtMs = tx.expiresAt ? tx.expiresAt * 1e3 : Date.now() + (params.expirySeconds ?? 3600) * 1e3;
|
|
21631
|
-
return {
|
|
21632
|
-
bolt11: tx.invoice ?? "",
|
|
21633
|
-
paymentHash: tx.paymentHash,
|
|
21634
|
-
amountMsats: tx.amountMsats || params.amountMsats,
|
|
21635
|
-
expiresAt: expiresAtMs
|
|
21636
|
-
};
|
|
21637
|
-
}
|
|
21638
|
-
async lookupInvoice(paymentHash) {
|
|
21639
|
-
const tx = await lookupInvoice(this.conn, { paymentHash }, this.opts);
|
|
21640
|
-
return {
|
|
21641
|
-
settled: tx.settledAt != null,
|
|
21642
|
-
preimage: tx.preimage,
|
|
21643
|
-
settledAt: tx.settledAt != null ? tx.settledAt * 1e3 : void 0,
|
|
21644
|
-
expiresAt: tx.expiresAt != null ? tx.expiresAt * 1e3 : void 0
|
|
21645
|
-
};
|
|
21646
|
-
}
|
|
21647
|
-
async listTransactions(options) {
|
|
21648
|
-
return listTransactions(this.conn, options, this.opts);
|
|
21649
|
-
}
|
|
21650
|
-
};
|
|
21651
|
-
|
|
21652
19671
|
// src/sdk/server/tempo-observer-health.ts
|
|
21653
19672
|
var TEMPO_OBSERVER_HEALTH_REFRESH_TTL_MS = 60 * 1e3;
|
|
21654
19673
|
var TEMPO_OBSERVER_HEALTH_MAX_CURSOR_AGE_MS = 2 * 60 * 1e3;
|
|
@@ -21820,7 +19839,6 @@ function createPlatformTempoObserverHealth(env) {
|
|
|
21820
19839
|
}
|
|
21821
19840
|
|
|
21822
19841
|
export {
|
|
21823
|
-
toLockPubkey,
|
|
21824
19842
|
initLockPubkeyTable,
|
|
21825
19843
|
seedLockPubkeyState,
|
|
21826
19844
|
loadLockPubkeyState,
|
|
@@ -21853,7 +19871,6 @@ export {
|
|
|
21853
19871
|
toCreditView,
|
|
21854
19872
|
attachCreditMenu,
|
|
21855
19873
|
toCreditTerms,
|
|
21856
|
-
isX402SettlementReconciliationReason,
|
|
21857
19874
|
resolvePriceFiat,
|
|
21858
19875
|
priceFiatMicro,
|
|
21859
19876
|
resolveFxSnapshot,
|
|
@@ -21867,35 +19884,7 @@ export {
|
|
|
21867
19884
|
isDerivedCreditId,
|
|
21868
19885
|
derivedFundCreditId,
|
|
21869
19886
|
drawSettlementRef,
|
|
21870
|
-
TEMPO_SESSION_UPDATED_REASON,
|
|
21871
19887
|
rateUnavailablePaymentError,
|
|
21872
|
-
backupSecretFile,
|
|
21873
|
-
readAgentMnemonic,
|
|
21874
|
-
assertAgentMnemonicReplaceable,
|
|
21875
|
-
writeAgentMnemonic,
|
|
21876
|
-
deleteAgentMnemonic,
|
|
21877
|
-
generateAgentMnemonic,
|
|
21878
|
-
deriveAgentMasterKeypair,
|
|
21879
|
-
validateAgentMnemonic,
|
|
21880
|
-
looksLikeMnemonic,
|
|
21881
|
-
mnemonicFingerprint,
|
|
21882
|
-
WALLET_VERSION,
|
|
21883
|
-
RECENT_SPENDS_CAP,
|
|
21884
|
-
DEFAULT_AGENT_MINT_URL,
|
|
21885
|
-
loadAgentWallet,
|
|
21886
|
-
saveAgentWallet,
|
|
21887
|
-
walletExists,
|
|
21888
|
-
withWalletLock,
|
|
21889
|
-
effectiveMints,
|
|
21890
|
-
effectiveFundingMint,
|
|
21891
|
-
PersistentCounterSource,
|
|
21892
|
-
checkMintHealth,
|
|
21893
|
-
canSwap,
|
|
21894
|
-
meltAmountBounds,
|
|
21895
|
-
amountBoundsVerdict,
|
|
21896
|
-
assertNutSupport,
|
|
21897
|
-
getWallet,
|
|
21898
|
-
X402_BATCH_CHANNEL_ABI,
|
|
21899
19888
|
X402_BATCH_SETTLEMENT_NETWORK,
|
|
21900
19889
|
X402_BATCH_SETTLEMENT_MAINNET_NETWORK,
|
|
21901
19890
|
X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS,
|
|
@@ -21918,68 +19907,11 @@ export {
|
|
|
21918
19907
|
repairX402ExactSettlementEffect,
|
|
21919
19908
|
CREDIT_REQUEST_SCHEMA,
|
|
21920
19909
|
handleCreditRequest,
|
|
21921
|
-
RECOMMENDED_MINTS,
|
|
21922
|
-
isTestMintUrl,
|
|
21923
|
-
deriveReceiptSecret,
|
|
21924
|
-
receiptPubkeyFromSecret,
|
|
21925
|
-
deriveReceiptKeypair,
|
|
21926
|
-
generateEphemeralReceiptKey,
|
|
21927
|
-
X402_SELF_RELAY_KEY,
|
|
21928
|
-
X402_RECEIVER_AUTHORIZER_KEY,
|
|
21929
|
-
X402_BATCH_SETTLEMENT_KEY,
|
|
21930
|
-
X402_RPC_URL_KEY,
|
|
21931
|
-
X402EnvValidationError,
|
|
21932
|
-
validateX402Env,
|
|
21933
|
-
SPANS_INSERTED_CHANNEL,
|
|
21934
|
-
SpanWriter,
|
|
21935
|
-
insertSpanBatch,
|
|
21936
|
-
DbTransport,
|
|
21937
|
-
HttpTransport,
|
|
21938
|
-
generateIdentity,
|
|
21939
|
-
loadIdentitySecret,
|
|
21940
|
-
writeIdentity,
|
|
21941
|
-
hashCapabilities,
|
|
21942
|
-
buildAttestation,
|
|
21943
|
-
signAttestation,
|
|
21944
|
-
verifyAttestation,
|
|
21945
|
-
computeResultHash,
|
|
21946
|
-
signReceipt,
|
|
21947
|
-
verifyReceipt,
|
|
21948
|
-
isSignedJobReceipt,
|
|
21949
|
-
signDrainReceipt,
|
|
21950
|
-
signFundingReceipt,
|
|
21951
|
-
isFundingReceipt,
|
|
21952
|
-
verifyFundingReceipt,
|
|
21953
|
-
isDrainReceipt,
|
|
21954
|
-
verifyDrainReceipt,
|
|
21955
|
-
attestedDvmIdentity,
|
|
21956
|
-
authenticatedBuilderPubkey,
|
|
21957
|
-
verifyJobReceipt,
|
|
21958
|
-
checkReceiptAttestation,
|
|
21959
|
-
verifyDrainReceiptChain,
|
|
21960
|
-
verifyFundingReceiptChain,
|
|
21961
|
-
weakestReceiptVerdict,
|
|
21962
|
-
drainReceiptsDisplay,
|
|
21963
|
-
drainReceiptsHint,
|
|
21964
|
-
receiptDisplay,
|
|
21965
|
-
receiptIssuedAtIso,
|
|
21966
|
-
epochMsToIso,
|
|
21967
|
-
receiptHint,
|
|
21968
|
-
receiptFailureDetail,
|
|
21969
|
-
isPublicRequestId,
|
|
21970
|
-
parseTraceparent,
|
|
21971
|
-
PRIVKEY_HEX_RE,
|
|
21972
|
-
derivePubkeyFromPrivkey,
|
|
21973
|
-
verifyChallenge,
|
|
21974
|
-
ADMIN_STATE_UNAVAILABLE,
|
|
21975
|
-
buildAdminChallenge,
|
|
21976
|
-
buildAdminAuthHeader,
|
|
21977
19910
|
installAdminCashuRoutes,
|
|
21978
19911
|
createAdminCashuNonceStore,
|
|
21979
19912
|
X402ExactSettlementServer,
|
|
21980
19913
|
X402ExactSettlementEvidenceMissingError,
|
|
21981
19914
|
jobCredentialGate,
|
|
21982
|
-
createMonotonicClock,
|
|
21983
19915
|
REQUEST_ID_FIELD,
|
|
21984
19916
|
protocolEnvelopeIgnoreFields,
|
|
21985
19917
|
stripProtocolEnvelope,
|
|
@@ -21998,18 +19930,5 @@ export {
|
|
|
21998
19930
|
revenueReporterBannerState,
|
|
21999
19931
|
createDVMServer,
|
|
22000
19932
|
unknownRouteNotFound,
|
|
22001
|
-
sha256hex,
|
|
22002
|
-
NwcError,
|
|
22003
|
-
nwcTimeoutMs,
|
|
22004
|
-
nwcDedupWindowMs,
|
|
22005
|
-
withNwcPool,
|
|
22006
|
-
parseNwcUri,
|
|
22007
|
-
payInvoiceReconciling,
|
|
22008
|
-
getBalance,
|
|
22009
|
-
getInfo,
|
|
22010
|
-
makeInvoice,
|
|
22011
|
-
lookupInvoice,
|
|
22012
|
-
listTransactions,
|
|
22013
|
-
NwcBackend,
|
|
22014
19933
|
createPlatformTempoObserverHealth
|
|
22015
19934
|
};
|