From d04d82cb4a3e022471726f87b24b2ee14b8ae0a2 Mon Sep 17 00:00:00 2001 From: Anton <14254374+0xmad@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:10:12 -0500 Subject: [PATCH] feat(coordinator): deploy subgraph from coordinator service - [x] Add subgraph controller - [x] Add subgraph deploy service - [x] Remove app controller and use proof controller - [x] Add env variables --- .github/workflows/coordinator-build.yml | 4 + .gitignore | 2 + .../utils/processMessagesInputHasher.circom | 2 +- coordinator/.env.example | 14 + coordinator/README.md | 30 ++ coordinator/package.json | 2 + coordinator/tests/app.test.ts | 2 +- coordinator/ts/app.module.ts | 8 +- coordinator/ts/common/errors.ts | 1 + coordinator/ts/common/index.ts | 1 + coordinator/ts/common/networks.ts | 64 +++++ .../__tests__/proof.controller.test.ts} | 28 +- .../proof.controller.ts} | 19 +- coordinator/ts/proof/proof.module.ts | 14 + .../__tests__/subgraph.controller.test.ts | 66 +++++ .../__tests__/subgraph.service.test.ts | 90 ++++++ coordinator/ts/subgraph/dto.ts | 68 +++++ .../ts/subgraph/subgraph.controller.ts | 43 +++ coordinator/ts/subgraph/subgraph.module.ts | 14 + coordinator/ts/subgraph/subgraph.service.ts | 104 +++++++ coordinator/ts/subgraph/types.ts | 41 +++ pnpm-lock.yaml | 263 ++---------------- subgraph/.gitignore | 2 - .../{optimism-sepolia.json => network.json} | 0 subgraph/config/scroll-sepolia.json | 5 - subgraph/package.json | 2 +- website/static/img/offlineProcessing.svg | 2 +- 27 files changed, 613 insertions(+), 278 deletions(-) create mode 100644 coordinator/ts/common/networks.ts rename coordinator/ts/{app.controller.test.ts => proof/__tests__/proof.controller.test.ts} (77%) rename coordinator/ts/{app.controller.ts => proof/proof.controller.ts} (79%) create mode 100644 coordinator/ts/proof/proof.module.ts create mode 100644 coordinator/ts/subgraph/__tests__/subgraph.controller.test.ts create mode 100644 coordinator/ts/subgraph/__tests__/subgraph.service.test.ts create mode 100644 coordinator/ts/subgraph/dto.ts create mode 100644 coordinator/ts/subgraph/subgraph.controller.ts create mode 100644 coordinator/ts/subgraph/subgraph.module.ts create mode 100644 coordinator/ts/subgraph/subgraph.service.ts create mode 100644 coordinator/ts/subgraph/types.ts rename subgraph/config/{optimism-sepolia.json => network.json} (100%) delete mode 100644 subgraph/config/scroll-sepolia.json diff --git a/.github/workflows/coordinator-build.yml b/.github/workflows/coordinator-build.yml index cb525c6e78..1e11ef4800 100644 --- a/.github/workflows/coordinator-build.yml +++ b/.github/workflows/coordinator-build.yml @@ -13,6 +13,10 @@ env: COORDINATOR_MESSAGE_PROCESS_ZKEY_NAME: "ProcessMessages_10-2-1-2_test" COORDINATOR_ZKEY_PATH: "./zkeys/" COORDINATOR_RAPIDSNARK_EXE: "~/rapidsnark/build/prover" + SUBGRAPH_FOLDER: "../subgraph" + SUBGRAPH_NAME: ${{ vars.SUBGRAPH_NAME }} + SUBGRAPH_PROVIDER_URL: ${{ vars.SUBGRAPH_PROVIDER_URL }} + SUBGRAPH_DEPLOY_KEY: ${{ secrets.SUBGRAPH_DEPLOY_KEY }} concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.gitignore b/.gitignore index 9bcc767c3e..c4a1be800a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ circuit.json input.json *.proof.json *.publicSignals.json +subgraph/config/*.json +!subgraph/config/network.json .vscode/ diff --git a/circuits/circom/utils/processMessagesInputHasher.circom b/circuits/circom/utils/processMessagesInputHasher.circom index d9dd1999a7..33b7b97e90 100644 --- a/circuits/circom/utils/processMessagesInputHasher.circom +++ b/circuits/circom/utils/processMessagesInputHasher.circom @@ -48,7 +48,7 @@ template ProcessMessagesInputHasher() { // 2. Hash coordPubKey. var computedPubKey = PoseidonHasher(2)(coordPubKey); - // 3. Hash the 6 inputs with SHA256. + // 3. Hash the 7 inputs with SHA256. hash <== Sha256Hasher(7)([ packedVals, computedPubKey, diff --git a/coordinator/.env.example b/coordinator/.env.example index 5b9172a83c..d0d9753c80 100644 --- a/coordinator/.env.example +++ b/coordinator/.env.example @@ -32,3 +32,17 @@ COORDINATOR_ALLOWED_ORIGINS= # Specify port for coordinator service (optional) COORDINATOR_PORT= + +# Subgraph name +SUBGRAPH_NAME="maci-subgraph" + +# Subgraph provider url +SUBGRAPH_PROVIDER_URL=https://api.studio.thegraph.com/deploy/ + +# Subgraph deploy key +SUBGRAPH_DEPLOY_KEY= + +# Subgraph project folder +SUBGRAPH_FOLDER=../subgraph + + diff --git a/coordinator/README.md b/coordinator/README.md index 50a897fdea..3ff05698a4 100644 --- a/coordinator/README.md +++ b/coordinator/README.md @@ -11,3 +11,33 @@ Make sure you set `COORDINATOR_ADDRESSES` env variable and sign any message with the addresses from your application (see [AccountSignatureGuard](./ts/auth/AccountSignatureGuard.service.ts)). 7. Proofs can be generated with `POST v1/proof/generate` API method or with Websockets (see [dto spec](./ts/proof/dto.ts), [controller](./ts/app.controller.ts) and [wsgateway](./ts/events/events.gateway.ts)). 8. [Swagger documentation for API methods](https://maci-coordinator.pse.dev/api) + +## Subgraph deployment + +It is possible to deploy subgraph using coordinator service. + +First, you need to setup subgraph and create a project. [Subgraph dashboard](https://thegraph.com/studio/). + +Then, set env variables: + +``` +# Subgraph name +SUBGRAPH_NAME="maci-subgraph" + +# Subgraph provider url +SUBGRAPH_PROVIDER_URL=https://api.studio.thegraph.com/deploy/ + +# Subgraph deploy key +SUBGRAPH_DEPLOY_KEY=******* + +# Subgraph project folder +SUBGRAPH_FOLDER=../subgraph +``` + +After deployment, subgraph url will be available in studio dashboard and you can use this type of url to get latest deployed version in your application: + +``` +https://api.studio.thegraph.com/.../{SUBGRAPH_NAME}/version/latest +``` + +API method Details are available [here](https://maci-coordinator.pse.dev/api) diff --git a/coordinator/package.json b/coordinator/package.json index 7ac0f463bf..b1cbbb0f4d 100644 --- a/coordinator/package.json +++ b/coordinator/package.json @@ -21,6 +21,7 @@ "generate-keypair": "ts-node ./scripts/generateKeypair.ts" }, "dependencies": { + "@graphprotocol/graph-cli": "^0.76.0", "@nestjs/common": "^10.3.8", "@nestjs/core": "^10.3.9", "@nestjs/platform-express": "^10.3.8", @@ -40,6 +41,7 @@ "maci-cli": "2.0.0-alpha", "maci-contracts": "2.0.0-alpha", "maci-domainobjs": "2.0.0-alpha", + "mustache": "^4.2.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.7.5", diff --git a/coordinator/tests/app.test.ts b/coordinator/tests/app.test.ts index a3b623f4b2..9782e88bd7 100644 --- a/coordinator/tests/app.test.ts +++ b/coordinator/tests/app.test.ts @@ -40,7 +40,7 @@ const MSG_TREE_DEPTH = 2; const VOTE_OPTION_TREE_DEPTH = 2; const MSG_BATCH_DEPTH = 1; -describe("AppController (e2e)", () => { +describe("e2e", () => { const coordinatorKeypair = new Keypair(); let app: INestApplication; let signer: Signer; diff --git a/coordinator/ts/app.module.ts b/coordinator/ts/app.module.ts index 0405118fbd..1691029158 100644 --- a/coordinator/ts/app.module.ts +++ b/coordinator/ts/app.module.ts @@ -1,11 +1,11 @@ import { Module } from "@nestjs/common"; import { ThrottlerModule } from "@nestjs/throttler"; -import { AppController } from "./app.controller"; import { CryptoModule } from "./crypto/crypto.module"; import { EventsModule } from "./events/events.module"; import { FileModule } from "./file/file.module"; -import { ProofGeneratorService } from "./proof/proof.service"; +import { ProofModule } from "./proof/proof.module"; +import { SubgraphModule } from "./subgraph/subgraph.module"; @Module({ imports: [ @@ -18,8 +18,8 @@ import { ProofGeneratorService } from "./proof/proof.service"; FileModule, CryptoModule, EventsModule, + SubgraphModule, + ProofModule, ], - controllers: [AppController], - providers: [ProofGeneratorService], }) export class AppModule {} diff --git a/coordinator/ts/common/errors.ts b/coordinator/ts/common/errors.ts index 9d38bf37d0..87606ae1b6 100644 --- a/coordinator/ts/common/errors.ts +++ b/coordinator/ts/common/errors.ts @@ -9,4 +9,5 @@ export enum ErrorCodes { DECRYPTION = "4", ENCRYPTION = "5", FILE_NOT_FOUND = "6", + SUBGRAPH_DEPLOY = "7", } diff --git a/coordinator/ts/common/index.ts b/coordinator/ts/common/index.ts index 63731d1416..78bdf4955a 100644 --- a/coordinator/ts/common/index.ts +++ b/coordinator/ts/common/index.ts @@ -1 +1,2 @@ export { ErrorCodes } from "./errors"; +export { ESupportedNetworks } from "./networks"; diff --git a/coordinator/ts/common/networks.ts b/coordinator/ts/common/networks.ts new file mode 100644 index 0000000000..a74e922ece --- /dev/null +++ b/coordinator/ts/common/networks.ts @@ -0,0 +1,64 @@ +export enum ESupportedNetworks { + ETHEREUM = "mainnet", + OPTIMISM = "optimism", + OPTIMISM_SEPOLIA = "optimism-sepolia", + BSC = "bsc", + BSC_CHAPEL = "chapel", + GNOSIS_CHAIN = "gnosis", + FUSE = "fuse", + POLYGON = "matic", + FANTOM_OPERA = "fantom", + ZKSYNC_ERA_TESTNET = "zksync-era-testnet", + BOBA = "boba", + MOONBEAM = "moonbeam", + MOONRIVER = "moonriver", + MOONBASE_ALPHA = "mbase", + FANTOM_TESTNET = "fantom-testnet", + ARBITRUM_ONE = "arbitrum-one", + CELO = "celo", + AVALANCHE_FUJI = "fuji", + AVALANCHE = "avalanche", + CELO_ALFAJORES = "celo-alfajores", + HOLESKY = "holesky", + AURORA = "aurora", + AURORA_TESTNET = "aurora-testnet", + HARMONY = "harmony", + LINEA_SEPOLIA = "linea-sepolia", + GNOSIS_CHIADO = "gnosis-chiado", + MODE_SEPOLIA = "mode-sepolia", + MODE = "mode-mainnet", + BASE_SEPOLIA = "base-sepolia", + ZKSYNC_ERA_SEPOLIA = "zksync-era-sepolia", + POLYGON_ZKEVM = "polygon-zkevm", + ZKSYNC_ERA = "zksync-era", + ETHEREUM_SEPOLIA = "sepolia", + ARBITRUM_SEPOLIA = "arbitrum-sepolia", + LINEA = "linea", + BASE = "base", + SCROLL_SEPOLIA = "scroll-sepolia", + SCROLL = "scroll", + BLAST_MAINNET = "blast-mainnet", + ASTAR_ZKEVM_MAINNET = "astar-zkevm-mainnet", + SEI_TESTNET = "sei-testnet", + BLAST_TESTNET = "blast-testnet", + ETHERLINK_TESTNET = "etherlink-testnet", + XLAYER_SEPOLIA = "xlayer-sepolia", + XLAYER_MAINNET = "xlayer-mainnet", + POLYGON_AMOY = "polygon-amoy", + ZKYOTO_TESTNET = "zkyoto-testnet", + POLYGON_ZKEVM_CARDONA = "polygon-zkevm-cardona", + SEI_MAINNET = "sei-mainnet", + ROOTSTOCK_MAINNET = "rootstock", + IOTEX_MAINNET = "iotex", + NEAR_MAINNET = "near-mainnet", + NEAR_TESTNET = "near-testnet", + COSMOS = "cosmoshub-4", + COSMOS_HUB = "theta-testnet-001", + OSMOSIS = "osmosis-1", + OSMO_TESTNET = "osmo-test-4", + ARWEAVE = "arweave-mainnet", + BITCOIN = "btc", + SOLANA = "solana-mainnet-beta", + INJECTIVE_MAINNET = "injective-mainnet", + INJECTIVE_TESTNET = "injective-testnet", +} diff --git a/coordinator/ts/app.controller.test.ts b/coordinator/ts/proof/__tests__/proof.controller.test.ts similarity index 77% rename from coordinator/ts/app.controller.test.ts rename to coordinator/ts/proof/__tests__/proof.controller.test.ts index d059f3ac14..04c04a18f1 100644 --- a/coordinator/ts/app.controller.test.ts +++ b/coordinator/ts/proof/__tests__/proof.controller.test.ts @@ -1,16 +1,16 @@ import { HttpException, HttpStatus } from "@nestjs/common"; import { Test } from "@nestjs/testing"; -import type { IGetPublicKeyData } from "./file/types"; -import type { IGenerateArgs, IGenerateData } from "./proof/types"; +import type { IGetPublicKeyData } from "../../file/types"; +import type { IGenerateArgs, IGenerateData } from "../types"; import type { TallyData } from "maci-cli"; -import { AppController } from "./app.controller"; -import { FileService } from "./file/file.service"; -import { ProofGeneratorService } from "./proof/proof.service"; +import { FileService } from "../../file/file.service"; +import { ProofController } from "../proof.controller"; +import { ProofGeneratorService } from "../proof.service"; -describe("AppController", () => { - let appController: AppController; +describe("ProofController", () => { + let proofController: ProofController; const defaultProofGeneratorArgs: IGenerateArgs = { poll: 0, @@ -41,7 +41,7 @@ describe("AppController", () => { beforeEach(async () => { const app = await Test.createTestingModule({ - controllers: [AppController], + controllers: [ProofController], }) .useMocker((token) => { if (token === ProofGeneratorService) { @@ -60,7 +60,7 @@ describe("AppController", () => { }) .compile(); - appController = app.get(AppController); + proofController = app.get(ProofController); }); afterEach(() => { @@ -69,15 +69,15 @@ describe("AppController", () => { describe("v1/proof/generate", () => { test("should return generated proof data", async () => { - const data = await appController.generate(defaultProofGeneratorArgs); + const data = await proofController.generate(defaultProofGeneratorArgs); expect(data).toStrictEqual(defaultProofGeneratorData); }); - test("should throw an error if proof generation if failed", async () => { + test("should throw an error if proof generation is failed", async () => { const error = new Error("error"); mockGeneratorService.generate.mockRejectedValue(error); - await expect(appController.generate(defaultProofGeneratorArgs)).rejects.toThrow( + await expect(proofController.generate(defaultProofGeneratorArgs)).rejects.toThrow( new HttpException(error.message, HttpStatus.BAD_REQUEST), ); }); @@ -85,7 +85,7 @@ describe("AppController", () => { describe("v1/proof/publicKey", () => { test("should return public key properly", async () => { - const data = await appController.getPublicKey(); + const data = await proofController.getPublicKey(); expect(data).toStrictEqual(defaultPublicKeyData); }); @@ -93,7 +93,7 @@ describe("AppController", () => { const error = new Error("error"); mockFileService.getPublicKey.mockRejectedValue(error); - await expect(appController.getPublicKey()).rejects.toThrow( + await expect(proofController.getPublicKey()).rejects.toThrow( new HttpException(error.message, HttpStatus.BAD_REQUEST), ); }); diff --git a/coordinator/ts/app.controller.ts b/coordinator/ts/proof/proof.controller.ts similarity index 79% rename from coordinator/ts/app.controller.ts rename to coordinator/ts/proof/proof.controller.ts index 1cce2f3cbf..5d886ce514 100644 --- a/coordinator/ts/app.controller.ts +++ b/coordinator/ts/proof/proof.controller.ts @@ -1,26 +1,27 @@ import { Body, Controller, Get, HttpException, HttpStatus, Logger, Post, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from "@nestjs/swagger"; -import type { IGetPublicKeyData } from "./file/types"; -import type { IGenerateData } from "./proof/types"; +import type { IGenerateData } from "./types"; +import type { IGetPublicKeyData } from "../file/types"; -import { AccountSignatureGuard, Public } from "./auth/AccountSignatureGuard.service"; -import { FileService } from "./file/file.service"; -import { GenerateProofDto } from "./proof/dto"; -import { ProofGeneratorService } from "./proof/proof.service"; +import { AccountSignatureGuard, Public } from "../auth/AccountSignatureGuard.service"; +import { FileService } from "../file/file.service"; + +import { GenerateProofDto } from "./dto"; +import { ProofGeneratorService } from "./proof.service"; @ApiTags("v1/proof") @ApiBearerAuth() @Controller("v1/proof") @UseGuards(AccountSignatureGuard) -export class AppController { +export class ProofController { /** * Logger */ - private readonly logger = new Logger(AppController.name); + private readonly logger = new Logger(ProofController.name); /** - * Initialize AppController + * Initialize ProofController * * @param proofGeneratorService - proof generator service * @param fileService - file service diff --git a/coordinator/ts/proof/proof.module.ts b/coordinator/ts/proof/proof.module.ts new file mode 100644 index 0000000000..1299636da5 --- /dev/null +++ b/coordinator/ts/proof/proof.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; + +import { CryptoModule } from "../crypto/crypto.module"; +import { FileModule } from "../file/file.module"; + +import { ProofController } from "./proof.controller"; +import { ProofGeneratorService } from "./proof.service"; + +@Module({ + imports: [FileModule, CryptoModule], + controllers: [ProofController], + providers: [ProofGeneratorService], +}) +export class ProofModule {} diff --git a/coordinator/ts/subgraph/__tests__/subgraph.controller.test.ts b/coordinator/ts/subgraph/__tests__/subgraph.controller.test.ts new file mode 100644 index 0000000000..c17909aaca --- /dev/null +++ b/coordinator/ts/subgraph/__tests__/subgraph.controller.test.ts @@ -0,0 +1,66 @@ +import { HttpException, HttpStatus } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; + +import type { IDeploySubgraphArgs, IDeploySubgraphReturn } from "../types"; + +import { ESupportedNetworks } from "../../common"; +import { SubgraphController } from "../subgraph.controller"; +import { SubgraphService } from "../subgraph.service"; + +describe("SubgraphController", () => { + let subgraphController: SubgraphController; + + const defaultSubgraphDeployArgs: IDeploySubgraphArgs = { + maciContractAddress: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + startBlock: 0, + network: ESupportedNetworks.OPTIMISM_SEPOLIA, + name: "subgraph", + tag: "v0.0.1", + }; + + const defaultSubgraphDeployData: IDeploySubgraphReturn = { + url: "url", + }; + + const mockSubgraphService = { + deploy: jest.fn(), + }; + + beforeEach(async () => { + const app = await Test.createTestingModule({ + controllers: [SubgraphController], + }) + .useMocker((token) => { + if (token === SubgraphService) { + mockSubgraphService.deploy.mockResolvedValue(defaultSubgraphDeployData); + + return mockSubgraphService; + } + + return jest.fn(); + }) + .compile(); + + subgraphController = app.get(SubgraphController); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("v1/subgraph/deploy", () => { + test("should return deployed subgraph url properly", async () => { + const data = await subgraphController.deploy(defaultSubgraphDeployArgs); + expect(data).toStrictEqual(defaultSubgraphDeployData); + }); + + test("should throw an error if proof generation is failed", async () => { + const error = new Error("error"); + mockSubgraphService.deploy.mockRejectedValue(error); + + await expect(subgraphController.deploy(defaultSubgraphDeployArgs)).rejects.toThrow( + new HttpException(error.message, HttpStatus.BAD_REQUEST), + ); + }); + }); +}); diff --git a/coordinator/ts/subgraph/__tests__/subgraph.service.test.ts b/coordinator/ts/subgraph/__tests__/subgraph.service.test.ts new file mode 100644 index 0000000000..3608784fa0 --- /dev/null +++ b/coordinator/ts/subgraph/__tests__/subgraph.service.test.ts @@ -0,0 +1,90 @@ +import dotenv from "dotenv"; + +import childProcess from "child_process"; +import fs from "fs"; + +import type { IDeploySubgraphArgs } from "../types"; + +import { ErrorCodes, ESupportedNetworks } from "../../common"; +import { transformToString } from "../dto"; +import { SubgraphService } from "../subgraph.service"; + +dotenv.config(); + +jest.mock("child_process", (): unknown => ({ + ...jest.requireActual("child_process"), + execFile: jest.fn(), +})); + +jest.mock("fs", (): unknown => ({ + ...jest.requireActual("fs"), + promises: { + writeFile: jest.fn(), + }, +})); + +jest.mock("util", (): unknown => ({ + promisify: jest.fn((func: jest.Mock) => func), +})); + +describe("SubgraphService", () => { + const defaultArgs: IDeploySubgraphArgs = { + maciContractAddress: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + startBlock: 0, + network: ESupportedNetworks.OPTIMISM_SEPOLIA, + name: "subgraph", + tag: "v0.0.1", + }; + + beforeEach(() => { + (childProcess.execFile as unknown as jest.Mock).mockResolvedValue({ + stdout: "https://subgraph.com https://test.com", + }); + + (fs.promises.writeFile as jest.Mock).mockResolvedValue(undefined); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should throw error if deploy is failed", async () => { + (childProcess.execFile as unknown as jest.Mock).mockRejectedValue(new Error()); + + const service = new SubgraphService(); + + await expect(service.deploy(defaultArgs)).rejects.toThrow(ErrorCodes.SUBGRAPH_DEPLOY); + }); + + test("should throw error if network is invalid", async () => { + (childProcess.execFile as unknown as jest.Mock).mockRejectedValue(new Error()); + + const service = new SubgraphService(); + + await expect(service.deploy({ ...defaultArgs, network: "unknown" as ESupportedNetworks })).rejects.toThrow( + ErrorCodes.SUBGRAPH_DEPLOY, + ); + }); + + test("should throw error if there is no subgraph url", async () => { + (childProcess.execFile as unknown as jest.Mock).mockResolvedValue({ stdout: "" }); + + const service = new SubgraphService(); + + await expect(service.deploy(defaultArgs)).rejects.toThrow(ErrorCodes.SUBGRAPH_DEPLOY); + }); + + test("should return deployed subgraph url properly", async () => { + const service = new SubgraphService(); + + const { url } = await service.deploy(defaultArgs); + + expect(url).toBe("https://test.com"); + }); + + test("should transform value to string properly", () => { + const value = transformToString({ value: "Network" }); + + expect(value).toBe("network"); + }); +}); diff --git a/coordinator/ts/subgraph/dto.ts b/coordinator/ts/subgraph/dto.ts new file mode 100644 index 0000000000..449048b120 --- /dev/null +++ b/coordinator/ts/subgraph/dto.ts @@ -0,0 +1,68 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Transform } from "class-transformer"; +import { IsEnum, IsEthereumAddress, IsInt, IsString, Matches, MaxLength, Min, MinLength } from "class-validator"; + +import { ESupportedNetworks } from "../common"; + +export const transformToString = ({ value }: { value: string }): string => value.toLowerCase(); + +/** + * Data transfer object for deploying subgraph + */ +export class DeploySubgraphDto { + /** + * MACI contract address + */ + @ApiProperty({ + description: "MACI contract address", + type: String, + }) + @IsEthereumAddress() + maciContractAddress!: string; + + /** + * Start block for event processing + */ + @ApiProperty({ + description: "Start block for event parsing", + minimum: 0, + type: Number, + }) + @IsInt() + @Min(0) + startBlock!: number; + + /** + * Network CLI name + */ + @ApiProperty({ + description: "Network CLI name (https://thegraph.com/docs/en/developing/supported-networks/)", + enum: ESupportedNetworks, + }) + @IsEnum(ESupportedNetworks) + @Transform(transformToString) + network!: ESupportedNetworks; + + /** + * Subgraph name + */ + @ApiProperty({ + description: "Subgraph name", + type: String, + }) + @IsString() + @MinLength(3) + @MaxLength(50) + name!: string; + + /** + * Version tag (ex: v0.0.1) + */ + @ApiProperty({ + description: "Version tag (ex: v0.0.1)", + type: String, + }) + @IsString() + @Matches(/^v\d+\.\d+\.\d+$/) + tag!: string; +} diff --git a/coordinator/ts/subgraph/subgraph.controller.ts b/coordinator/ts/subgraph/subgraph.controller.ts new file mode 100644 index 0000000000..452982af2b --- /dev/null +++ b/coordinator/ts/subgraph/subgraph.controller.ts @@ -0,0 +1,43 @@ +import { Body, Controller, HttpException, HttpStatus, Logger, Post } from "@nestjs/common"; +import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from "@nestjs/swagger"; + +import type { IDeploySubgraphReturn } from "./types"; + +import { DeploySubgraphDto } from "./dto"; +import { SubgraphService } from "./subgraph.service"; + +@ApiTags("v1/subgraph") +@ApiBearerAuth() +@Controller("v1/subgraph") +// @UseGuards(AccountSignatureGuard) +export class SubgraphController { + /** + * Logger + */ + private readonly logger = new Logger(SubgraphController.name); + + /** + * Initialize SubgraphController + * + * @param subgraphService - subgraph service + */ + constructor(private readonly subgraphService: SubgraphService) {} + + /** + * Generate proofs api method + * + * @param args - generate proof dto + * @returns generated proofs and tally data + */ + @ApiBody({ type: DeploySubgraphDto }) + @ApiResponse({ status: HttpStatus.CREATED, description: "The subgraph was successfully deployed" }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: "Forbidden" }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" }) + @Post("deploy") + async deploy(@Body() args: DeploySubgraphDto): Promise { + return this.subgraphService.deploy(args).catch((error: Error) => { + this.logger.error(`Error:`, error); + throw new HttpException(error.message, HttpStatus.BAD_REQUEST); + }); + } +} diff --git a/coordinator/ts/subgraph/subgraph.module.ts b/coordinator/ts/subgraph/subgraph.module.ts new file mode 100644 index 0000000000..4e0df34b22 --- /dev/null +++ b/coordinator/ts/subgraph/subgraph.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; + +import { CryptoModule } from "../crypto/crypto.module"; +import { FileModule } from "../file/file.module"; + +import { SubgraphController } from "./subgraph.controller"; +import { SubgraphService } from "./subgraph.service"; + +@Module({ + imports: [FileModule, CryptoModule], + controllers: [SubgraphController], + providers: [SubgraphService], +}) +export class SubgraphModule {} diff --git a/coordinator/ts/subgraph/subgraph.service.ts b/coordinator/ts/subgraph/subgraph.service.ts new file mode 100644 index 0000000000..41da7341bb --- /dev/null +++ b/coordinator/ts/subgraph/subgraph.service.ts @@ -0,0 +1,104 @@ +import { Injectable, Logger } from "@nestjs/common"; + +import childProcess from "child_process"; +import fs from "fs"; +import path from "path"; +import { promisify } from "util"; + +import type { IDeploySubgraphArgs, IDeploySubgraphReturn } from "./types"; + +import { ErrorCodes, ESupportedNetworks } from "../common"; + +const execFile = promisify(childProcess.execFile); + +const URL_REGEX = /(https?:\/\/[^\s]+)/g; + +/** + * SubgraphService is responsible for deploying subgraph. + */ +@Injectable() +export class SubgraphService { + /** + * Logger + */ + private readonly logger = new Logger(SubgraphService.name); + + /** + * Generate proofs for message processing and tally + * + * @param args - deploy subgraph arguments + * @returns - deployed subgraph url + * @throws error if deploy is not successful + */ + async deploy(args: IDeploySubgraphArgs): Promise { + try { + if (!Object.values(ESupportedNetworks).includes(args.network)) { + throw new Error("Invalid network"); + } + + const subgraphManifestPath = path.resolve(process.env.SUBGRAPH_FOLDER!, "subgraph.yaml"); + + await execFile("cp", [ + path.resolve(process.env.SUBGRAPH_FOLDER!, "schemas/schema.v1.graphql"), + path.resolve(process.env.SUBGRAPH_FOLDER!, "schema.graphql"), + ]); + + await fs.promises.writeFile( + path.resolve(process.env.SUBGRAPH_FOLDER!, `config/${args.network}.json`), + `${JSON.stringify( + { + network: args.network, + maciContractAddress: args.maciContractAddress, + maciContractStartBlock: args.startBlock, + }, + null, + 2, + )}\n`, + { flag: "w+" }, + ); + + const mustacheOutput = await execFile("mustache", [ + path.resolve(process.env.SUBGRAPH_FOLDER!, `config/${args.network}.json`), + path.resolve(process.env.SUBGRAPH_FOLDER!, "templates/subgraph.template.yaml"), + ]); + await fs.promises.writeFile(subgraphManifestPath, mustacheOutput.stdout, { flag: "w+" }); + + await execFile("graph", [ + "codegen", + subgraphManifestPath, + "--output-dir", + path.resolve(process.env.SUBGRAPH_FOLDER!, "generated"), + ]); + await execFile("graph", [ + "build", + subgraphManifestPath, + "--output-dir", + path.resolve(process.env.SUBGRAPH_FOLDER!, "build"), + ]); + + const deployOutput = await execFile("graph", [ + "deploy", + process.env.SUBGRAPH_NAME!, + subgraphManifestPath, + "--node", + process.env.SUBGRAPH_PROVIDER_URL!, + "--deploy-key", + process.env.SUBGRAPH_DEPLOY_KEY!, + "--version-label", + args.tag, + ]); + this.logger.log(deployOutput.stdout); + + const url = deployOutput.stdout.match(URL_REGEX)?.[1]?.trim().replace("\u001b[0m", ""); + + if (!url) { + throw new Error(ErrorCodes.SUBGRAPH_DEPLOY); + } + + return { url }; + } catch (error) { + this.logger.error("Error: ", error); + throw new Error(ErrorCodes.SUBGRAPH_DEPLOY); + } + } +} diff --git a/coordinator/ts/subgraph/types.ts b/coordinator/ts/subgraph/types.ts new file mode 100644 index 0000000000..24acf360a7 --- /dev/null +++ b/coordinator/ts/subgraph/types.ts @@ -0,0 +1,41 @@ +import type { ESupportedNetworks } from "../common"; + +/** + * Interface that represents deploy subgraph args + */ +export interface IDeploySubgraphArgs { + /** + * MACI contract address + */ + maciContractAddress: string; + + /** + * Start block + */ + startBlock: number; + + /** + * Network + */ + network: ESupportedNetworks; + + /** + * Subgraph name + */ + name: string; + + /** + * Version tag + */ + tag: string; +} + +/** + * Interface that represents deploy subgraph return data + */ +export interface IDeploySubgraphReturn { + /** + * Deployed subgraph url + */ + url: string; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b293f1839..84a3460aba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -326,6 +326,9 @@ importers: coordinator: dependencies: + '@graphprotocol/graph-cli': + specifier: ^0.76.0 + version: 0.76.0(@types/node@20.14.8)(node-fetch@3.3.2)(typescript@5.5.2) '@nestjs/common': specifier: ^10.3.8 version: 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -383,6 +386,9 @@ importers: maci-domainobjs: specifier: 2.0.0-alpha version: link:../domainobjs + mustache: + specifier: ^4.2.0 + version: 4.2.0 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -646,7 +652,7 @@ importers: dependencies: '@docusaurus/core': specifier: ^3.4.0 - version: 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + version: 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/preset-classic': specifier: ^3.4.0 version: 3.4.0(@algolia/client-search@4.23.3)(@types/react@18.3.3)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(search-insights@2.14.0)(typescript@5.5.2) @@ -2631,104 +2637,6 @@ packages: - webpack-cli dev: false - /@docusaurus/core@3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2): - resolution: {integrity: sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==} - engines: {node: '>=18.0'} - hasBin: true - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - dependencies: - '@babel/core': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-transform-runtime': 7.24.7(@babel/core@7.24.7) - '@babel/preset-env': 7.24.7(@babel/core@7.24.7) - '@babel/preset-react': 7.24.7(@babel/core@7.24.7) - '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) - '@babel/runtime': 7.24.7 - '@babel/runtime-corejs3': 7.24.7 - '@babel/traverse': 7.24.7 - '@docusaurus/cssnano-preset': 3.4.0 - '@docusaurus/logger': 3.4.0 - '@docusaurus/mdx-loader': 3.4.0(@docusaurus/types@3.4.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) - '@docusaurus/utils': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) - '@docusaurus/utils-common': 3.4.0(@docusaurus/types@3.4.0) - '@docusaurus/utils-validation': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) - autoprefixer: 10.4.19(postcss@8.4.38) - babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.91.0) - babel-plugin-dynamic-import-node: 2.3.3 - boxen: 6.2.1 - chalk: 4.1.2 - chokidar: 3.6.0 - clean-css: 5.3.3 - cli-table3: 0.6.5 - combine-promises: 1.2.0 - commander: 5.1.0 - copy-webpack-plugin: 11.0.0(webpack@5.91.0) - core-js: 3.37.1 - css-loader: 6.11.0(webpack@5.91.0) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.91.0) - cssnano: 6.1.2(postcss@8.4.38) - del: 6.1.1 - detect-port: 1.6.1 - escape-html: 1.0.3 - eta: 2.2.0 - eval: 0.1.8 - file-loader: 6.2.0(webpack@5.91.0) - fs-extra: 11.2.0 - html-minifier-terser: 7.2.0 - html-tags: 3.3.1 - html-webpack-plugin: 5.6.0(webpack@5.91.0) - leven: 3.1.0 - lodash: 4.17.21 - mini-css-extract-plugin: 2.9.0(webpack@5.91.0) - p-map: 4.0.0 - postcss: 8.4.38 - postcss-loader: 7.3.4(postcss@8.4.38)(typescript@5.5.2)(webpack@5.91.0) - prompts: 2.4.2 - react: 18.3.1 - react-dev-utils: 12.0.1(eslint@8.57.0)(typescript@5.5.2)(webpack@5.91.0) - react-dom: 18.3.1(react@18.3.1) - react-helmet-async: 1.3.0(react-dom@18.3.1)(react@18.3.1) - react-loadable: /@docusaurus/react-loadable@6.0.0(react@18.3.1) - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0)(webpack@5.91.0) - react-router: 5.3.4(react@18.3.1) - react-router-config: 5.1.1(react-router@5.3.4)(react@18.3.1) - react-router-dom: 5.3.4(react@18.3.1) - rtl-detect: 1.1.2 - semver: 7.6.2 - serve-handler: 6.1.5 - shelljs: 0.8.5 - terser-webpack-plugin: 5.3.10(webpack@5.91.0) - tslib: 2.6.3 - update-notifier: 6.0.2 - url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.91.0) - webpack: 5.91.0 - webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 4.15.2(webpack@5.91.0) - webpack-merge: 5.10.0 - webpackbar: 5.0.2(webpack@5.91.0) - transitivePeerDependencies: - - '@docusaurus/types' - - '@parcel/css' - - '@rspack/core' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - dev: false - /@docusaurus/cssnano-preset@3.4.0: resolution: {integrity: sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==} engines: {node: '>=18.0'} @@ -2819,7 +2727,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/logger': 3.4.0 '@docusaurus/mdx-loader': 3.4.0(@docusaurus/types@3.4.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) @@ -2901,50 +2809,6 @@ packages: - webpack-cli dev: false - /@docusaurus/plugin-content-docs@3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2): - resolution: {integrity: sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==} - engines: {node: '>=18.0'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) - '@docusaurus/logger': 3.4.0 - '@docusaurus/mdx-loader': 3.4.0(@docusaurus/types@3.4.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) - '@docusaurus/module-type-aliases': 3.4.0(react-dom@18.3.1)(react@18.3.1) - '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) - '@docusaurus/utils': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) - '@docusaurus/utils-common': 3.4.0(@docusaurus/types@3.4.0) - '@docusaurus/utils-validation': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) - '@types/react-router-config': 5.0.11 - combine-promises: 1.2.0 - fs-extra: 11.2.0 - js-yaml: 4.1.0 - lodash: 4.17.21 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tslib: 2.6.3 - utility-types: 3.11.0 - webpack: 5.91.0 - transitivePeerDependencies: - - '@parcel/css' - - '@rspack/core' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - dev: false - /@docusaurus/plugin-content-pages@3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2): resolution: {integrity: sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==} engines: {node: '>=18.0'} @@ -2952,7 +2816,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/mdx-loader': 3.4.0(@docusaurus/types@3.4.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/utils': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) @@ -2988,7 +2852,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/utils': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) fs-extra: 11.2.0 @@ -3022,7 +2886,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/utils-validation': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) react: 18.3.1 @@ -3054,7 +2918,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/utils-validation': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) '@types/gtag.js': 0.0.12 @@ -3087,7 +2951,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/utils-validation': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) react: 18.3.1 @@ -3119,7 +2983,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/logger': 3.4.0 '@docusaurus/types': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/utils': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) @@ -3156,9 +3020,9 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/plugin-content-blog': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) - '@docusaurus/plugin-content-docs': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/plugin-content-docs': 3.4.0(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/plugin-content-pages': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/plugin-debug': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/plugin-google-analytics': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) @@ -3208,11 +3072,11 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/mdx-loader': 3.4.0(@docusaurus/types@3.4.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/module-type-aliases': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/plugin-content-blog': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) - '@docusaurus/plugin-content-docs': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/plugin-content-docs': 3.4.0(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/plugin-content-pages': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/theme-common': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/theme-translations': 3.4.0 @@ -3265,7 +3129,7 @@ packages: '@docusaurus/mdx-loader': 3.4.0(@docusaurus/types@3.4.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/module-type-aliases': 3.4.0(react-dom@18.3.1)(react@18.3.1) '@docusaurus/plugin-content-blog': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) - '@docusaurus/plugin-content-docs': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/plugin-content-docs': 3.4.0(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/plugin-content-pages': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/utils': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) '@docusaurus/utils-common': 3.4.0(@docusaurus/types@3.4.0) @@ -3307,9 +3171,9 @@ packages: react-dom: ^18.0.0 dependencies: '@docsearch/react': 3.6.0(@algolia/client-search@4.23.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)(search-insights@2.14.0) - '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/core': 3.4.0(@docusaurus/types@3.4.0)(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/logger': 3.4.0 - '@docusaurus/plugin-content-docs': 3.4.0(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) + '@docusaurus/plugin-content-docs': 3.4.0(debug@4.3.5)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/theme-common': 3.4.0(@docusaurus/types@3.4.0)(eslint@8.57.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.2) '@docusaurus/theme-translations': 3.4.0 '@docusaurus/utils': 3.4.0(@docusaurus/types@3.4.0)(typescript@5.5.2) @@ -7750,7 +7614,7 @@ packages: /axios@1.7.2: resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} dependencies: - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6(debug@4.3.5) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -11581,6 +11445,7 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) + dev: false /follow-redirects@1.15.6(debug@4.3.5): resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} @@ -12864,25 +12729,6 @@ packages: - supports-color dev: true - /http-proxy-middleware@2.0.6(@types/express@4.17.21): - resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@types/express': ^4.17.13 - peerDependenciesMeta: - '@types/express': - optional: true - dependencies: - '@types/express': 4.17.21 - '@types/http-proxy': 1.17.14 - http-proxy: 1.18.1 - is-glob: 4.0.3 - is-plain-obj: 3.0.0 - micromatch: 4.0.7 - transitivePeerDependencies: - - debug - dev: false - /http-proxy-middleware@2.0.6(@types/express@4.17.21)(debug@4.3.5): resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} engines: {node: '>=12.0.0'} @@ -12902,17 +12748,6 @@ packages: - debug dev: false - /http-proxy@1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.15.6(debug@4.3.4) - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - dev: false - /http-proxy@1.18.1(debug@4.3.5): resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -16196,7 +16031,6 @@ packages: /mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - dev: true /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -21189,57 +21023,6 @@ packages: - utf-8-validate dev: false - /webpack-dev-server@4.15.2(webpack@5.91.0): - resolution: {integrity: sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==} - engines: {node: '>= 12.13.0'} - hasBin: true - peerDependencies: - webpack: ^4.37.0 || ^5.0.0 - webpack-cli: '*' - peerDependenciesMeta: - webpack: - optional: true - webpack-cli: - optional: true - dependencies: - '@types/bonjour': 3.5.13 - '@types/connect-history-api-fallback': 1.5.4 - '@types/express': 4.17.21 - '@types/serve-index': 1.9.4 - '@types/serve-static': 1.15.7 - '@types/sockjs': 0.3.36 - '@types/ws': 8.5.10 - ansi-html-community: 0.0.8 - bonjour-service: 1.2.1 - chokidar: 3.6.0 - colorette: 2.0.20 - compression: 1.7.4 - connect-history-api-fallback: 2.0.0 - default-gateway: 6.0.3 - express: 4.19.2 - graceful-fs: 4.2.11 - html-entities: 2.5.2 - http-proxy-middleware: 2.0.6(@types/express@4.17.21) - ipaddr.js: 2.2.0 - launch-editor: 2.6.1 - open: 8.4.2 - p-retry: 4.6.2 - rimraf: 3.0.2 - schema-utils: 4.2.0 - selfsigned: 2.4.1 - serve-index: 1.9.1 - sockjs: 0.3.24 - spdy: 4.0.2 - webpack: 5.91.0 - webpack-dev-middleware: 5.3.4(webpack@5.91.0) - ws: 8.17.1 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - dev: false - /webpack-merge@5.10.0: resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} engines: {node: '>=10.0.0'} diff --git a/subgraph/.gitignore b/subgraph/.gitignore index fb74381a00..477b9b7972 100644 --- a/subgraph/.gitignore +++ b/subgraph/.gitignore @@ -6,6 +6,4 @@ subgraph.yaml schema.graphql tests/.bin/ tests/.latest.json -config/*.json -!config/[network].json diff --git a/subgraph/config/optimism-sepolia.json b/subgraph/config/network.json similarity index 100% rename from subgraph/config/optimism-sepolia.json rename to subgraph/config/network.json diff --git a/subgraph/config/scroll-sepolia.json b/subgraph/config/scroll-sepolia.json deleted file mode 100644 index eb23a64afe..0000000000 --- a/subgraph/config/scroll-sepolia.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "network": "scroll-sepolia", - "maciContractAddress": "0x6aA5baD99aC44bB008930c68CE2249549Cf10944", - "maciContractStartBlock": 4634247 -} diff --git a/subgraph/package.json b/subgraph/package.json index 0d246cbf26..9db9e970ad 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -10,7 +10,7 @@ "scripts": { "precodegen": "rm -rf ./generated && pnpm generate:schema && pnpm generate:yaml", "codegen": "graph codegen", - "generate:yaml": "mustache ./config/${NETWORK:-optimism-sepolia}.json ./templates/subgraph.template.yaml > subgraph.yaml", + "generate:yaml": "mustache ./config/${NETWORK:-network}.json ./templates/subgraph.template.yaml > subgraph.yaml", "generate:schema": "cp ./schemas/schema.${VERSION:-v1}.graphql schema.graphql", "prebuild": "pnpm codegen", "build": "graph build", diff --git a/website/static/img/offlineProcessing.svg b/website/static/img/offlineProcessing.svg index 0eb156c577..6d96ca36d3 100644 --- a/website/static/img/offlineProcessing.svg +++ b/website/static/img/offlineProcessing.svg @@ -18,4 +18,4 @@ - Local message processing (TS core package)Coordinatorprocess messagesGoalstally votesgenerate inputs for zk-SNARK circuitsProcess2. generate Maci state locally 1. merge the message tree and state treemessage tree holds allmessages posted via aPoll contractstate tree holds all of the signed up users3. process messages2. Calculate the index of the message to processStepsNotesmessages are processed in batches to reduce the computing resources neededmessages are processed in reverse order1. Loop from 0 to batchSize index = batchStartIndex - batchSize - i (loop counter) - 1start from the last item in the batch and work your way down3. Check the message type as the processing will be slightly differentProcess topup messsageProcess vote messsageensure that the stateIndex is validextract the stateLeaf at position stateIndex, then add to the balancethe amount part of the messagewe want to store both the state before and after the action, to feedto the circuit1. generate the ECDH shared key using the coordinator's private key, and the encryption public key2. Decrypt the command into its unencrypted part and its signatureThis step might fail if the provided encryption public key was wrong or the message was encrypted incorrectly - if that happens we return a no-op message to the outer processing loop3. Verify that the signature is valid s4. Verify the nonce5. Extract the voice credit spent and check if there's enoughvoiceCreditsLeft = voiceCreditsBalance + voiceCreditsSpent ** 2 - newVoteWeight ** 2voiceCreditsLeft >= 06. Validate if the vote option is correct >= 0 <= maxVoteOption7. Create a new state leafwith the new public key (if any) and the new voice credit balance 8. Update the ballot with the new votes and the new nonceTa,,y4. Generate circuit inputs1. Process messages2. Generate commitments of the currentstatecurrentPerVOSpentVoiceCreditsCommitmentcurrentSpentVoiceCreditsCommitmentcurrentTallyCommitmentcurrentResultsCommitment3. Process the next batchadd to total votesfor x project the current voteadd to the spent per this vote optionthe squared voteadd to the total spent voice credits the squared votefor each ballot in this batch, loop through all of the vote options and do the processing below4. If this is the last batch then fillthe rest of the ballots array with empty ballots5. Generate random salts to be used while computing the new commitments6. Generate new commitments and circuit inputs \ No newline at end of file + Local message processing (TS core package)Coordinatorprocess messagesGoalstally votesgenerate inputs for zk-SNARK circuitsProcess2. generate Maci state locally 1. merge the message tree and state treemessage tree holds allmessages posted via aPoll contractstate tree holds all of the signed up users3. process messages2. Calculate the index of the message to processStepsNotesmessages are processed in batches to reduce the computing resources neededmessages are processed in reverse order1. Loop from 0 to batchSize index = batchStartIndex - batchSize - i (loop counter) - 1start from the last item in the batch and work your way down3. Check the message type as the processing will be slightly differentProcess topup messsageProcess vote messsageensure that the stateIndex is validextract the stateLeaf at position stateIndex, then add to the balancethe amount part of the messagewe want to store both the state before and after the action, to feedto the circuit1. generate the ECDH shared key using the coordinator's private key, and the encryption public key2. Decrypt the command into its unencrypted part and its signatureThis step might fail if the provided encryption public key was wrong or the message was encrypted incorrectly - if that happens we return a no-op message to the outer processing loop3. Verify that the signature is valid s4. Verify the nonce5. Extract the voice credit spent and check if there's enoughvoiceCreditsLeft = voiceCreditsBalance + voiceCreditsSpent ** 2 - newVoteWeight ** 2voiceCreditsLeft >= 06. Validate if the vote option is correct >= 0 <= maxVoteOption7. Create a new state leafwith the new public key (if any) and the new voice credit balance 8. Update the ballot with the new votes and the new nonceTally4. Generate circuit inputs1. Process messages2. Generate commitments of the currentstatecurrentPerVOSpentVoiceCreditsCommitmentcurrentSpentVoiceCreditsCommitmentcurrentTallyCommitmentcurrentResultsCommitment3. Process the next batchadd to total votesfor x project the current voteadd to the spent per this vote optionthe squared voteadd to the total spent voice credits the squared votefor each ballot in this batch, loop through all of the vote options and do the processing below4. If this is the last batch then fillthe rest of the ballots array with empty ballots5. Generate random salts to be used while computing the new commitments6. Generate new commitments and circuit inputs \ No newline at end of file