Skip to content

Commit

Permalink
feat(relayer): add ipfs integration
Browse files Browse the repository at this point in the history
- [x] Add IPFS service
- [x] Add Cron job integration
- [x] Use esm for jest
  • Loading branch information
0xmad committed Jan 13, 2025
1 parent a2e3469 commit d1f3adc
Show file tree
Hide file tree
Showing 19 changed files with 3,847 additions and 162 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ module.exports = {
"error",
{
builtinGlobals: true,
allow: ["location", "event", "history", "name", "status", "Option", "test", "expect"],
allow: ["location", "event", "history", "name", "status", "Option", "test", "expect", "jest"],
},
],
"@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }],
Expand Down
28 changes: 28 additions & 0 deletions apps/relayer/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"testTimeout": 900000,
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"roots": ["<rootDir>/ts", "<rootDir>/tests"],
"testRegex": ".*\\.test\\.ts$",
"transform": {
"^.+\\.(t|j)s$": [
"ts-jest",
{
"useESM": true
}
]
},
"preset": "ts-jest/presets/default-esm",
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.[jt]s$": "$1"
},
"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"
}
48 changes: 7 additions & 41 deletions apps/relayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@
"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",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit",
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --forceExit",
"types": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@helia/json": "^4.0.1",
"@nestjs/common": "^10.4.7",
"@nestjs/core": "^10.4.7",
"@nestjs/mongoose": "^10.1.0",
"@nestjs/platform-express": "^10.4.7",
"@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/schedule": "^4.1.2",
"@nestjs/swagger": "^8.0.3",
"@nestjs/throttler": "^6.3.0",
"@nestjs/websockets": "^10.4.7",
Expand All @@ -40,16 +42,19 @@
"dotenv": "^16.4.5",
"ethers": "^6.13.4",
"hardhat": "^2.22.15",
"helia": "^5.1.1",
"helmet": "^8.0.0",
"maci-contracts": "workspace:^2.5.0",
"maci-domainobjs": "workspace:^2.5.0",
"mongoose": "^8.9.3",
"multiformats": "^13.3.1",
"mustache": "^4.2.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"ts-node": "^10.9.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@nestjs/cli": "^10.4.2",
"@nestjs/schematics": "^10.1.2",
"@nestjs/testing": "^10.4.15",
Expand All @@ -63,44 +68,5 @@
"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"
}
}
4 changes: 4 additions & 0 deletions apps/relayer/ts/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { ScheduleModule } from "@nestjs/schedule";
import { ThrottlerModule } from "@nestjs/throttler";

import { IpfsModule } from "./ipfs/ipfs.module";
import { MessageModule } from "./message/message.module";
import { MessageBatchModule } from "./messageBatch/messageBatch.module";

Expand All @@ -13,6 +15,7 @@ import { MessageBatchModule } from "./messageBatch/messageBatch.module";
limit: Number(process.env.LIMIT),
},
]),
ScheduleModule.forRoot(),
MongooseModule.forRootAsync({
useFactory: async () => {
if (process.env.NODE_ENV === "test") {
Expand All @@ -31,6 +34,7 @@ import { MessageBatchModule } from "./messageBatch/messageBatch.module";
};
},
}),
IpfsModule,
MessageModule,
MessageBatchModule,
],
Expand Down
22 changes: 22 additions & 0 deletions apps/relayer/ts/ipfs/__tests__/ipfs.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IpfsService } from "../ipfs.service";

describe("IpfsService", () => {
const defaultData = { hello: "world" };

test("should not add or get data if adapter is not initialized", async () => {
const service = new IpfsService();

await expect(service.add(defaultData)).rejects.toThrow("IPFS adapter is not initialized");
await expect(service.get("cid")).rejects.toThrow("IPFS adapter is not initialized");
});

test("should add data and get data properly", async () => {
const service = new IpfsService();
await service.init();

const cid = await service.add(defaultData);
const data = await service.get(cid);

expect(data).toStrictEqual(defaultData);
});
});
9 changes: 9 additions & 0 deletions apps/relayer/ts/ipfs/ipfs.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";

import { IpfsService } from "./ipfs.service";

@Module({
exports: [IpfsService],
providers: [IpfsService],
})
export class IpfsModule {}
70 changes: 70 additions & 0 deletions apps/relayer/ts/ipfs/ipfs.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Injectable, Logger } from "@nestjs/common";

import type { JSON as JsonAdapter } from "@helia/json";

/**
* IpfsService is responsible for saving data to ipfs
*/
@Injectable()
export class IpfsService {
/**
* Logger
*/
private readonly logger: Logger = new Logger(IpfsService.name);

/**
* IPFS adapter
*/
private adapter?: JsonAdapter;

/**
* Initialize IpfsService
*/
async init(): Promise<void> {
if (!this.adapter) {
const { createHelia } = await import("helia");
const { json } = await import("@helia/json");

const helia = await createHelia();
this.adapter = json(helia);
}
}

/**
* Add data to IPFS and return the CID
*
* @param data data to be added to IPFS
* @returns cid
*/
async add<T>(data: T): Promise<string> {
this.checkAdapter();

return this.adapter!.add(data).then((cid) => cid.toString());
}

/**
* Get data from IPFS
*
* @param cid CID of the data to be fetched from IPFS
* @returns data
*/
async get<T>(cid: string): Promise<T> {
this.checkAdapter();

const { CID } = await import("multiformats");

return this.adapter!.get<T>(CID.parse(cid));
}

/**
* Check if IPFS adapter is initialized
*
* @throws Error if IPFS adapter is not initialized
*/
private checkAdapter(): void {
if (!this.adapter) {
this.logger.error("IPFS adapter is not initialized");
throw new Error("IPFS adapter is not initialized");
}
}
}
5 changes: 3 additions & 2 deletions apps/relayer/ts/message/__tests__/message.controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { jest } from "@jest/globals";
import { HttpException, HttpStatus } from "@nestjs/common";
import { Test } from "@nestjs/testing";

Expand All @@ -20,7 +21,7 @@ describe("MessageController", () => {
})
.useMocker((token) => {
if (token === MessageService) {
mockMessageService.saveMessages.mockResolvedValue(true);
mockMessageService.saveMessages.mockImplementation(() => Promise.resolve(true));

return mockMessageService;
}
Expand All @@ -45,7 +46,7 @@ describe("MessageController", () => {

test("should throw an error if messages saving is failed", async () => {
const error = new Error("error");
mockMessageService.saveMessages.mockRejectedValue(error);
mockMessageService.saveMessages.mockImplementation(() => Promise.reject(error));

await expect(controller.publish(defaultSaveMessagesArgs)).rejects.toThrow(
new HttpException(error.message, HttpStatus.BAD_REQUEST),
Expand Down
31 changes: 16 additions & 15 deletions apps/relayer/ts/message/__tests__/message.repository.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { jest } from "@jest/globals";
import { ZeroAddress } from "ethers";
import { Keypair } from "maci-domainobjs";
import { Model } from "mongoose";
Expand All @@ -18,25 +19,25 @@ describe("MessageRepository", () => {
];

const mockMessageModel = {
find: jest
.fn()
.mockReturnValue({ limit: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(defaultMessages) }) }),
insertMany: jest.fn().mockResolvedValue(defaultMessages),
} as unknown as Model<Message>;
find: jest.fn().mockReturnValue({
limit: jest.fn().mockReturnValue({ exec: jest.fn().mockImplementation(() => Promise.resolve(defaultMessages)) }),
}),
insertMany: jest.fn().mockImplementation(() => Promise.resolve(defaultMessages)),
};

beforeEach(() => {
mockMessageModel.find = jest
.fn()
.mockReturnValue({ limit: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(defaultMessages) }) });
mockMessageModel.insertMany = jest.fn().mockResolvedValue(defaultMessages);
mockMessageModel.find = jest.fn().mockReturnValue({
limit: jest.fn().mockReturnValue({ exec: jest.fn().mockImplementation(() => Promise.resolve(defaultMessages)) }),
});
mockMessageModel.insertMany = jest.fn().mockImplementation(() => Promise.resolve(defaultMessages));
});

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

test("should create messages properly", async () => {
const repository = new MessageRepository(mockMessageModel);
const repository = new MessageRepository(mockMessageModel as unknown as Model<Message>);

const result = await repository.create(defaultSaveMessagesArgs);

Expand All @@ -46,15 +47,15 @@ describe("MessageRepository", () => {
test("should throw an error if creation is failed", async () => {
const error = new Error("error");

(mockMessageModel.insertMany as jest.Mock).mockRejectedValue(error);
(mockMessageModel.insertMany as jest.Mock).mockImplementation(() => Promise.reject(error));

const repository = new MessageRepository(mockMessageModel);
const repository = new MessageRepository(mockMessageModel as unknown as Model<Message>);

await expect(repository.create(defaultSaveMessagesArgs)).rejects.toThrow(error);
});

test("should find messages properly", async () => {
const repository = new MessageRepository(mockMessageModel);
const repository = new MessageRepository(mockMessageModel as unknown as Model<Message>);

const result = await repository.find({});

Expand All @@ -66,11 +67,11 @@ describe("MessageRepository", () => {

(mockMessageModel.find as jest.Mock).mockReturnValue({
limit: jest.fn().mockReturnValue({
exec: jest.fn().mockRejectedValue(error),
exec: jest.fn().mockImplementation(() => Promise.reject(error)),
}),
});

const repository = new MessageRepository(mockMessageModel);
const repository = new MessageRepository(mockMessageModel as unknown as Model<Message>);

await expect(repository.find({})).rejects.toThrow(error);
});
Expand Down
Loading

0 comments on commit d1f3adc

Please sign in to comment.