mppx 0.3.9 → 0.3.12

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 (95) 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/client/Mppx.d.ts +1 -1
  10. package/dist/client/Mppx.d.ts.map +1 -1
  11. package/dist/client/internal/Fetch.d.ts +1 -1
  12. package/dist/client/internal/Fetch.d.ts.map +1 -1
  13. package/dist/client/internal/Fetch.js +23 -4
  14. package/dist/client/internal/Fetch.js.map +1 -1
  15. package/dist/internal/constantTimeEqual.d.ts.map +1 -1
  16. package/dist/internal/constantTimeEqual.js +4 -6
  17. package/dist/internal/constantTimeEqual.js.map +1 -1
  18. package/dist/internal/env.d.ts +2 -2
  19. package/dist/internal/env.d.ts.map +1 -1
  20. package/dist/internal/env.js +1 -2
  21. package/dist/internal/env.js.map +1 -1
  22. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  23. package/dist/middlewares/internal/mppx.js +6 -2
  24. package/dist/middlewares/internal/mppx.js.map +1 -1
  25. package/dist/server/Mppx.d.ts +13 -3
  26. package/dist/server/Mppx.d.ts.map +1 -1
  27. package/dist/server/Mppx.js +46 -3
  28. package/dist/server/Mppx.js.map +1 -1
  29. package/dist/tempo/client/Charge.d.ts +10 -0
  30. package/dist/tempo/client/Charge.d.ts.map +1 -1
  31. package/dist/tempo/client/Charge.js +23 -9
  32. package/dist/tempo/client/Charge.js.map +1 -1
  33. package/dist/tempo/client/Methods.d.ts +1 -0
  34. package/dist/tempo/client/Methods.d.ts.map +1 -1
  35. package/dist/tempo/internal/auto-swap.d.ts +49 -0
  36. package/dist/tempo/internal/auto-swap.d.ts.map +1 -0
  37. package/dist/tempo/internal/auto-swap.js +89 -0
  38. package/dist/tempo/internal/auto-swap.js.map +1 -0
  39. package/dist/tempo/internal/fee-payer.d.ts +15 -0
  40. package/dist/tempo/internal/fee-payer.d.ts.map +1 -0
  41. package/dist/tempo/internal/fee-payer.js +41 -0
  42. package/dist/tempo/internal/fee-payer.js.map +1 -0
  43. package/dist/tempo/internal/selectors.d.ts +5 -0
  44. package/dist/tempo/internal/selectors.d.ts.map +1 -0
  45. package/dist/tempo/internal/selectors.js +7 -0
  46. package/dist/tempo/internal/selectors.js.map +1 -0
  47. package/dist/tempo/internal/simulate.d.ts +21 -0
  48. package/dist/tempo/internal/simulate.d.ts.map +1 -0
  49. package/dist/tempo/internal/simulate.js +31 -0
  50. package/dist/tempo/internal/simulate.js.map +1 -0
  51. package/dist/tempo/server/Charge.d.ts +12 -0
  52. package/dist/tempo/server/Charge.d.ts.map +1 -1
  53. package/dist/tempo/server/Charge.js +36 -12
  54. package/dist/tempo/server/Charge.js.map +1 -1
  55. package/dist/tempo/server/Session.d.ts +14 -0
  56. package/dist/tempo/server/Session.d.ts.map +1 -1
  57. package/dist/tempo/server/Session.js +59 -40
  58. package/dist/tempo/server/Session.js.map +1 -1
  59. package/dist/tempo/session/Chain.d.ts +3 -0
  60. package/dist/tempo/session/Chain.d.ts.map +1 -1
  61. package/dist/tempo/session/Chain.js +27 -6
  62. package/dist/tempo/session/Chain.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/Challenge.ts +2 -0
  65. package/src/Errors.test.ts +43 -18
  66. package/src/Errors.ts +1 -4
  67. package/src/client/Mppx.test-d.ts +28 -0
  68. package/src/client/Mppx.test.ts +1 -0
  69. package/src/client/Mppx.ts +3 -3
  70. package/src/client/internal/Fetch.test.ts +410 -0
  71. package/src/client/internal/Fetch.ts +25 -7
  72. package/src/internal/constantTimeEqual.ts +5 -4
  73. package/src/internal/env.test.ts +2 -2
  74. package/src/internal/env.ts +4 -5
  75. package/src/middlewares/express.test.ts +5 -0
  76. package/src/middlewares/hono.test.ts +5 -0
  77. package/src/middlewares/internal/mppx.ts +5 -2
  78. package/src/middlewares/nextjs.test.ts +5 -0
  79. package/src/proxy/Proxy.test.ts +3 -0
  80. package/src/proxy/services/openai.test.ts +3 -0
  81. package/src/server/Mppx.test.ts +93 -2
  82. package/src/server/Mppx.ts +81 -6
  83. package/src/tempo/client/Charge.ts +40 -9
  84. package/src/tempo/internal/auto-swap.test.ts +113 -0
  85. package/src/tempo/internal/auto-swap.ts +141 -0
  86. package/src/tempo/internal/fee-payer.test.ts +223 -0
  87. package/src/tempo/internal/fee-payer.ts +53 -0
  88. package/src/tempo/internal/selectors.ts +10 -0
  89. package/src/tempo/internal/simulate.ts +49 -0
  90. package/src/tempo/server/Charge.test.ts +436 -3
  91. package/src/tempo/server/Charge.ts +52 -23
  92. package/src/tempo/server/Session.test.ts +49 -0
  93. package/src/tempo/server/Session.ts +76 -34
  94. package/src/tempo/session/Chain.test.ts +36 -0
  95. package/src/tempo/session/Chain.ts +38 -2
@@ -0,0 +1,223 @@
1
+ import { encodeFunctionData } from 'viem'
2
+ import { Abis, Addresses } from 'viem/tempo'
3
+ import { describe, expect, test } from 'vitest'
4
+ import { callScopes, FeePayerValidationError, validateCalls } from './fee-payer.js'
5
+ import * as Selectors from './selectors.js'
6
+
7
+ const details = { amount: '1', currency: '0x01', recipient: '0x02' }
8
+ const bogus = '0x0000000000000000000000000000000000000001' as const
9
+
10
+ describe('callScopes', () => {
11
+ test('has 4 allowed patterns', () => {
12
+ expect(callScopes).toHaveLength(4)
13
+ })
14
+
15
+ test('patterns use correct selectors', () => {
16
+ expect(callScopes).toEqual([
17
+ [Selectors.transfer],
18
+ [Selectors.transferWithMemo],
19
+ [Selectors.approve, Selectors.swapExactAmountOut, Selectors.transfer],
20
+ [Selectors.approve, Selectors.swapExactAmountOut, Selectors.transferWithMemo],
21
+ ])
22
+ })
23
+ })
24
+
25
+ describe('validateCalls', () => {
26
+ test('accepts single transfer', () => {
27
+ expect(() =>
28
+ validateCalls(
29
+ [
30
+ {
31
+ data: encodeFunctionData({
32
+ abi: Abis.tip20,
33
+ functionName: 'transfer',
34
+ args: [bogus, 100n],
35
+ }),
36
+ },
37
+ ],
38
+ details,
39
+ ),
40
+ ).not.toThrow()
41
+ })
42
+
43
+ test('accepts approve + buy + transfer', () => {
44
+ const swapSelector = Selectors.swapExactAmountOut
45
+ expect(() =>
46
+ validateCalls(
47
+ [
48
+ {
49
+ data: encodeFunctionData({
50
+ abi: Abis.tip20,
51
+ functionName: 'approve',
52
+ args: [Addresses.stablecoinDex, 100n],
53
+ }),
54
+ },
55
+ {
56
+ to: Addresses.stablecoinDex,
57
+ data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
58
+ },
59
+ {
60
+ data: encodeFunctionData({
61
+ abi: Abis.tip20,
62
+ functionName: 'transfer',
63
+ args: [bogus, 100n],
64
+ }),
65
+ },
66
+ ],
67
+ details,
68
+ ),
69
+ ).not.toThrow()
70
+ })
71
+
72
+ test('error: rejects empty calls', () => {
73
+ expect(() => validateCalls([], details)).toThrow(FeePayerValidationError)
74
+ })
75
+
76
+ test('error: rejects unknown selector', () => {
77
+ expect(() => validateCalls([{ data: '0xdeadbeef' as `0x${string}` }], details)).toThrow(
78
+ 'disallowed call pattern',
79
+ )
80
+ })
81
+
82
+ test('error: rejects extra calls beyond allowed patterns', () => {
83
+ const swapSelector = Selectors.swapExactAmountOut
84
+ expect(() =>
85
+ validateCalls(
86
+ [
87
+ {
88
+ data: encodeFunctionData({
89
+ abi: Abis.tip20,
90
+ functionName: 'approve',
91
+ args: [Addresses.stablecoinDex, 100n],
92
+ }),
93
+ },
94
+ {
95
+ to: Addresses.stablecoinDex,
96
+ data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
97
+ },
98
+ {
99
+ data: encodeFunctionData({
100
+ abi: Abis.tip20,
101
+ functionName: 'transfer',
102
+ args: [bogus, 100n],
103
+ }),
104
+ },
105
+ {
106
+ data: encodeFunctionData({
107
+ abi: Abis.tip20,
108
+ functionName: 'transfer',
109
+ args: [bogus, 100n],
110
+ }),
111
+ },
112
+ ],
113
+ details,
114
+ ),
115
+ ).toThrow('disallowed call pattern')
116
+ })
117
+
118
+ test('error: rejects wrong order (transfer before approve + buy)', () => {
119
+ const swapSelector = Selectors.swapExactAmountOut
120
+ expect(() =>
121
+ validateCalls(
122
+ [
123
+ {
124
+ data: encodeFunctionData({
125
+ abi: Abis.tip20,
126
+ functionName: 'transfer',
127
+ args: [bogus, 100n],
128
+ }),
129
+ },
130
+ {
131
+ data: encodeFunctionData({
132
+ abi: Abis.tip20,
133
+ functionName: 'approve',
134
+ args: [Addresses.stablecoinDex, 100n],
135
+ }),
136
+ },
137
+ {
138
+ to: Addresses.stablecoinDex,
139
+ data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
140
+ },
141
+ ],
142
+ details,
143
+ ),
144
+ ).toThrow('disallowed call pattern')
145
+ })
146
+
147
+ test('error: rejects approve with non-DEX spender', () => {
148
+ const swapSelector = Selectors.swapExactAmountOut
149
+ expect(() =>
150
+ validateCalls(
151
+ [
152
+ {
153
+ data: encodeFunctionData({
154
+ abi: Abis.tip20,
155
+ functionName: 'approve',
156
+ args: [bogus, 100n],
157
+ }),
158
+ },
159
+ {
160
+ to: Addresses.stablecoinDex,
161
+ data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
162
+ },
163
+ {
164
+ data: encodeFunctionData({
165
+ abi: Abis.tip20,
166
+ functionName: 'transfer',
167
+ args: [bogus, 100n],
168
+ }),
169
+ },
170
+ ],
171
+ details,
172
+ ),
173
+ ).toThrow('approve spender is not the DEX')
174
+ })
175
+
176
+ test('error: rejects buy targeting non-DEX address', () => {
177
+ const swapSelector = Selectors.swapExactAmountOut
178
+ expect(() =>
179
+ validateCalls(
180
+ [
181
+ {
182
+ data: encodeFunctionData({
183
+ abi: Abis.tip20,
184
+ functionName: 'approve',
185
+ args: [Addresses.stablecoinDex, 100n],
186
+ }),
187
+ },
188
+ { to: bogus, data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}` },
189
+ {
190
+ data: encodeFunctionData({
191
+ abi: Abis.tip20,
192
+ functionName: 'transfer',
193
+ args: [bogus, 100n],
194
+ }),
195
+ },
196
+ ],
197
+ details,
198
+ ),
199
+ ).toThrow('buy target is not the DEX')
200
+ })
201
+
202
+ test('error: rejects approve + buy without transfer', () => {
203
+ const swapSelector = Selectors.swapExactAmountOut
204
+ expect(() =>
205
+ validateCalls(
206
+ [
207
+ {
208
+ data: encodeFunctionData({
209
+ abi: Abis.tip20,
210
+ functionName: 'approve',
211
+ args: [Addresses.stablecoinDex, 100n],
212
+ }),
213
+ },
214
+ {
215
+ to: Addresses.stablecoinDex,
216
+ data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
217
+ },
218
+ ],
219
+ details,
220
+ ),
221
+ ).toThrow('disallowed call pattern')
222
+ })
223
+ })
@@ -0,0 +1,53 @@
1
+ import { decodeFunctionData, isAddressEqual } from 'viem'
2
+ import { Abis, Addresses } from 'viem/tempo'
3
+ import * as Selectors from './selectors.js'
4
+
5
+ /**
6
+ * Allowed call patterns for fee-payer sponsored transactions.
7
+ * Each inner array is an ordered list of function selectors.
8
+ */
9
+ export const callScopes = [
10
+ [Selectors.transfer],
11
+ [Selectors.transferWithMemo],
12
+ [Selectors.approve, Selectors.swapExactAmountOut, Selectors.transfer],
13
+ [Selectors.approve, Selectors.swapExactAmountOut, Selectors.transferWithMemo],
14
+ ]
15
+
16
+ /** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
17
+ export function validateCalls(
18
+ calls: readonly { data?: `0x${string}` | undefined; to?: `0x${string}` | undefined }[],
19
+ details: Record<string, string>,
20
+ ) {
21
+ const callSelectors = calls.map((c) => c.data?.slice(0, 10))
22
+ const allowed = callScopes.some(
23
+ (pattern) =>
24
+ pattern.length === callSelectors.length &&
25
+ pattern.every((sel, i) => sel === callSelectors[i]),
26
+ )
27
+ if (!allowed)
28
+ throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
29
+
30
+ // Validate approve spender and buy target are the DEX.
31
+ const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
32
+ if (approveCall) {
33
+ const { args } = decodeFunctionData({ abi: Abis.tip20, data: approveCall.data! })
34
+ if (!isAddressEqual((args as [`0x${string}`])[0]!, Addresses.stablecoinDex))
35
+ throw new FeePayerValidationError('approve spender is not the DEX', details)
36
+ }
37
+ const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
38
+ if (buyCall && (!buyCall.to || !isAddressEqual(buyCall.to, Addresses.stablecoinDex)))
39
+ throw new FeePayerValidationError('buy target is not the DEX', details)
40
+ }
41
+
42
+ export class FeePayerValidationError extends Error {
43
+ override readonly name = 'FeePayerValidationError'
44
+
45
+ constructor(reason: string, details: Record<string, string>) {
46
+ super(
47
+ [
48
+ `Invalid transaction: ${reason}`,
49
+ ...Object.entries(details).map(([k, v]) => ` - ${k}: ${v}`),
50
+ ].join('\n'),
51
+ )
52
+ }
53
+ }
@@ -0,0 +1,10 @@
1
+ import { AbiItem } from 'ox'
2
+ import { Abis } from 'viem/tempo'
3
+
4
+ export const approve = /*#__PURE__*/ AbiItem.getSelector(Abis.tip20, 'approve')
5
+ export const transfer = /*#__PURE__*/ AbiItem.getSelector(Abis.tip20, 'transfer')
6
+ export const transferWithMemo = /*#__PURE__*/ AbiItem.getSelector(Abis.tip20, 'transferWithMemo')
7
+ export const swapExactAmountOut = /*#__PURE__*/ AbiItem.getSelector(
8
+ Abis.stablecoinDex,
9
+ 'swapExactAmountOut',
10
+ )
@@ -0,0 +1,49 @@
1
+ import type { Address, Client } from 'viem'
2
+
3
+ /**
4
+ * Simulate a Tempo transaction via `eth_estimateGas` to catch reverts
5
+ * (e.g. insufficient balance, invalid calls) before broadcasting.
6
+ */
7
+ export async function simulateTransaction(
8
+ client: Client,
9
+ transaction: {
10
+ from: Address
11
+ chainId: number
12
+ nonce?: number | bigint | undefined
13
+ maxFeePerGas?: bigint | undefined
14
+ maxPriorityFeePerGas?: bigint | undefined
15
+ feeToken?: string | bigint | undefined
16
+ nonceKey?: bigint | undefined
17
+ validBefore?: number | undefined
18
+ calls?: readonly {
19
+ to?: string | undefined
20
+ value?: bigint | undefined
21
+ data?: string | undefined
22
+ }[]
23
+ },
24
+ ): Promise<void> {
25
+ const simCalls = (transaction.calls ?? []).map((c) => ({
26
+ to: c.to,
27
+ value: c.value ? `0x${c.value.toString(16)}` : '0x0',
28
+ input: c.data ?? '0x',
29
+ }))
30
+ await client.request({
31
+ method: 'eth_estimateGas' as never,
32
+ params: [
33
+ {
34
+ from: transaction.from,
35
+ chainId: `0x${transaction.chainId.toString(16)}`,
36
+ nonce: `0x${BigInt(transaction.nonce ?? 0).toString(16)}`,
37
+ gas: '0x2dc6c0', // 3M cap
38
+ maxFeePerGas: `0x${(transaction.maxFeePerGas ?? 0n).toString(16)}`,
39
+ maxPriorityFeePerGas: `0x${(transaction.maxPriorityFeePerGas ?? 0n).toString(16)}`,
40
+ feeToken: transaction.feeToken,
41
+ nonceKey: `0x${(transaction.nonceKey ?? 0n).toString(16)}`,
42
+ calls: simCalls,
43
+ ...(transaction.validBefore
44
+ ? { validBefore: `0x${transaction.validBefore.toString(16)}` }
45
+ : {}),
46
+ },
47
+ ] as never,
48
+ })
49
+ }