diff --git a/apps/agent/src/config/schemas.ts b/apps/agent/src/config/schemas.ts index 7eadd98..df3549e 100644 --- a/apps/agent/src/config/schemas.ts +++ b/apps/agent/src/config/schemas.ts @@ -1,4 +1,4 @@ -import { Caip2ChainId, Caip2Utils } from "@ebo-agent/blocknumber/src/index.js"; +import { Caip2ChainId, Caip2Utils } from "@ebo-agent/blocknumber"; import { isAddress, isHex } from "viem"; import { z } from "zod"; diff --git a/packages/automated-dispute/src/exceptions/decodeLogDataFailure.ts b/packages/automated-dispute/src/exceptions/decodeLogDataFailure.ts new file mode 100644 index 0000000..65b0526 --- /dev/null +++ b/packages/automated-dispute/src/exceptions/decodeLogDataFailure.ts @@ -0,0 +1,7 @@ +export class DecodeLogDataFailure extends Error { + constructor(err: unknown) { + super(`Error decoding log data: ${err}`); + + this.name = "DecodeLogDataFailure"; + } +} diff --git a/packages/automated-dispute/src/exceptions/index.ts b/packages/automated-dispute/src/exceptions/index.ts index b7906c1..0d3a97a 100644 --- a/packages/automated-dispute/src/exceptions/index.ts +++ b/packages/automated-dispute/src/exceptions/index.ts @@ -10,6 +10,9 @@ export * from "./responseAlreadyProposed.exception.js"; export * from "./rpcUrlsEmpty.exception.js"; export * from "./transactionExecutionError.exception.js"; export * from "./invalidAccountOnClient.exception.js"; +export * from "./unsupportedEvent.exception.js"; +export * from "./decodeLogDataFailure.js"; +export * from "./invalidBlockRangeError.exception.js"; export * from "./unknownCustomError.exception.js"; export * from "./customContractError.js"; export * from "./errorFactory.js"; diff --git a/packages/automated-dispute/src/exceptions/invalidBlockRangeError.exception.ts b/packages/automated-dispute/src/exceptions/invalidBlockRangeError.exception.ts new file mode 100644 index 0000000..9a2258d --- /dev/null +++ b/packages/automated-dispute/src/exceptions/invalidBlockRangeError.exception.ts @@ -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"; + } +} diff --git a/packages/automated-dispute/src/exceptions/unsupportedEvent.exception.ts b/packages/automated-dispute/src/exceptions/unsupportedEvent.exception.ts new file mode 100644 index 0000000..596f3a2 --- /dev/null +++ b/packages/automated-dispute/src/exceptions/unsupportedEvent.exception.ts @@ -0,0 +1,6 @@ +export class UnsupportedEvent extends Error { + constructor(message: string) { + super(message); + this.name = "UnsupportedEvent"; + } +} diff --git a/packages/automated-dispute/src/interfaces/protocolProvider.ts b/packages/automated-dispute/src/interfaces/protocolProvider.ts index 43c1587..75d0e2f 100644 --- a/packages/automated-dispute/src/interfaces/protocolProvider.ts +++ b/packages/automated-dispute/src/interfaces/protocolProvider.ts @@ -1,7 +1,15 @@ import { Caip2ChainId } from "@ebo-agent/blocknumber"; import { Address, Block } from "viem"; -import type { Dispute, EboEvent, EboEventName, Epoch, Request, Response } from "../types/index.js"; +import type { + Dispute, + EboEvent, + EboEventName, + Epoch, + Request, + RequestId, + Response, +} from "../types/index.js"; import { ProtocolContractsNames } from "../constants.js"; export type ProtocolContract = (typeof ProtocolContractsNames)[number]; @@ -194,3 +202,79 @@ export interface IProtocolProvider { */ read: IReadProvider; } + +/** + * @type DecodedLogArgsMap + * Represents the mapping of event names to their respective argument structures. + */ +export type DecodedLogArgsMap = { + /** + * Event arguments for the RequestCreated event. + * @property {RequestId} requestId - The ID of the request. + * @property {bigint} epoch - The epoch time when the request was created. + * @property {Caip2ChainId} chainId - The chain ID where the request was created. + */ + RequestCreated: { + requestId: RequestId; + epoch: bigint; + chainId: Caip2ChainId; + }; + + /** + * Event arguments for the ResponseProposed event. + * @property {RequestId} requestId - The ID of the request. + * @property {string} responseId - The ID of the response. + * @property {string} response - The response content. + */ + ResponseProposed: { + requestId: RequestId; + responseId: string; + response: string; + }; + + /** + * Event arguments for the ResponseDisputed event. + * @property {string} responseId - The ID of the response. + * @property {string} disputeId - The ID of the dispute. + * @property {string} dispute - The dispute content. + */ + ResponseDisputed: { + responseId: string; + disputeId: string; + dispute: string; + }; + + /** + * Event arguments for the DisputeStatusUpdated event. + * @property {string} disputeId - The ID of the dispute. + * @property {string} dispute - The dispute content. + * @property {number} status - The new status of the dispute. + */ + DisputeStatusUpdated: { + disputeId: string; + dispute: string; + status: number; + }; + + /** + * Event arguments for the DisputeEscalated event. + * @property {string} caller - The address of the caller who escalated the dispute. + * @property {string} disputeId - The ID of the dispute. + */ + DisputeEscalated: { + caller: string; + disputeId: string; + }; + + /** + * Event arguments for the OracleRequestFinalized event. + * @property {RequestId} requestId - The ID of the request. + * @property {string} responseId - The ID of the response. + * @property {string} caller - The address of the caller who finalized the request. + */ + OracleRequestFinalized: { + requestId: RequestId; + responseId: string; + caller: string; + }; +}; diff --git a/packages/automated-dispute/src/providers/protocolProvider.ts b/packages/automated-dispute/src/providers/protocolProvider.ts index 0dadd2d..09754b0 100644 --- a/packages/automated-dispute/src/providers/protocolProvider.ts +++ b/packages/automated-dispute/src/providers/protocolProvider.ts @@ -1,6 +1,7 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { Caip2ChainId } from "@ebo-agent/blocknumber"; import { UnixTimestamp } from "@ebo-agent/shared"; import { + AbiEvent, Address, BaseError, Block, @@ -9,6 +10,7 @@ import { createPublicClient, createWalletClient, decodeAbiParameters, + decodeEventLog, encodeAbiParameters, fallback, FallbackTransport, @@ -17,13 +19,22 @@ import { Hex, http, HttpTransport, + Log, PublicClient, WalletClient, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arbitrum, mainnet } from "viem/chains"; -import type { Dispute, EboEvent, EboEventName, Epoch, Request, Response } from "../types/index.js"; +import type { + Dispute, + EboEvent, + EboEventName, + Epoch, + Request, + RequestId, + Response, +} from "../types/index.js"; import { bondEscalationModuleAbi, eboRequestCreatorAbi, @@ -32,12 +43,16 @@ import { oracleAbi, } from "../abis/index.js"; import { + DecodeLogDataFailure, ErrorFactory, InvalidAccountOnClient, + InvalidBlockRangeError, RpcUrlsEmpty, TransactionExecutionError, + UnsupportedEvent, } from "../exceptions/index.js"; import { + DecodedLogArgsMap, IProtocolProvider, IReadProvider, IWriteProvider, @@ -163,6 +178,17 @@ export class ProtocolProvider implements IProtocolProvider { }); } + /** + * Class-level attribute to store Oracle event names. + */ + private static readonly ORACLE_EVENT_NAMES: EboEventName[] = [ + "ResponseProposed", + "ResponseDisputed", + "DisputeStatusUpdated", + "DisputeEscalated", + "OracleRequestFinalized", + ]; + public write: IWriteProvider = { createRequest: this.createRequest.bind(this), proposeResponse: this.proposeResponse.bind(this), @@ -231,6 +257,14 @@ export class ProtocolProvider implements IProtocolProvider { }); } + /** + * Precomputed array of Oracle event ABIs. + */ + private static readonly ORACLE_EVENTS_ABI: AbiEvent[] = ProtocolProvider.ORACLE_EVENT_NAMES.map( + (eventName) => + oracleAbi.find((e) => e.name === eventName && e.type === "event") as AbiEvent, + ); + /** * Returns the address of the account used for transactions. * @@ -274,21 +308,178 @@ export class ProtocolProvider implements IProtocolProvider { } /** - * Gets a list of events between two blocks. + * Decodes the log data for a specific event. + * + * @param eventName - The name of the event to decode. + * @param log - The log object containing the event data. + * @returns The decoded log data as an object. + * @throws {Error} If the event name is unsupported or if there's an error during decoding. + */ + private decodeLogData( + eventName: TEventName, + log: Log, + ): DecodedLogArgsMap[TEventName] { + let abi; + switch (eventName) { + case "RequestCreated": + abi = eboRequestCreatorAbi; + break; + case "ResponseProposed": + case "ResponseDisputed": + case "DisputeStatusUpdated": + case "DisputeEscalated": + case "OracleRequestFinalized": + abi = oracleAbi; + break; + default: + throw new UnsupportedEvent(`Unsupported event name: ${eventName}`); + } + + try { + const decodedLog = decodeEventLog({ + abi, + data: log.data, + topics: log.topics, + eventName, + strict: true, + }); + + return decodedLog.args as DecodedLogArgsMap[TEventName]; + } catch (error) { + throw new DecodeLogDataFailure(error); + } + } + + /** + * Parses an Oracle event log into an EboEvent. + * + * @param eventName - The name of the event. + * @param log - The event log to parse. + * @returns An EboEvent object. + */ + private parseOracleEvent(eventName: EboEventName, log: Log) { + const baseEvent = { + name: eventName, + blockNumber: log.blockNumber, + logIndex: log.logIndex, + rawLog: log, + }; + + const decodedLog = this.decodeLogData(eventName, log); + + let requestId: RequestId; + switch (eventName) { + // TODO: extract request ID properly from decoded log + case "ResponseProposed": + // @ts-expect-error: must extract request ID properly + requestId = decodedLog.requestId; + break; + case "ResponseDisputed": + // @ts-expect-error: must extract request ID properly + requestId = decodedLog.requestId; + break; + case "DisputeStatusUpdated": + case "DisputeEscalated": + // @ts-expect-error: must extract request ID properly + requestId = decodedLog.requestId; + break; + case "OracleRequestFinalized": + // @ts-expect-error: must extract request ID properly + requestId = decodedLog.requestId; + break; + default: + throw new UnsupportedEvent(`Unsupported event name: ${eventName}`); + } + + return { + ...baseEvent, + requestId, + metadata: decodedLog, + }; + } + + /** + * Fetches events from the Oracle 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. + * + */ + // OPTIMIZE: could remove decodeLogData and improve typing if using getContractEvents for each function + private async getOracleEvents(fromBlock: bigint, toBlock: bigint) { + const logs = await this.l2ReadClient.getLogs({ + address: this.oracleContract.address, + events: ProtocolProvider.ORACLE_EVENTS_ABI, + 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 {bigint} _fromBlock - The starting block number. - * @param {bigint} _toBlock - The ending block number. - * @returns {Promise[]>} A list of EBO events. + * @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. */ - async getEvents(_fromBlock: bigint, _toBlock: bigint): Promise[]> { - // TODO: implement actual method. - // - // We should decode events using the corresponding ABI and also "fabricate" new events - // if for some triggers there are no events (e.g. dispute window ended) - const eboRequestCreatorEvents: EboEvent[] = []; - const oracleEvents: EboEvent[] = []; - - return this.mergeEventStreams(eboRequestCreatorEvents, oracleEvents); + private async getEBORequestCreatorEvents(fromBlock: bigint, toBlock: bigint) { + const events = await this.l2ReadClient.getContractEvents({ + address: this.eboRequestCreatorContract.address, + abi: eboRequestCreatorAbi, + eventName: "RequestCreated", + fromBlock, + toBlock, + strict: true, + }); + + return events.map((event) => { + if (event.blockNumber === null || event.logIndex === null) { + throw new Error("event.blockNumber or event.logIndex is null"); + } + + return { + name: "RequestCreated" as const, + blockNumber: event.blockNumber, + logIndex: event.logIndex, + rawLog: event, + + requestId: event.args._requestId, + metadata: { + requestId: event.args._requestId, + epoch: event.args._epoch, + chainId: event.args._chainId, + }, + } as unknown as EboEvent<"RequestCreated">; + }); + } + + /** + * Retrieves events from all relevant contracts within a specified block range. + * + * @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 sorted by block number and log index. + * @throws {Error} If the block range is invalid or if there's an error fetching events. + */ + async getEvents(fromBlock: bigint, toBlock: bigint) { + if (fromBlock > toBlock) { + throw new InvalidBlockRangeError(fromBlock, toBlock); + } + + const [requestCreatorEvents, oracleEvents] = await Promise.all([ + this.getEBORequestCreatorEvents(fromBlock, toBlock), + this.getOracleEvents(fromBlock, toBlock), + ]); + + // TODO: update after requestId extracted properly + // @ts-expect-error: will error until requestId extracted from each event properly + return this.mergeEventStreams(requestCreatorEvents, oracleEvents); } /** @@ -302,11 +493,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; }); diff --git a/packages/automated-dispute/src/services/eboActor.ts b/packages/automated-dispute/src/services/eboActor.ts index 7672a99..47c5948 100644 --- a/packages/automated-dispute/src/services/eboActor.ts +++ b/packages/automated-dispute/src/services/eboActor.ts @@ -1,5 +1,4 @@ -import { BlockNumberService } from "@ebo-agent/blocknumber"; -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { BlockNumberService, Caip2ChainId } from "@ebo-agent/blocknumber"; import { Address, ILogger, UnixTimestamp } from "@ebo-agent/shared"; import { Mutex } from "async-mutex"; import { Heap } from "heap-js"; @@ -214,9 +213,9 @@ export class EboActor { this.registry, ); - case "DisputeStatusChanged": + case "DisputeStatusUpdated": return UpdateDisputeStatus.buildFromEvent( - event as EboEvent<"DisputeStatusChanged">, + event as EboEvent<"DisputeStatusUpdated">, this.registry, ); @@ -226,9 +225,9 @@ export class EboActor { this.registry, ); - case "RequestFinalized": + case "OracleRequestFinalized": return FinalizeRequest.buildFromEvent( - event as EboEvent<"RequestFinalized">, + event as EboEvent<"OracleRequestFinalized">, this.registry, ); @@ -261,8 +260,8 @@ export class EboActor { break; - case "DisputeStatusChanged": - await this.onDisputeStatusChanged(event as EboEvent<"DisputeStatusChanged">); + case "DisputeStatusUpdated": + await this.onDisputeStatusChanged(event as EboEvent<"DisputeStatusUpdated">); break; @@ -271,8 +270,8 @@ export class EboActor { break; - case "RequestFinalized": - await this.onRequestFinalized(event as EboEvent<"RequestFinalized">); + case "OracleRequestFinalized": + await this.onRequestFinalized(event as EboEvent<"OracleRequestFinalized">); break; @@ -857,7 +856,7 @@ export class EboActor { * * @param event `DisputeStatusChanged` event */ - private async onDisputeStatusChanged(event: EboEvent<"DisputeStatusChanged">): Promise { + private async onDisputeStatusChanged(event: EboEvent<"DisputeStatusUpdated">): Promise { const request = this.getActorRequest(); const disputeId = event.metadata.disputeId; const disputeStatus = event.metadata.status; @@ -921,7 +920,7 @@ export class EboActor { * * @param event `ResponseFinalized` event */ - private async onRequestFinalized(_event: EboEvent<"RequestFinalized">): Promise { + private async onRequestFinalized(_event: EboEvent<"OracleRequestFinalized">): Promise { const request = this.getActorRequest(); this.logger.info(`Request ${request.id} has been finalized.`); diff --git a/packages/automated-dispute/src/services/eboProcessor.ts b/packages/automated-dispute/src/services/eboProcessor.ts index 8c19969..0407161 100644 --- a/packages/automated-dispute/src/services/eboProcessor.ts +++ b/packages/automated-dispute/src/services/eboProcessor.ts @@ -1,6 +1,5 @@ import { isNativeError } from "util/types"; -import { BlockNumberService } from "@ebo-agent/blocknumber"; -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { BlockNumberService, Caip2ChainId } from "@ebo-agent/blocknumber"; import { Address, EBO_SUPPORTED_CHAIN_IDS, ILogger, UnixTimestamp } from "@ebo-agent/shared"; import { Block } from "viem"; diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/addRequest.ts b/packages/automated-dispute/src/services/eboRegistry/commands/addRequest.ts index 24a881e..5de865f 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/addRequest.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/addRequest.ts @@ -15,6 +15,7 @@ export class AddRequest implements EboRegistryCommand { event: EboEvent<"RequestCreated">, registry: EboRegistry, ): AddRequest { + // @ts-expect-error: must fetch request differently const eventRequest = event.metadata.request; const request: Request = { id: event.requestId, @@ -33,6 +34,7 @@ export class AddRequest implements EboRegistryCommand { eventRequest.responseModuleData, ), }, + // @ts-expect-error: must fetch request prophetData: event.metadata.request, status: "Active", }; diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/finalizeRequest.ts b/packages/automated-dispute/src/services/eboRegistry/commands/finalizeRequest.ts index a84f21f..dca4085 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/finalizeRequest.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/finalizeRequest.ts @@ -12,7 +12,7 @@ export class FinalizeRequest implements EboRegistryCommand { ) {} public static buildFromEvent( - event: EboEvent<"RequestFinalized">, + event: EboEvent<"OracleRequestFinalized">, registry: EboRegistry, ): FinalizeRequest { const requestId = event.metadata.requestId; diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts b/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts index 7fcea96..40b0a20 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/updateDisputeStatus.ts @@ -13,7 +13,7 @@ export class UpdateDisputeStatus implements EboRegistryCommand { ) {} public static buildFromEvent( - event: EboEvent<"DisputeStatusChanged" | "DisputeEscalated">, + event: EboEvent<"DisputeStatusUpdated" | "DisputeEscalated">, registry: EboRegistry, ): UpdateDisputeStatus { const disputeId = event.metadata.disputeId; @@ -27,8 +27,8 @@ export class UpdateDisputeStatus implements EboRegistryCommand { private static isDisputeStatusChangedEvent( event: EboEvent, - ): event is EboEvent<"DisputeStatusChanged"> { - return event.name === "DisputeStatusChanged"; + ): event is EboEvent<"DisputeStatusUpdated"> { + return event.name === "DisputeStatusUpdated"; } run(): void { diff --git a/packages/automated-dispute/src/types/actorRequest.ts b/packages/automated-dispute/src/types/actorRequest.ts index dd954f6..a46e168 100644 --- a/packages/automated-dispute/src/types/actorRequest.ts +++ b/packages/automated-dispute/src/types/actorRequest.ts @@ -1,4 +1,4 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { Caip2ChainId } from "@ebo-agent/blocknumber"; import { RequestId } from "./prophet.js"; diff --git a/packages/automated-dispute/src/types/events.ts b/packages/automated-dispute/src/types/events.ts index f0f4a63..1a831e9 100644 --- a/packages/automated-dispute/src/types/events.ts +++ b/packages/automated-dispute/src/types/events.ts @@ -1,24 +1,16 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { Caip2ChainId } from "@ebo-agent/blocknumber"; import { UnixTimestamp } from "@ebo-agent/shared"; import { Address, Hex, Log } from "viem"; -import { - Dispute, - DisputeId, - DisputeStatus, - Request, - RequestId, - Response, - ResponseId, -} from "./prophet.js"; +import { Dispute, DisputeId, DisputeStatus, RequestId, Response, ResponseId } from "./prophet.js"; export type EboEventName = | "RequestCreated" | "ResponseProposed" | "ResponseDisputed" - | "DisputeStatusChanged" + | "DisputeStatusUpdated" | "DisputeEscalated" - | "RequestFinalized"; + | "OracleRequestFinalized"; export interface ResponseProposed { requestId: Hex; @@ -29,7 +21,6 @@ export interface ResponseProposed { export interface RequestCreated { epoch: bigint; chainId: Caip2ChainId; - request: Request["prophetData"]; requestId: RequestId; } @@ -39,7 +30,7 @@ export interface ResponseDisputed { dispute: Dispute["prophetData"]; } -export interface DisputeStatusChanged { +export interface DisputeStatusUpdated { disputeId: DisputeId; dispute: Dispute["prophetData"]; status: DisputeStatus; @@ -52,7 +43,7 @@ export interface DisputeEscalated { blockNumber: bigint; } -export interface RequestFinalized { +export interface OracleRequestFinalized { requestId: RequestId; responseId: ResponseId; caller: Address; @@ -65,12 +56,12 @@ export type EboEventData = E extends "RequestCreated" ? ResponseProposed : E extends "ResponseDisputed" ? ResponseDisputed - : E extends "DisputeStatusChanged" - ? DisputeStatusChanged + : E extends "DisputeStatusUpdated" + ? DisputeStatusUpdated : E extends "DisputeEscalated" ? DisputeEscalated - : E extends "RequestFinalized" - ? RequestFinalized + : E extends "OracleRequestFinalized" + ? OracleRequestFinalized : never; export type EboEvent = { diff --git a/packages/automated-dispute/src/types/prophet.ts b/packages/automated-dispute/src/types/prophet.ts index 37ebb77..af1270e 100644 --- a/packages/automated-dispute/src/types/prophet.ts +++ b/packages/automated-dispute/src/types/prophet.ts @@ -1,4 +1,4 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { Caip2ChainId } from "@ebo-agent/blocknumber"; import { Branded, NormalizedAddress, UnixTimestamp } from "@ebo-agent/shared"; import { Address, Hex } from "viem"; diff --git a/packages/automated-dispute/tests/services/eboActor/onDisputeStatusChanged.spec.ts b/packages/automated-dispute/tests/services/eboActor/onDisputeStatusUpdated.spec.ts similarity index 93% rename from packages/automated-dispute/tests/services/eboActor/onDisputeStatusChanged.spec.ts rename to packages/automated-dispute/tests/services/eboActor/onDisputeStatusUpdated.spec.ts index 3b0e5a3..c7cd2e5 100644 --- a/packages/automated-dispute/tests/services/eboActor/onDisputeStatusChanged.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onDisputeStatusUpdated.spec.ts @@ -13,14 +13,14 @@ const logger: ILogger = { debug: vi.fn(), }; -describe("onDisputeStatusChanged", () => { +describe("onDisputeStatusUpdated", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; const response = mocks.buildResponse(actorRequest); it("updates the state of the dispute", async () => { const dispute = mocks.buildDispute(actorRequest, response, { status: "None" }); - const event: EboEvent<"DisputeStatusChanged"> = { - name: "DisputeStatusChanged", + const event: EboEvent<"DisputeStatusUpdated"> = { + name: "DisputeStatusUpdated", requestId: actorRequest.id, blockNumber: 1n, logIndex: 1, @@ -49,8 +49,8 @@ describe("onDisputeStatusChanged", () => { it("proposes a new response when dispute status goes into NoResolution", async () => { const proposerAddress = "0x1234567890abcdef1234567890abcdef12345678"; const dispute = mocks.buildDispute(actorRequest, response, { status: "Escalated" }); - const event: EboEvent<"DisputeStatusChanged"> = { - name: "DisputeStatusChanged", + const event: EboEvent<"DisputeStatusUpdated"> = { + name: "DisputeStatusUpdated", requestId: actorRequest.id, blockNumber: 1n, logIndex: 1, diff --git a/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts b/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts index 9da3552..941b6c2 100644 --- a/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts @@ -1,4 +1,4 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { Caip2ChainId } from "@ebo-agent/blocknumber"; import { ILogger } from "@ebo-agent/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/packages/automated-dispute/tests/services/eboActor/onRequestFinalized.spec.ts b/packages/automated-dispute/tests/services/eboActor/onRequestFinalized.spec.ts index e80a818..be6cd28 100644 --- a/packages/automated-dispute/tests/services/eboActor/onRequestFinalized.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onRequestFinalized.spec.ts @@ -10,11 +10,11 @@ const logger: ILogger = mocks.mockLogger(); describe("EboActor", () => { describe("processEvents", () => { - describe("when RequestFinalized is enqueued", () => { + describe("when OracleRequestFinalized is enqueued", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; - const event: EboEvent<"RequestFinalized"> = { - name: "RequestFinalized", + const event: EboEvent<"OracleRequestFinalized"> = { + name: "OracleRequestFinalized", requestId: actorRequest.id, blockNumber: 1n, logIndex: 1, diff --git a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts index 634d5e9..fd6c2b7 100644 --- a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts +++ b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/updateDisputeStatus.spec.ts @@ -14,8 +14,8 @@ describe("UpdateDisputeStatus", () => { const response = mocks.buildResponse(request); const dispute = mocks.buildDispute(request, response); - const event: EboEvent<"DisputeStatusChanged"> = { - name: "DisputeStatusChanged", + const event: EboEvent<"DisputeStatusUpdated"> = { + name: "DisputeStatusUpdated", blockNumber: 1n, logIndex: 1, requestId: request.id, diff --git a/packages/automated-dispute/tests/services/protocolProvider.spec.ts b/packages/automated-dispute/tests/services/protocolProvider.spec.ts index 25d95a6..f3b648e 100644 --- a/packages/automated-dispute/tests/services/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/services/protocolProvider.spec.ts @@ -1,12 +1,17 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; +import { Caip2ChainId } from "@ebo-agent/blocknumber"; import { + AbiEvent, ContractFunctionRevertedError, createPublicClient, createWalletClient, + encodeAbiParameters, fallback, getContract, + getEventSelector, http, isHex, + keccak256, + Log, WaitForTransactionReceiptTimeoutError, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; @@ -24,6 +29,7 @@ import { InvalidAccountOnClient, RpcUrlsEmpty, TransactionExecutionError, + UnsupportedEvent, } from "../../src/exceptions/index.js"; import { ProtocolContractsAddresses } from "../../src/interfaces/index.js"; import { ProtocolProvider } from "../../src/providers/index.js"; @@ -847,4 +853,328 @@ describe("ProtocolProvider", () => { ); }); }); + + describe("decodeLogData", () => { + it("successfully decodes RequestCreated event", () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const eboRequestCreatorAbi = [ + { + type: "event", + name: "RequestCreated", + inputs: [ + { + name: "_requestId", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { name: "_epoch", type: "uint256", indexed: true, internalType: "uint256" }, + { name: "_chainId", type: "string", indexed: true, internalType: "string" }, + ], + anonymous: false, + }, + ]; + + const eventAbi = eboRequestCreatorAbi[0]; + const eventSignature = getEventSelector(eventAbi as AbiEvent); + + const _requestId = "0x" + "123".padStart(64, "0"); + const _epoch = 1n; + const _chainId = "eip155:1"; + + const epochHex = "0x" + _epoch.toString(16).padStart(64, "0"); + const chainIdHash = keccak256(encodeAbiParameters([{ type: "string" }], [_chainId])); + + const topics = [eventSignature, _requestId, epochHex, chainIdHash] as [ + `0x${string}`, + ...`0x${string}`[], + ]; + + const mockLog: Log = { + address: "0x1234567890123456789012345678901234567890", + topics, + data: "0x", + blockNumber: 1n, + transactionHash: + "0x1234567890123456789012345678901234567890123456789012345678901234", + transactionIndex: 1, + blockHash: "0x1234567890123456789012345678901234567890123456789012345678901234", + logIndex: 1, + removed: false, + }; + + const result = (protocolProvider as any).decodeLogData("RequestCreated", mockLog); + + expect(result).toBeDefined(); + expect(result).toEqual({ + _requestId, + _epoch, + _chainId: chainIdHash, + }); + }); + + it("throws an error for unsupported event name", () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockLog: Log = { + address: "0x1234567890123456789012345678901234567890", + topics: ["0x0000000000000000000000000000000000000000000000000000000000000001"], + data: "0x", + blockNumber: 1n, + transactionHash: + "0x1234567890123456789012345678901234567890123456789012345678901234", + transactionIndex: 1, + blockHash: "0x1234567890123456789012345678901234567890123456789012345678901234", + logIndex: 1, + removed: false, + }; + + expect(() => + (protocolProvider as any).parseOracleEvent("UnsupportedEvent", mockLog), + ).toThrow("Unsupported event name: UnsupportedEvent"); + }); + }); + + describe("parseOracleEvent", () => { + it("successfully parses ResponseProposed event", () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockLog: Log = { + address: "0x1234567890123456789012345678901234567890", + topics: [ + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", + ], + data: "0x0000000000000000000000000000000000000000000000000000000000000003", + blockNumber: 1n, + transactionHash: + "0x1234567890123456789012345678901234567890123456789012345678901234", + transactionIndex: 1, + blockHash: "0x1234567890123456789012345678901234567890123456789012345678901234", + logIndex: 1, + removed: false, + }; + + vi.spyOn(protocolProvider as any, "decodeLogData").mockReturnValue({ + requestId: "0x0000000000000000000000000000000000000000000000000000000000000002", + responseId: "0x456", + response: "0x789", + blockNumber: 1n, + }); + + const result = (protocolProvider as any).parseOracleEvent("ResponseProposed", mockLog); + + expect(result).toEqual({ + name: "ResponseProposed", + blockNumber: 1n, + logIndex: 1, + rawLog: mockLog, + requestId: "0x0000000000000000000000000000000000000000000000000000000000000002", + metadata: { + requestId: "0x0000000000000000000000000000000000000000000000000000000000000002", + responseId: "0x456", + response: "0x789", + blockNumber: 1n, + }, + }); + }); + + it("throws UnsupportedEvent for unsupported event name", () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockLog: Log = { + address: "0x1234567890123456789012345678901234567890", + topics: ["0x0000000000000000000000000000000000000000000000000000000000000001"], + data: "0x", + blockNumber: 1n, + transactionHash: + "0x1234567890123456789012345678901234567890123456789012345678901234", + transactionIndex: 1, + blockHash: "0x1234567890123456789012345678901234567890123456789012345678901234", + logIndex: 1, + removed: false, + }; + + expect(() => + (protocolProvider as any).parseOracleEvent("UnsupportedEvent", mockLog), + ).toThrow(UnsupportedEvent); + + expect(() => + (protocolProvider as any).parseOracleEvent("UnsupportedEvent", mockLog), + ).toThrow("Unsupported event name: UnsupportedEvent"); + }); + }); + + describe("getOracleEvents", () => { + it("successfully fetches and parses Oracle events", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockLogs: Log[] = [ + { + address: "0x1234567890123456789012345678901234567890", + topics: [ + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", + ], + data: "0x0000000000000000000000000000000000000000000000000000000000000003", + blockNumber: 1n, + transactionHash: + "0x1234567890123456789012345678901234567890123456789012345678901234", + transactionIndex: 1, + blockHash: "0x1234567890123456789012345678901234567890123456789012345678901234", + logIndex: 1, + removed: false, + }, + ]; + + (protocolProvider["l2ReadClient"] as any).getLogs = vi.fn().mockResolvedValue(mockLogs); + + vi.spyOn(protocolProvider as any, "parseOracleEvent").mockReturnValue({ + name: "ResponseProposed", + blockNumber: 1n, + logIndex: 1, + rawLog: mockLogs[0], + requestId: "0x123", + metadata: {}, + }); + + const result = await (protocolProvider as any).getOracleEvents(0n, 100n); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + name: "ResponseProposed", + blockNumber: 1n, + logIndex: 1, + rawLog: mockLogs[0], + requestId: "0x123", + metadata: {}, + }); + }); + }); + + describe("getEBORequestCreatorEvents", () => { + it("successfully fetches and parses EBORequestCreator events", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + (protocolProvider["l2ReadClient"] as any).getContractEvents = vi + .fn() + .mockResolvedValue([ + { + address: "0x1234567890123456789012345678901234567890", + args: { + _requestId: "0x123", + _epoch: 1n, + _chainId: "eip155:1", + }, + blockNumber: 1n, + logIndex: 1, + blockHash: + "0x1234567890123456789012345678901234567890123456789012345678901234", + transactionHash: "0x1234567890123456789012345678901234567890", + transactionIndex: 1, + data: "0x0000000000000000000000000000000000000000000000000000000000000003", + removed: false, + topics: [ + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", + ], + }, + ]); + + const result = await (protocolProvider as any).getEBORequestCreatorEvents(0n, 100n); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + name: "RequestCreated", + blockNumber: 1n, + logIndex: 1, + requestId: "0x123", + metadata: { + requestId: "0x123", + epoch: 1n, + chainId: "eip155:1", + }, + rawLog: { + address: "0x1234567890123456789012345678901234567890", + args: { + _chainId: "eip155:1", + _epoch: 1n, + _requestId: "0x123", + }, + blockHash: "0x1234567890123456789012345678901234567890123456789012345678901234", + blockNumber: 1n, + data: "0x0000000000000000000000000000000000000000000000000000000000000003", + logIndex: 1, + removed: false, + topics: [ + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", + ], + transactionHash: "0x1234567890123456789012345678901234567890", + transactionIndex: 1, + }, + }); + }); + }); + + describe("getEvents", () => { + it("successfully merges and sorts events from all sources", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockRequestCreatorEvents = [ + { name: "RequestCreated", blockNumber: 1n, logIndex: 0 }, + { name: "RequestCreated", blockNumber: 3n, logIndex: 0 }, + ]; + + const mockOracleEvents = [ + { name: "ResponseDisputed", blockNumber: 2n, logIndex: 0 }, + { name: "ResponseProposed", blockNumber: 2n, logIndex: 1 }, + ]; + + vi.spyOn(protocolProvider as any, "getEBORequestCreatorEvents").mockResolvedValue( + mockRequestCreatorEvents, + ); + vi.spyOn(protocolProvider as any, "getOracleEvents").mockResolvedValue( + mockOracleEvents, + ); + + const result = await protocolProvider.getEvents(0n, 100n); + + expect(result).toEqual([ + { name: "RequestCreated", blockNumber: 1n, logIndex: 0 }, + { name: "ResponseDisputed", blockNumber: 2n, logIndex: 0 }, + { name: "ResponseProposed", blockNumber: 2n, logIndex: 1 }, + { name: "RequestCreated", blockNumber: 3n, logIndex: 0 }, + ]); + }); + }); });