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: add condition to terminate actors #36

Merged
merged 4 commits into from
Sep 9, 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
46 changes: 40 additions & 6 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import {
AddDispute,
AddRequest,
AddResponse,
Noop,
FinalizeRequest,
UpdateDisputeStatus,
} from "./services/index.js";
import {
Dispute,
DisputeStatus,
EboEvent,
EboEventName,
Request,
Expand Down Expand Up @@ -201,7 +202,10 @@ export class EboActor {
);

case "RequestFinalized":
return Noop.buildFromEvent();
return FinalizeRequest.buildFromEvent(
event as EboEvent<"RequestFinalized">,
this.registry,
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we move this entire method to an isolated CommandsFactory?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's a TODO comment already, in the docs of this method, for doing that hehe


default:
throw new UnknownEvent(event.name);
Expand Down Expand Up @@ -408,7 +412,7 @@ export class EboActor {
const request = this.getActorRequest();
const dispute = this.registry.getResponseDispute(response);
const disputeWindow =
response.createdAt + request.prophetData.responseModuleData.disputeWindow;
response.createdAt + request.prophetData.disputeModuleData.disputeWindow;

// Response is still able to be disputed
if (blockNumber <= disputeWindow) return false;
Expand All @@ -424,11 +428,41 @@ export class EboActor {
*
* Be aware that a request can be finalized but some of its disputes can still be pending resolution.
*
* @param blockNumber block number to check entities at
* @returns `true` if all entities are settled, otherwise `false`
*/
public canBeTerminated(): boolean {
// TODO
throw new Error("Implement me");
public canBeTerminated(blockNumber: bigint): boolean {
const request = this.getActorRequest();
const isRequestFinalized = request.status === "Finalized";
const nonSettledProposals = this.activeProposals(blockNumber);

return isRequestFinalized && nonSettledProposals.length === 0;
}

/**
* Check for any active proposals at a specific block number.
*
* @param blockNumber block number to check proposals' status against
* @returns an array of `Response` instances
*/
private activeProposals(blockNumber: bigint): Response[] {
const responses = this.registry.getResponses();

return responses.filter((response) => {
if (this.isResponseAccepted(response, blockNumber)) return false;

const dispute = this.registry.getResponseDispute(response);

// Response has not been disputed but is not accepted yet, so it's active.
if (!dispute) return true;

// The rest of the status (ie "Escalated" | "Won" | "Lost" | "NoResolution")
// cannot be changed by the EBO agent once they've been reached so they make
// the proposal non-active.
const activeStatus: DisputeStatus[] = ["None", "Active"];

return activeStatus.includes(dispute.status);
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./commandAlreadyRun.js";
export * from "./commandNotRun.js";
export * from "./disputeNotFound.js";
export * from "./requestNotFound.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class RequestNotFound extends Error {
constructor(requestId: string) {
super(`Request ${requestId} was not found.`);

this.name = "RequestNotFound";
}
}
17 changes: 16 additions & 1 deletion packages/automated-dispute/src/interfaces/eboRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Dispute, DisputeStatus, Request, RequestId, Response } from "../types/prophet.js";
import {
Dispute,
DisputeStatus,
Request,
RequestId,
RequestStatus,
Response,
} from "../types/index.js";

/** Registry that stores Prophet entities (ie. requests, responses and disputes) */
export interface EboRegistry {
Expand All @@ -17,6 +24,14 @@ export interface EboRegistry {
*/
getRequest(requestId: RequestId): Request | undefined;

/**
* Update the request status based on its ID.
*
* @param requestId the ID of the `Request`
* @param status the `Request` status
*/
updateRequestStatus(requestId: string, status: RequestStatus): void;

/**
* Remove a `Request` by its ID.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/automated-dispute/src/services/eboProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class EboProcessor {
await actor.processEvents();
await actor.onLastBlockUpdated(lastBlock);

if (actor.canBeTerminated()) {
if (actor.canBeTerminated(lastBlock)) {
this.terminateActor(requestId);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class AddRequest implements EboRegistryCommand {
epoch: event.metadata.epoch,
createdAt: event.blockNumber,
prophetData: event.metadata.request,
status: "Active",
};

return new AddRequest(registry, request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { CommandAlreadyRun, CommandNotRun, RequestNotFound } from "../../../exceptions/index.js";
import { EboRegistry, EboRegistryCommand } from "../../../interfaces/index.js";
import { EboEvent, Request, RequestStatus } from "../../../types/index.js";

export class FinalizeRequest implements EboRegistryCommand {
private wasRun: boolean = false;
private previousStatus?: RequestStatus;

private constructor(
private readonly registry: EboRegistry,
private readonly request: Request,
) {}

public static buildFromEvent(
event: EboEvent<"RequestFinalized">,
registry: EboRegistry,
): FinalizeRequest {
const requestId = event.metadata.requestId;
const request = registry.getRequest(requestId);

if (!request) throw new RequestNotFound(requestId);

return new FinalizeRequest(registry, request);
}

run(): void {
if (this.wasRun) throw new CommandAlreadyRun(FinalizeRequest.name);

this.previousStatus = this.request.status;

this.registry.updateRequestStatus(this.request.id, "Finalized");

this.wasRun = true;
}

undo(): void {
if (!this.wasRun || !this.previousStatus) throw new CommandNotRun(FinalizeRequest.name);

this.registry.updateRequestStatus(this.request.id, this.previousStatus);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./addDispute.js";
export * from "./addRequest.js";
export * from "./addResponse.js";
export * from "./noop.js";
export * from "./finalizeRequest.js";
export * from "./updateDisputeStatus.js";

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { DisputeNotFound } from "../../exceptions/index.js";
import { DisputeNotFound, RequestNotFound } from "../../exceptions/index.js";
import { EboRegistry } from "../../interfaces/index.js";
import { Dispute, DisputeStatus, Request, RequestId, Response } from "../../types/index.js";
import {
Dispute,
DisputeStatus,
Request,
RequestId,
RequestStatus,
Response,
} from "../../types/index.js";

export class EboMemoryRegistry implements EboRegistry {
constructor(
Expand All @@ -20,6 +27,17 @@ export class EboMemoryRegistry implements EboRegistry {
return this.requests.get(requestId);
}

public updateRequestStatus(requestId: RequestId, status: RequestStatus): void {
const request = this.getRequest(requestId);

if (request === undefined) throw new RequestNotFound(requestId);

this.requests.set(requestId, {
...request,
status: status,
});
}

/** @inheritdoc */
public removeRequest(requestId: RequestId): boolean {
return this.requests.delete(requestId);
Expand Down
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 @@ -44,7 +44,7 @@ export interface DisputeEscalated {
}

export interface RequestFinalized {
requestId: string;
requestId: RequestId;
responseId: string;
caller: string;
blockNumber: bigint;
Expand Down
2 changes: 2 additions & 0 deletions packages/automated-dispute/src/types/prophet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { NormalizedAddress } from "@ebo-agent/shared";
import { Address } from "viem";

export type RequestId = NormalizedAddress;
export type RequestStatus = "Active" | "Finalized";

export interface Request {
id: RequestId;
chainId: Caip2ChainId;
epoch: bigint;
createdAt: bigint;
status: RequestStatus;

prophetData: Readonly<{
requester: Address;
Expand Down
1 change: 1 addition & 0 deletions packages/automated-dispute/tests/eboActor/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const DEFAULT_MOCKED_REQUEST_CREATED_DATA: Request = {
chainId: "eip155:1",
epoch: 1n,
createdAt: 1n,
status: "Active",
prophetData: {
disputeModule: "0x01" as Address,
finalityModule: "0x02" as Address,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,68 @@
import { ILogger } from "@ebo-agent/shared";
import { describe, expect, it, vi } from "vitest";

import { FinalizeRequest } from "../../src/services/index.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("EboActor", () => {
describe("onRequestFinalized", () => {
const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA;

const event: EboEvent<"RequestFinalized"> = {
name: "RequestFinalized",
requestId: actorRequest.id,
blockNumber: 1n,
logIndex: 1,
metadata: {
blockNumber: 1n,
caller: "0x01",
describe("processEvents", () => {
describe("when RequestFinalized is enqueued", () => {
const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA;

const event: EboEvent<"RequestFinalized"> = {
name: "RequestFinalized",
requestId: actorRequest.id,
responseId: "0x02",
},
};
blockNumber: 1n,
logIndex: 1,
metadata: {
blockNumber: 1n,
caller: "0x01",
requestId: actorRequest.id,
responseId: "0x02",
},
};

it("logs a message during request finalization", async () => {
const { actor, registry } = mocks.buildEboActor(actorRequest, logger);

vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest);

const mockInfo = vi.spyOn(logger, "info");

actor.enqueue(event);

await actor.processEvents();

expect(mockInfo).toHaveBeenCalledWith(
expect.stringMatching(`Request ${actorRequest.id} has been finalized.`),
);
});

it("uses the FinalizeRequest registry command", async () => {
const { actor, registry } = mocks.buildEboActor(actorRequest, logger);

it("logs a message during request finalization", async () => {
const { actor, registry } = mocks.buildEboActor(actorRequest, logger);
vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest);

vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest);
const mockFinalizeRequest = {
run: vi.fn(),
undo: vi.fn(),
} as unknown as FinalizeRequest;

const mockInfo = vi.spyOn(logger, "info");
const mockBuildFromEvent = vi
.spyOn(FinalizeRequest, "buildFromEvent")
.mockReturnValue(mockFinalizeRequest);

actor.enqueue(event);
actor.enqueue(event);

await actor.processEvents();
await actor.processEvents();

expect(mockInfo).toHaveBeenCalledWith(
expect.stringMatching(`Request ${actorRequest.id} has been finalized.`),
);
expect(mockBuildFromEvent).toHaveBeenCalledWith(event, registry);
expect(mockFinalizeRequest.run).toHaveBeenCalled();
});
});
});
});
Loading