diff --git a/apps/agent/src/index.ts b/apps/agent/src/index.ts index d5b9778..98826f6 100644 --- a/apps/agent/src/index.ts +++ b/apps/agent/src/index.ts @@ -1,6 +1,7 @@ import { inspect } from "util"; import { EboActorsManager, EboProcessor } from "@ebo-agent/automated-dispute"; import { ProtocolProvider } from "@ebo-agent/automated-dispute/dist/providers/protocolProvider.js"; +import { DiscordNotifier } from "@ebo-agent/automated-dispute/src/services/index.js"; import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Logger } from "@ebo-agent/shared"; @@ -30,6 +31,8 @@ const config = { processor: { msBetweenChecks: 1, }, + DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN || "", + DISCORD_CHANNEL_ID: process.env.DISCORD_CHANNEL_ID || "", }; const logger = Logger.getInstance(); @@ -49,7 +52,18 @@ const main = async (): Promise => { const actorsManager = new EboActorsManager(); - const processor = new EboProcessor(protocolProvider, blockNumberService, actorsManager, logger); + const notifier = await DiscordNotifier.create({ + discordBotToken: config.DISCORD_BOT_TOKEN, + discordChannelId: config.DISCORD_CHANNEL_ID, + }); + + const processor = new EboProcessor( + protocolProvider, + blockNumberService, + actorsManager, + logger, + notifier, + ); await processor.start(config.processor.msBetweenChecks); }; diff --git a/packages/automated-dispute/package.json b/packages/automated-dispute/package.json index a8eac5f..0554e3d 100644 --- a/packages/automated-dispute/package.json +++ b/packages/automated-dispute/package.json @@ -28,6 +28,7 @@ "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" } } diff --git a/packages/automated-dispute/src/config.ts b/packages/automated-dispute/src/config.ts new file mode 100644 index 0000000..bea871f --- /dev/null +++ b/packages/automated-dispute/src/config.ts @@ -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); diff --git a/packages/automated-dispute/src/services/discordNotifier.ts b/packages/automated-dispute/src/services/discordNotifier.ts index 138232b..3c6d9a3 100644 --- a/packages/automated-dispute/src/services/discordNotifier.ts +++ b/packages/automated-dispute/src/services/discordNotifier.ts @@ -12,33 +12,29 @@ interface DiscordNotifierConfig { */ export class DiscordNotifier implements NotificationService { private client: Client; - private readyPromise: Promise; private config: DiscordNotifierConfig; + private constructor(client: Client, config: DiscordNotifierConfig) { + this.client = client; + this.config = config; + } + /** * Creates an instance of the DiscordNotifier. * @param {DiscordNotifierConfig} config - The configuration object for the DiscordNotifier. + * @returns {Promise} A promise that resolves to a DiscordNotifier instance. */ - constructor(config: DiscordNotifierConfig) { + public static async create(config: DiscordNotifierConfig): Promise { const intents = new IntentsBitField().add( IntentsBitField.Flags.Guilds, IntentsBitField.Flags.GuildMessages, ); - this.client = new Client({ intents }); - this.config = config; - this.readyPromise = this.initialize(); - } + const client = new Client({ intents }); - /** - * Initializes the Discord notifier by logging in with the bot token and waiting for the "ready" event. - * @returns {Promise} A promise that resolves when the Discord bot is ready. - * @throws {Error} If the initialization fails. - */ - private async initialize(): Promise { try { - await this.client.login(this.config.discordBotToken); + await client.login(config.discordBotToken); await new Promise((resolve) => { - this.client.once("ready", () => { + client.once("ready", () => { console.log("Discord bot is ready"); resolve(); }); @@ -47,6 +43,8 @@ export class DiscordNotifier implements NotificationService { console.error("Failed to initialize Discord notifier:", error); throw error; } + + return new DiscordNotifier(client, config); } /** @@ -55,9 +53,8 @@ export class DiscordNotifier implements NotificationService { * @param {any} context - Additional context information. * @returns {Promise} A promise that resolves when the message is sent. */ - async notifyError(error: Error, context: any): Promise { + public async notifyError(error: Error, context: any): Promise { try { - await this.readyPromise; 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"); @@ -78,6 +75,10 @@ export class DiscordNotifier implements NotificationService { * @returns {string} The formatted error message. */ private formatErrorMessage(error: Error, context: any): string { - return `**Error:** ${error.name} - ${error.message}\n**Context:**\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\``; + return `**Error:** ${error.name} - ${error.message}\n**Context:**\n\`\`\`json\n${JSON.stringify( + context, + null, + 2, + )}\n\`\`\``; } } diff --git a/packages/automated-dispute/src/services/eboActorsManager.ts b/packages/automated-dispute/src/services/eboActorsManager.ts index 5feb816..dd5689f 100644 --- a/packages/automated-dispute/src/services/eboActorsManager.ts +++ b/packages/automated-dispute/src/services/eboActorsManager.ts @@ -5,9 +5,9 @@ import { Mutex } from "async-mutex"; import { RequestAlreadyHandled } from "../exceptions/index.js"; import { ProtocolProvider } from "../providers/protocolProvider.js"; import { ActorRequest, RequestId } from "../types/index.js"; -import { DiscordNotifier } from "./discordNotifier.js"; import { EboActor } from "./eboActor.js"; import { EboMemoryRegistry } from "./eboRegistry/eboMemoryRegistry.js"; +import { NotificationService } from "./notificationService.js"; export class EboActorsManager { private readonly requestActorMap: Map; @@ -39,6 +39,7 @@ export class EboActorsManager { protocolProvider: ProtocolProvider, blockNumberService: BlockNumberService, logger: ILogger, + notifier: NotificationService, ): EboActor { const requestId = actorRequest.id; @@ -48,11 +49,6 @@ export class EboActorsManager { const eventProcessingMutex = new Mutex(); - const discordNotifier = new DiscordNotifier({ - discordBotToken: process.env.DISCORD_BOT_TOKEN!, - discordChannelId: process.env.DISCORD_CHANNEL_ID!, - }); - const actor = new EboActor( actorRequest, protocolProvider, @@ -60,7 +56,7 @@ export class EboActorsManager { registry, eventProcessingMutex, logger, - discordNotifier, + notifier, ); this.requestActorMap.set(requestId, actor); diff --git a/packages/automated-dispute/src/services/eboProcessor.ts b/packages/automated-dispute/src/services/eboProcessor.ts index 7950502..3549df5 100644 --- a/packages/automated-dispute/src/services/eboProcessor.ts +++ b/packages/automated-dispute/src/services/eboProcessor.ts @@ -8,6 +8,7 @@ import { ProtocolProvider } from "../providers/protocolProvider.js"; import { alreadyDeletedActorWarning, droppingUnhandledEventsWarning } from "../templates/index.js"; import { ActorRequest, EboEvent, EboEventName, Epoch, RequestId } from "../types/index.js"; import { EboActorsManager } from "./eboActorsManager.js"; +import { NotificationService } from "./notificationService.js"; const DEFAULT_MS_BETWEEN_CHECKS = 10 * 60 * 1000; // 10 minutes @@ -22,6 +23,7 @@ export class EboProcessor { private readonly blockNumberService: BlockNumberService, private readonly actorsManager: EboActorsManager, private readonly logger: ILogger, + private readonly notifier: NotificationService, ) {} /** @@ -249,6 +251,7 @@ export class EboProcessor { this.protocolProvider, this.blockNumberService, this.logger, + this.notifier, ); return actor; diff --git a/packages/automated-dispute/tests/services/discordNotifier.spec.ts b/packages/automated-dispute/tests/services/discordNotifier.spec.ts index a399734..69f2592 100644 --- a/packages/automated-dispute/tests/services/discordNotifier.spec.ts +++ b/packages/automated-dispute/tests/services/discordNotifier.spec.ts @@ -43,9 +43,9 @@ describe("DiscordNotifier", () => { let notifier: DiscordNotifier; - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); - notifier = new DiscordNotifier(mockConfig); + notifier = await DiscordNotifier.create(mockConfig); }); it("initializes the Discord client and login", async () => { @@ -55,8 +55,6 @@ describe("DiscordNotifier", () => { intents: expect.any(IntentsBitField), }); - await notifier["readyPromise"]; - const instance = ClientMock.mock.results[0].value; expect(instance.login).toHaveBeenCalledWith("mock-token"); expect(instance.once).toHaveBeenCalledWith("ready", expect.any(Function)); @@ -66,8 +64,6 @@ describe("DiscordNotifier", () => { const error = new Error("Test error"); const context = { key: "value" }; - await notifier["readyPromise"]; - await notifier.notifyError(error, context); const ClientMock = Client as unknown as vi.Mock; @@ -101,7 +97,7 @@ describe("DiscordNotifier", () => { error.name = "TestError"; const context = { key: "value" }; - const formattedMessage = notifier["formatErrorMessage"](error, context); + const formattedMessage = (notifier as any).formatErrorMessage(error, context); expect(formattedMessage).toContain("**Error:** TestError - Test error message"); expect(formattedMessage).toContain("**Context:**"); diff --git a/packages/automated-dispute/tests/services/eboActorsManager.spec.ts b/packages/automated-dispute/tests/services/eboActorsManager.spec.ts index 59d599d..a9e6e18 100644 --- a/packages/automated-dispute/tests/services/eboActorsManager.spec.ts +++ b/packages/automated-dispute/tests/services/eboActorsManager.spec.ts @@ -1,12 +1,11 @@ -import { beforeEach } from "node:test"; import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; import { ILogger } from "@ebo-agent/shared"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { RequestAlreadyHandled } from "../../src/exceptions/index.js"; import { ProtocolProvider } from "../../src/providers/index.js"; -import { EboActorsManager } from "../../src/services/index.js"; +import { EboActorsManager, NotificationService } from "../../src/services/index.js"; import mocks from "../mocks/index.js"; import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS, @@ -16,17 +15,6 @@ import { const logger: ILogger = mocks.mockLogger(); -vi.mock("../../src/services/discordNotifier", () => { - return { - DiscordNotifier: vi.fn().mockImplementation(() => { - return { - initialize: vi.fn().mockResolvedValue(undefined), - notifyError: vi.fn().mockResolvedValue(undefined), - }; - }), - }; -}); - describe("EboActorsManager", () => { const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; const actorRequest = { @@ -37,8 +25,15 @@ describe("EboActorsManager", () => { let protocolProvider: ProtocolProvider; let blockNumberService: BlockNumberService; + let notifier: NotificationService; beforeEach(() => { + vi.clearAllMocks(); + + notifier = { + notifyError: vi.fn().mockResolvedValue(undefined), + }; + const protocolProviderRpcUrls = ["http://localhost:8538"]; protocolProvider = new ProtocolProvider( protocolProviderRpcUrls, @@ -60,6 +55,7 @@ describe("EboActorsManager", () => { protocolProvider, blockNumberService, logger, + notifier, ); expect(actor).toMatchObject({ @@ -75,7 +71,13 @@ describe("EboActorsManager", () => { expect(actorsManager.getActor(request.id)).toBeUndefined(); - actorsManager.createActor(actorRequest, protocolProvider, blockNumberService, logger); + actorsManager.createActor( + actorRequest, + protocolProvider, + blockNumberService, + logger, + notifier, + ); const actor = actorsManager.getActor(request.id); @@ -85,7 +87,13 @@ describe("EboActorsManager", () => { it("throws if the request has already an actor linked to it", () => { const actorsManager = new EboActorsManager(); - actorsManager.createActor(actorRequest, protocolProvider, blockNumberService, logger); + actorsManager.createActor( + actorRequest, + protocolProvider, + blockNumberService, + logger, + notifier, + ); expect(() => { actorsManager.createActor( @@ -93,6 +101,7 @@ describe("EboActorsManager", () => { protocolProvider, blockNumberService, logger, + notifier, ); }).toThrowError(RequestAlreadyHandled); }); @@ -108,7 +117,13 @@ describe("EboActorsManager", () => { it("returns the request's linked actor", () => { const actorsManager = new EboActorsManager(); - actorsManager.createActor(actorRequest, protocolProvider, blockNumberService, logger); + actorsManager.createActor( + actorRequest, + protocolProvider, + blockNumberService, + logger, + notifier, + ); const actor = actorsManager.getActor(request.id); @@ -125,7 +140,13 @@ describe("EboActorsManager", () => { it("deletes the actor linked to the request", () => { const actorsManager = new EboActorsManager(); - actorsManager.createActor(actorRequest, protocolProvider, blockNumberService, logger); + actorsManager.createActor( + actorRequest, + protocolProvider, + blockNumberService, + logger, + notifier, + ); expect(actorsManager.getActor(request.id)).toBeDefined(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 100a091..959da57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,10 @@ importers: version: 2.5.0 viem: specifier: 2.17.11 - version: 2.17.11(typescript@5.5.3) + version: 2.17.11(typescript@5.5.3)(zod@3.23.8) + zod: + specifier: 3.23.8 + version: 3.23.8 packages/blocknumber: dependencies: @@ -115,7 +118,7 @@ importers: version: 4.0.0 viem: specifier: 2.17.10 - version: 2.17.10(typescript@5.5.3) + version: 2.17.10(typescript@5.5.3)(zod@3.23.8) devDependencies: "@types/jsonwebtoken": specifier: 9.0.6 @@ -3801,6 +3804,12 @@ packages: } engines: { node: ">=12.20" } + zod@3.23.8: + resolution: + { + integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==, + } + snapshots: "@adraffy/ens-normalize@1.10.0": {} @@ -4520,9 +4529,10 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 - abitype@1.0.5(typescript@5.5.3): + abitype@1.0.5(typescript@5.5.3)(zod@3.23.8): optionalDependencies: typescript: 5.5.3 + zod: 3.23.8 acorn-jsx@5.3.2(acorn@8.12.1): dependencies: @@ -5767,14 +5777,14 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - viem@2.17.10(typescript@5.5.3): + viem@2.17.10(typescript@5.5.3)(zod@3.23.8): dependencies: "@adraffy/ens-normalize": 1.10.0 "@noble/curves": 1.4.0 "@noble/hashes": 1.4.0 "@scure/bip32": 1.4.0 "@scure/bip39": 1.3.0 - abitype: 1.0.5(typescript@5.5.3) + abitype: 1.0.5(typescript@5.5.3)(zod@3.23.8) isows: 1.0.4(ws@8.17.1) ws: 8.17.1 optionalDependencies: @@ -5784,14 +5794,14 @@ snapshots: - utf-8-validate - zod - viem@2.17.11(typescript@5.5.3): + viem@2.17.11(typescript@5.5.3)(zod@3.23.8): dependencies: "@adraffy/ens-normalize": 1.10.0 "@noble/curves": 1.4.0 "@noble/hashes": 1.4.0 "@scure/bip32": 1.4.0 "@scure/bip39": 1.3.0 - abitype: 1.0.5(typescript@5.5.3) + abitype: 1.0.5(typescript@5.5.3)(zod@3.23.8) isows: 1.0.4(ws@8.17.1) ws: 8.17.1 optionalDependencies: @@ -5937,3 +5947,5 @@ snapshots: yocto-queue@0.1.0: {} yocto-queue@1.1.1: {} + + zod@3.23.8: {}