Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pending registry commands #32

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 27 additions & 39 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -378,9 +399,6 @@ export class EboActor {
* @param event `RequestCreated` event
*/
public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise<void> {
if (event.metadata.requestId != this.actorRequest.id)
throw new RequestMismatch(this.actorRequest.id, event.metadata.requestId);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why are we skiping this validation.

if (this.registry.getRequest(event.metadata.requestId)) {
this.logger.error(
`The request ${event.metadata.requestId} was already being handled by an actor.`,
Expand All @@ -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.
Expand Down Expand Up @@ -526,16 +534,6 @@ export class EboActor {
* @returns void
*/
public async onResponseProposed(event: EboEvent<"ResponseProposed">): Promise<void> {
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);

Expand Down Expand Up @@ -597,16 +595,12 @@ export class EboActor {
* @param event `ResponseDisputed` event.
*/
public async onResponseDisputed(event: EboEvent<"ResponseDisputed">): Promise<void> {
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);
Expand Down Expand Up @@ -700,16 +694,10 @@ export class EboActor {
* @param event `DisputeStatusChanged` event
*/
public async onDisputeStatusChanged(event: EboEvent<"DisputeStatusChanged">): Promise<void> {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
Expand Down
11 changes: 9 additions & 2 deletions packages/automated-dispute/src/interfaces/eboRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -81,4 +81,9 @@ export class EboMemoryRegistry implements EboRegistry {
status: status,
});
}

/** @inheritdoc */
removeDispute(disputeId: string): boolean {
return this.disputes.delete(disputeId);
}
}
2 changes: 1 addition & 1 deletion packages/automated-dispute/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface RequestCreated {
epoch: bigint;
chainId: Caip2ChainId;
request: Request["prophetData"];
requestId: string;
requestId: RequestId;
}

export interface ResponseDisputed {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading