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: implement get events #50

Merged
merged 14 commits into from
Oct 14, 2024
1 change: 1 addition & 0 deletions packages/automated-dispute/src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from "./transactionExecutionError.exception.js";
export * from "./invalidAccountOnClient.exception.js";
export * from "./unsupportedEvent.exception.js";
export * from "./decodeLogDataFailure.js";
export * from "./invalidBlockRangeError.exception.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class InvalidBlockRangeError extends Error {
constructor(fromBlock: bigint, toBlock: bigint) {
super(
`Invalid block range: fromBlock (${fromBlock}) must be less than or equal to toBlock (${toBlock})`,
);
this.name = "InvalidBlockRangeError";
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js";
import { Address } from "viem";
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the blocknumber package exposes Caip2ChainId already, it shouldn't be necessary to use /src/index.js

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed the /src/index.js that I added everywhere--not sure why my IDE was flagging it


import type {
Expand Down
142 changes: 45 additions & 97 deletions packages/automated-dispute/src/providers/protocolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Caip2ChainId, Caip2Utils, InvalidChainId } from "@ebo-agent/blocknumber";
import { Caip2ChainId, Caip2Utils, InvalidChainId } from "@ebo-agent/blocknumber/src/index.js";
import {
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

AbiEvent,
Address,
Expand Down Expand Up @@ -41,6 +41,7 @@ import {
import {
DecodeLogDataFailure,
InvalidAccountOnClient,
InvalidBlockRangeError,
RpcUrlsEmpty,
TransactionExecutionError,
UnsupportedEvent,
Expand Down Expand Up @@ -284,7 +285,7 @@ export class ProtocolProvider implements IProtocolProvider {
data: log.data,
topics: log.topics,
eventName,
strict: false,
strict: true,
});

return decodedLog.args as DecodedLogArgsMap[TEventName];
Expand All @@ -301,87 +302,39 @@ export class ProtocolProvider implements IProtocolProvider {
* @returns An EboEvent object.
*/
private parseOracleEvent(eventName: EboEventName, log: Log) {
if (
![
"ResponseProposed",
"ResponseDisputed",
"DisputeStatusChanged",
"DisputeEscalated",
"RequestFinalized",
].includes(eventName)
) {
throw new UnsupportedEvent(`Unsupported event name: ${eventName}`);
}

const baseEvent = {
name: eventName,
blockNumber: log.blockNumber ?? BigInt(0),
logIndex: log.logIndex ?? 0,
blockNumber: log.blockNumber,
logIndex: log.logIndex,
rawLog: log,
requestId: log.topics[1] as RequestId,
};

const decodedLog = this.decodeLogData(eventName, log);

Copy link
Collaborator

Choose a reason for hiding this comment

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

After setting strict: true while getting and decoding events, is it possible to do something like this to avoid using the switch statements?

Suggested change
const decodedLog = this.decodeLogData(eventName, log);
const decodedLog = this.decodeLogData(eventName, log);
return {
...baseEvent,
metadata: decodedLog
}

I feel like there has to be a way to generalize this somehow, I might be wrong though.

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 haven't found a way to get this to work yet, not sure if the ABI types are coming through properly

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could it be that the event's properties are prefixed with an underscore and we need to replicate those in our codebase?

Let's grab this event for example:

  event ResponseProposed(bytes32 indexed _requestId, bytes32 indexed _responseId, Response _response);

The corresponding type inside DecodedLogArgsMap is:

    ResponseProposed: {
        requestId: RequestId;
        responseId: string;
        response: string;
        blockNumber: bigint; // This property was removed yesterday
    };

Maybe refactoring those attributes by prefixing them with an underscore would comply with the ABI types? Feel free to do some quick validation on this.

let requestId: RequestId;
switch (eventName) {
case "ResponseProposed":
const responseProposedArgs = decodedLog as DecodedLogArgsMap["ResponseProposed"];
return {
...baseEvent,
metadata: {
requestId: responseProposedArgs.requestId,
responseId: responseProposedArgs.responseId,
response: responseProposedArgs.response,
blockNumber: responseProposedArgs.blockNumber,
},
};
requestId = decodedLog.requestId;
break;
case "ResponseDisputed":
const responseDisputedArgs = decodedLog as DecodedLogArgsMap["ResponseDisputed"];
return {
...baseEvent,
metadata: {
responseId: responseDisputedArgs.responseId,
disputeId: responseDisputedArgs.disputeId,
dispute: responseDisputedArgs.dispute,
blockNumber: responseDisputedArgs.blockNumber,
},
};
requestId = decodedLog.requestId;
break;
case "DisputeStatusChanged":
const disputeStatusChangedArgs =
decodedLog as DecodedLogArgsMap["DisputeStatusChanged"];
return {
...baseEvent,
metadata: {
disputeId: disputeStatusChangedArgs.disputeId,
dispute: disputeStatusChangedArgs.dispute,
status: disputeStatusChangedArgs.status,
blockNumber: disputeStatusChangedArgs.blockNumber,
},
};
case "DisputeEscalated":
const disputeEscalatedArgs = decodedLog as DecodedLogArgsMap["DisputeEscalated"];
return {
...baseEvent,
metadata: {
caller: disputeEscalatedArgs.caller,
disputeId: disputeEscalatedArgs.disputeId,
blockNumber: disputeEscalatedArgs.blockNumber,
},
};
requestId = decodedLog.requestId;
break;
case "RequestFinalized":
const requestFinalizedArgs = decodedLog as DecodedLogArgsMap["RequestFinalized"];
return {
...baseEvent,
metadata: {
requestId: requestFinalizedArgs.requestId,
responseId: requestFinalizedArgs.responseId,
caller: requestFinalizedArgs.caller,
blockNumber: requestFinalizedArgs.blockNumber,
},
};
requestId = decodedLog.requestId;
break;
default:
throw new UnsupportedEvent(`Unsupported event name: ${eventName}`);
throw new Error(`Unsupported event name: ${eventName}`);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Was removing UnsupportedEvent intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, I think this happened when i was merging things. Added it back in


return {
...baseEvent,
requestId,
metadata: decodedLog,
};
}

/**
Expand All @@ -399,31 +352,30 @@ export class ProtocolProvider implements IProtocolProvider {
"DisputeEscalated",
"RequestFinalized",
];
const eventPromises = eventNames.map((eventName) =>
this.readClient.getLogs({
address: this.oracleContract.address,
event: oracleAbi.find(
(e) => e.name === eventName && e.type === "event",
) as AbiEvent,
fromBlock,
toBlock,
}),
);

const allLogs = await Promise.all(eventPromises);
return allLogs.flatMap((logs: Log[], index: number) =>
logs.map((log) => this.parseOracleEvent(eventNames[index] as EboEventName, log)),
);
}
const logs = await this.readClient.getLogs({
address: this.oracleContract.address,
events: eventNames.map(
(eventName) =>
oracleAbi.find((e) => e.name === eventName && e.type === "event") as AbiEvent,
),
fromBlock,
toBlock,
strict: true,
});

return logs.map((log) => {
const eventName = log.eventName as EboEventName;
return this.parseOracleEvent(eventName, log);
});
}
/**
* Fetches events from the EBORequestCreator contract.
*
* @param fromBlock - The starting block number to fetch events from.
* @param toBlock - The ending block number to fetch events to.
* @returns A promise that resolves to an array of EboEvents.
*/

private async getEBORequestCreatorEvents(fromBlock: bigint, toBlock: bigint) {
const logs = await this.readClient.getLogs({
address: this.eboRequestCreatorContract.address,
Expand All @@ -441,15 +393,11 @@ export class ProtocolProvider implements IProtocolProvider {
) as DecodedLogArgsMap["RequestCreated"];
return {
name: "RequestCreated" as const,
blockNumber: log.blockNumber ?? BigInt(0),
logIndex: log.logIndex ?? 0,
blockNumber: log.blockNumber,
logIndex: log.logIndex,
rawLog: log,
requestId: decodedLog.requestId,
metadata: {
epoch: decodedLog.epoch,
chainId: decodedLog.chainId,
requestId: decodedLog.requestId,
},
requestId: decodedLog.requestId ?? "",
metadata: decodedLog,
};
});
}
Expand All @@ -464,7 +412,7 @@ export class ProtocolProvider implements IProtocolProvider {
*/
async getEvents(fromBlock: bigint, toBlock: bigint) {
if (fromBlock > toBlock) {
throw new Error("Invalid block range: fromBlock must be less than or equal to toBlock");
throw new InvalidBlockRangeError(fromBlock, toBlock);
}

const [requestCreatorEvents, oracleEvents] = await Promise.all([
Expand All @@ -487,11 +435,11 @@ export class ProtocolProvider implements IProtocolProvider {
return streams
.reduce((acc, curr) => acc.concat(curr), [])
.sort((a, b) => {
if (a.blockNumber < b.blockNumber) return 1;
if (a.blockNumber > b.blockNumber) return -1;
if (a.blockNumber > b.blockNumber) return 1;
if (a.blockNumber < b.blockNumber) return -1;

if (a.logIndex < b.logIndex) return 1;
if (a.logIndex > b.logIndex) return -1;
if (a.logIndex > b.logIndex) return 1;
if (a.logIndex < b.logIndex) return -1;

return 0;
});
Expand Down
4 changes: 3 additions & 1 deletion packages/automated-dispute/src/services/eboActor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BlockNumberService, Caip2ChainId } from "@ebo-agent/blocknumber";
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js";
import { Address, ILogger } from "@ebo-agent/shared";
import { Mutex } from "async-mutex";
import { Heap } from "heap-js";
Expand Down Expand Up @@ -610,6 +611,7 @@ export class EboActor {
const disputer = this.protocolProvider.getAccountAddress();
const dispute: Dispute["prophetData"] = {
disputer: disputer,
// TODO: populate proposer if does not exist on eventResponse
proposer: eventResponse.proposer,
responseId: Address.normalize(event.metadata.responseId) as ResponseId,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we are ok on this case, as the ResponseProposed event [1] is:

event ResponseProposed(bytes32 indexed _requestId, bytes32 indexed _responseId, Response _response);

And the Response struct [2] is:

struct Response {
  address proposer;
  bytes32 requestId;
  bytes response;
}

Thus, the proposer can be accessed through event.metadata.proposer while the response body is accessed through ProtocolProvider.decodeResponse(event.metadata.response.response).

Do you know any other case that might be broken?

[1] https://github.com/defi-wonderland/prophet-core/blob/4d28a0ec7e8cb082f4932c52a0845b122a0ca71a/solidity/interfaces/IOracle.sol#L27
[2] https://github.com/defi-wonderland/prophet-core/blob/4d28a0ec7e8cb082f4932c52a0845b122a0ca71a/solidity/interfaces/IOracle.sol#L227-L231

requestId: request.id,
Expand Down
3 changes: 2 additions & 1 deletion packages/automated-dispute/src/services/eboProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isNativeError } from "util/types";
import { BlockNumberService, Caip2ChainId } from "@ebo-agent/blocknumber";
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js";
import { Address, EBO_SUPPORTED_CHAIN_IDS, ILogger } from "@ebo-agent/shared";

import { PendingModulesApproval, ProcessorAlreadyStarted } from "../exceptions/index.js";
Expand Down
2 changes: 1 addition & 1 deletion packages/automated-dispute/src/types/actorRequest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js";

import { RequestId } from "./prophet.js";

Expand Down
37 changes: 13 additions & 24 deletions packages/automated-dispute/src/types/events.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber";
import { Address, Hex, Log } from "viem";
import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js";
import { Address, Log } from "viem";

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

export type EboEventName =
| "RequestCreated"
Expand All @@ -19,34 +11,31 @@ export type EboEventName =
| "DisputeEscalated"
| "RequestFinalized";

export interface ResponseProposed {
requestId: Hex;
responseId: Hex;
response: Response["prophetData"];
// TODO: block number?
}

export interface RequestCreated {
requestId: RequestId;
epoch: bigint;
chainId: Caip2ChainId;
//TODO: remove request?
request: Request["prophetData"];
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We might end up using Oracle's RequestCreated event instead of EBORequestCreator's RequestCreated event. I'll get back to you on this one

Copy link
Collaborator

Choose a reason for hiding this comment

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

So, we'll be using both events, but only reacting to EBORequestCreator.RequestCreated so this can stay as it is.

This will probably cause some type failure during request creation internally in the AddRequest command, as it won't be able to populate the prophetData field; for that, let's add a TODO and fill it in with DEFAULT_MOCKED_REQUEST_CREATED_DATA["prophetData"] mocked data.

We got GRT-205 already created to tackle this in another PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

are the ABIs up to date now since your PR was merged?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oracle's ABI should be up to date, yep. Viem types should be working as expected for all related functionality.


export interface ResponseProposed {
requestId: RequestId;
responseId: ResponseId;
response: string;
blockNumber: bigint;
}

export interface ResponseDisputed {
responseId: ResponseId;
disputeId: DisputeId;
dispute: Dispute["prophetData"];
//TODO: block number?
dispute: string;
blockNumber: bigint;
}

export interface DisputeStatusChanged {
disputeId: DisputeId;
dispute: Dispute["prophetData"];
dispute: string;
status: DisputeStatus;
blockNumber: bigint;
//TODO: should be disputeId, request, response,dispute?
}

export interface DisputeEscalated {
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
@@ -1,4 +1,4 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js";
import { Branded, NormalizedAddress } from "@ebo-agent/shared";
import { Address, Hex } from "viem";

Expand Down
24 changes: 6 additions & 18 deletions packages/automated-dispute/tests/services/protocolProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js";
import {
ContractFunctionRevertedError,
createPublicClient,
Expand Down Expand Up @@ -994,8 +994,8 @@ describe("ProtocolProvider", () => {
];

const mockOracleEvents = [
{ name: "ResponseProposed", blockNumber: 2n, logIndex: 0 },
{ name: "ResponseDisputed", blockNumber: 2n, logIndex: 1 },
{ name: "ResponseDisputed", blockNumber: 2n, logIndex: 0 },
{ name: "ResponseProposed", blockNumber: 2n, logIndex: 1 },
];

vi.spyOn(protocolProvider as any, "getEBORequestCreatorEvents").mockResolvedValue(
Expand All @@ -1008,23 +1008,11 @@ describe("ProtocolProvider", () => {
const result = await protocolProvider.getEvents(0n, 100n);

expect(result).toEqual([
{ name: "RequestCreated", blockNumber: 3n, logIndex: 0 },
{ name: "ResponseDisputed", blockNumber: 2n, logIndex: 1 },
{ name: "ResponseProposed", blockNumber: 2n, logIndex: 0 },
{ name: "RequestCreated", blockNumber: 1n, logIndex: 0 },
{ name: "ResponseDisputed", blockNumber: 2n, logIndex: 0 },
{ name: "ResponseProposed", blockNumber: 2n, logIndex: 1 },
{ name: "RequestCreated", blockNumber: 3n, logIndex: 0 },
]);
});

it("throws an error if fromBlock is greater than toBlock", async () => {
const protocolProvider = new ProtocolProvider(
mockRpcConfig,
mockContractAddress,
mockedPrivateKey,
);

await expect(protocolProvider.getEvents(100n, 0n)).rejects.toThrow(
"Invalid block range: fromBlock must be less than or equal to toBlock",
);
});
});
});
Loading