Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into feat/base-ebo-processor
Browse files Browse the repository at this point in the history
  • Loading branch information
0xyaco committed Aug 21, 2024
2 parents 746af83 + 770b554 commit fabce18
Show file tree
Hide file tree
Showing 13 changed files with 325 additions and 53 deletions.
152 changes: 119 additions & 33 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { ILogger } from "@ebo-agent/shared";
import { ContractFunctionRevertedError } from "viem";

import { InvalidActorState } from "./exceptions/invalidActorState.exception.js";
import { RequestMismatch } from "./exceptions/requestMismatch.js";
import {
InvalidActorState,
InvalidDisputeStatus,
RequestMismatch,
ResponseAlreadyProposed,
} from "./exceptions/index.js";
import { EboRegistry } from "./interfaces/eboRegistry.js";
import { ProtocolProvider } from "./protocolProvider.js";
import { EboEvent } from "./types/events.js";
Expand Down Expand Up @@ -90,30 +94,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 @@ -177,6 +163,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 @@ -357,25 +378,90 @@ export class EboActor {
}

/**
* Handle the `ResponseFinalized` event.
* Handle the `DisputeStatusChanged` event.
*
* @param event `ResponseFinalized` event
* @param event `DisputeStatusChanged` event
*/
public async onRequestFinalized(event: EboEvent<"RequestFinalized">): Promise<void> {
this.shouldHandleRequest(event.metadata.requestId);
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);
}

public async onDisputeStatusChanged(_event: EboEvent<"DisputeStatusChanged">): Promise<void> {
// TODO: implement
return;
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;
}
}
}

public async onDisputeEscalated(_event: EboEvent<"DisputeEscalated">): Promise<void> {
// TODO: implement
return;
/**
* Handle the `ResponseFinalized` event.
*
* @param event `ResponseFinalized` event
*/
public async onRequestFinalized(event: EboEvent<"RequestFinalized">): Promise<void> {
this.shouldHandleRequest(event.metadata.requestId);

const request = this.getActorRequest();

await this.onTerminate(request);
}
}
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";
6 changes: 4 additions & 2 deletions packages/automated-dispute/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from "./rpcUrlsEmpty.exception.js";
export * from "./invalidActorState.exception.js";
export * from "./invalidDisputeStatus.exception.js";
export * from "./requestAlreadyHandled.exception.js";
export * from "./requestMismatch.js";
export * from "./requestMismatch.exception.js";
export * from "./responseAlreadyProposed.exception.js";
export * from "./rpcUrlsEmpty.exception.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;
}
19 changes: 6 additions & 13 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,13 +38,8 @@ export interface ResponseDisputed {

export interface DisputeStatusChanged {
disputeId: string;
status: string;
blockNumber: bigint;
}

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

Expand All @@ -65,11 +60,9 @@ export type EboEventData<E extends EboEventName> = E extends "NewEpoch"
? ResponseDisputed
: E extends "DisputeStatusChanged"
? DisputeStatusChanged
: E extends "DisputeEscalated"
? DisputeEscalated
: E extends "RequestFinalized"
? RequestFinalized
: never;
: E extends "RequestFinalized"
? RequestFinalized
: never;

export type EboEvent<T extends EboEventName> = {
name: T;
Expand Down
Loading

0 comments on commit fabce18

Please sign in to comment.