@dvmkit/sdk 0.10.1 → 0.12.0-rc.1
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-STJBBVNL.js → chunk-2VGMZ7VF.js} +487 -71
- package/dist/{chunk-3Q6FTZDB.js → chunk-3YFFZLW7.js} +2 -2
- package/dist/{chunk-VMHYGKWT.js → chunk-566CFXGU.js} +9 -1
- package/dist/chunk-5H7P3ORL.js +92 -0
- package/dist/{chunk-3R7NQA3Z.js → chunk-7KQLPVV7.js} +1 -1
- package/dist/{chunk-JNECYURT.js → chunk-D4FFS3IS.js} +1 -1
- package/dist/{chunk-VTHYQCDB.js → chunk-E7ZBCT5U.js} +1 -1
- package/dist/{chunk-MMGSYMIC.js → chunk-EP7OVWI5.js} +1 -1
- package/dist/{chunk-LDIPVPKI.js → chunk-FFRTAQUW.js} +1 -1
- package/dist/chunk-H3GGFEE5.js +57 -0
- package/dist/{chunk-M3T5JYDJ.js → chunk-HNXMWHLU.js} +1 -1
- package/dist/{chunk-UKD3R2KU.js → chunk-HW2UCOOC.js} +4 -55
- package/dist/{chunk-FFXXGCVG.js → chunk-ITRSITFP.js} +1 -1
- package/dist/{chunk-MU3IE434.js → chunk-JLT2YIBS.js} +267 -52
- package/dist/{chunk-KFCEXCZM.js → chunk-KJZVTWMX.js} +1 -1
- package/dist/{chunk-W3VGCNG6.js → chunk-LDKV4UHI.js} +1 -1
- package/dist/{chunk-AJ6UHXUA.js → chunk-M4NFYEEY.js} +1 -1
- package/dist/{chunk-MB6R4XPZ.js → chunk-NS2OMIU7.js} +15 -3
- package/dist/{chunk-YOSZIWC3.js → chunk-POYUX2NN.js} +149 -11
- package/dist/{chunk-VUGEEMLT.js → chunk-RPUFWIT4.js} +11 -89
- package/dist/{chunk-SYBFMHXQ.js → chunk-S7PIFHE4.js} +590 -67
- package/dist/{chunk-2OGBZPPJ.js → chunk-TWKQFIOW.js} +21 -3
- package/dist/{chunk-EWZFANHJ.js → chunk-VWIMQ5H5.js} +1 -1
- package/dist/{chunk-2XQLL6N6.js → chunk-WFCVS6QB.js} +2 -2
- package/dist/{chunk-QGNXYVBO.js → chunk-Z4FOPROB.js} +17 -0
- package/dist/{content-retention-CrILCNxk.d.ts → content-retention-a1yRdmhI.d.ts} +1 -1
- package/dist/{credit-ledger-4PZNAMDT.js → credit-ledger-MF23UKRJ.js} +6 -3
- package/dist/{credit-menu-BWsVqMh-.d.ts → credit-menu-B2T-O9oY.d.ts} +11 -4
- package/dist/{fx-BM5h4o5S.d.ts → fx-DLhscRUh.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +6 -5
- package/dist/internal/caller.d.ts +8 -6
- package/dist/internal/caller.js +27 -22
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +24 -22
- package/dist/{job-store-92v8e1LQ.d.ts → job-store-B8Ijr5V6.d.ts} +3 -1
- package/dist/{lightning-backend-9oky_wJi.d.ts → lightning-backend-DmwMSgaM.d.ts} +1 -1
- package/dist/memory-credit-ledger-ERD764PS.js +14 -0
- package/dist/{mpp-setup-FURD45LI.js → mpp-setup-JU3MT7QX.js} +3 -3
- package/dist/{payout-reporter-HVJ6WUF3.js → payout-reporter-NXWQVKXW.js} +2 -2
- package/dist/{postgres-consumed-credential-store-WO6OCFXZ.js → postgres-consumed-credential-store-WXJLM6J7.js} +1 -1
- package/dist/{postgres-job-store-H45PRBLA.js → postgres-job-store-ZVMAN6L2.js} +2 -2
- package/dist/{postgres-replay-store-XXJSA457.js → postgres-replay-store-QJ4FG6AR.js} +2 -2
- package/dist/{pricing-X4XU5HXY.js → pricing-KCLENT2C.js} +3 -3
- package/dist/{revenue-reporter-EV3K5L6B.js → revenue-reporter-HD4GMXGD.js} +2 -2
- package/dist/server/index.d.ts +30 -12
- package/dist/server/index.js +32 -30
- package/dist/{step-cache--7yA5Ihr.d.ts → step-cache-DQqpwXa8.d.ts} +206 -47
- package/dist/{tempo-charge-store-5JOIYUIH.js → tempo-charge-store-7LTUAQHQ.js} +2 -2
- package/dist/{tempo-session-store-S6QMLQUA.js → tempo-session-store-LYAIDR2B.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +6 -6
- package/dist/{usd-C7lvLvLk.d.ts → usd-D5kk2nIT.d.ts} +1 -1
- package/dist/{x402-EN5LSOVM.js → x402-RUJ4LUKF.js} +3 -3
- package/package.json +1 -1
- package/recipes/postgres/financial-export-v1.sql +19 -5
- package/dist/memory-credit-ledger-RP7DT5IY.js +0 -11
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loggers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HNXMWHLU.js";
|
|
4
4
|
import {
|
|
5
5
|
diagnosticConsole
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-Z4FOPROB.js";
|
|
7
7
|
|
|
8
8
|
// src/sdk/server/mpp-setup.ts
|
|
9
9
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -858,31 +858,186 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
858
858
|
const getClient = config?.getClient;
|
|
859
859
|
const listActive = store?.listActive;
|
|
860
860
|
if (!config || !getClient || !store) return mppx;
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
861
|
+
const retainRefundEvidence = async (drain, refundedNative, payerExit) => {
|
|
862
|
+
try {
|
|
863
|
+
if (payerExit) {
|
|
864
|
+
if (refundedNative > 0n) {
|
|
865
|
+
if (!creditLedger?.recordTempoDrainPayment) throw new Error("Tempo drain payment evidence persistence is unavailable");
|
|
866
|
+
await creditLedger.recordTempoDrainPayment({ creditId: drain.creditId, drainId: drain.drainId });
|
|
867
|
+
}
|
|
868
|
+
} else {
|
|
869
|
+
if (!creditLedger?.recordTempoDrainRefund) throw new Error("Tempo drain refund evidence persistence is unavailable");
|
|
870
|
+
await creditLedger.recordTempoDrainRefund({ creditId: drain.creditId, drainId: drain.drainId, refundedNative });
|
|
871
|
+
}
|
|
872
|
+
} catch (error) {
|
|
873
|
+
loggers.tempo.warn("tempo_drain_refund_evidence_write_failed", { amount_micro: drain.amountMicro });
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
const nextDrainCursor = (page, after) => {
|
|
878
|
+
const last = page.at(-1);
|
|
879
|
+
if (page.length < 100 || !last) return void 0;
|
|
880
|
+
const next = { createdAt: last.createdAt, creditId: last.creditId, drainId: last.drainId };
|
|
881
|
+
if (after && !(next.createdAt > after.createdAt || next.createdAt === after.createdAt && (next.creditId > after.creditId || next.creditId === after.creditId && next.drainId > after.drainId))) {
|
|
882
|
+
throw new Error("Tempo drain pagination cursor did not advance");
|
|
883
|
+
}
|
|
884
|
+
return next;
|
|
885
|
+
};
|
|
886
|
+
const resolveChannelDrainProof = async (channel, chainState, proof) => {
|
|
887
|
+
const ledger = creditLedger;
|
|
888
|
+
const key = channel.channelId.toLowerCase();
|
|
889
|
+
const matches = [];
|
|
890
|
+
let after;
|
|
891
|
+
const createdBeforeMs = Date.now();
|
|
892
|
+
do {
|
|
893
|
+
const page = await ledger.listChannelDrains({
|
|
894
|
+
limit: 100,
|
|
895
|
+
after,
|
|
896
|
+
createdBeforeMs,
|
|
897
|
+
rail: "tempo",
|
|
898
|
+
includeWrittenOff: true
|
|
899
|
+
});
|
|
900
|
+
const next = nextDrainCursor(page, after);
|
|
901
|
+
for (const drain of page) {
|
|
902
|
+
if (drain.method !== "tempo" || drain.channelId?.toLowerCase() !== key || drain.status !== "pending" && drain.status !== "exit_required" || drain.operatorDecision) continue;
|
|
903
|
+
const credit = await ledger.get(drain.creditId);
|
|
904
|
+
if (credit?.tempoChannelId?.toLowerCase() !== key) throw new Error("Tempo drain channel does not match its credit");
|
|
905
|
+
matches.push(drain);
|
|
906
|
+
}
|
|
907
|
+
after = next;
|
|
908
|
+
} while (after);
|
|
909
|
+
if (!matches.length) return;
|
|
910
|
+
const intent = await store.getClose?.(key);
|
|
911
|
+
const matching = intent ? matches.filter((drain) => intent.channelId.toLowerCase() === key && drain.drainId === intent.drainId) : matches;
|
|
912
|
+
if (matching.length !== 1) throw new Error("Tempo close proof has no unique matching open drain");
|
|
913
|
+
if (proof.outcome === "ours") {
|
|
914
|
+
return { drain: matching[0], refundedNative: proof.receipt.refundedToPayer, payerExit: false };
|
|
915
|
+
} else {
|
|
916
|
+
const stored = await store.get(key);
|
|
917
|
+
const deposit = isStateRecord(stored) && typeof stored.depositBeforeFinalization === "bigint" ? stored.depositBeforeFinalization : channel.deposit;
|
|
918
|
+
return { drain: matching[0], refundedNative: deposit - maxBigInt(channel.settledOnChain, chainState.settled), payerExit: true };
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
const protectFinalizedBacking = async (channel, chainState, proof, classifying = true) => {
|
|
922
|
+
await store.update(channel.channelId.toLowerCase(), (value) => {
|
|
923
|
+
const active = readTempoSessionState(value);
|
|
924
|
+
if (!active || !isStateRecord(value)) throw new Error("Tempo finalized channel state is unavailable");
|
|
925
|
+
return { op: "set", value: {
|
|
926
|
+
...value,
|
|
927
|
+
finalized: true,
|
|
928
|
+
deposit: 0n,
|
|
929
|
+
closeRequestedAt: 0n,
|
|
930
|
+
creditLossPending: classifying || !active.finalized || value.creditLossPending === true,
|
|
931
|
+
...!active.finalized && { depositBeforeFinalization: active.deposit },
|
|
932
|
+
settledOnChain: maxBigInt(active.settledOnChain, chainState.settled),
|
|
933
|
+
// Publish positive payment and finalization together. The operator's
|
|
934
|
+
// channel lock then sees payment even if the drain writer fails.
|
|
935
|
+
...proof && proof.refundedNative > 0n && { tempoDrainPaymentObservation: {
|
|
936
|
+
creditId: proof.drain.creditId,
|
|
937
|
+
drainId: proof.drain.drainId
|
|
938
|
+
} }
|
|
939
|
+
}, result: void 0 };
|
|
940
|
+
});
|
|
941
|
+
};
|
|
942
|
+
const deferHostedCloseSearch = async (channel) => {
|
|
943
|
+
const obligations = config.hostedObligations;
|
|
944
|
+
if (!obligations) return false;
|
|
945
|
+
const key = channel.channelId.toLowerCase();
|
|
946
|
+
const intent = await store.getClose?.(key);
|
|
947
|
+
if (isTempoCloseTransaction(intent?.reference)) return false;
|
|
948
|
+
const pending = await obligations.findPendingTempo(key, "close");
|
|
949
|
+
if (pending?.state !== "unmatched" || isTempoCloseTransaction(pending.tx_hash) || pending.rail !== "tempo" || pending.operation !== "close" || pending.channel_id.toLowerCase() !== key || pending.network !== `eip155:${channel.chainId}` || pending.contract.toLowerCase() !== channel.escrowContract.toLowerCase() || typeof pending.original.drainId !== "string" || !pending.original.drainId) return false;
|
|
950
|
+
if (intent && intent.drainId !== pending.original.drainId) return false;
|
|
951
|
+
return !isTempoCloseTransaction(await obligations.tempoCloseTransaction(key, pending.original.drainId));
|
|
952
|
+
};
|
|
953
|
+
const terminalizeLostBacking = async (channel, chainState, proof) => {
|
|
954
|
+
if (chainState.deposit !== 0n || !creditLedger) return;
|
|
864
955
|
const ledger = creditLedger;
|
|
956
|
+
const key = channel.channelId.toLowerCase();
|
|
957
|
+
let resolved = proof;
|
|
958
|
+
const headroom = channel.highestVoucherAmount - maxBigInt(channel.settledOnChain, chainState.settled);
|
|
959
|
+
if (!resolved && headroom > 0n) {
|
|
960
|
+
try {
|
|
961
|
+
const stored = await store.get(key);
|
|
962
|
+
const intent = await store.getClose?.(key);
|
|
963
|
+
const reference = intent && (isTempoCloseTransaction(intent.reference) ? intent.reference : await config.hostedObligations?.tempoCloseTransaction(key, intent.drainId));
|
|
964
|
+
if (isTempoCloseTransaction(reference)) {
|
|
965
|
+
const client = await getClient({ chainId: channel.chainId });
|
|
966
|
+
resolved = { outcome: "ours", receipt: await chainOps.readChannelClose(client, channel.channelId, reference) };
|
|
967
|
+
} else if (!await deferHostedCloseSearch(channel)) {
|
|
968
|
+
const client = await getClient({ chainId: channel.chainId });
|
|
969
|
+
const snapshot = isStateRecord(stored) ? stored : {};
|
|
970
|
+
const before = typeof snapshot.creditLossSearchBefore === "bigint" ? snapshot.creditLossSearchBefore : void 0;
|
|
971
|
+
const createdAt = channel.createdAt ? Date.parse(channel.createdAt) : 0;
|
|
972
|
+
const search = await chainOps.findChannelClose(
|
|
973
|
+
client,
|
|
974
|
+
channel.channelId,
|
|
975
|
+
channel.escrowContract,
|
|
976
|
+
Number.isFinite(createdAt) ? createdAt : 0,
|
|
977
|
+
before,
|
|
978
|
+
Date.now() + TEMPO_CLOSE_POLL_SEARCH_BUDGET_MS
|
|
979
|
+
);
|
|
980
|
+
if (search.outcome === "closed") {
|
|
981
|
+
resolved = cooperativeCloseSenders(stored, config.account.address).has(search.sender.toLowerCase()) ? { outcome: "ours", receipt: await chainOps.readChannelClose(client, channel.channelId, search.txHash) } : { outcome: "exit" };
|
|
982
|
+
} else if (search.outcome === "exit_requested") {
|
|
983
|
+
resolved = { outcome: "exit" };
|
|
984
|
+
} else if (search.outcome === "paused") {
|
|
985
|
+
await store.update(key, (value) => isStateRecord(value) ? { op: "set", value: { ...value, creditLossSearchBefore: search.resumeAt }, result: void 0 } : { op: "noop", result: void 0 });
|
|
986
|
+
} else if (before !== void 0) {
|
|
987
|
+
await store.update(key, (value) => isStateRecord(value) ? { op: "set", value: Object.fromEntries(Object.entries(value).filter(([field]) => field !== "creditLossSearchBefore")), result: void 0 } : { op: "noop", result: void 0 });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
} catch (error) {
|
|
991
|
+
loggers.tempo.warn("tempo_session_close_reconcile_failed", {
|
|
992
|
+
channel_id: key,
|
|
993
|
+
error: error instanceof Error ? error.message : String(error)
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const pending = !resolved && headroom > 0n;
|
|
998
|
+
let drainProof;
|
|
999
|
+
try {
|
|
1000
|
+
if (resolved) drainProof = await resolveChannelDrainProof(channel, chainState, resolved);
|
|
1001
|
+
} finally {
|
|
1002
|
+
await protectFinalizedBacking(channel, chainState, drainProof);
|
|
1003
|
+
}
|
|
1004
|
+
if (drainProof) await retainRefundEvidence(drainProof.drain, drainProof.refundedNative, drainProof.payerExit);
|
|
1005
|
+
if (pending) return;
|
|
1006
|
+
const refundedNative = resolved?.outcome === "ours" ? resolved.receipt.refundedToPayer : 0n;
|
|
865
1007
|
const retire = async (lifecycle, tx) => {
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
{
|
|
870
|
-
channelId: channel.channelId,
|
|
871
|
-
settledOnChainNative: settledOnChain,
|
|
872
|
-
highestVoucherNative: lifecycle.highestVoucherAmount,
|
|
873
|
-
consumedNative: lifecycle.spent
|
|
874
|
-
},
|
|
875
|
-
tx
|
|
1008
|
+
const settledOnChainNative = maxBigInt(
|
|
1009
|
+
maxBigInt(lifecycle.settledOnChain, chainState.settled),
|
|
1010
|
+
resolved?.outcome === "ours" ? resolved.receipt.settledToPayee : 0n
|
|
876
1011
|
);
|
|
1012
|
+
if (!resolved && lifecycle.highestVoucherAmount > settledOnChainNative) return;
|
|
1013
|
+
const lossNative = maxBigInt(0n, (resolved?.outcome === "exit" ? lifecycle.spent : lifecycle.highestVoucherAmount) - settledOnChainNative - refundedNative);
|
|
1014
|
+
const closeAware = ledger.tempoCreditLossEvidenceVersion === 1;
|
|
1015
|
+
if (lossNative > 0n && !closeAware) return;
|
|
1016
|
+
const candidates2 = lossNative > 0n ? await ledger.terminalizeTempoCredits({
|
|
1017
|
+
channelId: key,
|
|
1018
|
+
settledOnChainNative,
|
|
1019
|
+
highestVoucherNative: lifecycle.highestVoucherAmount,
|
|
1020
|
+
consumedNative: lifecycle.spent,
|
|
1021
|
+
...resolved?.outcome === "ours" && { closeKind: "cooperative_close", refundedToPayerNative: refundedNative },
|
|
1022
|
+
...resolved?.outcome === "exit" && { closeKind: "payer_exit" }
|
|
1023
|
+
}, tx) : closeAware ? await ledger.listTempoCreditLosses({ channelId: key, limit: 1e3 }, tx) : void 0;
|
|
1024
|
+
return { settledOnChainNative, refundedNative, lossNative, losses: candidates2 };
|
|
877
1025
|
};
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
);
|
|
1026
|
+
const result = store.withFinalizedCreditLoss ? await store.withFinalizedCreditLoss(key, (tx, lifecycle) => retire(lifecycle, tx)) : await retire(channel);
|
|
1027
|
+
if (!result) return;
|
|
1028
|
+
let candidates = result.losses;
|
|
1029
|
+
try {
|
|
1030
|
+
candidates ??= await ledger.listTempoCreditLosses({ channelId: key, limit: 1e3 });
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
loggers.tempo.warn("tempo_session_close_reconcile_failed", {
|
|
1033
|
+
channel_id: key,
|
|
1034
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1035
|
+
});
|
|
883
1036
|
return;
|
|
884
1037
|
}
|
|
885
|
-
|
|
1038
|
+
const losses = candidates.filter((loss) => loss.channelId.toLowerCase() === key);
|
|
1039
|
+
await store.update(key, (value) => isStateRecord(value) ? { op: "set", value: { ...value, creditLossPending: false }, result: void 0 } : { op: "noop", result: void 0 });
|
|
1040
|
+
return { ...result, losses };
|
|
886
1041
|
};
|
|
887
1042
|
const getTempoSessionDrainState = async (channelId) => {
|
|
888
1043
|
const normalized = channelId.toLowerCase();
|
|
@@ -1074,11 +1229,19 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1074
1229
|
[channel.channelId],
|
|
1075
1230
|
channel.escrowContract
|
|
1076
1231
|
);
|
|
1077
|
-
await terminalizeLostBacking(channel, chainState);
|
|
1232
|
+
const backing = await terminalizeLostBacking(channel, chainState);
|
|
1078
1233
|
const chainFinalized = await writeBackChainState(channel, chainState);
|
|
1079
|
-
const settledOnChain = maxBigInt(
|
|
1234
|
+
const settledOnChain = maxBigInt(
|
|
1235
|
+
maxBigInt(channel.settledOnChain, chainState.settled),
|
|
1236
|
+
backing?.settledOnChainNative ?? 0n
|
|
1237
|
+
);
|
|
1080
1238
|
if (chainFinalized) {
|
|
1081
|
-
|
|
1239
|
+
if (!backing && creditLedger) return {
|
|
1240
|
+
outcome: "retryable_failure",
|
|
1241
|
+
channelId: normalized,
|
|
1242
|
+
error: "Finalized channel loss evidence or ledger capability is unavailable; retry reconciliation"
|
|
1243
|
+
};
|
|
1244
|
+
const owed = backing?.lossNative ?? channel.highestVoucherAmount - settledOnChain;
|
|
1082
1245
|
return owed > 0n ? {
|
|
1083
1246
|
outcome: "finalized_before_protection",
|
|
1084
1247
|
channelId: normalized,
|
|
@@ -1160,10 +1323,22 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1160
1323
|
for (const row of rows) {
|
|
1161
1324
|
const state = readTempoSessionState(row.state);
|
|
1162
1325
|
if (!state) continue;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1326
|
+
try {
|
|
1327
|
+
if (creditLedger && state.finalized && state.highestVoucherAmount > state.settledOnChain && await deferHostedCloseSearch(state)) {
|
|
1328
|
+
inFlight += 1;
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
const groupKey = `${state.chainId}:${state.escrowContract.toLowerCase()}`;
|
|
1332
|
+
const group = groups.get(groupKey) ?? [];
|
|
1333
|
+
group.push(state);
|
|
1334
|
+
groups.set(groupKey, group);
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
inFlight += 1;
|
|
1337
|
+
loggers.tempo.warn("tempo_session_close_reconcile_failed", {
|
|
1338
|
+
channel_id: state.channelId,
|
|
1339
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1167
1342
|
}
|
|
1168
1343
|
for (const groupedChannels of groups.values()) {
|
|
1169
1344
|
for (let offset = 0; offset < groupedChannels.length; offset += 100) {
|
|
@@ -1176,26 +1351,41 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1176
1351
|
first.escrowContract
|
|
1177
1352
|
);
|
|
1178
1353
|
for (const [index, channel] of channels.entries()) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1354
|
+
try {
|
|
1355
|
+
const chainState = chainStates[index];
|
|
1356
|
+
await terminalizeLostBacking(channel, chainState);
|
|
1357
|
+
const chainFinalized = await writeBackChainState(channel, chainState);
|
|
1358
|
+
const forcedClose = !chainFinalized && BigInt(chainState.closeRequestedAt) !== 0n;
|
|
1359
|
+
if (forcedClose) closeRequested += 1;
|
|
1360
|
+
if (!tempoSettlementDue({
|
|
1361
|
+
channel,
|
|
1362
|
+
chainSettled: chainState.settled,
|
|
1363
|
+
forcedClose,
|
|
1364
|
+
intervalMs: config.settlementSchedule?.intervalMs
|
|
1365
|
+
})) {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const result = await settleChannel(channel.channelId, client, chainState.settled, forcedClose);
|
|
1369
|
+
if (result.outcome === "settled") settled += 1;
|
|
1370
|
+
else if (result.outcome === "retryable_failure") inFlight += 1;
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
inFlight += 1;
|
|
1373
|
+
loggers.tempo.warn("tempo_session_close_reconcile_failed", {
|
|
1374
|
+
channel_id: channel.channelId,
|
|
1375
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1376
|
+
});
|
|
1191
1377
|
}
|
|
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;
|
|
1195
1378
|
}
|
|
1196
1379
|
}
|
|
1197
1380
|
}
|
|
1198
|
-
|
|
1381
|
+
try {
|
|
1382
|
+
await reconcileTempoSessionCloses();
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
inFlight += 1;
|
|
1385
|
+
loggers.tempo.warn("tempo_session_close_reconcile_failed", {
|
|
1386
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1199
1389
|
return { scanned: rows.length, closeRequested, settled, inFlight };
|
|
1200
1390
|
};
|
|
1201
1391
|
const recoverTempoSessionClose = async (channelId, drainId, creditId) => {
|
|
@@ -1228,6 +1418,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1228
1418
|
finalized = chainState.deposit === 0n;
|
|
1229
1419
|
}
|
|
1230
1420
|
if (!finalized) return null;
|
|
1421
|
+
if (current.finalized) await terminalizeLostBacking(current, { deposit: 0n, settled: current.settledOnChain });
|
|
1231
1422
|
const pending = await config.hostedObligations?.pendingTempoClose(channelId, drainId);
|
|
1232
1423
|
const selfHostedReport = !config.hostedObligations && !!config.onSessionSettlement && !!store.claimCloseReference && !!store.getCloseRecovery;
|
|
1233
1424
|
let reference = pending?.txHash ?? void 0;
|
|
@@ -1388,6 +1579,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1388
1579
|
await terminalizeLostBacking(current, chainState);
|
|
1389
1580
|
if (!await writeBackChainState(current, chainState)) continue;
|
|
1390
1581
|
}
|
|
1582
|
+
if (current.finalized) await terminalizeLostBacking(current, { deposit: 0n, settled: current.settledOnChain });
|
|
1391
1583
|
const found = await findFinalizedClose(current, stored, row.drainId, row.reservedAtMs, {
|
|
1392
1584
|
from: row.searchBefore,
|
|
1393
1585
|
obligationId: row.id,
|
|
@@ -1421,9 +1613,8 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1421
1613
|
let receipt = isTempoCloseTransaction(txHash) ? await chainOps.readChannelClose(client, current.channelId, txHash) : void 0;
|
|
1422
1614
|
const chainState = receipt ? { deposit: 0n, settled: receipt.settledToPayee, closeRequestedAt: 0 } : (await chainOps.getChannelStatesBatch(client, [current.channelId], current.escrowContract))[0];
|
|
1423
1615
|
if (!chainState || chainState.deposit !== 0n) return { outcome: "open" };
|
|
1424
|
-
await
|
|
1425
|
-
|
|
1426
|
-
let found = receipt ? { outcome: "ours", txHash } : void 0;
|
|
1616
|
+
await protectFinalizedBacking(current, chainState, receipt ? { drain, refundedNative: receipt.refundedToPayer } : void 0, receipt !== void 0);
|
|
1617
|
+
let found = receipt && isTempoCloseTransaction(txHash) ? { outcome: "ours", txHash } : void 0;
|
|
1427
1618
|
if (found?.outcome === "ours" && pending) {
|
|
1428
1619
|
await config.hostedObligations.recordSubmitted(pending.id, found.txHash);
|
|
1429
1620
|
}
|
|
@@ -1449,10 +1640,17 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1449
1640
|
}
|
|
1450
1641
|
let refundedNative;
|
|
1451
1642
|
let reference;
|
|
1643
|
+
let backing;
|
|
1452
1644
|
if (found.outcome === "ours") {
|
|
1453
1645
|
reference = found.txHash;
|
|
1454
1646
|
receipt ??= await chainOps.readChannelClose(client, current.channelId, found.txHash);
|
|
1455
1647
|
refundedNative = receipt.refundedToPayer;
|
|
1648
|
+
await protectFinalizedBacking(current, chainState, { drain, refundedNative });
|
|
1649
|
+
await retainRefundEvidence(drain, refundedNative, false);
|
|
1650
|
+
backing = await terminalizeLostBacking(current, chainState, { outcome: "ours", receipt });
|
|
1651
|
+
if (!backing) {
|
|
1652
|
+
throw new Error("Tempo close-aware loss classification is unavailable; upgrade the ledger and retry");
|
|
1653
|
+
}
|
|
1456
1654
|
if (!config.hostedObligations && config.onSessionSettlement && store.claimCloseReference) {
|
|
1457
1655
|
const claimed = await store.claimCloseReference(drain.channelId, drain.drainId, found.txHash);
|
|
1458
1656
|
if (claimed.claimed) {
|
|
@@ -1462,8 +1660,17 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1462
1660
|
} else if (found.outcome === "exit") {
|
|
1463
1661
|
const snapshot = stored;
|
|
1464
1662
|
const deposit = typeof snapshot.depositBeforeFinalization === "bigint" ? snapshot.depositBeforeFinalization : current.deposit;
|
|
1465
|
-
if (deposit <= 0n)
|
|
1663
|
+
if (deposit <= 0n) {
|
|
1664
|
+
await terminalizeLostBacking(current, chainState, { outcome: "exit" });
|
|
1665
|
+
throw new TempoDrainRefundUnprovenError("collateral_missing", "The pre-exit collateral is unavailable");
|
|
1666
|
+
}
|
|
1466
1667
|
refundedNative = deposit - maxBigInt(current.settledOnChain, chainState.settled);
|
|
1668
|
+
await protectFinalizedBacking(current, chainState, { drain, refundedNative });
|
|
1669
|
+
await retainRefundEvidence(drain, refundedNative, true);
|
|
1670
|
+
backing = await terminalizeLostBacking(current, chainState, { outcome: "exit" });
|
|
1671
|
+
if (!backing) {
|
|
1672
|
+
throw new Error("Tempo close-aware loss classification is unavailable; upgrade the ledger and retry");
|
|
1673
|
+
}
|
|
1467
1674
|
reference = `channel:${current.channelId}`;
|
|
1468
1675
|
} else {
|
|
1469
1676
|
if (!config.hostedObligations && config.onSessionSettlement) {
|
|
@@ -1479,6 +1686,8 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1479
1686
|
throw new TempoDrainRefundUnprovenError("refund_short", "Proven refund is below the drain debit");
|
|
1480
1687
|
}
|
|
1481
1688
|
const creditUnbacked = (await ledger.get(drain.creditId))?.status === "unbacked";
|
|
1689
|
+
const retainedLoss = backing?.losses.find((loss) => loss.creditId === drain.creditId);
|
|
1690
|
+
const builderLossNative = retainedLoss?.lossNative ?? (retainedLoss ? maxBigInt(0n, retainedLoss.highestVoucherNative - retainedLoss.settledOnChainNative - (retainedLoss.refundedToPayerNative ?? 0n)) : 0n);
|
|
1482
1691
|
await store.confirmClose?.(drain.channelId, drain.drainId, reference);
|
|
1483
1692
|
const changed = await finalizeDrain(drain, {
|
|
1484
1693
|
reference,
|
|
@@ -1493,7 +1702,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1493
1702
|
drain_id: drain.drainId,
|
|
1494
1703
|
amount_at_stake_native: drain.drainedNative.toString(),
|
|
1495
1704
|
refund_native: refundedNative.toString(),
|
|
1496
|
-
builder_loss_native:
|
|
1705
|
+
builder_loss_native: builderLossNative.toString()
|
|
1497
1706
|
});
|
|
1498
1707
|
}
|
|
1499
1708
|
return { outcome: "completed" };
|
|
@@ -1501,6 +1710,11 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1501
1710
|
let failure = error;
|
|
1502
1711
|
if (error instanceof TempoDrainRefundUnprovenError) {
|
|
1503
1712
|
try {
|
|
1713
|
+
try {
|
|
1714
|
+
await ledger.recordTempoDrainHold?.({ creditId: drain.creditId, drainId: drain.drainId, reason: error.reason });
|
|
1715
|
+
} catch {
|
|
1716
|
+
loggers.tempo.warn("tempo_drain_hold_reason_write_failed", { amount_micro: drain.amountMicro });
|
|
1717
|
+
}
|
|
1504
1718
|
await store.update(drain.channelId, (current) => {
|
|
1505
1719
|
if (!isStateRecord(current)) throw new Error("Channel history is unavailable");
|
|
1506
1720
|
const key = JSON.stringify([drain.creditId, drain.drainId, error.reason]);
|
|
@@ -1536,20 +1750,21 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
|
|
|
1536
1750
|
const ledger = creditLedger;
|
|
1537
1751
|
if (!ledger || !finalizeDrain) return;
|
|
1538
1752
|
let after;
|
|
1753
|
+
const createdBeforeMs = Date.now();
|
|
1539
1754
|
do {
|
|
1540
1755
|
const page = await ledger.listChannelDrains({
|
|
1541
1756
|
limit: 100,
|
|
1542
1757
|
after,
|
|
1543
|
-
createdBeforeMs
|
|
1758
|
+
createdBeforeMs,
|
|
1544
1759
|
rail: "tempo",
|
|
1545
1760
|
includeResolved: true
|
|
1546
1761
|
});
|
|
1762
|
+
const next = nextDrainCursor(page, after);
|
|
1547
1763
|
for (const drain of page) {
|
|
1548
1764
|
if (drain.status !== "exit_required" || !drain.channelId) continue;
|
|
1549
1765
|
await reconcileFinalizedDrain(drain);
|
|
1550
1766
|
}
|
|
1551
|
-
|
|
1552
|
-
after = page.length === 100 && last ? { createdAt: last.createdAt, creditId: last.creditId, drainId: last.drainId } : void 0;
|
|
1767
|
+
after = next;
|
|
1553
1768
|
} while (after);
|
|
1554
1769
|
};
|
|
1555
1770
|
const reconcileTempoSessionCloses = async () => {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./chunk-COYKJRMM.js";
|
|
8
8
|
import {
|
|
9
9
|
withPostgresTransactionRecovery
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-EP7OVWI5.js";
|
|
11
11
|
|
|
12
12
|
// src/sdk/server/tempo-session-store.ts
|
|
13
13
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -243,7 +243,8 @@ var PostgresTempoSessionStore = class {
|
|
|
243
243
|
const { rows } = await this.pool.query(
|
|
244
244
|
`SELECT channel_key, state_json, updated_at
|
|
245
245
|
FROM mpp_tempo_channels
|
|
246
|
-
WHERE (state_json::jsonb ->> 'finalized') IS DISTINCT FROM 'true'
|
|
246
|
+
WHERE ((state_json::jsonb ->> 'finalized') IS DISTINCT FROM 'true'
|
|
247
|
+
OR state_json::jsonb ->> 'creditLossPending' = 'true')
|
|
247
248
|
AND ($2::BIGINT IS NULL OR (updated_at, channel_key) > ($2, $3))
|
|
248
249
|
ORDER BY updated_at, channel_key
|
|
249
250
|
LIMIT $1`,
|
|
@@ -284,10 +285,13 @@ var PostgresTempoSessionStore = class {
|
|
|
284
285
|
);
|
|
285
286
|
const state = channel.rows[0] ? parseStoreValue(channel.rows[0].state_json) : null;
|
|
286
287
|
const settled = state?.settledOnChain;
|
|
288
|
+
if (typeof settled !== "bigint" || !/^\d{1,78}$/.test(settled.toString())) {
|
|
289
|
+
throw new Error("Tempo channel settlement snapshot is unavailable or invalid; refusing close broadcast");
|
|
290
|
+
}
|
|
287
291
|
await tx.query(
|
|
288
292
|
`UPDATE mpp_tempo_close_intents SET settled_before_close = $3
|
|
289
293
|
WHERE channel_id = $1 AND drain_id = $2`,
|
|
290
|
-
[channelId.toLowerCase(), drainId,
|
|
294
|
+
[channelId.toLowerCase(), drainId, settled.toString()]
|
|
291
295
|
);
|
|
292
296
|
return record;
|
|
293
297
|
});
|
|
@@ -521,7 +525,15 @@ function closeRecord(row) {
|
|
|
521
525
|
}
|
|
522
526
|
function assertMonotonicTransition(current, next) {
|
|
523
527
|
if (!isRecord(next)) throw new Error("Tempo session state must be an object");
|
|
528
|
+
const observation = next.tempoDrainPaymentObservation;
|
|
529
|
+
if (observation !== void 0 && (!isRecord(observation) || typeof observation.creditId !== "string" || !observation.creditId || typeof observation.drainId !== "string" || !observation.drainId)) {
|
|
530
|
+
throw new Error("Tempo drain payment observation requires a credit and drain identity");
|
|
531
|
+
}
|
|
524
532
|
if (!isRecord(current)) return;
|
|
533
|
+
const retained = current.tempoDrainPaymentObservation;
|
|
534
|
+
if (retained !== void 0 && (!isRecord(retained) || !isRecord(observation) || retained.creditId !== observation.creditId || retained.drainId !== observation.drainId)) {
|
|
535
|
+
throw new Error("Tempo drain payment observation cannot be erased or rebound");
|
|
536
|
+
}
|
|
525
537
|
for (const field of [
|
|
526
538
|
"channelId",
|
|
527
539
|
"chainId",
|