Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1168: Add test setup #5

Merged
merged 11 commits into from
May 22, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
coverage

# Editor directories and files
.vscode/*
Expand Down
23 changes: 23 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Config } from 'jest';

// https://jestjs.io/docs/configuration
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/dist/'],
prettierPath: null,
collectCoverageFrom: ['lib/**/*.{ts,js}'],
collectCoverage: true,
coverageThreshold: {
global: {
branches: 80,
functions: 90,
lines: 90,
statements: 90,
},
},
// Coverage is not collected for these.
coveragePathIgnorePatterns: ['lib/errors', 'lib/main.ts'],
};

export default config;
24 changes: 24 additions & 0 deletions lib/api/puffer-client-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { PufferClientHelpers } from './puffer-client-helpers';
import { Chain } from '../chains/constants';

describe('PufferClientHelpers', () => {
it('should create public client with defined config', () => {
const publicClient = PufferClientHelpers.createPublicClient({
chain: Chain.Anvil,
rpcUrls: ['rpcUrl'],
});

// `getBlockNumber` is a public action.
expect(publicClient.getBlockNumber).toBeTruthy();
});

it('should create wallet client with defined config', () => {
const walletClient = PufferClientHelpers.createWalletClient({
chain: Chain.Anvil,
provider: { request: jest.fn() },
});

// `sendTransaction` is a wallet action.
expect(walletClient.sendTransaction).toBeTruthy();
});
});
42 changes: 42 additions & 0 deletions lib/api/puffer-client-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
CustomTransportConfig,
createPublicClient as createPublicClientViem,
createWalletClient as createWalletClientViem,
http,
fallback,
custom,
Transport,
} from 'viem';
import { Chain, VIEM_CHAINS } from '../chains/constants';
import { TransportProvider } from '../utils/types';

export type ClientConfig = {
chain: Chain;
} & (
| { rpcUrls: string[] }
| { provider: TransportProvider; config?: CustomTransportConfig }
);

export class PufferClientHelpers {
public static createPublicClient(config: ClientConfig) {
return createPublicClientViem({
chain: VIEM_CHAINS[config.chain],
transport: PufferClientHelpers.extractTransportConfig(config),
});
}

public static createWalletClient(config: ClientConfig) {
return createWalletClientViem({
chain: VIEM_CHAINS[config.chain],
transport: PufferClientHelpers.extractTransportConfig(config),
});
}

private static extractTransportConfig(config: ClientConfig): Transport {
if ('rpcUrls' in config) {
return fallback(config.rpcUrls.map((url) => http(url)));
} else {
return custom(config.provider, config.config);
}
}
}
46 changes: 46 additions & 0 deletions lib/api/puffer-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Chain } from '../chains/constants';
import {
setupMockPublicClient,
setupMockWalletClient,
} from '../../test/mocks/setup-mock-clients';
import { PufferClient } from './puffer-client';
import { mockRpcRequest } from '../../test/mocks/mock-request';

describe('PufferClient', () => {
it('should request addresses', async () => {
const mockAddress = '0xEB77D02f8122B32273444a1b544C147Fb559CB41';

const walletRequest = mockRpcRequest({
eth_requestAccounts: [mockAddress],
});
const walletClient = setupMockWalletClient(walletRequest);

const pufferClient = new PufferClient(Chain.Anvil, walletClient);
const [address] = await pufferClient.requestAddresses();

expect(address).toBe(mockAddress);
expect(walletRequest).toHaveBeenCalled();
});

it('should deposit ETH', async () => {
const mockAddress = '0xEB77D02f8122B32273444a1b544C147Fb559CB41';
const mockGas = BigInt(1);

const walletRequest = mockRpcRequest({
eth_sendTransaction: mockAddress,
});
const walletClient = setupMockWalletClient(walletRequest);
const publicRequest = mockRpcRequest({ eth_estimateGas: mockGas });
const publicClient = setupMockPublicClient(publicRequest);

const pufferClient = new PufferClient(
Chain.Anvil,
walletClient,
publicClient,
);
const { transact, estimate } = pufferClient.depositETH(mockAddress);

expect(await transact(BigInt(1))).toBe(mockAddress);
expect(await estimate()).toBe(mockGas);
});
});
41 changes: 17 additions & 24 deletions lib/api/puffer-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import {
WalletClient,
createPublicClient,
createWalletClient,
custom,
getContract,
http,
} from 'viem';
import { Chain, VIEM_CHAINS, ViemChain } from '../chains/constants';
import { CHAIN_ADDRESSES } from '../contracts/addresses';
import { ValueOf } from '../utils/types';
import { CHAIN_ABIS } from '../contracts/abis/abis';
import { AccountError } from '../errors/validation-errors';

export class PufferClient {
private chainAddresses: ValueOf<typeof CHAIN_ADDRESSES>;
Expand All @@ -22,21 +20,27 @@ export class PufferClient {
private walletClient: WalletClient;
private publicClient: PublicClient;

constructor(chain: Chain) {
this.validateEnvironment();

constructor(
chain: Chain,
walletClient?: WalletClient,
publicClient?: PublicClient,
) {
this.chainAddresses = CHAIN_ADDRESSES[chain];
this.chainAbis = CHAIN_ABIS[chain];
this.viemChain = VIEM_CHAINS[chain];

this.walletClient = createWalletClient({
chain: this.viemChain,
transport: custom(window.ethereum!),
});
this.publicClient = createPublicClient({
chain: this.viemChain,
transport: http(),
});
this.walletClient =
walletClient ??
createWalletClient({
chain: this.viemChain,
transport: http(),
});
this.publicClient =
publicClient ??
createPublicClient({
chain: this.viemChain,
transport: http(),
});
}

public async requestAddresses() {
Expand Down Expand Up @@ -68,15 +72,4 @@ export class PufferClient {

return { transact, estimate };
}

/**
* Validates that the browser environment is correct.
*/
private validateEnvironment() {
if (!window.ethereum) {
throw new AccountError('JSON-RPC account not accessible.', {
fixMessage: 'Make sure a JSON-RPC wallet is set up in the browser.',
});
}
}
}
4 changes: 2 additions & 2 deletions lib/contracts/abis/abis.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Chain } from '../../chains/constants';
import { PufferVaultV2 as MainNetPufferVaultV2 } from './mainnet/PufferVaultV2';
import { PufferVaultV2 as MainnetPufferVaultV2 } from './mainnet/PufferVaultV2';
import { PufferVaultV2 as HoleskyPufferVaultV2 } from './holesky/PufferVaultV2';
import { PufferVaultV2 as AnvilPufferVaultV2 } from './anvil/PufferVaultV2';

export const CHAIN_ABIS = {
[Chain.Mainnet]: {
PufferVaultV2: MainNetPufferVaultV2,
PufferVaultV2: MainnetPufferVaultV2,
},
[Chain.Holesky]: {
PufferVaultV2: HoleskyPufferVaultV2,
Expand Down
8 changes: 5 additions & 3 deletions lib/errors/base-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export abstract class BaseError<
}

protected compileMessage(message: string, params: T) {
const hasParams = params && Object.keys(params).length > 0;

return [
message,
...(params ? [''] : []),
...(params
...(hasParams ? [''] : []), // New line to make log more readable.
...(hasParams
? Object.entries(params).map(([key, value]) => `${key}: ${value}`)
: []),
'',
'', // New line to make log more readable.
`v${version}`,
].join('\n');
}
Expand Down
18 changes: 18 additions & 0 deletions lib/errors/validation-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { version } from '../utils/version';
import { AccountError } from './validation-errors';

describe('ValidatorErrors', () => {
it('should throw a formatted error', () => {
const accountError = new AccountError('Random account error.', {
fixMessage: 'Fix the account',
});

expect(accountError.message).toMatchInlineSnapshot(`
"Random account error.

fixMessage: Fix the account

v${version}"
`);
});
});
1 change: 1 addition & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './api/puffer-client';
export * from './api/puffer-client-helpers';
export * from './chains/constants';
4 changes: 4 additions & 0 deletions lib/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export type ValueOf<T> = T[keyof T];

export type TransportProvider = {
request: (...args: any) => Promise<any>;
};
18 changes: 8 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,27 @@
"dev": "pnpm build --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "echo 'No tests'",
9inpachi marked this conversation as resolved.
Show resolved Hide resolved
"test": "jest",
"test:watch": "jest --watch",
"format": "prettier --write .",
"release": "release-it"
},
"dependencies": {
"@chainsafe/bls": "^8.1.0",
"@chainsafe/ssz": "^0.16.0",
"ethers": "^6.12.1",
"viem": "^2.10.8",
"web3": "^4.8.0"
},
"peerDependencies": {
"react": ">= 16"
"viem": "^2.10.8"
},
"devDependencies": {
"@eslint/js": "^9.2.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.11",
"@types/react": "^18.3.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.2.5",
"release-it": "^17.2.1",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0",
"vite": "^5.2.11",
Expand Down
Loading
Loading