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 ipfs integration #2000

Merged
merged 1 commit into from
Jan 14, 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
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
35 changes: 35 additions & 0 deletions apps/relayer/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"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
}
]
},
"preset": "ts-jest/presets/default-esm",
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.[jt]s$": "$1"
},
"extensionsToTreatAsEsm": [".ts"],
"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");
}
}
}
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
Loading