From 1ebe11eb733242674ef2551e34eb81bd99dc689e Mon Sep 17 00:00:00 2001 From: Anton <14254374+0xmad@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:37:17 -0600 Subject: [PATCH] feat(relayer): fetch message batches - [x] Add message batches fetch api method - [x] Refactoring for integration tests --- apps/relayer/jest.config.json | 2 + apps/relayer/package.json | 2 +- apps/relayer/tests/deploy.ts | 126 ++++++++++++++++++ apps/relayer/tests/messageBatches.test.ts | 118 ++++++++++++++++ apps/relayer/tests/messages.test.ts | 92 ++----------- apps/relayer/ts/jest/setup.ts | 4 + .../__tests__/messageBatch.controller.test.ts | 55 ++++++++ .../__tests__/messageBatch.service.test.ts | 19 ++- .../ts/messageBatch/__tests__/utils.ts | 9 +- .../messageBatch/__tests__/validation.test.ts | 2 +- .../messageBatch/messageBatch.controller.ts | 56 ++++++++ .../ts/messageBatch/messageBatch.dto.ts | 85 +++++++++++- .../ts/messageBatch/messageBatch.module.ts | 2 + .../messageBatch/messageBatch.repository.ts | 1 + .../ts/messageBatch/messageBatch.service.ts | 18 ++- apps/relayer/ts/messageBatch/validation.ts | 2 +- 16 files changed, 506 insertions(+), 87 deletions(-) create mode 100644 apps/relayer/tests/deploy.ts create mode 100644 apps/relayer/tests/messageBatches.test.ts create mode 100644 apps/relayer/ts/messageBatch/__tests__/messageBatch.controller.test.ts create mode 100644 apps/relayer/ts/messageBatch/messageBatch.controller.ts diff --git a/apps/relayer/jest.config.json b/apps/relayer/jest.config.json index 5a536da33..59b1070bc 100644 --- a/apps/relayer/jest.config.json +++ b/apps/relayer/jest.config.json @@ -18,6 +18,7 @@ } ] }, + "setupFilesAfterEnv": ["/ts/jest/setup.ts"], "preset": "ts-jest/presets/default-esm", "moduleNameMapper": { "^(\\.{1,2}/.*)\\.[jt]s$": "$1" @@ -25,6 +26,7 @@ "extensionsToTreatAsEsm": [".ts"], "collectCoverageFrom": [ "**/*.(t|j)s", + "!/tests/*.ts", "!/ts/main.ts", "!/ts/jest/*.js", "!/hardhat.config.js" diff --git a/apps/relayer/package.json b/apps/relayer/package.json index 4d95cd52c..8f6f9546a 100644 --- a/apps/relayer/package.json +++ b/apps/relayer/package.json @@ -20,7 +20,7 @@ "start": "pnpm run run:node ./ts/main.ts", "start:prod": "pnpm run run:node build/ts/main.js", "test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit", - "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --forceExit", + "test:coverage": "pnpm run test --coverage", "types": "tsc -p tsconfig.json --noEmit" }, "dependencies": { diff --git a/apps/relayer/tests/deploy.ts b/apps/relayer/tests/deploy.ts new file mode 100644 index 000000000..c45be97fb --- /dev/null +++ b/apps/relayer/tests/deploy.ts @@ -0,0 +1,126 @@ +import hardhat from "hardhat"; +import { deploy, deployPoll, deployVkRegistryContract, joinPoll, setVerifyingKeys, signup } from "maci-cli"; +import { genMaciStateFromContract } from "maci-contracts"; +import { Keypair } from "maci-domainobjs"; + +import { + INT_STATE_TREE_DEPTH, + MESSAGE_BATCH_SIZE, + STATE_TREE_DEPTH, + VOTE_OPTION_TREE_DEPTH, + pollJoinedZkey, + pollJoiningZkey, + processMessagesZkeyPathNonQv, + tallyVotesZkeyPathNonQv, + pollWasm, + pollWitgen, + rapidsnark, +} from "./constants"; + +interface IContractsData { + initialized: boolean; + user?: Keypair; + voiceCredits?: number; + timestamp?: number; + stateLeafIndex?: number; + maciContractAddress?: string; + maciState?: Awaited>; +} + +export class TestDeploy { + private static INSTANCE?: TestDeploy; + + readonly contractsData: IContractsData = { + initialized: false, + }; + + static async getInstance(): Promise { + if (!TestDeploy.INSTANCE) { + TestDeploy.INSTANCE = new TestDeploy(); + await TestDeploy.INSTANCE.contractsInit(); + } + + return TestDeploy.INSTANCE; + } + + private async contractsInit(): Promise { + const [signer] = await hardhat.ethers.getSigners(); + const coordinatorKeypair = new Keypair(); + const user = new Keypair(); + + const vkRegistry = await deployVkRegistryContract({ signer }); + await setVerifyingKeys({ + quiet: true, + vkRegistry, + stateTreeDepth: STATE_TREE_DEPTH, + intStateTreeDepth: INT_STATE_TREE_DEPTH, + voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, + messageBatchSize: MESSAGE_BATCH_SIZE, + processMessagesZkeyPathNonQv, + tallyVotesZkeyPathNonQv, + pollJoiningZkeyPath: pollJoiningZkey, + pollJoinedZkeyPath: pollJoinedZkey, + useQuadraticVoting: false, + signer, + }); + + const maciAddresses = await deploy({ stateTreeDepth: 10, signer }); + + await deployPoll({ + pollDuration: 30, + intStateTreeDepth: INT_STATE_TREE_DEPTH, + messageBatchSize: MESSAGE_BATCH_SIZE, + voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, + coordinatorPubkey: coordinatorKeypair.pubKey.serialize(), + useQuadraticVoting: false, + signer, + }); + + await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer }); + + const { pollStateIndex, timestamp, voiceCredits } = await joinPoll({ + maciAddress: maciAddresses.maciAddress, + pollId: 0n, + privateKey: user.privKey.serialize(), + stateIndex: 1n, + pollJoiningZkey, + pollWasm, + pollWitgen, + rapidsnark, + signer, + useWasm: true, + quiet: true, + }); + + const maciState = await genMaciStateFromContract( + signer.provider, + maciAddresses.maciAddress, + coordinatorKeypair, + 0n, + ); + + this.contractsData.maciState = maciState; + this.contractsData.maciContractAddress = maciAddresses.maciAddress; + this.contractsData.stateLeafIndex = Number(pollStateIndex); + this.contractsData.timestamp = Number(timestamp); + this.contractsData.voiceCredits = Number(voiceCredits); + this.contractsData.user = user; + this.contractsData.initialized = true; + } +} + +const testDeploy = await TestDeploy.getInstance(); + +export async function waitForInitialization(): Promise { + return new Promise((resolve) => { + const checkInitialization = () => { + if (testDeploy.contractsData.initialized) { + resolve(); + } else { + setTimeout(checkInitialization, 1000); + } + }; + + setTimeout(checkInitialization, 2000); + }); +} diff --git a/apps/relayer/tests/messageBatches.test.ts b/apps/relayer/tests/messageBatches.test.ts new file mode 100644 index 000000000..fd2e46e43 --- /dev/null +++ b/apps/relayer/tests/messageBatches.test.ts @@ -0,0 +1,118 @@ +import { jest } from "@jest/globals"; +import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; +import { formatProofForVerifierContract, genProofSnarkjs } from "maci-contracts"; +import { Keypair } from "maci-domainobjs"; +import request from "supertest"; + +import type { App } from "supertest/types"; + +import { AppModule } from "../ts/app.module"; + +import { pollJoinedWasm, pollJoinedZkey } from "./constants"; +import { TestDeploy, waitForInitialization } from "./deploy"; + +jest.unmock("maci-contracts/typechain-types"); + +describe("Integration message batches", () => { + let app: INestApplication; + let stateLeafIndex: number; + let maciContractAddress: string; + let user: Keypair; + + beforeAll(async () => { + await waitForInitialization(); + const testDeploy = await TestDeploy.getInstance(); + const poll = testDeploy.contractsData.maciState!.polls.get(0n); + + poll!.updatePoll(BigInt(testDeploy.contractsData.maciState!.pubKeys.length)); + + stateLeafIndex = Number(testDeploy.contractsData.stateLeafIndex); + maciContractAddress = testDeploy.contractsData.maciContractAddress!; + user = testDeploy.contractsData.user!; + + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + await app.listen(3001); + + const circuitInputs = poll!.joinedCircuitInputs({ + maciPrivKey: testDeploy.contractsData.user!.privKey, + stateLeafIndex: BigInt(testDeploy.contractsData.stateLeafIndex!), + voiceCreditsBalance: BigInt(testDeploy.contractsData.voiceCredits!), + joinTimestamp: BigInt(testDeploy.contractsData.timestamp!), + }); + + const { proof } = await genProofSnarkjs({ + inputs: circuitInputs as unknown as Record, + zkeyPath: pollJoinedZkey, + wasmPath: pollJoinedWasm, + }); + + const keypair = new Keypair(); + + await request(app.getHttpServer() as App) + .post("/v1/messages/publish") + .send({ + messages: [ + { + data: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + publicKey: keypair.pubKey.serialize(), + }, + ], + poll: 0, + maciContractAddress, + stateLeafIndex, + proof: formatProofForVerifierContract(proof), + }) + .expect(HttpStatus.CREATED); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("/v1/messageBatches/get", () => { + test("should throw an error if dto is invalid", async () => { + const result = await request(app.getHttpServer() as App) + .get("/v1/messageBatches/get") + .send({ + limit: 0, + poll: -1, + maciContractAddress: "invalid", + publicKey: "invalid", + ipfsHashes: ["invalid1", "invalid2"], + }) + .expect(HttpStatus.BAD_REQUEST); + + expect(result.body).toStrictEqual({ + error: "Bad Request", + statusCode: HttpStatus.BAD_REQUEST, + message: [ + "limit must be a positive number", + "poll must not be less than 0", + "maciContractAddress must be an Ethereum address", + "IPFS hash is invalid", + "Public key (invalid) is invalid", + ], + }); + }); + + test("should get message batches properly", async () => { + const result = await request(app.getHttpServer() as App) + .get("/v1/messageBatches/get") + .send({ + limit: 10, + poll: 0, + maciContractAddress, + publicKey: user!.pubKey.serialize(), + }) + .expect(HttpStatus.OK); + + expect(result.status).toBe(HttpStatus.OK); + }); + }); +}); diff --git a/apps/relayer/tests/messages.test.ts b/apps/relayer/tests/messages.test.ts index 1b5b3c2ec..6a4b824de 100644 --- a/apps/relayer/tests/messages.test.ts +++ b/apps/relayer/tests/messages.test.ts @@ -1,9 +1,7 @@ import { jest } from "@jest/globals"; import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common"; import { Test } from "@nestjs/testing"; -import hardhat from "hardhat"; -import { deploy, deployPoll, deployVkRegistryContract, joinPoll, setVerifyingKeys, signup } from "maci-cli"; -import { formatProofForVerifierContract, genMaciStateFromContract } from "maci-contracts"; +import { formatProofForVerifierContract } from "maci-contracts"; import { Keypair } from "maci-domainobjs"; import { genProofSnarkjs } from "maci-sdk"; import request from "supertest"; @@ -12,18 +10,8 @@ import type { App } from "supertest/types"; import { AppModule } from "../ts/app.module"; -import { - INT_STATE_TREE_DEPTH, - MESSAGE_BATCH_SIZE, - STATE_TREE_DEPTH, - VOTE_OPTION_TREE_DEPTH, - pollJoinedZkey, - pollJoiningZkey, - processMessagesZkeyPathNonQv, - tallyVotesZkeyPathNonQv, - pollWasm, - pollJoinedWasm, -} from "./constants"; +import { pollJoinedWasm, pollJoinedZkey } from "./constants"; +import { TestDeploy, waitForInitialization } from "./deploy"; jest.unmock("maci-contracts/typechain-types"); @@ -33,74 +21,22 @@ describe("Integration messages", () => { let stateLeafIndex: number; let maciContractAddress: string; - const coordinatorKeypair = new Keypair(); - const user = new Keypair(); - beforeAll(async () => { - const [signer] = await hardhat.ethers.getSigners(); - - const vkRegistry = await deployVkRegistryContract({ signer }); - await setVerifyingKeys({ - quiet: true, - vkRegistry, - stateTreeDepth: STATE_TREE_DEPTH, - intStateTreeDepth: INT_STATE_TREE_DEPTH, - voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, - messageBatchSize: MESSAGE_BATCH_SIZE, - processMessagesZkeyPathNonQv, - tallyVotesZkeyPathNonQv, - pollJoiningZkeyPath: pollJoiningZkey, - pollJoinedZkeyPath: pollJoinedZkey, - useQuadraticVoting: false, - signer, - }); - - const maciAddresses = await deploy({ stateTreeDepth: 10, signer }); - - maciContractAddress = maciAddresses.maciAddress; - - await deployPoll({ - pollDuration: 30, - intStateTreeDepth: INT_STATE_TREE_DEPTH, - messageBatchSize: MESSAGE_BATCH_SIZE, - voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH, - coordinatorPubkey: coordinatorKeypair.pubKey.serialize(), - useQuadraticVoting: false, - signer, - }); - - await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer }); - - const { pollStateIndex, timestamp, voiceCredits } = await joinPoll({ - maciAddress: maciAddresses.maciAddress, - pollId: 0n, - privateKey: user.privKey.serialize(), - stateIndex: 1n, - pollJoiningZkey, - pollWasm, - signer, - useWasm: true, - quiet: true, - }); - - const maciState = await genMaciStateFromContract( - signer.provider, - maciAddresses.maciAddress, - coordinatorKeypair, - 0n, - ); + await waitForInitialization(); + const testDeploy = await TestDeploy.getInstance(); + const poll = testDeploy.contractsData.maciState!.polls.get(0n); - const poll = maciState.polls.get(0n); + poll!.updatePoll(BigInt(testDeploy.contractsData.maciState!.pubKeys.length)); - poll!.updatePoll(BigInt(maciState.pubKeys.length)); + stateLeafIndex = Number(testDeploy.contractsData.stateLeafIndex); - stateLeafIndex = Number(pollStateIndex); + maciContractAddress = testDeploy.contractsData.maciContractAddress!; circuitInputs = poll!.joinedCircuitInputs({ - maciPrivKey: user.privKey, - stateLeafIndex: BigInt(pollStateIndex), - voiceCreditsBalance: BigInt(voiceCredits), - joinTimestamp: BigInt(timestamp), + maciPrivKey: testDeploy.contractsData.user!.privKey, + stateLeafIndex: BigInt(testDeploy.contractsData.stateLeafIndex!), + voiceCreditsBalance: BigInt(testDeploy.contractsData.voiceCredits!), + joinTimestamp: BigInt(testDeploy.contractsData.timestamp!), }) as unknown as typeof circuitInputs; const moduleFixture = await Test.createTestingModule({ @@ -109,7 +45,7 @@ describe("Integration messages", () => { app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ transform: true })); - await app.listen(3001); + await app.listen(3002); }); afterAll(async () => { diff --git a/apps/relayer/ts/jest/setup.ts b/apps/relayer/ts/jest/setup.ts index bcc226d41..7dde9c117 100644 --- a/apps/relayer/ts/jest/setup.ts +++ b/apps/relayer/ts/jest/setup.ts @@ -1,6 +1,10 @@ +import dotenv from "dotenv"; + import type { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; import type { ethers } from "ethers"; +dotenv.config(); + declare module "hardhat/types/runtime" { interface HardhatRuntimeEnvironment { // We omit the ethers field because it is redundant. diff --git a/apps/relayer/ts/messageBatch/__tests__/messageBatch.controller.test.ts b/apps/relayer/ts/messageBatch/__tests__/messageBatch.controller.test.ts new file mode 100644 index 000000000..c938013a8 --- /dev/null +++ b/apps/relayer/ts/messageBatch/__tests__/messageBatch.controller.test.ts @@ -0,0 +1,55 @@ +import { jest } from "@jest/globals"; +import { HttpException, HttpStatus } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; + +import { MessageBatchController } from "../messageBatch.controller"; +import { MessageBatchService } from "../messageBatch.service"; + +import { defaultGetMessageBatchesDto, defaultMessageBatches } from "./utils"; + +describe("MessageBatchController", () => { + let controller: MessageBatchController; + + const mockMessageBatchService = { + findMessageBatches: jest.fn(), + }; + + beforeEach(async () => { + const app = await Test.createTestingModule({ + controllers: [MessageBatchController], + }) + .useMocker((token) => { + if (token === MessageBatchService) { + mockMessageBatchService.findMessageBatches.mockImplementation(() => Promise.resolve(defaultMessageBatches)); + + return mockMessageBatchService; + } + + return jest.fn(); + }) + .compile(); + + controller = app.get(MessageBatchController); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("v1/messageBatches/get", () => { + test("should get message batches properly", async () => { + const data = await controller.get(defaultGetMessageBatchesDto); + + expect(data).toStrictEqual(defaultMessageBatches); + }); + + test("should throw an error if fetching is failed", async () => { + const error = new Error("error"); + mockMessageBatchService.findMessageBatches.mockImplementation(() => Promise.reject(error)); + + await expect(controller.get(defaultGetMessageBatchesDto)).rejects.toThrow( + new HttpException(error.message, HttpStatus.BAD_REQUEST), + ); + }); + }); +}); diff --git a/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts b/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts index dc6140d46..064600a05 100644 --- a/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts +++ b/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts @@ -25,6 +25,7 @@ describe("MessageBatchService", () => { const mockRepository = { create: jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches)), + find: jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches)), }; const mockMaciContract = { @@ -40,6 +41,7 @@ describe("MessageBatchService", () => { beforeEach(() => { mockRepository.create = jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches)); + mockRepository.find = jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches)); mockIpfsService.add = jest.fn().mockImplementation(() => Promise.resolve(defaultIpfsHash)); MACIFactory.connect = jest.fn().mockImplementation(() => mockMaciContract) as typeof MACIFactory.connect; @@ -50,18 +52,33 @@ describe("MessageBatchService", () => { jest.clearAllMocks(); }); - test("should save message batches properly", async () => { + test("should save and find message batches properly", async () => { const service = new MessageBatchService( mockIpfsService as unknown as IpfsService, mockRepository as unknown as MessageBatchRepository, ); const result = await service.saveMessageBatches(defaultMessageBatches); + const messageBatches = await service.findMessageBatches({}); expect(result).toStrictEqual(defaultMessageBatches); + expect(messageBatches).toStrictEqual(defaultMessageBatches); expect(mockPollContract.relayMessagesBatch).toHaveBeenCalledTimes(1); }); + test("should throw an error if can't find message batches", async () => { + const error = new Error("error"); + + (mockRepository.find as jest.Mock).mockImplementation(() => Promise.reject(error)); + + const service = new MessageBatchService( + mockIpfsService as unknown as IpfsService, + mockRepository as unknown as MessageBatchRepository, + ); + + await expect(service.findMessageBatches({})).rejects.toThrow(error); + }); + test("should throw an error if can't save message batches", async () => { const error = new Error("error"); diff --git a/apps/relayer/ts/messageBatch/__tests__/utils.ts b/apps/relayer/ts/messageBatch/__tests__/utils.ts index 76b77c40e..ab66e92f0 100644 --- a/apps/relayer/ts/messageBatch/__tests__/utils.ts +++ b/apps/relayer/ts/messageBatch/__tests__/utils.ts @@ -1,7 +1,7 @@ import { ZeroAddress } from "ethers"; import { Keypair } from "maci-domainobjs"; -import { MessageBatchDto } from "../messageBatch.dto"; +import { GetMessageBatchesDto, MAX_MESSAGES, MessageBatchDto } from "../messageBatch.dto"; const keypair = new Keypair(); @@ -19,3 +19,10 @@ defaultMessageBatch.messages = [ defaultMessageBatch.ipfsHash = defaultIpfsHash; export const defaultMessageBatches: MessageBatchDto[] = [defaultMessageBatch]; + +export const defaultGetMessageBatchesDto = new GetMessageBatchesDto(); +defaultGetMessageBatchesDto.limit = MAX_MESSAGES; +defaultGetMessageBatchesDto.maciContractAddress = ZeroAddress; +defaultGetMessageBatchesDto.poll = 0; +defaultGetMessageBatchesDto.ipfsHashes = [defaultIpfsHash]; +defaultGetMessageBatchesDto.publicKey = keypair.pubKey.serialize(); diff --git a/apps/relayer/ts/messageBatch/__tests__/validation.test.ts b/apps/relayer/ts/messageBatch/__tests__/validation.test.ts index 15d306012..9f98909b7 100644 --- a/apps/relayer/ts/messageBatch/__tests__/validation.test.ts +++ b/apps/relayer/ts/messageBatch/__tests__/validation.test.ts @@ -24,6 +24,6 @@ describe("IpfsHashValidator", () => { const result = validator.defaultMessage(); - expect(result).toBe("IPFS hash ($value) is invalid"); + expect(result).toBe("IPFS hash is invalid"); }); }); diff --git a/apps/relayer/ts/messageBatch/messageBatch.controller.ts b/apps/relayer/ts/messageBatch/messageBatch.controller.ts new file mode 100644 index 000000000..8f932425c --- /dev/null +++ b/apps/relayer/ts/messageBatch/messageBatch.controller.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import { Body, Controller, Get, HttpException, HttpStatus, Logger } from "@nestjs/common"; +import { ApiBody, ApiResponse, ApiTags } from "@nestjs/swagger"; +import set from "lodash/set"; + +import { GetMessageBatchesDto } from "./messageBatch.dto"; +import { MessageBatch } from "./messageBatch.schema"; +import { MessageBatchService } from "./messageBatch.service"; + +@ApiTags("v1/messageBatches") +@Controller("v1/messageBatches") +export class MessageBatchController { + /** + * Logger + */ + private readonly logger = new Logger(MessageBatchController.name); + + /** + * Initialize MessageBatchController + * + * @param messageBatchService message batch service + */ + constructor(private readonly messageBatchService: MessageBatchService) {} + + /** + * Fetch message batches api method. + * + * @param args fetch arguments + * @returns message batches + */ + @ApiBody({ type: GetMessageBatchesDto }) + @ApiResponse({ status: HttpStatus.OK, description: "The message batches have been successfully returned" }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" }) + @Get("get") + async get(@Body() args: GetMessageBatchesDto): Promise { + const { ipfsHashes, poll, maciContractAddress, publicKey, limit } = args; + + const filter = { + poll: { $eq: poll }, + maciContractAddress: { $eq: maciContractAddress }, + }; + + if (ipfsHashes) { + set(filter, "ipfsHash.$in", ipfsHashes); + } + + if (publicKey) { + set(filter, "messages.$elemMatch.publicKey.$eq", publicKey); + } + + return this.messageBatchService.findMessageBatches(filter, limit).catch((error: Error) => { + this.logger.error(`Error:`, error); + throw new HttpException(error.message, HttpStatus.BAD_REQUEST); + }); + } +} diff --git a/apps/relayer/ts/messageBatch/messageBatch.dto.ts b/apps/relayer/ts/messageBatch/messageBatch.dto.ts index 4ad3fdcd7..81c27316d 100644 --- a/apps/relayer/ts/messageBatch/messageBatch.dto.ts +++ b/apps/relayer/ts/messageBatch/messageBatch.dto.ts @@ -1,14 +1,28 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, ArrayMinSize, ArrayMaxSize, ValidateNested, Validate } from "class-validator"; +import { + IsArray, + ArrayMinSize, + ArrayMaxSize, + ValidateNested, + Validate, + IsInt, + IsPositive, + Max, + IsEthereumAddress, + Min, + IsOptional, + IsString, +} from "class-validator"; import { Message } from "../message/message.schema"; +import { PublicKeyValidator } from "../message/validation"; import { IpfsHashValidator } from "./validation"; /** * Max messages per batch */ -const MAX_MESSAGES = 100; +export const MAX_MESSAGES = Number(process.env.MAX_MESSAGES); /** * Data transfer object for message batch @@ -37,3 +51,70 @@ export class MessageBatchDto { @Validate(IpfsHashValidator) ipfsHash!: string; } + +/** + * Data transfer object for getting message batches + */ +export class GetMessageBatchesDto { + /** + * Limit + */ + @ApiProperty({ + description: "Limit", + minimum: 1, + maximum: MAX_MESSAGES, + type: Number, + }) + @IsInt() + @IsPositive() + @Max(MAX_MESSAGES) + limit!: number; + + /** + * 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; + + /** + * IPFS hashes + */ + @ApiProperty({ + description: "IPFS hashes", + type: [String], + }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + @ArrayMinSize(1) + @ArrayMaxSize(MAX_MESSAGES) + @Validate(IpfsHashValidator, { each: true }) + ipfsHashes?: string[]; + + /** + * Public key + */ + @ApiProperty({ + description: "Public key", + type: String, + }) + @IsOptional() + @Validate(PublicKeyValidator) + publicKey?: string; +} diff --git a/apps/relayer/ts/messageBatch/messageBatch.module.ts b/apps/relayer/ts/messageBatch/messageBatch.module.ts index c60ce773a..2762a134b 100644 --- a/apps/relayer/ts/messageBatch/messageBatch.module.ts +++ b/apps/relayer/ts/messageBatch/messageBatch.module.ts @@ -3,6 +3,7 @@ import { MongooseModule } from "@nestjs/mongoose"; import { IpfsModule } from "../ipfs/ipfs.module"; +import { MessageBatchController } from "./messageBatch.controller"; import { MessageBatchRepository } from "./messageBatch.repository"; import { MessageBatch, MessageBatchSchema } from "./messageBatch.schema"; import { MessageBatchService } from "./messageBatch.service"; @@ -10,6 +11,7 @@ import { MessageBatchService } from "./messageBatch.service"; @Module({ imports: [MongooseModule.forFeature([{ name: MessageBatch.name, schema: MessageBatchSchema }]), IpfsModule], exports: [MessageBatchService], + controllers: [MessageBatchController], providers: [MessageBatchService, MessageBatchRepository], }) export class MessageBatchModule {} diff --git a/apps/relayer/ts/messageBatch/messageBatch.repository.ts b/apps/relayer/ts/messageBatch/messageBatch.repository.ts index b2c2ed389..6ccdee824 100644 --- a/apps/relayer/ts/messageBatch/messageBatch.repository.ts +++ b/apps/relayer/ts/messageBatch/messageBatch.repository.ts @@ -40,6 +40,7 @@ export class MessageBatchRepository { * Find message batches with filter query * * @param filter filter query + * @param limit limit * @returns message batches */ async find(filter: RootFilterQuery, limit = MESSAGE_BATCHES_LIMIT): Promise { diff --git a/apps/relayer/ts/messageBatch/messageBatch.service.ts b/apps/relayer/ts/messageBatch/messageBatch.service.ts index 38c543719..2540becb1 100644 --- a/apps/relayer/ts/messageBatch/messageBatch.service.ts +++ b/apps/relayer/ts/messageBatch/messageBatch.service.ts @@ -5,12 +5,13 @@ import uniqBy from "lodash/uniqBy"; import { PubKey } from "maci-domainobjs"; import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-sdk"; -import type { MessageBatchDto } from "./messageBatch.dto"; +import type { MessageBatch } from "./messageBatch.schema"; +import type { RootFilterQuery } from "mongoose"; import { IpfsService } from "../ipfs/ipfs.service"; +import { MAX_MESSAGES, type MessageBatchDto } from "./messageBatch.dto"; import { MessageBatchRepository } from "./messageBatch.repository"; -import { MessageBatch } from "./messageBatch.schema"; /** * MessageBatchService is responsible for saving message batches and send them to ipfs @@ -33,6 +34,19 @@ export class MessageBatchService { private readonly messageBatchRepository: MessageBatchRepository, ) {} + /** + * Find message batches + * + * @param filter filter query + * @param limit limit + */ + async findMessageBatches(filter: RootFilterQuery, limit = MAX_MESSAGES): Promise { + return this.messageBatchRepository.find(filter, limit).catch((error) => { + this.logger.error(`Find message batches error:`, error); + throw error; + }); + } + /** * Save messages batch * diff --git a/apps/relayer/ts/messageBatch/validation.ts b/apps/relayer/ts/messageBatch/validation.ts index 08391abbb..83d1a7b3f 100644 --- a/apps/relayer/ts/messageBatch/validation.ts +++ b/apps/relayer/ts/messageBatch/validation.ts @@ -26,6 +26,6 @@ export class IpfsHashValidator implements ValidatorConstraintInterface { * @returns default validation message */ defaultMessage(): string { - return "IPFS hash ($value) is invalid"; + return "IPFS hash is invalid"; } }