@dvmkit/sdk 0.8.1 → 0.9.0

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