@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.
Files changed (52) hide show
  1. package/dist/{chunk-ILBSZMJL.js → chunk-2XQLL6N6.js} +1 -1
  2. package/dist/{chunk-UVS7MRJX.js → chunk-3Q6FTZDB.js} +2 -2
  3. package/dist/{chunk-OP7VCGQR.js → chunk-3R7NQA3Z.js} +1 -1
  4. package/dist/{chunk-RSNC5T6C.js → chunk-AJ6UHXUA.js} +1 -1
  5. package/dist/{chunk-ALWVJ2RB.js → chunk-EWZFANHJ.js} +3 -3
  6. package/dist/{chunk-J5J63WDC.js → chunk-FFXXGCVG.js} +3 -3
  7. package/dist/{chunk-UDJNZ3UG.js → chunk-JNECYURT.js} +1 -1
  8. package/dist/{chunk-IHQUBATH.js → chunk-KFCEXCZM.js} +3 -3
  9. package/dist/{chunk-NA2KOR6G.js → chunk-LDIPVPKI.js} +3 -3
  10. package/dist/{chunk-IOQIXRGM.js → chunk-M3T5JYDJ.js} +15 -8
  11. package/dist/{chunk-UWGBI5WK.js → chunk-MB6R4XPZ.js} +3 -3
  12. package/dist/{chunk-7PM4R4GD.js → chunk-MMGSYMIC.js} +1 -1
  13. package/dist/{chunk-QKE5NDPF.js → chunk-MU3IE434.js} +348 -80
  14. package/dist/{chunk-BFGQG4WY.js → chunk-QGNXYVBO.js} +52 -12
  15. package/dist/{chunk-JIZ4ZIXX.js → chunk-QNKK7SPY.js} +652 -245
  16. package/dist/{chunk-FM6QAGJC.js → chunk-STJBBVNL.js} +24 -8
  17. package/dist/{chunk-NU5RWJ5B.js → chunk-VMHYGKWT.js} +3 -3
  18. package/dist/{chunk-AVV5SYGO.js → chunk-VTHYQCDB.js} +1 -1
  19. package/dist/{chunk-ZEPJF4JC.js → chunk-VUGEEMLT.js} +4 -4
  20. package/dist/{chunk-2U6LAASN.js → chunk-W3VGCNG6.js} +1 -1
  21. package/dist/{chunk-G3MPHW5I.js → chunk-Y6DO5WSO.js} +3 -3
  22. package/dist/{chunk-AGE5XZKE.js → chunk-YOSZIWC3.js} +17 -4
  23. package/dist/{content-retention-B7ddtJ2y.d.ts → content-retention-CrILCNxk.d.ts} +1 -1
  24. package/dist/{credit-ledger-KIQIE6I6.js → credit-ledger-4PZNAMDT.js} +4 -4
  25. package/dist/{credit-menu-BH3g9GQa.d.ts → credit-menu-VwX_6KM7.d.ts} +4 -4
  26. package/dist/{fx-DN26_J7o.d.ts → fx-BM5h4o5S.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 +7 -7
  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-92v8e1LQ.d.ts} +1 -1
  34. package/dist/{lightning-backend-S8F8nf8q.d.ts → lightning-backend-9oky_wJi.d.ts} +1 -1
  35. package/dist/{memory-credit-ledger-ATYFAYBG.js → memory-credit-ledger-RP7DT5IY.js} +5 -5
  36. package/dist/{mpp-setup-ZTORW3VF.js → mpp-setup-FURD45LI.js} +5 -3
  37. package/dist/{payout-reporter-NN6K2IVQ.js → payout-reporter-HVJ6WUF3.js} +2 -2
  38. package/dist/{postgres-consumed-credential-store-XJTE5PCA.js → postgres-consumed-credential-store-WO6OCFXZ.js} +3 -3
  39. package/dist/{postgres-job-store-AR6ZJUQV.js → postgres-job-store-FSRUPYH2.js} +2 -2
  40. package/dist/{postgres-replay-store-33K33QV7.js → postgres-replay-store-XXJSA457.js} +2 -2
  41. package/dist/{pricing-G5IN5YJE.js → pricing-X4XU5HXY.js} +3 -3
  42. package/dist/{revenue-reporter-KDUZLD5F.js → revenue-reporter-EV3K5L6B.js} +2 -2
  43. package/dist/server/index.d.ts +78 -11
  44. package/dist/server/index.js +87 -45
  45. package/dist/{step-cache-CR12EZ1Q.d.ts → step-cache--7yA5Ihr.d.ts} +99 -12
  46. package/dist/{tempo-charge-store-XEGJYM3G.js → tempo-charge-store-5JOIYUIH.js} +2 -2
  47. package/dist/{tempo-session-store-3WTDOO7H.js → tempo-session-store-S6QMLQUA.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-C7lvLvLk.d.ts} +1 -1
  51. package/dist/{x402-HQ5GRQZU.js → x402-EN5LSOVM.js} +4 -4
  52. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  loggers
3
- } from "./chunk-IOQIXRGM.js";
3
+ } from "./chunk-M3T5JYDJ.js";
4
4
  import {
5
5
  diagnosticConsole
6
- } from "./chunk-BFGQG4WY.js";
6
+ } from "./chunk-QGNXYVBO.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";
@@ -654,6 +663,10 @@ function wrapMppx(mppx) {
654
663
  var TEMPO_CLOSE_LOG_WINDOW = 5000n;
655
664
  var TEMPO_CLOSE_LOG_SLACK_MS = 10 * 60 * 1e3;
656
665
  var TEMPO_CLOSE_LOG_MAX_WINDOWS = 500;
666
+ var TEMPO_CLOSE_SEARCH_BUDGET_MS = 3e4;
667
+ var TEMPO_CLOSE_POLL_SEARCH_BUDGET_MS = 1e4;
668
+ var TEMPO_CLOSE_SEARCH_TIMEOUT_MS = 6e4;
669
+ var TEMPO_CLOSE_CURSOR_SAVE_RESERVE_MS = 5e3;
657
670
  var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
658
671
  getChannelStatesBatch: TempoSession.Precompile.Chain.getChannelStatesBatch,
659
672
  settle: TempoSession.Server.settle,
@@ -664,7 +677,7 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
664
677
  channelId
665
678
  )
666
679
  ),
667
- findChannelClose: async (client, channelId, escrowContract, notBeforeMs, startBlock) => {
680
+ findChannelClose: async (client, channelId, escrowContract, notBeforeMs, startBlock, deadlineMs = Date.now() + TEMPO_CLOSE_SEARCH_BUDGET_MS) => {
668
681
  const { getBlock, getBlockNumber, getLogs, getTransaction } = await import("viem/actions");
669
682
  const eventNamed = (name) => {
670
683
  const event = TempoSession.Precompile.escrowAbi.find(
@@ -682,6 +695,8 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
682
695
  }
683
696
  let toBlock = startBlock ?? await getBlockNumber(client);
684
697
  for (let window = 0; window < TEMPO_CLOSE_LOG_MAX_WINDOWS; window += 1) {
698
+ const deadline = typeof deadlineMs === "function" ? deadlineMs() : deadlineMs;
699
+ if (Date.now() >= deadline) return { outcome: "paused", resumeAt: toBlock };
685
700
  const fromBlock = toBlock >= TEMPO_CLOSE_LOG_WINDOW ? toBlock - TEMPO_CLOSE_LOG_WINDOW + 1n : 0n;
686
701
  const range = { address: escrowContract, args: { channelId }, fromBlock, toBlock };
687
702
  const [closed, requested, cancelled] = await Promise.all([
@@ -717,9 +732,12 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
717
732
  };
718
733
  function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
719
734
  let creditLedger;
735
+ let finalizeDrain;
736
+ const finalizedCloseSearches = /* @__PURE__ */ new Map();
720
737
  Object.assign(mppx, {
721
- useTempoCreditLedger: (ledger) => {
738
+ useTempoCreditLedger: (ledger, finalizer) => {
722
739
  creditLedger = ledger;
740
+ finalizeDrain = finalizer;
723
741
  }
724
742
  });
725
743
  const chainOps = {
@@ -737,7 +755,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
737
755
  assertTempoSessionCloseVoucher: async (voucher) => {
738
756
  const descriptor = readChannelDescriptor(voucher.descriptor);
739
757
  if (!descriptor) {
740
- throw new MppxErrors.VerificationFailedError({
758
+ throw new TempoCloseDescriptorError({
741
759
  reason: "descriptor required for TIP-1034 session action"
742
760
  });
743
761
  }
@@ -755,22 +773,22 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
755
773
  escrow
756
774
  });
757
775
  if (computed.toLowerCase() !== voucher.channelId.toLowerCase()) {
758
- throw new MppxErrors.VerificationFailedError({
776
+ throw new TempoCloseDescriptorError({
759
777
  reason: "channel descriptor does not match channelId"
760
778
  });
761
779
  }
762
780
  if (!sameHex(descriptor.payee, voucher.request.recipient)) {
763
- throw new MppxErrors.VerificationFailedError({
781
+ throw new TempoCloseDescriptorError({
764
782
  reason: "channel descriptor payee does not match server destination"
765
783
  });
766
784
  }
767
785
  if (!sameHex(descriptor.token, voucher.request.currency)) {
768
- throw new MppxErrors.VerificationFailedError({
786
+ throw new TempoCloseDescriptorError({
769
787
  reason: "channel descriptor token does not match server token"
770
788
  });
771
789
  }
772
790
  if (!sameHex(descriptor.operator, details.operator ?? ZERO_ADDRESS)) {
773
- throw new MppxErrors.VerificationFailedError({
791
+ throw new TempoCloseDescriptorError({
774
792
  reason: "channel descriptor operator does not match server operator"
775
793
  });
776
794
  }
@@ -784,7 +802,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
784
802
  }
785
803
  const storedDescriptor = readChannelDescriptor(stored.descriptor);
786
804
  if (!storedDescriptor || !sameDescriptor(descriptor, storedDescriptor)) {
787
- throw new MppxErrors.VerificationFailedError({
805
+ throw new TempoCloseDescriptorError({
788
806
  reason: "credential descriptor does not match stored channel"
789
807
  });
790
808
  }
@@ -886,6 +904,9 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
886
904
  value: {
887
905
  ...value,
888
906
  finalized: active.finalized || chainFinalized,
907
+ ...chainFinalized && !active.finalized && {
908
+ depositBeforeFinalization: active.deposit
909
+ },
889
910
  closeRequestedAt: chainFinalized ? 0n : BigInt(chainState.closeRequestedAt),
890
911
  deposit: chainFinalized ? 0n : maxBigInt(active.deposit, chainState.deposit),
891
912
  settledOnChain: maxBigInt(active.settledOnChain, chainState.settled)
@@ -914,6 +935,9 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
914
935
  value: {
915
936
  ...current,
916
937
  finalized: active.finalized || chainFinalized,
938
+ ...chainFinalized && !active.finalized && {
939
+ depositBeforeFinalization: active.deposit
940
+ },
917
941
  closeRequestedAt: chainFinalized ? 0n : BigInt(chainState.closeRequestedAt),
918
942
  deposit: chainFinalized ? 0n : maxBigInt(active.deposit, chainState.deposit),
919
943
  settledOnChain: maxBigInt(active.settledOnChain, chainState.settled)
@@ -925,18 +949,46 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
925
949
  };
926
950
  const settlingHere = /* @__PURE__ */ new Set();
927
951
  const leaseMs = config.settlementSchedule?.leaseMs ?? TEMPO_SETTLEMENT_LEASE_MS;
928
- const claimSettlement = async (channelId) => {
929
- if (settlingHere.has(channelId)) return false;
952
+ const claimSettlement = async (channelId, chainSettled, forcedClose) => {
953
+ const inFlight = {
954
+ outcome: "retryable_failure",
955
+ channelId,
956
+ error: TEMPO_SETTLEMENT_IN_FLIGHT
957
+ };
958
+ if (settlingHere.has(channelId)) return inFlight;
930
959
  settlingHere.add(channelId);
931
960
  const leaseOwner = randomUUID();
932
961
  const leaseUntil = Date.now() + leaseMs;
933
962
  try {
934
- const claimed = await store.update(channelId, (current) => {
935
- if (!isStateRecord(current)) return { op: "noop", result: false };
963
+ const claim = await store.update(channelId, (current) => {
964
+ const channel = readTempoSessionState(current);
965
+ if (!channel || !isStateRecord(current)) {
966
+ return { op: "noop", result: { outcome: "unknown_channel", channelId } };
967
+ }
936
968
  const until = current[TEMPO_SETTLEMENT_LEASE_FIELD];
937
969
  const owner = current[TEMPO_SETTLEMENT_LEASE_OWNER_FIELD];
938
970
  if (typeof until === "number" && until > Date.now() && owner !== leaseOwner) {
939
- return { op: "noop", result: false };
971
+ return { op: "noop", result: inFlight };
972
+ }
973
+ const settledOnChain = maxBigInt(channel.settledOnChain, chainSettled);
974
+ if (channel.finalized && channel.highestVoucherAmount > settledOnChain) {
975
+ return {
976
+ op: "noop",
977
+ result: {
978
+ outcome: "finalized_before_protection",
979
+ channelId,
980
+ owed: channel.highestVoucherAmount - settledOnChain,
981
+ settledOnChain
982
+ }
983
+ };
984
+ }
985
+ if (channel.finalized || !tempoSettlementDue({
986
+ channel,
987
+ chainSettled: settledOnChain,
988
+ forcedClose,
989
+ intervalMs: config.settlementSchedule?.intervalMs
990
+ })) {
991
+ return { op: "noop", result: { outcome: "already_safe", channelId, settledOnChain } };
940
992
  }
941
993
  return {
942
994
  op: "set",
@@ -945,11 +997,11 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
945
997
  [TEMPO_SETTLEMENT_LEASE_FIELD]: leaseUntil,
946
998
  [TEMPO_SETTLEMENT_LEASE_OWNER_FIELD]: leaseOwner
947
999
  },
948
- result: true
1000
+ result: { outcome: "claimed", channel }
949
1001
  };
950
1002
  });
951
- if (!claimed) settlingHere.delete(channelId);
952
- return claimed;
1003
+ if (claim.outcome !== "claimed") settlingHere.delete(channelId);
1004
+ return claim;
953
1005
  } catch (err) {
954
1006
  settlingHere.delete(channelId);
955
1007
  throw err;
@@ -971,8 +1023,10 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
971
1023
  settlingHere.delete(channelId);
972
1024
  }
973
1025
  };
974
- const settleChannel = async (channel, client) => {
975
- if (!await claimSettlement(channel.channelId)) return false;
1026
+ const settleChannel = async (channelId, client, chainSettled, forcedClose) => {
1027
+ const claim = await claimSettlement(channelId, chainSettled, forcedClose);
1028
+ if (claim.outcome !== "claimed") return claim;
1029
+ const { channel } = claim;
976
1030
  try {
977
1031
  const recoveryVersion = await config.beforeSessionSettlement?.();
978
1032
  const reservation = await config.hostedObligations?.reserveTempo(channel.channelId, "settle");
@@ -1007,7 +1061,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1007
1061
  throw err;
1008
1062
  }
1009
1063
  await releaseSettlement(channel.channelId);
1010
- return true;
1064
+ return { outcome: "settled", channelId, settledAmount: channel.highestVoucherAmount };
1011
1065
  };
1012
1066
  const settleTempoSessionChannel = async (channelId) => {
1013
1067
  const normalized = channelId.toLowerCase();
@@ -1048,18 +1102,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1048
1102
  })) {
1049
1103
  return { outcome: "already_safe", channelId: normalized, settledOnChain };
1050
1104
  }
1051
- if (!await settleChannel(channel, client)) {
1052
- return {
1053
- outcome: "retryable_failure",
1054
- channelId: normalized,
1055
- error: TEMPO_SETTLEMENT_IN_FLIGHT
1056
- };
1057
- }
1058
- return {
1059
- outcome: "settled",
1060
- channelId: normalized,
1061
- settledAmount: channel.highestVoucherAmount
1062
- };
1105
+ return await settleChannel(normalized, client, chainState.settled, forcedClose);
1063
1106
  } catch (err) {
1064
1107
  return {
1065
1108
  outcome: "retryable_failure",
@@ -1146,18 +1189,29 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1146
1189
  })) {
1147
1190
  continue;
1148
1191
  }
1149
- if (await settleChannel(channel, client)) settled += 1;
1150
- else inFlight += 1;
1192
+ const result = await settleChannel(channel.channelId, client, chainState.settled, forcedClose);
1193
+ if (result.outcome === "settled") settled += 1;
1194
+ else if (result.outcome === "retryable_failure") inFlight += 1;
1151
1195
  }
1152
1196
  }
1153
1197
  }
1154
- await reconcileUnsubmittedCloses();
1198
+ await reconcileTempoSessionCloses();
1155
1199
  return { scanned: rows.length, closeRequested, settled, inFlight };
1156
1200
  };
1157
- const recoverTempoSessionClose = async (channelId, drainId) => {
1201
+ const recoverTempoSessionClose = async (channelId, drainId, creditId) => {
1158
1202
  if (!store.getClose || !store.confirmClose) return null;
1159
1203
  const intent = await store.getClose(channelId);
1160
1204
  if (intent?.drainId !== drainId) return null;
1205
+ const drain = creditId ? await creditLedger?.getDrain({ creditId, drainId }) : void 0;
1206
+ if (drain && drain.channelId !== channelId) throw new Error("Tempo drain channel does not match its credit");
1207
+ if (drain?.status === "pending" || drain?.status === "exit_required") {
1208
+ const result = await reconcileFinalizedDrain(drain, true);
1209
+ if (result.outcome === "retryable_failure") throw result.error;
1210
+ if (result.outcome === "refund_unproven") return { outcome: "refund_unproven" };
1211
+ if (result.outcome === "open") return null;
1212
+ const completed = await creditLedger.getDrain({ creditId: drain.creditId, drainId });
1213
+ return completed?.status === "sent" && typeof completed.sentRef?.tx_hash === "string" ? completed.sentRef.tx_hash : null;
1214
+ }
1161
1215
  if (intent.reference) return intent.reference;
1162
1216
  const stored = await store.get(channelId);
1163
1217
  const current = readTempoSessionState(stored);
@@ -1177,30 +1231,41 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1177
1231
  const pending = await config.hostedObligations?.pendingTempoClose(channelId, drainId);
1178
1232
  const selfHostedReport = !config.hostedObligations && !!config.onSessionSettlement && !!store.claimCloseReference && !!store.getCloseRecovery;
1179
1233
  let reference = pending?.txHash ?? void 0;
1180
- if (pending && !reference || selfHostedReport) {
1234
+ if (pending && pending.state !== "unmatched" && !reference || selfHostedReport) {
1181
1235
  const recovery = await store.getCloseRecovery?.(channelId);
1182
1236
  const notBeforeMs = pending?.reservedAtMs ?? recovery?.updatedAtMs;
1183
1237
  if (notBeforeMs === void 0) throw new Error(`Tempo session close intent ${channelId} not found`);
1184
1238
  const obligations = config.hostedObligations;
1185
- const found = await findFinalizedClose(current, stored, drainId, notBeforeMs, pending ? {
1186
- from: pending.searchBefore,
1187
- save: (resumeAt) => obligations.recordTempoCloseSearch(pending.id, resumeAt)
1188
- } : {
1189
- from: recovery?.searchBefore ?? null,
1190
- save: (resumeAt) => store.recordCloseSearch?.(channelId, drainId, resumeAt)
1191
- });
1239
+ const found = await findFinalizedClose(
1240
+ current,
1241
+ stored,
1242
+ drainId,
1243
+ notBeforeMs,
1244
+ pending ? {
1245
+ from: pending.searchBefore,
1246
+ obligationId: pending.id,
1247
+ save: (resumeAt) => obligations.recordTempoCloseSearch(pending.id, resumeAt)
1248
+ } : {
1249
+ from: recovery?.searchBefore ?? null,
1250
+ save: (resumeAt) => store.recordCloseSearch?.(channelId, drainId, resumeAt)
1251
+ },
1252
+ TEMPO_CLOSE_POLL_SEARCH_BUDGET_MS
1253
+ );
1192
1254
  if (pending) {
1193
1255
  reference = await settleUnsubmittedClose(current.channelId, drainId, pending.id, found);
1194
1256
  } else if (found.outcome === "ours") {
1257
+ const client = await getClient({ chainId: current.chainId });
1258
+ const { settledToPayee } = await chainOps.readChannelClose(client, current.channelId, found.txHash);
1195
1259
  const claimed = await store.claimCloseReference(channelId, drainId, found.txHash);
1196
1260
  if (claimed.claimed) {
1197
- await reportRecoveredClose(current, found.txHash, recovery?.settledBeforeClose ?? null);
1261
+ await reportRecoveredClose(current, found.txHash, recovery?.settledBeforeClose ?? null, settledToPayee);
1198
1262
  }
1199
1263
  return claimed.reference;
1200
1264
  } else if (found.outcome === "none") {
1201
1265
  loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1202
1266
  channel_id: current.channelId,
1203
- drain_id: drainId
1267
+ drain_id: drainId,
1268
+ amount_at_stake_native: (current.highestVoucherAmount - (recovery?.settledBeforeClose ?? current.settledOnChain)).toString()
1204
1269
  });
1205
1270
  }
1206
1271
  }
@@ -1211,30 +1276,67 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1211
1276
  );
1212
1277
  return recovered.reference;
1213
1278
  };
1214
- const findFinalizedClose = async (current, stored, drainId, notBeforeMs, cursor) => {
1215
- const client = await getClient({ chainId: current.chainId });
1216
- const search = await chainOps.findChannelClose(
1217
- client,
1218
- current.channelId,
1219
- current.escrowContract,
1220
- notBeforeMs,
1221
- cursor.from ?? void 0
1222
- );
1223
- if (search.outcome === "paused") {
1224
- await cursor.save(search.resumeAt);
1225
- loggers.tempo.warn("tempo_session_close_recovery_search_paused", {
1226
- channel_id: current.channelId,
1227
- drain_id: drainId,
1228
- resume_block: search.resumeAt.toString()
1229
- });
1230
- throw new Error(`Tempo close search for channel ${current.channelId} continues on the next poll`);
1279
+ const findFinalizedClose = async (current, stored, drainId, notBeforeMs, cursor, budgetMs = TEMPO_CLOSE_SEARCH_BUDGET_MS) => {
1280
+ const key = cursor.obligationId ?? JSON.stringify([current.chainId, current.escrowContract, current.channelId, drainId, notBeforeMs]);
1281
+ const existing = finalizedCloseSearches.get(key);
1282
+ if (existing) {
1283
+ existing.deadline.atMs = Math.min(existing.deadline.atMs ?? Infinity, Date.now() + budgetMs);
1284
+ return existing.promise;
1231
1285
  }
1232
- if (search.outcome === "closed") {
1233
- return cooperativeCloseSenders(stored, config.account.address).has(search.sender.toLowerCase()) ? { outcome: "ours", txHash: search.txHash } : { outcome: "exit" };
1286
+ const deadline = {};
1287
+ const backstopDeadlineMs = Date.now() + TEMPO_CLOSE_SEARCH_TIMEOUT_MS;
1288
+ let expired = false;
1289
+ const assertActive = () => {
1290
+ if (expired) throw new Error("Tempo close search exceeded its deadline; retry this drain");
1291
+ };
1292
+ let timeout;
1293
+ const work = (async () => {
1294
+ const client = await getClient({ chainId: current.chainId });
1295
+ assertActive();
1296
+ deadline.atMs = Math.min(deadline.atMs ?? Infinity, Date.now() + budgetMs);
1297
+ const search2 = await chainOps.findChannelClose(
1298
+ client,
1299
+ current.channelId,
1300
+ current.escrowContract,
1301
+ notBeforeMs,
1302
+ cursor.from ?? void 0,
1303
+ () => Math.min(
1304
+ deadline.atMs ?? Infinity,
1305
+ backstopDeadlineMs - TEMPO_CLOSE_CURSOR_SAVE_RESERVE_MS
1306
+ )
1307
+ );
1308
+ assertActive();
1309
+ if (search2.outcome === "paused") {
1310
+ await cursor.save(search2.resumeAt);
1311
+ assertActive();
1312
+ loggers.tempo.warn("tempo_session_close_recovery_search_paused", {
1313
+ channel_id: current.channelId,
1314
+ drain_id: drainId,
1315
+ resume_block: search2.resumeAt.toString()
1316
+ });
1317
+ throw new Error(`Tempo close search for channel ${current.channelId} continues on the next poll`);
1318
+ }
1319
+ if (search2.outcome === "closed") {
1320
+ return cooperativeCloseSenders(stored, config.account.address).has(search2.sender.toLowerCase()) ? { outcome: "ours", txHash: search2.txHash } : { outcome: "exit" };
1321
+ }
1322
+ if (search2.outcome === "exit_requested") return { outcome: "exit" };
1323
+ if (search2.stoppedAt !== void 0) await cursor.save(search2.stoppedAt);
1324
+ assertActive();
1325
+ return { outcome: "none", terminal: search2.stoppedAt !== void 0 };
1326
+ })();
1327
+ const search = Promise.race([work, new Promise((_resolve, reject) => {
1328
+ timeout = setTimeout(() => {
1329
+ expired = true;
1330
+ reject(new Error("Tempo close search exceeded its deadline; retry this drain"));
1331
+ }, TEMPO_CLOSE_SEARCH_TIMEOUT_MS);
1332
+ })]);
1333
+ finalizedCloseSearches.set(key, { promise: search, deadline });
1334
+ try {
1335
+ return await search;
1336
+ } finally {
1337
+ clearTimeout(timeout);
1338
+ finalizedCloseSearches.delete(key);
1234
1339
  }
1235
- if (search.outcome === "exit_requested") return { outcome: "exit" };
1236
- if (search.stoppedAt !== void 0) await cursor.save(search.stoppedAt);
1237
- return { outcome: "none" };
1238
1340
  };
1239
1341
  const settleUnsubmittedClose = async (channelId, drainId, obligationId, found) => {
1240
1342
  const obligations = config.hostedObligations;
@@ -1247,16 +1349,20 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1247
1349
  loggers.tempo.warn("tempo_session_close_exit_resolved", {
1248
1350
  channel_id: channelId,
1249
1351
  drain_id: drainId,
1250
- obligation_id: obligationId
1352
+ obligation_id: obligationId,
1353
+ amount_at_stake_native: await obligations.tempoCloseAmountAtStake(obligationId)
1251
1354
  });
1252
1355
  }
1253
1356
  return void 0;
1254
1357
  }
1255
- loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1256
- channel_id: channelId,
1257
- drain_id: drainId,
1258
- obligation_id: obligationId
1259
- });
1358
+ if (found.terminal && await obligations.markTempoCloseUnmatched(obligationId)) {
1359
+ loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1360
+ channel_id: channelId,
1361
+ drain_id: drainId,
1362
+ obligation_id: obligationId,
1363
+ amount_at_stake_native: await obligations.tempoCloseAmountAtStake(obligationId)
1364
+ });
1365
+ }
1260
1366
  return void 0;
1261
1367
  };
1262
1368
  const reconcileUnsubmittedCloses = async () => {
@@ -1267,6 +1373,11 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1267
1373
  const stored = await store.get(row.channelId);
1268
1374
  const current = readTempoSessionState(stored);
1269
1375
  if (!current) continue;
1376
+ const intent = await store.getClose?.(row.channelId);
1377
+ if (intent?.drainId === row.drainId && isTempoCloseTransaction(intent.reference)) {
1378
+ await obligations.recordSubmitted(row.id, intent.reference);
1379
+ continue;
1380
+ }
1270
1381
  if (!current.finalized) {
1271
1382
  const client = await getClient({ chainId: current.chainId });
1272
1383
  const [chainState] = await chainOps.getChannelStatesBatch(
@@ -1279,6 +1390,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1279
1390
  }
1280
1391
  const found = await findFinalizedClose(current, stored, row.drainId, row.reservedAtMs, {
1281
1392
  from: row.searchBefore,
1393
+ obligationId: row.id,
1282
1394
  save: (resumeAt) => obligations.recordTempoCloseSearch(row.id, resumeAt)
1283
1395
  });
1284
1396
  await settleUnsubmittedClose(current.channelId, row.drainId, row.id, found);
@@ -1292,13 +1404,164 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1292
1404
  }
1293
1405
  }
1294
1406
  };
1295
- const reportRecoveredClose = async (channel, txHash, settledBeforeClose) => {
1296
- const client = await getClient({ chainId: channel.chainId });
1297
- const { settledToPayee } = await chainOps.readChannelClose(client, channel.channelId, txHash);
1407
+ const reconcileFinalizedDrain = async (drain, recoverPending = false) => {
1408
+ const ledger = creditLedger;
1409
+ if (!ledger || !finalizeDrain || !drain.channelId) return { outcome: "open" };
1410
+ const searchBudgetMs = recoverPending ? TEMPO_CLOSE_POLL_SEARCH_BUDGET_MS : TEMPO_CLOSE_SEARCH_BUDGET_MS;
1411
+ try {
1412
+ const stored = await store.get(drain.channelId);
1413
+ const current = readTempoSessionState(stored);
1414
+ if (!current) throw new Error("Channel history is unavailable");
1415
+ const client = await getClient({ chainId: current.chainId });
1416
+ const recovery = await store.getCloseRecovery?.(drain.channelId);
1417
+ const pending = await config.hostedObligations?.pendingTempoClose(drain.channelId, drain.drainId);
1418
+ const intent = await store.getClose?.(drain.channelId);
1419
+ const recorded = intent?.drainId === drain.drainId && isTempoCloseTransaction(intent.reference) ? intent.reference : await config.hostedObligations?.tempoCloseTransaction(drain.channelId, drain.drainId);
1420
+ const txHash = recorded ?? pending?.txHash;
1421
+ let receipt = isTempoCloseTransaction(txHash) ? await chainOps.readChannelClose(client, current.channelId, txHash) : void 0;
1422
+ const chainState = receipt ? { deposit: 0n, settled: receipt.settledToPayee, closeRequestedAt: 0 } : (await chainOps.getChannelStatesBatch(client, [current.channelId], current.escrowContract))[0];
1423
+ if (!chainState || chainState.deposit !== 0n) return { outcome: "open" };
1424
+ await terminalizeLostBacking(current, chainState);
1425
+ await writeBackChainState(current, chainState);
1426
+ let found = receipt ? { outcome: "ours", txHash } : void 0;
1427
+ if (found?.outcome === "ours" && pending) {
1428
+ await config.hostedObligations.recordSubmitted(pending.id, found.txHash);
1429
+ }
1430
+ if (!found && pending && !pending.txHash) {
1431
+ if (pending.state === "unmatched") throw new TempoDrainRefundUnprovenError("close_history_unmatched", "Close history requires an operator retry");
1432
+ if (!recoverPending) return { outcome: "open" };
1433
+ found = await findFinalizedClose(current, stored, drain.drainId, pending.reservedAtMs, {
1434
+ from: pending.searchBefore,
1435
+ obligationId: pending.id,
1436
+ save: (resumeAt) => config.hostedObligations.recordTempoCloseSearch(pending.id, resumeAt)
1437
+ }, searchBudgetMs);
1438
+ await settleUnsubmittedClose(current.channelId, drain.drainId, pending.id, found);
1439
+ if (found.outcome === "none" && found.terminal) {
1440
+ throw new TempoDrainRefundUnprovenError("close_history_unmatched", "Close history requires an operator retry");
1441
+ }
1442
+ }
1443
+ found ??= pending?.txHash ? { outcome: "ours", txHash: pending.txHash } : await findFinalizedClose(current, stored, drain.drainId, drain.createdAt, {
1444
+ from: recovery?.searchBefore ?? null,
1445
+ save: (resumeAt) => store.recordCloseSearch?.(drain.channelId, drain.drainId, resumeAt)
1446
+ }, searchBudgetMs);
1447
+ if (found.outcome === "none" && !found.terminal) {
1448
+ throw new Error("Tempo close evidence is not yet available; retry this drain");
1449
+ }
1450
+ let refundedNative;
1451
+ let reference;
1452
+ if (found.outcome === "ours") {
1453
+ reference = found.txHash;
1454
+ receipt ??= await chainOps.readChannelClose(client, current.channelId, found.txHash);
1455
+ refundedNative = receipt.refundedToPayer;
1456
+ if (!config.hostedObligations && config.onSessionSettlement && store.claimCloseReference) {
1457
+ const claimed = await store.claimCloseReference(drain.channelId, drain.drainId, found.txHash);
1458
+ if (claimed.claimed) {
1459
+ await reportRecoveredClose(current, found.txHash, recovery?.settledBeforeClose ?? null, receipt.settledToPayee);
1460
+ }
1461
+ }
1462
+ } else if (found.outcome === "exit") {
1463
+ const snapshot = stored;
1464
+ const deposit = typeof snapshot.depositBeforeFinalization === "bigint" ? snapshot.depositBeforeFinalization : current.deposit;
1465
+ if (deposit <= 0n) throw new TempoDrainRefundUnprovenError("collateral_missing", "The pre-exit collateral is unavailable");
1466
+ refundedNative = deposit - maxBigInt(current.settledOnChain, chainState.settled);
1467
+ reference = `channel:${current.channelId}`;
1468
+ } else {
1469
+ if (!config.hostedObligations && config.onSessionSettlement) {
1470
+ loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1471
+ channel_id: current.channelId,
1472
+ drain_id: drain.drainId,
1473
+ amount_at_stake_native: (current.highestVoucherAmount - (recovery?.settledBeforeClose ?? current.settledOnChain)).toString()
1474
+ });
1475
+ }
1476
+ throw new TempoDrainRefundUnprovenError("close_evidence_missing", "Channel finalization has no matching refund evidence");
1477
+ }
1478
+ if (drain.drainedNative === null || refundedNative < BigInt(drain.drainedNative)) {
1479
+ throw new TempoDrainRefundUnprovenError("refund_short", "Proven refund is below the drain debit");
1480
+ }
1481
+ const creditUnbacked = (await ledger.get(drain.creditId))?.status === "unbacked";
1482
+ await store.confirmClose?.(drain.channelId, drain.drainId, reference);
1483
+ const changed = await finalizeDrain(drain, {
1484
+ reference,
1485
+ refundedNative,
1486
+ outcome: found.outcome === "ours" ? "cooperative_close" : "payer_exit",
1487
+ creditUnbacked
1488
+ });
1489
+ if (changed) {
1490
+ loggers.tempo.warn(creditUnbacked ? "tempo_session_drain_unbacked_resolved" : "tempo_session_drain_finalized", {
1491
+ channel_id: drain.channelId,
1492
+ credit_id: drain.creditId,
1493
+ drain_id: drain.drainId,
1494
+ amount_at_stake_native: drain.drainedNative.toString(),
1495
+ refund_native: refundedNative.toString(),
1496
+ builder_loss_native: (current.highestVoucherAmount > maxBigInt(current.settledOnChain, chainState.settled) ? current.highestVoucherAmount - maxBigInt(current.settledOnChain, chainState.settled) : 0n).toString()
1497
+ });
1498
+ }
1499
+ return { outcome: "completed" };
1500
+ } catch (error) {
1501
+ let failure = error;
1502
+ if (error instanceof TempoDrainRefundUnprovenError) {
1503
+ try {
1504
+ await store.update(drain.channelId, (current) => {
1505
+ if (!isStateRecord(current)) throw new Error("Channel history is unavailable");
1506
+ const key = JSON.stringify([drain.creditId, drain.drainId, error.reason]);
1507
+ const seen = Array.isArray(current.drainRefundWarnings) ? current.drainRefundWarnings : [];
1508
+ if (seen.includes(key)) return { op: "noop", result: false };
1509
+ loggers.tempo.warn("tempo_session_drain_refund_unproven", {
1510
+ channel_id: drain.channelId,
1511
+ credit_id: drain.creditId,
1512
+ drain_id: drain.drainId,
1513
+ amount_at_stake_native: drain.drainedNative?.toString() ?? null,
1514
+ amount_at_stake_micro: drain.amountMicro,
1515
+ currency: drain.currency,
1516
+ reason: error.reason,
1517
+ error: error.message
1518
+ });
1519
+ return { op: "set", value: { ...current, drainRefundWarnings: [...seen, key] }, result: true };
1520
+ });
1521
+ return { outcome: "refund_unproven" };
1522
+ } catch (claimError) {
1523
+ failure = claimError;
1524
+ }
1525
+ }
1526
+ loggers.tempo.warn("tempo_session_close_reconcile_failed", {
1527
+ channel_id: drain.channelId,
1528
+ credit_id: drain.creditId,
1529
+ drain_id: drain.drainId,
1530
+ error: failure instanceof Error ? failure.message : String(failure)
1531
+ });
1532
+ return { outcome: "retryable_failure", error: failure };
1533
+ }
1534
+ };
1535
+ const reconcileFinalizedDrains = async () => {
1536
+ const ledger = creditLedger;
1537
+ if (!ledger || !finalizeDrain) return;
1538
+ let after;
1539
+ do {
1540
+ const page = await ledger.listChannelDrains({
1541
+ limit: 100,
1542
+ after,
1543
+ createdBeforeMs: Date.now(),
1544
+ rail: "tempo",
1545
+ includeResolved: true
1546
+ });
1547
+ for (const drain of page) {
1548
+ if (drain.status !== "exit_required" || !drain.channelId) continue;
1549
+ await reconcileFinalizedDrain(drain);
1550
+ }
1551
+ const last = page.at(-1);
1552
+ after = page.length === 100 && last ? { createdAt: last.createdAt, creditId: last.creditId, drainId: last.drainId } : void 0;
1553
+ } while (after);
1554
+ };
1555
+ const reconcileTempoSessionCloses = async () => {
1556
+ await reconcileUnsubmittedCloses();
1557
+ await reconcileFinalizedDrains();
1558
+ };
1559
+ const reportRecoveredClose = async (channel, txHash, settledBeforeClose, settledToPayee) => {
1298
1560
  if (settledBeforeClose === null || settledToPayee < settledBeforeClose) {
1299
1561
  loggers.tempo.warn("tempo_session_close_settlement_unreported", {
1300
1562
  channel_id: channel.channelId,
1301
- tx_hash: txHash
1563
+ tx_hash: txHash,
1564
+ amount_at_stake_native: settledToPayee.toString()
1302
1565
  });
1303
1566
  return;
1304
1567
  }
@@ -1314,6 +1577,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1314
1577
  loggers.tempo.warn("tempo_session_close_settlement_callback_failed", {
1315
1578
  channel_id: channel.channelId,
1316
1579
  tx_hash: txHash,
1580
+ amount_at_stake_native: (settledToPayee - settledBeforeClose).toString(),
1317
1581
  error: error instanceof Error ? error.message : String(error)
1318
1582
  });
1319
1583
  }
@@ -1326,7 +1590,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1326
1590
  const confirmClose = store.confirmClose?.bind(store);
1327
1591
  return Object.assign(mppx, {
1328
1592
  sweepTempoSessions,
1329
- ...config?.hostedObligations && { reconcileTempoSessionCloses: reconcileUnsubmittedCloses },
1593
+ ...config?.hostedObligations && { reconcileTempoSessionCloses },
1330
1594
  ...beginClose && {
1331
1595
  beginTempoSessionClose: async (channelId, drainId) => {
1332
1596
  await beginClose(channelId, drainId);
@@ -1412,11 +1676,15 @@ function advertisedMethods(mppx) {
1412
1676
  var _testing = {
1413
1677
  attachTempoSessionRuntime
1414
1678
  };
1679
+ function isTempoCloseTransaction(reference) {
1680
+ return typeof reference === "string" && /^0x[0-9a-fA-F]{64}$/.test(reference);
1681
+ }
1415
1682
 
1416
1683
  export {
1417
1684
  TEMPO_SETTLEMENT_DEFAULT_LOW_BALANCE_MICRO,
1418
1685
  TempoSettlementReadiness,
1419
1686
  isTempoInsufficientFundsError,
1687
+ TempoCloseDescriptorError,
1420
1688
  MPPX_HMAC_MISMATCH_REASON,
1421
1689
  TEMPO_SETTLEMENT_LEASE_FIELD,
1422
1690
  TEMPO_SETTLEMENT_LEASE_OWNER_FIELD,