@dvmkit/sdk 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ILBSZMJL.js → chunk-4VV7WACY.js} +1 -1
- package/dist/{chunk-JIZ4ZIXX.js → chunk-73C4JN2E.js} +421 -233
- package/dist/{chunk-ALWVJ2RB.js → chunk-BAQRUYRL.js} +3 -3
- package/dist/{chunk-AVV5SYGO.js → chunk-GAZBBMAA.js} +1 -1
- package/dist/{chunk-G3MPHW5I.js → chunk-GODOH2AF.js} +3 -3
- package/dist/{chunk-2U6LAASN.js → chunk-HC4I7LAV.js} +1 -1
- package/dist/{chunk-7PM4R4GD.js → chunk-IBX2ORBV.js} +1 -1
- package/dist/{chunk-NA2KOR6G.js → chunk-IYQQXEHV.js} +3 -3
- package/dist/{chunk-J5J63WDC.js → chunk-K2MSIKMM.js} +3 -3
- package/dist/{chunk-RSNC5T6C.js → chunk-KGL3YMZA.js} +1 -1
- package/dist/{chunk-UVS7MRJX.js → chunk-LCCG5HQM.js} +2 -2
- package/dist/{chunk-QKE5NDPF.js → chunk-LFVIZH2G.js} +167 -25
- package/dist/{chunk-ZEPJF4JC.js → chunk-LW7KZQHG.js} +4 -4
- package/dist/{chunk-BFGQG4WY.js → chunk-OIP44KBB.js} +5 -0
- package/dist/{chunk-UWGBI5WK.js → chunk-Q3VYRU5R.js} +3 -3
- package/dist/{chunk-IOQIXRGM.js → chunk-QSZW2I5Y.js} +1 -1
- package/dist/{chunk-IHQUBATH.js → chunk-TFDR2YP2.js} +3 -3
- package/dist/{chunk-OP7VCGQR.js → chunk-TRY7BJA4.js} +1 -1
- package/dist/{chunk-NU5RWJ5B.js → chunk-WB3E7KE7.js} +3 -3
- package/dist/{chunk-UDJNZ3UG.js → chunk-WLPLHXNP.js} +1 -1
- package/dist/{chunk-FM6QAGJC.js → chunk-YYRY32NR.js} +24 -8
- package/dist/{chunk-AGE5XZKE.js → chunk-ZBQKXAYK.js} +17 -4
- package/dist/{content-retention-B7ddtJ2y.d.ts → content-retention-CVBYehRS.d.ts} +1 -1
- package/dist/{credit-ledger-KIQIE6I6.js → credit-ledger-CZQ56DYI.js} +4 -4
- package/dist/{credit-menu-BH3g9GQa.d.ts → credit-menu-QntEhZJS.d.ts} +4 -4
- package/dist/{fx-DN26_J7o.d.ts → fx-CMyNFenZ.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/internal/caller.d.ts +6 -6
- package/dist/internal/caller.js +16 -16
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +26 -26
- package/dist/{job-store-Cq21JT37.d.ts → job-store-qOD-FrQF.d.ts} +1 -1
- package/dist/{lightning-backend-S8F8nf8q.d.ts → lightning-backend-DtdG6UT5.d.ts} +1 -1
- package/dist/{memory-credit-ledger-ATYFAYBG.js → memory-credit-ledger-EMN7G3MO.js} +5 -5
- package/dist/{mpp-setup-ZTORW3VF.js → mpp-setup-VW53KFPO.js} +5 -3
- package/dist/{payout-reporter-NN6K2IVQ.js → payout-reporter-TZX2S7KG.js} +2 -2
- package/dist/{postgres-consumed-credential-store-XJTE5PCA.js → postgres-consumed-credential-store-V464OAJB.js} +3 -3
- package/dist/{postgres-job-store-AR6ZJUQV.js → postgres-job-store-27VOCSPO.js} +2 -2
- package/dist/{postgres-replay-store-33K33QV7.js → postgres-replay-store-7GJFAQXY.js} +2 -2
- package/dist/{pricing-G5IN5YJE.js → pricing-NNMBNP7N.js} +3 -3
- package/dist/{revenue-reporter-KDUZLD5F.js → revenue-reporter-4EAVPWXS.js} +2 -2
- package/dist/server/index.d.ts +14 -10
- package/dist/server/index.js +49 -45
- package/dist/{step-cache-CR12EZ1Q.d.ts → step-cache-B7svmPz3.d.ts} +54 -9
- package/dist/{tempo-charge-store-XEGJYM3G.js → tempo-charge-store-2RPOJBM2.js} +2 -2
- package/dist/{tempo-session-store-3WTDOO7H.js → tempo-session-store-LI3Q3A3U.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +7 -7
- package/dist/{usd-s_VZ_eS7.d.ts → usd-D3izFz71.d.ts} +1 -1
- package/dist/{x402-HQ5GRQZU.js → x402-SL36LSOD.js} +4 -4
- package/package.json +1 -1
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
installHostedTempoChargeGuard,
|
|
7
|
-
installHostedTempoCloseGuard,
|
|
8
|
-
isTempoInsufficientFundsError,
|
|
9
|
-
resolveTempoNetwork
|
|
10
|
-
} from "./chunk-QKE5NDPF.js";
|
|
2
|
+
MemoryProcessedPaymentStore,
|
|
3
|
+
PostgresProcessedPaymentStore,
|
|
4
|
+
ProcessedPaymentReplayError
|
|
5
|
+
} from "./chunk-XRLKGCAW.js";
|
|
11
6
|
import {
|
|
12
7
|
x402EarnedCeiling
|
|
13
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-K2MSIKMM.js";
|
|
14
9
|
import {
|
|
15
10
|
createX402FacilitatorAuthHeaders,
|
|
16
11
|
createX402FacilitatorClient,
|
|
@@ -19,13 +14,13 @@ import {
|
|
|
19
14
|
verifyWithFacilitator,
|
|
20
15
|
withPaymentDiagnostics,
|
|
21
16
|
withPaymentStage
|
|
22
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-GAZBBMAA.js";
|
|
23
18
|
import {
|
|
24
19
|
MemoryCreditLedger
|
|
25
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-ZBQKXAYK.js";
|
|
26
21
|
import {
|
|
27
22
|
PostgresJobStore
|
|
28
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-GODOH2AF.js";
|
|
29
24
|
import {
|
|
30
25
|
parseStoreValue
|
|
31
26
|
} from "./chunk-YG7G4DPZ.js";
|
|
@@ -36,7 +31,7 @@ import {
|
|
|
36
31
|
humanizeZodIssues,
|
|
37
32
|
signedRequestInput,
|
|
38
33
|
zodIssuesOf
|
|
39
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-4VV7WACY.js";
|
|
40
35
|
import {
|
|
41
36
|
MemoryJobStore,
|
|
42
37
|
UNCAPPABLE_ASK_TOTAL,
|
|
@@ -45,7 +40,7 @@ import {
|
|
|
45
40
|
isJobRetentionStore,
|
|
46
41
|
isReceiptIssuingStore,
|
|
47
42
|
isStreamableJobStore
|
|
48
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-TFDR2YP2.js";
|
|
49
44
|
import {
|
|
50
45
|
ContentHoldError,
|
|
51
46
|
JobCancelledError,
|
|
@@ -85,7 +80,7 @@ import {
|
|
|
85
80
|
signReceipt,
|
|
86
81
|
toLockPubkey,
|
|
87
82
|
verifyChallenge
|
|
88
|
-
} from "./chunk-
|
|
83
|
+
} from "./chunk-LW7KZQHG.js";
|
|
89
84
|
import {
|
|
90
85
|
X402_BATCH_SETTLEMENT_SCHEME,
|
|
91
86
|
X402_DEFAULT_FACILITATOR,
|
|
@@ -113,13 +108,7 @@ import {
|
|
|
113
108
|
} from "./chunk-UKD3R2KU.js";
|
|
114
109
|
import {
|
|
115
110
|
createConsoleLogger
|
|
116
|
-
} from "./chunk-
|
|
117
|
-
import {
|
|
118
|
-
loggers,
|
|
119
|
-
pinDimension,
|
|
120
|
-
withSpan,
|
|
121
|
-
withTraceContext
|
|
122
|
-
} from "./chunk-IOQIXRGM.js";
|
|
111
|
+
} from "./chunk-TRY7BJA4.js";
|
|
123
112
|
import {
|
|
124
113
|
StepCache
|
|
125
114
|
} from "./chunk-BQJIFRAZ.js";
|
|
@@ -136,7 +125,7 @@ import {
|
|
|
136
125
|
roundUsd,
|
|
137
126
|
satsToFiat,
|
|
138
127
|
validateCurrency
|
|
139
|
-
} from "./chunk-
|
|
128
|
+
} from "./chunk-HC4I7LAV.js";
|
|
140
129
|
import {
|
|
141
130
|
CreditLedger,
|
|
142
131
|
CreditLedgerError,
|
|
@@ -152,26 +141,38 @@ import {
|
|
|
152
141
|
isNonChannelBitcoinRail,
|
|
153
142
|
netOwedSats,
|
|
154
143
|
x402SettlementPending
|
|
155
|
-
} from "./chunk-
|
|
144
|
+
} from "./chunk-YYRY32NR.js";
|
|
145
|
+
import {
|
|
146
|
+
withSdkInitLock
|
|
147
|
+
} from "./chunk-COYKJRMM.js";
|
|
156
148
|
import {
|
|
157
149
|
withPostgresRecoveryContext
|
|
158
|
-
} from "./chunk-
|
|
150
|
+
} from "./chunk-IBX2ORBV.js";
|
|
159
151
|
import {
|
|
160
152
|
DvmError,
|
|
161
153
|
x402NetworkByCaip2
|
|
162
154
|
} from "./chunk-BPYMEMQX.js";
|
|
163
155
|
import {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
TEMPO_MODERATO_CHAIN_ID,
|
|
157
|
+
TEMPO_MODERATO_RPC,
|
|
158
|
+
TempoCloseDescriptorError,
|
|
159
|
+
advertisedMethods,
|
|
160
|
+
challengeMeta,
|
|
161
|
+
installHostedTempoChargeGuard,
|
|
162
|
+
installHostedTempoCloseGuard,
|
|
163
|
+
isTempoInsufficientFundsError,
|
|
164
|
+
resolveTempoNetwork
|
|
165
|
+
} from "./chunk-LFVIZH2G.js";
|
|
167
166
|
import {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
loggers,
|
|
168
|
+
pinDimension,
|
|
169
|
+
withSpan,
|
|
170
|
+
withTraceContext
|
|
171
|
+
} from "./chunk-QSZW2I5Y.js";
|
|
172
172
|
import {
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
createDiagnosticCapabilityAuthority,
|
|
174
|
+
diagnosticConsole
|
|
175
|
+
} from "./chunk-OIP44KBB.js";
|
|
175
176
|
|
|
176
177
|
// src/sdk/server/lock-pubkey-store.ts
|
|
177
178
|
async function initLockPubkeyTable(db) {
|
|
@@ -3883,7 +3884,7 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
3883
3884
|
x402Requirements
|
|
3884
3885
|
});
|
|
3885
3886
|
}
|
|
3886
|
-
const { verifyX402Payment } = await import("./x402-
|
|
3887
|
+
const { verifyX402Payment } = await import("./x402-SL36LSOD.js");
|
|
3887
3888
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
3888
3889
|
x402Payment,
|
|
3889
3890
|
x402Config,
|
|
@@ -4896,7 +4897,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
|
|
|
4896
4897
|
snapshot
|
|
4897
4898
|
});
|
|
4898
4899
|
}
|
|
4899
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4900
|
+
const { verifyX402Payment } = await import("./x402-SL36LSOD.js");
|
|
4900
4901
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
4901
4902
|
body.content.x402_payment,
|
|
4902
4903
|
opts.x402Config,
|
|
@@ -5327,7 +5328,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
|
|
|
5327
5328
|
try {
|
|
5328
5329
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5329
5330
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5330
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5331
|
+
const { verifyX402Payment } = await import("./x402-SL36LSOD.js");
|
|
5331
5332
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
5332
5333
|
body.content.x402_payment,
|
|
5333
5334
|
opts.x402Config,
|
|
@@ -6953,6 +6954,10 @@ async function completeTempoChannelDrain(args) {
|
|
|
6953
6954
|
tx_hash: args.reference,
|
|
6954
6955
|
channel_id: args.channelId,
|
|
6955
6956
|
...settlementEvidence,
|
|
6957
|
+
...args.finalization && {
|
|
6958
|
+
terminal_outcome: args.finalization.outcome,
|
|
6959
|
+
credit_unbacked: args.finalization.creditUnbacked
|
|
6960
|
+
},
|
|
6956
6961
|
...args.repaired ? { repaired: true, repair_kind: "tempo_close_reconciled" } : {}
|
|
6957
6962
|
},
|
|
6958
6963
|
tx: innerTx
|
|
@@ -7231,14 +7236,14 @@ async function handleDrain(c, deps, args) {
|
|
|
7231
7236
|
404
|
|
7232
7237
|
);
|
|
7233
7238
|
}
|
|
7234
|
-
|
|
7239
|
+
let existing = await deps.ledger.getDrain({ creditId, drainId: drain.drain_id });
|
|
7240
|
+
if (credit.status === "unbacked" && existing?.status !== "sent") {
|
|
7235
7241
|
const rejected2 = await burnNonce(c, deps, args.envelope);
|
|
7236
7242
|
if (rejected2) return rejected2;
|
|
7237
7243
|
return jsonPaymentError("credit_unbacked", 409, {
|
|
7238
7244
|
message: `Credit ${creditId} is terminal because its Tempo backing finalized before the DVM was protected.`
|
|
7239
7245
|
});
|
|
7240
7246
|
}
|
|
7241
|
-
let existing = await deps.ledger.getDrain({ creditId, drainId: drain.drain_id });
|
|
7242
7247
|
if (existing) {
|
|
7243
7248
|
const tempoDiscovery = existing.method === "tempo" && existing.status === "pending" && drain.method === "tempo" && !!credit.tempoChannelId && !c.req.header("Authorization");
|
|
7244
7249
|
if (!tempoDiscovery) {
|
|
@@ -7254,6 +7259,17 @@ async function handleDrain(c, deps, args) {
|
|
|
7254
7259
|
const released = await releaseTempoDrain(c, deps, existing, credit.tempoChannelId);
|
|
7255
7260
|
return respondDrain(c, deps, { record: released, credit });
|
|
7256
7261
|
}
|
|
7262
|
+
if (existing.status === "exit_required") {
|
|
7263
|
+
const mpp = (await deps.railOpts(c)).mpp;
|
|
7264
|
+
if (mpp && credit.tempoChannelId) {
|
|
7265
|
+
const recovered = await recoverTempoDrain(deps, mpp, existing, credit.tempoChannelId);
|
|
7266
|
+
if (recovered) {
|
|
7267
|
+
if (isHttpResponse(recovered)) return recovered;
|
|
7268
|
+
return respondDrain(c, deps, { record: recovered, credit });
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
return tempoExitRequiredResponse(existing);
|
|
7272
|
+
}
|
|
7257
7273
|
if (existing.method === "tempo" && existing.status === "pending" && credit.tempoChannelId) {
|
|
7258
7274
|
if (credit.balanceMicro !== credit.availableMicro) {
|
|
7259
7275
|
const released = await releaseTempoDrain(c, deps, existing, credit.tempoChannelId);
|
|
@@ -7491,7 +7507,8 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7491
7507
|
deps.hostedSettlementReports,
|
|
7492
7508
|
rail.mpp,
|
|
7493
7509
|
record,
|
|
7494
|
-
expectedChannelId
|
|
7510
|
+
expectedChannelId,
|
|
7511
|
+
deps.ledger
|
|
7495
7512
|
);
|
|
7496
7513
|
if (!rail.mpp?.permitHostedTempoClose) {
|
|
7497
7514
|
throw new Error("Hosted Tempo close credential guard is unavailable");
|
|
@@ -7521,13 +7538,21 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7521
7538
|
}
|
|
7522
7539
|
const wrongChannel = tempoWrongChannelRefusal(error, record);
|
|
7523
7540
|
if (wrongChannel) return wrongChannel;
|
|
7524
|
-
if (error instanceof HostedTempoCloseAttemptsExhaustedError) {
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7541
|
+
if (error instanceof HostedTempoCloseAttemptsExhaustedError || error instanceof TempoCloseDescriptorError) {
|
|
7542
|
+
const result = deps.ledger.requireTempoDrainExit ? await deps.ledger.requireTempoDrainExit({ creditId: record.creditId, drainId: record.drainId }) : { drain: record, replayed: false };
|
|
7543
|
+
if (!result.replayed && error instanceof TempoCloseDescriptorError) {
|
|
7544
|
+
loggers.tempo.warn("tempo_session_close_descriptor_refused", {
|
|
7545
|
+
credit_id: record.creditId,
|
|
7546
|
+
drain_id: record.drainId,
|
|
7547
|
+
channel_id: expectedChannelId,
|
|
7548
|
+
amount_at_stake_native: record.drainedNative?.toString() ?? null,
|
|
7549
|
+
amount_at_stake_micro: record.amountMicro,
|
|
7550
|
+
currency: record.currency,
|
|
7551
|
+
error: error.message
|
|
7552
|
+
});
|
|
7553
|
+
}
|
|
7554
|
+
if (result.drain.status === "sent") return respondDrain(c, deps, { record: result.drain, credit });
|
|
7555
|
+
return tempoExitRequiredResponse(result.drain);
|
|
7531
7556
|
}
|
|
7532
7557
|
const refused = tempoFloorRefusal(error, record);
|
|
7533
7558
|
if (refused) return refused;
|
|
@@ -7580,13 +7605,15 @@ var HOSTED_TEMPO_CLOSE_MAX_RELEASES = 3;
|
|
|
7580
7605
|
var HOSTED_TEMPO_CLOSE_IN_FLIGHT = "Hosted Tempo close already has an original effect attempt";
|
|
7581
7606
|
var HostedTempoCloseAttemptsExhaustedError = class extends Error {
|
|
7582
7607
|
};
|
|
7583
|
-
async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
7608
|
+
async function reserveHostedTempoClose(reports, mpp, record, channelId, ledger) {
|
|
7584
7609
|
const pending = await reports.pendingTempoClose(channelId, record.drainId);
|
|
7585
7610
|
if (!pending) {
|
|
7586
7611
|
const reserved = await reports.reserveTempo(channelId, "close", record.drainId);
|
|
7587
7612
|
if (reserved.replayed) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7588
7613
|
return reserved.id;
|
|
7589
7614
|
}
|
|
7615
|
+
if (pending.state === "exhausted") throw new HostedTempoCloseAttemptsExhaustedError();
|
|
7616
|
+
if (pending.state === "unmatched") throw new Error("Hosted Tempo close report requires an operator retry");
|
|
7590
7617
|
if (pending.txHash || !mpp.getTempoSessionDrainState) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7591
7618
|
const state = await mpp.getTempoSessionDrainState(channelId);
|
|
7592
7619
|
if (state.finalized || state.deposit === 0n || state.closeRequestedAt !== 0n) {
|
|
@@ -7594,7 +7621,10 @@ async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
|
7594
7621
|
}
|
|
7595
7622
|
const result = await reports.releaseStaleTempoClose(pending.id, channelId, record.drainId, {
|
|
7596
7623
|
staleAfterMs: HOSTED_TEMPO_CLOSE_STALE_MS,
|
|
7597
|
-
maxReleases: HOSTED_TEMPO_CLOSE_MAX_RELEASES
|
|
7624
|
+
maxReleases: HOSTED_TEMPO_CLOSE_MAX_RELEASES,
|
|
7625
|
+
onExhausted: async (tx) => {
|
|
7626
|
+
await ledger.requireTempoDrainExit?.({ creditId: record.creditId, drainId: record.drainId, tx });
|
|
7627
|
+
}
|
|
7598
7628
|
});
|
|
7599
7629
|
if (result.outcome === "in_flight") throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7600
7630
|
if (result.outcome === "exhausted") {
|
|
@@ -7603,7 +7633,8 @@ async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
|
7603
7633
|
drain_id: record.drainId,
|
|
7604
7634
|
channel_id: channelId,
|
|
7605
7635
|
obligation_id: pending.id,
|
|
7606
|
-
attempt: result.releases + 1
|
|
7636
|
+
attempt: result.releases + 1,
|
|
7637
|
+
amount_at_stake_native: await reports.tempoCloseAmountAtStake(pending.id)
|
|
7607
7638
|
});
|
|
7608
7639
|
throw new HostedTempoCloseAttemptsExhaustedError(
|
|
7609
7640
|
`Tempo drain ${record.drainId} exhausted its cooperative close attempts`
|
|
@@ -7618,6 +7649,20 @@ async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
|
7618
7649
|
});
|
|
7619
7650
|
return result.id;
|
|
7620
7651
|
}
|
|
7652
|
+
function tempoExitRequiredResponse(record) {
|
|
7653
|
+
const terminal = record.status === "exit_required";
|
|
7654
|
+
return jsonPaymentError("tempo_channel_exit_required", 409, {
|
|
7655
|
+
message: terminal ? `Tempo drain ${record.drainId} cannot cooperatively close this channel.` : `This Tempo close credential cannot complete drain ${record.drainId}.`,
|
|
7656
|
+
display: "Your funds remain in the channel. Use the timed on-chain exit to recover them.",
|
|
7657
|
+
hint: terminal ? "Start the channel's timed on-chain exit. A corrected close credential does not reopen this drain or debit it again." : "This drain remains pending. Use the channel's timed on-chain exit, or retry the same drain with a valid close credential if cooperative attempts remain. Retrying does not debit it again.",
|
|
7658
|
+
extra: {
|
|
7659
|
+
drain_registered: true,
|
|
7660
|
+
drain_id: record.drainId,
|
|
7661
|
+
channel_id: record.channelId,
|
|
7662
|
+
status: record.status
|
|
7663
|
+
}
|
|
7664
|
+
});
|
|
7665
|
+
}
|
|
7621
7666
|
function tempoCloseCredentialNeedsRefresh(error) {
|
|
7622
7667
|
return error instanceof MppxErrors.InvalidChallengeError || error instanceof MppxErrors.PaymentExpiredError;
|
|
7623
7668
|
}
|
|
@@ -8109,6 +8154,11 @@ function drainCopy(record) {
|
|
|
8109
8154
|
display: "The refund was sent to the payout destination you provided.",
|
|
8110
8155
|
hint: "See `sent` for the settlement reference (e.g. the on-chain transaction hash)."
|
|
8111
8156
|
};
|
|
8157
|
+
case "exit_required":
|
|
8158
|
+
return {
|
|
8159
|
+
display: "This drain cannot cooperatively close the channel. Your funds remain in the channel.",
|
|
8160
|
+
hint: "Use the channel's timed on-chain exit. A corrected credential does not reopen this drain."
|
|
8161
|
+
};
|
|
8112
8162
|
case "released":
|
|
8113
8163
|
return {
|
|
8114
8164
|
display: "The channel could not complete this refund, so the service restored the amount to your prepaid balance.",
|
|
@@ -9586,172 +9636,6 @@ function safeError(err) {
|
|
|
9586
9636
|
return "settlement_transport_error";
|
|
9587
9637
|
}
|
|
9588
9638
|
|
|
9589
|
-
// src/sdk/server/hosting-control.ts
|
|
9590
|
-
import { createHmac, randomUUID as randomUUID4, timingSafeEqual } from "crypto";
|
|
9591
|
-
|
|
9592
|
-
// src/sdk/server/hosting-cleanup-fence.ts
|
|
9593
|
-
var HostingCleanupFence = class {
|
|
9594
|
-
constructor(pool, dvmId, scope) {
|
|
9595
|
-
this.pool = pool;
|
|
9596
|
-
this.dvmId = dvmId;
|
|
9597
|
-
this.scope = scope;
|
|
9598
|
-
}
|
|
9599
|
-
pool;
|
|
9600
|
-
dvmId;
|
|
9601
|
-
scope;
|
|
9602
|
-
async assertNewIntent(tx, intent) {
|
|
9603
|
-
const continuation = intent?.admissionId || intent?.paymentSeq !== void 0 || intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash || intent?.creditId && intent.fundId && intent.callerPubkey;
|
|
9604
|
-
const { rows: rows2 } = await tx.query(
|
|
9605
|
-
`SELECT state, closed_at_ms, closed_at_us FROM sdk_hosting_control
|
|
9606
|
-
WHERE dvm_id=$1 AND scope=$2 ${continuation ? "" : "FOR UPDATE"}`,
|
|
9607
|
-
[this.dvmId, this.scope]
|
|
9608
|
-
);
|
|
9609
|
-
const control = rows2[0];
|
|
9610
|
-
if (!control) throw new Error("Hosted obligation fence is unavailable");
|
|
9611
|
-
if (control.state === "closed" && (control.closed_at_ms === null || control.closed_at_us === null)) {
|
|
9612
|
-
throw new Error("Hosted closure cutoff is unavailable");
|
|
9613
|
-
}
|
|
9614
|
-
if (control.state !== "closed" && !continuation) return;
|
|
9615
|
-
if (intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash) {
|
|
9616
|
-
const repair = !!intent.invoiceRepairPaymentHash;
|
|
9617
|
-
const invoice = await tx.query(
|
|
9618
|
-
`SELECT i.payment_hash FROM credit_invoices i
|
|
9619
|
-
LEFT JOIN sdk_hosting_admissions a ON a.id=i.hosting_admission_id
|
|
9620
|
-
WHERE i.payment_hash=$1 AND i.caller_pubkey=$4 AND i.amount_micro=$5
|
|
9621
|
-
AND (($10::boolean AND $3=i.payment_hash AND i.status IN ('blocked','written_off'))
|
|
9622
|
-
OR (NOT $10::boolean AND i.credit_id=$2 AND i.fund_id=$3 AND i.status='pending'))
|
|
9623
|
-
AND ((i.hosting_admission_id IS NULL AND
|
|
9624
|
-
($9::bigint IS NULL OR i.created_at_db_us <= $9)) OR
|
|
9625
|
-
(a.dvm_id=$6 AND a.scope=$7 AND a.credit_id=i.credit_id
|
|
9626
|
-
AND a.fund_id=i.fund_id AND a.caller_pubkey=i.caller_pubkey
|
|
9627
|
-
AND a.outcome IN ('pending','accepted','unresolved')
|
|
9628
|
-
AND ($8::bigint IS NULL OR a.admitted_at_ms <= $8)))`,
|
|
9629
|
-
[
|
|
9630
|
-
intent.invoicePaymentHash ?? intent.invoiceRepairPaymentHash,
|
|
9631
|
-
intent.creditId,
|
|
9632
|
-
intent.fundId,
|
|
9633
|
-
intent.callerPubkey,
|
|
9634
|
-
intent.amountMicro,
|
|
9635
|
-
this.dvmId,
|
|
9636
|
-
this.scope,
|
|
9637
|
-
control.closed_at_ms,
|
|
9638
|
-
control.closed_at_us,
|
|
9639
|
-
repair
|
|
9640
|
-
]
|
|
9641
|
-
);
|
|
9642
|
-
if (invoice.rows[0]) return;
|
|
9643
|
-
throw new Error("Hosted invoice has no original payable admission");
|
|
9644
|
-
}
|
|
9645
|
-
if (intent?.paymentSeq !== void 0 && intent.jobId) {
|
|
9646
|
-
const payment = await tx.query(
|
|
9647
|
-
`SELECT m.job_id FROM job_messages m JOIN jobs j ON j.id=m.job_id
|
|
9648
|
-
WHERE m.job_id=$1 AND m.seq=$2 AND m.status='pending-verification'
|
|
9649
|
-
AND m.created_at_ms IS NOT NULL
|
|
9650
|
-
AND ($3::bigint IS NULL OR m.created_at_ms <= $3) AND j.dvm_id=$4
|
|
9651
|
-
AND ($6::text IS NULL OR m.data->'content'->'tempo_credential'->'challenge'->>'id'=$6)
|
|
9652
|
-
AND EXISTS (SELECT 1 FROM sdk_hosting_admissions a
|
|
9653
|
-
WHERE a.dvm_id=$4 AND a.scope=$5 AND a.job_id=m.job_id AND a.outcome <> 'failed')`,
|
|
9654
|
-
[
|
|
9655
|
-
intent.jobId,
|
|
9656
|
-
intent.paymentSeq,
|
|
9657
|
-
control.closed_at_ms,
|
|
9658
|
-
this.dvmId,
|
|
9659
|
-
this.scope,
|
|
9660
|
-
intent.challengeId ?? null
|
|
9661
|
-
]
|
|
9662
|
-
);
|
|
9663
|
-
if (payment.rows[0]) return;
|
|
9664
|
-
throw new Error("Hosted payment has no pre-closure inbound intent");
|
|
9665
|
-
}
|
|
9666
|
-
if (!intent?.admissionId && !intent?.jobId && !(intent?.creditId && intent.fundId && intent.callerPubkey) || !intent?.jobId && !(intent?.creditId && intent.fundId)) {
|
|
9667
|
-
throw new Error("Hosted obligations are closed to new intents");
|
|
9668
|
-
}
|
|
9669
|
-
const accepted = await tx.query(
|
|
9670
|
-
`SELECT id FROM sdk_hosting_admissions
|
|
9671
|
-
WHERE ($1::text IS NULL OR id=$1) AND dvm_id=$2 AND scope=$3
|
|
9672
|
-
AND ($4::bigint IS NULL OR admitted_at_ms <= $4)
|
|
9673
|
-
AND (outcome IN ('pending', 'unresolved') OR ($11::boolean AND outcome='accepted'))
|
|
9674
|
-
AND ($5::text IS NULL OR job_id=$5)
|
|
9675
|
-
AND ($6::text IS NULL OR credit_id=$6)
|
|
9676
|
-
AND ($7::text IS NULL OR fund_id=$7)
|
|
9677
|
-
AND ($8::text IS NULL OR caller_pubkey=$8)
|
|
9678
|
-
AND ($9::text IS NULL OR funding_commitment=$9)
|
|
9679
|
-
AND ($10::text IS NULL OR payment_artifact_hash=$10)`,
|
|
9680
|
-
[
|
|
9681
|
-
intent.admissionId ?? null,
|
|
9682
|
-
this.dvmId,
|
|
9683
|
-
this.scope,
|
|
9684
|
-
control.closed_at_ms,
|
|
9685
|
-
intent.jobId ?? null,
|
|
9686
|
-
intent.creditId ?? null,
|
|
9687
|
-
intent.fundId ?? null,
|
|
9688
|
-
intent.callerPubkey ?? null,
|
|
9689
|
-
intent.fundingCommitment ?? null,
|
|
9690
|
-
intent.paymentArtifactHash ?? null,
|
|
9691
|
-
control.state !== "closed"
|
|
9692
|
-
]
|
|
9693
|
-
);
|
|
9694
|
-
if (!accepted.rows[0]) throw new Error("Hosted obligation has no pre-closure admission");
|
|
9695
|
-
}
|
|
9696
|
-
async withNewIntent(intent, operation) {
|
|
9697
|
-
return this.withNewIntentOn(this.pool, intent, operation);
|
|
9698
|
-
}
|
|
9699
|
-
async assertFundingReservation(tx, reservation) {
|
|
9700
|
-
await this.assertNewIntent(tx, {
|
|
9701
|
-
admissionId: reservation.admissionId,
|
|
9702
|
-
creditId: reservation.creditId,
|
|
9703
|
-
fundId: reservation.fundId,
|
|
9704
|
-
callerPubkey: reservation.callerPubkey,
|
|
9705
|
-
fundingCommitment: reservation.commitment,
|
|
9706
|
-
paymentArtifactHash: reservation.paymentArtifactHash
|
|
9707
|
-
});
|
|
9708
|
-
const verified = await tx.query(
|
|
9709
|
-
`SELECT id FROM sdk_hosting_admissions
|
|
9710
|
-
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND credit_id=$4 AND fund_id=$5
|
|
9711
|
-
AND caller_pubkey=$6 AND funding_commitment=$7 AND payment_artifact_hash=$8
|
|
9712
|
-
AND outcome IN ('pending','accepted','unresolved')`,
|
|
9713
|
-
[
|
|
9714
|
-
reservation.admissionId,
|
|
9715
|
-
this.dvmId,
|
|
9716
|
-
this.scope,
|
|
9717
|
-
reservation.creditId,
|
|
9718
|
-
reservation.fundId,
|
|
9719
|
-
reservation.callerPubkey,
|
|
9720
|
-
reservation.commitment,
|
|
9721
|
-
reservation.paymentArtifactHash
|
|
9722
|
-
]
|
|
9723
|
-
);
|
|
9724
|
-
if (!verified.rows[0]) throw new Error("Hosted funding has no authentic admission");
|
|
9725
|
-
}
|
|
9726
|
-
async fundingAdmission(tx, creditId, fundId, callerPubkey) {
|
|
9727
|
-
await this.assertNewIntent(tx, { creditId, fundId, callerPubkey });
|
|
9728
|
-
const { rows: rows2 } = await tx.query(
|
|
9729
|
-
`SELECT id FROM sdk_hosting_admissions
|
|
9730
|
-
WHERE dvm_id=$1 AND scope=$2 AND credit_id=$3 AND fund_id=$4
|
|
9731
|
-
AND caller_pubkey=$5 AND outcome IN ('pending','unresolved')
|
|
9732
|
-
ORDER BY admitted_at_ms,id LIMIT 2`,
|
|
9733
|
-
[this.dvmId, this.scope, creditId, fundId, callerPubkey]
|
|
9734
|
-
);
|
|
9735
|
-
if (rows2.length !== 1) throw new Error("Hosted invoice funding admission is missing or ambiguous");
|
|
9736
|
-
return rows2[0].id;
|
|
9737
|
-
}
|
|
9738
|
-
async withNewIntentOn(db, intent, operation) {
|
|
9739
|
-
const tx = await db.connect();
|
|
9740
|
-
try {
|
|
9741
|
-
await tx.query("BEGIN");
|
|
9742
|
-
await this.assertNewIntent(tx, intent);
|
|
9743
|
-
const result = await operation(tx);
|
|
9744
|
-
await tx.query("COMMIT");
|
|
9745
|
-
return result;
|
|
9746
|
-
} catch (error) {
|
|
9747
|
-
await tx.query("ROLLBACK").catch(() => void 0);
|
|
9748
|
-
throw error;
|
|
9749
|
-
} finally {
|
|
9750
|
-
tx.release();
|
|
9751
|
-
}
|
|
9752
|
-
}
|
|
9753
|
-
};
|
|
9754
|
-
|
|
9755
9639
|
// src/sdk/server/hosting-settlement-report-obligations.ts
|
|
9756
9640
|
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
9757
9641
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
@@ -9975,14 +9859,13 @@ var HostedSettlementReportObligations = class {
|
|
|
9975
9859
|
operation TEXT NOT NULL CHECK (operation IN ('claim','refund','settle','close')),
|
|
9976
9860
|
network TEXT NOT NULL, contract TEXT NOT NULL, channel_id TEXT NOT NULL,
|
|
9977
9861
|
original JSONB NOT NULL,
|
|
9978
|
-
state TEXT NOT NULL CHECK (state IN ('reserved','observed','report_queued','delivered','no_report')),
|
|
9862
|
+
state TEXT NOT NULL CHECK (state IN ('reserved','observed','report_queued','delivered','no_report','exhausted','unmatched')),
|
|
9863
|
+
close_search_before BIGINT, close_write_off_note TEXT,
|
|
9979
9864
|
tx_hash TEXT, block_number BIGINT, block_hash TEXT, delta_native NUMERIC(78,0),
|
|
9980
9865
|
payout_id TEXT, report_id TEXT, report_payload JSONB, delivered_at_ms BIGINT,
|
|
9981
9866
|
created_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
|
|
9982
9867
|
updated_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
9983
9868
|
);
|
|
9984
|
-
ALTER TABLE sdk_hosted_settlement_report_obligations
|
|
9985
|
-
ADD COLUMN IF NOT EXISTS close_search_before BIGINT;
|
|
9986
9869
|
CREATE INDEX IF NOT EXISTS sdk_hosted_settlement_report_pending
|
|
9987
9870
|
ON sdk_hosted_settlement_report_obligations (dvm_id,scope,state)
|
|
9988
9871
|
WHERE state NOT IN ('delivered','no_report');
|
|
@@ -9990,6 +9873,29 @@ var HostedSettlementReportObligations = class {
|
|
|
9990
9873
|
ON sdk_hosted_settlement_report_obligations (report_id)
|
|
9991
9874
|
WHERE report_id IS NOT NULL;
|
|
9992
9875
|
`);
|
|
9876
|
+
const columns = await this.pool.query(
|
|
9877
|
+
`SELECT attname FROM pg_attribute
|
|
9878
|
+
WHERE attrelid='sdk_hosted_settlement_report_obligations'::regclass AND NOT attisdropped`
|
|
9879
|
+
);
|
|
9880
|
+
for (const [name, type] of [["close_search_before", "BIGINT"], ["close_write_off_note", "TEXT"]]) {
|
|
9881
|
+
if (!columns.rows.some((column) => column.attname === name)) {
|
|
9882
|
+
await this.pool.query(`ALTER TABLE sdk_hosted_settlement_report_obligations ADD COLUMN ${name} ${type}`);
|
|
9883
|
+
}
|
|
9884
|
+
}
|
|
9885
|
+
const constraint = await this.pool.query(
|
|
9886
|
+
`SELECT pg_get_constraintdef(oid) AS definition FROM pg_constraint
|
|
9887
|
+
WHERE conrelid='sdk_hosted_settlement_report_obligations'::regclass
|
|
9888
|
+
AND conname='sdk_hosted_settlement_report_obligations_state_check'`
|
|
9889
|
+
);
|
|
9890
|
+
const states = ["reserved", "observed", "report_queued", "delivered", "no_report", "exhausted", "unmatched"];
|
|
9891
|
+
const expected = `CHECK ((state = ANY (ARRAY[${states.map((state) => `'${state}'::text`).join(", ")}])))`;
|
|
9892
|
+
if (constraint.rows[0]?.definition !== expected) {
|
|
9893
|
+
await this.pool.query(`ALTER TABLE sdk_hosted_settlement_report_obligations
|
|
9894
|
+
DROP CONSTRAINT IF EXISTS sdk_hosted_settlement_report_obligations_state_check;
|
|
9895
|
+
ALTER TABLE sdk_hosted_settlement_report_obligations
|
|
9896
|
+
ADD CONSTRAINT sdk_hosted_settlement_report_obligations_state_check
|
|
9897
|
+
CHECK (state IN ('reserved','observed','report_queued','delivered','no_report','exhausted','unmatched'))`);
|
|
9898
|
+
}
|
|
9993
9899
|
await this.pool.query(
|
|
9994
9900
|
`INSERT INTO sdk_hosted_settlement_report_history (singleton,complete)
|
|
9995
9901
|
VALUES (TRUE,$1) ON CONFLICT (singleton) DO NOTHING`,
|
|
@@ -10093,9 +9999,9 @@ var HostedSettlementReportObligations = class {
|
|
|
10093
9999
|
}
|
|
10094
10000
|
/**
|
|
10095
10001
|
* Retire a close reservation that never recorded a transaction and reserve
|
|
10096
|
-
* the drain's next attempt
|
|
10097
|
-
*
|
|
10098
|
-
*
|
|
10002
|
+
* the drain's next attempt in one transaction. At the retry cap the final
|
|
10003
|
+
* row becomes terminal `exhausted` and remains claimable by a late landing.
|
|
10004
|
+
* A close from the retired attempt that lands late is the same channel's close: `recordSubmitted` on the retired row
|
|
10099
10005
|
* writes it onto the successor, and recovery attaches it there if that
|
|
10100
10006
|
* attempt never got to write, so it is reported once.
|
|
10101
10007
|
*
|
|
@@ -10122,7 +10028,15 @@ var HostedSettlementReportObligations = class {
|
|
|
10122
10028
|
AND original->>'drainId'=$4 AND state='no_report' AND tx_hash IS NULL`,
|
|
10123
10029
|
[this.dvmId, this.scope, channelId.toLowerCase(), drainId]
|
|
10124
10030
|
)).rows[0].n);
|
|
10125
|
-
if (released >= limits.maxReleases)
|
|
10031
|
+
if (released >= limits.maxReleases) {
|
|
10032
|
+
await tx.query(
|
|
10033
|
+
`UPDATE sdk_hosted_settlement_report_obligations SET state='exhausted',
|
|
10034
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
10035
|
+
[id]
|
|
10036
|
+
);
|
|
10037
|
+
await limits.onExhausted?.(tx);
|
|
10038
|
+
return { outcome: "exhausted", releases: released };
|
|
10039
|
+
}
|
|
10126
10040
|
await tx.query(
|
|
10127
10041
|
`UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
|
|
10128
10042
|
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
@@ -10141,11 +10055,63 @@ var HostedSettlementReportObligations = class {
|
|
|
10141
10055
|
`UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
|
|
10142
10056
|
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10143
10057
|
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
|
|
10144
|
-
AND state
|
|
10058
|
+
AND state IN ('reserved','exhausted') AND tx_hash IS NULL`,
|
|
10059
|
+
[id, this.dvmId, this.scope]
|
|
10060
|
+
);
|
|
10061
|
+
return result.rowCount === 1;
|
|
10062
|
+
}
|
|
10063
|
+
async tempoCloseAmountAtStake(id) {
|
|
10064
|
+
const row = await this.lockRow(this.pool, id);
|
|
10065
|
+
if (row.rail !== "tempo" || row.operation !== "close") throw new Error("Not a Tempo close obligation");
|
|
10066
|
+
return (BigInt(String(row.original.highestVoucherAmount)) - BigInt(String(row.original.settledOnChain))).toString();
|
|
10067
|
+
}
|
|
10068
|
+
/** Stop automatic searches without declaring a possibly missing payout discharged. */
|
|
10069
|
+
async markTempoCloseUnmatched(id) {
|
|
10070
|
+
const result = await this.pool.query(
|
|
10071
|
+
`UPDATE sdk_hosted_settlement_report_obligations SET state='unmatched',
|
|
10072
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10073
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
|
|
10074
|
+
AND state IN ('reserved','exhausted') AND tx_hash IS NULL`,
|
|
10145
10075
|
[id, this.dvmId, this.scope]
|
|
10146
10076
|
);
|
|
10147
10077
|
return result.rowCount === 1;
|
|
10148
10078
|
}
|
|
10079
|
+
async writeOffTempoClose(id, note) {
|
|
10080
|
+
const result = await this.pool.query(
|
|
10081
|
+
`UPDATE sdk_hosted_settlement_report_obligations SET state='no_report', close_write_off_note=$4,
|
|
10082
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10083
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
|
|
10084
|
+
AND state='unmatched' AND tx_hash IS NULL RETURNING channel_id,original`,
|
|
10085
|
+
[id, this.dvmId, this.scope, note]
|
|
10086
|
+
);
|
|
10087
|
+
const row = result.rows[0];
|
|
10088
|
+
return row && { channelId: row.channel_id, amount: (BigInt(String(row.original.highestVoucherAmount)) - BigInt(String(row.original.settledOnChain))).toString() };
|
|
10089
|
+
}
|
|
10090
|
+
async reopenTempoClose(id) {
|
|
10091
|
+
return transaction(this.pool, async (tx) => {
|
|
10092
|
+
const owned = await tx.query(
|
|
10093
|
+
"SELECT id FROM sdk_hosted_settlement_report_obligations WHERE id=$1 AND dvm_id=$2 AND scope=$3",
|
|
10094
|
+
[id, this.dvmId, this.scope]
|
|
10095
|
+
);
|
|
10096
|
+
if (!owned.rowCount) return false;
|
|
10097
|
+
await this.lockIdentity(tx);
|
|
10098
|
+
const result = await tx.query(
|
|
10099
|
+
`UPDATE sdk_hosted_settlement_report_obligations target
|
|
10100
|
+
SET state='reserved', close_search_before=NULL,
|
|
10101
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10102
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
|
|
10103
|
+
AND state IN ('unmatched','no_report') AND tx_hash IS NULL
|
|
10104
|
+
AND NOT EXISTS (
|
|
10105
|
+
SELECT 1 FROM sdk_hosted_settlement_report_obligations other
|
|
10106
|
+
WHERE other.dvm_id=target.dvm_id AND other.scope=target.scope
|
|
10107
|
+
AND other.rail='tempo' AND other.operation='close' AND other.channel_id=target.channel_id
|
|
10108
|
+
AND other.id<>target.id AND (other.state<>'no_report' OR other.tx_hash IS NOT NULL)
|
|
10109
|
+
)`,
|
|
10110
|
+
[id, this.dvmId, this.scope]
|
|
10111
|
+
);
|
|
10112
|
+
return result.rowCount === 1;
|
|
10113
|
+
});
|
|
10114
|
+
}
|
|
10149
10115
|
async reserveTempoIn(tx, channelId, operation, drainId) {
|
|
10150
10116
|
const config = this.tempo;
|
|
10151
10117
|
if (!config || !hex(channelId, 32)) throw new Error("Hosted Tempo settlement configuration is unavailable");
|
|
@@ -10204,7 +10170,7 @@ var HostedSettlementReportObligations = class {
|
|
|
10204
10170
|
/** The original close obligation reserved for this drain, while its report is outstanding. */
|
|
10205
10171
|
async pendingTempoClose(channelId, drainId) {
|
|
10206
10172
|
const result = await this.pool.query(
|
|
10207
|
-
`SELECT id,tx_hash,created_at_ms,close_search_before FROM sdk_hosted_settlement_report_obligations
|
|
10173
|
+
`SELECT id,state,tx_hash,created_at_ms,close_search_before FROM sdk_hosted_settlement_report_obligations
|
|
10208
10174
|
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
|
|
10209
10175
|
AND original->>'drainId'=$4 AND state NOT IN ('delivered','no_report')
|
|
10210
10176
|
ORDER BY created_at_ms DESC LIMIT 1`,
|
|
@@ -10213,6 +10179,7 @@ var HostedSettlementReportObligations = class {
|
|
|
10213
10179
|
const row = result.rows[0];
|
|
10214
10180
|
return row && {
|
|
10215
10181
|
id: row.id,
|
|
10182
|
+
state: row.state,
|
|
10216
10183
|
txHash: row.tx_hash,
|
|
10217
10184
|
reservedAtMs: Number(row.created_at_ms),
|
|
10218
10185
|
searchBefore: searchCursor(row.close_search_before)
|
|
@@ -10346,6 +10313,7 @@ var HostedSettlementReportObligations = class {
|
|
|
10346
10313
|
}
|
|
10347
10314
|
await tx.query(
|
|
10348
10315
|
`UPDATE sdk_hosted_settlement_report_obligations SET tx_hash=COALESCE(tx_hash,$2),
|
|
10316
|
+
state=CASE WHEN state IN ('exhausted','unmatched') THEN 'reserved' ELSE state END,
|
|
10349
10317
|
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
10350
10318
|
[row.id, txHash.toLowerCase()]
|
|
10351
10319
|
);
|
|
@@ -10353,15 +10321,15 @@ var HostedSettlementReportObligations = class {
|
|
|
10353
10321
|
}
|
|
10354
10322
|
/**
|
|
10355
10323
|
* Close reservations that never recorded a transaction, oldest first. Each
|
|
10356
|
-
* one owes a payout report
|
|
10357
|
-
*
|
|
10324
|
+
* one owes a payout report if its close lands. Exhausted attempts remain
|
|
10325
|
+
* observable for late landings; unmatched searches require an explicit retry.
|
|
10358
10326
|
*/
|
|
10359
10327
|
async unsubmittedTempoCloses(limit = 100) {
|
|
10360
10328
|
const result = await this.pool.query(
|
|
10361
10329
|
`SELECT id,channel_id,original->>'drainId' AS drain_id,created_at_ms,close_search_before
|
|
10362
10330
|
FROM sdk_hosted_settlement_report_obligations
|
|
10363
10331
|
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close'
|
|
10364
|
-
AND state
|
|
10332
|
+
AND state IN ('reserved','exhausted') AND tx_hash IS NULL
|
|
10365
10333
|
ORDER BY created_at_ms,id LIMIT $3`,
|
|
10366
10334
|
[this.dvmId, this.scope, limit]
|
|
10367
10335
|
);
|
|
@@ -10859,10 +10827,192 @@ var HostedSettlementReportObligations = class {
|
|
|
10859
10827
|
return { id: row.id, replayed: true };
|
|
10860
10828
|
}
|
|
10861
10829
|
};
|
|
10830
|
+
async function retryHostedTempoCloseReport(args) {
|
|
10831
|
+
const obligations = new HostedSettlementReportObligations(args.db, args.dvmId, args.scope);
|
|
10832
|
+
return obligations.reopenTempoClose(args.obligationId);
|
|
10833
|
+
}
|
|
10834
|
+
async function writeOffHostedTempoCloseReport(args) {
|
|
10835
|
+
const note = args.note.trim();
|
|
10836
|
+
if (!note || note.length > 2e3) throw new Error("A write-off requires a note of 1 to 2000 characters");
|
|
10837
|
+
const row = await new HostedSettlementReportObligations(args.db, args.dvmId, args.scope).writeOffTempoClose(args.obligationId, note);
|
|
10838
|
+
if (!row) return false;
|
|
10839
|
+
loggers.tempo.warn("tempo_session_close_report_written_off", {
|
|
10840
|
+
channel_id: row.channelId,
|
|
10841
|
+
obligation_id: args.obligationId,
|
|
10842
|
+
amount_forgone_native: row.amount
|
|
10843
|
+
});
|
|
10844
|
+
return true;
|
|
10845
|
+
}
|
|
10862
10846
|
function searchCursor(value) {
|
|
10863
10847
|
return value === null ? null : BigInt(value);
|
|
10864
10848
|
}
|
|
10865
10849
|
|
|
10850
|
+
// src/sdk/server/hosting-control.ts
|
|
10851
|
+
import { createHmac, randomUUID as randomUUID4, timingSafeEqual } from "crypto";
|
|
10852
|
+
|
|
10853
|
+
// src/sdk/server/hosting-cleanup-fence.ts
|
|
10854
|
+
var HostingCleanupFence = class {
|
|
10855
|
+
constructor(pool, dvmId, scope) {
|
|
10856
|
+
this.pool = pool;
|
|
10857
|
+
this.dvmId = dvmId;
|
|
10858
|
+
this.scope = scope;
|
|
10859
|
+
}
|
|
10860
|
+
pool;
|
|
10861
|
+
dvmId;
|
|
10862
|
+
scope;
|
|
10863
|
+
async assertNewIntent(tx, intent) {
|
|
10864
|
+
const continuation = intent?.admissionId || intent?.paymentSeq !== void 0 || intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash || intent?.creditId && intent.fundId && intent.callerPubkey;
|
|
10865
|
+
const { rows: rows2 } = await tx.query(
|
|
10866
|
+
`SELECT state, closed_at_ms, closed_at_us FROM sdk_hosting_control
|
|
10867
|
+
WHERE dvm_id=$1 AND scope=$2 ${continuation ? "" : "FOR UPDATE"}`,
|
|
10868
|
+
[this.dvmId, this.scope]
|
|
10869
|
+
);
|
|
10870
|
+
const control = rows2[0];
|
|
10871
|
+
if (!control) throw new Error("Hosted obligation fence is unavailable");
|
|
10872
|
+
if (control.state === "closed" && (control.closed_at_ms === null || control.closed_at_us === null)) {
|
|
10873
|
+
throw new Error("Hosted closure cutoff is unavailable");
|
|
10874
|
+
}
|
|
10875
|
+
if (control.state !== "closed" && !continuation) return;
|
|
10876
|
+
if (intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash) {
|
|
10877
|
+
const repair = !!intent.invoiceRepairPaymentHash;
|
|
10878
|
+
const invoice = await tx.query(
|
|
10879
|
+
`SELECT i.payment_hash FROM credit_invoices i
|
|
10880
|
+
LEFT JOIN sdk_hosting_admissions a ON a.id=i.hosting_admission_id
|
|
10881
|
+
WHERE i.payment_hash=$1 AND i.caller_pubkey=$4 AND i.amount_micro=$5
|
|
10882
|
+
AND (($10::boolean AND $3=i.payment_hash AND i.status IN ('blocked','written_off'))
|
|
10883
|
+
OR (NOT $10::boolean AND i.credit_id=$2 AND i.fund_id=$3 AND i.status='pending'))
|
|
10884
|
+
AND ((i.hosting_admission_id IS NULL AND
|
|
10885
|
+
($9::bigint IS NULL OR i.created_at_db_us <= $9)) OR
|
|
10886
|
+
(a.dvm_id=$6 AND a.scope=$7 AND a.credit_id=i.credit_id
|
|
10887
|
+
AND a.fund_id=i.fund_id AND a.caller_pubkey=i.caller_pubkey
|
|
10888
|
+
AND a.outcome IN ('pending','accepted','unresolved')
|
|
10889
|
+
AND ($8::bigint IS NULL OR a.admitted_at_ms <= $8)))`,
|
|
10890
|
+
[
|
|
10891
|
+
intent.invoicePaymentHash ?? intent.invoiceRepairPaymentHash,
|
|
10892
|
+
intent.creditId,
|
|
10893
|
+
intent.fundId,
|
|
10894
|
+
intent.callerPubkey,
|
|
10895
|
+
intent.amountMicro,
|
|
10896
|
+
this.dvmId,
|
|
10897
|
+
this.scope,
|
|
10898
|
+
control.closed_at_ms,
|
|
10899
|
+
control.closed_at_us,
|
|
10900
|
+
repair
|
|
10901
|
+
]
|
|
10902
|
+
);
|
|
10903
|
+
if (invoice.rows[0]) return;
|
|
10904
|
+
throw new Error("Hosted invoice has no original payable admission");
|
|
10905
|
+
}
|
|
10906
|
+
if (intent?.paymentSeq !== void 0 && intent.jobId) {
|
|
10907
|
+
const payment = await tx.query(
|
|
10908
|
+
`SELECT m.job_id FROM job_messages m JOIN jobs j ON j.id=m.job_id
|
|
10909
|
+
WHERE m.job_id=$1 AND m.seq=$2 AND m.status='pending-verification'
|
|
10910
|
+
AND m.created_at_ms IS NOT NULL
|
|
10911
|
+
AND ($3::bigint IS NULL OR m.created_at_ms <= $3) AND j.dvm_id=$4
|
|
10912
|
+
AND ($6::text IS NULL OR m.data->'content'->'tempo_credential'->'challenge'->>'id'=$6)
|
|
10913
|
+
AND EXISTS (SELECT 1 FROM sdk_hosting_admissions a
|
|
10914
|
+
WHERE a.dvm_id=$4 AND a.scope=$5 AND a.job_id=m.job_id AND a.outcome <> 'failed')`,
|
|
10915
|
+
[
|
|
10916
|
+
intent.jobId,
|
|
10917
|
+
intent.paymentSeq,
|
|
10918
|
+
control.closed_at_ms,
|
|
10919
|
+
this.dvmId,
|
|
10920
|
+
this.scope,
|
|
10921
|
+
intent.challengeId ?? null
|
|
10922
|
+
]
|
|
10923
|
+
);
|
|
10924
|
+
if (payment.rows[0]) return;
|
|
10925
|
+
throw new Error("Hosted payment has no pre-closure inbound intent");
|
|
10926
|
+
}
|
|
10927
|
+
if (!intent?.admissionId && !intent?.jobId && !(intent?.creditId && intent.fundId && intent.callerPubkey) || !intent?.jobId && !(intent?.creditId && intent.fundId)) {
|
|
10928
|
+
throw new Error("Hosted obligations are closed to new intents");
|
|
10929
|
+
}
|
|
10930
|
+
const accepted = await tx.query(
|
|
10931
|
+
`SELECT id FROM sdk_hosting_admissions
|
|
10932
|
+
WHERE ($1::text IS NULL OR id=$1) AND dvm_id=$2 AND scope=$3
|
|
10933
|
+
AND ($4::bigint IS NULL OR admitted_at_ms <= $4)
|
|
10934
|
+
AND (outcome IN ('pending', 'unresolved') OR ($11::boolean AND outcome='accepted'))
|
|
10935
|
+
AND ($5::text IS NULL OR job_id=$5)
|
|
10936
|
+
AND ($6::text IS NULL OR credit_id=$6)
|
|
10937
|
+
AND ($7::text IS NULL OR fund_id=$7)
|
|
10938
|
+
AND ($8::text IS NULL OR caller_pubkey=$8)
|
|
10939
|
+
AND ($9::text IS NULL OR funding_commitment=$9)
|
|
10940
|
+
AND ($10::text IS NULL OR payment_artifact_hash=$10)`,
|
|
10941
|
+
[
|
|
10942
|
+
intent.admissionId ?? null,
|
|
10943
|
+
this.dvmId,
|
|
10944
|
+
this.scope,
|
|
10945
|
+
control.closed_at_ms,
|
|
10946
|
+
intent.jobId ?? null,
|
|
10947
|
+
intent.creditId ?? null,
|
|
10948
|
+
intent.fundId ?? null,
|
|
10949
|
+
intent.callerPubkey ?? null,
|
|
10950
|
+
intent.fundingCommitment ?? null,
|
|
10951
|
+
intent.paymentArtifactHash ?? null,
|
|
10952
|
+
control.state !== "closed"
|
|
10953
|
+
]
|
|
10954
|
+
);
|
|
10955
|
+
if (!accepted.rows[0]) throw new Error("Hosted obligation has no pre-closure admission");
|
|
10956
|
+
}
|
|
10957
|
+
async withNewIntent(intent, operation) {
|
|
10958
|
+
return this.withNewIntentOn(this.pool, intent, operation);
|
|
10959
|
+
}
|
|
10960
|
+
async assertFundingReservation(tx, reservation) {
|
|
10961
|
+
await this.assertNewIntent(tx, {
|
|
10962
|
+
admissionId: reservation.admissionId,
|
|
10963
|
+
creditId: reservation.creditId,
|
|
10964
|
+
fundId: reservation.fundId,
|
|
10965
|
+
callerPubkey: reservation.callerPubkey,
|
|
10966
|
+
fundingCommitment: reservation.commitment,
|
|
10967
|
+
paymentArtifactHash: reservation.paymentArtifactHash
|
|
10968
|
+
});
|
|
10969
|
+
const verified = await tx.query(
|
|
10970
|
+
`SELECT id FROM sdk_hosting_admissions
|
|
10971
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND credit_id=$4 AND fund_id=$5
|
|
10972
|
+
AND caller_pubkey=$6 AND funding_commitment=$7 AND payment_artifact_hash=$8
|
|
10973
|
+
AND outcome IN ('pending','accepted','unresolved')`,
|
|
10974
|
+
[
|
|
10975
|
+
reservation.admissionId,
|
|
10976
|
+
this.dvmId,
|
|
10977
|
+
this.scope,
|
|
10978
|
+
reservation.creditId,
|
|
10979
|
+
reservation.fundId,
|
|
10980
|
+
reservation.callerPubkey,
|
|
10981
|
+
reservation.commitment,
|
|
10982
|
+
reservation.paymentArtifactHash
|
|
10983
|
+
]
|
|
10984
|
+
);
|
|
10985
|
+
if (!verified.rows[0]) throw new Error("Hosted funding has no authentic admission");
|
|
10986
|
+
}
|
|
10987
|
+
async fundingAdmission(tx, creditId, fundId, callerPubkey) {
|
|
10988
|
+
await this.assertNewIntent(tx, { creditId, fundId, callerPubkey });
|
|
10989
|
+
const { rows: rows2 } = await tx.query(
|
|
10990
|
+
`SELECT id FROM sdk_hosting_admissions
|
|
10991
|
+
WHERE dvm_id=$1 AND scope=$2 AND credit_id=$3 AND fund_id=$4
|
|
10992
|
+
AND caller_pubkey=$5 AND outcome IN ('pending','unresolved')
|
|
10993
|
+
ORDER BY admitted_at_ms,id LIMIT 2`,
|
|
10994
|
+
[this.dvmId, this.scope, creditId, fundId, callerPubkey]
|
|
10995
|
+
);
|
|
10996
|
+
if (rows2.length !== 1) throw new Error("Hosted invoice funding admission is missing or ambiguous");
|
|
10997
|
+
return rows2[0].id;
|
|
10998
|
+
}
|
|
10999
|
+
async withNewIntentOn(db, intent, operation) {
|
|
11000
|
+
const tx = await db.connect();
|
|
11001
|
+
try {
|
|
11002
|
+
await tx.query("BEGIN");
|
|
11003
|
+
await this.assertNewIntent(tx, intent);
|
|
11004
|
+
const result = await operation(tx);
|
|
11005
|
+
await tx.query("COMMIT");
|
|
11006
|
+
return result;
|
|
11007
|
+
} catch (error) {
|
|
11008
|
+
await tx.query("ROLLBACK").catch(() => void 0);
|
|
11009
|
+
throw error;
|
|
11010
|
+
} finally {
|
|
11011
|
+
tx.release();
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
};
|
|
11015
|
+
|
|
10866
11016
|
// src/sdk/server/hosting-channel-evidence.ts
|
|
10867
11017
|
import { createHash as createHash7 } from "crypto";
|
|
10868
11018
|
import { Session as TempoSession2 } from "mppx/tempo";
|
|
@@ -18347,7 +18497,7 @@ async function requireWedgedTempoDrain(c, opts, args, admitBooked) {
|
|
|
18347
18497
|
)
|
|
18348
18498
|
};
|
|
18349
18499
|
}
|
|
18350
|
-
if (record.status !== "pending" && !(admitBooked && record.status === "sent")) {
|
|
18500
|
+
if (record.status !== "pending" && !(admitBooked && (record.status === "sent" || record.status === "exit_required"))) {
|
|
18351
18501
|
return { response: tempoNotWedgedRefusal(c, args, record.status) };
|
|
18352
18502
|
}
|
|
18353
18503
|
return { record, channelId: record.channelId };
|
|
@@ -19763,7 +19913,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
19763
19913
|
}
|
|
19764
19914
|
}
|
|
19765
19915
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
19766
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
19916
|
+
const { RevenueReporter } = await import("./revenue-reporter-4EAVPWXS.js");
|
|
19767
19917
|
const reporter = new RevenueReporter(
|
|
19768
19918
|
opts.db,
|
|
19769
19919
|
platformUrl,
|
|
@@ -19773,7 +19923,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
19773
19923
|
revenueReporter = reporter;
|
|
19774
19924
|
await reporter.init();
|
|
19775
19925
|
reporter.startRetryLoop();
|
|
19776
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
19926
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-TZX2S7KG.js");
|
|
19777
19927
|
payoutReporter = new PayoutReporter({
|
|
19778
19928
|
dvmId,
|
|
19779
19929
|
db: opts.db,
|
|
@@ -20074,7 +20224,43 @@ var DVMServer = class {
|
|
|
20074
20224
|
dvmId: this.receiptDvmId()
|
|
20075
20225
|
}) : void 0;
|
|
20076
20226
|
this.creditLedger = opts.creditLedger ?? new MemoryCreditLedger();
|
|
20077
|
-
this.opts.mpp?.useTempoCreditLedger?.(this.creditLedger)
|
|
20227
|
+
this.opts.mpp?.useTempoCreditLedger?.(this.creditLedger, async (drain, evidence) => {
|
|
20228
|
+
const completed = await completeTempoChannelDrain({
|
|
20229
|
+
deps: { ledger: this.creditLedger, drainReport: {
|
|
20230
|
+
db: this.opts.db,
|
|
20231
|
+
dvmId: this.opts.dvmId,
|
|
20232
|
+
enqueueCreditDrain: this.opts.enqueueCreditDrain
|
|
20233
|
+
} },
|
|
20234
|
+
creditId: drain.creditId,
|
|
20235
|
+
drainId: drain.drainId,
|
|
20236
|
+
channelId: drain.channelId,
|
|
20237
|
+
refundedNative: evidence.refundedNative,
|
|
20238
|
+
debitedNative: drain.drainedNative,
|
|
20239
|
+
reference: evidence.reference,
|
|
20240
|
+
finalization: {
|
|
20241
|
+
outcome: evidence.creditUnbacked ? `${evidence.outcome}_unbacked` : evidence.outcome,
|
|
20242
|
+
creditUnbacked: evidence.creditUnbacked
|
|
20243
|
+
}
|
|
20244
|
+
});
|
|
20245
|
+
if (!completed.replayed && this.receiptIssuer) {
|
|
20246
|
+
const receipt = this.receiptIssuer.issueDrain({
|
|
20247
|
+
creditId: drain.creditId,
|
|
20248
|
+
drainId: drain.drainId,
|
|
20249
|
+
event: "sent",
|
|
20250
|
+
method: drain.method,
|
|
20251
|
+
amountMicro: drain.amountMicro,
|
|
20252
|
+
balanceAfterMicro: drain.balanceAfterMicro,
|
|
20253
|
+
ledgerSeq: drain.ledgerSeq,
|
|
20254
|
+
callerPubkey: drain.callerPubkey
|
|
20255
|
+
});
|
|
20256
|
+
await this.creditLedger.appendDrainReceipt({
|
|
20257
|
+
creditId: drain.creditId,
|
|
20258
|
+
drainId: drain.drainId,
|
|
20259
|
+
receipt
|
|
20260
|
+
});
|
|
20261
|
+
}
|
|
20262
|
+
return !completed.replayed;
|
|
20263
|
+
});
|
|
20078
20264
|
this.processedPayments = opts.processedPayments ?? new MemoryProcessedPaymentStore();
|
|
20079
20265
|
this.jobManager = new JobManager(descriptor, {
|
|
20080
20266
|
receiptIssuer: this.receiptIssuer,
|
|
@@ -23277,6 +23463,8 @@ export {
|
|
|
23277
23463
|
X402ExactSettlementServer,
|
|
23278
23464
|
X402ExactSettlementEvidenceMissingError,
|
|
23279
23465
|
HostedSettlementReportObligations,
|
|
23466
|
+
retryHostedTempoCloseReport,
|
|
23467
|
+
writeOffHostedTempoCloseReport,
|
|
23280
23468
|
signHostingControl,
|
|
23281
23469
|
signHostingClosure,
|
|
23282
23470
|
jobCredentialGate,
|