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: changed bot token to webhook [WIP] #78

Closed
wants to merge 4 commits into from
Closed
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
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
3 changes: 1 addition & 2 deletions apps/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ cp .env.example .env
| `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 |
| `DISCORD_WEBHOOK` | Your Discord channel webhook for notifications | Yes |

**Notes:**

Expand Down
32 changes: 32 additions & 0 deletions apps/agent/config.tenderly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
protocolProvider:
rpcsConfig:
l1:
chainId: eip155:11155111
transactionReceiptConfirmations: 1
timeout: 10000
retryInterval: 150
l2:
chainId: eip155:421614
transactionReceiptConfirmations: 1
timeout: 10000
retryInterval: 150
contracts:
oracle: "0x10224eff6B1Caaf5daC49B2e7104b7161484B128"
epochManager: "0x7975475801BEf845f10Ce7784DC69aB1e0344f11"
eboRequestCreator: "0xa13318684281a820304C164427396385C306d870"
bondEscalationModule: "0x52d7728fE87826FfF51b21b303e2FF7cB04F6Aec"
horizonAccountingExtension: "0xbDAB27D1903da4e18B0D1BE873E18924514E52eC"

blockNumberService:
blockmetaConfig:
baseUrl: "localhost:443"
servicePaths:
blockByTime: /sf.blockmeta.v2.BlockByTime
block: /sf.blockmeta.v2.Block
bearerTokenExpirationWindow: 31536000000

processor:
msBetweenChecks: 7500
accountingModules:
responseModule: "0xb97C59331F89a852Ae21aee215Da28820c533649"
escalationModule: "0x52d7728fE87826FfF51b21b303e2FF7cB04F6Aec"
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
13 changes: 3 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,10 @@ 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));
logger.debug(`Discord webhook ${config.DISCORD_WEBHOOK}`);

// 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);

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().min(1),
});

export const config = ConfigSchema.parse(process.env);
1 change: 1 addition & 0 deletions packages/automated-dispute/src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./eboActor/index.js";
export * from "./eboProcessor/index.js";
export * from "./eboRegistry/index.js";

export * from "./notificationFailure.exception.js";
export * from "./invalidActorState.exception.js";
export * from "./invalidDisputeStatus.exception.js";
export * from "./requestAlreadyHandled.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";
}
}
22 changes: 11 additions & 11 deletions packages/automated-dispute/src/interfaces/notificationService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* Interface representing a notification service capable of sending error notifications.
*/
export interface IMessage {
title: string;
subtitle?: string;
description?: string;
username?: string;
avatarUrl?: string;
actionUrl?: string;
}

export interface NotificationService {
/**
* Sends an error notification along with optional contextual information.
*
* @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.
*/
notifyError(error: Error, context: any): Promise<void>;
// send(message: IMessage): Promise<void>;
notifyError(err: Error, context: unknown): Promise<void>;
}
129 changes: 61 additions & 68 deletions packages/automated-dispute/src/services/discordNotifier.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,36 @@
import { ILogger } from "@ebo-agent/shared";
import { Client, IntentsBitField, TextChannel } from "discord.js";
import type { APIEmbed, JSONEncodable } from "discord.js";
import { WebhookClient, WebhookMessageCreateOptions } from "discord.js";
import { stringify } from "viem";

import { NotificationService } from "../interfaces/index.js";
import { NotificationFailureException } from "../exceptions/index.js";
import { IMessage, NotificationService } from "../interfaces/index.js";

interface DiscordNotifierConfig {
discordBotToken: string;
discordChannelId: string;
}
export type WebhookMessage = WebhookMessageCreateOptions & {
content: string;
};

/**
* A notifier class for sending error notifications to a Discord channel.
* TODO: Refactor me, this was a quick and dirty implementation
*
* `notifiyError` and `formatErrorMessage` should not exist.
* `send` should be the method to use across the codebase. (added as comment here and in the interface)
* If there is a new Notifier service (Ex. TelegramNotifier) it should implement the same interface (NotificationService).
*/
export class DiscordNotifier implements NotificationService {
private client: Client;
private config: DiscordNotifierConfig;
private logger: ILogger;
private client: WebhookClient;

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

/**
* 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 });
async notifyError(err: Error, context: unknown): Promise<void> {
const message = this.formatErrorMessage(err, context);

const webhookMessage = this.buildWebhookMessage({ title: message });
try {
await client.login(config.discordBotToken);

await new Promise<void>((resolve, reject) => {
client.once("ready", () => {
logger.info("Discord bot is ready");

resolve();
});

client.once("error", (error: Error) => {
reject(error);
});
});
await this.client.send(webhookMessage);
} catch (error) {
logger.error(`Failed 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}`);
throw new NotificationFailureException("An error occured with Discord client.");
}
}

Expand All @@ -95,4 +47,45 @@ export class DiscordNotifier implements NotificationService {
2,
)}\n\`\`\``;
}

// /**
// * Sends a webhook message to Discord.
// * @param {WebhookMessage} message - The message to send.
// * @returns A promise that resolves when the message is sent.
// */
// async send(message: IMessage) {
// const webhookMessage = this.buildWebhookMessage(message);
// try {
// await this.client.send(webhookMessage);
// } catch (error) {
// throw new NotificationFailureException("An error occured with Discord client.");
// }
// }

/**
* Builds a Discord webhookMessage for an arbitrage opportunity.
* @param {ArbitrageOpportunity} data - The arbitrage data.
* @returns {WebhookMessage} The built Discord webhook message.
*/
buildWebhookMessage(message: IMessage): WebhookMessage {
return {
username: message.username || "EBO Agent",
content: message.title,
avatarURL:
message.avatarUrl || "https://i.ibb.co/x3HV1Tj/The-Graph-GRT-Symbol-Color.png", // TODO: change to a better image source
// embeds: [this.buildWebhookMessageEmbed(message)],
} as WebhookMessage;
}

buildWebhookMessageEmbed(message: IMessage) {
const title = message.subtitle;
const description = message.description;

return {
title,
description,
color: 2326507,
url: message.actionUrl,
} as APIEmbed | JSONEncodable<APIEmbed>;
}
}
Loading
Loading