Skip to content

Commit

Permalink
Merge pull request #1990 from privacy-scaling-explorations/feature/re…
Browse files Browse the repository at this point in the history
…layer

feat(relayer): add relayer service boilerplate
  • Loading branch information
0xmad authored Jan 7, 2025
2 parents f59e9c3 + 985f572 commit d8eefd8
Show file tree
Hide file tree
Showing 16 changed files with 3,145 additions and 116 deletions.
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

0 comments on commit d8eefd8

Please sign in to comment.