accounts 0.18.4 → 0.19.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 +20 -0
- package/dist/core/AccessKey.d.ts +5 -1
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +2 -0
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Adapter.d.ts +4 -0
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Client.d.ts.map +1 -1
- package/dist/core/Client.js +62 -2
- package/dist/core/Client.js.map +1 -1
- package/dist/core/ExecutionError.d.ts.map +1 -1
- package/dist/core/ExecutionError.js +38 -3
- package/dist/core/ExecutionError.js.map +1 -1
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +16 -4
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +43 -0
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/adapters/privy.d.ts.map +1 -1
- package/dist/core/adapters/privy.js +7 -2
- package/dist/core/adapters/privy.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +18 -0
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +13 -2
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/react/Remote.d.ts.map +1 -1
- package/dist/react/Remote.js +5 -1
- package/dist/react/Remote.js.map +1 -1
- package/dist/server/CliAuth.d.ts.map +1 -1
- package/dist/server/CliAuth.js +6 -4
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +0 -7
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +0 -46
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/sponsorship.d.ts +0 -2
- package/dist/server/internal/handlers/sponsorship.d.ts.map +1 -1
- package/dist/server/internal/handlers/sponsorship.js +1 -2
- package/dist/server/internal/handlers/sponsorship.js.map +1 -1
- package/dist/trusted-hosts.json +1 -0
- package/package.json +5 -5
- package/src/cli/Provider.localnet.test.ts +3 -3
- package/src/core/AccessKey.permissions.test.ts +73 -0
- package/src/core/AccessKey.test.ts +29 -27
- package/src/core/AccessKey.ts +7 -1
- package/src/core/Adapter.ts +4 -0
- package/src/core/Client.test.ts +153 -1
- package/src/core/Client.ts +73 -2
- package/src/core/ExecutionError.ts +39 -3
- package/src/core/Provider.localnet.test.ts +116 -1
- package/src/core/Provider.ts +25 -8
- package/src/core/Schema.test-d.ts +14 -0
- package/src/core/adapters/deviceCode/deviceCode.localnet.test.ts +4 -1
- package/src/core/adapters/privy.test.ts +150 -8
- package/src/core/adapters/privy.ts +13 -5
- package/src/core/zod/request.test.ts +26 -0
- package/src/core/zod/rpc.ts +15 -4
- package/src/react/Remote.test.ts +153 -0
- package/src/react/Remote.ts +7 -1
- package/src/server/CliAuth.test.ts +5 -3
- package/src/server/CliAuth.ts +6 -4
- package/src/server/internal/handlers/relay.ts +0 -56
- package/src/server/internal/handlers/sponsorship.ts +1 -4
- package/src/trusted-hosts.json +1 -0
- package/dist/server/internal/handlers/multisig.d.ts +0 -214
- package/dist/server/internal/handlers/multisig.d.ts.map +0 -1
- package/dist/server/internal/handlers/multisig.js +0 -505
- package/dist/server/internal/handlers/multisig.js.map +0 -1
- package/src/server/internal/handlers/multisig.localnet.test.ts +0 -94
- package/src/server/internal/handlers/multisig.test.ts +0 -743
- package/src/server/internal/handlers/multisig.ts +0 -774
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Json } from 'ox'
|
|
2
|
+
import { KeyAuthorization } from 'ox/tempo'
|
|
3
|
+
import { describe, expect, test } from 'vp/test'
|
|
4
|
+
|
|
5
|
+
import { accounts, privateKeys } from '../../test/config.js'
|
|
6
|
+
import * as Storage from './Storage.js'
|
|
7
|
+
import * as Store from './Store.js'
|
|
8
|
+
|
|
9
|
+
const account = accounts[0]!.address
|
|
10
|
+
const address = accounts[1]!.address
|
|
11
|
+
|
|
12
|
+
describe('stored permission semantics', () => {
|
|
13
|
+
test.each([undefined, [], [{ token: address, limit: 100n }]])(
|
|
14
|
+
'preserves limits through authorization updates and persistence: %#',
|
|
15
|
+
async (limits) => {
|
|
16
|
+
const storage = Storage.memory()
|
|
17
|
+
const store = Store.create({ chainId: 1, storage })
|
|
18
|
+
await Store.waitForHydration(store)
|
|
19
|
+
const scopes = limits === undefined ? undefined : limits.length === 0 ? [] : [{ address }]
|
|
20
|
+
const authorization = KeyAuthorization.from(
|
|
21
|
+
{ address, chainId: 1n, type: 'secp256k1', limits, scopes },
|
|
22
|
+
{ signature: `0x${'00'.repeat(65)}` },
|
|
23
|
+
)
|
|
24
|
+
const key = store.accessKeys.add({ account, authorization, privateKey: privateKeys[1] })
|
|
25
|
+
expect(key.permissionSemantics).toMatchInlineSnapshot(`1`)
|
|
26
|
+
expect(key.limits).toEqual(limits)
|
|
27
|
+
expect(key.scopes).toEqual(scopes)
|
|
28
|
+
|
|
29
|
+
// Replace legacy metadata with a complete authorization, then clear the pending signature.
|
|
30
|
+
store.setState({
|
|
31
|
+
accessKeys: [{ ...key, permissionSemantics: undefined, limits: [], scopes: [] }],
|
|
32
|
+
})
|
|
33
|
+
store.accessKeys.updateAuthorization({
|
|
34
|
+
account,
|
|
35
|
+
accessKey: address,
|
|
36
|
+
chainId: 1,
|
|
37
|
+
authorization,
|
|
38
|
+
})
|
|
39
|
+
store.setState((state) => ({
|
|
40
|
+
accessKeys: state.accessKeys.map((key) => ({ ...key, keyAuthorization: undefined })),
|
|
41
|
+
}))
|
|
42
|
+
const restored = Store.create({ chainId: 1, storage })
|
|
43
|
+
await Store.waitForHydration(restored)
|
|
44
|
+
expect(Json.stringify(restored.getState().accessKeys)).toBe(
|
|
45
|
+
Json.stringify([{ ...key, keyAuthorization: undefined }]),
|
|
46
|
+
)
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
test('does not mark legacy records during hydration', async () => {
|
|
51
|
+
const storage = Storage.memory()
|
|
52
|
+
const store = Store.create({ chainId: 1, storage })
|
|
53
|
+
await Store.waitForHydration(store)
|
|
54
|
+
store.setState({
|
|
55
|
+
accessKeys: [
|
|
56
|
+
{
|
|
57
|
+
address,
|
|
58
|
+
access: account,
|
|
59
|
+
chainId: 1,
|
|
60
|
+
keyType: 'secp256k1',
|
|
61
|
+
limits: [],
|
|
62
|
+
scopes: [],
|
|
63
|
+
privateKey: privateKeys[1],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
const restored = Store.create({ chainId: 1, storage })
|
|
68
|
+
await Store.waitForHydration(restored)
|
|
69
|
+
expect(restored.getState().accessKeys[0]?.permissionSemantics).toMatchInlineSnapshot(
|
|
70
|
+
`undefined`,
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -23,7 +23,7 @@ function createKeyAuthorization(
|
|
|
23
23
|
options: {
|
|
24
24
|
chainId?: bigint | undefined
|
|
25
25
|
expiry?: number | undefined
|
|
26
|
-
keyType?:
|
|
26
|
+
keyType?: AccessKey.prepareAuthorization.Options['keyType'] | undefined
|
|
27
27
|
limits?: { token: `0x${string}`; limit: bigint; period?: number | undefined }[] | undefined
|
|
28
28
|
scopes?: KeyAuthorization.Scope[] | undefined
|
|
29
29
|
} = {},
|
|
@@ -506,18 +506,19 @@ describe('authorize', () => {
|
|
|
506
506
|
`)
|
|
507
507
|
expect(store.getState().accessKeys.map(({ keyAuthorization: _, ...accessKey }) => accessKey))
|
|
508
508
|
.toMatchInlineSnapshot(`
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
509
|
+
[
|
|
510
|
+
{
|
|
511
|
+
"access": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
|
512
|
+
"address": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650",
|
|
513
|
+
"chainId": 1,
|
|
514
|
+
"expiry": 123,
|
|
515
|
+
"keyType": "secp256k1",
|
|
516
|
+
"limits": undefined,
|
|
517
|
+
"permissionSemantics": 1,
|
|
518
|
+
"scopes": undefined,
|
|
519
|
+
},
|
|
520
|
+
]
|
|
521
|
+
`)
|
|
521
522
|
})
|
|
522
523
|
|
|
523
524
|
test('behavior: provisions a handle-backed record via the keystore', async () => {
|
|
@@ -590,19 +591,20 @@ describe('authorize', () => {
|
|
|
590
591
|
|
|
591
592
|
expect(store.getState().accessKeys.map(({ keyAuthorization: _, ...accessKey }) => accessKey))
|
|
592
593
|
.toMatchInlineSnapshot(`
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
594
|
+
[
|
|
595
|
+
{
|
|
596
|
+
"access": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
|
597
|
+
"address": "${accessKey.address}",
|
|
598
|
+
"chainId": 1,
|
|
599
|
+
"expiry": 123,
|
|
600
|
+
"keyType": "secp256k1",
|
|
601
|
+
"limits": undefined,
|
|
602
|
+
"permissionSemantics": 1,
|
|
603
|
+
"privateKey": "${privateKeys[1]}",
|
|
604
|
+
"scopes": undefined,
|
|
605
|
+
},
|
|
606
|
+
]
|
|
607
|
+
`)
|
|
606
608
|
})
|
|
607
609
|
})
|
|
608
610
|
|
|
@@ -1078,7 +1080,7 @@ describe('getStatus', () => {
|
|
|
1078
1080
|
|
|
1079
1081
|
Docs: https://viem.sh/docs/contract/readContract
|
|
1080
1082
|
Details: RPC unavailable.
|
|
1081
|
-
Version: viem@2.56.
|
|
1083
|
+
Version: viem@2.56.9]
|
|
1082
1084
|
`)
|
|
1083
1085
|
})
|
|
1084
1086
|
|
package/src/core/AccessKey.ts
CHANGED
|
@@ -44,6 +44,8 @@ export type AccessKey = {
|
|
|
44
44
|
keyType: 'secp256k1' | 'p256' | 'webAuthn' | 'webCrypto'
|
|
45
45
|
/** TIP-20 spending limits for the access key. */
|
|
46
46
|
limits?: { token: Address.Address; limit: bigint; period?: number | undefined }[] | undefined
|
|
47
|
+
/** Version of stored permission semantics: omitted fields are unrestricted, empty arrays deny all. Absent on legacy records. */
|
|
48
|
+
permissionSemantics?: 1 | undefined
|
|
47
49
|
/** Call scopes restricting which contracts/selectors this key can call. */
|
|
48
50
|
scopes?:
|
|
49
51
|
| {
|
|
@@ -345,7 +347,9 @@ export declare namespace prepareAuthorization {
|
|
|
345
347
|
/** Keystore-created key material reference. */
|
|
346
348
|
key?: { handle: Keystore.Handle; publicKey: Hex.Hex } | undefined
|
|
347
349
|
/** Unsigned key authorization to sign with the root account. */
|
|
348
|
-
keyAuthorization: KeyAuthorization.KeyAuthorization<false>
|
|
350
|
+
keyAuthorization: KeyAuthorization.KeyAuthorization<false> & {
|
|
351
|
+
type: NonNullable<Options['keyType']>
|
|
352
|
+
}
|
|
349
353
|
/** Exported private key backing an external access key. */
|
|
350
354
|
privateKey?: Hex.Hex | undefined
|
|
351
355
|
}
|
|
@@ -558,6 +562,7 @@ export function add(options: add.Options): add.ReturnType {
|
|
|
558
562
|
expiry: authorization.expiry ?? undefined,
|
|
559
563
|
keyAuthorization: authorization,
|
|
560
564
|
keyType: authorization.type,
|
|
565
|
+
permissionSemantics: 1 as const,
|
|
561
566
|
limits: authorization.limits as AccessKey['limits'],
|
|
562
567
|
scopes: authorization.scopes as AccessKey['scopes'],
|
|
563
568
|
}
|
|
@@ -624,6 +629,7 @@ function updateAuthorization(options: updateAuthorization.Options): void {
|
|
|
624
629
|
patch: {
|
|
625
630
|
expiry: authorization.expiry ?? undefined,
|
|
626
631
|
keyAuthorization: authorization,
|
|
632
|
+
permissionSemantics: 1,
|
|
627
633
|
limits: authorization.limits as AccessKey['limits'],
|
|
628
634
|
scopes: authorization.scopes as AccessKey['scopes'],
|
|
629
635
|
},
|
package/src/core/Adapter.ts
CHANGED
|
@@ -443,6 +443,10 @@ export declare namespace revokeAccessKey {
|
|
|
443
443
|
address: Address
|
|
444
444
|
/** Address of the access key to revoke. */
|
|
445
445
|
accessKeyAddress: Address
|
|
446
|
+
/** App-provided fee sponsorship for the revocation transaction. */
|
|
447
|
+
feePayer?: boolean | string | undefined
|
|
448
|
+
/** Signed authorization for an access key that has not been published on-chain. */
|
|
449
|
+
keyAuthorization?: KeyAuthorization.Signed | undefined
|
|
446
450
|
}
|
|
447
451
|
}
|
|
448
452
|
|
package/src/core/Client.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { Secp256k1 } from 'ox'
|
|
2
|
+
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
1
3
|
import { custom, http } from 'viem'
|
|
4
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
5
|
+
import { Transaction } from 'viem/tempo'
|
|
2
6
|
import { tempo, tempoModerato } from 'viem/tempo/chains'
|
|
3
|
-
import { describe, expect, test } from 'vp/test'
|
|
7
|
+
import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
4
8
|
|
|
5
9
|
import { secp256k1 } from '../../test/adapters.js'
|
|
6
10
|
import * as Client from './Client.js'
|
|
@@ -252,6 +256,37 @@ describe('providerTransport', () => {
|
|
|
252
256
|
})
|
|
253
257
|
|
|
254
258
|
describe('feePayerTransport', () => {
|
|
259
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
260
|
+
|
|
261
|
+
const sender = privateKeyToAccount(
|
|
262
|
+
'0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
263
|
+
)
|
|
264
|
+
const feePayerPrivateKey = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
|
265
|
+
|
|
266
|
+
async function sponsoredTransaction(data: `0x${string}`) {
|
|
267
|
+
const requested = await sender.signTransaction(
|
|
268
|
+
{
|
|
269
|
+
calls: [{ data, to: '0x20c0000000000000000000000000000000000000' }],
|
|
270
|
+
chainId: tempo.id,
|
|
271
|
+
feePayer: true,
|
|
272
|
+
gas: 100_000n,
|
|
273
|
+
maxFeePerGas: 2n,
|
|
274
|
+
maxPriorityFeePerGas: 1n,
|
|
275
|
+
nonce: 1,
|
|
276
|
+
} as never,
|
|
277
|
+
{ serializer: Transaction.serialize },
|
|
278
|
+
)
|
|
279
|
+
const envelope = TxEnvelopeTempo.deserialize(requested as TxEnvelopeTempo.Serialized)
|
|
280
|
+
const feePayerSignature = Secp256k1.sign({
|
|
281
|
+
payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender: envelope.from! }),
|
|
282
|
+
privateKey: feePayerPrivateKey,
|
|
283
|
+
})
|
|
284
|
+
return {
|
|
285
|
+
requested,
|
|
286
|
+
signed: TxEnvelopeTempo.serialize(envelope, { feePayerSignature }),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
255
290
|
test('behavior: passes through unrelated methods to the base transport', async () => {
|
|
256
291
|
const store = setup()
|
|
257
292
|
const baseRequests: { method: string; params?: unknown }[] = []
|
|
@@ -337,4 +372,121 @@ describe('feePayerTransport', () => {
|
|
|
337
372
|
]
|
|
338
373
|
`)
|
|
339
374
|
})
|
|
375
|
+
|
|
376
|
+
test('behavior: accepts a fee payer signature for the requested transaction', async () => {
|
|
377
|
+
const store = setup()
|
|
378
|
+
const { requested, signed } = await sponsoredTransaction('0x1234')
|
|
379
|
+
vi.stubGlobal('fetch', async (_input: string | URL, init?: RequestInit) => {
|
|
380
|
+
const body = JSON.parse(String(init?.body)) as { id: number }
|
|
381
|
+
return Response.json({ id: body.id, jsonrpc: '2.0', result: signed })
|
|
382
|
+
})
|
|
383
|
+
const baseRequests: { method: string; params?: unknown }[] = []
|
|
384
|
+
const hash = `0x${'11'.repeat(32)}` as `0x${string}`
|
|
385
|
+
const client = Client.fromChainId(tempo.id, {
|
|
386
|
+
chains: [tempo],
|
|
387
|
+
feePayer: 'https://relay.example.com',
|
|
388
|
+
store,
|
|
389
|
+
transports: {
|
|
390
|
+
[tempo.id]: custom({
|
|
391
|
+
async request({ method, params }) {
|
|
392
|
+
baseRequests.push({ method, params })
|
|
393
|
+
return hash
|
|
394
|
+
},
|
|
395
|
+
}),
|
|
396
|
+
},
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const result = await client.request({
|
|
400
|
+
method: 'eth_sendRawTransaction',
|
|
401
|
+
params: [requested],
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
expect(result).toMatchInlineSnapshot(
|
|
405
|
+
`"0x1111111111111111111111111111111111111111111111111111111111111111"`,
|
|
406
|
+
)
|
|
407
|
+
expect(baseRequests.map((request) => request.method)).toMatchInlineSnapshot(`
|
|
408
|
+
[
|
|
409
|
+
"eth_sendRawTransaction",
|
|
410
|
+
]
|
|
411
|
+
`)
|
|
412
|
+
const sent = baseRequests[0]?.params as readonly unknown[] | undefined
|
|
413
|
+
expect(sent?.[0]).toBe(signed)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('error: rejects a fee payer signature for a different transaction', async () => {
|
|
417
|
+
const store = setup()
|
|
418
|
+
const { requested } = await sponsoredTransaction('0x1234')
|
|
419
|
+
const { signed } = await sponsoredTransaction('0x5678')
|
|
420
|
+
vi.stubGlobal('fetch', async (_input: string | URL, init?: RequestInit) => {
|
|
421
|
+
const body = JSON.parse(String(init?.body)) as { id: number }
|
|
422
|
+
return Response.json({ id: body.id, jsonrpc: '2.0', result: signed })
|
|
423
|
+
})
|
|
424
|
+
const baseRequests: unknown[] = []
|
|
425
|
+
const client = Client.fromChainId(tempo.id, {
|
|
426
|
+
chains: [tempo],
|
|
427
|
+
feePayer: 'https://relay.example.com',
|
|
428
|
+
store,
|
|
429
|
+
transports: {
|
|
430
|
+
[tempo.id]: custom({
|
|
431
|
+
async request(request) {
|
|
432
|
+
baseRequests.push(request)
|
|
433
|
+
return null
|
|
434
|
+
},
|
|
435
|
+
}),
|
|
436
|
+
},
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
await expect(
|
|
440
|
+
client.request({ method: 'eth_sendRawTransaction', params: [requested] }),
|
|
441
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
442
|
+
`[Error: Fee payer changed the requested transaction intent.]`,
|
|
443
|
+
)
|
|
444
|
+
expect(baseRequests).toMatchInlineSnapshot(`[]`)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('error: rejects a filled transaction that changes the requested call', async () => {
|
|
448
|
+
const store = setup()
|
|
449
|
+
const request = {
|
|
450
|
+
calls: [{ data: '0x1234', to: '0x20c0000000000000000000000000000000000000', value: '0x' }],
|
|
451
|
+
chainId: '0x1079',
|
|
452
|
+
feePayer: true,
|
|
453
|
+
from: sender.address,
|
|
454
|
+
type: '0x76',
|
|
455
|
+
} as const
|
|
456
|
+
vi.stubGlobal('fetch', async (_input: string | URL, init?: RequestInit) => {
|
|
457
|
+
const body = JSON.parse(String(init?.body)) as { id: number }
|
|
458
|
+
return Response.json({
|
|
459
|
+
id: body.id,
|
|
460
|
+
jsonrpc: '2.0',
|
|
461
|
+
result: {
|
|
462
|
+
raw: '0x',
|
|
463
|
+
tx: {
|
|
464
|
+
...request,
|
|
465
|
+
calls: [{ ...request.calls[0], data: '0x5678' }],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
const baseRequests: unknown[] = []
|
|
471
|
+
const client = Client.fromChainId(tempo.id, {
|
|
472
|
+
chains: [tempo],
|
|
473
|
+
feePayer: 'https://relay.example.com',
|
|
474
|
+
store,
|
|
475
|
+
transports: {
|
|
476
|
+
[tempo.id]: custom({
|
|
477
|
+
async request(request) {
|
|
478
|
+
baseRequests.push(request)
|
|
479
|
+
return null
|
|
480
|
+
},
|
|
481
|
+
}),
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
await expect(
|
|
486
|
+
client.request({ method: 'eth_fillTransaction' as never, params: [request] as never }),
|
|
487
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
488
|
+
`[Error: Fee payer changed the requested transaction intent.]`,
|
|
489
|
+
)
|
|
490
|
+
expect(baseRequests).toMatchInlineSnapshot(`[]`)
|
|
491
|
+
})
|
|
340
492
|
})
|
package/src/core/Client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Provider as ox_Provider } from 'ox'
|
|
2
|
+
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
2
3
|
import {
|
|
3
4
|
type Chain,
|
|
4
5
|
createClient,
|
|
@@ -163,11 +164,14 @@ function feePayerTransport(
|
|
|
163
164
|
typeof request === 'object' &&
|
|
164
165
|
'feePayer' in request &&
|
|
165
166
|
(request.feePayer === true || typeof request.feePayer === 'string')
|
|
166
|
-
)
|
|
167
|
-
|
|
167
|
+
) {
|
|
168
|
+
const response = await sponsor.request({
|
|
168
169
|
method,
|
|
169
170
|
params: [{ ...request, feePayer: true }],
|
|
170
171
|
})
|
|
172
|
+
assertFilledTransactionIntent(request, response)
|
|
173
|
+
return response
|
|
174
|
+
}
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
if (method === 'eth_sendRawTransaction' || method === 'eth_sendRawTransactionSync') {
|
|
@@ -182,6 +186,7 @@ function feePayerTransport(
|
|
|
182
186
|
method: 'eth_signRawTransaction',
|
|
183
187
|
params: [serialized],
|
|
184
188
|
})
|
|
189
|
+
assertSignedTransactionIntent(serialized, signed)
|
|
185
190
|
return await baseTransport.request({ method, params: [signed] })
|
|
186
191
|
}
|
|
187
192
|
}
|
|
@@ -192,3 +197,69 @@ function feePayerTransport(
|
|
|
192
197
|
} as ReturnType<Transport>
|
|
193
198
|
}
|
|
194
199
|
}
|
|
200
|
+
|
|
201
|
+
function assertFilledTransactionIntent(request: Record<string, unknown>, response: unknown) {
|
|
202
|
+
if (!isObject(response) || !isObject(response.tx))
|
|
203
|
+
throw new Error('Fee payer returned an invalid filled transaction.')
|
|
204
|
+
|
|
205
|
+
const transaction = response.tx
|
|
206
|
+
if (
|
|
207
|
+
(request.from !== undefined && !sameAddress(request.from, transaction.from)) ||
|
|
208
|
+
(request.chainId !== undefined && !sameQuantity(request.chainId, transaction.chainId)) ||
|
|
209
|
+
(request.calls !== undefined && !sameCalls(request.calls, transaction.calls))
|
|
210
|
+
)
|
|
211
|
+
throw new Error('Fee payer changed the requested transaction intent.')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function assertSignedTransactionIntent(serialized: string, signed: unknown) {
|
|
215
|
+
if (typeof signed !== 'string' || (!signed.startsWith('0x76') && !signed.startsWith('0x78')))
|
|
216
|
+
throw new Error('Fee payer returned an invalid signed transaction.')
|
|
217
|
+
|
|
218
|
+
const requested = TxEnvelopeTempo.deserialize(serialized as TxEnvelopeTempo.Serialized)
|
|
219
|
+
const transaction = TxEnvelopeTempo.deserialize(signed as TxEnvelopeTempo.Serialized)
|
|
220
|
+
if (
|
|
221
|
+
!transaction.feePayerSignature ||
|
|
222
|
+
!sameAddress(requested.from, transaction.from) ||
|
|
223
|
+
TxEnvelopeTempo.getSignPayload(requested) !== TxEnvelopeTempo.getSignPayload(transaction)
|
|
224
|
+
)
|
|
225
|
+
throw new Error('Fee payer changed the requested transaction intent.')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function sameCalls(a: unknown, b: unknown) {
|
|
229
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
|
|
230
|
+
return a.every((call, index) => {
|
|
231
|
+
const other = b[index]
|
|
232
|
+
return (
|
|
233
|
+
isObject(call) &&
|
|
234
|
+
isObject(other) &&
|
|
235
|
+
sameAddress(call.to, other.to) &&
|
|
236
|
+
sameQuantity(call.value, other.value) &&
|
|
237
|
+
sameHex(call.data, other.data)
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sameAddress(a: unknown, b: unknown) {
|
|
243
|
+
if (a === undefined && b === undefined) return true
|
|
244
|
+
return typeof a === 'string' && typeof b === 'string' && a.toLowerCase() === b.toLowerCase()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sameHex(a: unknown, b: unknown) {
|
|
248
|
+
const normalize = (value: unknown) =>
|
|
249
|
+
typeof value === 'string' ? value.toLowerCase() : value === undefined ? '0x' : undefined
|
|
250
|
+
return normalize(a) === normalize(b)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function sameQuantity(a: unknown, b: unknown) {
|
|
254
|
+
const normalize = (value: unknown) => {
|
|
255
|
+
if (value === undefined || value === null || value === '0x') return 0n
|
|
256
|
+
if (typeof value === 'bigint') return value
|
|
257
|
+
if (typeof value === 'number' || typeof value === 'string') return BigInt(value)
|
|
258
|
+
return undefined
|
|
259
|
+
}
|
|
260
|
+
return normalize(a) === normalize(b)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
264
|
+
return typeof value === 'object' && value !== null
|
|
265
|
+
}
|