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 publish message api method #1993

Merged
merged 1 commit into from
Jan 9, 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
1 change: 1 addition & 0 deletions apps/relayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"hardhat": "^2.22.15",
"helmet": "^8.0.0",
"maci-contracts": "workspace:^2.5.0",
"maci-domainobjs": "workspace:^2.5.0",
"mustache": "^4.2.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
Expand Down
89 changes: 89 additions & 0 deletions apps/relayer/tests/messages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common";
import { Test } from "@nestjs/testing";
import { ZeroAddress } from "ethers";
import { Keypair } from "maci-domainobjs";
import request from "supertest";

import type { App } from "supertest/types";

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

describe("e2e messages", () => {
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(3001);
});

afterAll(async () => {
await app.close();
});

describe("/v1/messages/publish", () => {
const keypair = new Keypair();

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

test("should throw an error if dto is invalid", async () => {
const result = await request(app.getHttpServer() as App)
.post("/v1/messages/publish")
.send({
maciContractAddress: "invalid",
poll: "-1",
messages: [],
})
.expect(HttpStatus.BAD_REQUEST);

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: [
"poll must not be less than 0",
"poll must be an integer number",
"maciContractAddress must be an Ethereum address",
"messages must contain at least 1 elements",
],
});
});

test("should throw an error if messages dto is invalid", async () => {
const result = await request(app.getHttpServer() as App)
.post("/v1/messages/publish")
.send({
...defaultSaveMessagesArgs,
messages: [{ data: [], publicKey: "invalid" }],
})
.expect(HttpStatus.BAD_REQUEST);

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: ["messages.0.data must contain at least 10 elements", "messages.0.Public key (invalid) is invalid"],
});
});

test("should publish user messages properly", async () => {
const result = await request(app.getHttpServer() as App)
.post("/v1/messages/publish")
.send(defaultSaveMessagesArgs)
.expect(HttpStatus.CREATED);

expect(result.status).toBe(HttpStatus.CREATED);
});
});
});
3 changes: 3 additions & 0 deletions apps/relayer/ts/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Module } from "@nestjs/common";
import { ThrottlerModule } from "@nestjs/throttler";

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

@Module({
imports: [
ThrottlerModule.forRoot([
Expand All @@ -9,6 +11,7 @@ import { ThrottlerModule } from "@nestjs/throttler";
limit: Number(process.env.LIMIT),
},
]),
MessageModule,
],
})
export class AppModule {}
55 changes: 55 additions & 0 deletions apps/relayer/ts/message/__tests__/message.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { HttpException, HttpStatus } from "@nestjs/common";
import { Test } from "@nestjs/testing";

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

import { defaultSaveMessagesArgs } from "./utils";

describe("MessageController", () => {
let controller: MessageController;

const mockMessageService = {
saveMessages: jest.fn(),
merge: jest.fn(),
};

beforeEach(async () => {
const app = await Test.createTestingModule({
controllers: [MessageController],
})
.useMocker((token) => {
if (token === MessageService) {
mockMessageService.saveMessages.mockResolvedValue(true);

return mockMessageService;
}

return jest.fn();
})
.compile();

controller = app.get<MessageController>(MessageController);
});

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

describe("v1/messages/publish", () => {
test("should publish user messages properly", async () => {
const data = await controller.publish(defaultSaveMessagesArgs);

expect(data).toBe(true);
});

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

await expect(controller.publish(defaultSaveMessagesArgs)).rejects.toThrow(
new HttpException(error.message, HttpStatus.BAD_REQUEST),
);
});
});
});
21 changes: 21 additions & 0 deletions apps/relayer/ts/message/__tests__/message.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MessageService } from "../message.service";

import { defaultSaveMessagesArgs } from "./utils";

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

const result = await service.saveMessages(defaultSaveMessagesArgs);

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

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

const result = await service.publishMessages(defaultSaveMessagesArgs);

expect(result).toStrictEqual({ hash: "", ipfsHash: "" });
});
});
15 changes: 15 additions & 0 deletions apps/relayer/ts/message/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ZeroAddress } from "ethers";
import { Keypair } from "maci-domainobjs";

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(),
},
],
};
30 changes: 30 additions & 0 deletions apps/relayer/ts/message/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Keypair } from "maci-domainobjs";

import { PublicKeyValidator } from "../validation";

describe("PublicKeyValidator", () => {
test("should validate valid public key", () => {
const keypair = new Keypair();
const validator = new PublicKeyValidator();

const result = validator.validate(keypair.pubKey.serialize());

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

test("should validate invalid public key", () => {
const validator = new PublicKeyValidator();

const result = validator.validate("invalid");

expect(result).toBe(false);
});

test("should return default message properly", () => {
const validator = new PublicKeyValidator();

const result = validator.defaultMessage();

expect(result).toBe("Public key ($value) is invalid");
});
});
88 changes: 88 additions & 0 deletions apps/relayer/ts/message/dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsEthereumAddress,
IsInt,
Min,
Validate,
IsArray,
ArrayMinSize,
ArrayMaxSize,
ValidateNested,
} from "class-validator";
import { Message } from "maci-domainobjs";

import { PublicKeyValidator } from "./validation";

/**
* Max messages per batch
*/
const MAX_MESSAGES = 20;

/**
* Data transfer object for user message
*/
export class MessageContractParamsDto {
/**
* Message data
*/
@ApiProperty({
description: "Message data",
type: [String],
})
@IsArray()
@ArrayMinSize(Message.DATA_LENGTH)
@ArrayMaxSize(Message.DATA_LENGTH)
data!: string[];

/**
* Public key
*/
@ApiProperty({
description: "Public key",
type: String,
})
@Validate(PublicKeyValidator)
publicKey!: string;
}

/**
* Data transfer object for publish messages
*/
export class PublishMessagesDto {
/**
* Poll id
*/
@ApiProperty({
description: "Poll id",
minimum: 0,
type: Number,
})
@IsInt()
@Min(0)
poll!: number;

/**
* Maci contract address
*/
@ApiProperty({
description: "MACI contract address",
type: String,
})
@IsEthereumAddress()
maciContractAddress!: string;

/**
* Messages
*/
@ApiProperty({
description: "User messages with public key",
type: [MessageContractParamsDto],
})
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(MAX_MESSAGES)
@ValidateNested({ each: true })
@Type(() => MessageContractParamsDto)
messages!: MessageContractParamsDto[];
}
41 changes: 41 additions & 0 deletions apps/relayer/ts/message/message.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-shadow */
import { Body, Controller, HttpException, HttpStatus, Logger, Post } from "@nestjs/common";
import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from "@nestjs/swagger";

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

@ApiTags("v1/messages")
@ApiBearerAuth()
@Controller("v1/messages")
export class MessageController {
/**
* Logger
*/
private readonly logger = new Logger(MessageController.name);

/**
* Initialize MessageController
*
*/
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
* @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> {
return this.messageService.saveMessages(args).catch((error: Error) => {
this.logger.error(`Error:`, error);
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
});
}
}
10 changes: 10 additions & 0 deletions apps/relayer/ts/message/message.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";

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

@Module({
controllers: [MessageController],
providers: [MessageService],
})
export class MessageModule {}
Loading
Loading