Skip to content

Commit

Permalink
feat(relayer): add database integration
Browse files Browse the repository at this point in the history
- [x] Add mongodb integration
- [x] Add message repository
- [x] Add message batch repository
- [x] Add message batch service
- [x] Add message and message batch schemas
- [x] Integrate message repository into message service
- [x] Integrate message batch service into message service
  • Loading branch information
0xmad committed Jan 10, 2025
1 parent 1587632 commit a80202c
Show file tree
Hide file tree
Showing 27 changed files with 1,120 additions and 73 deletions.
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

0 comments on commit a80202c

Please sign in to comment.