mppx 0.5.13 → 0.5.16

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 (83) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/Method.d.ts +5 -2
  3. package/dist/Method.d.ts.map +1 -1
  4. package/dist/Method.js.map +1 -1
  5. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  6. package/dist/mcp-sdk/server/Transport.js +8 -2
  7. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  8. package/dist/server/Mppx.d.ts.map +1 -1
  9. package/dist/server/Mppx.js +17 -10
  10. package/dist/server/Mppx.js.map +1 -1
  11. package/dist/server/Request.js +5 -1
  12. package/dist/server/Request.js.map +1 -1
  13. package/dist/server/Transport.d.ts.map +1 -1
  14. package/dist/server/Transport.js +4 -0
  15. package/dist/server/Transport.js.map +1 -1
  16. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  17. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  18. package/dist/stripe/server/internal/html.gen.js +1 -1
  19. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  20. package/dist/tempo/Methods.d.ts.map +1 -1
  21. package/dist/tempo/Methods.js +4 -2
  22. package/dist/tempo/Methods.js.map +1 -1
  23. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  24. package/dist/tempo/client/SessionManager.js +20 -10
  25. package/dist/tempo/client/SessionManager.js.map +1 -1
  26. package/dist/tempo/internal/fee-payer.d.ts +4 -1
  27. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  28. package/dist/tempo/internal/fee-payer.js +99 -23
  29. package/dist/tempo/internal/fee-payer.js.map +1 -1
  30. package/dist/tempo/server/Charge.d.ts.map +1 -1
  31. package/dist/tempo/server/Charge.js +6 -0
  32. package/dist/tempo/server/Charge.js.map +1 -1
  33. package/dist/tempo/server/Session.d.ts +4 -0
  34. package/dist/tempo/server/Session.d.ts.map +1 -1
  35. package/dist/tempo/server/Session.js +79 -48
  36. package/dist/tempo/server/Session.js.map +1 -1
  37. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  38. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  39. package/dist/tempo/server/internal/html.gen.js +1 -1
  40. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  41. package/dist/tempo/server/internal/transport.d.ts +0 -7
  42. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  43. package/dist/tempo/server/internal/transport.js +84 -13
  44. package/dist/tempo/server/internal/transport.js.map +1 -1
  45. package/dist/tempo/session/Chain.d.ts +5 -0
  46. package/dist/tempo/session/Chain.d.ts.map +1 -1
  47. package/dist/tempo/session/Chain.js +202 -63
  48. package/dist/tempo/session/Chain.js.map +1 -1
  49. package/dist/tempo/session/ChannelStore.d.ts +1 -0
  50. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  51. package/dist/tempo/session/ChannelStore.js +38 -15
  52. package/dist/tempo/session/ChannelStore.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/Method.ts +5 -2
  55. package/src/internal/changeset.test.ts +106 -0
  56. package/src/mcp-sdk/client/McpClient.integration.test.ts +634 -0
  57. package/src/mcp-sdk/server/Transport.test.ts +1 -0
  58. package/src/mcp-sdk/server/Transport.ts +10 -2
  59. package/src/proxy/Proxy.test.ts +149 -1
  60. package/src/server/Mppx.test.ts +120 -0
  61. package/src/server/Mppx.ts +27 -11
  62. package/src/server/Request.test.ts +46 -1
  63. package/src/server/Request.ts +6 -1
  64. package/src/server/Transport.test.ts +2 -0
  65. package/src/server/Transport.ts +4 -0
  66. package/src/stripe/server/internal/html.gen.ts +1 -1
  67. package/src/tempo/Methods.test.ts +13 -0
  68. package/src/tempo/Methods.ts +23 -16
  69. package/src/tempo/client/SessionManager.ts +32 -9
  70. package/src/tempo/internal/fee-payer.test.ts +88 -16
  71. package/src/tempo/internal/fee-payer.ts +118 -23
  72. package/src/tempo/server/Charge.test.ts +73 -0
  73. package/src/tempo/server/Charge.ts +6 -0
  74. package/src/tempo/server/Session.test.ts +934 -47
  75. package/src/tempo/server/Session.ts +100 -52
  76. package/src/tempo/server/internal/html.gen.ts +1 -1
  77. package/src/tempo/server/internal/transport.test.ts +321 -10
  78. package/src/tempo/server/internal/transport.ts +101 -14
  79. package/src/tempo/session/Chain.test.ts +225 -2
  80. package/src/tempo/session/Chain.ts +250 -65
  81. package/src/tempo/session/ChannelStore.test.ts +23 -0
  82. package/src/tempo/session/ChannelStore.ts +46 -13
  83. package/src/viem/Client.test.ts +52 -1
@@ -35,6 +35,55 @@ export type Policy = {
35
35
  maxValidityWindowSeconds: number
36
36
  }
37
37
 
38
+ // Reuse the exact object shape returned by `Transaction.deserialize()`.
39
+ // `typeof Transaction` gets the module value type, `['deserialize']` picks the
40
+ // deserialize function off that module, and `ReturnType<...>` asks TypeScript
41
+ // for that function's return type so this helper stays aligned with upstream
42
+ // Tempo transaction fields.
43
+ type SponsoredTransaction = ReturnType<(typeof Transaction)['deserialize']>
44
+
45
+ const preservedTransactionKeys = [
46
+ 'accessList',
47
+ 'calls',
48
+ 'chainId',
49
+ 'feeToken',
50
+ 'from',
51
+ 'gas',
52
+ 'keyAuthorization',
53
+ 'maxFeePerGas',
54
+ 'maxPriorityFeePerGas',
55
+ 'nonce',
56
+ 'nonceKey',
57
+ 'signature',
58
+ 'validAfter',
59
+ 'validBefore',
60
+ ] as const satisfies readonly (keyof SponsoredTransaction)[]
61
+
62
+ const rejectedTransactionKeys = [
63
+ 'blobVersionedHashes',
64
+ 'blobs',
65
+ 'data',
66
+ 'feePayerSignature',
67
+ 'gasPrice',
68
+ 'kzg',
69
+ 'maxFeePerBlobGas',
70
+ 'r',
71
+ 's',
72
+ 'sidecars',
73
+ 'to',
74
+ 'v',
75
+ 'value',
76
+ 'yParity',
77
+ ] as const
78
+
79
+ const rewrittenTransactionKeys = ['type'] as const
80
+
81
+ const supportedTransactionKeys = new Set<string>([
82
+ ...preservedTransactionKeys,
83
+ ...rejectedTransactionKeys,
84
+ ...rewrittenTransactionKeys,
85
+ ])
86
+
38
87
  /**
39
88
  * maxTotalFee must be high enough to cover `transferWithMemo` and
40
89
  * swap transactions at peak gas prices. Bumped from 0.01 ETH in #327.
@@ -98,14 +147,25 @@ export function validateCalls(
98
147
  throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
99
148
  }
100
149
 
101
- // Validate approve spender and buy target are the DEX.
150
+ // Bind the swap approval to the token the DEX call will actually spend.
151
+ const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
152
+ const buyArgs = buyCall
153
+ ? (decodeFunctionData({ abi: Abis.stablecoinDex, data: buyCall.data! }).args as [
154
+ `0x${string}`,
155
+ `0x${string}`,
156
+ bigint,
157
+ bigint,
158
+ ])
159
+ : undefined
160
+
102
161
  const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
103
162
  if (approveCall) {
104
163
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: approveCall.data! })
164
+ if (!approveCall.to || (buyArgs && !TempoAddress_internal.isEqual(approveCall.to, buyArgs[0])))
165
+ throw new FeePayerValidationError('approve target does not match swap tokenIn', details)
105
166
  if (!TempoAddress_internal.isEqual((args as [`0x${string}`])[0]!, Addresses.stablecoinDex))
106
167
  throw new FeePayerValidationError('approve spender is not the DEX', details)
107
168
  }
108
- const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
109
169
  if (
110
170
  buyCall &&
111
171
  (!buyCall.to || !TempoAddress_internal.isEqual(buyCall.to, Addresses.stablecoinDex))
@@ -121,7 +181,7 @@ export function prepareSponsoredTransaction(parameters: {
121
181
  expectedFeeToken?: TempoAddress.Address | undefined
122
182
  now?: Date | undefined
123
183
  policy?: Partial<Policy> | undefined
124
- transaction: ReturnType<(typeof Transaction)['deserialize']>
184
+ transaction: SponsoredTransaction
125
185
  }) {
126
186
  const {
127
187
  account,
@@ -134,6 +194,7 @@ export function prepareSponsoredTransaction(parameters: {
134
194
  transaction,
135
195
  } = parameters
136
196
  const policy = getPolicy(chainId, policyOverrides)
197
+ const transactionRecord = transaction as Record<string, unknown>
137
198
 
138
199
  const {
139
200
  accessList,
@@ -142,6 +203,7 @@ export function prepareSponsoredTransaction(parameters: {
142
203
  feeToken,
143
204
  from,
144
205
  gas,
206
+ keyAuthorization,
145
207
  maxFeePerGas,
146
208
  maxPriorityFeePerGas,
147
209
  nonce,
@@ -155,35 +217,61 @@ export function prepareSponsoredTransaction(parameters: {
155
217
  throw new FeePayerValidationError(reason, { ...details, ...extra })
156
218
  }
157
219
 
220
+ const unsupportedKeys = Object.entries(transaction).flatMap(([key, value]) => {
221
+ if (value === undefined) return []
222
+ if (supportedTransactionKeys.has(key)) return []
223
+ return [key]
224
+ })
225
+ if (unsupportedKeys.length > 0)
226
+ fail('fee-sponsored transaction contains unsupported fields', {
227
+ unsupportedFields: unsupportedKeys.join(', '),
228
+ })
229
+
230
+ const rejectedKeys = rejectedTransactionKeys.filter((key) => {
231
+ const value = transactionRecord[key]
232
+ return value !== undefined && value !== null
233
+ })
234
+ if (rejectedKeys.length > 0)
235
+ fail('fee-sponsored transaction contains rejected fields', {
236
+ rejectedFields: rejectedKeys.join(', '),
237
+ })
238
+
239
+ if (transaction.type !== undefined && transaction.type !== 'tempo')
240
+ fail('fee-sponsored transaction type is invalid', {
241
+ type: String(transaction.type),
242
+ })
243
+
158
244
  if (transactionChainId !== chainId)
159
245
  fail('fee-sponsored transaction chainId does not match challenge', {
160
246
  chainId: String(transactionChainId),
161
247
  })
162
248
 
163
249
  if (gas === undefined || gas <= 0n) fail('fee-sponsored transaction must declare gas')
164
- if (gas > policy.maxGas)
250
+ const gasLimit = gas
251
+ if (gasLimit > policy.maxGas)
165
252
  fail('fee-sponsored transaction gas exceeds sponsor policy', {
166
- gas: gas.toString(),
253
+ gas: gasLimit.toString(),
167
254
  })
168
255
 
169
256
  if (maxFeePerGas === undefined || maxFeePerGas <= 0n)
170
257
  fail('fee-sponsored transaction must declare maxFeePerGas')
171
- if (maxFeePerGas > policy.maxFeePerGas)
258
+ const maxFeePerGasValue = maxFeePerGas
259
+ if (maxFeePerGasValue > policy.maxFeePerGas)
172
260
  fail('fee-sponsored transaction maxFeePerGas exceeds sponsor policy', {
173
- maxFeePerGas: maxFeePerGas.toString(),
261
+ maxFeePerGas: maxFeePerGasValue.toString(),
174
262
  })
175
263
 
176
- const maxTotalFee = gas * maxFeePerGas
264
+ const maxTotalFee = gasLimit * maxFeePerGasValue
177
265
  if (maxTotalFee > policy.maxTotalFee)
178
266
  fail('fee-sponsored transaction total fee budget exceeds sponsor policy', {
179
- gas: gas.toString(),
180
- maxFeePerGas: maxFeePerGas.toString(),
267
+ gas: gasLimit.toString(),
268
+ maxFeePerGas: maxFeePerGasValue.toString(),
181
269
  totalFee: maxTotalFee.toString(),
182
270
  })
183
271
 
184
- if (maxPriorityFeePerGas !== undefined && maxPriorityFeePerGas > maxFeePerGas)
272
+ if (maxPriorityFeePerGas !== undefined && maxPriorityFeePerGas > maxFeePerGasValue)
185
273
  fail('fee-sponsored transaction maxPriorityFeePerGas exceeds maxFeePerGas', {
186
- maxFeePerGas: maxFeePerGas.toString(),
274
+ maxFeePerGas: maxFeePerGasValue.toString(),
187
275
  maxPriorityFeePerGas: maxPriorityFeePerGas.toString(),
188
276
  })
189
277
  if (maxPriorityFeePerGas !== undefined && maxPriorityFeePerGas > policy.maxPriorityFeePerGas)
@@ -194,11 +282,12 @@ export function prepareSponsoredTransaction(parameters: {
194
282
  if (nonceKey === undefined) fail('fee-sponsored transaction must use an expiring nonce')
195
283
  if (validBefore === undefined)
196
284
  fail('fee-sponsored transaction must declare validBefore for the expiring nonce')
285
+ const validBeforeValue = validBefore
197
286
 
198
287
  const nowSeconds = Math.floor(now.getTime() / 1_000)
199
- if (validBefore <= nowSeconds)
288
+ if (validBeforeValue <= nowSeconds)
200
289
  fail('fee-sponsored transaction has already expired', {
201
- validBefore: String(validBefore),
290
+ validBefore: String(validBeforeValue),
202
291
  })
203
292
 
204
293
  const challengeExpirySeconds = challengeExpires
@@ -208,16 +297,21 @@ export function prepareSponsoredTransaction(parameters: {
208
297
  nowSeconds + policy.maxValidityWindowSeconds,
209
298
  challengeExpirySeconds ? challengeExpirySeconds + 60 : Number.MAX_SAFE_INTEGER,
210
299
  )
211
- if (validBefore > maxValidBefore)
300
+ if (validBeforeValue > maxValidBefore)
212
301
  fail('fee-sponsored transaction validity window exceeds sponsor policy', {
213
- validBefore: String(validBefore),
302
+ validBefore: String(validBeforeValue),
214
303
  })
215
304
 
216
- if (feeToken !== undefined) {
305
+ const normalizedFeeToken = (() => {
306
+ if (feeToken === undefined) return undefined
217
307
  if (typeof feeToken !== 'string') fail('fee-sponsored transaction feeToken is invalid')
218
- if (expectedFeeToken && !TempoAddress_internal.isEqual(feeToken, expectedFeeToken))
308
+ return feeToken
309
+ })()
310
+
311
+ if (normalizedFeeToken !== undefined) {
312
+ if (expectedFeeToken && !TempoAddress_internal.isEqual(normalizedFeeToken, expectedFeeToken))
219
313
  fail('fee-sponsored transaction feeToken is not allowed', {
220
- feeToken,
314
+ feeToken: normalizedFeeToken,
221
315
  })
222
316
  }
223
317
 
@@ -227,17 +321,18 @@ export function prepareSponsoredTransaction(parameters: {
227
321
  calls,
228
322
  chainId: transactionChainId,
229
323
  feePayer: account,
230
- ...(feeToken ? { feeToken } : {}),
324
+ ...(normalizedFeeToken ? { feeToken: normalizedFeeToken } : {}),
231
325
  ...(from ? { from } : {}),
232
- gas,
326
+ gas: gasLimit,
327
+ ...(keyAuthorization !== undefined ? { keyAuthorization } : {}),
233
328
  ...(nonce !== undefined ? { nonce } : {}),
234
- maxFeePerGas,
329
+ maxFeePerGas: maxFeePerGasValue,
235
330
  ...(maxPriorityFeePerGas !== undefined ? { maxPriorityFeePerGas } : {}),
236
331
  nonceKey,
237
332
  ...(signature ? { signature } : {}),
238
333
  type: 'tempo' as const,
239
334
  ...(validAfter !== undefined ? { validAfter } : {}),
240
- validBefore,
335
+ validBefore: validBeforeValue,
241
336
  } satisfies ReturnType<(typeof Transaction)['deserialize']> & {
242
337
  account: Account
243
338
  feePayer: Account
@@ -1504,6 +1504,79 @@ describe('tempo', () => {
1504
1504
  httpServer.close()
1505
1505
  })
1506
1506
 
1507
+ test('behavior: fee payer simulates before broadcasting in confirmation mode', async () => {
1508
+ const rpcMethods: string[] = []
1509
+ const interceptingClient = createClient({
1510
+ account: accounts[0],
1511
+ chain: client.chain,
1512
+ transport: custom({
1513
+ async request(args: any) {
1514
+ rpcMethods.push(args.method)
1515
+ return client.transport.request(args)
1516
+ },
1517
+ }),
1518
+ })
1519
+
1520
+ const serverWithRpcTrace = Mppx_server.create({
1521
+ methods: [
1522
+ tempo_server.charge({
1523
+ getClient() {
1524
+ return interceptingClient
1525
+ },
1526
+ currency: asset,
1527
+ account: accounts[0],
1528
+ }),
1529
+ ],
1530
+ realm,
1531
+ secretKey,
1532
+ })
1533
+
1534
+ const mppx = Mppx_client.create({
1535
+ polyfill: false,
1536
+ methods: [
1537
+ tempo_client({
1538
+ account: accounts[1],
1539
+ getClient() {
1540
+ return client
1541
+ },
1542
+ }),
1543
+ ],
1544
+ })
1545
+
1546
+ const httpServer = await Http.createServer(async (req, res) => {
1547
+ const result = await Mppx_server.toNodeListener(
1548
+ serverWithRpcTrace.charge({
1549
+ feePayer: accounts[0],
1550
+ amount: '1',
1551
+ currency: asset,
1552
+ recipient: accounts[0].address,
1553
+ }),
1554
+ )(req, res)
1555
+ if (result.status === 402) return
1556
+ res.end('OK')
1557
+ })
1558
+
1559
+ const challengeResponse = await fetch(httpServer.url)
1560
+ expect(challengeResponse.status).toBe(402)
1561
+
1562
+ const credential = await mppx.createCredential(challengeResponse)
1563
+ rpcMethods.length = 0
1564
+
1565
+ const authResponse = await fetch(httpServer.url, {
1566
+ headers: { Authorization: credential },
1567
+ })
1568
+ expect(authResponse.status).toBe(200)
1569
+
1570
+ const broadcastIndex = rpcMethods.indexOf('eth_sendRawTransactionSync')
1571
+ const simulationIndex = rpcMethods.indexOf('eth_call')
1572
+
1573
+ expect(broadcastIndex).toBeGreaterThan(-1)
1574
+ expect(simulationIndex).toBeGreaterThan(-1)
1575
+ expect(simulationIndex).toBeLessThan(broadcastIndex)
1576
+
1577
+ httpServer.close()
1578
+ })
1579
+
1507
1580
  test('behavior: fee payer with splits', async () => {
1508
1581
  const mppx = Mppx_client.create({
1509
1582
  polyfill: false,
@@ -326,6 +326,12 @@ export function charge<const parameters extends charge.Parameters>(
326
326
  })()
327
327
 
328
328
  if (waitForConfirmation) {
329
+ await viem_call(client, {
330
+ ...transaction,
331
+ account: transaction.from,
332
+ feeToken: resolvedFeeToken,
333
+ calls: transaction.calls,
334
+ } as never)
329
335
  const receipt = await sendRawTransactionSync(client, {
330
336
  serializedTransaction: serializedTransaction_final,
331
337
  })