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.
- package/CHANGELOG.md +23 -0
- package/dist/Method.d.ts +5 -2
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js +8 -2
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +17 -10
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Request.js +5 -1
- package/dist/server/Request.js.map +1 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +4 -0
- package/dist/server/Transport.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +4 -2
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +20 -10
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +4 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +99 -23
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +6 -0
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +4 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +79 -48
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +0 -7
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +84 -13
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +5 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +202 -63
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +1 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +38 -15
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/package.json +2 -2
- package/src/Method.ts +5 -2
- package/src/internal/changeset.test.ts +106 -0
- package/src/mcp-sdk/client/McpClient.integration.test.ts +634 -0
- package/src/mcp-sdk/server/Transport.test.ts +1 -0
- package/src/mcp-sdk/server/Transport.ts +10 -2
- package/src/proxy/Proxy.test.ts +149 -1
- package/src/server/Mppx.test.ts +120 -0
- package/src/server/Mppx.ts +27 -11
- package/src/server/Request.test.ts +46 -1
- package/src/server/Request.ts +6 -1
- package/src/server/Transport.test.ts +2 -0
- package/src/server/Transport.ts +4 -0
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/Methods.test.ts +13 -0
- package/src/tempo/Methods.ts +23 -16
- package/src/tempo/client/SessionManager.ts +32 -9
- package/src/tempo/internal/fee-payer.test.ts +88 -16
- package/src/tempo/internal/fee-payer.ts +118 -23
- package/src/tempo/server/Charge.test.ts +73 -0
- package/src/tempo/server/Charge.ts +6 -0
- package/src/tempo/server/Session.test.ts +934 -47
- package/src/tempo/server/Session.ts +100 -52
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/transport.test.ts +321 -10
- package/src/tempo/server/internal/transport.ts +101 -14
- package/src/tempo/session/Chain.test.ts +225 -2
- package/src/tempo/session/Chain.ts +250 -65
- package/src/tempo/session/ChannelStore.test.ts +23 -0
- package/src/tempo/session/ChannelStore.ts +46 -13
- 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
|
-
//
|
|
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:
|
|
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
|
-
|
|
250
|
+
const gasLimit = gas
|
|
251
|
+
if (gasLimit > policy.maxGas)
|
|
165
252
|
fail('fee-sponsored transaction gas exceeds sponsor policy', {
|
|
166
|
-
gas:
|
|
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
|
-
|
|
258
|
+
const maxFeePerGasValue = maxFeePerGas
|
|
259
|
+
if (maxFeePerGasValue > policy.maxFeePerGas)
|
|
172
260
|
fail('fee-sponsored transaction maxFeePerGas exceeds sponsor policy', {
|
|
173
|
-
maxFeePerGas:
|
|
261
|
+
maxFeePerGas: maxFeePerGasValue.toString(),
|
|
174
262
|
})
|
|
175
263
|
|
|
176
|
-
const maxTotalFee =
|
|
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:
|
|
180
|
-
maxFeePerGas:
|
|
267
|
+
gas: gasLimit.toString(),
|
|
268
|
+
maxFeePerGas: maxFeePerGasValue.toString(),
|
|
181
269
|
totalFee: maxTotalFee.toString(),
|
|
182
270
|
})
|
|
183
271
|
|
|
184
|
-
if (maxPriorityFeePerGas !== undefined && maxPriorityFeePerGas >
|
|
272
|
+
if (maxPriorityFeePerGas !== undefined && maxPriorityFeePerGas > maxFeePerGasValue)
|
|
185
273
|
fail('fee-sponsored transaction maxPriorityFeePerGas exceeds maxFeePerGas', {
|
|
186
|
-
maxFeePerGas:
|
|
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 (
|
|
288
|
+
if (validBeforeValue <= nowSeconds)
|
|
200
289
|
fail('fee-sponsored transaction has already expired', {
|
|
201
|
-
validBefore: String(
|
|
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 (
|
|
300
|
+
if (validBeforeValue > maxValidBefore)
|
|
212
301
|
fail('fee-sponsored transaction validity window exceeds sponsor policy', {
|
|
213
|
-
validBefore: String(
|
|
302
|
+
validBefore: String(validBeforeValue),
|
|
214
303
|
})
|
|
215
304
|
|
|
216
|
-
|
|
305
|
+
const normalizedFeeToken = (() => {
|
|
306
|
+
if (feeToken === undefined) return undefined
|
|
217
307
|
if (typeof feeToken !== 'string') fail('fee-sponsored transaction feeToken is invalid')
|
|
218
|
-
|
|
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
|
-
...(
|
|
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
|
})
|