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: handle ProposeDisputed event #21

Merged
merged 6 commits into from
Aug 14, 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
123 changes: 112 additions & 11 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { ILogger } from "@ebo-agent/shared";
import { ContractFunctionRevertedError, Hex } from "viem";
import { ContractFunctionRevertedError } from "viem";

import { InvalidActorState } from "./exceptions/invalidActorState.exception.js";
import { RequestMismatch } from "./exceptions/requestMismatch.js";
Expand All @@ -17,7 +17,7 @@ import { Dispute, Request, Response, ResponseBody } from "./types/prophet.js";
export class EboActor {
constructor(
private readonly actorRequest: {
id: Hex;
id: string;
epoch: bigint;
epochTimestamp: bigint;
},
Expand Down Expand Up @@ -216,19 +216,120 @@ export class EboActor {
}
}

public async onResponseDisputed(_event: EboEvent<"ResponseDisputed">): Promise<void> {
// TODO: implement
return;
/**
* Returns the active actor request.
*
* @throws {InvalidActorState} when the request has not been added to the registry yet.
* @returns the actor `Request`
*/
private getActorRequest() {
const request = this.registry.getRequest(this.actorRequest.id);

if (request === undefined) throw new InvalidActorState();

return request;
}

private async disputeProposal(_dispute: Dispute): Promise<void> {
// TODO: implement
return;
/**
* Handle the `ResponseDisputed` event.
*
* @param event `ResponseDisputed` event.
*/
public async onResponseDisputed(event: EboEvent<"ResponseDisputed">): Promise<void> {
this.shouldHandleRequest(event.metadata.dispute.requestId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is just a double check right ? EboActor is selected by the EboProcessor using the requestId right ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was debating myself with this, between having this check or not.

I guess that we could rely on EboProcessor + EboActorsManager to work as expected, but I like the defensiveness of having this check within EboActor. If something goes wrong in the event → request → actor routing between the other two classes, this check will catch the error before even trying to do anything.

Copy link
Collaborator

Choose a reason for hiding this comment

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

excellent, would be different if it was a heavy computation , but in this case works perfectly :)


const dispute: Dispute = {
id: event.metadata.disputeId,
status: "Active",
prophetData: event.metadata.dispute,
};

this.registry.addDispute(event.metadata.disputeId, dispute);

const request = this.getActorRequest();
const proposedResponse = this.registry.getResponse(event.metadata.responseId);

if (!proposedResponse) throw new InvalidActorState();

const isValidDispute = await this.isValidDispute(proposedResponse);

if (isValidDispute) await this.pledgeFor(request, dispute);
else await this.pledgeAgainst(request, dispute);
}

private async isValidDispute(_dispute: Dispute): Promise<boolean> {
// TODO: implement
return true;
/**
* Check if a dispute is valid, comparing the already submitted and disputed proposal with
* the response this actor would propose.
*
* @param proposedResponse the already submitted response
* @returns true if the hypothetical proposal is different that the submitted one, false otherwise
*/
private async isValidDispute(proposedResponse: Response) {
const actorResponse = await this.buildResponse(
proposedResponse.prophetData.response.chainId,
);

const equalResponses = this.equalResponses(
actorResponse,
proposedResponse.prophetData.response,
);

return !equalResponses;
}

/**
* Pledge in favor of the dispute.
*
* @param request the dispute's `Request`
* @param dispute the `Dispute`
*/
private async pledgeFor(request: Request, dispute: Dispute) {
try {
this.logger.info(`Pledging against dispute ${dispute.id}`);

await this.protocolProvider.pledgeForDispute(request.prophetData, dispute.prophetData);
} catch (err) {
if (err instanceof ContractFunctionRevertedError) {
// TODO: handle each error appropriately
this.logger.warn(`Pledging for dispute ${dispute.id} was reverted. Skipping...`);
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 log the error here also ?

Copy link
Collaborator

@0xkenj1 0xkenj1 Aug 13, 2024

Choose a reason for hiding this comment

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

Maybe what we want is to have more granular errors thrown from the protocolProvider and handle them here. The actor should be the responsible to decide if errors are recoverable or unrecoverable.

wdyt ?

I will write it down on a linear task

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's right, the ideal would be a situation in which we are able to handle each revert explicitly if wanted. I didn't want to go deeper with that right now, as I prefer to have the whole base of the EboActor ready and when everything is set, tackle these details.

I'll add some TODOs in those error handling blocks too, with this piece of info, thanks for the Linear task 🫶

Copy link
Collaborator

Choose a reason for hiding this comment

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

perfect, just wanted to be sure we were on the same page with that :)

} else {
// TODO: handle each error appropriately
this.logger.error(
`Actor handling request ${this.actorRequest.id} is not able to continue.`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here

);

throw err;
}
}
}

/**
* Pledge against the dispute.
*
* @param request the dispute's `Request`
* @param dispute the `Dispute`
*/
private async pledgeAgainst(request: Request, dispute: Dispute) {
try {
this.logger.info(`Pledging for dispute ${dispute.id}`);

await this.protocolProvider.pledgeAgainstDispute(
request.prophetData,
dispute.prophetData,
);
} catch (err) {
if (err instanceof ContractFunctionRevertedError) {
// TODO: handle each error appropriately
this.logger.warn(`Pledging on dispute ${dispute.id} was reverted. Skipping...`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

ditto

} else {
// TODO: handle each error appropriately
this.logger.error(
`Actor handling request ${this.actorRequest.id} is not able to continue.`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

dittooo

);

throw err;
}
}
}

public async onFinalizeRequest(_event: EboEvent<"RequestFinalizable">): Promise<void> {
Expand Down
12 changes: 11 additions & 1 deletion packages/automated-dispute/src/eboMemoryRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class EboMemoryRegistry implements EboRegistry {
constructor(
private requests: Map<string, Request> = new Map(),
private responses: Map<string, Response> = new Map(),
private dispute: Map<string, Dispute> = new Map(),
private disputes: Map<string, Dispute> = new Map(),
) {}

/** @inheritdoc */
Expand All @@ -27,4 +27,14 @@ export class EboMemoryRegistry implements EboRegistry {
public getResponses() {
return this.responses;
}

/** @inheritdoc */
public getResponse(responseId: string): Response | undefined {
return this.responses.get(responseId);
}

/** @inheritdoc */
public addDispute(disputeId: string, dispute: Dispute): void {
this.disputes.set(disputeId, dispute);
}
Comment on lines +31 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

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

oop encapsulation 🤓

}
20 changes: 18 additions & 2 deletions packages/automated-dispute/src/interfaces/eboRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Request, Response } from "../types/prophet.js";
import { Dispute, Request, Response } from "../types/prophet.js";

/** Registry that stores Prophet entities (ie. requests, responses and disputes) */
export interface EboRegistry {
Expand All @@ -22,7 +22,7 @@ export interface EboRegistry {
* Add a `Response` by ID.
*
* @param responseId the ID of the `Response` to use as index
* @param response the `Resopnse`
* @param response the `Response`
*/
addResponse(responseId: string, response: Response): void;

Expand All @@ -32,4 +32,20 @@ export interface EboRegistry {
* @returns responses map
*/
getResponses(): Map<string, Response>;

/**
* Get a `Response` by ID.
*
* @param responseId response ID
* @returns the `Response` if already added into registry, `undefined` otherwise
*/
getResponse(responseId: string): Response | undefined;

/**
* 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;
}
10 changes: 8 additions & 2 deletions packages/automated-dispute/src/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,18 @@ export class ProtocolProvider {
return;
}

async pledgeForDispute(_request: Request, _dispute: Dispute): Promise<void> {
async pledgeForDispute(
_request: Request["prophetData"],
_dispute: Dispute["prophetData"],
): Promise<void> {
// TODO: implement actual method
return;
}

async pledgeAgaintsDispute(_request: Request, _dispute: Dispute): Promise<void> {
async pledgeAgainstDispute(
_request: Request["prophetData"],
_dispute: Dispute["prophetData"],
): Promise<void> {
// TODO: implement actual method
return;
}
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 @@ -32,8 +32,8 @@ export interface RequestCreated {
}

export interface ResponseDisputed {
requestId: string;
responseId: string;
disputeId: string;
dispute: Dispute["prophetData"];
}

Expand Down
4 changes: 3 additions & 1 deletion packages/automated-dispute/src/types/prophet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ export interface Response {

export type ResponseBody = Response["prophetData"]["response"];

export type DisputeStatus = "None" | "Active" | "Escalated" | "Won" | "Lost" | "NoResolution";

export interface Dispute {
id: string;
status: string;
status: DisputeStatus;

prophetData: {
disputer: Address;
Expand Down
79 changes: 79 additions & 0 deletions packages/automated-dispute/tests/eboActor/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types";
import { ILogger } from "@ebo-agent/shared";

import { EboActor } from "../../../src/eboActor";
import { EboMemoryRegistry } from "../../../src/eboMemoryRegistry";
import { ProtocolProvider } from "../../../src/protocolProvider";
import { Request, Response } from "../../../src/types/prophet";
import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS } from "../fixtures";

/**
* Builds a base `EboActor` scaffolded with all its dependencies.
*
* @param request a `Request` to populate the `EboActor` with
* @param logger logger
* @returns
*/
function buildEboActor(request: Request, logger: ILogger) {
const { id, chainId, epoch, epochTimestamp } = request;

const protocolProviderRpcUrls = ["http://localhost:8538"];
const protocolProvider = new ProtocolProvider(
protocolProviderRpcUrls,
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
);

const blockNumberRpcUrls = new Map<Caip2ChainId, string[]>([
[chainId, ["http://localhost:8539"]],
]);
const blockNumberService = new BlockNumberService(blockNumberRpcUrls, logger);

const registry = new EboMemoryRegistry();

const actor = new EboActor(
{ id, epoch, epochTimestamp },
protocolProvider,
blockNumberService,
registry,
logger,
);

return {
actor,
protocolProvider,
blockNumberService,
registry,
logger,
};
}

/**
* Helper function to build a response based on a request.
*
* @param request the `Request` to base the response on
* @param attributes custom attributes to set into the response to build
* @returns a `Response`
*/
function buildResponse(request: Request, attributes: Partial<Response> = {}): Response {
const baseResponse: Response = {
id: "0x01",
wasDisputed: false,
prophetData: {
proposer: "0x01",
requestId: request.id,
response: {
chainId: request.chainId,
block: 1n,
epoch: request.epoch,
},
},
};

return {
...baseResponse,
...attributes,
};
}

export default { buildEboActor, buildResponse };
Loading