From 93d1461f255f2ba07688dbcf1085bb60ec547098 Mon Sep 17 00:00:00 2001 From: Yaco 0x Date: Tue, 3 Sep 2024 17:52:20 -0300 Subject: [PATCH 1/6] feat: add missing ebo registry commands --- packages/automated-dispute/src/eboActor.ts | 66 +++++++-------- .../exceptions/invalidActorState.exception.ts | 6 +- .../src/interfaces/eboRegistry.ts | 11 ++- .../eboRegistry/commands/addDispute.ts | 39 +++++++++ .../services/eboRegistry/commands/index.ts | 5 +- .../src/services/eboRegistry/commands/noop.ts | 22 +++++ .../commands/updateDisputeStatus.ts | 44 ++++++++++ .../services/eboRegistry/eboMemoryRegistry.ts | 9 ++- .../automated-dispute/src/types/events.ts | 2 +- .../commands/addDispute.spec.ts | 76 +++++++++++++++++ .../eboMemoryRegistry/commands/noop.spec.ts | 24 ++++++ .../commands/updateDisputeStatus.spec.ts | 81 +++++++++++++++++++ 12 files changed, 337 insertions(+), 48 deletions(-) create mode 100644 packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts create mode 100644 packages/automated-dispute/src/services/eboRegistry/commands/noop.ts create mode 100644 packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts create mode 100644 packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts create mode 100644 packages/automated-dispute/tests/services/eboMemoryRegistry/commands/noop.spec.ts create mode 100644 packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts diff --git a/packages/automated-dispute/src/eboActor.ts b/packages/automated-dispute/src/eboActor.ts index 5e8c1ee..592b682 100644 --- a/packages/automated-dispute/src/eboActor.ts +++ b/packages/automated-dispute/src/eboActor.ts @@ -16,7 +16,13 @@ import { } from "./exceptions/index.js"; import { EboRegistry, EboRegistryCommand } from "./interfaces/index.js"; import { ProtocolProvider } from "./protocolProvider.js"; -import { AddRequest, AddResponse } from "./services/index.js"; +import { + AddDispute, + AddRequest, + AddResponse, + Noop, + UpdateDisputeStatus, +} from "./services/index.js"; import { Dispute, EboEvent, @@ -176,6 +182,21 @@ export class EboActor { this.registry, ); + case "ResponseDisputed": + return AddDispute.buildFromEvent( + event as EboEvent<"ResponseDisputed">, + this.registry, + ); + + case "DisputeStatusChanged": + return UpdateDisputeStatus.buildFromEvent( + event as EboEvent<"DisputeStatusChanged">, + this.registry, + ); + + case "RequestFinalized": + return Noop.buildFromEvent(); + default: throw new UnknownEvent(event.name); } @@ -378,9 +399,6 @@ export class EboActor { * @param event `RequestCreated` event */ public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise { - if (event.metadata.requestId != this.actorRequest.id) - throw new RequestMismatch(this.actorRequest.id, event.metadata.requestId); - if (this.registry.getRequest(event.metadata.requestId)) { this.logger.error( `The request ${event.metadata.requestId} was already being handled by an actor.`, @@ -389,16 +407,6 @@ export class EboActor { throw new InvalidActorState(); } - const request: Request = { - id: this.actorRequest.id, - chainId: event.metadata.chainId, - epoch: this.actorRequest.epoch, - createdAt: event.blockNumber, - prophetData: event.metadata.request, - }; - - this.registry.addRequest(request); - if (this.anyActiveProposal()) { // Skipping new proposal until the actor receives a ResponseDisputed event; // at that moment, it will be possible to re-propose again. @@ -526,16 +534,6 @@ export class EboActor { * @returns void */ public async onResponseProposed(event: EboEvent<"ResponseProposed">): Promise { - this.shouldHandleRequest(event.metadata.requestId); - - const response: Response = { - id: event.metadata.responseId, - createdAt: event.blockNumber, - prophetData: event.metadata.response, - }; - - this.registry.addResponse(response); - const eventResponse = event.metadata.response; const actorResponse = await this.buildResponse(eventResponse.response.chainId); @@ -597,16 +595,12 @@ export class EboActor { * @param event `ResponseDisputed` event. */ public async onResponseDisputed(event: EboEvent<"ResponseDisputed">): Promise { - this.shouldHandleRequest(event.metadata.dispute.requestId); + const dispute = this.registry.getDispute(event.metadata.disputeId); - const dispute: Dispute = { - id: event.metadata.disputeId, - createdAt: event.blockNumber, - status: "Active", - prophetData: event.metadata.dispute, - }; - - this.registry.addDispute(event.metadata.disputeId, dispute); + if (!dispute) + throw new InvalidActorState( + `Dispute ${event.metadata.disputeId} needs to be added to the internal registry.`, + ); const request = this.getActorRequest(); const proposedResponse = this.registry.getResponse(event.metadata.responseId); @@ -700,16 +694,10 @@ export class EboActor { * @param event `DisputeStatusChanged` event */ public async onDisputeStatusChanged(event: EboEvent<"DisputeStatusChanged">): Promise { - const requestId = event.metadata.dispute.requestId; - - this.shouldHandleRequest(requestId); - const request = this.getActorRequest(); const disputeId = event.metadata.disputeId; const disputeStatus = event.metadata.status; - this.registry.updateDisputeStatus(disputeId, disputeStatus); - this.logger.info(`Dispute ${disputeId} status changed to ${disputeStatus}.`); switch (disputeStatus) { diff --git a/packages/automated-dispute/src/exceptions/invalidActorState.exception.ts b/packages/automated-dispute/src/exceptions/invalidActorState.exception.ts index 74f8d47..1c2be50 100644 --- a/packages/automated-dispute/src/exceptions/invalidActorState.exception.ts +++ b/packages/automated-dispute/src/exceptions/invalidActorState.exception.ts @@ -1,7 +1,9 @@ export class InvalidActorState extends Error { - constructor() { + constructor(message?: string) { // TODO: we'll want to dump the Actor state into stderr at this point - super("The actor is in an invalid state."); + super( + `The actor is in an invalid state. ${message ? `Reason: ${message}` : "Unknown reason."}`, + ); this.name = "InvalidActorState"; } diff --git a/packages/automated-dispute/src/interfaces/eboRegistry.ts b/packages/automated-dispute/src/interfaces/eboRegistry.ts index 5b37946..50e06aa 100644 --- a/packages/automated-dispute/src/interfaces/eboRegistry.ts +++ b/packages/automated-dispute/src/interfaces/eboRegistry.ts @@ -65,10 +65,9 @@ export interface EboRegistry { /** * Add a dispute by ID. * - * @param disputeId the ID of the `Dispute` to use as index * @param dispute the `Dispute` */ - addDispute(disputeId: string, dispute: Dispute): void; + addDispute(dispute: Dispute): void; /** * Get all disputes @@ -92,4 +91,12 @@ export interface EboRegistry { * @param status the `Dispute` */ updateDisputeStatus(disputeId: string, status: DisputeStatus): void; + + /** + * Remove a `Dispute` by its ID. + * + * @param disputeId dispute ID + * @returns `true` if the dispute in the registry existed and has been removed, or `false` if the dispute does not exist + */ + removeDispute(disputeId: string): boolean; } diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts b/packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts new file mode 100644 index 0000000..7143cf2 --- /dev/null +++ b/packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts @@ -0,0 +1,39 @@ +import { CommandAlreadyRun, CommandNotRun } from "../../../exceptions/index.js"; +import { EboRegistry, EboRegistryCommand } from "../../../interfaces/index.js"; +import { Dispute, EboEvent } from "../../../types/index.js"; + +export class AddDispute implements EboRegistryCommand { + private wasRun: boolean = false; + + private constructor( + private readonly registry: EboRegistry, + private readonly dispute: Dispute, + ) {} + + public static buildFromEvent( + event: EboEvent<"ResponseDisputed">, + registry: EboRegistry, + ): AddDispute { + const dispute: Dispute = { + id: event.metadata.disputeId, + createdAt: event.blockNumber, + status: "Active", + prophetData: event.metadata.dispute, + }; + + return new AddDispute(registry, dispute); + } + + run(): void { + if (this.wasRun) throw new CommandAlreadyRun(AddDispute.name); + + this.registry.addDispute(this.dispute); + this.wasRun = true; + } + + undo(): void { + if (!this.wasRun) throw new CommandNotRun(AddDispute.name); + + this.registry.removeDispute(this.dispute.id); + } +} diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/index.ts b/packages/automated-dispute/src/services/eboRegistry/commands/index.ts index 8afc923..13e989e 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/index.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/index.ts @@ -1,4 +1,5 @@ export * from "./addRequest.js"; export * from "./addResponse.js"; - -// TODO: add the rest of the commands +export * from "./addDispute.js"; +export * from "./noop.js"; +export * from "./updateDisputeStatus.js"; diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/noop.ts b/packages/automated-dispute/src/services/eboRegistry/commands/noop.ts new file mode 100644 index 0000000..a15af24 --- /dev/null +++ b/packages/automated-dispute/src/services/eboRegistry/commands/noop.ts @@ -0,0 +1,22 @@ +import { CommandAlreadyRun, CommandNotRun } from "../../../exceptions/index.js"; +import { EboRegistryCommand } from "../../../interfaces/index.js"; + +export class Noop implements EboRegistryCommand { + private wasRun: boolean = false; + + private constructor() {} + + public static buildFromEvent(): Noop { + return new Noop(); + } + + run(): void { + if (this.wasRun) throw new CommandAlreadyRun(Noop.name); + + this.wasRun = true; + } + + undo(): void { + if (!this.wasRun) throw new CommandNotRun(Noop.name); + } +} diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts b/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts new file mode 100644 index 0000000..dfd8fa1 --- /dev/null +++ b/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts @@ -0,0 +1,44 @@ +import { CommandAlreadyRun, CommandNotRun, DisputeNotFound } from "../../../exceptions/index.js"; +import { EboRegistry, EboRegistryCommand } from "../../../interfaces/index.js"; +import { DisputeStatus, EboEvent } from "../../../types/index.js"; + +export class UpdateDisputeStatus implements EboRegistryCommand { + private wasRun: boolean = false; + private previousStatus?: DisputeStatus; + + private constructor( + private readonly registry: EboRegistry, + private readonly disputeId: string, + private readonly status: DisputeStatus, + ) {} + + public static buildFromEvent( + event: EboEvent<"DisputeStatusChanged">, + registry: EboRegistry, + ): UpdateDisputeStatus { + const disputeId = event.metadata.disputeId; + const status = event.metadata.status; + + return new UpdateDisputeStatus(registry, disputeId, status); + } + + run(): void { + if (this.wasRun) throw new CommandAlreadyRun(UpdateDisputeStatus.name); + + const dispute = this.registry.getDispute(this.disputeId); + + if (!dispute) throw new DisputeNotFound(this.disputeId); + + this.previousStatus = dispute.status; + + this.registry.updateDisputeStatus(this.disputeId, this.status); + + this.wasRun = true; + } + + undo(): void { + if (!this.wasRun || !this.previousStatus) throw new CommandNotRun(UpdateDisputeStatus.name); + + this.registry.updateDisputeStatus(this.disputeId, this.previousStatus); + } +} diff --git a/packages/automated-dispute/src/services/eboRegistry/eboMemoryRegistry.ts b/packages/automated-dispute/src/services/eboRegistry/eboMemoryRegistry.ts index 84d14c7..0321e69 100644 --- a/packages/automated-dispute/src/services/eboRegistry/eboMemoryRegistry.ts +++ b/packages/automated-dispute/src/services/eboRegistry/eboMemoryRegistry.ts @@ -46,8 +46,8 @@ export class EboMemoryRegistry implements EboRegistry { } /** @inheritdoc */ - public addDispute(disputeId: string, dispute: Dispute): void { - this.disputes.set(disputeId, dispute); + public addDispute(dispute: Dispute): void { + this.disputes.set(dispute.id, dispute); this.responsesDisputes.set(dispute.prophetData.responseId, dispute.id); } @@ -81,4 +81,9 @@ export class EboMemoryRegistry implements EboRegistry { status: status, }); } + + /** @inheritdoc */ + removeDispute(disputeId: string): boolean { + return this.disputes.delete(disputeId); + } } diff --git a/packages/automated-dispute/src/types/events.ts b/packages/automated-dispute/src/types/events.ts index c7218bc..4bbb0c6 100644 --- a/packages/automated-dispute/src/types/events.ts +++ b/packages/automated-dispute/src/types/events.ts @@ -27,7 +27,7 @@ export interface RequestCreated { epoch: bigint; chainId: Caip2ChainId; request: Request["prophetData"]; - requestId: string; + requestId: RequestId; } export interface ResponseDisputed { diff --git a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts new file mode 100644 index 0000000..8789a6d --- /dev/null +++ b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; + +import { CommandAlreadyRun, CommandNotRun } from "../../../../src/exceptions/index.js"; +import { EboRegistry } from "../../../../src/interfaces/index.js"; +import { AddDispute } from "../../../../src/services/index.js"; +import { EboEvent } from "../../../../src/types/index.js"; +import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "../../../eboActor/fixtures.js"; +import mocks from "../../../mocks/index.js"; + +describe("AddDispute", () => { + let registry: EboRegistry; + + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + const response = mocks.buildResponse(request); + const dispute = mocks.buildDispute(request, response); + + const event: EboEvent<"ResponseDisputed"> = { + name: "ResponseDisputed", + blockNumber: 1n, + logIndex: 1, + requestId: request.id, + metadata: { + dispute: dispute.prophetData, + disputeId: dispute.id, + responseId: response.id, + }, + }; + + beforeEach(() => { + registry = { + addDispute: vi.fn(), + removeDispute: vi.fn(), + } as unknown as EboRegistry; + }); + + describe("run", () => { + it("adds the dispute to the registry", () => { + const command = AddDispute.buildFromEvent(event, registry); + + command.run(); + + expect(registry.addDispute).toHaveBeenCalledWith( + expect.objectContaining({ + id: dispute.id, + }), + ); + }); + + it("throws if the command was already run", () => { + const command = AddDispute.buildFromEvent(event, registry); + + command.run(); + + expect(() => command.run()).toThrow(CommandAlreadyRun); + }); + }); + + describe("undo", () => { + it("removes the added request", () => { + const command = AddDispute.buildFromEvent(event, registry); + + const mockRemoveDispute = registry.removeDispute as Mock; + + command.run(); + command.undo(); + + expect(mockRemoveDispute).toHaveBeenCalledWith(request.id); + }); + + it("throws if undoing the command before being run", () => { + const command = AddDispute.buildFromEvent(event, registry); + + expect(() => command.undo()).toThrow(CommandNotRun); + }); + }); +}); diff --git a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/noop.spec.ts b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/noop.spec.ts new file mode 100644 index 0000000..fbd6c31 --- /dev/null +++ b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/noop.spec.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; + +import { CommandAlreadyRun, CommandNotRun } from "../../../../src/exceptions/index.js"; +import { Noop } from "../../../../src/services/index.js"; + +describe("Noop", () => { + describe("run", () => { + it("throws if the command was already run", () => { + const command = Noop.buildFromEvent(); + + command.run(); + + expect(() => command.run()).toThrow(CommandAlreadyRun); + }); + }); + + describe("undo", () => { + it("throws if undoing the command before being run", () => { + const command = Noop.buildFromEvent(); + + expect(() => command.undo()).toThrow(CommandNotRun); + }); + }); +}); diff --git a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts new file mode 100644 index 0000000..40e5c49 --- /dev/null +++ b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { CommandAlreadyRun, CommandNotRun } from "../../../../src/exceptions/index.js"; +import { EboRegistry } from "../../../../src/interfaces/index.js"; +import { UpdateDisputeStatus } from "../../../../src/services/index.js"; +import { EboEvent } from "../../../../src/types/index.js"; +import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "../../../eboActor/fixtures.js"; +import mocks from "../../../mocks/index.js"; + +describe("UpdateDisputeStatus", () => { + let registry: EboRegistry; + + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + const response = mocks.buildResponse(request); + const dispute = mocks.buildDispute(request, response); + + const event: EboEvent<"DisputeStatusChanged"> = { + name: "DisputeStatusChanged", + blockNumber: 1n, + logIndex: 1, + requestId: request.id, + metadata: { + blockNumber: 1n, + dispute: dispute.prophetData, + disputeId: dispute.id, + status: dispute.status === "Active" ? "Lost" : "Active", + }, + }; + + beforeEach(() => { + registry = { + getDispute: vi.fn().mockReturnValue(dispute), + updateDisputeStatus: vi.fn(), + } as unknown as EboRegistry; + }); + + describe("run", () => { + it("updates the dispute status in the registry", () => { + const command = UpdateDisputeStatus.buildFromEvent(event, registry); + + command.run(); + + expect(registry.updateDisputeStatus).toHaveBeenCalledWith( + event.metadata.disputeId, + event.metadata.status, + ); + }); + + it("throws if the command was already run", () => { + const command = UpdateDisputeStatus.buildFromEvent(event, registry); + + command.run(); + + expect(() => command.run()).toThrow(CommandAlreadyRun); + }); + }); + + describe("undo", () => { + it("reverts the dispute status to the previous status", () => { + const command = UpdateDisputeStatus.buildFromEvent(event, registry); + + const previousStatus = dispute.status; + + command.run(); + command.undo(); + + expect(registry.updateDisputeStatus).toHaveBeenCalledTimes(2); + expect(registry.updateDisputeStatus).toHaveBeenNthCalledWith( + 2, + event.metadata.disputeId, + previousStatus, + ); + }); + + it("throws if undoing the command before being run", () => { + const command = UpdateDisputeStatus.buildFromEvent(event, registry); + + expect(() => command.undo()).toThrow(CommandNotRun); + }); + }); +}); From 8386c8d9188af56e1c910497b029516a6f5c768e Mon Sep 17 00:00:00 2001 From: Yaco 0x Date: Tue, 3 Sep 2024 18:27:19 -0300 Subject: [PATCH 2/6] feat: use DisputeEscalated instead of DisputeStatusChanged --- packages/automated-dispute/src/eboActor.ts | 12 ++++++----- .../services/eboRegistry/commands/index.ts | 2 +- .../commands/updateDisputeStatus.ts | 15 ++++++++++--- .../automated-dispute/src/types/events.ts | 16 ++++++++++---- .../commands/updateDisputeStatus.spec.ts | 21 +++++++++++++++++++ 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/automated-dispute/src/eboActor.ts b/packages/automated-dispute/src/eboActor.ts index 592b682..dc9ed53 100644 --- a/packages/automated-dispute/src/eboActor.ts +++ b/packages/automated-dispute/src/eboActor.ts @@ -194,6 +194,12 @@ export class EboActor { this.registry, ); + case "DisputeEscalated": + return UpdateDisputeStatus.buildFromEvent( + event as EboEvent<"DisputeEscalated">, + this.registry, + ); + case "RequestFinalized": return Noop.buildFromEvent(); @@ -711,11 +717,7 @@ export class EboActor { case "Active": // Case handled by ResponseDisputed case "Lost": // Relevant during periodic request state checks case "Won": // Relevant during periodic request state checks - break; - - case "Escalated": - await this.onDisputeEscalated(disputeId, request); - + case "Escalated": // Case handled by DisputeEscalated break; case "NoResolution": diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/index.ts b/packages/automated-dispute/src/services/eboRegistry/commands/index.ts index 13e989e..c05c58f 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/index.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/index.ts @@ -1,5 +1,5 @@ +export * from "./addDispute.js"; export * from "./addRequest.js"; export * from "./addResponse.js"; -export * from "./addDispute.js"; export * from "./noop.js"; export * from "./updateDisputeStatus.js"; diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts b/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts index dfd8fa1..7fcea96 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts @@ -1,6 +1,6 @@ import { CommandAlreadyRun, CommandNotRun, DisputeNotFound } from "../../../exceptions/index.js"; import { EboRegistry, EboRegistryCommand } from "../../../interfaces/index.js"; -import { DisputeStatus, EboEvent } from "../../../types/index.js"; +import { DisputeStatus, EboEvent, EboEventName } from "../../../types/index.js"; export class UpdateDisputeStatus implements EboRegistryCommand { private wasRun: boolean = false; @@ -13,15 +13,24 @@ export class UpdateDisputeStatus implements EboRegistryCommand { ) {} public static buildFromEvent( - event: EboEvent<"DisputeStatusChanged">, + event: EboEvent<"DisputeStatusChanged" | "DisputeEscalated">, registry: EboRegistry, ): UpdateDisputeStatus { const disputeId = event.metadata.disputeId; - const status = event.metadata.status; + + const status = this.isDisputeStatusChangedEvent(event) + ? event.metadata.status + : "Escalated"; return new UpdateDisputeStatus(registry, disputeId, status); } + private static isDisputeStatusChangedEvent( + event: EboEvent, + ): event is EboEvent<"DisputeStatusChanged"> { + return event.name === "DisputeStatusChanged"; + } + run(): void { if (this.wasRun) throw new CommandAlreadyRun(UpdateDisputeStatus.name); diff --git a/packages/automated-dispute/src/types/events.ts b/packages/automated-dispute/src/types/events.ts index 4bbb0c6..096b522 100644 --- a/packages/automated-dispute/src/types/events.ts +++ b/packages/automated-dispute/src/types/events.ts @@ -1,5 +1,5 @@ import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; -import { Log } from "viem"; +import { Address, Log } from "viem"; import { Dispute, DisputeStatus, Request, RequestId, Response } from "./prophet.js"; @@ -43,6 +43,12 @@ export interface DisputeStatusChanged { blockNumber: bigint; } +export interface DisputeEscalated { + caller: Address; + disputeId: string; + blockNumber: bigint; +} + export interface RequestFinalized { requestId: string; responseId: string; @@ -60,9 +66,11 @@ export type EboEventData = E extends "NewEpoch" ? ResponseDisputed : E extends "DisputeStatusChanged" ? DisputeStatusChanged - : E extends "RequestFinalized" - ? RequestFinalized - : never; + : E extends "DisputeEscalated" + ? DisputeEscalated + : E extends "RequestFinalized" + ? RequestFinalized + : never; export type EboEvent = { name: T; diff --git a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts index 40e5c49..6f86c60 100644 --- a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts +++ b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts @@ -46,6 +46,27 @@ describe("UpdateDisputeStatus", () => { ); }); + it("escalates when the event is DisputeEscalated", () => { + const escalatedDisputeEvent: EboEvent<"DisputeEscalated"> = { + ...event, + name: "DisputeEscalated", + metadata: { + disputeId: "0x01", + blockNumber: event.blockNumber, + caller: "0x01", + }, + }; + + const command = UpdateDisputeStatus.buildFromEvent(escalatedDisputeEvent, registry); + + command.run(); + + expect(registry.updateDisputeStatus).toHaveBeenCalledWith( + event.metadata.disputeId, + "Escalated", + ); + }); + it("throws if the command was already run", () => { const command = UpdateDisputeStatus.buildFromEvent(event, registry); From 5e35d5f153e471c006b81f3915093239b4020fdf Mon Sep 17 00:00:00 2001 From: Yaco 0x Date: Wed, 4 Sep 2024 17:05:42 -0300 Subject: [PATCH 3/6] feat: implement onLastEvent handler on EboActor --- packages/automated-dispute/src/eboActor.ts | 73 +++++++++++++------ .../tests/services/eboActor.spec.ts | 18 +++-- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/packages/automated-dispute/src/eboActor.ts b/packages/automated-dispute/src/eboActor.ts index dc9ed53..8a1f054 100644 --- a/packages/automated-dispute/src/eboActor.ts +++ b/packages/automated-dispute/src/eboActor.ts @@ -143,7 +143,7 @@ export class EboActor { if (this.eventsQueue.isEmpty()) { // `event` is the last and most recent event thus // it needs to run some RPCs to keep Prophet's flow going on - await this.onNewEvent(event); + await this.onLastEvent(event); } } catch (err) { this.logger.error(`Error processing event ${event.name}: ${err}`); @@ -209,15 +209,47 @@ export class EboActor { } /** - * Handle a new event and triggers reactive interactions with smart contracts. + * Handle the last known event and triggers reactive interactions with smart contracts. * * A basic example would be reacting to a new request by proposing a response. * - * @param _event EBO event + * @param event EBO event */ - private async onNewEvent(_event: EboEvent) { - // TODO - return; + private async onLastEvent(event: EboEvent) { + switch (event.name) { + case "RequestCreated": + await this.onRequestCreated(event as EboEvent<"RequestCreated">); + + break; + + case "ResponseProposed": + await this.onResponseProposed(event as EboEvent<"ResponseProposed">); + + break; + + case "ResponseDisputed": + await this.onResponseDisputed(event as EboEvent<"ResponseDisputed">); + + break; + + case "DisputeStatusChanged": + await this.onDisputeStatusChanged(event as EboEvent<"DisputeStatusChanged">); + + break; + + case "DisputeEscalated": + await this.onDisputeEscalated(event as EboEvent<"DisputeEscalated">); + + break; + + case "RequestFinalized": + await this.onRequestFinalized(event as EboEvent<"RequestFinalized">); + + break; + + default: + throw new UnknownEvent(event.name); + } } /** @@ -404,15 +436,7 @@ export class EboActor { * * @param event `RequestCreated` event */ - public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise { - if (this.registry.getRequest(event.metadata.requestId)) { - this.logger.error( - `The request ${event.metadata.requestId} was already being handled by an actor.`, - ); - - throw new InvalidActorState(); - } - + private async onRequestCreated(event: EboEvent<"RequestCreated">): Promise { if (this.anyActiveProposal()) { // Skipping new proposal until the actor receives a ResponseDisputed event; // at that moment, it will be possible to re-propose again. @@ -539,7 +563,7 @@ export class EboActor { * @param event a `ResponseProposed` event * @returns void */ - public async onResponseProposed(event: EboEvent<"ResponseProposed">): Promise { + private async onResponseProposed(event: EboEvent<"ResponseProposed">): Promise { const eventResponse = event.metadata.response; const actorResponse = await this.buildResponse(eventResponse.response.chainId); @@ -600,7 +624,7 @@ export class EboActor { * * @param event `ResponseDisputed` event. */ - public async onResponseDisputed(event: EboEvent<"ResponseDisputed">): Promise { + private async onResponseDisputed(event: EboEvent<"ResponseDisputed">): Promise { const dispute = this.registry.getDispute(event.metadata.disputeId); if (!dispute) @@ -699,7 +723,7 @@ export class EboActor { * * @param event `DisputeStatusChanged` event */ - public async onDisputeStatusChanged(event: EboEvent<"DisputeStatusChanged">): Promise { + private async onDisputeStatusChanged(event: EboEvent<"DisputeStatusChanged">): Promise { const request = this.getActorRequest(); const disputeId = event.metadata.disputeId; const disputeStatus = event.metadata.status; @@ -730,9 +754,14 @@ export class EboActor { } } - private async onDisputeEscalated(disputeId: string, request: Request) { + private async onDisputeEscalated(event: EboEvent<"DisputeEscalated">) { + const request = this.getActorRequest(); + // TODO: notify - this.logger.info(`Dispute ${disputeId} for request ${request.id} has been escalated.`); + + this.logger.info( + `Dispute ${event.metadata.disputeId} for request ${request.id} has been escalated.`, + ); } private async onDisputeWithNoResolution(disputeId: string, request: Request) { @@ -763,9 +792,7 @@ export class EboActor { * * @param event `ResponseFinalized` event */ - public async onRequestFinalized(event: EboEvent<"RequestFinalized">): Promise { - this.shouldHandleRequest(event.metadata.requestId); - + private async onRequestFinalized(_event: EboEvent<"RequestFinalized">): Promise { const request = this.getActorRequest(); this.logger.info(`Request ${request.id} has been finalized.`); diff --git a/packages/automated-dispute/tests/services/eboActor.spec.ts b/packages/automated-dispute/tests/services/eboActor.spec.ts index c58cf58..6b1bc32 100644 --- a/packages/automated-dispute/tests/services/eboActor.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor.spec.ts @@ -54,7 +54,7 @@ describe("EboActor", () => { const { actor } = mocks.buildEboActor(request, logger); // TODO: mock the procol provider instead - actor["onNewEvent"] = vi.fn().mockImplementation(() => Promise.resolve()); + actor["onLastEvent"] = vi.fn().mockImplementation(() => Promise.resolve()); actor.enqueue(processedEvent); @@ -77,6 +77,8 @@ describe("EboActor", () => { const { actor } = mocks.buildEboActor(request, logger); const queue = actor["eventsQueue"]; + actor["onLastEvent"] = vi.fn().mockImplementation(() => Promise.resolve()); + actor.enqueue(event); expect(queue.size()).toEqual(1); @@ -92,7 +94,7 @@ describe("EboActor", () => { const mockEventsQueuePush = vi.spyOn(queue, "push"); - actor["onNewEvent"] = vi.fn().mockImplementation(() => Promise.reject()); + actor["onLastEvent"] = vi.fn().mockImplementation(() => Promise.reject()); actor.enqueue(event); @@ -109,7 +111,7 @@ describe("EboActor", () => { const firstEvent = { ...event }; const secondEvent = { ...firstEvent, blockNumber: firstEvent.blockNumber + 1n }; - actor["onNewEvent"] = vi.fn().mockImplementation(() => { + actor["onLastEvent"] = vi.fn().mockImplementation(() => { return new Promise((resolve, reject) => { setTimeout(() => { reject(); @@ -186,7 +188,7 @@ describe("EboActor", () => { const { actor } = mocks.buildEboActor(request, logger); - const onNewEventDelay20 = () => { + const onLastEventDelay20 = () => { callOrder.push(1); return new Promise((resolve) => { @@ -198,7 +200,7 @@ describe("EboActor", () => { }); }; - const onNewEventDelay1 = () => { + const onLastEventDelay1 = () => { callOrder.push(2); return new Promise((resolve) => { @@ -210,10 +212,10 @@ describe("EboActor", () => { }); }; - actor["onNewEvent"] = vi + actor["onLastEvent"] = vi .fn() - .mockImplementationOnce(onNewEventDelay20) - .mockImplementationOnce(onNewEventDelay1); + .mockImplementationOnce(onLastEventDelay20) + .mockImplementationOnce(onLastEventDelay1); setTimeout(() => { actor.enqueue(firstEvent); From cc6d6441bf99c2c6cb4ab2baae59304ac9e56246 Mon Sep 17 00:00:00 2001 From: Yaco 0x Date: Wed, 4 Sep 2024 17:06:22 -0300 Subject: [PATCH 4/6] fix: fix tests for all event handlers --- .../eboActor/onDisputeStatusChanged.spec.ts | 22 ++- .../tests/eboActor/onRequestCreated.spec.ts | 168 ++++-------------- .../tests/eboActor/onRequestFinalized.spec.ts | 24 +-- .../tests/eboActor/onResponseDisputed.spec.ts | 92 ++++------ .../tests/eboActor/onResponseProposed.spec.ts | 35 ++-- .../tests/services/eboProcessor.spec.ts | 55 +++--- 6 files changed, 133 insertions(+), 263 deletions(-) diff --git a/packages/automated-dispute/tests/eboActor/onDisputeStatusChanged.spec.ts b/packages/automated-dispute/tests/eboActor/onDisputeStatusChanged.spec.ts index b353df7..3849fef 100644 --- a/packages/automated-dispute/tests/eboActor/onDisputeStatusChanged.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onDisputeStatusChanged.spec.ts @@ -12,7 +12,7 @@ const logger: ILogger = { debug: vi.fn(), }; -describe.skip("onDisputeStatusChanged", () => { +describe("onDisputeStatusChanged", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; const response = mocks.buildResponse(actorRequest); @@ -20,6 +20,7 @@ describe.skip("onDisputeStatusChanged", () => { const dispute = mocks.buildDispute(actorRequest, response, { status: "None" }); const event: EboEvent<"DisputeStatusChanged"> = { name: "DisputeStatusChanged", + requestId: actorRequest.id, blockNumber: 1n, logIndex: 1, metadata: { @@ -37,17 +38,18 @@ describe.skip("onDisputeStatusChanged", () => { const mockUpdateDisputeStatus = vi.spyOn(registry, "updateDisputeStatus"); - await actor.onDisputeStatusChanged(event); + actor.enqueue(event); + + await actor.processEvents(); expect(mockUpdateDisputeStatus).toHaveBeenCalledWith(dispute.id, "Lost"); }); - it.skip("notifies when dispute has been escalated"); - it("proposes a new response when dispute status goes into NoResolution", async () => { const dispute = mocks.buildDispute(actorRequest, response, { status: "Escalated" }); const event: EboEvent<"DisputeStatusChanged"> = { name: "DisputeStatusChanged", + requestId: actorRequest.id, blockNumber: 1n, logIndex: 1, metadata: { @@ -65,13 +67,22 @@ describe.skip("onDisputeStatusChanged", () => { vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); vi.spyOn(registry, "getDispute").mockReturnValue(dispute); + + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + currentEpoch: actorRequest.epoch, + currentEpochBlockNumber: actorRequest.createdAt, + currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0)), + }); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( response.prophetData.response.block + 1n, ); const mockProposeResponse = vi.spyOn(protocolProvider, "proposeResponse"); - await actor.onDisputeStatusChanged(event); + actor.enqueue(event); + + await actor.processEvents(); expect(mockProposeResponse).toHaveBeenCalledWith( actorRequest.id, @@ -81,5 +92,6 @@ describe.skip("onDisputeStatusChanged", () => { ); }); + it.skip("notifies when dispute has been escalated"); it.skip("notifies if it will duplicate old proposal when handling NoResolution"); }); diff --git a/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts b/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts index f80dee1..bb31d50 100644 --- a/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts @@ -1,31 +1,24 @@ -import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; import { ILogger } from "@ebo-agent/shared"; -import { Mutex } from "async-mutex"; import { Address } from "viem"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; -import { EboActor } from "../../src/eboActor.js"; -import { RequestMismatch } from "../../src/exceptions/index.js"; -import { ProtocolProvider } from "../../src/protocolProvider.js"; -import { EboMemoryRegistry } from "../../src/services/index.js"; import { EboEvent, Response } from "../../src/types/index.js"; import mocks from "../mocks/index.js"; -import { - DEFAULT_MOCKED_PROTOCOL_CONTRACTS, - DEFAULT_MOCKED_REQUEST_CREATED_DATA, -} from "./fixtures.js"; +import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.js"; const logger: ILogger = mocks.mockLogger(); describe("EboActor", () => { describe("processEvents", () => { describe("when RequestCreated is enqueued", () => { - const requestId: Address = "0x12345"; - const indexedChainId: Caip2ChainId = "eip155:137"; + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + + const requestId: Address = request.id; + const indexedChainId: Caip2ChainId = request.chainId; const protocolEpoch = { - currentEpoch: 1n, + currentEpoch: request.epoch, currentEpochBlockNumber: 1n, currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), }; @@ -39,30 +32,14 @@ describe("EboActor", () => { chainId: indexedChainId, epoch: protocolEpoch.currentEpoch, requestId: requestId, - request: DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, + request: request.prophetData, }, }; - let protocolProvider: ProtocolProvider; - let blockNumberService: BlockNumberService; - let registry: EboMemoryRegistry; - let eventProcessingMutex: Mutex; - - beforeEach(() => { - protocolProvider = new ProtocolProvider( - ["http://localhost:8538"], - DEFAULT_MOCKED_PROTOCOL_CONTRACTS, - ); - - const chainRpcUrls = new Map(); - chainRpcUrls.set(indexedChainId, ["http://localhost:8539"]); - - blockNumberService = new BlockNumberService(chainRpcUrls, logger); - registry = new EboMemoryRegistry(); - eventProcessingMutex = new Mutex(); - }); - it("stores the new request", async () => { + const { actor, blockNumberService, protocolProvider, registry } = + mocks.buildEboActor(request, logger); + const indexedEpochBlockNumber = 48n; vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( @@ -73,21 +50,6 @@ describe("EboActor", () => { Promise.resolve(), ); - const requestConfig = { - id: requestId, - epoch: protocolEpoch.currentEpoch, - epochTimestamp: protocolEpoch.currentEpochTimestamp, - }; - - const actor = new EboActor( - requestConfig, - protocolProvider, - blockNumberService, - registry, - eventProcessingMutex, - logger, - ); - const mockRegistryAddRequest = vi .spyOn(registry, "addRequest") .mockImplementation(() => {}); @@ -103,15 +65,20 @@ describe("EboActor", () => { ); }); - it.skip("rollbacks state updates if the rpc call fails"); + it("proposes a response", async () => { + const { actor, blockNumberService, protocolProvider } = mocks.buildEboActor( + request, + logger, + ); - it.skip("proposes a response", async () => { const indexedEpochBlockNumber = 48n; vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( indexedEpochBlockNumber, ); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch); + const proposeResponseMock = vi.spyOn(protocolProvider, "proposeResponse"); proposeResponseMock.mockImplementation( @@ -123,21 +90,6 @@ describe("EboActor", () => { ) => Promise.resolve(), ); - const requestConfig = { - id: requestId, - epoch: protocolEpoch.currentEpoch, - epochTimestamp: protocolEpoch.currentEpochTimestamp, - }; - - const actor = new EboActor( - requestConfig, - protocolProvider, - blockNumberService, - registry, - eventProcessingMutex, - logger, - ); - actor.enqueue(requestCreatedEvent); await actor.processEvents(); @@ -150,7 +102,10 @@ describe("EboActor", () => { ); }); - it.skip("does not propose when already proposed the same block", async () => { + it("does not propose when already proposed the same block", async () => { + const { actor, protocolProvider, blockNumberService, registry } = + mocks.buildEboActor(request, logger); + const indexedEpochBlockNumber = 48n; vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( @@ -168,25 +123,10 @@ describe("EboActor", () => { ) => Promise.resolve(), ); - const requestConfig = { - id: requestId, - epoch: protocolEpoch.currentEpoch, - epochTimestamp: protocolEpoch.currentEpochTimestamp, - }; - - const actor = new EboActor( - requestConfig, - protocolProvider, - blockNumberService, - registry, - eventProcessingMutex, - logger, - ); - const previousResponses = new Map(); previousResponses.set("0x01", { id: "0x01", - wasDisputed: false, + createdAt: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), prophetData: { proposer: "0x02", requestId: requestId, @@ -198,66 +138,18 @@ describe("EboActor", () => { }, }); - vi.spyOn(registry, "getResponses").mockReturnValue(previousResponses); - - await actor.onRequestCreated(requestCreatedEvent); - - expect(proposeResponseMock).not.toHaveBeenCalled(); - }); - - it.skip("throws if the event's request id does not match with actor's", () => { - const noMatchRequestCreatedEvent: EboEvent<"RequestCreated"> = { - blockNumber: 34n, - logIndex: 1, - name: "RequestCreated", - metadata: { - chainId: "eip155:10", - epoch: protocolEpoch.currentEpoch, - requestId: "0x000000" as Address, - request: DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, - }, - }; - - const requestConfig = { - id: requestId, - epoch: protocolEpoch.currentEpoch, - epochTimestamp: protocolEpoch.currentEpochTimestamp, - }; - - const actor = new EboActor( - requestConfig, - protocolProvider, - blockNumberService, - registry, - eventProcessingMutex, - logger, + vi.spyOn(registry, "getResponses").mockReturnValue( + Object.values(previousResponses), ); - expect(actor.onRequestCreated(noMatchRequestCreatedEvent)).rejects.toThrowError( - RequestMismatch, - ); - }); - - it.skip("throws if the indexed chain block number cannot be fetched", () => { - vi.spyOn(blockNumberService, "getEpochBlockNumber").mockRejectedValue(new Error()); + actor.enqueue(requestCreatedEvent); - const requestConfig = { - id: requestId, - epoch: protocolEpoch.currentEpoch, - epochTimestamp: protocolEpoch.currentEpochTimestamp, - }; - - const actor = new EboActor( - requestConfig, - protocolProvider, - blockNumberService, - registry, - eventProcessingMutex, - logger, - ); + await actor.processEvents(); - expect(actor.onRequestCreated(requestCreatedEvent)).rejects.toBeDefined(); + expect(proposeResponseMock).not.toHaveBeenCalled(); }); + + it.todo("throws if the indexed chain block number cannot be fetched"); }); }); }); diff --git a/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts b/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts index 690b2f4..6f716eb 100644 --- a/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts @@ -1,19 +1,19 @@ import { ILogger } from "@ebo-agent/shared"; import { describe, expect, it, vi } from "vitest"; -import { InvalidActorState } from "../../src/exceptions/invalidActorState.exception.js"; -import { EboEvent } from "../../src/types/events.js"; +import { EboEvent } from "../../src/types/index.js"; import mocks from "../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.js"; const logger: ILogger = mocks.mockLogger(); -describe.skip("EboActor", () => { +describe("EboActor", () => { describe("onRequestFinalized", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; const event: EboEvent<"RequestFinalized"> = { name: "RequestFinalized", + requestId: actorRequest.id, blockNumber: 1n, logIndex: 1, metadata: { @@ -31,25 +31,13 @@ describe.skip("EboActor", () => { const mockInfo = vi.spyOn(logger, "info"); - await actor.onRequestFinalized(event); + actor.enqueue(event); + + await actor.processEvents(); expect(mockInfo).toHaveBeenCalledWith( expect.stringMatching(`Request ${actorRequest.id} has been finalized.`), ); }); - - it("throws if the event's request is not handled by actor", () => { - const { actor } = mocks.buildEboActor(actorRequest, logger); - - const otherRequestEvent = { - ...event, - metadata: { - ...event.metadata, - requestId: actorRequest.id + "123", - }, - }; - - expect(actor.onRequestFinalized(otherRequestEvent)).rejects.toThrow(InvalidActorState); - }); }); }); diff --git a/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts b/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts index cc40912..c3eba6d 100644 --- a/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts @@ -1,8 +1,6 @@ import { ILogger } from "@ebo-agent/shared"; -import { ContractFunctionRevertedError } from "viem"; import { describe, expect, it, vi } from "vitest"; -import { InvalidActorState } from "../../src/exceptions/invalidActorState.exception.js"; import { EboEvent } from "../../src/types/events.js"; import { Response } from "../../src/types/prophet.js"; import mocks from "../mocks/index.js"; @@ -10,12 +8,13 @@ import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.js"; const logger: ILogger = mocks.mockLogger(); -describe.skip("onResponseDisputed", () => { +describe("onResponseDisputed", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; const response: Response = mocks.buildResponse(actorRequest); const event: EboEvent<"ResponseDisputed"> = { name: "ResponseDisputed", + requestId: actorRequest.id, blockNumber: 1n, logIndex: 1, metadata: { @@ -39,13 +38,21 @@ describe.skip("onResponseDisputed", () => { vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); vi.spyOn(registry, "getResponse").mockReturnValue(response); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + currentEpoch: actorRequest.epoch, + currentEpochBlockNumber: response.prophetData.response.block, + currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 1, 0, 0, 0)), + }); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( response.prophetData.response.block + 1n, ); const mockPledgeForDispute = vi.spyOn(protocolProvider, "pledgeForDispute"); - await actor.onResponseDisputed(event); + actor.enqueue(event); + + await actor.processEvents(); expect(mockPledgeForDispute).toHaveBeenCalled(); }); @@ -59,13 +66,21 @@ describe.skip("onResponseDisputed", () => { vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); vi.spyOn(registry, "getResponse").mockReturnValue(response); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + currentEpoch: actorRequest.epoch, + currentEpochBlockNumber: response.prophetData.response.block, + currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 1, 0, 0, 0)), + }); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( response.prophetData.response.block, ); const mockPledgeAgainstDispute = vi.spyOn(protocolProvider, "pledgeAgainstDispute"); - await actor.onResponseDisputed(event); + actor.enqueue(event); + + await actor.processEvents(); expect(mockPledgeAgainstDispute).toHaveBeenCalled(); }); @@ -79,6 +94,12 @@ describe.skip("onResponseDisputed", () => { vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); vi.spyOn(registry, "getResponse").mockReturnValue(response); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + currentEpoch: actorRequest.epoch, + currentEpochBlockNumber: response.prophetData.response.block, + currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 1, 0, 0, 0)), + }); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( response.prophetData.response.block, ); @@ -87,63 +108,14 @@ describe.skip("onResponseDisputed", () => { const addResponseMock = vi.spyOn(registry, "addDispute"); - await actor.onResponseDisputed(event); - - expect(addResponseMock).toHaveBeenCalled(); - }); - - it("resolves if the pledge is reverted", async () => { - const { actor, blockNumberService, protocolProvider, registry } = mocks.buildEboActor( - actorRequest, - logger, - ); - - vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); - vi.spyOn(registry, "getResponse").mockReturnValue(response); - - vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( - response.prophetData.response.block + 1n, - ); - - vi.spyOn(protocolProvider, "pledgeForDispute").mockRejectedValue( - Object.create(ContractFunctionRevertedError.prototype), - ); - - expect(actor.onResponseDisputed(event)).resolves.toBeUndefined(); - }); - - it("throws if protocol provider cannot complete pledge", () => { - const { actor, blockNumberService, protocolProvider, registry } = mocks.buildEboActor( - actorRequest, - logger, - ); + actor.enqueue(event); - vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); - vi.spyOn(registry, "getResponse").mockReturnValue(response); + await actor.processEvents(); - vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( - response.prophetData.response.block + 1n, - ); - - vi.spyOn(protocolProvider, "pledgeForDispute").mockRejectedValue(new Error()); - - expect(actor.onResponseDisputed(event)).rejects.toThrow(); + expect(addResponseMock).toHaveBeenCalled(); }); - it("throws if the response's request is not handled by actor", () => { - const { actor } = mocks.buildEboActor(actorRequest, logger); - - const otherRequestEvent = { - ...event, - metadata: { - ...event.metadata, - dispute: { - ...event.metadata.dispute, - requestId: "0x02", - }, - }, - }; - - expect(actor.onResponseDisputed(otherRequestEvent)).rejects.toThrow(InvalidActorState); - }); + // TODO: handle when error handling of reverts is implemented + it.todo("resolves if the pledge is reverted"); + it.todo("throws if protocol provider cannot complete pledge"); }); diff --git a/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts b/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts index 9a803b7..11da0da 100644 --- a/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts @@ -1,7 +1,6 @@ import { ILogger } from "@ebo-agent/shared"; import { describe, expect, it, vi } from "vitest"; -import { InvalidActorState } from "../../src/exceptions/invalidActorState.exception"; import { EboEvent } from "../../src/types/events"; import mocks from "../mocks/index.ts"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.ts"; @@ -56,23 +55,7 @@ describe("EboActor", () => { expect(addResponseMock).toHaveBeenCalled(); }); - it.skip("throws if the response's request is not handled by actor", () => { - const { actor } = mocks.buildEboActor(actorRequest, logger); - - const otherRequestEvent = { - ...responseProposedEvent, - metadata: { - ...responseProposedEvent.metadata, - requestId: responseProposedEvent.metadata.requestId + "123", - }, - }; - - expect(actor.onResponseProposed(otherRequestEvent)).rejects.toThrowError( - InvalidActorState, - ); - }); - - it.skip("does not dispute the response if seems valid", async () => { + it("does not dispute the response if seems valid", async () => { const { actor, registry, blockNumberService, protocolProvider } = mocks.buildEboActor(actorRequest, logger); @@ -84,24 +67,34 @@ describe("EboActor", () => { const mockDisputeResponse = vi.spyOn(protocolProvider, "disputeResponse"); - await actor.onResponseProposed(responseProposedEvent); + actor.enqueue(responseProposedEvent); + + await actor.processEvents(); expect(mockDisputeResponse).not.toHaveBeenCalled(); }); - it.skip("dispute the response if it should be different", async () => { + it("disputes the response if it should be different", async () => { const { actor, registry, blockNumberService, protocolProvider } = mocks.buildEboActor(actorRequest, logger); vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + currentEpoch: proposeData.epoch, + currentEpochBlockNumber: 1n, + currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + }); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( proposeData.block + 1n, ); const mockDisputeResponse = vi.spyOn(protocolProvider, "disputeResponse"); - await actor.onResponseProposed(responseProposedEvent); + actor.enqueue(responseProposedEvent); + + await actor.processEvents(); expect(mockDisputeResponse).toHaveBeenCalled(); }); diff --git a/packages/automated-dispute/tests/services/eboProcessor.spec.ts b/packages/automated-dispute/tests/services/eboProcessor.spec.ts index 8439226..ac55fb7 100644 --- a/packages/automated-dispute/tests/services/eboProcessor.spec.ts +++ b/packages/automated-dispute/tests/services/eboProcessor.spec.ts @@ -4,8 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ProcessorAlreadyStarted } from "../../src/exceptions/index.js"; import { ProtocolProvider } from "../../src/protocolProvider.js"; -import { EboEvent, EboEventName } from "../../src/types/events.js"; -import { RequestId } from "../../src/types/prophet.js"; +import { EboEvent, EboEventName, RequestId } from "../../src/types/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "../eboActor/fixtures.js"; import mocks from "../mocks/index.js"; @@ -14,6 +13,8 @@ const msBetweenChecks = 1; describe("EboProcessor", () => { describe("start", () => { + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + beforeEach(() => { vi.useFakeTimers(); }); @@ -35,12 +36,12 @@ describe("EboProcessor", () => { name: "RequestCreated", blockNumber: 1n, logIndex: 1, - requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, + requestId: request.id, metadata: { - requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, - epoch: DEFAULT_MOCKED_REQUEST_CREATED_DATA.epoch, - chainId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.chainId, - request: DEFAULT_MOCKED_REQUEST_CREATED_DATA["prophetData"], + requestId: request.id, + epoch: request.epoch, + chainId: request.chainId, + request: request.prophetData, }, }; @@ -50,17 +51,18 @@ describe("EboProcessor", () => { ); vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([requestCreatedEvent]); - const mockCreateActor = vi.spyOn(actorsManager, "createActor"); + const { actor } = mocks.buildEboActor(request, logger); + const mockCreateActor = vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); await processor.start(msBetweenChecks); - const expectedNewActor = expect.objectContaining({ + const expectedActorRequest = expect.objectContaining({ id: requestCreatedEvent.requestId, epoch: currentEpoch.currentEpoch, }); expect(mockCreateActor).toHaveBeenCalledWith( - expectedNewActor, + expectedActorRequest, expect.any(ProtocolProvider), expect.any(BlockNumberService), logger, @@ -83,11 +85,13 @@ describe("EboProcessor", () => { vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([]); await processor.start(1); + expect(processor.start(1)).rejects.toThrow(ProcessorAlreadyStarted); }); it("fetches events since epoch start when starting", async () => { - const { processor, protocolProvider } = mocks.buildEboProcessor(logger); + const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); + const { actor } = mocks.buildEboActor(request, logger); const currentEpoch = { currentEpoch: 1n, @@ -101,17 +105,21 @@ describe("EboProcessor", () => { name: "RequestCreated", blockNumber: 1n, logIndex: 1, - requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, + requestId: request.id, metadata: { - requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, - epoch: DEFAULT_MOCKED_REQUEST_CREATED_DATA.epoch, - chainId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.chainId, - request: DEFAULT_MOCKED_REQUEST_CREATED_DATA["prophetData"], + requestId: request.id, + epoch: request.epoch, + chainId: request.chainId, + request: request.prophetData, }, }; vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); + vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); + vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "canBeTerminated").mockResolvedValue(false); const mockGetEvents = vi.spyOn(protocolProvider, "getEvents"); mockGetEvents.mockResolvedValue([requestCreatedEvent]); @@ -125,7 +133,8 @@ describe("EboProcessor", () => { }); it("fetches events since last block checked after first events fetch", async () => { - const { processor, protocolProvider } = mocks.buildEboProcessor(logger); + const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); + const { actor } = mocks.buildEboActor(request, logger); const mockLastCheckedBlock = 5n; processor["lastCheckedBlock"] = mockLastCheckedBlock; @@ -153,6 +162,10 @@ describe("EboProcessor", () => { vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); + vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); + vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "canBeTerminated").mockResolvedValue(false); const mockGetEvents = vi.spyOn(protocolProvider, "getEvents"); mockGetEvents.mockResolvedValue([requestCreatedEvent]); @@ -216,7 +229,7 @@ describe("EboProcessor", () => { .spyOn(actor, "processEvents") .mockImplementation(() => Promise.resolve()); - vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => {}); + vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); vi.spyOn(actorsManager, "createActor").mockResolvedValue(actor); vi.spyOn(actorsManager, "getActor").mockReturnValue(actor); @@ -294,8 +307,8 @@ describe("EboProcessor", () => { vi.spyOn(actor1, "processEvents").mockImplementation(() => Promise.resolve()); vi.spyOn(actor2, "processEvents").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor1, "onLastBlockUpdated").mockImplementation(() => {}); - vi.spyOn(actor2, "onLastBlockUpdated").mockImplementation(() => {}); + vi.spyOn(actor1, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor2, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); vi.spyOn(actorsManager, "getActor").mockImplementation((requestId: RequestId) => { switch (requestId) { @@ -338,7 +351,7 @@ describe("EboProcessor", () => { const { actor } = mocks.buildEboActor(request, logger); - vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => {}); + vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); vi.spyOn(actor, "canBeTerminated").mockReturnValue(true); vi.spyOn(actorsManager, "createActor").mockResolvedValue(actor); From f86f2cb7f82194644c7f3be4b8541900e7051b19 Mon Sep 17 00:00:00 2001 From: Yaco 0x Date: Wed, 4 Sep 2024 17:10:57 -0300 Subject: [PATCH 5/6] refactor: redefine EboActor.shouldHandleRequest --- packages/automated-dispute/src/eboActor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/automated-dispute/src/eboActor.ts b/packages/automated-dispute/src/eboActor.ts index 8a1f054..1bb9b7d 100644 --- a/packages/automated-dispute/src/eboActor.ts +++ b/packages/automated-dispute/src/eboActor.ts @@ -92,7 +92,7 @@ export class EboActor { * @param event EBO event */ public enqueue(event: EboEvent): void { - if (this.shouldHandleRequest(event.requestId)) { + if (!this.shouldHandleRequest(event.requestId)) { this.logger.error(`The request ${event.requestId} is not handled by this actor.`); throw new RequestMismatch(this.actorRequest.id, event.requestId); @@ -602,7 +602,7 @@ export class EboActor { * @returns `true` if the actor is handling the request, `false` otherwise */ private shouldHandleRequest(requestId: string) { - return this.actorRequest.id.toLowerCase() !== requestId.toLowerCase(); + return this.actorRequest.id.toLowerCase() === requestId.toLowerCase(); } /** From 373d810fd92a336a81b664e0fc61aa1576093a8c Mon Sep 17 00:00:00 2001 From: Yaco 0x Date: Thu, 5 Sep 2024 14:08:58 -0300 Subject: [PATCH 6/6] refactor: remove NewEpoch event --- .../automated-dispute/src/types/events.ts | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/automated-dispute/src/types/events.ts b/packages/automated-dispute/src/types/events.ts index 096b522..0709879 100644 --- a/packages/automated-dispute/src/types/events.ts +++ b/packages/automated-dispute/src/types/events.ts @@ -4,7 +4,6 @@ import { Address, Log } from "viem"; import { Dispute, DisputeStatus, Request, RequestId, Response } from "./prophet.js"; export type EboEventName = - | "NewEpoch" | "RequestCreated" | "ResponseProposed" | "ResponseDisputed" @@ -12,11 +11,6 @@ export type EboEventName = | "DisputeEscalated" | "RequestFinalized"; -export interface NewEpoch { - epoch: bigint; - epochBlockNumber: bigint; -} - export interface ResponseProposed { requestId: string; responseId: string; @@ -56,21 +50,19 @@ export interface RequestFinalized { blockNumber: bigint; } -export type EboEventData = E extends "NewEpoch" - ? NewEpoch - : E extends "RequestCreated" - ? RequestCreated - : E extends "ResponseProposed" - ? ResponseProposed - : E extends "ResponseDisputed" - ? ResponseDisputed - : E extends "DisputeStatusChanged" - ? DisputeStatusChanged - : E extends "DisputeEscalated" - ? DisputeEscalated - : E extends "RequestFinalized" - ? RequestFinalized - : never; +export type EboEventData = E extends "RequestCreated" + ? RequestCreated + : E extends "ResponseProposed" + ? ResponseProposed + : E extends "ResponseDisputed" + ? ResponseDisputed + : E extends "DisputeStatusChanged" + ? DisputeStatusChanged + : E extends "DisputeEscalated" + ? DisputeEscalated + : E extends "RequestFinalized" + ? RequestFinalized + : never; export type EboEvent = { name: T;