mppx 0.3.8 → 0.3.11

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 (61) hide show
  1. package/README.md +3 -3
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +2 -0
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/Errors.d.ts +0 -2
  6. package/dist/Errors.d.ts.map +1 -1
  7. package/dist/Errors.js +1 -3
  8. package/dist/Errors.js.map +1 -1
  9. package/dist/internal/constantTimeEqual.d.ts.map +1 -1
  10. package/dist/internal/constantTimeEqual.js +4 -6
  11. package/dist/internal/constantTimeEqual.js.map +1 -1
  12. package/dist/internal/env.d.ts +2 -2
  13. package/dist/internal/env.d.ts.map +1 -1
  14. package/dist/internal/env.js +1 -2
  15. package/dist/internal/env.js.map +1 -1
  16. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  17. package/dist/middlewares/internal/mppx.js +6 -2
  18. package/dist/middlewares/internal/mppx.js.map +1 -1
  19. package/dist/server/Mppx.d.ts +13 -3
  20. package/dist/server/Mppx.d.ts.map +1 -1
  21. package/dist/server/Mppx.js +46 -3
  22. package/dist/server/Mppx.js.map +1 -1
  23. package/dist/tempo/internal/simulate.d.ts +21 -0
  24. package/dist/tempo/internal/simulate.d.ts.map +1 -0
  25. package/dist/tempo/internal/simulate.js +31 -0
  26. package/dist/tempo/internal/simulate.js.map +1 -0
  27. package/dist/tempo/server/Charge.d.ts +12 -0
  28. package/dist/tempo/server/Charge.d.ts.map +1 -1
  29. package/dist/tempo/server/Charge.js +28 -6
  30. package/dist/tempo/server/Charge.js.map +1 -1
  31. package/dist/tempo/server/Session.d.ts +18 -1
  32. package/dist/tempo/server/Session.d.ts.map +1 -1
  33. package/dist/tempo/server/Session.js +66 -46
  34. package/dist/tempo/server/Session.js.map +1 -1
  35. package/dist/tempo/session/Chain.d.ts +5 -2
  36. package/dist/tempo/session/Chain.d.ts.map +1 -1
  37. package/dist/tempo/session/Chain.js +78 -10
  38. package/dist/tempo/session/Chain.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/Challenge.ts +2 -0
  41. package/src/Errors.test.ts +43 -18
  42. package/src/Errors.ts +1 -4
  43. package/src/client/Mppx.test.ts +1 -0
  44. package/src/internal/constantTimeEqual.ts +5 -4
  45. package/src/internal/env.test.ts +2 -2
  46. package/src/internal/env.ts +4 -5
  47. package/src/middlewares/express.test.ts +5 -0
  48. package/src/middlewares/hono.test.ts +5 -0
  49. package/src/middlewares/internal/mppx.ts +5 -2
  50. package/src/middlewares/nextjs.test.ts +5 -0
  51. package/src/proxy/Proxy.test.ts +3 -0
  52. package/src/proxy/services/openai.test.ts +3 -0
  53. package/src/server/Mppx.test.ts +93 -2
  54. package/src/server/Mppx.ts +81 -6
  55. package/src/tempo/internal/simulate.ts +49 -0
  56. package/src/tempo/server/Charge.test.ts +62 -0
  57. package/src/tempo/server/Charge.ts +44 -6
  58. package/src/tempo/server/Session.test.ts +51 -2
  59. package/src/tempo/server/Session.ts +97 -38
  60. package/src/tempo/session/Chain.test.ts +190 -0
  61. package/src/tempo/session/Chain.ts +109 -5
@@ -524,6 +524,68 @@ describe('tempo', () => {
524
524
  })
525
525
  })
526
526
 
527
+ describe('intent: charge; type: transaction; waitForConfirmation: false', () => {
528
+ test('returns receipt without waiting for confirmation', async () => {
529
+ const serverNoWait = Mppx_server.create({
530
+ methods: [
531
+ tempo_server.charge({
532
+ getClient() {
533
+ return client
534
+ },
535
+ currency: asset,
536
+ account: accounts[0],
537
+ waitForConfirmation: false,
538
+ }),
539
+ ],
540
+ realm,
541
+ secretKey,
542
+ })
543
+
544
+ const mppx = Mppx_client.create({
545
+ polyfill: false,
546
+ methods: [
547
+ tempo_client({
548
+ account: accounts[1],
549
+ getClient() {
550
+ return client
551
+ },
552
+ }),
553
+ ],
554
+ })
555
+
556
+ const httpServer = await Http.createServer(async (req, res) => {
557
+ const result = await Mppx_server.toNodeListener(
558
+ serverNoWait.charge({
559
+ amount: '1',
560
+ currency: asset,
561
+ recipient: accounts[0].address,
562
+ }),
563
+ )(req, res)
564
+ if (result.status === 402) return
565
+ res.end('OK')
566
+ })
567
+
568
+ const response = await fetch(httpServer.url)
569
+ expect(response.status).toBe(402)
570
+
571
+ const credential = await mppx.createCredential(response)
572
+
573
+ {
574
+ const response = await fetch(httpServer.url, {
575
+ headers: { Authorization: credential },
576
+ })
577
+ expect(response.status).toBe(200)
578
+
579
+ const receipt = Receipt.fromResponse(response)
580
+ expect(receipt.status).toBe('success')
581
+ expect(receipt.method).toBe('tempo')
582
+ expect(receipt.reference).toBeDefined()
583
+ }
584
+
585
+ httpServer.close()
586
+ })
587
+ })
588
+
527
589
  describe('intent: unknown', () => {
528
590
  test('behavior: returns 402 for invalid payload schema', async () => {
529
591
  const httpServer = await Http.createServer(async (req, res) => {
@@ -5,7 +5,12 @@ import {
5
5
  type TransactionReceipt,
6
6
  toFunctionSelector,
7
7
  } from 'viem'
8
- import { getTransactionReceipt, sendRawTransactionSync, signTransaction } from 'viem/actions'
8
+ import {
9
+ getTransactionReceipt,
10
+ sendRawTransaction,
11
+ sendRawTransactionSync,
12
+ signTransaction,
13
+ } from 'viem/actions'
9
14
  import { tempo as tempo_chain } from 'viem/chains'
10
15
  import { Abis, Transaction } from 'viem/tempo'
11
16
  import { PaymentExpiredError } from '../../Errors.js'
@@ -14,6 +19,7 @@ import * as Method from '../../Method.js'
14
19
  import * as Client from '../../viem/Client.js'
15
20
  import * as Account from '../internal/account.js'
16
21
  import * as defaults from '../internal/defaults.js'
22
+ import { simulateTransaction } from '../internal/simulate.js'
17
23
  import type * as types from '../internal/types.js'
18
24
  import * as Methods from '../Methods.js'
19
25
 
@@ -45,6 +51,7 @@ export function charge<const parameters extends charge.Parameters>(
45
51
  description,
46
52
  externalId,
47
53
  memo,
54
+ waitForConfirmation = true,
48
55
  } = parameters
49
56
 
50
57
  const { recipient, feePayer } = Account.resolve(parameters)
@@ -250,11 +257,30 @@ export function charge<const parameters extends charge.Parameters>(
250
257
  return serializedTransaction
251
258
  })()
252
259
 
253
- const receipt = await sendRawTransactionSync(client, {
254
- serializedTransaction: serializedTransaction_final,
255
- })
256
-
257
- return toReceipt(receipt)
260
+ if (waitForConfirmation) {
261
+ const receipt = await sendRawTransactionSync(client, {
262
+ serializedTransaction: serializedTransaction_final,
263
+ })
264
+ return toReceipt(receipt)
265
+ } else {
266
+ // Optimistic path: simulate to catch obvious reverts, then broadcast
267
+ // without waiting for on-chain confirmation. The returned receipt
268
+ // assumes success — callers opt into this risk via waitForConfirmation: false.
269
+ await simulateTransaction(client, {
270
+ ...transaction,
271
+ from: transaction.from as `0x${string}`,
272
+ calls,
273
+ })
274
+ const hash = await sendRawTransaction(client, {
275
+ serializedTransaction: serializedTransaction_final,
276
+ })
277
+ return {
278
+ method: 'tempo',
279
+ status: 'success',
280
+ timestamp: new Date().toISOString(),
281
+ reference: hash,
282
+ } as const
283
+ }
258
284
  }
259
285
 
260
286
  default:
@@ -270,6 +296,18 @@ export declare namespace charge {
270
296
  type Parameters = {
271
297
  /** Testnet mode. */
272
298
  testnet?: boolean | undefined
299
+ /**
300
+ * Whether to wait for the charge transaction to confirm on-chain before
301
+ * responding. @default true
302
+ *
303
+ * When `false`, the transaction is simulated via `eth_estimateGas` and
304
+ * broadcast without waiting for inclusion. The receipt will optimistically
305
+ * report `status: 'success'` based on simulation alone — if the
306
+ * transaction reverts on-chain after broadcast (e.g. due to a state
307
+ * change between simulation and inclusion), the receipt will not reflect
308
+ * the failure.
309
+ */
310
+ waitForConfirmation?: boolean | undefined
273
311
  } & Client.getResolver.Parameters &
274
312
  Account.resolve.Parameters &
275
313
  Defaults
@@ -929,7 +929,7 @@ describe('session', () => {
929
929
  request: makeRequest(),
930
930
  })
931
931
 
932
- const settleTxHash = await settle(store, client, channelId, escrowContract)
932
+ const settleTxHash = await settle(store, client, channelId, { escrowContract })
933
933
  expect(settleTxHash).toMatch(/^0x/)
934
934
 
935
935
  const ch = await store.getChannel(channelId)
@@ -939,7 +939,7 @@ describe('session', () => {
939
939
  test('settle rejects when no channel found', async () => {
940
940
  const fakeChannelId =
941
941
  '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex
942
- await expect(settle(store, client, fakeChannelId, escrowContract)).rejects.toThrow(
942
+ await expect(settle(store, client, fakeChannelId, { escrowContract })).rejects.toThrow(
943
943
  ChannelNotFoundError,
944
944
  )
945
945
  })
@@ -1101,6 +1101,55 @@ describe('session', () => {
1101
1101
  expect(result).toBeUndefined()
1102
1102
  })
1103
1103
 
1104
+ test('returns undefined for open POST with content-length > 0 (content request)', () => {
1105
+ const server = createServer()
1106
+ const result = server.respond!({
1107
+ credential: {
1108
+ challenge: makeChallenge({
1109
+ channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
1110
+ }),
1111
+ payload: { action: 'open' },
1112
+ },
1113
+ input: new Request('http://localhost', {
1114
+ method: 'POST',
1115
+ headers: { 'content-length': '42' },
1116
+ }),
1117
+ } as any)
1118
+ expect(result).toBeUndefined()
1119
+ })
1120
+
1121
+ test('returns undefined for open POST with transfer-encoding header (content request)', () => {
1122
+ const server = createServer()
1123
+ const result = server.respond!({
1124
+ credential: {
1125
+ challenge: makeChallenge({
1126
+ channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
1127
+ }),
1128
+ payload: { action: 'open' },
1129
+ },
1130
+ input: new Request('http://localhost', {
1131
+ method: 'POST',
1132
+ headers: { 'transfer-encoding': 'chunked' },
1133
+ }),
1134
+ } as any)
1135
+ expect(result).toBeUndefined()
1136
+ })
1137
+
1138
+ test('returns 204 for GET with topUp action', () => {
1139
+ const server = createServer()
1140
+ const result = server.respond!({
1141
+ credential: {
1142
+ challenge: makeChallenge({
1143
+ channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
1144
+ }),
1145
+ payload: { action: 'topUp' },
1146
+ },
1147
+ input: new Request('http://localhost', { method: 'GET' }),
1148
+ } as any)
1149
+ expect(result).toBeInstanceOf(Response)
1150
+ expect((result as Response).status).toBe(204)
1151
+ })
1152
+
1104
1153
  test('returns undefined for voucher POST with content-length > 0 (content request)', () => {
1105
1154
  const server = createServer()
1106
1155
  const result = server.respond!({
@@ -85,13 +85,17 @@ export function session<const parameters extends session.Parameters>(p?: paramet
85
85
  const parameters = p as parameters
86
86
  const {
87
87
  amount,
88
+ channelStateTtl = 60_000,
88
89
  currency = defaults.resolveCurrency(parameters),
89
90
  decimals = defaults.decimals,
90
91
  store: rawStore = Store.memory(),
91
92
  suggestedDeposit,
92
93
  unitType,
94
+ waitForConfirmation = true,
93
95
  } = parameters
94
96
 
97
+ const lastOnChainVerified = new Map<Hex, number>()
98
+
95
99
  const store = ChannelStore.fromStore(rawStore)
96
100
 
97
101
  const getClient = Client.getResolver({
@@ -187,7 +191,9 @@ export function session<const parameters extends session.Parameters>(p?: paramet
187
191
  payload,
188
192
  methodDetails,
189
193
  resolvedFeePayer,
194
+ waitForConfirmation,
190
195
  )
196
+ lastOnChainVerified.set(payload.channelId, Date.now())
191
197
  break
192
198
 
193
199
  case 'topUp':
@@ -199,6 +205,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
199
205
  methodDetails,
200
206
  resolvedFeePayer,
201
207
  )
208
+ lastOnChainVerified.set(payload.channelId, Date.now())
202
209
  break
203
210
 
204
211
  case 'voucher':
@@ -209,6 +216,8 @@ export function session<const parameters extends session.Parameters>(p?: paramet
209
216
  challenge,
210
217
  payload,
211
218
  methodDetails,
219
+ channelStateTtl,
220
+ lastOnChainVerified,
212
221
  )
213
222
  break
214
223
 
@@ -220,6 +229,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
220
229
  payload,
221
230
  methodDetails,
222
231
  account,
232
+ resolvedFeePayer,
223
233
  )
224
234
  break
225
235
 
@@ -237,34 +247,30 @@ export function session<const parameters extends session.Parameters>(p?: paramet
237
247
  // invoking the user's route handler. When it returns undefined, the
238
248
  // user's handler runs normally and serves content.
239
249
  //
240
- // Management actions (open, topUp, close) are always gated — they
241
- // return 204 regardless of request method.
250
+ // close and topUp are always gated (204) — they are pure management.
242
251
  //
243
- // Voucher POSTs are gated only when they have no request body, which
244
- // signals a mid-session voucher update (the client is just topping up
245
- // the channel balance). Voucher POSTs WITH a body are content requests
246
- // (e.g., an API call to a POST endpoint) and fall through to the
247
- // user's handler. GET requests with vouchers always fall through so
248
- // auto-mode clients (whose fetch wrapper bundles open+voucher into a
249
- // single GET retry) receive content as expected.
252
+ // open and voucher are gated only for bodyless POSTs (management
253
+ // updates). POSTs with a body are content requests the client's
254
+ // original request piggybacked on the credential so they fall
255
+ // through to serve content. GETs always fall through so auto-mode
256
+ // clients (whose fetch wrapper bundles open+voucher into a single
257
+ // GET retry) receive content as expected.
250
258
  respond({ credential, input }) {
251
259
  const { payload } = credential as Credential.Credential<SessionCredentialPayload>
252
260
 
253
261
  if (payload.action === 'close') return new Response(null, { status: 204 })
254
-
255
- const isManagement = payload.action === 'open' || payload.action === 'topUp'
256
- if (isManagement && input.method === 'POST') return new Response(null, { status: 204 })
257
-
258
- const isVoucher = payload.action === 'voucher'
259
- if (!isVoucher) return undefined
260
-
261
- // Only gate voucher POSTs with no body (mid-session balance updates).
262
- // POSTs with a body are content requests that should reach the handler.
263
- if (input.method !== 'POST') return undefined
264
- const contentLength = input.headers.get('content-length')
265
- if (contentLength !== null && contentLength !== '0') return undefined
266
- if (input.headers.has('transfer-encoding')) return undefined
267
- return new Response(null, { status: 204 })
262
+ if (payload.action === 'topUp') return new Response(null, { status: 204 })
263
+
264
+ // open and voucher: gate only bodyless POSTs (management updates).
265
+ // POSTs with a body are content requests — fall through so the
266
+ // upstream response is returned to the client.
267
+ if (input.method === 'POST') {
268
+ const contentLength = input.headers.get('content-length')
269
+ if (contentLength !== null && contentLength !== '0') return undefined
270
+ if (input.headers.has('transfer-encoding')) return undefined
271
+ return new Response(null, { status: 204 })
272
+ }
273
+ return undefined
268
274
  },
269
275
  })
270
276
  }
@@ -276,8 +282,22 @@ export declare namespace session {
276
282
  >
277
283
 
278
284
  type Parameters = {
285
+ /** TTL in milliseconds for cached on-chain channel state. After this duration, the server re-queries on-chain state during voucher handling to detect forced close requests. @default 60_000 */
286
+ channelStateTtl?: number | undefined
279
287
  /** Minimum voucher delta to accept (numeric string, default: "0"). */
280
288
  minVoucherDelta?: string | undefined
289
+ /**
290
+ * Whether to wait for the open transaction to confirm on-chain before
291
+ * responding. @default true
292
+ *
293
+ * When `false`, the transaction is simulated via `eth_estimateGas` and
294
+ * broadcast without waiting for inclusion. The receipt will optimistically
295
+ * report `status: 'success'` based on simulation alone — if the
296
+ * transaction reverts on-chain after broadcast (e.g. due to a state
297
+ * change between simulation and inclusion), the receipt will not reflect
298
+ * the failure.
299
+ */
300
+ waitForConfirmation?: boolean | undefined
281
301
  /** Store backend for channel state. */
282
302
  store?: Store.Store | undefined
283
303
  /**
@@ -310,7 +330,10 @@ export async function settle(
310
330
  store: ChannelStore.ChannelStore,
311
331
  client: viem_Client,
312
332
  channelId: Hex,
313
- escrowContract?: Address | undefined,
333
+ options?: {
334
+ escrowContract?: Address | undefined
335
+ feePayer?: viem_Account | undefined
336
+ },
314
337
  ): Promise<Hex> {
315
338
  const channel = await store.getChannel(channelId)
316
339
  if (!channel) throw new ChannelNotFoundError({ reason: 'channel not found' })
@@ -318,11 +341,17 @@ export async function settle(
318
341
 
319
342
  const chainId = client.chain?.id
320
343
  const resolvedEscrow =
321
- escrowContract ?? defaults.escrowContract[chainId as keyof typeof defaults.escrowContract]
344
+ options?.escrowContract ??
345
+ defaults.escrowContract[chainId as keyof typeof defaults.escrowContract]
322
346
  if (!resolvedEscrow) throw new Error(`No escrow contract for chainId ${chainId}.`)
323
347
 
324
348
  const settledAmount = channel.highestVoucher.cumulativeAmount
325
- const txHash = await settleOnChain(client, resolvedEscrow, channel.highestVoucher)
349
+ const txHash = await settleOnChain(
350
+ client,
351
+ resolvedEscrow,
352
+ channel.highestVoucher,
353
+ options?.feePayer,
354
+ )
326
355
 
327
356
  await store.updateChannel(channelId, (current) => {
328
357
  if (!current) return null
@@ -482,7 +511,12 @@ async function verifyAndAcceptVoucher(parameters: {
482
511
  }
483
512
 
484
513
  /**
485
- * Handle 'open' action - broadcast open transaction, verify voucher, and create channel.
514
+ * Handle 'open' action - verify voucher, create channel, and broadcast.
515
+ *
516
+ * When `waitForConfirmation` is true (default), the open transaction is
517
+ * broadcast and confirmed on-chain before responding. When false, the
518
+ * transaction is validated and simulated via `eth_estimateGas`; the receipt
519
+ * is returned immediately and the broadcast runs in the background.
486
520
  */
487
521
  async function handleOpen(
488
522
  store: ChannelStore.ChannelStore,
@@ -491,6 +525,7 @@ async function handleOpen(
491
525
  payload: SessionCredentialPayload & { action: 'open' },
492
526
  methodDetails: SessionMethodDetails,
493
527
  feePayer: viem_Account | undefined,
528
+ waitForConfirmation: boolean,
494
529
  ): Promise<SessionReceipt> {
495
530
  const voucher = parseVoucherFromPayload(
496
531
  payload.channelId,
@@ -510,6 +545,7 @@ async function handleOpen(
510
545
  recipient,
511
546
  currency,
512
547
  feePayer,
548
+ waitForConfirmation,
513
549
  })
514
550
 
515
551
  validateOnChainChannel(onChain, recipient, currency, amount)
@@ -621,6 +657,7 @@ async function handleTopUp(
621
657
  serializedTransaction: payload.transaction,
622
658
  escrowContract: methodDetails.escrowContract,
623
659
  channelId: payload.channelId,
660
+ currency: challenge.request.currency as Address,
624
661
  declaredDeposit,
625
662
  previousDeposit: channel.deposit,
626
663
  feePayer,
@@ -645,11 +682,13 @@ async function handleTopUp(
645
682
  */
646
683
  async function handleVoucher(
647
684
  store: ChannelStore.ChannelStore,
648
- _client: viem_Client,
685
+ client: viem_Client,
649
686
  minVoucherDelta: bigint,
650
687
  challenge: Challenge.Challenge,
651
688
  payload: SessionCredentialPayload & { action: 'voucher' },
652
689
  methodDetails: SessionMethodDetails,
690
+ channelStateTtl: number,
691
+ lastOnChainVerified: Map<Hex, number>,
653
692
  ): Promise<SessionReceipt> {
654
693
  const channel = await store.getChannel(payload.channelId)
655
694
  if (!channel) {
@@ -671,15 +710,28 @@ async function handleVoucher(
671
710
  // same session can safely use the cached deposit/signer values.
672
711
  // This avoids an RPC round-trip per voucher, which is critical for
673
712
  // high-frequency SSE streaming where vouchers arrive per-token.
674
- const cachedOnChain: OnChainChannel = {
675
- payer: channel.payer,
676
- payee: channel.payee,
677
- token: channel.token,
678
- deposit: channel.deposit,
679
- settled: channel.settledOnChain,
680
- finalized: channel.finalized,
681
- authorizedSigner: channel.authorizedSigner,
682
- closeRequestedAt: 0n,
713
+ //
714
+ // To guard against the payer initiating a forced close while vouchers
715
+ // are still being accepted, re-query on-chain state when the cache
716
+ // exceeds the configured staleness TTL.
717
+ const lastVerified = lastOnChainVerified.get(payload.channelId) ?? 0
718
+ const isStale = Date.now() - lastVerified > channelStateTtl
719
+
720
+ let cachedOnChain: OnChainChannel
721
+ if (isStale) {
722
+ cachedOnChain = await getOnChainChannel(client, methodDetails.escrowContract, payload.channelId)
723
+ lastOnChainVerified.set(payload.channelId, Date.now())
724
+ } else {
725
+ cachedOnChain = {
726
+ payer: channel.payer,
727
+ payee: channel.payee,
728
+ token: channel.token,
729
+ deposit: channel.deposit,
730
+ settled: channel.settledOnChain,
731
+ finalized: channel.finalized,
732
+ authorizedSigner: channel.authorizedSigner,
733
+ closeRequestedAt: 0n,
734
+ }
683
735
  }
684
736
 
685
737
  return verifyAndAcceptVoucher({
@@ -704,6 +756,7 @@ async function handleClose(
704
756
  payload: SessionCredentialPayload & { action: 'close' },
705
757
  methodDetails: SessionMethodDetails,
706
758
  account?: viem_Account,
759
+ feePayer?: viem_Account,
707
760
  ): Promise<SessionReceipt> {
708
761
  const channel = await store.getChannel(payload.channelId)
709
762
  if (!channel) {
@@ -754,7 +807,13 @@ async function handleClose(
754
807
  throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
755
808
  }
756
809
 
757
- const txHash = await closeOnChain(client, methodDetails.escrowContract, voucher, account)
810
+ const txHash = await closeOnChain(
811
+ client,
812
+ methodDetails.escrowContract,
813
+ voucher,
814
+ account,
815
+ feePayer,
816
+ )
758
817
 
759
818
  const updated = await store.updateChannel(payload.channelId, (current) => {
760
819
  if (!current) return null