Skip to content

Commit

Permalink
feat: implement EboActor.onDisputeStatusChanged handler
Browse files Browse the repository at this point in the history
  • Loading branch information
0xyaco committed Aug 16, 2024
1 parent fc1901e commit e5d52fc
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 32 deletions.
141 changes: 115 additions & 26 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { ILogger } from "@ebo-agent/shared";
import { ContractFunctionRevertedError } from "viem";

import { InvalidActorState } from "./exceptions/invalidActorState.exception.js";
import { InvalidDisputeStatus } from "./exceptions/invalidDisputeStatus.exception.js";
import { RequestMismatch } from "./exceptions/requestMismatch.js";
import { ResponseAlreadyProposed } from "./exceptions/responseAlreadyProposed.js";
import { EboRegistry } from "./interfaces/eboRegistry.js";
import { ProtocolProvider } from "./protocolProvider.js";
import { EboEvent } from "./types/events.js";
Expand Down Expand Up @@ -81,30 +83,12 @@ export class EboActor {
}

const { chainId } = event.metadata;
const response = await this.buildResponse(chainId);

if (this.alreadyProposed(response.epoch, response.chainId, response.block)) return;

try {
await this.protocolProvider.proposeResponse(
this.actorRequest.id,
response.epoch,
response.chainId,
response.block,
);
await this.proposeResponse(chainId);
} catch (err) {
if (err instanceof ContractFunctionRevertedError) {
this.logger.warn(
`Block ${response.block} for epoch ${response.epoch} and ` +
`chain ${response.chainId} was not proposed. Skipping proposal...`,
);
} else {
this.logger.error(
`Actor handling request ${this.actorRequest.id} is not able to continue.`,
);

throw err;
}
if (err instanceof ResponseAlreadyProposed) this.logger.info(err.message);
else throw err;
}
}

Expand Down Expand Up @@ -168,6 +152,41 @@ export class EboActor {
};
}

/**
* Propose an actor request's response for a particular chain.
*
* @param chainId the CAIP-2 compliant chain ID
*/
private async proposeResponse(chainId: Caip2ChainId): Promise<void> {
const response = await this.buildResponse(chainId);

if (this.alreadyProposed(response.epoch, response.chainId, response.block)) {
throw new ResponseAlreadyProposed(response);
}

try {
await this.protocolProvider.proposeResponse(
this.actorRequest.id,
response.epoch,
response.chainId,
response.block,
);
} catch (err) {
if (err instanceof ContractFunctionRevertedError) {
this.logger.warn(
`Block ${response.block} for epoch ${response.epoch} and ` +
`chain ${response.chainId} was not proposed. Skipping proposal...`,
);
} else {
this.logger.error(
`Actor handling request ${this.actorRequest.id} is not able to continue.`,
);

throw err;
}
}
}

/**
* Handle `ResponseProposed` event.
*
Expand Down Expand Up @@ -347,6 +366,81 @@ export class EboActor {
}
}

/**
* Handle the `DisputeStatusChanged` event.
*
* @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) {
case "None":
this.logger.warn(
`Agent does not know how to handle status changing to 'None' on dispute ${disputeId}.`,
);

break;

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

break;

case "NoResolution":
await this.onDisputeWithNoResolution(disputeId, request);

break;

default:
throw new InvalidDisputeStatus(disputeId, disputeStatus);
}
}

private async onDisputeEscalated(disputeId: string, request: Request) {
// TODO: notify

await this.onTerminate(request);
}

private async onDisputeWithNoResolution(disputeId: string, request: Request) {
try {
await this.proposeResponse(request.chainId);
} catch (err) {
if (err instanceof ResponseAlreadyProposed) {
// This is an extremely weird case. If no other agent proposes
// a different response, the request will probably be finalized
// with no valid response.
//
// This actor will just wait until the proposal window ends.
this.logger.warn(err.message);

// TODO: notify
} else {
this.logger.error(
`Could not handle dispute ${disputeId} changing to NoResolution status.`,
);

throw err;
}
}
}

/**
* Handle the `ResponseFinalized` event.
*
Expand All @@ -359,9 +453,4 @@ export class EboActor {

await this.onTerminate(request);
}

public async onDisputeStatusChanged(_event: EboEvent<"DisputeStatusChanged">): Promise<void> {
// TODO: implement
return;
}
}
20 changes: 19 additions & 1 deletion packages/automated-dispute/src/eboMemoryRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DisputeNotFound } from "./exceptions/eboRegistry/disputeNotFound.js";
import { EboRegistry } from "./interfaces/eboRegistry.js";
import { Dispute, Request, Response } from "./types/prophet.js";
import { Dispute, DisputeStatus, Request, Response } from "./types/prophet.js";

export class EboMemoryRegistry implements EboRegistry {
constructor(
Expand Down Expand Up @@ -37,4 +38,21 @@ export class EboMemoryRegistry implements EboRegistry {
public addDispute(disputeId: string, dispute: Dispute): void {
this.disputes.set(disputeId, dispute);
}

/** @inheritdoc */
public getDispute(disputeId: string): Dispute | undefined {
return this.disputes.get(disputeId);
}

/** @inheritdoc */
public updateDisputeStatus(disputeId: string, status: DisputeStatus): void {
const dispute = this.getDispute(disputeId);

if (dispute === undefined) throw new DisputeNotFound(disputeId);

this.disputes.set(disputeId, {
...dispute,
status: status,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class DisputeNotFound extends Error {
constructor(disputeId: string) {
super(`Dispute ${disputeId} was not found.`);

this.name = "DisputeNotFound";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./disputeNotFound.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class InvalidDisputeStatus extends Error {
constructor(disputeId: string, status: string) {
super(`Invalid status ${status} for dispute ${disputeId}`);

this.name = "InvalidDisputeStatus";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ResponseBody } from "../types/prophet.js";

export class ResponseAlreadyProposed extends Error {
constructor(response: ResponseBody) {
super(
`Block ${response.block} was already proposed for epoch ${response.epoch} on chain ${response.chainId}`,
);

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

/** Registry that stores Prophet entities (ie. requests, responses and disputes) */
export interface EboRegistry {
Expand Down Expand Up @@ -48,4 +48,20 @@ export interface EboRegistry {
* @param dispute the `Dispute`
*/
addDispute(disputeId: string, dispute: Dispute): void;

/**
* Get a `Dispute` by ID.
*
* @param disputeId dispute ID
* @returns the `Dispute` if already added into registry, `undefined` otherwise
*/
getDispute(disputeId: string): Dispute | undefined;

/**
* Update the dispute status based on its ID.
*
* @param disputeId the ID of the `Dispute`
* @param status the `Dispute`
*/
updateDisputeStatus(disputeId: string, status: DisputeStatus): void;
}
5 changes: 3 additions & 2 deletions packages/automated-dispute/src/types/events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Log } from "viem";

import { Dispute, Request, Response } from "./prophet.js";
import { Dispute, DisputeStatus, Request, Response } from "./prophet.js";

export type EboEventName =
| "NewEpoch"
Expand Down Expand Up @@ -38,7 +38,8 @@ export interface ResponseDisputed {

export interface DisputeStatusChanged {
disputeId: string;
status: string;
dispute: Dispute["prophetData"];
status: DisputeStatus;
blockNumber: bigint;
}

Expand Down
26 changes: 24 additions & 2 deletions packages/automated-dispute/tests/eboActor/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { vi } from "vitest";
import { EboActor } from "../../../src/eboActor";
import { EboMemoryRegistry } from "../../../src/eboMemoryRegistry";
import { ProtocolProvider } from "../../../src/protocolProvider";
import { Request, Response } from "../../../src/types/prophet";
import { Dispute, Request, Response } from "../../../src/types/prophet";
import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS } from "../fixtures";

/**
Expand Down Expand Up @@ -81,4 +81,26 @@ function buildResponse(request: Request, attributes: Partial<Response> = {}): Re
};
}

export default { buildEboActor, buildResponse };
function buildDispute(
request: Request,
response: Response,
attributes: Partial<Dispute> = {},
): Dispute {
const baseDispute: Dispute = {
id: "0x01",
status: "Active",
prophetData: {
disputer: "0x01",
proposer: response.prophetData.proposer,
requestId: request.id,
responseId: response.id,
},
};

return {
...baseDispute,
...attributes,
};
}

export default { buildEboActor, buildResponse, buildDispute };
Loading

0 comments on commit e5d52fc

Please sign in to comment.