mppx 0.4.6 → 0.4.8

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 (89) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/Store.d.ts +5 -4
  3. package/dist/Store.d.ts.map +1 -1
  4. package/dist/Store.js.map +1 -1
  5. package/dist/cli/cli.d.ts.map +1 -1
  6. package/dist/cli/cli.js +22 -7
  7. package/dist/cli/cli.js.map +1 -1
  8. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  9. package/dist/cli/plugins/tempo.js +9 -22
  10. package/dist/cli/plugins/tempo.js.map +1 -1
  11. package/dist/middlewares/elysia.d.ts.map +1 -1
  12. package/dist/middlewares/elysia.js +5 -1
  13. package/dist/middlewares/elysia.js.map +1 -1
  14. package/dist/proxy/Proxy.d.ts.map +1 -1
  15. package/dist/proxy/Proxy.js +3 -1
  16. package/dist/proxy/Proxy.js.map +1 -1
  17. package/dist/proxy/internal/Route.d.ts +2 -2
  18. package/dist/proxy/internal/Route.d.ts.map +1 -1
  19. package/dist/proxy/internal/Route.js +4 -2
  20. package/dist/proxy/internal/Route.js.map +1 -1
  21. package/dist/server/Mppx.d.ts.map +1 -1
  22. package/dist/server/Mppx.js +26 -8
  23. package/dist/server/Mppx.js.map +1 -1
  24. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  25. package/dist/tempo/client/SessionManager.js +12 -1
  26. package/dist/tempo/client/SessionManager.js.map +1 -1
  27. package/dist/tempo/internal/address.d.ts +3 -0
  28. package/dist/tempo/internal/address.d.ts.map +1 -0
  29. package/dist/tempo/internal/address.js +4 -0
  30. package/dist/tempo/internal/address.js.map +1 -0
  31. package/dist/tempo/internal/auto-swap.js +3 -3
  32. package/dist/tempo/internal/auto-swap.js.map +1 -1
  33. package/dist/tempo/internal/fee-payer.d.ts +4 -1
  34. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  35. package/dist/tempo/internal/fee-payer.js +11 -3
  36. package/dist/tempo/internal/fee-payer.js.map +1 -1
  37. package/dist/tempo/server/Charge.d.ts +11 -0
  38. package/dist/tempo/server/Charge.d.ts.map +1 -1
  39. package/dist/tempo/server/Charge.js +109 -50
  40. package/dist/tempo/server/Charge.js.map +1 -1
  41. package/dist/tempo/server/Session.d.ts +1 -1
  42. package/dist/tempo/server/Session.d.ts.map +1 -1
  43. package/dist/tempo/server/Session.js +39 -32
  44. package/dist/tempo/server/Session.js.map +1 -1
  45. package/dist/tempo/server/internal/transport.d.ts +1 -1
  46. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  47. package/dist/tempo/server/internal/transport.js +41 -1
  48. package/dist/tempo/server/internal/transport.js.map +1 -1
  49. package/dist/tempo/session/Chain.d.ts.map +1 -1
  50. package/dist/tempo/session/Chain.js +51 -10
  51. package/dist/tempo/session/Chain.js.map +1 -1
  52. package/dist/tempo/session/ChannelStore.d.ts +2 -0
  53. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  54. package/dist/tempo/session/ChannelStore.js +4 -2
  55. package/dist/tempo/session/ChannelStore.js.map +1 -1
  56. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  57. package/dist/tempo/session/Voucher.js +3 -2
  58. package/dist/tempo/session/Voucher.js.map +1 -1
  59. package/package.json +6 -2
  60. package/src/Store.test-d.ts +58 -0
  61. package/src/Store.ts +6 -4
  62. package/src/cli/cli.test.ts +124 -0
  63. package/src/cli/cli.ts +19 -7
  64. package/src/cli/plugins/tempo.ts +17 -23
  65. package/src/middlewares/elysia.test.ts +89 -0
  66. package/src/middlewares/elysia.ts +4 -1
  67. package/src/proxy/Proxy.test.ts +56 -0
  68. package/src/proxy/Proxy.ts +6 -1
  69. package/src/proxy/internal/Route.test.ts +57 -0
  70. package/src/proxy/internal/Route.ts +3 -1
  71. package/src/server/Mppx.test.ts +246 -0
  72. package/src/server/Mppx.ts +27 -8
  73. package/src/tempo/client/SessionManager.ts +11 -1
  74. package/src/tempo/internal/address.ts +6 -0
  75. package/src/tempo/internal/auto-swap.ts +3 -3
  76. package/src/tempo/internal/fee-payer.ts +18 -4
  77. package/src/tempo/server/Charge.test.ts +1080 -31
  78. package/src/tempo/server/Charge.ts +158 -63
  79. package/src/tempo/server/Session.test.ts +929 -111
  80. package/src/tempo/server/Session.ts +48 -33
  81. package/src/tempo/server/Sse.test.ts +1 -0
  82. package/src/tempo/server/internal/transport.test.ts +29 -0
  83. package/src/tempo/server/internal/transport.ts +41 -2
  84. package/src/tempo/session/Chain.test.ts +144 -0
  85. package/src/tempo/session/Chain.ts +58 -10
  86. package/src/tempo/session/ChannelStore.test.ts +10 -0
  87. package/src/tempo/session/ChannelStore.ts +6 -3
  88. package/src/tempo/session/Sse.test.ts +1 -0
  89. package/src/tempo/session/Voucher.ts +3 -2
@@ -329,19 +329,36 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
329
329
  }
330
330
 
331
331
  // Verify the credential's challenge matches this route's configured
332
- // request. Prevents cross-route scope confusion where a credential
333
- // issued for a cheap route is presented at an expensive route.
332
+ // method, intent, realm, and request. Prevents cross-route scope
333
+ // confusion where a credential issued for a cheap route (or different
334
+ // method/intent) is presented at an expensive route.
334
335
  // Note: we compare specific payment parameters rather than the full
335
336
  // request because the `request` hook may produce credential-dependent
336
337
  // output (e.g. `feePayer` differs between 402 and credential calls).
337
338
  {
339
+ for (const field of ['method', 'intent', 'realm'] as const) {
340
+ if (credential.challenge[field] !== challenge[field]) {
341
+ const response = await transport.respondChallenge({
342
+ challenge,
343
+ input,
344
+ error: new Errors.InvalidChallengeError({
345
+ id: credential.challenge.id,
346
+ reason: `credential ${field} does not match this route's requirements`,
347
+ }),
348
+ })
349
+ return { challenge: response, status: 402 }
350
+ }
351
+ }
352
+
338
353
  const routeReq = challenge.request as Record<string, unknown>
339
354
  const echoedReq = credential.challenge.request as Record<string, unknown>
355
+ const routeDetails = (routeReq.methodDetails ?? {}) as Record<string, unknown>
356
+ const echoedDetails = (echoedReq.methodDetails ?? {}) as Record<string, unknown>
340
357
  for (const field of ['amount', 'currency', 'recipient'] as const) {
358
+ const routeVal = routeReq[field] ?? routeDetails[field]
341
359
  if (
342
- routeReq[field] !== undefined &&
343
- echoedReq[field] !== undefined &&
344
- String(routeReq[field]) !== String(echoedReq[field])
360
+ routeVal !== undefined &&
361
+ String(routeVal) !== String(echoedReq[field] ?? echoedDetails[field])
345
362
  ) {
346
363
  const response = await transport.respondChallenge({
347
364
  challenge,
@@ -578,22 +595,24 @@ export function compose(
578
595
  if (credential) {
579
596
  const { method: credMethod, intent: credIntent } = credential.challenge
580
597
  const credReq = credential.challenge.request as Record<string, unknown>
598
+ const credDetails = (credReq.methodDetails ?? {}) as Record<string, unknown>
581
599
 
582
600
  // Filter by name+intent, then narrow by comparing stable request fields
583
601
  // from the echoed challenge against each handler's canonical request.
584
602
  // Uses the schema-parsed canonical form (not raw options) so that
585
603
  // transformed fields (e.g. amount with decimals) match correctly.
604
+ // Also checks inside methodDetails for fields moved there by transforms.
586
605
  const candidates = handlers.filter((h) => {
587
606
  const meta = (h as ConfiguredHandler)._internal
588
607
  if (!meta || meta.name !== credMethod || meta.intent !== credIntent) return false
589
608
  const canonical = meta._canonicalRequest
590
609
  if (!canonical) return true
610
+ const canonicalDetails = (canonical.methodDetails ?? {}) as Record<string, unknown>
591
611
  for (const field of ['amount', 'currency', 'recipient', 'chainId'] as const) {
592
- const canonicalVal = canonical[field]
612
+ const canonicalVal = canonical[field] ?? canonicalDetails[field]
593
613
  if (
594
614
  canonicalVal !== undefined &&
595
- credReq[field] !== undefined &&
596
- String(canonicalVal) !== String(credReq[field])
615
+ String(canonicalVal) !== String(credReq[field] ?? credDetails[field])
597
616
  )
598
617
  return false
599
618
  }
@@ -59,6 +59,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
59
59
  let channel: ChannelEntry | null = null
60
60
  let lastChallenge: Challenge.Challenge | null = null
61
61
  let lastUrl: RequestInfo | URL | null = null
62
+ let spent = 0n
62
63
 
63
64
  const method = sessionPlugin({
64
65
  account: parameters.account,
@@ -68,6 +69,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
68
69
  decimals: parameters.decimals,
69
70
  maxDeposit: parameters.maxDeposit,
70
71
  onChannelUpdate(entry) {
72
+ if (entry.channelId !== channel?.channelId) spent = 0n
71
73
  channel = entry
72
74
  },
73
75
  })
@@ -81,9 +83,16 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
81
83
  },
82
84
  })
83
85
 
86
+ function updateSpentFromReceipt(receipt: SessionReceipt | null | undefined) {
87
+ if (!receipt || receipt.channelId !== channel?.channelId) return
88
+ const next = BigInt(receipt.spent)
89
+ spent = spent > next ? spent : next
90
+ }
91
+
84
92
  function toPaymentResponse(response: Response): PaymentResponse {
85
93
  const receiptHeader = response.headers.get('Payment-Receipt')
86
94
  const receipt = receiptHeader ? deserializeSessionReceipt(receiptHeader) : null
95
+ updateSpentFromReceipt(receipt)
87
96
  return Object.assign(response, {
88
97
  receipt,
89
98
  challenge: lastChallenge,
@@ -216,6 +225,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
216
225
  }
217
226
 
218
227
  case 'payment-receipt':
228
+ updateSpentFromReceipt(event.data)
219
229
  onReceipt?.(event.data)
220
230
  break
221
231
  }
@@ -237,7 +247,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
237
247
  context: {
238
248
  action: 'close',
239
249
  channelId: channel.channelId,
240
- cumulativeAmountRaw: channel.cumulativeAmount.toString(),
250
+ cumulativeAmountRaw: spent.toString(),
241
251
  },
242
252
  })
243
253
 
@@ -0,0 +1,6 @@
1
+ // TODO: Add `isEqual` to `TempoAddress`.
2
+ import type { TempoAddress } from 'ox/tempo'
3
+
4
+ export function isEqual(a: TempoAddress.Address, b: TempoAddress.Address) {
5
+ return a.toLowerCase() === b.toLowerCase()
6
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Address, Client } from 'viem'
2
- import { isAddressEqual } from 'viem'
3
2
  import { readContract } from 'viem/actions'
4
3
  import { Actions, Addresses } from 'viem/tempo'
4
+ import * as TempoAddress from './address.js'
5
5
  import * as defaults from './defaults.js'
6
6
 
7
7
  /** Basis-point denominator (100% = 10 000 bps). */
@@ -26,7 +26,7 @@ export async function findCalls(
26
26
  ): Promise<findCalls.ReturnType> {
27
27
  const { account, amountOut, tokenOut, tokenIn, slippage } = parameters
28
28
 
29
- const candidates = tokenIn.filter((t) => !isAddressEqual(t, tokenOut))
29
+ const candidates = tokenIn.filter((t) => !TempoAddress.isEqual(t, tokenOut))
30
30
 
31
31
  const balanceResults = await Promise.allSettled([
32
32
  readContract(client, Actions.token.getBalance.call({ account, token: tokenOut }) as never),
@@ -108,7 +108,7 @@ export function resolve(
108
108
  const tokenIn = value.tokenIn
109
109
  ? [
110
110
  ...value.tokenIn,
111
- ...defaultCurrencies.filter((d) => !value.tokenIn!.some((c) => isAddressEqual(c, d))),
111
+ ...defaultCurrencies.filter((d) => !value.tokenIn!.some((c) => TempoAddress.isEqual(c, d))),
112
112
  ]
113
113
  : defaultCurrencies
114
114
  return {
@@ -1,7 +1,18 @@
1
- import { decodeFunctionData, isAddressEqual } from 'viem'
1
+ import type { TempoAddress } from 'ox/tempo'
2
+ import { TxEnvelopeTempo } from 'ox/tempo'
3
+ import { decodeFunctionData } from 'viem'
2
4
  import { Abis, Addresses } from 'viem/tempo'
5
+ import * as TempoAddress_internal from './address.js'
3
6
  import * as Selectors from './selectors.js'
4
7
 
8
+ /** Returns true if the serialized transaction has a Tempo envelope prefix. */
9
+ export function isTempoTransaction(serialized: string | undefined): boolean {
10
+ return (
11
+ serialized?.startsWith(TxEnvelopeTempo.serializedType) === true ||
12
+ serialized?.startsWith(TxEnvelopeTempo.feePayerMagic) === true
13
+ )
14
+ }
15
+
5
16
  /**
6
17
  * Allowed call patterns for fee-payer sponsored transactions.
7
18
  * Each inner array is an ordered list of function selectors.
@@ -15,7 +26,7 @@ export const callScopes = [
15
26
 
16
27
  /** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
17
28
  export function validateCalls(
18
- calls: readonly { data?: `0x${string}` | undefined; to?: `0x${string}` | undefined }[],
29
+ calls: readonly { data?: `0x${string}` | undefined; to?: TempoAddress.Address | undefined }[],
19
30
  details: Record<string, string>,
20
31
  ) {
21
32
  const callSelectors = calls.map((c) => c.data?.slice(0, 10))
@@ -31,11 +42,14 @@ export function validateCalls(
31
42
  const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
32
43
  if (approveCall) {
33
44
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: approveCall.data! })
34
- if (!isAddressEqual((args as [`0x${string}`])[0]!, Addresses.stablecoinDex))
45
+ if (!TempoAddress_internal.isEqual((args as [`0x${string}`])[0]!, Addresses.stablecoinDex))
35
46
  throw new FeePayerValidationError('approve spender is not the DEX', details)
36
47
  }
37
48
  const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
38
- if (buyCall && (!buyCall.to || !isAddressEqual(buyCall.to, Addresses.stablecoinDex)))
49
+ if (
50
+ buyCall &&
51
+ (!buyCall.to || !TempoAddress_internal.isEqual(buyCall.to, Addresses.stablecoinDex))
52
+ )
39
53
  throw new FeePayerValidationError('buy target is not the DEX', details)
40
54
  }
41
55