mppx 0.8.13 → 0.8.14

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 (123) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/Errors.d.ts +10 -0
  3. package/dist/Errors.d.ts.map +1 -1
  4. package/dist/Errors.js +11 -2
  5. package/dist/Errors.js.map +1 -1
  6. package/dist/Method.d.ts +105 -3
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +102 -2
  9. package/dist/Method.js.map +1 -1
  10. package/dist/client/internal/Fetch.d.ts.map +1 -1
  11. package/dist/client/internal/Fetch.js +12 -2
  12. package/dist/client/internal/Fetch.js.map +1 -1
  13. package/dist/client/internal/MethodChallenge.d.ts +17 -0
  14. package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
  15. package/dist/client/internal/MethodChallenge.js +11 -0
  16. package/dist/client/internal/MethodChallenge.js.map +1 -0
  17. package/dist/server/Mppx.d.ts +23 -4
  18. package/dist/server/Mppx.d.ts.map +1 -1
  19. package/dist/server/Mppx.js +91 -82
  20. package/dist/server/Mppx.js.map +1 -1
  21. package/dist/tempo/client/Methods.d.ts +1 -0
  22. package/dist/tempo/client/Methods.d.ts.map +1 -1
  23. package/dist/tempo/internal/fee-payer.d.ts +1 -0
  24. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  25. package/dist/tempo/internal/fee-payer.js +7 -2
  26. package/dist/tempo/internal/fee-payer.js.map +1 -1
  27. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  28. package/dist/tempo/legacy/client/ChannelOps.js +2 -1
  29. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  30. package/dist/tempo/server/Charge.d.ts +38 -77
  31. package/dist/tempo/server/Charge.d.ts.map +1 -1
  32. package/dist/tempo/server/Charge.js +289 -209
  33. package/dist/tempo/server/Charge.js.map +1 -1
  34. package/dist/tempo/server/Methods.d.ts +12 -1
  35. package/dist/tempo/server/Methods.d.ts.map +1 -1
  36. package/dist/tempo/server/Methods.js +4 -0
  37. package/dist/tempo/server/Methods.js.map +1 -1
  38. package/dist/tempo/server/Relay.d.ts +48 -0
  39. package/dist/tempo/server/Relay.d.ts.map +1 -0
  40. package/dist/tempo/server/Relay.js +177 -0
  41. package/dist/tempo/server/Relay.js.map +1 -0
  42. package/dist/tempo/server/SponsorBudget.d.ts +59 -0
  43. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
  44. package/dist/tempo/server/SponsorBudget.js +144 -0
  45. package/dist/tempo/server/SponsorBudget.js.map +1 -0
  46. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  47. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/html.gen.js +1 -1
  49. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  50. package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
  51. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  52. package/dist/tempo/session/client/ChannelOps.js +9 -4
  53. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  54. package/dist/tempo/session/client/CredentialState.d.ts +5 -1
  55. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  56. package/dist/tempo/session/client/CredentialState.js +37 -19
  57. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  58. package/dist/tempo/session/client/Runtime.d.ts +8 -0
  59. package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
  60. package/dist/tempo/session/client/Runtime.js +17 -0
  61. package/dist/tempo/session/client/Runtime.js.map +1 -1
  62. package/dist/tempo/session/client/Session.d.ts +15 -5
  63. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  64. package/dist/tempo/session/client/Session.js +93 -36
  65. package/dist/tempo/session/client/Session.js.map +1 -1
  66. package/dist/tempo/session/client/SessionManager.d.ts +8 -1
  67. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  68. package/dist/tempo/session/client/SessionManager.js +54 -26
  69. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  70. package/dist/tempo/session/client/Transports.d.ts +7 -5
  71. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  72. package/dist/tempo/session/client/Transports.js +48 -24
  73. package/dist/tempo/session/client/Transports.js.map +1 -1
  74. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  75. package/dist/tempo/session/precompile/Chain.js +96 -8
  76. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  77. package/dist/tempo/session/precompile/Channel.d.ts +2 -0
  78. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
  79. package/dist/tempo/session/precompile/Channel.js +4 -0
  80. package/dist/tempo/session/precompile/Channel.js.map +1 -1
  81. package/dist/tempo/session/server/RequestState.js +3 -1
  82. package/dist/tempo/session/server/RequestState.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/Errors.test.ts +23 -0
  85. package/src/Errors.ts +21 -2
  86. package/src/Method.test.ts +102 -1
  87. package/src/Method.ts +241 -5
  88. package/src/client/Mppx.test-d.ts +3 -3
  89. package/src/client/internal/Fetch.ts +16 -7
  90. package/src/client/internal/MethodChallenge.ts +30 -0
  91. package/src/server/Mppx.test-d.ts +2 -0
  92. package/src/server/Mppx.test.ts +205 -0
  93. package/src/server/Mppx.ts +167 -103
  94. package/src/tempo/PublicExports.test-d.ts +20 -0
  95. package/src/tempo/internal/fee-payer.ts +7 -3
  96. package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
  97. package/src/tempo/legacy/client/ChannelOps.ts +2 -1
  98. package/src/tempo/server/Charge.test.ts +337 -63
  99. package/src/tempo/server/Charge.ts +435 -267
  100. package/src/tempo/server/Methods.ts +11 -0
  101. package/src/tempo/server/Relay.test.ts +523 -0
  102. package/src/tempo/server/Relay.ts +288 -0
  103. package/src/tempo/server/SponsorBudget.test.ts +125 -0
  104. package/src/tempo/server/SponsorBudget.ts +213 -0
  105. package/src/tempo/server/internal/html.gen.ts +1 -1
  106. package/src/tempo/session/client/ChannelOps.test.ts +17 -2
  107. package/src/tempo/session/client/ChannelOps.ts +12 -4
  108. package/src/tempo/session/client/CredentialState.ts +61 -20
  109. package/src/tempo/session/client/Runtime.test.ts +12 -0
  110. package/src/tempo/session/client/Runtime.ts +22 -0
  111. package/src/tempo/session/client/Session.test.ts +235 -2
  112. package/src/tempo/session/client/Session.ts +130 -41
  113. package/src/tempo/session/client/SessionManager.test.ts +102 -4
  114. package/src/tempo/session/client/SessionManager.ts +71 -27
  115. package/src/tempo/session/client/Transports.test.ts +32 -1
  116. package/src/tempo/session/client/Transports.ts +79 -35
  117. package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
  118. package/src/tempo/session/precompile/Chain.test.ts +197 -10
  119. package/src/tempo/session/precompile/Chain.ts +102 -8
  120. package/src/tempo/session/precompile/Channel.test.ts +9 -0
  121. package/src/tempo/session/precompile/Channel.ts +5 -0
  122. package/src/tempo/session/server/RequestState.test.ts +8 -1
  123. package/src/tempo/session/server/RequestState.ts +2 -2
@@ -1,5 +1,5 @@
1
1
  import type { Account, Address, Client, Hex } from 'viem'
2
- import { encodeFunctionData, isAddressEqual, parseEventLogs } from 'viem'
2
+ import { decodeFunctionData, encodeFunctionData, isAddressEqual, parseEventLogs } from 'viem'
3
3
  import {
4
4
  call,
5
5
  prepareTransactionRequest,
@@ -10,7 +10,7 @@ import {
10
10
  signTransaction,
11
11
  waitForTransactionReceipt,
12
12
  } from 'viem/actions'
13
- import { Transaction } from 'viem/tempo'
13
+ import { Abis, Addresses, Transaction } from 'viem/tempo'
14
14
 
15
15
  import { BadRequestError, VerificationFailedError } from '../../../Errors.js'
16
16
  import * as FeePayer from '../../internal/fee-payer.js'
@@ -401,6 +401,7 @@ export type ChannelTransactionOptions = {
401
401
 
402
402
  type ParsedPrecompileCredentialTransaction = {
403
403
  call: Transaction.TransactionTempo['calls'][number] & { data: Hex; to: Address }
404
+ prefixCalls: readonly Transaction.TransactionTempo['calls'][number][]
404
405
  transaction: ReturnType<(typeof Transaction)['deserialize']>
405
406
  }
406
407
 
@@ -418,11 +419,11 @@ function parsePrecompileCredentialTransaction(parameters: {
418
419
  serializedTransaction as Transaction.TransactionSerializedTempo,
419
420
  )
420
421
  const calls = transaction.calls
421
- if (calls.length !== 1)
422
+ if (calls.length !== 1 && calls.length !== 3)
422
423
  throw new VerificationFailedError({
423
- reason: `TIP-1034 ${label} transaction must contain exactly one call`,
424
+ reason: `TIP-1034 ${label} transaction must contain one management call, optionally preceded by an auto-swap`,
424
425
  })
425
- const call = calls[0]!
426
+ const call = calls[calls.length - 1]!
426
427
  if (!call.to || !isAddressEqual(call.to, escrowContract))
427
428
  throw new VerificationFailedError({
428
429
  reason: `TIP-1034 ${label} transaction targets the wrong address`,
@@ -431,7 +432,88 @@ function parsePrecompileCredentialTransaction(parameters: {
431
432
  throw new VerificationFailedError({
432
433
  reason: `TIP-1034 ${label} transaction is missing calldata`,
433
434
  })
434
- return { transaction, call: { ...call, data: call.data, to: call.to } }
435
+ return {
436
+ transaction,
437
+ call: { ...call, data: call.data, to: call.to },
438
+ prefixCalls: calls.slice(0, -1),
439
+ }
440
+ }
441
+
442
+ function validateAutoSwapPrefix(parameters: {
443
+ amountOut: bigint
444
+ currency: Address
445
+ label: 'open' | 'topUp'
446
+ prefixCalls: readonly Transaction.TransactionTempo['calls'][number][]
447
+ }) {
448
+ const { amountOut, currency, label, prefixCalls } = parameters
449
+ if (prefixCalls.length === 0) return
450
+
451
+ const fail = (reason: string): never => {
452
+ throw new VerificationFailedError({ reason: `TIP-1034 ${label} auto-swap ${reason}` })
453
+ }
454
+ if (prefixCalls.length !== 2) fail('must contain exactly approve and swap calls')
455
+
456
+ const approveCall = prefixCalls[0]!
457
+ const swapCall = prefixCalls[1]!
458
+ const approveTo = approveCall.to
459
+ const approveData = approveCall.data
460
+ const swapTo = swapCall.to
461
+ const swapData = swapCall.data
462
+ if (!approveTo || !approveData || !swapTo || !swapData)
463
+ fail('call is missing a target or calldata')
464
+ const checkedApproveTo = approveTo as Address
465
+ const checkedApproveData = approveData as Hex
466
+ const checkedSwapTo = swapTo as Address
467
+ const checkedSwapData = swapData as Hex
468
+ if ((approveCall.value ?? 0n) !== 0n || (swapCall.value ?? 0n) !== 0n)
469
+ fail('calls must not transfer native value')
470
+ if (!isAddressEqual(checkedSwapTo, Addresses.stablecoinDex)) fail('targets the wrong DEX')
471
+
472
+ const approve = (() => {
473
+ try {
474
+ return decodeFunctionData({ abi: Abis.tip20, data: checkedApproveData })
475
+ } catch {
476
+ return fail('approval calldata is invalid')
477
+ }
478
+ })()
479
+ const swap = (() => {
480
+ try {
481
+ return decodeFunctionData({ abi: Abis.stablecoinDex, data: checkedSwapData })
482
+ } catch {
483
+ return fail('swap calldata is invalid')
484
+ }
485
+ })()
486
+ if (approve.functionName !== 'approve' || swap.functionName !== 'swapExactAmountOut')
487
+ fail('must contain approve followed by swapExactAmountOut')
488
+
489
+ const [spender, approvedAmount] = approve.args as readonly [Address, bigint]
490
+ const [tokenIn, tokenOut, swapAmountOut, maxAmountIn] = swap.args as readonly [
491
+ Address,
492
+ Address,
493
+ bigint,
494
+ bigint,
495
+ ]
496
+ if (!isAddressEqual(checkedApproveTo, tokenIn)) fail('approval token does not match swap input')
497
+ if (!isAddressEqual(spender, Addresses.stablecoinDex)) fail('approval spender is not the DEX')
498
+ if (approvedAmount !== maxAmountIn) fail('approval amount does not match swap maximum input')
499
+ if (!isAddressEqual(tokenOut, currency)) fail('output token does not match channel currency')
500
+ if (swapAmountOut !== amountOut) fail('output amount does not match channel deposit')
501
+ if (isAddressEqual(tokenIn, tokenOut)) fail('input and output tokens must differ')
502
+
503
+ const canonicalApprove = encodeFunctionData({
504
+ abi: Abis.tip20,
505
+ functionName: 'approve',
506
+ args: [spender, approvedAmount],
507
+ })
508
+ const canonicalSwap = encodeFunctionData({
509
+ abi: Abis.stablecoinDex,
510
+ functionName: 'swapExactAmountOut',
511
+ args: [tokenIn, tokenOut, swapAmountOut, maxAmountIn],
512
+ })
513
+ if (checkedApproveData.toLowerCase() !== canonicalApprove.toLowerCase())
514
+ fail('approval calldata is not canonical')
515
+ if (checkedSwapData.toLowerCase() !== canonicalSwap.toLowerCase())
516
+ fail('swap calldata is not canonical')
435
517
  }
436
518
 
437
519
  async function simulateTempoTransaction(client: Client, request: unknown) {
@@ -796,7 +878,7 @@ export type BroadcastOpenTransactionParameters = {
796
878
  export async function broadcastOpenTransaction(
797
879
  parameters: BroadcastOpenTransactionParameters,
798
880
  ): Promise<BroadcastOpenTransactionResult> {
799
- const { transaction, call } = parsePrecompileCredentialTransaction({
881
+ const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction({
800
882
  escrowContract: parameters.escrowContract,
801
883
  feePayer: parameters.feePayer,
802
884
  label: 'open',
@@ -812,6 +894,12 @@ export async function broadcastOpenTransaction(
812
894
  authorizedSigner: parameters.expectedAuthorizedSigner,
813
895
  },
814
896
  })
897
+ validateAutoSwapPrefix({
898
+ amountOut: open.deposit,
899
+ currency: parameters.expectedCurrency,
900
+ label: 'open',
901
+ prefixCalls,
902
+ })
815
903
  const descriptor = ChannelOps.descriptorFromOpen({
816
904
  chainId: parameters.chainId,
817
905
  escrow: parameters.escrowContract,
@@ -920,7 +1008,7 @@ export type BroadcastTopUpTransactionParameters = {
920
1008
  export async function broadcastTopUpTransaction(
921
1009
  parameters: BroadcastTopUpTransactionParameters,
922
1010
  ): Promise<BroadcastTopUpTransactionResult> {
923
- const { transaction, call } = parsePrecompileCredentialTransaction({
1011
+ const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction({
924
1012
  escrowContract: parameters.escrowContract,
925
1013
  feePayer: parameters.feePayer,
926
1014
  label: 'topUp',
@@ -933,6 +1021,12 @@ export async function broadcastTopUpTransaction(
933
1021
  additionalDeposit: parameters.additionalDeposit,
934
1022
  },
935
1023
  })
1024
+ validateAutoSwapPrefix({
1025
+ amountOut: parameters.additionalDeposit,
1026
+ currency: parameters.expectedCurrency,
1027
+ label: 'topUp',
1028
+ prefixCalls,
1029
+ })
936
1030
  const receipt = await sendCredentialTransaction({
937
1031
  challengeExpires: parameters.challengeExpires,
938
1032
  chainId: parameters.chainId,
@@ -27,6 +27,15 @@ const transaction = {
27
27
  } as const satisfies Channel.ExpiringNonceTransaction
28
28
 
29
29
  describe('precompile Channel.computeId', () => {
30
+ test('uses the payer when the descriptor delegates to the zero address', () => {
31
+ expect(
32
+ Channel.resolveAuthorizedSigner({
33
+ ...descriptor,
34
+ authorizedSigner: '0x0000000000000000000000000000000000000000',
35
+ }),
36
+ ).toBe(descriptor.payer)
37
+ })
38
+
30
39
  test('returns deterministic 32-byte hash for fixed inputs', () => {
31
40
  const id = Channel.computeId({ ...descriptor, chainId })
32
41
  expect(Channel.computeId({ ...descriptor, chainId })).toBe(id)
@@ -9,6 +9,11 @@ import type { ChannelDescriptor } from './Protocol.js'
9
9
  /** Re-export of the TIP-1034 channel descriptor shape. */
10
10
  export type { ChannelDescriptor } from './Protocol.js'
11
11
 
12
+ /** Resolves the descriptor's effective voucher signer; zero delegates to the payer. */
13
+ export function resolveAuthorizedSigner(descriptor: ChannelDescriptor): Address {
14
+ return BigInt(descriptor.authorizedSigner) === 0n ? descriptor.payer : descriptor.authorizedSigner
15
+ }
16
+
12
17
  /** Tempo transaction shape used to derive the TIP-1034 `expiringNonceHash`. */
13
18
  export type ExpiringNonceTransaction = (
14
19
  | z_TransactionSerializableTempo
@@ -544,10 +544,17 @@ describe('SessionSnapshotHints', () => {
544
544
  }
545
545
  })
546
546
 
547
- test('omits hints for missing, non-precompile, or finalized channels', async () => {
547
+ test('omits hints for missing, non-precompile, closing, or finalized channels', async () => {
548
548
  await expect(
549
549
  resolveSessionSnapshot({ amount: 1n, channelId, store: store(null) }),
550
550
  ).resolves.toBe(undefined)
551
+ await expect(
552
+ resolveSessionSnapshot({
553
+ amount: 1n,
554
+ channelId,
555
+ store: store(channel({ closeRequestedAt: 1n })),
556
+ }),
557
+ ).resolves.toBe(undefined)
551
558
  await expect(
552
559
  resolveSessionSnapshot({
553
560
  amount: 1n,
@@ -143,6 +143,7 @@ export async function resolveSessionSnapshot(
143
143
  const channel = await store.getChannel(ChannelStore.normalizeChannelId(channelId))
144
144
  if (!channel || !ChannelStore.isPrecompileState(channel)) return undefined
145
145
  if (channel.finalized) return undefined
146
+ if (channel.closeRequestedAt !== 0n) return undefined
146
147
  if (!channel.highestVoucher) return undefined
147
148
  if (channel.highestVoucher.cumulativeAmount !== channel.highestVoucherAmount) return undefined
148
149
  if (expected && !matchesSnapshotPaymentFields(channel, expected)) return undefined
@@ -151,8 +152,7 @@ export async function resolveSessionSnapshot(
151
152
  acceptedCumulative: channel.highestVoucherAmount.toString(),
152
153
  chainId: channel.chainId,
153
154
  channelId: channel.channelId,
154
- closeRequestedAt:
155
- channel.closeRequestedAt === 0n ? undefined : channel.closeRequestedAt.toString(),
155
+ closeRequestedAt: undefined,
156
156
  deposit: channel.deposit.toString(),
157
157
  descriptor: channel.descriptor,
158
158
  escrow: channel.escrowContract,