@dvmkit/sdk 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{chunk-5K5E6ME6.js → chunk-4VV7WACY.js} +1 -1
  2. package/dist/{chunk-GSPV3UIM.js → chunk-73C4JN2E.js} +632 -262
  3. package/dist/{chunk-2I3OHMTY.js → chunk-BAQRUYRL.js} +3 -3
  4. package/dist/{chunk-ATXDKX6F.js → chunk-GAZBBMAA.js} +1 -1
  5. package/dist/{chunk-QPXI7YPH.js → chunk-GODOH2AF.js} +3 -3
  6. package/dist/{chunk-5CQKSLAU.js → chunk-HC4I7LAV.js} +1 -1
  7. package/dist/{chunk-6FEHRZO7.js → chunk-IBX2ORBV.js} +1 -1
  8. package/dist/{chunk-TFYY5APM.js → chunk-IYQQXEHV.js} +3 -3
  9. package/dist/{chunk-X3JEME6P.js → chunk-K2MSIKMM.js} +3 -3
  10. package/dist/{chunk-55NKZQOC.js → chunk-KGL3YMZA.js} +1 -1
  11. package/dist/{chunk-YPSJU3OH.js → chunk-LCCG5HQM.js} +2 -2
  12. package/dist/{chunk-GB5XPAA5.js → chunk-LFVIZH2G.js} +361 -44
  13. package/dist/{chunk-RTFBVXCN.js → chunk-LW7KZQHG.js} +4 -4
  14. package/dist/{chunk-DZQYQN2X.js → chunk-OIP44KBB.js} +12 -0
  15. package/dist/{chunk-P5WEZXJ2.js → chunk-Q3VYRU5R.js} +60 -3
  16. package/dist/{chunk-BL6LRWXD.js → chunk-QSZW2I5Y.js} +1 -1
  17. package/dist/{chunk-I6PHA54S.js → chunk-TFDR2YP2.js} +3 -3
  18. package/dist/{chunk-IFWUWHTW.js → chunk-TRY7BJA4.js} +1 -1
  19. package/dist/{chunk-PRSKC7AD.js → chunk-WB3E7KE7.js} +3 -3
  20. package/dist/{chunk-XXLUIXPO.js → chunk-WLPLHXNP.js} +1 -1
  21. package/dist/{chunk-VYP6ADCS.js → chunk-YYRY32NR.js} +24 -8
  22. package/dist/{chunk-EBCLWOKF.js → chunk-ZBQKXAYK.js} +17 -4
  23. package/dist/{content-retention-D2K4MKNO.d.ts → content-retention-CVBYehRS.d.ts} +1 -1
  24. package/dist/{credit-ledger-AOU6S34N.js → credit-ledger-CZQ56DYI.js} +4 -4
  25. package/dist/{credit-menu-C-8g2cHT.d.ts → credit-menu-QntEhZJS.d.ts} +4 -4
  26. package/dist/{fx-Ql4nvdbh.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-DCvJQUX0.d.ts → job-store-qOD-FrQF.d.ts} +1 -1
  34. package/dist/{lightning-backend-CY5ML3km.d.ts → lightning-backend-DtdG6UT5.d.ts} +1 -1
  35. package/dist/{memory-credit-ledger-4HWYINKJ.js → memory-credit-ledger-EMN7G3MO.js} +5 -5
  36. package/dist/{mpp-setup-2BKTPEK6.js → mpp-setup-VW53KFPO.js} +5 -3
  37. package/dist/{payout-reporter-HQSIIBFL.js → payout-reporter-TZX2S7KG.js} +2 -2
  38. package/dist/{postgres-consumed-credential-store-R7DNU6CM.js → postgres-consumed-credential-store-V464OAJB.js} +3 -3
  39. package/dist/{postgres-job-store-6VGFL6XE.js → postgres-job-store-27VOCSPO.js} +2 -2
  40. package/dist/{postgres-replay-store-SY26NUCI.js → postgres-replay-store-7GJFAQXY.js} +2 -2
  41. package/dist/{pricing-H2RA4X2R.js → pricing-NNMBNP7N.js} +3 -3
  42. package/dist/{revenue-reporter-X52GPZ5U.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-BOWAn_BH.d.ts → step-cache-B7svmPz3.d.ts} +157 -8
  46. package/dist/{tempo-charge-store-NKXJ2AXE.js → tempo-charge-store-2RPOJBM2.js} +2 -2
  47. package/dist/{tempo-session-store-ZSDQIWSU.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-MAtMp3DB.d.ts → usd-D3izFz71.d.ts} +1 -1
  51. package/dist/{x402-Q4S36NHQ.js → x402-SL36LSOD.js} +4 -4
  52. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
- import {
2
- diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
4
1
  import {
5
2
  withSdkInitLock
6
3
  } from "./chunk-COYKJRMM.js";
4
+ import {
5
+ diagnosticConsole
6
+ } from "./chunk-OIP44KBB.js";
7
7
 
8
8
  // src/sdk/server/postgres-replay-store.ts
9
9
  var DEFAULT_TABLE_NAME = "signed_request_replays";
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-66HGCPBU.js";
7
7
  import {
8
8
  diagnosticConsole
9
- } from "./chunk-DZQYQN2X.js";
9
+ } from "./chunk-OIP44KBB.js";
10
10
 
11
11
  // src/observability/payment-diagnostics.ts
12
12
  import { AsyncLocalStorage } from "async_hooks";
@@ -2,12 +2,12 @@ import {
2
2
  ContentHoldError,
3
3
  applyContentHoldMutation
4
4
  } from "./chunk-AFV77S4C.js";
5
- import {
6
- diagnosticConsole
7
- } from "./chunk-DZQYQN2X.js";
8
5
  import {
9
6
  withSdkInitLock
10
7
  } from "./chunk-COYKJRMM.js";
8
+ import {
9
+ diagnosticConsole
10
+ } from "./chunk-OIP44KBB.js";
11
11
 
12
12
  // src/sdk/server/postgres-job-store.ts
13
13
  import { createHash } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
3
+ } from "./chunk-OIP44KBB.js";
4
4
 
5
5
  // src/sdk/pricing/currency.ts
6
6
  var InvalidCurrencyError = class extends Error {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
3
+ } from "./chunk-OIP44KBB.js";
4
4
 
5
5
  // src/lib/pg-retry.ts
6
6
  import { AsyncLocalStorage } from "async_hooks";
@@ -2,12 +2,12 @@ import {
2
2
  parseStoreValue,
3
3
  stringifyStoreValue
4
4
  } from "./chunk-YG7G4DPZ.js";
5
- import {
6
- diagnosticConsole
7
- } from "./chunk-DZQYQN2X.js";
8
5
  import {
9
6
  withSdkInitLock
10
7
  } from "./chunk-COYKJRMM.js";
8
+ import {
9
+ diagnosticConsole
10
+ } from "./chunk-OIP44KBB.js";
11
11
 
12
12
  // src/sdk/server/tempo-charge-store.ts
13
13
  import { Credential } from "mppx";
@@ -1,9 +1,9 @@
1
- import {
2
- diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
4
1
  import {
5
2
  withSdkInitLock
6
3
  } from "./chunk-COYKJRMM.js";
4
+ import {
5
+ diagnosticConsole
6
+ } from "./chunk-OIP44KBB.js";
7
7
 
8
8
  // src/sdk/server/payout-reporter.ts
9
9
  import { createHash, randomUUID } from "crypto";
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  diagnosticConsole,
6
6
  projectSpanRow
7
- } from "./chunk-DZQYQN2X.js";
7
+ } from "./chunk-OIP44KBB.js";
8
8
 
9
9
  // src/lib/x402/env.ts
10
10
  import { isAddress } from "viem";
@@ -2,7 +2,7 @@ import {
2
2
  createX402FacilitatorAuthHeaders,
3
3
  settleWithFacilitator,
4
4
  verifyWithFacilitator
5
- } from "./chunk-ATXDKX6F.js";
5
+ } from "./chunk-GAZBBMAA.js";
6
6
  import {
7
7
  buildPaymentRequirements,
8
8
  decodePayment,
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-66HGCPBU.js";
16
16
  import {
17
17
  diagnosticConsole
18
- } from "./chunk-DZQYQN2X.js";
18
+ } from "./chunk-OIP44KBB.js";
19
19
 
20
20
  // src/lib/x402/verify.ts
21
21
  async function verifyX402Payment(paymentHeader, config, requiredUsdcMicro, resource, btcUsdRate, exactVersions = { v1: true, v2: true }) {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  loggers
3
- } from "./chunk-BL6LRWXD.js";
3
+ } from "./chunk-QSZW2I5Y.js";
4
4
  import {
5
5
  diagnosticConsole
6
- } from "./chunk-DZQYQN2X.js";
6
+ } from "./chunk-OIP44KBB.js";
7
7
 
8
8
  // src/sdk/server/mpp-setup.ts
9
9
  import { createHash, randomUUID } from "crypto";
@@ -313,6 +313,15 @@ function logStateFailure(_error) {
313
313
  }
314
314
 
315
315
  // src/sdk/server/mpp-setup.ts
316
+ var TempoDrainRefundUnprovenError = class extends Error {
317
+ constructor(reason, message) {
318
+ super(message);
319
+ this.reason = reason;
320
+ }
321
+ reason;
322
+ };
323
+ var TempoCloseDescriptorError = class extends MppxErrors.VerificationFailedError {
324
+ };
316
325
  var MPPX_HMAC_MISMATCH_REASON = "challenge was not issued by this server";
317
326
  var TEMPO_SETTLEMENT_LEASE_FIELD = "settlementLeaseUntil";
318
327
  var TEMPO_SETTLEMENT_LEASE_OWNER_FIELD = "settlementLeaseOwner";
@@ -557,7 +566,7 @@ function buildMppMethods(opts, env) {
557
566
  chainId: opts.tempoSession.chainId ?? chainId,
558
567
  ...opts.tempoSession.getClient && { getClient: opts.tempoSession.getClient },
559
568
  ...opts.tempoSession.onSessionSettlement && {
560
- onSessionSettlement: opts.tempoSession.onSessionSettlement
569
+ onSessionSettlement: claimingCloseSettlement(opts.tempoSession)
561
570
  }
562
571
  })
563
572
  ));
@@ -566,6 +575,29 @@ function buildMppMethods(opts, env) {
566
575
  if (methods.length === 0 || !tempoConfiguration) return void 0;
567
576
  return { methods, tempoConfiguration };
568
577
  }
578
+ function claimingCloseSettlement(session) {
579
+ const callback = session.onSessionSettlement;
580
+ const store = session.store;
581
+ if (session.hostedObligations || !store.getClose || !store.claimCloseReference) return callback;
582
+ return async (context) => {
583
+ if (context.trigger === "close") {
584
+ const intent = await store.getClose(context.channelId);
585
+ if (intent) {
586
+ let claimed = true;
587
+ try {
588
+ claimed = (await store.claimCloseReference(
589
+ context.channelId,
590
+ intent.drainId,
591
+ context.txHash
592
+ )).claimed;
593
+ } catch {
594
+ }
595
+ if (!claimed) return;
596
+ }
597
+ }
598
+ await callback(context);
599
+ };
600
+ }
569
601
  function withAuthenticationOnlyValidation(method) {
570
602
  if (method.validate) return method;
571
603
  const validate = async ({ credential, request }) => ({
@@ -641,37 +673,64 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
641
673
  channelId
642
674
  )
643
675
  ),
644
- findChannelClose: async (client, channelId, escrowContract, notBeforeMs) => {
645
- const { getBlock, getBlockNumber, getLogs } = await import("viem/actions");
646
- const event = TempoSession.Precompile.escrowAbi.find(
647
- (item) => item.type === "event" && item.name === "ChannelClosed"
648
- );
649
- if (!event || event.type !== "event") throw new Error("Tempo escrow ABI has no ChannelClosed event");
650
- let toBlock = await getBlockNumber(client);
676
+ findChannelClose: async (client, channelId, escrowContract, notBeforeMs, startBlock) => {
677
+ const { getBlock, getBlockNumber, getLogs, getTransaction } = await import("viem/actions");
678
+ const eventNamed = (name) => {
679
+ const event = TempoSession.Precompile.escrowAbi.find(
680
+ (item) => item.type === "event" && item.name === name
681
+ );
682
+ if (!event || event.type !== "event") throw new Error(`Tempo escrow ABI has no ${name} event`);
683
+ return event;
684
+ };
685
+ const closedEvent = eventNamed("ChannelClosed");
686
+ const requestedEvent = eventNamed("CloseRequested");
687
+ const cancelledEvent = eventNamed("CloseRequestCancelled");
688
+ const beforeBound = async (blockNumber) => Number((await getBlock(client, { blockNumber })).timestamp) * 1e3 < notBeforeMs - TEMPO_CLOSE_LOG_SLACK_MS;
689
+ if (startBlock !== void 0 && await beforeBound(startBlock)) {
690
+ return { outcome: "none", stoppedAt: startBlock };
691
+ }
692
+ let toBlock = startBlock ?? await getBlockNumber(client);
651
693
  for (let window = 0; window < TEMPO_CLOSE_LOG_MAX_WINDOWS; window += 1) {
652
694
  const fromBlock = toBlock >= TEMPO_CLOSE_LOG_WINDOW ? toBlock - TEMPO_CLOSE_LOG_WINDOW + 1n : 0n;
653
- const logs = await getLogs(client, {
654
- address: escrowContract,
655
- event,
656
- args: { channelId },
657
- fromBlock,
658
- toBlock
659
- });
660
- const closed = logs.find((log) => log.transactionHash);
661
- if (closed?.transactionHash) return closed.transactionHash;
662
- if (fromBlock === 0n) return null;
663
- const start = await getBlock(client, { blockNumber: fromBlock });
664
- if (Number(start.timestamp) * 1e3 < notBeforeMs - TEMPO_CLOSE_LOG_SLACK_MS) return null;
695
+ const range = { address: escrowContract, args: { channelId }, fromBlock, toBlock };
696
+ const [closed, requested, cancelled] = await Promise.all([
697
+ getLogs(client, { ...range, event: closedEvent }),
698
+ getLogs(client, { ...range, event: requestedEvent }),
699
+ getLogs(client, { ...range, event: cancelledEvent })
700
+ ]);
701
+ const newest = [
702
+ ...closed.map((log) => ({ log, kind: "closed" })),
703
+ ...requested.map((log) => ({ log, kind: "requested" })),
704
+ ...cancelled.map((log) => ({ log, kind: "cancelled" }))
705
+ ].filter(({ log }) => log.transactionHash && log.blockNumber !== null).sort((left, right) => left.log.blockNumber === right.log.blockNumber ? Number(right.log.logIndex ?? 0) - Number(left.log.logIndex ?? 0) : right.log.blockNumber > left.log.blockNumber ? 1 : -1)[0];
706
+ if (newest?.kind === "closed") {
707
+ const transaction = await getTransaction(client, {
708
+ hash: newest.log.transactionHash
709
+ });
710
+ return { outcome: "closed", txHash: newest.log.transactionHash, sender: transaction.from };
711
+ }
712
+ if (newest?.kind === "requested") {
713
+ const graceEnd = newest.log.args.closeGraceEnd;
714
+ const head = await getBlock(client, { blockTag: "latest" });
715
+ if (graceEnd === void 0 || head.timestamp < graceEnd) return { outcome: "none" };
716
+ return { outcome: "exit_requested" };
717
+ }
718
+ if (newest) return { outcome: "none", stoppedAt: 0n };
719
+ if (fromBlock === 0n || await beforeBound(fromBlock)) {
720
+ return { outcome: "none", stoppedAt: fromBlock };
721
+ }
665
722
  toBlock = fromBlock - 1n;
666
723
  }
667
- throw new Error(`Tempo close search for channel ${channelId} exceeded its block budget`);
724
+ return { outcome: "paused", resumeAt: toBlock };
668
725
  }
669
726
  };
670
727
  function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
671
728
  let creditLedger;
729
+ let finalizeDrain;
672
730
  Object.assign(mppx, {
673
- useTempoCreditLedger: (ledger) => {
731
+ useTempoCreditLedger: (ledger, finalizer) => {
674
732
  creditLedger = ledger;
733
+ finalizeDrain = finalizer;
675
734
  }
676
735
  });
677
736
  const chainOps = {
@@ -689,7 +748,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
689
748
  assertTempoSessionCloseVoucher: async (voucher) => {
690
749
  const descriptor = readChannelDescriptor(voucher.descriptor);
691
750
  if (!descriptor) {
692
- throw new MppxErrors.VerificationFailedError({
751
+ throw new TempoCloseDescriptorError({
693
752
  reason: "descriptor required for TIP-1034 session action"
694
753
  });
695
754
  }
@@ -707,22 +766,22 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
707
766
  escrow
708
767
  });
709
768
  if (computed.toLowerCase() !== voucher.channelId.toLowerCase()) {
710
- throw new MppxErrors.VerificationFailedError({
769
+ throw new TempoCloseDescriptorError({
711
770
  reason: "channel descriptor does not match channelId"
712
771
  });
713
772
  }
714
773
  if (!sameHex(descriptor.payee, voucher.request.recipient)) {
715
- throw new MppxErrors.VerificationFailedError({
774
+ throw new TempoCloseDescriptorError({
716
775
  reason: "channel descriptor payee does not match server destination"
717
776
  });
718
777
  }
719
778
  if (!sameHex(descriptor.token, voucher.request.currency)) {
720
- throw new MppxErrors.VerificationFailedError({
779
+ throw new TempoCloseDescriptorError({
721
780
  reason: "channel descriptor token does not match server token"
722
781
  });
723
782
  }
724
783
  if (!sameHex(descriptor.operator, details.operator ?? ZERO_ADDRESS)) {
725
- throw new MppxErrors.VerificationFailedError({
784
+ throw new TempoCloseDescriptorError({
726
785
  reason: "channel descriptor operator does not match server operator"
727
786
  });
728
787
  }
@@ -736,7 +795,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
736
795
  }
737
796
  const storedDescriptor = readChannelDescriptor(stored.descriptor);
738
797
  if (!storedDescriptor || !sameDescriptor(descriptor, storedDescriptor)) {
739
- throw new MppxErrors.VerificationFailedError({
798
+ throw new TempoCloseDescriptorError({
740
799
  reason: "credential descriptor does not match stored channel"
741
800
  });
742
801
  }
@@ -838,6 +897,9 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
838
897
  value: {
839
898
  ...value,
840
899
  finalized: active.finalized || chainFinalized,
900
+ ...chainFinalized && !active.finalized && {
901
+ depositBeforeFinalization: active.deposit
902
+ },
841
903
  closeRequestedAt: chainFinalized ? 0n : BigInt(chainState.closeRequestedAt),
842
904
  deposit: chainFinalized ? 0n : maxBigInt(active.deposit, chainState.deposit),
843
905
  settledOnChain: maxBigInt(active.settledOnChain, chainState.settled)
@@ -866,6 +928,9 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
866
928
  value: {
867
929
  ...current,
868
930
  finalized: active.finalized || chainFinalized,
931
+ ...chainFinalized && !active.finalized && {
932
+ depositBeforeFinalization: active.deposit
933
+ },
869
934
  closeRequestedAt: chainFinalized ? 0n : BigInt(chainState.closeRequestedAt),
870
935
  deposit: chainFinalized ? 0n : maxBigInt(active.deposit, chainState.deposit),
871
936
  settledOnChain: maxBigInt(active.settledOnChain, chainState.settled)
@@ -1103,6 +1168,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1103
1168
  }
1104
1169
  }
1105
1170
  }
1171
+ await reconcileTempoSessionCloses();
1106
1172
  return { scanned: rows.length, closeRequested, settled, inFlight };
1107
1173
  };
1108
1174
  const recoverTempoSessionClose = async (channelId, drainId) => {
@@ -1110,7 +1176,8 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1110
1176
  const intent = await store.getClose(channelId);
1111
1177
  if (intent?.drainId !== drainId) return null;
1112
1178
  if (intent.reference) return intent.reference;
1113
- const current = readTempoSessionState(await store.get(channelId));
1179
+ const stored = await store.get(channelId);
1180
+ const current = readTempoSessionState(stored);
1114
1181
  if (!current) throw new Error(`Tempo session channel ${channelId} not found`);
1115
1182
  let finalized = current.finalized;
1116
1183
  if (!finalized) {
@@ -1125,23 +1192,35 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1125
1192
  }
1126
1193
  if (!finalized) return null;
1127
1194
  const pending = await config.hostedObligations?.pendingTempoClose(channelId, drainId);
1195
+ const selfHostedReport = !config.hostedObligations && !!config.onSessionSettlement && !!store.claimCloseReference && !!store.getCloseRecovery;
1128
1196
  let reference = pending?.txHash ?? void 0;
1129
- if (pending && !reference) {
1130
- const client = await getClient({ chainId: current.chainId });
1131
- const txHash = await chainOps.findChannelClose(
1132
- client,
1133
- current.channelId,
1134
- current.escrowContract,
1135
- pending.reservedAtMs
1136
- );
1137
- if (txHash) {
1138
- await config.hostedObligations.recordSubmitted(pending.id, txHash);
1139
- reference = txHash;
1140
- } else {
1197
+ if (pending && pending.state !== "unmatched" && !reference || selfHostedReport) {
1198
+ const recovery = await store.getCloseRecovery?.(channelId);
1199
+ const notBeforeMs = pending?.reservedAtMs ?? recovery?.updatedAtMs;
1200
+ if (notBeforeMs === void 0) throw new Error(`Tempo session close intent ${channelId} not found`);
1201
+ const obligations = config.hostedObligations;
1202
+ const found = await findFinalizedClose(current, stored, drainId, notBeforeMs, pending ? {
1203
+ from: pending.searchBefore,
1204
+ save: (resumeAt) => obligations.recordTempoCloseSearch(pending.id, resumeAt)
1205
+ } : {
1206
+ from: recovery?.searchBefore ?? null,
1207
+ save: (resumeAt) => store.recordCloseSearch?.(channelId, drainId, resumeAt)
1208
+ });
1209
+ if (pending) {
1210
+ reference = await settleUnsubmittedClose(current.channelId, drainId, pending.id, found);
1211
+ } else if (found.outcome === "ours") {
1212
+ const client = await getClient({ chainId: current.chainId });
1213
+ const { settledToPayee } = await chainOps.readChannelClose(client, current.channelId, found.txHash);
1214
+ const claimed = await store.claimCloseReference(channelId, drainId, found.txHash);
1215
+ if (claimed.claimed) {
1216
+ await reportRecoveredClose(current, found.txHash, recovery?.settledBeforeClose ?? null, settledToPayee);
1217
+ }
1218
+ return claimed.reference;
1219
+ } else if (found.outcome === "none") {
1141
1220
  loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1142
1221
  channel_id: current.channelId,
1143
1222
  drain_id: drainId,
1144
- obligation_id: pending.id
1223
+ amount_at_stake_native: (current.highestVoucherAmount - (recovery?.settledBeforeClose ?? current.settledOnChain)).toString()
1145
1224
  });
1146
1225
  }
1147
1226
  }
@@ -1152,6 +1231,234 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1152
1231
  );
1153
1232
  return recovered.reference;
1154
1233
  };
1234
+ const findFinalizedClose = async (current, stored, drainId, notBeforeMs, cursor) => {
1235
+ const client = await getClient({ chainId: current.chainId });
1236
+ const search = await chainOps.findChannelClose(
1237
+ client,
1238
+ current.channelId,
1239
+ current.escrowContract,
1240
+ notBeforeMs,
1241
+ cursor.from ?? void 0
1242
+ );
1243
+ if (search.outcome === "paused") {
1244
+ await cursor.save(search.resumeAt);
1245
+ loggers.tempo.warn("tempo_session_close_recovery_search_paused", {
1246
+ channel_id: current.channelId,
1247
+ drain_id: drainId,
1248
+ resume_block: search.resumeAt.toString()
1249
+ });
1250
+ throw new Error(`Tempo close search for channel ${current.channelId} continues on the next poll`);
1251
+ }
1252
+ if (search.outcome === "closed") {
1253
+ return cooperativeCloseSenders(stored, config.account.address).has(search.sender.toLowerCase()) ? { outcome: "ours", txHash: search.txHash } : { outcome: "exit" };
1254
+ }
1255
+ if (search.outcome === "exit_requested") return { outcome: "exit" };
1256
+ if (search.stoppedAt !== void 0) await cursor.save(search.stoppedAt);
1257
+ return { outcome: "none", terminal: search.stoppedAt !== void 0 };
1258
+ };
1259
+ const settleUnsubmittedClose = async (channelId, drainId, obligationId, found) => {
1260
+ const obligations = config.hostedObligations;
1261
+ if (found.outcome === "ours") {
1262
+ await obligations.recordSubmitted(obligationId, found.txHash);
1263
+ return found.txHash;
1264
+ }
1265
+ if (found.outcome === "exit") {
1266
+ if (await obligations.resolveTempoCloseExit(obligationId)) {
1267
+ loggers.tempo.warn("tempo_session_close_exit_resolved", {
1268
+ channel_id: channelId,
1269
+ drain_id: drainId,
1270
+ obligation_id: obligationId,
1271
+ amount_at_stake_native: await obligations.tempoCloseAmountAtStake(obligationId)
1272
+ });
1273
+ }
1274
+ return void 0;
1275
+ }
1276
+ if (found.terminal && await obligations.markTempoCloseUnmatched(obligationId)) {
1277
+ loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1278
+ channel_id: channelId,
1279
+ drain_id: drainId,
1280
+ obligation_id: obligationId,
1281
+ amount_at_stake_native: await obligations.tempoCloseAmountAtStake(obligationId)
1282
+ });
1283
+ }
1284
+ return void 0;
1285
+ };
1286
+ const reconcileUnsubmittedCloses = async () => {
1287
+ const obligations = config.hostedObligations;
1288
+ if (!obligations) return;
1289
+ for (const row of await obligations.unsubmittedTempoCloses()) {
1290
+ try {
1291
+ const stored = await store.get(row.channelId);
1292
+ const current = readTempoSessionState(stored);
1293
+ if (!current) continue;
1294
+ if (!current.finalized) {
1295
+ const client = await getClient({ chainId: current.chainId });
1296
+ const [chainState] = await chainOps.getChannelStatesBatch(
1297
+ client,
1298
+ [current.channelId],
1299
+ current.escrowContract
1300
+ );
1301
+ await terminalizeLostBacking(current, chainState);
1302
+ if (!await writeBackChainState(current, chainState)) continue;
1303
+ }
1304
+ const found = await findFinalizedClose(current, stored, row.drainId, row.reservedAtMs, {
1305
+ from: row.searchBefore,
1306
+ save: (resumeAt) => obligations.recordTempoCloseSearch(row.id, resumeAt)
1307
+ });
1308
+ await settleUnsubmittedClose(current.channelId, row.drainId, row.id, found);
1309
+ } catch (error) {
1310
+ loggers.tempo.warn("tempo_session_close_reconcile_failed", {
1311
+ channel_id: row.channelId,
1312
+ drain_id: row.drainId,
1313
+ obligation_id: row.id,
1314
+ error: error instanceof Error ? error.message : String(error)
1315
+ });
1316
+ }
1317
+ }
1318
+ };
1319
+ const reconcileFinalizedDrains = async () => {
1320
+ const ledger = creditLedger;
1321
+ if (!ledger || !finalizeDrain) return;
1322
+ let after;
1323
+ do {
1324
+ const page = await ledger.listChannelDrains({
1325
+ limit: 100,
1326
+ after,
1327
+ createdBeforeMs: Date.now(),
1328
+ rail: "tempo",
1329
+ includeResolved: true
1330
+ });
1331
+ for (const drain of page) {
1332
+ if (drain.status !== "exit_required" || !drain.channelId) continue;
1333
+ try {
1334
+ const stored = await store.get(drain.channelId);
1335
+ const current = readTempoSessionState(stored);
1336
+ if (!current) throw new Error("Channel history is unavailable");
1337
+ const client = await getClient({ chainId: current.chainId });
1338
+ const [chainState] = await chainOps.getChannelStatesBatch(client, [current.channelId], current.escrowContract);
1339
+ if (!chainState || chainState.deposit !== 0n) continue;
1340
+ await terminalizeLostBacking(current, chainState);
1341
+ await writeBackChainState(current, chainState);
1342
+ const pending = await config.hostedObligations?.pendingTempoClose(drain.channelId, drain.drainId);
1343
+ if (pending && !pending.txHash) {
1344
+ if (pending.state === "unmatched") throw new TempoDrainRefundUnprovenError("close_history_unmatched", "Close history requires an operator retry");
1345
+ continue;
1346
+ }
1347
+ const recovery = await store.getCloseRecovery?.(drain.channelId);
1348
+ const found = pending?.txHash ? { outcome: "ours", txHash: pending.txHash } : await findFinalizedClose(current, stored, drain.drainId, drain.createdAt, {
1349
+ from: recovery?.searchBefore ?? null,
1350
+ save: (resumeAt) => store.recordCloseSearch?.(drain.channelId, drain.drainId, resumeAt)
1351
+ });
1352
+ let refundedNative;
1353
+ let reference;
1354
+ if (found.outcome === "ours") {
1355
+ reference = found.txHash;
1356
+ const receipt = await chainOps.readChannelClose(client, current.channelId, found.txHash);
1357
+ refundedNative = receipt.refundedToPayer;
1358
+ if (!config.hostedObligations && config.onSessionSettlement && store.claimCloseReference) {
1359
+ const claimed = await store.claimCloseReference(drain.channelId, drain.drainId, found.txHash);
1360
+ if (claimed.claimed) {
1361
+ await reportRecoveredClose(current, found.txHash, recovery?.settledBeforeClose ?? null, receipt.settledToPayee);
1362
+ }
1363
+ }
1364
+ } else if (found.outcome === "exit") {
1365
+ const snapshot = stored;
1366
+ const deposit = typeof snapshot.depositBeforeFinalization === "bigint" ? snapshot.depositBeforeFinalization : current.deposit;
1367
+ if (deposit <= 0n) throw new TempoDrainRefundUnprovenError("collateral_missing", "The pre-exit collateral is unavailable");
1368
+ refundedNative = deposit - maxBigInt(current.settledOnChain, chainState.settled);
1369
+ reference = `channel:${current.channelId}`;
1370
+ } else {
1371
+ throw new TempoDrainRefundUnprovenError("close_evidence_missing", "Channel finalization has no matching refund evidence");
1372
+ }
1373
+ if (drain.drainedNative === null || refundedNative < BigInt(drain.drainedNative)) {
1374
+ throw new TempoDrainRefundUnprovenError("refund_short", "Proven refund is below the drain debit");
1375
+ }
1376
+ const creditUnbacked = (await ledger.get(drain.creditId))?.status === "unbacked";
1377
+ await store.confirmClose?.(drain.channelId, drain.drainId, reference);
1378
+ const changed = await finalizeDrain(drain, {
1379
+ reference,
1380
+ refundedNative,
1381
+ outcome: found.outcome === "ours" ? "cooperative_close" : "payer_exit",
1382
+ creditUnbacked
1383
+ });
1384
+ if (changed) {
1385
+ loggers.tempo.warn(creditUnbacked ? "tempo_session_drain_unbacked_resolved" : "tempo_session_drain_finalized", {
1386
+ channel_id: drain.channelId,
1387
+ credit_id: drain.creditId,
1388
+ drain_id: drain.drainId,
1389
+ amount_at_stake_native: drain.drainedNative.toString(),
1390
+ refund_native: refundedNative.toString(),
1391
+ builder_loss_native: (current.highestVoucherAmount > maxBigInt(current.settledOnChain, chainState.settled) ? current.highestVoucherAmount - maxBigInt(current.settledOnChain, chainState.settled) : 0n).toString()
1392
+ });
1393
+ }
1394
+ } catch (error) {
1395
+ let failure = error;
1396
+ if (error instanceof TempoDrainRefundUnprovenError) {
1397
+ try {
1398
+ await store.update(drain.channelId, (current) => {
1399
+ if (!isStateRecord(current)) throw new Error("Channel history is unavailable");
1400
+ const key = JSON.stringify([drain.creditId, drain.drainId, error.reason]);
1401
+ const seen = Array.isArray(current.drainRefundWarnings) ? current.drainRefundWarnings : [];
1402
+ if (seen.includes(key)) return { op: "noop", result: false };
1403
+ loggers.tempo.warn("tempo_session_drain_refund_unproven", {
1404
+ channel_id: drain.channelId,
1405
+ credit_id: drain.creditId,
1406
+ drain_id: drain.drainId,
1407
+ amount_at_stake_native: drain.drainedNative?.toString() ?? null,
1408
+ amount_at_stake_micro: drain.amountMicro,
1409
+ currency: drain.currency,
1410
+ reason: error.reason,
1411
+ error: error.message
1412
+ });
1413
+ return { op: "set", value: { ...current, drainRefundWarnings: [...seen, key] }, result: true };
1414
+ });
1415
+ continue;
1416
+ } catch (claimError) {
1417
+ failure = claimError;
1418
+ }
1419
+ }
1420
+ loggers.tempo.warn("tempo_session_close_reconcile_failed", {
1421
+ channel_id: drain.channelId,
1422
+ credit_id: drain.creditId,
1423
+ drain_id: drain.drainId,
1424
+ error: failure instanceof Error ? failure.message : String(failure)
1425
+ });
1426
+ }
1427
+ }
1428
+ const last = page.at(-1);
1429
+ after = page.length === 100 && last ? { createdAt: last.createdAt, creditId: last.creditId, drainId: last.drainId } : void 0;
1430
+ } while (after);
1431
+ };
1432
+ const reconcileTempoSessionCloses = async () => {
1433
+ await reconcileUnsubmittedCloses();
1434
+ await reconcileFinalizedDrains();
1435
+ };
1436
+ const reportRecoveredClose = async (channel, txHash, settledBeforeClose, settledToPayee) => {
1437
+ if (settledBeforeClose === null || settledToPayee < settledBeforeClose) {
1438
+ loggers.tempo.warn("tempo_session_close_settlement_unreported", {
1439
+ channel_id: channel.channelId,
1440
+ tx_hash: txHash,
1441
+ amount_at_stake_native: settledToPayee.toString()
1442
+ });
1443
+ return;
1444
+ }
1445
+ try {
1446
+ await config.onSessionSettlement({
1447
+ txHash,
1448
+ channelId: channel.channelId,
1449
+ trigger: "close",
1450
+ amount: settledToPayee,
1451
+ delta: settledToPayee - settledBeforeClose
1452
+ });
1453
+ } catch (error) {
1454
+ loggers.tempo.warn("tempo_session_close_settlement_callback_failed", {
1455
+ channel_id: channel.channelId,
1456
+ tx_hash: txHash,
1457
+ amount_at_stake_native: (settledToPayee - settledBeforeClose).toString(),
1458
+ error: error instanceof Error ? error.message : String(error)
1459
+ });
1460
+ }
1461
+ };
1155
1462
  const getClose = store.getClose?.bind(store) ?? (() => Promise.resolve(null));
1156
1463
  const beginClose = store.beginClose?.bind(store);
1157
1464
  const withCloseIntent = store.withCloseIntent?.bind(store);
@@ -1160,6 +1467,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1160
1467
  const confirmClose = store.confirmClose?.bind(store);
1161
1468
  return Object.assign(mppx, {
1162
1469
  sweepTempoSessions,
1470
+ ...config?.hostedObligations && { reconcileTempoSessionCloses },
1163
1471
  ...beginClose && {
1164
1472
  beginTempoSessionClose: async (channelId, drainId) => {
1165
1473
  await beginClose(channelId, drainId);
@@ -1179,6 +1487,14 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1179
1487
  }
1180
1488
  });
1181
1489
  }
1490
+ function cooperativeCloseSenders(stored, fallback) {
1491
+ const record = stored && typeof stored === "object" ? stored : {};
1492
+ const descriptor = record.descriptor && typeof record.descriptor === "object" ? record.descriptor : {};
1493
+ const senders = new Set(
1494
+ [record.operator, record.payee, descriptor.operator, descriptor.payee].filter((value) => typeof value === "string" && HEX_ADDRESS_RE.test(value)).map((value) => value.toLowerCase())
1495
+ );
1496
+ return senders.size > 0 ? senders : /* @__PURE__ */ new Set([fallback.toLowerCase()]);
1497
+ }
1182
1498
  function readTempoSessionState(value) {
1183
1499
  if (!value || typeof value !== "object") return void 0;
1184
1500
  const state = value;
@@ -1242,6 +1558,7 @@ export {
1242
1558
  TEMPO_SETTLEMENT_DEFAULT_LOW_BALANCE_MICRO,
1243
1559
  TempoSettlementReadiness,
1244
1560
  isTempoInsufficientFundsError,
1561
+ TempoCloseDescriptorError,
1245
1562
  MPPX_HMAC_MISMATCH_REASON,
1246
1563
  TEMPO_SETTLEMENT_LEASE_FIELD,
1247
1564
  TEMPO_SETTLEMENT_LEASE_OWNER_FIELD,