@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.
- package/dist/{chunk-ILBSZMJL.js → chunk-2XQLL6N6.js} +1 -1
- package/dist/{chunk-UVS7MRJX.js → chunk-3Q6FTZDB.js} +2 -2
- package/dist/{chunk-OP7VCGQR.js → chunk-3R7NQA3Z.js} +1 -1
- package/dist/{chunk-RSNC5T6C.js → chunk-AJ6UHXUA.js} +1 -1
- package/dist/{chunk-ALWVJ2RB.js → chunk-EWZFANHJ.js} +3 -3
- package/dist/{chunk-J5J63WDC.js → chunk-FFXXGCVG.js} +3 -3
- package/dist/{chunk-UDJNZ3UG.js → chunk-JNECYURT.js} +1 -1
- package/dist/{chunk-IHQUBATH.js → chunk-KFCEXCZM.js} +3 -3
- package/dist/{chunk-NA2KOR6G.js → chunk-LDIPVPKI.js} +3 -3
- package/dist/{chunk-IOQIXRGM.js → chunk-M3T5JYDJ.js} +15 -8
- package/dist/{chunk-UWGBI5WK.js → chunk-MB6R4XPZ.js} +3 -3
- package/dist/{chunk-7PM4R4GD.js → chunk-MMGSYMIC.js} +1 -1
- package/dist/{chunk-QKE5NDPF.js → chunk-MU3IE434.js} +348 -80
- package/dist/{chunk-BFGQG4WY.js → chunk-QGNXYVBO.js} +52 -12
- package/dist/{chunk-JIZ4ZIXX.js → chunk-QNKK7SPY.js} +652 -245
- package/dist/{chunk-FM6QAGJC.js → chunk-STJBBVNL.js} +24 -8
- package/dist/{chunk-NU5RWJ5B.js → chunk-VMHYGKWT.js} +3 -3
- package/dist/{chunk-AVV5SYGO.js → chunk-VTHYQCDB.js} +1 -1
- package/dist/{chunk-ZEPJF4JC.js → chunk-VUGEEMLT.js} +4 -4
- package/dist/{chunk-2U6LAASN.js → chunk-W3VGCNG6.js} +1 -1
- package/dist/{chunk-G3MPHW5I.js → chunk-Y6DO5WSO.js} +3 -3
- package/dist/{chunk-AGE5XZKE.js → chunk-YOSZIWC3.js} +17 -4
- package/dist/{content-retention-B7ddtJ2y.d.ts → content-retention-CrILCNxk.d.ts} +1 -1
- package/dist/{credit-ledger-KIQIE6I6.js → credit-ledger-4PZNAMDT.js} +4 -4
- package/dist/{credit-menu-BH3g9GQa.d.ts → credit-menu-VwX_6KM7.d.ts} +4 -4
- package/dist/{fx-DN26_J7o.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 +16 -16
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +26 -26
- package/dist/{job-store-Cq21JT37.d.ts → job-store-92v8e1LQ.d.ts} +1 -1
- package/dist/{lightning-backend-S8F8nf8q.d.ts → lightning-backend-9oky_wJi.d.ts} +1 -1
- package/dist/{memory-credit-ledger-ATYFAYBG.js → memory-credit-ledger-RP7DT5IY.js} +5 -5
- package/dist/{mpp-setup-ZTORW3VF.js → mpp-setup-FURD45LI.js} +5 -3
- package/dist/{payout-reporter-NN6K2IVQ.js → payout-reporter-HVJ6WUF3.js} +2 -2
- package/dist/{postgres-consumed-credential-store-XJTE5PCA.js → postgres-consumed-credential-store-WO6OCFXZ.js} +3 -3
- package/dist/{postgres-job-store-AR6ZJUQV.js → postgres-job-store-FSRUPYH2.js} +2 -2
- package/dist/{postgres-replay-store-33K33QV7.js → postgres-replay-store-XXJSA457.js} +2 -2
- package/dist/{pricing-G5IN5YJE.js → pricing-X4XU5HXY.js} +3 -3
- package/dist/{revenue-reporter-KDUZLD5F.js → revenue-reporter-EV3K5L6B.js} +2 -2
- package/dist/server/index.d.ts +78 -11
- package/dist/server/index.js +87 -45
- package/dist/{step-cache-CR12EZ1Q.d.ts → step-cache--7yA5Ihr.d.ts} +99 -12
- package/dist/{tempo-charge-store-XEGJYM3G.js → tempo-charge-store-5JOIYUIH.js} +2 -2
- package/dist/{tempo-session-store-3WTDOO7H.js → tempo-session-store-S6QMLQUA.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +7 -7
- package/dist/{usd-s_VZ_eS7.d.ts → usd-C7lvLvLk.d.ts} +1 -1
- package/dist/{x402-HQ5GRQZU.js → x402-EN5LSOVM.js} +4 -4
- 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";
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
935
|
-
|
|
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:
|
|
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:
|
|
1000
|
+
result: { outcome: "claimed", channel }
|
|
949
1001
|
};
|
|
950
1002
|
});
|
|
951
|
-
if (
|
|
952
|
-
return
|
|
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 (
|
|
975
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
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
|
|
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(
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
|
1296
|
-
const
|
|
1297
|
-
|
|
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
|
|
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,
|