mppx 0.5.12 → 0.5.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.
- package/CHANGELOG.md +16 -0
- package/dist/server/internal/html/config.d.ts.map +1 -1
- package/dist/server/internal/html/config.js +8 -1
- package/dist/server/internal/html/config.js.map +1 -1
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +6 -2
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +11 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +14 -2
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +6 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +31 -4
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +17 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +13 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +6 -0
- package/dist/tempo/server/Methods.d.ts.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 +36 -28
- 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/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/server/Transport.test.ts +20 -0
- package/src/server/internal/html/config.ts +9 -1
- package/src/tempo/Methods.test.ts +25 -0
- package/src/tempo/Methods.ts +30 -22
- package/src/tempo/client/Charge.ts +20 -6
- package/src/tempo/internal/fee-payer.test.ts +122 -12
- package/src/tempo/internal/fee-payer.ts +49 -4
- package/src/tempo/server/Charge.test.ts +259 -1
- package/src/tempo/server/Charge.ts +31 -0
- package/src/tempo/server/Session.test.ts +130 -1
- package/src/tempo/server/Session.ts +41 -35
- package/src/tempo/server/internal/html/main.ts +2 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
- 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
|
@@ -39,6 +39,31 @@ describe('charge', () => {
|
|
|
39
39
|
expect(result.success).toBe(true)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
test('schema: validates request with supportedModes', () => {
|
|
43
|
+
const result = Methods.charge.schema.request.safeParse({
|
|
44
|
+
amount: '1',
|
|
45
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
46
|
+
decimals: 6,
|
|
47
|
+
recipient: '0x1234567890abcdef1234567890abcdef12345678',
|
|
48
|
+
supportedModes: ['pull'],
|
|
49
|
+
})
|
|
50
|
+
expect(result.success).toBe(true)
|
|
51
|
+
if (!result.success) return
|
|
52
|
+
|
|
53
|
+
expect(result.data.methodDetails?.supportedModes).toEqual(['pull'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('schema: rejects empty supportedModes', () => {
|
|
57
|
+
const result = Methods.charge.schema.request.safeParse({
|
|
58
|
+
amount: '1',
|
|
59
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
60
|
+
decimals: 6,
|
|
61
|
+
recipient: '0x1234567890abcdef1234567890abcdef12345678',
|
|
62
|
+
supportedModes: [],
|
|
63
|
+
})
|
|
64
|
+
expect(result.success).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
42
67
|
test('schema: validates request with memo', () => {
|
|
43
68
|
const result = Methods.charge.schema.request.safeParse({
|
|
44
69
|
amount: '1',
|
package/src/tempo/Methods.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { parseUnits } from 'viem'
|
|
|
4
4
|
import * as Method from '../Method.js'
|
|
5
5
|
import * as z from '../zod.js'
|
|
6
6
|
|
|
7
|
+
export const chargeModes = ['push', 'pull'] as const
|
|
8
|
+
export type ChargeMode = (typeof chargeModes)[number]
|
|
9
|
+
|
|
7
10
|
const split = z.object({
|
|
8
11
|
amount: z.amount(),
|
|
9
12
|
memo: z.optional(z.hash()),
|
|
@@ -47,6 +50,7 @@ export const charge = Method.from({
|
|
|
47
50
|
memo: z.optional(z.hash()),
|
|
48
51
|
recipient: z.optional(z.string()),
|
|
49
52
|
splits: z.optional(z.array(split).check(z.minLength(1), z.maxLength(10))),
|
|
53
|
+
supportedModes: z.optional(z.array(z.enum(chargeModes)).check(z.minLength(1))),
|
|
50
54
|
})
|
|
51
55
|
.check(
|
|
52
56
|
z.refine(({ amount, decimals, splits }) => {
|
|
@@ -64,28 +68,32 @@ export const charge = Method.from({
|
|
|
64
68
|
)
|
|
65
69
|
}, 'Invalid splits'),
|
|
66
70
|
),
|
|
67
|
-
z.transform(
|
|
68
|
-
...rest
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
z.transform(
|
|
72
|
+
({ amount, chainId, decimals, feePayer, memo, splits, supportedModes, ...rest }) => ({
|
|
73
|
+
...rest,
|
|
74
|
+
amount: parseUnits(amount, decimals).toString(),
|
|
75
|
+
...(chainId !== undefined ||
|
|
76
|
+
feePayer !== undefined ||
|
|
77
|
+
memo !== undefined ||
|
|
78
|
+
splits !== undefined ||
|
|
79
|
+
supportedModes !== undefined
|
|
80
|
+
? {
|
|
81
|
+
methodDetails: {
|
|
82
|
+
...(chainId !== undefined && { chainId }),
|
|
83
|
+
...(feePayer !== undefined && { feePayer }),
|
|
84
|
+
...(memo !== undefined && { memo }),
|
|
85
|
+
...(splits !== undefined && {
|
|
86
|
+
splits: splits.map((split) => ({
|
|
87
|
+
...split,
|
|
88
|
+
amount: parseUnits(split.amount, decimals).toString(),
|
|
89
|
+
})),
|
|
90
|
+
}),
|
|
91
|
+
...(supportedModes !== undefined && { supportedModes }),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
89
97
|
),
|
|
90
98
|
},
|
|
91
99
|
})
|
|
@@ -47,7 +47,7 @@ export function charge(parameters: charge.Parameters = {}) {
|
|
|
47
47
|
context: z.object({
|
|
48
48
|
account: z.optional(z.custom<Account.getResolver.Parameters['account']>()),
|
|
49
49
|
autoSwap: z.optional(z.custom<charge.AutoSwap>()),
|
|
50
|
-
mode: z.optional(z.enum(
|
|
50
|
+
mode: z.optional(z.enum(Methods.chargeModes)),
|
|
51
51
|
}),
|
|
52
52
|
|
|
53
53
|
async createCredential({ challenge, context }) {
|
|
@@ -74,11 +74,7 @@ export function charge(parameters: charge.Parameters = {}) {
|
|
|
74
74
|
})
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const mode =
|
|
78
|
-
context?.mode ?? parameters.mode ?? (account.type === 'json-rpc' ? 'push' : 'pull')
|
|
79
|
-
|
|
80
77
|
const currency = request.currency as Address
|
|
81
|
-
|
|
82
78
|
if (parameters.expectedRecipients) {
|
|
83
79
|
const allowed = new Set(parameters.expectedRecipients.map((a) => a.toLowerCase()))
|
|
84
80
|
const splits = methodDetails?.splits as readonly { recipient: string }[] | undefined
|
|
@@ -89,6 +85,21 @@ export function charge(parameters: charge.Parameters = {}) {
|
|
|
89
85
|
}
|
|
90
86
|
}
|
|
91
87
|
}
|
|
88
|
+
const supportedModes = (methodDetails?.supportedModes as
|
|
89
|
+
| readonly Methods.ChargeMode[]
|
|
90
|
+
| undefined) ?? ['pull', 'push']
|
|
91
|
+
const mode = (() => {
|
|
92
|
+
const explicitMode = context?.mode ?? parameters.mode
|
|
93
|
+
if (explicitMode) {
|
|
94
|
+
if (!supportedModes.includes(explicitMode))
|
|
95
|
+
throw new Error(`Challenge does not support ${explicitMode} mode.`)
|
|
96
|
+
return explicitMode
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const preferredMode = account.type === 'json-rpc' ? 'push' : 'pull'
|
|
100
|
+
if (supportedModes.includes(preferredMode)) return preferredMode
|
|
101
|
+
return supportedModes[0]!
|
|
102
|
+
})()
|
|
92
103
|
|
|
93
104
|
const memo = methodDetails?.memo
|
|
94
105
|
? (methodDetails.memo as Hex.Hex)
|
|
@@ -193,9 +204,12 @@ export declare namespace charge {
|
|
|
193
204
|
* - `'push'`: Client broadcasts the transaction and sends the tx hash to the server.
|
|
194
205
|
* - `'pull'`: Client signs the transaction and sends the serialized tx to the server for broadcast.
|
|
195
206
|
*
|
|
207
|
+
* If the server advertises `supportedModes`, this setting must be one of
|
|
208
|
+
* the supported values for the challenge.
|
|
209
|
+
*
|
|
196
210
|
* @default `'push'` for JSON-RPC accounts, `'pull'` for local accounts.
|
|
197
211
|
*/
|
|
198
|
-
mode?:
|
|
212
|
+
mode?: Methods.ChargeMode | undefined
|
|
199
213
|
} & Account.getResolver.Parameters &
|
|
200
214
|
Client.getResolver.Parameters
|
|
201
215
|
}
|
|
@@ -12,6 +12,13 @@ import * as Selectors from './selectors.js'
|
|
|
12
12
|
|
|
13
13
|
const details = { amount: '1', currency: '0x01', recipient: '0x02' }
|
|
14
14
|
const bogus = '0x0000000000000000000000000000000000000001' as const
|
|
15
|
+
const swapTokenIn = '0x0000000000000000000000000000000000000003' as const
|
|
16
|
+
const swapTokenOut = '0x0000000000000000000000000000000000000004' as const
|
|
17
|
+
const swapData = encodeFunctionData({
|
|
18
|
+
abi: Abis.stablecoinDex,
|
|
19
|
+
functionName: 'swapExactAmountOut',
|
|
20
|
+
args: [swapTokenIn, swapTokenOut, 100n, 100n],
|
|
21
|
+
})
|
|
15
22
|
const sponsor = { address: bogus, type: 'local' } as any
|
|
16
23
|
|
|
17
24
|
describe('callScopes', () => {
|
|
@@ -48,11 +55,11 @@ describe('validateCalls', () => {
|
|
|
48
55
|
})
|
|
49
56
|
|
|
50
57
|
test('accepts approve + buy + transfer', () => {
|
|
51
|
-
const swapSelector = Selectors.swapExactAmountOut
|
|
52
58
|
expect(() =>
|
|
53
59
|
validateCalls(
|
|
54
60
|
[
|
|
55
61
|
{
|
|
62
|
+
to: swapTokenIn,
|
|
56
63
|
data: encodeFunctionData({
|
|
57
64
|
abi: Abis.tip20,
|
|
58
65
|
functionName: 'approve',
|
|
@@ -61,7 +68,7 @@ describe('validateCalls', () => {
|
|
|
61
68
|
},
|
|
62
69
|
{
|
|
63
70
|
to: Addresses.stablecoinDex,
|
|
64
|
-
data:
|
|
71
|
+
data: swapData,
|
|
65
72
|
},
|
|
66
73
|
{
|
|
67
74
|
data: encodeFunctionData({
|
|
@@ -77,11 +84,11 @@ describe('validateCalls', () => {
|
|
|
77
84
|
})
|
|
78
85
|
|
|
79
86
|
test('accepts multiple transfers after swap prefix', () => {
|
|
80
|
-
const swapSelector = Selectors.swapExactAmountOut
|
|
81
87
|
expect(() =>
|
|
82
88
|
validateCalls(
|
|
83
89
|
[
|
|
84
90
|
{
|
|
91
|
+
to: swapTokenIn,
|
|
85
92
|
data: encodeFunctionData({
|
|
86
93
|
abi: Abis.tip20,
|
|
87
94
|
functionName: 'approve',
|
|
@@ -90,7 +97,7 @@ describe('validateCalls', () => {
|
|
|
90
97
|
},
|
|
91
98
|
{
|
|
92
99
|
to: Addresses.stablecoinDex,
|
|
93
|
-
data:
|
|
100
|
+
data: swapData,
|
|
94
101
|
},
|
|
95
102
|
{
|
|
96
103
|
data: encodeFunctionData({
|
|
@@ -142,7 +149,6 @@ describe('validateCalls', () => {
|
|
|
142
149
|
})
|
|
143
150
|
|
|
144
151
|
test('error: rejects wrong order (transfer before approve + buy)', () => {
|
|
145
|
-
const swapSelector = Selectors.swapExactAmountOut
|
|
146
152
|
expect(() =>
|
|
147
153
|
validateCalls(
|
|
148
154
|
[
|
|
@@ -154,6 +160,7 @@ describe('validateCalls', () => {
|
|
|
154
160
|
}),
|
|
155
161
|
},
|
|
156
162
|
{
|
|
163
|
+
to: swapTokenIn,
|
|
157
164
|
data: encodeFunctionData({
|
|
158
165
|
abi: Abis.tip20,
|
|
159
166
|
functionName: 'approve',
|
|
@@ -162,7 +169,7 @@ describe('validateCalls', () => {
|
|
|
162
169
|
},
|
|
163
170
|
{
|
|
164
171
|
to: Addresses.stablecoinDex,
|
|
165
|
-
data:
|
|
172
|
+
data: swapData,
|
|
166
173
|
},
|
|
167
174
|
],
|
|
168
175
|
details,
|
|
@@ -171,11 +178,11 @@ describe('validateCalls', () => {
|
|
|
171
178
|
})
|
|
172
179
|
|
|
173
180
|
test('error: rejects approve with non-DEX spender', () => {
|
|
174
|
-
const swapSelector = Selectors.swapExactAmountOut
|
|
175
181
|
expect(() =>
|
|
176
182
|
validateCalls(
|
|
177
183
|
[
|
|
178
184
|
{
|
|
185
|
+
to: swapTokenIn,
|
|
179
186
|
data: encodeFunctionData({
|
|
180
187
|
abi: Abis.tip20,
|
|
181
188
|
functionName: 'approve',
|
|
@@ -184,7 +191,7 @@ describe('validateCalls', () => {
|
|
|
184
191
|
},
|
|
185
192
|
{
|
|
186
193
|
to: Addresses.stablecoinDex,
|
|
187
|
-
data:
|
|
194
|
+
data: swapData,
|
|
188
195
|
},
|
|
189
196
|
{
|
|
190
197
|
data: encodeFunctionData({
|
|
@@ -199,19 +206,48 @@ describe('validateCalls', () => {
|
|
|
199
206
|
).toThrow('approve spender is not the DEX')
|
|
200
207
|
})
|
|
201
208
|
|
|
209
|
+
test('behavior: rejects approve targeting a non-token contract', () => {
|
|
210
|
+
expect(() =>
|
|
211
|
+
validateCalls(
|
|
212
|
+
[
|
|
213
|
+
{
|
|
214
|
+
to: bogus,
|
|
215
|
+
data: encodeFunctionData({
|
|
216
|
+
abi: Abis.tip20,
|
|
217
|
+
functionName: 'approve',
|
|
218
|
+
args: [Addresses.stablecoinDex, 100n],
|
|
219
|
+
}),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
to: Addresses.stablecoinDex,
|
|
223
|
+
data: swapData,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
data: encodeFunctionData({
|
|
227
|
+
abi: Abis.tip20,
|
|
228
|
+
functionName: 'transfer',
|
|
229
|
+
args: [bogus, 100n],
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
details,
|
|
234
|
+
),
|
|
235
|
+
).toThrow(FeePayerValidationError)
|
|
236
|
+
})
|
|
237
|
+
|
|
202
238
|
test('error: rejects buy targeting non-DEX address', () => {
|
|
203
|
-
const swapSelector = Selectors.swapExactAmountOut
|
|
204
239
|
expect(() =>
|
|
205
240
|
validateCalls(
|
|
206
241
|
[
|
|
207
242
|
{
|
|
243
|
+
to: swapTokenIn,
|
|
208
244
|
data: encodeFunctionData({
|
|
209
245
|
abi: Abis.tip20,
|
|
210
246
|
functionName: 'approve',
|
|
211
247
|
args: [Addresses.stablecoinDex, 100n],
|
|
212
248
|
}),
|
|
213
249
|
},
|
|
214
|
-
{ to: bogus, data:
|
|
250
|
+
{ to: bogus, data: swapData },
|
|
215
251
|
{
|
|
216
252
|
data: encodeFunctionData({
|
|
217
253
|
abi: Abis.tip20,
|
|
@@ -226,11 +262,11 @@ describe('validateCalls', () => {
|
|
|
226
262
|
})
|
|
227
263
|
|
|
228
264
|
test('error: rejects approve + buy without transfer', () => {
|
|
229
|
-
const swapSelector = Selectors.swapExactAmountOut
|
|
230
265
|
expect(() =>
|
|
231
266
|
validateCalls(
|
|
232
267
|
[
|
|
233
268
|
{
|
|
269
|
+
to: swapTokenIn,
|
|
234
270
|
data: encodeFunctionData({
|
|
235
271
|
abi: Abis.tip20,
|
|
236
272
|
functionName: 'approve',
|
|
@@ -239,7 +275,7 @@ describe('validateCalls', () => {
|
|
|
239
275
|
},
|
|
240
276
|
{
|
|
241
277
|
to: Addresses.stablecoinDex,
|
|
242
|
-
data:
|
|
278
|
+
data: swapData,
|
|
243
279
|
},
|
|
244
280
|
],
|
|
245
281
|
details,
|
|
@@ -285,6 +321,80 @@ describe('prepareSponsoredTransaction', () => {
|
|
|
285
321
|
).not.toThrow()
|
|
286
322
|
})
|
|
287
323
|
|
|
324
|
+
test('accepts higher Moderato priority fees by default', () => {
|
|
325
|
+
expect(() =>
|
|
326
|
+
prepareSponsoredTransaction({
|
|
327
|
+
account: sponsor,
|
|
328
|
+
chainId: 42431,
|
|
329
|
+
details,
|
|
330
|
+
expectedFeeToken: bogus,
|
|
331
|
+
transaction: {
|
|
332
|
+
...baseTransaction,
|
|
333
|
+
gas: 626_497n,
|
|
334
|
+
maxFeePerGas: 24_000_000_000n,
|
|
335
|
+
maxPriorityFeePerGas: 24_000_000_000n,
|
|
336
|
+
} as any,
|
|
337
|
+
}),
|
|
338
|
+
).not.toThrow()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('accepts fee-payer policy overrides', () => {
|
|
342
|
+
expect(() =>
|
|
343
|
+
prepareSponsoredTransaction({
|
|
344
|
+
account: sponsor,
|
|
345
|
+
chainId: 4217,
|
|
346
|
+
details,
|
|
347
|
+
expectedFeeToken: bogus,
|
|
348
|
+
policy: { maxPriorityFeePerGas: 50_000_000_000n },
|
|
349
|
+
transaction: {
|
|
350
|
+
...baseTransaction,
|
|
351
|
+
chainId: 4217,
|
|
352
|
+
gas: 626_497n,
|
|
353
|
+
maxFeePerGas: 24_000_000_000n,
|
|
354
|
+
maxPriorityFeePerGas: 24_000_000_000n,
|
|
355
|
+
} as any,
|
|
356
|
+
}),
|
|
357
|
+
).not.toThrow()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('error: rejects excessive priority fee under a custom policy override', () => {
|
|
361
|
+
expect(() =>
|
|
362
|
+
prepareSponsoredTransaction({
|
|
363
|
+
account: sponsor,
|
|
364
|
+
chainId: 4217,
|
|
365
|
+
details,
|
|
366
|
+
expectedFeeToken: bogus,
|
|
367
|
+
policy: { maxPriorityFeePerGas: 20_000_000_000n },
|
|
368
|
+
transaction: {
|
|
369
|
+
...baseTransaction,
|
|
370
|
+
chainId: 4217,
|
|
371
|
+
gas: 626_497n,
|
|
372
|
+
maxFeePerGas: 24_000_000_000n,
|
|
373
|
+
maxPriorityFeePerGas: 24_000_000_000n,
|
|
374
|
+
} as any,
|
|
375
|
+
}),
|
|
376
|
+
).toThrow('maxPriorityFeePerGas exceeds sponsor policy')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('ignores undefined policy override values', () => {
|
|
380
|
+
expect(() =>
|
|
381
|
+
prepareSponsoredTransaction({
|
|
382
|
+
account: sponsor,
|
|
383
|
+
chainId: 4217,
|
|
384
|
+
details,
|
|
385
|
+
expectedFeeToken: bogus,
|
|
386
|
+
policy: { maxPriorityFeePerGas: undefined } as any,
|
|
387
|
+
transaction: {
|
|
388
|
+
...baseTransaction,
|
|
389
|
+
chainId: 4217,
|
|
390
|
+
gas: 626_497n,
|
|
391
|
+
maxFeePerGas: 24_000_000_000n,
|
|
392
|
+
maxPriorityFeePerGas: 24_000_000_000n,
|
|
393
|
+
} as any,
|
|
394
|
+
}),
|
|
395
|
+
).toThrow('maxPriorityFeePerGas exceeds sponsor policy')
|
|
396
|
+
})
|
|
397
|
+
|
|
288
398
|
test('drops unknown top-level fields from the sponsored transaction', () => {
|
|
289
399
|
const sponsored = prepareSponsoredTransaction({
|
|
290
400
|
account: sponsor,
|
|
@@ -5,6 +5,7 @@ import { decodeFunctionData } from 'viem'
|
|
|
5
5
|
import { Abis, Addresses, Transaction } from 'viem/tempo'
|
|
6
6
|
|
|
7
7
|
import * as TempoAddress_internal from './address.js'
|
|
8
|
+
import * as defaults from './defaults.js'
|
|
8
9
|
import * as Selectors from './selectors.js'
|
|
9
10
|
|
|
10
11
|
/** Returns true if the serialized transaction has a Tempo envelope prefix. */
|
|
@@ -26,17 +27,47 @@ export const callScopes = [
|
|
|
26
27
|
[Selectors.approve, Selectors.swapExactAmountOut, Selectors.transferWithMemo],
|
|
27
28
|
]
|
|
28
29
|
|
|
30
|
+
export type Policy = {
|
|
31
|
+
maxGas: bigint
|
|
32
|
+
maxFeePerGas: bigint
|
|
33
|
+
maxPriorityFeePerGas: bigint
|
|
34
|
+
maxTotalFee: bigint
|
|
35
|
+
maxValidityWindowSeconds: number
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* maxTotalFee must be high enough to cover `transferWithMemo` and
|
|
31
40
|
* swap transactions at peak gas prices. Bumped from 0.01 ETH in #327.
|
|
32
41
|
*/
|
|
33
|
-
const
|
|
42
|
+
const defaultPolicy: Policy = {
|
|
34
43
|
maxGas: 2_000_000n,
|
|
35
44
|
maxFeePerGas: 100_000_000_000n,
|
|
36
45
|
maxPriorityFeePerGas: 10_000_000_000n,
|
|
37
46
|
maxTotalFee: 50_000_000_000_000_000n,
|
|
38
47
|
maxValidityWindowSeconds: 15 * 60,
|
|
39
|
-
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const policyByChainId = {
|
|
51
|
+
[defaults.chainId.mainnet]: defaultPolicy,
|
|
52
|
+
// Moderato regularly needs a higher priority fee than mainnet.
|
|
53
|
+
[defaults.chainId.testnet]: {
|
|
54
|
+
...defaultPolicy,
|
|
55
|
+
maxPriorityFeePerGas: 50_000_000_000n,
|
|
56
|
+
},
|
|
57
|
+
} as const satisfies Record<defaults.ChainId, Policy>
|
|
58
|
+
|
|
59
|
+
function getPolicy(chainId: number, overrides: Partial<Policy> | undefined): Policy {
|
|
60
|
+
const base = policyByChainId[chainId as defaults.ChainId] ?? defaultPolicy
|
|
61
|
+
if (!overrides) return base
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
maxGas: overrides.maxGas ?? base.maxGas,
|
|
65
|
+
maxFeePerGas: overrides.maxFeePerGas ?? base.maxFeePerGas,
|
|
66
|
+
maxPriorityFeePerGas: overrides.maxPriorityFeePerGas ?? base.maxPriorityFeePerGas,
|
|
67
|
+
maxTotalFee: overrides.maxTotalFee ?? base.maxTotalFee,
|
|
68
|
+
maxValidityWindowSeconds: overrides.maxValidityWindowSeconds ?? base.maxValidityWindowSeconds,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
40
71
|
|
|
41
72
|
/** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
|
|
42
73
|
export function validateCalls(
|
|
@@ -67,14 +98,25 @@ export function validateCalls(
|
|
|
67
98
|
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
68
99
|
}
|
|
69
100
|
|
|
70
|
-
//
|
|
101
|
+
// Bind the swap approval to the token the DEX call will actually spend.
|
|
102
|
+
const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
|
|
103
|
+
const buyArgs = buyCall
|
|
104
|
+
? (decodeFunctionData({ abi: Abis.stablecoinDex, data: buyCall.data! }).args as [
|
|
105
|
+
`0x${string}`,
|
|
106
|
+
`0x${string}`,
|
|
107
|
+
bigint,
|
|
108
|
+
bigint,
|
|
109
|
+
])
|
|
110
|
+
: undefined
|
|
111
|
+
|
|
71
112
|
const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
|
|
72
113
|
if (approveCall) {
|
|
73
114
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: approveCall.data! })
|
|
115
|
+
if (!approveCall.to || (buyArgs && !TempoAddress_internal.isEqual(approveCall.to, buyArgs[0])))
|
|
116
|
+
throw new FeePayerValidationError('approve target does not match swap tokenIn', details)
|
|
74
117
|
if (!TempoAddress_internal.isEqual((args as [`0x${string}`])[0]!, Addresses.stablecoinDex))
|
|
75
118
|
throw new FeePayerValidationError('approve spender is not the DEX', details)
|
|
76
119
|
}
|
|
77
|
-
const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
|
|
78
120
|
if (
|
|
79
121
|
buyCall &&
|
|
80
122
|
(!buyCall.to || !TempoAddress_internal.isEqual(buyCall.to, Addresses.stablecoinDex))
|
|
@@ -89,6 +131,7 @@ export function prepareSponsoredTransaction(parameters: {
|
|
|
89
131
|
details: Record<string, string>
|
|
90
132
|
expectedFeeToken?: TempoAddress.Address | undefined
|
|
91
133
|
now?: Date | undefined
|
|
134
|
+
policy?: Partial<Policy> | undefined
|
|
92
135
|
transaction: ReturnType<(typeof Transaction)['deserialize']>
|
|
93
136
|
}) {
|
|
94
137
|
const {
|
|
@@ -98,8 +141,10 @@ export function prepareSponsoredTransaction(parameters: {
|
|
|
98
141
|
details,
|
|
99
142
|
expectedFeeToken,
|
|
100
143
|
now = new Date(),
|
|
144
|
+
policy: policyOverrides,
|
|
101
145
|
transaction,
|
|
102
146
|
} = parameters
|
|
147
|
+
const policy = getPolicy(chainId, policyOverrides)
|
|
103
148
|
|
|
104
149
|
const {
|
|
105
150
|
accessList,
|