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.
- package/CHANGELOG.md +65 -0
- package/dist/Errors.d.ts +94 -0
- package/dist/Errors.js +83 -0
- package/dist/Store.d.ts +413 -0
- package/dist/Store.js +837 -0
- package/dist/cli/cli.js +141 -136
- package/dist/cli/internal.js +28 -6
- package/dist/cli/plugins/index.js +16 -11
- package/dist/cli/sessions/Manager.js +12 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +6 -0
- package/dist/cli/validate/challenge.d.ts +4 -0
- package/dist/cli/validate/challenge.js +9 -6
- package/dist/client/Mppx.d.ts +1 -1
- package/dist/client/Mppx.js +7 -3
- package/dist/client/internal/Fetch.d.ts +1 -1
- package/dist/client/internal/Fetch.js +9 -0
- package/dist/client/internal/MethodResponse.d.ts +8 -0
- package/dist/client/internal/MethodResponse.js +9 -0
- package/dist/client/internal/ProofRealm.d.ts +3 -3
- package/dist/client/internal/ProofRealm.js +4 -4
- package/dist/client/internal/protocols/Mcp.d.ts +4 -0
- package/dist/client/internal/protocols/Mcp.js +27 -14
- package/dist/cloudflare/ChannelStore.d.ts +97 -0
- package/dist/cloudflare/ChannelStore.js +304 -0
- package/dist/cloudflare/Channels.d.ts +92 -0
- package/dist/cloudflare/Channels.js +325 -0
- package/dist/cloudflare/index.d.ts +5 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/identity/Client.js +7 -1
- package/dist/internal/sse.d.ts +21 -0
- package/dist/internal/sse.js +82 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.d.ts +2 -2
- package/dist/mcp/client/McpClient.js +5 -2
- package/dist/proxy/Proxy.d.ts +2 -0
- package/dist/proxy/Proxy.js +31 -6
- package/dist/proxy/internal/Replay.js +1 -1
- package/dist/server/Mppx.js +5 -4
- package/dist/server/Transport.js +1 -1
- package/dist/server/internal/html/config.d.ts +7 -1
- package/dist/server/internal/html/config.js +4 -0
- package/dist/stripe/internal/types.d.ts +82 -6
- package/dist/stripe/server/Charge.js +1 -2
- package/dist/stripe/server/Methods.d.ts +2 -0
- package/dist/stripe/server/Methods.js +2 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/record-payment.d.ts +49 -0
- package/dist/stripe/server/internal/record-payment.js +75 -18
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.js +18 -2
- package/dist/tempo/client/Charge.d.ts +32 -1
- package/dist/tempo/client/Charge.js +201 -3
- package/dist/tempo/client/Methods.d.ts +16 -0
- package/dist/tempo/internal/fee-payer.d.ts +64 -14
- package/dist/tempo/internal/fee-payer.js +231 -20
- package/dist/tempo/internal/zone.d.ts +79 -0
- package/dist/tempo/internal/zone.js +133 -0
- package/dist/tempo/legacy/session/Chain.js +2 -0
- package/dist/tempo/server/Charge.d.ts +7 -3
- package/dist/tempo/server/Charge.js +100 -6
- package/dist/tempo/server/Methods.d.ts +40 -0
- package/dist/tempo/server/Subscription.js +1 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +6 -0
- package/dist/tempo/server/internal/transport.js +21 -21
- package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/client/CredentialState.js +2 -0
- package/dist/tempo/session/client/Session.d.ts +11 -1
- package/dist/tempo/session/client/Session.js +39 -6
- package/dist/tempo/session/client/SessionManager.d.ts +1 -1
- package/dist/tempo/session/client/SessionManager.js +8 -4
- package/dist/tempo/session/client/Transports.d.ts +2 -0
- package/dist/tempo/session/client/Transports.js +4 -1
- package/dist/tempo/session/precompile/Chain.js +1 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +14 -0
- package/dist/tempo/session/server/ChannelStore.js +10 -3
- package/dist/tempo/session/server/CredentialVerification.js +1 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
- package/dist/tempo/session/server/MeteredStream.js +6 -1
- package/dist/tempo/session/server/Sse.d.ts +59 -0
- package/dist/tempo/session/server/Sse.js +175 -10
- package/dist/validation/core.js +2 -0
- package/dist/zod.d.ts +2 -1
- package/dist/zod.js +1 -1
- package/package.json +5 -3
- package/dist/stripe/server/internal/html/types.d.ts +0 -2
- 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
|
|
179
|
-
* fee
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
65
|
-
*
|
|
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>;
|