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

fix: use webhooks for discord notification #84

Merged
merged 13 commits into from
Nov 6, 2024
7 changes: 2 additions & 5 deletions apps/agent/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,5 @@ BLOCK_NUMBER_BLOCKMETA_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuaWNlIjoidH
# Path to the agent YAML configuration file
EBO_AGENT_CONFIG_FILE_PATH="./config.example.yml"

# Discord bot token for notifications
DISCORD_BOT_TOKEN=YOUR_DISCORD_BOT_TOKEN

# Discord channel ID for notifications
DISCORD_CHANNEL_ID=YOUR_DISCORD_CHANNEL_ID
# Discord webhook notifications
DISCORD_WEBHOOK=YOUR_DISCORD_WEBHOOK
19 changes: 9 additions & 10 deletions apps/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,15 @@ cp .env.example .env

**Environment Variables**:

| Variable | Description | Required |
| ------------------------------- | ------------------------------------------------------------------------- | -------- |
| `PROTOCOL_PROVIDER_PRIVATE_KEY` | Private key for the Protocol Provider | Yes |
| `PROTOCOL_PROVIDER_L1_RPC_URLS` | Comma-separated URLs for Layer 1 RPC endpoints | Yes |
| `PROTOCOL_PROVIDER_L2_RPC_URLS` | Comma-separated URLs for Layer 2 RPC endpoints | Yes |
| `BLOCK_NUMBER_RPC_URLS_MAP` | JSON map of chain IDs to arrays of RPC URLs for Block Number calculations | Yes |
| `BLOCK_NUMBER_BLOCKMETA_TOKEN` | Bearer token for the Blockmeta service (see notes below on how to obtain) | Yes |
| `EBO_AGENT_CONFIG_FILE_PATH` | Path to the agent YAML configuration file | Yes |
| `DISCORD_BOT_TOKEN` | Your Discord bot’s token | Yes |
| `DISCORD_CHANNEL_ID` | Discord channel ID for notifications | Yes |
| Variable | Description | Required |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `PROTOCOL_PROVIDER_PRIVATE_KEY` | Private key for the Protocol Provider | Yes |
| `PROTOCOL_PROVIDER_L1_RPC_URLS` | Comma-separated URLs for Layer 1 RPC endpoints | Yes |
| `PROTOCOL_PROVIDER_L2_RPC_URLS` | Comma-separated URLs for Layer 2 RPC endpoints | Yes |
| `BLOCK_NUMBER_RPC_URLS_MAP` | JSON map of chain IDs to arrays of RPC URLs for Block Number calculations | Yes |
| `BLOCK_NUMBER_BLOCKMETA_TOKEN` | Bearer token for the Blockmeta service (see notes below on how to obtain) | Yes |
| `EBO_AGENT_CONFIG_FILE_PATH` | Path to the agent YAML configuration file | Yes |
| `DISCORD_WEBHOOK` | Your Discord channel webhook for notifications [Learn how to create a Discord webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) | No |

**Notes:**

Expand Down
3 changes: 3 additions & 0 deletions apps/agent/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ processor:
requestModule: "0x1234567890123456789012345678901234567890" # Address of the Request module
responseModule: "0x1234567890123456789012345678901234567890" # Address of the Response module
escalationModule: "0x1234567890123456789012345678901234567890" # Address of the Escalation module

notifier:
discordDefaultAvatarUrl: "https://cryptologos.cc/logos/the-graph-grt-logo.png" # Default avatar URL for Discord notifications
3 changes: 1 addition & 2 deletions apps/agent/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,5 @@ export const config = {
},
},
processor: { ...configData.processor },
DISCORD_BOT_TOKEN: envData.DISCORD_BOT_TOKEN,
DISCORD_CHANNEL_ID: envData.DISCORD_CHANNEL_ID,
DISCORD_WEBHOOK: envData.DISCORD_WEBHOOK,
} as const;
3 changes: 1 addition & 2 deletions apps/agent/src/config/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ 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(),
DISCORD_WEBHOOK: z.string(),
});

const addressSchema = z.string().refine((address) => isAddress(address));
Expand Down
14 changes: 4 additions & 10 deletions apps/agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { inspect } from "util";
import { isNativeError } from "util/types";
import {
DiscordNotifier,
EboActorsManager,
EboProcessor,
NotificationService,
ProtocolProvider,
} from "@ebo-agent/automated-dispute";
import { BlockNumberService } from "@ebo-agent/blocknumber";
Expand Down Expand Up @@ -39,17 +39,11 @@ const main = async (): Promise<void> => {

logger.debug("Protocol provider initialized.");

const discordConfig = {
discordBotToken: config.DISCORD_BOT_TOKEN,
discordChannelId: config.DISCORD_CHANNEL_ID,
};

logger.debug("Initializing notifier...");
logger.debug(stringify(discordConfig));

// const notifier = await DiscordNotifier.create(discordConfig, logger);
// FIXME: during E2E DiscordNotifier is not able to start even if setting a valid token
const notifier = { notifyError: (_e, _ctx) => {} } as NotificationService;
const notifier = new DiscordNotifier(config.DISCORD_WEBHOOK, logger);

logger.debug("Notifier initialized...");

const actorsManager = new EboActorsManager();

Expand Down
6 changes: 2 additions & 4 deletions apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ describe.sequential("single agent", () => {
BLOCK_NUMBER_RPC_URLS_MAP: new Map<Caip2ChainId, string[]>([
[PROTOCOL_L2_CHAIN_ID, [PROTOCOL_L2_LOCAL_URL]],
]),
DISCORD_BOT_TOKEN: "",
DISCORD_CHANNEL_ID: "",
DISCORD_WEBHOOK: "",
},
});

Expand Down Expand Up @@ -514,8 +513,7 @@ describe.sequential("single agent", () => {
BLOCK_NUMBER_RPC_URLS_MAP: new Map<Caip2ChainId, string[]>([
[PROTOCOL_L2_CHAIN_ID, [PROTOCOL_L2_LOCAL_URL]],
]),
DISCORD_BOT_TOKEN: "",
DISCORD_CHANNEL_ID: "",
DISCORD_WEBHOOK: "",
},
});

Expand Down
3 changes: 1 addition & 2 deletions packages/automated-dispute/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { z } from "zod";

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

export const config = ConfigSchema.parse(process.env);
6 changes: 5 additions & 1 deletion packages/automated-dispute/src/exceptions/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export class ErrorHandler {
this.logger.error(`Error executing custom action: ${actionError}`);
} finally {
if (strategy.shouldNotify) {
await this.notificationService.notifyError(error, context);
await this.notificationService.sendError(
"An error occurred in the custom contract",
context,
error,
);
}

if (strategy.shouldReenqueue && context.reenqueueEvent) {
Expand Down
2 changes: 2 additions & 0 deletions packages/automated-dispute/src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export * from "./decodeLogDataFailure.js";
export * from "./eboActor/index.js";
export * from "./eboProcessor/index.js";
export * from "./eboRegistry/index.js";

export * from "./notificationFailure.exception.js";
export * from "./errorFactory.js";
export * from "./invalidAccountOnClient.exception.js";
export * from "./invalidActorState.exception.js";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class NotificationFailureException extends Error {
constructor(message: string) {
super(`Failed to send notification: ${message}`);
this.name = "NotificationFailureException";
}
}
74 changes: 68 additions & 6 deletions packages/automated-dispute/src/interfaces/notificationService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,75 @@
/**
* Interface representing a notification service capable of sending error notifications.
* Represents a notification message.
*/
export interface IMessage {
/**
* The main content of the message.
*/
title: string;
/**
* An optional subtitle for the message.
*/
subtitle?: string;
/**
* An optional description providing more details.
*/
description?: string;
/**
* The username to display as the sender.
*/
username?: string;
/**
* The URL of the avatar image to display.
*/
avatarUrl?: string;
/**
* An optional URL associated with the message.
*/
actionUrl?: string;
}

/**
* Interface representing a notification service capable of sending notifications.
*/
export interface NotificationService {
/**
* Sends an error notification along with optional contextual information.
* Sends a notification message.
*
* @param {IMessage} message - The message to send.
* @returns {Promise<void>} A promise that resolves when the message is sent.
*/
send(message: IMessage): Promise<void>;

/**
* Sends a notification message and throws an exception if sending fails.
*
* @param {IMessage} message - The message to send.
* @returns {Promise<void>} A promise that resolves when the message is sent.
* @throws {NotificationFailureException} If sending the message fails.
*/
sendOrThrow(message: IMessage): Promise<void>;

/**
* Creates an IMessage from an error.
*
* @param {string} defaultMessage - A default message describing the error context.
* @param {unknown} [context] - Additional context for the error.
* @param {unknown} [err] - The error object.
* @returns {IMessage} An IMessage object ready to be sent via the notifier.
*/
createErrorMessage(defaultMessage: string, context?: unknown, err?: unknown): IMessage;

/**
* Sends an error notification message.
*
* @param error - The error object containing information about the error that occurred.
* @param context - Additional context or data related to the error
* @returns A promise that resolves when the notification process is complete.
* @param {string} defaultMessage - A default message describing the error context.
* @param {Record<string, unknown>} [context] - Additional context for the error.
* @param {unknown} [err] - The error object.
* @returns {Promise<void>} A promise that resolves when the message is sent.
*/
notifyError(error: Error, context: any): Promise<void>;
sendError(
defaultMessage: string,
context?: Record<string, unknown>,
err?: unknown,
): Promise<void>;
}
Loading