@dvmkit/sdk 0.8.0 → 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-5K5E6ME6.js → chunk-4VV7WACY.js} +1 -1
- package/dist/{chunk-GSPV3UIM.js → chunk-73C4JN2E.js} +632 -262
- package/dist/{chunk-2I3OHMTY.js → chunk-BAQRUYRL.js} +3 -3
- package/dist/{chunk-ATXDKX6F.js → chunk-GAZBBMAA.js} +1 -1
- package/dist/{chunk-QPXI7YPH.js → chunk-GODOH2AF.js} +3 -3
- package/dist/{chunk-5CQKSLAU.js → chunk-HC4I7LAV.js} +1 -1
- package/dist/{chunk-6FEHRZO7.js → chunk-IBX2ORBV.js} +1 -1
- package/dist/{chunk-TFYY5APM.js → chunk-IYQQXEHV.js} +3 -3
- package/dist/{chunk-X3JEME6P.js → chunk-K2MSIKMM.js} +3 -3
- package/dist/{chunk-55NKZQOC.js → chunk-KGL3YMZA.js} +1 -1
- package/dist/{chunk-YPSJU3OH.js → chunk-LCCG5HQM.js} +2 -2
- package/dist/{chunk-GB5XPAA5.js → chunk-LFVIZH2G.js} +361 -44
- package/dist/{chunk-RTFBVXCN.js → chunk-LW7KZQHG.js} +4 -4
- package/dist/{chunk-DZQYQN2X.js → chunk-OIP44KBB.js} +12 -0
- package/dist/{chunk-P5WEZXJ2.js → chunk-Q3VYRU5R.js} +60 -3
- package/dist/{chunk-BL6LRWXD.js → chunk-QSZW2I5Y.js} +1 -1
- package/dist/{chunk-I6PHA54S.js → chunk-TFDR2YP2.js} +3 -3
- package/dist/{chunk-IFWUWHTW.js → chunk-TRY7BJA4.js} +1 -1
- package/dist/{chunk-PRSKC7AD.js → chunk-WB3E7KE7.js} +3 -3
- package/dist/{chunk-XXLUIXPO.js → chunk-WLPLHXNP.js} +1 -1
- package/dist/{chunk-VYP6ADCS.js → chunk-YYRY32NR.js} +24 -8
- package/dist/{chunk-EBCLWOKF.js → chunk-ZBQKXAYK.js} +17 -4
- package/dist/{content-retention-D2K4MKNO.d.ts → content-retention-CVBYehRS.d.ts} +1 -1
- package/dist/{credit-ledger-AOU6S34N.js → credit-ledger-CZQ56DYI.js} +4 -4
- package/dist/{credit-menu-C-8g2cHT.d.ts → credit-menu-QntEhZJS.d.ts} +4 -4
- package/dist/{fx-Ql4nvdbh.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-DCvJQUX0.d.ts → job-store-qOD-FrQF.d.ts} +1 -1
- package/dist/{lightning-backend-CY5ML3km.d.ts → lightning-backend-DtdG6UT5.d.ts} +1 -1
- package/dist/{memory-credit-ledger-4HWYINKJ.js → memory-credit-ledger-EMN7G3MO.js} +5 -5
- package/dist/{mpp-setup-2BKTPEK6.js → mpp-setup-VW53KFPO.js} +5 -3
- package/dist/{payout-reporter-HQSIIBFL.js → payout-reporter-TZX2S7KG.js} +2 -2
- package/dist/{postgres-consumed-credential-store-R7DNU6CM.js → postgres-consumed-credential-store-V464OAJB.js} +3 -3
- package/dist/{postgres-job-store-6VGFL6XE.js → postgres-job-store-27VOCSPO.js} +2 -2
- package/dist/{postgres-replay-store-SY26NUCI.js → postgres-replay-store-7GJFAQXY.js} +2 -2
- package/dist/{pricing-H2RA4X2R.js → pricing-NNMBNP7N.js} +3 -3
- package/dist/{revenue-reporter-X52GPZ5U.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-BOWAn_BH.d.ts → step-cache-B7svmPz3.d.ts} +157 -8
- package/dist/{tempo-charge-store-NKXJ2AXE.js → tempo-charge-store-2RPOJBM2.js} +2 -2
- package/dist/{tempo-session-store-ZSDQIWSU.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-MAtMp3DB.d.ts → usd-D3izFz71.d.ts} +1 -1
- package/dist/{x402-Q4S36NHQ.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-GB5XPAA5.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-BL6LRWXD.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);
|
|
@@ -7487,17 +7503,17 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7487
7503
|
await beginClose(expectedChannelId, record.drainId);
|
|
7488
7504
|
recoveryVersion = await deps.tempoSettlementReadiness?.captureRecoveryVersion();
|
|
7489
7505
|
if (deps.hostedSettlementReports) {
|
|
7490
|
-
|
|
7506
|
+
hostedObligationId = await reserveHostedTempoClose(
|
|
7507
|
+
deps.hostedSettlementReports,
|
|
7508
|
+
rail.mpp,
|
|
7509
|
+
record,
|
|
7491
7510
|
expectedChannelId,
|
|
7492
|
-
|
|
7493
|
-
record.drainId
|
|
7511
|
+
deps.ledger
|
|
7494
7512
|
);
|
|
7495
|
-
if (reserved.replayed) throw new Error("Hosted Tempo close already has an original effect attempt");
|
|
7496
|
-
hostedObligationId = reserved.id;
|
|
7497
7513
|
if (!rail.mpp?.permitHostedTempoClose) {
|
|
7498
7514
|
throw new Error("Hosted Tempo close credential guard is unavailable");
|
|
7499
7515
|
}
|
|
7500
|
-
rail.mpp.permitHostedTempoClose(rail.mppCredential,
|
|
7516
|
+
rail.mpp.permitHostedTempoClose(rail.mppCredential, hostedObligationId);
|
|
7501
7517
|
}
|
|
7502
7518
|
}
|
|
7503
7519
|
});
|
|
@@ -7522,6 +7538,22 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7522
7538
|
}
|
|
7523
7539
|
const wrongChannel = tempoWrongChannelRefusal(error, record);
|
|
7524
7540
|
if (wrongChannel) return wrongChannel;
|
|
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);
|
|
7556
|
+
}
|
|
7525
7557
|
const refused = tempoFloorRefusal(error, record);
|
|
7526
7558
|
if (refused) return refused;
|
|
7527
7559
|
if (tempoCloseCredentialNeedsRefresh(error)) {
|
|
@@ -7568,6 +7600,69 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7568
7600
|
});
|
|
7569
7601
|
}
|
|
7570
7602
|
}
|
|
7603
|
+
var HOSTED_TEMPO_CLOSE_STALE_MS = 3e5;
|
|
7604
|
+
var HOSTED_TEMPO_CLOSE_MAX_RELEASES = 3;
|
|
7605
|
+
var HOSTED_TEMPO_CLOSE_IN_FLIGHT = "Hosted Tempo close already has an original effect attempt";
|
|
7606
|
+
var HostedTempoCloseAttemptsExhaustedError = class extends Error {
|
|
7607
|
+
};
|
|
7608
|
+
async function reserveHostedTempoClose(reports, mpp, record, channelId, ledger) {
|
|
7609
|
+
const pending = await reports.pendingTempoClose(channelId, record.drainId);
|
|
7610
|
+
if (!pending) {
|
|
7611
|
+
const reserved = await reports.reserveTempo(channelId, "close", record.drainId);
|
|
7612
|
+
if (reserved.replayed) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7613
|
+
return reserved.id;
|
|
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");
|
|
7617
|
+
if (pending.txHash || !mpp.getTempoSessionDrainState) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7618
|
+
const state = await mpp.getTempoSessionDrainState(channelId);
|
|
7619
|
+
if (state.finalized || state.deposit === 0n || state.closeRequestedAt !== 0n) {
|
|
7620
|
+
throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7621
|
+
}
|
|
7622
|
+
const result = await reports.releaseStaleTempoClose(pending.id, channelId, record.drainId, {
|
|
7623
|
+
staleAfterMs: HOSTED_TEMPO_CLOSE_STALE_MS,
|
|
7624
|
+
maxReleases: HOSTED_TEMPO_CLOSE_MAX_RELEASES,
|
|
7625
|
+
onExhausted: async (tx) => {
|
|
7626
|
+
await ledger.requireTempoDrainExit?.({ creditId: record.creditId, drainId: record.drainId, tx });
|
|
7627
|
+
}
|
|
7628
|
+
});
|
|
7629
|
+
if (result.outcome === "in_flight") throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7630
|
+
if (result.outcome === "exhausted") {
|
|
7631
|
+
loggers.tempo.warn("tempo_session_close_attempts_exhausted", {
|
|
7632
|
+
credit_id: record.creditId,
|
|
7633
|
+
drain_id: record.drainId,
|
|
7634
|
+
channel_id: channelId,
|
|
7635
|
+
obligation_id: pending.id,
|
|
7636
|
+
attempt: result.releases + 1,
|
|
7637
|
+
amount_at_stake_native: await reports.tempoCloseAmountAtStake(pending.id)
|
|
7638
|
+
});
|
|
7639
|
+
throw new HostedTempoCloseAttemptsExhaustedError(
|
|
7640
|
+
`Tempo drain ${record.drainId} exhausted its cooperative close attempts`
|
|
7641
|
+
);
|
|
7642
|
+
}
|
|
7643
|
+
loggers.tempo.warn("tempo_session_close_reservation_released", {
|
|
7644
|
+
credit_id: record.creditId,
|
|
7645
|
+
drain_id: record.drainId,
|
|
7646
|
+
channel_id: channelId,
|
|
7647
|
+
obligation_id: pending.id,
|
|
7648
|
+
attempt: result.releases + 1
|
|
7649
|
+
});
|
|
7650
|
+
return result.id;
|
|
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
|
+
}
|
|
7571
7666
|
function tempoCloseCredentialNeedsRefresh(error) {
|
|
7572
7667
|
return error instanceof MppxErrors.InvalidChallengeError || error instanceof MppxErrors.PaymentExpiredError;
|
|
7573
7668
|
}
|
|
@@ -8059,6 +8154,11 @@ function drainCopy(record) {
|
|
|
8059
8154
|
display: "The refund was sent to the payout destination you provided.",
|
|
8060
8155
|
hint: "See `sent` for the settlement reference (e.g. the on-chain transaction hash)."
|
|
8061
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
|
+
};
|
|
8062
8162
|
case "released":
|
|
8063
8163
|
return {
|
|
8064
8164
|
display: "The channel could not complete this refund, so the service restored the amount to your prepaid balance.",
|
|
@@ -9536,172 +9636,6 @@ function safeError(err) {
|
|
|
9536
9636
|
return "settlement_transport_error";
|
|
9537
9637
|
}
|
|
9538
9638
|
|
|
9539
|
-
// src/sdk/server/hosting-control.ts
|
|
9540
|
-
import { createHmac, randomUUID as randomUUID4, timingSafeEqual } from "crypto";
|
|
9541
|
-
|
|
9542
|
-
// src/sdk/server/hosting-cleanup-fence.ts
|
|
9543
|
-
var HostingCleanupFence = class {
|
|
9544
|
-
constructor(pool, dvmId, scope) {
|
|
9545
|
-
this.pool = pool;
|
|
9546
|
-
this.dvmId = dvmId;
|
|
9547
|
-
this.scope = scope;
|
|
9548
|
-
}
|
|
9549
|
-
pool;
|
|
9550
|
-
dvmId;
|
|
9551
|
-
scope;
|
|
9552
|
-
async assertNewIntent(tx, intent) {
|
|
9553
|
-
const continuation = intent?.admissionId || intent?.paymentSeq !== void 0 || intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash || intent?.creditId && intent.fundId && intent.callerPubkey;
|
|
9554
|
-
const { rows: rows2 } = await tx.query(
|
|
9555
|
-
`SELECT state, closed_at_ms, closed_at_us FROM sdk_hosting_control
|
|
9556
|
-
WHERE dvm_id=$1 AND scope=$2 ${continuation ? "" : "FOR UPDATE"}`,
|
|
9557
|
-
[this.dvmId, this.scope]
|
|
9558
|
-
);
|
|
9559
|
-
const control = rows2[0];
|
|
9560
|
-
if (!control) throw new Error("Hosted obligation fence is unavailable");
|
|
9561
|
-
if (control.state === "closed" && (control.closed_at_ms === null || control.closed_at_us === null)) {
|
|
9562
|
-
throw new Error("Hosted closure cutoff is unavailable");
|
|
9563
|
-
}
|
|
9564
|
-
if (control.state !== "closed" && !continuation) return;
|
|
9565
|
-
if (intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash) {
|
|
9566
|
-
const repair = !!intent.invoiceRepairPaymentHash;
|
|
9567
|
-
const invoice = await tx.query(
|
|
9568
|
-
`SELECT i.payment_hash FROM credit_invoices i
|
|
9569
|
-
LEFT JOIN sdk_hosting_admissions a ON a.id=i.hosting_admission_id
|
|
9570
|
-
WHERE i.payment_hash=$1 AND i.caller_pubkey=$4 AND i.amount_micro=$5
|
|
9571
|
-
AND (($10::boolean AND $3=i.payment_hash AND i.status IN ('blocked','written_off'))
|
|
9572
|
-
OR (NOT $10::boolean AND i.credit_id=$2 AND i.fund_id=$3 AND i.status='pending'))
|
|
9573
|
-
AND ((i.hosting_admission_id IS NULL AND
|
|
9574
|
-
($9::bigint IS NULL OR i.created_at_db_us <= $9)) OR
|
|
9575
|
-
(a.dvm_id=$6 AND a.scope=$7 AND a.credit_id=i.credit_id
|
|
9576
|
-
AND a.fund_id=i.fund_id AND a.caller_pubkey=i.caller_pubkey
|
|
9577
|
-
AND a.outcome IN ('pending','accepted','unresolved')
|
|
9578
|
-
AND ($8::bigint IS NULL OR a.admitted_at_ms <= $8)))`,
|
|
9579
|
-
[
|
|
9580
|
-
intent.invoicePaymentHash ?? intent.invoiceRepairPaymentHash,
|
|
9581
|
-
intent.creditId,
|
|
9582
|
-
intent.fundId,
|
|
9583
|
-
intent.callerPubkey,
|
|
9584
|
-
intent.amountMicro,
|
|
9585
|
-
this.dvmId,
|
|
9586
|
-
this.scope,
|
|
9587
|
-
control.closed_at_ms,
|
|
9588
|
-
control.closed_at_us,
|
|
9589
|
-
repair
|
|
9590
|
-
]
|
|
9591
|
-
);
|
|
9592
|
-
if (invoice.rows[0]) return;
|
|
9593
|
-
throw new Error("Hosted invoice has no original payable admission");
|
|
9594
|
-
}
|
|
9595
|
-
if (intent?.paymentSeq !== void 0 && intent.jobId) {
|
|
9596
|
-
const payment = await tx.query(
|
|
9597
|
-
`SELECT m.job_id FROM job_messages m JOIN jobs j ON j.id=m.job_id
|
|
9598
|
-
WHERE m.job_id=$1 AND m.seq=$2 AND m.status='pending-verification'
|
|
9599
|
-
AND m.created_at_ms IS NOT NULL
|
|
9600
|
-
AND ($3::bigint IS NULL OR m.created_at_ms <= $3) AND j.dvm_id=$4
|
|
9601
|
-
AND ($6::text IS NULL OR m.data->'content'->'tempo_credential'->'challenge'->>'id'=$6)
|
|
9602
|
-
AND EXISTS (SELECT 1 FROM sdk_hosting_admissions a
|
|
9603
|
-
WHERE a.dvm_id=$4 AND a.scope=$5 AND a.job_id=m.job_id AND a.outcome <> 'failed')`,
|
|
9604
|
-
[
|
|
9605
|
-
intent.jobId,
|
|
9606
|
-
intent.paymentSeq,
|
|
9607
|
-
control.closed_at_ms,
|
|
9608
|
-
this.dvmId,
|
|
9609
|
-
this.scope,
|
|
9610
|
-
intent.challengeId ?? null
|
|
9611
|
-
]
|
|
9612
|
-
);
|
|
9613
|
-
if (payment.rows[0]) return;
|
|
9614
|
-
throw new Error("Hosted payment has no pre-closure inbound intent");
|
|
9615
|
-
}
|
|
9616
|
-
if (!intent?.admissionId && !intent?.jobId && !(intent?.creditId && intent.fundId && intent.callerPubkey) || !intent?.jobId && !(intent?.creditId && intent.fundId)) {
|
|
9617
|
-
throw new Error("Hosted obligations are closed to new intents");
|
|
9618
|
-
}
|
|
9619
|
-
const accepted = await tx.query(
|
|
9620
|
-
`SELECT id FROM sdk_hosting_admissions
|
|
9621
|
-
WHERE ($1::text IS NULL OR id=$1) AND dvm_id=$2 AND scope=$3
|
|
9622
|
-
AND ($4::bigint IS NULL OR admitted_at_ms <= $4)
|
|
9623
|
-
AND (outcome IN ('pending', 'unresolved') OR ($11::boolean AND outcome='accepted'))
|
|
9624
|
-
AND ($5::text IS NULL OR job_id=$5)
|
|
9625
|
-
AND ($6::text IS NULL OR credit_id=$6)
|
|
9626
|
-
AND ($7::text IS NULL OR fund_id=$7)
|
|
9627
|
-
AND ($8::text IS NULL OR caller_pubkey=$8)
|
|
9628
|
-
AND ($9::text IS NULL OR funding_commitment=$9)
|
|
9629
|
-
AND ($10::text IS NULL OR payment_artifact_hash=$10)`,
|
|
9630
|
-
[
|
|
9631
|
-
intent.admissionId ?? null,
|
|
9632
|
-
this.dvmId,
|
|
9633
|
-
this.scope,
|
|
9634
|
-
control.closed_at_ms,
|
|
9635
|
-
intent.jobId ?? null,
|
|
9636
|
-
intent.creditId ?? null,
|
|
9637
|
-
intent.fundId ?? null,
|
|
9638
|
-
intent.callerPubkey ?? null,
|
|
9639
|
-
intent.fundingCommitment ?? null,
|
|
9640
|
-
intent.paymentArtifactHash ?? null,
|
|
9641
|
-
control.state !== "closed"
|
|
9642
|
-
]
|
|
9643
|
-
);
|
|
9644
|
-
if (!accepted.rows[0]) throw new Error("Hosted obligation has no pre-closure admission");
|
|
9645
|
-
}
|
|
9646
|
-
async withNewIntent(intent, operation) {
|
|
9647
|
-
return this.withNewIntentOn(this.pool, intent, operation);
|
|
9648
|
-
}
|
|
9649
|
-
async assertFundingReservation(tx, reservation) {
|
|
9650
|
-
await this.assertNewIntent(tx, {
|
|
9651
|
-
admissionId: reservation.admissionId,
|
|
9652
|
-
creditId: reservation.creditId,
|
|
9653
|
-
fundId: reservation.fundId,
|
|
9654
|
-
callerPubkey: reservation.callerPubkey,
|
|
9655
|
-
fundingCommitment: reservation.commitment,
|
|
9656
|
-
paymentArtifactHash: reservation.paymentArtifactHash
|
|
9657
|
-
});
|
|
9658
|
-
const verified = await tx.query(
|
|
9659
|
-
`SELECT id FROM sdk_hosting_admissions
|
|
9660
|
-
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND credit_id=$4 AND fund_id=$5
|
|
9661
|
-
AND caller_pubkey=$6 AND funding_commitment=$7 AND payment_artifact_hash=$8
|
|
9662
|
-
AND outcome IN ('pending','accepted','unresolved')`,
|
|
9663
|
-
[
|
|
9664
|
-
reservation.admissionId,
|
|
9665
|
-
this.dvmId,
|
|
9666
|
-
this.scope,
|
|
9667
|
-
reservation.creditId,
|
|
9668
|
-
reservation.fundId,
|
|
9669
|
-
reservation.callerPubkey,
|
|
9670
|
-
reservation.commitment,
|
|
9671
|
-
reservation.paymentArtifactHash
|
|
9672
|
-
]
|
|
9673
|
-
);
|
|
9674
|
-
if (!verified.rows[0]) throw new Error("Hosted funding has no authentic admission");
|
|
9675
|
-
}
|
|
9676
|
-
async fundingAdmission(tx, creditId, fundId, callerPubkey) {
|
|
9677
|
-
await this.assertNewIntent(tx, { creditId, fundId, callerPubkey });
|
|
9678
|
-
const { rows: rows2 } = await tx.query(
|
|
9679
|
-
`SELECT id FROM sdk_hosting_admissions
|
|
9680
|
-
WHERE dvm_id=$1 AND scope=$2 AND credit_id=$3 AND fund_id=$4
|
|
9681
|
-
AND caller_pubkey=$5 AND outcome IN ('pending','unresolved')
|
|
9682
|
-
ORDER BY admitted_at_ms,id LIMIT 2`,
|
|
9683
|
-
[this.dvmId, this.scope, creditId, fundId, callerPubkey]
|
|
9684
|
-
);
|
|
9685
|
-
if (rows2.length !== 1) throw new Error("Hosted invoice funding admission is missing or ambiguous");
|
|
9686
|
-
return rows2[0].id;
|
|
9687
|
-
}
|
|
9688
|
-
async withNewIntentOn(db, intent, operation) {
|
|
9689
|
-
const tx = await db.connect();
|
|
9690
|
-
try {
|
|
9691
|
-
await tx.query("BEGIN");
|
|
9692
|
-
await this.assertNewIntent(tx, intent);
|
|
9693
|
-
const result = await operation(tx);
|
|
9694
|
-
await tx.query("COMMIT");
|
|
9695
|
-
return result;
|
|
9696
|
-
} catch (error) {
|
|
9697
|
-
await tx.query("ROLLBACK").catch(() => void 0);
|
|
9698
|
-
throw error;
|
|
9699
|
-
} finally {
|
|
9700
|
-
tx.release();
|
|
9701
|
-
}
|
|
9702
|
-
}
|
|
9703
|
-
};
|
|
9704
|
-
|
|
9705
9639
|
// src/sdk/server/hosting-settlement-report-obligations.ts
|
|
9706
9640
|
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
9707
9641
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
@@ -9925,7 +9859,8 @@ var HostedSettlementReportObligations = class {
|
|
|
9925
9859
|
operation TEXT NOT NULL CHECK (operation IN ('claim','refund','settle','close')),
|
|
9926
9860
|
network TEXT NOT NULL, contract TEXT NOT NULL, channel_id TEXT NOT NULL,
|
|
9927
9861
|
original JSONB NOT NULL,
|
|
9928
|
-
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,
|
|
9929
9864
|
tx_hash TEXT, block_number BIGINT, block_hash TEXT, delta_native NUMERIC(78,0),
|
|
9930
9865
|
payout_id TEXT, report_id TEXT, report_payload JSONB, delivered_at_ms BIGINT,
|
|
9931
9866
|
created_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
|
|
@@ -9938,6 +9873,29 @@ var HostedSettlementReportObligations = class {
|
|
|
9938
9873
|
ON sdk_hosted_settlement_report_obligations (report_id)
|
|
9939
9874
|
WHERE report_id IS NOT NULL;
|
|
9940
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
|
+
}
|
|
9941
9899
|
await this.pool.query(
|
|
9942
9900
|
`INSERT INTO sdk_hosted_settlement_report_history (singleton,complete)
|
|
9943
9901
|
VALUES (TRUE,$1) ON CONFLICT (singleton) DO NOTHING`,
|
|
@@ -10037,53 +9995,169 @@ var HostedSettlementReportObligations = class {
|
|
|
10037
9995
|
});
|
|
10038
9996
|
}
|
|
10039
9997
|
async reserveTempo(channelId, operation, drainId) {
|
|
10040
|
-
|
|
10041
|
-
|
|
9998
|
+
return transaction(this.pool, (tx) => this.reserveTempoIn(tx, channelId, operation, drainId));
|
|
9999
|
+
}
|
|
10000
|
+
/**
|
|
10001
|
+
* Retire a close reservation that never recorded a transaction and reserve
|
|
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
|
|
10005
|
+
* writes it onto the successor, and recovery attaches it there if that
|
|
10006
|
+
* attempt never got to write, so it is reported once.
|
|
10007
|
+
*
|
|
10008
|
+
* The caller has just read the channel open on chain. The age bound only
|
|
10009
|
+
* spares a reservation young enough to be inside its first receipt wait; an
|
|
10010
|
+
* attempt slowed past it by a degraded RPC can still be running.
|
|
10011
|
+
*/
|
|
10012
|
+
async releaseStaleTempoClose(id, channelId, drainId, limits) {
|
|
10042
10013
|
return transaction(this.pool, async (tx) => {
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10014
|
+
await this.lockIdentity(tx);
|
|
10015
|
+
const row = await this.lockRow(tx, id);
|
|
10016
|
+
if (row.rail !== "tempo" || row.operation !== "close" || row.channel_id !== channelId.toLowerCase() || row.original.drainId !== drainId || row.state !== "reserved" || row.tx_hash) {
|
|
10017
|
+
return { outcome: "in_flight" };
|
|
10018
|
+
}
|
|
10019
|
+
const age = await tx.query(
|
|
10020
|
+
`SELECT updated_at_ms <= FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint - $2 AS stale
|
|
10021
|
+
FROM sdk_hosted_settlement_report_obligations WHERE id=$1`,
|
|
10022
|
+
[id, limits.staleAfterMs]
|
|
10046
10023
|
);
|
|
10047
|
-
if (!
|
|
10048
|
-
const
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
[channelId.toLowerCase()]
|
|
10024
|
+
if (!age.rows[0]?.stale) return { outcome: "in_flight" };
|
|
10025
|
+
const released = Number((await tx.query(
|
|
10026
|
+
`SELECT COUNT(*)::text AS n FROM sdk_hosted_settlement_report_obligations
|
|
10027
|
+
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
|
|
10028
|
+
AND original->>'drainId'=$4 AND state='no_report' AND tx_hash IS NULL`,
|
|
10029
|
+
[this.dvmId, this.scope, channelId.toLowerCase(), drainId]
|
|
10030
|
+
)).rows[0].n);
|
|
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]
|
|
10060
10036
|
);
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
const
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10037
|
+
await limits.onExhausted?.(tx);
|
|
10038
|
+
return { outcome: "exhausted", releases: released };
|
|
10039
|
+
}
|
|
10040
|
+
await tx.query(
|
|
10041
|
+
`UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
|
|
10042
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
10043
|
+
[id]
|
|
10044
|
+
);
|
|
10045
|
+
const next = await this.reserveTempoIn(tx, channelId, "close", drainId);
|
|
10046
|
+
return { outcome: "reserved", id: next.id, releases: released + 1 };
|
|
10047
|
+
});
|
|
10048
|
+
}
|
|
10049
|
+
/**
|
|
10050
|
+
* Resolve an unsubmitted close reservation whose channel the payer exited
|
|
10051
|
+
* on its own: no cooperative close of ours can land on a finalized channel.
|
|
10052
|
+
*/
|
|
10053
|
+
async resolveTempoCloseExit(id) {
|
|
10054
|
+
const result = await this.pool.query(
|
|
10055
|
+
`UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
|
|
10056
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10057
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
|
|
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`,
|
|
10075
|
+
[id, this.dvmId, this.scope]
|
|
10076
|
+
);
|
|
10077
|
+
return result.rowCount === 1;
|
|
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]
|
|
10084
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;
|
|
10085
10113
|
});
|
|
10086
10114
|
}
|
|
10115
|
+
async reserveTempoIn(tx, channelId, operation, drainId) {
|
|
10116
|
+
const config = this.tempo;
|
|
10117
|
+
if (!config || !hex(channelId, 32)) throw new Error("Hosted Tempo settlement configuration is unavailable");
|
|
10118
|
+
const state = await tx.query(
|
|
10119
|
+
"SELECT state_json FROM mpp_tempo_channels WHERE channel_key=$1",
|
|
10120
|
+
[channelId.toLowerCase()]
|
|
10121
|
+
);
|
|
10122
|
+
if (!state.rows[0]) throw new Error("Hosted Tempo channel is not durable");
|
|
10123
|
+
const identity = await this.lockIdentity(tx);
|
|
10124
|
+
const parsed = parseStoreValue(state.rows[0].state_json);
|
|
10125
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Hosted Tempo state is invalid");
|
|
10126
|
+
const row = parsed;
|
|
10127
|
+
if (row.chainId !== Number(config.network.split(":")[1]) || String(row.escrowContract).toLowerCase() !== config.escrow.toLowerCase() || String(row.payee).toLowerCase() !== config.payee.toLowerCase() || String(row.token).toLowerCase() !== config.token.toLowerCase()) {
|
|
10128
|
+
throw new Error("Hosted Tempo channel has another chain or recipient");
|
|
10129
|
+
}
|
|
10130
|
+
if (operation === "close") {
|
|
10131
|
+
if (!drainId) throw new Error("Hosted Tempo close requires the original drain");
|
|
10132
|
+
const close = await tx.query(
|
|
10133
|
+
"SELECT drain_id FROM mpp_tempo_close_intents WHERE channel_id=$1",
|
|
10134
|
+
[channelId.toLowerCase()]
|
|
10135
|
+
);
|
|
10136
|
+
if (close.rows[0]?.drain_id !== drainId) throw new Error("Hosted Tempo close has no matching drain");
|
|
10137
|
+
}
|
|
10138
|
+
const highestVoucherAmount = nativeAmount(row.highestVoucherAmount);
|
|
10139
|
+
const settledOnChain = nativeAmount(row.settledOnChain);
|
|
10140
|
+
const spent = nativeAmount(row.spent);
|
|
10141
|
+
if (BigInt(settledOnChain) > BigInt(highestVoucherAmount) || BigInt(spent) > BigInt(highestVoucherAmount)) {
|
|
10142
|
+
throw new Error("Hosted Tempo settlement source has inconsistent amounts");
|
|
10143
|
+
}
|
|
10144
|
+
const original = operation === "settle" ? {
|
|
10145
|
+
highestVoucherAmount,
|
|
10146
|
+
settledOnChain,
|
|
10147
|
+
spent
|
|
10148
|
+
} : { drainId, highestVoucherAmount, settledOnChain, spent };
|
|
10149
|
+
return this.insert(
|
|
10150
|
+
tx,
|
|
10151
|
+
identity,
|
|
10152
|
+
"tempo",
|
|
10153
|
+
operation,
|
|
10154
|
+
config.network,
|
|
10155
|
+
config.escrow,
|
|
10156
|
+
channelId.toLowerCase(),
|
|
10157
|
+
original,
|
|
10158
|
+
operation === "close" ? [drainId] : [highestVoucherAmount]
|
|
10159
|
+
);
|
|
10160
|
+
}
|
|
10087
10161
|
async findPendingTempo(channelId, operation) {
|
|
10088
10162
|
const result = await this.pool.query(
|
|
10089
10163
|
`SELECT * FROM sdk_hosted_settlement_report_obligations
|
|
@@ -10096,14 +10170,33 @@ var HostedSettlementReportObligations = class {
|
|
|
10096
10170
|
/** The original close obligation reserved for this drain, while its report is outstanding. */
|
|
10097
10171
|
async pendingTempoClose(channelId, drainId) {
|
|
10098
10172
|
const result = await this.pool.query(
|
|
10099
|
-
`SELECT id,tx_hash,created_at_ms FROM sdk_hosted_settlement_report_obligations
|
|
10173
|
+
`SELECT id,state,tx_hash,created_at_ms,close_search_before FROM sdk_hosted_settlement_report_obligations
|
|
10100
10174
|
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
|
|
10101
10175
|
AND original->>'drainId'=$4 AND state NOT IN ('delivered','no_report')
|
|
10102
10176
|
ORDER BY created_at_ms DESC LIMIT 1`,
|
|
10103
10177
|
[this.dvmId, this.scope, channelId.toLowerCase(), drainId]
|
|
10104
10178
|
);
|
|
10105
10179
|
const row = result.rows[0];
|
|
10106
|
-
return row && {
|
|
10180
|
+
return row && {
|
|
10181
|
+
id: row.id,
|
|
10182
|
+
state: row.state,
|
|
10183
|
+
txHash: row.tx_hash,
|
|
10184
|
+
reservedAtMs: Number(row.created_at_ms),
|
|
10185
|
+
searchBefore: searchCursor(row.close_search_before)
|
|
10186
|
+
};
|
|
10187
|
+
}
|
|
10188
|
+
/**
|
|
10189
|
+
* Where the next search for this close obligation's transaction resumes. It
|
|
10190
|
+
* lives on the obligation, not the drain's close intent, because releasing
|
|
10191
|
+
* the drain deletes the intent while the obligation still needs the search.
|
|
10192
|
+
*/
|
|
10193
|
+
async recordTempoCloseSearch(id, resumeAt) {
|
|
10194
|
+
await this.pool.query(
|
|
10195
|
+
`UPDATE sdk_hosted_settlement_report_obligations
|
|
10196
|
+
SET close_search_before = LEAST(COALESCE(close_search_before, $4), $4)
|
|
10197
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'`,
|
|
10198
|
+
[id, this.dvmId, this.scope, resumeAt.toString()]
|
|
10199
|
+
);
|
|
10107
10200
|
}
|
|
10108
10201
|
async submittedIds(rail, operation, txHash, batchId) {
|
|
10109
10202
|
if (!hex(txHash, 32)) throw new Error("Hosted effect has no exact transaction hash");
|
|
@@ -10211,17 +10304,68 @@ var HostedSettlementReportObligations = class {
|
|
|
10211
10304
|
if (!hex(txHash, 32)) throw new Error("Hosted settlement has no transaction hash");
|
|
10212
10305
|
const write = this.x402SettlementStore ? this.x402Transaction.bind(this) : (operation) => transaction(this.pool, operation);
|
|
10213
10306
|
await write(async (tx) => {
|
|
10214
|
-
|
|
10307
|
+
let row = await this.lockRow(tx, id);
|
|
10308
|
+
if (row.state === "no_report" && !row.tx_hash) {
|
|
10309
|
+
row = await this.successorOfRetiredClose(tx, row);
|
|
10310
|
+
}
|
|
10215
10311
|
if (row.tx_hash && row.tx_hash.toLowerCase() !== txHash.toLowerCase()) {
|
|
10216
10312
|
throw new Error("Hosted settlement transaction replay conflicts");
|
|
10217
10313
|
}
|
|
10218
10314
|
await tx.query(
|
|
10219
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,
|
|
10220
10317
|
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
10221
|
-
[id, txHash.toLowerCase()]
|
|
10318
|
+
[row.id, txHash.toLowerCase()]
|
|
10222
10319
|
);
|
|
10223
10320
|
});
|
|
10224
10321
|
}
|
|
10322
|
+
/**
|
|
10323
|
+
* Close reservations that never recorded a transaction, oldest first. Each
|
|
10324
|
+
* one owes a payout report if its close lands. Exhausted attempts remain
|
|
10325
|
+
* observable for late landings; unmatched searches require an explicit retry.
|
|
10326
|
+
*/
|
|
10327
|
+
async unsubmittedTempoCloses(limit = 100) {
|
|
10328
|
+
const result = await this.pool.query(
|
|
10329
|
+
`SELECT id,channel_id,original->>'drainId' AS drain_id,created_at_ms,close_search_before
|
|
10330
|
+
FROM sdk_hosted_settlement_report_obligations
|
|
10331
|
+
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close'
|
|
10332
|
+
AND state IN ('reserved','exhausted') AND tx_hash IS NULL
|
|
10333
|
+
ORDER BY created_at_ms,id LIMIT $3`,
|
|
10334
|
+
[this.dvmId, this.scope, limit]
|
|
10335
|
+
);
|
|
10336
|
+
return result.rows.flatMap((row) => row.drain_id === null ? [] : [{
|
|
10337
|
+
id: row.id,
|
|
10338
|
+
channelId: row.channel_id,
|
|
10339
|
+
drainId: row.drain_id,
|
|
10340
|
+
reservedAtMs: Number(row.created_at_ms),
|
|
10341
|
+
searchBefore: searchCursor(row.close_search_before)
|
|
10342
|
+
}]);
|
|
10343
|
+
}
|
|
10344
|
+
/**
|
|
10345
|
+
* A retired close reservation's attempt can still be running when its
|
|
10346
|
+
* successor is reserved, and its close can land. The close is the same
|
|
10347
|
+
* channel's, so it belongs to the drain's outstanding obligation; writing it
|
|
10348
|
+
* onto the retired row would leave that obligation without a transaction and
|
|
10349
|
+
* the payout unreported.
|
|
10350
|
+
*/
|
|
10351
|
+
async successorOfRetiredClose(tx, retired) {
|
|
10352
|
+
const drainId = retired.original.drainId;
|
|
10353
|
+
if (retired.rail !== "tempo" || retired.operation !== "close" || typeof drainId !== "string") {
|
|
10354
|
+
throw new Error("Hosted settlement obligation was resolved without a transaction");
|
|
10355
|
+
}
|
|
10356
|
+
const successor = await tx.query(
|
|
10357
|
+
`SELECT id FROM sdk_hosted_settlement_report_obligations
|
|
10358
|
+
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
|
|
10359
|
+
AND original->>'drainId'=$4 AND state NOT IN ('delivered','no_report')
|
|
10360
|
+
ORDER BY created_at_ms DESC LIMIT 1`,
|
|
10361
|
+
[this.dvmId, this.scope, retired.channel_id, drainId]
|
|
10362
|
+
);
|
|
10363
|
+
if (!successor.rows[0]) {
|
|
10364
|
+
throw new Error("Hosted Tempo close was retired and the drain has no outstanding obligation");
|
|
10365
|
+
}
|
|
10366
|
+
const locked = await this.lockRow(tx, successor.rows[0].id);
|
|
10367
|
+
return locked.state === "no_report" && !locked.tx_hash ? this.successorOfRetiredClose(tx, locked) : locked;
|
|
10368
|
+
}
|
|
10225
10369
|
async recoveryCandidates() {
|
|
10226
10370
|
const result = await this.pool.query(
|
|
10227
10371
|
`SELECT * FROM sdk_hosted_settlement_report_obligations
|
|
@@ -10683,6 +10827,191 @@ var HostedSettlementReportObligations = class {
|
|
|
10683
10827
|
return { id: row.id, replayed: true };
|
|
10684
10828
|
}
|
|
10685
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
|
+
}
|
|
10846
|
+
function searchCursor(value) {
|
|
10847
|
+
return value === null ? null : BigInt(value);
|
|
10848
|
+
}
|
|
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
|
+
};
|
|
10686
11015
|
|
|
10687
11016
|
// src/sdk/server/hosting-channel-evidence.ts
|
|
10688
11017
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -18168,7 +18497,7 @@ async function requireWedgedTempoDrain(c, opts, args, admitBooked) {
|
|
|
18168
18497
|
)
|
|
18169
18498
|
};
|
|
18170
18499
|
}
|
|
18171
|
-
if (record.status !== "pending" && !(admitBooked && record.status === "sent")) {
|
|
18500
|
+
if (record.status !== "pending" && !(admitBooked && (record.status === "sent" || record.status === "exit_required"))) {
|
|
18172
18501
|
return { response: tempoNotWedgedRefusal(c, args, record.status) };
|
|
18173
18502
|
}
|
|
18174
18503
|
return { record, channelId: record.channelId };
|
|
@@ -19584,7 +19913,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
19584
19913
|
}
|
|
19585
19914
|
}
|
|
19586
19915
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
19587
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
19916
|
+
const { RevenueReporter } = await import("./revenue-reporter-4EAVPWXS.js");
|
|
19588
19917
|
const reporter = new RevenueReporter(
|
|
19589
19918
|
opts.db,
|
|
19590
19919
|
platformUrl,
|
|
@@ -19594,7 +19923,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
19594
19923
|
revenueReporter = reporter;
|
|
19595
19924
|
await reporter.init();
|
|
19596
19925
|
reporter.startRetryLoop();
|
|
19597
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
19926
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-TZX2S7KG.js");
|
|
19598
19927
|
payoutReporter = new PayoutReporter({
|
|
19599
19928
|
dvmId,
|
|
19600
19929
|
db: opts.db,
|
|
@@ -19895,7 +20224,43 @@ var DVMServer = class {
|
|
|
19895
20224
|
dvmId: this.receiptDvmId()
|
|
19896
20225
|
}) : void 0;
|
|
19897
20226
|
this.creditLedger = opts.creditLedger ?? new MemoryCreditLedger();
|
|
19898
|
-
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
|
+
});
|
|
19899
20264
|
this.processedPayments = opts.processedPayments ?? new MemoryProcessedPaymentStore();
|
|
19900
20265
|
this.jobManager = new JobManager(descriptor, {
|
|
19901
20266
|
receiptIssuer: this.receiptIssuer,
|
|
@@ -20051,6 +20416,9 @@ var DVMServer = class {
|
|
|
20051
20416
|
if (!certificateOnly && await control.close(command, support) === "conflict") {
|
|
20052
20417
|
return c.json({ error: "hosting_closure_conflict" }, 409);
|
|
20053
20418
|
}
|
|
20419
|
+
await this.opts.mpp?.reconcileTempoSessionCloses?.().catch(() => {
|
|
20420
|
+
diagnosticConsole.warn({ level: "tempo_routine_sweep_failed", dvm_id: this.opts.dvmId, error: void 0 });
|
|
20421
|
+
});
|
|
20054
20422
|
const evidence = await control.certificate({ ...command, action: "certificate" }, support);
|
|
20055
20423
|
if (!evidence) return c.json({ error: "hosting_closure_stale" }, 409);
|
|
20056
20424
|
return c.json(evidence);
|
|
@@ -23095,6 +23463,8 @@ export {
|
|
|
23095
23463
|
X402ExactSettlementServer,
|
|
23096
23464
|
X402ExactSettlementEvidenceMissingError,
|
|
23097
23465
|
HostedSettlementReportObligations,
|
|
23466
|
+
retryHostedTempoCloseReport,
|
|
23467
|
+
writeOffHostedTempoCloseReport,
|
|
23098
23468
|
signHostingControl,
|
|
23099
23469
|
signHostingClosure,
|
|
23100
23470
|
jobCredentialGate,
|