@dvmkit/sdk 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-XSF5YTEY.js → chunk-2PN4DKZW.js} +1 -1
- package/dist/{chunk-F2LNWZCV.js → chunk-3KRFFLAT.js} +1 -1
- package/dist/{chunk-SGSHDMHP.js → chunk-4QYADZFN.js} +613 -153
- package/dist/{chunk-V73WRF46.js → chunk-5CCQEAQV.js} +1 -1
- package/dist/{chunk-4IFZHJQK.js → chunk-5RLNRLLB.js} +1 -1
- package/dist/{chunk-7QGS27UT.js → chunk-72A4PHLD.js} +1 -1
- package/dist/{chunk-XBQTFOIT.js → chunk-7RWE6ELV.js} +1 -1
- package/dist/{chunk-N4SBDUVD.js → chunk-A6EY4K46.js} +1 -1
- package/dist/{chunk-5U2RG3ZW.js → chunk-ALTE7I6C.js} +5 -0
- package/dist/{chunk-K4ZQE7IB.js → chunk-B5MPLDRC.js} +2 -2
- package/dist/{chunk-PP5WHB6T.js → chunk-C5XNZLZ7.js} +1 -1
- package/dist/{chunk-2UZOJRRS.js → chunk-CVN2YNMG.js} +1 -1
- package/dist/{chunk-MD34LE6M.js → chunk-CXRI4N4Y.js} +1 -1
- package/dist/{chunk-ELNWQS24.js → chunk-EIDRCGTY.js} +1 -1
- package/dist/{chunk-ICHC4VDZ.js → chunk-KRX7VNBG.js} +1 -1
- package/dist/{chunk-TN6MWTZI.js → chunk-LY2JYSJS.js} +1 -1
- package/dist/{chunk-7N5VDCG3.js → chunk-MWDUGMN2.js} +2 -2
- package/dist/{chunk-FRGKANTZ.js → chunk-ORBWKYDE.js} +1 -1
- package/dist/{chunk-VQHBJXBL.js → chunk-QTKGFG3I.js} +1 -1
- package/dist/{chunk-PBT3H2LZ.js → chunk-RX6FT7XA.js} +1 -1
- package/dist/{chunk-OYBSDJR6.js → chunk-VKFR5PCW.js} +2 -2
- package/dist/{chunk-V4CBI5HB.js → chunk-Y6JJDOGO.js} +1 -1
- package/dist/{content-retention-BASsjgKw.d.ts → content-retention-B_hOQS36.d.ts} +1 -1
- package/dist/{credit-ledger-VWZ5NP7V.js → credit-ledger-5HMJ6VUX.js} +3 -3
- package/dist/{credit-menu-B239nIY5.d.ts → credit-menu-D-h0zJCZ.d.ts} +42 -8
- package/dist/{fx-BmvWGISH.d.ts → fx-CpqQaIfg.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/internal/caller.d.ts +14 -8
- package/dist/internal/caller.js +7 -7
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +20 -20
- package/dist/{job-store-CA7u1nHV.d.ts → job-store-CgXT79tS.d.ts} +1 -1
- package/dist/{lightning-backend-BOcopzXk.d.ts → lightning-backend-B1hRUIOb.d.ts} +1 -1
- package/dist/{memory-credit-ledger-DZWRQGSF.js → memory-credit-ledger-ME6FH462.js} +4 -4
- package/dist/{mpp-setup-3NJY354I.js → mpp-setup-BLIYBORK.js} +3 -3
- package/dist/{payout-reporter-P3652TLG.js → payout-reporter-5QAHCYIO.js} +2 -2
- package/dist/{postgres-consumed-credential-store-S2CBQ4NO.js → postgres-consumed-credential-store-4PCJEZBD.js} +1 -1
- package/dist/{postgres-job-store-JRSG54VJ.js → postgres-job-store-E5GXLRB4.js} +2 -2
- package/dist/{postgres-replay-store-ZPILIJIW.js → postgres-replay-store-GH5AINQR.js} +2 -2
- package/dist/{pricing-3P7KAONY.js → pricing-JJYLTTWK.js} +3 -3
- package/dist/{revenue-reporter-2P3PWJJM.js → revenue-reporter-6XARUEG6.js} +2 -2
- package/dist/server/index.d.ts +10 -10
- package/dist/server/index.js +33 -29
- package/dist/{step-cache-BCRHimiY.d.ts → step-cache-Bezn-_ZL.d.ts} +41 -3
- package/dist/{tempo-charge-store-2LK6IDLS.js → tempo-charge-store-A3CDF7D5.js} +2 -2
- package/dist/{tempo-session-store-5MHCH3YW.js → tempo-session-store-EWBE5HJM.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +6 -6
- package/dist/{usd-B8_WMfHT.d.ts → usd-CJwU3-A4.d.ts} +1 -1
- package/dist/{x402-6T73AZQ6.js → x402-HVCUEWWU.js} +3 -3
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-XRLKGCAW.js";
|
|
6
6
|
import {
|
|
7
7
|
x402EarnedCeiling
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-KRX7VNBG.js";
|
|
9
9
|
import {
|
|
10
10
|
createX402FacilitatorAuthHeaders,
|
|
11
11
|
createX402FacilitatorClient,
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
verifyWithFacilitator,
|
|
15
15
|
withPaymentDiagnostics,
|
|
16
16
|
withPaymentStage
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-3KRFFLAT.js";
|
|
18
18
|
import {
|
|
19
19
|
MemoryCreditLedger
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-C5XNZLZ7.js";
|
|
21
21
|
import {
|
|
22
22
|
PostgresJobStore
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-A6EY4K46.js";
|
|
24
24
|
import {
|
|
25
25
|
SIGNED_ENVELOPE_FIELDS,
|
|
26
26
|
SignedRequestError,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
humanizeZodIssues,
|
|
29
29
|
signedRequestInput,
|
|
30
30
|
zodIssuesOf
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-5CCQEAQV.js";
|
|
32
32
|
import {
|
|
33
33
|
MemoryJobStore,
|
|
34
34
|
UNCAPPABLE_ASK_TOTAL,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
isJobRetentionStore,
|
|
38
38
|
isReceiptIssuingStore,
|
|
39
39
|
isStreamableJobStore
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-Y6JJDOGO.js";
|
|
41
41
|
import {
|
|
42
42
|
ContentHoldError,
|
|
43
43
|
JobCancelledError,
|
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
receiptPubkeyFromSecret,
|
|
79
79
|
toLockPubkey,
|
|
80
80
|
verifyChallenge
|
|
81
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-7RWE6ELV.js";
|
|
82
82
|
import {
|
|
83
83
|
X402_BATCH_SETTLEMENT_SCHEME,
|
|
84
84
|
X402_DEFAULT_FACILITATOR,
|
|
@@ -105,7 +105,7 @@ import {
|
|
|
105
105
|
} from "./chunk-SXL44PL5.js";
|
|
106
106
|
import {
|
|
107
107
|
createConsoleLogger
|
|
108
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-QTKGFG3I.js";
|
|
109
109
|
import {
|
|
110
110
|
StepCache
|
|
111
111
|
} from "./chunk-BQJIFRAZ.js";
|
|
@@ -122,7 +122,7 @@ import {
|
|
|
122
122
|
roundUsd,
|
|
123
123
|
satsToFiat,
|
|
124
124
|
validateCurrency
|
|
125
|
-
} from "./chunk-
|
|
125
|
+
} from "./chunk-72A4PHLD.js";
|
|
126
126
|
import {
|
|
127
127
|
CreditLedger,
|
|
128
128
|
CreditLedgerError,
|
|
@@ -138,7 +138,7 @@ import {
|
|
|
138
138
|
isNonChannelBitcoinRail,
|
|
139
139
|
netOwedSats,
|
|
140
140
|
x402SettlementPending
|
|
141
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-B5MPLDRC.js";
|
|
142
142
|
import {
|
|
143
143
|
parseStoreValue
|
|
144
144
|
} from "./chunk-YG7G4DPZ.js";
|
|
@@ -156,7 +156,7 @@ import {
|
|
|
156
156
|
} from "./chunk-5UC7TOQC.js";
|
|
157
157
|
import {
|
|
158
158
|
withPostgresRecoveryContext
|
|
159
|
-
} from "./chunk-
|
|
159
|
+
} from "./chunk-EIDRCGTY.js";
|
|
160
160
|
import {
|
|
161
161
|
DvmError,
|
|
162
162
|
x402NetworkByCaip2
|
|
@@ -174,17 +174,17 @@ import {
|
|
|
174
174
|
installHostedTempoCloseGuard,
|
|
175
175
|
isTempoInsufficientFundsError,
|
|
176
176
|
resolveTempoNetwork
|
|
177
|
-
} from "./chunk-
|
|
177
|
+
} from "./chunk-MWDUGMN2.js";
|
|
178
178
|
import {
|
|
179
179
|
loggers,
|
|
180
180
|
pinDimension,
|
|
181
181
|
withSpan,
|
|
182
182
|
withTraceContext
|
|
183
|
-
} from "./chunk-
|
|
183
|
+
} from "./chunk-LY2JYSJS.js";
|
|
184
184
|
import {
|
|
185
185
|
createDiagnosticCapabilityAuthority,
|
|
186
186
|
diagnosticConsole
|
|
187
|
-
} from "./chunk-
|
|
187
|
+
} from "./chunk-ALTE7I6C.js";
|
|
188
188
|
|
|
189
189
|
// src/sdk/server/lock-pubkey-store.ts
|
|
190
190
|
async function initLockPubkeyTable(db) {
|
|
@@ -428,6 +428,27 @@ async function insertAccumulatorRows(db, args) {
|
|
|
428
428
|
client.release();
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
|
+
async function claimAccumulatorRequestId(tx, dvmId, requestId) {
|
|
432
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
433
|
+
JSON.stringify(["wallet_accumulator_request", dvmId, requestId])
|
|
434
|
+
]);
|
|
435
|
+
const { rows: rows2 } = await tx.query(
|
|
436
|
+
`SELECT 1 FROM wallet_accumulator WHERE dvm_id = $1 AND request_id = $2 LIMIT 1`,
|
|
437
|
+
[dvmId, requestId]
|
|
438
|
+
);
|
|
439
|
+
if (rows2.length > 0) throw new AccumulatorReplayError(dvmId, requestId);
|
|
440
|
+
}
|
|
441
|
+
async function acceptedRequestIds(db, dvmId, secrets) {
|
|
442
|
+
if (secrets.length === 0) return [];
|
|
443
|
+
const hashes = secrets.map(hashProofSecret);
|
|
444
|
+
const { rows: rows2 } = await db.query(
|
|
445
|
+
`SELECT proof_secret_hash, request_id FROM wallet_accumulator
|
|
446
|
+
WHERE dvm_id = $1 AND proof_secret_hash = ANY($2::text[])`,
|
|
447
|
+
[dvmId, hashes]
|
|
448
|
+
);
|
|
449
|
+
const byHash = new Map(rows2.map((row) => [row.proof_secret_hash, row.request_id]));
|
|
450
|
+
return hashes.map((hash3) => byHash.get(hash3));
|
|
451
|
+
}
|
|
431
452
|
async function listPendingBatches(db, dvmId) {
|
|
432
453
|
const { rows: rows2 } = await db.query(
|
|
433
454
|
`SELECT id, lock_pubkey, mint_url, proof_keyset_id, proof_secret, proof_c, proof_amount, proof_dleq, received_at, melt_quote_id, t_expire
|
|
@@ -3226,7 +3247,7 @@ function paymentFailureFields(error) {
|
|
|
3226
3247
|
}
|
|
3227
3248
|
|
|
3228
3249
|
// src/sdk/server/payment.ts
|
|
3229
|
-
import { createHash as
|
|
3250
|
+
import { createHash as createHash5, randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
3230
3251
|
import { Challenge } from "mppx";
|
|
3231
3252
|
|
|
3232
3253
|
// src/lib/cashu/verify.ts
|
|
@@ -3320,6 +3341,63 @@ function methodNativeAmountToDecimal(amount, methodName) {
|
|
|
3320
3341
|
return `${nativeAmount2 / scale}.${(nativeAmount2 % scale).toString().padStart(decimals, "0")}`;
|
|
3321
3342
|
}
|
|
3322
3343
|
|
|
3344
|
+
// src/sdk/server/cashu-payment-request.ts
|
|
3345
|
+
import { createHash as createHash4 } from "crypto";
|
|
3346
|
+
import { PaymentRequest } from "@cashu/cashu-ts";
|
|
3347
|
+
function cashuPaymentRequest(args) {
|
|
3348
|
+
return new PaymentRequest(
|
|
3349
|
+
void 0,
|
|
3350
|
+
void 0,
|
|
3351
|
+
Math.ceil(args.requiredMsats / 1e3),
|
|
3352
|
+
"sat",
|
|
3353
|
+
args.mints,
|
|
3354
|
+
void 0,
|
|
3355
|
+
false,
|
|
3356
|
+
{ kind: "P2PK", data: args.lockPubkey, tags: [] }
|
|
3357
|
+
).toEncodedCreqA();
|
|
3358
|
+
}
|
|
3359
|
+
function proofSetRequestId(secrets) {
|
|
3360
|
+
const canonical = JSON.stringify([...secrets].sort());
|
|
3361
|
+
return `${DERIVED_REQUEST_ID_PREFIX}${createHash4("sha256").update(canonical).digest("hex")}`;
|
|
3362
|
+
}
|
|
3363
|
+
var DERIVED_REQUEST_ID_PREFIX = "nut24-";
|
|
3364
|
+
function withFxHistory(inner, opts = {}) {
|
|
3365
|
+
const holdWindowMs = opts.holdWindowMs ?? FX_HOLD_WINDOW_MS;
|
|
3366
|
+
const now = opts.now ?? Date.now;
|
|
3367
|
+
const history = [];
|
|
3368
|
+
return {
|
|
3369
|
+
async fetch() {
|
|
3370
|
+
const snapshot = await inner.fetch();
|
|
3371
|
+
const latest = history.at(-1);
|
|
3372
|
+
if (latest && sameRates(latest.snapshot, snapshot)) {
|
|
3373
|
+
latest.lastPricedAt = now();
|
|
3374
|
+
} else {
|
|
3375
|
+
history.push({ snapshot, lastPricedAt: now() });
|
|
3376
|
+
if (history.length > 4) history.shift();
|
|
3377
|
+
}
|
|
3378
|
+
return snapshot;
|
|
3379
|
+
},
|
|
3380
|
+
supportedCurrencies: () => inner.supportedCurrencies(),
|
|
3381
|
+
...inner.lastKnown && { lastKnown: inner.lastKnown.bind(inner) },
|
|
3382
|
+
previousFor(pricedAt) {
|
|
3383
|
+
for (let i = history.length - 1; i > 0; i--) {
|
|
3384
|
+
if (!sameRates(history[i].snapshot, pricedAt)) continue;
|
|
3385
|
+
const replaced = history[i - 1];
|
|
3386
|
+
return now() - replaced.lastPricedAt <= holdWindowMs ? replaced.snapshot : void 0;
|
|
3387
|
+
}
|
|
3388
|
+
return void 0;
|
|
3389
|
+
}
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
function sameRates(a, b) {
|
|
3393
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a.rates), ...Object.keys(b.rates)]);
|
|
3394
|
+
for (const key of keys) {
|
|
3395
|
+
if (a.rates[key] !== b.rates[key]) return false;
|
|
3396
|
+
}
|
|
3397
|
+
return true;
|
|
3398
|
+
}
|
|
3399
|
+
var FX_HOLD_WINDOW_MS = 10 * 6e4;
|
|
3400
|
+
|
|
3323
3401
|
// src/lib/mints.ts
|
|
3324
3402
|
function isTestMintUrl(url) {
|
|
3325
3403
|
let host;
|
|
@@ -3341,7 +3419,11 @@ import {
|
|
|
3341
3419
|
Amount as Amount2,
|
|
3342
3420
|
CheckStateEnum,
|
|
3343
3421
|
getP2PKExpectedWitnessPubkeys,
|
|
3422
|
+
getSecretData,
|
|
3423
|
+
getSecretKind,
|
|
3344
3424
|
getTagInt,
|
|
3425
|
+
getTags,
|
|
3426
|
+
parseP2PKSecret,
|
|
3345
3427
|
verifyDleqIfPresent
|
|
3346
3428
|
} from "@cashu/cashu-ts";
|
|
3347
3429
|
function valueCashuFunding(wallet, proofs) {
|
|
@@ -3358,17 +3440,109 @@ function valueCashuFunding(wallet, proofs) {
|
|
|
3358
3440
|
netMsats: netSats.multiplyBy(1e3).toNumber()
|
|
3359
3441
|
};
|
|
3360
3442
|
}
|
|
3443
|
+
var CASHU_MIN_LOCKTIME_SECONDS = 604800;
|
|
3444
|
+
var CASHU_LOCKTIME_RESUME_MARGIN_SECONDS = 86400;
|
|
3445
|
+
var P2PK_TAG_KEYS = /* @__PURE__ */ new Set(["locktime", "refund", "pubkeys", "n_sigs", "sigflag", "n_sigs_refund"]);
|
|
3446
|
+
var SINGLE_VALUE_TAG_KEYS = /* @__PURE__ */ new Set(["locktime", "n_sigs", "sigflag", "n_sigs_refund"]);
|
|
3447
|
+
function assessCashuLock(secret, lockPubkeys, nowSec) {
|
|
3448
|
+
try {
|
|
3449
|
+
parseP2PKSecret(secret);
|
|
3450
|
+
if (getSecretKind(secret) !== "P2PK") throw new Error("not P2PK");
|
|
3451
|
+
} catch {
|
|
3452
|
+
return { kind: "rejected", code: "not_p2pk_locked", message: "Proof secret is not a NUT-11 P2PK secret." };
|
|
3453
|
+
}
|
|
3454
|
+
const dvmKeyFor = (x) => x === void 0 ? void 0 : lockPubkeys.find((pk) => compressedX(pk) === x);
|
|
3455
|
+
const data = getSecretData(secret).data;
|
|
3456
|
+
const dataX = typeof data === "string" ? compressedX(data) : void 0;
|
|
3457
|
+
const lockPubkey = dvmKeyFor(dataX);
|
|
3458
|
+
if (!dataX || !lockPubkey) {
|
|
3459
|
+
return {
|
|
3460
|
+
kind: "rejected",
|
|
3461
|
+
code: "wrong_lock_pubkey",
|
|
3462
|
+
message: "Proof is not locked to one of this DVM's active lock pubkeys, spelled as a 33-byte compressed hex key."
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
const notExclusive = (message, locktimeBelowFloor) => ({
|
|
3466
|
+
kind: "rejected",
|
|
3467
|
+
code: "lock_not_exclusive",
|
|
3468
|
+
message,
|
|
3469
|
+
...locktimeBelowFloor && { locktimeBelowFloor }
|
|
3470
|
+
});
|
|
3471
|
+
try {
|
|
3472
|
+
getP2PKExpectedWitnessPubkeys(secret);
|
|
3473
|
+
} catch (err) {
|
|
3474
|
+
return notExclusive(`Proof carries an unreadable spending condition: ${err instanceof Error ? err.message : String(err)}`);
|
|
3475
|
+
}
|
|
3476
|
+
const tags = /* @__PURE__ */ new Map();
|
|
3477
|
+
for (const tag of getTags(secret)) {
|
|
3478
|
+
if (!Array.isArray(tag) || !tag.every((item) => typeof item === "string")) {
|
|
3479
|
+
return notExclusive("Proof carries a malformed tag.");
|
|
3480
|
+
}
|
|
3481
|
+
const [key, ...values] = tag;
|
|
3482
|
+
if (key === void 0 || !P2PK_TAG_KEYS.has(key)) return notExclusive(`Proof carries the unsupported tag ${JSON.stringify(key)}.`);
|
|
3483
|
+
if (tags.has(key)) return notExclusive(`Proof repeats the ${key} tag.`);
|
|
3484
|
+
if (values.length === 0) return notExclusive(`Proof carries a ${key} tag without a value.`);
|
|
3485
|
+
if (SINGLE_VALUE_TAG_KEYS.has(key) && values.length !== 1) return notExclusive(`Proof's ${key} tag must carry exactly one value.`);
|
|
3486
|
+
tags.set(key, values);
|
|
3487
|
+
}
|
|
3488
|
+
const sigflag = tags.get("sigflag")?.[0];
|
|
3489
|
+
if (sigflag !== void 0 && sigflag !== "SIG_INPUTS") {
|
|
3490
|
+
return notExclusive(`Proof requires sigflag ${sigflag}; this DVM accepts SIG_INPUTS only.`);
|
|
3491
|
+
}
|
|
3492
|
+
const nSigs = tags.get("n_sigs")?.[0];
|
|
3493
|
+
if (nSigs !== void 0 && nSigs !== "1") {
|
|
3494
|
+
return notExclusive(`Proof requires n_sigs=${nSigs}; this DVM accepts a single-signature lock only.`);
|
|
3495
|
+
}
|
|
3496
|
+
const mainX = [dataX];
|
|
3497
|
+
for (const key of tags.get("pubkeys") ?? []) {
|
|
3498
|
+
const x = compressedX(key);
|
|
3499
|
+
if (!x || !dvmKeyFor(x)) return notExclusive("Proof's pubkeys tag names a key other than this DVM's lock pubkeys.");
|
|
3500
|
+
if (mainX.includes(x)) return notExclusive("Proof repeats a key across data and pubkeys.");
|
|
3501
|
+
mainX.push(x);
|
|
3502
|
+
}
|
|
3503
|
+
const refundX = [];
|
|
3504
|
+
for (const key of tags.get("refund") ?? []) {
|
|
3505
|
+
const x = compressedX(key);
|
|
3506
|
+
if (!x) return notExclusive("Proof's refund tag names a key that is not a 33-byte compressed hex key.");
|
|
3507
|
+
if (refundX.includes(x)) return notExclusive("Proof repeats a key in its refund tag.");
|
|
3508
|
+
refundX.push(x);
|
|
3509
|
+
}
|
|
3510
|
+
const nSigsRefund = tags.get("n_sigs_refund")?.[0];
|
|
3511
|
+
if (nSigsRefund !== void 0 && !(/^[1-9][0-9]*$/.test(nSigsRefund) && Number(nSigsRefund) <= refundX.length)) {
|
|
3512
|
+
return notExclusive(`Proof's n_sigs_refund ${nSigsRefund} is not a count between 1 and its number of refund keys.`);
|
|
3513
|
+
}
|
|
3514
|
+
const locktimeValue = tags.get("locktime")?.[0];
|
|
3515
|
+
if (locktimeValue !== void 0) {
|
|
3516
|
+
const locktime = /^(0|[1-9][0-9]*)$/.test(locktimeValue) ? Number(locktimeValue) : NaN;
|
|
3517
|
+
if (!Number.isSafeInteger(locktime)) return notExclusive(`Proof's locktime ${locktimeValue} is not a unix-seconds integer.`);
|
|
3518
|
+
const floor = nowSec + CASHU_MIN_LOCKTIME_SECONDS - CASHU_LOCKTIME_RESUME_MARGIN_SECONDS;
|
|
3519
|
+
if (locktime < floor) {
|
|
3520
|
+
return notExclusive(
|
|
3521
|
+
`Proof locktime ${locktime} is earlier than the accepted floor ${floor} (unix seconds).`,
|
|
3522
|
+
true
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
return { kind: "exclusive", lockPubkey };
|
|
3527
|
+
}
|
|
3361
3528
|
async function acceptAccumulatorPayment(opts) {
|
|
3362
3529
|
if (opts.disposable && !opts.mints.every(isTestMintUrl)) {
|
|
3363
3530
|
throw new Error("Disposable Cashu accepts only known test or loopback mints.");
|
|
3364
3531
|
}
|
|
3365
|
-
if (!opts.requestId) {
|
|
3532
|
+
if (!opts.requestId && !opts.deriveRequestId) {
|
|
3366
3533
|
return {
|
|
3367
3534
|
kind: "rejected",
|
|
3368
3535
|
code: "missing_request_id",
|
|
3369
3536
|
message: "X-Cashu-Request-Id header required for the agent-wallet path."
|
|
3370
3537
|
};
|
|
3371
3538
|
}
|
|
3539
|
+
if (opts.requestId?.startsWith(DERIVED_REQUEST_ID_PREFIX)) {
|
|
3540
|
+
return {
|
|
3541
|
+
kind: "rejected",
|
|
3542
|
+
code: "reserved_request_id",
|
|
3543
|
+
message: `A Cashu request id may not start with "${DERIVED_REQUEST_ID_PREFIX}".`
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3372
3546
|
if (opts.lockPubkeys.length === 0) {
|
|
3373
3547
|
return {
|
|
3374
3548
|
kind: "rejected",
|
|
@@ -3389,6 +3563,9 @@ async function acceptAccumulatorPayment(opts) {
|
|
|
3389
3563
|
if (meta.unit !== "sat" || proofs.length === 0) {
|
|
3390
3564
|
throw new Error("Cashu payment requires nonempty sat proofs.");
|
|
3391
3565
|
}
|
|
3566
|
+
if (new Set(proofs.map((proof) => proof.secret)).size !== proofs.length) {
|
|
3567
|
+
throw new Error("Cashu payment carries the same proof more than once.");
|
|
3568
|
+
}
|
|
3392
3569
|
for (const proof of proofs) {
|
|
3393
3570
|
if (!proof.dleq || !verifyDleqIfPresent(proof, wallet.getKeyset(proof.id))) {
|
|
3394
3571
|
throw new Error(
|
|
@@ -3404,46 +3581,32 @@ async function acceptAccumulatorPayment(opts) {
|
|
|
3404
3581
|
message: err instanceof Error ? err.message : "Cashu token decode failed"
|
|
3405
3582
|
};
|
|
3406
3583
|
}
|
|
3584
|
+
const requestId = opts.requestId || proofSetRequestId(proofs.map((proof) => proof.secret));
|
|
3407
3585
|
const acceptedValue = {
|
|
3408
3586
|
...valuation,
|
|
3409
3587
|
mintUrl,
|
|
3410
|
-
paidMsats: valuation.netMsats
|
|
3588
|
+
paidMsats: valuation.netMsats,
|
|
3589
|
+
requestId
|
|
3411
3590
|
};
|
|
3591
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
3412
3592
|
const tExpireMsByIndex = [];
|
|
3413
3593
|
const matchedLockPubkeyByIndex = [];
|
|
3594
|
+
let belowFloorMessage;
|
|
3414
3595
|
for (const p of proofs) {
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
kind: "rejected",
|
|
3421
|
-
code: "not_p2pk_locked",
|
|
3422
|
-
message: "Proof secret is not a NUT-11 P2PK secret."
|
|
3423
|
-
};
|
|
3424
|
-
}
|
|
3425
|
-
if (allowed.length === 0) {
|
|
3426
|
-
return {
|
|
3427
|
-
kind: "rejected",
|
|
3428
|
-
code: "not_p2pk_locked",
|
|
3429
|
-
message: "Proof is unlocked or the P2PK lock has expired."
|
|
3430
|
-
};
|
|
3431
|
-
}
|
|
3432
|
-
const allowedNormalised = allowed.map(toLockPubkey);
|
|
3433
|
-
if (opts.disposable && (allowedNormalised.length !== 1 || (getTagInt(p.secret, "n_sigs") ?? 1) !== 1)) {
|
|
3434
|
-
return { kind: "rejected", code: "not_p2pk_locked", message: "Disposable Cashu requires an exclusive single-signature P2PK lock." };
|
|
3596
|
+
const lock = assessCashuLock(p.secret, accepted, nowSec);
|
|
3597
|
+
if (lock.kind === "rejected") {
|
|
3598
|
+
if (!lock.locktimeBelowFloor) return { kind: "rejected", code: lock.code, message: lock.message };
|
|
3599
|
+
belowFloorMessage ??= lock.message;
|
|
3600
|
+
continue;
|
|
3435
3601
|
}
|
|
3436
|
-
|
|
3437
|
-
if (!match) {
|
|
3438
|
-
return {
|
|
3439
|
-
kind: "rejected",
|
|
3440
|
-
code: "wrong_lock_pubkey",
|
|
3441
|
-
message: "Proof is not locked to one of this DVM's active lock pubkeys."
|
|
3442
|
-
};
|
|
3443
|
-
}
|
|
3444
|
-
matchedLockPubkeyByIndex.push(match);
|
|
3602
|
+
matchedLockPubkeyByIndex.push(lock.lockPubkey);
|
|
3445
3603
|
tExpireMsByIndex.push(parseNut11LocktimeMs(p.secret));
|
|
3446
3604
|
}
|
|
3605
|
+
if (belowFloorMessage) {
|
|
3606
|
+
const banked = await acceptedUnder(opts, proofs);
|
|
3607
|
+
if (banked.some((id) => id !== void 0)) return replayRejected(banked, requestId);
|
|
3608
|
+
return { kind: "rejected", code: "lock_not_exclusive", message: belowFloorMessage };
|
|
3609
|
+
}
|
|
3447
3610
|
try {
|
|
3448
3611
|
const walletProvider = opts.walletProvider ?? getWallet;
|
|
3449
3612
|
const wallet = await walletProvider(mintUrl);
|
|
@@ -3485,23 +3648,24 @@ async function acceptAccumulatorPayment(opts) {
|
|
|
3485
3648
|
lockPubkeyByIndex: matchedLockPubkeyByIndex,
|
|
3486
3649
|
mintUrl,
|
|
3487
3650
|
proofs,
|
|
3488
|
-
requestId
|
|
3651
|
+
requestId,
|
|
3489
3652
|
tExpireMsByIndex
|
|
3490
3653
|
};
|
|
3491
3654
|
try {
|
|
3492
3655
|
if (opts.disposable) {
|
|
3493
3656
|
const account = opts.withDisposableLedger;
|
|
3494
3657
|
if (!account) throw new Error("Disposable Cashu requires atomic credit accounting.");
|
|
3495
|
-
await opts.disposable.commit(
|
|
3658
|
+
await opts.disposable.commit(requestId, proofs, (ledger) => account(ledger, acceptedValue));
|
|
3496
3659
|
} else if (!opts.db) {
|
|
3497
3660
|
throw new Error("Cashu accumulator requires a storage backend.");
|
|
3498
|
-
} else
|
|
3499
|
-
const withCommitTx = opts.withCommitTx;
|
|
3661
|
+
} else {
|
|
3500
3662
|
const client = await opts.db.connect();
|
|
3501
3663
|
try {
|
|
3502
3664
|
await client.query("BEGIN");
|
|
3665
|
+
await client.query("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
|
|
3666
|
+
await claimAccumulatorRequestId(client, opts.dvmId, requestId);
|
|
3503
3667
|
await insertAccumulatorRows(opts.db, { ...insertArgs, tx: client });
|
|
3504
|
-
await withCommitTx(client, acceptedValue);
|
|
3668
|
+
await opts.withCommitTx?.(client, acceptedValue);
|
|
3505
3669
|
await client.query("COMMIT");
|
|
3506
3670
|
} catch (err) {
|
|
3507
3671
|
try {
|
|
@@ -3512,16 +3676,12 @@ async function acceptAccumulatorPayment(opts) {
|
|
|
3512
3676
|
} finally {
|
|
3513
3677
|
client.release();
|
|
3514
3678
|
}
|
|
3515
|
-
} else {
|
|
3516
|
-
await insertAccumulatorRows(opts.db, insertArgs);
|
|
3517
3679
|
}
|
|
3518
3680
|
} catch (err) {
|
|
3519
3681
|
if (err instanceof AccumulatorReplayError) {
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
message: "Proofs or request id already accepted at this DVM."
|
|
3524
|
-
};
|
|
3682
|
+
const banked = await acceptedUnder(opts, proofs);
|
|
3683
|
+
if (banked.every((id) => id === void 0)) return requestIdReused(requestId);
|
|
3684
|
+
return replayRejected(banked, requestId);
|
|
3525
3685
|
}
|
|
3526
3686
|
throw err;
|
|
3527
3687
|
}
|
|
@@ -3531,7 +3691,33 @@ async function acceptAccumulatorPayment(opts) {
|
|
|
3531
3691
|
grossMsats: valuation.grossMsats,
|
|
3532
3692
|
inputFeeMsats: valuation.inputFeeMsats,
|
|
3533
3693
|
netMsats: valuation.netMsats,
|
|
3534
|
-
mintUrl
|
|
3694
|
+
mintUrl,
|
|
3695
|
+
requestId
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
async function acceptedUnder(opts, proofs) {
|
|
3699
|
+
const secrets = proofs.map((p) => p.secret);
|
|
3700
|
+
if (opts.disposable) return await opts.disposable.acceptedRequestIds(secrets);
|
|
3701
|
+
if (opts.db) return await acceptedRequestIds(opts.db, opts.dvmId, secrets);
|
|
3702
|
+
return secrets.map(() => void 0);
|
|
3703
|
+
}
|
|
3704
|
+
function requestIdReused(requestId) {
|
|
3705
|
+
return {
|
|
3706
|
+
kind: "rejected",
|
|
3707
|
+
code: "request_id_reused",
|
|
3708
|
+
requestId,
|
|
3709
|
+
message: "This request id already carries a different payment at this DVM; these proofs were not accepted."
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
function replayRejected(accepted, requestId) {
|
|
3713
|
+
const [first] = accepted;
|
|
3714
|
+
const bound = first !== void 0 && accepted.every((id) => id === first);
|
|
3715
|
+
return {
|
|
3716
|
+
kind: "rejected",
|
|
3717
|
+
code: "replay_rejected",
|
|
3718
|
+
message: "Proofs or request id already accepted at this DVM.",
|
|
3719
|
+
requestId,
|
|
3720
|
+
...bound && { acceptedRequestId: first }
|
|
3535
3721
|
};
|
|
3536
3722
|
}
|
|
3537
3723
|
function parseNut11LocktimeMs(secret) {
|
|
@@ -3542,6 +3728,9 @@ function parseNut11LocktimeMs(secret) {
|
|
|
3542
3728
|
return null;
|
|
3543
3729
|
}
|
|
3544
3730
|
}
|
|
3731
|
+
function compressedX(raw) {
|
|
3732
|
+
return /^0[23][0-9a-fA-F]{64}$/.test(raw) ? raw.slice(2).toLowerCase() : void 0;
|
|
3733
|
+
}
|
|
3545
3734
|
|
|
3546
3735
|
// src/sdk/server/payment.ts
|
|
3547
3736
|
var UPFRONT_CHALLENGE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -3764,7 +3953,7 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
3764
3953
|
}
|
|
3765
3954
|
};
|
|
3766
3955
|
}
|
|
3767
|
-
if (
|
|
3956
|
+
if (cashuAccumulatorReady(opts)) {
|
|
3768
3957
|
return verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx);
|
|
3769
3958
|
}
|
|
3770
3959
|
return {
|
|
@@ -3928,7 +4117,7 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
3928
4117
|
x402Requirements
|
|
3929
4118
|
});
|
|
3930
4119
|
}
|
|
3931
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4120
|
+
const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
|
|
3932
4121
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
3933
4122
|
x402Payment,
|
|
3934
4123
|
x402Config,
|
|
@@ -4383,6 +4572,8 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
4383
4572
|
opts.fundOnly?.allowOneShotStablecoin,
|
|
4384
4573
|
opts.priceFiat
|
|
4385
4574
|
) : void 0;
|
|
4575
|
+
const lockPubkey = opts.lockPubkeys?.[0];
|
|
4576
|
+
const cashuRequest = opts.nut24 && opts.nut24.mints.length > 0 && !opts.fundOnly && cashuAccumulatorReady(opts) && lockPubkey ? cashuPaymentRequest({ requiredMsats, mints: opts.nut24.mints, lockPubkey }) : void 0;
|
|
4386
4577
|
return {
|
|
4387
4578
|
paidMsats: 0,
|
|
4388
4579
|
receivedProofs: [],
|
|
@@ -4393,6 +4584,7 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
4393
4584
|
...mints && { mints }
|
|
4394
4585
|
},
|
|
4395
4586
|
status: 402,
|
|
4587
|
+
...cashuRequest && { cashuPaymentRequest: cashuRequest },
|
|
4396
4588
|
...mppChallenges && { mppChallenges },
|
|
4397
4589
|
...x402Requirements && { x402Requirements },
|
|
4398
4590
|
...x402BatchRequirements && { x402V2Requirements: x402BatchRequirements }
|
|
@@ -4526,6 +4718,11 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4526
4718
|
hint: "Request a fresh quote and pay again \u2014 the previous payment can't be reused.",
|
|
4527
4719
|
retryable: false
|
|
4528
4720
|
},
|
|
4721
|
+
payment_method_requires_signature: {
|
|
4722
|
+
display: "This service accepts unsigned requests only when they are paid with Cashu, so this payment was not applied to the job.",
|
|
4723
|
+
hint: "Pay with Cashu to call without signing, or sign the request (secp256k1 Schnorr) to use another payment method.",
|
|
4724
|
+
retryable: false
|
|
4725
|
+
},
|
|
4529
4726
|
x402_not_offered: {
|
|
4530
4727
|
display: "This service does not offer x402 payment for prices in its configured currency. Nothing was charged.",
|
|
4531
4728
|
hint: "Use one of the payment methods advertised by this service and submit the request again.",
|
|
@@ -4751,6 +4948,11 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4751
4948
|
hint: "Include the X-Cashu-Request-Id header and pay again.",
|
|
4752
4949
|
retryable: false
|
|
4753
4950
|
},
|
|
4951
|
+
reserved_request_id: {
|
|
4952
|
+
display: "The payment's request identifier isn't allowed, so the payment wasn't processed. Nothing was charged.",
|
|
4953
|
+
hint: 'Send a fresh UUIDv4 as X-Cashu-Request-Id. Ids starting with "nut24-" are reserved for payments sent without one.',
|
|
4954
|
+
retryable: false
|
|
4955
|
+
},
|
|
4754
4956
|
missing_lock_pubkey: {
|
|
4755
4957
|
display: "This service can't accept the payment right now. Nothing was charged.",
|
|
4756
4958
|
hint: "The service is missing payment configuration; retrying won't help. Contact the service operator.",
|
|
@@ -4771,6 +4973,11 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4771
4973
|
hint: "Re-fetch this service's current lock key from /v1/info and lock a fresh token to it.",
|
|
4772
4974
|
retryable: false
|
|
4773
4975
|
},
|
|
4976
|
+
lock_not_exclusive: {
|
|
4977
|
+
display: "The payment token's lock would let a key other than this service's spend it, so it wasn't accepted. Nothing was charged.",
|
|
4978
|
+
hint: "Lock a fresh token with data set to cashu.lock_pubkeys[0] from /v1/info as a 33-byte compressed hex key. Use only the locktime, refund, pubkeys, n_sigs, sigflag and n_sigs_refund tags, each once and with a value: pubkeys may list only other cashu.lock_pubkeys entries, n_sigs must be 1, sigflag SIG_INPUTS, refund keys compressed and distinct, n_sigs_refund at most the number of refund keys, and any locktime at least cashu.min_locktime minus 24 hours in the future.",
|
|
4979
|
+
retryable: false
|
|
4980
|
+
},
|
|
4774
4981
|
proof_not_unspent: {
|
|
4775
4982
|
display: "The payment token has already been spent. Nothing was charged.",
|
|
4776
4983
|
hint: "Those funds are gone; pay with a fresh token.",
|
|
@@ -4812,6 +5019,11 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4812
5019
|
// agent could follow straight into paying twice for the same charge.
|
|
4813
5020
|
hint: "Don't re-send this payment \u2014 the proofs already landed under this request id. Only a genuinely new charge needs a fresh token and request id.",
|
|
4814
5021
|
retryable: false
|
|
5022
|
+
},
|
|
5023
|
+
request_id_reused: {
|
|
5024
|
+
display: "This payment was not accepted and nothing was charged.",
|
|
5025
|
+
hint: "Each X-Cashu-Request-Id belongs to the first payment made under it. Send these proofs again under a new request id.",
|
|
5026
|
+
retryable: false
|
|
4815
5027
|
}
|
|
4816
5028
|
};
|
|
4817
5029
|
function paymentErrorBody(code, opts) {
|
|
@@ -4953,7 +5165,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
|
|
|
4953
5165
|
snapshot
|
|
4954
5166
|
});
|
|
4955
5167
|
}
|
|
4956
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5168
|
+
const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
|
|
4957
5169
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
4958
5170
|
body.content.x402_payment,
|
|
4959
5171
|
opts.x402Config,
|
|
@@ -5404,7 +5616,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
|
|
|
5404
5616
|
try {
|
|
5405
5617
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5406
5618
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5407
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5619
|
+
const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
|
|
5408
5620
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
5409
5621
|
body.content.x402_payment,
|
|
5410
5622
|
opts.x402Config,
|
|
@@ -5529,6 +5741,16 @@ function creditPendingPayment(job) {
|
|
|
5529
5741
|
job.pendingPaymentMsats = void 0;
|
|
5530
5742
|
}
|
|
5531
5743
|
}
|
|
5744
|
+
function cashuAccumulatorReady(opts) {
|
|
5745
|
+
return opts.cashuMode === "p2pk-accumulator" && Boolean(opts.accumulatorDb || opts.disposableCashu) && Boolean(opts.dvmId) && (opts.lockPubkeys?.length ?? 0) > 0;
|
|
5746
|
+
}
|
|
5747
|
+
var NUT24_PAYER_FIXABLE = /* @__PURE__ */ new Set([
|
|
5748
|
+
"invalid_token",
|
|
5749
|
+
"not_p2pk_locked",
|
|
5750
|
+
"wrong_lock_pubkey",
|
|
5751
|
+
"lock_not_exclusive",
|
|
5752
|
+
"payment_insufficient"
|
|
5753
|
+
]);
|
|
5532
5754
|
async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
5533
5755
|
const { cashuToken, accumulatorDb, dvmId, lockPubkeys, mintHealthTracker } = opts;
|
|
5534
5756
|
if (!cashuToken || !accumulatorDb && !opts.disposableCashu || !dvmId || !lockPubkeys || lockPubkeys.length === 0) {
|
|
@@ -5577,7 +5799,13 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5577
5799
|
};
|
|
5578
5800
|
}
|
|
5579
5801
|
let credited;
|
|
5580
|
-
const
|
|
5802
|
+
const headerless = !opts.requestId && opts.nut24 !== void 0;
|
|
5803
|
+
const previousRequired = headerless ? opts.nut24?.previousRequiredMsats : void 0;
|
|
5804
|
+
const requiredFor = (paidMsats) => requiredMsats !== void 0 && paidMsats < requiredMsats && previousRequired !== void 0 && paidMsats >= previousRequired ? previousRequired : requiredMsats;
|
|
5805
|
+
const held = (ctx, paidMsats) => ({
|
|
5806
|
+
...ctx,
|
|
5807
|
+
requiredMsats: requiredFor(paidMsats) ?? ctx.requiredMsats
|
|
5808
|
+
});
|
|
5581
5809
|
const signedFund = ledgerCtx?.creditEnvelope?.fund;
|
|
5582
5810
|
const beforeCommit = ledgerCtx && signedFund ? (accepted) => {
|
|
5583
5811
|
const expected = fundedMicroFor(
|
|
@@ -5589,12 +5817,12 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5589
5817
|
throw new CashuFundingAmountMismatchError(signedFund.amountMicro, expected);
|
|
5590
5818
|
}
|
|
5591
5819
|
} : void 0;
|
|
5592
|
-
const withCommitTx = ledgerCtx
|
|
5820
|
+
const withCommitTx = ledgerCtx ? async (tx, accepted) => {
|
|
5593
5821
|
credited = await fundAndDraw(
|
|
5594
|
-
ledgerCtx,
|
|
5822
|
+
held(ledgerCtx, accepted.paidMsats),
|
|
5595
5823
|
{
|
|
5596
5824
|
rail: "cashu",
|
|
5597
|
-
paymentId: requestId,
|
|
5825
|
+
paymentId: accepted.requestId,
|
|
5598
5826
|
paidMsats: accepted.paidMsats,
|
|
5599
5827
|
grossPaidMsats: accepted.grossMsats,
|
|
5600
5828
|
cashuInputFeeMsats: accepted.inputFeeMsats,
|
|
@@ -5608,13 +5836,14 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5608
5836
|
result = await acceptAccumulatorPayment({
|
|
5609
5837
|
cashuToken,
|
|
5610
5838
|
requestId: opts.requestId,
|
|
5839
|
+
deriveRequestId: headerless,
|
|
5611
5840
|
mints,
|
|
5612
5841
|
db: accumulatorDb,
|
|
5613
5842
|
disposable: opts.disposableCashu,
|
|
5614
|
-
withDisposableLedger: ledgerCtx
|
|
5615
|
-
credited = await fundAndDraw({ ...ledgerCtx, ledger }, {
|
|
5843
|
+
withDisposableLedger: ledgerCtx ? async (ledger, accepted) => {
|
|
5844
|
+
credited = await fundAndDraw({ ...held(ledgerCtx, accepted.paidMsats), ledger }, {
|
|
5616
5845
|
rail: "cashu",
|
|
5617
|
-
paymentId: requestId,
|
|
5846
|
+
paymentId: accepted.requestId,
|
|
5618
5847
|
paidMsats: accepted.paidMsats,
|
|
5619
5848
|
grossPaidMsats: accepted.grossMsats,
|
|
5620
5849
|
cashuInputFeeMsats: accepted.inputFeeMsats,
|
|
@@ -5638,24 +5867,35 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5638
5867
|
throw err;
|
|
5639
5868
|
}
|
|
5640
5869
|
if (result.kind === "rejected") {
|
|
5641
|
-
const status = result.code === "replay_rejected" ? 409 : 402;
|
|
5870
|
+
const status = result.code === "replay_rejected" || result.code === "request_id_reused" ? 409 : result.code === "reserved_request_id" || headerless && NUT24_PAYER_FIXABLE.has(result.code) ? 400 : 402;
|
|
5871
|
+
const requestId = result.requestId;
|
|
5642
5872
|
return {
|
|
5643
5873
|
paidMsats: 0,
|
|
5644
5874
|
receivedProofs: [],
|
|
5645
5875
|
error: {
|
|
5646
|
-
body: paymentErrorBody(result.code, {
|
|
5876
|
+
body: paymentErrorBody(result.code, {
|
|
5877
|
+
message: result.message,
|
|
5878
|
+
// Without a request id the proof set is the payment's identity, so
|
|
5879
|
+
// only these exact proofs with the original body recover its job.
|
|
5880
|
+
...headerless && result.code === "replay_rejected" && {
|
|
5881
|
+
hint: "These proofs already paid for an earlier request. Resend them with that request's exact body to fetch its job; a new request needs new proofs."
|
|
5882
|
+
},
|
|
5883
|
+
extra: { mints }
|
|
5884
|
+
}),
|
|
5647
5885
|
status,
|
|
5648
|
-
// The
|
|
5649
|
-
//
|
|
5650
|
-
//
|
|
5651
|
-
|
|
5886
|
+
// The request id is also the funding id a mid-job top-up committed
|
|
5887
|
+
// under and the job's payment reference, so the route can return the
|
|
5888
|
+
// original evidence beside the 409. It is passed on only when every
|
|
5889
|
+
// presented proof was accepted under this same request id.
|
|
5890
|
+
...requestId && result.acceptedRequestId === requestId && { replayPaymentId: requestId }
|
|
5652
5891
|
}
|
|
5653
5892
|
};
|
|
5654
5893
|
}
|
|
5655
5894
|
if (credited?.drawRefusal) {
|
|
5656
5895
|
return creditLedgerErrorResult(credited.drawRefusal, void 0, credited.deposit);
|
|
5657
5896
|
}
|
|
5658
|
-
|
|
5897
|
+
const heldRequired = requiredFor(result.paidMsats);
|
|
5898
|
+
if (heldRequired && result.paidMsats < heldRequired && !ledgerCtx?.fundOnly && !ledgerCtx?.creditEnvelope) {
|
|
5659
5899
|
let forfeit;
|
|
5660
5900
|
if (credited?.draw && ledgerCtx && !ledgerCtx.midJob) {
|
|
5661
5901
|
const drawId = credited.draw.drawId;
|
|
@@ -5677,36 +5917,39 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5677
5917
|
});
|
|
5678
5918
|
}
|
|
5679
5919
|
}
|
|
5680
|
-
const
|
|
5920
|
+
const held2 = ledgerCtx?.midJob ? credited : void 0;
|
|
5681
5921
|
return {
|
|
5682
5922
|
paidMsats: 0,
|
|
5683
5923
|
receivedProofs: [],
|
|
5684
5924
|
error: {
|
|
5685
5925
|
body: paymentErrorBody("payment_insufficient", {
|
|
5686
5926
|
message: `Payment of ${result.paidMsats} msats is short of the ${requiredMsats} msats required.`,
|
|
5687
|
-
...
|
|
5927
|
+
...held2 && {
|
|
5688
5928
|
// Per-code copy can't say where the money went, and this is the one
|
|
5689
5929
|
// site where that is the caller's next action.
|
|
5690
|
-
hint: `The ${result.paidMsats} msats already sent are held on credit ${
|
|
5930
|
+
hint: `The ${result.paidMsats} msats already sent are held on credit ${held2.creditId} and do not reduce what is owed \u2014 retry with a single payment of required_msats, or reclaim the held balance with op "drain" at POST /v1/credit.`
|
|
5931
|
+
},
|
|
5932
|
+
...!held2 && headerless && {
|
|
5933
|
+
hint: "The proofs already sent are kept and do not count toward this request. Pay again with new proofs worth required_msats after the mint's input fee."
|
|
5691
5934
|
},
|
|
5692
5935
|
extra: {
|
|
5693
5936
|
required_msats: requiredMsats,
|
|
5694
5937
|
received_msats: result.paidMsats,
|
|
5695
5938
|
mints,
|
|
5696
|
-
...
|
|
5697
|
-
credit_id:
|
|
5698
|
-
credited_micro:
|
|
5699
|
-
credit_currency:
|
|
5939
|
+
...held2 && {
|
|
5940
|
+
credit_id: held2.creditId,
|
|
5941
|
+
credited_micro: held2.deposit.amountMicro,
|
|
5942
|
+
credit_currency: held2.deposit.currency
|
|
5700
5943
|
}
|
|
5701
5944
|
}
|
|
5702
5945
|
}),
|
|
5703
|
-
status: 402,
|
|
5946
|
+
status: headerless ? 400 : 402,
|
|
5704
5947
|
forfeit,
|
|
5705
5948
|
// Report the deposit on the failure path too, or the platform never
|
|
5706
5949
|
// learns about a liability it is carrying. Mutually exclusive with
|
|
5707
5950
|
// `forfeit` by construction: one is money still owed, the other is
|
|
5708
5951
|
// money nothing is owed against.
|
|
5709
|
-
...
|
|
5952
|
+
...held2 && { funding: held2.deposit }
|
|
5710
5953
|
}
|
|
5711
5954
|
};
|
|
5712
5955
|
}
|
|
@@ -5716,13 +5959,11 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5716
5959
|
paymentMint: result.mintUrl,
|
|
5717
5960
|
paymentRail: "cashu",
|
|
5718
5961
|
receivedProofs: [],
|
|
5719
|
-
//
|
|
5720
|
-
//
|
|
5721
|
-
//
|
|
5722
|
-
//
|
|
5723
|
-
|
|
5724
|
-
// it serves as a stable id even though it isn't an on-chain hash.
|
|
5725
|
-
paymentTxHash: opts.requestId,
|
|
5962
|
+
// The request id the proofs were banked under, supplied or derived, is
|
|
5963
|
+
// the per-call settlement reference for revenue_events.tx_hash. Unique
|
|
5964
|
+
// per (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint,
|
|
5965
|
+
// so it serves as a stable id even though it isn't an on-chain hash.
|
|
5966
|
+
paymentTxHash: result.requestId,
|
|
5726
5967
|
cashuFlow: "p2pk_accumulator"
|
|
5727
5968
|
},
|
|
5728
5969
|
ledgerCtx,
|
|
@@ -5798,7 +6039,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
|
|
|
5798
6039
|
draw_id: snapshot.drawId,
|
|
5799
6040
|
...extra,
|
|
5800
6041
|
...typeof opts.jobId === "string" && {
|
|
5801
|
-
job_id_hash:
|
|
6042
|
+
job_id_hash: createHash5("sha256").update(opts.jobId, "utf8").digest("hex")
|
|
5802
6043
|
}
|
|
5803
6044
|
}
|
|
5804
6045
|
);
|
|
@@ -9501,7 +9742,7 @@ async function handle(c, opts, action) {
|
|
|
9501
9742
|
}
|
|
9502
9743
|
|
|
9503
9744
|
// src/sdk/server/x402-exact-settlement.ts
|
|
9504
|
-
import { createHash as
|
|
9745
|
+
import { createHash as createHash6 } from "crypto";
|
|
9505
9746
|
var X402ExactSettlementServer = class {
|
|
9506
9747
|
constructor(opts) {
|
|
9507
9748
|
this.opts = opts;
|
|
@@ -9827,7 +10068,7 @@ function settlementMayHaveMoved(response) {
|
|
|
9827
10068
|
return reason.includes("authorization_already_used") || reason.includes("authorization already used") || /facilitator_status_5\d\d/.test(reason);
|
|
9828
10069
|
}
|
|
9829
10070
|
function hash(parts) {
|
|
9830
|
-
return
|
|
10071
|
+
return createHash6("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
9831
10072
|
}
|
|
9832
10073
|
function safeError(err) {
|
|
9833
10074
|
if (err instanceof Error) return err.name;
|
|
@@ -9835,7 +10076,7 @@ function safeError(err) {
|
|
|
9835
10076
|
}
|
|
9836
10077
|
|
|
9837
10078
|
// src/sdk/server/hosting-settlement-report-obligations.ts
|
|
9838
|
-
import { createHash as
|
|
10079
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
|
|
9839
10080
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
9840
10081
|
import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS3 } from "@x402/evm";
|
|
9841
10082
|
import { computeChannelId as computeChannelId2 } from "@x402/evm/batch-settlement/client";
|
|
@@ -9908,7 +10149,7 @@ var TEMPO_CLOSED_EVENT = TempoSession.Precompile.escrowAbi.find(
|
|
|
9908
10149
|
(item) => item.type === "event" && item.name === "ChannelClosed"
|
|
9909
10150
|
);
|
|
9910
10151
|
function hash2(value) {
|
|
9911
|
-
return
|
|
10152
|
+
return createHash7("sha256").update(JSON.stringify(value)).digest("hex");
|
|
9912
10153
|
}
|
|
9913
10154
|
function hex(value, bytes) {
|
|
9914
10155
|
return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
|
|
@@ -11296,7 +11537,7 @@ var HostingCleanupFence = class {
|
|
|
11296
11537
|
};
|
|
11297
11538
|
|
|
11298
11539
|
// src/sdk/server/hosting-channel-evidence.ts
|
|
11299
|
-
import { createHash as
|
|
11540
|
+
import { createHash as createHash8 } from "crypto";
|
|
11300
11541
|
import { Session as TempoSession2 } from "mppx/tempo";
|
|
11301
11542
|
import { createPublicClient as createPublicClient5, http as http5 } from "viem";
|
|
11302
11543
|
import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS4 } from "@x402/evm";
|
|
@@ -11347,7 +11588,7 @@ async function readHostedChannelSnapshot(query) {
|
|
|
11347
11588
|
};
|
|
11348
11589
|
const x402HistoryComplete = await history(names[6]);
|
|
11349
11590
|
const tempoHistoryComplete = await history(names[7]);
|
|
11350
|
-
const digest =
|
|
11591
|
+
const digest = createHash8("sha256").update(JSON.stringify({
|
|
11351
11592
|
present: [...exists].sort(),
|
|
11352
11593
|
x402,
|
|
11353
11594
|
x402Tombstones,
|
|
@@ -12497,9 +12738,9 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
12497
12738
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
12498
12739
|
|
|
12499
12740
|
// src/sdk/server/instrumented-kv.ts
|
|
12500
|
-
import { createHash as
|
|
12741
|
+
import { createHash as createHash9 } from "crypto";
|
|
12501
12742
|
function hashKey(key) {
|
|
12502
|
-
return
|
|
12743
|
+
return createHash9("sha256").update(key).digest("hex").slice(0, 16);
|
|
12503
12744
|
}
|
|
12504
12745
|
function instrumentKVStore(inner) {
|
|
12505
12746
|
return {
|
|
@@ -12813,7 +13054,7 @@ function buildContext(job, opts) {
|
|
|
12813
13054
|
let x402Nonce;
|
|
12814
13055
|
let x402AmountUsdcMicro;
|
|
12815
13056
|
const hasExplicitX402 = Boolean(payOpts?.x402);
|
|
12816
|
-
if ((opts.pricingCurrency ?? "usd") === "usd") {
|
|
13057
|
+
if (!opts.cashuOnly && (opts.pricingCurrency ?? "usd") === "usd") {
|
|
12817
13058
|
if (payOpts?.x402) {
|
|
12818
13059
|
paymentRequestContent.x402 = payOpts.x402;
|
|
12819
13060
|
} else if (opts.paymentMethods?.includes("x402") && opts.x402 && (opts.x402FacilitatorHealth?.support().available ?? true)) {
|
|
@@ -12852,7 +13093,8 @@ function buildContext(job, opts) {
|
|
|
12852
13093
|
job.pendingX402Nonce = x402Nonce;
|
|
12853
13094
|
job.pendingX402AmountUsdcMicro = x402AmountUsdcMicro;
|
|
12854
13095
|
let mppChallengeIds;
|
|
12855
|
-
if (
|
|
13096
|
+
if (opts.cashuOnly) {
|
|
13097
|
+
} else if (payOpts?.mpp) {
|
|
12856
13098
|
const challenges = payOpts.mpp.challenges.filter((c) => c.intent !== "session");
|
|
12857
13099
|
if (challenges.length > 0) {
|
|
12858
13100
|
paymentRequestContent.tempo = { challenges };
|
|
@@ -13260,6 +13502,39 @@ function resultHashFor(record) {
|
|
|
13260
13502
|
return computeResultHash(summary, artifacts);
|
|
13261
13503
|
}
|
|
13262
13504
|
|
|
13505
|
+
// src/sdk/server/unsigned-request.ts
|
|
13506
|
+
function takesUnsignedPath(auth, body) {
|
|
13507
|
+
if (!auth?.allowUnsignedPaid) return false;
|
|
13508
|
+
const raw = signedRequestInput(body);
|
|
13509
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return true;
|
|
13510
|
+
return !SIGNED_ENVELOPE_FIELDS.some((field) => field in raw);
|
|
13511
|
+
}
|
|
13512
|
+
function carriesCreditFields(raw) {
|
|
13513
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
13514
|
+
return CREDIT_ENVELOPE_KEYS.some((field) => field in raw);
|
|
13515
|
+
}
|
|
13516
|
+
function unsignedInputSchema(schema) {
|
|
13517
|
+
if (narrowed.has(schema)) return narrowed.get(schema);
|
|
13518
|
+
const result = narrow(schema);
|
|
13519
|
+
narrowed.set(schema, result);
|
|
13520
|
+
return result;
|
|
13521
|
+
}
|
|
13522
|
+
var narrowed = /* @__PURE__ */ new WeakMap();
|
|
13523
|
+
function narrow(schema) {
|
|
13524
|
+
const candidate = schema;
|
|
13525
|
+
const def = candidate._zod?.def;
|
|
13526
|
+
if (def?.type !== "object" || typeof candidate.clone !== "function" || candidate.shape === null || typeof candidate.shape !== "object") {
|
|
13527
|
+
return void 0;
|
|
13528
|
+
}
|
|
13529
|
+
const shape = { ...candidate.shape };
|
|
13530
|
+
for (const field of SIGNED_ENVELOPE_FIELDS) delete shape[field];
|
|
13531
|
+
try {
|
|
13532
|
+
return candidate.clone({ ...def, shape });
|
|
13533
|
+
} catch {
|
|
13534
|
+
return void 0;
|
|
13535
|
+
}
|
|
13536
|
+
}
|
|
13537
|
+
|
|
13263
13538
|
// src/lib/cashu/mint-info.ts
|
|
13264
13539
|
async function getKeysetFinalExpiry(mintUrl, keysetId) {
|
|
13265
13540
|
try {
|
|
@@ -13723,11 +13998,12 @@ var JobManager = class {
|
|
|
13723
13998
|
* `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
|
|
13724
13999
|
* Returns `undefined` for dynamic-priced capabilities (those that declare
|
|
13725
14000
|
* `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
|
|
14001
|
+
* A caller that already holds the snapshot it prices at passes it as `fx`.
|
|
13726
14002
|
*/
|
|
13727
|
-
async currentPriceMsats(capability) {
|
|
14003
|
+
async currentPriceMsats(capability, fx) {
|
|
13728
14004
|
const cap = this.requireCapability(capability);
|
|
13729
14005
|
if (cap.price === void 0) return void 0;
|
|
13730
|
-
return resolveStaticPriceMsats(cap.price, this.opts.fxFetcher);
|
|
14006
|
+
return resolveStaticPriceMsats(cap.price, fx ?? await this.opts.fxFetcher.fetch());
|
|
13731
14007
|
}
|
|
13732
14008
|
/**
|
|
13733
14009
|
* Translate a capability's static `price` into the fiat envelope used by
|
|
@@ -13760,12 +14036,16 @@ var JobManager = class {
|
|
|
13760
14036
|
* explicit draw as an unknown key, before the envelope was even extracted.
|
|
13761
14037
|
* The persisted `record.input` the reactivation path re-reads is the
|
|
13762
14038
|
* pre-Zod wire form, so it comes through the second branch carrying them too.
|
|
14039
|
+
*
|
|
14040
|
+
* `unsigned` validates against the schema without its envelope fields; pass
|
|
14041
|
+
* it only when {@link unsignedPaidInput} accepted the same body.
|
|
13763
14042
|
*/
|
|
13764
|
-
parseInput(body, capability) {
|
|
14043
|
+
parseInput(body, capability, unsigned2 = false) {
|
|
13765
14044
|
const cap = this.requireCapability(capability);
|
|
13766
14045
|
const rawSchema = cap.input;
|
|
13767
14046
|
if (!rawSchema || !isZodSchema(rawSchema)) return body.input ?? "";
|
|
13768
|
-
const schema = rawSchema;
|
|
14047
|
+
const schema = unsigned2 ? unsignedInputSchema(rawSchema) : rawSchema;
|
|
14048
|
+
if (!schema) throw new Error(`capability "${capability}" cannot validate unsigned input`);
|
|
13769
14049
|
if (body.data !== void 0) {
|
|
13770
14050
|
return schema.parse(stripProtocolEnvelope(schema, body.data));
|
|
13771
14051
|
}
|
|
@@ -13775,6 +14055,16 @@ var JobManager = class {
|
|
|
13775
14055
|
}
|
|
13776
14056
|
throw new MissingStructuredInputError();
|
|
13777
14057
|
}
|
|
14058
|
+
/**
|
|
14059
|
+
* Whether this body takes the unsigned paid path: the auth scheme allows it,
|
|
14060
|
+
* the raw input carries no envelope field, and the capability schema can be
|
|
14061
|
+
* narrowed to exclude the envelope. Otherwise the signed path applies.
|
|
14062
|
+
*/
|
|
14063
|
+
unsignedPaidInput(body, capability) {
|
|
14064
|
+
if (!takesUnsignedPath(this.descriptor.auth, body)) return false;
|
|
14065
|
+
const rawSchema = this.requireCapability(capability).input;
|
|
14066
|
+
return isZodSchema(rawSchema) && unsignedInputSchema(rawSchema) !== void 0;
|
|
14067
|
+
}
|
|
13778
14068
|
/**
|
|
13779
14069
|
* Look up the per-capability descriptor by name. Throws when the
|
|
13780
14070
|
* name doesn't exist — the route layer validates body.capability first, so
|
|
@@ -15261,8 +15551,9 @@ var JobManager = class {
|
|
|
15261
15551
|
* it re-verified clean.
|
|
15262
15552
|
*/
|
|
15263
15553
|
reactivateJob(record) {
|
|
15264
|
-
const
|
|
15265
|
-
|
|
15554
|
+
const unsigned2 = record.authRequestPath === void 0 && record.requesterPubkey === void 0 && this.unsignedPaidInput({ input: record.input }, record.capability);
|
|
15555
|
+
const parsedInput = this.parseInput({ input: record.input }, record.capability, unsigned2);
|
|
15556
|
+
if (this.descriptor.auth && !unsigned2) {
|
|
15266
15557
|
const cap = this.requireCapability(record.capability);
|
|
15267
15558
|
if (!cap.input || !isZodSchema(cap.input)) {
|
|
15268
15559
|
throw new ReactivationAuthSchemaError(record.id, record.capability);
|
|
@@ -15806,6 +16097,7 @@ var JobManager = class {
|
|
|
15806
16097
|
store: this.opts.store,
|
|
15807
16098
|
env: this.opts.env,
|
|
15808
16099
|
paymentMethods: this.opts.paymentMethods,
|
|
16100
|
+
cashuOnly: Boolean(this.descriptor.auth?.allowUnsignedPaid) && job.requesterPubkey === void 0,
|
|
15809
16101
|
x402: this.opts.x402,
|
|
15810
16102
|
x402FacilitatorHealth: this.opts.x402FacilitatorHealth,
|
|
15811
16103
|
mpp: this.opts.mpp,
|
|
@@ -16610,9 +16902,8 @@ function unappliedCopy(reason, creditId) {
|
|
|
16610
16902
|
}
|
|
16611
16903
|
}[reason];
|
|
16612
16904
|
}
|
|
16613
|
-
|
|
16905
|
+
function resolveStaticPriceMsats(price, fx) {
|
|
16614
16906
|
const usd = parseUsdPrice(price, "price");
|
|
16615
|
-
const fx = await fxFetcher.fetch();
|
|
16616
16907
|
return fiatToSatsCeil(usd, fxRateFor(fx, "usd")) * 1e3;
|
|
16617
16908
|
}
|
|
16618
16909
|
function deriveAuthFromParsedInput(descriptor, parsedInput) {
|
|
@@ -17678,7 +17969,7 @@ function withWallClockDeadline(promise, timeoutMs) {
|
|
|
17678
17969
|
}
|
|
17679
17970
|
|
|
17680
17971
|
// src/sdk/server/balance-payment.ts
|
|
17681
|
-
import { createHash as
|
|
17972
|
+
import { createHash as createHash10 } from "crypto";
|
|
17682
17973
|
import { z as z2 } from "zod";
|
|
17683
17974
|
var BALANCE_PAYMENT_SCHEMA = z2.object({
|
|
17684
17975
|
credit_id: z2.string().min(1).max(256),
|
|
@@ -17708,7 +17999,7 @@ async function commitBalancePayment(args) {
|
|
|
17708
17999
|
args.auth.recheckSignature(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
|
|
17709
18000
|
const auth = BALANCE_PAYMENT_SCHEMA.parse(args.raw);
|
|
17710
18001
|
const { pubkey, credit_id, draw_id, payment_id, request_seq, amount_micro, currency } = auth;
|
|
17711
|
-
const fingerprint =
|
|
18002
|
+
const fingerprint = createHash10("sha256").update(JSON.stringify([
|
|
17712
18003
|
args.domain,
|
|
17713
18004
|
args.jobId,
|
|
17714
18005
|
pubkey,
|
|
@@ -17832,7 +18123,7 @@ async function commitBalancePayment(args) {
|
|
|
17832
18123
|
}
|
|
17833
18124
|
|
|
17834
18125
|
// src/sdk/server/app.ts
|
|
17835
|
-
import { createHash as
|
|
18126
|
+
import { createHash as createHash12, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
|
|
17836
18127
|
import { Hono } from "hono";
|
|
17837
18128
|
import { getCookie } from "hono/cookie";
|
|
17838
18129
|
import { cors } from "hono/cors";
|
|
@@ -20883,7 +21174,7 @@ initSchemaForm();
|
|
|
20883
21174
|
}
|
|
20884
21175
|
|
|
20885
21176
|
// src/sdk/server/request-fingerprint.ts
|
|
20886
|
-
import { createHash as
|
|
21177
|
+
import { createHash as createHash11 } from "crypto";
|
|
20887
21178
|
function requestFingerprint(req, opts) {
|
|
20888
21179
|
const canonical = canonicalize({
|
|
20889
21180
|
capability: req.capability,
|
|
@@ -20891,7 +21182,7 @@ function requestFingerprint(req, opts) {
|
|
|
20891
21182
|
params: req.params ?? {},
|
|
20892
21183
|
data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
|
|
20893
21184
|
});
|
|
20894
|
-
return
|
|
21185
|
+
return createHash11("sha256").update(canonical).digest("hex");
|
|
20895
21186
|
}
|
|
20896
21187
|
var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
|
|
20897
21188
|
function stripEnvelope(data) {
|
|
@@ -21014,7 +21305,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
21014
21305
|
}
|
|
21015
21306
|
}
|
|
21016
21307
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
21017
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
21308
|
+
const { RevenueReporter } = await import("./revenue-reporter-6XARUEG6.js");
|
|
21018
21309
|
const reporter = new RevenueReporter(
|
|
21019
21310
|
opts.db,
|
|
21020
21311
|
platformUrl,
|
|
@@ -21024,7 +21315,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
21024
21315
|
revenueReporter = reporter;
|
|
21025
21316
|
await reporter.init();
|
|
21026
21317
|
reporter.startRetryLoop();
|
|
21027
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
21318
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5QAHCYIO.js");
|
|
21028
21319
|
payoutReporter = new PayoutReporter({
|
|
21029
21320
|
dvmId,
|
|
21030
21321
|
db: opts.db,
|
|
@@ -21305,7 +21596,9 @@ var DVMServer = class {
|
|
|
21305
21596
|
this.consumedCredentialStore = opts.consumedCredentialStore ?? new MemoryConsumedCredentialStore();
|
|
21306
21597
|
this.adminCashuNonceStore = opts.adminCashuNonceStore ?? createAdminCashuNonceStore();
|
|
21307
21598
|
const fxEnv = opts.env;
|
|
21308
|
-
this.fxFetcher =
|
|
21599
|
+
this.fxFetcher = withFxHistory(
|
|
21600
|
+
opts.fxFetcher ?? createFxFetcher({ source: resolveFxSourceFromEnv(fxEnv) })
|
|
21601
|
+
);
|
|
21309
21602
|
const fxCurrencies = this.fxFetcher.supportedCurrencies();
|
|
21310
21603
|
if (!fxCurrencies.includes(this.pricingCurrency)) {
|
|
21311
21604
|
throw new Error(
|
|
@@ -21414,6 +21707,15 @@ var DVMServer = class {
|
|
|
21414
21707
|
this.app = new Hono();
|
|
21415
21708
|
try {
|
|
21416
21709
|
this.registerRoutes();
|
|
21710
|
+
if (opts.mints?.length && !this.resolvePaymentMethods().includes("cashu")) {
|
|
21711
|
+
diagnosticConsole.warn({
|
|
21712
|
+
event: "cashu_not_advertised",
|
|
21713
|
+
cashu_explicitly_excluded: opts.paymentMethods !== void 0 && !opts.paymentMethods.includes("cashu"),
|
|
21714
|
+
cashu_mode_missing: opts.cashuMode !== "p2pk-accumulator",
|
|
21715
|
+
cashu_lock_pubkey_missing: !opts.lockPubkey,
|
|
21716
|
+
cashu_store_missing: !opts.db && !opts.disposableCashu
|
|
21717
|
+
});
|
|
21718
|
+
}
|
|
21417
21719
|
if (opts.hostingControlInstance) {
|
|
21418
21720
|
this.hostingSweepTimer = setInterval(() => {
|
|
21419
21721
|
void this.sweepHostingDeadline().catch((error) => {
|
|
@@ -21623,7 +21925,7 @@ var DVMServer = class {
|
|
|
21623
21925
|
})
|
|
21624
21926
|
);
|
|
21625
21927
|
if (this.opts.devMode) {
|
|
21626
|
-
this.app.use("*", cors());
|
|
21928
|
+
this.app.use("*", cors({ exposeHeaders: ["X-Cashu"] }));
|
|
21627
21929
|
this.app.get("/_dev", (c) => c.html(devPageHtml(this.descriptor.name)));
|
|
21628
21930
|
this.app.all("/_proxy/*", (c) => this.handleProxy(c));
|
|
21629
21931
|
}
|
|
@@ -21937,7 +22239,7 @@ var DVMServer = class {
|
|
|
21937
22239
|
const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
|
|
21938
22240
|
return {
|
|
21939
22241
|
dvm_id: dvmId,
|
|
21940
|
-
builder_pubkey:
|
|
22242
|
+
builder_pubkey: createHash12("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
|
|
21941
22243
|
};
|
|
21942
22244
|
}
|
|
21943
22245
|
throw new AuthAudienceError();
|
|
@@ -22035,7 +22337,7 @@ var DVMServer = class {
|
|
|
22035
22337
|
const methods = this.resolvePaymentMethods().filter(
|
|
22036
22338
|
(method) => method !== "tempo" || creditMpp
|
|
22037
22339
|
);
|
|
22038
|
-
if (this.x402CurrencySupported() && this.x402BatchAvailable() && !methods.includes("x402")) {
|
|
22340
|
+
if ((this.opts.paymentMethods === void 0 || this.opts.paymentMethods.includes("x402")) && this.x402CurrencySupported() && this.x402BatchAvailable() && !methods.includes("x402")) {
|
|
22039
22341
|
methods.push("x402");
|
|
22040
22342
|
}
|
|
22041
22343
|
if (this.opts.lightningReceive?.available()) methods.push("lightning");
|
|
@@ -22058,7 +22360,9 @@ var DVMServer = class {
|
|
|
22058
22360
|
* distinction, and a reason it can relay.
|
|
22059
22361
|
*/
|
|
22060
22362
|
creditMppSplit() {
|
|
22061
|
-
if (!this.opts.mpp)
|
|
22363
|
+
if (!this.opts.mpp || !this.resolvePaymentMethods().includes("tempo")) {
|
|
22364
|
+
return { offered: [], withheld: [] };
|
|
22365
|
+
}
|
|
22062
22366
|
const configured = advertisedMethods(this.opts.mpp).filter(
|
|
22063
22367
|
(method) => method.intent === "charge" || method.intent === "session"
|
|
22064
22368
|
).sort(
|
|
@@ -22290,7 +22594,7 @@ var DVMServer = class {
|
|
|
22290
22594
|
const paymentAuthorization = ctx.req.header("Authorization");
|
|
22291
22595
|
const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
|
|
22292
22596
|
const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
|
|
22293
|
-
const paymentArtifactHash = attachedPayment ?
|
|
22597
|
+
const paymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
22294
22598
|
hostingFundingArtifactHash = paymentArtifactHash;
|
|
22295
22599
|
if (!await control.admit(admissionId, workId, {
|
|
22296
22600
|
creditId,
|
|
@@ -22390,7 +22694,7 @@ var DVMServer = class {
|
|
|
22390
22694
|
name: this.descriptor.name,
|
|
22391
22695
|
description: this.descriptor.description || void 0,
|
|
22392
22696
|
tags: this.descriptor.tags,
|
|
22393
|
-
auth: this.descriptor.auth ? [this.descriptor.auth.
|
|
22697
|
+
auth: !this.descriptor.auth ? ["none"] : this.descriptor.auth.allowUnsignedPaid ? [this.descriptor.auth.id, "none"] : [this.descriptor.auth.id],
|
|
22394
22698
|
...this.authAudience ? { auth_audience: this.authAudience } : {},
|
|
22395
22699
|
protocol_version: 1,
|
|
22396
22700
|
sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
|
|
@@ -22409,7 +22713,7 @@ var DVMServer = class {
|
|
|
22409
22713
|
if (this.opts.mints) info.mints = this.advertisedMints();
|
|
22410
22714
|
info.capabilities = this.renderCapabilities();
|
|
22411
22715
|
info.payment = { methods: this.resolvePaymentMethods() };
|
|
22412
|
-
if (
|
|
22716
|
+
if (info.payment.methods.includes("x402") && this.opts.x402 && this.x402Support()?.available) {
|
|
22413
22717
|
info.x402 = {
|
|
22414
22718
|
pay_to: this.opts.x402.payTo,
|
|
22415
22719
|
network: this.opts.x402.network,
|
|
@@ -22419,7 +22723,7 @@ var DVMServer = class {
|
|
|
22419
22723
|
dual_serve: this.opts.x402.dualServe ?? true
|
|
22420
22724
|
};
|
|
22421
22725
|
}
|
|
22422
|
-
if (this.opts.mpp) {
|
|
22726
|
+
if (info.payment.methods.includes("tempo") && this.opts.mpp) {
|
|
22423
22727
|
const configured = advertisedMethods(this.opts.mpp);
|
|
22424
22728
|
const withheld = configured.flatMap((method) => {
|
|
22425
22729
|
const reason = this.tempoSessionWithheldReason(method);
|
|
@@ -22455,7 +22759,7 @@ var DVMServer = class {
|
|
|
22455
22759
|
mints,
|
|
22456
22760
|
lock_pubkeys: activePubkeySet(active),
|
|
22457
22761
|
lock_pubkey_grace_seconds: this.graceSeconds(),
|
|
22458
|
-
min_locktime:
|
|
22762
|
+
min_locktime: CASHU_MIN_LOCKTIME_SECONDS,
|
|
22459
22763
|
canonical_id: this.opts.canonicalDvmId ?? this.opts.dvmId ?? null
|
|
22460
22764
|
};
|
|
22461
22765
|
}
|
|
@@ -22794,6 +23098,48 @@ var DVMServer = class {
|
|
|
22794
23098
|
400
|
|
22795
23099
|
);
|
|
22796
23100
|
}
|
|
23101
|
+
/**
|
|
23102
|
+
* Refuse an x402 or Tempo mid-job payment on an unsigned paid job before it
|
|
23103
|
+
* is recorded or verified: those rails settle into credit owned by a caller
|
|
23104
|
+
* pubkey, which the job lacks. Signed jobs always store their pubkey.
|
|
23105
|
+
*/
|
|
23106
|
+
unsignedJobRailRefusal(job, body) {
|
|
23107
|
+
if (!this.descriptor.auth?.allowUnsignedPaid || job.requesterPubkey !== void 0) return void 0;
|
|
23108
|
+
if (body.type !== "payment" || !body.content.x402_payment && !body.content.tempo_credential) {
|
|
23109
|
+
return void 0;
|
|
23110
|
+
}
|
|
23111
|
+
return buildPaymentErrorResponse(
|
|
23112
|
+
paymentErrorBody("payment_method_requires_signature", {
|
|
23113
|
+
message: "Unsigned jobs may be paid only with Cashu; an x402 or Tempo payment was sent."
|
|
23114
|
+
}),
|
|
23115
|
+
void 0,
|
|
23116
|
+
void 0,
|
|
23117
|
+
402
|
|
23118
|
+
);
|
|
23119
|
+
}
|
|
23120
|
+
/** Whether this DVM takes Cashu, the only rail an unsigned payer may use. */
|
|
23121
|
+
unsignedCashuOffered() {
|
|
23122
|
+
return (this.opts.mints?.length ?? 0) > 0 && this.resolvePaymentMethods().includes("cashu");
|
|
23123
|
+
}
|
|
23124
|
+
/**
|
|
23125
|
+
* The error the signed path raises for this envelope-free body, so a refused
|
|
23126
|
+
* unsigned request reads exactly as it does on a DVM without the option.
|
|
23127
|
+
*/
|
|
23128
|
+
unsignedRequestRefusal(c, schema, body, capabilityName) {
|
|
23129
|
+
try {
|
|
23130
|
+
if (this.descriptor.auth && isZodSchema(schema)) {
|
|
23131
|
+
this.descriptor.auth.checkAuth(
|
|
23132
|
+
schema,
|
|
23133
|
+
signedRequestInput(body),
|
|
23134
|
+
this.signedRequestDomain(c, capabilityName)
|
|
23135
|
+
);
|
|
23136
|
+
}
|
|
23137
|
+
} catch (err) {
|
|
23138
|
+
if (err instanceof SignedRequestError) return err;
|
|
23139
|
+
throw err;
|
|
23140
|
+
}
|
|
23141
|
+
return new SignedRequestError("signed request required", "signature_invalid");
|
|
23142
|
+
}
|
|
22797
23143
|
/**
|
|
22798
23144
|
* On authenticated DVMs, envelope-only parse failures defer to auth
|
|
22799
23145
|
* (401 signature_invalid). Builder input issues remain 400 invalid_input.
|
|
@@ -22823,6 +23169,25 @@ var DVMServer = class {
|
|
|
22823
23169
|
if (healthy.length > 0) return healthy;
|
|
22824
23170
|
return all.filter((m) => !tracker.isNutIncompatible(m));
|
|
22825
23171
|
}
|
|
23172
|
+
/**
|
|
23173
|
+
* This request's price at the rates `pricedAt` replaced, or `undefined` when
|
|
23174
|
+
* there are none or they were replaced longer ago than the hold window.
|
|
23175
|
+
* Computed the way the current price is, so the figure matches the one an
|
|
23176
|
+
* earlier 402 quoted.
|
|
23177
|
+
*/
|
|
23178
|
+
previousPriceMsats(cap, dynamicUpfront, pricedAt) {
|
|
23179
|
+
const previous = pricedAt && this.fxFetcher.previousFor(pricedAt);
|
|
23180
|
+
if (!previous) return void 0;
|
|
23181
|
+
try {
|
|
23182
|
+
if (dynamicUpfront) {
|
|
23183
|
+
return fiatToSatsCeil(dynamicUpfront.amount, fxRateFor(previous, dynamicUpfront.currency)) * 1e3;
|
|
23184
|
+
}
|
|
23185
|
+
if (cap.price === void 0) return void 0;
|
|
23186
|
+
return fiatToSatsCeil(parseUsdPrice(cap.price, "price"), fxRateFor(previous, "usd")) * 1e3;
|
|
23187
|
+
} catch {
|
|
23188
|
+
return void 0;
|
|
23189
|
+
}
|
|
23190
|
+
}
|
|
22826
23191
|
/** Authenticated, read-only recovery of a previously accepted public request id. */
|
|
22827
23192
|
async handleRequestResult(c) {
|
|
22828
23193
|
const requestId = c.req.param("requestId");
|
|
@@ -22893,6 +23258,14 @@ var DVMServer = class {
|
|
|
22893
23258
|
400
|
|
22894
23259
|
);
|
|
22895
23260
|
}
|
|
23261
|
+
if (c.req.header("X-Cashu-Request-Id")?.startsWith(DERIVED_REQUEST_ID_PREFIX)) {
|
|
23262
|
+
return c.json(
|
|
23263
|
+
paymentErrorBody("reserved_request_id", {
|
|
23264
|
+
message: `X-Cashu-Request-Id may not start with "${DERIVED_REQUEST_ID_PREFIX}".`
|
|
23265
|
+
}),
|
|
23266
|
+
400
|
|
23267
|
+
);
|
|
23268
|
+
}
|
|
22896
23269
|
const publicRequestId = requestIdFromEnvelope(body.data);
|
|
22897
23270
|
const recoveryStore = requestRecoveryStore(this.jobManager.store);
|
|
22898
23271
|
const resumedRequestId = resumeOnly ? c.req.param("requestId") : void 0;
|
|
@@ -22911,6 +23284,17 @@ var DVMServer = class {
|
|
|
22911
23284
|
const capabilityResult = this.requireCapabilityFromBody(c, body.capability);
|
|
22912
23285
|
if ("error" in capabilityResult) return capabilityResult.error;
|
|
22913
23286
|
const capabilityName = capabilityResult.name;
|
|
23287
|
+
const unsignedPaid = this.jobManager.unsignedPaidInput(body, capabilityName);
|
|
23288
|
+
if (unsignedPaid && publicRequestId) {
|
|
23289
|
+
return c.json(
|
|
23290
|
+
{
|
|
23291
|
+
error: "request_recovery_unsupported",
|
|
23292
|
+
message: "Public request ids require a signed request.",
|
|
23293
|
+
hint: "Sign the request to use request_id recovery, or omit request_id."
|
|
23294
|
+
},
|
|
23295
|
+
400
|
|
23296
|
+
);
|
|
23297
|
+
}
|
|
22914
23298
|
pinDimension("capability", capabilityName);
|
|
22915
23299
|
const cap = this.descriptor.capabilities[capabilityName];
|
|
22916
23300
|
let parsedInput;
|
|
@@ -22919,13 +23303,23 @@ var DVMServer = class {
|
|
|
22919
23303
|
parsedInput = await loggers.isolate.span(
|
|
22920
23304
|
"dvm.input.validate",
|
|
22921
23305
|
{ capability: capabilityName, stage: "input" },
|
|
22922
|
-
() => this.jobManager.parseInput(body, capabilityName),
|
|
23306
|
+
() => this.jobManager.parseInput(body, capabilityName, unsignedPaid),
|
|
22923
23307
|
{ onError: () => ({ "error.code": "invalid_input" }) }
|
|
22924
23308
|
);
|
|
22925
23309
|
} catch (err) {
|
|
22926
|
-
if (!this.deferToAuthGate(err)) return this.invalidInputResponse(c, err);
|
|
23310
|
+
if (unsignedPaid || !this.deferToAuthGate(err)) return this.invalidInputResponse(c, err);
|
|
22927
23311
|
deferredInputError = err;
|
|
22928
23312
|
}
|
|
23313
|
+
if (unsignedPaid && carriesCreditFields(signedRequestInput(body))) {
|
|
23314
|
+
return c.json(
|
|
23315
|
+
{
|
|
23316
|
+
error: "invalid_credit_fields",
|
|
23317
|
+
message: "credit_id, draw_id and fund require a signed request.",
|
|
23318
|
+
hint: "Sign the request to draw on or fund prepaid credit, or remove the credit fields and pay per call."
|
|
23319
|
+
},
|
|
23320
|
+
400
|
|
23321
|
+
);
|
|
23322
|
+
}
|
|
22929
23323
|
let creditEnvelope;
|
|
22930
23324
|
try {
|
|
22931
23325
|
creditEnvelope = extractCreditEnvelope(body.data);
|
|
@@ -22942,9 +23336,26 @@ var DVMServer = class {
|
|
|
22942
23336
|
}
|
|
22943
23337
|
throw err;
|
|
22944
23338
|
}
|
|
23339
|
+
if (unsignedPaid && (cap.price === void 0 && !cap.onQuote || devModeSkipsPaymentVerification(this.opts) || !this.unsignedCashuOffered())) {
|
|
23340
|
+
return buildAuthErrorResponse(
|
|
23341
|
+
c,
|
|
23342
|
+
this.unsignedRequestRefusal(c, cap.input, body, capabilityName),
|
|
23343
|
+
this.publicOrigin
|
|
23344
|
+
);
|
|
23345
|
+
}
|
|
23346
|
+
if (unsignedPaid && await carriesNonCashuPayment(c)) {
|
|
23347
|
+
return buildPaymentErrorResponse(
|
|
23348
|
+
paymentErrorBody("payment_method_requires_signature", {
|
|
23349
|
+
message: "Unsigned requests may pay only with Cashu; an x402 or Tempo payment was attached."
|
|
23350
|
+
}),
|
|
23351
|
+
void 0,
|
|
23352
|
+
void 0,
|
|
23353
|
+
402
|
|
23354
|
+
);
|
|
23355
|
+
}
|
|
22945
23356
|
let authEnvelope;
|
|
22946
23357
|
let authPubkey;
|
|
22947
|
-
if (this.descriptor.auth) {
|
|
23358
|
+
if (this.descriptor.auth && !unsignedPaid) {
|
|
22948
23359
|
const auth = this.descriptor.auth;
|
|
22949
23360
|
try {
|
|
22950
23361
|
const result = await loggers.isolate.span(
|
|
@@ -22992,14 +23403,14 @@ var DVMServer = class {
|
|
|
22992
23403
|
{ capability: capabilityName, input: body.input, params: body.params, data: body.data },
|
|
22993
23404
|
{ stripAuthEnvelope: this.descriptor.auth !== void 0 }
|
|
22994
23405
|
);
|
|
22995
|
-
const authFingerprint =
|
|
23406
|
+
const authFingerprint = authEnvelope ? requestFingerprint(
|
|
22996
23407
|
{ capability: capabilityName, input: body.input, params: body.params, data: body.data },
|
|
22997
23408
|
{ stripAuthEnvelope: false }
|
|
22998
23409
|
) : void 0;
|
|
22999
23410
|
const authorization = c.req.header("Authorization");
|
|
23000
23411
|
const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
|
|
23001
23412
|
const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
|
|
23002
|
-
const hostingPaymentArtifactHash = attachedPayment ?
|
|
23413
|
+
const hostingPaymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
23003
23414
|
if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
|
|
23004
23415
|
const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
|
|
23005
23416
|
hostingPaymentArtifactHash
|
|
@@ -23081,9 +23492,11 @@ var DVMServer = class {
|
|
|
23081
23492
|
}
|
|
23082
23493
|
const skipPayment = devModeSkipsPaymentVerification(this.opts);
|
|
23083
23494
|
let priceMsats = resumeOnly ? payment.paidMsats || void 0 : void 0;
|
|
23495
|
+
let pricedAt;
|
|
23084
23496
|
if (!resumeOnly && !skipPayment) {
|
|
23085
23497
|
try {
|
|
23086
|
-
|
|
23498
|
+
if (cap.price !== void 0) pricedAt = await this.fxFetcher.fetch();
|
|
23499
|
+
priceMsats = await this.jobManager.currentPriceMsats(capabilityName, pricedAt);
|
|
23087
23500
|
} catch (err) {
|
|
23088
23501
|
const fxErr = pickFxRateUnavailable(err);
|
|
23089
23502
|
if (fxErr) return c.json({ error: fxErr }, 503);
|
|
@@ -23097,17 +23510,26 @@ var DVMServer = class {
|
|
|
23097
23510
|
parsedInput,
|
|
23098
23511
|
normalisedBody,
|
|
23099
23512
|
authPubkey,
|
|
23100
|
-
skipPayment
|
|
23513
|
+
skipPayment,
|
|
23514
|
+
unsignedPaid
|
|
23101
23515
|
);
|
|
23102
23516
|
if (dyn.error) return c.json(dyn.error.body, dyn.error.status);
|
|
23103
23517
|
priceMsats = dyn.priceMsats;
|
|
23104
23518
|
dynamicUpfront = dyn.upfront;
|
|
23519
|
+
pricedAt = dyn.pricedAt;
|
|
23105
23520
|
}
|
|
23106
23521
|
const priceFiat = resumeOnly ? void 0 : resolvePriceFiat({
|
|
23107
23522
|
price: typeof cap.price === "string" ? cap.price : void 0,
|
|
23108
23523
|
priceMsats,
|
|
23109
23524
|
dynamicUpfront
|
|
23110
23525
|
});
|
|
23526
|
+
if (unsignedPaid && !priceMsats) {
|
|
23527
|
+
return buildAuthErrorResponse(
|
|
23528
|
+
c,
|
|
23529
|
+
this.unsignedRequestRefusal(c, cap.input, body, capabilityName),
|
|
23530
|
+
this.publicOrigin
|
|
23531
|
+
);
|
|
23532
|
+
}
|
|
23111
23533
|
if (!resumeOnly && creditEnvelope?.creditId) {
|
|
23112
23534
|
await this.settleLightningFunding(authPubkey, creditEnvelope.creditId);
|
|
23113
23535
|
}
|
|
@@ -23130,7 +23552,13 @@ var DVMServer = class {
|
|
|
23130
23552
|
}
|
|
23131
23553
|
}
|
|
23132
23554
|
if (!skipPayment && !resumedAcceptedPayment) {
|
|
23133
|
-
const railOpts = await this.railVerifyOpts(c);
|
|
23555
|
+
const railOpts = unsignedPaid ? cashuOnlyRailOpts(await this.railVerifyOpts(c)) : await this.railVerifyOpts(c);
|
|
23556
|
+
if (!this.descriptor.auth || unsignedPaid) {
|
|
23557
|
+
railOpts.nut24 = {
|
|
23558
|
+
mints: this.advertisedMints(),
|
|
23559
|
+
previousRequiredMsats: this.previousPriceMsats(cap, dynamicUpfront, pricedAt)
|
|
23560
|
+
};
|
|
23561
|
+
}
|
|
23134
23562
|
hostingPaymentMayHaveLanded = Boolean(
|
|
23135
23563
|
creditEnvelope || railOpts.cashuToken || railOpts.x402Payment || railOpts.mppCredential
|
|
23136
23564
|
);
|
|
@@ -23168,7 +23596,7 @@ var DVMServer = class {
|
|
|
23168
23596
|
this.reportShortPayForfeit(err.detail.forfeit);
|
|
23169
23597
|
if (err.detail.body.error === "replay_rejected") {
|
|
23170
23598
|
const recovered = await this.recoverReplayedJob(c, {
|
|
23171
|
-
requestId: err.detail.replayPaymentId
|
|
23599
|
+
requestId: err.detail.replayPaymentId,
|
|
23172
23600
|
fingerprint,
|
|
23173
23601
|
callerPubkey: authPubkey
|
|
23174
23602
|
});
|
|
@@ -23196,15 +23624,17 @@ var DVMServer = class {
|
|
|
23196
23624
|
}
|
|
23197
23625
|
}
|
|
23198
23626
|
if (!resumedAcceptedPayment) {
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
23627
|
+
if (!unsignedPaid) {
|
|
23628
|
+
attachCreditMenu(
|
|
23629
|
+
err.detail.body,
|
|
23630
|
+
await this.creditMenu(this.creditCurrency(), authPubkey)
|
|
23631
|
+
);
|
|
23632
|
+
}
|
|
23203
23633
|
if (priceFiat) {
|
|
23204
23634
|
err.detail.body.price_micro = priceFiat.amountMicro;
|
|
23205
23635
|
err.detail.body.price_currency = priceFiat.currency;
|
|
23206
23636
|
}
|
|
23207
|
-
|
|
23637
|
+
const response2 = buildPaymentErrorResponse(
|
|
23208
23638
|
err.detail.body,
|
|
23209
23639
|
err.detail.mppChallenges,
|
|
23210
23640
|
err.detail.x402Requirements,
|
|
@@ -23214,6 +23644,11 @@ var DVMServer = class {
|
|
|
23214
23644
|
this.x402Support()?.exact,
|
|
23215
23645
|
err.detail.x402ErrorCode
|
|
23216
23646
|
);
|
|
23647
|
+
if (err.detail.cashuPaymentRequest) {
|
|
23648
|
+
response2.headers.set("X-Cashu", err.detail.cashuPaymentRequest);
|
|
23649
|
+
response2.headers.set("Cache-Control", "no-store");
|
|
23650
|
+
}
|
|
23651
|
+
return response2;
|
|
23217
23652
|
}
|
|
23218
23653
|
}
|
|
23219
23654
|
throw err;
|
|
@@ -23297,7 +23732,7 @@ var DVMServer = class {
|
|
|
23297
23732
|
}
|
|
23298
23733
|
const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
|
|
23299
23734
|
const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
|
|
23300
|
-
const requesterTokenHash = jobToken ?
|
|
23735
|
+
const requesterTokenHash = jobToken ? createHash12("sha256").update(jobToken).digest("hex") : void 0;
|
|
23301
23736
|
const job = jm.createJob(
|
|
23302
23737
|
normalisedBody,
|
|
23303
23738
|
requesterId,
|
|
@@ -23310,7 +23745,7 @@ var DVMServer = class {
|
|
|
23310
23745
|
requestFingerprint: fingerprint,
|
|
23311
23746
|
requesterPubkey: authPubkey,
|
|
23312
23747
|
requestId: publicRequestId,
|
|
23313
|
-
authRequestPath:
|
|
23748
|
+
authRequestPath: authEnvelope ? canonicalRequestPath(c.req.url) : void 0,
|
|
23314
23749
|
jobId
|
|
23315
23750
|
}
|
|
23316
23751
|
);
|
|
@@ -23406,15 +23841,16 @@ var DVMServer = class {
|
|
|
23406
23841
|
}
|
|
23407
23842
|
/**
|
|
23408
23843
|
* Recover a lost paid response only for matching request fingerprint,
|
|
23409
|
-
* authenticated pubkey and requester identity.
|
|
23410
|
-
*
|
|
23844
|
+
* authenticated pubkey and requester identity. `requestId` must be the
|
|
23845
|
+
* payment reference the rail bound the presented payment to; a spent bearer
|
|
23846
|
+
* token alone is not proof of request ownership.
|
|
23411
23847
|
*/
|
|
23412
23848
|
async recoverReplayedJob(c, opts) {
|
|
23413
23849
|
if (!opts.requestId) return void 0;
|
|
23414
23850
|
const prior = await this.jobManager.store.findJobByPaymentTxHash(opts.requestId);
|
|
23415
23851
|
if (!prior) return void 0;
|
|
23416
23852
|
const bodyMatches = prior.requestFingerprint !== void 0 && prior.requestFingerprint === opts.fingerprint;
|
|
23417
|
-
const pubkeyMatches = this.descriptor.auth === void 0 || prior.requesterPubkey
|
|
23853
|
+
const pubkeyMatches = this.descriptor.auth === void 0 || prior.requesterPubkey === opts.callerPubkey;
|
|
23418
23854
|
const requesterMatches = prior.requesterId === c.get("requesterId");
|
|
23419
23855
|
if (!bodyMatches || !pubkeyMatches || !requesterMatches) {
|
|
23420
23856
|
diagnosticConsole.warn(
|
|
@@ -23547,16 +23983,17 @@ var DVMServer = class {
|
|
|
23547
23983
|
* schema separately. Refuse invalid input/results, currency mismatch or missing
|
|
23548
23984
|
* FX when payment needs a price (400/500/503 as classified below).
|
|
23549
23985
|
*/
|
|
23550
|
-
async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey, skipRateConversion = false) {
|
|
23986
|
+
async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey, skipRateConversion = false, unsigned2 = false) {
|
|
23551
23987
|
let data = parsedInput;
|
|
23552
23988
|
if (isZodSchema(onQuote.schema)) {
|
|
23553
|
-
const
|
|
23989
|
+
const schema = unsigned2 ? unsignedInputSchema(onQuote.schema) ?? onQuote.schema : onQuote.schema;
|
|
23990
|
+
const parsed = schema.safeParse(parsedInput);
|
|
23554
23991
|
if (!parsed.success) {
|
|
23555
23992
|
return {
|
|
23556
23993
|
error: {
|
|
23557
23994
|
body: {
|
|
23558
23995
|
error: "quote_unavailable",
|
|
23559
|
-
message: "DVM uses dynamic pricing but the job input does not satisfy onQuote.schema. Submit the same payload to /v1/quote to confirm pricing."
|
|
23996
|
+
message: unsigned2 ? "DVM uses dynamic pricing but the unsigned job input does not satisfy onQuote.schema. Sign the request, or send input that matches the published schema." : "DVM uses dynamic pricing but the job input does not satisfy onQuote.schema. Submit the same payload to /v1/quote to confirm pricing."
|
|
23560
23997
|
},
|
|
23561
23998
|
status: 400
|
|
23562
23999
|
}
|
|
@@ -23586,7 +24023,8 @@ var DVMServer = class {
|
|
|
23586
24023
|
const priceMsats = fiatToSatsCeil(validated.upfront.amount, fxRateFor(fx, upfrontCurrency)) * 1e3;
|
|
23587
24024
|
return {
|
|
23588
24025
|
priceMsats,
|
|
23589
|
-
upfront: { amount: validated.upfront.amount, currency: upfrontCurrency }
|
|
24026
|
+
upfront: { amount: validated.upfront.amount, currency: upfrontCurrency },
|
|
24027
|
+
pricedAt: fx
|
|
23590
24028
|
};
|
|
23591
24029
|
} catch (err) {
|
|
23592
24030
|
if (err instanceof InvalidCurrencyError || err instanceof UnsupportedCurrencyError) {
|
|
@@ -23868,6 +24306,8 @@ var DVMServer = class {
|
|
|
23868
24306
|
this.opts.devMode === true
|
|
23869
24307
|
);
|
|
23870
24308
|
if (contentErr2) return c.json({ error: "invalid_message", message: contentErr2 }, 400);
|
|
24309
|
+
const railRefusal2 = this.unsignedJobRailRefusal(activeJob, body2);
|
|
24310
|
+
if (railRefusal2) return railRefusal2;
|
|
23871
24311
|
const inboundSeq2 = await jm.recordInboundPending(activeJob, body2.type, body2.content);
|
|
23872
24312
|
const lockPubkeys2 = await this.resolveLockPubkeysForReceive();
|
|
23873
24313
|
let unappliedCredit2;
|
|
@@ -23928,6 +24368,8 @@ var DVMServer = class {
|
|
|
23928
24368
|
}
|
|
23929
24369
|
const contentErr = validateMessageContent(body.type, body.content, this.opts.devMode === true);
|
|
23930
24370
|
if (contentErr) return c.json({ error: "invalid_message", message: contentErr }, 400);
|
|
24371
|
+
const railRefusal = this.unsignedJobRailRefusal(record, body);
|
|
24372
|
+
if (railRefusal) return railRefusal;
|
|
23931
24373
|
if (isStreamableJobStore(jm.store)) {
|
|
23932
24374
|
record.messages = await jm.store.getMessages(id, 0);
|
|
23933
24375
|
record.seq = Math.max(record.seq, ...record.messages.map((message) => message.seq));
|
|
@@ -24127,7 +24569,7 @@ function isJobOwner(c, job) {
|
|
|
24127
24569
|
if (job.requesterTokenHash) {
|
|
24128
24570
|
const token = c.req.header("X-Job-Token");
|
|
24129
24571
|
if (!token) return false;
|
|
24130
|
-
const provided =
|
|
24572
|
+
const provided = createHash12("sha256").update(token).digest("hex");
|
|
24131
24573
|
return timingSafeEqualHex(provided, job.requesterTokenHash);
|
|
24132
24574
|
}
|
|
24133
24575
|
return c.get("requesterId") === job.requesterId;
|
|
@@ -24250,6 +24692,24 @@ function removeUnavailableContent(fields) {
|
|
|
24250
24692
|
delete fields.payload_in;
|
|
24251
24693
|
}
|
|
24252
24694
|
}
|
|
24695
|
+
async function carriesNonCashuPayment(c) {
|
|
24696
|
+
if (c.req.header("PAYMENT-SIGNATURE") || c.req.header("X-PAYMENT")) return true;
|
|
24697
|
+
const authorization = c.req.header("Authorization");
|
|
24698
|
+
if (!authorization) return false;
|
|
24699
|
+
return Boolean((await import("mppx")).Credential.extractPaymentScheme(authorization));
|
|
24700
|
+
}
|
|
24701
|
+
function cashuOnlyRailOpts(opts) {
|
|
24702
|
+
return {
|
|
24703
|
+
...opts,
|
|
24704
|
+
x402Payment: void 0,
|
|
24705
|
+
x402Config: void 0,
|
|
24706
|
+
x402ExactSettlement: void 0,
|
|
24707
|
+
x402ExactVersions: void 0,
|
|
24708
|
+
x402BatchSettlement: void 0,
|
|
24709
|
+
mppCredential: void 0,
|
|
24710
|
+
mpp: void 0
|
|
24711
|
+
};
|
|
24712
|
+
}
|
|
24253
24713
|
function terminalResponseFields(job) {
|
|
24254
24714
|
const fields = {};
|
|
24255
24715
|
if (job.upfrontPayment !== void 0) fields.upfront_payment = job.upfrontPayment;
|