diff --git a/packages/brain/src/modules/apiClients/dappmanager/index.ts b/packages/brain/src/modules/apiClients/dappmanager/index.ts index f17cc338..ca03d81d 100644 --- a/packages/brain/src/modules/apiClients/dappmanager/index.ts +++ b/packages/brain/src/modules/apiClients/dappmanager/index.ts @@ -1,27 +1,23 @@ import logger from "../../logger/index.js"; import { StandardApi } from "../standard.js"; -import { NotificationType } from "./types.js"; +import { Manifest } from "./types.js"; export class DappmanagerApi extends StandardApi { /** - * Triggers a notification in the dappmanager. + * Fetches the notification settings from the dappmanager. */ - public async sendDappmanagerNotification({ - notificationType, - title, - body - }: { - notificationType: NotificationType; - title: string; - body: string; - }): Promise { + public async getPackageManifest({ dnpName }: { dnpName: string }): Promise { try { - await this.request({ - method: "POST", - endpoint: `/notification-send?type=${encodeURIComponent(notificationType)}&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}` + const response = await this.request({ + method: "GET", + endpoint: `/package-manifest/${encodeURIComponent(dnpName)}`, + headers: { + Accept: "application/json" + } }); + return response; } catch (error) { - logger.error("Failed to send notification to dappmanager", error); + logger.error("Failed to fetch notification settings from dappmanager", error); throw error; } } diff --git a/packages/brain/src/modules/apiClients/dappmanager/types.ts b/packages/brain/src/modules/apiClients/dappmanager/types.ts index 69229824..9deccf85 100644 --- a/packages/brain/src/modules/apiClients/dappmanager/types.ts +++ b/packages/brain/src/modules/apiClients/dappmanager/types.ts @@ -1,7 +1,17 @@ -// Must be in sync with dappmanager -export enum NotificationType { - Success = "success", - Info = "info", - Warning = "warning", - Danger = "danger" +export interface Manifest { + notifications?: { + customEndpoints?: CustomEndpoint[]; + }; +} + +export interface CustomEndpoint { + name: string; + enabled: boolean; + description: string; + metric?: { + treshold: number; + min: number; + max: number; + unit: string; + }; } diff --git a/packages/brain/src/modules/apiClients/index.ts b/packages/brain/src/modules/apiClients/index.ts index c16314ba..68ad956f 100644 --- a/packages/brain/src/modules/apiClients/index.ts +++ b/packages/brain/src/modules/apiClients/index.ts @@ -7,10 +7,12 @@ import { Web3SignerApi } from "./signer/index.js"; import { PostgresClient } from "./postgres/index.js"; import { BrainConfig } from "../config/types.js"; import { DappnodeSignatureVerifier } from "./signatureVerifier/index.js"; +import { NotificationsApi } from "./notifications/index.js"; export { BlockExplorerApi, DappmanagerApi, + NotificationsApi, PrometheusApi, BeaconchainApi, ValidatorApi, @@ -27,6 +29,7 @@ export const getApiClients = (brainConfig: BrainConfig) => { validatorUrl, beaconchainUrl, dappmanagerUrl, + notificationsUrl, postgresUrl, token, tlsCert, @@ -60,6 +63,7 @@ export const getApiClients = (brainConfig: BrainConfig) => { ), beaconchainApi: new BeaconchainApi({ baseUrl: beaconchainUrl }, network), dappmanagerApi: new DappmanagerApi({ baseUrl: dappmanagerUrl }, network), - postgresClient: new PostgresClient(postgresUrl) + postgresClient: new PostgresClient(postgresUrl), + notificationsApi: new NotificationsApi({ baseUrl: notificationsUrl }, network) }; }; diff --git a/packages/brain/src/modules/apiClients/notifications/error.ts b/packages/brain/src/modules/apiClients/notifications/error.ts new file mode 100644 index 00000000..eaf78a07 --- /dev/null +++ b/packages/brain/src/modules/apiClients/notifications/error.ts @@ -0,0 +1,8 @@ +import { ApiError } from "../error.js"; + +export class NotificationsApiError extends ApiError { + constructor(message: string) { + super(message); + this.name = "NotificationsApiError"; + } +} diff --git a/packages/brain/src/modules/apiClients/notifications/index.ts b/packages/brain/src/modules/apiClients/notifications/index.ts new file mode 100644 index 00000000..ce190d8a --- /dev/null +++ b/packages/brain/src/modules/apiClients/notifications/index.ts @@ -0,0 +1,25 @@ +import logger from "../../logger/index.js"; +import { StandardApi } from "../standard.js"; +import { NotificationPayload } from "./types.js"; + +export class NotificationsApi extends StandardApi { + /** + * Sends a notification to the notifier service of the notifications pkg. + */ + public async sendNotification({ notificationPayload }: { notificationPayload: NotificationPayload }): Promise { + try { + await this.request({ + method: "POST", + endpoint: `/api/v1/notification`, + body: JSON.stringify(notificationPayload), + headers: { + "Content-Type": "application/json", + Accept: "application/json" + } + }); + } catch (error) { + logger.error("Failed to send notification to dappmanager", error); + throw error; + } + } +} diff --git a/packages/brain/src/modules/apiClients/notifications/types.ts b/packages/brain/src/modules/apiClients/notifications/types.ts new file mode 100644 index 00000000..ad6e2fc7 --- /dev/null +++ b/packages/brain/src/modules/apiClients/notifications/types.ts @@ -0,0 +1,11 @@ +export interface NotificationPayload { + title: string; + body: string; + category: string; + dnpName: string; + callToAction?: { + title: string; + url: string; + }; + errors?: string; +} diff --git a/packages/brain/src/modules/config/types.ts b/packages/brain/src/modules/config/types.ts index c148e27d..d8078959 100644 --- a/packages/brain/src/modules/config/types.ts +++ b/packages/brain/src/modules/config/types.ts @@ -13,6 +13,7 @@ export interface ApisConfig { beaconchainUrl: string; executionClientUrl: string; dappmanagerUrl: string; + notificationsUrl: string; postgresUrl: string; token: string; tlsCert: Buffer | null; diff --git a/packages/brain/src/modules/cron/sendValidatorsNotifications/index.ts b/packages/brain/src/modules/cron/sendValidatorsNotifications/index.ts new file mode 100644 index 00000000..a51ff4c0 --- /dev/null +++ b/packages/brain/src/modules/cron/sendValidatorsNotifications/index.ts @@ -0,0 +1,43 @@ +import { Network } from "@stakingbrain/common"; +import { BeaconchainApi, DappmanagerApi, NotificationsApi } from "../../apiClients/index.js"; +import { getWeb3signerDnpName } from "./params.js"; +import logger from "../../logger/index.js"; +import { logPrefix } from "./logPrefix.js"; +import { BrainDataBase } from "../../db/index.js"; +import { sendValidatorsOfflineNotification } from "./sendValidatorsOfflineNotification.js"; + +export async function sendValidatorsNotifications({ + network, + brainDb, + beaconchainApi, + dappmanagerApi, + notificationsApi +}: { + network: Network; + brainDb: BrainDataBase; + beaconchainApi: BeaconchainApi; + dappmanagerApi: DappmanagerApi; + notificationsApi: NotificationsApi; +}): Promise { + // Get web3signer notifications settings from dappmanager API + const web3signerDnpName = getWeb3signerDnpName(network); + const manifest = await dappmanagerApi.getPackageManifest({ dnpName: web3signerDnpName }); + + const customEndpoints = manifest.notifications?.customEndpoints; + + if (!customEndpoints?.length) { + logger.warn(`${logPrefix}No notification settings found for ${web3signerDnpName}. Notifications will not be sent.`); + return; + } + + // Send notifications if any + + // validator offline + const validatorOfflineNotificationEnabled = customEndpoints.some( + (endpoint) => endpoint.name === "Validator offline" && endpoint.enabled + ); + if (validatorOfflineNotificationEnabled) + await sendValidatorsOfflineNotification(brainDb, beaconchainApi, notificationsApi); + + // validator liveness +} diff --git a/packages/brain/src/modules/cron/sendValidatorsNotifications/logPrefix.ts b/packages/brain/src/modules/cron/sendValidatorsNotifications/logPrefix.ts new file mode 100644 index 00000000..c9272f9a --- /dev/null +++ b/packages/brain/src/modules/cron/sendValidatorsNotifications/logPrefix.ts @@ -0,0 +1 @@ +export const logPrefix = "[CRON - sendValidatorsNotification]: "; diff --git a/packages/brain/src/modules/cron/sendValidatorsNotifications/params.ts b/packages/brain/src/modules/cron/sendValidatorsNotifications/params.ts new file mode 100644 index 00000000..1e580bb0 --- /dev/null +++ b/packages/brain/src/modules/cron/sendValidatorsNotifications/params.ts @@ -0,0 +1,20 @@ +import { Network } from "@stakingbrain/common"; + +export const getWeb3signerDnpName = (network: Network): string => { + switch (network) { + case Network.Mainnet: + return "web3signer.dnp.dappnode.eth"; + case Network.Gnosis: + return "web3signer-gnosis.dnp.dappnode.eth"; + case Network.Holesky: + return "web3signer-holesky.dnp.dapopnode.eth"; + case Network.Prater: + return "web3signer-prater.dnp.dappnode.eth"; + case Network.Lukso: + return "web3signer-lukso.dnp.dappnode.eth"; + case Network.Hoodi: + return "web3signer-hoodi.dnp.dappnode.eth"; + default: + throw new Error("Invalid network"); + } +}; diff --git a/packages/brain/src/modules/cron/sendValidatorsNotifications/sendValidatorsOfflineNotification.ts b/packages/brain/src/modules/cron/sendValidatorsNotifications/sendValidatorsOfflineNotification.ts new file mode 100644 index 00000000..ea71629d --- /dev/null +++ b/packages/brain/src/modules/cron/sendValidatorsNotifications/sendValidatorsOfflineNotification.ts @@ -0,0 +1,8 @@ +import { BrainDataBase } from "../../db/index.js"; +import { BeaconchainApi, NotificationsApi } from "../../apiClients/index.js"; + +export async function sendValidatorsOfflineNotification( + brainDb: BrainDataBase, + beaconchainApi: BeaconchainApi, + notificationsApi: NotificationsApi +): Promise {}