mppx 0.13.4 → 0.13.6
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 +48 -0
- package/dist/Credential.d.ts +3 -0
- package/dist/Credential.js +9 -0
- package/dist/Errors.d.ts +63 -2
- package/dist/Errors.js +52 -2
- package/dist/Method.d.ts +10 -0
- package/dist/cli/cli.js +92 -92
- package/dist/cli/plugins/evm.js +2 -2
- package/dist/cli/plugins/index.js +12 -12
- package/dist/client/Mppx.d.ts +2 -0
- package/dist/client/Mppx.js +8 -10
- package/dist/client/internal/Fetch.js +107 -8
- package/dist/client/internal/MethodChallenge.d.ts +2 -0
- package/dist/client/internal/MethodChallenge.js +4 -0
- package/dist/client/internal/MethodResponse.d.ts +6 -0
- package/dist/client/internal/MethodResponse.js +16 -0
- package/dist/client/internal/protocols/Mcp.js +36 -5
- package/dist/evm/server/Charge.js +3 -1
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/internal/warnOnce.d.ts +3 -0
- package/dist/internal/warnOnce.js +9 -0
- package/dist/server/Mppx.d.ts +5 -0
- package/dist/server/Mppx.js +45 -18
- package/dist/server/Transport.js +10 -0
- package/dist/stripe/server/Methods.js +4 -1
- package/dist/tempo/Attribution.d.ts +24 -9
- package/dist/tempo/Attribution.js +25 -5
- package/dist/tempo/client/Charge.d.ts +116 -10
- package/dist/tempo/client/Charge.js +117 -22
- package/dist/tempo/client/Methods.d.ts +88 -9
- package/dist/tempo/index.d.ts +1 -0
- package/dist/tempo/index.js +1 -0
- package/dist/tempo/internal/auto-swap.d.ts +10 -0
- package/dist/tempo/internal/auto-swap.js +35 -8
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.js +20 -8
- package/dist/tempo/legacy/session/Chain.js +8 -2
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.js +160 -29
- package/dist/tempo/server/Methods.js +28 -5
- package/dist/tempo/server/Relay.js +6 -22
- package/dist/tempo/server/Subscription.js +5 -1
- package/dist/tempo/server/internal/fallback-store.d.ts +9 -0
- package/dist/tempo/server/internal/fallback-store.js +13 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +3 -0
- package/dist/tempo/server/internal/transport.js +5 -1
- package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
- package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
- package/dist/tempo/session/client/ChannelStore.js +47 -0
- package/dist/tempo/session/client/CredentialState.js +8 -3
- package/dist/tempo/session/client/Session.js +50 -33
- package/dist/tempo/session/client/SessionManager.js +186 -65
- package/dist/tempo/session/client/Transports.d.ts +6 -0
- package/dist/tempo/session/client/Transports.js +52 -7
- package/dist/tempo/session/precompile/Chain.d.ts +2 -0
- package/dist/tempo/session/precompile/Chain.js +8 -3
- package/dist/tempo/session/server/ChannelStore.d.ts +34 -0
- package/dist/tempo/session/server/ChannelStore.js +69 -9
- package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
- package/dist/tempo/session/server/CredentialVerification.js +220 -48
- package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
- package/dist/tempo/session/server/MeteredStream.js +10 -26
- package/dist/tempo/session/server/Session.d.ts +29 -2
- package/dist/tempo/session/server/Session.js +124 -44
- package/dist/tempo/session/server/Settlement.d.ts +9 -0
- package/dist/tempo/session/server/Settlement.js +23 -3
- package/dist/tempo/session/server/Sse.d.ts +4 -3
- package/dist/tempo/session/server/Sse.js +3 -3
- package/dist/tempo/session/server/Ws.d.ts +9 -1
- package/dist/tempo/session/server/Ws.js +1 -0
- package/package.json +1 -1
|
@@ -368,14 +368,22 @@ async function validateVoucherCredential(parameters, payload) {
|
|
|
368
368
|
assertCredentialSourceCanSpend({ chainId, channel, source: credentialSource });
|
|
369
369
|
if (channel.finalized)
|
|
370
370
|
throw new ChannelClosedError({ reason: 'channel is finalized' });
|
|
371
|
-
const channelState =
|
|
371
|
+
const channelState = canUseStoredChannelState({
|
|
372
372
|
channel,
|
|
373
373
|
channelId,
|
|
374
|
-
channelStateTtl,
|
|
375
|
-
client,
|
|
376
|
-
escrow,
|
|
377
374
|
lastOnChainVerified,
|
|
378
|
-
|
|
375
|
+
voucher,
|
|
376
|
+
voucherStateMaxAge: parameters.voucherStateMaxAge ?? 0,
|
|
377
|
+
})
|
|
378
|
+
? storedChannelState(channel)
|
|
379
|
+
: await resolveVoucherChannelState({
|
|
380
|
+
channel,
|
|
381
|
+
channelId,
|
|
382
|
+
channelStateTtl,
|
|
383
|
+
client,
|
|
384
|
+
escrow,
|
|
385
|
+
lastOnChainVerified,
|
|
386
|
+
});
|
|
379
387
|
if (parameters.allowPendingClose && channelState.closeRequestedAt !== 0)
|
|
380
388
|
return;
|
|
381
389
|
await ChannelStore.validateVoucher({
|
|
@@ -387,17 +395,36 @@ async function validateVoucherCredential(parameters, payload) {
|
|
|
387
395
|
});
|
|
388
396
|
}
|
|
389
397
|
async function resolveVoucherChannelState(parameters) {
|
|
390
|
-
const {
|
|
398
|
+
const { channelId, channelStateTtl, client, escrow, forceRefresh, lastOnChainVerified } = parameters;
|
|
391
399
|
const shouldRefresh = forceRefresh || Date.now() - (lastOnChainVerified.get(channelId) ?? 0) > channelStateTtl;
|
|
392
400
|
const state = shouldRefresh ? await Chain.getChannelState(client, channelId, escrow) : undefined;
|
|
393
|
-
if (state
|
|
401
|
+
if (state && (state.closeRequestedAt !== 0 || state.deposit === 0n))
|
|
394
402
|
lastOnChainVerified.delete(channelId);
|
|
395
403
|
else if (state)
|
|
396
404
|
lastOnChainVerified.set(channelId, Date.now());
|
|
405
|
+
if (state)
|
|
406
|
+
return state;
|
|
407
|
+
const { channel } = parameters;
|
|
408
|
+
return storedChannelState(typeof channel === 'function' ? await channel() : channel);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Whether a voucher may be checked against the stored channel instead of waiting for a chain read.
|
|
412
|
+
* A payer can withdraw only after the escrow's close grace period, so a voucher within the deposit
|
|
413
|
+
* of recently verified open state stays settleable while a fresh read finishes.
|
|
414
|
+
*/
|
|
415
|
+
function canUseStoredChannelState(parameters) {
|
|
416
|
+
const { channel, channelId, lastOnChainVerified, voucher, voucherStateMaxAge } = parameters;
|
|
417
|
+
const verifiedAt = Math.max(lastOnChainVerified.get(channelId) ?? 0, channel.onChainVerifiedAt ?? 0);
|
|
418
|
+
return (voucherStateMaxAge > 0 &&
|
|
419
|
+
channel.closeRequestedAt === 0n &&
|
|
420
|
+
Date.now() - verifiedAt <= voucherStateMaxAge &&
|
|
421
|
+
voucher.cumulativeAmount <= channel.deposit);
|
|
422
|
+
}
|
|
423
|
+
function storedChannelState(channel) {
|
|
397
424
|
return {
|
|
398
|
-
deposit:
|
|
399
|
-
settled:
|
|
400
|
-
closeRequestedAt:
|
|
425
|
+
deposit: uint96(channel.deposit),
|
|
426
|
+
settled: uint96(channel.settledOnChain),
|
|
427
|
+
closeRequestedAt: Number(channel.closeRequestedAt),
|
|
401
428
|
};
|
|
402
429
|
}
|
|
403
430
|
async function validateCloseCredential(parameters, payload) {
|
|
@@ -602,26 +629,81 @@ async function handleVoucherCredential(parameters) {
|
|
|
602
629
|
const voucher = Voucher.parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature);
|
|
603
630
|
assertDescriptor(payload);
|
|
604
631
|
validateChannelDescriptor(payload.descriptor, channelId, chainId, escrow, request.recipient, request.currency, expectedOperator);
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
632
|
+
const voucherStateMaxAge = parameters.voucherStateMaxAge ?? 0;
|
|
633
|
+
const { chargeAmount, onCharged } = parameters;
|
|
634
|
+
let loadingChannel;
|
|
635
|
+
const loadChannel = () => {
|
|
636
|
+
loadingChannel ??= ChannelStore.loadPrecompileChannel({
|
|
637
|
+
descriptor: payload.descriptor,
|
|
638
|
+
channelId,
|
|
639
|
+
chainId,
|
|
640
|
+
escrow,
|
|
641
|
+
store,
|
|
642
|
+
validateDescriptor: true,
|
|
643
|
+
});
|
|
644
|
+
return loadingChannel;
|
|
645
|
+
};
|
|
646
|
+
// The on-chain read does not depend on the stored channel, so it overlaps the store read.
|
|
647
|
+
// Store errors still take precedence because the stored channel is awaited first.
|
|
648
|
+
let reading;
|
|
649
|
+
const readChannelState = () => {
|
|
650
|
+
if (reading)
|
|
651
|
+
return reading;
|
|
652
|
+
const startedAt = Date.now();
|
|
653
|
+
const state = resolveVoucherChannelState({
|
|
654
|
+
channel: loadChannel,
|
|
655
|
+
channelId,
|
|
656
|
+
channelStateTtl,
|
|
657
|
+
client,
|
|
658
|
+
escrow,
|
|
659
|
+
forceRefresh: true,
|
|
660
|
+
lastOnChainVerified,
|
|
661
|
+
});
|
|
662
|
+
state.catch(() => { });
|
|
663
|
+
reading = { startedAt, state };
|
|
664
|
+
return reading;
|
|
665
|
+
};
|
|
666
|
+
const recentlyRead = voucherStateMaxAge > 0 &&
|
|
667
|
+
Date.now() - (lastOnChainVerified.get(channelId) ?? 0) <=
|
|
668
|
+
Math.min(channelStateTtl, voucherStateMaxAge);
|
|
669
|
+
if (!recentlyRead)
|
|
670
|
+
readChannelState();
|
|
671
|
+
if (voucherStateMaxAge > 0 && store.updateChannelResult) {
|
|
672
|
+
const receipt = await acceptVoucherInOneUpdate(parameters, voucher, payload.descriptor);
|
|
673
|
+
if (receipt) {
|
|
674
|
+
if (reading)
|
|
675
|
+
recordOnChainReadInBackground(parameters, channelId, reading);
|
|
676
|
+
return receipt;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const channel = await loadChannel();
|
|
613
680
|
assertCredentialSourceCanSpend({ chainId, channel, source: credentialSource });
|
|
614
681
|
if (channel.finalized)
|
|
615
682
|
throw new ChannelClosedError({ reason: 'channel is finalized' });
|
|
616
|
-
|
|
683
|
+
if (canUseStoredChannelState({
|
|
617
684
|
channel,
|
|
618
685
|
channelId,
|
|
619
|
-
channelStateTtl,
|
|
620
|
-
client,
|
|
621
|
-
escrow,
|
|
622
|
-
forceRefresh: true,
|
|
623
686
|
lastOnChainVerified,
|
|
624
|
-
|
|
687
|
+
voucher,
|
|
688
|
+
voucherStateMaxAge,
|
|
689
|
+
})) {
|
|
690
|
+
if (reading)
|
|
691
|
+
recordOnChainReadInBackground(parameters, channelId, reading);
|
|
692
|
+
return ChannelStore.verifyAndAcceptVoucher({
|
|
693
|
+
store,
|
|
694
|
+
minVoucherDelta,
|
|
695
|
+
requireAdvance: parameters.requireVoucherAdvance,
|
|
696
|
+
challenge,
|
|
697
|
+
channel,
|
|
698
|
+
voucher,
|
|
699
|
+
channelState: storedChannelState(channel),
|
|
700
|
+
methodDetails: { chainId, escrowContract: escrow },
|
|
701
|
+
chargeAmount,
|
|
702
|
+
onCharged,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const { startedAt, state } = readChannelState();
|
|
706
|
+
const channelState = await state;
|
|
625
707
|
const reconciled = channel.closeRequestedAt === 0n
|
|
626
708
|
? channel
|
|
627
709
|
: ((await store.updateChannel(channelId, (current) => ChannelStore.reconcileExpiredPendingClose({
|
|
@@ -630,30 +712,7 @@ async function handleVoucherCredential(parameters) {
|
|
|
630
712
|
state: channelState,
|
|
631
713
|
}))) ?? channel);
|
|
632
714
|
if (channelState.closeRequestedAt !== 0) {
|
|
633
|
-
|
|
634
|
-
? {
|
|
635
|
-
...current,
|
|
636
|
-
settledOnChain: channelState.settled > current.settledOnChain
|
|
637
|
-
? channelState.settled
|
|
638
|
-
: current.settledOnChain,
|
|
639
|
-
closeRequestedAt: BigInt(channelState.closeRequestedAt) > current.closeRequestedAt
|
|
640
|
-
? BigInt(channelState.closeRequestedAt)
|
|
641
|
-
: current.closeRequestedAt,
|
|
642
|
-
}
|
|
643
|
-
: current);
|
|
644
|
-
if (closing && !closing.finalized && channelState.deposit > 0n)
|
|
645
|
-
await maybeSettleScheduled({
|
|
646
|
-
account: parameters.account,
|
|
647
|
-
channel: closing,
|
|
648
|
-
client,
|
|
649
|
-
feePayer: parameters.feePayer,
|
|
650
|
-
feePayerPolicy: parameters.feePayerPolicy,
|
|
651
|
-
feeToken: parameters.feeToken,
|
|
652
|
-
onSessionSettlement: parameters.onSessionSettlement,
|
|
653
|
-
onSessionSettlementFailure: parameters.onSessionSettlementFailure,
|
|
654
|
-
schedule: {},
|
|
655
|
-
store,
|
|
656
|
-
});
|
|
715
|
+
await recordPendingClose(parameters, channelId, channelState);
|
|
657
716
|
throw new ChannelClosedError({ reason: 'channel has a pending close request' });
|
|
658
717
|
}
|
|
659
718
|
return ChannelStore.verifyAndAcceptVoucher({
|
|
@@ -665,8 +724,121 @@ async function handleVoucherCredential(parameters) {
|
|
|
665
724
|
voucher,
|
|
666
725
|
channelState,
|
|
667
726
|
methodDetails: { chainId, escrowContract: escrow },
|
|
727
|
+
verifiedAt: startedAt,
|
|
728
|
+
chargeAmount,
|
|
729
|
+
onCharged,
|
|
668
730
|
});
|
|
669
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Accepts a voucher, and charges its request, in one store update with no separate read: the
|
|
734
|
+
* update checks the stored channel itself. Returns `undefined` without writing when the stored
|
|
735
|
+
* on-chain state was not verified recently enough to accept against.
|
|
736
|
+
*/
|
|
737
|
+
async function acceptVoucherInOneUpdate(parameters, voucher, descriptor) {
|
|
738
|
+
const { challenge, chainId, chargeAmount, credentialSource, escrow, lastOnChainVerified, minVoucherDelta, requireVoucherAdvance, store, } = parameters;
|
|
739
|
+
const voucherStateMaxAge = parameters.voucherStateMaxAge ?? 0;
|
|
740
|
+
const valid = await Voucher.verifyVoucher(escrow, chainId, voucher, authorizedSigner(descriptor));
|
|
741
|
+
if (!valid)
|
|
742
|
+
throw new InvalidSignatureError({ reason: 'invalid voucher signature' });
|
|
743
|
+
const outcome = await store.updateChannelResult(voucher.channelId, (current) => {
|
|
744
|
+
if (!current)
|
|
745
|
+
throw new ChannelNotFoundError({ reason: 'channel not found' });
|
|
746
|
+
if (!ChannelStore.isPrecompileState(current))
|
|
747
|
+
throw new VerificationFailedError({ reason: 'channel is not precompile-backed' });
|
|
748
|
+
assertSameDescriptor(descriptor, current.descriptor);
|
|
749
|
+
validateChannelDescriptor(descriptor, voucher.channelId, chainId, escrow, current.payee, current.token);
|
|
750
|
+
assertCredentialSourceCanSpend({ chainId, channel: current, source: credentialSource });
|
|
751
|
+
if (current.finalized)
|
|
752
|
+
throw new ChannelClosedError({ reason: 'channel is finalized' });
|
|
753
|
+
if (!canUseStoredChannelState({
|
|
754
|
+
channel: current,
|
|
755
|
+
channelId: voucher.channelId,
|
|
756
|
+
lastOnChainVerified,
|
|
757
|
+
voucher,
|
|
758
|
+
voucherStateMaxAge,
|
|
759
|
+
}))
|
|
760
|
+
return { op: 'noop', result: undefined };
|
|
761
|
+
if (requireVoucherAdvance && voucher.cumulativeAmount <= current.highestVoucherAmount)
|
|
762
|
+
throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
|
|
763
|
+
const channelState = storedChannelState(current);
|
|
764
|
+
const { alreadyAccepted } = ChannelStore.assertVoucherAmounts({
|
|
765
|
+
channel: current,
|
|
766
|
+
channelState,
|
|
767
|
+
minVoucherDelta,
|
|
768
|
+
voucher,
|
|
769
|
+
});
|
|
770
|
+
if (alreadyAccepted && chargeAmount === undefined)
|
|
771
|
+
return { op: 'noop', result: { channel: current, charged: false } };
|
|
772
|
+
const acceptance = ChannelStore.planVoucherAcceptance({
|
|
773
|
+
accepted: ChannelStore.acceptVoucherStateUpdate({ channelState, current, voucher }),
|
|
774
|
+
chargeAmount,
|
|
775
|
+
});
|
|
776
|
+
return { op: 'set', value: acceptance.channel, result: acceptance };
|
|
777
|
+
});
|
|
778
|
+
if (!outcome)
|
|
779
|
+
return undefined;
|
|
780
|
+
if (chargeAmount !== undefined) {
|
|
781
|
+
if (!outcome.charged)
|
|
782
|
+
throw ChannelStore.insufficientBalanceForCharge(outcome.channel, chargeAmount);
|
|
783
|
+
parameters.onCharged?.(outcome.channel);
|
|
784
|
+
}
|
|
785
|
+
return createSessionReceipt({
|
|
786
|
+
challengeId: challenge.id,
|
|
787
|
+
channelId: voucher.channelId,
|
|
788
|
+
acceptedCumulative: outcome.channel.highestVoucherAmount,
|
|
789
|
+
spent: outcome.channel.spent,
|
|
790
|
+
units: outcome.channel.units,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
/** Persists a pending close seen on chain and settles the channel while the payee still can. */
|
|
794
|
+
async function recordPendingClose(parameters, channelId, channelState) {
|
|
795
|
+
const { client, store } = parameters;
|
|
796
|
+
const closing = await store.updateChannel(channelId, (current) => current
|
|
797
|
+
? {
|
|
798
|
+
...current,
|
|
799
|
+
settledOnChain: channelState.settled > current.settledOnChain
|
|
800
|
+
? channelState.settled
|
|
801
|
+
: current.settledOnChain,
|
|
802
|
+
closeRequestedAt: BigInt(channelState.closeRequestedAt) > current.closeRequestedAt
|
|
803
|
+
? BigInt(channelState.closeRequestedAt)
|
|
804
|
+
: current.closeRequestedAt,
|
|
805
|
+
}
|
|
806
|
+
: current);
|
|
807
|
+
if (closing && !closing.finalized && channelState.deposit > 0n)
|
|
808
|
+
await maybeSettleScheduled({
|
|
809
|
+
account: parameters.account,
|
|
810
|
+
channel: closing,
|
|
811
|
+
client,
|
|
812
|
+
feePayer: parameters.feePayer,
|
|
813
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
814
|
+
feeToken: parameters.feeToken,
|
|
815
|
+
onSessionSettlement: parameters.onSessionSettlement,
|
|
816
|
+
onSessionSettlementFailure: parameters.onSessionSettlementFailure,
|
|
817
|
+
schedule: {},
|
|
818
|
+
store,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Records an on-chain read that a voucher did not wait for: a pending close is persisted so later
|
|
823
|
+
* vouchers are rejected, and open state extends the verified age of the stored channel.
|
|
824
|
+
*/
|
|
825
|
+
function recordOnChainReadInBackground(parameters, channelId, read) {
|
|
826
|
+
read.state
|
|
827
|
+
.then(async (state) => {
|
|
828
|
+
if (state.closeRequestedAt !== 0)
|
|
829
|
+
return recordPendingClose(parameters, channelId, state);
|
|
830
|
+
if (state.deposit === 0n)
|
|
831
|
+
return;
|
|
832
|
+
await parameters.store.updateChannel(channelId, (current) => current && !current.finalized && current.closeRequestedAt === 0n
|
|
833
|
+
? {
|
|
834
|
+
...current,
|
|
835
|
+
...ChannelStore.mergeActiveOnChainState(current, state),
|
|
836
|
+
onChainVerifiedAt: Math.max(read.startedAt, current.onChainVerifiedAt ?? 0),
|
|
837
|
+
}
|
|
838
|
+
: current);
|
|
839
|
+
})
|
|
840
|
+
.catch(() => { });
|
|
841
|
+
}
|
|
670
842
|
const sendMethods = new Set([
|
|
671
843
|
'eth_sendRawTransaction',
|
|
672
844
|
'eth_sendRawTransactionSync',
|
|
@@ -5,12 +5,11 @@ import * as ChannelStore from './ChannelStore.js';
|
|
|
5
5
|
/** Controller passed to manual-charge streaming generators. */
|
|
6
6
|
export type SessionController = {
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Commit a charge before performing billable work.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* charge is
|
|
12
|
-
*
|
|
13
|
-
* generator fails before the charge can be committed.
|
|
10
|
+
* Waits for sufficient voucher coverage and commits the charge before resolving.
|
|
11
|
+
* Once resolved, the charge is retained even if the stream is canceled or the
|
|
12
|
+
* generator fails. Await this method before starting the corresponding work.
|
|
14
13
|
*
|
|
15
14
|
* Pass an explicit raw-unit `amount` for request-aware or otherwise dynamic
|
|
16
15
|
* pricing. When omitted, the session challenge's configured tick cost is
|
|
@@ -32,6 +31,8 @@ export type MeteredStreamOptions = {
|
|
|
32
31
|
formatNeedVoucher(parameters: NeedVoucherEvent): string;
|
|
33
32
|
/** Async source or manual-charge source. */
|
|
34
33
|
generate: SessionStreamGenerator;
|
|
34
|
+
/** Verifies that the channel can still fund work and delivery; failures stop the stream. */
|
|
35
|
+
beforeCharge?: (() => MaybePromise<void>) | undefined;
|
|
35
36
|
/** Runs after a nonzero reserved charge has been committed. */
|
|
36
37
|
onChargeCommitted?: ((channel: ChannelStore.State) => MaybePromise<unknown>) | undefined;
|
|
37
38
|
/** Store polling interval when `waitForUpdate` is unavailable. */
|
|
@@ -1,38 +1,24 @@
|
|
|
1
|
-
import { commitReservedCharges, releaseReservedCharges,
|
|
1
|
+
import { commitReservedCharges, releaseReservedCharges, reserveChargeOrWait } from './Transports.js';
|
|
2
2
|
/** Applies voucher reservation and spend commits to an async session stream. */
|
|
3
3
|
export async function* meterIterable(options) {
|
|
4
|
+
const manual = typeof options.generate === 'function';
|
|
4
5
|
let prepaidUnits = options.prepaidUnits ?? 0;
|
|
5
|
-
const
|
|
6
|
-
const signal = controller.signal;
|
|
6
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
7
7
|
const reservation = {
|
|
8
8
|
store: options.store,
|
|
9
9
|
channelId: options.channelId,
|
|
10
10
|
reservationId: globalThis.crypto.randomUUID(),
|
|
11
11
|
};
|
|
12
12
|
let pending = false;
|
|
13
|
-
let cancelled = false;
|
|
14
|
-
const onAbort = () => {
|
|
15
|
-
if (signal.aborted)
|
|
16
|
-
return;
|
|
17
|
-
cancelled = true;
|
|
18
|
-
controller.abort(options.signal?.reason);
|
|
19
|
-
};
|
|
20
|
-
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
21
|
-
if (options.signal?.aborted)
|
|
22
|
-
onAbort();
|
|
23
|
-
const renewal = setInterval(() => {
|
|
24
|
-
if (pending)
|
|
25
|
-
void renewReservedCharges(reservation).catch((error) => controller.abort(error));
|
|
26
|
-
}, streamReservationLeaseMs / 3);
|
|
27
|
-
renewal.unref?.();
|
|
28
13
|
const releaseOnAbort = () => {
|
|
29
|
-
clearInterval(renewal);
|
|
30
14
|
void releaseReservedCharges(reservation).catch(() => { });
|
|
31
15
|
};
|
|
32
16
|
signal.addEventListener('abort', releaseOnAbort, { once: true });
|
|
33
17
|
if (signal.aborted)
|
|
34
18
|
releaseOnAbort();
|
|
35
19
|
const charge = async (amount) => {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
await options.beforeCharge?.();
|
|
36
22
|
signal.throwIfAborted();
|
|
37
23
|
if (amount === undefined && prepaidUnits > 0) {
|
|
38
24
|
prepaidUnits -= 1;
|
|
@@ -50,9 +36,13 @@ export async function* meterIterable(options) {
|
|
|
50
36
|
signal,
|
|
51
37
|
});
|
|
52
38
|
pending = true;
|
|
39
|
+
if (manual)
|
|
40
|
+
await commit();
|
|
53
41
|
signal.throwIfAborted();
|
|
54
42
|
};
|
|
55
43
|
const commit = async () => {
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
await options.beforeCharge?.();
|
|
56
46
|
signal.throwIfAborted();
|
|
57
47
|
if (!pending)
|
|
58
48
|
return;
|
|
@@ -63,7 +53,6 @@ export async function* meterIterable(options) {
|
|
|
63
53
|
await options.onChargeCommitted?.(channel);
|
|
64
54
|
};
|
|
65
55
|
try {
|
|
66
|
-
const manual = typeof options.generate === 'function';
|
|
67
56
|
const iterable = typeof options.generate === 'function'
|
|
68
57
|
? options.generate({ charge, signal })
|
|
69
58
|
: options.generate;
|
|
@@ -74,7 +63,7 @@ export async function* meterIterable(options) {
|
|
|
74
63
|
if (signal.aborted)
|
|
75
64
|
break;
|
|
76
65
|
if (done) {
|
|
77
|
-
if (manual)
|
|
66
|
+
if (manual && pending)
|
|
78
67
|
await commit();
|
|
79
68
|
break;
|
|
80
69
|
}
|
|
@@ -83,18 +72,13 @@ export async function* meterIterable(options) {
|
|
|
83
72
|
await commit();
|
|
84
73
|
yield value;
|
|
85
74
|
}
|
|
86
|
-
if (!cancelled)
|
|
87
|
-
signal.throwIfAborted();
|
|
88
75
|
}
|
|
89
76
|
finally {
|
|
90
|
-
clearInterval(renewal);
|
|
91
77
|
await releaseReservedCharges(reservation);
|
|
92
78
|
await iterator.return?.();
|
|
93
79
|
}
|
|
94
80
|
}
|
|
95
81
|
finally {
|
|
96
|
-
clearInterval(renewal);
|
|
97
|
-
options.signal?.removeEventListener('abort', onAbort);
|
|
98
82
|
signal.removeEventListener('abort', releaseOnAbort);
|
|
99
83
|
await releaseReservedCharges(reservation);
|
|
100
84
|
}
|
|
@@ -21,6 +21,8 @@ import { type ResolveSessionChannelId } from './RequestState.js';
|
|
|
21
21
|
import { type SettleChargedSessionChannel } from './Settlement.js';
|
|
22
22
|
import { type OnSessionSettlement, type OnSessionSettlementFailure, type SettlementSchedule } from './Settlement.js';
|
|
23
23
|
import * as Ws from './Ws.js';
|
|
24
|
+
/** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
|
|
25
|
+
export declare function verifyStream(method: object, channelId: Hex): Promise<void>;
|
|
24
26
|
/** Server-side automatic settlement schedule. */
|
|
25
27
|
export type { SettlementSchedule } from './Settlement.js';
|
|
26
28
|
/** Server-side settlement event hook types. */
|
|
@@ -131,15 +133,27 @@ export declare namespace session {
|
|
|
131
133
|
serveWebSocket: (options: ServeWebSocketOptions) => Promise<void>;
|
|
132
134
|
};
|
|
133
135
|
/** WebSocket server options supplied after the session binds shared dependencies. */
|
|
134
|
-
type ServeWebSocketOptions = Omit<Ws.serve.Options, 'onChargeCommitted' | 'settleScheduled' | 'store'>;
|
|
136
|
+
type ServeWebSocketOptions = Omit<Ws.serve.Options, 'beforeCharge' | 'onChargeCommitted' | 'settleScheduled' | 'store'>;
|
|
135
137
|
/** Request defaults inferred from the Tempo session method schema. */
|
|
136
138
|
type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.session>, 'escrowContract' | 'feePayer' | 'recipient'>;
|
|
137
139
|
/** Partial fee-sponsor policy used for server-submitted session transactions. */
|
|
138
140
|
type FeePayerPolicy = Partial<FeePayer.Policy>;
|
|
139
141
|
/** Parameters accepted by the TIP-1034 server session payment method. */
|
|
140
142
|
type Parameters = {
|
|
141
|
-
/** TTL in milliseconds for cached on-chain channel state during non-mutating validation
|
|
143
|
+
/** TTL in milliseconds for cached on-chain channel state during non-mutating validation, and before a voucher accepted under `voucherStateMaxAge` starts a fresh read. @default 5_000 */
|
|
142
144
|
channelStateTtl?: number | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* Maximum age in milliseconds of verified on-chain channel state that a voucher may be validated
|
|
147
|
+
* and accepted against without waiting for a fresh read. The fresh read then finishes in the background and
|
|
148
|
+
* records a pending close, so later vouchers are rejected. `0` reads on-chain state before every
|
|
149
|
+
* acceptance.
|
|
150
|
+
*
|
|
151
|
+
* A payer can withdraw only after the escrow's `CLOSE_GRACE_PERIOD` (900 s on Tempo) following a
|
|
152
|
+
* close request, so vouchers accepted this way stay settleable as long as the payee settles them
|
|
153
|
+
* within the grace period minus this age. Keep it well below that bound.
|
|
154
|
+
* @default 0
|
|
155
|
+
*/
|
|
156
|
+
voucherStateMaxAge?: number | undefined;
|
|
143
157
|
/** Override the fee-sponsor policy used for sponsored open/topUp transactions and server-driven close transactions. */
|
|
144
158
|
feePayerPolicy?: FeePayerPolicy | undefined;
|
|
145
159
|
/** Minimum voucher delta to accept (numeric string, default: "0"). */
|
|
@@ -159,6 +173,19 @@ export declare namespace session {
|
|
|
159
173
|
* containing the signed highest voucher for client rehydration.
|
|
160
174
|
*/
|
|
161
175
|
bootstrap?: boolean | undefined;
|
|
176
|
+
/**
|
|
177
|
+
* Lets a session challenge pay for later requests to the same route until it expires. The
|
|
178
|
+
* challenge is bound to its route, currency, recipient and channel terms but not to the amount
|
|
179
|
+
* it quoted, and every request is charged its own price from the channel's accepted balance.
|
|
180
|
+
* A client can then send a voucher with a request instead of waiting for a `402`.
|
|
181
|
+
*/
|
|
182
|
+
reusableChallenges?: boolean | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* Charges request-priced content (`unitType: 'request'`) in the same store update that accepts
|
|
185
|
+
* its voucher, before the response, instead of in a second update when the response is
|
|
186
|
+
* delivered. The server must then refund requests it cannot serve.
|
|
187
|
+
*/
|
|
188
|
+
chargeOnAcceptance?: boolean | undefined;
|
|
162
189
|
/**
|
|
163
190
|
* Atomic store backend for channel state.
|
|
164
191
|
*
|