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: on last event #34

Merged
merged 8 commits into from
Sep 5, 2024
77 changes: 52 additions & 25 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class EboActor {
* @param event EBO event
*/
public enqueue(event: EboEvent<EboEventName>): 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);
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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<EboEventName>) {
// TODO
return;
private async onLastEvent(event: EboEvent<EboEventName>) {
switch (event.name) {
case "RequestCreated":
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we not need to handle NewEpoch?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cool that you've realized that there was this NewEpoch event! Right now we are not going to use it, as the new epoch will be handled by the EboProcessor so let me delete it from the events.

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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

if EboEventName can be something other than the few specified in type EboEventName I'm wondering if we'd need to have a 'string' option in the type and log the event without trying to access event.name 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A handled event must be one of the listed events.

This is more of an explicitness + maintainability thingy, as we want to force anyone who wants to support a new event to include the new event handler here. Without the default: throw case this would be run silently while there's probably something wrong upstream with the event fetching logic (ie including unsupported events).

}
}

/**
Expand Down Expand Up @@ -404,15 +436,7 @@ export class EboActor {
*
* @param event `RequestCreated` event
*/
public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise<void> {
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<void> {
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 @@ -539,7 +563,7 @@ export class EboActor {
* @param event a `ResponseProposed` event
* @returns void
*/
public async onResponseProposed(event: EboEvent<"ResponseProposed">): Promise<void> {
private async onResponseProposed(event: EboEvent<"ResponseProposed">): Promise<void> {
const eventResponse = event.metadata.response;
const actorResponse = await this.buildResponse(eventResponse.response.chainId);

Expand Down Expand Up @@ -578,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();
}

/**
Expand All @@ -600,7 +624,7 @@ export class EboActor {
*
* @param event `ResponseDisputed` event.
*/
public async onResponseDisputed(event: EboEvent<"ResponseDisputed">): Promise<void> {
private async onResponseDisputed(event: EboEvent<"ResponseDisputed">): Promise<void> {
const dispute = this.registry.getDispute(event.metadata.disputeId);

if (!dispute)
Expand Down Expand Up @@ -699,7 +723,7 @@ export class EboActor {
*
* @param event `DisputeStatusChanged` event
*/
public async onDisputeStatusChanged(event: EboEvent<"DisputeStatusChanged">): Promise<void> {
private async onDisputeStatusChanged(event: EboEvent<"DisputeStatusChanged">): Promise<void> {
const request = this.getActorRequest();
const disputeId = event.metadata.disputeId;
const disputeStatus = event.metadata.status;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -763,9 +792,7 @@ export class EboActor {
*
* @param event `ResponseFinalized` event
*/
public async onRequestFinalized(event: EboEvent<"RequestFinalized">): Promise<void> {
this.shouldHandleRequest(event.metadata.requestId);

private async onRequestFinalized(_event: EboEvent<"RequestFinalized">): Promise<void> {
const request = this.getActorRequest();

this.logger.info(`Request ${request.id} has been finalized.`);
Expand Down
34 changes: 13 additions & 21 deletions packages/automated-dispute/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@ import { Address, Log } from "viem";
import { Dispute, DisputeStatus, Request, RequestId, Response } from "./prophet.js";

export type EboEventName =
| "NewEpoch"
| "RequestCreated"
| "ResponseProposed"
| "ResponseDisputed"
| "DisputeStatusChanged"
| "DisputeEscalated"
| "RequestFinalized";

export interface NewEpoch {
epoch: bigint;
epochBlockNumber: bigint;
}

export interface ResponseProposed {
requestId: string;
responseId: string;
Expand Down Expand Up @@ -56,21 +50,19 @@ export interface RequestFinalized {
blockNumber: bigint;
}

export type EboEventData<E extends EboEventName> = 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 EboEventName> = 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<T extends EboEventName> = {
name: T;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ const logger: ILogger = {
debug: vi.fn(),
};

describe.skip("onDisputeStatusChanged", () => {
describe("onDisputeStatusChanged", () => {
const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA;
const response = mocks.buildResponse(actorRequest);

it("updates the state of the dispute", async () => {
const dispute = mocks.buildDispute(actorRequest, response, { status: "None" });
const event: EboEvent<"DisputeStatusChanged"> = {
name: "DisputeStatusChanged",
requestId: actorRequest.id,
blockNumber: 1n,
logIndex: 1,
metadata: {
Expand All @@ -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: {
Expand All @@ -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,
Expand All @@ -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");
});
Loading