Skip to content

Commit

Permalink
Add brain API (#398)
Browse files Browse the repository at this point in the history
* add brain api

* fix tag imports

* Implement middleware validation

* fix path-to-regexp breaking change

* remove logging
  • Loading branch information
pablomendezroyo authored Oct 14, 2024
1 parent fc30cc7 commit a17ac7c
Show file tree
Hide file tree
Showing 16 changed files with 145 additions and 14 deletions.
2 changes: 1 addition & 1 deletion packages/brain/src/calls/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tag } from "../modules/db/types.js";
import { Tag } from "@stakingbrain/common";

export type ActionRequestOrigin = "ui" | "api";

Expand Down
6 changes: 3 additions & 3 deletions packages/brain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,16 @@ import {
PostgresClient,
PrometheusApi
} from "./modules/apiClients/index.js";
import { startUiServer, startLaunchpadApi } from "./modules/apiServers/index.js";
import { startUiServer, startLaunchpadApi, startBrainApi } from "./modules/apiServers/index.js";
import * as dotenv from "dotenv";
import process from "node:process";
import { params } from "./params.js";
import {
CronJob,
reloadValidators,
// trackValidatorsPerformanceCron,
sendProofsOfValidation,
trackValidatorsPerformanceCron
} from "./modules/cron/index.js";
// import { PostgresClient } from "./modules/apiClients/index.js";
import { brainConfig } from "./modules/config/index.js";

logger.info(`Starting brain...`);
Expand Down Expand Up @@ -99,6 +97,7 @@ export const postgresClient = new PostgresClient(postgresUrl);
// Start server APIs
const uiServer = startUiServer(path.resolve(__dirname, params.uiBuildDirName), network);
const launchpadServer = startLaunchpadApi();
const brainApiServer = startBrainApi();

await brainDb.initialize(signerApi, validatorApi);
logger.debug(brainDb.data);
Expand Down Expand Up @@ -141,6 +140,7 @@ function handle(signal: string): void {
postgresClient.close().catch((err) => logger.error(`Error closing postgres client`, err)); // postgresClient db connection is the only external resource that needs to be closed
uiServer.close();
launchpadServer.close();
brainApiServer.close();
logger.debug(`Stopped all cron jobs and closed all connections.`);
process.exit(0);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/brain/src/modules/apiClients/signer/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tag } from "../../db/types.js";
import { Tag } from "@stakingbrain/common";

export type Web3SignerStatus = "UP" | "DOWN" | "UNKNOWN" | "LOADING" | "ERROR";

Expand Down
3 changes: 3 additions & 0 deletions packages/brain/src/modules/apiServers/brain/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const corsOptions = {
origin: ["http://csm-lido.dappnode", "http://csm-lido.testnet.dappnode"] // TODO: update with DAppNodePackage-lido-csm.dnp.dappnode.eth domains
};
1 change: 1 addition & 0 deletions packages/brain/src/modules/apiServers/brain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { startBrainApi } from "./startBrainApi.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./validators/index.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import validatorsRouter from "./route.js";

export { validatorsRouter };
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Tag } from "@stakingbrain/common";
import express from "express";
import logger from "../../../../logger/index.js";
import { brainDb } from "../../../../../index.js";
import { validateQueryParams } from "./validation.js";
import { RequestParsed } from "./types.js";

const validatorsRouter = express.Router();

const validatorsEndpoint = "/api/v0/brain/validators";

validatorsRouter.get(validatorsEndpoint, validateQueryParams, async (req: RequestParsed, res) => {
const { format, tag } = req.query;

try {
const validators = brainDb.getData();

const tagValidatorsMap = new Map<Tag, string[]>();

for (const [pubkey, details] of Object.entries(validators)) {
if (tag && !tag.includes(details.tag)) continue;

const tagList = tagValidatorsMap.get(details.tag) || [];

if (format === "index") {
if (!details.index) {
logger.warn(
`Validator ${pubkey} does not have an index, a possible cause is that the deposit has not been processed yet`
);
continue;
}
tagList.push(details.index.toString());
} else tagList.push(pubkey);

tagValidatorsMap.set(details.tag, tagList);
}

res.send(Object.fromEntries(tagValidatorsMap));
} catch (e) {
logger.error(e);
res.status(500).send({ message: "Internal server error" });
}
});

export default validatorsRouter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Request } from "express";
import { Tag } from "@stakingbrain/common"; // Assuming this is defined somewhere

// The query parameters before they are parsed or validated
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type
export type RequestReceived = Request<{}, any, any, QueryParamsReceived>;

type QueryParamsReceived = {
format: "pubkey" | "index";
tag?: Tag[] | Tag; // Can be an array or a single value before validation
};

// The query parameters after they are validated and parsed
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type
export type RequestParsed = Request<{}, any, any, QueryParamsParsed>;

type QueryParamsParsed = {
format: "pubkey" | "index";
tag?: Tag[]; // After validation, tag should be an array
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Response, NextFunction } from "express";
import { Tag, tags } from "@stakingbrain/common";
import { RequestReceived } from "./types.js";

// Validation middleware for query parameters
export function validateQueryParams(req: RequestReceived, res: Response, next: NextFunction): void {
const { format, tag } = req.query;

// Validate format
if (!format || (format !== "pubkey" && format !== "index")) {
res.status(400).json({ message: "format is required and must be either 'pubkey' or 'index'" });
return;
}

// Validate tag
if (tag) {
// tag may be of type string or array of strings otherwise return 400
if (typeof tag !== "string" && !Array.isArray(tag)) {
res.status(400).json({ message: "tag must be a string or an array of strings" });
}

// if tag is a string, convert it to an array
const tagsArray = Array.isArray(tag) ? tag : [tag];
const invalidTag = tagsArray.find((t) => !tags.includes(t as Tag));

if (invalidTag) {
res.status(400).json({ message: `invalid tag received: ${invalidTag}. Allowed tags are ${tags.join(", ")}` });
return;
}

// If validation passed, update req.query.tag to ensure it is always an array for downstream middleware
req.query.tag = tagsArray;
}

next(); // Continue to the next middleware or route handler
}
22 changes: 22 additions & 0 deletions packages/brain/src/modules/apiServers/brain/startBrainApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import express from "express";
import cors from "cors";
import logger from "../../logger/index.js";
import http from "node:http";
import { params } from "../../../params.js";
import { corsOptions } from "./config.js";
import { validatorsRouter } from "./routes/index.js";

export function startBrainApi(): http.Server {
const app = express();
app.use(express.json());
app.use(cors(corsOptions));

app.use(validatorsRouter);

const server = new http.Server(app);
server.listen(params.brainPort, () => {
logger.info(`Brain API listening on port ${params.brainPort}`);
});

return server;
}
1 change: 1 addition & 0 deletions packages/brain/src/modules/apiServers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./ui/index.js";
export * from "./launchpad/index.js";
export * from "./brain/index.js";
5 changes: 4 additions & 1 deletion packages/brain/src/modules/apiServers/ui/startUiServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ export function startUiServer(uiBuildPath: string, network: Network): http.Serve
);
app.use(express.json());
app.use(express.static(uiBuildPath));
app.get("*", (req, res) => {
// path-to-regexp (used by express >=5.0.0) has a breaking change
// where it does not allow to use wildcard among other characters
// https://github.com/pillarjs/path-to-regexp?tab=readme-ov-file#errors
app.get("/", (_, res) => {
logger.debug("request received");
res.sendFile(path.join(uiBuildPath, "index.html"));
});
Expand Down
9 changes: 2 additions & 7 deletions packages/brain/src/modules/db/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Tag } from "@stakingbrain/common";

/**
* DbSlot represents the line in the database for a given public key:
* @param pubkey - the public key
Expand Down Expand Up @@ -25,13 +27,6 @@ export interface PubkeyDetails {
index?: number; // index of the validator. Only available if the validator is active.
}

export const tags = ["obol", "diva", "ssv", "rocketpool", "stakewise", "stakehouse", "solo", "stader", "lido"] as const;

export const nonEditableFeeRecipientTags = ["rocketpool", "stader", "stakewise", "lido"] as const;

export type NonEditableFeeRecipientTag = (typeof nonEditableFeeRecipientTags)[number];

/**
* Tag describes the protocol of the public key imported
*/
export type Tag = (typeof tags)[number];
2 changes: 1 addition & 1 deletion packages/brain/src/modules/db/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tag, tags } from "./types.js";
import { Tag, tags } from "@stakingbrain/common";

export function isValidTag(tag: Tag): boolean {
return tags.includes(tag);
Expand Down
1 change: 1 addition & 0 deletions packages/brain/src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const params = {
defaultTag: "solo" as Tag,
uiPort: 80,
launchpadPort: 3000,
brainPort: 5000,
defaultValidatorsMonitorUrl: "https://validators-proofs.dappnode.io",
defaultProofsOfValidationCron: 24 * 60 * 60 * 1000 // 1 day in ms
};

0 comments on commit a17ac7c

Please sign in to comment.