Skip to content

Commit

Permalink
Add tests for SDK (#54)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
gsteenkamp89 and pxrl authored Oct 30, 2024
1 parent 0b09340 commit a728b6b
Show file tree
Hide file tree
Showing 31 changed files with 8,882 additions and 11,008 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/build-lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,19 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: pnpm

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install dependencies
run: pnpm install

- name: create test env file
run: |
echo VITE_ANVIL_ALCHEMY_KEY=${{ secrets.VITE_ANVIL_ALCHEMY_KEY }} >> packages/sdk/.env
echo VITE_MOCK_API=false >> packages/sdk/.env
- name: Run CI
run: pnpm run ci
1 change: 1 addition & 0 deletions packages/sdk/.example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_ANVIL_ALCHEMY_KEY="<key>"
4 changes: 3 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"type-check": "tsc",
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
"test": "vitest run --reporter=verbose",
"test": "vitest run --config ./vitest.config.mts",
"ci": "pnpm run build && pnpm run check-exports pnpm npm run lint && pnpm run test",
"typedoc": "typedoc --out docs src/index.ts"
},
Expand All @@ -48,7 +48,9 @@
"@total-typescript/ts-reset": "^0.6.0",
"@types/node": "^20",
"eslint": "^8.57.0",
"msw": "^2.4.9",
"prettier": "^3.2.5",
"prool": "^0.0.16",
"tsup": "^8.0.2",
"typedoc": "^0.26.7",
"typedoc-plugin-markdown": "^4.2.8",
Expand Down
17 changes: 17 additions & 0 deletions packages/sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Account,
Address,
Chain,
GetEventArgs,
Hash,
Hex,
PublicClient,
Expand All @@ -10,6 +11,8 @@ import {
} from "viem";
import { STATUS } from "../constants";
import { AcrossChain } from "../utils/getSupportedChains";
import { spokePoolAbi } from "../abis/SpokePool";
import { NoNullValuesOfObject } from "../utils/typeUtils";

export type Status = keyof typeof STATUS;

Expand Down Expand Up @@ -78,3 +81,17 @@ export type Deposit = {
};

export type ChainInfoMap = Map<number, AcrossChain>;

type MaybeFilledV3RelayEvent = GetEventArgs<
typeof spokePoolAbi,
"FilledV3Relay",
{ IndexedOnly: false }
>;

type MaybeDepositV3Event = GetEventArgs<
typeof spokePoolAbi,
"V3FundsDeposited",
{ IndexedOnly: false }
>;
export type FilledV3RelayEvent = NoNullValuesOfObject<MaybeFilledV3RelayEvent>;
export type V3FundsDepositedEvent = NoNullValuesOfObject<MaybeDepositV3Event>;
2 changes: 1 addition & 1 deletion packages/sdk/src/utils/configurePublicClients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function configurePublicClients(
const customTransport = transports?.[chain.id];
const transport =
customTransport ??
(rpcUrl?.startsWith("wss") ? webSocket(rpcUrl) : http(rpcUrl));
(rpcUrl?.startsWith("ws") ? webSocket(rpcUrl) : http(rpcUrl));
return [
chain.id,
createPublicClient({
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/utils/typeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export type MakeOptional<T extends object, K extends keyof T> = Omit<T, K> &
Partial<Pick<T, K>>;

export type NoNullValuesOfObject<T extends object> = {
[Property in keyof T]-?: NonNullable<T[Property]>;
};
8 changes: 0 additions & 8 deletions packages/sdk/test/actions/getAvailableRoutes.test.ts

This file was deleted.

191 changes: 191 additions & 0 deletions packages/sdk/test/common/anvil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { arbitrum, type Chain, mainnet } from "viem/chains";
import {
BLOCK_NUMBER_ARBITRUM,
BLOCK_NUMBER_MAINNET,
FORK_URL_ARBITRUM,
FORK_URL_MAINNET,
pool,
PRIVATE_KEY,
} from "./constants";
import {
type Account,
type Client,
createPublicClient,
createTestClient,
createWalletClient,
http,
publicActions,
type PublicActions,
type TestActions,
type TestRpcSchema,
type Transport,
type WalletActions,
walletActions,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { type Compute } from "./utils";
import { createServer } from "prool";
import { anvil } from "prool/instances";

export function getRpcUrls({ port }: { port: number }) {
return {
port,
rpcUrls: {
default: {
// append the test worker id to the rpc urls.
http: [`http://localhost:${port}/${pool}`],
webSocket: [`ws://localhost:${port}/${pool}`],
},
public: {
http: [`http://localhost:${port}/${pool}`],
webSocket: [`ws://localhost:${port}/${pool}`],
},
},
} as const;
}

type Fork = { blockNumber: bigint; url: string; opStack?: boolean };

export type AnvilChain = Compute<
Chain & {
fork: Fork;
port: number;
}
>;

const mainnetFork = {
blockNumber: BLOCK_NUMBER_MAINNET,
url: FORK_URL_MAINNET,
} as const satisfies Fork;

const arbitrumFork = {
blockNumber: BLOCK_NUMBER_ARBITRUM,
url: FORK_URL_ARBITRUM,
} as const satisfies Fork;

// PORT: 8547
const mainnetAnvil = {
...mainnet,
...getRpcUrls({ port: 8547 }),
fork: mainnetFork,
} as const satisfies AnvilChain;

// PORT: 8548
const arbitrumAnvil = {
...arbitrum,
...getRpcUrls({ port: 8548 }),
fork: arbitrumFork,
} as const satisfies AnvilChain;

const account = privateKeyToAccount(PRIVATE_KEY);

// TEST (CHAIN) CLIENTS
export const chainClientMainnet: ChainClient = createTestClient({
account,
chain: mainnetAnvil,
mode: "anvil",
transport: http(),
})
.extend(forkMethods)
.extend(publicActions)
.extend(walletActions);

export const chainClientArbitrum: ChainClient = createTestClient({
account,
chain: arbitrumAnvil,
mode: "anvil",
transport: http(),
})
.extend(forkMethods)
.extend(publicActions)
.extend(walletActions);

// PUBLIC CLIENTS
export const publicClientMainnet = createPublicClient({
chain: mainnetAnvil,
transport: http(),
});

export const publicClientArbitrum = createPublicClient({
chain: arbitrumAnvil,
transport: http(),
});

// WALLET CLIENTS
export const testWalletMainnet = createWalletClient({
account,
chain: mainnetAnvil,
transport: http(),
});

export const testWalletArbitrum = createWalletClient({
account,
chain: arbitrumAnvil,
transport: http(),
});

export type ChainClient = Client<
Transport,
AnvilChain,
Account,
TestRpcSchema<"anvil">,
TestActions & ForkMethods & PublicActions & WalletActions
>;

type ForkMethods = ReturnType<typeof forkMethods>;

function forkMethods(
client: Client<
Transport,
AnvilChain,
Account,
TestRpcSchema<"anvil">,
TestActions
>,
) {
return {
async restart() {
return await fetch(`${client.chain.rpcUrls.default.http[0]}/restart`);
},
async stop() {
return await fetch(`${client.chain.rpcUrls.default.http[0]}/stop`);
},
async healthcheck() {
return await fetch(`${client.chain.rpcUrls.default.http[0]}/healthcheck`);
},
async start() {
const server = createServer({
instance: anvil({
chainId: client.chain.id,
forkUrl: client.chain.fork.url,
forkBlockNumber: client.chain.fork.blockNumber,
blockTime: 1,
}),
port: client.chain.port,
});

await server.start();

const res = await this.healthcheck();
console.log(res);
return server;
},
/** Resets fork attached to chain at starting block number. */
resetFork() {
return client.reset({
jsonRpcUrl: client.chain.fork.url,
blockNumber: client.chain.fork.blockNumber,
});
},
};
}

export const chains = {
arbitrumAnvil,
mainnetAnvil,
};

export const chainClients: Record<string, ChainClient> = {
chainClientMainnet,
chainClientArbitrum,
};
57 changes: 57 additions & 0 deletions packages/sdk/test/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getAddress } from "viem";

export const pool = Number(process.env.VITEST_POOL_ID ?? 1);

// Test accounts
export const ACCOUNTS = [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65",
"0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"0x976EA74026E726554dB657fA54763abd0C3a0aa9",
"0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
"0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f",
"0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
] as const;

// Intentionally disclosed private key for 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
export const PRIVATE_KEY =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

// Named accounts
export const [ALICE, BOB, RELAYER] = ACCOUNTS;

export const USDC_MAINNET = getAddress(
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
);
export const USDC_WHALE = getAddress(
"0x55fe002aeff02f77364de339a1292923a15844b8",
);

function getEnv(key: string): string {
if (!process.env[key]) {
throw new Error(`Missing environment variable "${key}"`);
}
return process.env[key];
}

function getMaybeEnv(key: string): string | undefined {
return process.env[key];
}
const ALCHEMY_KEY = getEnv("VITE_ANVIL_ALCHEMY_KEY");
// FORK URLs
export const FORK_URL_OPTIMISM = `https://opt-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;
export const FORK_URL_BASE = `https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;
export const FORK_URL_MAINNET = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;
export const FORK_URL_ARBITRUM = `https://arb-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`;

// FORK BLOCK NUMBERS
export const BLOCK_NUMBER_OPTIMISM = BigInt(126951625);
export const BLOCK_NUMBER_BASE = BigInt(21363706);
export const BLOCK_NUMBER_MAINNET = BigInt(21020558);
export const BLOCK_NUMBER_ARBITRUM = BigInt(266447962);

export const TENDERLY_KEY = getMaybeEnv("VITE_TENDERLY_KEY");
export const MOCK_API = getMaybeEnv("VITE_MOCK_API") === "true";
11 changes: 11 additions & 0 deletions packages/sdk/test/common/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { chainClients } from "./anvil";

export default async function () {
const servers = await Promise.all(
Object.values(chainClients).map((chain) => chain.start()),
);

return async () => {
await Promise.all(servers.map((chain) => chain.stop()));
};
}
Loading

0 comments on commit a728b6b

Please sign in to comment.