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
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import type { RpcRequest } from 'ox'
|
|
2
|
-
import { SignatureEnvelope, Transaction as core_Transaction, TxEnvelopeTempo } from 'ox/tempo'
|
|
3
|
-
import { parseUnits } from 'viem'
|
|
4
|
-
import { sendTransaction, sendTransactionSync, waitForTransactionReceipt } from 'viem/actions'
|
|
5
|
-
import { Actions, Transaction, withFeePayer } from 'viem/tempo'
|
|
6
|
-
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
|
|
7
|
-
|
|
8
|
-
import { accounts, addresses, chain, getClient, http } from '../../../../test/config.js'
|
|
9
|
-
import { createServer, type Server } from '../../../../test/utils.js'
|
|
10
|
-
import { feePayer } from './feePayer.js'
|
|
11
|
-
|
|
12
|
-
const userAccount = accounts[9]!
|
|
13
|
-
const feePayerAccount = accounts[0]!
|
|
14
|
-
|
|
15
|
-
let server: Server
|
|
16
|
-
let fp: ReturnType<typeof getClient>
|
|
17
|
-
let requests: RpcRequest.RpcRequest[] = []
|
|
18
|
-
|
|
19
|
-
beforeAll(async () => {
|
|
20
|
-
server = await createServer(
|
|
21
|
-
feePayer({
|
|
22
|
-
account: feePayerAccount,
|
|
23
|
-
chains: [chain],
|
|
24
|
-
transports: { [chain.id]: http() },
|
|
25
|
-
onRequest: async (request) => {
|
|
26
|
-
requests.push(request)
|
|
27
|
-
},
|
|
28
|
-
}).listener,
|
|
29
|
-
)
|
|
30
|
-
fp = getClient({ transport: http(server.url) })
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
afterAll(() => {
|
|
34
|
-
server.close()
|
|
35
|
-
process.on('SIGINT', () => {
|
|
36
|
-
server.close()
|
|
37
|
-
process.exit(0)
|
|
38
|
-
})
|
|
39
|
-
process.on('SIGTERM', () => {
|
|
40
|
-
server.close()
|
|
41
|
-
process.exit(0)
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
requests = []
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
/** Signs a sponsor-bound Tempo transaction, preserving the feePayerSignature. */
|
|
50
|
-
async function signSponsoredTx(account: (typeof accounts)[number], transaction: object) {
|
|
51
|
-
const serialized = (await Transaction.serialize(transaction as never)) as `0x76${string}`
|
|
52
|
-
const envelope = TxEnvelopeTempo.deserialize(serialized)
|
|
53
|
-
const signature = await account.sign({
|
|
54
|
-
hash: TxEnvelopeTempo.getSignPayload(envelope),
|
|
55
|
-
})
|
|
56
|
-
return TxEnvelopeTempo.serialize(envelope, {
|
|
57
|
-
signature: SignatureEnvelope.from(signature),
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
describe('POST /', () => {
|
|
62
|
-
test('default: eth_fillTransaction returns a sponsor-bound transaction the sender can broadcast', async () => {
|
|
63
|
-
const response = (await fp.request({
|
|
64
|
-
method: 'eth_fillTransaction',
|
|
65
|
-
params: [
|
|
66
|
-
{
|
|
67
|
-
chainId: chain.id,
|
|
68
|
-
feePayer: true,
|
|
69
|
-
from: userAccount.address,
|
|
70
|
-
to: '0x0000000000000000000000000000000000000000',
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
})) as {
|
|
74
|
-
sponsor: { address: string }
|
|
75
|
-
tx: Record<string, unknown>
|
|
76
|
-
}
|
|
77
|
-
const prepared = core_Transaction.fromRpc(response.tx as never) as {
|
|
78
|
-
feePayerSignature?: unknown
|
|
79
|
-
}
|
|
80
|
-
const signed = await signSponsoredTx(userAccount, prepared)
|
|
81
|
-
const receipt = (await getClient().request({
|
|
82
|
-
method: 'eth_sendRawTransactionSync',
|
|
83
|
-
params: [signed],
|
|
84
|
-
})) as { feePayer?: string | undefined }
|
|
85
|
-
|
|
86
|
-
expect(response.sponsor.address).toBe(feePayerAccount.address)
|
|
87
|
-
expect(prepared?.feePayerSignature).toBeDefined()
|
|
88
|
-
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
89
|
-
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
|
|
90
|
-
[
|
|
91
|
-
"eth_fillTransaction",
|
|
92
|
-
]
|
|
93
|
-
`)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('behavior: mutating a sponsor-bound transaction invalidates the fee payer binding', async () => {
|
|
97
|
-
const response = (await fp.request({
|
|
98
|
-
method: 'eth_fillTransaction',
|
|
99
|
-
params: [
|
|
100
|
-
{
|
|
101
|
-
chainId: chain.id,
|
|
102
|
-
feePayer: true,
|
|
103
|
-
from: userAccount.address,
|
|
104
|
-
to: '0x0000000000000000000000000000000000000000',
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
})) as { tx: Record<string, unknown> }
|
|
108
|
-
const prepared = core_Transaction.fromRpc(response.tx as never) as {
|
|
109
|
-
gas?: bigint | undefined
|
|
110
|
-
feePayerSignature?: unknown
|
|
111
|
-
}
|
|
112
|
-
const signed = await signSponsoredTx(userAccount, {
|
|
113
|
-
...prepared,
|
|
114
|
-
gas: (prepared?.gas ?? 0n) + 1n,
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
await expect(
|
|
118
|
-
getClient().request({
|
|
119
|
-
method: 'eth_sendRawTransactionSync',
|
|
120
|
-
params: [signed],
|
|
121
|
-
}),
|
|
122
|
-
).rejects.toThrowError()
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test('behavior: eth_signRawTransaction', async () => {
|
|
126
|
-
const client = getClient({
|
|
127
|
-
account: userAccount,
|
|
128
|
-
transport: withFeePayer(http(), http(server.url)),
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const receipt = await sendTransactionSync(client, {
|
|
132
|
-
feePayer: true,
|
|
133
|
-
to: '0x0000000000000000000000000000000000000000',
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
137
|
-
|
|
138
|
-
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
|
|
139
|
-
[
|
|
140
|
-
"eth_signRawTransaction",
|
|
141
|
-
]
|
|
142
|
-
`)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
test('behavior: eth_sendRawTransaction', async () => {
|
|
146
|
-
const client = getClient({
|
|
147
|
-
account: userAccount,
|
|
148
|
-
transport: withFeePayer(http(), http(server.url), {
|
|
149
|
-
policy: 'sign-and-broadcast',
|
|
150
|
-
}),
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
const hash = await sendTransaction(client, {
|
|
154
|
-
feePayer: true,
|
|
155
|
-
to: '0x0000000000000000000000000000000000000001',
|
|
156
|
-
})
|
|
157
|
-
const receipt = await waitForTransactionReceipt(getClient(), { hash })
|
|
158
|
-
|
|
159
|
-
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
160
|
-
|
|
161
|
-
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
|
|
162
|
-
[
|
|
163
|
-
"eth_sendRawTransaction",
|
|
164
|
-
]
|
|
165
|
-
`)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
test('behavior: eth_sendRawTransactionSync', async () => {
|
|
169
|
-
const client = getClient({
|
|
170
|
-
account: userAccount,
|
|
171
|
-
transport: withFeePayer(http(), http(server.url), {
|
|
172
|
-
policy: 'sign-and-broadcast',
|
|
173
|
-
}),
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const receipt = await sendTransactionSync(client, {
|
|
177
|
-
feePayer: true,
|
|
178
|
-
to: '0x0000000000000000000000000000000000000002',
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
182
|
-
|
|
183
|
-
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
|
|
184
|
-
[
|
|
185
|
-
"eth_sendRawTransactionSync",
|
|
186
|
-
]
|
|
187
|
-
`)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
test('behavior: unsupported method', async () => {
|
|
191
|
-
await expect(fp.request({ method: 'eth_chainId' })).rejects.toThrowError()
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
test('behavior: internal error', async () => {
|
|
195
|
-
await expect(
|
|
196
|
-
fp.request({
|
|
197
|
-
method: 'eth_signRawTransaction' as never,
|
|
198
|
-
params: ['0xinvalid'],
|
|
199
|
-
}),
|
|
200
|
-
).rejects.toThrowError()
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
describe('behavior: conditional sponsoring', () => {
|
|
205
|
-
const rejectedAccount = accounts[3]!
|
|
206
|
-
let conditionalServer: Server
|
|
207
|
-
let conditionalFp: ReturnType<typeof getClient>
|
|
208
|
-
|
|
209
|
-
beforeAll(async () => {
|
|
210
|
-
// Fund accounts with alphaUsd for transfers + fee payment.
|
|
211
|
-
const rpc = getClient()
|
|
212
|
-
await Actions.token.mintSync(rpc, {
|
|
213
|
-
account: accounts[0]!,
|
|
214
|
-
token: addresses.alphaUsd,
|
|
215
|
-
amount: parseUnits('100', 6),
|
|
216
|
-
to: userAccount.address,
|
|
217
|
-
})
|
|
218
|
-
await Actions.fee.setUserToken(rpc, { account: userAccount, token: addresses.alphaUsd })
|
|
219
|
-
await Actions.token.mintSync(rpc, {
|
|
220
|
-
account: accounts[0]!,
|
|
221
|
-
token: addresses.alphaUsd,
|
|
222
|
-
amount: parseUnits('100', 6),
|
|
223
|
-
to: rejectedAccount.address,
|
|
224
|
-
})
|
|
225
|
-
await Actions.fee.setUserToken(rpc, { account: rejectedAccount, token: addresses.alphaUsd })
|
|
226
|
-
|
|
227
|
-
conditionalServer = await createServer(
|
|
228
|
-
feePayer({
|
|
229
|
-
account: feePayerAccount,
|
|
230
|
-
chains: [chain],
|
|
231
|
-
transports: { [chain.id]: http() },
|
|
232
|
-
validate: (request) =>
|
|
233
|
-
request.from?.toLowerCase() !== rejectedAccount.address.toLowerCase(),
|
|
234
|
-
}).listener,
|
|
235
|
-
)
|
|
236
|
-
conditionalFp = getClient({ transport: http(conditionalServer.url) })
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
afterAll(() => {
|
|
240
|
-
conditionalServer.close()
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
test('behavior: approved tx is sponsored and can be broadcast', async () => {
|
|
244
|
-
const { data, to } = Actions.token.transfer.call({
|
|
245
|
-
token: addresses.alphaUsd,
|
|
246
|
-
to: rejectedAccount.address,
|
|
247
|
-
amount: 1n,
|
|
248
|
-
})
|
|
249
|
-
const response = (await conditionalFp.request({
|
|
250
|
-
method: 'eth_fillTransaction',
|
|
251
|
-
params: [
|
|
252
|
-
{
|
|
253
|
-
chainId: chain.id,
|
|
254
|
-
feePayer: true,
|
|
255
|
-
from: userAccount.address,
|
|
256
|
-
to,
|
|
257
|
-
data,
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
})) as { sponsor?: { address: string }; tx: Record<string, unknown> }
|
|
261
|
-
|
|
262
|
-
const prepared = core_Transaction.fromRpc(response.tx as never) as {
|
|
263
|
-
feePayerSignature?: unknown
|
|
264
|
-
}
|
|
265
|
-
expect(prepared.feePayerSignature).toBeDefined()
|
|
266
|
-
expect(response.sponsor?.address).toBe(feePayerAccount.address)
|
|
267
|
-
|
|
268
|
-
const signed = await signSponsoredTx(userAccount, prepared)
|
|
269
|
-
const receipt = (await getClient().request({
|
|
270
|
-
method: 'eth_sendRawTransactionSync',
|
|
271
|
-
params: [signed],
|
|
272
|
-
})) as { feePayer?: string | undefined }
|
|
273
|
-
|
|
274
|
-
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
test('behavior: rejected tx is not sponsored and can be self-paid', async () => {
|
|
278
|
-
const { data, to } = Actions.token.transfer.call({
|
|
279
|
-
token: addresses.alphaUsd,
|
|
280
|
-
to: userAccount.address,
|
|
281
|
-
amount: 1n,
|
|
282
|
-
})
|
|
283
|
-
const response = (await conditionalFp.request({
|
|
284
|
-
method: 'eth_fillTransaction',
|
|
285
|
-
params: [
|
|
286
|
-
{
|
|
287
|
-
chainId: chain.id,
|
|
288
|
-
feePayer: true,
|
|
289
|
-
from: rejectedAccount.address,
|
|
290
|
-
to,
|
|
291
|
-
data,
|
|
292
|
-
},
|
|
293
|
-
],
|
|
294
|
-
})) as { sponsor?: { address: string }; tx: Record<string, unknown> }
|
|
295
|
-
|
|
296
|
-
const prepared = core_Transaction.fromRpc(response.tx as never) as {
|
|
297
|
-
feePayerSignature?: unknown
|
|
298
|
-
}
|
|
299
|
-
expect(prepared.feePayerSignature).toBeUndefined()
|
|
300
|
-
expect(response.sponsor).toBeUndefined()
|
|
301
|
-
|
|
302
|
-
const signed = await signSponsoredTx(rejectedAccount, prepared)
|
|
303
|
-
const receipt = (await getClient().request({
|
|
304
|
-
method: 'eth_sendRawTransactionSync',
|
|
305
|
-
params: [signed],
|
|
306
|
-
})) as { feePayer?: string | undefined }
|
|
307
|
-
|
|
308
|
-
// Sender pays their own fee — no external fee payer.
|
|
309
|
-
expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
test('behavior: rejected raw transaction returns error', async () => {
|
|
313
|
-
// First fill without the conditional server to get a signed tx.
|
|
314
|
-
const response = (await fp.request({
|
|
315
|
-
method: 'eth_fillTransaction',
|
|
316
|
-
params: [
|
|
317
|
-
{
|
|
318
|
-
chainId: chain.id,
|
|
319
|
-
feePayer: true,
|
|
320
|
-
from: rejectedAccount.address,
|
|
321
|
-
to: '0x0000000000000000000000000000000000000000',
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
})) as { tx: Record<string, unknown> }
|
|
325
|
-
const prepared = core_Transaction.fromRpc(response.tx as never)
|
|
326
|
-
const signed = await signSponsoredTx(rejectedAccount, prepared)
|
|
327
|
-
|
|
328
|
-
// Submit the signed tx to the conditional server — should reject.
|
|
329
|
-
await expect(
|
|
330
|
-
conditionalFp.request({
|
|
331
|
-
method: 'eth_signRawTransaction' as never,
|
|
332
|
-
params: [signed],
|
|
333
|
-
}),
|
|
334
|
-
).rejects.toThrowError()
|
|
335
|
-
})
|
|
336
|
-
})
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import { RpcRequest, RpcResponse, Signature } from 'ox'
|
|
2
|
-
import { Transaction as core_Transaction, TxEnvelopeTempo } from 'ox/tempo'
|
|
3
|
-
import type { Address, Chain, Client, Transport } from 'viem'
|
|
4
|
-
import { createClient, http } from 'viem'
|
|
5
|
-
import type { LocalAccount } from 'viem/accounts'
|
|
6
|
-
import { signTransaction } from 'viem/actions'
|
|
7
|
-
import { tempo, tempoModerato } from 'viem/chains'
|
|
8
|
-
import { Transaction } from 'viem/tempo'
|
|
9
|
-
|
|
10
|
-
import { type Handler, from } from '../../Handler.js'
|
|
11
|
-
import * as Utils from './utils.js'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Instantiates a fee payer service request handler that can be used to
|
|
15
|
-
* sponsor the fee for user transactions.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```ts
|
|
19
|
-
* import { privateKeyToAccount } from 'viem/accounts'
|
|
20
|
-
* import { Handler } from 'accounts/server'
|
|
21
|
-
*
|
|
22
|
-
* const handler = Handler.feePayer({
|
|
23
|
-
* account: privateKeyToAccount('0x...'),
|
|
24
|
-
* })
|
|
25
|
-
*
|
|
26
|
-
* // Plug handler into your server framework of choice:
|
|
27
|
-
* createServer(handler.listener) // Node.js
|
|
28
|
-
* Bun.serve(handler) // Bun
|
|
29
|
-
* Deno.serve(handler) // Deno
|
|
30
|
-
* app.use(handler.listener) // Express
|
|
31
|
-
* app.all('*', c => handler.fetch(c.req.raw)) // Hono
|
|
32
|
-
* export const GET = handler.fetch // Next.js
|
|
33
|
-
* export const POST = handler.fetch // Next.js
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* @param options - Options.
|
|
37
|
-
* @returns Request handler.
|
|
38
|
-
*/
|
|
39
|
-
export function feePayer(options: feePayer.Options): Handler {
|
|
40
|
-
const {
|
|
41
|
-
account,
|
|
42
|
-
chains = [tempo, tempoModerato],
|
|
43
|
-
name,
|
|
44
|
-
onRequest,
|
|
45
|
-
path = '/',
|
|
46
|
-
validate,
|
|
47
|
-
transports = {},
|
|
48
|
-
url,
|
|
49
|
-
...rest
|
|
50
|
-
} = options
|
|
51
|
-
|
|
52
|
-
const clients = new Map<number, Client>()
|
|
53
|
-
for (const chain of chains) {
|
|
54
|
-
const transport = transports[chain.id] ?? http()
|
|
55
|
-
clients.set(chain.id, createClient({ chain, transport }))
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function getClient(chainId?: number): Client {
|
|
59
|
-
if (chainId) {
|
|
60
|
-
const client = clients.get(chainId)
|
|
61
|
-
if (!client) throw new Error(`Chain ${chainId} not configured`)
|
|
62
|
-
return client
|
|
63
|
-
}
|
|
64
|
-
return clients.get(chains[0]!.id)!
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const sponsor = {
|
|
68
|
-
address: account.address,
|
|
69
|
-
...(name ? { name } : {}),
|
|
70
|
-
...(url ? { url } : {}),
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const router = from(rest)
|
|
74
|
-
|
|
75
|
-
router.post(path, async (c) => {
|
|
76
|
-
const request = RpcRequest.from((await c.req.raw.json()) as any)
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
await onRequest?.(request)
|
|
80
|
-
|
|
81
|
-
const method = request.method as string
|
|
82
|
-
if (
|
|
83
|
-
method !== 'eth_fillTransaction' &&
|
|
84
|
-
method !== 'eth_signRawTransaction' &&
|
|
85
|
-
method !== 'eth_sendRawTransaction' &&
|
|
86
|
-
method !== 'eth_sendRawTransactionSync'
|
|
87
|
-
)
|
|
88
|
-
return Response.json(
|
|
89
|
-
RpcResponse.from(
|
|
90
|
-
{
|
|
91
|
-
error: new RpcResponse.MethodNotSupportedError({
|
|
92
|
-
message: `Method not supported: ${request.method}`,
|
|
93
|
-
}),
|
|
94
|
-
},
|
|
95
|
-
{ request },
|
|
96
|
-
),
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
if (method === 'eth_fillTransaction') {
|
|
100
|
-
const [parameters] = Utils.parseParams.parse(request.params) as [Record<string, unknown>]
|
|
101
|
-
const chainId = Utils.resolveChainId(parameters.chainId)
|
|
102
|
-
const client = getClient(chainId)
|
|
103
|
-
const normalized = Utils.normalizeFillTransactionRequest(parameters)
|
|
104
|
-
const sender =
|
|
105
|
-
typeof parameters.from === 'string' ? (parameters.from as Address) : undefined
|
|
106
|
-
|
|
107
|
-
let transaction = await (async () => {
|
|
108
|
-
if (isPreparedFeePayerTransaction(parameters))
|
|
109
|
-
return Utils.normalizeTempoTransaction(parameters)
|
|
110
|
-
|
|
111
|
-
const fillRequest = Utils.formatFillTransactionRequest(client, {
|
|
112
|
-
...normalized,
|
|
113
|
-
...(typeof chainId !== 'undefined' ? { chainId } : {}),
|
|
114
|
-
feePayer: true,
|
|
115
|
-
})
|
|
116
|
-
const result = (await client.request({
|
|
117
|
-
method: 'eth_fillTransaction' as never,
|
|
118
|
-
params: [fillRequest],
|
|
119
|
-
})) as { tx?: Record<string, unknown> | undefined }
|
|
120
|
-
return Utils.normalizeTempoTransaction(result.tx)
|
|
121
|
-
})()
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
validate &&
|
|
125
|
-
// @ts-expect-error - TODO: Convert to `TransactionRequest` properly.
|
|
126
|
-
!(await validate({
|
|
127
|
-
...transaction,
|
|
128
|
-
from: sender,
|
|
129
|
-
} as Transaction.TransactionRequest))
|
|
130
|
-
) {
|
|
131
|
-
// Re-fill without feePayer so gas/nonce are correct for self-payment.
|
|
132
|
-
const fillRequest = Utils.formatFillTransactionRequest(client, {
|
|
133
|
-
...normalized,
|
|
134
|
-
...(typeof chainId !== 'undefined' ? { chainId } : {}),
|
|
135
|
-
})
|
|
136
|
-
const result = (await client.request({
|
|
137
|
-
method: 'eth_fillTransaction' as never,
|
|
138
|
-
params: [fillRequest],
|
|
139
|
-
})) as { tx?: Record<string, unknown> | undefined }
|
|
140
|
-
transaction = Utils.normalizeTempoTransaction(result.tx)
|
|
141
|
-
|
|
142
|
-
return Utils.rpcResult(request, {
|
|
143
|
-
tx: core_Transaction.toRpc(transaction as core_Transaction.Transaction),
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const signed = await sign({ account, transaction, sender })
|
|
148
|
-
|
|
149
|
-
return Utils.rpcResult(request, {
|
|
150
|
-
sponsor,
|
|
151
|
-
tx: core_Transaction.toRpc(signed as core_Transaction.Transaction),
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const serialized = request.params?.[0] as `0x76${string}` | undefined
|
|
156
|
-
|
|
157
|
-
if (!serialized?.startsWith('0x76') && !serialized?.startsWith('0x78'))
|
|
158
|
-
throw new RpcResponse.InvalidParamsError({
|
|
159
|
-
message: 'Only Tempo (0x76/0x78) transactions are supported.',
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
const transaction = Transaction.deserialize(serialized)
|
|
163
|
-
|
|
164
|
-
if (!transaction.signature || !transaction.from)
|
|
165
|
-
throw new RpcResponse.InvalidParamsError({
|
|
166
|
-
message: 'Transaction must be signed by the sender before fee payer signing.',
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
if (validate && !(await validate(transaction as Transaction.TransactionRequest)))
|
|
170
|
-
throw new RpcResponse.InvalidParamsError({
|
|
171
|
-
message: 'Sponsorship rejected.',
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const client = getClient(transaction.chainId)
|
|
175
|
-
const serializedTransaction = toSerializedTransaction(
|
|
176
|
-
await signTransaction(client, {
|
|
177
|
-
...transaction,
|
|
178
|
-
account,
|
|
179
|
-
feePayer: account,
|
|
180
|
-
} as never),
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
if (method === 'eth_signRawTransaction')
|
|
184
|
-
return Response.json(RpcResponse.from({ result: serializedTransaction }, { request }))
|
|
185
|
-
|
|
186
|
-
const result = await client.request({
|
|
187
|
-
method: method as never,
|
|
188
|
-
params: [serializedTransaction],
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
return Response.json(RpcResponse.from({ result }, { request }))
|
|
192
|
-
} catch (error) {
|
|
193
|
-
return Utils.rpcError(request, error)
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
return router
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export declare namespace feePayer {
|
|
201
|
-
type Options = from.Options & {
|
|
202
|
-
/** Account to use as the fee payer. */
|
|
203
|
-
account: LocalAccount
|
|
204
|
-
/**
|
|
205
|
-
* Supported chains. The handler resolves the client based on the
|
|
206
|
-
* `chainId` in the incoming transaction.
|
|
207
|
-
* @default [tempo, tempoModerato]
|
|
208
|
-
*/
|
|
209
|
-
chains?: readonly [Chain, ...Chain[]] | undefined
|
|
210
|
-
/** Function to call before handling the request. */
|
|
211
|
-
onRequest?: (request: RpcRequest.RpcRequest) => Promise<void>
|
|
212
|
-
/** Path to use for the handler. */
|
|
213
|
-
path?: string | undefined
|
|
214
|
-
/**
|
|
215
|
-
* Validates whether to sponsor the transaction. When omitted, all
|
|
216
|
-
* transactions are sponsored. Return `false` to reject sponsorship.
|
|
217
|
-
*/
|
|
218
|
-
validate?: ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>) | undefined
|
|
219
|
-
/** Sponsor display name returned from `eth_fillTransaction`. */
|
|
220
|
-
name?: string | undefined
|
|
221
|
-
/** Transports keyed by chain ID. Defaults to `http()` for each chain. */
|
|
222
|
-
transports?: Record<number, Transport> | undefined
|
|
223
|
-
/** Sponsor URL returned from `eth_fillTransaction`. */
|
|
224
|
-
url?: string | undefined
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Signs a filled transaction as the fee payer. */
|
|
229
|
-
export async function sign(options: {
|
|
230
|
-
account: LocalAccount
|
|
231
|
-
transaction: Record<string, unknown>
|
|
232
|
-
sender?: `0x${string}` | undefined
|
|
233
|
-
}) {
|
|
234
|
-
const { account, transaction, sender } = options
|
|
235
|
-
const from = (transaction.from as `0x${string}` | undefined) ?? sender
|
|
236
|
-
const { signature: _, ...withoutSenderSig } = transaction
|
|
237
|
-
const prepared = { ...withoutSenderSig, from }
|
|
238
|
-
|
|
239
|
-
if (!prepared.from)
|
|
240
|
-
throw new RpcResponse.InvalidParamsError({
|
|
241
|
-
message: 'Transaction sender must be provided before fee payer signing.',
|
|
242
|
-
})
|
|
243
|
-
if (!account.sign) throw new Error('Fee payer account cannot sign transactions.')
|
|
244
|
-
|
|
245
|
-
const feePayerSignature = Signature.from(
|
|
246
|
-
await account.sign({
|
|
247
|
-
hash: TxEnvelopeTempo.getFeePayerSignPayload(TxEnvelopeTempo.from(prepared as never), {
|
|
248
|
-
sender: prepared.from,
|
|
249
|
-
}),
|
|
250
|
-
}),
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
return { ...prepared, feePayerSignature }
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function isPreparedFeePayerTransaction(value: Record<string, unknown>) {
|
|
257
|
-
return (
|
|
258
|
-
typeof value.from === 'string' &&
|
|
259
|
-
typeof Utils.resolveChainId(value.chainId) === 'number' &&
|
|
260
|
-
typeof value.gas !== 'undefined' &&
|
|
261
|
-
typeof value.nonce !== 'undefined' &&
|
|
262
|
-
(typeof value.maxFeePerGas !== 'undefined' || typeof value.gasPrice !== 'undefined')
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function toSerializedTransaction(value: unknown) {
|
|
267
|
-
if (typeof value === 'string') return value
|
|
268
|
-
if (value && typeof value === 'object' && 'raw' in value && typeof value.raw === 'string')
|
|
269
|
-
return value.raw
|
|
270
|
-
throw new Error('Expected a serialized transaction result.')
|
|
271
|
-
}
|