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(relayer): add relayer service boilerplate #1990

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
51 changes: 51 additions & 0 deletions .github/workflows/relayer-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Relayer

on:
push:
branches: [dev]
pull_request:

env:
RPC_URL: "http://localhost:8545"

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"

- name: Install
run: |
pnpm install --frozen-lockfile --prefer-offline

- name: Build
run: |
pnpm run build

- name: Run hardhat
run: |
pnpm run hardhat &
sleep 5
working-directory: apps/relayer

- name: Test
run: pnpm run test:coverage
working-directory: apps/relayer

- name: Stop Hardhat
if: always()
run: kill $(lsof -t -i:8545)
15 changes: 15 additions & 0 deletions apps/relayer/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Rate limit configuation
TTL=60000
LIMIT=10

# Coordinator RPC url
RELAYER_RPC_URL=http://localhost:8545

# Allowed origin host, use comma to separate each of them
ALLOWED_ORIGINS=

# Specify port for coordinator service (optional)
PORT=

# Mnemonic phrase
MNEMONIC=""
32 changes: 32 additions & 0 deletions apps/relayer/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const path = require("path");

module.exports = {
root: true,
env: {
node: true,
jest: true,
},
extends: ["../../.eslintrc.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: path.resolve(__dirname, "./tsconfig.json"),
sourceType: "module",
typescript: true,
ecmaVersion: 2022,
experimentalDecorators: true,
requireConfigFile: false,
ecmaFeatures: {
classes: true,
impliedStrict: true,
},
warnOnUnsupportedTypeScriptVersion: true,
},
overrides: [
{
files: ["./ts/**/*.module.ts"],
rules: {
"@typescript-eslint/no-extraneous-class": "off",
},
},
],
};
3 changes: 3 additions & 0 deletions apps/relayer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/
coverage/
.env
5 changes: 5 additions & 0 deletions apps/relayer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Relayer service

## Instructions

1. Add `.env` file (see `.env.example`).
33 changes: 33 additions & 0 deletions apps/relayer/hardhat.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-var-requires */
require("@nomicfoundation/hardhat-toolbox");
const dotenv = require("dotenv");

const path = require("path");

dotenv.config();

const parentDir = __dirname.includes("build") ? ".." : "";
const TEST_MNEMONIC = "test test test test test test test test test test test junk";

module.exports = {
defaultNetwork: "hardhat",
networks: {
localhost: {
url: process.env.RELAYER_RPC_URL || "",
accounts: {
mnemonic: process.env.MNEMONIC || TEST_MNEMONIC,
path: "m/44'/60'/0'/0",
initialIndex: 0,
count: 20,
},
loggingEnabled: false,
},
hardhat: {
loggingEnabled: false,
},
},
paths: {
sources: path.resolve(__dirname, parentDir, "./node_modules/maci-contracts/contracts"),
artifacts: path.resolve(__dirname, parentDir, "./node_modules/maci-contracts/build/artifacts"),
},
};
8 changes: 8 additions & 0 deletions apps/relayer/nest-cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "ts",
"compilerOptions": {
"deleteOutDir": true
}
}
102 changes: 102 additions & 0 deletions apps/relayer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"name": "maci-relayer",
"version": "0.1.0",
"private": true,
"description": "Relayer service for MACI",
"main": "build/ts/main.js",
"type": "module",
"exports": {
".": "./build/ts/main.js"
},
"files": [
"build",
"CHANGELOG.md",
"README.md"
],
"scripts": {
"hardhat": "hardhat node",
"build": "nest build",
"run:node": "node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));'",
"start": "pnpm run run:node ./ts/main.ts",
"start:prod": "pnpm run run:node build/ts/main.js",
"test": "jest --forceExit",
"test:coverage": "jest --coverage --forceExit",
"types": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@nestjs/common": "^10.4.7",
"@nestjs/core": "^10.4.7",
"@nestjs/platform-express": "^10.4.7",
"@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/swagger": "^8.0.3",
"@nestjs/throttler": "^6.3.0",
"@nestjs/websockets": "^10.4.7",
"@nomicfoundation/hardhat-ethers": "^3.0.8",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"ethers": "^6.13.4",
"hardhat": "^2.22.15",
"helmet": "^8.0.0",
"maci-contracts": "workspace:^2.5.0",
"mustache": "^4.2.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"ts-node": "^10.9.1"
},
"devDependencies": {
"@nestjs/cli": "^10.4.2",
"@nestjs/schematics": "^10.1.2",
"@nestjs/testing": "^10.4.15",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^22.10.5",
"@types/supertest": "^6.0.2",
"fast-check": "^3.23.1",
"jest": "^29.5.0",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2"
},
"jest": {
"testTimeout": 900000,
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"roots": [
"<rootDir>/ts",
"<rootDir>/tests"
],
"testRegex": ".*\\.test\\.ts$",
"transform": {
"^.+\\.js$": [
"<rootDir>/ts/jest/transform.js",
{
"useESM": true
}
],
"^.+\\.(t|j)s$": [
"ts-jest",
{
"useESM": true
}
]
},
"collectCoverageFrom": [
"**/*.(t|j)s",
"!<rootDir>/ts/main.ts",
"!<rootDir>/ts/jest/*.js",
"!<rootDir>/hardhat.config.js"
],
"coveragePathIgnorePatterns": [
"<rootDir>/ts/sessionKeys/__tests__/utils.ts"
],
"coverageDirectory": "<rootDir>/coverage",
"testEnvironment": "node"
}
}
38 changes: 38 additions & 0 deletions apps/relayer/tests/app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common";
import { Test } from "@nestjs/testing";
import request from "supertest";

import type { App } from "supertest/types";

import { AppModule } from "../ts/app.module";

describe("e2e", () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(3000);
});

afterAll(async () => {
await app.close();
});

test("should throw an error if api is not found", async () => {
const result = await request(app.getHttpServer() as App)
.get("/unknown")
.send()
.expect(404);

expect(result.body).toStrictEqual({
error: "Not Found",
statusCode: HttpStatus.NOT_FOUND,
message: "Cannot GET /unknown",
});
});
});
14 changes: 14 additions & 0 deletions apps/relayer/ts/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { ThrottlerModule } from "@nestjs/throttler";

@Module({
imports: [
ThrottlerModule.forRoot([
{
ttl: Number(process.env.TTL),
limit: Number(process.env.LIMIT),
},
]),
],
})
export class AppModule {}
9 changes: 9 additions & 0 deletions apps/relayer/ts/jest/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types";
import type { ethers } from "ethers";

declare module "hardhat/types/runtime" {
interface HardhatRuntimeEnvironment {
// We omit the ethers field because it is redundant.
ethers: typeof ethers & HardhatEthersHelpers;
}
}
11 changes: 11 additions & 0 deletions apps/relayer/ts/jest/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */

export function process(sourceText) {
return {
code: sourceText.replace("#!/usr/bin/env node", ""),
};
}

export default {
process,
};
55 changes: 55 additions & 0 deletions apps/relayer/ts/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import dotenv from "dotenv";
import helmet from "helmet";

import path from "path";
import url from "url";

/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/no-shadow */
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/* eslint-enable no-underscore-dangle */
/* eslint-disable @typescript-eslint/no-shadow */

dotenv.config({
path: [path.resolve(__dirname, "../.env"), path.resolve(__dirname, "../.env.example")],
});

async function bootstrap() {
const { AppModule } = await import("./app.module");
const app = await NestFactory.create(AppModule, {
logger: ["log", "fatal", "error", "warn"],
});

app.useGlobalPipes(new ValidationPipe({ transform: true }));
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: [`'self'`],
styleSrc: [`'self'`, `'unsafe-inline'`],
imgSrc: [`'self'`, "data:", "validator.swagger.io"],
scriptSrc: [`'self'`, `https: 'unsafe-inline'`],
},
},
}),
);
app.enableCors({ origin: process.env.ALLOWED_ORIGINS?.split(",") });

const config = new DocumentBuilder()
.setTitle("Relayer service")
.setDescription("Relayer service API methods")
.setVersion("1.0")
.addTag("relayer")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("api", app, document);

await app.listen(process.env.PORT || 3000);
}

bootstrap();
Loading
Loading