@dvmkit/sdk 0.10.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-STJBBVNL.js → chunk-2VGMZ7VF.js} +487 -71
- package/dist/{chunk-3Q6FTZDB.js → chunk-3YFFZLW7.js} +2 -2
- package/dist/{chunk-VMHYGKWT.js → chunk-566CFXGU.js} +9 -1
- package/dist/chunk-5H7P3ORL.js +92 -0
- package/dist/{chunk-3R7NQA3Z.js → chunk-7KQLPVV7.js} +1 -1
- package/dist/{chunk-JNECYURT.js → chunk-D4FFS3IS.js} +1 -1
- package/dist/{chunk-VTHYQCDB.js → chunk-E7ZBCT5U.js} +1 -1
- package/dist/{chunk-MMGSYMIC.js → chunk-EP7OVWI5.js} +1 -1
- package/dist/{chunk-LDIPVPKI.js → chunk-FFRTAQUW.js} +1 -1
- package/dist/chunk-H3GGFEE5.js +57 -0
- package/dist/{chunk-M3T5JYDJ.js → chunk-HNXMWHLU.js} +1 -1
- package/dist/{chunk-UKD3R2KU.js → chunk-HW2UCOOC.js} +4 -55
- package/dist/{chunk-FFXXGCVG.js → chunk-ITRSITFP.js} +1 -1
- package/dist/{chunk-MU3IE434.js → chunk-JLT2YIBS.js} +267 -52
- package/dist/{chunk-KFCEXCZM.js → chunk-KJZVTWMX.js} +1 -1
- package/dist/{chunk-W3VGCNG6.js → chunk-LDKV4UHI.js} +1 -1
- package/dist/{chunk-AJ6UHXUA.js → chunk-M4NFYEEY.js} +1 -1
- package/dist/{chunk-MB6R4XPZ.js → chunk-NS2OMIU7.js} +15 -3
- package/dist/{chunk-YOSZIWC3.js → chunk-POYUX2NN.js} +149 -11
- package/dist/{chunk-VUGEEMLT.js → chunk-RPUFWIT4.js} +11 -89
- package/dist/{chunk-SYBFMHXQ.js → chunk-S7PIFHE4.js} +590 -67
- package/dist/{chunk-2OGBZPPJ.js → chunk-TWKQFIOW.js} +21 -3
- package/dist/{chunk-EWZFANHJ.js → chunk-VWIMQ5H5.js} +1 -1
- package/dist/{chunk-2XQLL6N6.js → chunk-WFCVS6QB.js} +2 -2
- package/dist/{chunk-QGNXYVBO.js → chunk-Z4FOPROB.js} +17 -0
- package/dist/{content-retention-CrILCNxk.d.ts → content-retention-a1yRdmhI.d.ts} +1 -1
- package/dist/{credit-ledger-4PZNAMDT.js → credit-ledger-MF23UKRJ.js} +6 -3
- package/dist/{credit-menu-BWsVqMh-.d.ts → credit-menu-B2T-O9oY.d.ts} +11 -4
- package/dist/{fx-BM5h4o5S.d.ts → fx-DLhscRUh.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +6 -5
- package/dist/internal/caller.d.ts +8 -6
- package/dist/internal/caller.js +27 -22
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +24 -22
- package/dist/{job-store-92v8e1LQ.d.ts → job-store-B8Ijr5V6.d.ts} +3 -1
- package/dist/{lightning-backend-9oky_wJi.d.ts → lightning-backend-DmwMSgaM.d.ts} +1 -1
- package/dist/memory-credit-ledger-ERD764PS.js +14 -0
- package/dist/{mpp-setup-FURD45LI.js → mpp-setup-JU3MT7QX.js} +3 -3
- package/dist/{payout-reporter-HVJ6WUF3.js → payout-reporter-NXWQVKXW.js} +2 -2
- package/dist/{postgres-consumed-credential-store-WO6OCFXZ.js → postgres-consumed-credential-store-WXJLM6J7.js} +1 -1
- package/dist/{postgres-job-store-H45PRBLA.js → postgres-job-store-ZVMAN6L2.js} +2 -2
- package/dist/{postgres-replay-store-XXJSA457.js → postgres-replay-store-QJ4FG6AR.js} +2 -2
- package/dist/{pricing-X4XU5HXY.js → pricing-KCLENT2C.js} +3 -3
- package/dist/{revenue-reporter-EV3K5L6B.js → revenue-reporter-HD4GMXGD.js} +2 -2
- package/dist/server/index.d.ts +30 -12
- package/dist/server/index.js +32 -30
- package/dist/{step-cache--7yA5Ihr.d.ts → step-cache-DQqpwXa8.d.ts} +206 -47
- package/dist/{tempo-charge-store-5JOIYUIH.js → tempo-charge-store-7LTUAQHQ.js} +2 -2
- package/dist/{tempo-session-store-S6QMLQUA.js → tempo-session-store-LYAIDR2B.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +6 -6
- package/dist/{usd-C7lvLvLk.d.ts → usd-D5kk2nIT.d.ts} +1 -1
- package/dist/{x402-EN5LSOVM.js → x402-RUJ4LUKF.js} +3 -3
- package/package.json +1 -1
- package/recipes/postgres/financial-export-v1.sql +19 -5
- package/dist/memory-credit-ledger-RP7DT5IY.js +0 -11
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-XRLKGCAW.js";
|
|
6
6
|
import {
|
|
7
7
|
x402EarnedCeiling
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ITRSITFP.js";
|
|
9
9
|
import {
|
|
10
10
|
createX402FacilitatorAuthHeaders,
|
|
11
11
|
createX402FacilitatorClient,
|
|
@@ -14,16 +14,13 @@ import {
|
|
|
14
14
|
verifyWithFacilitator,
|
|
15
15
|
withPaymentDiagnostics,
|
|
16
16
|
withPaymentStage
|
|
17
|
-
} from "./chunk-
|
|
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-
|
|
24
|
-
import {
|
|
25
|
-
parseStoreValue
|
|
26
|
-
} from "./chunk-YG7G4DPZ.js";
|
|
23
|
+
} from "./chunk-TWKQFIOW.js";
|
|
27
24
|
import {
|
|
28
25
|
SIGNED_ENVELOPE_FIELDS,
|
|
29
26
|
SignedRequestError,
|
|
@@ -31,7 +28,7 @@ import {
|
|
|
31
28
|
humanizeZodIssues,
|
|
32
29
|
signedRequestInput,
|
|
33
30
|
zodIssuesOf
|
|
34
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-WFCVS6QB.js";
|
|
35
32
|
import {
|
|
36
33
|
MemoryJobStore,
|
|
37
34
|
UNCAPPABLE_ASK_TOTAL,
|
|
@@ -40,7 +37,7 @@ import {
|
|
|
40
37
|
isJobRetentionStore,
|
|
41
38
|
isReceiptIssuingStore,
|
|
42
39
|
isStreamableJobStore
|
|
43
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-KJZVTWMX.js";
|
|
44
41
|
import {
|
|
45
42
|
ContentHoldError,
|
|
46
43
|
JobCancelledError,
|
|
@@ -66,7 +63,6 @@ import {
|
|
|
66
63
|
attestedDvmIdentity,
|
|
67
64
|
buildAdminChallenge,
|
|
68
65
|
checkMintHealth,
|
|
69
|
-
computeResultHash,
|
|
70
66
|
createMonotonicClock,
|
|
71
67
|
formatStaleness,
|
|
72
68
|
getWallet,
|
|
@@ -75,12 +71,9 @@ import {
|
|
|
75
71
|
isX402SettlementReconciliationReason,
|
|
76
72
|
parseTraceparent,
|
|
77
73
|
receiptPubkeyFromSecret,
|
|
78
|
-
signDrainReceipt,
|
|
79
|
-
signFundingReceipt,
|
|
80
|
-
signReceipt,
|
|
81
74
|
toLockPubkey,
|
|
82
75
|
verifyChallenge
|
|
83
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-RPUFWIT4.js";
|
|
84
77
|
import {
|
|
85
78
|
X402_BATCH_SETTLEMENT_SCHEME,
|
|
86
79
|
X402_DEFAULT_FACILITATOR,
|
|
@@ -103,12 +96,11 @@ import {
|
|
|
103
96
|
import {
|
|
104
97
|
RAIL_REFUNDABLE,
|
|
105
98
|
canonicalRequestPath,
|
|
106
|
-
canonicalize,
|
|
107
99
|
isZodSchema
|
|
108
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-HW2UCOOC.js";
|
|
109
101
|
import {
|
|
110
102
|
createConsoleLogger
|
|
111
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-7KQLPVV7.js";
|
|
112
104
|
import {
|
|
113
105
|
StepCache
|
|
114
106
|
} from "./chunk-BQJIFRAZ.js";
|
|
@@ -125,7 +117,7 @@ import {
|
|
|
125
117
|
roundUsd,
|
|
126
118
|
satsToFiat,
|
|
127
119
|
validateCurrency
|
|
128
|
-
} from "./chunk-
|
|
120
|
+
} from "./chunk-LDKV4UHI.js";
|
|
129
121
|
import {
|
|
130
122
|
CreditLedger,
|
|
131
123
|
CreditLedgerError,
|
|
@@ -141,17 +133,29 @@ import {
|
|
|
141
133
|
isNonChannelBitcoinRail,
|
|
142
134
|
netOwedSats,
|
|
143
135
|
x402SettlementPending
|
|
144
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-2VGMZ7VF.js";
|
|
137
|
+
import {
|
|
138
|
+
parseStoreValue
|
|
139
|
+
} from "./chunk-YG7G4DPZ.js";
|
|
145
140
|
import {
|
|
146
141
|
withSdkInitLock
|
|
147
142
|
} from "./chunk-COYKJRMM.js";
|
|
143
|
+
import {
|
|
144
|
+
computeResultHash,
|
|
145
|
+
signDrainReceipt,
|
|
146
|
+
signFundingReceipt,
|
|
147
|
+
signReceipt
|
|
148
|
+
} from "./chunk-5H7P3ORL.js";
|
|
148
149
|
import {
|
|
149
150
|
withPostgresRecoveryContext
|
|
150
|
-
} from "./chunk-
|
|
151
|
+
} from "./chunk-EP7OVWI5.js";
|
|
151
152
|
import {
|
|
152
153
|
DvmError,
|
|
153
154
|
x402NetworkByCaip2
|
|
154
155
|
} from "./chunk-BPYMEMQX.js";
|
|
156
|
+
import {
|
|
157
|
+
canonicalize
|
|
158
|
+
} from "./chunk-H3GGFEE5.js";
|
|
155
159
|
import {
|
|
156
160
|
TEMPO_MODERATO_CHAIN_ID,
|
|
157
161
|
TEMPO_MODERATO_RPC,
|
|
@@ -162,17 +166,17 @@ import {
|
|
|
162
166
|
installHostedTempoCloseGuard,
|
|
163
167
|
isTempoInsufficientFundsError,
|
|
164
168
|
resolveTempoNetwork
|
|
165
|
-
} from "./chunk-
|
|
169
|
+
} from "./chunk-JLT2YIBS.js";
|
|
166
170
|
import {
|
|
167
171
|
loggers,
|
|
168
172
|
pinDimension,
|
|
169
173
|
withSpan,
|
|
170
174
|
withTraceContext
|
|
171
|
-
} from "./chunk-
|
|
175
|
+
} from "./chunk-HNXMWHLU.js";
|
|
172
176
|
import {
|
|
173
177
|
createDiagnosticCapabilityAuthority,
|
|
174
178
|
diagnosticConsole
|
|
175
|
-
} from "./chunk-
|
|
179
|
+
} from "./chunk-Z4FOPROB.js";
|
|
176
180
|
|
|
177
181
|
// src/sdk/server/lock-pubkey-store.ts
|
|
178
182
|
async function initLockPubkeyTable(db) {
|
|
@@ -991,6 +995,7 @@ function attachCreditMenu(body, menu) {
|
|
|
991
995
|
}
|
|
992
996
|
function toCreditTerms(menu) {
|
|
993
997
|
return {
|
|
998
|
+
...menu.balance_payment && { balance_payment: true },
|
|
994
999
|
min_micro: menu.min_micro,
|
|
995
1000
|
max_micro: menu.max_micro,
|
|
996
1001
|
ttl_ms: menu.ttl_ms,
|
|
@@ -3884,7 +3889,7 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
3884
3889
|
x402Requirements
|
|
3885
3890
|
});
|
|
3886
3891
|
}
|
|
3887
|
-
const { verifyX402Payment } = await import("./x402-
|
|
3892
|
+
const { verifyX402Payment } = await import("./x402-RUJ4LUKF.js");
|
|
3888
3893
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
3889
3894
|
x402Payment,
|
|
3890
3895
|
x402Config,
|
|
@@ -4908,7 +4913,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
|
|
|
4908
4913
|
snapshot
|
|
4909
4914
|
});
|
|
4910
4915
|
}
|
|
4911
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4916
|
+
const { verifyX402Payment } = await import("./x402-RUJ4LUKF.js");
|
|
4912
4917
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
4913
4918
|
body.content.x402_payment,
|
|
4914
4919
|
opts.x402Config,
|
|
@@ -5339,7 +5344,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
|
|
|
5339
5344
|
try {
|
|
5340
5345
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5341
5346
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5342
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5347
|
+
const { verifyX402Payment } = await import("./x402-RUJ4LUKF.js");
|
|
5343
5348
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
5344
5349
|
body.content.x402_payment,
|
|
5345
5350
|
opts.x402Config,
|
|
@@ -6943,13 +6948,37 @@ async function fundingReceiptFor(ledger, issuer, funding) {
|
|
|
6943
6948
|
|
|
6944
6949
|
// src/sdk/server/tempo-drain-leg.ts
|
|
6945
6950
|
async function completeTempoChannelDrain(args) {
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6951
|
+
if (!args.tx && args.refundedNative !== void 0 && /^\d{1,78}$/.test(String(args.refundedNative))) {
|
|
6952
|
+
try {
|
|
6953
|
+
if (args.finalization?.outcome === "payer_exit" || args.finalization?.outcome === "payer_exit_unbacked") {
|
|
6954
|
+
if (BigInt(args.refundedNative) > 0n) await args.deps.ledger.recordTempoDrainPayment?.({ creditId: args.creditId, drainId: args.drainId });
|
|
6955
|
+
} else {
|
|
6956
|
+
await args.deps.ledger.recordTempoDrainRefund?.({ creditId: args.creditId, drainId: args.drainId, refundedNative: args.refundedNative });
|
|
6957
|
+
}
|
|
6958
|
+
} catch (error) {
|
|
6959
|
+
loggers.tempo.warn("tempo_drain_refund_evidence_write_failed", {});
|
|
6960
|
+
throw error;
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
let settlementEvidence;
|
|
6964
|
+
try {
|
|
6965
|
+
settlementEvidence = refundFloorEvidence({
|
|
6966
|
+
rail: "tempo",
|
|
6967
|
+
refundedNative: args.refundedNative,
|
|
6968
|
+
debitedNative: args.debitedNative,
|
|
6969
|
+
creditId: args.creditId,
|
|
6970
|
+
drainId: args.drainId
|
|
6971
|
+
});
|
|
6972
|
+
} catch (error) {
|
|
6973
|
+
if (error instanceof DrainRefundFloorError && !args.tx) {
|
|
6974
|
+
try {
|
|
6975
|
+
await args.deps.ledger.recordTempoDrainHold?.({ creditId: args.creditId, drainId: args.drainId, reason: "refund_short" });
|
|
6976
|
+
} catch {
|
|
6977
|
+
loggers.tempo.warn("tempo_drain_hold_reason_write_failed", {});
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
throw error;
|
|
6981
|
+
}
|
|
6953
6982
|
return commitTerminalDrain(
|
|
6954
6983
|
args.deps.drainReport,
|
|
6955
6984
|
async (innerTx) => {
|
|
@@ -7690,6 +7719,11 @@ async function recoverTempoDrain(deps, mpp, record, channelId) {
|
|
|
7690
7719
|
}
|
|
7691
7720
|
if (!result) return null;
|
|
7692
7721
|
if (!/^0x[0-9a-fA-F]{64}$/.test(result)) {
|
|
7722
|
+
try {
|
|
7723
|
+
await deps.ledger.recordTempoDrainHold?.({ creditId: record.creditId, drainId: record.drainId, reason: "close_evidence_missing" });
|
|
7724
|
+
} catch {
|
|
7725
|
+
loggers.tempo.warn("tempo_drain_hold_reason_write_failed", { amount_micro: record.amountMicro });
|
|
7726
|
+
}
|
|
7693
7727
|
loggers.tempo.warn("tempo_session_drain_refund_unproven", {
|
|
7694
7728
|
channel_id: channelId,
|
|
7695
7729
|
credit_id: record.creditId,
|
|
@@ -10130,6 +10164,7 @@ var HostedSettlementReportObligations = class {
|
|
|
10130
10164
|
FROM sdk_hosted_settlement_report_obligations target
|
|
10131
10165
|
WHERE target.dvm_id=$1 AND target.scope=$2 AND target.rail='tempo' AND target.operation='close'
|
|
10132
10166
|
AND target.state IN ('unmatched','no_report')
|
|
10167
|
+
AND ($7::text IS NULL OR target.channel_id=$7)
|
|
10133
10168
|
)
|
|
10134
10169
|
SELECT * FROM candidates
|
|
10135
10170
|
WHERE ($6::boolean OR (state='unmatched' AND (retryable OR write_off_allowed)))
|
|
@@ -10141,7 +10176,8 @@ var HostedSettlementReportObligations = class {
|
|
|
10141
10176
|
args.limit,
|
|
10142
10177
|
args.after?.createdAtMs ?? null,
|
|
10143
10178
|
args.after?.obligationId ?? null,
|
|
10144
|
-
args.all ?? false
|
|
10179
|
+
args.all ?? false,
|
|
10180
|
+
args.channelId ?? null
|
|
10145
10181
|
]
|
|
10146
10182
|
);
|
|
10147
10183
|
return result.rows.map((row) => ({
|
|
@@ -12605,6 +12641,7 @@ function buildContext(job, opts) {
|
|
|
12605
12641
|
const paymentRequestContent = {
|
|
12606
12642
|
amount_msats: amountMsats,
|
|
12607
12643
|
reason,
|
|
12644
|
+
...pendingFiat && { credit: { amount_micro: pendingFiat.amountMicro, currency: pendingFiat.currency } },
|
|
12608
12645
|
...fiatAmount && { fiat_amount: fiatAmount },
|
|
12609
12646
|
...payOpts?.mints && { mints: payOpts.mints },
|
|
12610
12647
|
...payOpts?.minLocktimeSeconds && { min_locktime_seconds: payOpts.minLocktimeSeconds },
|
|
@@ -12683,8 +12720,16 @@ function buildContext(job, opts) {
|
|
|
12683
12720
|
if (job.replayPaymentIndex < payments.length) {
|
|
12684
12721
|
const payment = payments[job.replayPaymentIndex];
|
|
12685
12722
|
job.replayPaymentIndex++;
|
|
12686
|
-
|
|
12687
|
-
|
|
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;
|
|
12688
12733
|
}
|
|
12689
12734
|
job.status = "processing";
|
|
12690
12735
|
creditedToHandlerMsats += amountMsats;
|
|
@@ -12753,7 +12798,7 @@ function buildContext(job, opts) {
|
|
|
12753
12798
|
try {
|
|
12754
12799
|
const counters = await getCounters();
|
|
12755
12800
|
if (!counters || resolved) return;
|
|
12756
|
-
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);
|
|
12757
12802
|
if (satisfied) {
|
|
12758
12803
|
const resume = async () => {
|
|
12759
12804
|
job.paidMsats = counters.paidMsats;
|
|
@@ -13257,6 +13302,9 @@ var JobManager = class {
|
|
|
13257
13302
|
statusChecksInFlight = /* @__PURE__ */ new Set();
|
|
13258
13303
|
/** Job ids that were notified mid-re-read and must be re-checked. */
|
|
13259
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();
|
|
13260
13308
|
// Every local input resume holds this queue through its durable wait update.
|
|
13261
13309
|
// A prompt response and a payment NOTIFY must not mistake each other for a
|
|
13262
13310
|
// different machine's successful claim.
|
|
@@ -13683,7 +13731,59 @@ var JobManager = class {
|
|
|
13683
13731
|
}
|
|
13684
13732
|
async claimLocalInput(job) {
|
|
13685
13733
|
await job.messageAppenderTail;
|
|
13686
|
-
|
|
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;
|
|
13687
13787
|
}
|
|
13688
13788
|
async resumeClaimedPayment(job, resume) {
|
|
13689
13789
|
const retry = async () => {
|
|
@@ -14048,8 +14148,9 @@ var JobManager = class {
|
|
|
14048
14148
|
const jobId = job.id;
|
|
14049
14149
|
job.messageAppender = (msg, opts) => {
|
|
14050
14150
|
const prev = job.messageAppenderTail ?? Promise.resolve();
|
|
14051
|
-
const next = prev.then(
|
|
14052
|
-
|
|
14151
|
+
const next = prev.then(() => {
|
|
14152
|
+
const pending = job.pendingPayment;
|
|
14153
|
+
return store.appendOutgoing(
|
|
14053
14154
|
jobId,
|
|
14054
14155
|
{
|
|
14055
14156
|
from: msg.from,
|
|
@@ -14058,7 +14159,11 @@ var JobManager = class {
|
|
|
14058
14159
|
content: msg.content
|
|
14059
14160
|
},
|
|
14060
14161
|
opts
|
|
14061
|
-
).
|
|
14162
|
+
).then((stored) => {
|
|
14163
|
+
if (msg.type === "payment-request" && pending && job.pendingPayment === pending) {
|
|
14164
|
+
this.paymentAskSeqs.set(pending, stored);
|
|
14165
|
+
}
|
|
14166
|
+
}).catch((_err) => {
|
|
14062
14167
|
diagnosticConsole.error(
|
|
14063
14168
|
{
|
|
14064
14169
|
level: "job_message_append_failed",
|
|
@@ -14072,8 +14177,8 @@ var JobManager = class {
|
|
|
14072
14177
|
error: void 0
|
|
14073
14178
|
}
|
|
14074
14179
|
);
|
|
14075
|
-
})
|
|
14076
|
-
);
|
|
14180
|
+
});
|
|
14181
|
+
});
|
|
14077
14182
|
job.messageAppenderTail = next;
|
|
14078
14183
|
};
|
|
14079
14184
|
}
|
|
@@ -14804,10 +14909,13 @@ var JobManager = class {
|
|
|
14804
14909
|
async claimAwaitingInputForReactivation(jobId) {
|
|
14805
14910
|
if (!isStreamableJobStore(this.jobStore)) return true;
|
|
14806
14911
|
const store = this.jobStore;
|
|
14912
|
+
const scaffold = this._activeJobs.get(jobId);
|
|
14913
|
+
const askSeq = scaffold ? this.reactivationAskSeqs.get(scaffold) : void 0;
|
|
14807
14914
|
const deadline = this.monotonicNowMs() + this.reactivationClaimGraceMs;
|
|
14808
14915
|
for (; ; ) {
|
|
14809
14916
|
if (this.monotonicNowMs() >= deadline) {
|
|
14810
|
-
|
|
14917
|
+
if (scaffold && this._activeJobs.get(jobId) !== scaffold) return false;
|
|
14918
|
+
return this.claimInputForAsk(jobId, askSeq);
|
|
14811
14919
|
}
|
|
14812
14920
|
const record = await store.get(jobId);
|
|
14813
14921
|
if (record?.status !== "awaiting-input") return false;
|
|
@@ -14917,6 +15025,8 @@ var JobManager = class {
|
|
|
14917
15025
|
}
|
|
14918
15026
|
}
|
|
14919
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);
|
|
14920
15030
|
this._activeJobs.set(job.id, job);
|
|
14921
15031
|
this.wireMessageAppender(job);
|
|
14922
15032
|
this.watchDurableStatus(job);
|
|
@@ -15334,7 +15444,23 @@ var JobManager = class {
|
|
|
15334
15444
|
onYield: () => this.persistJob(job),
|
|
15335
15445
|
onPaymentResume: () => retryJobSnapshot(() => this.persistJob(job)),
|
|
15336
15446
|
subscribeNotifications: isStreamableJobStore(jobStore) ? (cb) => jobStore.subscribeNotifications(job.id, cb) : void 0,
|
|
15337
|
-
|
|
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,
|
|
15338
15464
|
drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
|
|
15339
15465
|
pricingCurrency: this.pricingCurrency,
|
|
15340
15466
|
claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
|
|
@@ -15343,10 +15469,13 @@ var JobManager = class {
|
|
|
15343
15469
|
if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
15344
15470
|
if (!await this.claimLocalInput(job)) return false;
|
|
15345
15471
|
if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
15472
|
+
let resumed = false;
|
|
15346
15473
|
await this.resumeClaimedPayment(job, async () => {
|
|
15474
|
+
if (!await this.adoptPaymentMetadata(job, pending)) return;
|
|
15347
15475
|
await resume?.();
|
|
15476
|
+
resumed = true;
|
|
15348
15477
|
});
|
|
15349
|
-
return
|
|
15478
|
+
return resumed;
|
|
15350
15479
|
});
|
|
15351
15480
|
} : void 0,
|
|
15352
15481
|
onSuperseded: () => {
|
|
@@ -15379,6 +15508,12 @@ var JobManager = class {
|
|
|
15379
15508
|
* fall back on is a slice of the credit's rail value at a ratio pinned
|
|
15380
15509
|
* whenever that credit was funded, so it cannot safely fund a new ask.
|
|
15381
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
|
+
}
|
|
15382
15517
|
async resolveDrawPool(job) {
|
|
15383
15518
|
const ledger = this.opts.creditLedger;
|
|
15384
15519
|
if (!ledger || !job.creditId || !job.drawId) return { basis: "rail" };
|
|
@@ -15398,8 +15533,10 @@ var JobManager = class {
|
|
|
15398
15533
|
return { basis: "unpriced" };
|
|
15399
15534
|
}
|
|
15400
15535
|
const { rail, drawMsats, creditCurrency } = draw.railValue;
|
|
15401
|
-
if (!rail || drawMsats <= 0)
|
|
15402
|
-
|
|
15536
|
+
if (!rail || drawMsats <= 0) {
|
|
15537
|
+
const acceptedBalance = await this.hasAcceptedBalancePayment(job.id);
|
|
15538
|
+
if (!acceptedBalance) return { basis: "rail", creditCurrency: creditCurrency ?? void 0 };
|
|
15539
|
+
}
|
|
15403
15540
|
if (!creditCurrency) {
|
|
15404
15541
|
this.warnDrawPool(job, "credit_currency_missing");
|
|
15405
15542
|
return { basis: "unpriced" };
|
|
@@ -15839,7 +15976,7 @@ var JobManager = class {
|
|
|
15839
15976
|
const onJobCompleted = this.opts.onJobCompleted;
|
|
15840
15977
|
const onRevenueSkippedNoRail = this.opts.onRevenueSkippedNoRail;
|
|
15841
15978
|
if (!onJobCompleted && !onRevenueSkippedNoRail) return false;
|
|
15842
|
-
if (job.status !== "completed"
|
|
15979
|
+
if (job.status !== "completed") return false;
|
|
15843
15980
|
let credit = {};
|
|
15844
15981
|
let creditRail;
|
|
15845
15982
|
let drawAmountMicro;
|
|
@@ -15876,6 +16013,7 @@ var JobManager = class {
|
|
|
15876
16013
|
settledAt: draw.resolvedAt ?? void 0
|
|
15877
16014
|
};
|
|
15878
16015
|
}
|
|
16016
|
+
if (job.paidMsats <= 0 && !(drawAmountMicro !== void 0 && drawAmountMicro > 0 && typeof credit.creditCurrency === "string" && creditRail && isFundingRail(creditRail))) return false;
|
|
15879
16017
|
if (!job.paymentRail) {
|
|
15880
16018
|
if (job.creditId && job.drawId) {
|
|
15881
16019
|
const creditCurrency = typeof credit.creditCurrency === "string" ? credit.creditCurrency : void 0;
|
|
@@ -17151,13 +17289,167 @@ function withWallClockDeadline(promise, timeoutMs) {
|
|
|
17151
17289
|
});
|
|
17152
17290
|
}
|
|
17153
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
|
+
|
|
17154
17446
|
// src/sdk/server/app.ts
|
|
17155
|
-
import { createHash as
|
|
17447
|
+
import { createHash as createHash11, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
|
|
17156
17448
|
import { Hono } from "hono";
|
|
17157
17449
|
import { getCookie } from "hono/cookie";
|
|
17158
17450
|
import { cors } from "hono/cors";
|
|
17159
17451
|
import { streamSSE } from "hono/streaming";
|
|
17160
|
-
import { toJSONSchema, z as
|
|
17452
|
+
import { toJSONSchema, z as z3 } from "zod";
|
|
17161
17453
|
|
|
17162
17454
|
// src/lib/git-sha.ts
|
|
17163
17455
|
var DIRTY_SUFFIX = "-dirty";
|
|
@@ -17170,6 +17462,8 @@ function shortSha(sha) {
|
|
|
17170
17462
|
}
|
|
17171
17463
|
|
|
17172
17464
|
// src/sdk/server/admin-credit.ts
|
|
17465
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
17466
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
17173
17467
|
import {
|
|
17174
17468
|
Amount as Amount3,
|
|
17175
17469
|
CheckStateEnum as CheckStateEnum3,
|
|
@@ -17177,6 +17471,7 @@ import {
|
|
|
17177
17471
|
getTokenMetadata as getTokenMetadata2
|
|
17178
17472
|
} from "@cashu/cashu-ts";
|
|
17179
17473
|
function installAdminCreditRoutes(app, opts) {
|
|
17474
|
+
app.post("/admin/credit/refund-draw", (c) => handleRefundDraw(c, opts));
|
|
17180
17475
|
app.get("/admin/credit/pending-drains", (c) => handlePendingDrains(c, opts));
|
|
17181
17476
|
app.get("/admin/credit/bitcoin-liability", (c) => handleBitcoinLiability(c, opts));
|
|
17182
17477
|
app.post("/admin/credit/park-drain", (c) => handleParkDrain(c, opts));
|
|
@@ -17197,9 +17492,68 @@ function installAdminCreditRoutes(app, opts) {
|
|
|
17197
17492
|
app.post("/admin/credit/write-off-tempo-close-report", (c) => handleTempoCloseReportAction(c, opts, "write-off"));
|
|
17198
17493
|
app.get("/admin/credit/tempo-drains", (c) => handleTempoDrains(c, opts));
|
|
17199
17494
|
app.get("/admin/credit/tempo-losses", (c) => handleTempoLosses(c, opts));
|
|
17495
|
+
app.post("/admin/credit/book-tempo-drain", (c) => handleTempoDrainDecision(c, opts, "book_refunded"));
|
|
17496
|
+
app.post("/admin/credit/release-tempo-drain", (c) => handleTempoDrainDecision(c, opts, "release"));
|
|
17200
17497
|
app.post("/admin/credit/reconcile-tempo-drain", (c) => handleReconcileTempo(c, opts));
|
|
17201
17498
|
app.post("/admin/credit/write-off-tempo-drain", (c) => handleWriteOffTempo(c, opts));
|
|
17202
17499
|
}
|
|
17500
|
+
async function handleRefundDraw(c, opts) {
|
|
17501
|
+
const auth = await requireAuth(c, opts);
|
|
17502
|
+
if ("response" in auth) return auth.response;
|
|
17503
|
+
const body = await c.req.json().catch(() => null);
|
|
17504
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
17505
|
+
return c.json({ error: "invalid_body", message: "Request body must be a JSON object." }, 400);
|
|
17506
|
+
}
|
|
17507
|
+
const value = body;
|
|
17508
|
+
const nonempty = (item) => typeof item === "string" && item.length > 0;
|
|
17509
|
+
const byJob = nonempty(value.jobId) && value.creditId === void 0 && value.drawId === void 0;
|
|
17510
|
+
const byDraw = value.jobId === void 0 && nonempty(value.creditId) && nonempty(value.drawId);
|
|
17511
|
+
if (!byJob && !byDraw || !nonempty(value.refundId) || value.amountMicro !== void 0 && (typeof value.amountMicro !== "number" || !Number.isSafeInteger(value.amountMicro) || value.amountMicro <= 0) || value.reason !== void 0 && (typeof value.reason !== "string" || value.reason.length > 280)) {
|
|
17512
|
+
return c.json({ error: "invalid_body", message: "Provide jobId or creditId + drawId, refundId, an optional positive safe integer amountMicro, and an optional reason of at most 280 characters." }, 400);
|
|
17513
|
+
}
|
|
17514
|
+
if (!opts.ledger.durable || !opts.enqueueCreditDrawRefund) {
|
|
17515
|
+
return c.json({ error: "credit_refund_unavailable", hint: "Configure the persistent credit store and platform refund reporter, then retry with the original refund ID." }, 503);
|
|
17516
|
+
}
|
|
17517
|
+
try {
|
|
17518
|
+
const result = await opts.ledger.refundDraw({
|
|
17519
|
+
...byJob ? { jobId: value.jobId } : { creditId: value.creditId, drawId: value.drawId },
|
|
17520
|
+
refundId: value.refundId,
|
|
17521
|
+
...value.amountMicro === void 0 ? {} : { amountMicro: value.amountMicro },
|
|
17522
|
+
...value.reason === void 0 ? {} : { reason: value.reason },
|
|
17523
|
+
outbox: { dvmId: opts.dvmId, enqueue: opts.enqueueCreditDrawRefund },
|
|
17524
|
+
...opts.receiptIssuer ? { trustedReceiptPubkey: opts.receiptIssuer.pubkey } : {},
|
|
17525
|
+
...opts.now ? { nowMs: opts.now() } : {}
|
|
17526
|
+
});
|
|
17527
|
+
return c.json({
|
|
17528
|
+
status: "refunded",
|
|
17529
|
+
refund_id: result.refundId,
|
|
17530
|
+
credit_id: result.creditId,
|
|
17531
|
+
draw_id: result.drawId,
|
|
17532
|
+
job_id: result.jobId,
|
|
17533
|
+
amount_micro: result.amountMicro,
|
|
17534
|
+
currency: result.currency,
|
|
17535
|
+
ledger_seq: result.ledgerSeq,
|
|
17536
|
+
refunded_at: result.refundedAt,
|
|
17537
|
+
balance_after_micro: result.balanceAfterMicro,
|
|
17538
|
+
replayed: result.replayed
|
|
17539
|
+
});
|
|
17540
|
+
} catch (error) {
|
|
17541
|
+
if (!(error instanceof CreditLedgerError)) throw error;
|
|
17542
|
+
const hints = {
|
|
17543
|
+
draw_not_found: "Check the job ID or credit/draw IDs. If they are correct, this DVM may lack trusted ownership evidence; ask an operator to inspect that evidence. Retrying alone cannot establish ownership.",
|
|
17544
|
+
draw_not_completed: "Wait for the job to complete. Failed or cancelled jobs release their pending hold automatically.",
|
|
17545
|
+
refund_exceeds_draw: "Refund no more than the remaining unrefunded charge. Omit amountMicro to refund that remainder.",
|
|
17546
|
+
credit_not_open: "The original credit is closed or expired. Ask an operator to inspect the original payment before arranging a refund.",
|
|
17547
|
+
credit_refund_channel_unsupported: "This channel returns unused collateral through its normal exit. Ask an operator about refunding completed spending.",
|
|
17548
|
+
credit_refund_basis_unavailable: "The original payment value cannot be proven automatically. Ask an operator to inspect independent original-payment evidence before arranging a repair. Retrying alone cannot establish that evidence.",
|
|
17549
|
+
credit_refund_state_unavailable: "The original credit reopening order cannot be proven automatically. Ask an operator to inspect independent funding and drain evidence before arranging a repair. Retrying alone cannot establish that evidence.",
|
|
17550
|
+
refund_conflict: "Repeat the original arguments, or deliberately choose a new refund ID for a different refund."
|
|
17551
|
+
};
|
|
17552
|
+
const hint = hints[error.code];
|
|
17553
|
+
if (!hint) throw error;
|
|
17554
|
+
return c.json({ error: error.code, hint }, error.code === "draw_not_found" ? 404 : error.code === "refund_conflict" ? 409 : 422);
|
|
17555
|
+
}
|
|
17556
|
+
}
|
|
17203
17557
|
async function handleX402ExactSettlements(c, opts) {
|
|
17204
17558
|
const auth = await requireAuth(c, opts);
|
|
17205
17559
|
if ("response" in auth) return auth.response;
|
|
@@ -17318,6 +17672,9 @@ async function handleTempoLosses(c, opts) {
|
|
|
17318
17672
|
currency: loss.currency,
|
|
17319
17673
|
former_balance_micro: loss.formerBalanceMicro,
|
|
17320
17674
|
settled_on_chain_native: loss.settledOnChainNative.toString(),
|
|
17675
|
+
refunded_to_payer_native: loss.refundedToPayerNative?.toString() ?? null,
|
|
17676
|
+
loss_native: loss.lossNative?.toString() ?? null,
|
|
17677
|
+
loss_basis: loss.lossBasis ?? null,
|
|
17321
17678
|
highest_voucher_native: loss.highestVoucherNative.toString(),
|
|
17322
17679
|
consumed_service_micro: loss.consumedServiceMicro,
|
|
17323
17680
|
consumed_native: loss.consumedNative.toString(),
|
|
@@ -18304,13 +18661,18 @@ async function handleTempoDrains(c, opts) {
|
|
|
18304
18661
|
...after ? { after } : {},
|
|
18305
18662
|
createdBeforeMs: nowMs - minAge,
|
|
18306
18663
|
rail: "tempo",
|
|
18307
|
-
includeResolved: c.req.query("include_resolved") === "true"
|
|
18664
|
+
includeResolved: c.req.query("include_resolved") === "true",
|
|
18665
|
+
includeWrittenOff: true
|
|
18308
18666
|
});
|
|
18309
18667
|
const wantsEvidence = c.req.query("evidence") !== "false";
|
|
18310
18668
|
const rows2 = [];
|
|
18311
18669
|
for (const drain of drains) {
|
|
18312
18670
|
const evidence = wantsEvidence && drain.channelId ? await classifyTempoCloseEvidence(rail.rail, drain.channelId) : void 0;
|
|
18313
|
-
|
|
18671
|
+
const closeReports = drain.channelId && opts.hostedSettlementReports ? await opts.hostedSettlementReports.store.listTempoCloseReports({ limit: 50, all: true, channelId: drain.channelId }) : [];
|
|
18672
|
+
rows2.push({
|
|
18673
|
+
...tempoDrainBody(drain, evidence),
|
|
18674
|
+
close_reports: closeReports.map((row) => ({ obligation_id: row.id, status: row.status, written_off: row.writtenOff }))
|
|
18675
|
+
});
|
|
18314
18676
|
}
|
|
18315
18677
|
const last = drains.length === limit ? drains[drains.length - 1] : void 0;
|
|
18316
18678
|
return c.json({
|
|
@@ -18324,6 +18686,90 @@ async function handleTempoDrains(c, opts) {
|
|
|
18324
18686
|
} : {}
|
|
18325
18687
|
});
|
|
18326
18688
|
}
|
|
18689
|
+
async function handleTempoDrainDecision(c, opts, decision) {
|
|
18690
|
+
const auth = await requireAuth(c, opts);
|
|
18691
|
+
if ("response" in auth) return auth.response;
|
|
18692
|
+
const body = await c.req.json().catch(() => null);
|
|
18693
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
18694
|
+
return c.json({ error: "invalid_body", message: "Request body must be a JSON object." }, 400);
|
|
18695
|
+
}
|
|
18696
|
+
const value = body;
|
|
18697
|
+
const text = (item, max) => typeof item === "string" && item.trim().length > 0 && item.trim().length <= max;
|
|
18698
|
+
if (Object.keys(value).some((key) => !["credit_id", "drain_id", "reason", "evidence"].includes(key)) || typeof value.credit_id !== "string" || value.credit_id.length === 0 || typeof value.drain_id !== "string" || value.drain_id.length === 0 || !text(value.reason, 2e3) || value.evidence !== void 0 && !text(value.evidence, 4e3)) {
|
|
18699
|
+
return c.json({ error: "invalid_body", message: "Provide credit_id, drain_id, a reason of 1\u20132000 characters, and optional evidence of 1\u20134000 characters." }, 400);
|
|
18700
|
+
}
|
|
18701
|
+
if (!opts.ledger.durable || !opts.ledger.decideTempoDrain) {
|
|
18702
|
+
return c.json({ error: "tempo_drain_decision_unavailable", message: "A durable Tempo decision store is required." }, 503);
|
|
18703
|
+
}
|
|
18704
|
+
const args = {
|
|
18705
|
+
creditId: value.credit_id,
|
|
18706
|
+
drainId: value.drain_id,
|
|
18707
|
+
decision,
|
|
18708
|
+
reason: value.reason.trim(),
|
|
18709
|
+
...value.evidence === void 0 ? {} : { evidence: value.evidence.trim() },
|
|
18710
|
+
nowMs: opts.now ? opts.now() : Date.now()
|
|
18711
|
+
};
|
|
18712
|
+
if (decision === "book_refunded" && opts.requireDrainReport && !opts.enqueueCreditDrain) {
|
|
18713
|
+
const existing = await opts.ledger.getDrain(args);
|
|
18714
|
+
if (existing && !existing.operatorDecision && (existing.status === "pending" || existing.status === "exit_required")) {
|
|
18715
|
+
return c.json({ error: "tempo_drain_decision_unavailable", message: "Hosted booking requires a transactional drain reporter. Restore its configuration, then retry the original decision." }, 503);
|
|
18716
|
+
}
|
|
18717
|
+
}
|
|
18718
|
+
const client = await opts.db.connect();
|
|
18719
|
+
let result;
|
|
18720
|
+
try {
|
|
18721
|
+
await client.query("BEGIN");
|
|
18722
|
+
let decided;
|
|
18723
|
+
const transition = async () => {
|
|
18724
|
+
decided = await opts.ledger.decideTempoDrain({ ...args, tx: client });
|
|
18725
|
+
return decided;
|
|
18726
|
+
};
|
|
18727
|
+
if (decision === "book_refunded") {
|
|
18728
|
+
await commitTerminalDrain({ db: opts.db, dvmId: opts.dvmId, enqueueCreditDrain: opts.enqueueCreditDrain }, transition, client);
|
|
18729
|
+
} else {
|
|
18730
|
+
await transition();
|
|
18731
|
+
}
|
|
18732
|
+
result = decided;
|
|
18733
|
+
await client.query("COMMIT");
|
|
18734
|
+
} catch (error) {
|
|
18735
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
18736
|
+
if (error instanceof CreditLedgerError) {
|
|
18737
|
+
return c.json(
|
|
18738
|
+
{ error: error.code, message: error.message },
|
|
18739
|
+
error.code === "drain_not_found" || error.code === "credit_not_found" ? 404 : ["drain_decision_conflict", "tempo_drain_close_pending", "tempo_drain_refund_paid", "tempo_drain_refund_unknown"].includes(error.code) ? 409 : 422
|
|
18740
|
+
);
|
|
18741
|
+
}
|
|
18742
|
+
throw error;
|
|
18743
|
+
} finally {
|
|
18744
|
+
client.release();
|
|
18745
|
+
}
|
|
18746
|
+
if (!result.replayed) {
|
|
18747
|
+
diagnosticConsole.warn({
|
|
18748
|
+
dataset: "tempo",
|
|
18749
|
+
event: decision === "book_refunded" ? "tempo_drain_booked_as_refunded" : "tempo_drain_released",
|
|
18750
|
+
credit_id: result.drain.creditId,
|
|
18751
|
+
drain_id: result.drain.drainId,
|
|
18752
|
+
channel_id: result.drain.channelId,
|
|
18753
|
+
drain_reference: `sha256:${bytesToHex(sha256(new TextEncoder().encode(JSON.stringify([result.drain.creditId, result.drain.drainId]))))}`,
|
|
18754
|
+
amount_micro: result.drain.amountMicro,
|
|
18755
|
+
amount_native: result.drain.drainedNative?.toString() ?? null,
|
|
18756
|
+
currency: result.drain.currency
|
|
18757
|
+
});
|
|
18758
|
+
}
|
|
18759
|
+
return c.json({
|
|
18760
|
+
status: result.drain.status,
|
|
18761
|
+
credit_id: result.drain.creditId,
|
|
18762
|
+
drain_id: result.drain.drainId,
|
|
18763
|
+
channel_id: result.drain.channelId,
|
|
18764
|
+
amount_micro: result.drain.amountMicro,
|
|
18765
|
+
amount_native: result.drain.drainedNative,
|
|
18766
|
+
currency: result.drain.currency,
|
|
18767
|
+
decision: result.drain.operatorDecision,
|
|
18768
|
+
balance_micro: result.credit.balanceMicro,
|
|
18769
|
+
spendable_micro: result.spendableMicro,
|
|
18770
|
+
replayed: result.replayed
|
|
18771
|
+
});
|
|
18772
|
+
}
|
|
18327
18773
|
async function handleReconcileTempo(c, opts) {
|
|
18328
18774
|
const auth = await requireAuth(c, opts);
|
|
18329
18775
|
if ("response" in auth) return auth.response;
|
|
@@ -18337,6 +18783,12 @@ async function handleReconcileTempo(c, opts) {
|
|
|
18337
18783
|
if (record.status === "sent") return tempoReconciledBody(c, record, channelId, true);
|
|
18338
18784
|
const evidence = await classifyTempoCloseEvidence(rail.rail, channelId);
|
|
18339
18785
|
if (evidence.chain !== "found") return tempoEvidenceRefusal(c, evidence);
|
|
18786
|
+
try {
|
|
18787
|
+
await opts.ledger.recordTempoDrainRefund?.({ creditId: record.creditId, drainId: record.drainId, refundedNative: evidence.refundedToPayer });
|
|
18788
|
+
} catch (error) {
|
|
18789
|
+
diagnosticConsole.warn({ dataset: "tempo", event: "tempo_drain_refund_evidence_write_failed", amount_micro: record.amountMicro });
|
|
18790
|
+
throw error;
|
|
18791
|
+
}
|
|
18340
18792
|
const client = await opts.db.connect();
|
|
18341
18793
|
let booked;
|
|
18342
18794
|
try {
|
|
@@ -18825,6 +19277,10 @@ function tempoDrainBody(drain, evidence) {
|
|
|
18825
19277
|
created_at: drain.createdAt,
|
|
18826
19278
|
write_off_note: drain.writeOffNote,
|
|
18827
19279
|
written_off_at: drain.writtenOffAt,
|
|
19280
|
+
decision: drain.operatorDecision ?? null,
|
|
19281
|
+
proven_refund_native: drain.provenRefundNative ?? null,
|
|
19282
|
+
refund_payment_proven: drain.refundPaymentProven ?? false,
|
|
19283
|
+
held_reason: drain.status === "pending" || drain.status === "exit_required" ? drain.holdReason ?? (evidence?.chain === "found" && drain.drainedNative !== null && BigInt(evidence.refundedToPayer) < BigInt(drain.drainedNative) ? "refund_short" : drain.status === "exit_required" ? "exit_required" : "refund_unproven") : null,
|
|
18828
19284
|
...evidence ? { chain: evidence } : {}
|
|
18829
19285
|
};
|
|
18830
19286
|
}
|
|
@@ -20039,7 +20495,7 @@ initSchemaForm();
|
|
|
20039
20495
|
}
|
|
20040
20496
|
|
|
20041
20497
|
// src/sdk/server/request-fingerprint.ts
|
|
20042
|
-
import { createHash as
|
|
20498
|
+
import { createHash as createHash10 } from "crypto";
|
|
20043
20499
|
function requestFingerprint(req, opts) {
|
|
20044
20500
|
const canonical = canonicalize({
|
|
20045
20501
|
capability: req.capability,
|
|
@@ -20047,7 +20503,7 @@ function requestFingerprint(req, opts) {
|
|
|
20047
20503
|
params: req.params ?? {},
|
|
20048
20504
|
data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
|
|
20049
20505
|
});
|
|
20050
|
-
return
|
|
20506
|
+
return createHash10("sha256").update(canonical).digest("hex");
|
|
20051
20507
|
}
|
|
20052
20508
|
var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
|
|
20053
20509
|
function stripEnvelope(data) {
|
|
@@ -20170,7 +20626,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
20170
20626
|
}
|
|
20171
20627
|
}
|
|
20172
20628
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
20173
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
20629
|
+
const { RevenueReporter } = await import("./revenue-reporter-HD4GMXGD.js");
|
|
20174
20630
|
const reporter = new RevenueReporter(
|
|
20175
20631
|
opts.db,
|
|
20176
20632
|
platformUrl,
|
|
@@ -20180,7 +20636,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
20180
20636
|
revenueReporter = reporter;
|
|
20181
20637
|
await reporter.init();
|
|
20182
20638
|
reporter.startRetryLoop();
|
|
20183
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
20639
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-NXWQVKXW.js");
|
|
20184
20640
|
payoutReporter = new PayoutReporter({
|
|
20185
20641
|
dvmId,
|
|
20186
20642
|
db: opts.db,
|
|
@@ -20208,6 +20664,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
20208
20664
|
const effectiveOnRevenueSkippedNoRail = opts.onRevenueSkippedNoRail ?? (activeRevenueReporter ? (info) => activeRevenueReporter.reportRevenueSkippedNoRail(info) : void 0);
|
|
20209
20665
|
const enqueueCreditDeposit = !opts.onCreditFunded && activeRevenueReporter ? activeRevenueReporter.enqueueDeposit.bind(activeRevenueReporter) : void 0;
|
|
20210
20666
|
const enqueueCreditDrain = activeRevenueReporter ? activeRevenueReporter.enqueueCreditDrain.bind(activeRevenueReporter) : void 0;
|
|
20667
|
+
const enqueueCreditDrawRefund = activeRevenueReporter ? activeRevenueReporter.enqueueCreditDrawRefund.bind(activeRevenueReporter) : void 0;
|
|
20211
20668
|
const enqueueCreditDrawRelease = activeRevenueReporter ? activeRevenueReporter.enqueueCreditDrawRelease.bind(activeRevenueReporter) : void 0;
|
|
20212
20669
|
unsubscribeTempoSettlementReadiness = opts.tempoSettlementReadiness?.subscribe({
|
|
20213
20670
|
onBalance: (observation) => activeRevenueReporter && dvmId ? activeRevenueReporter.reportTempoSettlementBalance({
|
|
@@ -20363,6 +20820,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
20363
20820
|
} : void 0;
|
|
20364
20821
|
server = new DVMServer(descriptor, {
|
|
20365
20822
|
...opts,
|
|
20823
|
+
platformUrl,
|
|
20366
20824
|
hostingControlInstance: hostingControl,
|
|
20367
20825
|
hostedSettlementReports,
|
|
20368
20826
|
hostingClosureSupport,
|
|
@@ -20377,6 +20835,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
20377
20835
|
enqueueCreditDeposit,
|
|
20378
20836
|
enqueueCreditDrain,
|
|
20379
20837
|
enqueueCreditDrawRelease,
|
|
20838
|
+
enqueueCreditDrawRefund,
|
|
20380
20839
|
x402BatchSettlement,
|
|
20381
20840
|
x402BatchSettlementFactory,
|
|
20382
20841
|
x402ExactSettlement,
|
|
@@ -20832,8 +21291,10 @@ var DVMServer = class {
|
|
|
20832
21291
|
receiptIssuer: this.receiptIssuer,
|
|
20833
21292
|
creditTtlMs: () => this.creditTtlMs(),
|
|
20834
21293
|
creditCurrency: () => this.creditCurrency(),
|
|
21294
|
+
enqueueCreditDrawRefund: this.opts.enqueueCreditDrawRefund,
|
|
20835
21295
|
enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
|
|
20836
21296
|
enqueueCreditDrain: this.opts.enqueueCreditDrain,
|
|
21297
|
+
requireDrainReport: !!this.opts.platformUrl,
|
|
20837
21298
|
lightningReceive: this.opts.lightningReceive,
|
|
20838
21299
|
// x402 repair queue rides the same surface, and resolves
|
|
20839
21300
|
// the rail the same lazy way the verify path does — constructing the
|
|
@@ -21088,7 +21549,7 @@ var DVMServer = class {
|
|
|
21088
21549
|
const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
|
|
21089
21550
|
return {
|
|
21090
21551
|
dvm_id: dvmId,
|
|
21091
|
-
builder_pubkey:
|
|
21552
|
+
builder_pubkey: createHash11("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
|
|
21092
21553
|
};
|
|
21093
21554
|
}
|
|
21094
21555
|
throw new AuthAudienceError();
|
|
@@ -21369,6 +21830,9 @@ var DVMServer = class {
|
|
|
21369
21830
|
creditTtlMs: this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : void 0
|
|
21370
21831
|
};
|
|
21371
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
|
+
}
|
|
21372
21836
|
/**
|
|
21373
21837
|
* Assemble this DVM's funding menu for a response denominated in
|
|
21374
21838
|
* `currency`. All the refusal logic — including the credits-spec
|
|
@@ -21379,7 +21843,7 @@ var DVMServer = class {
|
|
|
21379
21843
|
async creditMenu(currency, callerPubkey) {
|
|
21380
21844
|
await this.ensureTempoSessionHealth();
|
|
21381
21845
|
const mppSplit = this.creditMppSplit();
|
|
21382
|
-
|
|
21846
|
+
const menu = await buildCreditMenu({
|
|
21383
21847
|
runtime: "container",
|
|
21384
21848
|
config: this.descriptor.credit,
|
|
21385
21849
|
currency,
|
|
@@ -21398,6 +21862,8 @@ var DVMServer = class {
|
|
|
21398
21862
|
devMode: this.opts.devMode,
|
|
21399
21863
|
callerPubkey
|
|
21400
21864
|
});
|
|
21865
|
+
if (menu && this.balancePaymentSupported()) menu.balance_payment = true;
|
|
21866
|
+
return menu;
|
|
21401
21867
|
}
|
|
21402
21868
|
async handleCredit(c) {
|
|
21403
21869
|
let hostingFundingAdmissionId;
|
|
@@ -21436,7 +21902,7 @@ var DVMServer = class {
|
|
|
21436
21902
|
const paymentAuthorization = ctx.req.header("Authorization");
|
|
21437
21903
|
const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
|
|
21438
21904
|
const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
|
|
21439
|
-
const paymentArtifactHash = attachedPayment ?
|
|
21905
|
+
const paymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
21440
21906
|
hostingFundingArtifactHash = paymentArtifactHash;
|
|
21441
21907
|
if (!await control.admit(admissionId, workId, {
|
|
21442
21908
|
creditId,
|
|
@@ -21739,7 +22205,7 @@ var DVMServer = class {
|
|
|
21739
22205
|
);
|
|
21740
22206
|
} catch (err) {
|
|
21741
22207
|
const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
21742
|
-
const { message, hint } = err instanceof
|
|
22208
|
+
const { message, hint } = err instanceof z3.ZodError ? humanizeZodError(err, { schemaUrl }) : {
|
|
21743
22209
|
message: "Quote-input validation failed",
|
|
21744
22210
|
hint: `Inspect ${schemaUrl} for the expected input schema.`
|
|
21745
22211
|
};
|
|
@@ -22145,7 +22611,7 @@ var DVMServer = class {
|
|
|
22145
22611
|
const authorization = c.req.header("Authorization");
|
|
22146
22612
|
const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
|
|
22147
22613
|
const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
|
|
22148
|
-
const hostingPaymentArtifactHash = attachedPayment ?
|
|
22614
|
+
const hostingPaymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
22149
22615
|
if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
|
|
22150
22616
|
const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
|
|
22151
22617
|
hostingPaymentArtifactHash
|
|
@@ -22443,7 +22909,7 @@ var DVMServer = class {
|
|
|
22443
22909
|
}
|
|
22444
22910
|
const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
|
|
22445
22911
|
const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
|
|
22446
|
-
const requesterTokenHash = jobToken ?
|
|
22912
|
+
const requesterTokenHash = jobToken ? createHash11("sha256").update(jobToken).digest("hex") : void 0;
|
|
22447
22913
|
const job = jm.createJob(
|
|
22448
22914
|
normalisedBody,
|
|
22449
22915
|
requesterId,
|
|
@@ -22939,6 +23405,53 @@ var DVMServer = class {
|
|
|
22939
23405
|
const id = c.req.param("id") ?? "";
|
|
22940
23406
|
const jm = this.jobManager;
|
|
22941
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
|
+
}
|
|
22942
23455
|
if (activeJob) {
|
|
22943
23456
|
if (!isJobOwner(c, activeJob)) return c.json({ error: "Job not found" }, 404);
|
|
22944
23457
|
if (isTerminal(activeJob.status)) {
|
|
@@ -23008,6 +23521,10 @@ var DVMServer = class {
|
|
|
23008
23521
|
}
|
|
23009
23522
|
const contentErr = validateMessageContent(body.type, body.content, this.opts.devMode === true);
|
|
23010
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
|
+
}
|
|
23011
23528
|
let job;
|
|
23012
23529
|
try {
|
|
23013
23530
|
({ job } = jm.reactivateJob(record));
|
|
@@ -23108,6 +23625,10 @@ var DVMServer = class {
|
|
|
23108
23625
|
*/
|
|
23109
23626
|
async runReactivation(jm, job, body, priorStatus) {
|
|
23110
23627
|
if (priorStatus === "awaiting-input") {
|
|
23628
|
+
if (jm.activeJobs.get(job.id) !== job) {
|
|
23629
|
+
this.teardownReactivationScaffold(jm, job);
|
|
23630
|
+
return;
|
|
23631
|
+
}
|
|
23111
23632
|
const claimed = await jm.claimAwaitingInputForReactivation(job.id);
|
|
23112
23633
|
if (!claimed) {
|
|
23113
23634
|
this.teardownReactivationScaffold(jm, job);
|
|
@@ -23137,9 +23658,11 @@ var DVMServer = class {
|
|
|
23137
23658
|
* must not persist our stale snapshot; clearing the appender prevents that.
|
|
23138
23659
|
*/
|
|
23139
23660
|
teardownReactivationScaffold(jm, job) {
|
|
23140
|
-
jm.
|
|
23141
|
-
|
|
23142
|
-
|
|
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
|
+
}
|
|
23143
23666
|
job.messageAppender = void 0;
|
|
23144
23667
|
}
|
|
23145
23668
|
async handleDeleteJob(c) {
|
|
@@ -23189,7 +23712,7 @@ function isJobOwner(c, job) {
|
|
|
23189
23712
|
if (job.requesterTokenHash) {
|
|
23190
23713
|
const token = c.req.header("X-Job-Token");
|
|
23191
23714
|
if (!token) return false;
|
|
23192
|
-
const provided =
|
|
23715
|
+
const provided = createHash11("sha256").update(token).digest("hex");
|
|
23193
23716
|
return timingSafeEqualHex(provided, job.requesterTokenHash);
|
|
23194
23717
|
}
|
|
23195
23718
|
return c.get("requesterId") === job.requesterId;
|