@dvmkit/sdk 0.11.0-rc.1 → 0.12.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K2ZNIQZP.js → chunk-2VGMZ7VF.js} +29 -2
- package/dist/{chunk-55DVLBCD.js → chunk-POYUX2NN.js} +15 -4
- package/dist/{chunk-AIQGF52I.js → chunk-S7PIFHE4.js} +351 -32
- package/dist/{chunk-HQPQ4PFI.js → chunk-TWKQFIOW.js} +20 -2
- package/dist/{content-retention-ySTD1wwZ.d.ts → content-retention-a1yRdmhI.d.ts} +1 -1
- package/dist/{credit-ledger-4CBYLSLY.js → credit-ledger-MF23UKRJ.js} +1 -1
- package/dist/{credit-menu-BYySnZBk.d.ts → credit-menu-B2T-O9oY.d.ts} +11 -4
- package/dist/{fx-DVTNs9q1.d.ts → fx-DLhscRUh.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/internal/caller.d.ts +8 -6
- package/dist/internal/caller.js +1 -0
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +4 -4
- package/dist/{job-store-DRwXCJDI.d.ts → job-store-B8Ijr5V6.d.ts} +3 -1
- package/dist/{lightning-backend-BX5YvjnM.d.ts → lightning-backend-DmwMSgaM.d.ts} +1 -1
- package/dist/{memory-credit-ledger-DKCN7FY2.js → memory-credit-ledger-ERD764PS.js} +2 -2
- package/dist/{postgres-job-store-WIIVRZPD.js → postgres-job-store-ZVMAN6L2.js} +1 -1
- package/dist/server/index.d.ts +12 -10
- package/dist/server/index.js +8 -8
- package/dist/{step-cache-d-VKen4K.d.ts → step-cache-DQqpwXa8.d.ts} +59 -34
- package/dist/testing/index.d.ts +3 -3
- package/dist/{usd-DSroMJ--.d.ts → usd-D5kk2nIT.d.ts} +1 -1
- package/package.json +1 -1
|
@@ -17,10 +17,10 @@ import {
|
|
|
17
17
|
} from "./chunk-E7ZBCT5U.js";
|
|
18
18
|
import {
|
|
19
19
|
MemoryCreditLedger
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-POYUX2NN.js";
|
|
21
21
|
import {
|
|
22
22
|
PostgresJobStore
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-TWKQFIOW.js";
|
|
24
24
|
import {
|
|
25
25
|
SIGNED_ENVELOPE_FIELDS,
|
|
26
26
|
SignedRequestError,
|
|
@@ -133,7 +133,7 @@ import {
|
|
|
133
133
|
isNonChannelBitcoinRail,
|
|
134
134
|
netOwedSats,
|
|
135
135
|
x402SettlementPending
|
|
136
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-2VGMZ7VF.js";
|
|
137
137
|
import {
|
|
138
138
|
parseStoreValue
|
|
139
139
|
} from "./chunk-YG7G4DPZ.js";
|
|
@@ -995,6 +995,7 @@ function attachCreditMenu(body, menu) {
|
|
|
995
995
|
}
|
|
996
996
|
function toCreditTerms(menu) {
|
|
997
997
|
return {
|
|
998
|
+
...menu.balance_payment && { balance_payment: true },
|
|
998
999
|
min_micro: menu.min_micro,
|
|
999
1000
|
max_micro: menu.max_micro,
|
|
1000
1001
|
ttl_ms: menu.ttl_ms,
|
|
@@ -12640,6 +12641,7 @@ function buildContext(job, opts) {
|
|
|
12640
12641
|
const paymentRequestContent = {
|
|
12641
12642
|
amount_msats: amountMsats,
|
|
12642
12643
|
reason,
|
|
12644
|
+
...pendingFiat && { credit: { amount_micro: pendingFiat.amountMicro, currency: pendingFiat.currency } },
|
|
12643
12645
|
...fiatAmount && { fiat_amount: fiatAmount },
|
|
12644
12646
|
...payOpts?.mints && { mints: payOpts.mints },
|
|
12645
12647
|
...payOpts?.minLocktimeSeconds && { min_locktime_seconds: payOpts.minLocktimeSeconds },
|
|
@@ -12718,8 +12720,16 @@ function buildContext(job, opts) {
|
|
|
12718
12720
|
if (job.replayPaymentIndex < payments.length) {
|
|
12719
12721
|
const payment = payments[job.replayPaymentIndex];
|
|
12720
12722
|
job.replayPaymentIndex++;
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
+
const balance = payment.content.credit;
|
|
12724
|
+
const accepted = balance ? await opts.acceptedBalancePayment?.(payment.seq, pendingFiat?.amountMicro, pendingFiat?.currency) : job.reactivationPaymentApplied === true;
|
|
12725
|
+
if (!accepted) throw new Error("[internal_error] payment replay reached without credit");
|
|
12726
|
+
if (balance) {
|
|
12727
|
+
job.pendingPaymentMsats = void 0;
|
|
12728
|
+
job.pendingPaymentFiatMicro = void 0;
|
|
12729
|
+
job.pendingPaymentFiatCurrency = void 0;
|
|
12730
|
+
job.pendingMppChallengeIds = void 0;
|
|
12731
|
+
job.pendingX402Nonce = void 0;
|
|
12732
|
+
job.pendingX402AmountUsdcMicro = void 0;
|
|
12723
12733
|
}
|
|
12724
12734
|
job.status = "processing";
|
|
12725
12735
|
creditedToHandlerMsats += amountMsats;
|
|
@@ -12788,7 +12798,7 @@ function buildContext(job, opts) {
|
|
|
12788
12798
|
try {
|
|
12789
12799
|
const counters = await getCounters();
|
|
12790
12800
|
if (!counters || resolved) return;
|
|
12791
|
-
const satisfied = poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats;
|
|
12801
|
+
const satisfied = counters.balancePaymentSatisfied === true || (poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats);
|
|
12792
12802
|
if (satisfied) {
|
|
12793
12803
|
const resume = async () => {
|
|
12794
12804
|
job.paidMsats = counters.paidMsats;
|
|
@@ -13292,6 +13302,9 @@ var JobManager = class {
|
|
|
13292
13302
|
statusChecksInFlight = /* @__PURE__ */ new Set();
|
|
13293
13303
|
/** Job ids that were notified mid-re-read and must be re-checked. */
|
|
13294
13304
|
statusChecksQueued = /* @__PURE__ */ new Set();
|
|
13305
|
+
// Bind live resolvers and recovery scaffolds to the store-allocated ask identity.
|
|
13306
|
+
paymentAskSeqs = /* @__PURE__ */ new WeakMap();
|
|
13307
|
+
reactivationAskSeqs = /* @__PURE__ */ new WeakMap();
|
|
13295
13308
|
// Every local input resume holds this queue through its durable wait update.
|
|
13296
13309
|
// A prompt response and a payment NOTIFY must not mistake each other for a
|
|
13297
13310
|
// different machine's successful claim.
|
|
@@ -13718,7 +13731,59 @@ var JobManager = class {
|
|
|
13718
13731
|
}
|
|
13719
13732
|
async claimLocalInput(job) {
|
|
13720
13733
|
await job.messageAppenderTail;
|
|
13721
|
-
|
|
13734
|
+
const askSeq = job.pendingPayment ? this.paymentAskSeqs.get(job.pendingPayment) : void 0;
|
|
13735
|
+
if (job.pendingPayment && this.opts.db && this.jobStore instanceof PostgresJobStore && askSeq === void 0) return false;
|
|
13736
|
+
return this.claimInputForAsk(job.id, askSeq);
|
|
13737
|
+
}
|
|
13738
|
+
async claimInputForAsk(jobId, askSeq) {
|
|
13739
|
+
if (!isStreamableJobStore(this.jobStore)) return true;
|
|
13740
|
+
if (askSeq === void 0 || !this.opts.db || !(this.jobStore instanceof PostgresJobStore)) return this.jobStore.claimForProcessing(jobId);
|
|
13741
|
+
const tx = await this.opts.db.connect();
|
|
13742
|
+
try {
|
|
13743
|
+
await tx.query("BEGIN");
|
|
13744
|
+
const { rows: [record] } = await tx.query("SELECT status FROM jobs WHERE id=$1 FOR UPDATE", [jobId]);
|
|
13745
|
+
const { rows: [ask] } = await tx.query(
|
|
13746
|
+
`SELECT MAX(seq) AS seq FROM job_messages WHERE job_id=$1 AND status='verified'
|
|
13747
|
+
AND data->>'from'='provider' AND data->>'type'='payment-request'`,
|
|
13748
|
+
[jobId]
|
|
13749
|
+
);
|
|
13750
|
+
if (record?.status !== "awaiting-input" || Number(ask?.seq) !== askSeq) {
|
|
13751
|
+
await tx.query("ROLLBACK");
|
|
13752
|
+
return false;
|
|
13753
|
+
}
|
|
13754
|
+
await tx.query(
|
|
13755
|
+
`UPDATE jobs SET status='processing', last_activity_at=$2,
|
|
13756
|
+
caller_wait_ms=caller_wait_ms+GREATEST(0,$2-COALESCE(awaiting_input_at_ms,last_activity_at)),
|
|
13757
|
+
awaiting_input_at_ms=NULL WHERE id=$1`,
|
|
13758
|
+
[jobId, Date.now()]
|
|
13759
|
+
);
|
|
13760
|
+
await tx.query("COMMIT");
|
|
13761
|
+
return true;
|
|
13762
|
+
} catch (error) {
|
|
13763
|
+
await tx.query("ROLLBACK").catch(() => void 0);
|
|
13764
|
+
throw error;
|
|
13765
|
+
} finally {
|
|
13766
|
+
tx.release();
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
async adoptPaymentMetadata(job, pending) {
|
|
13770
|
+
const record = await this.jobStore.get(job.id);
|
|
13771
|
+
if (!record || job.pendingPayment !== pending || isTerminal(job.status) || isTerminal(record.status)) return false;
|
|
13772
|
+
job.paidMsats = record.paidMsats;
|
|
13773
|
+
job.pendingPaymentMsats = record.pendingPaymentMsats;
|
|
13774
|
+
job.creditId = record.creditId;
|
|
13775
|
+
job.drawId = record.drawId;
|
|
13776
|
+
job.paymentRail = record.paymentRail;
|
|
13777
|
+
job.paymentMint = record.paymentMint;
|
|
13778
|
+
job.paymentTxHash = record.paymentTxHash;
|
|
13779
|
+
job.nativeAmount = record.nativeAmount;
|
|
13780
|
+
job.nativeAsset = record.nativeAsset;
|
|
13781
|
+
job.pendingPaymentFiatMicro = record.pendingPaymentFiatMicro;
|
|
13782
|
+
job.pendingPaymentFiatCurrency = record.pendingPaymentFiatCurrency;
|
|
13783
|
+
job.pendingMppChallengeIds = record.pendingMppChallengeIds;
|
|
13784
|
+
job.pendingX402Nonce = record.pendingX402Nonce;
|
|
13785
|
+
job.pendingX402AmountUsdcMicro = record.pendingX402AmountUsdcMicro;
|
|
13786
|
+
return true;
|
|
13722
13787
|
}
|
|
13723
13788
|
async resumeClaimedPayment(job, resume) {
|
|
13724
13789
|
const retry = async () => {
|
|
@@ -14083,8 +14148,9 @@ var JobManager = class {
|
|
|
14083
14148
|
const jobId = job.id;
|
|
14084
14149
|
job.messageAppender = (msg, opts) => {
|
|
14085
14150
|
const prev = job.messageAppenderTail ?? Promise.resolve();
|
|
14086
|
-
const next = prev.then(
|
|
14087
|
-
|
|
14151
|
+
const next = prev.then(() => {
|
|
14152
|
+
const pending = job.pendingPayment;
|
|
14153
|
+
return store.appendOutgoing(
|
|
14088
14154
|
jobId,
|
|
14089
14155
|
{
|
|
14090
14156
|
from: msg.from,
|
|
@@ -14093,7 +14159,11 @@ var JobManager = class {
|
|
|
14093
14159
|
content: msg.content
|
|
14094
14160
|
},
|
|
14095
14161
|
opts
|
|
14096
|
-
).
|
|
14162
|
+
).then((stored) => {
|
|
14163
|
+
if (msg.type === "payment-request" && pending && job.pendingPayment === pending) {
|
|
14164
|
+
this.paymentAskSeqs.set(pending, stored);
|
|
14165
|
+
}
|
|
14166
|
+
}).catch((_err) => {
|
|
14097
14167
|
diagnosticConsole.error(
|
|
14098
14168
|
{
|
|
14099
14169
|
level: "job_message_append_failed",
|
|
@@ -14107,8 +14177,8 @@ var JobManager = class {
|
|
|
14107
14177
|
error: void 0
|
|
14108
14178
|
}
|
|
14109
14179
|
);
|
|
14110
|
-
})
|
|
14111
|
-
);
|
|
14180
|
+
});
|
|
14181
|
+
});
|
|
14112
14182
|
job.messageAppenderTail = next;
|
|
14113
14183
|
};
|
|
14114
14184
|
}
|
|
@@ -14839,10 +14909,13 @@ var JobManager = class {
|
|
|
14839
14909
|
async claimAwaitingInputForReactivation(jobId) {
|
|
14840
14910
|
if (!isStreamableJobStore(this.jobStore)) return true;
|
|
14841
14911
|
const store = this.jobStore;
|
|
14912
|
+
const scaffold = this._activeJobs.get(jobId);
|
|
14913
|
+
const askSeq = scaffold ? this.reactivationAskSeqs.get(scaffold) : void 0;
|
|
14842
14914
|
const deadline = this.monotonicNowMs() + this.reactivationClaimGraceMs;
|
|
14843
14915
|
for (; ; ) {
|
|
14844
14916
|
if (this.monotonicNowMs() >= deadline) {
|
|
14845
|
-
|
|
14917
|
+
if (scaffold && this._activeJobs.get(jobId) !== scaffold) return false;
|
|
14918
|
+
return this.claimInputForAsk(jobId, askSeq);
|
|
14846
14919
|
}
|
|
14847
14920
|
const record = await store.get(jobId);
|
|
14848
14921
|
if (record?.status !== "awaiting-input") return false;
|
|
@@ -14952,6 +15025,8 @@ var JobManager = class {
|
|
|
14952
15025
|
}
|
|
14953
15026
|
}
|
|
14954
15027
|
const job = fromJobRecord(record);
|
|
15028
|
+
const ask = record.messages.findLast((message) => message.from === "provider" && message.type === "payment-request");
|
|
15029
|
+
if (ask) this.reactivationAskSeqs.set(job, ask.seq);
|
|
14955
15030
|
this._activeJobs.set(job.id, job);
|
|
14956
15031
|
this.wireMessageAppender(job);
|
|
14957
15032
|
this.watchDurableStatus(job);
|
|
@@ -15369,7 +15444,23 @@ var JobManager = class {
|
|
|
15369
15444
|
onYield: () => this.persistJob(job),
|
|
15370
15445
|
onPaymentResume: () => retryJobSnapshot(() => this.persistJob(job)),
|
|
15371
15446
|
subscribeNotifications: isStreamableJobStore(jobStore) ? (cb) => jobStore.subscribeNotifications(job.id, cb) : void 0,
|
|
15372
|
-
|
|
15447
|
+
acceptedBalancePayment: this.opts.db && jobStore instanceof PostgresJobStore ? async (seq, amountMicro, currency) => {
|
|
15448
|
+
const { rowCount } = await this.opts.db.query(
|
|
15449
|
+
`SELECT 1 FROM credit_balance_payments p
|
|
15450
|
+
JOIN job_messages m ON m.job_id=p.job_id AND m.seq=(p.response->>'seq')::bigint AND m.status='verified'
|
|
15451
|
+
JOIN job_messages a ON a.job_id=p.job_id AND a.seq=p.request_seq AND a.status='verified'
|
|
15452
|
+
WHERE p.job_id=$1 AND m.seq=$2 AND a.data->>'type'='payment-request'
|
|
15453
|
+
AND (m.data->'content'->'credit'->>'request_seq')::bigint=p.request_seq
|
|
15454
|
+
AND a.data->'content'->'credit'->>'amount_micro'=$3
|
|
15455
|
+
AND a.data->'content'->'credit'->>'currency'=$4`,
|
|
15456
|
+
[job.id, seq, amountMicro === void 0 ? null : String(amountMicro), currency ?? null]
|
|
15457
|
+
);
|
|
15458
|
+
return rowCount === 1;
|
|
15459
|
+
} : void 0,
|
|
15460
|
+
getCounters: isStreamableJobStore(jobStore) ? async () => {
|
|
15461
|
+
await job.messageAppenderTail;
|
|
15462
|
+
return jobStore.getCounters(job.id);
|
|
15463
|
+
} : void 0,
|
|
15373
15464
|
drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
|
|
15374
15465
|
pricingCurrency: this.pricingCurrency,
|
|
15375
15466
|
claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
|
|
@@ -15378,10 +15469,13 @@ var JobManager = class {
|
|
|
15378
15469
|
if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
15379
15470
|
if (!await this.claimLocalInput(job)) return false;
|
|
15380
15471
|
if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
15472
|
+
let resumed = false;
|
|
15381
15473
|
await this.resumeClaimedPayment(job, async () => {
|
|
15474
|
+
if (!await this.adoptPaymentMetadata(job, pending)) return;
|
|
15382
15475
|
await resume?.();
|
|
15476
|
+
resumed = true;
|
|
15383
15477
|
});
|
|
15384
|
-
return
|
|
15478
|
+
return resumed;
|
|
15385
15479
|
});
|
|
15386
15480
|
} : void 0,
|
|
15387
15481
|
onSuperseded: () => {
|
|
@@ -15414,6 +15508,12 @@ var JobManager = class {
|
|
|
15414
15508
|
* fall back on is a slice of the credit's rail value at a ratio pinned
|
|
15415
15509
|
* whenever that credit was funded, so it cannot safely fund a new ask.
|
|
15416
15510
|
*/
|
|
15511
|
+
async hasAcceptedBalancePayment(jobId) {
|
|
15512
|
+
return this.jobStore instanceof PostgresJobStore && !!this.opts.db && !!(await this.opts.db.query(
|
|
15513
|
+
"SELECT 1 FROM credit_balance_payments WHERE job_id=$1 LIMIT 1",
|
|
15514
|
+
[jobId]
|
|
15515
|
+
)).rowCount;
|
|
15516
|
+
}
|
|
15417
15517
|
async resolveDrawPool(job) {
|
|
15418
15518
|
const ledger = this.opts.creditLedger;
|
|
15419
15519
|
if (!ledger || !job.creditId || !job.drawId) return { basis: "rail" };
|
|
@@ -15433,8 +15533,10 @@ var JobManager = class {
|
|
|
15433
15533
|
return { basis: "unpriced" };
|
|
15434
15534
|
}
|
|
15435
15535
|
const { rail, drawMsats, creditCurrency } = draw.railValue;
|
|
15436
|
-
if (!rail || drawMsats <= 0)
|
|
15437
|
-
|
|
15536
|
+
if (!rail || drawMsats <= 0) {
|
|
15537
|
+
const acceptedBalance = await this.hasAcceptedBalancePayment(job.id);
|
|
15538
|
+
if (!acceptedBalance) return { basis: "rail", creditCurrency: creditCurrency ?? void 0 };
|
|
15539
|
+
}
|
|
15438
15540
|
if (!creditCurrency) {
|
|
15439
15541
|
this.warnDrawPool(job, "credit_currency_missing");
|
|
15440
15542
|
return { basis: "unpriced" };
|
|
@@ -15874,7 +15976,7 @@ var JobManager = class {
|
|
|
15874
15976
|
const onJobCompleted = this.opts.onJobCompleted;
|
|
15875
15977
|
const onRevenueSkippedNoRail = this.opts.onRevenueSkippedNoRail;
|
|
15876
15978
|
if (!onJobCompleted && !onRevenueSkippedNoRail) return false;
|
|
15877
|
-
if (job.status !== "completed"
|
|
15979
|
+
if (job.status !== "completed") return false;
|
|
15878
15980
|
let credit = {};
|
|
15879
15981
|
let creditRail;
|
|
15880
15982
|
let drawAmountMicro;
|
|
@@ -15911,6 +16013,7 @@ var JobManager = class {
|
|
|
15911
16013
|
settledAt: draw.resolvedAt ?? void 0
|
|
15912
16014
|
};
|
|
15913
16015
|
}
|
|
16016
|
+
if (job.paidMsats <= 0 && !(drawAmountMicro !== void 0 && drawAmountMicro > 0 && typeof credit.creditCurrency === "string" && creditRail && isFundingRail(creditRail))) return false;
|
|
15914
16017
|
if (!job.paymentRail) {
|
|
15915
16018
|
if (job.creditId && job.drawId) {
|
|
15916
16019
|
const creditCurrency = typeof credit.creditCurrency === "string" ? credit.creditCurrency : void 0;
|
|
@@ -17186,13 +17289,167 @@ function withWallClockDeadline(promise, timeoutMs) {
|
|
|
17186
17289
|
});
|
|
17187
17290
|
}
|
|
17188
17291
|
|
|
17292
|
+
// src/sdk/server/balance-payment.ts
|
|
17293
|
+
import { createHash as createHash9 } from "crypto";
|
|
17294
|
+
import { z as z2 } from "zod";
|
|
17295
|
+
var BALANCE_PAYMENT_SCHEMA = z2.object({
|
|
17296
|
+
credit_id: z2.string().min(1).max(256),
|
|
17297
|
+
draw_id: z2.string().min(1).max(256),
|
|
17298
|
+
payment_id: z2.string().min(1).max(256),
|
|
17299
|
+
request_seq: z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
17300
|
+
amount_micro: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
17301
|
+
currency: z2.string().regex(/^[a-z]{3}$/),
|
|
17302
|
+
pubkey: z2.string(),
|
|
17303
|
+
signature: z2.string(),
|
|
17304
|
+
timestamp: z2.number(),
|
|
17305
|
+
nonce: z2.string()
|
|
17306
|
+
});
|
|
17307
|
+
var BalancePaymentError = class extends Error {
|
|
17308
|
+
constructor(status, body) {
|
|
17309
|
+
super(String(body.error));
|
|
17310
|
+
this.status = status;
|
|
17311
|
+
this.body = body;
|
|
17312
|
+
}
|
|
17313
|
+
status;
|
|
17314
|
+
body;
|
|
17315
|
+
};
|
|
17316
|
+
function conflict(error = "credit_payment_conflict") {
|
|
17317
|
+
throw new BalancePaymentError(409, { error });
|
|
17318
|
+
}
|
|
17319
|
+
async function commitBalancePayment(args) {
|
|
17320
|
+
args.auth.recheckSignature(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
|
|
17321
|
+
const auth = BALANCE_PAYMENT_SCHEMA.parse(args.raw);
|
|
17322
|
+
const { pubkey, credit_id, draw_id, payment_id, request_seq, amount_micro, currency } = auth;
|
|
17323
|
+
const fingerprint = createHash9("sha256").update(JSON.stringify([
|
|
17324
|
+
args.domain,
|
|
17325
|
+
args.jobId,
|
|
17326
|
+
pubkey,
|
|
17327
|
+
credit_id,
|
|
17328
|
+
draw_id,
|
|
17329
|
+
payment_id,
|
|
17330
|
+
request_seq,
|
|
17331
|
+
amount_micro,
|
|
17332
|
+
currency
|
|
17333
|
+
])).digest("hex");
|
|
17334
|
+
const tx = await args.pool.connect();
|
|
17335
|
+
try {
|
|
17336
|
+
await tx.query("BEGIN");
|
|
17337
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`balance-payment:${payment_id}`]);
|
|
17338
|
+
const prior = await tx.query(
|
|
17339
|
+
"SELECT fingerprint, response FROM credit_balance_payments WHERE payment_id=$1",
|
|
17340
|
+
[payment_id]
|
|
17341
|
+
);
|
|
17342
|
+
if (prior.rows[0]) {
|
|
17343
|
+
if (prior.rows[0].fingerprint !== fingerprint) conflict();
|
|
17344
|
+
await tx.query("COMMIT");
|
|
17345
|
+
return { response: prior.rows[0].response, replayed: true };
|
|
17346
|
+
}
|
|
17347
|
+
if (args.allowFresh === false) conflict("balance_payment_unavailable");
|
|
17348
|
+
args.auth.checkAuth(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
|
|
17349
|
+
const jobs = await tx.query("SELECT * FROM jobs WHERE id=$1 FOR UPDATE", [args.jobId]);
|
|
17350
|
+
const job = jobs.rows[0];
|
|
17351
|
+
if (!job || job.dvm_id !== null && job.dvm_id !== args.dvmId || job.requester_pubkey !== pubkey) conflict("credit_payment_owner_mismatch");
|
|
17352
|
+
if (["completed", "failed", "cancelled"].includes(job.status)) conflict("job_not_active");
|
|
17353
|
+
const asks = await tx.query(
|
|
17354
|
+
`SELECT data FROM job_messages WHERE job_id=$1 AND data->>'type'='payment-request'
|
|
17355
|
+
AND data->>'from'='provider' AND status='verified' ORDER BY seq DESC LIMIT 1`,
|
|
17356
|
+
[args.jobId]
|
|
17357
|
+
);
|
|
17358
|
+
const ask = asks.rows[0]?.data;
|
|
17359
|
+
if (!ask || ask.seq !== request_seq || !ask.content.credit || ask.content.credit.amount_micro !== amount_micro || ask.content.credit.currency !== currency || Number(job.pending_payment_fiat_micro) !== amount_micro || job.pending_payment_fiat_currency !== currency || Number(job.pending_payment_msats) <= 0 || job.asked_topup_micro === null || Number(job.asked_topup_micro) < amount_micro) {
|
|
17360
|
+
conflict("credit_payment_ask_mismatch");
|
|
17361
|
+
}
|
|
17362
|
+
if ((job.credit_id !== null || job.draw_id !== null) && (job.credit_id !== credit_id || job.draw_id !== draw_id)) conflict();
|
|
17363
|
+
const { rows: [credit] } = await tx.query(
|
|
17364
|
+
"SELECT caller_pubkey,currency FROM credits WHERE credit_id=$1",
|
|
17365
|
+
[credit_id]
|
|
17366
|
+
);
|
|
17367
|
+
if (!credit || credit.caller_pubkey !== pubkey) conflict("credit_payment_owner_mismatch");
|
|
17368
|
+
if (credit.currency !== currency) conflict("credit_payment_currency_mismatch");
|
|
17369
|
+
const seq = Number(job.next_seq);
|
|
17370
|
+
const message = { seq, from: "requester", type: "payment", timestamp: Math.floor(Date.now() / 1e3), content: { credit: args.raw } };
|
|
17371
|
+
await tx.query(`INSERT INTO job_messages (job_id,seq,data,status,created_at_ms)
|
|
17372
|
+
VALUES ($1,$2,$3,'pending-verification',$4)`, [args.jobId, seq, JSON.stringify(message), Date.now()]);
|
|
17373
|
+
if (job.credit_id === null) {
|
|
17374
|
+
const existing = await tx.query("SELECT 1 FROM credit_draws WHERE credit_id=$1 AND draw_id=$2", [credit_id, draw_id]);
|
|
17375
|
+
if (existing.rowCount) conflict();
|
|
17376
|
+
await args.ledger.draw({
|
|
17377
|
+
creditId: credit_id,
|
|
17378
|
+
drawId: draw_id,
|
|
17379
|
+
amountMicro: 0,
|
|
17380
|
+
jobId: args.jobId,
|
|
17381
|
+
hostingPaymentSeq: seq,
|
|
17382
|
+
tx
|
|
17383
|
+
});
|
|
17384
|
+
}
|
|
17385
|
+
const grown = await args.ledger.growDraw({
|
|
17386
|
+
creditId: credit_id,
|
|
17387
|
+
drawId: draw_id,
|
|
17388
|
+
jobId: args.jobId,
|
|
17389
|
+
spendExistingBalance: true,
|
|
17390
|
+
hostingPaymentSeq: seq,
|
|
17391
|
+
addAmountMicro: amount_micro,
|
|
17392
|
+
capMicro: Number(job.asked_topup_micro),
|
|
17393
|
+
tx
|
|
17394
|
+
});
|
|
17395
|
+
if (grown.addedAmountMicro !== amount_micro) conflict("credit_payment_ask_mismatch");
|
|
17396
|
+
const basis = grown.railValue;
|
|
17397
|
+
const nativeAsset = basis.nativeAsset;
|
|
17398
|
+
await tx.query(
|
|
17399
|
+
`UPDATE jobs SET next_seq=next_seq+1,
|
|
17400
|
+
paid_msats=paid_msats+$2, pending_payment_msats=NULL,
|
|
17401
|
+
pending_payment_fiat_micro=NULL, pending_payment_fiat_currency=NULL,
|
|
17402
|
+
pending_mpp_challenge_ids=NULL, pending_x402_nonce=NULL, pending_x402_amount_usdc_micro=NULL,
|
|
17403
|
+
credit_id=$3, draw_id=$4, payment_rail=$5, payment_mint=$6, payment_tx_hash=$7,
|
|
17404
|
+
native_amount=$8, native_asset=$9, last_activity_at=$10 WHERE id=$1`,
|
|
17405
|
+
[
|
|
17406
|
+
args.jobId,
|
|
17407
|
+
grown.addedRailValue.drawMsats,
|
|
17408
|
+
credit_id,
|
|
17409
|
+
draw_id,
|
|
17410
|
+
basis.rail,
|
|
17411
|
+
basis.mint,
|
|
17412
|
+
drawSettlementRef(args.dvmId, credit_id, draw_id),
|
|
17413
|
+
basis.drawNative,
|
|
17414
|
+
nativeAsset,
|
|
17415
|
+
Date.now()
|
|
17416
|
+
]
|
|
17417
|
+
);
|
|
17418
|
+
await tx.query("UPDATE job_messages SET status='verified' WHERE job_id=$1 AND seq=$2", [args.jobId, seq]);
|
|
17419
|
+
const response = { seq };
|
|
17420
|
+
await tx.query(
|
|
17421
|
+
"INSERT INTO credit_balance_payments(payment_id,fingerprint,response,job_id,request_seq) VALUES($1,$2,$3,$4,$5)",
|
|
17422
|
+
[payment_id, fingerprint, JSON.stringify(response), args.jobId, request_seq]
|
|
17423
|
+
);
|
|
17424
|
+
await tx.query("SELECT pg_notify('job_messages', $1)", [args.jobId]);
|
|
17425
|
+
await tx.query("COMMIT");
|
|
17426
|
+
return { response, replayed: false };
|
|
17427
|
+
} catch (error) {
|
|
17428
|
+
await tx.query("ROLLBACK").catch(() => void 0);
|
|
17429
|
+
if (error instanceof CreditLedgerError && error.code === "insufficient_credit") {
|
|
17430
|
+
const remaining = error.details.availableMicro ?? 0;
|
|
17431
|
+
throw new BalancePaymentError(402, {
|
|
17432
|
+
error: "insufficient_credit",
|
|
17433
|
+
remaining_micro: remaining,
|
|
17434
|
+
shortfall_micro: Math.max(0, auth.amount_micro - remaining)
|
|
17435
|
+
});
|
|
17436
|
+
}
|
|
17437
|
+
if (error instanceof CreditLedgerError) {
|
|
17438
|
+
throw new BalancePaymentError(409, { error: error.code });
|
|
17439
|
+
}
|
|
17440
|
+
throw error;
|
|
17441
|
+
} finally {
|
|
17442
|
+
tx.release();
|
|
17443
|
+
}
|
|
17444
|
+
}
|
|
17445
|
+
|
|
17189
17446
|
// src/sdk/server/app.ts
|
|
17190
|
-
import { createHash as
|
|
17447
|
+
import { createHash as createHash11, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
|
|
17191
17448
|
import { Hono } from "hono";
|
|
17192
17449
|
import { getCookie } from "hono/cookie";
|
|
17193
17450
|
import { cors } from "hono/cors";
|
|
17194
17451
|
import { streamSSE } from "hono/streaming";
|
|
17195
|
-
import { toJSONSchema, z as
|
|
17452
|
+
import { toJSONSchema, z as z3 } from "zod";
|
|
17196
17453
|
|
|
17197
17454
|
// src/lib/git-sha.ts
|
|
17198
17455
|
var DIRTY_SUFFIX = "-dirty";
|
|
@@ -20238,7 +20495,7 @@ initSchemaForm();
|
|
|
20238
20495
|
}
|
|
20239
20496
|
|
|
20240
20497
|
// src/sdk/server/request-fingerprint.ts
|
|
20241
|
-
import { createHash as
|
|
20498
|
+
import { createHash as createHash10 } from "crypto";
|
|
20242
20499
|
function requestFingerprint(req, opts) {
|
|
20243
20500
|
const canonical = canonicalize({
|
|
20244
20501
|
capability: req.capability,
|
|
@@ -20246,7 +20503,7 @@ function requestFingerprint(req, opts) {
|
|
|
20246
20503
|
params: req.params ?? {},
|
|
20247
20504
|
data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
|
|
20248
20505
|
});
|
|
20249
|
-
return
|
|
20506
|
+
return createHash10("sha256").update(canonical).digest("hex");
|
|
20250
20507
|
}
|
|
20251
20508
|
var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
|
|
20252
20509
|
function stripEnvelope(data) {
|
|
@@ -21292,7 +21549,7 @@ var DVMServer = class {
|
|
|
21292
21549
|
const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
|
|
21293
21550
|
return {
|
|
21294
21551
|
dvm_id: dvmId,
|
|
21295
|
-
builder_pubkey:
|
|
21552
|
+
builder_pubkey: createHash11("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
|
|
21296
21553
|
};
|
|
21297
21554
|
}
|
|
21298
21555
|
throw new AuthAudienceError();
|
|
@@ -21573,6 +21830,9 @@ var DVMServer = class {
|
|
|
21573
21830
|
creditTtlMs: this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : void 0
|
|
21574
21831
|
};
|
|
21575
21832
|
}
|
|
21833
|
+
balancePaymentSupported(includeDisabled = false) {
|
|
21834
|
+
return (includeDisabled || this.opts.env.DVMKIT_BALANCE_PAYMENTS !== "false") && !!this.descriptor.credit && !!this.opts.db && this.creditLedger instanceof CreditLedger && this.jobManager.store instanceof PostgresJobStore && !!this.authAudience && !!this.descriptor.auth;
|
|
21835
|
+
}
|
|
21576
21836
|
/**
|
|
21577
21837
|
* Assemble this DVM's funding menu for a response denominated in
|
|
21578
21838
|
* `currency`. All the refusal logic — including the credits-spec
|
|
@@ -21583,7 +21843,7 @@ var DVMServer = class {
|
|
|
21583
21843
|
async creditMenu(currency, callerPubkey) {
|
|
21584
21844
|
await this.ensureTempoSessionHealth();
|
|
21585
21845
|
const mppSplit = this.creditMppSplit();
|
|
21586
|
-
|
|
21846
|
+
const menu = await buildCreditMenu({
|
|
21587
21847
|
runtime: "container",
|
|
21588
21848
|
config: this.descriptor.credit,
|
|
21589
21849
|
currency,
|
|
@@ -21602,6 +21862,8 @@ var DVMServer = class {
|
|
|
21602
21862
|
devMode: this.opts.devMode,
|
|
21603
21863
|
callerPubkey
|
|
21604
21864
|
});
|
|
21865
|
+
if (menu && this.balancePaymentSupported()) menu.balance_payment = true;
|
|
21866
|
+
return menu;
|
|
21605
21867
|
}
|
|
21606
21868
|
async handleCredit(c) {
|
|
21607
21869
|
let hostingFundingAdmissionId;
|
|
@@ -21640,7 +21902,7 @@ var DVMServer = class {
|
|
|
21640
21902
|
const paymentAuthorization = ctx.req.header("Authorization");
|
|
21641
21903
|
const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
|
|
21642
21904
|
const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
|
|
21643
|
-
const paymentArtifactHash = attachedPayment ?
|
|
21905
|
+
const paymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
21644
21906
|
hostingFundingArtifactHash = paymentArtifactHash;
|
|
21645
21907
|
if (!await control.admit(admissionId, workId, {
|
|
21646
21908
|
creditId,
|
|
@@ -21943,7 +22205,7 @@ var DVMServer = class {
|
|
|
21943
22205
|
);
|
|
21944
22206
|
} catch (err) {
|
|
21945
22207
|
const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
21946
|
-
const { message, hint } = err instanceof
|
|
22208
|
+
const { message, hint } = err instanceof z3.ZodError ? humanizeZodError(err, { schemaUrl }) : {
|
|
21947
22209
|
message: "Quote-input validation failed",
|
|
21948
22210
|
hint: `Inspect ${schemaUrl} for the expected input schema.`
|
|
21949
22211
|
};
|
|
@@ -22349,7 +22611,7 @@ var DVMServer = class {
|
|
|
22349
22611
|
const authorization = c.req.header("Authorization");
|
|
22350
22612
|
const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
|
|
22351
22613
|
const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
|
|
22352
|
-
const hostingPaymentArtifactHash = attachedPayment ?
|
|
22614
|
+
const hostingPaymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
22353
22615
|
if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
|
|
22354
22616
|
const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
|
|
22355
22617
|
hostingPaymentArtifactHash
|
|
@@ -22647,7 +22909,7 @@ var DVMServer = class {
|
|
|
22647
22909
|
}
|
|
22648
22910
|
const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
|
|
22649
22911
|
const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
|
|
22650
|
-
const requesterTokenHash = jobToken ?
|
|
22912
|
+
const requesterTokenHash = jobToken ? createHash11("sha256").update(jobToken).digest("hex") : void 0;
|
|
22651
22913
|
const job = jm.createJob(
|
|
22652
22914
|
normalisedBody,
|
|
22653
22915
|
requesterId,
|
|
@@ -23143,6 +23405,53 @@ var DVMServer = class {
|
|
|
23143
23405
|
const id = c.req.param("id") ?? "";
|
|
23144
23406
|
const jm = this.jobManager;
|
|
23145
23407
|
const activeJob = jm.activeJobs.get(id);
|
|
23408
|
+
const wire = await c.req.raw.clone().json().catch(() => void 0);
|
|
23409
|
+
if (wire?.type === "payment" && wire.content && typeof wire.content === "object" && "credit" in wire.content) {
|
|
23410
|
+
const record2 = await jm.store.get(id);
|
|
23411
|
+
if (!record2 || !isJobOwner(c, record2)) return c.json({ error: "Job not found" }, 404);
|
|
23412
|
+
if (Object.keys(wire.content).some((key) => key !== "credit")) {
|
|
23413
|
+
return c.json({ error: "invalid_message", message: "Credit authorization cannot be combined with a rail payment" }, 400);
|
|
23414
|
+
}
|
|
23415
|
+
if (!this.balancePaymentSupported(true) || !this.opts.db || !this.descriptor.auth || !(this.creditLedger instanceof CreditLedger)) {
|
|
23416
|
+
return c.json({ error: "balance_payment_unavailable" }, 409);
|
|
23417
|
+
}
|
|
23418
|
+
try {
|
|
23419
|
+
const result = await commitBalancePayment({
|
|
23420
|
+
pool: this.opts.db,
|
|
23421
|
+
ledger: this.creditLedger,
|
|
23422
|
+
auth: this.descriptor.auth,
|
|
23423
|
+
domain: this.signedRequestDomain(c, null),
|
|
23424
|
+
jobId: id,
|
|
23425
|
+
dvmId: this.opts.dvmId ?? "",
|
|
23426
|
+
raw: wire.content.credit,
|
|
23427
|
+
allowFresh: this.opts.env.DVMKIT_BALANCE_PAYMENTS !== "false"
|
|
23428
|
+
});
|
|
23429
|
+
try {
|
|
23430
|
+
await this.opts.db.query("SELECT pg_notify('job_messages', $1)", [id]);
|
|
23431
|
+
if (!activeJob) {
|
|
23432
|
+
const fresh = await jm.store.get(id);
|
|
23433
|
+
if (fresh?.status === "awaiting-input" && isStreamableJobStore(jm.store)) {
|
|
23434
|
+
fresh.messages = await jm.store.getMessages(id, 0);
|
|
23435
|
+
fresh.seq = Math.max(fresh.seq, ...fresh.messages.map((message) => message.seq));
|
|
23436
|
+
const ask = fresh.messages.findLast((message) => message.from === "provider" && message.type === "payment-request");
|
|
23437
|
+
const authorized = wire.content.credit;
|
|
23438
|
+
if (ask?.seq === authorized.request_seq && !jm.activeJobs.has(id)) {
|
|
23439
|
+
const { job: job2 } = jm.reactivateJob(fresh);
|
|
23440
|
+
const lock = await jm.store.tryReactivationLock(id, () => this.runReactivation(jm, job2, { type: "payment", content: wire.content }, fresh.status));
|
|
23441
|
+
if (!lock.acquired) this.teardownReactivationScaffold(jm, job2);
|
|
23442
|
+
}
|
|
23443
|
+
}
|
|
23444
|
+
}
|
|
23445
|
+
} catch {
|
|
23446
|
+
diagnosticConsole.error({ level: "balance_payment_resume_unavailable", job_id: id });
|
|
23447
|
+
}
|
|
23448
|
+
return c.json(result.response, 201);
|
|
23449
|
+
} catch (error) {
|
|
23450
|
+
if (error instanceof SignedRequestError) return buildAuthErrorResponse(c, error, this.publicOrigin);
|
|
23451
|
+
if (error instanceof BalancePaymentError) return c.json(error.body, error.status);
|
|
23452
|
+
throw error;
|
|
23453
|
+
}
|
|
23454
|
+
}
|
|
23146
23455
|
if (activeJob) {
|
|
23147
23456
|
if (!isJobOwner(c, activeJob)) return c.json({ error: "Job not found" }, 404);
|
|
23148
23457
|
if (isTerminal(activeJob.status)) {
|
|
@@ -23212,6 +23521,10 @@ var DVMServer = class {
|
|
|
23212
23521
|
}
|
|
23213
23522
|
const contentErr = validateMessageContent(body.type, body.content, this.opts.devMode === true);
|
|
23214
23523
|
if (contentErr) return c.json({ error: "invalid_message", message: contentErr }, 400);
|
|
23524
|
+
if (isStreamableJobStore(jm.store)) {
|
|
23525
|
+
record.messages = await jm.store.getMessages(id, 0);
|
|
23526
|
+
record.seq = Math.max(record.seq, ...record.messages.map((message) => message.seq));
|
|
23527
|
+
}
|
|
23215
23528
|
let job;
|
|
23216
23529
|
try {
|
|
23217
23530
|
({ job } = jm.reactivateJob(record));
|
|
@@ -23312,6 +23625,10 @@ var DVMServer = class {
|
|
|
23312
23625
|
*/
|
|
23313
23626
|
async runReactivation(jm, job, body, priorStatus) {
|
|
23314
23627
|
if (priorStatus === "awaiting-input") {
|
|
23628
|
+
if (jm.activeJobs.get(job.id) !== job) {
|
|
23629
|
+
this.teardownReactivationScaffold(jm, job);
|
|
23630
|
+
return;
|
|
23631
|
+
}
|
|
23315
23632
|
const claimed = await jm.claimAwaitingInputForReactivation(job.id);
|
|
23316
23633
|
if (!claimed) {
|
|
23317
23634
|
this.teardownReactivationScaffold(jm, job);
|
|
@@ -23341,9 +23658,11 @@ var DVMServer = class {
|
|
|
23341
23658
|
* must not persist our stale snapshot; clearing the appender prevents that.
|
|
23342
23659
|
*/
|
|
23343
23660
|
teardownReactivationScaffold(jm, job) {
|
|
23344
|
-
jm.
|
|
23345
|
-
|
|
23346
|
-
|
|
23661
|
+
if (jm.activeJobs.get(job.id) === job) {
|
|
23662
|
+
jm.clearIdleTimer(job.id);
|
|
23663
|
+
jm.unwatchDurableStatus(job.id);
|
|
23664
|
+
jm.activeJobs.delete(job.id);
|
|
23665
|
+
}
|
|
23347
23666
|
job.messageAppender = void 0;
|
|
23348
23667
|
}
|
|
23349
23668
|
async handleDeleteJob(c) {
|
|
@@ -23393,7 +23712,7 @@ function isJobOwner(c, job) {
|
|
|
23393
23712
|
if (job.requesterTokenHash) {
|
|
23394
23713
|
const token = c.req.header("X-Job-Token");
|
|
23395
23714
|
if (!token) return false;
|
|
23396
|
-
const provided =
|
|
23715
|
+
const provided = createHash11("sha256").update(token).digest("hex");
|
|
23397
23716
|
return timingSafeEqualHex(provided, job.requesterTokenHash);
|
|
23398
23717
|
}
|
|
23399
23718
|
return c.get("requesterId") === job.requesterId;
|