mppx 0.13.4 → 0.13.6

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 (73) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/Credential.d.ts +3 -0
  3. package/dist/Credential.js +9 -0
  4. package/dist/Errors.d.ts +63 -2
  5. package/dist/Errors.js +52 -2
  6. package/dist/Method.d.ts +10 -0
  7. package/dist/cli/cli.js +92 -92
  8. package/dist/cli/plugins/evm.js +2 -2
  9. package/dist/cli/plugins/index.js +12 -12
  10. package/dist/client/Mppx.d.ts +2 -0
  11. package/dist/client/Mppx.js +8 -10
  12. package/dist/client/internal/Fetch.js +107 -8
  13. package/dist/client/internal/MethodChallenge.d.ts +2 -0
  14. package/dist/client/internal/MethodChallenge.js +4 -0
  15. package/dist/client/internal/MethodResponse.d.ts +6 -0
  16. package/dist/client/internal/MethodResponse.js +16 -0
  17. package/dist/client/internal/protocols/Mcp.js +36 -5
  18. package/dist/evm/server/Charge.js +3 -1
  19. package/dist/internal/version.d.ts +2 -2
  20. package/dist/internal/version.js +1 -1
  21. package/dist/internal/warnOnce.d.ts +3 -0
  22. package/dist/internal/warnOnce.js +9 -0
  23. package/dist/server/Mppx.d.ts +5 -0
  24. package/dist/server/Mppx.js +45 -18
  25. package/dist/server/Transport.js +10 -0
  26. package/dist/stripe/server/Methods.js +4 -1
  27. package/dist/tempo/Attribution.d.ts +24 -9
  28. package/dist/tempo/Attribution.js +25 -5
  29. package/dist/tempo/client/Charge.d.ts +116 -10
  30. package/dist/tempo/client/Charge.js +117 -22
  31. package/dist/tempo/client/Methods.d.ts +88 -9
  32. package/dist/tempo/index.d.ts +1 -0
  33. package/dist/tempo/index.js +1 -0
  34. package/dist/tempo/internal/auto-swap.d.ts +10 -0
  35. package/dist/tempo/internal/auto-swap.js +35 -8
  36. package/dist/tempo/internal/fee-payer.d.ts +8 -0
  37. package/dist/tempo/internal/fee-payer.js +20 -8
  38. package/dist/tempo/legacy/session/Chain.js +8 -2
  39. package/dist/tempo/server/Charge.d.ts +11 -0
  40. package/dist/tempo/server/Charge.js +160 -29
  41. package/dist/tempo/server/Methods.js +28 -5
  42. package/dist/tempo/server/Relay.js +6 -22
  43. package/dist/tempo/server/Subscription.js +5 -1
  44. package/dist/tempo/server/internal/fallback-store.d.ts +9 -0
  45. package/dist/tempo/server/internal/fallback-store.js +13 -0
  46. package/dist/tempo/server/internal/html.gen.js +1 -1
  47. package/dist/tempo/server/internal/transport.d.ts +3 -0
  48. package/dist/tempo/server/internal/transport.js +5 -1
  49. package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
  50. package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
  51. package/dist/tempo/session/client/ChannelStore.js +47 -0
  52. package/dist/tempo/session/client/CredentialState.js +8 -3
  53. package/dist/tempo/session/client/Session.js +50 -33
  54. package/dist/tempo/session/client/SessionManager.js +186 -65
  55. package/dist/tempo/session/client/Transports.d.ts +6 -0
  56. package/dist/tempo/session/client/Transports.js +52 -7
  57. package/dist/tempo/session/precompile/Chain.d.ts +2 -0
  58. package/dist/tempo/session/precompile/Chain.js +8 -3
  59. package/dist/tempo/session/server/ChannelStore.d.ts +34 -0
  60. package/dist/tempo/session/server/ChannelStore.js +69 -9
  61. package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
  62. package/dist/tempo/session/server/CredentialVerification.js +220 -48
  63. package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
  64. package/dist/tempo/session/server/MeteredStream.js +10 -26
  65. package/dist/tempo/session/server/Session.d.ts +29 -2
  66. package/dist/tempo/session/server/Session.js +124 -44
  67. package/dist/tempo/session/server/Settlement.d.ts +9 -0
  68. package/dist/tempo/session/server/Settlement.js +23 -3
  69. package/dist/tempo/session/server/Sse.d.ts +4 -3
  70. package/dist/tempo/session/server/Sse.js +3 -3
  71. package/dist/tempo/session/server/Ws.d.ts +9 -1
  72. package/dist/tempo/session/server/Ws.js +1 -0
  73. package/package.json +1 -1
@@ -298,12 +298,22 @@ export async function readProblemDetail(response) {
298
298
  return body;
299
299
  }
300
300
  }
301
+ /** A dispatched deposit may have executed despite a failed or cancelled response. */
302
+ export class UncertainFundingError extends Error {
303
+ constructor(reason) {
304
+ const detail = reason instanceof Error || reason instanceof DOMException ? reason.message : String(reason);
305
+ super(`${detail}; payment outcome unknown`);
306
+ this.name = 'UncertainFundingError';
307
+ }
308
+ }
301
309
  /** Posts a top-up management credential and returns its receipt, when present. */
302
310
  export async function postTopUp(parameters) {
303
311
  const { channel, channelId } = parameters;
304
312
  if (!channel?.descriptor || channel.channelId !== channelId) {
305
313
  throw new Error('Cannot top up session: no local channel descriptor available.');
306
314
  }
315
+ if (channel.uncertainTopUp)
316
+ throw new Error('Session funding outcome is unknown; reconcile the pending payment before another top-up.');
307
317
  const response = await postManagementCredential({
308
318
  challenge: parameters.challenge,
309
319
  context: {
@@ -316,6 +326,12 @@ export async function postTopUp(parameters) {
316
326
  fetch: parameters.fetch,
317
327
  input: parameters.input,
318
328
  signal: parameters.signal,
329
+ }).catch(async (error) => {
330
+ if (error instanceof UncertainFundingError) {
331
+ channel.uncertainTopUp = true;
332
+ await parameters.onUncertainFunding?.(channel);
333
+ }
334
+ throw error;
319
335
  });
320
336
  if (!response.ok)
321
337
  throw new Error(`Top-up POST failed with status ${response.status}`);
@@ -324,13 +340,42 @@ export async function postTopUp(parameters) {
324
340
  }
325
341
  /** Posts a management credential and retries once with a refreshed TIP-1034 challenge. */
326
342
  async function postManagementCredential(parameters) {
327
- const post = async (challenge) => parameters.fetch(managementInput(parameters.input), {
328
- method: 'POST',
329
- ...(parameters.signal ? { signal: parameters.signal } : {}),
330
- headers: {
331
- [Constants.Headers.authorization]: await parameters.createSessionCredential(challenge, parameters.context),
332
- },
333
- });
343
+ const post = async (challenge) => {
344
+ parameters.signal?.throwIfAborted();
345
+ const authorization = await parameters.createSessionCredential(challenge, parameters.context);
346
+ parameters.signal?.throwIfAborted();
347
+ const controller = new AbortController();
348
+ const abort = () => controller.abort(parameters.signal?.reason);
349
+ parameters.signal?.addEventListener('abort', abort, { once: true });
350
+ const timeout = setTimeout(() => controller.abort(new DOMException('Session management request timed out; payment outcome unknown', 'TimeoutError')), 30_000);
351
+ let onAbort;
352
+ const aborted = new Promise((_, reject) => {
353
+ onAbort = () => reject(controller.signal.reason);
354
+ controller.signal.addEventListener('abort', onAbort, { once: true });
355
+ });
356
+ try {
357
+ // Custom fetch implementations may ignore abort. Do not let their late
358
+ // response update funding state or trigger a refreshed-challenge retry.
359
+ return await Promise.race([
360
+ parameters.fetch(managementInput(parameters.input), {
361
+ method: 'POST',
362
+ signal: controller.signal,
363
+ headers: { [Constants.Headers.authorization]: authorization },
364
+ }),
365
+ aborted,
366
+ ]);
367
+ }
368
+ catch (error) {
369
+ if (parameters.context.action === 'topUp')
370
+ throw new UncertainFundingError(error);
371
+ throw error;
372
+ }
373
+ finally {
374
+ clearTimeout(timeout);
375
+ parameters.signal?.removeEventListener('abort', abort);
376
+ controller.signal.removeEventListener('abort', onAbort);
377
+ }
378
+ };
334
379
  const response = await post(parameters.challenge);
335
380
  if (response.status !== 402)
336
381
  return response;
@@ -155,6 +155,8 @@ export type ReadbackRetryOptions = {
155
155
  retries?: number | undefined;
156
156
  /** Delay between attempts, in milliseconds. @default 250 */
157
157
  delayMs?: number | undefined;
158
+ /** Whether a failed attempt may be retried. @default every failure */
159
+ shouldRetry?: ((error: unknown) => boolean) | undefined;
158
160
  };
159
161
  /**
160
162
  * Retry an on-chain readback that is pinned to the block containing a just-sent
@@ -169,7 +169,7 @@ export async function getChannelStatesBatch(client, channelIds, escrow = tip20Ch
169
169
  * verification even though the open transaction succeeded.
170
170
  */
171
171
  export async function readbackWithRetry(read, options = {}) {
172
- const { retries = 5, delayMs = 250 } = options;
172
+ const { retries = 5, delayMs = 250, shouldRetry = () => true } = options;
173
173
  let lastError;
174
174
  for (let attempt = 0; attempt <= retries; attempt++) {
175
175
  try {
@@ -177,6 +177,8 @@ export async function readbackWithRetry(read, options = {}) {
177
177
  }
178
178
  catch (error) {
179
179
  lastError = error;
180
+ if (!shouldRetry(error))
181
+ break;
180
182
  if (attempt < retries)
181
183
  await new Promise((resolve) => setTimeout(resolve, delayMs));
182
184
  }
@@ -261,8 +263,8 @@ function validateAutoSwapPrefix(parameters) {
261
263
  fail('approval amount does not match swap maximum input');
262
264
  if (!isAddressEqual(tokenOut, currency))
263
265
  fail('output token does not match channel currency');
264
- if (swapAmountOut !== amountOut)
265
- fail('output amount does not match channel deposit');
266
+ if (swapAmountOut <= 0n || swapAmountOut > amountOut)
267
+ fail('output amount must be positive and not exceed channel deposit');
266
268
  if (isAddressEqual(tokenIn, tokenOut))
267
269
  fail('input and output tokens must differ');
268
270
  const canonicalApprove = encodeFunctionData({
@@ -442,6 +444,7 @@ export async function sendCredentialTransaction(parameters) {
442
444
  // execution as the sender. It intentionally omits fee fields here; the
443
445
  // transport is responsible for validating its final sponsored envelope.
444
446
  await simulateTempoTransaction(client, FeePayer.simulationTransaction(transaction, { feePayer: true }));
447
+ FeePayer.assertRemainingValidity({ transaction, details });
445
448
  const txHash = await sendTransaction(client, serializedTransaction);
446
449
  return waitForSuccessfulReceipt(client, txHash);
447
450
  }
@@ -465,7 +468,9 @@ export async function sendCredentialTransaction(parameters) {
465
468
  return { feePayer: feePayer.address, transaction: sponsored };
466
469
  },
467
470
  });
471
+ FeePayer.assertRemainingValidity({ transaction: completed.transaction, details });
468
472
  const serialized = await signTempoTransaction(client, completed.transaction);
473
+ FeePayer.assertRemainingValidity({ transaction: completed.transaction, details });
469
474
  const receipt = await sendRawTransactionSync(client, {
470
475
  serializedTransaction: serialized,
471
476
  });
@@ -1,5 +1,6 @@
1
1
  import type { Address, Hex } from 'viem';
2
2
  import type * as Challenge from '../../../Challenge.js';
3
+ import { InsufficientBalanceError } from '../../../Errors.js';
3
4
  import type * as Store from '../../../Store.js';
4
5
  import type * as Chain from '../precompile/Chain.js';
5
6
  import type * as PrecompileChannel from '../precompile/Channel.js';
@@ -109,6 +110,8 @@ export type AcceptVoucherStateUpdateParameters = {
109
110
  channelState: Chain.ChannelState;
110
111
  /** Existing channel state being updated. */
111
112
  current: State | null;
113
+ /** When `channelState` was read from chain; omitted when it came from the store. */
114
+ verifiedAt?: number | undefined;
112
115
  /** Verified signed voucher. */
113
116
  voucher: SessionSignedVoucher;
114
117
  };
@@ -193,8 +196,14 @@ export type VerifyAndAcceptVoucherParameters = {
193
196
  requireAdvance?: boolean | undefined;
194
197
  /** Server channel store. */
195
198
  store: ChannelStore;
199
+ /** When `channelState` was read from chain; omitted when it came from the store. */
200
+ verifiedAt?: number | undefined;
196
201
  /** Signed cumulative voucher to verify and accept. */
197
202
  voucher: SignedVoucher;
203
+ /** Request price to charge in the same store update that accepts the voucher. */
204
+ chargeAmount?: bigint | undefined;
205
+ /** Called with the stored channel after `chargeAmount` was charged. */
206
+ onCharged?: ((channel: State) => void) | undefined;
198
207
  };
199
208
  /** Inputs for validating a cumulative voucher without recording it. */
200
209
  export type ValidateVoucherParameters = Omit<VerifyAndAcceptVoucherParameters, 'challenge' | 'store'>;
@@ -257,6 +266,8 @@ export interface BaseState {
257
266
  token: Address;
258
267
  /** Number of charge operations (API requests) fulfilled in the current session. */
259
268
  units: number;
269
+ /** Unix milliseconds when the channel's on-chain deposit, settlement and close state were last read. */
270
+ onChainVerifiedAt?: number | undefined;
260
271
  /** Uncommitted stream charges shared across workers. */
261
272
  streamReservations?: Record<string, {
262
273
  amount: bigint;
@@ -316,6 +327,29 @@ export declare function finalizeClosedChannelState(parameters: FinalizeClosedCha
316
327
  export declare function loadPrecompileChannel(parameters: LoadPrecompileChannelParameters): Promise<StoredPrecompileChannel>;
317
328
  /** Verifies a cumulative voucher without changing persisted channel state. */
318
329
  export declare function validateVoucher(parameters: ValidateVoucherParameters): Promise<VoucherValidation>;
330
+ /**
331
+ * Checks a cumulative voucher's amounts against channel state, without its signature, so the
332
+ * check can run inside the store update that accepts it.
333
+ */
334
+ export declare function assertVoucherAmounts(parameters: {
335
+ channel: Pick<State, 'highestVoucherAmount'>;
336
+ channelState: Chain.ChannelState;
337
+ minVoucherDelta: bigint;
338
+ voucher: Pick<SignedVoucher, 'cumulativeAmount'>;
339
+ }): VoucherValidation;
340
+ /**
341
+ * Charges `chargeAmount` to a channel state that just accepted a voucher, for the same store
342
+ * update. A charge the available balance cannot cover leaves the accepted state uncharged.
343
+ */
344
+ export declare function planVoucherAcceptance(parameters: {
345
+ accepted: State;
346
+ chargeAmount?: bigint | undefined;
347
+ }): {
348
+ channel: State;
349
+ charged: boolean;
350
+ };
351
+ /** Reports a request charge that the channel's available balance could not cover. */
352
+ export declare function insufficientBalanceForCharge(channel: State, chargeAmount: bigint): InsufficientBalanceError;
319
353
  /** Verifies a cumulative voucher and returns a session receipt after store reconciliation. */
320
354
  export declare function verifyAndAcceptVoucher(parameters: VerifyAndAcceptVoucherParameters): Promise<SessionReceipt>;
321
355
  /**
@@ -1,4 +1,4 @@
1
- import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, DeltaTooSmallError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
1
+ import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, DeltaTooSmallError, InsufficientBalanceError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
2
2
  import { createSessionReceipt } from '../precompile/Protocol.js';
3
3
  import { uint96 } from '../precompile/Protocol.js';
4
4
  import * as Voucher from '../precompile/Voucher.js';
@@ -100,14 +100,19 @@ export function topUpChannelState(parameters) {
100
100
  }
101
101
  /** Merges an accepted voucher into persisted channel state after signature and on-chain checks pass. */
102
102
  export function acceptVoucherStateUpdate(parameters) {
103
- const { channelState, current, voucher } = parameters;
103
+ const { channelState, current, verifiedAt, voucher } = parameters;
104
104
  if (!current)
105
105
  throw new ChannelNotFoundError({ reason: 'channel not found' });
106
106
  if (current.finalized)
107
107
  throw new ChannelClosedError({ reason: 'channel is finalized' });
108
108
  if (current.closeRequestedAt !== 0n)
109
109
  throw new ChannelClosedError({ reason: 'channel has a pending close request' });
110
- const onChain = mergeActiveOnChainState(current, channelState);
110
+ const onChain = {
111
+ ...mergeActiveOnChainState(current, channelState),
112
+ ...(verifiedAt === undefined
113
+ ? {}
114
+ : { onChainVerifiedAt: Math.max(verifiedAt, current.onChainVerifiedAt ?? 0) }),
115
+ };
111
116
  if (voucher.cumulativeAmount <= current.highestVoucherAmount) {
112
117
  return { ...current, ...onChain };
113
118
  }
@@ -239,13 +244,58 @@ export async function validateVoucher(parameters) {
239
244
  });
240
245
  return { alreadyAccepted: false };
241
246
  }
247
+ /**
248
+ * Checks a cumulative voucher's amounts against channel state, without its signature, so the
249
+ * check can run inside the store update that accepts it.
250
+ */
251
+ export function assertVoucherAmounts(parameters) {
252
+ const { channel, channelState, minVoucherDelta, voucher } = parameters;
253
+ validateChannelState(channelState);
254
+ if (voucher.cumulativeAmount > channelState.deposit)
255
+ throw new AmountExceedsDepositError({ reason: 'voucher amount exceeds on-chain deposit' });
256
+ if (voucher.cumulativeAmount < channel.highestVoucherAmount)
257
+ throw new VerificationFailedError({
258
+ reason: 'voucher cumulativeAmount must be strictly greater than highest accepted voucher',
259
+ });
260
+ if (voucher.cumulativeAmount <= channelState.settled)
261
+ throw new VerificationFailedError({
262
+ reason: 'voucher cumulativeAmount is below on-chain settled amount',
263
+ });
264
+ if (voucher.cumulativeAmount === channel.highestVoucherAmount)
265
+ return { alreadyAccepted: true };
266
+ const delta = voucher.cumulativeAmount - channel.highestVoucherAmount;
267
+ if (delta < minVoucherDelta)
268
+ throw new DeltaTooSmallError({
269
+ reason: `voucher delta ${delta} below minimum ${minVoucherDelta}`,
270
+ });
271
+ return { alreadyAccepted: false };
272
+ }
273
+ /**
274
+ * Charges `chargeAmount` to a channel state that just accepted a voucher, for the same store
275
+ * update. A charge the available balance cannot cover leaves the accepted state uncharged.
276
+ */
277
+ export function planVoucherAcceptance(parameters) {
278
+ const { accepted, chargeAmount } = parameters;
279
+ if (chargeAmount === undefined)
280
+ return { channel: accepted, charged: false };
281
+ const deduction = planDeduction(accepted, chargeAmount);
282
+ if (deduction.op !== 'set')
283
+ return { channel: accepted, charged: false };
284
+ return { channel: deduction.value, charged: true };
285
+ }
286
+ /** Reports a request charge that the channel's available balance could not cover. */
287
+ export function insufficientBalanceForCharge(channel, chargeAmount) {
288
+ return new InsufficientBalanceError({
289
+ reason: `requested ${chargeAmount}, available ${channel.highestVoucherAmount - channel.spent - reservedStreamAmount(channel)}`,
290
+ });
291
+ }
242
292
  /** Verifies a cumulative voucher and returns a session receipt after store reconciliation. */
243
293
  export async function verifyAndAcceptVoucher(parameters) {
244
- const { store, challenge, channel, voucher, channelState, requireAdvance } = parameters;
294
+ const { store, challenge, channel, voucher, channelState, requireAdvance, verifiedAt, chargeAmount, } = parameters;
245
295
  const validation = await validateVoucher(parameters);
246
- if (validation.alreadyAccepted) {
247
- if (requireAdvance)
248
- throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
296
+ if (validation.alreadyAccepted && requireAdvance)
297
+ throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
298
+ if (validation.alreadyAccepted && chargeAmount === undefined)
249
299
  return createSessionReceipt({
250
300
  challengeId: challenge.id,
251
301
  channelId: voucher.channelId,
@@ -253,16 +303,26 @@ export async function verifyAndAcceptVoucher(parameters) {
253
303
  spent: channel.spent,
254
304
  units: channel.units,
255
305
  });
256
- }
306
+ let charged = false;
257
307
  const updated = await store.updateChannel(voucher.channelId, (current) => {
258
308
  if (requireAdvance &&
259
309
  current !== null &&
260
310
  voucher.cumulativeAmount <= current.highestVoucherAmount)
261
311
  throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
262
- return acceptVoucherStateUpdate({ channelState, current, voucher });
312
+ const acceptance = planVoucherAcceptance({
313
+ accepted: acceptVoucherStateUpdate({ channelState, current, verifiedAt, voucher }),
314
+ chargeAmount,
315
+ });
316
+ charged = acceptance.charged;
317
+ return acceptance.channel;
263
318
  });
264
319
  if (!updated)
265
320
  throw new ChannelNotFoundError({ reason: 'channel not found' });
321
+ if (chargeAmount !== undefined) {
322
+ if (!charged)
323
+ throw insufficientBalanceForCharge(updated, chargeAmount);
324
+ parameters.onCharged?.(updated);
325
+ }
266
326
  return createSessionReceipt({
267
327
  challengeId: challenge.id,
268
328
  channelId: voucher.channelId,
@@ -85,6 +85,12 @@ export type BroadcastCredentialPayloadParameters = {
85
85
  requireVoucherAdvance?: boolean | undefined;
86
86
  /** Server-side channel store. */
87
87
  store: ChannelStore.ChannelStore;
88
+ /** Maximum age of verified on-chain state a voucher may be accepted against. See `session.Parameters`. @default 0 */
89
+ voucherStateMaxAge?: number | undefined;
90
+ /** Request price a voucher credential pays in the same store update that accepts it. */
91
+ chargeAmount?: bigint | undefined;
92
+ /** Called with the stored channel after `chargeAmount` was charged. */
93
+ onCharged?: ((channel: ChannelStore.State) => void) | undefined;
88
94
  };
89
95
  /** @deprecated Use {@link BroadcastCredentialPayloadParameters}. */
90
96
  export type VerifyCredentialPayloadParameters = BroadcastCredentialPayloadParameters;
@@ -106,7 +112,7 @@ export type VoucherCredentialActionParameters = BroadcastCredentialActionParamet
106
112
  /** Inputs for broadcasting and settling a cooperative close credential. */
107
113
  export type CloseCredentialActionParameters = BroadcastCredentialActionParameters<'close'>;
108
114
  /** Inputs for validating a session credential without applying its state transition. */
109
- export type ValidateCredentialPayloadParameters = Pick<BroadcastCredentialPayloadParameters, 'account' | 'channelStateTtl' | 'chainId' | 'client' | 'credentialSource' | 'escrow' | 'expectedOperator' | 'feePayer' | 'feePayerPolicy' | 'feeToken' | 'lastOnChainVerified' | 'minVoucherDelta' | 'payload' | 'store' | 'challenge'> & {
115
+ export type ValidateCredentialPayloadParameters = Pick<BroadcastCredentialPayloadParameters, 'account' | 'channelStateTtl' | 'chainId' | 'client' | 'credentialSource' | 'escrow' | 'expectedOperator' | 'feePayer' | 'feePayerPolicy' | 'feeToken' | 'lastOnChainVerified' | 'minVoucherDelta' | 'payload' | 'store' | 'challenge' | 'voucherStateMaxAge'> & {
110
116
  /** Allows terminal broadcast to recover accepted funds before rejecting a pending close. */
111
117
  allowPendingClose?: boolean | undefined;
112
118
  };