Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement notifications in error handling logic (#52)
Browse files Browse the repository at this point in the history
jahabeebs authored and 0xyaco committed Oct 14, 2024
1 parent 0d7fd66 commit b7a9920
Showing 21 changed files with 5,249 additions and 6,445 deletions.
2 changes: 2 additions & 0 deletions apps/agent/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -67,4 +67,6 @@ export const config = {
},
},
processor: { ...configData.processor },
DISCORD_BOT_TOKEN: envData.DISCORD_BOT_TOKEN,
DISCORD_CHANNEL_ID: envData.DISCORD_CHANNEL_ID,
} as const;
2 changes: 2 additions & 0 deletions apps/agent/src/config/schemas.ts
Original file line number Diff line number Diff line change
@@ -30,6 +30,8 @@ export const envSchema = z.object({
BLOCK_NUMBER_RPC_URLS_MAP: stringToJSONSchema.pipe(chainRpcUrlSchema),
BLOCK_NUMBER_BLOCKMETA_TOKEN: z.string(),
EBO_AGENT_CONFIG_FILE_PATH: z.string(),
DISCORD_BOT_TOKEN: z.string(),
DISCORD_CHANNEL_ID: z.string(),
});

const addressSchema = z.string().refine((address) => isAddress(address));
10 changes: 10 additions & 0 deletions apps/agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { inspect } from "util";
import { isNativeError } from "util/types";
import { EboActorsManager, EboProcessor, ProtocolProvider } from "@ebo-agent/automated-dispute";
import { DiscordNotifier } from "@ebo-agent/automated-dispute/src/services/index.js";
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Logger } from "@ebo-agent/shared";

@@ -23,12 +24,21 @@ const main = async (): Promise<void> => {

const actorsManager = new EboActorsManager();

const notifier = await DiscordNotifier.create(
{
discordBotToken: config.DISCORD_BOT_TOKEN,
discordChannelId: config.DISCORD_CHANNEL_ID,
},
logger,
);

const processor = new EboProcessor(
config.processor.accountingModules,
protocolProvider,
blockNumberService,
actorsManager,
logger,
notifier,
);

await processor.start(config.processor.msBetweenChecks);
4 changes: 3 additions & 1 deletion packages/automated-dispute/package.json
Original file line number Diff line number Diff line change
@@ -26,7 +26,9 @@
"@ebo-agent/blocknumber": "workspace:*",
"@ebo-agent/shared": "workspace:*",
"async-mutex": "0.5.0",
"discord.js": "14.16.2",
"heap-js": "2.5.0",
"viem": "2.17.11"
"viem": "2.17.11",
"zod": "3.23.8"
}
}
8 changes: 8 additions & 0 deletions packages/automated-dispute/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from "zod";

const ConfigSchema = z.object({
DISCORD_BOT_TOKEN: z.string().min(1),
DISCORD_CHANNEL_ID: z.string().min(1),
});

export const config = ConfigSchema.parse(process.env);
26 changes: 13 additions & 13 deletions packages/automated-dispute/src/exceptions/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { ILogger } from "@ebo-agent/shared";

import { CustomContractError } from "../exceptions/index.js";
import { ErrorContext } from "../types/index.js";
import { NotificationService } from "../services/index.js";

export class ErrorHandler {
public static async handle(error: CustomContractError, logger: ILogger): Promise<void> {
private notificationService: NotificationService;
private logger: ILogger;

constructor(notificationService: NotificationService, logger: ILogger) {
this.notificationService = notificationService;
this.logger = logger;
}

public async handle(error: CustomContractError): Promise<void> {
const strategy = error.strategy;
const context = error.getContext();

logger.error(`Error occurred: ${error.message}`);
this.logger.error(`Error occurred: ${error.message}`);

try {
await error.executeCustomAction();
} catch (actionError) {
logger.error(`Error executing custom action: ${actionError}`);
this.logger.error(`Error executing custom action: ${actionError}`);
} finally {
if (strategy.shouldNotify) {
await this.notifyError(error, context);
await this.notificationService.notifyError(error, context);
}

if (strategy.shouldReenqueue && context.reenqueueEvent) {
@@ -28,12 +36,4 @@ export class ErrorHandler {
}
}
}

private static async notifyError(
error: CustomContractError,
context: ErrorContext,
): Promise<void> {
// TODO: notification logic
console.log(error, context);
}
}
91 changes: 91 additions & 0 deletions packages/automated-dispute/src/services/discordNotifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { ILogger } from "@ebo-agent/shared";
import { Client, IntentsBitField, TextChannel } from "discord.js";
import { stringify } from "viem";

import { NotificationService } from "./notificationService.js";

interface DiscordNotifierConfig {
discordBotToken: string;
discordChannelId: string;
}

/**
* A notifier class for sending error notifications to a Discord channel.
*/
export class DiscordNotifier implements NotificationService {
private client: Client;
private config: DiscordNotifierConfig;
private logger: ILogger;

private constructor(client: Client, config: DiscordNotifierConfig, logger: ILogger) {
this.client = client;
this.config = config;
this.logger = logger;
}

/**
* Creates an instance of the DiscordNotifier.
* @param {DiscordNotifierConfig} config - The configuration object for the DiscordNotifier.
* @param {ILogger} logger - The logger instance.
* @returns {Promise<DiscordNotifier>} A promise that resolves to a DiscordNotifier instance.
*/
public static async create(
config: DiscordNotifierConfig,
logger: ILogger,
): Promise<DiscordNotifier> {
const intents = new IntentsBitField().add(
IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.GuildMessages,
);
const client = new Client({ intents });

try {
await client.login(config.discordBotToken);
await new Promise<void>((resolve) => {
client.once("ready", () => {
logger.info("Discord bot is ready");
resolve();
});
});
} catch (error) {
logger.error(`FFailed to initialize Discord notifier: ${error}`);
throw error;
}

return new DiscordNotifier(client, config, logger);
}

/**
* Sends an error notification to the specified Discord channel.
* @param {Error} error - The error to notify about.
* @param {any} context - Additional context information.
* @returns {Promise<void>} A promise that resolves when the message is sent.
*/
public async notifyError(error: Error, context: any): Promise<void> {
try {
const channel = await this.client.channels.fetch(this.config.discordChannelId);
if (!channel || !channel.isTextBased()) {
throw new Error("Discord channel not found or is not text-based");
}
const errorMessage = this.formatErrorMessage(error, context);
await (channel as TextChannel).send(errorMessage);
this.logger.info("Error notification sent to Discord");
} catch (err) {
this.logger.error(`Failed to send error notification to Discord: ${err}`);
}
}

/**
* Formats the error message to be sent to Discord.
* @param {Error} error - The error object.
* @param {any} context - Additional context information.
* @returns {string} The formatted error message.
*/
private formatErrorMessage(error: Error, context: unknown): string {
return `**Error:** ${error.name} - ${error.message}\n**Context:**\n\`\`\`json\n${stringify(
context,
null,
2,
)}\n\`\`\``;
}
}
31 changes: 13 additions & 18 deletions packages/automated-dispute/src/services/eboActor.ts
Original file line number Diff line number Diff line change
@@ -37,9 +37,10 @@ import {
AddRequest,
AddResponse,
FinalizeRequest,
NotificationService,
UpdateDisputeStatus,
} from "../services/index.js";
import { ActorRequest } from "../types/actorRequest.js";
import { ActorRequest } from "../types/index.js";

/**
* Compare function to sort events chronologically in ascending order by block number
@@ -75,16 +76,20 @@ export class EboActor {
*/
private readonly eventsQueue: Heap<EboEvent<EboEventName>>;
private lastEventProcessed: EboEvent<EboEventName> | undefined;
private errorHandler: ErrorHandler;

/**
* Creates an `EboActor` instance.
*
* @param actorRequest.id request ID this actor will handle
* @param actorRequest.epoch requested epoch
* @param actorRequest
* @param protocolProvider a `ProtocolProvider` instance
* @param blockNumberService a `BlockNumberService` instance
* @param registry an `EboRegistry` instance
* @param eventProcessingMutex
* @param logger an `ILogger` instance
* @param notificationService for notifying about any agent or contract errors
*/
constructor(
public readonly actorRequest: ActorRequest,
@@ -93,8 +98,10 @@ export class EboActor {
private readonly registry: EboRegistry,
private readonly eventProcessingMutex: Mutex,
private readonly logger: ILogger,
private readonly notificationService: NotificationService,
) {
this.eventsQueue = new Heap(EBO_EVENT_COMPARATOR);
this.errorHandler = new ErrorHandler(this.notificationService, this.logger);
}

/**
@@ -169,12 +176,7 @@ export class EboActor {
},
);

await ErrorHandler.handle(err, this.logger);

if (err.strategy.shouldNotify) {
// TODO: add notification logic
continue;
}
await this.errorHandler.handle(err);
return;
} else {
throw err;
@@ -402,8 +404,7 @@ export class EboActor {
dispute.prophetData,
);
this.logger.info(`Dispute ${dispute.id} escalated.`);

await ErrorHandler.handle(customError, this.logger);
await this.errorHandler.handle(customError);
} catch (escalationError) {
this.logger.error(
`Failed to escalate dispute ${dispute.id}: ${escalationError}`,
@@ -619,8 +620,7 @@ export class EboActor {
};
customError.setContext(context);

await ErrorHandler.handle(customError, this.logger);

await this.errorHandler.handle(customError);
this.logger.warn(
`Block ${responseBody.block} for epoch ${request.epoch} and ` +
`chain ${chainId} was not proposed. Skipping proposal...`,
@@ -807,7 +807,7 @@ export class EboActor {
};
customError.setContext(context);

await ErrorHandler.handle(customError, this.logger);
await this.errorHandler.handle(customError);
} else {
throw err;
}
@@ -845,7 +845,7 @@ export class EboActor {
};
customError.setContext(context);

await ErrorHandler.handle(customError, this.logger);
await this.errorHandler.handle(customError);
} else {
throw err;
}
@@ -890,9 +890,6 @@ export class EboActor {

private async onDisputeEscalated(event: EboEvent<"DisputeEscalated">) {
const request = this.getActorRequest();

// TODO: notify

this.logger.info(
`Dispute ${event.metadata.disputeId} for request ${request.id} has been escalated.`,
);
@@ -909,8 +906,6 @@ export class EboActor {
//
// This actor will just wait until the proposal window ends.
this.logger.warn(err.message);

// TODO: notify
} else {
this.logger.error(
`Could not handle dispute ${disputeId} changing to NoResolution status.`,
3 changes: 3 additions & 0 deletions packages/automated-dispute/src/services/eboActorsManager.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import { ProtocolProvider } from "../providers/protocolProvider.js";
import { ActorRequest, RequestId } from "../types/index.js";
import { EboActor } from "./eboActor.js";
import { EboMemoryRegistry } from "./eboRegistry/eboMemoryRegistry.js";
import { NotificationService } from "./notificationService.js";

export class EboActorsManager {
private readonly requestActorMap: Map<RequestId, EboActor>;
@@ -40,6 +41,7 @@ export class EboActorsManager {
protocolProvider: ProtocolProvider,
blockNumberService: BlockNumberService,
logger: ILogger,
notifier: NotificationService,
): EboActor {
const requestId = actorRequest.id;

@@ -56,6 +58,7 @@ export class EboActorsManager {
registry,
eventProcessingMutex,
logger,
notifier,
);

this.requestActorMap.set(requestId, actor);
Loading

0 comments on commit b7a9920

Please sign in to comment.