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

Implement (first approach) send dappmanager notification #370

Merged
merged 2 commits into from
Oct 1, 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
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 });
});
});