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: build onRequestCreated #18

Merged
merged 14 commits into from
Aug 8, 2024
Merged
5 changes: 3 additions & 2 deletions packages/automated-dispute/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"viem": "2.17.11",
"@ebo-agent/blocknumber": "workspace:*"
"@ebo-agent/blocknumber": "workspace:*",
"@ebo-agent/shared": "workspace:*",
"viem": "2.17.11"
}
}
35 changes: 30 additions & 5 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
import { BlockNumberService } from "@ebo-agent/blocknumber";

import { EboRegistry } from "./eboRegistry.js";
import { RequestMismatch } from "./exceptions/requestMismatch.js";
import { ProtocolProvider } from "./protocolProvider.js";
import { EboEvent } from "./types/events.js";
import { Dispute, Response } from "./types/prophet.js";

export class EboActor {
private requestActivity: unknown[];
private registry: EboRegistry;

constructor(
private readonly protocolProvider: ProtocolProvider,
private readonly blockNumberService: BlockNumberService,
private readonly requestId: string,
) {
this.requestActivity = [];
this.registry = new EboRegistry();
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't the registry be injected as a Dependency here?
also, maybe use an interface IEboRegistry, so if we wanted to later change for a real DB instead of in-memory maps we can easily switch to that. (i understand that the registry is sort of Database/Cache)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're absolutely right!

}

public async onRequestCreated(_event: EboEvent<"RequestCreated">): Promise<void> {
// TODO: implement
return;
/**
* Handle RequestCreated event.
*
* @param event RequestCreated event
*/
public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise<void> {
if (event.metadata.requestId != this.requestId)
throw new RequestMismatch(this.requestId, event.metadata.requestId);

this.registry.addRequest(event.metadata.requestId, event.metadata.request);

const { chainId } = event.metadata;
const { currentEpoch, currentEpochTimestamp } =
await this.protocolProvider.getCurrentEpoch();

const epochBlockNumber = await this.blockNumberService.getEpochBlockNumber(
currentEpochTimestamp,
chainId,
);

await this.protocolProvider.proposeResponse(
this.requestId,
currentEpoch,
chainId,
epochBlockNumber,
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should handle the case in which proposeResponse fails, if it is an rpc failure or if a propose was already posted for the requestId

}

public async onResponseProposed(_event: EboEvent<"ResponseDisputed">): Promise<void> {
Expand Down
23 changes: 23 additions & 0 deletions packages/automated-dispute/src/eboRegistry.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

probably not used now but getRequest should exist 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.

We'll probably need that, yep.

Figured we'll add methods in the registry as needed, it'd be easier to know what are we going to use while implementing the EboActor handlers (probably all the get.../add...); there are some edge cases that I want to think about while implemeting the EboActor.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Dispute, Request, Response } from "./types/prophet.js";

export class EboRegistry {
private requests: Map<string, Request>;
private responses: Map<string, Response>;
private dispute: Map<string, Dispute>;

constructor() {
this.requests = new Map();
this.responses = new Map();
this.dispute = new Map();
}

/**
* Add a `Request` by ID.
*
* @param requestId the ID of the `Request`
* @param request the `Request`
*/
public addRequest(requestId: string, request: Request) {
this.requests.set(requestId, request);
}
}
6 changes: 6 additions & 0 deletions packages/automated-dispute/src/exceptions/requestMismatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class RequestMismatch extends Error {
constructor(requestId: string, eventRequestId: string) {
super(`Actor handling request ${requestId} received a request ${eventRequestId} event.`);
this.name = "RequestMismatch";
}
}
32 changes: 26 additions & 6 deletions packages/automated-dispute/src/protocolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Timestamp } from "@ebo-agent/shared";
import {
Address,
createPublicClient,
Expand Down Expand Up @@ -53,17 +55,28 @@ export class ProtocolProvider {
}

/**
* Gets the current epoch and the block number of the current epoch
* @returns The current epoch and the block number of the current epoch
* Gets the current epoch, the block number and its timestamp of the current epoch
*
* @returns The current epoch, its block number and its timestamp
*/
async getCurrentEpoch(): Promise<{ currentEpoch: bigint; currentEpochBlock: bigint }> {
const [currentEpoch, currentEpochBlock] = await Promise.all([
async getCurrentEpoch(): Promise<{
currentEpoch: bigint;
currentEpochBlockNumber: bigint;
currentEpochTimestamp: Timestamp;
}> {
const [currentEpoch, currentEpochBlockNumber] = await Promise.all([
this.epochManagerContract.read.currentEpoch(),
this.epochManagerContract.read.currentEpochBlock(),
]);

const currentEpochBlock = await this.client.getBlock({
blockNumber: currentEpochBlockNumber,
});

return {
currentEpoch,
currentEpochBlock,
currentEpochBlockNumber,
currentEpochTimestamp: currentEpochBlock.timestamp,
};
}

Expand All @@ -79,6 +92,8 @@ export class ProtocolProvider {
logIndex: 1,
metadata: {
requestId: "0x01",
chainId: "eip155:1",
epoch: 1n,
request: {
requester: "0x12345678901234567890123456789012",
requestModule: "0x12345678901234567890123456789012",
Expand Down Expand Up @@ -173,7 +188,12 @@ export class ProtocolProvider {
return;
}

async proposeResponse(_request: Request, _response: Response): Promise<void> {
async proposeResponse(
_requestId: string,
_epoch: bigint,
_chainId: Caip2ChainId,
_blockNumber: bigint,
): Promise<void> {
// TODO: implement actual method
return;
}
Expand Down
16 changes: 10 additions & 6 deletions packages/automated-dispute/src/types/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Log } from "viem";

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

export type EboEventName =
| "NewEpoch"
Expand All @@ -17,14 +18,17 @@ export interface NewEpoch {
epochBlockNumber: bigint;
}

export interface ResponseCreated {
export interface ResponseProposed {
requestId: string;
request: Request;
responseId: string;
response: Response;
}

export interface RequestCreated {
requestId: string;
epoch: bigint;
chainId: Caip2ChainId;
request: Request;
requestId: string;
}

export interface ResponseDisputed {
Expand Down Expand Up @@ -60,8 +64,8 @@ export type EboEventData<E extends EboEventName> = E extends "NewEpoch"
? NewEpoch
: E extends "RequestCreated"
? RequestCreated
: E extends "ResponseCreated"
? ResponseCreated
: E extends "ResponseProposed"
? ResponseProposed
Copy link
Collaborator

Choose a reason for hiding this comment

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

🤣

: E extends "ResponseDisputed"
? ResponseDisputed
: E extends "DisputeStatusChanged"
Expand Down
2 changes: 1 addition & 1 deletion packages/automated-dispute/src/types/prophet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface Request {
export interface Response {
proposer: Address;
requestId: string;
response: Uint8Array;
response: string;
}

export interface Dispute {
Expand Down
120 changes: 118 additions & 2 deletions packages/automated-dispute/tests/eboActor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,123 @@
import { describe } from "vitest";
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Logger } from "@ebo-agent/shared";
import { Address } from "viem";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { EboActor } from "../src/eboActor.js";
import { RequestMismatch } from "../src/exceptions/requestMismatch.js";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
import { RequestMismatch } from "../src/exceptions/requestMismatch.js";
import { RequestMismatch } from "../src/exceptions/requestMismatch.js";
import { InvalidActorState } from "../src/exceptions/invalidActorState.exception.js";

this is related to the other comment

import { ProtocolProvider } from "../src/protocolProvider.js";
import { EboEvent } from "../src/types/events.js";

const logger = Logger.getInstance();

const protocolContracts = {
oracle: "0x123456" as Address,
epochManager: "0x654321" as Address,
};

describe("EboActor", () => {
describe.skip("onRequestCreated");
describe("onRequestCreated", () => {
const requestId: Address = "0x12345";
const indexedChainId: Caip2ChainId = "eip155:137";

const protocolEpoch = {
currentEpoch: 1n,
currentEpochBlockNumber: 1n,
currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)),
};

const requestCreatedEvent: EboEvent<"RequestCreated"> = {
blockNumber: 34n,
logIndex: 1,
name: "RequestCreated",
metadata: {
chainId: "eip155:10",
epoch: protocolEpoch.currentEpoch,
requestId: requestId,
},
};

let protocolProvider: ProtocolProvider;
let blockNumberService: BlockNumberService;

beforeEach(() => {
protocolProvider = new ProtocolProvider(["http://localhost:8538"], protocolContracts);

const chainRpcUrls = new Map<Caip2ChainId, string[]>();
chainRpcUrls.set(indexedChainId, ["http://localhost:8539"]);

blockNumberService = new BlockNumberService(chainRpcUrls, logger);
});

it("proposes a response", async () => {
const indexedEpochBlockNumber = 48n;

vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch);
vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue(
indexedEpochBlockNumber,
);

const proposeResponseMock = vi.spyOn(protocolProvider, "proposeResponse");

proposeResponseMock.mockImplementation(
(
_requestId: string,
_epoch: bigint,
_chainId: Caip2ChainId,
_blockNumbre: bigint,
) => Promise.resolve(),
);

const actor = new EboActor(protocolProvider, blockNumberService, requestId);

await actor.onRequestCreated(requestCreatedEvent);

expect(proposeResponseMock).toHaveBeenCalledWith(
requestCreatedEvent.metadata.requestId,
protocolEpoch.currentEpoch,
requestCreatedEvent.metadata.chainId,
indexedEpochBlockNumber,
);
});

it("throws if the event's request id does not match with actor's", () => {
const noMatchRequestCreatedEvent: EboEvent<"RequestCreated"> = {
blockNumber: 34n,
logIndex: 1,
name: "RequestCreated",
metadata: {
chainId: "eip155:10",
epoch: protocolEpoch.currentEpoch,
requestId: "0x000000" as Address,
},
};

const actor = new EboActor(protocolProvider, blockNumberService, requestId);

expect(actor.onRequestCreated(noMatchRequestCreatedEvent)).rejects.toBeInstanceOf(
RequestMismatch,
);
});

it("throws if current epoch cannot be fetched", () => {
vi.spyOn(protocolProvider, "getCurrentEpoch").mockRejectedValue(new Error());

const actor = new EboActor(protocolProvider, blockNumberService, requestId);

expect(actor.onRequestCreated(requestCreatedEvent)).rejects.toBeDefined();
});

it("throws if the indexed chain block number cannot be fetched", () => {
vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch);
vi.spyOn(blockNumberService, "getEpochBlockNumber").mockRejectedValue(new Error());

const actor = new EboActor(protocolProvider, blockNumberService, requestId);

expect(actor.onRequestCreated(requestCreatedEvent)).rejects.toBeDefined();
});
});

describe.skip("onResponseProposed");
describe.skip("onResponseDisputed");
describe.skip("onFinalizeRequest");
Expand Down
12 changes: 9 additions & 3 deletions packages/automated-dispute/tests/protocolProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,28 @@ describe("ProtocolProvider", () => {
});
describe("getCurrentEpoch", () => {
it("returns currentEpoch and currentEpochBlock successfully", async () => {
const protocolProvider = new ProtocolProvider(mockRpcUrls, mockContractAddress);

const mockEpoch = BigInt(1);
const mockEpochBlock = BigInt(12345);
const mockEpochTimestamp = BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0));

(createPublicClient as Mock).mockReturnValue({
getBlock: vi.fn().mockResolvedValue({ timestamp: mockEpochTimestamp }),
});

const protocolProvider = new ProtocolProvider(mockRpcUrls, mockContractAddress);

(protocolProvider["epochManagerContract"].read.currentEpoch as Mock).mockResolvedValue(
mockEpoch,
);

(
protocolProvider["epochManagerContract"].read.currentEpochBlock as Mock
).mockResolvedValue(mockEpochBlock);

const result = await protocolProvider.getCurrentEpoch();

expect(result.currentEpoch).toBe(mockEpoch);
expect(result.currentEpochBlock).toBe(mockEpochBlock);
expect(result.currentEpochBlockNumber).toBe(mockEpochBlock);
});
it("throws when current epoch request fails", async () => {
const protocolProvider = new ProtocolProvider(mockRpcUrls, mockContractAddress);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Timestamp } from "@ebo-agent/shared";

export class UnsupportedBlockNumber extends Error {
constructor(timestamp: bigint) {
constructor(timestamp: Timestamp) {
super(`Block with null block number at ${timestamp}`);

this.name = "UnsupportedBlockNumber";
Expand Down
4 changes: 3 additions & 1 deletion packages/blocknumber/src/providers/blockNumberProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Timestamp } from "@ebo-agent/shared";

export interface BlockNumberProvider {
/**
* Get the block number corresponding to the beginning of the epoch.
Expand All @@ -9,5 +11,5 @@ export interface BlockNumberProvider {
*
* @returns the corresponding block number of a chain at a specific timestamp
*/
getEpochBlockNumber(timestamp: number): Promise<bigint>;
getEpochBlockNumber(timestamp: Timestamp): Promise<bigint>;
}
Loading