From 1aa4ea318c0cc5f6fdadc12c59357cf4bb9b177e Mon Sep 17 00:00:00 2001 From: Anton <14254374+0xmad@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:18:55 -0600 Subject: [PATCH] feat(relayer): relay messages onchain --- .github/workflows/relayer-build.yml | 2 + apps/relayer/package.json | 2 + .../__tests__/messageBatch.service.test.ts | 26 ++++++++++++ .../ts/messageBatch/messageBatch.service.ts | 40 ++++++++++++++++--- pnpm-lock.yaml | 11 +++++ 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/.github/workflows/relayer-build.yml b/.github/workflows/relayer-build.yml index 6a5c76490..9997ea183 100644 --- a/.github/workflows/relayer-build.yml +++ b/.github/workflows/relayer-build.yml @@ -9,10 +9,12 @@ env: RELAYER_RPC_URL: "http://localhost:8545" TTL: ${{ vars.RELAYER_TTL }} LIMIT: ${{ vars.RELAYER_LIMIT }} + ALLOWED_ORIGINS: ${{ vars.ALLOWED_ORIGINS }} MONGO_DB_URI: ${{ secrets.RELAYER_MONGO_DB_URI }} MONGODB_USER: ${{ secrets.MONGODB_USER }} MONGODB_PASSWORD: ${{ secrets.MONGODB_PASSWORD }} MONGODB_DATABASE: ${{ secrets.MONGODB_DATABASE }} + MNEMONIC: ${{ secrets.RELAYER_MNEMONIC }} concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/apps/relayer/package.json b/apps/relayer/package.json index c450c3985..8d6b0c490 100644 --- a/apps/relayer/package.json +++ b/apps/relayer/package.json @@ -44,6 +44,7 @@ "hardhat": "^2.22.18", "helia": "^5.2.0", "helmet": "^8.0.0", + "lodash": "^4.17.21", "maci-contracts": "workspace:^2.5.0", "maci-domainobjs": "workspace:^2.5.0", "mongoose": "^8.9.5", @@ -60,6 +61,7 @@ "@nestjs/testing": "^11.0.3", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", + "@types/lodash": "^4.17.14", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", "fast-check": "^3.23.2", diff --git a/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts b/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts index 7469abe92..2b9ebe2ab 100644 --- a/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts +++ b/apps/relayer/ts/messageBatch/__tests__/messageBatch.service.test.ts @@ -1,4 +1,6 @@ import { jest } from "@jest/globals"; +import { ZeroAddress } from "ethers"; +import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-contracts"; import { IpfsService } from "../../ipfs/ipfs.service"; import { MessageBatchDto } from "../dto"; @@ -7,6 +9,15 @@ import { MessageBatchService } from "../messageBatch.service"; import { defaultIpfsHash, defaultMessageBatches } from "./utils"; +jest.mock("maci-contracts", (): unknown => ({ + MACI__factory: { + connect: jest.fn(), + }, + Poll__factory: { + connect: jest.fn(), + }, +})); + describe("MessageBatchService", () => { const mockIpfsService = { add: jest.fn().mockImplementation(() => Promise.resolve(defaultIpfsHash)), @@ -16,9 +27,23 @@ describe("MessageBatchService", () => { create: jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches)), }; + const mockMaciContract = { + polls: jest.fn().mockImplementation(() => Promise.resolve({ poll: ZeroAddress })), + }; + + const mockPollContract = { + hashMessageAndEncPubKey: jest.fn().mockImplementation(() => Promise.resolve("hash")), + relayMessagesBatch: jest + .fn() + .mockImplementation(() => Promise.resolve({ wait: jest.fn().mockImplementation(() => Promise.resolve()) })), + }; + beforeEach(() => { mockRepository.create = jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches)); mockIpfsService.add = jest.fn().mockImplementation(() => Promise.resolve(defaultIpfsHash)); + + MACIFactory.connect = jest.fn().mockImplementation(() => mockMaciContract) as typeof MACIFactory.connect; + PollFactory.connect = jest.fn().mockImplementation(() => mockPollContract) as typeof PollFactory.connect; }); afterEach(() => { @@ -34,6 +59,7 @@ describe("MessageBatchService", () => { const result = await service.saveMessageBatches(defaultMessageBatches); expect(result).toStrictEqual(defaultMessageBatches); + expect(mockPollContract.relayMessagesBatch).toHaveBeenCalledTimes(1); }); test("should throw an error if can't save message batches", async () => { diff --git a/apps/relayer/ts/messageBatch/messageBatch.service.ts b/apps/relayer/ts/messageBatch/messageBatch.service.ts index ba4cdc053..64aa635ee 100644 --- a/apps/relayer/ts/messageBatch/messageBatch.service.ts +++ b/apps/relayer/ts/messageBatch/messageBatch.service.ts @@ -1,5 +1,9 @@ import { Injectable, Logger } from "@nestjs/common"; import { validate } from "class-validator"; +import flatten from "lodash/flatten"; +import uniqBy from "lodash/uniqBy"; +import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-contracts"; +import { PubKey } from "maci-domainobjs"; import type { MessageBatchDto } from "./dto"; @@ -49,14 +53,40 @@ export class MessageBatchService { throw new Error("Validation error"); } - const ipfsHash = await this.ipfsService.add(args.map(({ messages }) => messages)).catch((error) => { + const allMessages = flatten(args.map((item) => item.messages)); + + const ipfsHash = await this.ipfsService.add(allMessages).catch((error) => { this.logger.error(`Upload message batches to ipfs error:`, error); throw error; }); - return this.messageBatchRepository.create(args.map(({ messages }) => ({ messages, ipfsHash }))).catch((error) => { - this.logger.error(`Save message batch error:`, error); - throw error; - }); + const messageBatches = await this.messageBatchRepository + .create(args.map(({ messages }) => ({ messages, ipfsHash }))) + .catch((error) => { + this.logger.error(`Save message batch error:`, error); + throw error; + }); + + const [{ maciAddress, pollId }] = uniqBy( + allMessages.map(({ maciContractAddress, poll }) => ({ + maciAddress: maciContractAddress, + pollId: poll, + })), + "maciContractAddress", + ); + + const maciContract = MACIFactory.connect(maciAddress); + const pollAddresses = await maciContract.polls(pollId); + const pollContract = PollFactory.connect(pollAddresses.poll); + + const messageHashes = await Promise.all( + allMessages.map(({ data, publicKey }) => + pollContract.hashMessageAndEncPubKey({ data }, PubKey.deserialize(publicKey).asContractParam()), + ), + ); + + await pollContract.relayMessagesBatch(messageHashes, ipfsHash).then((tx) => tx.wait()); + + return messageBatches; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bae5daf0..8f279b93d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,6 +158,9 @@ importers: helmet: specifier: ^8.0.0 version: 8.0.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 maci-contracts: specifier: workspace:^2.5.0 version: link:../../packages/contracts @@ -201,6 +204,9 @@ importers: '@types/jest': specifier: ^29.5.2 version: 29.5.14 + '@types/lodash': + specifier: ^4.17.14 + version: 4.17.14 '@types/node': specifier: ^22.10.7 version: 22.10.7 @@ -4558,6 +4564,9 @@ packages: '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/lodash@4.17.14': + resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==} + '@types/lodash@4.17.4': resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==} @@ -20980,6 +20989,8 @@ snapshots: '@types/katex@0.16.7': {} + '@types/lodash@4.17.14': {} + '@types/lodash@4.17.4': {} '@types/long@4.0.2': {}