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

feat: sample lib & app #6

Merged
merged 10 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js → .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
node: true,
jest: true,
},
ignorePatterns: [".eslintrc.js"],
ignorePatterns: [".eslintrc.cjs"],
rules: {
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,29 @@

## Features

### Boilerplate monorepo setup

Quickly start developing your offchain monorepo project with
minimal configuration overhead using Turborepo

### Sample library with Viem

Simple provider that uses Viem client to query account balances

### Sample contracts with Foundry

Basic Greeter contract with an external interface

Foundry configuration out-of-the-box

### Sample app that consumes the library

How much ETH do Vitalik and the Zero address hold together?

### Testing

Unit test setup with Vitest framework

### Lint and format

Use ESLint and Prettier to easily find issues as you code
Expand All @@ -18,11 +35,29 @@ Lint code and check commit messages format on every push.

Run all tests and see the coverage before merging changes.

## Overview

This repository is a monorepo consisting of 2 packages and 1 app:

- [`@ts-turborepo-boilerplate/contracts`](./packages/contracts): A library for writing all required smart contracts
- [`@ts-turborepo-boilerplate/sample-lib`](./packages/sample-lib): A sample library for querying account balances
- [`@ts-turborepo-boilerplate/sample-app`](./apps/sample-app): A demo sample app that uses the sample-lib

## 📋 Prerequisites

- Ensure you have `node 20` and `pnpm 9.7.1` installed.

## Tech stack

- [pnpm](https://pnpm.io/): package and workspace manager
- [turborepo](https://turbo.build/repo/docs): for managing the monorepo and the build system
- [foundry](https://book.getfoundry.sh/forge/): for writing Solidity smart contracts
- [husky](https://typicode.github.io/husky/): tool for managing git hooks
- tsc: for transpiling TS and building source code
- [prettier](https://prettier.io/): code formatter
- [eslint](https://typescript-eslint.io/): code linter
- [vitest](https://vitest.dev/): modern testing framework
- [Viem](https://viem.sh/): lightweight library to interface with EVM based blockchains

### Configuring Prettier sort import plugin

Expand Down
1 change: 1 addition & 0 deletions apps/sample-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RPC_URL= # RPC URL. Example: https://eth.llamarpc.com
43 changes: 43 additions & 0 deletions apps/sample-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# ts-turborepo-boilerplate: sample-app

> Note: use this app as reference but preferred way is to re-write app
> from zero instead of refactoring this one.
> When you don't need this anymore, you can delete it

Sample app that uses [sample-lib](../../packages/sample-lib) Blockchain
provider to fetch Vitalik and Zero address native balance and sums them

## Setup

1. Change package name to follow yours project one in [`package.json`](./package.json)
2. Install dependencies running `pnpm install`

### ⚙️ Setting up env variables

- Create `.env` file and copy paste `.env.example` content in there.

```
$ cp .env.example .env
```

Available options:
| Name | Description | Default | Required | Notes |
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------|-----------|----------------------------------|-----------------------------------------------------------------|
| `RPC_URL` | RPC URL to use for querying balances | N/A | Yes | |

## Available Scripts

Available scripts that can be run using `pnpm`:

| Script | Description |
| ------------- | ------------------------------------------------------- |
| `build` | Build library using tsc |
| `check-types` | Check types issues using tsc |
| `clean` | Remove `dist` folder |
| `lint` | Run ESLint to check for coding standards |
| `lint:fix` | Run linter and automatically fix code formatting issues |
| `format` | Check code formatting and style using Prettier |
| `format:fix` | Run formatter and automatically fix issues |
| `start` | Run the app |
| `test` | Run tests using vitest |
| `test:cov` | Run tests with coverage report |
23 changes: 23 additions & 0 deletions apps/sample-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@ts-turborepo-boilerplate/sample-app",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"check-types": "tsc --noEmit -p ./tsconfig.json",
"clean": "rm -rf dist",
"format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"",
"format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"",
"lint": "eslint \"{src,test}/**/*.{js,ts,json}\"",
"lint:fix": "pnpm lint --fix",
"start": "node dist/index.js",
"test": "vitest run --config vitest.config.ts --passWithNoTests",
"test:cov": "vitest run --config vitest.config.ts --coverage"
},
"dependencies": {
"@ts-turborepo-boilerplate/sample-lib": "workspace:*",
"dotenv": "16.4.5",
"zod": "3.23.8"
}
}
18 changes: 18 additions & 0 deletions apps/sample-app/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import dotenv from "dotenv";
import { z } from "zod";

dotenv.config();

const validationSchema = z.object({
RPC_URL: z.string().url(),
});

const env = validationSchema.safeParse(process.env);

if (!env.success) {
console.error(env.error.issues.map((issue) => JSON.stringify(issue)).join("\n"));
process.exit(1);
}

export const environment = env.data;
export type Environment = z.infer<typeof validationSchema>;
1 change: 1 addition & 0 deletions apps/sample-app/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./env.js";
32 changes: 32 additions & 0 deletions apps/sample-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { inspect } from "util";
import { BlockchainProvider, IBlockchainProvider } from "@ts-turborepo-boilerplate/sample-lib";

import { environment } from "./config/env.js";
import { BalancesController } from "./stats/index.js";

const main = async (): Promise<void> => {
const dataProvider: IBlockchainProvider = new BlockchainProvider(environment.RPC_URL);

const balanceController = new BalancesController(dataProvider);

const vitalikAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const zeroAddress = "0x0000000000000000000000000000000000000000";

const balance = await balanceController.addBalances(vitalikAddress, zeroAddress);

console.log(`Vitalik's and Zero summed balance is: ${balance}`);
};

process.on("unhandledRejection", (reason, p) => {
console.error(`Unhandled Rejection at: \n${inspect(p, undefined, 100)}, \nreason: ${reason}`);
});

process.on("uncaughtException", (error: Error) => {
console.error(
`An uncaught exception occurred: ${error}\n` + `Exception origin: ${error.stack}`,
);
});

main().catch((err) => {
console.error(`Caught error in main handler: ${err}`);
});
14 changes: 14 additions & 0 deletions apps/sample-app/src/stats/balances.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Address, IBlockchainProvider } from "@ts-turborepo-boilerplate/sample-lib";

export class BalancesController {
constructor(private readonly blockchainProvider: IBlockchainProvider) {}

public async addBalances(addressA: Address, addressB: Address): Promise<bigint> {
const balances = await Promise.all([
this.blockchainProvider.getBalance(addressA),
this.blockchainProvider.getBalance(addressB),
]);

return balances[0] + balances[1];
}
}
1 change: 1 addition & 0 deletions apps/sample-app/src/stats/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./balances.controller.js";
39 changes: 39 additions & 0 deletions apps/sample-app/test/stats/controller/balances.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IBlockchainProvider } from "@ts-turborepo-boilerplate/sample-lib";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { BalancesController } from "../../../src/stats/index.js";

describe("BalancesController", () => {
let balancesController: BalancesController;
let blockchainProviderMock: IBlockchainProvider;

beforeEach(() => {
blockchainProviderMock = {
getBalance: vi.fn(),
};
balancesController = new BalancesController(blockchainProviderMock);
});

afterEach(() => {
vi.clearAllMocks();
});

describe("addBalances", () => {
it("should return the sum of balances for two addresses", async () => {
const addressA = "0x1234567890abcdef";
const addressB = "0xabcdef1234567890";
const balanceA = BigInt(1000000000000000000);
const balanceB = BigInt(2000000000000000000);

vi.spyOn(blockchainProviderMock, "getBalance").mockResolvedValueOnce(balanceA);
vi.spyOn(blockchainProviderMock, "getBalance").mockResolvedValueOnce(balanceB);

const result = await balancesController.addBalances(addressA, addressB);

expect(result).toEqual(balanceA + balanceB);
expect(blockchainProviderMock.getBalance).toHaveBeenCalledTimes(2);
expect(blockchainProviderMock.getBalance).toHaveBeenCalledWith(addressA);
expect(blockchainProviderMock.getBalance).toHaveBeenCalledWith(addressB);
});
});
});
8 changes: 8 additions & 0 deletions apps/sample-app/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
4 changes: 4 additions & 0 deletions apps/sample-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*"]
}
22 changes: 22 additions & 0 deletions apps/sample-app/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from "path";
import { configDefaults, defineConfig } from "vitest/config";

export default defineConfig({
test: {
globals: true, // Use Vitest's global API without importing it in each file
environment: "node", // Use the Node.js environment
include: ["test/**/*.spec.ts"], // Include test files
exclude: ["node_modules", "dist"], // Exclude certain directories
coverage: {
provider: "v8",
reporter: ["text", "json", "html"], // Coverage reporters
exclude: ["node_modules", "dist", "src/index.ts", ...configDefaults.exclude], // Files to exclude from coverage
},
},
resolve: {
alias: {
// Setup path alias based on tsconfig paths
"@": path.resolve(__dirname, "src"),
},
},
});
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@types/node": "22.5.4",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@vitest/coverage-v8": "2.0.5",
"commitlint": "19.4.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
Expand All @@ -41,7 +42,8 @@
"prettier": "3.3.3",
"sort-package-json": "2.10.1",
"turbo": "2.1.1",
"typescript": "5.5.4"
"typescript": "5.5.4",
"vitest": "2.0.5"
},
"packageManager": "pnpm@9.7.1+sha256.46f1bbc8f8020aa9869568c387198f1a813f21fb44c82f400e7d1dbde6c70b40",
"engines": {
Expand Down
66 changes: 66 additions & 0 deletions packages/sample-lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ts-turborepo-boilerplate: sample-lib package

> Note: use this lib as reference but preferred way is to re-write package
> from zero instead of refactoring this one.
> When you don't need this anymore, you can delete it

Sample library that exposes a Blockchain provider to query
account balances on Ethereum mainnet or EVM-compatible blockchains

## Setup

1. Change package name to follow yours project one in [`package.json`](./package.json)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get "to follow yours project one," maybe you meant "Change package name to your own"

2. Install dependencies running `pnpm install`

## Available Scripts

Available scripts that can be run using `pnpm`:

| Script | Description |
| ------------- | ------------------------------------------------------- |
| `build` | Build library using tsc |
| `check-types` | Check types issues using tsc |
| `clean` | Remove `dist` folder |
| `lint` | Run ESLint to check for coding standards |
| `lint:fix` | Run linter and automatically fix code formatting issues |
| `format` | Check code formatting and style using Prettier |
| `format:fix` | Run formatter and automatically fix issues |
| `test` | Run tests using vitest |
| `test:cov` | Run tests with coverage report |

## Usage

### Importing the Package

You can import the package in your TypeScript or JavaScript files as follows:

```typescript
import { BlockchainProvider } from "@ts-turborepo-boilerplate/sample-lib";
```

### Example

```typescript
// EVM-provider
const rpcUrl = ""; //non-empty valid url
const address = "0x...";

const provider = new BlockchainProvider(rpcUrl);

const balance = await provider.getBalance(address);

console.log(`Balance of ${address} is ${balance}`);
```

## API

### [IBlockchainProvider](./src/interfaces/blockchainProvider.interface.ts)

Available methods

- `getBalance(address: Address)`

## References

- [Viem](https://viem.sh/)
- [Offchain docs: Internal module pattern](https://www.notion.so/defi-wonderland/Best-Practices-c08b71f28e59490f8dadef64cf61c9ac?pvs=4#89f99d33053a426285bacc6275d994c0)
24 changes: 24 additions & 0 deletions packages/sample-lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@ts-turborepo-boilerplate/sample-lib",
"version": "0.0.1",
"private": true,
"description": "",
"license": "MIT",
"author": "Wonderland",
"type": "module",
"main": "./dist/src/index.js",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"check-types": "tsc --noEmit -p ./tsconfig.json",
"clean": "rm -rf dist",
"format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"",
"format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"",
"lint": "eslint \"{src,test}/**/*.{js,ts,json}\"",
"lint:fix": "pnpm lint --fix",
"test": "vitest run --config vitest.config.ts --passWithNoTests",
"test:cov": "vitest run --config vitest.config.ts --coverage"
},
"dependencies": {
"viem": "2.21.4"
}
}
1 change: 1 addition & 0 deletions packages/sample-lib/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./invalidRpcUrl.exception.js";
Loading