accounts 0.6.7 → 0.7.0
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/core/ExecutionError.d.ts +25 -0
- package/dist/core/ExecutionError.d.ts.map +1 -0
- package/dist/core/ExecutionError.js +170 -0
- package/dist/core/ExecutionError.js.map +1 -0
- package/dist/core/Schema.d.ts +33 -7
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/zod/rpc.d.ts +14 -1
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +14 -1
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/server/CliAuth.d.ts +110 -43
- package/dist/server/CliAuth.d.ts.map +1 -1
- package/dist/server/CliAuth.js +243 -155
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/Handler.d.ts +0 -1
- package/dist/server/Handler.d.ts.map +1 -1
- package/dist/server/Handler.js +0 -1
- package/dist/server/Handler.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +29 -12
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +180 -125
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/sponsorship.d.ts +77 -0
- package/dist/server/internal/handlers/sponsorship.d.ts.map +1 -0
- package/dist/server/internal/handlers/sponsorship.js +96 -0
- package/dist/server/internal/handlers/sponsorship.js.map +1 -0
- package/dist/server/internal/handlers/utils.d.ts +3 -1
- package/dist/server/internal/handlers/utils.d.ts.map +1 -1
- package/dist/server/internal/handlers/utils.js +15 -12
- package/dist/server/internal/handlers/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ExecutionError.test.ts +205 -0
- package/src/core/ExecutionError.ts +189 -0
- package/src/core/Provider.test.ts +4 -2
- package/src/core/zod/rpc.ts +18 -1
- package/src/server/CliAuth.test-d.ts +6 -0
- package/src/server/CliAuth.test.ts +83 -0
- package/src/server/CliAuth.ts +331 -208
- package/src/server/Handler.ts +0 -1
- package/src/server/internal/handlers/relay.test.ts +318 -108
- package/src/server/internal/handlers/relay.ts +243 -138
- package/src/server/internal/handlers/sponsorship.ts +172 -0
- package/src/server/internal/handlers/utils.ts +15 -10
- package/dist/server/internal/handlers/feePayer.d.ts +0 -73
- package/dist/server/internal/handlers/feePayer.d.ts.map +0 -1
- package/dist/server/internal/handlers/feePayer.js +0 -184
- package/dist/server/internal/handlers/feePayer.js.map +0 -1
- package/src/server/internal/handlers/feePayer.test.ts +0 -336
- package/src/server/internal/handlers/feePayer.ts +0 -271
|
@@ -6,21 +6,22 @@ import {
|
|
|
6
6
|
type Chain,
|
|
7
7
|
type Client,
|
|
8
8
|
createClient,
|
|
9
|
-
decodeErrorResult,
|
|
10
9
|
formatUnits,
|
|
11
10
|
http,
|
|
12
11
|
type Log,
|
|
13
|
-
parseAbi,
|
|
14
12
|
parseEventLogs,
|
|
15
13
|
type Transport,
|
|
14
|
+
zeroAddress,
|
|
16
15
|
} from 'viem'
|
|
16
|
+
import type { LocalAccount } from 'viem/accounts'
|
|
17
17
|
import { simulateCalls } from 'viem/actions'
|
|
18
18
|
import { tempo, tempoLocalnet, tempoMainnet, tempoModerato } from 'viem/chains'
|
|
19
19
|
import { Abis, Actions, Addresses, Capabilities, Transaction } from 'viem/tempo'
|
|
20
20
|
|
|
21
|
+
import * as ExecutionError from '../../../core/ExecutionError.js'
|
|
21
22
|
import * as Schema from '../../../core/Schema.js'
|
|
22
23
|
import { type Handler, from } from '../../Handler.js'
|
|
23
|
-
import * as
|
|
24
|
+
import * as Sponsorship from './sponsorship.js'
|
|
24
25
|
import * as Utils from './utils.js'
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -51,12 +52,9 @@ import * as Utils from './utils.js'
|
|
|
51
52
|
* import { privateKeyToAccount } from 'viem/accounts'
|
|
52
53
|
* import { Handler } from 'accounts/server'
|
|
53
54
|
*
|
|
54
|
-
* // With sponsorship — signs as fee payer when `validate` approves.
|
|
55
55
|
* const handler = Handler.relay({
|
|
56
56
|
* feePayer: {
|
|
57
57
|
* account: privateKeyToAccount('0x...'),
|
|
58
|
-
* // Optional: validate sponsorship approval.
|
|
59
|
-
* // validate: (request) => request.from !== BLOCKED_ADDRESS,
|
|
60
58
|
* },
|
|
61
59
|
* })
|
|
62
60
|
* ```
|
|
@@ -67,7 +65,6 @@ import * as Utils from './utils.js'
|
|
|
67
65
|
export function relay(options: relay.Options = {}): Handler {
|
|
68
66
|
const {
|
|
69
67
|
chains = [tempo, tempoModerato],
|
|
70
|
-
feePayer: feePayerOptions,
|
|
71
68
|
onRequest,
|
|
72
69
|
path = '/',
|
|
73
70
|
resolveTokens = (chainId) =>
|
|
@@ -75,10 +72,21 @@ export function relay(options: relay.Options = {}): Handler {
|
|
|
75
72
|
transports = {},
|
|
76
73
|
...rest
|
|
77
74
|
} = options
|
|
75
|
+
const feePayerOptions = options.feePayer
|
|
76
|
+
|
|
77
|
+
const features = {
|
|
78
|
+
autoSwap: options.autoSwap ?? options.features === 'all',
|
|
79
|
+
feeTokenResolution: options.resolveTokens ?? options.features === 'all',
|
|
80
|
+
simulate: options.features === 'all',
|
|
81
|
+
}
|
|
78
82
|
|
|
79
83
|
const autoSwap = (() => {
|
|
84
|
+
if (!features.autoSwap) return undefined
|
|
80
85
|
if (options.autoSwap === false) return undefined
|
|
81
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
slippage:
|
|
88
|
+
(typeof options.autoSwap === 'object' ? options.autoSwap?.slippage : undefined) ?? 0.05,
|
|
89
|
+
}
|
|
82
90
|
})()
|
|
83
91
|
|
|
84
92
|
const clients = new Map<number, Client>()
|
|
@@ -99,92 +107,105 @@ export function relay(options: relay.Options = {}): Handler {
|
|
|
99
107
|
return clients.get(chains[0]!.id)!
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
async function handleRequest(
|
|
110
|
+
async function handleRequest(
|
|
111
|
+
request: RpcRequest.RpcRequest<Schema.Ox>,
|
|
112
|
+
options?: { chainId?: number | undefined },
|
|
113
|
+
) {
|
|
103
114
|
await onRequest?.(request)
|
|
104
115
|
|
|
105
|
-
// Resolve chainId
|
|
116
|
+
// Resolve chainId from: 1) explicit option (URL path), 2) first param object, 3) default chain.
|
|
106
117
|
const params = 'params' in request && Array.isArray(request.params) ? request.params : []
|
|
107
118
|
const first =
|
|
108
119
|
typeof params[0] === 'object' && params[0]
|
|
109
120
|
? (params[0] as Record<string, unknown>)
|
|
110
121
|
: undefined
|
|
111
|
-
const chainId = Utils.resolveChainId(first?.chainId) ?? chains[0]!.id
|
|
122
|
+
const chainId = Utils.resolveChainId(first?.chainId) ?? options?.chainId ?? chains[0]!.id
|
|
112
123
|
const client = getClient(chainId)
|
|
113
124
|
|
|
114
125
|
switch (request.method) {
|
|
115
126
|
case 'eth_fillTransaction': {
|
|
116
127
|
try {
|
|
117
|
-
const parameters =
|
|
128
|
+
const parameters = params[0] as Record<string, unknown>
|
|
129
|
+
const from =
|
|
130
|
+
typeof parameters.from === 'string' ? (parameters.from as Address) : undefined
|
|
131
|
+
const requestFeeToken =
|
|
132
|
+
typeof parameters.feeToken === 'string' ? (parameters.feeToken as Address) : undefined
|
|
133
|
+
const requestsSponsorship = !!feePayerOptions && parameters.feePayer !== false
|
|
118
134
|
|
|
119
135
|
// 1. Resolve fee token.
|
|
120
|
-
const feeToken =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
const feeToken = features.feeTokenResolution
|
|
137
|
+
? await resolveFeeToken(client, {
|
|
138
|
+
account: from,
|
|
139
|
+
feeToken: requestFeeToken,
|
|
140
|
+
tokens: resolveTokens(chainId),
|
|
141
|
+
})
|
|
142
|
+
: requestFeeToken
|
|
125
143
|
|
|
126
144
|
// 2. Fill transaction via RPC node (with AMM resolution on InsufficientBalance).
|
|
127
145
|
const normalized = Utils.normalizeFillTransactionRequest(parameters)
|
|
128
146
|
const transaction = {
|
|
129
147
|
...normalized,
|
|
130
148
|
...(typeof chainId !== 'undefined' ? { chainId } : {}),
|
|
131
|
-
...(
|
|
149
|
+
...(requestsSponsorship ? { feePayer: true } : {}),
|
|
132
150
|
...(feeToken ? { feeToken } : {}),
|
|
133
151
|
}
|
|
134
|
-
let filled = await
|
|
152
|
+
let filled = await (async () => {
|
|
153
|
+
if (requestsSponsorship && Sponsorship.isPreparedTransaction(transaction))
|
|
154
|
+
return { transaction: Utils.normalizeTempoTransaction(transaction) }
|
|
155
|
+
return await fill(client, { autoSwap, feeToken, transaction })
|
|
156
|
+
})()
|
|
135
157
|
|
|
136
158
|
// 3. Check if the fee payer approves this transaction.
|
|
137
159
|
const sponsored =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
requestsSponsorship && feePayerOptions
|
|
161
|
+
? await Sponsorship.shouldSponsor({
|
|
162
|
+
sender: from,
|
|
163
|
+
transaction: filled.transaction,
|
|
164
|
+
validate: feePayerOptions.validate,
|
|
165
|
+
})
|
|
166
|
+
: false
|
|
145
167
|
|
|
146
168
|
// Re-fill without feePayer when sponsorship is rejected so the
|
|
147
169
|
// gas estimate and nonce are correct for a self-paid transaction.
|
|
148
|
-
if (
|
|
170
|
+
if (requestsSponsorship && !sponsored) {
|
|
149
171
|
const { feePayer: _, ...tx } = transaction
|
|
150
172
|
filled = await fill(client, { autoSwap, feeToken, transaction: tx })
|
|
151
173
|
}
|
|
152
174
|
|
|
153
|
-
const
|
|
175
|
+
const transaction_filled = filled.transaction
|
|
176
|
+
const swap = 'swap' in filled ? filled.swap : undefined
|
|
154
177
|
|
|
155
178
|
// 4. Simulate and compute balance diffs + fee.
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
const { balanceDiffs, fee } = features.simulate
|
|
180
|
+
? await simulateAndParseDiffs(client, {
|
|
181
|
+
account: from,
|
|
182
|
+
calls: extractCalls(transaction_filled),
|
|
183
|
+
swap,
|
|
184
|
+
feeToken: transaction_filled.feeToken,
|
|
185
|
+
gas: transaction_filled.gas,
|
|
186
|
+
maxFeePerGas: transaction_filled.maxFeePerGas,
|
|
187
|
+
})
|
|
188
|
+
: { balanceDiffs: undefined, fee: undefined }
|
|
189
|
+
|
|
190
|
+
// 5. Sign as fee payer (if sponsored and not already signed).
|
|
191
|
+
const alreadySigned =
|
|
192
|
+
'feePayerSignature' in transaction_filled &&
|
|
193
|
+
transaction_filled.feePayerSignature != null
|
|
167
194
|
const transaction_final = await (async () => {
|
|
168
|
-
if (!sponsored) return transaction_filled
|
|
169
|
-
return await
|
|
195
|
+
if (!sponsored || !feePayerOptions || alreadySigned) return transaction_filled
|
|
196
|
+
return await Sponsorship.sign({
|
|
170
197
|
account: feePayerOptions.account,
|
|
171
|
-
sender:
|
|
198
|
+
sender: from,
|
|
172
199
|
transaction: transaction_filled,
|
|
173
200
|
})
|
|
174
201
|
})()
|
|
175
202
|
|
|
176
|
-
const sponsor =
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
address: feePayerOptions.account.address,
|
|
180
|
-
...(feePayerOptions.name ? { name: feePayerOptions.name } : {}),
|
|
181
|
-
...(feePayerOptions.url ? { url: feePayerOptions.url } : {}),
|
|
182
|
-
}
|
|
183
|
-
})()
|
|
203
|
+
const sponsor =
|
|
204
|
+
sponsored && feePayerOptions ? Sponsorship.getSponsor(feePayerOptions) : undefined
|
|
184
205
|
|
|
185
206
|
// 6. Resolve autoSwap metadata (when AMM path was taken).
|
|
186
207
|
const autoSwap_ = await (async () => {
|
|
187
|
-
if (!swap) return undefined
|
|
208
|
+
if (!autoSwap || !swap) return undefined
|
|
188
209
|
const [inMeta, outMeta] = await Promise.all([
|
|
189
210
|
resolveTokenMetadata(client, swap.tokenIn).catch(() => undefined),
|
|
190
211
|
resolveTokenMetadata(client, swap.tokenOut).catch(() => undefined),
|
|
@@ -215,6 +236,7 @@ export function relay(options: relay.Options = {}): Handler {
|
|
|
215
236
|
return RpcResponse.from(
|
|
216
237
|
{
|
|
217
238
|
result: {
|
|
239
|
+
...(sponsor ? { sponsor } : {}),
|
|
218
240
|
tx: core_Transaction.toRpc(transaction_final as core_Transaction.Transaction),
|
|
219
241
|
capabilities: {
|
|
220
242
|
balanceDiffs,
|
|
@@ -227,6 +249,111 @@ export function relay(options: relay.Options = {}): Handler {
|
|
|
227
249
|
},
|
|
228
250
|
{ request },
|
|
229
251
|
)
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (!(error instanceof Error)) return Utils.rpcErrorJson(request, error)
|
|
254
|
+
|
|
255
|
+
const revert = ExecutionError.parse(error)
|
|
256
|
+
|
|
257
|
+
const parameters = request.params[0]
|
|
258
|
+
const stub = {
|
|
259
|
+
from: parameters.from,
|
|
260
|
+
to: parameters.to ?? null,
|
|
261
|
+
gas: '0x0',
|
|
262
|
+
nonce: '0x0',
|
|
263
|
+
value: '0x0',
|
|
264
|
+
maxFeePerGas: '0x0',
|
|
265
|
+
maxPriorityFeePerGas: '0x0',
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (revert?.errorName === 'InsufficientBalance') {
|
|
269
|
+
const args = revert.args as [bigint, bigint, Address]
|
|
270
|
+
const [available, required, token] = args
|
|
271
|
+
|
|
272
|
+
const normalized = Utils.normalizeFillTransactionRequest(parameters)
|
|
273
|
+
|
|
274
|
+
// Simulate from zero address for optimistic balance diffs.
|
|
275
|
+
const optimisticCalls = normalized ? extractCalls(normalized) : undefined
|
|
276
|
+
const { balanceDiffs } = optimisticCalls
|
|
277
|
+
? await simulateAndParseDiffs(client, {
|
|
278
|
+
account: zeroAddress,
|
|
279
|
+
calls: optimisticCalls,
|
|
280
|
+
})
|
|
281
|
+
: { balanceDiffs: undefined }
|
|
282
|
+
|
|
283
|
+
// Re-key balance diffs from zero address to the real sender.
|
|
284
|
+
const senderDiffs =
|
|
285
|
+
parameters.from && balanceDiffs
|
|
286
|
+
? { [parameters.from]: balanceDiffs[zeroAddress] ?? [] }
|
|
287
|
+
: balanceDiffs
|
|
288
|
+
|
|
289
|
+
const metadata = await resolveTokenMetadata(client, token).catch(() => undefined)
|
|
290
|
+
const deficit = required - available
|
|
291
|
+
return RpcResponse.from(
|
|
292
|
+
{
|
|
293
|
+
result: {
|
|
294
|
+
tx: stub,
|
|
295
|
+
capabilities: {
|
|
296
|
+
balanceDiffs: senderDiffs,
|
|
297
|
+
error: ExecutionError.serialize(revert),
|
|
298
|
+
requireFunds: metadata
|
|
299
|
+
? {
|
|
300
|
+
amount: Hex.fromNumber(deficit) as `0x${string}`,
|
|
301
|
+
decimals: metadata.decimals,
|
|
302
|
+
formatted: formatUnits(deficit, metadata.decimals),
|
|
303
|
+
token,
|
|
304
|
+
symbol: metadata.symbol,
|
|
305
|
+
}
|
|
306
|
+
: undefined,
|
|
307
|
+
sponsored: false,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{ request },
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return RpcResponse.from(
|
|
316
|
+
{
|
|
317
|
+
result: {
|
|
318
|
+
tx: stub,
|
|
319
|
+
capabilities: {
|
|
320
|
+
error: ExecutionError.serialize(revert),
|
|
321
|
+
sponsored: false,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{ request },
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// @ts-expect-error
|
|
331
|
+
case 'eth_signRawTransaction':
|
|
332
|
+
case 'eth_sendRawTransaction':
|
|
333
|
+
case 'eth_sendRawTransactionSync': {
|
|
334
|
+
try {
|
|
335
|
+
if (!feePayerOptions) {
|
|
336
|
+
const result = await client.request(request as never)
|
|
337
|
+
return RpcResponse.from({ result }, { request })
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const serialized = params[0]
|
|
341
|
+
if (
|
|
342
|
+
typeof serialized !== 'string' ||
|
|
343
|
+
!Sponsorship.requestsRawSponsorship(serialized as `0x${string}`)
|
|
344
|
+
) {
|
|
345
|
+
const result = await client.request(request as never)
|
|
346
|
+
return RpcResponse.from({ result }, { request })
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const result = await Sponsorship.handleRawTransaction({
|
|
350
|
+
account: feePayerOptions.account,
|
|
351
|
+
getClient,
|
|
352
|
+
method: request.method as Sponsorship.handleRawTransaction.Options['method'],
|
|
353
|
+
request: { params: 'params' in request ? request.params : undefined },
|
|
354
|
+
validate: feePayerOptions.validate,
|
|
355
|
+
})
|
|
356
|
+
return RpcResponse.from({ result } as never, { request } as never)
|
|
230
357
|
} catch (error) {
|
|
231
358
|
return Utils.rpcErrorJson(request, error)
|
|
232
359
|
}
|
|
@@ -241,22 +368,28 @@ export function relay(options: relay.Options = {}): Handler {
|
|
|
241
368
|
|
|
242
369
|
const router = from(rest)
|
|
243
370
|
|
|
244
|
-
|
|
371
|
+
async function handlePost(c: { req: { raw: Request; param: (key: string) => string } }) {
|
|
372
|
+
const chainId = Utils.resolveChainId(c.req.param('chainId'))
|
|
245
373
|
const body = await c.req.raw.json()
|
|
246
374
|
const isBatch = Array.isArray(body)
|
|
247
375
|
|
|
248
376
|
if (!isBatch) {
|
|
249
|
-
const request = RpcRequest.from(body) as RpcRequest.RpcRequest<Schema.Ox>
|
|
250
|
-
return Response.json(await handleRequest(request))
|
|
377
|
+
const request = RpcRequest.from(body as never) as RpcRequest.RpcRequest<Schema.Ox>
|
|
378
|
+
return Response.json(await handleRequest(request, { chainId }))
|
|
251
379
|
}
|
|
252
380
|
|
|
253
381
|
const responses = await Promise.all(
|
|
254
382
|
(body as unknown[]).map((item) =>
|
|
255
|
-
handleRequest(RpcRequest.from(item as never) as RpcRequest.RpcRequest<Schema.Ox
|
|
383
|
+
handleRequest(RpcRequest.from(item as never) as RpcRequest.RpcRequest<Schema.Ox>, {
|
|
384
|
+
chainId,
|
|
385
|
+
}),
|
|
256
386
|
),
|
|
257
387
|
)
|
|
258
388
|
return Response.json(responses)
|
|
259
|
-
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
router.post(path, handlePost as never)
|
|
392
|
+
router.post(`${path === '/' ? '' : path}/:chainId`, handlePost as never)
|
|
260
393
|
|
|
261
394
|
return router
|
|
262
395
|
}
|
|
@@ -296,6 +429,16 @@ export namespace relay {
|
|
|
296
429
|
} as const
|
|
297
430
|
|
|
298
431
|
export type Options = from.Options & {
|
|
432
|
+
/**
|
|
433
|
+
* Auto-swap options.
|
|
434
|
+
*/
|
|
435
|
+
autoSwap?:
|
|
436
|
+
| false
|
|
437
|
+
| {
|
|
438
|
+
/** Slippage tolerance (e.g. 0.05 = 5%). @default 0.05 */
|
|
439
|
+
slippage?: number | undefined
|
|
440
|
+
}
|
|
441
|
+
| undefined
|
|
299
442
|
/**
|
|
300
443
|
* Supported chains. The handler resolves the client based on the
|
|
301
444
|
* `chainId` in the incoming transaction.
|
|
@@ -307,17 +450,20 @@ export namespace relay {
|
|
|
307
450
|
* sign `feePayerSignature` on the filled transaction.
|
|
308
451
|
*/
|
|
309
452
|
feePayer?:
|
|
310
|
-
| Omit<FeePayer.feePayer.Options, 'chains' | 'transports' | 'path' | 'onRequest'>
|
|
311
|
-
| undefined
|
|
312
|
-
/**
|
|
313
|
-
* AMM swap options for automatic insufficient balance resolution.
|
|
314
|
-
* Set to `false` to disable. @default {}
|
|
315
|
-
*/
|
|
316
|
-
autoSwap?:
|
|
317
|
-
| false
|
|
318
453
|
| {
|
|
319
|
-
/**
|
|
320
|
-
|
|
454
|
+
/** Account to use as the fee payer. */
|
|
455
|
+
account: LocalAccount
|
|
456
|
+
/**
|
|
457
|
+
* Validates whether to sponsor the transaction. When omitted, all
|
|
458
|
+
* transactions are sponsored. Return `false` to reject sponsorship.
|
|
459
|
+
*/
|
|
460
|
+
validate?:
|
|
461
|
+
| ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>)
|
|
462
|
+
| undefined
|
|
463
|
+
/** Sponsor display name returned from `eth_fillTransaction`. */
|
|
464
|
+
name?: string | undefined
|
|
465
|
+
/** Sponsor URL returned from `eth_fillTransaction`. */
|
|
466
|
+
url?: string | undefined
|
|
321
467
|
}
|
|
322
468
|
| undefined
|
|
323
469
|
/**
|
|
@@ -326,6 +472,14 @@ export namespace relay {
|
|
|
326
472
|
* highest balance.
|
|
327
473
|
*/
|
|
328
474
|
resolveTokens?: ((chainId?: number | undefined) => readonly Address[]) | undefined
|
|
475
|
+
/**
|
|
476
|
+
* Relay features.
|
|
477
|
+
*
|
|
478
|
+
* - `'all'` — enables fee token resolution, auto-swap,
|
|
479
|
+
* fee payer, and simulation (balance diffs + fee breakdown).
|
|
480
|
+
* - `undefined` (default) — only fee payers.
|
|
481
|
+
*/
|
|
482
|
+
features?: 'all' | undefined
|
|
329
483
|
/** Function to call before handling the request. */
|
|
330
484
|
onRequest?: ((request: RpcRequest.RpcRequest) => Promise<void>) | undefined
|
|
331
485
|
/** Path to use for the handler. @default "/" */
|
|
@@ -358,33 +512,27 @@ async function fill(
|
|
|
358
512
|
return { transaction: Utils.normalizeTempoTransaction(result.tx) }
|
|
359
513
|
} catch (error) {
|
|
360
514
|
if (!(error instanceof Error)) throw error
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
515
|
+
if (!feeToken || !autoSwap) throw error
|
|
516
|
+
|
|
517
|
+
const revert = ExecutionError.parse(error)
|
|
518
|
+
if (revert?.errorName !== 'InsufficientBalance') throw error
|
|
519
|
+
|
|
520
|
+
const [available, required, token] = revert.args
|
|
521
|
+
if (typeof available === 'undefined' || typeof required === 'undefined' || !token) throw error
|
|
364
522
|
|
|
365
|
-
|
|
523
|
+
if (token.toLowerCase() === feeToken.toLowerCase()) throw error
|
|
524
|
+
|
|
525
|
+
const deficit = required - available
|
|
366
526
|
const maxAmountIn = deficit + (deficit * BigInt(Math.round(autoSwap.slippage * 1000))) / 1000n
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const originalCalls: Call[] = existingCalls
|
|
372
|
-
? [...existingCalls]
|
|
373
|
-
: request.to
|
|
374
|
-
? [
|
|
375
|
-
{
|
|
376
|
-
to: request.to as Address,
|
|
377
|
-
data: request.data as `0x${string}`,
|
|
378
|
-
value: (request.value as bigint) ?? 0n,
|
|
379
|
-
},
|
|
380
|
-
]
|
|
381
|
-
: []
|
|
382
|
-
const { to: _, data: __, value: ___, calls: ____, ...rest } = request
|
|
527
|
+
|
|
528
|
+
const originalCalls = (request.calls as Call[] | undefined) ?? []
|
|
529
|
+
const swapCalls = buildSwapCalls(feeToken, token, deficit, maxAmountIn)
|
|
530
|
+
|
|
383
531
|
const result = await client.request({
|
|
384
532
|
method: 'eth_fillTransaction',
|
|
385
533
|
params: [
|
|
386
534
|
format({
|
|
387
|
-
...
|
|
535
|
+
...request,
|
|
388
536
|
calls: [...swapCalls, ...originalCalls],
|
|
389
537
|
}) as never,
|
|
390
538
|
],
|
|
@@ -394,7 +542,7 @@ async function fill(
|
|
|
394
542
|
swap: {
|
|
395
543
|
calls: swapCalls,
|
|
396
544
|
tokenIn: feeToken,
|
|
397
|
-
tokenOut:
|
|
545
|
+
tokenOut: token,
|
|
398
546
|
amountOut: deficit,
|
|
399
547
|
maxAmountIn,
|
|
400
548
|
},
|
|
@@ -517,7 +665,10 @@ async function simulateAndParseDiffs(
|
|
|
517
665
|
const { account, calls, swap, feeToken, gas, maxFeePerGas } = options
|
|
518
666
|
|
|
519
667
|
try {
|
|
520
|
-
const { results, tokenMetadata } = await simulate(client, {
|
|
668
|
+
const { results, tokenMetadata } = await simulate(client, {
|
|
669
|
+
account: account === zeroAddress ? undefined : account,
|
|
670
|
+
calls,
|
|
671
|
+
})
|
|
521
672
|
|
|
522
673
|
// Collect all logs across all call results.
|
|
523
674
|
const logs: (typeof results)[number]['logs'] = []
|
|
@@ -540,7 +691,7 @@ async function simulateAndParseDiffs(
|
|
|
540
691
|
gas,
|
|
541
692
|
maxFeePerGas,
|
|
542
693
|
tokenMetadata: tokenMetadata as never,
|
|
543
|
-
})
|
|
694
|
+
}).catch(() => undefined)
|
|
544
695
|
|
|
545
696
|
return { balanceDiffs, fee }
|
|
546
697
|
} catch {
|
|
@@ -704,7 +855,7 @@ async function computeFee(
|
|
|
704
855
|
},
|
|
705
856
|
) {
|
|
706
857
|
const { feeToken, gas, maxFeePerGas, tokenMetadata } = options
|
|
707
|
-
if (!feeToken || !gas || !maxFeePerGas) return
|
|
858
|
+
if (!feeToken || !gas || !maxFeePerGas) return undefined
|
|
708
859
|
|
|
709
860
|
try {
|
|
710
861
|
const metadata = await resolveTokenMetadata(client, feeToken, tokenMetadata)
|
|
@@ -717,54 +868,8 @@ async function computeFee(
|
|
|
717
868
|
symbol: metadata.symbol,
|
|
718
869
|
}
|
|
719
870
|
} catch {
|
|
720
|
-
return
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const insufficientBalanceAbi = parseAbi([
|
|
725
|
-
'error InsufficientBalance(uint256 available, uint256 required, address token)',
|
|
726
|
-
])
|
|
727
|
-
const insufficientBalancePattern =
|
|
728
|
-
/InsufficientBalance\(\s*InsufficientBalance\s*\{\s*available:\s*(\d+),\s*required:\s*(\d+),\s*token:\s*(0x[0-9a-fA-F]+)\s*\}\s*\)/
|
|
729
|
-
|
|
730
|
-
function parseInsufficientBalance(error: Error) {
|
|
731
|
-
const message = (error as { shortMessage?: string }).shortMessage ?? error.message
|
|
732
|
-
|
|
733
|
-
const match = insufficientBalancePattern.exec(message)
|
|
734
|
-
if (match)
|
|
735
|
-
return {
|
|
736
|
-
available: BigInt(match[1]!),
|
|
737
|
-
required: BigInt(match[2]!),
|
|
738
|
-
token: match[3]! as Address,
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const data = extractRevertData(error)
|
|
742
|
-
if (!data) return null
|
|
743
|
-
try {
|
|
744
|
-
const decoded = decodeErrorResult({ abi: insufficientBalanceAbi, data })
|
|
745
|
-
return {
|
|
746
|
-
available: decoded.args[0],
|
|
747
|
-
required: decoded.args[1],
|
|
748
|
-
token: decoded.args[2] as Address,
|
|
749
|
-
}
|
|
750
|
-
} catch {
|
|
751
|
-
return null
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function extractRevertData(error: unknown): `0x${string}` | null {
|
|
756
|
-
if (!error || typeof error !== 'object') return null
|
|
757
|
-
const e = error as Record<string, unknown>
|
|
758
|
-
if (typeof e.data === 'string' && e.data.startsWith('0x')) return e.data as `0x${string}`
|
|
759
|
-
if (e.cause) return extractRevertData(e.cause)
|
|
760
|
-
if (e.error) return extractRevertData(e.error)
|
|
761
|
-
if (typeof e.walk === 'function') {
|
|
762
|
-
const inner = (e as { walk: (fn: (e: unknown) => boolean) => unknown }).walk(
|
|
763
|
-
(e) => typeof (e as Record<string, unknown>).data === 'string',
|
|
764
|
-
)
|
|
765
|
-
if (inner) return extractRevertData(inner)
|
|
871
|
+
return undefined
|
|
766
872
|
}
|
|
767
|
-
return null
|
|
768
873
|
}
|
|
769
874
|
|
|
770
875
|
function buildSwapCalls(
|