@dvmkit/sdk 0.8.1 → 0.10.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-2XQLL6N6.js} +1 -1
- package/dist/{chunk-UVS7MRJX.js → chunk-3Q6FTZDB.js} +2 -2
- package/dist/{chunk-OP7VCGQR.js → chunk-3R7NQA3Z.js} +1 -1
- package/dist/{chunk-RSNC5T6C.js → chunk-AJ6UHXUA.js} +1 -1
- package/dist/{chunk-ALWVJ2RB.js → chunk-EWZFANHJ.js} +3 -3
- package/dist/{chunk-J5J63WDC.js → chunk-FFXXGCVG.js} +3 -3
- package/dist/{chunk-UDJNZ3UG.js → chunk-JNECYURT.js} +1 -1
- package/dist/{chunk-IHQUBATH.js → chunk-KFCEXCZM.js} +3 -3
- package/dist/{chunk-NA2KOR6G.js → chunk-LDIPVPKI.js} +3 -3
- package/dist/{chunk-IOQIXRGM.js → chunk-M3T5JYDJ.js} +15 -8
- package/dist/{chunk-UWGBI5WK.js → chunk-MB6R4XPZ.js} +3 -3
- package/dist/{chunk-7PM4R4GD.js → chunk-MMGSYMIC.js} +1 -1
- package/dist/{chunk-QKE5NDPF.js → chunk-MU3IE434.js} +348 -80
- package/dist/{chunk-BFGQG4WY.js → chunk-QGNXYVBO.js} +52 -12
- package/dist/{chunk-JIZ4ZIXX.js → chunk-QNKK7SPY.js} +652 -245
- package/dist/{chunk-FM6QAGJC.js → chunk-STJBBVNL.js} +24 -8
- package/dist/{chunk-NU5RWJ5B.js → chunk-VMHYGKWT.js} +3 -3
- package/dist/{chunk-AVV5SYGO.js → chunk-VTHYQCDB.js} +1 -1
- package/dist/{chunk-ZEPJF4JC.js → chunk-VUGEEMLT.js} +4 -4
- package/dist/{chunk-2U6LAASN.js → chunk-W3VGCNG6.js} +1 -1
- package/dist/{chunk-G3MPHW5I.js → chunk-Y6DO5WSO.js} +3 -3
- package/dist/{chunk-AGE5XZKE.js → chunk-YOSZIWC3.js} +17 -4
- package/dist/{content-retention-B7ddtJ2y.d.ts → content-retention-CrILCNxk.d.ts} +1 -1
- package/dist/{credit-ledger-KIQIE6I6.js → credit-ledger-4PZNAMDT.js} +4 -4
- package/dist/{credit-menu-BH3g9GQa.d.ts → credit-menu-VwX_6KM7.d.ts} +4 -4
- package/dist/{fx-DN26_J7o.d.ts → fx-BM5h4o5S.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/internal/caller.d.ts +7 -7
- 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-92v8e1LQ.d.ts} +1 -1
- package/dist/{lightning-backend-S8F8nf8q.d.ts → lightning-backend-9oky_wJi.d.ts} +1 -1
- package/dist/{memory-credit-ledger-ATYFAYBG.js → memory-credit-ledger-RP7DT5IY.js} +5 -5
- package/dist/{mpp-setup-ZTORW3VF.js → mpp-setup-FURD45LI.js} +5 -3
- package/dist/{payout-reporter-NN6K2IVQ.js → payout-reporter-HVJ6WUF3.js} +2 -2
- package/dist/{postgres-consumed-credential-store-XJTE5PCA.js → postgres-consumed-credential-store-WO6OCFXZ.js} +3 -3
- package/dist/{postgres-job-store-AR6ZJUQV.js → postgres-job-store-FSRUPYH2.js} +2 -2
- package/dist/{postgres-replay-store-33K33QV7.js → postgres-replay-store-XXJSA457.js} +2 -2
- package/dist/{pricing-G5IN5YJE.js → pricing-X4XU5HXY.js} +3 -3
- package/dist/{revenue-reporter-KDUZLD5F.js → revenue-reporter-EV3K5L6B.js} +2 -2
- package/dist/server/index.d.ts +78 -11
- package/dist/server/index.js +87 -45
- package/dist/{step-cache-CR12EZ1Q.d.ts → step-cache--7yA5Ihr.d.ts} +99 -12
- package/dist/{tempo-charge-store-XEGJYM3G.js → tempo-charge-store-5JOIYUIH.js} +2 -2
- package/dist/{tempo-session-store-3WTDOO7H.js → tempo-session-store-S6QMLQUA.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-C7lvLvLk.d.ts} +1 -1
- package/dist/{x402-HQ5GRQZU.js → x402-EN5LSOVM.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-FFXXGCVG.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-VTHYQCDB.js";
|
|
23
18
|
import {
|
|
24
19
|
MemoryCreditLedger
|
|
25
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-YOSZIWC3.js";
|
|
26
21
|
import {
|
|
27
22
|
PostgresJobStore
|
|
28
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-Y6DO5WSO.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-2XQLL6N6.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-KFCEXCZM.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-VUGEEMLT.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-3R7NQA3Z.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-W3VGCNG6.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-STJBBVNL.js";
|
|
145
|
+
import {
|
|
146
|
+
withSdkInitLock
|
|
147
|
+
} from "./chunk-COYKJRMM.js";
|
|
156
148
|
import {
|
|
157
149
|
withPostgresRecoveryContext
|
|
158
|
-
} from "./chunk-
|
|
150
|
+
} from "./chunk-MMGSYMIC.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-MU3IE434.js";
|
|
167
166
|
import {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
loggers,
|
|
168
|
+
pinDimension,
|
|
169
|
+
withSpan,
|
|
170
|
+
withTraceContext
|
|
171
|
+
} from "./chunk-M3T5JYDJ.js";
|
|
172
172
|
import {
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
createDiagnosticCapabilityAuthority,
|
|
174
|
+
diagnosticConsole
|
|
175
|
+
} from "./chunk-QGNXYVBO.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-EN5LSOVM.js");
|
|
3887
3888
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
3888
3889
|
x402Payment,
|
|
3889
3890
|
x402Config,
|
|
@@ -4192,7 +4193,18 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
4192
4193
|
replayRealm,
|
|
4193
4194
|
cred.challenge.id,
|
|
4194
4195
|
CONSUMED_CREDENTIAL_TTL_MS / 1e3
|
|
4195
|
-
))
|
|
4196
|
+
)).catch((error) => {
|
|
4197
|
+
const transactionHash = tempoReceiptTransactionHash(tempoReceipt);
|
|
4198
|
+
if (!transactionHash) throw error;
|
|
4199
|
+
diagnosticConsole.error({
|
|
4200
|
+
level: "error",
|
|
4201
|
+
event: "tempo_replay_mark_failed",
|
|
4202
|
+
phase: "upfront",
|
|
4203
|
+
rail: "tempo",
|
|
4204
|
+
transaction_hash: transactionHash,
|
|
4205
|
+
...paymentFailureFields(error)
|
|
4206
|
+
});
|
|
4207
|
+
});
|
|
4196
4208
|
}
|
|
4197
4209
|
if (ledgerCtx) {
|
|
4198
4210
|
try {
|
|
@@ -4896,7 +4908,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
|
|
|
4896
4908
|
snapshot
|
|
4897
4909
|
});
|
|
4898
4910
|
}
|
|
4899
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4911
|
+
const { verifyX402Payment } = await import("./x402-EN5LSOVM.js");
|
|
4900
4912
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
4901
4913
|
body.content.x402_payment,
|
|
4902
4914
|
opts.x402Config,
|
|
@@ -5327,7 +5339,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
|
|
|
5327
5339
|
try {
|
|
5328
5340
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5329
5341
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5330
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5342
|
+
const { verifyX402Payment } = await import("./x402-EN5LSOVM.js");
|
|
5331
5343
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
5332
5344
|
body.content.x402_payment,
|
|
5333
5345
|
opts.x402Config,
|
|
@@ -6953,6 +6965,10 @@ async function completeTempoChannelDrain(args) {
|
|
|
6953
6965
|
tx_hash: args.reference,
|
|
6954
6966
|
channel_id: args.channelId,
|
|
6955
6967
|
...settlementEvidence,
|
|
6968
|
+
...args.finalization && {
|
|
6969
|
+
terminal_outcome: args.finalization.outcome,
|
|
6970
|
+
credit_unbacked: args.finalization.creditUnbacked
|
|
6971
|
+
},
|
|
6956
6972
|
...args.repaired ? { repaired: true, repair_kind: "tempo_close_reconciled" } : {}
|
|
6957
6973
|
},
|
|
6958
6974
|
tx: innerTx
|
|
@@ -7231,14 +7247,14 @@ async function handleDrain(c, deps, args) {
|
|
|
7231
7247
|
404
|
|
7232
7248
|
);
|
|
7233
7249
|
}
|
|
7234
|
-
|
|
7250
|
+
let existing = await deps.ledger.getDrain({ creditId, drainId: drain.drain_id });
|
|
7251
|
+
if (credit.status === "unbacked" && existing?.status !== "sent") {
|
|
7235
7252
|
const rejected2 = await burnNonce(c, deps, args.envelope);
|
|
7236
7253
|
if (rejected2) return rejected2;
|
|
7237
7254
|
return jsonPaymentError("credit_unbacked", 409, {
|
|
7238
7255
|
message: `Credit ${creditId} is terminal because its Tempo backing finalized before the DVM was protected.`
|
|
7239
7256
|
});
|
|
7240
7257
|
}
|
|
7241
|
-
let existing = await deps.ledger.getDrain({ creditId, drainId: drain.drain_id });
|
|
7242
7258
|
if (existing) {
|
|
7243
7259
|
const tempoDiscovery = existing.method === "tempo" && existing.status === "pending" && drain.method === "tempo" && !!credit.tempoChannelId && !c.req.header("Authorization");
|
|
7244
7260
|
if (!tempoDiscovery) {
|
|
@@ -7254,6 +7270,17 @@ async function handleDrain(c, deps, args) {
|
|
|
7254
7270
|
const released = await releaseTempoDrain(c, deps, existing, credit.tempoChannelId);
|
|
7255
7271
|
return respondDrain(c, deps, { record: released, credit });
|
|
7256
7272
|
}
|
|
7273
|
+
if (existing.status === "exit_required") {
|
|
7274
|
+
const mpp = (await deps.railOpts(c)).mpp;
|
|
7275
|
+
if (mpp && credit.tempoChannelId) {
|
|
7276
|
+
const recovered = await recoverTempoDrain(deps, mpp, existing, credit.tempoChannelId);
|
|
7277
|
+
if (recovered) {
|
|
7278
|
+
if (isHttpResponse(recovered)) return recovered;
|
|
7279
|
+
return respondDrain(c, deps, { record: recovered, credit });
|
|
7280
|
+
}
|
|
7281
|
+
}
|
|
7282
|
+
return tempoExitRequiredResponse(existing);
|
|
7283
|
+
}
|
|
7257
7284
|
if (existing.method === "tempo" && existing.status === "pending" && credit.tempoChannelId) {
|
|
7258
7285
|
if (credit.balanceMicro !== credit.availableMicro) {
|
|
7259
7286
|
const released = await releaseTempoDrain(c, deps, existing, credit.tempoChannelId);
|
|
@@ -7491,7 +7518,8 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7491
7518
|
deps.hostedSettlementReports,
|
|
7492
7519
|
rail.mpp,
|
|
7493
7520
|
record,
|
|
7494
|
-
expectedChannelId
|
|
7521
|
+
expectedChannelId,
|
|
7522
|
+
deps.ledger
|
|
7495
7523
|
);
|
|
7496
7524
|
if (!rail.mpp?.permitHostedTempoClose) {
|
|
7497
7525
|
throw new Error("Hosted Tempo close credential guard is unavailable");
|
|
@@ -7521,13 +7549,21 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7521
7549
|
}
|
|
7522
7550
|
const wrongChannel = tempoWrongChannelRefusal(error, record);
|
|
7523
7551
|
if (wrongChannel) return wrongChannel;
|
|
7524
|
-
if (error instanceof HostedTempoCloseAttemptsExhaustedError) {
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7552
|
+
if (error instanceof HostedTempoCloseAttemptsExhaustedError || error instanceof TempoCloseDescriptorError) {
|
|
7553
|
+
const result = deps.ledger.requireTempoDrainExit ? await deps.ledger.requireTempoDrainExit({ creditId: record.creditId, drainId: record.drainId }) : { drain: record, replayed: false };
|
|
7554
|
+
if (!result.replayed && error instanceof TempoCloseDescriptorError) {
|
|
7555
|
+
loggers.tempo.warn("tempo_session_close_descriptor_refused", {
|
|
7556
|
+
credit_id: record.creditId,
|
|
7557
|
+
drain_id: record.drainId,
|
|
7558
|
+
channel_id: expectedChannelId,
|
|
7559
|
+
amount_at_stake_native: record.drainedNative?.toString() ?? null,
|
|
7560
|
+
amount_at_stake_micro: record.amountMicro,
|
|
7561
|
+
currency: record.currency,
|
|
7562
|
+
error: error.message
|
|
7563
|
+
});
|
|
7564
|
+
}
|
|
7565
|
+
if (result.drain.status === "sent") return respondDrain(c, deps, { record: result.drain, credit });
|
|
7566
|
+
return tempoExitRequiredResponse(result.drain);
|
|
7531
7567
|
}
|
|
7532
7568
|
const refused = tempoFloorRefusal(error, record);
|
|
7533
7569
|
if (refused) return refused;
|
|
@@ -7547,13 +7583,8 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7547
7583
|
try {
|
|
7548
7584
|
const state = await rail.mpp.getTempoSessionDrainState?.(expectedChannelId);
|
|
7549
7585
|
if (state?.finalized || state?.deposit === 0n) {
|
|
7550
|
-
const recovered2 = await rail.mpp
|
|
7551
|
-
|
|
7552
|
-
record.drainId
|
|
7553
|
-
);
|
|
7554
|
-
if (recovered2) {
|
|
7555
|
-
return await markTempoDrainSent(deps, rail.mpp, record, expectedChannelId, recovered2);
|
|
7556
|
-
}
|
|
7586
|
+
const recovered2 = await recoverTempoDrain(deps, rail.mpp, record, expectedChannelId);
|
|
7587
|
+
if (recovered2) return recovered2;
|
|
7557
7588
|
}
|
|
7558
7589
|
if (state && (state.closeRequestedAt !== 0n || isDefinitiveTempoCloseFailure(error))) {
|
|
7559
7590
|
return await releaseTempoDrain(c, deps, record, expectedChannelId);
|
|
@@ -7580,13 +7611,15 @@ var HOSTED_TEMPO_CLOSE_MAX_RELEASES = 3;
|
|
|
7580
7611
|
var HOSTED_TEMPO_CLOSE_IN_FLIGHT = "Hosted Tempo close already has an original effect attempt";
|
|
7581
7612
|
var HostedTempoCloseAttemptsExhaustedError = class extends Error {
|
|
7582
7613
|
};
|
|
7583
|
-
async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
7614
|
+
async function reserveHostedTempoClose(reports, mpp, record, channelId, ledger) {
|
|
7584
7615
|
const pending = await reports.pendingTempoClose(channelId, record.drainId);
|
|
7585
7616
|
if (!pending) {
|
|
7586
7617
|
const reserved = await reports.reserveTempo(channelId, "close", record.drainId);
|
|
7587
7618
|
if (reserved.replayed) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7588
7619
|
return reserved.id;
|
|
7589
7620
|
}
|
|
7621
|
+
if (pending.state === "exhausted") throw new HostedTempoCloseAttemptsExhaustedError();
|
|
7622
|
+
if (pending.state === "unmatched") throw new Error("Hosted Tempo close report requires an operator retry");
|
|
7590
7623
|
if (pending.txHash || !mpp.getTempoSessionDrainState) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7591
7624
|
const state = await mpp.getTempoSessionDrainState(channelId);
|
|
7592
7625
|
if (state.finalized || state.deposit === 0n || state.closeRequestedAt !== 0n) {
|
|
@@ -7594,7 +7627,10 @@ async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
|
7594
7627
|
}
|
|
7595
7628
|
const result = await reports.releaseStaleTempoClose(pending.id, channelId, record.drainId, {
|
|
7596
7629
|
staleAfterMs: HOSTED_TEMPO_CLOSE_STALE_MS,
|
|
7597
|
-
maxReleases: HOSTED_TEMPO_CLOSE_MAX_RELEASES
|
|
7630
|
+
maxReleases: HOSTED_TEMPO_CLOSE_MAX_RELEASES,
|
|
7631
|
+
onExhausted: async (tx) => {
|
|
7632
|
+
await ledger.requireTempoDrainExit?.({ creditId: record.creditId, drainId: record.drainId, tx });
|
|
7633
|
+
}
|
|
7598
7634
|
});
|
|
7599
7635
|
if (result.outcome === "in_flight") throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
|
|
7600
7636
|
if (result.outcome === "exhausted") {
|
|
@@ -7603,7 +7639,8 @@ async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
|
7603
7639
|
drain_id: record.drainId,
|
|
7604
7640
|
channel_id: channelId,
|
|
7605
7641
|
obligation_id: pending.id,
|
|
7606
|
-
attempt: result.releases + 1
|
|
7642
|
+
attempt: result.releases + 1,
|
|
7643
|
+
amount_at_stake_native: await reports.tempoCloseAmountAtStake(pending.id)
|
|
7607
7644
|
});
|
|
7608
7645
|
throw new HostedTempoCloseAttemptsExhaustedError(
|
|
7609
7646
|
`Tempo drain ${record.drainId} exhausted its cooperative close attempts`
|
|
@@ -7618,13 +7655,55 @@ async function reserveHostedTempoClose(reports, mpp, record, channelId) {
|
|
|
7618
7655
|
});
|
|
7619
7656
|
return result.id;
|
|
7620
7657
|
}
|
|
7658
|
+
function tempoExitRequiredResponse(record) {
|
|
7659
|
+
const terminal = record.status === "exit_required";
|
|
7660
|
+
return jsonPaymentError("tempo_channel_exit_required", 409, {
|
|
7661
|
+
message: terminal ? `Tempo drain ${record.drainId} cannot cooperatively close this channel.` : `This Tempo close credential cannot complete drain ${record.drainId}.`,
|
|
7662
|
+
display: "Your funds remain in the channel. Use the timed on-chain exit to recover them.",
|
|
7663
|
+
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.",
|
|
7664
|
+
extra: {
|
|
7665
|
+
drain_registered: true,
|
|
7666
|
+
drain_id: record.drainId,
|
|
7667
|
+
channel_id: record.channelId,
|
|
7668
|
+
status: record.status
|
|
7669
|
+
}
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7621
7672
|
function tempoCloseCredentialNeedsRefresh(error) {
|
|
7622
7673
|
return error instanceof MppxErrors.InvalidChallengeError || error instanceof MppxErrors.PaymentExpiredError;
|
|
7623
7674
|
}
|
|
7675
|
+
function tempoRefundUnprovenResponse(record) {
|
|
7676
|
+
return jsonPaymentError("settlement_pending", 409, {
|
|
7677
|
+
message: "The full Tempo refund could not be proven yet.",
|
|
7678
|
+
display: "Your funds remain held and have not been lost. The DVM operator has been alerted.",
|
|
7679
|
+
hint: `Keep drain ${record.drainId}; no replacement drain is needed.`,
|
|
7680
|
+
extra: { drain_registered: true, drain_id: record.drainId }
|
|
7681
|
+
});
|
|
7682
|
+
}
|
|
7624
7683
|
async function recoverTempoDrain(deps, mpp, record, channelId) {
|
|
7625
7684
|
try {
|
|
7626
|
-
const
|
|
7627
|
-
|
|
7685
|
+
const result = await mpp.recoverTempoSessionClose?.(channelId, record.drainId, record.creditId);
|
|
7686
|
+
const durable = await deps.ledger.getDrain({ creditId: record.creditId, drainId: record.drainId });
|
|
7687
|
+
if (durable?.status === "sent") return durable;
|
|
7688
|
+
if (result && typeof result === "object") {
|
|
7689
|
+
return tempoRefundUnprovenResponse(durable ?? record);
|
|
7690
|
+
}
|
|
7691
|
+
if (!result) return null;
|
|
7692
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(result)) {
|
|
7693
|
+
loggers.tempo.warn("tempo_session_drain_refund_unproven", {
|
|
7694
|
+
channel_id: channelId,
|
|
7695
|
+
credit_id: record.creditId,
|
|
7696
|
+
drain_id: record.drainId,
|
|
7697
|
+
amount_at_stake_native: record.drainedNative?.toString() ?? null,
|
|
7698
|
+
amount_at_stake_micro: record.amountMicro,
|
|
7699
|
+
currency: record.currency,
|
|
7700
|
+
reason: "close_evidence_missing",
|
|
7701
|
+
error: "Recovery returned no proven refund"
|
|
7702
|
+
});
|
|
7703
|
+
return tempoRefundUnprovenResponse(durable ?? record);
|
|
7704
|
+
}
|
|
7705
|
+
if (durable?.status === "exit_required") return null;
|
|
7706
|
+
return await markTempoDrainSent(deps, mpp, durable ?? record, channelId, result);
|
|
7628
7707
|
} catch (error) {
|
|
7629
7708
|
const refused = tempoFloorRefusal(error, record);
|
|
7630
7709
|
if (refused) return refused;
|
|
@@ -7634,7 +7713,9 @@ async function recoverTempoDrain(deps, mpp, record, channelId) {
|
|
|
7634
7713
|
error: error instanceof Error ? error.message : String(error)
|
|
7635
7714
|
});
|
|
7636
7715
|
return jsonPaymentError("payment_invalid", 402, {
|
|
7637
|
-
message: "The Tempo session close is still being reconciled; retry this drain."
|
|
7716
|
+
message: "The Tempo session close is still being reconciled; retry this drain.",
|
|
7717
|
+
hint: `Retry this same drain ${record.drainId} after the service can read and persist the close evidence.`,
|
|
7718
|
+
retryable: true
|
|
7638
7719
|
});
|
|
7639
7720
|
}
|
|
7640
7721
|
}
|
|
@@ -8109,6 +8190,11 @@ function drainCopy(record) {
|
|
|
8109
8190
|
display: "The refund was sent to the payout destination you provided.",
|
|
8110
8191
|
hint: "See `sent` for the settlement reference (e.g. the on-chain transaction hash)."
|
|
8111
8192
|
};
|
|
8193
|
+
case "exit_required":
|
|
8194
|
+
return {
|
|
8195
|
+
display: "This drain cannot cooperatively close the channel. Your funds remain in the channel.",
|
|
8196
|
+
hint: "Use the channel's timed on-chain exit. A corrected credential does not reopen this drain."
|
|
8197
|
+
};
|
|
8112
8198
|
case "released":
|
|
8113
8199
|
return {
|
|
8114
8200
|
display: "The channel could not complete this refund, so the service restored the amount to your prepaid balance.",
|
|
@@ -9586,172 +9672,6 @@ function safeError(err) {
|
|
|
9586
9672
|
return "settlement_transport_error";
|
|
9587
9673
|
}
|
|
9588
9674
|
|
|
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
9675
|
// src/sdk/server/hosting-settlement-report-obligations.ts
|
|
9756
9676
|
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
9757
9677
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
@@ -9762,6 +9682,16 @@ import { createPublicClient as createPublicClient4, decodeEventLog, decodeFuncti
|
|
|
9762
9682
|
var HOSTED_SETTLEMENT_UNRESOLVED_SQL = `state NOT IN ('delivered','no_report')
|
|
9763
9683
|
OR (rail='x402' AND operation IN ('claim','refund')
|
|
9764
9684
|
AND COALESCE(original->>'authorizationCallData','') !~* '^0x([0-9a-f]{2})+$')`;
|
|
9685
|
+
var TEMPO_CLOSE_RETRYABLE_SQL = `target.rail='tempo' AND target.operation='close'
|
|
9686
|
+
AND target.state IN ('unmatched','no_report') AND target.tx_hash IS NULL
|
|
9687
|
+
AND NOT EXISTS (
|
|
9688
|
+
SELECT 1 FROM sdk_hosted_settlement_report_obligations other
|
|
9689
|
+
WHERE other.dvm_id=target.dvm_id AND other.scope=target.scope
|
|
9690
|
+
AND other.rail='tempo' AND other.operation='close' AND other.channel_id=target.channel_id
|
|
9691
|
+
AND other.id<>target.id AND (other.state<>'no_report' OR other.tx_hash IS NOT NULL)
|
|
9692
|
+
)`;
|
|
9693
|
+
var TEMPO_CLOSE_WRITE_OFF_ALLOWED_SQL = `target.rail='tempo' AND target.operation='close'
|
|
9694
|
+
AND target.state='unmatched' AND target.tx_hash IS NULL`;
|
|
9765
9695
|
var X402_SETTLED_EVENT = parseAbiItem2(
|
|
9766
9696
|
"event Settled(address indexed receiver,address indexed token,address indexed sender,uint128 amount)"
|
|
9767
9697
|
);
|
|
@@ -9975,14 +9905,13 @@ var HostedSettlementReportObligations = class {
|
|
|
9975
9905
|
operation TEXT NOT NULL CHECK (operation IN ('claim','refund','settle','close')),
|
|
9976
9906
|
network TEXT NOT NULL, contract TEXT NOT NULL, channel_id TEXT NOT NULL,
|
|
9977
9907
|
original JSONB NOT NULL,
|
|
9978
|
-
state TEXT NOT NULL CHECK (state IN ('reserved','observed','report_queued','delivered','no_report')),
|
|
9908
|
+
state TEXT NOT NULL CHECK (state IN ('reserved','observed','report_queued','delivered','no_report','exhausted','unmatched')),
|
|
9909
|
+
close_search_before BIGINT, close_write_off_note TEXT, close_written_off_at_ms BIGINT,
|
|
9979
9910
|
tx_hash TEXT, block_number BIGINT, block_hash TEXT, delta_native NUMERIC(78,0),
|
|
9980
9911
|
payout_id TEXT, report_id TEXT, report_payload JSONB, delivered_at_ms BIGINT,
|
|
9981
9912
|
created_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
|
|
9982
9913
|
updated_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
9983
9914
|
);
|
|
9984
|
-
ALTER TABLE sdk_hosted_settlement_report_obligations
|
|
9985
|
-
ADD COLUMN IF NOT EXISTS close_search_before BIGINT;
|
|
9986
9915
|
CREATE INDEX IF NOT EXISTS sdk_hosted_settlement_report_pending
|
|
9987
9916
|
ON sdk_hosted_settlement_report_obligations (dvm_id,scope,state)
|
|
9988
9917
|
WHERE state NOT IN ('delivered','no_report');
|
|
@@ -9990,6 +9919,29 @@ var HostedSettlementReportObligations = class {
|
|
|
9990
9919
|
ON sdk_hosted_settlement_report_obligations (report_id)
|
|
9991
9920
|
WHERE report_id IS NOT NULL;
|
|
9992
9921
|
`);
|
|
9922
|
+
const columns = await this.pool.query(
|
|
9923
|
+
`SELECT attname FROM pg_attribute
|
|
9924
|
+
WHERE attrelid='sdk_hosted_settlement_report_obligations'::regclass AND NOT attisdropped`
|
|
9925
|
+
);
|
|
9926
|
+
for (const [name, type] of [["close_search_before", "BIGINT"], ["close_write_off_note", "TEXT"], ["close_written_off_at_ms", "BIGINT"]]) {
|
|
9927
|
+
if (!columns.rows.some((column) => column.attname === name)) {
|
|
9928
|
+
await this.pool.query(`ALTER TABLE sdk_hosted_settlement_report_obligations ADD COLUMN ${name} ${type}`);
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
const constraint = await this.pool.query(
|
|
9932
|
+
`SELECT pg_get_constraintdef(oid) AS definition FROM pg_constraint
|
|
9933
|
+
WHERE conrelid='sdk_hosted_settlement_report_obligations'::regclass
|
|
9934
|
+
AND conname='sdk_hosted_settlement_report_obligations_state_check'`
|
|
9935
|
+
);
|
|
9936
|
+
const states = ["reserved", "observed", "report_queued", "delivered", "no_report", "exhausted", "unmatched"];
|
|
9937
|
+
const expected = `CHECK ((state = ANY (ARRAY[${states.map((state) => `'${state}'::text`).join(", ")}])))`;
|
|
9938
|
+
if (constraint.rows[0]?.definition !== expected) {
|
|
9939
|
+
await this.pool.query(`ALTER TABLE sdk_hosted_settlement_report_obligations
|
|
9940
|
+
DROP CONSTRAINT IF EXISTS sdk_hosted_settlement_report_obligations_state_check;
|
|
9941
|
+
ALTER TABLE sdk_hosted_settlement_report_obligations
|
|
9942
|
+
ADD CONSTRAINT sdk_hosted_settlement_report_obligations_state_check
|
|
9943
|
+
CHECK (state IN ('reserved','observed','report_queued','delivered','no_report','exhausted','unmatched'))`);
|
|
9944
|
+
}
|
|
9993
9945
|
await this.pool.query(
|
|
9994
9946
|
`INSERT INTO sdk_hosted_settlement_report_history (singleton,complete)
|
|
9995
9947
|
VALUES (TRUE,$1) ON CONFLICT (singleton) DO NOTHING`,
|
|
@@ -10093,9 +10045,9 @@ var HostedSettlementReportObligations = class {
|
|
|
10093
10045
|
}
|
|
10094
10046
|
/**
|
|
10095
10047
|
* Retire a close reservation that never recorded a transaction and reserve
|
|
10096
|
-
* the drain's next attempt
|
|
10097
|
-
*
|
|
10098
|
-
*
|
|
10048
|
+
* the drain's next attempt in one transaction. At the retry cap the final
|
|
10049
|
+
* row becomes terminal `exhausted` and remains claimable by a late landing.
|
|
10050
|
+
* A close from the retired attempt that lands late is the same channel's close: `recordSubmitted` on the retired row
|
|
10099
10051
|
* writes it onto the successor, and recovery attaches it there if that
|
|
10100
10052
|
* attempt never got to write, so it is reported once.
|
|
10101
10053
|
*
|
|
@@ -10122,7 +10074,15 @@ var HostedSettlementReportObligations = class {
|
|
|
10122
10074
|
AND original->>'drainId'=$4 AND state='no_report' AND tx_hash IS NULL`,
|
|
10123
10075
|
[this.dvmId, this.scope, channelId.toLowerCase(), drainId]
|
|
10124
10076
|
)).rows[0].n);
|
|
10125
|
-
if (released >= limits.maxReleases)
|
|
10077
|
+
if (released >= limits.maxReleases) {
|
|
10078
|
+
await tx.query(
|
|
10079
|
+
`UPDATE sdk_hosted_settlement_report_obligations SET state='exhausted',
|
|
10080
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
10081
|
+
[id]
|
|
10082
|
+
);
|
|
10083
|
+
await limits.onExhausted?.(tx);
|
|
10084
|
+
return { outcome: "exhausted", releases: released };
|
|
10085
|
+
}
|
|
10126
10086
|
await tx.query(
|
|
10127
10087
|
`UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
|
|
10128
10088
|
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
@@ -10141,11 +10101,126 @@ var HostedSettlementReportObligations = class {
|
|
|
10141
10101
|
`UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
|
|
10142
10102
|
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10143
10103
|
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
|
|
10144
|
-
AND state
|
|
10104
|
+
AND state IN ('reserved','exhausted') AND tx_hash IS NULL`,
|
|
10105
|
+
[id, this.dvmId, this.scope]
|
|
10106
|
+
);
|
|
10107
|
+
return result.rowCount === 1;
|
|
10108
|
+
}
|
|
10109
|
+
async tempoCloseAmountAtStake(id) {
|
|
10110
|
+
const row = await this.lockRow(this.pool, id);
|
|
10111
|
+
if (row.rail !== "tempo" || row.operation !== "close") throw new Error("Not a Tempo close obligation");
|
|
10112
|
+
return (BigInt(String(row.original.highestVoucherAmount)) - BigInt(String(row.original.settledOnChain))).toString();
|
|
10113
|
+
}
|
|
10114
|
+
/** Stop automatic searches without declaring a possibly missing payout discharged. */
|
|
10115
|
+
async markTempoCloseUnmatched(id) {
|
|
10116
|
+
const result = await this.pool.query(
|
|
10117
|
+
`UPDATE sdk_hosted_settlement_report_obligations SET state='unmatched',
|
|
10118
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10119
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
|
|
10120
|
+
AND state IN ('reserved','exhausted') AND tx_hash IS NULL`,
|
|
10121
|
+
[id, this.dvmId, this.scope]
|
|
10122
|
+
);
|
|
10123
|
+
return result.rowCount === 1;
|
|
10124
|
+
}
|
|
10125
|
+
async listTempoCloseReports(args) {
|
|
10126
|
+
const result = await this.pool.query(
|
|
10127
|
+
`WITH candidates AS (
|
|
10128
|
+
SELECT target.*, (${TEMPO_CLOSE_RETRYABLE_SQL}) AS retryable,
|
|
10129
|
+
(${TEMPO_CLOSE_WRITE_OFF_ALLOWED_SQL}) AS write_off_allowed
|
|
10130
|
+
FROM sdk_hosted_settlement_report_obligations target
|
|
10131
|
+
WHERE target.dvm_id=$1 AND target.scope=$2 AND target.rail='tempo' AND target.operation='close'
|
|
10132
|
+
AND target.state IN ('unmatched','no_report')
|
|
10133
|
+
)
|
|
10134
|
+
SELECT * FROM candidates
|
|
10135
|
+
WHERE ($6::boolean OR (state='unmatched' AND (retryable OR write_off_allowed)))
|
|
10136
|
+
AND ($4::bigint IS NULL OR (created_at_ms,id)>($4,$5))
|
|
10137
|
+
ORDER BY created_at_ms,id LIMIT $3`,
|
|
10138
|
+
[
|
|
10139
|
+
this.dvmId,
|
|
10140
|
+
this.scope,
|
|
10141
|
+
args.limit,
|
|
10142
|
+
args.after?.createdAtMs ?? null,
|
|
10143
|
+
args.after?.obligationId ?? null,
|
|
10144
|
+
args.all ?? false
|
|
10145
|
+
]
|
|
10146
|
+
);
|
|
10147
|
+
return result.rows.map((row) => ({
|
|
10148
|
+
id: row.id,
|
|
10149
|
+
channelId: row.channel_id,
|
|
10150
|
+
amountNative: tempoCloseAmount(row.original),
|
|
10151
|
+
status: row.state,
|
|
10152
|
+
createdAtMs: Number(row.created_at_ms),
|
|
10153
|
+
updatedAtMs: Number(row.updated_at_ms),
|
|
10154
|
+
deliveredAtMs: row.delivered_at_ms === null ? null : Number(row.delivered_at_ms),
|
|
10155
|
+
retryable: row.retryable,
|
|
10156
|
+
writeOffAllowed: row.write_off_allowed,
|
|
10157
|
+
writtenOff: row.close_write_off_note !== null,
|
|
10158
|
+
writeOffNote: row.close_write_off_note,
|
|
10159
|
+
writtenOffAtMs: row.close_written_off_at_ms === null ? null : Number(row.close_written_off_at_ms)
|
|
10160
|
+
}));
|
|
10161
|
+
}
|
|
10162
|
+
async hasTempoCloseReport(id) {
|
|
10163
|
+
const result = await this.pool.query(
|
|
10164
|
+
`SELECT id FROM sdk_hosted_settlement_report_obligations
|
|
10165
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'`,
|
|
10145
10166
|
[id, this.dvmId, this.scope]
|
|
10146
10167
|
);
|
|
10147
10168
|
return result.rowCount === 1;
|
|
10148
10169
|
}
|
|
10170
|
+
async writeOffTempoClose(id, note) {
|
|
10171
|
+
const result = await this.pool.query(
|
|
10172
|
+
`UPDATE sdk_hosted_settlement_report_obligations target
|
|
10173
|
+
SET state='no_report', close_write_off_note=$4,
|
|
10174
|
+
close_written_off_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
|
|
10175
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10176
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND ${TEMPO_CLOSE_WRITE_OFF_ALLOWED_SQL}
|
|
10177
|
+
RETURNING channel_id,original`,
|
|
10178
|
+
[id, this.dvmId, this.scope, note]
|
|
10179
|
+
);
|
|
10180
|
+
const row = result.rows[0];
|
|
10181
|
+
if (!row) return void 0;
|
|
10182
|
+
const written = { channelId: row.channel_id, amount: tempoCloseAmount(row.original) };
|
|
10183
|
+
loggers.tempo.warn("tempo_session_close_report_written_off", {
|
|
10184
|
+
channel_id: written.channelId,
|
|
10185
|
+
obligation_id: id,
|
|
10186
|
+
amount_forgone_native: written.amount
|
|
10187
|
+
});
|
|
10188
|
+
return written;
|
|
10189
|
+
}
|
|
10190
|
+
async reopenTempoClose(id) {
|
|
10191
|
+
const reopened = await transaction(this.pool, async (tx) => {
|
|
10192
|
+
const owned = await tx.query(
|
|
10193
|
+
"SELECT id FROM sdk_hosted_settlement_report_obligations WHERE id=$1 AND dvm_id=$2 AND scope=$3",
|
|
10194
|
+
[id, this.dvmId, this.scope]
|
|
10195
|
+
);
|
|
10196
|
+
if (!owned.rowCount) return void 0;
|
|
10197
|
+
await this.lockIdentity(tx);
|
|
10198
|
+
const prior = await tx.query(
|
|
10199
|
+
`SELECT channel_id,state,close_write_off_note,original FROM sdk_hosted_settlement_report_obligations
|
|
10200
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 FOR UPDATE`,
|
|
10201
|
+
[id, this.dvmId, this.scope]
|
|
10202
|
+
);
|
|
10203
|
+
const row = prior.rows[0];
|
|
10204
|
+
if (!row) return void 0;
|
|
10205
|
+
const result = await tx.query(
|
|
10206
|
+
`UPDATE sdk_hosted_settlement_report_obligations target
|
|
10207
|
+
SET state='reserved', close_search_before=NULL,
|
|
10208
|
+
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
10209
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND ${TEMPO_CLOSE_RETRYABLE_SQL}`,
|
|
10210
|
+
[id, this.dvmId, this.scope]
|
|
10211
|
+
);
|
|
10212
|
+
return result.rowCount === 1 ? row : void 0;
|
|
10213
|
+
});
|
|
10214
|
+
if (!reopened) return false;
|
|
10215
|
+
loggers.tempo.info("tempo_session_close_report_reopened", {
|
|
10216
|
+
obligation_id: id,
|
|
10217
|
+
channel_id: reopened.channel_id,
|
|
10218
|
+
prior_status: reopened.state,
|
|
10219
|
+
written_off: reopened.close_write_off_note !== null,
|
|
10220
|
+
amount_native: tempoCloseAmount(reopened.original)
|
|
10221
|
+
});
|
|
10222
|
+
return true;
|
|
10223
|
+
}
|
|
10149
10224
|
async reserveTempoIn(tx, channelId, operation, drainId) {
|
|
10150
10225
|
const config = this.tempo;
|
|
10151
10226
|
if (!config || !hex(channelId, 32)) throw new Error("Hosted Tempo settlement configuration is unavailable");
|
|
@@ -10204,7 +10279,7 @@ var HostedSettlementReportObligations = class {
|
|
|
10204
10279
|
/** The original close obligation reserved for this drain, while its report is outstanding. */
|
|
10205
10280
|
async pendingTempoClose(channelId, drainId) {
|
|
10206
10281
|
const result = await this.pool.query(
|
|
10207
|
-
`SELECT id,tx_hash,created_at_ms,close_search_before FROM sdk_hosted_settlement_report_obligations
|
|
10282
|
+
`SELECT id,state,tx_hash,created_at_ms,close_search_before FROM sdk_hosted_settlement_report_obligations
|
|
10208
10283
|
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
|
|
10209
10284
|
AND original->>'drainId'=$4 AND state NOT IN ('delivered','no_report')
|
|
10210
10285
|
ORDER BY created_at_ms DESC LIMIT 1`,
|
|
@@ -10213,11 +10288,23 @@ var HostedSettlementReportObligations = class {
|
|
|
10213
10288
|
const row = result.rows[0];
|
|
10214
10289
|
return row && {
|
|
10215
10290
|
id: row.id,
|
|
10291
|
+
state: row.state,
|
|
10216
10292
|
txHash: row.tx_hash,
|
|
10217
10293
|
reservedAtMs: Number(row.created_at_ms),
|
|
10218
10294
|
searchBefore: searchCursor(row.close_search_before)
|
|
10219
10295
|
};
|
|
10220
10296
|
}
|
|
10297
|
+
/** A recorded close transaction remains refund evidence after its report is delivered. */
|
|
10298
|
+
async tempoCloseTransaction(channelId, drainId) {
|
|
10299
|
+
const result = await this.pool.query(
|
|
10300
|
+
`SELECT tx_hash FROM sdk_hosted_settlement_report_obligations
|
|
10301
|
+
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
|
|
10302
|
+
AND original->>'drainId'=$4 AND tx_hash IS NOT NULL
|
|
10303
|
+
ORDER BY created_at_ms DESC LIMIT 1`,
|
|
10304
|
+
[this.dvmId, this.scope, channelId.toLowerCase(), drainId]
|
|
10305
|
+
);
|
|
10306
|
+
return result.rows[0]?.tx_hash;
|
|
10307
|
+
}
|
|
10221
10308
|
/**
|
|
10222
10309
|
* Where the next search for this close obligation's transaction resumes. It
|
|
10223
10310
|
* lives on the obligation, not the drain's close intent, because releasing
|
|
@@ -10346,6 +10433,7 @@ var HostedSettlementReportObligations = class {
|
|
|
10346
10433
|
}
|
|
10347
10434
|
await tx.query(
|
|
10348
10435
|
`UPDATE sdk_hosted_settlement_report_obligations SET tx_hash=COALESCE(tx_hash,$2),
|
|
10436
|
+
state=CASE WHEN state IN ('exhausted','unmatched') THEN 'reserved' ELSE state END,
|
|
10349
10437
|
updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
|
|
10350
10438
|
[row.id, txHash.toLowerCase()]
|
|
10351
10439
|
);
|
|
@@ -10353,15 +10441,15 @@ var HostedSettlementReportObligations = class {
|
|
|
10353
10441
|
}
|
|
10354
10442
|
/**
|
|
10355
10443
|
* Close reservations that never recorded a transaction, oldest first. Each
|
|
10356
|
-
* one owes a payout report
|
|
10357
|
-
*
|
|
10444
|
+
* one owes a payout report if its close lands. Exhausted attempts remain
|
|
10445
|
+
* observable for late landings; unmatched searches require an explicit retry.
|
|
10358
10446
|
*/
|
|
10359
10447
|
async unsubmittedTempoCloses(limit = 100) {
|
|
10360
10448
|
const result = await this.pool.query(
|
|
10361
10449
|
`SELECT id,channel_id,original->>'drainId' AS drain_id,created_at_ms,close_search_before
|
|
10362
10450
|
FROM sdk_hosted_settlement_report_obligations
|
|
10363
10451
|
WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close'
|
|
10364
|
-
AND state
|
|
10452
|
+
AND state IN ('reserved','exhausted') AND tx_hash IS NULL
|
|
10365
10453
|
ORDER BY created_at_ms,id LIMIT $3`,
|
|
10366
10454
|
[this.dvmId, this.scope, limit]
|
|
10367
10455
|
);
|
|
@@ -10859,10 +10947,190 @@ var HostedSettlementReportObligations = class {
|
|
|
10859
10947
|
return { id: row.id, replayed: true };
|
|
10860
10948
|
}
|
|
10861
10949
|
};
|
|
10950
|
+
async function retryHostedTempoCloseReport(args) {
|
|
10951
|
+
const obligations = new HostedSettlementReportObligations(args.db, args.dvmId, args.scope);
|
|
10952
|
+
return obligations.reopenTempoClose(args.obligationId);
|
|
10953
|
+
}
|
|
10954
|
+
async function writeOffHostedTempoCloseReport(args) {
|
|
10955
|
+
const note = args.note.trim();
|
|
10956
|
+
if (!note || note.length > 2e3) throw new Error("A write-off requires a note of 1 to 2000 characters");
|
|
10957
|
+
const row = await new HostedSettlementReportObligations(args.db, args.dvmId, args.scope).writeOffTempoClose(args.obligationId, note);
|
|
10958
|
+
if (!row) return false;
|
|
10959
|
+
return args.includeDetails ? { channelId: row.channelId, amountForgoneNative: row.amount } : true;
|
|
10960
|
+
}
|
|
10961
|
+
function tempoCloseAmount(original) {
|
|
10962
|
+
return (BigInt(String(original.highestVoucherAmount)) - BigInt(String(original.settledOnChain))).toString();
|
|
10963
|
+
}
|
|
10862
10964
|
function searchCursor(value) {
|
|
10863
10965
|
return value === null ? null : BigInt(value);
|
|
10864
10966
|
}
|
|
10865
10967
|
|
|
10968
|
+
// src/sdk/server/hosting-control.ts
|
|
10969
|
+
import { createHmac, randomUUID as randomUUID4, timingSafeEqual } from "crypto";
|
|
10970
|
+
|
|
10971
|
+
// src/sdk/server/hosting-cleanup-fence.ts
|
|
10972
|
+
var HostingCleanupFence = class {
|
|
10973
|
+
constructor(pool, dvmId, scope) {
|
|
10974
|
+
this.pool = pool;
|
|
10975
|
+
this.dvmId = dvmId;
|
|
10976
|
+
this.scope = scope;
|
|
10977
|
+
}
|
|
10978
|
+
pool;
|
|
10979
|
+
dvmId;
|
|
10980
|
+
scope;
|
|
10981
|
+
async assertNewIntent(tx, intent) {
|
|
10982
|
+
const continuation = intent?.admissionId || intent?.paymentSeq !== void 0 || intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash || intent?.creditId && intent.fundId && intent.callerPubkey;
|
|
10983
|
+
const { rows: rows2 } = await tx.query(
|
|
10984
|
+
`SELECT state, closed_at_ms, closed_at_us FROM sdk_hosting_control
|
|
10985
|
+
WHERE dvm_id=$1 AND scope=$2 ${continuation ? "" : "FOR UPDATE"}`,
|
|
10986
|
+
[this.dvmId, this.scope]
|
|
10987
|
+
);
|
|
10988
|
+
const control = rows2[0];
|
|
10989
|
+
if (!control) throw new Error("Hosted obligation fence is unavailable");
|
|
10990
|
+
if (control.state === "closed" && (control.closed_at_ms === null || control.closed_at_us === null)) {
|
|
10991
|
+
throw new Error("Hosted closure cutoff is unavailable");
|
|
10992
|
+
}
|
|
10993
|
+
if (control.state !== "closed" && !continuation) return;
|
|
10994
|
+
if (intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash) {
|
|
10995
|
+
const repair = !!intent.invoiceRepairPaymentHash;
|
|
10996
|
+
const invoice = await tx.query(
|
|
10997
|
+
`SELECT i.payment_hash FROM credit_invoices i
|
|
10998
|
+
LEFT JOIN sdk_hosting_admissions a ON a.id=i.hosting_admission_id
|
|
10999
|
+
WHERE i.payment_hash=$1 AND i.caller_pubkey=$4 AND i.amount_micro=$5
|
|
11000
|
+
AND (($10::boolean AND $3=i.payment_hash AND i.status IN ('blocked','written_off'))
|
|
11001
|
+
OR (NOT $10::boolean AND i.credit_id=$2 AND i.fund_id=$3 AND i.status='pending'))
|
|
11002
|
+
AND ((i.hosting_admission_id IS NULL AND
|
|
11003
|
+
($9::bigint IS NULL OR i.created_at_db_us <= $9)) OR
|
|
11004
|
+
(a.dvm_id=$6 AND a.scope=$7 AND a.credit_id=i.credit_id
|
|
11005
|
+
AND a.fund_id=i.fund_id AND a.caller_pubkey=i.caller_pubkey
|
|
11006
|
+
AND a.outcome IN ('pending','accepted','unresolved')
|
|
11007
|
+
AND ($8::bigint IS NULL OR a.admitted_at_ms <= $8)))`,
|
|
11008
|
+
[
|
|
11009
|
+
intent.invoicePaymentHash ?? intent.invoiceRepairPaymentHash,
|
|
11010
|
+
intent.creditId,
|
|
11011
|
+
intent.fundId,
|
|
11012
|
+
intent.callerPubkey,
|
|
11013
|
+
intent.amountMicro,
|
|
11014
|
+
this.dvmId,
|
|
11015
|
+
this.scope,
|
|
11016
|
+
control.closed_at_ms,
|
|
11017
|
+
control.closed_at_us,
|
|
11018
|
+
repair
|
|
11019
|
+
]
|
|
11020
|
+
);
|
|
11021
|
+
if (invoice.rows[0]) return;
|
|
11022
|
+
throw new Error("Hosted invoice has no original payable admission");
|
|
11023
|
+
}
|
|
11024
|
+
if (intent?.paymentSeq !== void 0 && intent.jobId) {
|
|
11025
|
+
const payment = await tx.query(
|
|
11026
|
+
`SELECT m.job_id FROM job_messages m JOIN jobs j ON j.id=m.job_id
|
|
11027
|
+
WHERE m.job_id=$1 AND m.seq=$2 AND m.status='pending-verification'
|
|
11028
|
+
AND m.created_at_ms IS NOT NULL
|
|
11029
|
+
AND ($3::bigint IS NULL OR m.created_at_ms <= $3) AND j.dvm_id=$4
|
|
11030
|
+
AND ($6::text IS NULL OR m.data->'content'->'tempo_credential'->'challenge'->>'id'=$6)
|
|
11031
|
+
AND EXISTS (SELECT 1 FROM sdk_hosting_admissions a
|
|
11032
|
+
WHERE a.dvm_id=$4 AND a.scope=$5 AND a.job_id=m.job_id AND a.outcome <> 'failed')`,
|
|
11033
|
+
[
|
|
11034
|
+
intent.jobId,
|
|
11035
|
+
intent.paymentSeq,
|
|
11036
|
+
control.closed_at_ms,
|
|
11037
|
+
this.dvmId,
|
|
11038
|
+
this.scope,
|
|
11039
|
+
intent.challengeId ?? null
|
|
11040
|
+
]
|
|
11041
|
+
);
|
|
11042
|
+
if (payment.rows[0]) return;
|
|
11043
|
+
throw new Error("Hosted payment has no pre-closure inbound intent");
|
|
11044
|
+
}
|
|
11045
|
+
if (!intent?.admissionId && !intent?.jobId && !(intent?.creditId && intent.fundId && intent.callerPubkey) || !intent?.jobId && !(intent?.creditId && intent.fundId)) {
|
|
11046
|
+
throw new Error("Hosted obligations are closed to new intents");
|
|
11047
|
+
}
|
|
11048
|
+
const accepted = await tx.query(
|
|
11049
|
+
`SELECT id FROM sdk_hosting_admissions
|
|
11050
|
+
WHERE ($1::text IS NULL OR id=$1) AND dvm_id=$2 AND scope=$3
|
|
11051
|
+
AND ($4::bigint IS NULL OR admitted_at_ms <= $4)
|
|
11052
|
+
AND (outcome IN ('pending', 'unresolved') OR ($11::boolean AND outcome='accepted'))
|
|
11053
|
+
AND ($5::text IS NULL OR job_id=$5)
|
|
11054
|
+
AND ($6::text IS NULL OR credit_id=$6)
|
|
11055
|
+
AND ($7::text IS NULL OR fund_id=$7)
|
|
11056
|
+
AND ($8::text IS NULL OR caller_pubkey=$8)
|
|
11057
|
+
AND ($9::text IS NULL OR funding_commitment=$9)
|
|
11058
|
+
AND ($10::text IS NULL OR payment_artifact_hash=$10)`,
|
|
11059
|
+
[
|
|
11060
|
+
intent.admissionId ?? null,
|
|
11061
|
+
this.dvmId,
|
|
11062
|
+
this.scope,
|
|
11063
|
+
control.closed_at_ms,
|
|
11064
|
+
intent.jobId ?? null,
|
|
11065
|
+
intent.creditId ?? null,
|
|
11066
|
+
intent.fundId ?? null,
|
|
11067
|
+
intent.callerPubkey ?? null,
|
|
11068
|
+
intent.fundingCommitment ?? null,
|
|
11069
|
+
intent.paymentArtifactHash ?? null,
|
|
11070
|
+
control.state !== "closed"
|
|
11071
|
+
]
|
|
11072
|
+
);
|
|
11073
|
+
if (!accepted.rows[0]) throw new Error("Hosted obligation has no pre-closure admission");
|
|
11074
|
+
}
|
|
11075
|
+
async withNewIntent(intent, operation) {
|
|
11076
|
+
return this.withNewIntentOn(this.pool, intent, operation);
|
|
11077
|
+
}
|
|
11078
|
+
async assertFundingReservation(tx, reservation) {
|
|
11079
|
+
await this.assertNewIntent(tx, {
|
|
11080
|
+
admissionId: reservation.admissionId,
|
|
11081
|
+
creditId: reservation.creditId,
|
|
11082
|
+
fundId: reservation.fundId,
|
|
11083
|
+
callerPubkey: reservation.callerPubkey,
|
|
11084
|
+
fundingCommitment: reservation.commitment,
|
|
11085
|
+
paymentArtifactHash: reservation.paymentArtifactHash
|
|
11086
|
+
});
|
|
11087
|
+
const verified = await tx.query(
|
|
11088
|
+
`SELECT id FROM sdk_hosting_admissions
|
|
11089
|
+
WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND credit_id=$4 AND fund_id=$5
|
|
11090
|
+
AND caller_pubkey=$6 AND funding_commitment=$7 AND payment_artifact_hash=$8
|
|
11091
|
+
AND outcome IN ('pending','accepted','unresolved')`,
|
|
11092
|
+
[
|
|
11093
|
+
reservation.admissionId,
|
|
11094
|
+
this.dvmId,
|
|
11095
|
+
this.scope,
|
|
11096
|
+
reservation.creditId,
|
|
11097
|
+
reservation.fundId,
|
|
11098
|
+
reservation.callerPubkey,
|
|
11099
|
+
reservation.commitment,
|
|
11100
|
+
reservation.paymentArtifactHash
|
|
11101
|
+
]
|
|
11102
|
+
);
|
|
11103
|
+
if (!verified.rows[0]) throw new Error("Hosted funding has no authentic admission");
|
|
11104
|
+
}
|
|
11105
|
+
async fundingAdmission(tx, creditId, fundId, callerPubkey) {
|
|
11106
|
+
await this.assertNewIntent(tx, { creditId, fundId, callerPubkey });
|
|
11107
|
+
const { rows: rows2 } = await tx.query(
|
|
11108
|
+
`SELECT id FROM sdk_hosting_admissions
|
|
11109
|
+
WHERE dvm_id=$1 AND scope=$2 AND credit_id=$3 AND fund_id=$4
|
|
11110
|
+
AND caller_pubkey=$5 AND outcome IN ('pending','unresolved')
|
|
11111
|
+
ORDER BY admitted_at_ms,id LIMIT 2`,
|
|
11112
|
+
[this.dvmId, this.scope, creditId, fundId, callerPubkey]
|
|
11113
|
+
);
|
|
11114
|
+
if (rows2.length !== 1) throw new Error("Hosted invoice funding admission is missing or ambiguous");
|
|
11115
|
+
return rows2[0].id;
|
|
11116
|
+
}
|
|
11117
|
+
async withNewIntentOn(db, intent, operation) {
|
|
11118
|
+
const tx = await db.connect();
|
|
11119
|
+
try {
|
|
11120
|
+
await tx.query("BEGIN");
|
|
11121
|
+
await this.assertNewIntent(tx, intent);
|
|
11122
|
+
const result = await operation(tx);
|
|
11123
|
+
await tx.query("COMMIT");
|
|
11124
|
+
return result;
|
|
11125
|
+
} catch (error) {
|
|
11126
|
+
await tx.query("ROLLBACK").catch(() => void 0);
|
|
11127
|
+
throw error;
|
|
11128
|
+
} finally {
|
|
11129
|
+
tx.release();
|
|
11130
|
+
}
|
|
11131
|
+
}
|
|
11132
|
+
};
|
|
11133
|
+
|
|
10866
11134
|
// src/sdk/server/hosting-channel-evidence.ts
|
|
10867
11135
|
import { createHash as createHash7 } from "crypto";
|
|
10868
11136
|
import { Session as TempoSession2 } from "mppx/tempo";
|
|
@@ -12024,7 +12292,7 @@ function jobCredentialGate(opts) {
|
|
|
12024
12292
|
// to a human verbatim, so the conditional advice lives in `hint`, which
|
|
12025
12293
|
// is what the agent acts on.
|
|
12026
12294
|
display: "Couldn't retrieve your result \u2014 the request was missing its job credential.",
|
|
12027
|
-
hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/dvm-cli@
|
|
12295
|
+
hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/dvm-cli@latest, then retry the same command with --endpoint <url> --job-token <hex> (or set DVM_JOB_ENDPOINT and DVM_JOB_TOKEN).`
|
|
12028
12296
|
};
|
|
12029
12297
|
}
|
|
12030
12298
|
};
|
|
@@ -16883,6 +17151,9 @@ function installAdminCreditRoutes(app, opts) {
|
|
|
16883
17151
|
"/admin/credit/reconcile-x402-exact-settlement",
|
|
16884
17152
|
(c) => handleReconcileX402Exact(c, opts)
|
|
16885
17153
|
);
|
|
17154
|
+
app.get("/admin/credit/tempo-close-reports", (c) => handleTempoCloseReports(c, opts));
|
|
17155
|
+
app.post("/admin/credit/retry-tempo-close-report", (c) => handleTempoCloseReportAction(c, opts, "retry"));
|
|
17156
|
+
app.post("/admin/credit/write-off-tempo-close-report", (c) => handleTempoCloseReportAction(c, opts, "write-off"));
|
|
16886
17157
|
app.get("/admin/credit/tempo-drains", (c) => handleTempoDrains(c, opts));
|
|
16887
17158
|
app.get("/admin/credit/tempo-losses", (c) => handleTempoLosses(c, opts));
|
|
16888
17159
|
app.post("/admin/credit/reconcile-tempo-drain", (c) => handleReconcileTempo(c, opts));
|
|
@@ -17858,6 +18129,101 @@ async function handleWriteOffX402(c, opts) {
|
|
|
17858
18129
|
replayed: !outcome.written
|
|
17859
18130
|
});
|
|
17860
18131
|
}
|
|
18132
|
+
function tempoCloseReportsUnavailable(c) {
|
|
18133
|
+
return c.json({
|
|
18134
|
+
error: "tempo_close_report_unavailable",
|
|
18135
|
+
message: "Hosted settlement reports are not configured on this DVM."
|
|
18136
|
+
}, 503);
|
|
18137
|
+
}
|
|
18138
|
+
async function handleTempoCloseReports(c, opts) {
|
|
18139
|
+
const auth = await requireAuth(c, opts);
|
|
18140
|
+
if ("response" in auth) return auth.response;
|
|
18141
|
+
if (!opts.hostedSettlementReports) return tempoCloseReportsUnavailable(c);
|
|
18142
|
+
const limit = parseTempoLimit(c.req.query("limit"));
|
|
18143
|
+
if (limit === void 0) {
|
|
18144
|
+
return c.json({ error: "invalid_query", message: "limit must be an integer in 1..50." }, 400);
|
|
18145
|
+
}
|
|
18146
|
+
let after;
|
|
18147
|
+
const rawAfter = c.req.query("after");
|
|
18148
|
+
if (rawAfter !== void 0) {
|
|
18149
|
+
try {
|
|
18150
|
+
const value = JSON.parse(Buffer.from(rawAfter, "base64url").toString("utf8"));
|
|
18151
|
+
if (!Array.isArray(value) || value.length !== 2 || !Number.isSafeInteger(value[0]) || value[0] < 0 || typeof value[1] !== "string" || !value[1]) throw new Error("invalid cursor");
|
|
18152
|
+
after = { createdAtMs: value[0], obligationId: value[1] };
|
|
18153
|
+
} catch {
|
|
18154
|
+
return c.json({ error: "invalid_query", message: "after must be a next_after value from a previous response." }, 400);
|
|
18155
|
+
}
|
|
18156
|
+
}
|
|
18157
|
+
const all = c.req.query("all");
|
|
18158
|
+
if (all !== void 0 && all !== "0" && all !== "1") {
|
|
18159
|
+
return c.json({ error: "invalid_query", message: "all must be 0 or 1." }, 400);
|
|
18160
|
+
}
|
|
18161
|
+
const rows2 = await opts.hostedSettlementReports.store.listTempoCloseReports({ limit, after, all: all === "1" });
|
|
18162
|
+
const last = rows2.length === limit ? rows2.at(-1) : void 0;
|
|
18163
|
+
const response = {
|
|
18164
|
+
reports: rows2.map((row) => ({
|
|
18165
|
+
obligation_id: row.id,
|
|
18166
|
+
channel_id: row.channelId,
|
|
18167
|
+
amount_native: row.amountNative,
|
|
18168
|
+
status: row.status,
|
|
18169
|
+
created_at_ms: row.createdAtMs,
|
|
18170
|
+
updated_at_ms: row.updatedAtMs,
|
|
18171
|
+
delivered_at_ms: row.deliveredAtMs,
|
|
18172
|
+
retryable: row.retryable,
|
|
18173
|
+
write_off_allowed: row.writeOffAllowed,
|
|
18174
|
+
written_off: row.writtenOff,
|
|
18175
|
+
write_off_note: row.writeOffNote,
|
|
18176
|
+
written_off_at_ms: row.writtenOffAtMs
|
|
18177
|
+
})),
|
|
18178
|
+
...last ? { next_after: Buffer.from(JSON.stringify([last.createdAtMs, last.id])).toString("base64url") } : {}
|
|
18179
|
+
};
|
|
18180
|
+
return c.json(response);
|
|
18181
|
+
}
|
|
18182
|
+
async function handleTempoCloseReportAction(c, opts, action) {
|
|
18183
|
+
const auth = await requireAuth(c, opts);
|
|
18184
|
+
if ("response" in auth) return auth.response;
|
|
18185
|
+
const reports = opts.hostedSettlementReports;
|
|
18186
|
+
if (!reports) return tempoCloseReportsUnavailable(c);
|
|
18187
|
+
const body = await c.req.json().catch(() => null);
|
|
18188
|
+
if (!body || typeof body.obligation_id !== "string" || !body.obligation_id.trim()) {
|
|
18189
|
+
return c.json({ error: "invalid_request", message: "obligation_id must be a non-empty string." }, 400);
|
|
18190
|
+
}
|
|
18191
|
+
const obligationId = body.obligation_id.trim();
|
|
18192
|
+
if (action === "retry") {
|
|
18193
|
+
if (await reports.store.reopenTempoClose(obligationId)) {
|
|
18194
|
+
const response = { status: "reserved", obligation_id: obligationId };
|
|
18195
|
+
return c.json(response);
|
|
18196
|
+
}
|
|
18197
|
+
} else {
|
|
18198
|
+
if (typeof body.note !== "string" || !body.note.trim() || body.note.trim().length > 2e3) {
|
|
18199
|
+
return c.json({ error: "invalid_request", message: "note must contain 1 to 2000 characters after trimming." }, 400);
|
|
18200
|
+
}
|
|
18201
|
+
const note = body.note.trim();
|
|
18202
|
+
const result = await reports.store.writeOffTempoClose(obligationId, note);
|
|
18203
|
+
if (result) {
|
|
18204
|
+
const response = {
|
|
18205
|
+
status: "written_off",
|
|
18206
|
+
obligation_id: obligationId,
|
|
18207
|
+
channel_id: result.channelId,
|
|
18208
|
+
amount_forgone_native: result.amount,
|
|
18209
|
+
note
|
|
18210
|
+
};
|
|
18211
|
+
return c.json(response);
|
|
18212
|
+
}
|
|
18213
|
+
}
|
|
18214
|
+
if (!await reports.store.hasTempoCloseReport(obligationId)) {
|
|
18215
|
+
return c.json({
|
|
18216
|
+
error: "tempo_close_report_not_found",
|
|
18217
|
+
message: "Close-report obligation not found.",
|
|
18218
|
+
obligation_id: obligationId
|
|
18219
|
+
}, 404);
|
|
18220
|
+
}
|
|
18221
|
+
return c.json({
|
|
18222
|
+
error: "tempo_close_report_conflict",
|
|
18223
|
+
message: action === "retry" ? "Retry requires an unsubmitted unmatched or no_report close obligation without a competing report." : "Write-off requires an unsubmitted unmatched close obligation.",
|
|
18224
|
+
obligation_id: obligationId
|
|
18225
|
+
}, 409);
|
|
18226
|
+
}
|
|
17861
18227
|
async function handleTempoDrains(c, opts) {
|
|
17862
18228
|
const auth = await requireAuth(c, opts);
|
|
17863
18229
|
if ("response" in auth) return auth.response;
|
|
@@ -18347,7 +18713,7 @@ async function requireWedgedTempoDrain(c, opts, args, admitBooked) {
|
|
|
18347
18713
|
)
|
|
18348
18714
|
};
|
|
18349
18715
|
}
|
|
18350
|
-
if (record.status !== "pending" && !(admitBooked && record.status === "sent")) {
|
|
18716
|
+
if (record.status !== "pending" && !(admitBooked && (record.status === "sent" || record.status === "exit_required"))) {
|
|
18351
18717
|
return { response: tempoNotWedgedRefusal(c, args, record.status) };
|
|
18352
18718
|
}
|
|
18353
18719
|
return { record, channelId: record.channelId };
|
|
@@ -19763,7 +20129,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
19763
20129
|
}
|
|
19764
20130
|
}
|
|
19765
20131
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
19766
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
20132
|
+
const { RevenueReporter } = await import("./revenue-reporter-EV3K5L6B.js");
|
|
19767
20133
|
const reporter = new RevenueReporter(
|
|
19768
20134
|
opts.db,
|
|
19769
20135
|
platformUrl,
|
|
@@ -19773,7 +20139,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
19773
20139
|
revenueReporter = reporter;
|
|
19774
20140
|
await reporter.init();
|
|
19775
20141
|
reporter.startRetryLoop();
|
|
19776
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
20142
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-HVJ6WUF3.js");
|
|
19777
20143
|
payoutReporter = new PayoutReporter({
|
|
19778
20144
|
dvmId,
|
|
19779
20145
|
db: opts.db,
|
|
@@ -20074,7 +20440,43 @@ var DVMServer = class {
|
|
|
20074
20440
|
dvmId: this.receiptDvmId()
|
|
20075
20441
|
}) : void 0;
|
|
20076
20442
|
this.creditLedger = opts.creditLedger ?? new MemoryCreditLedger();
|
|
20077
|
-
this.opts.mpp?.useTempoCreditLedger?.(this.creditLedger)
|
|
20443
|
+
this.opts.mpp?.useTempoCreditLedger?.(this.creditLedger, async (drain, evidence) => {
|
|
20444
|
+
const completed = await completeTempoChannelDrain({
|
|
20445
|
+
deps: { ledger: this.creditLedger, drainReport: {
|
|
20446
|
+
db: this.opts.db,
|
|
20447
|
+
dvmId: this.opts.dvmId,
|
|
20448
|
+
enqueueCreditDrain: this.opts.enqueueCreditDrain
|
|
20449
|
+
} },
|
|
20450
|
+
creditId: drain.creditId,
|
|
20451
|
+
drainId: drain.drainId,
|
|
20452
|
+
channelId: drain.channelId,
|
|
20453
|
+
refundedNative: evidence.refundedNative,
|
|
20454
|
+
debitedNative: drain.drainedNative,
|
|
20455
|
+
reference: evidence.reference,
|
|
20456
|
+
finalization: {
|
|
20457
|
+
outcome: evidence.creditUnbacked ? `${evidence.outcome}_unbacked` : evidence.outcome,
|
|
20458
|
+
creditUnbacked: evidence.creditUnbacked
|
|
20459
|
+
}
|
|
20460
|
+
});
|
|
20461
|
+
if (!completed.replayed && this.receiptIssuer) {
|
|
20462
|
+
const receipt = this.receiptIssuer.issueDrain({
|
|
20463
|
+
creditId: drain.creditId,
|
|
20464
|
+
drainId: drain.drainId,
|
|
20465
|
+
event: "sent",
|
|
20466
|
+
method: drain.method,
|
|
20467
|
+
amountMicro: drain.amountMicro,
|
|
20468
|
+
balanceAfterMicro: drain.balanceAfterMicro,
|
|
20469
|
+
ledgerSeq: drain.ledgerSeq,
|
|
20470
|
+
callerPubkey: drain.callerPubkey
|
|
20471
|
+
});
|
|
20472
|
+
await this.creditLedger.appendDrainReceipt({
|
|
20473
|
+
creditId: drain.creditId,
|
|
20474
|
+
drainId: drain.drainId,
|
|
20475
|
+
receipt
|
|
20476
|
+
});
|
|
20477
|
+
}
|
|
20478
|
+
return !completed.replayed;
|
|
20479
|
+
});
|
|
20078
20480
|
this.processedPayments = opts.processedPayments ?? new MemoryProcessedPaymentStore();
|
|
20079
20481
|
this.jobManager = new JobManager(descriptor, {
|
|
20080
20482
|
receiptIssuer: this.receiptIssuer,
|
|
@@ -20404,6 +20806,9 @@ var DVMServer = class {
|
|
|
20404
20806
|
// DVM wired with an in-memory session store resolves to a rail with no
|
|
20405
20807
|
// close-intent read and the verbs answer 503.
|
|
20406
20808
|
tempoDrainRail: () => Promise.resolve(this.opts.mpp),
|
|
20809
|
+
...this.opts.hostedSettlementReports ? {
|
|
20810
|
+
hostedSettlementReports: { store: this.opts.hostedSettlementReports }
|
|
20811
|
+
} : {},
|
|
20407
20812
|
// Health-filtered, and read per request rather than captured: the
|
|
20408
20813
|
// servicer picking a mint to buy a refund at wants the list that
|
|
20409
20814
|
// excludes a mint currently failing its probe.
|
|
@@ -23277,6 +23682,8 @@ export {
|
|
|
23277
23682
|
X402ExactSettlementServer,
|
|
23278
23683
|
X402ExactSettlementEvidenceMissingError,
|
|
23279
23684
|
HostedSettlementReportObligations,
|
|
23685
|
+
retryHostedTempoCloseReport,
|
|
23686
|
+
writeOffHostedTempoCloseReport,
|
|
23280
23687
|
signHostingControl,
|
|
23281
23688
|
signHostingClosure,
|
|
23282
23689
|
jobCredentialGate,
|