mppx 0.17.1 → 0.17.3

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 (63) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/Store.d.ts +225 -0
  3. package/dist/Store.js +358 -0
  4. package/dist/cli/cli.js +95 -94
  5. package/dist/cli/plugins/index.js +12 -11
  6. package/dist/cli/sessions/Manager.js +12 -1
  7. package/dist/cli/utils.d.ts +2 -0
  8. package/dist/cli/utils.js +6 -0
  9. package/dist/client/Mppx.d.ts +1 -1
  10. package/dist/client/Mppx.js +7 -3
  11. package/dist/client/internal/Fetch.d.ts +1 -1
  12. package/dist/client/internal/Fetch.js +9 -0
  13. package/dist/client/internal/MethodResponse.d.ts +8 -0
  14. package/dist/client/internal/MethodResponse.js +9 -0
  15. package/dist/client/internal/ProofRealm.d.ts +3 -3
  16. package/dist/client/internal/ProofRealm.js +4 -4
  17. package/dist/client/internal/protocols/Mcp.d.ts +4 -0
  18. package/dist/client/internal/protocols/Mcp.js +27 -14
  19. package/dist/identity/Client.js +7 -1
  20. package/dist/internal/sse.d.ts +21 -0
  21. package/dist/internal/sse.js +65 -0
  22. package/dist/internal/version.d.ts +2 -2
  23. package/dist/internal/version.js +1 -1
  24. package/dist/mcp/client/McpClient.d.ts +2 -2
  25. package/dist/mcp/client/McpClient.js +5 -2
  26. package/dist/proxy/Proxy.d.ts +2 -0
  27. package/dist/proxy/Proxy.js +31 -6
  28. package/dist/proxy/internal/Replay.js +1 -1
  29. package/dist/server/Mppx.js +5 -4
  30. package/dist/server/Transport.js +1 -1
  31. package/dist/server/internal/html/config.d.ts +7 -1
  32. package/dist/server/internal/html/config.js +4 -0
  33. package/dist/stripe/internal/types.d.ts +82 -6
  34. package/dist/stripe/server/Charge.js +1 -2
  35. package/dist/stripe/server/internal/html.gen.js +1 -1
  36. package/dist/tempo/internal/fee-payer.d.ts +85 -10
  37. package/dist/tempo/internal/fee-payer.js +183 -19
  38. package/dist/tempo/legacy/session/Chain.js +2 -0
  39. package/dist/tempo/server/Charge.js +2 -2
  40. package/dist/tempo/server/Subscription.js +1 -0
  41. package/dist/tempo/server/internal/html.gen.js +1 -1
  42. package/dist/tempo/server/internal/transport.d.ts +6 -0
  43. package/dist/tempo/server/internal/transport.js +21 -21
  44. package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
  45. package/dist/tempo/session/client/CredentialState.js +2 -0
  46. package/dist/tempo/session/client/Session.d.ts +11 -1
  47. package/dist/tempo/session/client/Session.js +39 -6
  48. package/dist/tempo/session/client/SessionManager.d.ts +1 -1
  49. package/dist/tempo/session/client/SessionManager.js +8 -4
  50. package/dist/tempo/session/client/Transports.d.ts +2 -0
  51. package/dist/tempo/session/client/Transports.js +4 -1
  52. package/dist/tempo/session/precompile/Chain.js +1 -0
  53. package/dist/tempo/session/server/ChannelStore.d.ts +9 -0
  54. package/dist/tempo/session/server/ChannelStore.js +8 -1
  55. package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
  56. package/dist/tempo/session/server/MeteredStream.js +6 -1
  57. package/dist/tempo/session/server/Sse.d.ts +59 -0
  58. package/dist/tempo/session/server/Sse.js +175 -10
  59. package/dist/zod.d.ts +2 -1
  60. package/dist/zod.js +1 -1
  61. package/package.json +1 -3
  62. package/dist/stripe/server/internal/html/types.d.ts +0 -2
  63. package/dist/stripe/server/internal/html/types.js +0 -2
@@ -1,4 +1,4 @@
1
- import { Address, Secp256k1 } from 'ox';
1
+ import { Address, Secp256k1, Signature } from 'ox';
2
2
  import { TempoAddress, TxEnvelopeTempo } from 'ox/tempo';
3
3
  import { decodeFunctionData, encodeFunctionData, maxUint256, toFunctionSelector, toHex } from 'viem';
4
4
  import { Abis, Addresses, Transaction } from 'viem/tempo';
@@ -163,6 +163,12 @@ export async function fillHostedFeePayerTransaction(parameters) {
163
163
  })();
164
164
  return {
165
165
  feePayer,
166
+ // `Signature.fromRpc` rejects a yParity of 0 that arrives without `v`.
167
+ feePayerSignature: Signature.from({
168
+ r: BigInt(feePayerSignature.r),
169
+ s: BigInt(feePayerSignature.s),
170
+ yParity: feePayerSignature.yParity,
171
+ }),
166
172
  feeToken,
167
173
  serializedTransaction: await Transaction.serialize({
168
174
  ...transaction,
@@ -175,17 +181,36 @@ export async function fillHostedFeePayerTransaction(parameters) {
175
181
  /**
176
182
  * Returns a transaction shape suitable for pre-broadcast simulation.
177
183
  *
178
- * Sponsored transactions are first simulated as calls from the sender with no
179
- * fee fields or signatures. This checks call execution without requiring the
180
- * sender to hold the fee token; transferred value and call-level balances are
181
- * still checked by the RPC.
184
+ * Sponsored transactions are first simulated as calls from the sender, without
185
+ * signatures, fee payer or fee token, and with zero fee fields: Tempo nodes
186
+ * (v1.18+) skip the fee-token balance check for a zero-fee `eth_call`, so the
187
+ * sender's fee balance is irrelevant. The call keeps the transaction's exact
188
+ * gas limit and the fields that set its intrinsic gas (nonce, nonce key,
189
+ * validity window, key authorization, signature type), so it is the gas-bounded
190
+ * check at the broadcast limit: a transaction whose gas is too low fails here
191
+ * with out of gas instead of reverting onchain at the sponsor's expense.
192
+ * Transferred value and call-level balances are still checked by the RPC.
182
193
  */
183
194
  export function simulationTransaction(transaction, options) {
184
- if (options.feePayer)
195
+ if (options.feePayer) {
196
+ const keyType = senderKeyType(transaction.signature);
185
197
  return {
186
198
  account: transaction.from,
187
199
  calls: transaction.calls,
200
+ ...(transaction.accessList?.length ? { accessList: transaction.accessList } : {}),
201
+ ...(transaction.gas !== undefined ? { gas: transaction.gas } : {}),
202
+ ...(transaction.keyAuthorization !== undefined
203
+ ? { keyAuthorization: transaction.keyAuthorization }
204
+ : {}),
205
+ ...(keyType ? { keyType } : {}),
206
+ maxFeePerGas: 0n,
207
+ maxPriorityFeePerGas: 0n,
208
+ ...(transaction.nonce !== undefined ? { nonce: transaction.nonce } : {}),
209
+ ...(transaction.nonceKey !== undefined ? { nonceKey: transaction.nonceKey } : {}),
210
+ ...(transaction.validAfter !== undefined ? { validAfter: transaction.validAfter } : {}),
211
+ ...(transaction.validBefore !== undefined ? { validBefore: transaction.validBefore } : {}),
188
212
  };
213
+ }
189
214
  return {
190
215
  ...transaction,
191
216
  account: transaction.from,
@@ -193,43 +218,182 @@ export function simulationTransaction(transaction, options) {
193
218
  feePayerSignature: undefined,
194
219
  };
195
220
  }
221
+ /** The key type hint for a sender signature whose verification costs more than secp256k1. */
222
+ function senderKeyType(signature) {
223
+ const envelope = signature;
224
+ const type = envelope?.type === 'keychain' ? envelope.inner?.type : envelope?.type;
225
+ return type === 'p256' || type === 'webAuthn' ? type : undefined;
226
+ }
196
227
  /**
197
228
  * Returns the final fee-sponsored transaction shape for pre-broadcast
198
- * simulation. RPC `eth_call` does not carry either transaction signature.
229
+ * simulation.
230
+ *
231
+ * Tempo nodes recover the fee payer of an `eth_call` only from
232
+ * `feePayerSignature`; a bare `feePayer` address is ignored and the sender is
233
+ * billed instead. Pass the sponsor's signature so the node charges the fee
234
+ * token to the sponsor, not to a sender that may not hold it. The sender's own
235
+ * signature is never sent.
199
236
  */
200
237
  export function sponsoredSimulationTransaction(transaction, options) {
201
- const { feePayer, feeToken } = options;
238
+ const { feePayer, feePayerSignature, feeToken } = options;
202
239
  return {
203
240
  ...transaction,
204
241
  account: transaction.from,
205
242
  calls: transaction.calls,
206
243
  feePayer,
207
- feePayerSignature: undefined,
244
+ feePayerSignature: feePayerSignature ? Signature.toRpc(feePayerSignature) : undefined,
208
245
  ...(feeToken !== undefined ? { feeToken } : {}),
209
246
  signature: undefined,
210
247
  };
211
248
  }
212
249
  /**
213
- * Runs execution-only sender and final-envelope simulations around sponsorship.
250
+ * Signs the fee-payer payload of a sponsored envelope with a local fee-payer
251
+ * account, the same payload `Transaction.serialize` signs before broadcast.
252
+ * Returns `undefined` when the envelope's fee payer cannot sign locally.
253
+ */
254
+ export async function signFeePayerSimulation(transaction) {
255
+ const { account: _account, feePayer, ...rest } = transaction;
256
+ if (typeof feePayer !== 'object' || feePayer === null)
257
+ return undefined;
258
+ const sign = feePayer.sign;
259
+ if (typeof sign !== 'function' || !rest.from)
260
+ return undefined;
261
+ const payload = TxEnvelopeTempo.getFeePayerSignPayload(TxEnvelopeTempo.from({
262
+ ...rest,
263
+ chainId: Number(rest.chainId),
264
+ feePayerSignature: undefined,
265
+ signature: undefined,
266
+ type: 'tempo',
267
+ }), { sender: rest.from });
268
+ return Signature.from(await sign({ hash: payload }));
269
+ }
270
+ /**
271
+ * Runs the sender and final-envelope simulations around sponsorship.
272
+ *
273
+ * First, it simulates the sender's calls at the transaction's exact gas limit
274
+ * with zero fee fields (see {@link simulationTransaction}). This is the
275
+ * gas-bounded check at the broadcast limit, and needs no fee-token balance.
276
+ * Next, `complete` resolves the sponsor and produces the co-signed transaction.
277
+ * Finally, it simulates that transaction with its concrete fee payer, fee token
278
+ * and fee payer signature, so the node bills the sponsor rather than the sender
279
+ * and checks the sponsor's fee balance.
214
280
  *
215
- * First, it simulates the calls with the sender as `from`, omitting fee fields
216
- * and signatures so the sender's fee balance is irrelevant. Next, `complete`
217
- * resolves the sponsor and produces the co-signed transaction. Finally, it
218
- * simulates that transaction with its concrete fee payer and fee token.
281
+ * A sponsored `eth_call` charges more gas than the mined transaction uses. A
282
+ * local fee payer therefore signs a simulation-only copy whose gas is raised by
283
+ * up to {@link sponsoredSimulationGasBuffer}, clamped so the copy stays within
284
+ * the same fee payer `policy` (`maxGas`, `maxTotalFee`) as the real
285
+ * transaction. The copy is never returned or broadcast. When the final
286
+ * simulation runs without the full buffer (hosted signatures cover the client's
287
+ * gas and cannot be padded, or the padding was clamped), a failure that is only
288
+ * out of gas is tolerated, since the sender simulation already bounded gas at
289
+ * the broadcast limit. Every other failure, such as an insufficient fee balance
290
+ * or a revert, is rethrown.
219
291
  *
220
292
  * `complete` runs only after sender-context execution succeeds, so a reverting
221
293
  * transaction never reaches a local signer or hosted fee-payer.
222
294
  */
223
295
  export async function preflightSponsorship(parameters) {
224
- const { complete, simulate, transaction } = parameters;
296
+ const { complete, policy, simulate, transaction } = parameters;
225
297
  await simulate(simulationTransaction(transaction, { feePayer: true }));
226
298
  const completed = await complete();
227
- await simulate(sponsoredSimulationTransaction(completed.transaction, {
228
- feePayer: completed.feePayer,
229
- feeToken: completed.feeToken,
230
- }));
299
+ const { gas } = completed.transaction;
300
+ const simulated = await (async () => {
301
+ if (completed.feePayerSignature)
302
+ return { feePayerSignature: completed.feePayerSignature, transaction: completed.transaction };
303
+ // The signature binds the gas limit, so a local fee payer signs a simulation-only copy
304
+ // with headroom for the node's sponsored `eth_call` overhead. The broadcast keeps its gas.
305
+ const padded = gas === undefined
306
+ ? completed.transaction
307
+ : {
308
+ ...completed.transaction,
309
+ gas: sponsoredSimulationGas(completed.transaction, policy),
310
+ };
311
+ const feePayerSignature = await signFeePayerSimulation(padded);
312
+ return feePayerSignature
313
+ ? { feePayerSignature, transaction: padded }
314
+ : { feePayerSignature: undefined, transaction: completed.transaction };
315
+ })();
316
+ const simulatedGas = simulated.transaction.gas;
317
+ try {
318
+ await simulate(sponsoredSimulationTransaction(simulated.transaction, {
319
+ feePayer: completed.feePayer,
320
+ feePayerSignature: simulated.feePayerSignature,
321
+ feeToken: completed.feeToken,
322
+ }));
323
+ }
324
+ catch (error) {
325
+ const unpadded = gas !== undefined &&
326
+ simulatedGas !== undefined &&
327
+ simulatedGas < gas + sponsoredSimulationGasBuffer;
328
+ if (!unpadded || !isOutOfGasError(error))
329
+ throw error;
330
+ }
231
331
  return completed;
232
332
  }
333
+ /**
334
+ * Returns the gas limit of a local fee payer's simulation-only copy: the
335
+ * transaction's gas plus {@link sponsoredSimulationGasBuffer}, clamped to the
336
+ * fee payer policy's `maxGas` and to `maxTotalFee / maxFeePerGas`, so the
337
+ * signed copy never exceeds a cap the real transaction is held to.
338
+ */
339
+ export function sponsoredSimulationGas(transaction, policyOverrides) {
340
+ const { chainId, gas, maxFeePerGas } = transaction;
341
+ if (gas === undefined)
342
+ throw new FeePayerValidationError('fee-sponsored transaction must declare gas', {});
343
+ const policy = getPolicy(Number(chainId), policyOverrides);
344
+ let cap = policy.maxGas;
345
+ if (maxFeePerGas !== undefined && maxFeePerGas > 0n) {
346
+ const feeCap = policy.maxTotalFee / maxFeePerGas;
347
+ if (feeCap < cap)
348
+ cap = feeCap;
349
+ }
350
+ if (gas > cap)
351
+ throw new FeePayerValidationError('fee-sponsored transaction gas exceeds sponsor policy', {
352
+ gas: gas.toString(),
353
+ });
354
+ const padded = gas + sponsoredSimulationGasBuffer;
355
+ return padded < cap ? padded : cap;
356
+ }
357
+ /**
358
+ * Returns true when a simulation failed only because it ran out of gas.
359
+ *
360
+ * Tempo nodes report out of gas as `out of gas: …` (code -32003, shared with
361
+ * `insufficient fee token balance`) or `intrinsic gas too low` (code -32000),
362
+ * so this reads the node's own message rather than the code. Only the terminal
363
+ * RPC failure counts: the deepest `details` in the cause chain (viem's
364
+ * `RpcRequestError` carries the node's message there), or the deepest message
365
+ * when no error has details. Wrapper messages are ignored, the match is anchored
366
+ * to the start of the node's message, and an error carrying revert data is never
367
+ * treated as gas exhaustion, so a revert reason or wrapper text that merely
368
+ * mentions gas cannot hide another failure.
369
+ */
370
+ export function isOutOfGasError(error) {
371
+ let terminal;
372
+ let fallback;
373
+ for (let current = error, depth = 0; current && depth < 10; depth++) {
374
+ const { cause, data, details, message } = current;
375
+ if (typeof data === 'string' && data !== '0x')
376
+ return false;
377
+ if (typeof details === 'string')
378
+ terminal = details;
379
+ if (typeof message === 'string')
380
+ fallback = message;
381
+ current = cause;
382
+ }
383
+ const nodeMessage = (terminal ?? fallback)?.trim();
384
+ if (!nodeMessage)
385
+ return false;
386
+ return /^(?:out of gas(?::|$)|intrinsic gas too low(?::|$))/i.test(nodeMessage);
387
+ }
388
+ /**
389
+ * Extra gas a local fee payer signs into its simulation-only envelope. Tempo's sponsored
390
+ * `eth_call` charges more gas than the same transaction uses when mined (about 6,000 for a
391
+ * session top-up on v1.18.1), so simulating at the signed limit would reject transactions
392
+ * that succeed onchain. The padding is clamped to the fee payer policy; hosted signatures
393
+ * cover the client's gas and are simulated as signed. In both cases the gas bound at the
394
+ * broadcast limit comes from the sender simulation.
395
+ */
396
+ export const sponsoredSimulationGasBuffer = 10000n;
233
397
  /**
234
398
  * maxTotalFee must be high enough to cover `transferWithMemo` and
235
399
  * swap transactions at peak gas prices. Bumped from 0.01 ETH in #327.
@@ -355,6 +355,7 @@ export async function broadcastOpenTransaction(parameters) {
355
355
  reason: 'transaction does not contain a valid escrow open call',
356
356
  });
357
357
  const completed = await FeePayer.preflightSponsorship({
358
+ policy: feePayerPolicy,
358
359
  transaction,
359
360
  simulate: (request) => call(client, request),
360
361
  async complete() {
@@ -470,6 +471,7 @@ export async function broadcastTopUpTransaction(parameters) {
470
471
  });
471
472
  const defaultFeeToken = defaults.currency[client.chain?.id];
472
473
  const completed = await FeePayer.preflightSponsorship({
474
+ policy: feePayerPolicy,
473
475
  transaction,
474
476
  simulate: (request) => call(client, request),
475
477
  async complete() {
@@ -20,7 +20,6 @@ import * as Proof from '../internal/proof.js';
20
20
  import * as Selectors from '../internal/selectors.js';
21
21
  import * as Methods from '../Methods.js';
22
22
  import { fallbackStore } from './internal/fallback-store.js';
23
- import { html as htmlContent } from './internal/html.gen.js';
24
23
  import * as Relay from './Relay.js';
25
24
  import * as SponsorBudget from './SponsorBudget.js';
26
25
  /**
@@ -322,7 +321,7 @@ export function charge(parameters = {}) {
322
321
  html: html
323
322
  ? {
324
323
  config: {},
325
- content: htmlContent,
324
+ content: async () => (await import('./internal/html.gen.js')).html,
326
325
  formatAmount: async (request) => {
327
326
  try {
328
327
  const chainId = request.methodDetails?.chainId;
@@ -481,6 +480,7 @@ export function charge(parameters = {}) {
481
480
  if (feePayerAccount && methodDetails?.feePayer !== false) {
482
481
  const selectableFeeTokens = allowedFeeTokens ?? FeePayer.defaultAllowedFeeTokens(chainId);
483
482
  const completed = await FeePayer.preflightSponsorship({
483
+ policy: transactionFeePayerPolicy,
484
484
  transaction,
485
485
  simulate: (request) => viem_call(client, request),
486
486
  async complete() {
@@ -713,6 +713,7 @@ async function submitSubscriptionPayment(parameters) {
713
713
  const userSerialized = await signTransaction(client, prepared);
714
714
  const userTransaction = Transaction.deserialize(userSerialized);
715
715
  const completed = await FeePayer.preflightSponsorship({
716
+ policy: feePayerPolicy,
716
717
  transaction: userTransaction,
717
718
  simulate: (request) => viem_call(client, request),
718
719
  async complete() {