mppx 0.17.3 → 0.17.4

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 (41) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/Errors.d.ts +94 -0
  3. package/dist/Errors.js +83 -0
  4. package/dist/Store.d.ts +199 -11
  5. package/dist/Store.js +527 -48
  6. package/dist/cli/cli.js +141 -137
  7. package/dist/cli/internal.js +28 -6
  8. package/dist/cli/plugins/index.js +16 -12
  9. package/dist/cli/validate/challenge.d.ts +4 -0
  10. package/dist/cli/validate/challenge.js +9 -6
  11. package/dist/cloudflare/ChannelStore.d.ts +97 -0
  12. package/dist/cloudflare/ChannelStore.js +304 -0
  13. package/dist/cloudflare/Channels.d.ts +92 -0
  14. package/dist/cloudflare/Channels.js +325 -0
  15. package/dist/cloudflare/index.d.ts +5 -0
  16. package/dist/cloudflare/index.js +5 -0
  17. package/dist/internal/sse.js +32 -15
  18. package/dist/internal/version.d.ts +2 -2
  19. package/dist/internal/version.js +1 -1
  20. package/dist/stripe/server/Methods.d.ts +2 -0
  21. package/dist/stripe/server/Methods.js +2 -1
  22. package/dist/stripe/server/internal/record-payment.d.ts +49 -0
  23. package/dist/stripe/server/internal/record-payment.js +75 -18
  24. package/dist/tempo/Methods.d.ts +8 -0
  25. package/dist/tempo/Methods.js +18 -2
  26. package/dist/tempo/client/Charge.d.ts +32 -1
  27. package/dist/tempo/client/Charge.js +201 -3
  28. package/dist/tempo/client/Methods.d.ts +16 -0
  29. package/dist/tempo/internal/fee-payer.d.ts +0 -25
  30. package/dist/tempo/internal/fee-payer.js +50 -3
  31. package/dist/tempo/internal/zone.d.ts +79 -0
  32. package/dist/tempo/internal/zone.js +133 -0
  33. package/dist/tempo/server/Charge.d.ts +7 -3
  34. package/dist/tempo/server/Charge.js +98 -4
  35. package/dist/tempo/server/Methods.d.ts +40 -0
  36. package/dist/tempo/server/internal/html.gen.js +1 -1
  37. package/dist/tempo/session/server/ChannelStore.d.ts +5 -0
  38. package/dist/tempo/session/server/ChannelStore.js +2 -2
  39. package/dist/tempo/session/server/CredentialVerification.js +1 -1
  40. package/dist/validation/core.js +2 -0
  41. package/package.json +5 -1
@@ -2,6 +2,7 @@ import { parseUnits } from 'viem';
2
2
  import * as Constants from '../Constants.js';
3
3
  import * as Method from '../Method.js';
4
4
  import * as z from '../zod.js';
5
+ import * as Zone from './internal/zone.js';
5
6
  export const chargeModes = ['push', 'pull'];
6
7
  const split = z.object({
7
8
  amount: z.amount(),
@@ -79,6 +80,12 @@ export const charge = Method.from({
79
80
  z.object({ hash: z.hash(), type: z.literal('hash') }),
80
81
  z.object({ signature: z.signature(), type: z.literal('transaction') }),
81
82
  z.object({ signature: z.signature(), type: z.literal('proof') }),
83
+ z.object({
84
+ hash: z.hash(),
85
+ senderTag: z.hash(),
86
+ type: z.literal('zone'),
87
+ zoneId: z.number().check(z.refine(Zone.isId, 'Invalid Zone ID')),
88
+ }),
82
89
  ]),
83
90
  },
84
91
  request: z.pipe(z
@@ -95,6 +102,13 @@ export const charge = Method.from({
95
102
  recipient: z.optional(z.string()),
96
103
  splits: z.optional(z.array(split).check(z.minLength(1), z.maxLength(10))),
97
104
  supportedModes: z.optional(z.array(z.enum(chargeModes)).check(z.minLength(1))),
105
+ // Zone IDs the payee accepts withdrawals from, or true for any Zone on the challenge's chain.
106
+ zones: z.optional(z.union([
107
+ z.literal(true),
108
+ z
109
+ .array(z.number().check(z.refine(Zone.isId, 'Invalid Zone ID')))
110
+ .check(z.minLength(1)),
111
+ ])),
98
112
  })
99
113
  .check(z.refine(({ amount, decimals, splits }) => {
100
114
  if (!splits)
@@ -103,14 +117,15 @@ export const charge = Method.from({
103
117
  const splitTotal = splits.reduce((sum, split) => sum + parseUnits(split.amount, decimals), 0n);
104
118
  return (splits.every((split) => parseUnits(split.amount, decimals) > 0n) &&
105
119
  splitTotal < totalAmount);
106
- }, 'Invalid splits')), z.transform(({ machineTokenEnabled, amount, chainId, decimals, feePayer, memo: _memo, splits, supportedModes, ...rest }) => ({
120
+ }, 'Invalid splits')), z.transform(({ machineTokenEnabled, amount, chainId, decimals, feePayer, memo: _memo, splits, supportedModes, zones, ...rest }) => ({
107
121
  ...rest,
108
122
  amount: parseUnits(amount, decimals).toString(),
109
123
  ...(machineTokenEnabled !== undefined ||
110
124
  chainId !== undefined ||
111
125
  feePayer !== undefined ||
112
126
  splits !== undefined ||
113
- supportedModes !== undefined
127
+ supportedModes !== undefined ||
128
+ zones !== undefined
114
129
  ? {
115
130
  methodDetails: {
116
131
  ...(machineTokenEnabled !== undefined && { machineTokenEnabled }),
@@ -123,6 +138,7 @@ export const charge = Method.from({
123
138
  })),
124
139
  }),
125
140
  ...(supportedModes !== undefined && { supportedModes }),
141
+ ...(zones !== undefined && { zones }),
126
142
  },
127
143
  }
128
144
  : {}),
@@ -1,4 +1,4 @@
1
- import { type Address } from 'viem';
1
+ import { type Address, type Client as viem_Client } from 'viem';
2
2
  import * as Method from '../../Method.js';
3
3
  import * as Account from '../../viem/Account.js';
4
4
  import * as Client from '../../viem/Client.js';
@@ -61,6 +61,11 @@ export declare function charge(parameters?: charge.Parameters): {
61
61
  }, z.core.$strip>, z.ZodMiniObject<{
62
62
  signature: z.ZodMiniString<string>;
63
63
  type: z.ZodMiniLiteral<"proof">;
64
+ }, z.core.$strip>, z.ZodMiniObject<{
65
+ hash: z.ZodMiniString<string>;
66
+ senderTag: z.ZodMiniString<string>;
67
+ type: z.ZodMiniLiteral<"zone">;
68
+ zoneId: z.ZodMiniNumber<number>;
64
69
  }, z.core.$strip>], "type">;
65
70
  };
66
71
  request: z.ZodMiniPipe<z.ZodMiniObject<{
@@ -83,6 +88,7 @@ export declare function charge(parameters?: charge.Parameters): {
83
88
  pull: "pull";
84
89
  push: "push";
85
90
  }>>>;
91
+ zones: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniLiteral<true>, z.ZodMiniArray<z.ZodMiniNumber<number>>]>>;
86
92
  }, z.core.$strip>, z.ZodMiniTransform<{
87
93
  currency: string;
88
94
  description?: string | undefined;
@@ -99,6 +105,7 @@ export declare function charge(parameters?: charge.Parameters): {
99
105
  amount: string;
100
106
  }[] | undefined;
101
107
  supportedModes?: ("pull" | "push")[] | undefined;
108
+ zones?: true | number[] | undefined;
102
109
  } | undefined;
103
110
  }, {
104
111
  machineTokenEnabled?: boolean | undefined;
@@ -117,6 +124,7 @@ export declare function charge(parameters?: charge.Parameters): {
117
124
  recipient: `0x${string}`;
118
125
  }[] | undefined;
119
126
  supportedModes?: ("pull" | "push")[] | undefined;
127
+ zones?: true | number[] | undefined;
120
128
  }>>;
121
129
  };
122
130
  readonly createCredential: Method.CreateCredentialFn<{
@@ -133,6 +141,11 @@ export declare function charge(parameters?: charge.Parameters): {
133
141
  }, z.core.$strip>, z.ZodMiniObject<{
134
142
  signature: z.ZodMiniString<string>;
135
143
  type: z.ZodMiniLiteral<"proof">;
144
+ }, z.core.$strip>, z.ZodMiniObject<{
145
+ hash: z.ZodMiniString<string>;
146
+ senderTag: z.ZodMiniString<string>;
147
+ type: z.ZodMiniLiteral<"zone">;
148
+ zoneId: z.ZodMiniNumber<number>;
136
149
  }, z.core.$strip>], "type">;
137
150
  };
138
151
  request: z.ZodMiniPipe<z.ZodMiniObject<{
@@ -155,6 +168,7 @@ export declare function charge(parameters?: charge.Parameters): {
155
168
  pull: "pull";
156
169
  push: "push";
157
170
  }>>>;
171
+ zones: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniLiteral<true>, z.ZodMiniArray<z.ZodMiniNumber<number>>]>>;
158
172
  }, z.core.$strip>, z.ZodMiniTransform<{
159
173
  currency: string;
160
174
  description?: string | undefined;
@@ -171,6 +185,7 @@ export declare function charge(parameters?: charge.Parameters): {
171
185
  amount: string;
172
186
  }[] | undefined;
173
187
  supportedModes?: ("pull" | "push")[] | undefined;
188
+ zones?: true | number[] | undefined;
174
189
  } | undefined;
175
190
  }, {
176
191
  machineTokenEnabled?: boolean | undefined;
@@ -189,6 +204,7 @@ export declare function charge(parameters?: charge.Parameters): {
189
204
  recipient: `0x${string}`;
190
205
  }[] | undefined;
191
206
  supportedModes?: ("pull" | "push")[] | undefined;
207
+ zones?: true | number[] | undefined;
192
208
  }>>;
193
209
  };
194
210
  }, {
@@ -203,6 +219,15 @@ export declare namespace charge {
203
219
  type Context = ChargeContext;
204
220
  type ResolveAccount = AccountResolution.ResolveAccount;
205
221
  type ResolveAccountInfo = AccountResolution.ResolveAccountInfo;
222
+ /** Pays from a Zone balance when a challenge accepts that Zone. */
223
+ type Zone = {
224
+ /** Client for the Zone chain, with the paying account and Zone RPC authorization. */
225
+ client: viem_Client;
226
+ /** Zone ID. Derived from `client.chain.id` when omitted. */
227
+ zoneId?: number | undefined;
228
+ /** Milliseconds to wait for the withdrawal to settle on Tempo. @default 60_000 */
229
+ settlementTimeoutMs?: number | undefined;
230
+ };
206
231
  type Parameters = {
207
232
  /**
208
233
  * Automatically swap from a fallback currency (pathUsd, USDC.e) via the
@@ -277,6 +302,12 @@ export declare namespace charge {
277
302
  mode?: Methods.ChargeMode | undefined;
278
303
  /** Selects the account that signs this charge after the challenge and chain are known. */
279
304
  resolveAccount?: ResolveAccount | undefined;
305
+ /**
306
+ * Pays from a Zone balance instead of the account's Tempo balance when the
307
+ * challenge accepts this Zone (`methodDetails.zones`). Other challenges,
308
+ * and split payments, are paid on Tempo as usual.
309
+ */
310
+ zone?: Zone | undefined;
280
311
  } & Account.getResolver.Parameters & Client.getResolver.Parameters;
281
312
  }
282
313
  //# sourceMappingURL=Charge.d.ts.map
@@ -1,10 +1,11 @@
1
1
  import { TxEnvelopeTempo } from 'ox/tempo';
2
- import { keccak256, parseUnits } from 'viem';
3
- import { prepareTransactionRequest, sendCallsSync, sendTransactionSync, signTypedData, signTransaction, } from 'viem/actions';
4
- import { Actions } from 'viem/tempo';
2
+ import { keccak256, parseEventLogs, parseUnits, } from 'viem';
3
+ import { prepareTransactionRequest, sendCallsSync, sendRawTransactionSync, sendTransactionSync, signTypedData, signTransaction, waitForTransactionReceipt, } from 'viem/actions';
4
+ import { Abis, Actions, Addresses } from 'viem/tempo';
5
5
  import { tempo as tempo_chain } from 'viem/tempo/chains';
6
6
  import * as MethodResponse from '../../client/internal/MethodResponse.js';
7
7
  import * as Credential from '../../Credential.js';
8
+ import * as Errors from '../../Errors.js';
8
9
  import * as Method from '../../Method.js';
9
10
  import * as Account from '../../viem/Account.js';
10
11
  import * as Client from '../../viem/Client.js';
@@ -16,6 +17,7 @@ import * as defaults from '../internal/defaults.js';
16
17
  import { defaultFeeTokens, resolveFeeToken } from '../internal/fee-token.js';
17
18
  import * as MachineTokenCharge from '../internal/machine-token-charge.js';
18
19
  import * as Proof from '../internal/proof.js';
20
+ import * as Zone from '../internal/zone.js';
19
21
  import * as Methods from '../Methods.js';
20
22
  /**
21
23
  * Seconds from now the client sets as the expiring-nonce `validBefore`,
@@ -63,6 +65,9 @@ export function charge(parameters = {}) {
63
65
  rpcUrl: defaults.rpcUrl,
64
66
  });
65
67
  const getAccount = Account.getResolver({ account: parameters.account });
68
+ assertZone(parameters.zone);
69
+ // Zone withdrawals still settling, by challenge ID, so a retry resumes waiting instead of paying again.
70
+ const pendingZoneWithdrawals = new Map();
66
71
  const method = Method.toClient(Methods.charge, {
67
72
  // Skip offers that violate client policy so an acceptable offer can be chosen.
68
73
  canHandleChallenge({ challenge }) {
@@ -91,6 +96,36 @@ export function charge(parameters = {}) {
91
96
  const { request } = challenge;
92
97
  const { amount, methodDetails } = request;
93
98
  const supportedModes = methodDetails?.supportedModes ?? ['pull', 'push'];
99
+ const zoneChain = parameters.zone?.client.chain;
100
+ const zoneId = parameters.zone
101
+ ? (parameters.zone.zoneId ?? Zone.idFromChainId(zoneChain?.id ?? 0))
102
+ : undefined;
103
+ // A Zone ID is only unique per parent chain, so the Zone must settle on the challenge's chain.
104
+ const zoneParentChainId = zoneChain?.sourceId ??
105
+ (zoneChain ? Zone.parentChainIdFromChainId(zoneChain.id) : undefined);
106
+ const explicitMode = context?.mode ?? parameters.mode;
107
+ // Pay from a Zone balance when the challenge accepts the configured Zone.
108
+ // Zone payments are push payments, so they follow the challenge's and client's modes.
109
+ if (BigInt(amount) > 0n &&
110
+ parameters.zone &&
111
+ zoneId !== undefined &&
112
+ zoneParentChainId === chainId &&
113
+ supportedModes.includes('push') &&
114
+ (explicitMode === undefined || explicitMode === 'push') &&
115
+ Zone.isAccepted(methodDetails?.zones, zoneId) &&
116
+ !methodDetails?.splits &&
117
+ // A withdrawal can't be undone, so start one only when it can settle before the
118
+ // challenge expires; an expired challenge would invite another payment.
119
+ (pendingZoneWithdrawals.has(zoneWithdrawalKey(challenge)) ||
120
+ settlesBeforeExpiry(challenge.expires, parameters.zone.settlementTimeoutMs)))
121
+ return createZoneCredential({
122
+ challenge,
123
+ clientId,
124
+ client,
125
+ pending: pendingZoneWithdrawals,
126
+ zone: parameters.zone,
127
+ zoneId,
128
+ });
94
129
  const defaultAccount = getAccount(client, context);
95
130
  const resolveAccount = async (operation) => (await parameters.resolveAccount?.({
96
131
  account: defaultAccount,
@@ -326,6 +361,8 @@ function transactionHash(credential) {
326
361
  const payload = parsed.data;
327
362
  if (payload.type === 'hash')
328
363
  return payload.hash;
364
+ if (payload.type === 'zone')
365
+ return payload.hash;
329
366
  const serialized = payload.signature;
330
367
  if (payload.type !== 'transaction' || !serialized.startsWith(TxEnvelopeTempo.serializedType))
331
368
  return undefined;
@@ -337,6 +374,167 @@ function transactionHash(credential) {
337
374
  return undefined;
338
375
  return keccak256(serialized);
339
376
  }
377
+ /**
378
+ * Pays a challenge from a Zone balance: requests a withdrawal to the recipient
379
+ * with the challenge-bound memo, and returns a `zone` credential that names it
380
+ * by sender tag and the Tempo transaction hash that settled it.
381
+ */
382
+ async function createZoneCredential(options) {
383
+ const { challenge, client, pending, zone, zoneId } = options;
384
+ const request = challenge.request;
385
+ const recipient = request.recipient;
386
+ // The whole challenge, so a challenge from another realm or request with the same ID never matches.
387
+ const key = zoneWithdrawalKey(challenge);
388
+ // A withdrawal can't be undone, so a retry after a failed wait resumes it instead of paying again.
389
+ // Concurrent calls for one challenge share the request, which is stored before it is sent.
390
+ let withdrawalRequest = pending.get(key);
391
+ if (!withdrawalRequest) {
392
+ const state = { sent: false };
393
+ withdrawalRequest = requestZoneWithdrawal(options, state);
394
+ pending.set(key, withdrawalRequest);
395
+ // Only a failure before the transaction was sent is safe to retry. After that, the
396
+ // withdrawal may have gone through, so retries of this challenge keep failing.
397
+ withdrawalRequest.catch(() => {
398
+ if (!state.sent)
399
+ pending.delete(key);
400
+ });
401
+ }
402
+ const senderTag = await withdrawalRequest;
403
+ let hash;
404
+ try {
405
+ hash = await Zone.waitForSettlement(client, {
406
+ portal: Zone.portalAddress(zoneId),
407
+ senderTag,
408
+ timeoutMs: settlementTimeoutMs(challenge.expires, zone.settlementTimeoutMs),
409
+ to: recipient,
410
+ });
411
+ }
412
+ catch (cause) {
413
+ throw new Errors.ZoneSettlementPendingError({
414
+ cause,
415
+ challengeId: challenge.id,
416
+ senderTag,
417
+ zoneId,
418
+ });
419
+ }
420
+ pending.delete(key);
421
+ return Credential.serialize({
422
+ challenge,
423
+ payload: { hash, senderTag, type: 'zone', zoneId },
424
+ });
425
+ }
426
+ /**
427
+ * Throws on Zone options that would fail only after an irreversible withdrawal:
428
+ * a `settlementTimeoutMs` that is not a finite, non-negative number, or a
429
+ * `zoneId` that names a different Zone from the one `zone.client` serves.
430
+ */
431
+ function assertZone(zone) {
432
+ const timeoutMs = zone?.settlementTimeoutMs;
433
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0))
434
+ throw new Error('`zone.settlementTimeoutMs` must be a finite, non-negative number.');
435
+ const chainId = zone?.client.chain?.id;
436
+ if (zone?.zoneId === undefined || chainId === undefined)
437
+ return;
438
+ // Only chain IDs that follow a known Zone scheme name their Zone.
439
+ if (Zone.parentChainIdFromChainId(chainId) === undefined)
440
+ return;
441
+ const derived = Zone.idFromChainId(chainId);
442
+ if (derived !== zone.zoneId)
443
+ throw new Error(`zone.zoneId ${zone.zoneId} does not match Zone ${derived} of the zone.client chain ${chainId}.`);
444
+ }
445
+ /** Time the client allows for sending a Zone withdrawal before it waits for settlement. */
446
+ const zoneSubmissionMarginMs = 15_000;
447
+ /**
448
+ * Returns whether a Zone withdrawal can be sent and waited on for up to
449
+ * `settlementTimeoutMs` before `expires`.
450
+ */
451
+ function settlesBeforeExpiry(expires, settlementTimeoutMs) {
452
+ if (!expires)
453
+ return true;
454
+ const timeoutMs = settlementTimeoutMs ?? Zone.defaultSettlementTimeoutMs;
455
+ return new Date(expires).getTime() - Date.now() > timeoutMs + zoneSubmissionMarginMs;
456
+ }
457
+ /** Returns how long to wait for settlement: `settlementTimeoutMs`, cut short at `expires`. */
458
+ function settlementTimeoutMs(expires, settlementTimeoutMs) {
459
+ const timeoutMs = settlementTimeoutMs ?? Zone.defaultSettlementTimeoutMs;
460
+ if (!expires)
461
+ return timeoutMs;
462
+ return Math.max(0, Math.min(timeoutMs, new Date(expires).getTime() - Date.now()));
463
+ }
464
+ /** Keys a Zone withdrawal by its whole Challenge. Unlike `Challenge.serialize`, it accepts any field value. */
465
+ function zoneWithdrawalKey(challenge) {
466
+ return JSON.stringify(challenge);
467
+ }
468
+ /** How long to look for a withdrawal's receipt on the Zone after its send response fails. */
469
+ const zoneReceiptRecoveryTimeoutMs = 15_000;
470
+ /** Requests a Zone withdrawal of the challenge amount to its recipient, bound to the challenge by its memo. */
471
+ async function requestZoneWithdrawal(options, state) {
472
+ const { challenge, clientId, zone } = options;
473
+ const request = challenge.request;
474
+ const account = zone.client.account;
475
+ if (!account)
476
+ throw new Error('`zone.client` requires an account.');
477
+ const amount = BigInt(request.amount);
478
+ const currency = request.currency;
479
+ const recipient = request.recipient;
480
+ const memo = Attribution.encode({
481
+ challengeId: challenge.id,
482
+ clientId,
483
+ serverId: challenge.realm,
484
+ });
485
+ // The outbox pulls the amount plus its withdrawal fee.
486
+ const fee = await Actions.zone.getWithdrawalFee(zone.client);
487
+ const [, withdrawal] = Actions.zone.requestWithdrawal.calls({
488
+ amount,
489
+ // A bounced payout refunds the payer, not the payee's Zone address.
490
+ fallbackRecipient: account.address,
491
+ memo,
492
+ to: recipient,
493
+ token: currency,
494
+ });
495
+ // Prepare and sign first: a failure there sent nothing, so the challenge may be retried.
496
+ const prepared = await prepareTransactionRequest(zone.client, {
497
+ account,
498
+ calls: [
499
+ Actions.token.approve.call({
500
+ amount: amount + fee,
501
+ spender: Addresses.zoneOutbox,
502
+ token: currency,
503
+ }),
504
+ withdrawal,
505
+ ],
506
+ });
507
+ const serializedTransaction = await signTransaction(zone.client, prepared);
508
+ state.sent = true;
509
+ // The signed transaction fixes its hash, so a lost send response can still be recovered from the Zone.
510
+ const hash = keccak256(serializedTransaction);
511
+ const receipt = await sendRawTransactionSync(zone.client, { serializedTransaction }).catch((cause) => waitForTransactionReceipt(zone.client, {
512
+ hash,
513
+ timeout: zoneReceiptRecoveryTimeoutMs,
514
+ }).catch(() => {
515
+ const error = new Error(`Zone withdrawal outcome is unknown: transaction ${hash} may have been sent. Retrying this challenge does not withdraw again.`);
516
+ // Defined manually (non-enumerable, like native `cause`) so consumers on pre-ES2022 libs still typecheck.
517
+ Object.defineProperty(error, 'cause', { configurable: true, value: cause, writable: true });
518
+ throw error;
519
+ }));
520
+ // A reverted withdrawal moved nothing, so the challenge may be retried.
521
+ if (receipt.status === 'reverted') {
522
+ state.sent = false;
523
+ throw new Error(`Zone withdrawal transaction ${receipt.transactionHash} reverted.`);
524
+ }
525
+ const [requested] = parseEventLogs({
526
+ abi: Abis.zoneOutbox,
527
+ eventName: 'WithdrawalRequested',
528
+ logs: receipt.logs,
529
+ });
530
+ if (!requested)
531
+ throw new Error('Zone withdrawal request did not emit `WithdrawalRequested`.');
532
+ return Zone.senderTag({
533
+ fallbackNonce: requested.args.fallbackNonce,
534
+ sender: requested.args.sender,
535
+ transactionHash: receipt.transactionHash,
536
+ });
537
+ }
340
538
  /** Parses `maxAmount` to base units, rejecting values that cannot be represented exactly. */
341
539
  function parseMaxAmount(maxAmount, decimals) {
342
540
  if (maxAmount === undefined)
@@ -43,6 +43,11 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
43
43
  }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
44
44
  signature: import("zod/mini").ZodMiniString<string>;
45
45
  type: import("zod/mini").ZodMiniLiteral<"proof">;
46
+ }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
47
+ hash: import("zod/mini").ZodMiniString<string>;
48
+ senderTag: import("zod/mini").ZodMiniString<string>;
49
+ type: import("zod/mini").ZodMiniLiteral<"zone">;
50
+ zoneId: import("zod/mini").ZodMiniNumber<number>;
46
51
  }, import("zod/v4/core").$strip>], "type">;
47
52
  };
48
53
  request: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniObject<{
@@ -65,6 +70,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
65
70
  pull: "pull";
66
71
  push: "push";
67
72
  }>>>;
73
+ zones: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniUnion<readonly [import("zod/mini").ZodMiniLiteral<true>, import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniNumber<number>>]>>;
68
74
  }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniTransform<{
69
75
  currency: string;
70
76
  description?: string | undefined;
@@ -81,6 +87,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
81
87
  amount: string;
82
88
  }[] | undefined;
83
89
  supportedModes?: ("pull" | "push")[] | undefined;
90
+ zones?: true | number[] | undefined;
84
91
  } | undefined;
85
92
  }, {
86
93
  machineTokenEnabled?: boolean | undefined;
@@ -99,6 +106,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
99
106
  recipient: `0x${string}`;
100
107
  }[] | undefined;
101
108
  supportedModes?: ("pull" | "push")[] | undefined;
109
+ zones?: true | number[] | undefined;
102
110
  }>>;
103
111
  };
104
112
  readonly createCredential: import("../../Method.js").CreateCredentialFn<{
@@ -115,6 +123,11 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
115
123
  }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
116
124
  signature: import("zod/mini").ZodMiniString<string>;
117
125
  type: import("zod/mini").ZodMiniLiteral<"proof">;
126
+ }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
127
+ hash: import("zod/mini").ZodMiniString<string>;
128
+ senderTag: import("zod/mini").ZodMiniString<string>;
129
+ type: import("zod/mini").ZodMiniLiteral<"zone">;
130
+ zoneId: import("zod/mini").ZodMiniNumber<number>;
118
131
  }, import("zod/v4/core").$strip>], "type">;
119
132
  };
120
133
  request: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniObject<{
@@ -137,6 +150,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
137
150
  pull: "pull";
138
151
  push: "push";
139
152
  }>>>;
153
+ zones: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniUnion<readonly [import("zod/mini").ZodMiniLiteral<true>, import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniNumber<number>>]>>;
140
154
  }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniTransform<{
141
155
  currency: string;
142
156
  description?: string | undefined;
@@ -153,6 +167,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
153
167
  amount: string;
154
168
  }[] | undefined;
155
169
  supportedModes?: ("pull" | "push")[] | undefined;
170
+ zones?: true | number[] | undefined;
156
171
  } | undefined;
157
172
  }, {
158
173
  machineTokenEnabled?: boolean | undefined;
@@ -171,6 +186,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
171
186
  recipient: `0x${string}`;
172
187
  }[] | undefined;
173
188
  supportedModes?: ("pull" | "push")[] | undefined;
189
+ zones?: true | number[] | undefined;
174
190
  }>>;
175
191
  };
176
192
  }, {
@@ -124,31 +124,6 @@ export type PreflightSponsorship = {
124
124
  * Returns `undefined` when the envelope's fee payer cannot sign locally.
125
125
  */
126
126
  export declare function signFeePayerSimulation(transaction: SponsoredTransaction): Promise<Signature.Signature | undefined>;
127
- /**
128
- * Runs the sender and final-envelope simulations around sponsorship.
129
- *
130
- * First, it simulates the sender's calls at the transaction's exact gas limit
131
- * with zero fee fields (see {@link simulationTransaction}). This is the
132
- * gas-bounded check at the broadcast limit, and needs no fee-token balance.
133
- * Next, `complete` resolves the sponsor and produces the co-signed transaction.
134
- * Finally, it simulates that transaction with its concrete fee payer, fee token
135
- * and fee payer signature, so the node bills the sponsor rather than the sender
136
- * and checks the sponsor's fee balance.
137
- *
138
- * A sponsored `eth_call` charges more gas than the mined transaction uses. A
139
- * local fee payer therefore signs a simulation-only copy whose gas is raised by
140
- * up to {@link sponsoredSimulationGasBuffer}, clamped so the copy stays within
141
- * the same fee payer `policy` (`maxGas`, `maxTotalFee`) as the real
142
- * transaction. The copy is never returned or broadcast. When the final
143
- * simulation runs without the full buffer (hosted signatures cover the client's
144
- * gas and cannot be padded, or the padding was clamped), a failure that is only
145
- * out of gas is tolerated, since the sender simulation already bounded gas at
146
- * the broadcast limit. Every other failure, such as an insufficient fee balance
147
- * or a revert, is rethrown.
148
- *
149
- * `complete` runs only after sender-context execution succeeds, so a reverting
150
- * transaction never reaches a local signer or hosted fee-payer.
151
- */
152
127
  export declare function preflightSponsorship<sponsorship extends PreflightSponsorship>(parameters: {
153
128
  complete: () => Promise<sponsorship>;
154
129
  /** The fee payer policy the completed transaction was checked against. */
@@ -2,6 +2,7 @@ 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';
5
+ import { PaymentSimulationError } from '../../Errors.js';
5
6
  import { escrowAbi as legacyEscrowAbi } from '../legacy/session/escrow.abi.js';
6
7
  import { escrowAbi } from '../session/precompile/escrow.abi.js';
7
8
  import * as TempoAddress_internal from './address.js';
@@ -243,6 +244,10 @@ export function sponsoredSimulationTransaction(transaction, options) {
243
244
  feePayer,
244
245
  feePayerSignature: feePayerSignature ? Signature.toRpc(feePayerSignature) : undefined,
245
246
  ...(feeToken !== undefined ? { feeToken } : {}),
247
+ // A deserialized envelope omits a zero priority fee, which the signed envelope encodes as 0.
248
+ // The node rebuilds the fee payer payload from the request, so an absent fee fails signature
249
+ // recovery (or takes the node's own value) for a hosted sponsor's signature.
250
+ maxPriorityFeePerGas: transaction.maxPriorityFeePerGas ?? 0n,
246
251
  signature: undefined,
247
252
  };
248
253
  }
@@ -287,14 +292,48 @@ export async function signFeePayerSimulation(transaction) {
287
292
  * gas and cannot be padded, or the padding was clamped), a failure that is only
288
293
  * out of gas is tolerated, since the sender simulation already bounded gas at
289
294
  * the broadcast limit. Every other failure, such as an insufficient fee balance
290
- * or a revert, is rethrown.
295
+ * or a revert, is thrown as a `PaymentSimulationError` that names the failing
296
+ * phase, sender, fee payer and payment token, with the node's error as `cause`.
291
297
  *
292
298
  * `complete` runs only after sender-context execution succeeds, so a reverting
293
299
  * transaction never reaches a local signer or hosted fee-payer.
294
300
  */
301
+ /** viem's errors for an RPC request that never got a node's answer, matched by name so wrapped copies match. */
302
+ const transportErrorNames = new Set([
303
+ 'HttpRequestError',
304
+ 'SocketClosedError',
305
+ 'TimeoutError',
306
+ 'WebSocketRequestError',
307
+ ]);
308
+ /** Whether an error or its cause chain is a transport failure, rather than the node rejecting the call. */
309
+ function isTransportError(error) {
310
+ for (let current = error, depth = 0; current && depth < 10; depth++) {
311
+ if (current instanceof Error && transportErrorNames.has(current.name))
312
+ return true;
313
+ current = current.cause;
314
+ }
315
+ return false;
316
+ }
295
317
  export async function preflightSponsorship(parameters) {
296
318
  const { complete, policy, simulate, transaction } = parameters;
297
- await simulate(simulationTransaction(transaction, { feePayer: true }));
319
+ const paymentCall = transaction.calls.length === 1 ? transaction.calls[0] : undefined;
320
+ const paymentToken = paymentCall?.data?.startsWith(Selectors.transfer) ||
321
+ paymentCall?.data?.startsWith(Selectors.transferWithMemo)
322
+ ? paymentCall.to
323
+ : undefined;
324
+ try {
325
+ await simulate(simulationTransaction(transaction, { feePayer: true }));
326
+ }
327
+ catch (cause) {
328
+ if (isTransportError(cause))
329
+ throw cause;
330
+ throw new PaymentSimulationError({
331
+ cause,
332
+ paymentToken,
333
+ phase: 'sender',
334
+ sender: transaction.from,
335
+ });
336
+ }
298
337
  const completed = await complete();
299
338
  const { gas } = completed.transaction;
300
339
  const simulated = await (async () => {
@@ -325,8 +364,16 @@ export async function preflightSponsorship(parameters) {
325
364
  const unpadded = gas !== undefined &&
326
365
  simulatedGas !== undefined &&
327
366
  simulatedGas < gas + sponsoredSimulationGasBuffer;
328
- if (!unpadded || !isOutOfGasError(error))
367
+ if (isTransportError(error))
329
368
  throw error;
369
+ if (!unpadded || !isOutOfGasError(error))
370
+ throw new PaymentSimulationError({
371
+ cause: error,
372
+ feePayer: TempoAddress.resolve(completed.feePayer),
373
+ paymentToken,
374
+ phase: 'sponsored',
375
+ sender: transaction.from,
376
+ });
330
377
  }
331
378
  return completed;
332
379
  }