mppx 0.11.0 → 0.13.1
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 +83 -0
- package/README.md +22 -1
- package/dist/Method.d.ts +7 -1
- package/dist/Method.js +7 -2
- package/dist/Receipt.d.ts +2 -0
- package/dist/Receipt.js +2 -0
- package/dist/cli/agent.d.ts +3 -0
- package/dist/cli/agent.js +20 -0
- package/dist/cli/cli.d.ts +9 -1
- package/dist/cli/cli.js +134 -136
- package/dist/cli/plugins/index.js +12 -14
- package/dist/cli/plugins/tempo.js +9 -2
- package/dist/cli/utils.d.ts +11 -1
- package/dist/cli/utils.js +15 -2
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.js +19 -4
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.js +10 -1
- package/dist/cli/validate/payment.js +63 -24
- package/dist/client/Methods.d.ts +0 -2
- package/dist/client/Methods.js +0 -2
- package/dist/client/Mppx.js +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.js +8 -2
- package/dist/client/internal/MethodChallenge.d.ts +1 -0
- package/dist/evm/Types.js +1 -1
- package/dist/evm/server/Charge.js +5 -0
- package/dist/evm/server/Methods.d.ts +164 -3
- package/dist/evm/server/Methods.js +50 -2
- package/dist/integrations/x402/Evm.js +4 -3
- package/dist/internal/HeaderCodec.d.ts +2 -2
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.js +4 -7
- package/dist/middlewares/elysia.d.ts +11 -8
- package/dist/middlewares/elysia.js +67 -14
- package/dist/middlewares/express.js +13 -6
- package/dist/middlewares/hono.js +4 -4
- package/dist/middlewares/nextjs.js +9 -5
- package/dist/proxy/internal/Headers.js +65 -0
- package/dist/proxy/services/stripe.js +1 -0
- package/dist/server/Mppx.d.ts +18 -2
- package/dist/server/Mppx.js +204 -92
- package/dist/server/NodeListener.js +71 -6
- package/dist/server/Request.d.ts +11 -1
- package/dist/server/Request.js +132 -13
- package/dist/server/Transport.d.ts +14 -1
- package/dist/server/Transport.js +1 -0
- package/dist/server/internal/html/compose.main.gen.d.ts +1 -1
- package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
- package/dist/stripe/server/Methods.d.ts +10 -3
- package/dist/stripe/server/Methods.js +19 -6
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/Methods.d.ts +1 -0
- package/dist/tempo/Methods.js +1 -0
- package/dist/tempo/client/Charge.js +17 -13
- package/dist/tempo/client/Methods.d.ts +1 -5
- package/dist/tempo/client/Methods.js +10 -5
- package/dist/tempo/client/ResolveAccount.d.ts +3 -0
- package/dist/tempo/client/index.d.ts +0 -3
- package/dist/tempo/client/index.js +0 -2
- package/dist/tempo/internal/currencies.d.ts +19 -0
- package/dist/tempo/internal/currencies.js +41 -0
- package/dist/tempo/internal/defaults.d.ts +5 -3
- package/dist/tempo/internal/defaults.js +6 -2
- package/dist/tempo/internal/fee-payer.d.ts +11 -7
- package/dist/tempo/internal/fee-payer.js +30 -11
- package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
- package/dist/tempo/internal/machine-token-charge.js +2 -1
- package/dist/tempo/legacy/index.d.ts +1 -3
- package/dist/tempo/legacy/index.js +1 -3
- package/dist/tempo/server/Charge.d.ts +12 -1
- package/dist/tempo/server/Charge.js +109 -78
- package/dist/tempo/server/Methods.d.ts +5048 -40
- package/dist/tempo/server/Methods.js +104 -13
- package/dist/tempo/server/Subscription.d.ts +1 -1
- package/dist/tempo/server/Subscription.js +31 -4
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.js +28 -11
- package/dist/tempo/session/client/ChannelOps.js +5 -0
- package/dist/tempo/session/client/Session.d.ts +1 -0
- package/dist/tempo/session/client/Session.js +7 -4
- package/dist/tempo/session/client/SessionManager.js +3 -0
- package/dist/tempo/session/client/Transports.d.ts +8 -1
- package/dist/tempo/session/client/Transports.js +12 -3
- package/dist/tempo/session/precompile/Chain.js +16 -1
- package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
- package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
- package/dist/tempo/session/precompile/Voucher.js +48 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +22 -2
- package/dist/tempo/session/server/ChannelStore.js +37 -7
- package/dist/tempo/session/server/CredentialVerification.d.ts +6 -1
- package/dist/tempo/session/server/CredentialVerification.js +56 -19
- package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
- package/dist/tempo/session/server/MeteredStream.js +85 -29
- package/dist/tempo/session/server/RequestState.d.ts +6 -0
- package/dist/tempo/session/server/RequestState.js +11 -4
- package/dist/tempo/session/server/Session.d.ts +2 -1
- package/dist/tempo/session/server/Session.js +10 -2
- package/dist/tempo/session/server/Settlement.d.ts +23 -0
- package/dist/tempo/session/server/Settlement.js +105 -25
- package/dist/tempo/session/server/Sse.d.ts +3 -1
- package/dist/tempo/session/server/Sse.js +105 -32
- package/dist/tempo/session/server/Transports.d.ts +24 -19
- package/dist/tempo/session/server/Transports.js +169 -58
- package/dist/tempo/session/server/Ws.d.ts +5 -1
- package/dist/tempo/session/server/Ws.js +75 -20
- package/dist/tempo/subscription/KeyAuthorization.d.ts +70 -8
- package/dist/tempo/subscription/Store.d.ts +2 -0
- package/dist/tempo/subscription/Store.js +3 -0
- package/dist/validation/core.js +2 -1
- package/dist/x402/server/EvmCharge.d.ts +3 -1
- package/dist/x402/server/EvmCharge.js +14 -1
- package/package.json +8 -8
- package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
- package/dist/tempo/legacy/client/ChannelOps.js +0 -168
- package/dist/tempo/legacy/client/Session.d.ts +0 -185
- package/dist/tempo/legacy/client/Session.js +0 -279
- package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
- package/dist/tempo/legacy/client/SessionManager.js +0 -706
- package/dist/tempo/legacy/client/index.d.ts +0 -7
- package/dist/tempo/legacy/client/index.js +0 -5
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { Secp256k1 } from 'ox';
|
|
2
|
-
import { TxEnvelopeTempo } from 'ox/tempo';
|
|
3
|
-
import { decodeFunctionData, encodeFunctionData, maxUint256, toHex } from 'viem';
|
|
1
|
+
import { Address, Secp256k1 } from 'ox';
|
|
2
|
+
import { TempoAddress, TxEnvelopeTempo } from 'ox/tempo';
|
|
3
|
+
import { decodeFunctionData, encodeFunctionData, maxUint256, toFunctionSelector, toHex } from 'viem';
|
|
4
4
|
import { Abis, Addresses, Transaction } from 'viem/tempo';
|
|
5
|
+
import { escrowAbi as legacyEscrowAbi } from '../legacy/session/escrow.abi.js';
|
|
6
|
+
import { escrowAbi } from '../session/precompile/escrow.abi.js';
|
|
5
7
|
import * as TempoAddress_internal from './address.js';
|
|
6
8
|
import * as defaults from './defaults.js';
|
|
7
9
|
import { defaultFeeTokens } from './fee-token.js';
|
|
10
|
+
import { machineTokenSwapperAbi } from './machine-token-charge.js';
|
|
8
11
|
import * as Selectors from './selectors.js';
|
|
9
12
|
/** Returns true if the serialized transaction has a Tempo envelope prefix. */
|
|
10
13
|
export function isTempoTransaction(serialized) {
|
|
@@ -63,15 +66,17 @@ const supportedTransactionKeys = new Set([
|
|
|
63
66
|
export function defaultAllowedFeeTokens(chainId) {
|
|
64
67
|
return defaultFeeTokens(chainId);
|
|
65
68
|
}
|
|
66
|
-
/**
|
|
69
|
+
/** Validates the fee-token address and, when supplied, the server's allowlist. */
|
|
67
70
|
export function assertAllowedFeeToken(transaction, allowedFeeTokens) {
|
|
68
71
|
const { feeToken } = transaction;
|
|
69
72
|
if (feeToken === undefined)
|
|
70
73
|
return;
|
|
71
|
-
if (typeof feeToken !== 'string'
|
|
74
|
+
if (typeof feeToken !== 'string' ||
|
|
75
|
+
!(Address.validate(feeToken, { strict: false }) || TempoAddress.validate(feeToken)))
|
|
72
76
|
throw new FeePayerValidationError('fee-sponsored transaction feeToken is invalid', {});
|
|
73
77
|
const normalized = feeToken;
|
|
74
|
-
if (
|
|
78
|
+
if (allowedFeeTokens &&
|
|
79
|
+
!allowedFeeTokens.some((allowed) => TempoAddress_internal.isEqual(allowed, normalized)))
|
|
75
80
|
throw new FeePayerValidationError('fee-sponsored transaction feeToken is not allowed', {
|
|
76
81
|
feeToken,
|
|
77
82
|
});
|
|
@@ -230,7 +235,7 @@ export async function preflightSponsorship(parameters) {
|
|
|
230
235
|
* swap transactions at peak gas prices. Bumped from 0.01 ETH in #327.
|
|
231
236
|
*/
|
|
232
237
|
const defaultPolicy = {
|
|
233
|
-
allowKeyAuthorization:
|
|
238
|
+
allowKeyAuthorization: false,
|
|
234
239
|
maxGas: 2000000n,
|
|
235
240
|
maxFeePerGas: 100000000000n,
|
|
236
241
|
maxPriorityFeePerGas: 10000000000n,
|
|
@@ -261,16 +266,26 @@ function getPolicy(chainId, overrides) {
|
|
|
261
266
|
function isExpiringNonceKey(nonceKey) {
|
|
262
267
|
return nonceKey === 'expiring' || nonceKey === maxUint256;
|
|
263
268
|
}
|
|
264
|
-
const sponsoredCallAbi = [
|
|
269
|
+
const sponsoredCallAbi = [
|
|
270
|
+
...Abis.tip20,
|
|
271
|
+
...Abis.stablecoinDex,
|
|
272
|
+
...machineTokenSwapperAbi,
|
|
273
|
+
...legacyEscrowAbi,
|
|
274
|
+
...escrowAbi,
|
|
275
|
+
];
|
|
265
276
|
const sponsoredCallSelectors = new Set([
|
|
266
277
|
Selectors.approve,
|
|
267
278
|
Selectors.transfer,
|
|
268
279
|
Selectors.transferWithMemo,
|
|
269
280
|
Selectors.swapExactAmountOut,
|
|
281
|
+
...machineTokenSwapperAbi.map((item) => toFunctionSelector(item)),
|
|
282
|
+
...[...legacyEscrowAbi, ...escrowAbi]
|
|
283
|
+
.filter((item) => item.type === 'function' && item.stateMutability === 'nonpayable')
|
|
284
|
+
.map((item) => toFunctionSelector(item)),
|
|
270
285
|
]);
|
|
271
286
|
function canonicalSponsoredCallData(data, call, fail) {
|
|
272
287
|
if (!sponsoredCallSelectors.has(data.slice(0, 10)))
|
|
273
|
-
|
|
288
|
+
fail('fee-sponsored transaction call calldata is invalid', { call });
|
|
274
289
|
let decoded;
|
|
275
290
|
try {
|
|
276
291
|
decoded = decodeFunctionData({ abi: sponsoredCallAbi, data });
|
|
@@ -310,13 +325,16 @@ function assertCanonicalSponsoredTransaction(transaction, fail) {
|
|
|
310
325
|
call: callIndex,
|
|
311
326
|
});
|
|
312
327
|
const canonical = canonicalSponsoredCallData(data, callIndex, fail);
|
|
313
|
-
if (
|
|
328
|
+
if (data.toLowerCase() !== canonical.toLowerCase())
|
|
314
329
|
fail('fee-sponsored transaction call calldata is not canonical', {
|
|
315
330
|
call: callIndex,
|
|
316
331
|
});
|
|
317
332
|
}
|
|
318
333
|
}
|
|
319
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Validates an allowed fee-payer pattern. Returns the funding currency when the
|
|
336
|
+
* calls are bound to the expected payment transfers.
|
|
337
|
+
*/
|
|
320
338
|
export function validateCalls(calls, details, options) {
|
|
321
339
|
if (calls.length === 0)
|
|
322
340
|
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details);
|
|
@@ -416,6 +434,7 @@ export function validateCalls(calls, details, options) {
|
|
|
416
434
|
throw new FeePayerValidationError('payment transfer does not match challenge', details);
|
|
417
435
|
used.add(matchIndex);
|
|
418
436
|
}
|
|
437
|
+
return approveCall?.to ?? currency;
|
|
419
438
|
}
|
|
420
439
|
/** Validates sponsor fee policy limits for a fee-payer transaction. */
|
|
421
440
|
export function assertTransactionPolicy(parameters) {
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import type * as Hex from 'ox/Hex';
|
|
2
2
|
import { type Address, type Client } from 'viem';
|
|
3
|
+
/** ABI for the first-party machine-token settlement swapper. */
|
|
4
|
+
export declare const machineTokenSwapperAbi: readonly [{
|
|
5
|
+
readonly name: "swapTo";
|
|
6
|
+
readonly type: 'function';
|
|
7
|
+
readonly stateMutability: "nonpayable";
|
|
8
|
+
readonly inputs: readonly [{
|
|
9
|
+
readonly type: "address";
|
|
10
|
+
readonly name: "inputToken";
|
|
11
|
+
}, {
|
|
12
|
+
readonly type: "uint256";
|
|
13
|
+
readonly name: "amount";
|
|
14
|
+
}, {
|
|
15
|
+
readonly type: "address";
|
|
16
|
+
readonly name: "targetToken";
|
|
17
|
+
}, {
|
|
18
|
+
readonly type: "address";
|
|
19
|
+
readonly name: "recipient";
|
|
20
|
+
}, {
|
|
21
|
+
readonly type: "bytes32";
|
|
22
|
+
readonly name: "memo";
|
|
23
|
+
}];
|
|
24
|
+
readonly outputs: readonly [];
|
|
25
|
+
}];
|
|
3
26
|
type Call = {
|
|
4
27
|
data?: Hex.Hex | undefined;
|
|
5
28
|
to?: Address | undefined;
|
|
@@ -2,7 +2,8 @@ import { decodeFunctionData, encodeFunctionData, isAddress, isAddressEqual, pars
|
|
|
2
2
|
import { call, readContract } from 'viem/actions';
|
|
3
3
|
import { Abis, Actions } from 'viem/tempo';
|
|
4
4
|
import * as defaults from './defaults.js';
|
|
5
|
-
|
|
5
|
+
/** ABI for the first-party machine-token settlement swapper. */
|
|
6
|
+
export const machineTokenSwapperAbi = parseAbi([
|
|
6
7
|
'function swapTo(address inputToken,uint256 amount,address targetToken,address recipient,bytes32 memo)',
|
|
7
8
|
]);
|
|
8
9
|
function getDeployment(chainId) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Legacy contract-backed session
|
|
2
|
-
export * as Client from './client/index.js';
|
|
3
|
-
/** Legacy contract-backed session channel primitives used by the client. */
|
|
1
|
+
/** Legacy contract-backed session channel primitives retained for compatibility. */
|
|
4
2
|
export * as Session from './session/index.js';
|
|
5
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Legacy contract-backed session
|
|
2
|
-
export * as Client from './client/index.js';
|
|
3
|
-
/** Legacy contract-backed session channel primitives used by the client. */
|
|
1
|
+
/** Legacy contract-backed session channel primitives retained for compatibility. */
|
|
4
2
|
export * as Session from './session/index.js';
|
|
5
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -45,6 +45,16 @@ export declare namespace charge {
|
|
|
45
45
|
} | undefined;
|
|
46
46
|
};
|
|
47
47
|
type Parameters = {
|
|
48
|
+
/**
|
|
49
|
+
* Tokens permitted for sponsored charge transaction fees, including tokens
|
|
50
|
+
* selected by a remote fee payer. Local sponsorship defaults to pathUSD and
|
|
51
|
+
* USDC.e on mainnet, and pathUSD on other chains. Hosted sponsorship has no
|
|
52
|
+
* token allowlist by default and trusts the configured provider's choice.
|
|
53
|
+
* A custom list replaces the local defaults and restricts hosted sponsorship.
|
|
54
|
+
* It must contain at least one token. Address validation and other sponsorship
|
|
55
|
+
* checks still apply. This does not change the payment currency or sessions.
|
|
56
|
+
*/
|
|
57
|
+
allowedFeeTokens?: readonly `0x${string}`[] | undefined;
|
|
48
58
|
/** Enables first-party machine-token funding through the canonical swapper. */
|
|
49
59
|
machineTokenEnabled?: boolean | undefined;
|
|
50
60
|
/** Render payment page when Accept header is text/html (e.g. in browsers) */
|
|
@@ -58,7 +68,8 @@ export declare namespace charge {
|
|
|
58
68
|
feePayerPolicy?: FeePayerPolicy | undefined;
|
|
59
69
|
/**
|
|
60
70
|
* Token a local fee payer uses to pay gas. If omitted, mppx selects a
|
|
61
|
-
* funded allowed token
|
|
71
|
+
* funded allowed token in `allowedFeeTokens` order (pathUSD first by default).
|
|
72
|
+
* An explicit token must also be in the allowed list.
|
|
62
73
|
*
|
|
63
74
|
* This option is not supported with a remote fee-payer URL, which selects
|
|
64
75
|
* its own token.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as SignatureEnvelope from 'ox/tempo/SignatureEnvelope';
|
|
2
2
|
import { decodeFunctionData, formatUnits, keccak256, parseEventLogs, } from 'viem';
|
|
3
|
-
import { getTransactionReceipt, sendRawTransaction, sendRawTransactionSync, signTransaction, verifyTypedData, call as viem_call, } from 'viem/actions';
|
|
3
|
+
import { getTransaction, getTransactionReceipt, sendRawTransaction, sendRawTransactionSync, signTransaction, verifyTypedData, 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
6
|
import { PaymentError, VerificationFailedError } from '../../Errors.js';
|
|
@@ -33,7 +33,9 @@ import * as SponsorBudget from './SponsorBudget.js';
|
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
35
|
export function charge(parameters = {}) {
|
|
36
|
-
const { machineTokenEnabled, amount, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, description, externalId, feeToken: configuredFeeToken, feePayerPolicy, html, relay, sponsorBudget, splits, supportedModes, validateSender, waitForConfirmation = true, } = 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
|
+
if (configuredAllowedFeeTokens?.length === 0)
|
|
38
|
+
throw new Error('`allowedFeeTokens` must contain at least one token.');
|
|
37
39
|
const storeKeyPrefix = parameters.storeKeyPrefix ?? '';
|
|
38
40
|
const rawStore = (parameters.store ?? Store.memory());
|
|
39
41
|
const store = Store.from(rawStore, { keyPrefix: storeKeyPrefix });
|
|
@@ -92,7 +94,12 @@ export function charge(parameters = {}) {
|
|
|
92
94
|
Expires.assert(challenge.expires, challenge.id);
|
|
93
95
|
if (isZeroAmount && payload.type !== 'proof')
|
|
94
96
|
throw new MismatchError('Zero-amount challenges require a proof credential.', {});
|
|
97
|
+
// Request-level local sponsorship takes precedence over the hosted provider.
|
|
98
|
+
const localFeePayer = Account.is(request.feePayer) ? request.feePayer : feePayer;
|
|
99
|
+
const allowedFeeTokens = configuredAllowedFeeTokens ??
|
|
100
|
+
(localFeePayer || !remoteFeePayer ? FeePayer.defaultAllowedFeeTokens(chainId) : undefined);
|
|
95
101
|
return {
|
|
102
|
+
allowedFeeTokens,
|
|
96
103
|
amount,
|
|
97
104
|
machineTokenEnabled,
|
|
98
105
|
chainId,
|
|
@@ -109,6 +116,43 @@ export function charge(parameters = {}) {
|
|
|
109
116
|
supportedModes,
|
|
110
117
|
};
|
|
111
118
|
}
|
|
119
|
+
/** Validates a recognized payment route and retains its funding metadata. */
|
|
120
|
+
function validatePaymentRoute(calls, context, transfers, isFeePayerTx = false) {
|
|
121
|
+
const { amount, currency, machineTokenEnabled, recipient } = context;
|
|
122
|
+
const machineRoute = machineTokenEnabled
|
|
123
|
+
? MachineTokenCharge.matchRoute({
|
|
124
|
+
calls,
|
|
125
|
+
chainId: context.chainId ?? context.client.chain?.id,
|
|
126
|
+
currency,
|
|
127
|
+
transfers,
|
|
128
|
+
})
|
|
129
|
+
: undefined;
|
|
130
|
+
const matchedCalls = machineRoute?.transfers ??
|
|
131
|
+
assertTransferCalls(calls, {
|
|
132
|
+
currency,
|
|
133
|
+
exactCount: isFeePayerTx,
|
|
134
|
+
transfers,
|
|
135
|
+
});
|
|
136
|
+
assertChallengeBoundCallMemo(matchedCalls, {
|
|
137
|
+
challengeId: context.challenge.id,
|
|
138
|
+
realm: context.challenge.realm,
|
|
139
|
+
});
|
|
140
|
+
if (machineRoute)
|
|
141
|
+
return {
|
|
142
|
+
settlementSenders: [machineRoute.settlementSender],
|
|
143
|
+
fundingCurrency: machineRoute.calls[0].to,
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
const fundingCurrency = FeePayer.validateCalls(calls, { amount, currency, recipient }, { currency, expectedTransfers: transfers });
|
|
147
|
+
return { settlementSenders: [], fundingCurrency };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (isFeePayerTx)
|
|
151
|
+
throw error;
|
|
152
|
+
// Unrecognized routes remain valid for unsponsored payments, without funding metadata.
|
|
153
|
+
return { settlementSenders: [], fundingCurrency: undefined };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
112
156
|
async function validateHashCredential(credential, payload, context) {
|
|
113
157
|
const { amount, chainId, challenge, client, currency, methodDetails, recipient, settlementSenders, supportedModes, } = context;
|
|
114
158
|
if (supportedModes && !supportedModes.includes('push'))
|
|
@@ -134,7 +178,7 @@ export function charge(parameters = {}) {
|
|
|
134
178
|
challengeId: challenge.id,
|
|
135
179
|
realm: challenge.realm,
|
|
136
180
|
});
|
|
137
|
-
return { receipt
|
|
181
|
+
return { receipt, sender, transfers };
|
|
138
182
|
}
|
|
139
183
|
async function validateProofCredential(credential, payload, context) {
|
|
140
184
|
const { chainId, challenge, client, isZeroAmount } = context;
|
|
@@ -174,7 +218,7 @@ export function charge(parameters = {}) {
|
|
|
174
218
|
return { sender: source.address };
|
|
175
219
|
}
|
|
176
220
|
async function validateTransactionCredential(credential, payload, request, context) {
|
|
177
|
-
const {
|
|
221
|
+
const { amount, chainId, challenge, client, methodDetails, recipient, requestAllowsFeePayer, supportedModes, } = context;
|
|
178
222
|
if (supportedModes && !supportedModes.includes('pull'))
|
|
179
223
|
throw new MismatchError('Transaction credentials are not supported for this challenge.', {});
|
|
180
224
|
const serializedTransaction = payload.signature;
|
|
@@ -183,40 +227,30 @@ export function charge(parameters = {}) {
|
|
|
183
227
|
const transaction = Transaction.deserialize(serializedTransaction);
|
|
184
228
|
if (!transaction.signature || !transaction.from)
|
|
185
229
|
throw new MismatchError('Transaction must be signed by the sender before fee payer co-signing.', {});
|
|
230
|
+
if (credential.source) {
|
|
231
|
+
const source = Proof.parsePkhSource(credential.source);
|
|
232
|
+
const resolvedChainId = challenge.request.methodDetails?.chainId ?? chainId ?? client.chain?.id;
|
|
233
|
+
if (!source || source.chainId !== resolvedChainId)
|
|
234
|
+
throw new MismatchError('Transaction credential source is invalid.', {});
|
|
235
|
+
if (!TempoAddress.isEqual(source.address, transaction.from))
|
|
236
|
+
throw new MismatchError('Transaction credential source does not match the sender.', {});
|
|
237
|
+
}
|
|
186
238
|
const isFeePayerTx = methodDetails?.feePayer === true &&
|
|
187
239
|
requestAllowsFeePayer &&
|
|
188
240
|
!!(Account.is(request.feePayer) ? request.feePayer : feePayer || remoteFeePayer);
|
|
189
241
|
const transfers = getExpectedTransfers({ amount, methodDetails, recipient });
|
|
190
|
-
const
|
|
191
|
-
? MachineTokenCharge.matchRoute({
|
|
192
|
-
calls: transaction.calls ?? [],
|
|
193
|
-
chainId: chainId ?? client.chain?.id,
|
|
194
|
-
currency,
|
|
195
|
-
transfers,
|
|
196
|
-
})
|
|
197
|
-
: undefined;
|
|
198
|
-
const matchedCalls = machineTokenRoute?.transfers ??
|
|
199
|
-
assertTransferCalls(transaction.calls ?? [], {
|
|
200
|
-
currency,
|
|
201
|
-
exactCount: isFeePayerTx,
|
|
202
|
-
transfers,
|
|
203
|
-
});
|
|
204
|
-
assertChallengeBoundCallMemo(matchedCalls, {
|
|
205
|
-
challengeId: challenge.id,
|
|
206
|
-
realm: challenge.realm,
|
|
207
|
-
});
|
|
242
|
+
const { settlementSenders, fundingCurrency } = validatePaymentRoute(transaction.calls ?? [], context, transfers, isFeePayerTx);
|
|
208
243
|
if (isFeePayerTx) {
|
|
209
|
-
|
|
210
|
-
FeePayer.validateCalls(transaction.calls, { amount, currency, recipient }, { currency, expectedTransfers: transfers });
|
|
211
|
-
FeePayer.assertAllowedFeeToken(transaction, FeePayer.defaultAllowedFeeTokens(chainId));
|
|
244
|
+
FeePayer.assertAllowedFeeToken(transaction, context.allowedFeeTokens);
|
|
212
245
|
}
|
|
213
246
|
else {
|
|
214
247
|
await viem_call(client, FeePayer.simulationTransaction(transaction, { feePayer: false }));
|
|
215
248
|
}
|
|
216
249
|
return {
|
|
250
|
+
fundingCurrency,
|
|
217
251
|
isFeePayerTx,
|
|
218
252
|
serializedTransaction,
|
|
219
|
-
settlementSenders
|
|
253
|
+
settlementSenders,
|
|
220
254
|
transaction,
|
|
221
255
|
transfers,
|
|
222
256
|
};
|
|
@@ -237,7 +271,7 @@ export function charge(parameters = {}) {
|
|
|
237
271
|
return { details: { mode: 'proof', sender }, type: 'proof' };
|
|
238
272
|
}
|
|
239
273
|
case 'transaction': {
|
|
240
|
-
const { isFeePayerTx, serializedTransaction, settlementSenders, transaction, transfers } = await validateTransactionCredential(credential, context.payload, request, context);
|
|
274
|
+
const { fundingCurrency, isFeePayerTx, serializedTransaction, settlementSenders, transaction, transfers, } = await validateTransactionCredential(credential, context.payload, request, context);
|
|
241
275
|
return {
|
|
242
276
|
details: {
|
|
243
277
|
mode: 'pull',
|
|
@@ -245,6 +279,7 @@ export function charge(parameters = {}) {
|
|
|
245
279
|
serializedTransaction,
|
|
246
280
|
transfers,
|
|
247
281
|
},
|
|
282
|
+
fundingCurrency,
|
|
248
283
|
isFeePayerTx,
|
|
249
284
|
serializedTransaction,
|
|
250
285
|
settlementSenders,
|
|
@@ -305,6 +340,8 @@ export function charge(parameters = {}) {
|
|
|
305
340
|
const chainId = await (async () => {
|
|
306
341
|
if (request.chainId)
|
|
307
342
|
return request.chainId;
|
|
343
|
+
if (parameters.chainId !== undefined)
|
|
344
|
+
return parameters.chainId;
|
|
308
345
|
if (parameters.testnet)
|
|
309
346
|
return defaults.chainId.testnet;
|
|
310
347
|
return (await getClient({})).chain?.id;
|
|
@@ -374,7 +411,19 @@ export function charge(parameters = {}) {
|
|
|
374
411
|
reason: 'Transaction hash has already been used',
|
|
375
412
|
});
|
|
376
413
|
}
|
|
377
|
-
|
|
414
|
+
// Only final hash receipts need this lookup; validation stays receipt-only.
|
|
415
|
+
const transaction = await getTransaction(client, { hash }).catch(() => undefined);
|
|
416
|
+
let fundingCurrency;
|
|
417
|
+
if (transaction && TempoAddress.isEqual(transaction.from, validated.details.sender)) {
|
|
418
|
+
try {
|
|
419
|
+
fundingCurrency = validatePaymentRoute(transaction.calls ??
|
|
420
|
+
(transaction.to ? [{ to: transaction.to, data: transaction.input }] : []), context, validated.details.transfers).fundingCurrency;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Unrecognized calls do not invalidate an already verified payment.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return toReceipt(receipt.transactionHash, fundingCurrency);
|
|
378
427
|
}
|
|
379
428
|
case 'proof': {
|
|
380
429
|
if (proofStore && !(await tryClaimProof(proofStore, challenge.id, replayExpires))) {
|
|
@@ -382,15 +431,10 @@ export function charge(parameters = {}) {
|
|
|
382
431
|
reason: 'Proof credential has already been used',
|
|
383
432
|
});
|
|
384
433
|
}
|
|
385
|
-
return
|
|
386
|
-
method: 'tempo',
|
|
387
|
-
status: 'success',
|
|
388
|
-
timestamp: new Date().toISOString(),
|
|
389
|
-
reference: challenge.id,
|
|
390
|
-
};
|
|
434
|
+
return toReceipt(challenge.id);
|
|
391
435
|
}
|
|
392
436
|
case 'transaction': {
|
|
393
|
-
const { isFeePayerTx, serializedTransaction, settlementSenders, transaction, transfers } = validated;
|
|
437
|
+
const { fundingCurrency, isFeePayerTx, serializedTransaction, settlementSenders, transaction, transfers, } = validated;
|
|
394
438
|
// Pre-broadcast dedup: catch exact byte-for-byte replays early.
|
|
395
439
|
const hash = keccak256(serializedTransaction);
|
|
396
440
|
if (!(await tryClaimHash(store, hash, replayExpires))) {
|
|
@@ -402,12 +446,10 @@ export function charge(parameters = {}) {
|
|
|
402
446
|
let finalHash;
|
|
403
447
|
let reservation;
|
|
404
448
|
try {
|
|
405
|
-
const allowedFeeTokens =
|
|
406
|
-
if (isFeePayerTx)
|
|
407
|
-
FeePayer.assertAllowedFeeToken(transaction, allowedFeeTokens);
|
|
408
|
-
const selectableFeeTokens = allowedFeeTokens;
|
|
449
|
+
const { allowedFeeTokens } = context;
|
|
409
450
|
const completedTransaction = await (async () => {
|
|
410
451
|
if (feePayerAccount && methodDetails?.feePayer !== false) {
|
|
452
|
+
const selectableFeeTokens = allowedFeeTokens ?? FeePayer.defaultAllowedFeeTokens(chainId);
|
|
411
453
|
const completed = await FeePayer.preflightSponsorship({
|
|
412
454
|
transaction,
|
|
413
455
|
simulate: (request) => viem_call(client, request),
|
|
@@ -422,7 +464,7 @@ export function charge(parameters = {}) {
|
|
|
422
464
|
}));
|
|
423
465
|
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
424
466
|
account: feePayerAccount,
|
|
425
|
-
allowedFeeTokens,
|
|
467
|
+
allowedFeeTokens: selectableFeeTokens,
|
|
426
468
|
challengeExpires: expires,
|
|
427
469
|
chainId: chainId ?? client.chain.id,
|
|
428
470
|
details: { amount, currency, recipient },
|
|
@@ -534,7 +576,7 @@ export function charge(parameters = {}) {
|
|
|
534
576
|
throw new VerificationFailedError({
|
|
535
577
|
reason: 'Broadcast transaction hash does not match the signed transaction',
|
|
536
578
|
});
|
|
537
|
-
return toReceipt(receipt);
|
|
579
|
+
return toReceipt(receipt.transactionHash, fundingCurrency);
|
|
538
580
|
}
|
|
539
581
|
// Optimistic path: broadcast without waiting for confirmation
|
|
540
582
|
// (simulation above already ran). The returned receipt assumes
|
|
@@ -551,12 +593,7 @@ export function charge(parameters = {}) {
|
|
|
551
593
|
throw new VerificationFailedError({
|
|
552
594
|
reason: 'Sponsor budget reservation ownership was lost after broadcast',
|
|
553
595
|
});
|
|
554
|
-
return
|
|
555
|
-
method: 'tempo',
|
|
556
|
-
status: 'success',
|
|
557
|
-
timestamp: new Date().toISOString(),
|
|
558
|
-
reference,
|
|
559
|
-
};
|
|
596
|
+
return toReceipt(reference, fundingCurrency);
|
|
560
597
|
}
|
|
561
598
|
catch (error) {
|
|
562
599
|
if (!broadcastAttempted) {
|
|
@@ -687,6 +724,8 @@ function decodeTransferCall(call, currency) {
|
|
|
687
724
|
return null;
|
|
688
725
|
}
|
|
689
726
|
async function assertTransferLogs(receipt, parameters) {
|
|
727
|
+
if (receipt.status !== 'success')
|
|
728
|
+
throw new Error(`Transaction reverted: ${receipt.transactionHash}`);
|
|
690
729
|
const logs = getTransferLogEffects(receipt);
|
|
691
730
|
const used = new Set();
|
|
692
731
|
const matched = [];
|
|
@@ -889,46 +928,38 @@ async function isActiveAccessKey(client, parameters) {
|
|
|
889
928
|
}
|
|
890
929
|
}
|
|
891
930
|
/** @internal */
|
|
892
|
-
function toReceipt(
|
|
893
|
-
const { status, transactionHash } = receipt;
|
|
894
|
-
if (status !== 'success') {
|
|
895
|
-
throw new Error(`Transaction reverted: ${transactionHash}`);
|
|
896
|
-
}
|
|
931
|
+
function toReceipt(reference, fundingCurrency) {
|
|
897
932
|
return {
|
|
933
|
+
...(fundingCurrency ? { fundingCurrency } : {}),
|
|
898
934
|
method: 'tempo',
|
|
899
935
|
status: 'success',
|
|
900
936
|
timestamp: new Date().toISOString(),
|
|
901
|
-
reference
|
|
937
|
+
reference,
|
|
902
938
|
};
|
|
903
939
|
}
|
|
904
|
-
/**
|
|
905
|
-
* Asserts that at least one of the matched payment logs carries a
|
|
906
|
-
* challenge-bound memo nonce (keccak256(challengeId)[0..6] in bytes 25–31).
|
|
907
|
-
* Only checks logs that were matched by `assertTransferLogs`, not the
|
|
908
|
-
* entire receipt — preventing unrelated dust transfers from satisfying
|
|
909
|
-
* the binding.
|
|
910
|
-
* @internal
|
|
911
|
-
*/
|
|
912
940
|
function assertChallengeBoundMemo(matchedLogs, parameters) {
|
|
913
|
-
|
|
914
|
-
if (log.kind !== 'memo')
|
|
915
|
-
return false;
|
|
916
|
-
if (!Attribution.verifyServer(log.args.memo, parameters.realm))
|
|
917
|
-
return false;
|
|
918
|
-
return Attribution.verifyChallengeBinding(log.args.memo, parameters.challengeId);
|
|
919
|
-
});
|
|
920
|
-
if (!bound)
|
|
921
|
-
throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
|
|
941
|
+
assertChallengeBoundMemos(matchedLogs.map((log) => (log.kind === 'memo' ? log.args.memo : undefined)), parameters);
|
|
922
942
|
}
|
|
923
943
|
function assertChallengeBoundCallMemo(matchedCalls, parameters) {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
944
|
+
assertChallengeBoundMemos(matchedCalls.map((call) => call.memo), parameters);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Requires a challenge-bound attribution memo and rejects conflicting attribution
|
|
948
|
+
* on matched payment transfers. Otherwise, a split payment could authorize another
|
|
949
|
+
* challenge after the first challenge's replay marker expires. Unmatched transfers
|
|
950
|
+
* cannot establish the binding; plain transfers and non-MPP memos remain allowed.
|
|
951
|
+
* @internal
|
|
952
|
+
*/
|
|
953
|
+
function assertChallengeBoundMemos(memos, parameters) {
|
|
954
|
+
let bound = false;
|
|
955
|
+
for (const memo of memos) {
|
|
956
|
+
if (!memo || !Attribution.isMppMemo(memo))
|
|
957
|
+
continue;
|
|
958
|
+
if (!Attribution.verifyServer(memo, parameters.realm) ||
|
|
959
|
+
!Attribution.verifyChallengeBinding(memo, parameters.challengeId))
|
|
960
|
+
throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
|
|
961
|
+
bound = true;
|
|
962
|
+
}
|
|
932
963
|
if (!bound)
|
|
933
964
|
throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
|
|
934
965
|
}
|