Skip to content

Commit d2264e2

Browse files
committed
feat: ensure viem's adapter switch chain whenever possible
1 parent 7cbff6a commit d2264e2

File tree

7 files changed

+415
-17
lines changed

7 files changed

+415
-17
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
"publish:results": "tsx scripts/publishResults.ts",
2424
"release": "pnpm changeset publish",
2525
"spec": "vitest --project spec",
26+
"test:client": "vitest --project client",
2627
"test:react": "vitest --project react",
27-
"test": "pnpm test:react",
28+
"test": "pnpm test:client && pnpm test:react",
2829
"typecheck:spec": "pnpm --filter spec typecheck"
2930
},
3031
"license": "MIT",

packages/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@bgd-labs/aave-address-book": "^4.25.3",
9090
"@privy-io/server-auth": "^1.28.8",
9191
"ethers": "^6.14.4",
92+
"msw": "^2.10.5",
9293
"thirdweb": "^5.105.25",
9394
"tsup": "^8.5.0",
9495
"typescript": "^5.9.2",

packages/client/src/rpc.helpers.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { type HexString, invariant, isObject } from '@aave/types-next';
2+
import { type HttpResponse, http, type PathParams, passthrough } from 'msw';
3+
import { setupServer } from 'msw/node';
4+
import { afterAll, beforeAll } from 'vitest';
5+
import { ETHEREUM_FORK_RPC_URL } from './test-utils';
6+
7+
export type JsonRpcId = number | string | null;
8+
9+
export interface JsonRpcRequest<M extends string, P = unknown> {
10+
jsonrpc: '2.0';
11+
id: JsonRpcId;
12+
method: M;
13+
params: P;
14+
}
15+
16+
export interface JsonRpcSuccess<T = unknown> {
17+
jsonrpc: '2.0';
18+
id: JsonRpcId;
19+
result: T;
20+
}
21+
22+
export interface JsonRpcError<E = unknown> {
23+
jsonrpc: '2.0';
24+
id: JsonRpcId;
25+
error: {
26+
code: number;
27+
message: string;
28+
data?: E;
29+
};
30+
}
31+
32+
export type JsonRpcResponse<T = unknown, E = unknown> =
33+
| JsonRpcSuccess<T>
34+
| JsonRpcError<E>;
35+
36+
export type AnyOtherJsonRpcRequest = JsonRpcRequest<'__ANY_METHOD__'>;
37+
38+
export type EthChainIdRequest = JsonRpcRequest<'eth_chainId', []>;
39+
40+
export type WalletSwitchEthereumChainRequest = JsonRpcRequest<
41+
'wallet_switchEthereumChain',
42+
[{ chainId: HexString }]
43+
>;
44+
45+
export type WalletAddEthereumChainRequest = JsonRpcRequest<
46+
'wallet_addEthereumChain',
47+
[
48+
{
49+
chainId: HexString;
50+
chainName?: string;
51+
nativeCurrency?: {
52+
name: string;
53+
symbol: string;
54+
decimals: number;
55+
};
56+
rpcUrls?: string[];
57+
blockExplorerUrls?: string[];
58+
iconUrls?: string[];
59+
},
60+
]
61+
>;
62+
63+
export type SupportedJsonRpcRequest =
64+
| EthChainIdRequest
65+
| WalletSwitchEthereumChainRequest
66+
| WalletAddEthereumChainRequest
67+
| AnyOtherJsonRpcRequest;
68+
69+
function isJsonRpcRequest(body: unknown): body is SupportedJsonRpcRequest {
70+
return isObject(body) && 'jsonrpc' in body && 'method' in body;
71+
}
72+
73+
export function setupRpcInterceptor(
74+
handler: (
75+
body: SupportedJsonRpcRequest,
76+
) => HttpResponse<JsonRpcResponse> | undefined,
77+
) {
78+
const server = setupServer(
79+
http.post<PathParams, SupportedJsonRpcRequest>(
80+
ETHEREUM_FORK_RPC_URL,
81+
async ({ request }) => {
82+
const body = await request.clone().json();
83+
84+
invariant(
85+
isJsonRpcRequest(body),
86+
'body is not a valid JSON-RPC request',
87+
);
88+
89+
return handler(body);
90+
},
91+
),
92+
http.post(ETHEREUM_FORK_RPC_URL, () => passthrough()),
93+
);
94+
95+
beforeAll(() => {
96+
server.listen();
97+
});
98+
99+
afterAll(() => {
100+
server.close();
101+
});
102+
103+
return server;
104+
}

packages/client/src/test-utils.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ export const ETHEREUM_DAI_ADDRESS = evmAddress(
4848

4949
export const ETHEREUM_MARKET_ETH_CORRELATED_EMODE_CATEGORY = 1;
5050

51-
const ETHEREUM_FORK_RPC_URL = import.meta.env.ETHEREUM_TENDERLY_PUBLIC_RPC;
51+
export const ETHEREUM_FORK_RPC_URL = import.meta.env
52+
.ETHEREUM_TENDERLY_PUBLIC_RPC;
5253

53-
const ETHEREUM_FORK_RPC_URL_ADMIN = import.meta.env.ETHEREUM_TENDERLY_ADMIN_RPC;
54+
export const ETHEREUM_FORK_RPC_URL_ADMIN = import.meta.env
55+
.ETHEREUM_TENDERLY_ADMIN_RPC;
5456

5557
export const ethereumForkChain: Chain = defineChain({
5658
id: ETHEREUM_FORK_ID,
@@ -77,14 +79,14 @@ export const client = AaveClient.create({
7779
},
7880
});
7981

80-
export function createNewWallet(privateKey?: `0x${string}`): WalletClient {
81-
const privateKeyToUse = privateKey ?? generatePrivateKey();
82-
const wallet = createWalletClient({
83-
account: privateKeyToAccount(privateKeyToUse),
82+
export function createNewWallet(
83+
privateKey: `0x${string}` = generatePrivateKey(),
84+
): WalletClient {
85+
return createWalletClient({
86+
account: privateKeyToAccount(privateKey),
8487
chain: ethereumForkChain,
8588
transport: http(),
8689
});
87-
return wallet;
8890
}
8991

9092
// Tenderly RPC type for setBalance

packages/client/src/viem.test.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { CancelError, SigningError } from '@aave/core-next';
2+
import type { TransactionRequest } from '@aave/graphql-next';
3+
import {
4+
assertErr,
5+
assertOk,
6+
type BlockchainData,
7+
chainId,
8+
evmAddress,
9+
} from '@aave/types-next';
10+
import { HttpResponse } from 'msw';
11+
import {
12+
MethodNotSupportedRpcError,
13+
SwitchChainError,
14+
UserRejectedRequestError,
15+
} from 'viem';
16+
import { describe, expect, it } from 'vitest';
17+
import { setupRpcInterceptor } from './rpc.helpers';
18+
import { createNewWallet } from './test-utils';
19+
import { sendWith } from './viem';
20+
21+
const walletClient = createNewWallet();
22+
23+
describe(`Given a viem's WalletClient instance`, () => {
24+
describe(`And the '${sendWith.name}' handler is used to send a TransactionRequest`, () => {
25+
const request: TransactionRequest = {
26+
__typename: 'TransactionRequest',
27+
to: evmAddress(walletClient.account!.address),
28+
from: evmAddress(walletClient.account!.address),
29+
data: '0x' as BlockchainData,
30+
value: 0n,
31+
chainId: chainId(1),
32+
operations: [],
33+
};
34+
35+
describe('When the wallet is on a different chain than the TransactionRequest chain', () => {
36+
let walletChainId = `0x${(42).toString(16)}`;
37+
38+
setupRpcInterceptor((request) => {
39+
switch (request.method) {
40+
case 'wallet_switchEthereumChain':
41+
walletChainId = request.params[0].chainId;
42+
return HttpResponse.json({
43+
jsonrpc: '2.0',
44+
id: request.id,
45+
result: null,
46+
});
47+
48+
case 'eth_chainId':
49+
return HttpResponse.json({
50+
jsonrpc: '2.0',
51+
id: request.id,
52+
result: walletChainId,
53+
});
54+
}
55+
return;
56+
});
57+
58+
it('Then it should switch the chain and continue', async () => {
59+
const result = await sendWith(walletClient)(request);
60+
61+
assertOk(result);
62+
});
63+
});
64+
65+
describe('When the wallet does not support the TransactionRequest chain', () => {
66+
let walletChainId = `0x${(42).toString(16)}`;
67+
68+
setupRpcInterceptor((request) => {
69+
switch (request.method) {
70+
case 'wallet_switchEthereumChain':
71+
return HttpResponse.json({
72+
jsonrpc: '2.0',
73+
id: request.id,
74+
error: {
75+
code: SwitchChainError.code,
76+
message: 'Unrecognized chain ID',
77+
},
78+
});
79+
80+
case 'wallet_addEthereumChain':
81+
walletChainId = request.params[0].chainId;
82+
return HttpResponse.json({
83+
jsonrpc: '2.0',
84+
id: request.id,
85+
result: null,
86+
});
87+
88+
case 'eth_chainId':
89+
return HttpResponse.json({
90+
jsonrpc: '2.0',
91+
id: request.id,
92+
result: walletChainId,
93+
});
94+
}
95+
return;
96+
});
97+
98+
it('Then it should add the chain to the wallet and continue', async () => {
99+
const result = await sendWith(walletClient)(request);
100+
101+
assertOk(result);
102+
});
103+
});
104+
105+
describe('When the wallet fails to add the chain to the wallet', () => {
106+
setupRpcInterceptor((request) => {
107+
switch (request.method) {
108+
case 'wallet_switchEthereumChain':
109+
return HttpResponse.json({
110+
jsonrpc: '2.0',
111+
id: request.id,
112+
error: {
113+
code: SwitchChainError.code,
114+
message: 'Unrecognized chain ID',
115+
},
116+
});
117+
118+
case 'wallet_addEthereumChain':
119+
return HttpResponse.json({
120+
jsonrpc: '2.0',
121+
id: request.id,
122+
error: {
123+
code: MethodNotSupportedRpcError.code,
124+
message: 'Resource not available',
125+
},
126+
});
127+
128+
case 'eth_chainId':
129+
return HttpResponse.json({
130+
jsonrpc: '2.0',
131+
id: request.id,
132+
result: `0x${(42).toString(16)}`,
133+
});
134+
}
135+
return;
136+
});
137+
138+
it('Then it should fail with a SigningError', async () => {
139+
const result = await sendWith(walletClient)(request);
140+
141+
assertErr(result);
142+
expect(result.error).toBeInstanceOf(SigningError);
143+
});
144+
});
145+
146+
describe('When the user rejects the add chain request in their wallet', () => {
147+
setupRpcInterceptor((request) => {
148+
switch (request.method) {
149+
case 'wallet_switchEthereumChain':
150+
return HttpResponse.json({
151+
jsonrpc: '2.0',
152+
id: request.id,
153+
error: {
154+
code: SwitchChainError.code,
155+
message: 'Unrecognized chain ID',
156+
},
157+
});
158+
159+
case 'wallet_addEthereumChain':
160+
return HttpResponse.json({
161+
jsonrpc: '2.0',
162+
id: request.id,
163+
error: {
164+
code: UserRejectedRequestError.code,
165+
message: 'User rejected the request.',
166+
},
167+
});
168+
169+
case 'eth_chainId':
170+
return HttpResponse.json({
171+
jsonrpc: '2.0',
172+
id: request.id,
173+
result: `0x${(42).toString(16)}`,
174+
});
175+
}
176+
return;
177+
});
178+
179+
it('Then it should fail with a CancelError', async () => {
180+
const result = await sendWith(walletClient)(request);
181+
182+
assertErr(result);
183+
expect(result.error).toBeInstanceOf(CancelError);
184+
});
185+
});
186+
187+
describe(`When the wallet does not support 'wallet_switchEthereumChain'`, () => {
188+
setupRpcInterceptor((request) => {
189+
switch (request.method) {
190+
case 'wallet_switchEthereumChain':
191+
return HttpResponse.json({
192+
jsonrpc: '2.0',
193+
id: request.id,
194+
error: {
195+
code: MethodNotSupportedRpcError.code,
196+
message: 'method wallet_switchEthereumChain not supported',
197+
},
198+
});
199+
200+
case 'eth_chainId':
201+
return HttpResponse.json({
202+
jsonrpc: '2.0',
203+
id: request.id,
204+
result: `0x${(42).toString(16)}`,
205+
});
206+
}
207+
return;
208+
});
209+
210+
it('Then it should fail with a SigningError', async () => {
211+
const result = await sendWith(walletClient)(request);
212+
213+
assertErr(result);
214+
expect(result.error).toBeInstanceOf(SigningError);
215+
});
216+
});
217+
});
218+
});

0 commit comments

Comments
 (0)