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.
Files changed (126) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +22 -1
  3. package/dist/Method.d.ts +7 -1
  4. package/dist/Method.js +7 -2
  5. package/dist/Receipt.d.ts +2 -0
  6. package/dist/Receipt.js +2 -0
  7. package/dist/cli/agent.d.ts +3 -0
  8. package/dist/cli/agent.js +20 -0
  9. package/dist/cli/cli.d.ts +9 -1
  10. package/dist/cli/cli.js +134 -136
  11. package/dist/cli/plugins/index.js +12 -14
  12. package/dist/cli/plugins/tempo.js +9 -2
  13. package/dist/cli/utils.d.ts +11 -1
  14. package/dist/cli/utils.js +15 -2
  15. package/dist/cli/validate/challenge.d.ts +2 -0
  16. package/dist/cli/validate/challenge.js +19 -4
  17. package/dist/cli/validate/discovery.d.ts +1 -0
  18. package/dist/cli/validate/discovery.js +10 -1
  19. package/dist/cli/validate/payment.js +63 -24
  20. package/dist/client/Methods.d.ts +0 -2
  21. package/dist/client/Methods.js +0 -2
  22. package/dist/client/Mppx.js +1 -0
  23. package/dist/client/index.d.ts +1 -1
  24. package/dist/client/index.js +1 -1
  25. package/dist/client/internal/Fetch.d.ts +2 -0
  26. package/dist/client/internal/Fetch.js +8 -2
  27. package/dist/client/internal/MethodChallenge.d.ts +1 -0
  28. package/dist/evm/Types.js +1 -1
  29. package/dist/evm/server/Charge.js +5 -0
  30. package/dist/evm/server/Methods.d.ts +164 -3
  31. package/dist/evm/server/Methods.js +50 -2
  32. package/dist/integrations/x402/Evm.js +4 -3
  33. package/dist/internal/HeaderCodec.d.ts +2 -2
  34. package/dist/internal/version.d.ts +2 -2
  35. package/dist/internal/version.js +1 -1
  36. package/dist/mcp/client/McpClient.js +4 -7
  37. package/dist/middlewares/elysia.d.ts +11 -8
  38. package/dist/middlewares/elysia.js +67 -14
  39. package/dist/middlewares/express.js +13 -6
  40. package/dist/middlewares/hono.js +4 -4
  41. package/dist/middlewares/nextjs.js +9 -5
  42. package/dist/proxy/internal/Headers.js +65 -0
  43. package/dist/proxy/services/stripe.js +1 -0
  44. package/dist/server/Mppx.d.ts +18 -2
  45. package/dist/server/Mppx.js +204 -92
  46. package/dist/server/NodeListener.js +71 -6
  47. package/dist/server/Request.d.ts +11 -1
  48. package/dist/server/Request.js +132 -13
  49. package/dist/server/Transport.d.ts +14 -1
  50. package/dist/server/Transport.js +1 -0
  51. package/dist/server/internal/html/compose.main.gen.d.ts +1 -1
  52. package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
  53. package/dist/stripe/server/Methods.d.ts +10 -3
  54. package/dist/stripe/server/Methods.js +19 -6
  55. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  56. package/dist/stripe/server/internal/html.gen.js +1 -1
  57. package/dist/tempo/Methods.d.ts +1 -0
  58. package/dist/tempo/Methods.js +1 -0
  59. package/dist/tempo/client/Charge.js +17 -13
  60. package/dist/tempo/client/Methods.d.ts +1 -5
  61. package/dist/tempo/client/Methods.js +10 -5
  62. package/dist/tempo/client/ResolveAccount.d.ts +3 -0
  63. package/dist/tempo/client/index.d.ts +0 -3
  64. package/dist/tempo/client/index.js +0 -2
  65. package/dist/tempo/internal/currencies.d.ts +19 -0
  66. package/dist/tempo/internal/currencies.js +41 -0
  67. package/dist/tempo/internal/defaults.d.ts +5 -3
  68. package/dist/tempo/internal/defaults.js +6 -2
  69. package/dist/tempo/internal/fee-payer.d.ts +11 -7
  70. package/dist/tempo/internal/fee-payer.js +30 -11
  71. package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
  72. package/dist/tempo/internal/machine-token-charge.js +2 -1
  73. package/dist/tempo/legacy/index.d.ts +1 -3
  74. package/dist/tempo/legacy/index.js +1 -3
  75. package/dist/tempo/server/Charge.d.ts +12 -1
  76. package/dist/tempo/server/Charge.js +109 -78
  77. package/dist/tempo/server/Methods.d.ts +5048 -40
  78. package/dist/tempo/server/Methods.js +104 -13
  79. package/dist/tempo/server/Subscription.d.ts +1 -1
  80. package/dist/tempo/server/Subscription.js +31 -4
  81. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  82. package/dist/tempo/server/internal/html.gen.js +1 -1
  83. package/dist/tempo/server/internal/transport.js +28 -11
  84. package/dist/tempo/session/client/ChannelOps.js +5 -0
  85. package/dist/tempo/session/client/Session.d.ts +1 -0
  86. package/dist/tempo/session/client/Session.js +7 -4
  87. package/dist/tempo/session/client/SessionManager.js +3 -0
  88. package/dist/tempo/session/client/Transports.d.ts +8 -1
  89. package/dist/tempo/session/client/Transports.js +12 -3
  90. package/dist/tempo/session/precompile/Chain.js +16 -1
  91. package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
  92. package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
  93. package/dist/tempo/session/precompile/Voucher.js +48 -0
  94. package/dist/tempo/session/server/ChannelStore.d.ts +22 -2
  95. package/dist/tempo/session/server/ChannelStore.js +37 -7
  96. package/dist/tempo/session/server/CredentialVerification.d.ts +6 -1
  97. package/dist/tempo/session/server/CredentialVerification.js +56 -19
  98. package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
  99. package/dist/tempo/session/server/MeteredStream.js +85 -29
  100. package/dist/tempo/session/server/RequestState.d.ts +6 -0
  101. package/dist/tempo/session/server/RequestState.js +11 -4
  102. package/dist/tempo/session/server/Session.d.ts +2 -1
  103. package/dist/tempo/session/server/Session.js +10 -2
  104. package/dist/tempo/session/server/Settlement.d.ts +23 -0
  105. package/dist/tempo/session/server/Settlement.js +105 -25
  106. package/dist/tempo/session/server/Sse.d.ts +3 -1
  107. package/dist/tempo/session/server/Sse.js +105 -32
  108. package/dist/tempo/session/server/Transports.d.ts +24 -19
  109. package/dist/tempo/session/server/Transports.js +169 -58
  110. package/dist/tempo/session/server/Ws.d.ts +5 -1
  111. package/dist/tempo/session/server/Ws.js +75 -20
  112. package/dist/tempo/subscription/KeyAuthorization.d.ts +70 -8
  113. package/dist/tempo/subscription/Store.d.ts +2 -0
  114. package/dist/tempo/subscription/Store.js +3 -0
  115. package/dist/validation/core.js +2 -1
  116. package/dist/x402/server/EvmCharge.d.ts +3 -1
  117. package/dist/x402/server/EvmCharge.js +14 -1
  118. package/package.json +8 -8
  119. package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
  120. package/dist/tempo/legacy/client/ChannelOps.js +0 -168
  121. package/dist/tempo/legacy/client/Session.d.ts +0 -185
  122. package/dist/tempo/legacy/client/Session.js +0 -279
  123. package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
  124. package/dist/tempo/legacy/client/SessionManager.js +0 -706
  125. package/dist/tempo/legacy/client/index.d.ts +0 -7
  126. 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
- /** Rejects a sponsored fee token outside the server's allowlist. */
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 (!allowedFeeTokens.some((allowed) => TempoAddress_internal.isEqual(allowed, normalized)))
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: true,
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 = [...Abis.tip20, ...Abis.stablecoinDex];
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
- return undefined;
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 (canonical && data.toLowerCase() !== canonical.toLowerCase())
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
- /** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
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
- const machineTokenSwapperAbi = parseAbi([
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 client surface. */
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 client surface. */
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, preferring pathUSD.
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: toReceipt(receipt), sender, transfers };
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 { machineTokenEnabled, amount, chainId, challenge, client, currency, methodDetails, recipient, requestAllowsFeePayer, supportedModes, } = context;
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 machineTokenRoute = machineTokenEnabled
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
- if (!machineTokenRoute)
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: machineTokenRoute ? [machineTokenRoute.settlementSender] : [],
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
- return receipt;
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 = FeePayer.defaultAllowedFeeTokens(chainId);
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(receipt) {
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: transactionHash,
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
- const bound = matchedLogs.some((log) => {
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
- const bound = matchedCalls.some((call) => {
925
- if (!call.memo)
926
- return false;
927
- const memo = call.memo;
928
- if (!Attribution.verifyServer(memo, parameters.realm))
929
- return false;
930
- return Attribution.verifyChallengeBinding(memo, parameters.challengeId);
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
  }