mppx 0.13.5 → 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 +27 -0
- package/dist/Credential.d.ts +3 -0
- package/dist/Credential.js +9 -0
- package/dist/cli/cli.js +131 -131
- package/dist/cli/plugins/evm.js +2 -2
- package/dist/cli/plugins/index.js +11 -11
- package/dist/client/internal/MethodChallenge.d.ts +2 -0
- package/dist/client/internal/MethodChallenge.js +4 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/server/Mppx.js +31 -5
- package/dist/stripe/server/Methods.js +4 -1
- package/dist/tempo/client/Charge.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 -12
- package/dist/tempo/legacy/session/Chain.js +8 -2
- package/dist/tempo/server/Methods.js +12 -1
- package/dist/tempo/server/Relay.js +6 -22
- package/dist/tempo/server/Subscription.js +3 -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.js +5 -2
- 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 +114 -33
- package/dist/tempo/session/server/Settlement.d.ts +9 -0
- package/dist/tempo/session/server/Settlement.js +12 -0
- 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
|
@@ -17,4 +17,6 @@ export declare function register<const method extends Method.AnyClient>(method:
|
|
|
17
17
|
export declare function has(method: Method.AnyClient): boolean;
|
|
18
18
|
/** Runs method-specific work before creating a challenge credential. */
|
|
19
19
|
export declare function handle(method: Method.AnyClient, parameters: HandlerParameters): Promise<void>;
|
|
20
|
+
/** Returns the registered internal hook so a manager can coordinate its work. */
|
|
21
|
+
export declare function get(method: Method.AnyClient): Handler | undefined;
|
|
20
22
|
//# sourceMappingURL=MethodChallenge.d.ts.map
|
|
@@ -12,4 +12,8 @@ export function has(method) {
|
|
|
12
12
|
export function handle(method, parameters) {
|
|
13
13
|
return Promise.resolve(handlers.get(method)?.(parameters));
|
|
14
14
|
}
|
|
15
|
+
/** Returns the registered internal hook so a manager can coordinate its work. */
|
|
16
|
+
export function get(method) {
|
|
17
|
+
return handlers.get(method);
|
|
18
|
+
}
|
|
15
19
|
//# sourceMappingURL=MethodChallenge.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Current mppx package version. */
|
|
2
|
-
export declare const version = "0.13.
|
|
2
|
+
export declare const version = "0.13.6";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.13.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.13.6";
|
|
5
5
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/internal/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.ts.
|
|
2
2
|
/** Current mppx package version. */
|
|
3
|
-
export const version = '0.13.
|
|
3
|
+
export const version = '0.13.6';
|
|
4
4
|
/** Canonical SDK identifier for this mppx release. */
|
|
5
5
|
export const sdkIdentifier = `mppx/${version}`;
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/server/Mppx.js
CHANGED
|
@@ -1417,6 +1417,19 @@ function getPinnedRequestBindingMismatch(expectedRequest, actualRequest) {
|
|
|
1417
1417
|
return (getCoreBindingMismatch(expected.coreBinding, actual.coreBinding) ??
|
|
1418
1418
|
getMethodBindingMismatch(expected.methodBinding, actual.methodBinding));
|
|
1419
1419
|
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Compares a credential with a composed handler's route request when dispatching. The route
|
|
1422
|
+
* request lacks method details that the handler's `request()` hook adds (for example a session's
|
|
1423
|
+
* `chainId` and `sessionProtocol`), so only the method fields it defines are compared. The
|
|
1424
|
+
* selected handler still checks every pinned field against its full challenge.
|
|
1425
|
+
*/
|
|
1426
|
+
function getConfiguredRequestBindingMismatch(configuredRequest, actualRequest) {
|
|
1427
|
+
const expected = getPinnedRequestBinding(configuredRequest);
|
|
1428
|
+
const actual = getPinnedRequestBinding(actualRequest);
|
|
1429
|
+
return (getCoreBindingMismatch(expected.coreBinding, actual.coreBinding) ??
|
|
1430
|
+
methodBindingFields.find((field) => expected.methodBinding[field] !== undefined &&
|
|
1431
|
+
!isDeepStrictEqual(expected.methodBinding[field], actual.methodBinding[field])));
|
|
1432
|
+
}
|
|
1420
1433
|
function getCoreBindingMismatch(expected, actual) {
|
|
1421
1434
|
return coreBindingFields.find((field) => !isDeepStrictEqual(expected[field], actual[field]));
|
|
1422
1435
|
}
|
|
@@ -1448,6 +1461,15 @@ function getPinnedRequestBinding(request) {
|
|
|
1448
1461
|
},
|
|
1449
1462
|
};
|
|
1450
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Compares a credential with a composed handler's stable route binding when dispatching. The route
|
|
1466
|
+
* request lacks fields its `request()` hook adds, so only the fields it defines are compared; the
|
|
1467
|
+
* selected handler still checks the full binding against its challenge.
|
|
1468
|
+
*/
|
|
1469
|
+
function getConfiguredStableBindingMismatch(expected, actual) {
|
|
1470
|
+
return Object.keys(expected).find((field) => expected[field] !== undefined &&
|
|
1471
|
+
!isDeepStrictEqual(normalizeComparable(expected[field]), normalizeComparable(actual[field])));
|
|
1472
|
+
}
|
|
1451
1473
|
function getRequestBindingMismatch(expected, actual) {
|
|
1452
1474
|
const fields = [
|
|
1453
1475
|
...Object.keys(expected),
|
|
@@ -1602,9 +1624,13 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1602
1624
|
if (internal.name !== credMethod || internal.intent !== credIntent)
|
|
1603
1625
|
return false;
|
|
1604
1626
|
const mismatch = internal._stableBinding
|
|
1605
|
-
?
|
|
1606
|
-
:
|
|
1607
|
-
|
|
1627
|
+
? getConfiguredStableBindingMismatch(getStableBinding(internal._canonicalRequest, internal._stableBinding), getStableBinding(credReq, internal._stableBinding))
|
|
1628
|
+
: getConfiguredRequestBindingMismatch(internal._canonicalRequest, credReq);
|
|
1629
|
+
// A handler without a static scope issues challenges scoped to the request's route.
|
|
1630
|
+
const effectiveMeta = internal.scope === undefined
|
|
1631
|
+
? Scope.merge({ meta: internal.meta, scope: Scope.get(input) })
|
|
1632
|
+
: internal.meta;
|
|
1633
|
+
return !mismatch && opaqueValuesMatch(effectiveMeta, credential.challenge.meta);
|
|
1608
1634
|
}
|
|
1609
1635
|
catch {
|
|
1610
1636
|
return false;
|
|
@@ -1806,8 +1832,8 @@ function composeMcpHandlers(handlers, transport) {
|
|
|
1806
1832
|
const match = candidates.find((handler) => getConfiguredOffers(handler).some((internal) => {
|
|
1807
1833
|
try {
|
|
1808
1834
|
const mismatch = internal._stableBinding
|
|
1809
|
-
?
|
|
1810
|
-
:
|
|
1835
|
+
? getConfiguredStableBindingMismatch(getStableBinding(internal._canonicalRequest, internal._stableBinding), getStableBinding(credential.challenge.request, internal._stableBinding))
|
|
1836
|
+
: getConfiguredRequestBindingMismatch(internal._canonicalRequest, credential.challenge.request);
|
|
1811
1837
|
return !mismatch && opaqueValuesMatch(internal.meta, credential.challenge.meta);
|
|
1812
1838
|
}
|
|
1813
1839
|
catch {
|
|
@@ -133,7 +133,7 @@ export function stripe(parameters) {
|
|
|
133
133
|
const suppliedStore = params.store;
|
|
134
134
|
if (suppliedStore !== undefined && suppliedStore !== store)
|
|
135
135
|
throw new Error('stripe.create() Tempo sessions must use the factory-level shared atomic `store`.');
|
|
136
|
-
const { recipient, onSessionSettlement, ...rest } = params;
|
|
136
|
+
const { recipient, canOffer, onSessionSettlement, ...rest } = params;
|
|
137
137
|
return tempoSession({
|
|
138
138
|
currency: tempoCurrency,
|
|
139
139
|
recipient,
|
|
@@ -141,6 +141,9 @@ export function stripe(parameters) {
|
|
|
141
141
|
...(hostedTempoFeePayer && { feePayer: hostedTempoFeePayer }),
|
|
142
142
|
...rest,
|
|
143
143
|
store,
|
|
144
|
+
canOffer: canOffer
|
|
145
|
+
? async (context) => cryptoCanOffer(context) && (await canOffer(context))
|
|
146
|
+
: cryptoCanOffer,
|
|
144
147
|
async onSessionSettlement(context) {
|
|
145
148
|
await tempoPaymentHandler({
|
|
146
149
|
intent: 'session',
|
|
@@ -17,6 +17,8 @@ export declare namespace findCalls {
|
|
|
17
17
|
amountOut: bigint;
|
|
18
18
|
/** Candidate input tokens to swap from, in priority order. */
|
|
19
19
|
tokenIn: readonly Address[];
|
|
20
|
+
/** Maximum input-token units to spend. Defaults to the missing output amount plus slippage (1:1 units). */
|
|
21
|
+
maxAmountIn?: bigint | undefined;
|
|
20
22
|
/** Max slippage tolerance as a percentage (e.g. `1` = 1%). */
|
|
21
23
|
slippage: number;
|
|
22
24
|
/** Address of the target token to acquire. */
|
|
@@ -29,8 +31,15 @@ export declare namespace findCalls {
|
|
|
29
31
|
export declare function resolve(value: resolve.Value | undefined, defaultCurrencies: readonly Address[]): resolve.Resolved | false;
|
|
30
32
|
export declare namespace resolve {
|
|
31
33
|
type Options = {
|
|
34
|
+
/** Maximum input-token units per swap. Defaults to parity with the missing output amount plus slippage; set explicitly for non-parity pairs. */
|
|
35
|
+
maxAmountIn?: bigint | undefined;
|
|
32
36
|
/** Fallback tokens to try swapping from, in priority order. */
|
|
33
37
|
tokenIn?: Address[] | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Whether to also try the default currencies after `tokenIn`. Set `false` to swap only from
|
|
40
|
+
* `tokenIn`, as an allowlist. Ignored without `tokenIn`. @default true
|
|
41
|
+
*/
|
|
42
|
+
fallback?: boolean | undefined;
|
|
34
43
|
/** Max slippage tolerance as a percentage (e.g. `1` = 1%). @default 1 */
|
|
35
44
|
slippage?: number | undefined;
|
|
36
45
|
};
|
|
@@ -38,6 +47,7 @@ export declare namespace resolve {
|
|
|
38
47
|
type Resolved = {
|
|
39
48
|
tokenIn: readonly Address[];
|
|
40
49
|
slippage: number;
|
|
50
|
+
maxAmountIn?: bigint | undefined;
|
|
41
51
|
};
|
|
42
52
|
}
|
|
43
53
|
export declare class InsufficientFundsError extends Error {
|
|
@@ -8,6 +8,7 @@ const bps = 10000n;
|
|
|
8
8
|
export const defaultCurrencies = [
|
|
9
9
|
defaults.tokens.pathUsd,
|
|
10
10
|
defaults.tokens.usdc,
|
|
11
|
+
defaults.tokens.ousd,
|
|
11
12
|
];
|
|
12
13
|
/**
|
|
13
14
|
* Finds the optimal swap calls to acquire `amountOut` of `tokenOut`,
|
|
@@ -18,15 +19,34 @@ export const defaultCurrencies = [
|
|
|
18
19
|
*/
|
|
19
20
|
export async function findCalls(client, parameters) {
|
|
20
21
|
const { account, amountOut, tokenOut, tokenIn, slippage } = parameters;
|
|
22
|
+
if (!Number.isFinite(slippage) || slippage < 0 || slippage > 50)
|
|
23
|
+
throw new Error('Auto-swap slippage must be between 0 and 50 percent.');
|
|
24
|
+
if (parameters.maxAmountIn !== undefined && parameters.maxAmountIn <= 0n)
|
|
25
|
+
throw new Error('Auto-swap maxAmountIn must be positive.');
|
|
21
26
|
const candidates = tokenIn.filter((t) => !TempoAddress.isEqual(t, tokenOut));
|
|
27
|
+
// Reads never carry the client's account: a node refuses every call from an access key whose
|
|
28
|
+
// authorization is not yet published, which would hide every route from a new key.
|
|
22
29
|
const balanceResults = await Promise.allSettled([
|
|
23
|
-
readContract(client,
|
|
24
|
-
|
|
30
|
+
readContract(client, {
|
|
31
|
+
...Actions.token.getBalance.call(client, { account, token: tokenOut }),
|
|
32
|
+
account: null,
|
|
33
|
+
}),
|
|
34
|
+
...candidates.map((t) => readContract(client, {
|
|
35
|
+
...Actions.token.getBalance.call(client, { account, token: t }),
|
|
36
|
+
account: null,
|
|
37
|
+
})),
|
|
25
38
|
]);
|
|
26
39
|
// If the account already has enough of the target token, no swap needed.
|
|
27
40
|
const targetBalance = balanceResults[0];
|
|
28
|
-
if (targetBalance.status
|
|
41
|
+
if (targetBalance.status !== 'fulfilled')
|
|
42
|
+
throw targetBalance.reason;
|
|
43
|
+
const held = targetBalance.value;
|
|
44
|
+
if (held >= amountOut)
|
|
29
45
|
return undefined;
|
|
46
|
+
const deficit = amountOut - held;
|
|
47
|
+
const slippageBps = BigInt(Math.round(slippage * 100));
|
|
48
|
+
// Bound price independently of the DEX quote. Stablecoin units default to parity.
|
|
49
|
+
const spendLimit = parameters.maxAmountIn ?? deficit + (deficit * slippageBps) / bps;
|
|
30
50
|
// Find first candidate with enough balance to cover a swap.
|
|
31
51
|
for (let i = 0; i < candidates.length; i++) {
|
|
32
52
|
const result = balanceResults[i + 1];
|
|
@@ -38,12 +58,16 @@ export async function findCalls(client, parameters) {
|
|
|
38
58
|
const tokenIn = candidates[i];
|
|
39
59
|
try {
|
|
40
60
|
const quotedAmountIn = await Actions.dex.getBuyQuote(client, {
|
|
61
|
+
account: null,
|
|
41
62
|
tokenIn,
|
|
42
63
|
tokenOut,
|
|
43
|
-
amountOut,
|
|
64
|
+
amountOut: deficit,
|
|
44
65
|
});
|
|
45
|
-
if (
|
|
46
|
-
|
|
66
|
+
if (quotedAmountIn <= 0n || quotedAmountIn > spendLimit)
|
|
67
|
+
continue;
|
|
68
|
+
const quotedLimit = quotedAmountIn + (quotedAmountIn * slippageBps) / bps;
|
|
69
|
+
const maxAmountIn = quotedLimit < spendLimit ? quotedLimit : spendLimit;
|
|
70
|
+
if (balance >= maxAmountIn) {
|
|
47
71
|
return [
|
|
48
72
|
Actions.token.approve.call(client, {
|
|
49
73
|
amount: maxAmountIn,
|
|
@@ -53,7 +77,7 @@ export async function findCalls(client, parameters) {
|
|
|
53
77
|
Actions.dex.buy.call({
|
|
54
78
|
tokenIn,
|
|
55
79
|
tokenOut,
|
|
56
|
-
amountOut,
|
|
80
|
+
amountOut: deficit,
|
|
57
81
|
maxAmountIn,
|
|
58
82
|
}),
|
|
59
83
|
];
|
|
@@ -72,12 +96,15 @@ export function resolve(value, defaultCurrencies) {
|
|
|
72
96
|
const tokenIn = value.tokenIn
|
|
73
97
|
? [
|
|
74
98
|
...value.tokenIn,
|
|
75
|
-
...
|
|
99
|
+
...(value.fallback === false
|
|
100
|
+
? []
|
|
101
|
+
: defaultCurrencies.filter((d) => !value.tokenIn.some((c) => TempoAddress.isEqual(c, d)))),
|
|
76
102
|
]
|
|
77
103
|
: defaultCurrencies;
|
|
78
104
|
return {
|
|
79
105
|
tokenIn,
|
|
80
106
|
slippage: value.slippage ?? 1,
|
|
107
|
+
...(value.maxAmountIn !== undefined ? { maxAmountIn: value.maxAmountIn } : {}),
|
|
81
108
|
};
|
|
82
109
|
}
|
|
83
110
|
export class InsufficientFundsError extends Error {
|
|
@@ -124,6 +124,14 @@ export declare function validateCalls(calls: readonly {
|
|
|
124
124
|
currency?: TempoAddress.Address | undefined;
|
|
125
125
|
expectedTransfers?: readonly ExpectedTransfer[] | undefined;
|
|
126
126
|
}): TempoAddress.Address | undefined;
|
|
127
|
+
/** Rejects transactions that can expire before sponsorship and submission finish. */
|
|
128
|
+
export declare function assertRemainingValidity(parameters: {
|
|
129
|
+
details?: Record<string, string> | undefined;
|
|
130
|
+
now?: Date | undefined;
|
|
131
|
+
transaction: {
|
|
132
|
+
validBefore?: number | undefined;
|
|
133
|
+
};
|
|
134
|
+
}): undefined;
|
|
127
135
|
/** Validates sponsor fee policy limits for a fee-payer transaction. */
|
|
128
136
|
export declare function assertTransactionPolicy(parameters: {
|
|
129
137
|
challengeExpires?: string | undefined;
|
|
@@ -385,8 +385,8 @@ export function validateCalls(calls, details, options) {
|
|
|
385
385
|
const expectedAmountOut = expectedTransfers.reduce((sum, transfer) => sum + BigInt(transfer.amount), 0n);
|
|
386
386
|
if (!TempoAddress_internal.isEqual(tokenOut, currency))
|
|
387
387
|
throw new FeePayerValidationError('swap tokenOut does not match payment currency', details);
|
|
388
|
-
if (amountOut
|
|
389
|
-
throw new FeePayerValidationError('swap output
|
|
388
|
+
if (amountOut <= 0n || amountOut > expectedAmountOut)
|
|
389
|
+
throw new FeePayerValidationError('swap output must be positive and no greater than payment amount', details);
|
|
390
390
|
}
|
|
391
391
|
const transferCalls = calls.slice(hasSwapPrefix ? 2 : 0);
|
|
392
392
|
const sorted = [...expectedTransfers].sort((a, b) => {
|
|
@@ -436,6 +436,20 @@ export function validateCalls(calls, details, options) {
|
|
|
436
436
|
}
|
|
437
437
|
return approveCall?.to ?? currency;
|
|
438
438
|
}
|
|
439
|
+
/** Rejects transactions that can expire before sponsorship and submission finish. */
|
|
440
|
+
export function assertRemainingValidity(parameters) {
|
|
441
|
+
const { details = {}, now = new Date(), transaction } = parameters;
|
|
442
|
+
const { validBefore } = transaction;
|
|
443
|
+
const fail = (reason) => {
|
|
444
|
+
throw new FeePayerValidationError(reason, { ...details, validBefore: String(validBefore) });
|
|
445
|
+
};
|
|
446
|
+
if (validBefore === undefined)
|
|
447
|
+
return fail('fee-sponsored transaction must declare validBefore for the expiring nonce');
|
|
448
|
+
if (validBefore <= Math.floor(now.getTime() / 1_000))
|
|
449
|
+
fail('fee-sponsored transaction has already expired');
|
|
450
|
+
if (validBefore < Math.ceil(now.getTime() / 1_000) + 15)
|
|
451
|
+
fail('fee-sponsored transaction needs at least 15 seconds of remaining validity');
|
|
452
|
+
}
|
|
439
453
|
/** Validates sponsor fee policy limits for a fee-payer transaction. */
|
|
440
454
|
export function assertTransactionPolicy(parameters) {
|
|
441
455
|
const { challengeExpires, chainId, details, now = new Date(), policy: policyOverrides, transaction, } = parameters;
|
|
@@ -490,19 +504,13 @@ export function assertTransactionPolicy(parameters) {
|
|
|
490
504
|
if (validBefore === undefined)
|
|
491
505
|
fail('fee-sponsored transaction must declare validBefore for the expiring nonce');
|
|
492
506
|
const validBeforeValue = validBefore;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
validBefore: String(validBeforeValue),
|
|
497
|
-
});
|
|
498
|
-
if (validBeforeValue < Math.ceil(now.getTime() / 1_000) + 15)
|
|
499
|
-
fail('fee-sponsored transaction needs at least 15 seconds of remaining validity', {
|
|
500
|
-
validBefore: String(validBeforeValue),
|
|
501
|
-
});
|
|
507
|
+
if (!Number.isInteger(policy.maxValidityWindowSeconds) || policy.maxValidityWindowSeconds < 15)
|
|
508
|
+
fail('sponsor maxValidityWindowSeconds must be an integer of at least 15 seconds');
|
|
509
|
+
assertRemainingValidity({ transaction, details, now });
|
|
502
510
|
const challengeExpirySeconds = challengeExpires
|
|
503
511
|
? Math.floor(new Date(challengeExpires).getTime() / 1_000)
|
|
504
512
|
: undefined;
|
|
505
|
-
const maxValidBefore = Math.min(
|
|
513
|
+
const maxValidBefore = Math.min(Math.ceil(now.getTime() / 1_000) + policy.maxValidityWindowSeconds, challengeExpirySeconds ? challengeExpirySeconds + 60 : Number.MAX_SAFE_INTEGER);
|
|
506
514
|
if (validBeforeValue > maxValidBefore)
|
|
507
515
|
fail('fee-sponsored transaction validity window exceeds sponsor policy', {
|
|
508
516
|
validBefore: String(validBeforeValue),
|
|
@@ -373,7 +373,10 @@ export async function broadcastOpenTransaction(parameters) {
|
|
|
373
373
|
return { feePayer: feePayer.address, transaction: sponsored };
|
|
374
374
|
},
|
|
375
375
|
});
|
|
376
|
-
|
|
376
|
+
FeePayer.assertRemainingValidity({ transaction: completed.transaction });
|
|
377
|
+
const serialized = await signTransaction(client, completed.transaction);
|
|
378
|
+
FeePayer.assertRemainingValidity({ transaction: completed.transaction });
|
|
379
|
+
return serialized;
|
|
377
380
|
}
|
|
378
381
|
return serializedTransaction;
|
|
379
382
|
};
|
|
@@ -491,7 +494,10 @@ export async function broadcastTopUpTransaction(parameters) {
|
|
|
491
494
|
return { feePayer: feePayer.address, transaction: sponsored };
|
|
492
495
|
},
|
|
493
496
|
});
|
|
494
|
-
|
|
497
|
+
FeePayer.assertRemainingValidity({ transaction: completed.transaction });
|
|
498
|
+
const serialized = await signTransaction(client, completed.transaction);
|
|
499
|
+
FeePayer.assertRemainingValidity({ transaction: completed.transaction });
|
|
500
|
+
return serialized;
|
|
495
501
|
}
|
|
496
502
|
return serializedTransaction;
|
|
497
503
|
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Store from '../../Store.js';
|
|
2
2
|
import * as Currencies from '../internal/currencies.js';
|
|
3
3
|
import * as ChannelStore from '../session/server/ChannelStore.js';
|
|
4
|
-
import { charge as sessionCharge_, session as session_, settle as settle_, settleBatch as settleBatch_, } from '../session/server/Session.js';
|
|
4
|
+
import { charge as sessionCharge_, session as session_, settle as settle_, settleBatch as settleBatch_, verifyStream as verifyStream_, } from '../session/server/Session.js';
|
|
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';
|
|
@@ -129,6 +129,7 @@ function sessionOffers(parameters) {
|
|
|
129
129
|
const [first, ...rest] = resolveCurrencies(parameters ?? {});
|
|
130
130
|
const store = parameters?.store ?? fallbackStore('tempo.session');
|
|
131
131
|
const settlements = new Map();
|
|
132
|
+
const verifiers = new Map();
|
|
132
133
|
const key = (chainId, currency) => `${chainId}:${currency.toLowerCase()}`;
|
|
133
134
|
const settleScheduled = (channel) => {
|
|
134
135
|
const settle = settlements.get(key(channel.chainId, channel.token));
|
|
@@ -141,6 +142,15 @@ function sessionOffers(parameters) {
|
|
|
141
142
|
serveWebSocket: (options) => Ws_.serve({
|
|
142
143
|
...options,
|
|
143
144
|
store: ChannelStore.fromStore(store),
|
|
145
|
+
beforeCharge: async (channelId) => {
|
|
146
|
+
const channel = await ChannelStore.fromStore(store).getChannel(channelId);
|
|
147
|
+
if (!channel)
|
|
148
|
+
throw new Error('stream channel not found');
|
|
149
|
+
const verify = verifiers.get(key(channel.chainId, channel.token));
|
|
150
|
+
if (!verify)
|
|
151
|
+
throw new Error('Channel currency is not configured for this session handler.');
|
|
152
|
+
await verify(channelId);
|
|
153
|
+
},
|
|
144
154
|
// Failed settlements are reported by the session handler; the charged request stays served.
|
|
145
155
|
onChargeCommitted: (channel) => settleScheduled(channel).catch(Settlement.ignoreRetryableSettlementFailure),
|
|
146
156
|
}),
|
|
@@ -149,6 +159,7 @@ function sessionOffers(parameters) {
|
|
|
149
159
|
const configured = { ...shared, ...currency, store };
|
|
150
160
|
const method = session_(configured);
|
|
151
161
|
settlements.set(key(currency.chainId, currency.currency), method.extensions.settleScheduled);
|
|
162
|
+
verifiers.set(key(currency.chainId, currency.currency), (channelId) => verifyStream_(method, channelId));
|
|
152
163
|
return { ...method, extensions };
|
|
153
164
|
}
|
|
154
165
|
return [create(first), ...rest.map(create)];
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Bytes, Hash, Hex, Json } from 'ox';
|
|
2
1
|
import { PaymentExpiredError, VerificationFailedError } from '../../Errors.js';
|
|
3
2
|
import * as Receipt from '../../Receipt.js';
|
|
4
3
|
const defaultApiBaseUrl = 'https://api.tempo.xyz';
|
|
@@ -43,9 +42,7 @@ export function configure(method, options) {
|
|
|
43
42
|
};
|
|
44
43
|
const broadcast = async (parameters) => {
|
|
45
44
|
const input = toRelayInput(parameters.credential);
|
|
46
|
-
const receipt = await request.broadcast(input
|
|
47
|
-
idempotencyKey: idempotencyKey(input),
|
|
48
|
-
});
|
|
45
|
+
const receipt = await request.broadcast(input);
|
|
49
46
|
if (receipt.method !== method.name)
|
|
50
47
|
throw failure();
|
|
51
48
|
try {
|
|
@@ -72,7 +69,7 @@ function createRequest(options) {
|
|
|
72
69
|
const apiBaseUrl = new URL(options.apiBaseUrl ?? defaultApiBaseUrl);
|
|
73
70
|
if (!apiBaseUrl.pathname.endsWith('/'))
|
|
74
71
|
apiBaseUrl.pathname += '/';
|
|
75
|
-
async function post(path, input
|
|
72
|
+
async function post(path, input) {
|
|
76
73
|
let response;
|
|
77
74
|
try {
|
|
78
75
|
response = await fetch(new URL(path, apiBaseUrl), {
|
|
@@ -81,7 +78,6 @@ function createRequest(options) {
|
|
|
81
78
|
Accept: 'application/json',
|
|
82
79
|
'content-type': 'application/json',
|
|
83
80
|
'tempo-api-key': options.apiKey,
|
|
84
|
-
...headers,
|
|
85
81
|
},
|
|
86
82
|
method: 'POST',
|
|
87
83
|
});
|
|
@@ -98,10 +94,10 @@ function createRequest(options) {
|
|
|
98
94
|
if (!isValidateSuccess(response))
|
|
99
95
|
throw failure(response);
|
|
100
96
|
};
|
|
101
|
-
const broadcast = async (input
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
const broadcast = async (input) => {
|
|
98
|
+
// A cached settlement receipt must not authorize another protected operation.
|
|
99
|
+
// Omitting the idempotency key leaves credential consumption to the relay.
|
|
100
|
+
const response = await post('v1/mpp/broadcast', input);
|
|
105
101
|
if (!isBroadcastSuccess(response))
|
|
106
102
|
throw failure(response);
|
|
107
103
|
return response.receipt;
|
|
@@ -118,18 +114,6 @@ function toRelayInput(credential) {
|
|
|
118
114
|
...(credential.source ? { source: credential.source } : {}),
|
|
119
115
|
};
|
|
120
116
|
}
|
|
121
|
-
function idempotencyKey(input) {
|
|
122
|
-
const payload = input.payload;
|
|
123
|
-
if (isRecord(payload) &&
|
|
124
|
-
payload.type === 'transaction' &&
|
|
125
|
-
typeof payload.signature === 'string' &&
|
|
126
|
-
Hex.validate(payload.signature)) {
|
|
127
|
-
const transactionHash = Hash.keccak256(Hex.toBytes(payload.signature), { as: 'Hex' });
|
|
128
|
-
return `mpp_${transactionHash}`;
|
|
129
|
-
}
|
|
130
|
-
const hash = Hash.sha256(Bytes.fromString(Json.canonicalize(input)), { as: 'Hex' });
|
|
131
|
-
return `mpp_${hash}`;
|
|
132
|
-
}
|
|
133
117
|
function failure(value) {
|
|
134
118
|
const code = relayErrorCodeFrom(value);
|
|
135
119
|
if (code === 'expired')
|
|
@@ -738,6 +738,7 @@ async function submitSubscriptionPayment(parameters) {
|
|
|
738
738
|
};
|
|
739
739
|
},
|
|
740
740
|
});
|
|
741
|
+
FeePayer.assertRemainingValidity({ transaction: completed.transaction });
|
|
741
742
|
return await signTransaction(client, completed.transaction);
|
|
742
743
|
})();
|
|
743
744
|
const transaction = Transaction.deserialize(serializedTransaction);
|
|
@@ -748,6 +749,8 @@ async function submitSubscriptionPayment(parameters) {
|
|
|
748
749
|
calls: transaction.calls,
|
|
749
750
|
feePayerSignature: undefined,
|
|
750
751
|
});
|
|
752
|
+
if (feePayer)
|
|
753
|
+
FeePayer.assertRemainingValidity({ transaction });
|
|
751
754
|
if (!waitForConfirmation) {
|
|
752
755
|
// Optimistic mode has no receipt to inspect, so it cannot detect a T6
|
|
753
756
|
// (TIP-1028) held transfer. Use `waitForConfirmation: true` for T6-safe proof.
|