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(coordinator): deploy subgraph with websockets #1651

Merged
merged 1 commit into from
Jul 16, 2024
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
177 changes: 174 additions & 3 deletions coordinator/tests/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ import path from "path";
import type { App } from "supertest/types";

import { AppModule } from "../ts/app.module";
import { ErrorCodes } from "../ts/common";
import { ErrorCodes, ESupportedNetworks } from "../ts/common";
import { CryptoService } from "../ts/crypto/crypto.service";
import { EProofGenerationEvents } from "../ts/events/types";
import { FileModule } from "../ts/file/file.module";
import { IGenerateArgs } from "../ts/proof/types";
import { EProofGenerationEvents, IGenerateArgs } from "../ts/proof/types";
import { ESubgraphEvents, IDeploySubgraphArgs } from "../ts/subgraph/types";

const STATE_TREE_DEPTH = 10;
const INT_STATE_TREE_DEPTH = 1;
Expand Down Expand Up @@ -341,6 +341,177 @@ describe("e2e", () => {
});
});

describe("validation /v1/subgraph/deploy POST", () => {
test("should throw an error if network is invalid", async () => {
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
.post("/v1/subgraph/deploy")
.set("Authorization", encryptedHeader)
.send({
network: "unknown",
maciContractAddress: maciAddresses.maciAddress,
startBlock: 0,
name: "subgraph",
tag: "v0.0.1",
})
.expect(400);

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: [`network must be one of the following values: ${Object.values(ESupportedNetworks).join(", ")}`],
});
});

test("should throw an error if network is invalid (ws)", async () => {
const args: IDeploySubgraphArgs = {
network: "unknown" as ESupportedNetworks,
maciContractAddress: maciAddresses.maciAddress,
startBlock: 0,
name: "subgraph",
tag: "v0.0.1",
};

const result = await new Promise<{ network?: string }>((resolve) => {
socket.emit(ESubgraphEvents.START, args).on(ESubgraphEvents.ERROR, (errors: ValidationError[]) => {
const error = errors[0]?.constraints;

resolve({ network: error?.isEnum });
});
});

expect(result.network).toBe(
`network must be one of the following values: ${Object.values(ESupportedNetworks).join(", ")}`,
);
});

test("should throw an error if maci contract is invalid", async () => {
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
.post("/v1/subgraph/deploy")
.set("Authorization", encryptedHeader)
.send({
network: ESupportedNetworks.OPTIMISM_SEPOLIA,
maciContractAddress: "unknown",
startBlock: 0,
name: "subgraph",
tag: "v0.0.1",
})
.expect(400);

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: ["maciContractAddress must be an Ethereum address"],
});
});

test("should throw an error if maci contract is invalid (ws)", async () => {
const args: IDeploySubgraphArgs = {
network: ESupportedNetworks.OPTIMISM_SEPOLIA,
maciContractAddress: "unknown",
startBlock: 0,
name: "subgraph",
tag: "v0.0.1",
};

const result = await new Promise<{ contract?: string }>((resolve) => {
socket.emit(ESubgraphEvents.START, args).on(ESubgraphEvents.ERROR, (errors: ValidationError[]) => {
const error = errors[0]?.constraints;

resolve({ contract: error?.isEthereumAddress });
});
});

expect(result.contract).toBe("maciContractAddress must be an Ethereum address");
});

test("should throw an error if tag is invalid", async () => {
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
.post("/v1/subgraph/deploy")
.set("Authorization", encryptedHeader)
.send({
network: ESupportedNetworks.OPTIMISM_SEPOLIA,
maciContractAddress: maciAddresses.maciAddress,
startBlock: 0,
name: "subgraph",
tag: "unknown",
})
.expect(400);

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: ["tag must match /^v\\d+\\.\\d+\\.\\d+$/ regular expression"],
});
});

test("should throw an error if tag is invalid (ws)", async () => {
const args: IDeploySubgraphArgs = {
network: ESupportedNetworks.OPTIMISM_SEPOLIA,
maciContractAddress: maciAddresses.maciAddress,
startBlock: 0,
name: "subgraph",
tag: "unknown",
};

const result = await new Promise<{ tag?: string }>((resolve) => {
socket.emit(ESubgraphEvents.START, args).on(ESubgraphEvents.ERROR, (errors: ValidationError[]) => {
const error = errors[0]?.constraints;

resolve({ tag: error?.matches });
});
});

expect(result.tag).toBe("tag must match /^v\\d+\\.\\d+\\.\\d+$/ regular expression");
});
});

describe("/v1/subgraph/deploy POST", () => {
test("should throw an error if there is no authorization header", async () => {
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.send({
network: ESupportedNetworks.OPTIMISM_SEPOLIA,
maciContractAddress: maciAddresses.maciAddress,
startBlock: 0,
name: "subgraph",
tag: "v0.0.1",
})
.expect(403);

expect(result.body).toStrictEqual({
error: "Forbidden",
message: "Forbidden resource",
statusCode: HttpStatus.FORBIDDEN,
});
});

test("should throw an error if there is no authorization header (ws)", async () => {
const args: IDeploySubgraphArgs = {
network: ESupportedNetworks.OPTIMISM_SEPOLIA,
maciContractAddress: maciAddresses.maciAddress,
startBlock: 0,
name: "subgraph",
tag: "v0.0.1",
};

const unauthorizedSocket = io(await app.getUrl());

const result = await new Promise<Error>((resolve) => {
unauthorizedSocket.emit(ESubgraphEvents.START, args).on(ESubgraphEvents.ERROR, (error: Error) => {
resolve(error);
});
}).finally(() => unauthorizedSocket.disconnect());

expect(result.message).toBe("Forbidden resource");
});
});

describe("/v1/proof/publicKey GET", () => {
test("should get public key properly", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
Expand Down
2 changes: 0 additions & 2 deletions coordinator/ts/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Module } from "@nestjs/common";
import { ThrottlerModule } from "@nestjs/throttler";

import { CryptoModule } from "./crypto/crypto.module";
import { EventsModule } from "./events/events.module";
import { FileModule } from "./file/file.module";
import { ProofModule } from "./proof/proof.module";
import { SubgraphModule } from "./subgraph/subgraph.module";
Expand All @@ -17,7 +16,6 @@ import { SubgraphModule } from "./subgraph/subgraph.module";
]),
FileModule,
CryptoModule,
EventsModule,
SubgraphModule,
ProofModule,
],
Expand Down
13 changes: 0 additions & 13 deletions coordinator/ts/events/events.module.ts

This file was deleted.

9 changes: 0 additions & 9 deletions coordinator/ts/events/types.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { Test } from "@nestjs/testing";
import { IGenerateProofsOptions } from "maci-contracts";
import { Server } from "socket.io";

import type { IGenerateArgs, IGenerateData } from "../../proof/types";
import type { IGenerateArgs, IGenerateData } from "../types";
import type { TallyData } from "maci-cli";

import { ProofGeneratorService } from "../../proof/proof.service";
import { EventsGateway } from "../events.gateway";
import { ProofGateway } from "../proof.gateway";
import { ProofGeneratorService } from "../proof.service";
import { EProofGenerationEvents } from "../types";

describe("EventsGateway", () => {
let gateway: EventsGateway;
describe("ProofGateway", () => {
let gateway: ProofGateway;

const defaultProofGeneratorArgs: IGenerateArgs = {
poll: 0,
Expand All @@ -34,7 +34,7 @@ describe("EventsGateway", () => {
const mockEmit = jest.fn();

beforeEach(async () => {
const testModule = await Test.createTestingModule({ providers: [EventsGateway] })
const testModule = await Test.createTestingModule({ providers: [ProofGateway] })
.useMocker((token) => {
if (token === ProofGeneratorService) {
mockGeneratorService.generate.mockImplementation((_, options?: IGenerateProofsOptions) => {
Expand All @@ -53,7 +53,7 @@ describe("EventsGateway", () => {
})
.compile();

gateway = testModule.get<EventsGateway>(EventsGateway);
gateway = testModule.get<ProofGateway>(ProofGateway);

gateway.server = { emit: mockEmit } as unknown as Server;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@ import { IGenerateProofsBatchData, type Proof, type TallyData } from "maci-contr
import type { Server } from "socket.io";

import { AccountSignatureGuard } from "../auth/AccountSignatureGuard.service";
import { GenerateProofDto } from "../proof/dto";
import { ProofGeneratorService } from "../proof/proof.service";

import { GenerateProofDto } from "./dto";
import { ProofGeneratorService } from "./proof.service";
import { EProofGenerationEvents } from "./types";

/**
* ProofGateway is responsible for websockets integration between client and ProofGeneratorService.
*/
@WebSocketGateway({
cors: {
origin: process.env.COORDINATOR_ALLOWED_ORIGINS?.split(","),
},
})
@UseGuards(AccountSignatureGuard)
export class EventsGateway {
export class ProofGateway {
/**
* Logger
*/
private readonly logger = new Logger(EventsGateway.name);
private readonly logger = new Logger(ProofGateway.name);

/**
* Websocket server
Expand All @@ -29,7 +32,7 @@ export class EventsGateway {
server!: Server;

/**
* Initialize EventsGateway
* Initialize ProofGateway
*
* @param proofGeneratorService - proof generator service
*/
Expand Down
3 changes: 2 additions & 1 deletion coordinator/ts/proof/proof.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { CryptoModule } from "../crypto/crypto.module";
import { FileModule } from "../file/file.module";

import { ProofController } from "./proof.controller";
import { ProofGateway } from "./proof.gateway";
import { ProofGeneratorService } from "./proof.service";

@Module({
imports: [FileModule, CryptoModule],
controllers: [ProofController],
providers: [ProofGeneratorService],
providers: [ProofGeneratorService, ProofGateway],
})
export class ProofModule {}
10 changes: 10 additions & 0 deletions coordinator/ts/proof/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import type { TallyData } from "maci-cli";
import type { Proof } from "maci-contracts";

/**
* WS events for proof generation
*/
export enum EProofGenerationEvents {
START = "start-generation",
PROGRESS = "progress-generation",
FINISH = "finish-generation",
ERROR = "exception",
}

/**
* Interface that represents generate proofs arguments
*/
Expand Down
Loading
Loading