mppx 0.17.1 → 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 (89) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/Errors.d.ts +94 -0
  3. package/dist/Errors.js +83 -0
  4. package/dist/Store.d.ts +413 -0
  5. package/dist/Store.js +837 -0
  6. package/dist/cli/cli.js +141 -136
  7. package/dist/cli/internal.js +28 -6
  8. package/dist/cli/plugins/index.js +16 -11
  9. package/dist/cli/sessions/Manager.js +12 -1
  10. package/dist/cli/utils.d.ts +2 -0
  11. package/dist/cli/utils.js +6 -0
  12. package/dist/cli/validate/challenge.d.ts +4 -0
  13. package/dist/cli/validate/challenge.js +9 -6
  14. package/dist/client/Mppx.d.ts +1 -1
  15. package/dist/client/Mppx.js +7 -3
  16. package/dist/client/internal/Fetch.d.ts +1 -1
  17. package/dist/client/internal/Fetch.js +9 -0
  18. package/dist/client/internal/MethodResponse.d.ts +8 -0
  19. package/dist/client/internal/MethodResponse.js +9 -0
  20. package/dist/client/internal/ProofRealm.d.ts +3 -3
  21. package/dist/client/internal/ProofRealm.js +4 -4
  22. package/dist/client/internal/protocols/Mcp.d.ts +4 -0
  23. package/dist/client/internal/protocols/Mcp.js +27 -14
  24. package/dist/cloudflare/ChannelStore.d.ts +97 -0
  25. package/dist/cloudflare/ChannelStore.js +304 -0
  26. package/dist/cloudflare/Channels.d.ts +92 -0
  27. package/dist/cloudflare/Channels.js +325 -0
  28. package/dist/cloudflare/index.d.ts +5 -0
  29. package/dist/cloudflare/index.js +5 -0
  30. package/dist/identity/Client.js +7 -1
  31. package/dist/internal/sse.d.ts +21 -0
  32. package/dist/internal/sse.js +82 -0
  33. package/dist/internal/version.d.ts +2 -2
  34. package/dist/internal/version.js +1 -1
  35. package/dist/mcp/client/McpClient.d.ts +2 -2
  36. package/dist/mcp/client/McpClient.js +5 -2
  37. package/dist/proxy/Proxy.d.ts +2 -0
  38. package/dist/proxy/Proxy.js +31 -6
  39. package/dist/proxy/internal/Replay.js +1 -1
  40. package/dist/server/Mppx.js +5 -4
  41. package/dist/server/Transport.js +1 -1
  42. package/dist/server/internal/html/config.d.ts +7 -1
  43. package/dist/server/internal/html/config.js +4 -0
  44. package/dist/stripe/internal/types.d.ts +82 -6
  45. package/dist/stripe/server/Charge.js +1 -2
  46. package/dist/stripe/server/Methods.d.ts +2 -0
  47. package/dist/stripe/server/Methods.js +2 -1
  48. package/dist/stripe/server/internal/html.gen.js +1 -1
  49. package/dist/stripe/server/internal/record-payment.d.ts +49 -0
  50. package/dist/stripe/server/internal/record-payment.js +75 -18
  51. package/dist/tempo/Methods.d.ts +8 -0
  52. package/dist/tempo/Methods.js +18 -2
  53. package/dist/tempo/client/Charge.d.ts +32 -1
  54. package/dist/tempo/client/Charge.js +201 -3
  55. package/dist/tempo/client/Methods.d.ts +16 -0
  56. package/dist/tempo/internal/fee-payer.d.ts +64 -14
  57. package/dist/tempo/internal/fee-payer.js +231 -20
  58. package/dist/tempo/internal/zone.d.ts +79 -0
  59. package/dist/tempo/internal/zone.js +133 -0
  60. package/dist/tempo/legacy/session/Chain.js +2 -0
  61. package/dist/tempo/server/Charge.d.ts +7 -3
  62. package/dist/tempo/server/Charge.js +100 -6
  63. package/dist/tempo/server/Methods.d.ts +40 -0
  64. package/dist/tempo/server/Subscription.js +1 -0
  65. package/dist/tempo/server/internal/html.gen.js +1 -1
  66. package/dist/tempo/server/internal/transport.d.ts +6 -0
  67. package/dist/tempo/server/internal/transport.js +21 -21
  68. package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
  69. package/dist/tempo/session/client/CredentialState.js +2 -0
  70. package/dist/tempo/session/client/Session.d.ts +11 -1
  71. package/dist/tempo/session/client/Session.js +39 -6
  72. package/dist/tempo/session/client/SessionManager.d.ts +1 -1
  73. package/dist/tempo/session/client/SessionManager.js +8 -4
  74. package/dist/tempo/session/client/Transports.d.ts +2 -0
  75. package/dist/tempo/session/client/Transports.js +4 -1
  76. package/dist/tempo/session/precompile/Chain.js +1 -0
  77. package/dist/tempo/session/server/ChannelStore.d.ts +14 -0
  78. package/dist/tempo/session/server/ChannelStore.js +10 -3
  79. package/dist/tempo/session/server/CredentialVerification.js +1 -1
  80. package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
  81. package/dist/tempo/session/server/MeteredStream.js +6 -1
  82. package/dist/tempo/session/server/Sse.d.ts +59 -0
  83. package/dist/tempo/session/server/Sse.js +175 -10
  84. package/dist/validation/core.js +2 -0
  85. package/dist/zod.d.ts +2 -1
  86. package/dist/zod.js +1 -1
  87. package/package.json +5 -3
  88. package/dist/stripe/server/internal/html/types.d.ts +0 -2
  89. package/dist/stripe/server/internal/html/types.js +0 -2
@@ -1,7 +1,8 @@
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';
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';
@@ -163,6 +164,12 @@ export async function fillHostedFeePayerTransaction(parameters) {
163
164
  })();
164
165
  return {
165
166
  feePayer,
167
+ // `Signature.fromRpc` rejects a yParity of 0 that arrives without `v`.
168
+ feePayerSignature: Signature.from({
169
+ r: BigInt(feePayerSignature.r),
170
+ s: BigInt(feePayerSignature.s),
171
+ yParity: feePayerSignature.yParity,
172
+ }),
166
173
  feeToken,
167
174
  serializedTransaction: await Transaction.serialize({
168
175
  ...transaction,
@@ -175,17 +182,36 @@ export async function fillHostedFeePayerTransaction(parameters) {
175
182
  /**
176
183
  * Returns a transaction shape suitable for pre-broadcast simulation.
177
184
  *
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.
185
+ * Sponsored transactions are first simulated as calls from the sender, without
186
+ * signatures, fee payer or fee token, and with zero fee fields: Tempo nodes
187
+ * (v1.18+) skip the fee-token balance check for a zero-fee `eth_call`, so the
188
+ * sender's fee balance is irrelevant. The call keeps the transaction's exact
189
+ * gas limit and the fields that set its intrinsic gas (nonce, nonce key,
190
+ * validity window, key authorization, signature type), so it is the gas-bounded
191
+ * check at the broadcast limit: a transaction whose gas is too low fails here
192
+ * with out of gas instead of reverting onchain at the sponsor's expense.
193
+ * Transferred value and call-level balances are still checked by the RPC.
182
194
  */
183
195
  export function simulationTransaction(transaction, options) {
184
- if (options.feePayer)
196
+ if (options.feePayer) {
197
+ const keyType = senderKeyType(transaction.signature);
185
198
  return {
186
199
  account: transaction.from,
187
200
  calls: transaction.calls,
201
+ ...(transaction.accessList?.length ? { accessList: transaction.accessList } : {}),
202
+ ...(transaction.gas !== undefined ? { gas: transaction.gas } : {}),
203
+ ...(transaction.keyAuthorization !== undefined
204
+ ? { keyAuthorization: transaction.keyAuthorization }
205
+ : {}),
206
+ ...(keyType ? { keyType } : {}),
207
+ maxFeePerGas: 0n,
208
+ maxPriorityFeePerGas: 0n,
209
+ ...(transaction.nonce !== undefined ? { nonce: transaction.nonce } : {}),
210
+ ...(transaction.nonceKey !== undefined ? { nonceKey: transaction.nonceKey } : {}),
211
+ ...(transaction.validAfter !== undefined ? { validAfter: transaction.validAfter } : {}),
212
+ ...(transaction.validBefore !== undefined ? { validBefore: transaction.validBefore } : {}),
188
213
  };
214
+ }
189
215
  return {
190
216
  ...transaction,
191
217
  account: transaction.from,
@@ -193,43 +219,228 @@ export function simulationTransaction(transaction, options) {
193
219
  feePayerSignature: undefined,
194
220
  };
195
221
  }
222
+ /** The key type hint for a sender signature whose verification costs more than secp256k1. */
223
+ function senderKeyType(signature) {
224
+ const envelope = signature;
225
+ const type = envelope?.type === 'keychain' ? envelope.inner?.type : envelope?.type;
226
+ return type === 'p256' || type === 'webAuthn' ? type : undefined;
227
+ }
196
228
  /**
197
229
  * Returns the final fee-sponsored transaction shape for pre-broadcast
198
- * simulation. RPC `eth_call` does not carry either transaction signature.
230
+ * simulation.
231
+ *
232
+ * Tempo nodes recover the fee payer of an `eth_call` only from
233
+ * `feePayerSignature`; a bare `feePayer` address is ignored and the sender is
234
+ * billed instead. Pass the sponsor's signature so the node charges the fee
235
+ * token to the sponsor, not to a sender that may not hold it. The sender's own
236
+ * signature is never sent.
199
237
  */
200
238
  export function sponsoredSimulationTransaction(transaction, options) {
201
- const { feePayer, feeToken } = options;
239
+ const { feePayer, feePayerSignature, feeToken } = options;
202
240
  return {
203
241
  ...transaction,
204
242
  account: transaction.from,
205
243
  calls: transaction.calls,
206
244
  feePayer,
207
- feePayerSignature: undefined,
245
+ feePayerSignature: feePayerSignature ? Signature.toRpc(feePayerSignature) : undefined,
208
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,
209
251
  signature: undefined,
210
252
  };
211
253
  }
212
254
  /**
213
- * Runs execution-only sender and final-envelope simulations around sponsorship.
255
+ * Signs the fee-payer payload of a sponsored envelope with a local fee-payer
256
+ * account, the same payload `Transaction.serialize` signs before broadcast.
257
+ * Returns `undefined` when the envelope's fee payer cannot sign locally.
258
+ */
259
+ export async function signFeePayerSimulation(transaction) {
260
+ const { account: _account, feePayer, ...rest } = transaction;
261
+ if (typeof feePayer !== 'object' || feePayer === null)
262
+ return undefined;
263
+ const sign = feePayer.sign;
264
+ if (typeof sign !== 'function' || !rest.from)
265
+ return undefined;
266
+ const payload = TxEnvelopeTempo.getFeePayerSignPayload(TxEnvelopeTempo.from({
267
+ ...rest,
268
+ chainId: Number(rest.chainId),
269
+ feePayerSignature: undefined,
270
+ signature: undefined,
271
+ type: 'tempo',
272
+ }), { sender: rest.from });
273
+ return Signature.from(await sign({ hash: payload }));
274
+ }
275
+ /**
276
+ * Runs the sender and final-envelope simulations around sponsorship.
277
+ *
278
+ * First, it simulates the sender's calls at the transaction's exact gas limit
279
+ * with zero fee fields (see {@link simulationTransaction}). This is the
280
+ * gas-bounded check at the broadcast limit, and needs no fee-token balance.
281
+ * Next, `complete` resolves the sponsor and produces the co-signed transaction.
282
+ * Finally, it simulates that transaction with its concrete fee payer, fee token
283
+ * and fee payer signature, so the node bills the sponsor rather than the sender
284
+ * and checks the sponsor's fee balance.
214
285
  *
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.
286
+ * A sponsored `eth_call` charges more gas than the mined transaction uses. A
287
+ * local fee payer therefore signs a simulation-only copy whose gas is raised by
288
+ * up to {@link sponsoredSimulationGasBuffer}, clamped so the copy stays within
289
+ * the same fee payer `policy` (`maxGas`, `maxTotalFee`) as the real
290
+ * transaction. The copy is never returned or broadcast. When the final
291
+ * simulation runs without the full buffer (hosted signatures cover the client's
292
+ * gas and cannot be padded, or the padding was clamped), a failure that is only
293
+ * out of gas is tolerated, since the sender simulation already bounded gas at
294
+ * the broadcast limit. Every other failure, such as an insufficient fee balance
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`.
219
297
  *
220
298
  * `complete` runs only after sender-context execution succeeds, so a reverting
221
299
  * transaction never reaches a local signer or hosted fee-payer.
222
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
+ }
223
317
  export async function preflightSponsorship(parameters) {
224
- const { complete, simulate, transaction } = parameters;
225
- await simulate(simulationTransaction(transaction, { feePayer: true }));
318
+ const { complete, policy, simulate, transaction } = parameters;
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
+ }
226
337
  const completed = await complete();
227
- await simulate(sponsoredSimulationTransaction(completed.transaction, {
228
- feePayer: completed.feePayer,
229
- feeToken: completed.feeToken,
230
- }));
338
+ const { gas } = completed.transaction;
339
+ const simulated = await (async () => {
340
+ if (completed.feePayerSignature)
341
+ return { feePayerSignature: completed.feePayerSignature, transaction: completed.transaction };
342
+ // The signature binds the gas limit, so a local fee payer signs a simulation-only copy
343
+ // with headroom for the node's sponsored `eth_call` overhead. The broadcast keeps its gas.
344
+ const padded = gas === undefined
345
+ ? completed.transaction
346
+ : {
347
+ ...completed.transaction,
348
+ gas: sponsoredSimulationGas(completed.transaction, policy),
349
+ };
350
+ const feePayerSignature = await signFeePayerSimulation(padded);
351
+ return feePayerSignature
352
+ ? { feePayerSignature, transaction: padded }
353
+ : { feePayerSignature: undefined, transaction: completed.transaction };
354
+ })();
355
+ const simulatedGas = simulated.transaction.gas;
356
+ try {
357
+ await simulate(sponsoredSimulationTransaction(simulated.transaction, {
358
+ feePayer: completed.feePayer,
359
+ feePayerSignature: simulated.feePayerSignature,
360
+ feeToken: completed.feeToken,
361
+ }));
362
+ }
363
+ catch (error) {
364
+ const unpadded = gas !== undefined &&
365
+ simulatedGas !== undefined &&
366
+ simulatedGas < gas + sponsoredSimulationGasBuffer;
367
+ if (isTransportError(error))
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
+ });
377
+ }
231
378
  return completed;
232
379
  }
380
+ /**
381
+ * Returns the gas limit of a local fee payer's simulation-only copy: the
382
+ * transaction's gas plus {@link sponsoredSimulationGasBuffer}, clamped to the
383
+ * fee payer policy's `maxGas` and to `maxTotalFee / maxFeePerGas`, so the
384
+ * signed copy never exceeds a cap the real transaction is held to.
385
+ */
386
+ export function sponsoredSimulationGas(transaction, policyOverrides) {
387
+ const { chainId, gas, maxFeePerGas } = transaction;
388
+ if (gas === undefined)
389
+ throw new FeePayerValidationError('fee-sponsored transaction must declare gas', {});
390
+ const policy = getPolicy(Number(chainId), policyOverrides);
391
+ let cap = policy.maxGas;
392
+ if (maxFeePerGas !== undefined && maxFeePerGas > 0n) {
393
+ const feeCap = policy.maxTotalFee / maxFeePerGas;
394
+ if (feeCap < cap)
395
+ cap = feeCap;
396
+ }
397
+ if (gas > cap)
398
+ throw new FeePayerValidationError('fee-sponsored transaction gas exceeds sponsor policy', {
399
+ gas: gas.toString(),
400
+ });
401
+ const padded = gas + sponsoredSimulationGasBuffer;
402
+ return padded < cap ? padded : cap;
403
+ }
404
+ /**
405
+ * Returns true when a simulation failed only because it ran out of gas.
406
+ *
407
+ * Tempo nodes report out of gas as `out of gas: …` (code -32003, shared with
408
+ * `insufficient fee token balance`) or `intrinsic gas too low` (code -32000),
409
+ * so this reads the node's own message rather than the code. Only the terminal
410
+ * RPC failure counts: the deepest `details` in the cause chain (viem's
411
+ * `RpcRequestError` carries the node's message there), or the deepest message
412
+ * when no error has details. Wrapper messages are ignored, the match is anchored
413
+ * to the start of the node's message, and an error carrying revert data is never
414
+ * treated as gas exhaustion, so a revert reason or wrapper text that merely
415
+ * mentions gas cannot hide another failure.
416
+ */
417
+ export function isOutOfGasError(error) {
418
+ let terminal;
419
+ let fallback;
420
+ for (let current = error, depth = 0; current && depth < 10; depth++) {
421
+ const { cause, data, details, message } = current;
422
+ if (typeof data === 'string' && data !== '0x')
423
+ return false;
424
+ if (typeof details === 'string')
425
+ terminal = details;
426
+ if (typeof message === 'string')
427
+ fallback = message;
428
+ current = cause;
429
+ }
430
+ const nodeMessage = (terminal ?? fallback)?.trim();
431
+ if (!nodeMessage)
432
+ return false;
433
+ return /^(?:out of gas(?::|$)|intrinsic gas too low(?::|$))/i.test(nodeMessage);
434
+ }
435
+ /**
436
+ * Extra gas a local fee payer signs into its simulation-only envelope. Tempo's sponsored
437
+ * `eth_call` charges more gas than the same transaction uses when mined (about 6,000 for a
438
+ * session top-up on v1.18.1), so simulating at the signed limit would reject transactions
439
+ * that succeed onchain. The padding is clamped to the fee payer policy; hosted signatures
440
+ * cover the client's gas and are simulated as signed. In both cases the gas bound at the
441
+ * broadcast limit comes from the sender simulation.
442
+ */
443
+ export const sponsoredSimulationGasBuffer = 10000n;
233
444
  /**
234
445
  * maxTotalFee must be high enough to cover `transferWithMemo` and
235
446
  * swap transactions at peak gas prices. Bumped from 0.01 ETH in #327.
@@ -0,0 +1,79 @@
1
+ import { type Address, type Client, type Hex, type TransactionReceipt } from 'viem';
2
+ /** Default time a payer waits for a Zone withdrawal to settle on Tempo. */
3
+ export declare const defaultSettlementTimeoutMs = 60000;
4
+ /** A withdrawal as `ZonePortal.processWithdrawals` receives it on Tempo. */
5
+ export type Withdrawal = {
6
+ token: Address;
7
+ senderTag: Hex;
8
+ to: Address;
9
+ amount: bigint;
10
+ memo: Hex;
11
+ gasLimit: bigint;
12
+ fallbackNonce: bigint;
13
+ callbackData: Hex;
14
+ encryptedSender: Hex;
15
+ };
16
+ /** A `ZonePortal.WithdrawalProcessed` event. */
17
+ export type ProcessedEvent = {
18
+ to: Address;
19
+ senderTag: Hex;
20
+ token: Address;
21
+ amount: bigint;
22
+ callbackSuccess: boolean;
23
+ };
24
+ /** Zones a challenge accepts: a list of Zone IDs, or `true` for any Zone. */
25
+ export type Accepted = readonly number[] | true;
26
+ /** Returns whether a value is a valid Zone ID: a positive integer below the Zone chain ID ranges. */
27
+ export declare function isId(zoneId: number): boolean;
28
+ /** Returns whether a challenge's `zones` accepts withdrawals from a Zone. */
29
+ export declare function isAccepted(zones: Accepted | undefined, zoneId: number): boolean;
30
+ /** Returns the Tempo portal address of a Zone. */
31
+ export declare function portalAddress(zoneId: number): Address;
32
+ /**
33
+ * Derives a Zone ID from a Zone chain ID.
34
+ *
35
+ * Zones on Tempo mainnet and Moderato use reserved bases (`421700000` and
36
+ * `1424310000`). Zones on any other parent chain use `(parentChainId << 32) | zoneId`.
37
+ */
38
+ export declare function idFromChainId(chainId: number): number;
39
+ /**
40
+ * Derives the parent Tempo chain ID of a Zone chain ID, or `undefined` when
41
+ * the chain ID doesn't follow a known Zone scheme.
42
+ */
43
+ export declare function parentChainIdFromChainId(chainId: number): number | undefined;
44
+ /**
45
+ * Derives a withdrawal's sender tag, which identifies it in the
46
+ * `WithdrawalProcessed` event on Tempo.
47
+ */
48
+ export declare function senderTag(parameters: {
49
+ fallbackNonce: bigint;
50
+ sender: Address;
51
+ transactionHash: Hex;
52
+ }): Hex;
53
+ /** Returns the `WithdrawalProcessed` event a portal emitted for a sender tag. */
54
+ export declare function findProcessedEvent(receipt: Pick<TransactionReceipt, 'logs'>, parameters: {
55
+ portal: Address;
56
+ senderTag: Hex;
57
+ }): ProcessedEvent | undefined;
58
+ /**
59
+ * Finds the withdrawal for a sender tag in the calldata of a settlement
60
+ * transaction's top-level `processWithdrawals` calls to the portal.
61
+ */
62
+ export declare function findWithdrawal(calls: readonly {
63
+ to?: Address | null | undefined;
64
+ data?: Hex | undefined;
65
+ }[], parameters: {
66
+ portal: Address;
67
+ senderTag: Hex;
68
+ }): Withdrawal | undefined;
69
+ /**
70
+ * Waits until a portal emits `WithdrawalProcessed` for a sender tag and
71
+ * returns the Tempo transaction hash that settled it.
72
+ */
73
+ export declare function waitForSettlement(client: Client, parameters: {
74
+ portal: Address;
75
+ senderTag: Hex;
76
+ to?: Address | undefined;
77
+ timeoutMs?: number | undefined;
78
+ }): Promise<Hex>;
79
+ //# sourceMappingURL=zone.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { AbiParameters, Hash } from 'ox';
2
+ import { decodeFunctionData, getAbiItem, parseEventLogs, } from 'viem';
3
+ import { getBlockNumber, getLogs } from 'viem/actions';
4
+ import { Abis, Addresses } from 'viem/tempo';
5
+ import * as TempoAddress from './address.js';
6
+ /** Default time a payer waits for a Zone withdrawal to settle on Tempo. */
7
+ export const defaultSettlementTimeoutMs = 60_000;
8
+ /** Number of Tempo blocks searched back for a settled Zone withdrawal. */
9
+ const lookbackBlocks = 7200n;
10
+ /** Time between polls for a settled Zone withdrawal. */
11
+ const pollingIntervalMs = 500;
12
+ const withdrawalProcessed = getAbiItem({ abi: Abis.zonePortal, name: 'WithdrawalProcessed' });
13
+ /**
14
+ * Zone IDs from here up read as Zone chain IDs in viem's portal lookup, which
15
+ * would map them to another Zone's portal.
16
+ */
17
+ const maxIdExclusive = 421_700_000;
18
+ /** Returns whether a value is a valid Zone ID: a positive integer below the Zone chain ID ranges. */
19
+ export function isId(zoneId) {
20
+ return Number.isInteger(zoneId) && zoneId > 0 && zoneId < maxIdExclusive;
21
+ }
22
+ /** Returns whether a challenge's `zones` accepts withdrawals from a Zone. */
23
+ export function isAccepted(zones, zoneId) {
24
+ if (!isId(zoneId))
25
+ return false;
26
+ return zones === true || (zones?.includes(zoneId) ?? false);
27
+ }
28
+ /** Returns the Tempo portal address of a Zone. */
29
+ export function portalAddress(zoneId) {
30
+ return Addresses.zonePortal(zoneId);
31
+ }
32
+ /**
33
+ * Derives a Zone ID from a Zone chain ID.
34
+ *
35
+ * Zones on Tempo mainnet and Moderato use reserved bases (`421700000` and
36
+ * `1424310000`). Zones on any other parent chain use `(parentChainId << 32) | zoneId`.
37
+ */
38
+ export function idFromChainId(chainId) {
39
+ if (chainId >= 2 ** 32)
40
+ return chainId % 2 ** 32;
41
+ if (chainId > 1_424_310_000 && chainId < 1_424_320_000)
42
+ return chainId - 1_424_310_000;
43
+ if (chainId > 421_700_000 && chainId < 421_710_000)
44
+ return chainId - 421_700_000;
45
+ throw new Error(`Cannot derive a Zone ID from chain ID ${chainId}. Pass \`zoneId\`.`);
46
+ }
47
+ /**
48
+ * Derives the parent Tempo chain ID of a Zone chain ID, or `undefined` when
49
+ * the chain ID doesn't follow a known Zone scheme.
50
+ */
51
+ export function parentChainIdFromChainId(chainId) {
52
+ if (chainId >= 2 ** 32)
53
+ return Math.floor(chainId / 2 ** 32);
54
+ if (chainId > 1_424_310_000 && chainId < 1_424_320_000)
55
+ return 42_431;
56
+ if (chainId > 421_700_000 && chainId < 421_710_000)
57
+ return 4_217;
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Derives a withdrawal's sender tag, which identifies it in the
62
+ * `WithdrawalProcessed` event on Tempo.
63
+ */
64
+ export function senderTag(parameters) {
65
+ return Hash.keccak256(AbiParameters.encodePacked(['address', 'bytes32', 'uint64'], [parameters.sender, parameters.transactionHash, parameters.fallbackNonce]));
66
+ }
67
+ /** Returns the `WithdrawalProcessed` event a portal emitted for a sender tag. */
68
+ export function findProcessedEvent(receipt, parameters) {
69
+ const logs = parseEventLogs({ abi: [withdrawalProcessed], logs: receipt.logs });
70
+ const log = logs.find((log) => TempoAddress.isEqual(log.address, parameters.portal) &&
71
+ log.args.senderTag.toLowerCase() === parameters.senderTag.toLowerCase());
72
+ return log?.args;
73
+ }
74
+ /**
75
+ * Finds the withdrawal for a sender tag in the calldata of a settlement
76
+ * transaction's top-level `processWithdrawals` calls to the portal.
77
+ */
78
+ export function findWithdrawal(calls, parameters) {
79
+ for (const call of calls) {
80
+ if (!call.to || !call.data || !TempoAddress.isEqual(call.to, parameters.portal))
81
+ continue;
82
+ let decoded;
83
+ try {
84
+ decoded = decodeFunctionData({ abi: Abis.zonePortal, data: call.data });
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ if (decoded.functionName !== 'processWithdrawals')
90
+ continue;
91
+ const [withdrawals] = decoded.args;
92
+ const withdrawal = withdrawals.find((withdrawal) => withdrawal.senderTag.toLowerCase() === parameters.senderTag.toLowerCase());
93
+ if (withdrawal)
94
+ return withdrawal;
95
+ }
96
+ return undefined;
97
+ }
98
+ /**
99
+ * Waits until a portal emits `WithdrawalProcessed` for a sender tag and
100
+ * returns the Tempo transaction hash that settled it.
101
+ */
102
+ export async function waitForSettlement(client, parameters) {
103
+ const { timeoutMs = defaultSettlementTimeoutMs } = parameters;
104
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
105
+ throw new Error('Zone settlement timeout must be a finite, non-negative number.');
106
+ let latest = await getBlockNumber(client, { cacheTime: 0 });
107
+ // Scan the lookback window once, then only blocks added since the previous poll.
108
+ let fromBlock = latest > lookbackBlocks ? latest - lookbackBlocks : 0n;
109
+ const deadline = Date.now() + timeoutMs;
110
+ while (true) {
111
+ if (fromBlock <= latest) {
112
+ const logs = await getLogs(client, {
113
+ address: parameters.portal,
114
+ event: withdrawalProcessed,
115
+ args: {
116
+ senderTag: parameters.senderTag,
117
+ ...(parameters.to ? { to: parameters.to } : {}),
118
+ },
119
+ fromBlock,
120
+ toBlock: latest,
121
+ });
122
+ const hash = logs[0]?.transactionHash;
123
+ if (hash)
124
+ return hash;
125
+ fromBlock = latest + 1n;
126
+ }
127
+ if (Date.now() + pollingIntervalMs > deadline)
128
+ throw new Error(`Zone withdrawal ${parameters.senderTag} did not settle on Tempo within ${timeoutMs}ms.`);
129
+ await new Promise((resolve) => setTimeout(resolve, pollingIntervalMs));
130
+ latest = await getBlockNumber(client, { cacheTime: 0 });
131
+ }
132
+ }
133
+ //# sourceMappingURL=zone.js.map
@@ -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() {
@@ -61,8 +61,10 @@ export declare namespace charge {
61
61
  * two thirds of this budget. If it fails with an unknown outcome (e.g. a
62
62
  * timeout, a lost connection, or "already known"), mppx polls for the
63
63
  * receipt until the deadline, or until 2 seconds after the transaction's
64
- * `validBefore` followed by one final lookup, before surfacing the broadcast
65
- * error. Definitive rejections fail immediately.
64
+ * `validBefore` followed by one final lookup, before throwing
65
+ * `Errors.BroadcastOutcomeUnknownError` with the transaction hash and
66
+ * `validBefore`, and the broadcast error as `cause`. Definitive rejections
67
+ * fail immediately with the RPC error.
66
68
  * @default 30_000
67
69
  */
68
70
  confirmationTimeoutMs?: number | undefined;
@@ -143,7 +145,9 @@ export declare namespace charge {
143
145
  * report `status: 'success'` based on simulation alone — if the
144
146
  * transaction reverts on-chain after broadcast (e.g. due to a state
145
147
  * change between simulation and inclusion), the receipt will not reflect
146
- * the failure.
148
+ * the failure. An ambiguous broadcast failure (a timeout, a lost
149
+ * connection, a 5xx) throws `Errors.BroadcastOutcomeUnknownError` without
150
+ * polling for a receipt.
147
151
  */
148
152
  waitForConfirmation?: boolean | undefined;
149
153
  } & Client.getResolver.Parameters & Account.resolve.Parameters & Defaults & Method.ComposableHooks<typeof Methods.charge>;