Skip to content

Commit a728b6b

Browse files
gsteenkamp89pxrl
andauthored
Add tests for SDK (#54)
* setup vitest with anvil * get op stack and non-op chains running * fixes * bork * example env file * fund wallet with USDC * passing * simplify execute quote file, clean up * fill as relayer, check fill receipt * clean up * run tests in CI * update example env file * remove duplicate across client instance * let echo create the env file Co-authored-by: Paul <[email protected]> * make prool dev dependency * remove unused op stack anvil chains * reset forks after each test file * add comment about exposed private key * use latestblock * don't mock api * remove log --------- Co-authored-by: Paul <[email protected]>
1 parent 0b09340 commit a728b6b

31 files changed

+8882
-11008
lines changed

.github/workflows/build-lint-test.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,19 @@ jobs:
2424
- name: Setup Node
2525
uses: actions/setup-node@v4
2626
with:
27-
node-version: 20
27+
node-version: 22
2828
cache: pnpm
2929

30+
- name: Install Foundry
31+
uses: foundry-rs/foundry-toolchain@v1
32+
3033
- name: Install dependencies
3134
run: pnpm install
3235

36+
- name: create test env file
37+
run: |
38+
echo VITE_ANVIL_ALCHEMY_KEY=${{ secrets.VITE_ANVIL_ALCHEMY_KEY }} >> packages/sdk/.env
39+
echo VITE_MOCK_API=false >> packages/sdk/.env
40+
3341
- name: Run CI
3442
run: pnpm run ci

packages/sdk/.example.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_ANVIL_ALCHEMY_KEY="<key>"

packages/sdk/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
3838
"type-check": "tsc",
3939
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
40-
"test": "vitest run --reporter=verbose",
40+
"test": "vitest run --config ./vitest.config.mts",
4141
"ci": "pnpm run build && pnpm run check-exports pnpm npm run lint && pnpm run test",
4242
"typedoc": "typedoc --out docs src/index.ts"
4343
},
@@ -48,7 +48,9 @@
4848
"@total-typescript/ts-reset": "^0.6.0",
4949
"@types/node": "^20",
5050
"eslint": "^8.57.0",
51+
"msw": "^2.4.9",
5152
"prettier": "^3.2.5",
53+
"prool": "^0.0.16",
5254
"tsup": "^8.0.2",
5355
"typedoc": "^0.26.7",
5456
"typedoc-plugin-markdown": "^4.2.8",

packages/sdk/src/types/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
Account,
33
Address,
44
Chain,
5+
GetEventArgs,
56
Hash,
67
Hex,
78
PublicClient,
@@ -10,6 +11,8 @@ import {
1011
} from "viem";
1112
import { STATUS } from "../constants";
1213
import { AcrossChain } from "../utils/getSupportedChains";
14+
import { spokePoolAbi } from "../abis/SpokePool";
15+
import { NoNullValuesOfObject } from "../utils/typeUtils";
1316

1417
export type Status = keyof typeof STATUS;
1518

@@ -78,3 +81,17 @@ export type Deposit = {
7881
};
7982

8083
export type ChainInfoMap = Map<number, AcrossChain>;
84+
85+
type MaybeFilledV3RelayEvent = GetEventArgs<
86+
typeof spokePoolAbi,
87+
"FilledV3Relay",
88+
{ IndexedOnly: false }
89+
>;
90+
91+
type MaybeDepositV3Event = GetEventArgs<
92+
typeof spokePoolAbi,
93+
"V3FundsDeposited",
94+
{ IndexedOnly: false }
95+
>;
96+
export type FilledV3RelayEvent = NoNullValuesOfObject<MaybeFilledV3RelayEvent>;
97+
export type V3FundsDepositedEvent = NoNullValuesOfObject<MaybeDepositV3Event>;

packages/sdk/src/utils/configurePublicClients.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function configurePublicClients(
1818
const customTransport = transports?.[chain.id];
1919
const transport =
2020
customTransport ??
21-
(rpcUrl?.startsWith("wss") ? webSocket(rpcUrl) : http(rpcUrl));
21+
(rpcUrl?.startsWith("ws") ? webSocket(rpcUrl) : http(rpcUrl));
2222
return [
2323
chain.id,
2424
createPublicClient({

packages/sdk/src/utils/typeUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
export type MakeOptional<T extends object, K extends keyof T> = Omit<T, K> &
22
Partial<Pick<T, K>>;
3+
4+
export type NoNullValuesOfObject<T extends object> = {
5+
[Property in keyof T]-?: NonNullable<T[Property]>;
6+
};

packages/sdk/test/actions/getAvailableRoutes.test.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/sdk/test/common/anvil.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { arbitrum, type Chain, mainnet } from "viem/chains";
2+
import {
3+
BLOCK_NUMBER_ARBITRUM,
4+
BLOCK_NUMBER_MAINNET,
5+
FORK_URL_ARBITRUM,
6+
FORK_URL_MAINNET,
7+
pool,
8+
PRIVATE_KEY,
9+
} from "./constants";
10+
import {
11+
type Account,
12+
type Client,
13+
createPublicClient,
14+
createTestClient,
15+
createWalletClient,
16+
http,
17+
publicActions,
18+
type PublicActions,
19+
type TestActions,
20+
type TestRpcSchema,
21+
type Transport,
22+
type WalletActions,
23+
walletActions,
24+
} from "viem";
25+
import { privateKeyToAccount } from "viem/accounts";
26+
import { type Compute } from "./utils";
27+
import { createServer } from "prool";
28+
import { anvil } from "prool/instances";
29+
30+
export function getRpcUrls({ port }: { port: number }) {
31+
return {
32+
port,
33+
rpcUrls: {
34+
default: {
35+
// append the test worker id to the rpc urls.
36+
http: [`http://localhost:${port}/${pool}`],
37+
webSocket: [`ws://localhost:${port}/${pool}`],
38+
},
39+
public: {
40+
http: [`http://localhost:${port}/${pool}`],
41+
webSocket: [`ws://localhost:${port}/${pool}`],
42+
},
43+
},
44+
} as const;
45+
}
46+
47+
type Fork = { blockNumber: bigint; url: string; opStack?: boolean };
48+
49+
export type AnvilChain = Compute<
50+
Chain & {
51+
fork: Fork;
52+
port: number;
53+
}
54+
>;
55+
56+
const mainnetFork = {
57+
blockNumber: BLOCK_NUMBER_MAINNET,
58+
url: FORK_URL_MAINNET,
59+
} as const satisfies Fork;
60+
61+
const arbitrumFork = {
62+
blockNumber: BLOCK_NUMBER_ARBITRUM,
63+
url: FORK_URL_ARBITRUM,
64+
} as const satisfies Fork;
65+
66+
// PORT: 8547
67+
const mainnetAnvil = {
68+
...mainnet,
69+
...getRpcUrls({ port: 8547 }),
70+
fork: mainnetFork,
71+
} as const satisfies AnvilChain;
72+
73+
// PORT: 8548
74+
const arbitrumAnvil = {
75+
...arbitrum,
76+
...getRpcUrls({ port: 8548 }),
77+
fork: arbitrumFork,
78+
} as const satisfies AnvilChain;
79+
80+
const account = privateKeyToAccount(PRIVATE_KEY);
81+
82+
// TEST (CHAIN) CLIENTS
83+
export const chainClientMainnet: ChainClient = createTestClient({
84+
account,
85+
chain: mainnetAnvil,
86+
mode: "anvil",
87+
transport: http(),
88+
})
89+
.extend(forkMethods)
90+
.extend(publicActions)
91+
.extend(walletActions);
92+
93+
export const chainClientArbitrum: ChainClient = createTestClient({
94+
account,
95+
chain: arbitrumAnvil,
96+
mode: "anvil",
97+
transport: http(),
98+
})
99+
.extend(forkMethods)
100+
.extend(publicActions)
101+
.extend(walletActions);
102+
103+
// PUBLIC CLIENTS
104+
export const publicClientMainnet = createPublicClient({
105+
chain: mainnetAnvil,
106+
transport: http(),
107+
});
108+
109+
export const publicClientArbitrum = createPublicClient({
110+
chain: arbitrumAnvil,
111+
transport: http(),
112+
});
113+
114+
// WALLET CLIENTS
115+
export const testWalletMainnet = createWalletClient({
116+
account,
117+
chain: mainnetAnvil,
118+
transport: http(),
119+
});
120+
121+
export const testWalletArbitrum = createWalletClient({
122+
account,
123+
chain: arbitrumAnvil,
124+
transport: http(),
125+
});
126+
127+
export type ChainClient = Client<
128+
Transport,
129+
AnvilChain,
130+
Account,
131+
TestRpcSchema<"anvil">,
132+
TestActions & ForkMethods & PublicActions & WalletActions
133+
>;
134+
135+
type ForkMethods = ReturnType<typeof forkMethods>;
136+
137+
function forkMethods(
138+
client: Client<
139+
Transport,
140+
AnvilChain,
141+
Account,
142+
TestRpcSchema<"anvil">,
143+
TestActions
144+
>,
145+
) {
146+
return {
147+
async restart() {
148+
return await fetch(`${client.chain.rpcUrls.default.http[0]}/restart`);
149+
},
150+
async stop() {
151+
return await fetch(`${client.chain.rpcUrls.default.http[0]}/stop`);
152+
},
153+
async healthcheck() {
154+
return await fetch(`${client.chain.rpcUrls.default.http[0]}/healthcheck`);
155+
},
156+
async start() {
157+
const server = createServer({
158+
instance: anvil({
159+
chainId: client.chain.id,
160+
forkUrl: client.chain.fork.url,
161+
forkBlockNumber: client.chain.fork.blockNumber,
162+
blockTime: 1,
163+
}),
164+
port: client.chain.port,
165+
});
166+
167+
await server.start();
168+
169+
const res = await this.healthcheck();
170+
console.log(res);
171+
return server;
172+
},
173+
/** Resets fork attached to chain at starting block number. */
174+
resetFork() {
175+
return client.reset({
176+
jsonRpcUrl: client.chain.fork.url,
177+
blockNumber: client.chain.fork.blockNumber,
178+
});
179+
},
180+
};
181+
}
182+
183+
export const chains = {
184+
arbitrumAnvil,
185+
mainnetAnvil,
186+
};
187+
188+
export const chainClients: Record<string, ChainClient> = {
189+
chainClientMainnet,
190+
chainClientArbitrum,
191+
};

packages/sdk/test/common/constants.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { getAddress } from "viem";
2+
3+
export const pool = Number(process.env.VITEST_POOL_ID ?? 1);
4+
5+
// Test accounts
6+
export const ACCOUNTS = [
7+
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
8+
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
9+
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
10+
"0x90F79bf6EB2c4f870365E785982E1f101E93b906",
11+
"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65",
12+
"0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
13+
"0x976EA74026E726554dB657fA54763abd0C3a0aa9",
14+
"0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
15+
"0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f",
16+
"0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
17+
] as const;
18+
19+
// Intentionally disclosed private key for 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
20+
export const PRIVATE_KEY =
21+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
22+
23+
// Named accounts
24+
export const [ALICE, BOB, RELAYER] = ACCOUNTS;
25+
26+
export const USDC_MAINNET = getAddress(
27+
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
28+
);
29+
export const USDC_WHALE = getAddress(
30+
"0x55fe002aeff02f77364de339a1292923a15844b8",
31+
);
32+
33+
function getEnv(key: string): string {
34+
if (!process.env[key]) {
35+
throw new Error(`Missing environment variable "${key}"`);
36+
}
37+
return process.env[key];
38+
}
39+
40+
function getMaybeEnv(key: string): string | undefined {
41+
return process.env[key];
42+
}
43+
const ALCHEMY_KEY = getEnv("VITE_ANVIL_ALCHEMY_KEY");
44+
// FORK URLs
45+
export const FORK_URL_OPTIMISM = `https://opt-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;
46+
export const FORK_URL_BASE = `https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;
47+
export const FORK_URL_MAINNET = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;
48+
export const FORK_URL_ARBITRUM = `https://arb-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;
49+
50+
// FORK BLOCK NUMBERS
51+
export const BLOCK_NUMBER_OPTIMISM = BigInt(126951625);
52+
export const BLOCK_NUMBER_BASE = BigInt(21363706);
53+
export const BLOCK_NUMBER_MAINNET = BigInt(21020558);
54+
export const BLOCK_NUMBER_ARBITRUM = BigInt(266447962);
55+
56+
export const TENDERLY_KEY = getMaybeEnv("VITE_TENDERLY_KEY");
57+
export const MOCK_API = getMaybeEnv("VITE_MOCK_API") === "true";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { chainClients } from "./anvil";
2+
3+
export default async function () {
4+
const servers = await Promise.all(
5+
Object.values(chainClients).map((chain) => chain.start()),
6+
);
7+
8+
return async () => {
9+
await Promise.all(servers.map((chain) => chain.stop()));
10+
};
11+
}

0 commit comments

Comments
 (0)