mppx 0.13.4 → 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 +21 -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/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 +5 -0
- package/dist/server/Mppx.js +14 -13
- package/dist/server/Transport.js +10 -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.js +16 -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/precompile/Chain.d.ts +2 -0
- package/dist/tempo/session/precompile/Chain.js +3 -1
- package/dist/tempo/session/server/Session.js +10 -11
- package/dist/tempo/session/server/Settlement.js +11 -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 {
|
|
@@ -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 [
|
|
@@ -101,12 +103,22 @@ function resolveCurrencies(parameters) {
|
|
|
101
103
|
...(parameters.currency === undefined ? {} : { currencies: [parameters.currency] }),
|
|
102
104
|
});
|
|
103
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
|
+
}
|
|
104
115
|
/** Creates OUSD-first charge offers, with explicit currency lists replacing the defaults. */
|
|
105
116
|
function chargeOffers(parameters) {
|
|
106
117
|
const { currencies: _currencies, ...shared } = parameters ?? {};
|
|
107
118
|
const [first, ...rest] = resolveCurrencies(parameters ?? {});
|
|
119
|
+
const sponsorBudget = sharedSponsorBudget(parameters);
|
|
108
120
|
function create(currency) {
|
|
109
|
-
const configured = { ...shared, ...currency };
|
|
121
|
+
const configured = { ...shared, ...currency, sponsorBudget };
|
|
110
122
|
return charge_(configured);
|
|
111
123
|
}
|
|
112
124
|
return [create(first), ...rest.map(create)];
|
|
@@ -115,7 +127,7 @@ function chargeOffers(parameters) {
|
|
|
115
127
|
function sessionOffers(parameters) {
|
|
116
128
|
const { currencies: _currencies, ...shared } = parameters ?? {};
|
|
117
129
|
const [first, ...rest] = resolveCurrencies(parameters ?? {});
|
|
118
|
-
const store = parameters?.store ??
|
|
130
|
+
const store = parameters?.store ?? fallbackStore('tempo.session');
|
|
119
131
|
const settlements = new Map();
|
|
120
132
|
const key = (chainId, currency) => `${chainId}:${currency.toLowerCase()}`;
|
|
121
133
|
const settleScheduled = (channel) => {
|
|
@@ -145,7 +157,7 @@ function sessionOffers(parameters) {
|
|
|
145
157
|
function subscriptionOffers(parameters) {
|
|
146
158
|
const { currencies: _currencies, ...shared } = parameters;
|
|
147
159
|
const [first, ...rest] = resolveCurrencies(parameters);
|
|
148
|
-
const store = parameters.store ??
|
|
160
|
+
const store = parameters.store ?? fallbackStore('tempo.subscription');
|
|
149
161
|
function create(currency) {
|
|
150
162
|
const configured = { ...shared, ...currency, store };
|
|
151
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
|