mppx 0.13.3 → 0.13.5
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 +32 -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 +133 -133
- package/dist/cli/plugins/index.js +12 -12
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.js +2 -1
- 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/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 +6 -1
- package/dist/server/Mppx.js +20 -16
- package/dist/server/Transport.js +10 -0
- package/dist/server/internal/challengeMeta.d.ts +5 -0
- package/dist/server/internal/challengeMeta.js +14 -0
- 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 +116 -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/fee-payer.js +4 -0
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.js +160 -29
- package/dist/tempo/server/Methods.d.ts +5 -0
- package/dist/tempo/server/Methods.js +18 -4
- package/dist/tempo/server/Subscription.js +2 -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/session/client/Transports.js +39 -12
- package/dist/tempo/session/precompile/Chain.d.ts +4 -0
- package/dist/tempo/session/precompile/Chain.js +4 -1
- package/dist/tempo/session/server/Session.js +12 -11
- package/dist/tempo/session/server/Settlement.d.ts +7 -0
- package/dist/tempo/session/server/Settlement.js +33 -8
- package/dist/x402/server/EvmCharge.js +10 -3
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as SignatureEnvelope from 'ox/tempo/SignatureEnvelope';
|
|
2
|
-
import { decodeFunctionData, formatUnits, keccak256, parseEventLogs, } from 'viem';
|
|
3
|
-
import { getTransaction, getTransactionReceipt, sendRawTransaction, sendRawTransactionSync, signTransaction, verifyTypedData, call as viem_call, } from 'viem/actions';
|
|
2
|
+
import { decodeFunctionData, formatUnits, keccak256, parseEventLogs, WaitForTransactionReceiptTimeoutError, } from 'viem';
|
|
3
|
+
import { getTransaction, getTransactionReceipt, sendRawTransaction, sendRawTransactionSync, signTransaction, verifyTypedData, waitForTransactionReceipt, call as viem_call, } from 'viem/actions';
|
|
4
4
|
import { Abis, Actions, Transaction } from 'viem/tempo';
|
|
5
5
|
import { tempo as tempo_chain } from 'viem/tempo/chains';
|
|
6
|
-
import { PaymentError, VerificationFailedError } from '../../Errors.js';
|
|
6
|
+
import { CredentialReplayedError, PaymentError, VerificationFailedError } from '../../Errors.js';
|
|
7
7
|
import * as Expires from '../../Expires.js';
|
|
8
8
|
import * as Method from '../../Method.js';
|
|
9
9
|
import * as Store from '../../Store.js';
|
|
@@ -19,6 +19,7 @@ import * as MachineTokenCharge from '../internal/machine-token-charge.js';
|
|
|
19
19
|
import * as Proof from '../internal/proof.js';
|
|
20
20
|
import * as Selectors from '../internal/selectors.js';
|
|
21
21
|
import * as Methods from '../Methods.js';
|
|
22
|
+
import { fallbackStore } from './internal/fallback-store.js';
|
|
22
23
|
import { html as htmlContent } from './internal/html.gen.js';
|
|
23
24
|
import * as Relay from './Relay.js';
|
|
24
25
|
import * as SponsorBudget from './SponsorBudget.js';
|
|
@@ -33,11 +34,12 @@ import * as SponsorBudget from './SponsorBudget.js';
|
|
|
33
34
|
* ```
|
|
34
35
|
*/
|
|
35
36
|
export function charge(parameters = {}) {
|
|
36
|
-
const { machineTokenEnabled, allowedFeeTokens: configuredAllowedFeeTokens, amount, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, description, externalId, feeToken: configuredFeeToken, feePayerPolicy, html, relay, sponsorBudget, splits, supportedModes, validateSender, waitForConfirmation = true, } = parameters;
|
|
37
|
+
const { machineTokenEnabled, allowedFeeTokens: configuredAllowedFeeTokens, amount, confirmationTimeoutMs = 30_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, description, externalId, feeToken: configuredFeeToken, feePayerPolicy, html, relay, sponsorBudget, splits, supportedModes, validateSender, waitForConfirmation = true, } = parameters;
|
|
37
38
|
if (configuredAllowedFeeTokens?.length === 0)
|
|
38
39
|
throw new Error('`allowedFeeTokens` must contain at least one token.');
|
|
39
40
|
const storeKeyPrefix = parameters.storeKeyPrefix ?? '';
|
|
40
|
-
const rawStore = (parameters.store ??
|
|
41
|
+
const rawStore = (parameters.store ??
|
|
42
|
+
fallbackStore('tempo.charge'));
|
|
41
43
|
const store = Store.from(rawStore, { keyPrefix: storeKeyPrefix });
|
|
42
44
|
const { maxInFlightReservations: legacyMaxInFlightReservations, maxInFlightTotalFee: legacyMaxInFlightTotalFee, ...transactionFeePayerPolicy } = feePayerPolicy ?? {};
|
|
43
45
|
const sponsorBudgetConfig = sponsorBudget === false ? undefined : (sponsorBudget ?? {});
|
|
@@ -178,7 +180,13 @@ export function charge(parameters = {}) {
|
|
|
178
180
|
challengeId: challenge.id,
|
|
179
181
|
realm: challenge.realm,
|
|
180
182
|
});
|
|
181
|
-
|
|
183
|
+
// The payer is the account the matched transfers debited, not the submitter.
|
|
184
|
+
// Settlement-contract transfers are funded by the transaction sender.
|
|
185
|
+
const payers = new Set(matchedLogs.map(({ args }) => (settlementSenders.some((settlementSender) => TempoAddress.isEqual(settlementSender, args.from))
|
|
186
|
+
? receipt.from
|
|
187
|
+
: args.from).toLowerCase()));
|
|
188
|
+
const payer = payers.size === 1 ? [...payers][0] : undefined;
|
|
189
|
+
return { payer, receipt, sender, transfers };
|
|
182
190
|
}
|
|
183
191
|
async function validateProofCredential(credential, payload, context) {
|
|
184
192
|
const { chainId, challenge, client, isZeroAmount } = context;
|
|
@@ -258,17 +266,22 @@ export function charge(parameters = {}) {
|
|
|
258
266
|
async function validateCredential(credential, request, context) {
|
|
259
267
|
switch (context.payload.type) {
|
|
260
268
|
case 'hash': {
|
|
261
|
-
const { receipt, sender, transfers } = await validateHashCredential(credential, context.payload, context);
|
|
269
|
+
const { payer, receipt, sender, transfers } = await validateHashCredential(credential, context.payload, context);
|
|
262
270
|
return {
|
|
263
271
|
details: { mode: 'push', sender, transfers },
|
|
264
272
|
hash: context.payload.hash,
|
|
273
|
+
payer,
|
|
265
274
|
receipt,
|
|
266
275
|
type: 'hash',
|
|
267
276
|
};
|
|
268
277
|
}
|
|
269
278
|
case 'proof': {
|
|
270
279
|
const { sender } = await validateProofCredential(credential, context.payload, context);
|
|
271
|
-
return {
|
|
280
|
+
return {
|
|
281
|
+
details: { mode: 'proof', sender },
|
|
282
|
+
payer: sender,
|
|
283
|
+
type: 'proof',
|
|
284
|
+
};
|
|
272
285
|
}
|
|
273
286
|
case 'transaction': {
|
|
274
287
|
const { fundingCurrency, isFeePayerTx, serializedTransaction, settlementSenders, transaction, transfers, } = await validateTransactionCredential(credential, context.payload, request, context);
|
|
@@ -281,6 +294,7 @@ export function charge(parameters = {}) {
|
|
|
281
294
|
},
|
|
282
295
|
fundingCurrency,
|
|
283
296
|
isFeePayerTx,
|
|
297
|
+
payer: transaction.from,
|
|
284
298
|
serializedTransaction,
|
|
285
299
|
settlementSenders,
|
|
286
300
|
transaction,
|
|
@@ -387,6 +401,7 @@ export function charge(parameters = {}) {
|
|
|
387
401
|
details: validated.details,
|
|
388
402
|
intent: 'charge',
|
|
389
403
|
method: 'tempo',
|
|
404
|
+
payer: validated.payer,
|
|
390
405
|
request: context.resolvedRequest,
|
|
391
406
|
source: credential.source,
|
|
392
407
|
};
|
|
@@ -407,7 +422,7 @@ export function charge(parameters = {}) {
|
|
|
407
422
|
case 'hash': {
|
|
408
423
|
const { hash, receipt } = validated;
|
|
409
424
|
if (!(await tryClaimHash(store, hash, replayExpires))) {
|
|
410
|
-
throw new
|
|
425
|
+
throw new CredentialReplayedError({
|
|
411
426
|
reason: 'Transaction hash has already been used',
|
|
412
427
|
});
|
|
413
428
|
}
|
|
@@ -427,7 +442,7 @@ export function charge(parameters = {}) {
|
|
|
427
442
|
}
|
|
428
443
|
case 'proof': {
|
|
429
444
|
if (proofStore && !(await tryClaimProof(proofStore, challenge.id, replayExpires))) {
|
|
430
|
-
throw new
|
|
445
|
+
throw new CredentialReplayedError({
|
|
431
446
|
reason: 'Proof credential has already been used',
|
|
432
447
|
});
|
|
433
448
|
}
|
|
@@ -438,7 +453,7 @@ export function charge(parameters = {}) {
|
|
|
438
453
|
// Pre-broadcast dedup: catch exact byte-for-byte replays early.
|
|
439
454
|
const hash = keccak256(serializedTransaction);
|
|
440
455
|
if (!(await tryClaimHash(store, hash, replayExpires))) {
|
|
441
|
-
throw new
|
|
456
|
+
throw new CredentialReplayedError({
|
|
442
457
|
reason: 'Transaction hash has already been used',
|
|
443
458
|
});
|
|
444
459
|
}
|
|
@@ -480,6 +495,13 @@ export function charge(parameters = {}) {
|
|
|
480
495
|
return { feePayer: feePayerAccount.address, transaction: sponsored };
|
|
481
496
|
},
|
|
482
497
|
});
|
|
498
|
+
FeePayer.assertTransactionPolicy({
|
|
499
|
+
challengeExpires: expires,
|
|
500
|
+
chainId: chainId ?? client.chain.id,
|
|
501
|
+
details: { amount, currency, recipient },
|
|
502
|
+
policy: transactionFeePayerPolicy,
|
|
503
|
+
transaction: completed.transaction,
|
|
504
|
+
});
|
|
483
505
|
return {
|
|
484
506
|
serializedTransaction: await signTransaction(client, completed.transaction),
|
|
485
507
|
sponsor: completed.feePayer,
|
|
@@ -515,7 +537,7 @@ export function charge(parameters = {}) {
|
|
|
515
537
|
finalHash = keccak256(serializedTransaction_final);
|
|
516
538
|
if (finalHash.toLowerCase() !== hash.toLowerCase()) {
|
|
517
539
|
if (!(await tryClaimHash(store, finalHash, replayExpires)))
|
|
518
|
-
throw new
|
|
540
|
+
throw new CredentialReplayedError({
|
|
519
541
|
reason: 'Transaction hash has already been used',
|
|
520
542
|
});
|
|
521
543
|
claimedFinalHash = finalHash;
|
|
@@ -560,11 +582,51 @@ export function charge(parameters = {}) {
|
|
|
560
582
|
throw new VerificationFailedError({
|
|
561
583
|
reason: 'Sponsor budget reservation ownership was lost before broadcast',
|
|
562
584
|
});
|
|
585
|
+
if (isFeePayerTx)
|
|
586
|
+
FeePayer.assertTransactionPolicy({
|
|
587
|
+
challengeExpires: expires,
|
|
588
|
+
chainId: chainId ?? client.chain.id,
|
|
589
|
+
details: { amount, currency, recipient },
|
|
590
|
+
policy: transactionFeePayerPolicy,
|
|
591
|
+
transaction,
|
|
592
|
+
});
|
|
563
593
|
broadcastAttempted = true;
|
|
564
594
|
if (waitForConfirmation) {
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
595
|
+
const start = Date.now();
|
|
596
|
+
// The sync send gets at most 2/3 of the budget, reserving the rest for recovery.
|
|
597
|
+
const sendTimeoutMs = Math.floor((confirmationTimeoutMs * 2) / 3);
|
|
598
|
+
const deadline = start + confirmationTimeoutMs;
|
|
599
|
+
// An expiring transaction can no longer be included after this point.
|
|
600
|
+
const validUntil = transaction.validBefore === undefined
|
|
601
|
+
? Number.POSITIVE_INFINITY
|
|
602
|
+
: transaction.validBefore * 1_000 + 2_000;
|
|
603
|
+
let receipt;
|
|
604
|
+
try {
|
|
605
|
+
receipt = await withDeadline(sendRawTransactionSync(client, {
|
|
606
|
+
serializedTransaction: serializedTransaction_final,
|
|
607
|
+
timeout: sendTimeoutMs,
|
|
608
|
+
}), start + sendTimeoutMs, new WaitForTransactionReceiptTimeoutError({ hash: finalHash }));
|
|
609
|
+
}
|
|
610
|
+
catch (broadcastError) {
|
|
611
|
+
if (!isAmbiguousBroadcastError(broadcastError))
|
|
612
|
+
throw broadcastError;
|
|
613
|
+
// The node may have accepted the transaction, so poll for its receipt.
|
|
614
|
+
const timeout = Math.min(deadline, validUntil) - Date.now();
|
|
615
|
+
let recovered;
|
|
616
|
+
if (timeout > 0)
|
|
617
|
+
recovered = await waitForTransactionReceipt(client, {
|
|
618
|
+
checkReplacement: false,
|
|
619
|
+
hash: finalHash,
|
|
620
|
+
timeout,
|
|
621
|
+
}).catch(() => undefined);
|
|
622
|
+
// When the validity cap ends polling, a transaction mined just before
|
|
623
|
+
// `validBefore` may only now be visible, so look it up exactly once more.
|
|
624
|
+
if (!recovered && validUntil < deadline)
|
|
625
|
+
recovered = await withDeadline(getTransactionReceipt(client, { hash: finalHash }), Math.max(deadline, Date.now() + client.pollingInterval), broadcastError).catch(() => undefined);
|
|
626
|
+
if (!recovered)
|
|
627
|
+
throw broadcastError;
|
|
628
|
+
receipt = recovered;
|
|
629
|
+
}
|
|
568
630
|
if (reservation)
|
|
569
631
|
await SponsorBudget.release(sponsorBudgetStore, reservation);
|
|
570
632
|
const matchedLogs = await assertTransferLogs(receipt, {
|
|
@@ -601,13 +663,27 @@ export function charge(parameters = {}) {
|
|
|
601
663
|
return toReceipt(reference, fundingCurrency);
|
|
602
664
|
}
|
|
603
665
|
catch (error) {
|
|
604
|
-
if (
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
666
|
+
if (reservation) {
|
|
667
|
+
try {
|
|
668
|
+
// A broadcast transaction may still land, so its fee stays reserved
|
|
669
|
+
// until reconciliation observes a receipt or the reservation expires.
|
|
670
|
+
if (broadcastAttempted)
|
|
671
|
+
await SponsorBudget.transition(sponsorBudgetStore, reservation, 'pending');
|
|
672
|
+
else
|
|
673
|
+
await SponsorBudget.release(sponsorBudgetStore, reservation);
|
|
674
|
+
}
|
|
675
|
+
catch (budgetError) {
|
|
676
|
+
// The reservation expires on its own; the settlement error and
|
|
677
|
+
// claim cleanup take precedence.
|
|
678
|
+
console.error('mppx: sponsor budget cleanup error', budgetError);
|
|
679
|
+
}
|
|
610
680
|
}
|
|
681
|
+
// Only a successful settlement consumes the credential. A signed
|
|
682
|
+
// transaction can be mined at most once, so a retry re-broadcasts it
|
|
683
|
+
// and recovers the receipt instead of being rejected as a replay.
|
|
684
|
+
if (claimedFinalHash)
|
|
685
|
+
await releaseHashUse(store, claimedFinalHash);
|
|
686
|
+
await releaseHashUse(store, hash);
|
|
611
687
|
throw error;
|
|
612
688
|
}
|
|
613
689
|
}
|
|
@@ -932,6 +1008,57 @@ async function isActiveAccessKey(client, parameters) {
|
|
|
932
1008
|
return false;
|
|
933
1009
|
}
|
|
934
1010
|
}
|
|
1011
|
+
const ambiguousErrorNames = new Set([
|
|
1012
|
+
'AbortError',
|
|
1013
|
+
'SocketClosedError',
|
|
1014
|
+
'TimeoutError',
|
|
1015
|
+
'WaitForTransactionReceiptTimeoutError',
|
|
1016
|
+
]);
|
|
1017
|
+
/**
|
|
1018
|
+
* Whether a failed synchronous broadcast may still have reached the mempool,
|
|
1019
|
+
* so its receipt is worth polling for. Every other error is a definitive
|
|
1020
|
+
* rejection (e.g. method not found, invalid params, insufficient funds).
|
|
1021
|
+
*
|
|
1022
|
+
* Ambiguous outcomes, matched anywhere in the `cause` chain:
|
|
1023
|
+
* - Timeouts and aborts: errors named `TimeoutError` or `AbortError`, or the
|
|
1024
|
+
* local confirmation deadline (`WaitForTransactionReceiptTimeoutError`).
|
|
1025
|
+
* - Lost connections: `SocketClosedError`, or an `HttpRequestError` with no
|
|
1026
|
+
* status (network failure) or a 5xx status.
|
|
1027
|
+
* - JSON-RPC code `4`: the EIP-7966 timeout for a pooled, unconfirmed transaction.
|
|
1028
|
+
* - Messages containing the words "already known" or "known transaction": already
|
|
1029
|
+
* pooled ("unknown transaction type" does not match).
|
|
1030
|
+
*/
|
|
1031
|
+
function isAmbiguousBroadcastError(error) {
|
|
1032
|
+
for (let cause = error; cause; cause = cause.cause) {
|
|
1033
|
+
if (typeof cause !== 'object')
|
|
1034
|
+
return false;
|
|
1035
|
+
const { code, message, name, status } = cause;
|
|
1036
|
+
if (name === 'HttpRequestError')
|
|
1037
|
+
return status === undefined || Number(status) >= 500;
|
|
1038
|
+
if (ambiguousErrorNames.has(name))
|
|
1039
|
+
return true;
|
|
1040
|
+
if (code === 4)
|
|
1041
|
+
return true;
|
|
1042
|
+
if (typeof message === 'string' && /\balready ?known\b|\bknown transaction\b/i.test(message))
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
/** Rejects with `error` if `promise` has not settled by `deadline` (epoch ms). */
|
|
1048
|
+
async function withDeadline(promise, deadline, error) {
|
|
1049
|
+
let timer;
|
|
1050
|
+
try {
|
|
1051
|
+
return await Promise.race([
|
|
1052
|
+
promise,
|
|
1053
|
+
new Promise((_, reject) => {
|
|
1054
|
+
timer = setTimeout(() => reject(error), deadline - Date.now());
|
|
1055
|
+
}),
|
|
1056
|
+
]);
|
|
1057
|
+
}
|
|
1058
|
+
finally {
|
|
1059
|
+
clearTimeout(timer);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
935
1062
|
/** @internal */
|
|
936
1063
|
function toReceipt(reference, fundingCurrency) {
|
|
937
1064
|
return {
|
|
@@ -943,30 +1070,34 @@ function toReceipt(reference, fundingCurrency) {
|
|
|
943
1070
|
};
|
|
944
1071
|
}
|
|
945
1072
|
function assertChallengeBoundMemo(matchedLogs, parameters) {
|
|
946
|
-
|
|
1073
|
+
const memos = matchedLogs.map((log) => (log.kind === 'memo' ? log.args.memo : undefined));
|
|
1074
|
+
// The transfer already settled to the recipient, so another payment would not help.
|
|
1075
|
+
if (!isChallengeBound(memos, parameters))
|
|
1076
|
+
throw new CredentialReplayedError({ reason: 'memo is not bound to this challenge' });
|
|
947
1077
|
}
|
|
948
1078
|
function assertChallengeBoundCallMemo(matchedCalls, parameters) {
|
|
949
|
-
|
|
1079
|
+
const memos = matchedCalls.map((call) => call.memo);
|
|
1080
|
+
if (!isChallengeBound(memos, parameters))
|
|
1081
|
+
throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
|
|
950
1082
|
}
|
|
951
1083
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
1084
|
+
* Whether matched payment transfers carry a challenge-bound attribution memo and
|
|
1085
|
+
* no conflicting attribution. Otherwise, a split payment could authorize another
|
|
954
1086
|
* challenge after the first challenge's replay marker expires. Unmatched transfers
|
|
955
1087
|
* cannot establish the binding; plain transfers and non-MPP memos remain allowed.
|
|
956
1088
|
* @internal
|
|
957
1089
|
*/
|
|
958
|
-
function
|
|
1090
|
+
function isChallengeBound(memos, parameters) {
|
|
959
1091
|
let bound = false;
|
|
960
1092
|
for (const memo of memos) {
|
|
961
1093
|
if (!memo || !Attribution.isMppMemo(memo))
|
|
962
1094
|
continue;
|
|
963
1095
|
if (!Attribution.verifyServer(memo, parameters.realm) ||
|
|
964
1096
|
!Attribution.verifyChallengeBinding(memo, parameters.challengeId))
|
|
965
|
-
|
|
1097
|
+
return false;
|
|
966
1098
|
bound = true;
|
|
967
1099
|
}
|
|
968
|
-
|
|
969
|
-
throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
|
|
1100
|
+
return bound;
|
|
970
1101
|
}
|
|
971
1102
|
/** @internal */
|
|
972
1103
|
class MismatchError extends PaymentError {
|
|
@@ -2,6 +2,7 @@ import type { NoExtraKeys } from '../../internal/types.js';
|
|
|
2
2
|
import * as Store from '../../Store.js';
|
|
3
3
|
import * as Currencies from '../internal/currencies.js';
|
|
4
4
|
import { charge as sessionCharge_, session as session_, settle as settle_, settleBatch as settleBatch_ } from '../session/server/Session.js';
|
|
5
|
+
import * as Settlement from '../session/server/Settlement.js';
|
|
5
6
|
import type { SessionController as SessionController_ } from '../session/server/Sse.js';
|
|
6
7
|
import * as Ws_ from '../session/server/Ws.js';
|
|
7
8
|
import { charge as charge_ } from './Charge.js';
|
|
@@ -2425,6 +2426,10 @@ export declare namespace tempo {
|
|
|
2425
2426
|
const settle: typeof settle_;
|
|
2426
2427
|
/** Batch-settle precompile-backed session channels. */
|
|
2427
2428
|
const settleBatch: typeof settleBatch_;
|
|
2429
|
+
/** Raised by `settle` when its transaction confirmed but the channel store could not record it. */
|
|
2430
|
+
const SettlementCheckpointError: typeof Settlement.SettlementCheckpointError;
|
|
2431
|
+
/** Raised by `settle` when its transaction confirmed but the channel store could not record it. */
|
|
2432
|
+
type SettlementCheckpointError = Settlement.SettlementCheckpointError;
|
|
2428
2433
|
/** Types for Tempo session streams. */
|
|
2429
2434
|
namespace Sse {
|
|
2430
2435
|
/** Controller passed to manual-charge SSE generators. */
|
|
@@ -5,6 +5,7 @@ import { charge as sessionCharge_, session as session_, settle as settle_, settl
|
|
|
5
5
|
import * as Settlement from '../session/server/Settlement.js';
|
|
6
6
|
import * as Ws_ from '../session/server/Ws.js';
|
|
7
7
|
import { charge as charge_ } from './Charge.js';
|
|
8
|
+
import { fallbackStore } from './internal/fallback-store.js';
|
|
8
9
|
import { renew as renewSubscription_, subscription as subscription_ } from './Subscription.js';
|
|
9
10
|
const sessionServer = Object.assign(session_, {
|
|
10
11
|
charge: sessionCharge_,
|
|
@@ -58,9 +59,10 @@ export function tempo(parameters) {
|
|
|
58
59
|
}
|
|
59
60
|
const { currencies: _currencies, ...shared } = parameters ?? {};
|
|
60
61
|
const [first, ...rest] = Currencies.resolve(parameters ?? {});
|
|
62
|
+
const sponsorBudget = sharedSponsorBudget(parameters);
|
|
61
63
|
const sessions = sessionOffers(parameters);
|
|
62
64
|
function createMethods(currency, index) {
|
|
63
|
-
const configured = { ...shared, ...currency };
|
|
65
|
+
const configured = { ...shared, ...currency, sponsorBudget };
|
|
64
66
|
return [createChargeMethod(configured), sessions[index]];
|
|
65
67
|
}
|
|
66
68
|
return [
|
|
@@ -87,6 +89,8 @@ export function tempo(parameters) {
|
|
|
87
89
|
tempo.settle = settle_;
|
|
88
90
|
/** Batch-settle precompile-backed session channels. */
|
|
89
91
|
tempo.settleBatch = settleBatch_;
|
|
92
|
+
/** Raised by `settle` when its transaction confirmed but the channel store could not record it. */
|
|
93
|
+
tempo.SettlementCheckpointError = Settlement.SettlementCheckpointError;
|
|
90
94
|
/** Experimental websocket helpers for Tempo sessions. */
|
|
91
95
|
tempo.Ws = Ws_;
|
|
92
96
|
})(tempo || (tempo = {}));
|
|
@@ -99,12 +103,22 @@ function resolveCurrencies(parameters) {
|
|
|
99
103
|
...(parameters.currency === undefined ? {} : { currencies: [parameters.currency] }),
|
|
100
104
|
});
|
|
101
105
|
}
|
|
106
|
+
/** Shares sponsor exposure across sibling offers without changing replay-store policy. */
|
|
107
|
+
function sharedSponsorBudget(parameters = {}) {
|
|
108
|
+
if (parameters.sponsorBudget === false)
|
|
109
|
+
return false;
|
|
110
|
+
return {
|
|
111
|
+
...parameters.sponsorBudget,
|
|
112
|
+
store: parameters.sponsorBudget?.store ?? parameters.store ?? Store.memory(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
102
115
|
/** Creates OUSD-first charge offers, with explicit currency lists replacing the defaults. */
|
|
103
116
|
function chargeOffers(parameters) {
|
|
104
117
|
const { currencies: _currencies, ...shared } = parameters ?? {};
|
|
105
118
|
const [first, ...rest] = resolveCurrencies(parameters ?? {});
|
|
119
|
+
const sponsorBudget = sharedSponsorBudget(parameters);
|
|
106
120
|
function create(currency) {
|
|
107
|
-
const configured = { ...shared, ...currency };
|
|
121
|
+
const configured = { ...shared, ...currency, sponsorBudget };
|
|
108
122
|
return charge_(configured);
|
|
109
123
|
}
|
|
110
124
|
return [create(first), ...rest.map(create)];
|
|
@@ -113,7 +127,7 @@ function chargeOffers(parameters) {
|
|
|
113
127
|
function sessionOffers(parameters) {
|
|
114
128
|
const { currencies: _currencies, ...shared } = parameters ?? {};
|
|
115
129
|
const [first, ...rest] = resolveCurrencies(parameters ?? {});
|
|
116
|
-
const store = parameters?.store ??
|
|
130
|
+
const store = parameters?.store ?? fallbackStore('tempo.session');
|
|
117
131
|
const settlements = new Map();
|
|
118
132
|
const key = (chainId, currency) => `${chainId}:${currency.toLowerCase()}`;
|
|
119
133
|
const settleScheduled = (channel) => {
|
|
@@ -143,7 +157,7 @@ function sessionOffers(parameters) {
|
|
|
143
157
|
function subscriptionOffers(parameters) {
|
|
144
158
|
const { currencies: _currencies, ...shared } = parameters;
|
|
145
159
|
const [first, ...rest] = resolveCurrencies(parameters);
|
|
146
|
-
const store = parameters.store ??
|
|
160
|
+
const store = parameters.store ?? fallbackStore('tempo.subscription');
|
|
147
161
|
function create(currency) {
|
|
148
162
|
const configured = { ...shared, ...currency, store };
|
|
149
163
|
return subscription_(configured);
|
|
@@ -18,6 +18,7 @@ import * as Methods from '../Methods.js';
|
|
|
18
18
|
import { assertSubscriptionTiming, toSubscriptionPeriodSeconds, verifySubscriptionKeyAuthorization, } from '../subscription/KeyAuthorization.js';
|
|
19
19
|
import * as SubscriptionReceipt from '../subscription/Receipt.js';
|
|
20
20
|
import * as SubscriptionStore from '../subscription/Store.js';
|
|
21
|
+
import { fallbackStore } from './internal/fallback-store.js';
|
|
21
22
|
/**
|
|
22
23
|
* Creates a Tempo subscription method for recurring TIP-20 token payments.
|
|
23
24
|
*
|
|
@@ -25,7 +26,7 @@ import * as SubscriptionStore from '../subscription/Store.js';
|
|
|
25
26
|
*/
|
|
26
27
|
export function subscription(p) {
|
|
27
28
|
const parameters = p;
|
|
28
|
-
const rawStore = (parameters.store ??
|
|
29
|
+
const rawStore = (parameters.store ?? fallbackStore('tempo.subscription'));
|
|
29
30
|
if (typeof rawStore.update !== 'function') {
|
|
30
31
|
throw new Error('tempo.subscription() requires an atomic store with `update`.');
|
|
31
32
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Store from '../../../Store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates the in-memory store used when a Tempo server method is configured without `store`.
|
|
4
|
+
*
|
|
5
|
+
* Replay protection backed by this store only covers the current instance, so this warns once
|
|
6
|
+
* per `method` per process.
|
|
7
|
+
*/
|
|
8
|
+
export declare function fallbackStore(method: string): Store.AtomicStore;
|
|
9
|
+
//# sourceMappingURL=fallback-store.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { warnOnce } from '../../../internal/warnOnce.js';
|
|
2
|
+
import * as Store from '../../../Store.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the in-memory store used when a Tempo server method is configured without `store`.
|
|
5
|
+
*
|
|
6
|
+
* Replay protection backed by this store only covers the current instance, so this warns once
|
|
7
|
+
* per `method` per process.
|
|
8
|
+
*/
|
|
9
|
+
export function fallbackStore(method) {
|
|
10
|
+
warnOnce(`memory-store:${method}`, `${method}() has no \`store\` configured and is using an in-memory store. Replay protection and payment state are scoped to this instance, so a credential accepted by one replica, serverless isolate, or per-request \`Mppx\` instance can be accepted again by another. Pass a shared atomic store (e.g. \`Store.redis()\`, \`Store.upstash()\`, or \`Store.cloudflare()\` with \`update\`) when running more than one instance.`);
|
|
11
|
+
return Store.memory();
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=fallback-store.js.map
|