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); + }); + }); +});