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
@@ -1,4 +1,5 @@
1
1
  import { type Address, encodeFunctionData, erc20Abi, type Hex } from 'viem'
2
+ import { waitForTransactionReceipt } from 'viem/actions'
2
3
  import { Addresses, Transaction } from 'viem/tempo'
3
4
  import { beforeAll, describe, expect, test } from 'vitest'
4
5
  import {
@@ -346,6 +347,38 @@ describe('on-chain', () => {
346
347
  expect(result.txHash).toBeUndefined()
347
348
  expect(result.onChain.deposit).toBe(deposit)
348
349
  })
350
+
351
+ test('waitForConfirmation: false returns derived on-chain state', async () => {
352
+ const salt = nextSalt()
353
+ const deposit = 10_000_000n
354
+
355
+ const { channelId, serializedTransaction } = await signOpenChannel({
356
+ escrow: escrowContract,
357
+ payer,
358
+ payee: recipient,
359
+ token: currency,
360
+ deposit,
361
+ salt,
362
+ })
363
+
364
+ const result = await broadcastOpenTransaction({
365
+ client,
366
+ serializedTransaction,
367
+ escrowContract,
368
+ channelId,
369
+ recipient,
370
+ currency,
371
+ waitForConfirmation: false,
372
+ })
373
+
374
+ expect(result.txHash).toBeDefined()
375
+ expect(result.onChain.payer.toLowerCase()).toBe(payer.address.toLowerCase())
376
+ expect(result.onChain.payee.toLowerCase()).toBe(recipient.toLowerCase())
377
+ expect(result.onChain.token.toLowerCase()).toBe(currency.toLowerCase())
378
+ expect(result.onChain.deposit).toBe(deposit)
379
+ expect(result.onChain.settled).toBe(0n)
380
+ expect(result.onChain.finalized).toBe(false)
381
+ })
349
382
  })
350
383
 
351
384
  describe('broadcastTopUpTransaction', () => {
@@ -380,6 +413,7 @@ describe('on-chain', () => {
380
413
  serializedTransaction,
381
414
  escrowContract,
382
415
  channelId: wrongChannelId,
416
+ currency: asset,
383
417
  declaredDeposit: topUpAmount,
384
418
  previousDeposit: deposit,
385
419
  }),
@@ -414,6 +448,7 @@ describe('on-chain', () => {
414
448
  serializedTransaction,
415
449
  escrowContract,
416
450
  channelId,
451
+ currency: asset,
417
452
  declaredDeposit: 9_999_999n,
418
453
  previousDeposit: deposit,
419
454
  }),
@@ -447,6 +482,7 @@ describe('on-chain', () => {
447
482
  serializedTransaction,
448
483
  escrowContract,
449
484
  channelId,
485
+ currency: asset,
450
486
  declaredDeposit: topUpAmount,
451
487
  previousDeposit: deposit,
452
488
  })
@@ -501,6 +537,7 @@ describe('on-chain', () => {
501
537
  serializedTransaction: tampered,
502
538
  escrowContract,
503
539
  channelId,
540
+ currency: asset,
504
541
  declaredDeposit: topUpAmount,
505
542
  previousDeposit: deposit,
506
543
  feePayer: accounts[0],
@@ -508,4 +545,157 @@ describe('on-chain', () => {
508
545
  ).rejects.toThrow('fee-sponsored topUp transaction contains an unauthorized call')
509
546
  })
510
547
  })
548
+
549
+ describe('settleOnChain', () => {
550
+ test('settles a channel', async () => {
551
+ const salt = nextSalt()
552
+ const deposit = 10_000_000n
553
+ const settleAmount = 5_000_000n
554
+
555
+ const { channelId } = await openChannel({
556
+ escrow: escrowContract,
557
+ payer,
558
+ payee: recipient,
559
+ token: currency,
560
+ deposit,
561
+ salt,
562
+ })
563
+
564
+ const signature = await signVoucher(
565
+ client,
566
+ payer,
567
+ { channelId, cumulativeAmount: settleAmount },
568
+ escrowContract,
569
+ chain.id,
570
+ )
571
+
572
+ const txHash = await settleOnChain(client, escrowContract, {
573
+ channelId,
574
+ cumulativeAmount: settleAmount,
575
+ signature,
576
+ })
577
+
578
+ expect(txHash).toBeDefined()
579
+ await waitForTransactionReceipt(client, { hash: txHash })
580
+ const channel = await getOnChainChannel(client, escrowContract, channelId)
581
+ expect(channel.settled).toBe(settleAmount)
582
+ expect(channel.finalized).toBe(false)
583
+ })
584
+
585
+ test('settles a channel with fee payer', async () => {
586
+ const salt = nextSalt()
587
+ const deposit = 10_000_000n
588
+ const settleAmount = 5_000_000n
589
+
590
+ const { channelId } = await openChannel({
591
+ escrow: escrowContract,
592
+ payer,
593
+ payee: recipient,
594
+ token: currency,
595
+ deposit,
596
+ salt,
597
+ })
598
+
599
+ const signature = await signVoucher(
600
+ client,
601
+ payer,
602
+ { channelId, cumulativeAmount: settleAmount },
603
+ escrowContract,
604
+ chain.id,
605
+ )
606
+
607
+ const txHash = await settleOnChain(
608
+ client,
609
+ escrowContract,
610
+ {
611
+ channelId,
612
+ cumulativeAmount: settleAmount,
613
+ signature,
614
+ },
615
+ accounts[0],
616
+ )
617
+
618
+ expect(txHash).toBeDefined()
619
+ await waitForTransactionReceipt(client, { hash: txHash })
620
+ const channel = await getOnChainChannel(client, escrowContract, channelId)
621
+ expect(channel.settled).toBe(settleAmount)
622
+ expect(channel.finalized).toBe(false)
623
+ })
624
+ })
625
+
626
+ describe('closeOnChain', () => {
627
+ test('closes a channel', async () => {
628
+ const salt = nextSalt()
629
+ const deposit = 10_000_000n
630
+ const closeAmount = 5_000_000n
631
+
632
+ const { channelId } = await openChannel({
633
+ escrow: escrowContract,
634
+ payer,
635
+ payee: recipient,
636
+ token: currency,
637
+ deposit,
638
+ salt,
639
+ })
640
+
641
+ const signature = await signVoucher(
642
+ client,
643
+ payer,
644
+ { channelId, cumulativeAmount: closeAmount },
645
+ escrowContract,
646
+ chain.id,
647
+ )
648
+
649
+ const txHash = await closeOnChain(client, escrowContract, {
650
+ channelId,
651
+ cumulativeAmount: closeAmount,
652
+ signature,
653
+ })
654
+
655
+ expect(txHash).toBeDefined()
656
+ await waitForTransactionReceipt(client, { hash: txHash })
657
+ const channel = await getOnChainChannel(client, escrowContract, channelId)
658
+ expect(channel.finalized).toBe(true)
659
+ })
660
+
661
+ test('closes a channel with fee payer', async () => {
662
+ const salt = nextSalt()
663
+ const deposit = 10_000_000n
664
+ const closeAmount = 5_000_000n
665
+
666
+ const { channelId } = await openChannel({
667
+ escrow: escrowContract,
668
+ payer,
669
+ payee: recipient,
670
+ token: currency,
671
+ deposit,
672
+ salt,
673
+ })
674
+
675
+ const signature = await signVoucher(
676
+ client,
677
+ payer,
678
+ { channelId, cumulativeAmount: closeAmount },
679
+ escrowContract,
680
+ chain.id,
681
+ )
682
+
683
+ const txHash = await closeOnChain(
684
+ client,
685
+ escrowContract,
686
+ {
687
+ channelId,
688
+ cumulativeAmount: closeAmount,
689
+ signature,
690
+ },
691
+ undefined,
692
+ accounts[0],
693
+ )
694
+
695
+ expect(txHash).toBeDefined()
696
+ await waitForTransactionReceipt(client, { hash: txHash })
697
+ const channel = await getOnChainChannel(client, escrowContract, channelId)
698
+ expect(channel.finalized).toBe(true)
699
+ })
700
+ })
511
701
  }) // end describe('on-chain')
@@ -3,15 +3,26 @@ import {
3
3
  type Address,
4
4
  type Client,
5
5
  decodeFunctionData,
6
+ encodeFunctionData,
6
7
  getAbiItem,
8
+ getAddress,
7
9
  type Hex,
8
10
  isAddressEqual,
9
11
  type ReadContractReturnType,
10
12
  toFunctionSelector,
11
13
  } from 'viem'
12
- import { readContract, sendRawTransactionSync, signTransaction, writeContract } from 'viem/actions'
14
+ import {
15
+ prepareTransactionRequest,
16
+ readContract,
17
+ sendRawTransaction,
18
+ sendRawTransactionSync,
19
+ signTransaction,
20
+ writeContract,
21
+ } from 'viem/actions'
13
22
  import { Transaction } from 'viem/tempo'
14
23
  import { BadRequestError, ChannelClosedError, VerificationFailedError } from '../../Errors.js'
24
+ import * as defaults from '../internal/defaults.js'
25
+ import { simulateTransaction } from '../internal/simulate.js'
15
26
  import { escrowAbi } from './escrow.abi.js'
16
27
  import type { SignedVoucher } from './Types.js'
17
28
 
@@ -75,15 +86,21 @@ export async function settleOnChain(
75
86
  client: Client,
76
87
  escrowContract: Address,
77
88
  voucher: SignedVoucher,
89
+ feePayer?: Account | undefined,
78
90
  ): Promise<Hex> {
79
91
  assertUint128(voucher.cumulativeAmount)
92
+ const args = [voucher.channelId, voucher.cumulativeAmount, voucher.signature] as const
93
+ if (feePayer) {
94
+ const data = encodeFunctionData({ abi: escrowAbi, functionName: 'settle', args })
95
+ return sendFeePayerTx(client, feePayer, escrowContract, data, 'settle')
96
+ }
80
97
  return writeContract(client, {
81
98
  account: client.account!,
82
99
  chain: client.chain,
83
100
  address: escrowContract,
84
101
  abi: escrowAbi,
85
102
  functionName: 'settle',
86
- args: [voucher.channelId, voucher.cumulativeAmount, voucher.signature],
103
+ args,
87
104
  })
88
105
  }
89
106
 
@@ -95,6 +112,7 @@ export async function closeOnChain(
95
112
  escrowContract: Address,
96
113
  voucher: SignedVoucher,
97
114
  account?: Account,
115
+ feePayer?: Account | undefined,
98
116
  ): Promise<Hex> {
99
117
  assertUint128(voucher.cumulativeAmount)
100
118
  const resolved = account ?? client.account
@@ -102,14 +120,67 @@ export async function closeOnChain(
102
120
  throw new Error(
103
121
  'Cannot close channel: no account available. Provide an `account` in the session config or a `getClient` that returns an account-bearing client.',
104
122
  )
123
+ const args = [voucher.channelId, voucher.cumulativeAmount, voucher.signature] as const
124
+ if (feePayer) {
125
+ const data = encodeFunctionData({ abi: escrowAbi, functionName: 'close', args })
126
+ return sendFeePayerTx(client, feePayer, escrowContract, data, 'close')
127
+ }
105
128
  return writeContract(client, {
106
129
  account: resolved,
107
130
  chain: client.chain,
108
131
  address: escrowContract,
109
132
  abi: escrowAbi,
110
133
  functionName: 'close',
111
- args: [voucher.channelId, voucher.cumulativeAmount, voucher.signature],
134
+ args,
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Build, sign, and broadcast a fee-sponsored type-0x76 transaction.
140
+ *
141
+ * Follows the same signTransaction + sendRawTransactionSync pattern used
142
+ * by broadcastOpenTransaction / broadcastTopUpTransaction, but originates
143
+ * the transaction server-side (estimating gas and fees first).
144
+ */
145
+ async function sendFeePayerTx(
146
+ client: Client,
147
+ feePayer: Account,
148
+ to: Address,
149
+ data: Hex,
150
+ label: string,
151
+ ): Promise<Hex> {
152
+ // Resolve the fee token for this chain so the tx pays gas in the correct
153
+ // token. `feePayer: true` tells the prepare hook to use expiring nonces but
154
+ // does NOT set feeToken automatically, so we must provide it explicitly.
155
+ const chainId = client.chain?.id
156
+ const feeToken = chainId
157
+ ? defaults.currency[chainId as keyof typeof defaults.currency]
158
+ : undefined
159
+
160
+ const prepared = await prepareTransactionRequest(client, {
161
+ account: feePayer,
162
+ calls: [{ to, data }],
163
+ feePayer: true,
164
+ ...(feeToken ? { feeToken } : {}),
165
+ } as never)
166
+
167
+ const serialized = (await signTransaction(client, {
168
+ ...prepared,
169
+ account: feePayer,
170
+ feePayer,
171
+ } as never)) as Hex
172
+
173
+ const receipt = await sendRawTransactionSync(client, {
174
+ serializedTransaction: serialized as Transaction.TransactionSerializedTempo,
112
175
  })
176
+
177
+ if (receipt.status !== 'success') {
178
+ throw new VerificationFailedError({
179
+ reason: `${label} transaction reverted: ${receipt.transactionHash}`,
180
+ })
181
+ }
182
+
183
+ return receipt.transactionHash
113
184
  }
114
185
 
115
186
  const escrowOpenSelector = /*#__PURE__*/ toFunctionSelector(
@@ -137,6 +208,8 @@ export async function broadcastOpenTransaction(parameters: {
137
208
  recipient: Address
138
209
  currency: Address
139
210
  feePayer?: Account | undefined
211
+ /** When false, simulates instead of waiting for confirmation and returns derived on-chain state. @default true */
212
+ waitForConfirmation?: boolean | undefined
140
213
  }): Promise<BroadcastResult> {
141
214
  const {
142
215
  client,
@@ -146,6 +219,7 @@ export async function broadcastOpenTransaction(parameters: {
146
219
  recipient,
147
220
  currency,
148
221
  feePayer,
222
+ waitForConfirmation = true,
149
223
  } = parameters
150
224
 
151
225
  const transaction = Transaction.deserialize(
@@ -184,7 +258,13 @@ export async function broadcastOpenTransaction(parameters: {
184
258
  }
185
259
 
186
260
  const { args: openArgs } = decodeFunctionData({ abi: escrowAbi, data: openCall.data! })
187
- const [payee, token] = openArgs as readonly [Address, Address, ...unknown[]]
261
+ const [payee, token, deposit, , authorizedSigner] = openArgs as readonly [
262
+ Address,
263
+ Address,
264
+ bigint,
265
+ Hex,
266
+ Address,
267
+ ]
188
268
 
189
269
  if (!isAddressEqual(payee, recipient)) {
190
270
  throw new VerificationFailedError({
@@ -208,6 +288,28 @@ export async function broadcastOpenTransaction(parameters: {
208
288
  return serializedTransaction
209
289
  })()
210
290
 
291
+ if (!waitForConfirmation) {
292
+ const from = getAddress(transaction.from as Address)
293
+ await simulateTransaction(client, { ...transaction, from, calls })
294
+ const txHash = await sendRawTransaction(client, {
295
+ serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
296
+ })
297
+
298
+ return {
299
+ txHash,
300
+ onChain: {
301
+ payer: from,
302
+ payee,
303
+ token,
304
+ authorizedSigner,
305
+ deposit,
306
+ settled: 0n,
307
+ closeRequestedAt: 0n,
308
+ finalized: false,
309
+ } as OnChainChannel,
310
+ }
311
+ }
312
+
211
313
  let txHash: Hex | undefined
212
314
  try {
213
315
  const receipt = await sendRawTransactionSync(client, {
@@ -239,6 +341,7 @@ export async function broadcastTopUpTransaction(parameters: {
239
341
  serializedTransaction: Hex
240
342
  escrowContract: Address
241
343
  channelId: Hex
344
+ currency: Address
242
345
  declaredDeposit: bigint
243
346
  previousDeposit: bigint
244
347
  feePayer?: Account | undefined
@@ -248,6 +351,7 @@ export async function broadcastTopUpTransaction(parameters: {
248
351
  serializedTransaction,
249
352
  escrowContract,
250
353
  channelId,
354
+ currency,
251
355
  declaredDeposit,
252
356
  previousDeposit,
253
357
  feePayer,
@@ -279,7 +383,7 @@ export async function broadcastTopUpTransaction(parameters: {
279
383
  const selector = call.data.slice(0, 10)
280
384
  const isEscrowTopUp =
281
385
  isAddressEqual(call.to, escrowContract) && selector === escrowTopUpSelector
282
- const isTokenApprove = selector === erc20ApproveSelector
386
+ const isTokenApprove = isAddressEqual(call.to, currency) && selector === erc20ApproveSelector
283
387
  if (!isEscrowTopUp && !isTokenApprove) {
284
388
  throw new BadRequestError({
285
389
  reason: 'fee-sponsored topUp transaction contains an unauthorized call',