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