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 database integration #1995

Merged
merged 1 commit into from
Jan 13, 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
8 changes: 7 additions & 1 deletion .github/workflows/relayer-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ on:
pull_request:

env:
RPC_URL: "http://localhost:8545"
RELAYER_RPC_URL: "http://localhost:8545"
TTL: ${{ vars.RELAYER_TTL }}
LIMIT: ${{ vars.RELAYER_LIMIT }}
MONGO_DB_URI: ${{ secrets.RELAYER_MONGO_DB_URI }}
MONGODB_USER: ${{ secrets.MONGODB_USER }}
MONGODB_PASSWORD: ${{ secrets.MONGODB_PASSWORD }}
MONGODB_DATABASE: ${{ secrets.MONGODB_DATABASE }}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down
6 changes: 6 additions & 0 deletions apps/relayer/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ LIMIT=10
# Coordinator RPC url
RELAYER_RPC_URL=http://localhost:8545

# MongoDB configuration
MONGO_DB_URI=mongodb://localhost
MONGODB_USER=maci
MONGODB_PASSWORD=
MONGODB_DATABASE=maci-relayer

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

Expand Down
3 changes: 3 additions & 0 deletions apps/relayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"dependencies": {
"@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/swagger": "^8.0.3",
Expand All @@ -42,6 +43,7 @@
"helmet": "^8.0.0",
"maci-contracts": "workspace:^2.5.0",
"maci-domainobjs": "workspace:^2.5.0",
"mongoose": "^8.9.3",
"mustache": "^4.2.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
Expand All @@ -57,6 +59,7 @@
"@types/supertest": "^6.0.2",
"fast-check": "^3.23.1",
"jest": "^29.5.0",
"mongodb-memory-server": "^10.1.3",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2"
Expand Down
2 changes: 1 addition & 1 deletion apps/relayer/tests/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { App } from "supertest/types";

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

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

beforeAll(async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/relayer/tests/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { App } from "supertest/types";

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

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

beforeAll(async () => {
Expand Down
21 changes: 21 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 { ThrottlerModule } from "@nestjs/throttler";

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

@Module({
imports: [
Expand All @@ -11,7 +13,26 @@ import { MessageModule } from "./message/message.module";
limit: Number(process.env.LIMIT),
},
]),
MongooseModule.forRootAsync({
useFactory: async () => {
if (process.env.NODE_ENV === "test") {
const { getTestMongooseModuleOptions } = await import("./jest/mongo");

return getTestMongooseModuleOptions();
}

return {
uri: process.env.MONGO_DB_URI,
auth: {
username: process.env.MONGODB_USER,
password: process.env.MONGODB_PASSWORD,
},
dbName: process.env.MONGODB_DATABASE,
};
},
}),
MessageModule,
MessageBatchModule,
],
})
export class AppModule {}
20 changes: 20 additions & 0 deletions apps/relayer/ts/jest/mongo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { MongooseModuleOptions } from "@nestjs/mongoose";

/**
* Get test mongoose module options
*
* @param options mongoose module options
* @returns mongoose module options for testing
*/
export const getTestMongooseModuleOptions = async (
options: MongooseModuleOptions = {},
): Promise<MongooseModuleOptions> => {
// eslint-disable-next-line import/no-extraneous-dependencies
const { MongoMemoryServer } = await import("mongodb-memory-server");
const mongod = await MongoMemoryServer.create();

return {
uri: mongod.getUri(),
...options,
};
};
77 changes: 77 additions & 0 deletions apps/relayer/ts/message/__tests__/message.repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ZeroAddress } from "ethers";
import { Keypair } from "maci-domainobjs";
import { Model } from "mongoose";

import { MessageRepository } from "../message.repository";
import { Message } from "../message.schema";

import { defaultSaveMessagesArgs } from "./utils";

describe("MessageRepository", () => {
const defaultMessages: Message[] = [
{
publicKey: new Keypair().pubKey.serialize(),
data: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
maciContractAddress: ZeroAddress,
poll: 0,
},
];

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>;

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

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

test("should create messages properly", async () => {
const repository = new MessageRepository(mockMessageModel);

const result = await repository.create(defaultSaveMessagesArgs);

expect(result).toStrictEqual(defaultMessages);
});

test("should throw an error if creation is failed", async () => {
const error = new Error("error");

(mockMessageModel.insertMany as jest.Mock).mockRejectedValue(error);

const repository = new MessageRepository(mockMessageModel);

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

test("should find messages properly", async () => {
const repository = new MessageRepository(mockMessageModel);

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

expect(result).toStrictEqual(defaultMessages);
});

test("should throw an error if find is failed", async () => {
const error = new Error("error");

(mockMessageModel.find as jest.Mock).mockReturnValue({
limit: jest.fn().mockReturnValue({
exec: jest.fn().mockRejectedValue(error),
}),
});

const repository = new MessageRepository(mockMessageModel);

await expect(repository.find({})).rejects.toThrow(error);
});
});
53 changes: 48 additions & 5 deletions apps/relayer/ts/message/__tests__/message.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,64 @@
import type { MessageBatchService } from "../../messageBatch/messageBatch.service";
import type { MessageRepository } from "../message.repository";

import { MessageService } from "../message.service";

import { defaultSaveMessagesArgs } from "./utils";
import { defaultMessages, defaultSaveMessagesArgs } from "./utils";

describe("MessageService", () => {
const mockMessageBatchService = {
saveMessageBatches: jest.fn().mockImplementation((args) => Promise.resolve(args)),
} as unknown as MessageBatchService;

const mockRepository = {
create: jest.fn().mockResolvedValue(defaultMessages),
find: jest.fn().mockResolvedValue(defaultMessages),
} as unknown as MessageRepository;

beforeEach(() => {
mockMessageBatchService.saveMessageBatches = jest.fn().mockImplementation((args) => Promise.resolve(args));

mockRepository.create = jest.fn().mockResolvedValue(defaultMessages);
mockRepository.find = jest.fn().mockResolvedValue(defaultMessages);
});

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

test("should save messages properly", async () => {
const service = new MessageService();
const service = new MessageService(mockMessageBatchService, mockRepository);

const result = await service.saveMessages(defaultSaveMessagesArgs);

expect(result).toBe(true);
expect(result).toStrictEqual(defaultMessages);
});

test("should throw an error if can't save messages", async () => {
const error = new Error("error");

(mockRepository.create as jest.Mock).mockRejectedValue(error);

const service = new MessageService(mockMessageBatchService, mockRepository);

await expect(service.saveMessages(defaultSaveMessagesArgs)).rejects.toThrow(error);
});

test("should publish messages properly", async () => {
const service = new MessageService();
const service = new MessageService(mockMessageBatchService, mockRepository);

const result = await service.publishMessages(defaultSaveMessagesArgs);
const result = await service.publishMessages();

expect(result).toStrictEqual({ hash: "", ipfsHash: "" });
});

test("should throw an error if can't save message batch", async () => {
const error = new Error("error");

(mockMessageBatchService.saveMessageBatches as jest.Mock).mockRejectedValue(error);

const service = new MessageService(mockMessageBatchService, mockRepository);

await expect(service.publishMessages()).rejects.toThrow(error);
});
});
24 changes: 14 additions & 10 deletions apps/relayer/ts/message/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { ZeroAddress } from "ethers";
import { Keypair } from "maci-domainobjs";

import { defaultMessageBatches } from "../../messageBatch/__tests__/utils";
import { PublishMessagesDto } from "../dto";

const keypair = new Keypair();

export const defaultSaveMessagesArgs = {
maciContractAddress: ZeroAddress,
poll: 0,
messages: [
{
data: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
publicKey: keypair.pubKey.serialize(),
},
],
};
export const defaultMessages = defaultMessageBatches[0].messages;

export const defaultSaveMessagesArgs = new PublishMessagesDto();
defaultSaveMessagesArgs.maciContractAddress = ZeroAddress;
defaultSaveMessagesArgs.poll = 0;
defaultSaveMessagesArgs.messages = [
{
data: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
publicKey: keypair.pubKey.serialize(),
},
];
6 changes: 4 additions & 2 deletions apps/relayer/ts/message/message.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Body, Controller, HttpException, HttpStatus, Logger, Post } from "@nest
import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from "@nestjs/swagger";

import { PublishMessagesDto } from "./dto";
import { Message } from "./message.schema";
import { MessageService } from "./message.service";

@ApiTags("v1/messages")
Expand All @@ -17,22 +18,23 @@ export class MessageController {
/**
* Initialize MessageController
*
* @param messageService message service
*/
constructor(private readonly messageService: MessageService) {}

/**
* Publish user messages api method.
* Saves messages batch and then send them onchain by calling `publishMessages` method via cron job.
*
* @param args - publish messages dto
* @param args publish messages dto
* @returns success or not
*/
@ApiBody({ type: PublishMessagesDto })
@ApiResponse({ status: HttpStatus.CREATED, description: "The messages have been successfully accepted" })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: "Forbidden" })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" })
@Post("publish")
async publish(@Body() args: PublishMessagesDto): Promise<boolean> {
async publish(@Body() args: PublishMessagesDto): Promise<Message[]> {
return this.messageService.saveMessages(args).catch((error: Error) => {
this.logger.error(`Error:`, error);
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
Expand Down
8 changes: 7 additions & 1 deletion apps/relayer/ts/message/message.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";

import { MessageBatchModule } from "../messageBatch/messageBatch.module";

import { MessageController } from "./message.controller";
import { MessageRepository } from "./message.repository";
import { Message, MessageSchema } from "./message.schema";
import { MessageService } from "./message.service";

@Module({
imports: [MongooseModule.forFeature([{ name: Message.name, schema: MessageSchema }]), MessageBatchModule],
controllers: [MessageController],
providers: [MessageService],
providers: [MessageService, MessageRepository],
})
export class MessageModule {}
Loading
Loading