mppx 0.13.2 → 0.13.4
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/CHANGELOG.md +21 -0
- package/dist/cli/cli.js +3 -3
- package/dist/cli/plugins/index.js +1 -1
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.js +2 -1
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.js +6 -3
- package/dist/server/internal/challengeMeta.d.ts +5 -0
- package/dist/server/internal/challengeMeta.js +14 -0
- package/dist/tempo/server/Methods.d.ts +5 -0
- package/dist/tempo/server/Methods.js +5 -1
- package/dist/tempo/server/index.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/client/Transports.js +39 -12
- package/dist/tempo/session/precompile/Chain.d.ts +4 -0
- package/dist/tempo/session/precompile/Chain.js +8 -2
- package/dist/tempo/session/server/ChannelStore.d.ts +8 -0
- package/dist/tempo/session/server/ChannelStore.js +17 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts +3 -1
- package/dist/tempo/session/server/CredentialVerification.js +56 -11
- package/dist/tempo/session/server/Session.d.ts +4 -2
- package/dist/tempo/session/server/Session.js +23 -5
- package/dist/tempo/session/server/Settlement.d.ts +40 -1
- package/dist/tempo/session/server/Settlement.js +105 -34
- package/dist/tempo/session/server/index.d.ts +1 -1
- package/dist/x402/server/EvmCharge.js +10 -3
- package/package.json +1 -1
|
@@ -679,11 +679,12 @@ export async function probeWebSocketSession(parameters) {
|
|
|
679
679
|
}
|
|
680
680
|
/** Probes a WebSocket endpoint over HTTP and creates the opening credential. */
|
|
681
681
|
export async function prepareWebSocketSession(parameters) {
|
|
682
|
+
parameters.signal?.throwIfAborted();
|
|
682
683
|
const probed = await probeWebSocketSession(parameters);
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
};
|
|
684
|
+
parameters.signal?.throwIfAborted();
|
|
685
|
+
const credential = await parameters.createSessionCredential(probed.challenge, {});
|
|
686
|
+
parameters.signal?.throwIfAborted();
|
|
687
|
+
return { ...probed, credential };
|
|
687
688
|
}
|
|
688
689
|
/** Creates the initial runtime state for a paid WebSocket from its opening credential. */
|
|
689
690
|
export function createActiveSocketSession(parameters) {
|
|
@@ -741,6 +742,8 @@ export function validateSocketPaymentReceipt(parameters) {
|
|
|
741
742
|
*/
|
|
742
743
|
export async function openWebSocketSession(parameters) {
|
|
743
744
|
const { challenge, credential, options, WebSocket: WebSocketImpl, wsUrl } = parameters;
|
|
745
|
+
const signal = options?.signal;
|
|
746
|
+
signal?.throwIfAborted();
|
|
744
747
|
const rawSocket = new WebSocketImpl(wsUrl, options?.protocols);
|
|
745
748
|
const socketState = createActiveSocketSession({
|
|
746
749
|
challenge,
|
|
@@ -757,7 +760,13 @@ export async function openWebSocketSession(parameters) {
|
|
|
757
760
|
rawSocket.close(ClientWebSocketProtocolErrorCloseCode, message);
|
|
758
761
|
}
|
|
759
762
|
};
|
|
763
|
+
const onAbort = () => {
|
|
764
|
+
parameters.rejectReceipt(new Error('WebSocket payment flow aborted.'));
|
|
765
|
+
parameters.rejectCloseReady(new Error('WebSocket payment flow aborted.'));
|
|
766
|
+
rawSocket.close();
|
|
767
|
+
};
|
|
760
768
|
rawSocket.addEventListener('close', (event) => {
|
|
769
|
+
signal?.removeEventListener('abort', onAbort);
|
|
761
770
|
socketState.socket = null;
|
|
762
771
|
socketState.expectedCloseAmount = null;
|
|
763
772
|
parameters.rejectReceipt(new Error('WebSocket closed before the payment flow completed.'));
|
|
@@ -785,29 +794,47 @@ export async function openWebSocketSession(parameters) {
|
|
|
785
794
|
socketState,
|
|
786
795
|
});
|
|
787
796
|
});
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
797
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
798
|
+
try {
|
|
799
|
+
await waitForSocketOpen(rawSocket, managedSocket.emit, wsUrl, signal);
|
|
800
|
+
signal?.throwIfAborted();
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
signal?.removeEventListener('abort', onAbort);
|
|
791
804
|
rawSocket.close();
|
|
792
|
-
|
|
793
|
-
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
794
807
|
rawSocket.send(Ws.formatAuthorizationMessage(credential));
|
|
795
808
|
await parameters.waitForReceipt();
|
|
796
809
|
return managedSocket.socket;
|
|
797
810
|
}
|
|
798
|
-
function waitForSocketOpen(rawSocket, managedEmit, wsUrl) {
|
|
811
|
+
function waitForSocketOpen(rawSocket, managedEmit, wsUrl, signal) {
|
|
799
812
|
return new Promise((resolve, reject) => {
|
|
800
|
-
const
|
|
813
|
+
const cleanup = () => {
|
|
814
|
+
rawSocket.removeEventListener('open', onOpen);
|
|
801
815
|
rawSocket.removeEventListener('error', onError);
|
|
816
|
+
rawSocket.removeEventListener('close', onError);
|
|
817
|
+
signal?.removeEventListener('abort', onAbort);
|
|
818
|
+
};
|
|
819
|
+
const onOpen = () => {
|
|
820
|
+
cleanup();
|
|
802
821
|
managedEmit('open', { type: 'open' });
|
|
803
822
|
resolve();
|
|
804
823
|
};
|
|
805
824
|
const onError = () => {
|
|
806
|
-
|
|
825
|
+
cleanup();
|
|
807
826
|
reject(new Error(`WebSocket connection to ${wsUrl} failed to open.`));
|
|
808
827
|
};
|
|
828
|
+
const onAbort = () => {
|
|
829
|
+
cleanup();
|
|
830
|
+
reject(signal?.reason);
|
|
831
|
+
};
|
|
809
832
|
rawSocket.addEventListener('open', onOpen, { once: true });
|
|
810
833
|
rawSocket.addEventListener('error', onError, { once: true });
|
|
834
|
+
rawSocket.addEventListener('close', onError, { once: true });
|
|
835
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
836
|
+
if (signal?.aborted)
|
|
837
|
+
onAbort();
|
|
811
838
|
});
|
|
812
839
|
}
|
|
813
840
|
async function handleSocketMessage(parameters) {
|
|
@@ -71,6 +71,8 @@ export type TopUpReceiptFields = {
|
|
|
71
71
|
};
|
|
72
72
|
/** Typed fields decoded from a Settled receipt event. */
|
|
73
73
|
export type SettledReceiptFields = {
|
|
74
|
+
/** Amount this settlement paid to the payee. */
|
|
75
|
+
deltaPaid: bigint;
|
|
74
76
|
/** New cumulative amount settled on-chain. */
|
|
75
77
|
newSettled: bigint;
|
|
76
78
|
};
|
|
@@ -183,6 +185,8 @@ export type ChannelTransactionOptions = {
|
|
|
183
185
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined;
|
|
184
186
|
/** Explicit fee token for the transaction. */
|
|
185
187
|
feeToken?: Address | undefined;
|
|
188
|
+
/** Unix time in seconds after which the expiring-nonce transaction cannot be included. */
|
|
189
|
+
validBefore?: number | undefined;
|
|
186
190
|
};
|
|
187
191
|
/** Simulates an unsponsored client-signed session transaction without broadcasting it. */
|
|
188
192
|
export declare function simulateCredentialTransaction(parameters: {
|
|
@@ -39,6 +39,7 @@ export function readTopUpReceiptFields(event) {
|
|
|
39
39
|
/** Reads and validates typed fields from a Settled receipt event. */
|
|
40
40
|
export function readSettledReceiptFields(event) {
|
|
41
41
|
return {
|
|
42
|
+
deltaPaid: readUint96(event.args.deltaPaid, 'Settled deltaPaid'),
|
|
42
43
|
newSettled: readUint96(event.args.newSettled, 'Settled newSettled'),
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -326,7 +327,7 @@ async function signTempoTransaction(client, transaction) {
|
|
|
326
327
|
return (await signTransaction(client, transaction));
|
|
327
328
|
}
|
|
328
329
|
async function prepareFeePayerCallTransaction(client, parameters) {
|
|
329
|
-
const { account, data, feeToken, to } = parameters;
|
|
330
|
+
const { account, data, feeToken, to, validBefore } = parameters;
|
|
330
331
|
// viem's stable request type does not expose Tempo fee-payer transaction
|
|
331
332
|
// fields for this call shape. Keep the cast at the boundary.
|
|
332
333
|
return prepareTransactionRequest(client, {
|
|
@@ -335,10 +336,11 @@ async function prepareFeePayerCallTransaction(client, parameters) {
|
|
|
335
336
|
feePayer: true,
|
|
336
337
|
nonceKey: 'expiring',
|
|
337
338
|
...(feeToken ? { feeToken } : {}),
|
|
339
|
+
...(validBefore ? { validBefore } : {}),
|
|
338
340
|
});
|
|
339
341
|
}
|
|
340
342
|
function sendPrecompileContractCall(client, parameters) {
|
|
341
|
-
const { account, data, feePayer, feeToken, to } = parameters;
|
|
343
|
+
const { account, data, feePayer, feeToken, to, validBefore } = parameters;
|
|
342
344
|
// `feeToken` is Tempo-specific and not represented on viem's base
|
|
343
345
|
// transaction request type.
|
|
344
346
|
return sendViemTransaction(client, {
|
|
@@ -349,6 +351,7 @@ function sendPrecompileContractCall(client, parameters) {
|
|
|
349
351
|
nonceKey: 'expiring',
|
|
350
352
|
...(feePayer ? { feePayer } : {}),
|
|
351
353
|
...(feeToken ? { feeToken } : {}),
|
|
354
|
+
...(validBefore ? { validBefore } : {}),
|
|
352
355
|
});
|
|
353
356
|
}
|
|
354
357
|
/**
|
|
@@ -674,6 +677,7 @@ async function sendPrecompileTransaction(client, to, data, label, options) {
|
|
|
674
677
|
feePayer: true,
|
|
675
678
|
feeToken: options.feeToken,
|
|
676
679
|
to,
|
|
680
|
+
validBefore: options.validBefore,
|
|
677
681
|
});
|
|
678
682
|
}
|
|
679
683
|
if (feePayer && !selfSponsored) {
|
|
@@ -690,6 +694,7 @@ async function sendPrecompileTransaction(client, to, data, label, options) {
|
|
|
690
694
|
data,
|
|
691
695
|
feeToken,
|
|
692
696
|
to,
|
|
697
|
+
validBefore: options.validBefore,
|
|
693
698
|
});
|
|
694
699
|
assertPrecompileFeePayerPolicy({ prepared, policy: options.feePayerPolicy });
|
|
695
700
|
const serialized = await signTempoTransaction(client, {
|
|
@@ -719,6 +724,7 @@ async function sendPrecompileTransaction(client, to, data, label, options) {
|
|
|
719
724
|
to,
|
|
720
725
|
data,
|
|
721
726
|
feeToken,
|
|
727
|
+
validBefore: options?.validBefore,
|
|
722
728
|
});
|
|
723
729
|
}
|
|
724
730
|
//# sourceMappingURL=Chain.js.map
|
|
@@ -300,6 +300,14 @@ export declare function acceptVoucherStateUpdate(parameters: AcceptVoucherStateU
|
|
|
300
300
|
* close voucher and current on-chain deposit.
|
|
301
301
|
*/
|
|
302
302
|
export declare function resolveCloseCaptureAmount(parameters: ResolveCloseCaptureAmountParameters): bigint;
|
|
303
|
+
/** Seconds a server close transaction stays valid after its pending-close marker is written. */
|
|
304
|
+
export declare const pendingCloseValiditySeconds = 25;
|
|
305
|
+
/** Clears a server pending-close marker whose close transaction expired without closing the channel. */
|
|
306
|
+
export declare function reconcileExpiredPendingClose(parameters: {
|
|
307
|
+
current: State | null;
|
|
308
|
+
now: number;
|
|
309
|
+
state: OnChainChannelState;
|
|
310
|
+
}): State | null;
|
|
303
311
|
/** Marks local channel state as pending close and returns the bounded capture amount. */
|
|
304
312
|
export declare function markPendingClose(parameters: MarkPendingCloseParameters): PendingCloseUpdate;
|
|
305
313
|
/** Finalizes local channel state after a successful close transaction. */
|
|
@@ -142,6 +142,23 @@ export function resolveCloseCaptureAmount(parameters) {
|
|
|
142
142
|
}
|
|
143
143
|
return captureAmount;
|
|
144
144
|
}
|
|
145
|
+
/** Seconds a server close transaction stays valid after its pending-close marker is written. */
|
|
146
|
+
export const pendingCloseValiditySeconds = 25;
|
|
147
|
+
/** Extra seconds allowed for clock differences between the server and the chain. */
|
|
148
|
+
const pendingCloseClockSkewSeconds = 30;
|
|
149
|
+
/** Clears a server pending-close marker whose close transaction expired without closing the channel. */
|
|
150
|
+
export function reconcileExpiredPendingClose(parameters) {
|
|
151
|
+
const { current, now, state } = parameters;
|
|
152
|
+
if (!current || current.finalized || current.closeRequestedAt === 0n)
|
|
153
|
+
return current;
|
|
154
|
+
// An open channel with no on-chain close request proves the expired close never executed.
|
|
155
|
+
if (BigInt(state.closeRequestedAt) !== 0n || state.deposit === 0n)
|
|
156
|
+
return current;
|
|
157
|
+
const expiresAt = current.closeRequestedAt + BigInt(pendingCloseValiditySeconds);
|
|
158
|
+
if (BigInt(now) <= expiresAt + BigInt(pendingCloseClockSkewSeconds))
|
|
159
|
+
return current;
|
|
160
|
+
return { ...current, closeRequestedAt: 0n };
|
|
161
|
+
}
|
|
145
162
|
/** Marks local channel state as pending close and returns the bounded capture amount. */
|
|
146
163
|
export function markPendingClose(parameters) {
|
|
147
164
|
const { closeRequestedAt, cumulativeAmount, current, onChainSettled, onChainDeposit } = parameters;
|
|
@@ -5,7 +5,7 @@ import * as Chain from '../precompile/Chain.js';
|
|
|
5
5
|
import * as Channel from '../precompile/Channel.js';
|
|
6
6
|
import { type SessionCredentialPayload, type SessionReceipt } from '../precompile/Protocol.js';
|
|
7
7
|
import * as ChannelStore from './ChannelStore.js';
|
|
8
|
-
import { type OnSessionSettlement } from './Settlement.js';
|
|
8
|
+
import { type OnSessionSettlement, type OnSessionSettlementFailure } from './Settlement.js';
|
|
9
9
|
/** Returns the effective voucher signer for a TIP-1034 descriptor. */
|
|
10
10
|
export declare function authorizedSigner(descriptor: Channel.ChannelDescriptor): Address;
|
|
11
11
|
/** Asserts that a credential payload includes a TIP-1034 descriptor. */
|
|
@@ -77,6 +77,8 @@ export type BroadcastCredentialPayloadParameters = {
|
|
|
77
77
|
minVoucherDelta: bigint;
|
|
78
78
|
/** Callback invoked after an on-chain settlement or close transaction is confirmed. */
|
|
79
79
|
onSessionSettlement?: OnSessionSettlement | undefined;
|
|
80
|
+
/** Callback invoked when a scheduled settlement or close transaction fails. */
|
|
81
|
+
onSessionSettlementFailure?: OnSessionSettlementFailure | undefined;
|
|
80
82
|
/** Discriminated session credential payload to verify. */
|
|
81
83
|
payload: SessionCredentialPayload;
|
|
82
84
|
/** Whether an open or voucher credential must add new funds for this request. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAddress, isAddressEqual, zeroAddress, } from 'viem';
|
|
1
|
+
import { createClient, custom, isAddress, isAddressEqual, zeroAddress, } from 'viem';
|
|
2
2
|
import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, DeltaTooSmallError, InsufficientBalanceError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
|
|
3
3
|
import * as Chain from '../precompile/Chain.js';
|
|
4
4
|
import { readChannelClosedReceiptFields } from '../precompile/Chain.js';
|
|
@@ -7,7 +7,7 @@ import { createSessionReceipt, uint96, } from '../precompile/Protocol.js';
|
|
|
7
7
|
import * as Voucher from '../precompile/Voucher.js';
|
|
8
8
|
import * as ChannelStore from './ChannelStore.js';
|
|
9
9
|
import { getChallengePaymentFields } from './RequestState.js';
|
|
10
|
-
import { assertSettlementSender, getClientAccount, maybeSettleScheduled, } from './Settlement.js';
|
|
10
|
+
import { assertSettlementSender, getClientAccount, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
|
|
11
11
|
/** Returns the effective voucher signer for a TIP-1034 descriptor. */
|
|
12
12
|
export function authorizedSigner(descriptor) {
|
|
13
13
|
return isAddressEqual(descriptor.authorizedSigner, zeroAddress)
|
|
@@ -622,6 +622,13 @@ async function handleVoucherCredential(parameters) {
|
|
|
622
622
|
forceRefresh: true,
|
|
623
623
|
lastOnChainVerified,
|
|
624
624
|
});
|
|
625
|
+
const reconciled = channel.closeRequestedAt === 0n
|
|
626
|
+
? channel
|
|
627
|
+
: ((await store.updateChannel(channelId, (current) => ChannelStore.reconcileExpiredPendingClose({
|
|
628
|
+
current,
|
|
629
|
+
now: Math.floor(Date.now() / 1000),
|
|
630
|
+
state: channelState,
|
|
631
|
+
}))) ?? channel);
|
|
625
632
|
if (channelState.closeRequestedAt !== 0) {
|
|
626
633
|
const closing = await store.updateChannel(channelId, (current) => current
|
|
627
634
|
? {
|
|
@@ -643,6 +650,7 @@ async function handleVoucherCredential(parameters) {
|
|
|
643
650
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
644
651
|
feeToken: parameters.feeToken,
|
|
645
652
|
onSessionSettlement: parameters.onSessionSettlement,
|
|
653
|
+
onSessionSettlementFailure: parameters.onSessionSettlementFailure,
|
|
646
654
|
schedule: {},
|
|
647
655
|
store,
|
|
648
656
|
});
|
|
@@ -653,12 +661,36 @@ async function handleVoucherCredential(parameters) {
|
|
|
653
661
|
minVoucherDelta,
|
|
654
662
|
requireAdvance: parameters.requireVoucherAdvance,
|
|
655
663
|
challenge,
|
|
656
|
-
channel,
|
|
664
|
+
channel: reconciled,
|
|
657
665
|
voucher,
|
|
658
666
|
channelState,
|
|
659
667
|
methodDetails: { chainId, escrowContract: escrow },
|
|
660
668
|
});
|
|
661
669
|
}
|
|
670
|
+
const sendMethods = new Set([
|
|
671
|
+
'eth_sendRawTransaction',
|
|
672
|
+
'eth_sendRawTransactionSync',
|
|
673
|
+
'eth_sendTransaction',
|
|
674
|
+
]);
|
|
675
|
+
/** Wraps a client so a failed close can tell whether its transaction was handed to the node. */
|
|
676
|
+
function trackSendAttempts(client) {
|
|
677
|
+
const tracked = { attempted: false };
|
|
678
|
+
const trackedClient = createClient({
|
|
679
|
+
account: client.account,
|
|
680
|
+
chain: client.chain,
|
|
681
|
+
pollingInterval: client.pollingInterval,
|
|
682
|
+
transport: custom({
|
|
683
|
+
request(parameters) {
|
|
684
|
+
if (sendMethods.has(parameters.method))
|
|
685
|
+
tracked.attempted = true;
|
|
686
|
+
return client.request(parameters);
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
// The wrapped client already owns retries.
|
|
690
|
+
{ retryCount: 0 }),
|
|
691
|
+
});
|
|
692
|
+
return { client: trackedClient, tracked };
|
|
693
|
+
}
|
|
662
694
|
async function handleCloseCredential(parameters) {
|
|
663
695
|
const { store, client, challenge, payload, chainId, escrow } = parameters;
|
|
664
696
|
const request = getChallengePaymentFields(challenge);
|
|
@@ -698,14 +730,14 @@ async function handleCloseCredential(parameters) {
|
|
|
698
730
|
let captureAmount = uint96(channel.spent > state.settled ? channel.spent : state.settled);
|
|
699
731
|
if (captureAmount > state.deposit)
|
|
700
732
|
throw new AmountExceedsDepositError({ reason: 'close capture amount exceeds on-chain deposit' });
|
|
701
|
-
const
|
|
702
|
-
const
|
|
733
|
+
const now = Math.floor(Date.now() / 1000) || 1;
|
|
734
|
+
const pendingCloseStartedAt = BigInt(now);
|
|
703
735
|
let pendingCloseMarked = false;
|
|
704
736
|
await store.updateChannel(channelId, (current) => {
|
|
705
737
|
const next = ChannelStore.markPendingClose({
|
|
706
738
|
closeRequestedAt: pendingCloseStartedAt,
|
|
707
739
|
cumulativeAmount,
|
|
708
|
-
current,
|
|
740
|
+
current: ChannelStore.reconcileExpiredPendingClose({ current, now, state }),
|
|
709
741
|
onChainDeposit: state.deposit,
|
|
710
742
|
onChainSettled: state.settled,
|
|
711
743
|
});
|
|
@@ -716,8 +748,11 @@ async function handleCloseCredential(parameters) {
|
|
|
716
748
|
return next.state;
|
|
717
749
|
});
|
|
718
750
|
const account = parameters.account ?? getClientAccount(client);
|
|
719
|
-
|
|
751
|
+
// Pin the close transaction's expiry to the marker so an expired marker proves it can no longer land.
|
|
752
|
+
const validBefore = now + ChannelStore.pendingCloseValiditySeconds;
|
|
753
|
+
const send = trackSendAttempts(client);
|
|
720
754
|
let receipt;
|
|
755
|
+
let txHash;
|
|
721
756
|
try {
|
|
722
757
|
assertSettlementSender({
|
|
723
758
|
operation: 'close',
|
|
@@ -726,21 +761,31 @@ async function handleCloseCredential(parameters) {
|
|
|
726
761
|
payee: channel.payee,
|
|
727
762
|
sender: account?.address,
|
|
728
763
|
});
|
|
729
|
-
txHash = await Chain.closeOnChain(client, channel.descriptor, cumulativeAmount, captureAmount, payload.signature, escrow, account
|
|
764
|
+
txHash = await Chain.closeOnChain(send.client, channel.descriptor, cumulativeAmount, captureAmount, payload.signature, escrow, account
|
|
730
765
|
? {
|
|
731
766
|
account,
|
|
732
767
|
...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
|
|
733
768
|
...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
|
|
734
769
|
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
735
770
|
candidateFeeTokens: [channel.token],
|
|
771
|
+
validBefore,
|
|
736
772
|
}
|
|
737
|
-
:
|
|
773
|
+
: { validBefore });
|
|
738
774
|
receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
|
|
739
775
|
}
|
|
740
776
|
catch (error) {
|
|
741
|
-
|
|
777
|
+
await reportSessionSettlementFailure(parameters.onSessionSettlementFailure, {
|
|
778
|
+
chainId,
|
|
779
|
+
channelId,
|
|
780
|
+
error,
|
|
781
|
+
trigger: 'close',
|
|
782
|
+
});
|
|
783
|
+
// A close whose send RPC was attempted may still land, so it keeps the marker until it expires and
|
|
784
|
+
// a later credential reconciles it. Verification errors prove a rejection or revert.
|
|
785
|
+
const closeFailed = !send.tracked.attempted || error instanceof VerificationFailedError;
|
|
786
|
+
if (pendingCloseMarked && closeFailed) {
|
|
742
787
|
await store.updateChannel(channelId, (current) => current && current.closeRequestedAt === pendingCloseStartedAt
|
|
743
|
-
? { ...current, closeRequestedAt:
|
|
788
|
+
? { ...current, closeRequestedAt: 0n }
|
|
744
789
|
: current);
|
|
745
790
|
}
|
|
746
791
|
throw error;
|
|
@@ -19,12 +19,12 @@ import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as
|
|
|
19
19
|
import * as ChannelStore from './ChannelStore.js';
|
|
20
20
|
import { type ResolveSessionChannelId } from './RequestState.js';
|
|
21
21
|
import { type SettleChargedSessionChannel } from './Settlement.js';
|
|
22
|
-
import { type OnSessionSettlement, type SettlementSchedule } from './Settlement.js';
|
|
22
|
+
import { type OnSessionSettlement, type OnSessionSettlementFailure, type SettlementSchedule } from './Settlement.js';
|
|
23
23
|
import * as Ws from './Ws.js';
|
|
24
24
|
/** Server-side automatic settlement schedule. */
|
|
25
25
|
export type { SettlementSchedule } from './Settlement.js';
|
|
26
26
|
/** Server-side settlement event hook types. */
|
|
27
|
-
export type { OnSessionSettlement, SessionSettlementContext } from './Settlement.js';
|
|
27
|
+
export type { OnSessionSettlement, OnSessionSettlementFailure, SessionSettlementContext, SessionSettlementFailureContext, } from './Settlement.js';
|
|
28
28
|
/** Server-side hook types for request-identity channel bootstrap. */
|
|
29
29
|
export type { ResolveSessionChannelId, ResolveSessionChannelIdParameters, SessionChannelIdRequest, } from './RequestState.js';
|
|
30
30
|
export { settle, settleBatch } from './Settlement.js';
|
|
@@ -175,6 +175,8 @@ export declare namespace session {
|
|
|
175
175
|
escrowContract?: Address | undefined;
|
|
176
176
|
/** Callback invoked after any on-chain settlement or close transaction is confirmed. */
|
|
177
177
|
onSessionSettlement?: OnSessionSettlement | undefined;
|
|
178
|
+
/** Callback invoked when a scheduled settlement or close transaction fails. Observer errors are ignored. */
|
|
179
|
+
onSessionSettlementFailure?: OnSessionSettlementFailure | undefined;
|
|
178
180
|
/** Server-owned automatic settlement cadence. Clients do not receive or control this schedule. */
|
|
179
181
|
settlementSchedule?: SettlementSchedule | undefined;
|
|
180
182
|
/** Optional fee token for management and server-driven settle/close transactions. */
|
|
@@ -12,6 +12,7 @@ import * as Constants from '../../../Constants.js';
|
|
|
12
12
|
import * as Errors from '../../../Errors.js';
|
|
13
13
|
import * as Expires from '../../../Expires.js';
|
|
14
14
|
import * as Method from '../../../Method.js';
|
|
15
|
+
import * as ChallengeMeta from '../../../server/internal/challengeMeta.js';
|
|
15
16
|
import * as Store from '../../../Store.js';
|
|
16
17
|
import * as Client from '../../../viem/Client.js';
|
|
17
18
|
import * as Account from '../../internal/account.js';
|
|
@@ -27,7 +28,7 @@ import { requireSessionCredentialPayload } from './CredentialVerification.js';
|
|
|
27
28
|
import { resolveCredentialVerificationContext, resolveSessionChannelId, resolveSessionSnapshot, resolveSessionPaymentRequest, } from './RequestState.js';
|
|
28
29
|
import { respondToSessionCredential } from './RequestState.js';
|
|
29
30
|
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, } from './Settlement.js';
|
|
30
|
-
import { isSettlementDue, maybeSettleScheduled } from './Settlement.js';
|
|
31
|
+
import { ignoreRetryableSettlementFailure, isSettlementDue, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
|
|
31
32
|
import { resolveSettlementSchedule, } from './Settlement.js';
|
|
32
33
|
import * as Ws from './Ws.js';
|
|
33
34
|
export { settle, settleBatch } from './Settlement.js';
|
|
@@ -58,6 +59,7 @@ function isBootstrapChargeCredential(credential) {
|
|
|
58
59
|
function createBootstrapChallenge(parameters) {
|
|
59
60
|
return Challenge.fromMethod(Methods.charge, {
|
|
60
61
|
expires: parameters.expires,
|
|
62
|
+
meta: ChallengeMeta.withNonce(undefined),
|
|
61
63
|
realm: parameters.realm,
|
|
62
64
|
request: parameters.request,
|
|
63
65
|
secretKey: parameters.secretKey,
|
|
@@ -196,6 +198,7 @@ export function session(p) {
|
|
|
196
198
|
const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore = Store.memory(), suggestedDeposit, unitType, } = parameters;
|
|
197
199
|
const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals);
|
|
198
200
|
const onSessionSettlement = parameters.onSessionSettlement;
|
|
201
|
+
const onSessionSettlementFailure = parameters.onSessionSettlementFailure;
|
|
199
202
|
const store = ChannelStore.fromStore(rawStore);
|
|
200
203
|
const lastOnChainVerified = new Map();
|
|
201
204
|
const { account, feePayer, remoteFeePayer, recipient } = Account.resolve(parameters);
|
|
@@ -209,21 +212,34 @@ export function session(p) {
|
|
|
209
212
|
const settleScheduled = async (channel) => {
|
|
210
213
|
if (!isSettlementDue(channel, settlementSchedule))
|
|
211
214
|
return undefined;
|
|
215
|
+
const client = await (async () => getClient({ chainId: channel.chainId }))().catch(async (error) => {
|
|
216
|
+
await reportSessionSettlementFailure(onSessionSettlementFailure, {
|
|
217
|
+
chainId: channel.chainId,
|
|
218
|
+
channelId: channel.channelId,
|
|
219
|
+
error,
|
|
220
|
+
trigger: 'scheduled',
|
|
221
|
+
});
|
|
222
|
+
throw error;
|
|
223
|
+
});
|
|
212
224
|
return maybeSettleScheduled({
|
|
213
225
|
account,
|
|
214
226
|
channel,
|
|
215
|
-
client
|
|
227
|
+
client,
|
|
216
228
|
...(configuredFeePayer ? { feePayer: configuredFeePayer } : {}),
|
|
217
229
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
218
230
|
feeToken: parameters.feeToken,
|
|
219
231
|
onSessionSettlement,
|
|
232
|
+
onSessionSettlementFailure,
|
|
220
233
|
schedule: settlementSchedule,
|
|
221
234
|
store,
|
|
222
235
|
});
|
|
223
236
|
};
|
|
237
|
+
// A failed scheduled settlement is reported and retried by the next one; the charged request
|
|
238
|
+
// stays served because the payer's voucher already covers the charge.
|
|
239
|
+
const settleCharged = (channel) => settleScheduled(channel).catch(ignoreRetryableSettlementFailure);
|
|
224
240
|
const serveWebSocket = (options) => Ws.serve({
|
|
225
241
|
...options,
|
|
226
|
-
onChargeCommitted:
|
|
242
|
+
onChargeCommitted: settleCharged,
|
|
227
243
|
store,
|
|
228
244
|
});
|
|
229
245
|
const bootstrapCharge = ChargeServer.charge({
|
|
@@ -238,7 +254,7 @@ export function session(p) {
|
|
|
238
254
|
});
|
|
239
255
|
const transport = parameters.sse
|
|
240
256
|
? Transport.sse({
|
|
241
|
-
settleCharged
|
|
257
|
+
settleCharged,
|
|
242
258
|
store,
|
|
243
259
|
...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
|
|
244
260
|
})
|
|
@@ -305,6 +321,7 @@ export function session(p) {
|
|
|
305
321
|
lastOnChainVerified,
|
|
306
322
|
minVoucherDelta: context.minVoucherDelta,
|
|
307
323
|
onSessionSettlement,
|
|
324
|
+
onSessionSettlementFailure,
|
|
308
325
|
payload,
|
|
309
326
|
requireVoucherAdvance: shouldApplyVerifiedHttpAccounting({
|
|
310
327
|
capturedRequest: envelope?.capturedRequest,
|
|
@@ -328,10 +345,11 @@ export function session(p) {
|
|
|
328
345
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
329
346
|
feeToken: parameters.feeToken,
|
|
330
347
|
onSessionSettlement,
|
|
348
|
+
onSessionSettlementFailure,
|
|
331
349
|
schedule: settlementSchedule,
|
|
332
350
|
store,
|
|
333
351
|
channel,
|
|
334
|
-
}),
|
|
352
|
+
}).catch(ignoreRetryableSettlementFailure),
|
|
335
353
|
});
|
|
336
354
|
};
|
|
337
355
|
const method = Method.toServer(Methods.session, {
|
|
@@ -86,6 +86,19 @@ export type SessionSettlementContext = Readonly<{
|
|
|
86
86
|
}>;
|
|
87
87
|
/** Callback invoked after an on-chain settlement or close transaction is confirmed. */
|
|
88
88
|
export type OnSessionSettlement = (context: SessionSettlementContext) => MaybePromise<void>;
|
|
89
|
+
/** Context emitted when a scheduled settlement or close transaction fails. */
|
|
90
|
+
export type SessionSettlementFailureContext = Readonly<{
|
|
91
|
+
/** Chain ID of the channel. */
|
|
92
|
+
chainId: number;
|
|
93
|
+
/** Channel ID whose settlement or close failed. */
|
|
94
|
+
channelId: Hex;
|
|
95
|
+
/** Error thrown by the settlement or close. */
|
|
96
|
+
error: unknown;
|
|
97
|
+
/** `close` for a failed close transaction; `scheduled` for a failed scheduled settlement. */
|
|
98
|
+
trigger: 'close' | 'scheduled';
|
|
99
|
+
}>;
|
|
100
|
+
/** Callback invoked when a scheduled settlement or close transaction fails. */
|
|
101
|
+
export type OnSessionSettlementFailure = (context: SessionSettlementFailureContext) => MaybePromise<void>;
|
|
89
102
|
/** Inputs used to mark a channel after automatic scheduled settlement succeeds. */
|
|
90
103
|
export type MarkSettlementCompleteParameters = {
|
|
91
104
|
channelId: ChannelStore.State['channelId'];
|
|
@@ -105,7 +118,7 @@ export declare function resolveSettlementSchedule(schedule: SettlementSchedule |
|
|
|
105
118
|
export declare function resolveSettlementProgress(channel: ChannelStore.State): SettlementProgress | undefined;
|
|
106
119
|
/** Returns whether the precompile channel has crossed any configured settlement threshold. */
|
|
107
120
|
export declare function isSettlementDue(channel: ChannelStore.State, schedule: ResolvedSettlementSchedule | undefined): boolean;
|
|
108
|
-
/**
|
|
121
|
+
/** Releases a completed scheduled settlement lease; {@link settle} records the settled counters. */
|
|
109
122
|
export declare function markSettlementComplete(parameters: MarkSettlementCompleteParameters): Promise<void>;
|
|
110
123
|
/** Atomically claims one due scheduled settlement across server workers. */
|
|
111
124
|
export declare function claimScheduledSettlement(parameters: {
|
|
@@ -208,6 +221,8 @@ export type MaybeSettleScheduledParameters = {
|
|
|
208
221
|
feeToken?: Address | undefined;
|
|
209
222
|
/** Callback invoked after the scheduled settlement transaction is confirmed. */
|
|
210
223
|
onSessionSettlement?: OnSessionSettlement | undefined;
|
|
224
|
+
/** Callback invoked when the scheduled settlement fails. */
|
|
225
|
+
onSessionSettlementFailure?: OnSessionSettlementFailure | undefined;
|
|
211
226
|
/** Resolved server-owned settlement cadence. */
|
|
212
227
|
schedule: ResolvedSettlementSchedule | undefined;
|
|
213
228
|
/** Server-side channel store. */
|
|
@@ -225,6 +240,30 @@ export declare function assertSettlementSender(parameters: SettlementSenderParam
|
|
|
225
240
|
export declare function maybeSettleScheduled(parameters: MaybeSettleScheduledParameters): Promise<Hex | undefined>;
|
|
226
241
|
/** Settles the highest accepted voucher for a precompile-backed session channel. */
|
|
227
242
|
export declare function settle(store_: SessionStoreInput, client: Chain.TransactionClient, channelId_: Hex, options?: SettlementTransactionOptions): Promise<Hex>;
|
|
243
|
+
/** Raised when a settlement transaction confirmed but the channel store could not record it. */
|
|
244
|
+
export declare class SettlementCheckpointError extends Error {
|
|
245
|
+
readonly name = "SettlementCheckpointError";
|
|
246
|
+
/** Hash of the confirmed settlement transaction. */
|
|
247
|
+
readonly txHash: Hex;
|
|
248
|
+
constructor(options: {
|
|
249
|
+
cause: unknown;
|
|
250
|
+
txHash: Hex;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/** Raised when a scheduled settlement could not claim its lease; no transaction was submitted. */
|
|
254
|
+
export declare class SettlementLeaseError extends Error {
|
|
255
|
+
readonly name = "SettlementLeaseError";
|
|
256
|
+
constructor(options: {
|
|
257
|
+
cause: unknown;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
228
260
|
/** Settles multiple precompile-backed session channels with the same validation as {@link settle}. */
|
|
229
261
|
export declare function settleBatch(store: SessionStoreInput, client: Chain.TransactionClient, channelIds: readonly Hex[], options?: SettlementTransactionOptions): Promise<Hex[]>;
|
|
262
|
+
/**
|
|
263
|
+
* @internal Keeps a charged request served when its scheduled settlement hit a transport or RPC
|
|
264
|
+
* failure; the next settlement retries it. Reverts and configuration errors fail the request.
|
|
265
|
+
*/
|
|
266
|
+
export declare function ignoreRetryableSettlementFailure(error: unknown): undefined;
|
|
267
|
+
/** @internal Reports a settlement failure without letting observer errors replace it. */
|
|
268
|
+
export declare function reportSessionSettlementFailure(onSessionSettlementFailure: OnSessionSettlementFailure | undefined, context: SessionSettlementFailureContext): Promise<void>;
|
|
230
269
|
//# sourceMappingURL=Settlement.d.ts.map
|