accounts 0.6.1 → 0.6.2
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 +8 -0
- package/dist/core/Schema.d.ts +12 -12
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +3 -1
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +9 -9
- package/dist/core/zod/rpc.js +1 -1
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/server/CliAuth.d.ts +11 -11
- package/dist/server/CliAuth.js +1 -1
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/Handler.d.ts +4 -252
- package/dist/server/Handler.d.ts.map +1 -1
- package/dist/server/Handler.js +4 -573
- package/dist/server/Handler.js.map +1 -1
- package/dist/server/internal/handlers/codeAuth.d.ts +41 -0
- package/dist/server/internal/handlers/codeAuth.d.ts.map +1 -0
- package/dist/server/internal/handlers/codeAuth.js +104 -0
- package/dist/server/internal/handlers/codeAuth.js.map +1 -0
- package/dist/server/internal/handlers/feePayer.d.ts +73 -0
- package/dist/server/internal/handlers/feePayer.d.ts.map +1 -0
- package/dist/server/internal/handlers/feePayer.js +184 -0
- package/dist/server/internal/handlers/feePayer.js.map +1 -0
- package/dist/server/internal/handlers/relay.d.ts +148 -0
- package/dist/server/internal/handlers/relay.d.ts.map +1 -0
- package/dist/server/internal/handlers/relay.js +600 -0
- package/dist/server/internal/handlers/relay.js.map +1 -0
- package/dist/server/internal/handlers/utils.d.ts +12 -0
- package/dist/server/internal/handlers/utils.d.ts.map +1 -0
- package/dist/server/internal/handlers/utils.js +80 -0
- package/dist/server/internal/handlers/utils.js.map +1 -0
- package/dist/server/internal/handlers/webAuthn.d.ts +57 -0
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -0
- package/dist/server/internal/handlers/webAuthn.js +143 -0
- package/dist/server/internal/handlers/webAuthn.js.map +1 -0
- package/package.json +2 -2
- package/src/core/Provider.connect.browser.test.ts +23 -2
- package/src/core/adapters/dialog.ts +6 -1
- package/src/core/zod/rpc.ts +1 -1
- package/src/server/CliAuth.ts +1 -1
- package/src/server/Handler.test.ts +3 -418
- package/src/server/Handler.ts +5 -766
- package/src/server/internal/handlers/codeAuth.ts +148 -0
- package/src/server/internal/handlers/feePayer.test.ts +335 -0
- package/src/server/internal/handlers/feePayer.ts +271 -0
- package/src/server/internal/handlers/relay.test.ts +767 -0
- package/src/server/internal/handlers/relay.ts +817 -0
- package/src/server/internal/handlers/utils.ts +96 -0
- package/src/server/internal/handlers/webAuthn.test.ts +170 -0
- package/src/server/internal/handlers/webAuthn.ts +213 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Chain, Client, Transport } from 'viem'
|
|
2
|
+
import { createClient, http } from 'viem'
|
|
3
|
+
import { tempo, tempoModerato } from 'viem/chains'
|
|
4
|
+
import * as z from 'zod/mini'
|
|
5
|
+
|
|
6
|
+
import * as CliAuth from '../../CliAuth.js'
|
|
7
|
+
import { type Handler, from } from '../../Handler.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Instantiates a generic device-code handler for access-key bootstrap.
|
|
11
|
+
*
|
|
12
|
+
* Exposes 4 endpoints:
|
|
13
|
+
* - `GET /auth/pkce/pending/:code`
|
|
14
|
+
* - `POST /auth/pkce/code`
|
|
15
|
+
* - `POST /auth/pkce/poll/:code`
|
|
16
|
+
* - `POST /auth/pkce`
|
|
17
|
+
*
|
|
18
|
+
* @param options - Options.
|
|
19
|
+
* @returns Request handler.
|
|
20
|
+
*/
|
|
21
|
+
export function codeAuth(options: codeAuth.Options = {}): Handler {
|
|
22
|
+
const {
|
|
23
|
+
chains = [tempo, tempoModerato],
|
|
24
|
+
now,
|
|
25
|
+
path = '/auth/pkce',
|
|
26
|
+
policy,
|
|
27
|
+
random,
|
|
28
|
+
store = CliAuth.Store.memory(),
|
|
29
|
+
transports = {},
|
|
30
|
+
ttlMs,
|
|
31
|
+
...rest
|
|
32
|
+
} = options
|
|
33
|
+
|
|
34
|
+
const clients = new Map<number, Client>()
|
|
35
|
+
for (const chain of chains) {
|
|
36
|
+
const transport = transports[chain.id] ?? http()
|
|
37
|
+
clients.set(chain.id, createClient({ chain, transport }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getClient(chainId?: bigint | number): Client {
|
|
41
|
+
if (typeof chainId !== 'undefined') {
|
|
42
|
+
const id = Number(chainId)
|
|
43
|
+
const client = clients.get(id)
|
|
44
|
+
if (!client) throw new Error(`Chain ${id} not configured`)
|
|
45
|
+
return client
|
|
46
|
+
}
|
|
47
|
+
return clients.get(chains[0]!.id)!
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const router = from(rest)
|
|
51
|
+
|
|
52
|
+
router.get(`${path}/pending/:code`, async (c) => {
|
|
53
|
+
try {
|
|
54
|
+
const code = c.req.param('code')
|
|
55
|
+
const result = await CliAuth.pending({
|
|
56
|
+
code,
|
|
57
|
+
...(now ? { now } : {}),
|
|
58
|
+
store,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return Response.json(z.encode(CliAuth.pendingResponse, result))
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const status = error instanceof CliAuth.PendingError ? error.status : 400
|
|
64
|
+
return Response.json({ error: (error as Error).message }, { status })
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
router.post(`${path}/code`, async (c) => {
|
|
69
|
+
try {
|
|
70
|
+
const request = z.decode(CliAuth.createRequest, await c.req.raw.json())
|
|
71
|
+
const chainId = request.chainId ?? chains[0]!.id
|
|
72
|
+
getClient(chainId)
|
|
73
|
+
const result = await CliAuth.createDeviceCode({
|
|
74
|
+
chainId,
|
|
75
|
+
...(now ? { now } : {}),
|
|
76
|
+
...(policy ? { policy } : {}),
|
|
77
|
+
...(random ? { random } : {}),
|
|
78
|
+
request,
|
|
79
|
+
store,
|
|
80
|
+
...(typeof ttlMs !== 'undefined' ? { ttlMs } : {}),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return Response.json(z.encode(CliAuth.createResponse, result))
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
router.post(`${path}/poll/:code`, async (c) => {
|
|
90
|
+
try {
|
|
91
|
+
const request = z.decode(CliAuth.pollRequest, await c.req.raw.json())
|
|
92
|
+
const code = c.req.param('code')
|
|
93
|
+
const result = await CliAuth.poll({
|
|
94
|
+
code,
|
|
95
|
+
...(now ? { now } : {}),
|
|
96
|
+
request,
|
|
97
|
+
store,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return Response.json(z.encode(CliAuth.pollResponse, result))
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
router.post(path, async (c) => {
|
|
107
|
+
try {
|
|
108
|
+
const request = z.decode(CliAuth.authorizeRequest, await c.req.raw.json())
|
|
109
|
+
const result = await CliAuth.authorize({
|
|
110
|
+
client: getClient(request.keyAuthorization.chainId),
|
|
111
|
+
...(now ? { now } : {}),
|
|
112
|
+
request,
|
|
113
|
+
store,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return Response.json(z.encode(CliAuth.authorizeResponse, result))
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return router
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export declare namespace codeAuth {
|
|
126
|
+
export type Options = from.Options & {
|
|
127
|
+
/**
|
|
128
|
+
* Supported chains. The handler resolves the client based on chain IDs carried
|
|
129
|
+
* by device-code requests and key authorizations.
|
|
130
|
+
* @default [tempo, tempoModerato]
|
|
131
|
+
*/
|
|
132
|
+
chains?: readonly [Chain, ...Chain[]] | undefined
|
|
133
|
+
/** Time source used for TTL evaluation. */
|
|
134
|
+
now?: (() => number) | undefined
|
|
135
|
+
/** Path prefix for the code auth endpoints. @default "/auth/pkce" */
|
|
136
|
+
path?: string | undefined
|
|
137
|
+
/** Policy used to validate and default requested CLI auth fields. */
|
|
138
|
+
policy?: CliAuth.Policy | undefined
|
|
139
|
+
/** Random byte generator used for device-code allocation. */
|
|
140
|
+
random?: ((size: number) => Uint8Array) | undefined
|
|
141
|
+
/** Device-code store. */
|
|
142
|
+
store?: CliAuth.Store | undefined
|
|
143
|
+
/** Transports keyed by chain ID. Defaults to `http()` for each chain. */
|
|
144
|
+
transports?: Record<number, Transport> | undefined
|
|
145
|
+
/** Pending entry TTL in milliseconds. @default 600000 */
|
|
146
|
+
ttlMs?: number | undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type { RpcRequest } from 'ox'
|
|
2
|
+
import { SignatureEnvelope, Transaction as core_Transaction, TxEnvelopeTempo } from 'ox/tempo'
|
|
3
|
+
import { parseUnits } from 'viem'
|
|
4
|
+
import { sendTransactionSync } 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 receipt = await sendTransactionSync(client, {
|
|
154
|
+
feePayer: true,
|
|
155
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
159
|
+
|
|
160
|
+
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
|
|
161
|
+
[
|
|
162
|
+
"eth_sendRawTransactionSync",
|
|
163
|
+
]
|
|
164
|
+
`)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('behavior: eth_sendRawTransactionSync', async () => {
|
|
168
|
+
const client = getClient({
|
|
169
|
+
account: userAccount,
|
|
170
|
+
transport: withFeePayer(http(), http(server.url), {
|
|
171
|
+
policy: 'sign-and-broadcast',
|
|
172
|
+
}),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const receipt = await sendTransactionSync(client, {
|
|
176
|
+
feePayer: true,
|
|
177
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
181
|
+
|
|
182
|
+
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
|
|
183
|
+
[
|
|
184
|
+
"eth_sendRawTransactionSync",
|
|
185
|
+
]
|
|
186
|
+
`)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('behavior: unsupported method', async () => {
|
|
190
|
+
await expect(fp.request({ method: 'eth_chainId' })).rejects.toThrowError()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('behavior: internal error', async () => {
|
|
194
|
+
await expect(
|
|
195
|
+
fp.request({
|
|
196
|
+
method: 'eth_signRawTransaction' as never,
|
|
197
|
+
params: ['0xinvalid'],
|
|
198
|
+
}),
|
|
199
|
+
).rejects.toThrowError()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('behavior: conditional sponsoring', () => {
|
|
204
|
+
const rejectedAccount = accounts[3]!
|
|
205
|
+
let conditionalServer: Server
|
|
206
|
+
let conditionalFp: ReturnType<typeof getClient>
|
|
207
|
+
|
|
208
|
+
beforeAll(async () => {
|
|
209
|
+
// Fund accounts with alphaUsd for transfers + fee payment.
|
|
210
|
+
const rpc = getClient()
|
|
211
|
+
await Actions.token.mintSync(rpc, {
|
|
212
|
+
account: accounts[0]!,
|
|
213
|
+
token: addresses.alphaUsd,
|
|
214
|
+
amount: parseUnits('100', 6),
|
|
215
|
+
to: userAccount.address,
|
|
216
|
+
})
|
|
217
|
+
await Actions.fee.setUserToken(rpc, { account: userAccount, token: addresses.alphaUsd })
|
|
218
|
+
await Actions.token.mintSync(rpc, {
|
|
219
|
+
account: accounts[0]!,
|
|
220
|
+
token: addresses.alphaUsd,
|
|
221
|
+
amount: parseUnits('100', 6),
|
|
222
|
+
to: rejectedAccount.address,
|
|
223
|
+
})
|
|
224
|
+
await Actions.fee.setUserToken(rpc, { account: rejectedAccount, token: addresses.alphaUsd })
|
|
225
|
+
|
|
226
|
+
conditionalServer = await createServer(
|
|
227
|
+
feePayer({
|
|
228
|
+
account: feePayerAccount,
|
|
229
|
+
chains: [chain],
|
|
230
|
+
transports: { [chain.id]: http() },
|
|
231
|
+
validate: (request) =>
|
|
232
|
+
request.from?.toLowerCase() !== rejectedAccount.address.toLowerCase(),
|
|
233
|
+
}).listener,
|
|
234
|
+
)
|
|
235
|
+
conditionalFp = getClient({ transport: http(conditionalServer.url) })
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
afterAll(() => {
|
|
239
|
+
conditionalServer.close()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('behavior: approved tx is sponsored and can be broadcast', async () => {
|
|
243
|
+
const { data, to } = Actions.token.transfer.call({
|
|
244
|
+
token: addresses.alphaUsd,
|
|
245
|
+
to: rejectedAccount.address,
|
|
246
|
+
amount: 1n,
|
|
247
|
+
})
|
|
248
|
+
const response = (await conditionalFp.request({
|
|
249
|
+
method: 'eth_fillTransaction',
|
|
250
|
+
params: [
|
|
251
|
+
{
|
|
252
|
+
chainId: chain.id,
|
|
253
|
+
feePayer: true,
|
|
254
|
+
from: userAccount.address,
|
|
255
|
+
to,
|
|
256
|
+
data,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
})) as { sponsor?: { address: string }; tx: Record<string, unknown> }
|
|
260
|
+
|
|
261
|
+
const prepared = core_Transaction.fromRpc(response.tx as never) as {
|
|
262
|
+
feePayerSignature?: unknown
|
|
263
|
+
}
|
|
264
|
+
expect(prepared.feePayerSignature).toBeDefined()
|
|
265
|
+
expect(response.sponsor?.address).toBe(feePayerAccount.address)
|
|
266
|
+
|
|
267
|
+
const signed = await signSponsoredTx(userAccount, prepared)
|
|
268
|
+
const receipt = (await getClient().request({
|
|
269
|
+
method: 'eth_sendRawTransactionSync',
|
|
270
|
+
params: [signed],
|
|
271
|
+
})) as { feePayer?: string | undefined }
|
|
272
|
+
|
|
273
|
+
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('behavior: rejected tx is not sponsored and can be self-paid', async () => {
|
|
277
|
+
const { data, to } = Actions.token.transfer.call({
|
|
278
|
+
token: addresses.alphaUsd,
|
|
279
|
+
to: userAccount.address,
|
|
280
|
+
amount: 1n,
|
|
281
|
+
})
|
|
282
|
+
const response = (await conditionalFp.request({
|
|
283
|
+
method: 'eth_fillTransaction',
|
|
284
|
+
params: [
|
|
285
|
+
{
|
|
286
|
+
chainId: chain.id,
|
|
287
|
+
feePayer: true,
|
|
288
|
+
from: rejectedAccount.address,
|
|
289
|
+
to,
|
|
290
|
+
data,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
})) as { sponsor?: { address: string }; tx: Record<string, unknown> }
|
|
294
|
+
|
|
295
|
+
const prepared = core_Transaction.fromRpc(response.tx as never) as {
|
|
296
|
+
feePayerSignature?: unknown
|
|
297
|
+
}
|
|
298
|
+
expect(prepared.feePayerSignature).toBeUndefined()
|
|
299
|
+
expect(response.sponsor).toBeUndefined()
|
|
300
|
+
|
|
301
|
+
const signed = await signSponsoredTx(rejectedAccount, prepared)
|
|
302
|
+
const receipt = (await getClient().request({
|
|
303
|
+
method: 'eth_sendRawTransactionSync',
|
|
304
|
+
params: [signed],
|
|
305
|
+
})) as { feePayer?: string | undefined }
|
|
306
|
+
|
|
307
|
+
// Sender pays their own fee — no external fee payer.
|
|
308
|
+
expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('behavior: rejected raw transaction returns error', async () => {
|
|
312
|
+
// First fill without the conditional server to get a signed tx.
|
|
313
|
+
const response = (await fp.request({
|
|
314
|
+
method: 'eth_fillTransaction',
|
|
315
|
+
params: [
|
|
316
|
+
{
|
|
317
|
+
chainId: chain.id,
|
|
318
|
+
feePayer: true,
|
|
319
|
+
from: rejectedAccount.address,
|
|
320
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
})) as { tx: Record<string, unknown> }
|
|
324
|
+
const prepared = core_Transaction.fromRpc(response.tx as never)
|
|
325
|
+
const signed = await signSponsoredTx(rejectedAccount, prepared)
|
|
326
|
+
|
|
327
|
+
// Submit the signed tx to the conditional server — should reject.
|
|
328
|
+
await expect(
|
|
329
|
+
conditionalFp.request({
|
|
330
|
+
method: 'eth_signRawTransaction' as never,
|
|
331
|
+
params: [signed],
|
|
332
|
+
}),
|
|
333
|
+
).rejects.toThrowError()
|
|
334
|
+
})
|
|
335
|
+
})
|