Skip to content

Commit

Permalink
Implement (first approach) send dappmanager notification (#370)
Browse files Browse the repository at this point in the history
* Implement send dappmanager notification

* rename to insertPerformanceDataAndSendNotification and relocate
  • Loading branch information
pablomendezroyo authored Oct 1, 2024
1 parent 2ce7722 commit 79faebe
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 11 deletions.
8 changes: 6 additions & 2 deletions packages/brain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
BeaconchainApi,
BlockExplorerApi,
ValidatorApi,
DappnodeSignatureVerifier
DappnodeSignatureVerifier,
DappmanagerApi
} from "./modules/apiClients/index.js";
import { startUiServer, startLaunchpadApi } from "./modules/apiServers/index.js";
import * as dotenv from "dotenv";
Expand Down Expand Up @@ -75,6 +76,7 @@ export const validatorApi = new ValidatorApi(
);
export const beaconchainApi = new BeaconchainApi({ baseUrl: beaconchainUrl }, network);
export const dappnodeSignatureVerifierApi = new DappnodeSignatureVerifier(network, validatorsMonitorUrl);
export const dappmanagerApi = new DappmanagerApi({ baseUrl: "http://my.dappnode" }, network);

// Create DB instance
export const brainDb = new BrainDataBase(
Expand Down Expand Up @@ -111,7 +113,9 @@ export const trackValidatorsPerformanceCronTask = new CronJob(
postgresClient,
beaconchainApi,
executionClient,
consensusClient
consensusClient,
dappmanagerApi,
sendNotification: true
});
}
);
Expand Down
8 changes: 8 additions & 0 deletions packages/brain/src/modules/apiClients/dappmanager/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApiError } from "../error.js";

export class DappmanagerApiError extends ApiError {
constructor(message: string) {
super(message);
this.name = "DappmanagerApiError";
}
}
28 changes: 28 additions & 0 deletions packages/brain/src/modules/apiClients/dappmanager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import logger from "../../logger/index.js";
import { StandardApi } from "../standard.js";
import { NotificationType } from "./types.js";

export class DappmanagerApi extends StandardApi {
/**
* Triggers a notification in the dappmanager.
*/
public async sendDappmanagerNotification({
notificationType,
title,
body
}: {
notificationType: NotificationType;
title: string;
body: string;
}): Promise<void> {
try {
await this.request({
method: "POST",
endpoint: `/notification-send?type=${encodeURIComponent(notificationType)}&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`
});
} catch (error) {
logger.error("Failed to send notification to dappmanager", error);
throw error;
}
}
}
7 changes: 7 additions & 0 deletions packages/brain/src/modules/apiClients/dappmanager/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Must be in sync with dappmanager
export enum NotificationType {
Success = "success",
Info = "info",
Warning = "warning",
Danger = "danger"
}
1 change: 1 addition & 0 deletions packages/brain/src/modules/apiClients/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { BlockExplorerApi } from "./blockExplorer/index.js";
export { DappmanagerApi } from "./dappmanager/index.js";
export { BeaconchainApi } from "./beaconchain/index.js";
export { ValidatorApi } from "./validator/index.js";
export { StandardApi } from "./standard.js";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ConsensusClient, ExecutionClient } from "@stakingbrain/common";
import { PostgresClient } from "../../apiClients/index.js";
import { DappmanagerApi, PostgresClient } from "../../apiClients/index.js";
import {
BlockProposalStatus,
ValidatorPerformance,
Expand All @@ -9,6 +9,7 @@ import {
import { TotalRewards } from "../../apiClients/types.js";
import logger from "../../logger/index.js";
import { logPrefix } from "./logPrefix.js";
import { sendValidatorsPerformanceNotifications } from "./sendValidatorsPerformanceNotifications.js";

/**
* Insert the performance data for the validators in the Postgres DB. On any error
Expand All @@ -21,7 +22,9 @@ import { logPrefix } from "./logPrefix.js";
* @param validatorBlockStatusMap - Map with the block proposal status of each validator.
* @param validatorsAttestationsTotalRewards - Array of total rewards for the validators.
*/
export async function insertPerformanceData({
export async function insertPerformanceDataAndSendNotification({
sendNotification,
dappmanagerApi,
postgresClient,
activeValidatorsIndexes,
currentEpoch,
Expand All @@ -31,6 +34,8 @@ export async function insertPerformanceData({
consensusClient,
error
}: {
sendNotification: boolean;
dappmanagerApi: DappmanagerApi;
postgresClient: PostgresClient;
activeValidatorsIndexes: string[];
currentEpoch: number;
Expand Down Expand Up @@ -104,6 +109,15 @@ export async function insertPerformanceData({
attestationsTotalRewards
}
});

await sendValidatorsPerformanceNotifications({
sendNotification,
dappmanagerApi,
currentEpoch: currentEpoch.toString(),
validatorBlockStatusMap,
validatorsAttestationsTotalRewards,
error
});
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { DappmanagerApi } from "../../apiClients/index.js";
import { NotificationType } from "../../apiClients/dappmanager/types.js";
import { BlockProposalStatus, ValidatorPerformanceError } from "../../apiClients/postgres/types.js";
import logger from "../../logger/index.js";
import { logPrefix } from "./logPrefix.js";
import { TotalRewards } from "../../apiClients/types.js";

/**
* Sends validator performance notification to the dappmanager. The notification will have the following format:
* ```
* **Validator(s) performance notification for epoch **
* - Blocks:
* - Proposed: Validator(s) proposed a block
* - Missed: Validator(s) missed a block
* - Attestations
* - Error
* ```
*/
export async function sendValidatorsPerformanceNotifications({
sendNotification,
dappmanagerApi,
currentEpoch,
validatorBlockStatusMap,
validatorsAttestationsTotalRewards,
error
}: {
sendNotification: boolean;
dappmanagerApi: DappmanagerApi;
currentEpoch: string;
validatorBlockStatusMap: Map<string, BlockProposalStatus>;
validatorsAttestationsTotalRewards: TotalRewards[];
error?: ValidatorPerformanceError;
}): Promise<void> {
if (!sendNotification) return;
if (error)
await dappmanagerApi.sendDappmanagerNotification({
title: "Failed to fetch performance data",
notificationType: NotificationType.Danger,
body: `Failed to fetch performance data for epoch ${currentEpoch}: ${error}`
});
else {
await Promise.all([
sendSuccessNotificationNotThrow({ dappmanagerApi, validatorBlockStatusMap, currentEpoch }),
sendWarningNotificationNotThrow({
dappmanagerApi,
validatorBlockStatusMap,
validatorsAttestationsTotalRewards,
currentEpoch
})
]);
}
}

/**
* Triggers sending success notification in the dappmanager if any:
* - blocks proposed
*/
async function sendSuccessNotificationNotThrow({
dappmanagerApi,
validatorBlockStatusMap,
currentEpoch
}: {
dappmanagerApi: DappmanagerApi;
validatorBlockStatusMap: Map<string, BlockProposalStatus>;
currentEpoch: string;
}): Promise<void> {
const validatorsProposedBlocks = Array.from(validatorBlockStatusMap).filter(
([_, blockStatus]) => blockStatus === "Proposed"
);

if (validatorsProposedBlocks.length === 0) return;
await dappmanagerApi
.sendDappmanagerNotification({
title: `Validator(s) proposed a block in epoch ${currentEpoch}`,
notificationType: NotificationType.Success,
body: `Validator(s) ${validatorsProposedBlocks.join(", ")} proposed a block`
})
.catch((error) => logger.error(`${logPrefix}Failed to send success notification to dappmanager`, error));
}

/**
* Triggers sending warning notification in the dappmanager if any:
* - blocks missed
* - attestations missed
*/
async function sendWarningNotificationNotThrow({
dappmanagerApi,
validatorBlockStatusMap,
validatorsAttestationsTotalRewards,
currentEpoch
}: {
dappmanagerApi: DappmanagerApi;
validatorBlockStatusMap: Map<string, BlockProposalStatus>;
validatorsAttestationsTotalRewards: TotalRewards[];
currentEpoch: string;
}): Promise<void> {
// Send the warning notification together: block missed and att missed
const validatorsMissedBlocks = Array.from(validatorBlockStatusMap).filter(
([_, blockStatus]) => blockStatus === "Missed"
);
const validatorsMissedAttestations = validatorsAttestationsTotalRewards
.filter((validator) => parseInt(validator.source) === 0)
.map((validator) => validator.validator_index);

if (validatorsMissedBlocks.length === 0 && validatorsMissedAttestations.length === 0) return;
await dappmanagerApi
.sendDappmanagerNotification({
title: `Validator(s) missed a block or attestation in epoch ${currentEpoch}`,
notificationType: NotificationType.Warning,
body: `Validator(s) ${validatorsMissedBlocks.join(", ")} missed a block. Validator(s) ${validatorsMissedAttestations.join(
", "
)} missed an attestation`
})
.catch((error) => logger.error(`${logPrefix}Failed to send warning notification to dappmanager`, error));
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ import { BeaconchainApi } from "../../apiClients/beaconchain/index.js";
import { PostgresClient } from "../../apiClients/postgres/index.js";
import logger from "../../logger/index.js";
import { BrainDataBase } from "../../db/index.js";
import { insertPerformanceData } from "./insertPerformanceData.js";
import { insertPerformanceDataAndSendNotification } from "./insertPerformanceDataAndSendNotification.js";
import { getAttestationsTotalRewards } from "./getAttestationsTotalRewards.js";
import { getBlockProposalStatusMap } from "./getBlockProposalStatusMap.js";
import { getActiveValidatorsLoadedInBrain } from "./getActiveValidatorsLoadedInBrain.js";
import { logPrefix } from "./logPrefix.js";
import { ConsensusClient, ExecutionClient } from "@stakingbrain/common";
import { TotalRewards } from "../../apiClients/types.js";
import { ValidatorPerformanceError, ValidatorPerformanceErrorCode } from "../../apiClients/postgres/types.js";
import {
BlockProposalStatus,
ValidatorPerformanceError,
ValidatorPerformanceErrorCode
} from "../../apiClients/postgres/types.js";
import { BeaconchainApiError } from "../../apiClients/beaconchain/error.js";
import { BrainDbError } from "../../db/error.js";
import { ExecutionOfflineError, NodeSyncingError } from "./error.js";
import { DappmanagerApi } from "../../apiClients/index.js";

let lastProcessedEpoch: number | undefined = undefined;
let lastEpochProcessedWithError = false;
Expand All @@ -22,13 +27,17 @@ export async function trackValidatorsPerformanceCron({
postgresClient,
beaconchainApi,
executionClient,
consensusClient
consensusClient,
dappmanagerApi,
sendNotification
}: {
brainDb: BrainDataBase;
postgresClient: PostgresClient;
beaconchainApi: BeaconchainApi;
executionClient: ExecutionClient;
consensusClient: ConsensusClient;
dappmanagerApi: DappmanagerApi;
sendNotification: boolean;
}): Promise<void> {
try {
// Get finalized epoch from finality endpoint instead of from header endpoint.
Expand All @@ -50,7 +59,9 @@ export async function trackValidatorsPerformanceCron({
beaconchainApi,
executionClient,
consensusClient,
currentEpoch
currentEpoch,
dappmanagerApi,
sendNotification
});
lastProcessedEpoch = currentEpoch;
}
Expand All @@ -65,18 +76,22 @@ export async function fetchAndInsertPerformanceCron({
beaconchainApi,
executionClient,
consensusClient,
currentEpoch
currentEpoch,
dappmanagerApi,
sendNotification
}: {
brainDb: BrainDataBase;
postgresClient: PostgresClient;
beaconchainApi: BeaconchainApi;
executionClient: ExecutionClient;
consensusClient: ConsensusClient;
currentEpoch: number;
dappmanagerApi: DappmanagerApi;
sendNotification: boolean;
}): Promise<void> {
let validatorPerformanceError: ValidatorPerformanceError | undefined;
let activeValidatorsIndexes: string[] = [];
let validatorBlockStatusMap = new Map();
let validatorBlockStatusMap: Map<string, BlockProposalStatus> = new Map();
let validatorsAttestationsTotalRewards: TotalRewards[] = [];

try {
Expand Down Expand Up @@ -117,7 +132,9 @@ export async function fetchAndInsertPerformanceCron({
lastEpochProcessedWithError = true;
} finally {
// Always call storeData in the finally block, regardless of success or failure in try block
await insertPerformanceData({
await insertPerformanceDataAndSendNotification({
sendNotification,
dappmanagerApi,
postgresClient,
activeValidatorsIndexes,
currentEpoch,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Network } from "@stakingbrain/common";
import { DappmanagerApi } from "../../../../src/modules/apiClients/index.js";
import { NotificationType } from "../../../../src/modules/apiClients/dappmanager/types.js";

describe.skip("Dappmanager API", () => {
const dappmanagerApi = new DappmanagerApi({ baseUrl: "http://my.dappnode" }, Network.Holesky);

it("should send a notification to the dappmanager", async () => {
const title = "Test title";
const body = "Test body";

await dappmanagerApi.sendDappmanagerNotification({ notificationType: NotificationType.Success, title, body });
});
});

0 comments on commit 79faebe

Please sign in to comment.