@dvmkit/sdk 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4VV7WACY.js → chunk-2XQLL6N6.js} +1 -1
- package/dist/{chunk-LCCG5HQM.js → chunk-3Q6FTZDB.js} +2 -2
- package/dist/{chunk-TRY7BJA4.js → chunk-3R7NQA3Z.js} +1 -1
- package/dist/{chunk-KGL3YMZA.js → chunk-AJ6UHXUA.js} +1 -1
- package/dist/{chunk-BAQRUYRL.js → chunk-EWZFANHJ.js} +1 -1
- package/dist/{chunk-K2MSIKMM.js → chunk-FFXXGCVG.js} +1 -1
- package/dist/{chunk-WLPLHXNP.js → chunk-JNECYURT.js} +1 -1
- package/dist/{chunk-TFDR2YP2.js → chunk-KFCEXCZM.js} +1 -1
- package/dist/{chunk-IYQQXEHV.js → chunk-LDIPVPKI.js} +1 -1
- package/dist/{chunk-QSZW2I5Y.js → chunk-M3T5JYDJ.js} +15 -8
- package/dist/{chunk-Q3VYRU5R.js → chunk-MB6R4XPZ.js} +1 -1
- package/dist/{chunk-IBX2ORBV.js → chunk-MMGSYMIC.js} +1 -1
- package/dist/{chunk-LFVIZH2G.js → chunk-MU3IE434.js} +278 -152
- package/dist/{chunk-OIP44KBB.js → chunk-QGNXYVBO.js} +47 -12
- package/dist/{chunk-73C4JN2E.js → chunk-QNKK7SPY.js} +273 -54
- package/dist/{chunk-YYRY32NR.js → chunk-STJBBVNL.js} +2 -2
- package/dist/{chunk-WB3E7KE7.js → chunk-VMHYGKWT.js} +1 -1
- package/dist/{chunk-GAZBBMAA.js → chunk-VTHYQCDB.js} +1 -1
- package/dist/{chunk-LW7KZQHG.js → chunk-VUGEEMLT.js} +1 -1
- package/dist/{chunk-HC4I7LAV.js → chunk-W3VGCNG6.js} +1 -1
- package/dist/{chunk-GODOH2AF.js → chunk-Y6DO5WSO.js} +1 -1
- package/dist/{chunk-ZBQKXAYK.js → chunk-YOSZIWC3.js} +1 -1
- package/dist/{content-retention-CVBYehRS.d.ts → content-retention-CrILCNxk.d.ts} +1 -1
- package/dist/{credit-ledger-CZQ56DYI.js → credit-ledger-4PZNAMDT.js} +3 -3
- package/dist/{credit-menu-QntEhZJS.d.ts → credit-menu-VwX_6KM7.d.ts} +4 -4
- package/dist/{fx-CMyNFenZ.d.ts → fx-BM5h4o5S.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/internal/caller.d.ts +7 -7
- package/dist/internal/caller.js +7 -7
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +20 -20
- package/dist/{job-store-qOD-FrQF.d.ts → job-store-92v8e1LQ.d.ts} +1 -1
- package/dist/{lightning-backend-DtdG6UT5.d.ts → lightning-backend-9oky_wJi.d.ts} +1 -1
- package/dist/{memory-credit-ledger-EMN7G3MO.js → memory-credit-ledger-RP7DT5IY.js} +4 -4
- package/dist/{mpp-setup-VW53KFPO.js → mpp-setup-FURD45LI.js} +3 -3
- package/dist/{payout-reporter-TZX2S7KG.js → payout-reporter-HVJ6WUF3.js} +2 -2
- package/dist/{postgres-consumed-credential-store-V464OAJB.js → postgres-consumed-credential-store-WO6OCFXZ.js} +1 -1
- package/dist/{postgres-job-store-27VOCSPO.js → postgres-job-store-FSRUPYH2.js} +2 -2
- package/dist/{postgres-replay-store-7GJFAQXY.js → postgres-replay-store-XXJSA457.js} +2 -2
- package/dist/{pricing-NNMBNP7N.js → pricing-X4XU5HXY.js} +3 -3
- package/dist/{revenue-reporter-4EAVPWXS.js → revenue-reporter-EV3K5L6B.js} +2 -2
- package/dist/server/index.d.ts +74 -11
- package/dist/server/index.js +65 -27
- package/dist/{step-cache-B7svmPz3.d.ts → step-cache--7yA5Ihr.d.ts} +48 -6
- package/dist/{tempo-charge-store-2RPOJBM2.js → tempo-charge-store-5JOIYUIH.js} +2 -2
- package/dist/{tempo-session-store-LI3Q3A3U.js → tempo-session-store-S6QMLQUA.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +6 -6
- package/dist/{usd-D3izFz71.d.ts → usd-C7lvLvLk.d.ts} +1 -1
- package/dist/{x402-SL36LSOD.js → x402-EN5LSOVM.js} +3 -3
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loggers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-M3T5JYDJ.js";
|
|
4
4
|
import {
|
|
5
5
|
diagnosticConsole
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QGNXYVBO.js";
|
|
7
7
|
|
|
8
8
|
// src/sdk/server/mpp-setup.ts
|
|
9
9
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -663,6 +663,10 @@ function wrapMppx(mppx) {
|
|
|
663
663
|
var TEMPO_CLOSE_LOG_WINDOW = 5000n;
|
|
664
664
|
var TEMPO_CLOSE_LOG_SLACK_MS = 10 * 60 * 1e3;
|
|
665
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;
|
|
666
670
|
var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
|
|
667
671
|
getChannelStatesBatch: TempoSession.Precompile.Chain.getChannelStatesBatch,
|
|
668
672
|
settle: TempoSession.Server.settle,
|
|
@@ -673,7 +677,7 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
|
|
|
673
677
|
channelId
|
|
674
678
|
)
|
|
675
679
|
),
|
|
676
|
-
findChannelClose: async (client, channelId, escrowContract, notBeforeMs, startBlock) => {
|
|
680
|
+
findChannelClose: async (client, channelId, escrowContract, notBeforeMs, startBlock, deadlineMs = Date.now() + TEMPO_CLOSE_SEARCH_BUDGET_MS) => {
|
|
677
681
|
const { getBlock, getBlockNumber, getLogs, getTransaction } = await import("viem/actions");
|
|
678
682
|
const eventNamed = (name) => {
|
|
679
683
|
const event = TempoSession.Precompile.escrowAbi.find(
|
|
@@ -691,6 +695,8 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
|
|
|
691
695
|
}
|
|
692
696
|
let toBlock = startBlock ?? await getBlockNumber(client);
|
|
693
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 };
|
|
694
700
|
const fromBlock = toBlock >= TEMPO_CLOSE_LOG_WINDOW ? toBlock - TEMPO_CLOSE_LOG_WINDOW + 1n : 0n;
|
|
695
701
|
const range = { address: escrowContract, args: { channelId }, fromBlock, toBlock };
|
|
696
702
|
const [closed, requested, cancelled] = await Promise.all([
|
|
@@ -727,6 +733,7 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
|
|
|
727
733
|
function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
728
734
|
let creditLedger;
|
|
729
735
|
let finalizeDrain;
|
|
736
|
+
const finalizedCloseSearches = /* @__PURE__ */ new Map();
|
|
730
737
|
Object.assign(mppx, {
|
|
731
738
|
useTempoCreditLedger: (ledger, finalizer) => {
|
|
732
739
|
creditLedger = ledger;
|
|
@@ -942,18 +949,46 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
942
949
|
};
|
|
943
950
|
const settlingHere = /* @__PURE__ */ new Set();
|
|
944
951
|
const leaseMs = config.settlementSchedule?.leaseMs ?? TEMPO_SETTLEMENT_LEASE_MS;
|
|
945
|
-
const claimSettlement = async (channelId) => {
|
|
946
|
-
|
|
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;
|
|
947
959
|
settlingHere.add(channelId);
|
|
948
960
|
const leaseOwner = randomUUID();
|
|
949
961
|
const leaseUntil = Date.now() + leaseMs;
|
|
950
962
|
try {
|
|
951
|
-
const
|
|
952
|
-
|
|
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
|
+
}
|
|
953
968
|
const until = current[TEMPO_SETTLEMENT_LEASE_FIELD];
|
|
954
969
|
const owner = current[TEMPO_SETTLEMENT_LEASE_OWNER_FIELD];
|
|
955
970
|
if (typeof until === "number" && until > Date.now() && owner !== leaseOwner) {
|
|
956
|
-
return { op: "noop", result:
|
|
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 } };
|
|
957
992
|
}
|
|
958
993
|
return {
|
|
959
994
|
op: "set",
|
|
@@ -962,11 +997,11 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
962
997
|
[TEMPO_SETTLEMENT_LEASE_FIELD]: leaseUntil,
|
|
963
998
|
[TEMPO_SETTLEMENT_LEASE_OWNER_FIELD]: leaseOwner
|
|
964
999
|
},
|
|
965
|
-
result:
|
|
1000
|
+
result: { outcome: "claimed", channel }
|
|
966
1001
|
};
|
|
967
1002
|
});
|
|
968
|
-
if (
|
|
969
|
-
return
|
|
1003
|
+
if (claim.outcome !== "claimed") settlingHere.delete(channelId);
|
|
1004
|
+
return claim;
|
|
970
1005
|
} catch (err) {
|
|
971
1006
|
settlingHere.delete(channelId);
|
|
972
1007
|
throw err;
|
|
@@ -988,8 +1023,10 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
988
1023
|
settlingHere.delete(channelId);
|
|
989
1024
|
}
|
|
990
1025
|
};
|
|
991
|
-
const settleChannel = async (
|
|
992
|
-
|
|
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;
|
|
993
1030
|
try {
|
|
994
1031
|
const recoveryVersion = await config.beforeSessionSettlement?.();
|
|
995
1032
|
const reservation = await config.hostedObligations?.reserveTempo(channel.channelId, "settle");
|
|
@@ -1024,7 +1061,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1024
1061
|
throw err;
|
|
1025
1062
|
}
|
|
1026
1063
|
await releaseSettlement(channel.channelId);
|
|
1027
|
-
return
|
|
1064
|
+
return { outcome: "settled", channelId, settledAmount: channel.highestVoucherAmount };
|
|
1028
1065
|
};
|
|
1029
1066
|
const settleTempoSessionChannel = async (channelId) => {
|
|
1030
1067
|
const normalized = channelId.toLowerCase();
|
|
@@ -1065,18 +1102,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1065
1102
|
})) {
|
|
1066
1103
|
return { outcome: "already_safe", channelId: normalized, settledOnChain };
|
|
1067
1104
|
}
|
|
1068
|
-
|
|
1069
|
-
return {
|
|
1070
|
-
outcome: "retryable_failure",
|
|
1071
|
-
channelId: normalized,
|
|
1072
|
-
error: TEMPO_SETTLEMENT_IN_FLIGHT
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1075
|
-
return {
|
|
1076
|
-
outcome: "settled",
|
|
1077
|
-
channelId: normalized,
|
|
1078
|
-
settledAmount: channel.highestVoucherAmount
|
|
1079
|
-
};
|
|
1105
|
+
return await settleChannel(normalized, client, chainState.settled, forcedClose);
|
|
1080
1106
|
} catch (err) {
|
|
1081
1107
|
return {
|
|
1082
1108
|
outcome: "retryable_failure",
|
|
@@ -1163,18 +1189,29 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1163
1189
|
})) {
|
|
1164
1190
|
continue;
|
|
1165
1191
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
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;
|
|
1168
1195
|
}
|
|
1169
1196
|
}
|
|
1170
1197
|
}
|
|
1171
1198
|
await reconcileTempoSessionCloses();
|
|
1172
1199
|
return { scanned: rows.length, closeRequested, settled, inFlight };
|
|
1173
1200
|
};
|
|
1174
|
-
const recoverTempoSessionClose = async (channelId, drainId) => {
|
|
1201
|
+
const recoverTempoSessionClose = async (channelId, drainId, creditId) => {
|
|
1175
1202
|
if (!store.getClose || !store.confirmClose) return null;
|
|
1176
1203
|
const intent = await store.getClose(channelId);
|
|
1177
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
|
+
}
|
|
1178
1215
|
if (intent.reference) return intent.reference;
|
|
1179
1216
|
const stored = await store.get(channelId);
|
|
1180
1217
|
const current = readTempoSessionState(stored);
|
|
@@ -1199,13 +1236,21 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1199
1236
|
const notBeforeMs = pending?.reservedAtMs ?? recovery?.updatedAtMs;
|
|
1200
1237
|
if (notBeforeMs === void 0) throw new Error(`Tempo session close intent ${channelId} not found`);
|
|
1201
1238
|
const obligations = config.hostedObligations;
|
|
1202
|
-
const found = await findFinalizedClose(
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
+
);
|
|
1209
1254
|
if (pending) {
|
|
1210
1255
|
reference = await settleUnsubmittedClose(current.channelId, drainId, pending.id, found);
|
|
1211
1256
|
} else if (found.outcome === "ours") {
|
|
@@ -1231,30 +1276,67 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1231
1276
|
);
|
|
1232
1277
|
return recovered.reference;
|
|
1233
1278
|
};
|
|
1234
|
-
const findFinalizedClose = async (current, stored, drainId, notBeforeMs, cursor) => {
|
|
1235
|
-
const
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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`);
|
|
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;
|
|
1251
1285
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
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);
|
|
1254
1339
|
}
|
|
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
1340
|
};
|
|
1259
1341
|
const settleUnsubmittedClose = async (channelId, drainId, obligationId, found) => {
|
|
1260
1342
|
const obligations = config.hostedObligations;
|
|
@@ -1291,6 +1373,11 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1291
1373
|
const stored = await store.get(row.channelId);
|
|
1292
1374
|
const current = readTempoSessionState(stored);
|
|
1293
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
|
+
}
|
|
1294
1381
|
if (!current.finalized) {
|
|
1295
1382
|
const client = await getClient({ chainId: current.chainId });
|
|
1296
1383
|
const [chainState] = await chainOps.getChannelStatesBatch(
|
|
@@ -1303,6 +1390,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1303
1390
|
}
|
|
1304
1391
|
const found = await findFinalizedClose(current, stored, row.drainId, row.reservedAtMs, {
|
|
1305
1392
|
from: row.searchBefore,
|
|
1393
|
+
obligationId: row.id,
|
|
1306
1394
|
save: (resumeAt) => obligations.recordTempoCloseSearch(row.id, resumeAt)
|
|
1307
1395
|
});
|
|
1308
1396
|
await settleUnsubmittedClose(current.channelId, row.drainId, row.id, found);
|
|
@@ -1316,6 +1404,134 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1316
1404
|
}
|
|
1317
1405
|
}
|
|
1318
1406
|
};
|
|
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
|
+
};
|
|
1319
1535
|
const reconcileFinalizedDrains = async () => {
|
|
1320
1536
|
const ledger = creditLedger;
|
|
1321
1537
|
if (!ledger || !finalizeDrain) return;
|
|
@@ -1330,100 +1546,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1330
1546
|
});
|
|
1331
1547
|
for (const drain of page) {
|
|
1332
1548
|
if (drain.status !== "exit_required" || !drain.channelId) continue;
|
|
1333
|
-
|
|
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
|
-
}
|
|
1549
|
+
await reconcileFinalizedDrain(drain);
|
|
1427
1550
|
}
|
|
1428
1551
|
const last = page.at(-1);
|
|
1429
1552
|
after = page.length === 100 && last ? { createdAt: last.createdAt, creditId: last.creditId, drainId: last.drainId } : void 0;
|
|
@@ -1553,6 +1676,9 @@ function advertisedMethods(mppx) {
|
|
|
1553
1676
|
var _testing = {
|
|
1554
1677
|
attachTempoSessionRuntime
|
|
1555
1678
|
};
|
|
1679
|
+
function isTempoCloseTransaction(reference) {
|
|
1680
|
+
return typeof reference === "string" && /^0x[0-9a-fA-F]{64}$/.test(reference);
|
|
1681
|
+
}
|
|
1556
1682
|
|
|
1557
1683
|
export {
|
|
1558
1684
|
TEMPO_SETTLEMENT_DEFAULT_LOW_BALANCE_MICRO,
|