diff --git a/packages/automated-dispute/src/interfaces/protocolProvider.ts b/packages/automated-dispute/src/interfaces/protocolProvider.ts index d30708a..421c797 100644 --- a/packages/automated-dispute/src/interfaces/protocolProvider.ts +++ b/packages/automated-dispute/src/interfaces/protocolProvider.ts @@ -1,8 +1,6 @@ -import { Timestamp } from "@ebo-agent/shared"; import { Address } from "viem"; -import type { EboEvent, EboEventName } from "../types/events.js"; -import type { Dispute, Request, Response } from "../types/prophet.js"; +import type { Dispute, EboEvent, EboEventName, Epoch, Request, Response } from "../types/index.js"; import { ProtocolContractsNames } from "../constants.js"; export type ProtocolContract = (typeof ProtocolContractsNames)[number]; @@ -17,11 +15,7 @@ export interface IReadProvider { * * @returns A promise that resolves with the current epoch, block number, and timestamp. */ - getCurrentEpoch(): Promise<{ - currentEpoch: bigint; - currentEpochBlockNumber: bigint; - currentEpochTimestamp: Timestamp; - }>; + getCurrentEpoch(): Promise; /** * Gets the last finalized block number. diff --git a/packages/automated-dispute/src/providers/protocolProvider.ts b/packages/automated-dispute/src/providers/protocolProvider.ts index 2fcf262..dbb9d7b 100644 --- a/packages/automated-dispute/src/providers/protocolProvider.ts +++ b/packages/automated-dispute/src/providers/protocolProvider.ts @@ -1,4 +1,4 @@ -import { Timestamp } from "@ebo-agent/shared"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; import { Address, BaseError, @@ -18,8 +18,7 @@ import { import { privateKeyToAccount } from "viem/accounts"; import { arbitrum } from "viem/chains"; -import type { EboEvent, EboEventName } from "../types/events.js"; -import type { Dispute, Request, Response } from "../types/prophet.js"; +import type { Dispute, EboEvent, EboEventName, Epoch, Request, Response } from "../types/index.js"; import { eboRequestCreatorAbi, epochManagerAbi, oracleAbi } from "../abis/index.js"; import { InvalidAccountOnClient, @@ -153,24 +152,20 @@ export class ProtocolProvider implements IProtocolProvider { * * @returns The current epoch, its block number and its timestamp */ - async getCurrentEpoch(): Promise<{ - currentEpoch: bigint; - currentEpochBlockNumber: bigint; - currentEpochTimestamp: Timestamp; - }> { - const [currentEpoch, currentEpochBlockNumber] = await Promise.all([ + async getCurrentEpoch(): Promise { + const [epoch, epochFirstBlockNumber] = await Promise.all([ this.epochManagerContract.read.currentEpoch(), this.epochManagerContract.read.currentEpochBlock(), ]); - const currentEpochBlock = await this.readClient.getBlock({ - blockNumber: currentEpochBlockNumber, + const epochFirstBlock = await this.readClient.getBlock({ + blockNumber: epochFirstBlockNumber, }); return { - currentEpoch, - currentEpochBlockNumber, - currentEpochTimestamp: currentEpochBlock.timestamp, + number: epoch, + firstBlockNumber: epochFirstBlockNumber, + startTimestamp: epochFirstBlock.timestamp, }; } @@ -257,7 +252,7 @@ export class ProtocolProvider implements IProtocolProvider { } // TODO: use Caip2 Chain ID instead of string in return type - async getAvailableChains(): Promise { + async getAvailableChains(): Promise { // TODO: implement actual method return ["eip155:1", "eip155:42161"]; } diff --git a/packages/automated-dispute/src/services/eboActor.ts b/packages/automated-dispute/src/services/eboActor.ts index 218f100..1f19a13 100644 --- a/packages/automated-dispute/src/services/eboActor.ts +++ b/packages/automated-dispute/src/services/eboActor.ts @@ -5,6 +5,16 @@ import { Mutex } from "async-mutex"; import { Heap } from "heap-js"; import { ContractFunctionRevertedError } from "viem"; +import type { + Dispute, + DisputeStatus, + EboEvent, + EboEventName, + Epoch, + Request, + Response, + ResponseBody, +} from "../types/index.js"; import { DisputeWithoutResponse, EBORequestCreator_ChainNotAdded, @@ -27,16 +37,7 @@ import { FinalizeRequest, UpdateDisputeStatus, } from "../services/index.js"; -import { - Dispute, - DisputeStatus, - EboEvent, - EboEventName, - Request, - RequestId, - Response, - ResponseBody, -} from "../types/index.js"; +import { ActorRequest } from "../types/actorRequest.js"; /** * Compare function to sort events chronologically in ascending order by block number @@ -78,10 +79,7 @@ export class EboActor { * @param logger an `ILogger` instance */ constructor( - private readonly actorRequest: { - id: RequestId; - epoch: bigint; - }, + public readonly actorRequest: ActorRequest, private readonly protocolProvider: ProtocolProvider, private readonly blockNumberService: BlockNumberService, private readonly registry: EboRegistry, @@ -432,15 +430,19 @@ export class EboActor { * * Be aware that a request can be finalized but some of its disputes can still be pending resolution. * + * At last, actors must be kept alive until their epoch concludes, to ensure no actor/request duplication. + * + * @param currentEpoch the epoch to check against actor termination * @param blockNumber block number to check entities at * @returns `true` if all entities are settled, otherwise `false` */ - public canBeTerminated(blockNumber: bigint): boolean { + public canBeTerminated(currentEpoch: Epoch["number"], blockNumber: bigint): boolean { const request = this.getActorRequest(); + const isPastEpoch = currentEpoch > request.epoch; const isRequestFinalized = request.status === "Finalized"; const nonSettledProposals = this.activeProposals(blockNumber); - return isRequestFinalized && nonSettledProposals.length === 0; + return isPastEpoch && isRequestFinalized && nonSettledProposals.length === 0; } /** @@ -556,10 +558,11 @@ export class EboActor { private async buildResponse(chainId: Caip2ChainId): Promise { // FIXME(non-current epochs): adapt this code to fetch timestamps corresponding // to the first block of any epoch, not just the current epoch - const { currentEpochTimestamp } = await this.protocolProvider.getCurrentEpoch(); + const { startTimestamp: epochStartTimestamp } = + await this.protocolProvider.getCurrentEpoch(); const epochBlockNumber = await this.blockNumberService.getEpochBlockNumber( - currentEpochTimestamp, + epochStartTimestamp, chainId, ); diff --git a/packages/automated-dispute/src/services/eboActorsManager.ts b/packages/automated-dispute/src/services/eboActorsManager.ts index 4643343..4b09f97 100644 --- a/packages/automated-dispute/src/services/eboActorsManager.ts +++ b/packages/automated-dispute/src/services/eboActorsManager.ts @@ -4,7 +4,7 @@ import { Mutex } from "async-mutex"; import { RequestAlreadyHandled } from "../exceptions/index.js"; import { ProtocolProvider } from "../providers/protocolProvider.js"; -import { RequestId } from "../types/prophet.js"; +import { ActorRequest, RequestId } from "../types/index.js"; import { EboActor } from "./eboActor.js"; import { EboMemoryRegistry } from "./eboRegistry/eboMemoryRegistry.js"; @@ -21,7 +21,11 @@ export class EboActorsManager { * @returns array of normalized request IDs */ public getRequestIds(): RequestId[] { - return [...this.requestActorMap.entries()].map((entry) => Address.normalize(entry[0])); + return [...this.requestActorMap.keys()].map((requestId) => Address.normalize(requestId)); + } + + public getActorsRequests(): ActorRequest[] { + return [...this.requestActorMap.values()].map((actor) => actor.actorRequest); } /** @@ -30,10 +34,7 @@ export class EboActorsManager { * @param actor an `EboActor` instance that handles a request. */ public createActor( - actorRequest: { - id: RequestId; - epoch: bigint; - }, + actorRequest: ActorRequest, protocolProvider: ProtocolProvider, blockNumberService: BlockNumberService, logger: ILogger, diff --git a/packages/automated-dispute/src/services/eboProcessor.ts b/packages/automated-dispute/src/services/eboProcessor.ts index ce18690..7950502 100644 --- a/packages/automated-dispute/src/services/eboProcessor.ts +++ b/packages/automated-dispute/src/services/eboProcessor.ts @@ -1,12 +1,12 @@ import { isNativeError } from "util/types"; import { BlockNumberService } from "@ebo-agent/blocknumber"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; import { Address, ILogger } from "@ebo-agent/shared"; import { ProcessorAlreadyStarted } from "../exceptions/index.js"; import { ProtocolProvider } from "../providers/protocolProvider.js"; import { alreadyDeletedActorWarning, droppingUnhandledEventsWarning } from "../templates/index.js"; -import { EboEvent, EboEventName } from "../types/events.js"; -import { RequestId } from "../types/prophet.js"; +import { ActorRequest, EboEvent, EboEventName, Epoch, RequestId } from "../types/index.js"; import { EboActorsManager } from "./eboActorsManager.js"; const DEFAULT_MS_BETWEEN_CHECKS = 10 * 60 * 1000; // 10 minutes @@ -49,13 +49,11 @@ export class EboProcessor { /** Sync new blocks and their events with their corresponding actors. */ private async sync() { - // TODO: detect new epoch by comparing subgraph's data with EpochManager's current epoch - // and trigger a request creation if there's no actor handling an request. - // This process should somehow check if there's already a request created for the epoch - // and chain that has no agent assigned and create it if that's the case. try { + const currentEpoch = await this.getCurrentEpoch(); + if (!this.lastCheckedBlock) { - this.lastCheckedBlock = await this.getEpochStartBlock(); + this.lastCheckedBlock = currentEpoch.firstBlockNumber; } const lastBlock = await this.getLastFinalizedBlock(); @@ -74,7 +72,7 @@ export class EboProcessor { try { const events = eventsByRequestId.get(requestId) ?? []; - await this.syncRequest(requestId, events, lastBlock); + await this.syncRequest(requestId, events, currentEpoch.number, lastBlock); } catch (err) { this.onActorError(requestId, err as Error); } @@ -84,6 +82,8 @@ export class EboProcessor { this.logger.info(`Consumed events up to block ${lastBlock}.`); + this.createMissingRequests(currentEpoch.number); + this.lastCheckedBlock = lastBlock; } catch (err) { if (isNativeError(err)) { @@ -97,18 +97,18 @@ export class EboProcessor { } /** - * Fetches the first block of the current epoch. + * Fetches the current epoch for the protocol chain. * - * @returns the first block of the current epoch + * @returns the current epoch properties of the protocol chain. */ - private async getEpochStartBlock(): Promise { - this.logger.info("Fetching current epoch start block..."); + private async getCurrentEpoch(): Promise { + this.logger.info("Fetching current epoch..."); - const { currentEpochBlockNumber } = await this.protocolProvider.getCurrentEpoch(); + const currentEpoch = await this.protocolProvider.getCurrentEpoch(); - this.logger.info(`Current epoch start block ${currentEpochBlockNumber} fetched.`); + this.logger.info(`Current epoch fetched.`); - return currentEpochBlockNumber; + return currentEpoch; } /** @@ -182,9 +182,15 @@ export class EboProcessor { * * @param requestId the ID of the `Request` * @param events a stream of consumed events + * @param currentEpoch the current epoch based on the last block * @param lastBlock the last block checked */ - private async syncRequest(requestId: RequestId, events: EboEventStream, lastBlock: bigint) { + private async syncRequest( + requestId: RequestId, + events: EboEventStream, + currentEpoch: Epoch["number"], + lastBlock: bigint, + ) { const firstEvent = events[0]; const actor = this.getOrCreateActor(requestId, firstEvent); @@ -199,7 +205,7 @@ export class EboProcessor { await actor.processEvents(); await actor.onLastBlockUpdated(lastBlock); - if (actor.canBeTerminated(lastBlock)) { + if (actor.canBeTerminated(currentEpoch, lastBlock)) { this.terminateActor(requestId); } } @@ -235,6 +241,7 @@ export class EboProcessor { const actorRequest = { id: Address.normalize(event.requestId), epoch: event.metadata.epoch, + chainId: event.metadata.chainId, }; const actor = this.actorsManager.createActor( @@ -259,6 +266,72 @@ export class EboProcessor { this.terminateActor(requestId); } + /** + * Creates missing requests for the specified epoch, based on the + * available chains and the currently being handled requests. + * + * @param epoch the epoch number + */ + private async createMissingRequests(epoch: Epoch["number"]): Promise { + try { + const handledEpochChains = this.actorsManager + .getActorsRequests() + .reduce((actorRequestMap, actorRequest: ActorRequest) => { + const epochRequests = actorRequestMap.get(actorRequest.epoch) ?? new Set(); + + epochRequests.add(actorRequest.chainId); + + return actorRequestMap.set(actorRequest.epoch, epochRequests); + }, new Map>()); + + this.logger.info("Fetching available chains..."); + + const availableChains: Caip2ChainId[] = + await this.protocolProvider.getAvailableChains(); + + this.logger.info("Available chains fetched."); + + const unhandledEpochChain = availableChains.filter((chain) => { + const epochRequests = handledEpochChains.get(epoch); + const isHandled = epochRequests && epochRequests.has(chain); + + return !isHandled; + }); + + this.logger.info("Creating missing requests..."); + + const epochChainRequests = unhandledEpochChain.map(async (chain) => { + try { + this.logger.info(`Creating request for chain ${chain} and epoch ${epoch}...`); + + await this.protocolProvider.createRequest(epoch, [chain]); + + this.logger.info(`Request created for chain ${chain} and epoch ${epoch}`); + } catch (err) { + // Request creation must be notified but it's not critical, as it will be + // retried during next sync. + + // TODO: warn when getting a EBORequestCreator_RequestAlreadyCreated + // TODO: notify under any other error + + this.logger.error( + `Could not create a request for epoch ${epoch} and chain ${chain}.`, + ); + } + }); + + await Promise.all(epochChainRequests); + + this.logger.info("Missing requests created."); + } catch (err) { + // TODO: notify + + this.logger.error( + `Requests creation missing: ${isNativeError(err) ? err.message : err}`, + ); + } + } + /** * Removes the actor from tracking the request. * diff --git a/packages/automated-dispute/src/types/actorRequest.ts b/packages/automated-dispute/src/types/actorRequest.ts new file mode 100644 index 0000000..3c83f11 --- /dev/null +++ b/packages/automated-dispute/src/types/actorRequest.ts @@ -0,0 +1,5 @@ +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; + +import { RequestId } from "./prophet.js"; + +export type ActorRequest = { id: RequestId; epoch: bigint; chainId: Caip2ChainId }; diff --git a/packages/automated-dispute/src/types/epochs.ts b/packages/automated-dispute/src/types/epochs.ts new file mode 100644 index 0000000..e6fc028 --- /dev/null +++ b/packages/automated-dispute/src/types/epochs.ts @@ -0,0 +1,14 @@ +import { Timestamp } from "@ebo-agent/shared"; + +/** + * Type representing an epoch's data. + * + * @property {bigint} epoch - epoch number + * @property {bigint} epochFirstBlockNumber - number of the first block of the epoch + * @property {Timestamp} epochStartTimestamp - timestamp of the first block of the epoch + */ +export type Epoch = { + number: bigint; + firstBlockNumber: bigint; + startTimestamp: Timestamp; +}; diff --git a/packages/automated-dispute/src/types/index.ts b/packages/automated-dispute/src/types/index.ts index 518c0ca..3139df0 100644 --- a/packages/automated-dispute/src/types/index.ts +++ b/packages/automated-dispute/src/types/index.ts @@ -1,3 +1,5 @@ +export * from "./actorRequest.js"; +export * from "./epochs.js"; export * from "./events.js"; export * from "../interfaces/protocolProvider.js"; export * from "./prophet.js"; diff --git a/packages/automated-dispute/tests/services/eboActor.spec.ts b/packages/automated-dispute/tests/services/eboActor.spec.ts index ec4af4e..6329837 100644 --- a/packages/automated-dispute/tests/services/eboActor.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor.spec.ts @@ -253,7 +253,9 @@ describe("EboActor", () => { vi.spyOn(registry, "getRequest").mockReturnValue(request); vi.spyOn(registry, "getResponses").mockReturnValue([]); - expect(actor.canBeTerminated(currentBlockNumber)).toBe(false); + expect(actor.canBeTerminated(actor.actorRequest.epoch + 1n, currentBlockNumber)).toBe( + false, + ); }); it("returns false if there's one disputable response", () => { @@ -266,7 +268,9 @@ describe("EboActor", () => { vi.spyOn(registry, "getResponses").mockReturnValue([response]); vi.spyOn(registry, "getResponseDispute").mockReturnValue(undefined); - expect(actor.canBeTerminated(currentBlockNumber)).toBe(false); + expect(actor.canBeTerminated(actor.actorRequest.epoch + 1n, currentBlockNumber)).toBe( + false, + ); }); it("returns false if the request is finalized but there's one active dispute", () => { @@ -286,11 +290,69 @@ describe("EboActor", () => { vi.spyOn(registry, "getResponses").mockReturnValue([response]); vi.spyOn(registry, "getResponseDispute").mockReturnValue(dispute); - const canBeTerminated = actor.canBeTerminated(currentBlockNumber); + const canBeTerminated = actor.canBeTerminated( + actor.actorRequest.epoch + 1n, + currentBlockNumber, + ); expect(canBeTerminated).toBe(false); }); + it("returns false if we are still in the same epoch", () => { + const request: Request = { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, + status: "Finalized", + }; + + const disputedResponse = mocks.buildResponse(request, { id: "0x01" }); + const undisputedResponse = mocks.buildResponse(request, { + id: "0x02", + createdAt: request.prophetData.responseModuleData.deadline - 1n, + }); + + const escalatedDispute = mocks.buildDispute(request, disputedResponse, { + status: "Escalated", + }); + + const { actor, registry } = mocks.buildEboActor(request, logger); + const currentBlockNumber = + undisputedResponse.createdAt + + request.prophetData.disputeModuleData.disputeWindow + + 1n; + + vi.spyOn(registry, "getRequest").mockReturnValue(request); + + vi.spyOn(registry, "getResponses").mockReturnValue([ + disputedResponse, + undisputedResponse, + ]); + + vi.spyOn(registry, "getResponseDispute").mockImplementation((response) => { + switch (response.id) { + case disputedResponse.id: + return escalatedDispute; + + case undisputedResponse.id: + return undefined; + } + }); + + const canBeTerminatedDuringCurrentEpoch = actor.canBeTerminated( + actor.actorRequest.epoch, + currentBlockNumber, + ); + + const canBeTerminatedDuringNextEpoch = actor.canBeTerminated( + actor.actorRequest.epoch + 1n, + currentBlockNumber, + ); + + expect(canBeTerminatedDuringCurrentEpoch).toBe(false); + // This is to validate that the change in the current epoch is the one that + // changes the output + expect(canBeTerminatedDuringNextEpoch).toBe(true); + }); + it("returns true once everything is settled", () => { const request: Request = { ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, @@ -330,7 +392,10 @@ describe("EboActor", () => { } }); - const canBeTerminated = actor.canBeTerminated(currentBlockNumber); + const canBeTerminated = actor.canBeTerminated( + actor.actorRequest.epoch + 1n, + currentBlockNumber, + ); expect(canBeTerminated).toBe(true); }); diff --git a/packages/automated-dispute/tests/services/eboProcessor.spec.ts b/packages/automated-dispute/tests/services/eboProcessor.spec.ts index 97a9f35..26559f8 100644 --- a/packages/automated-dispute/tests/services/eboProcessor.spec.ts +++ b/packages/automated-dispute/tests/services/eboProcessor.spec.ts @@ -94,12 +94,12 @@ describe("EboProcessor", () => { const { actor } = mocks.buildEboActor(request, logger); const currentEpoch = { - currentEpoch: 1n, - currentEpochBlockNumber: 1n, - currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), }; - const currentBlock = currentEpoch.currentEpochBlockNumber + 10n; + const currentBlock = currentEpoch.firstBlockNumber + 10n; const requestCreatedEvent: EboEvent<"RequestCreated"> = { name: "RequestCreated", @@ -126,10 +126,7 @@ describe("EboProcessor", () => { await processor.start(msBetweenChecks); - expect(mockGetEvents).toHaveBeenCalledWith( - currentEpoch.currentEpochBlockNumber, - currentBlock, - ); + expect(mockGetEvents).toHaveBeenCalledWith(currentEpoch.firstBlockNumber, currentBlock); }); it("keeps the last block checked unaltered when something fails during sync", async () => { @@ -139,9 +136,9 @@ describe("EboProcessor", () => { const { actor } = mocks.buildEboActor(request, logger); const currentEpoch = { - currentEpoch: 1n, - currentEpochBlockNumber: 1n, - currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), }; const mockProtocolProviderGetEvents = vi @@ -166,7 +163,7 @@ describe("EboProcessor", () => { expect(mockProtocolProviderGetEvents).toHaveBeenNthCalledWith( 1, - currentEpoch.currentEpochBlockNumber, + currentEpoch.firstBlockNumber, initialCurrentBlock + 10n, ); @@ -177,7 +174,7 @@ describe("EboProcessor", () => { expect(mockProtocolProviderGetEvents).toHaveBeenNthCalledWith( 2, - currentEpoch.currentEpochBlockNumber, + currentEpoch.firstBlockNumber, initialCurrentBlock + 20n, ); }); @@ -190,12 +187,12 @@ describe("EboProcessor", () => { processor["lastCheckedBlock"] = mockLastCheckedBlock; const currentEpoch = { - currentEpoch: 1n, - currentEpochBlockNumber: 1n, - currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), }; - const currentBlock = currentEpoch.currentEpochBlockNumber + 10n; + const currentBlock = currentEpoch.firstBlockNumber + 10n; const requestCreatedEvent: EboEvent<"RequestCreated"> = { name: "RequestCreated", @@ -231,12 +228,12 @@ describe("EboProcessor", () => { const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); const currentEpoch = { - currentEpoch: 1n, - currentEpochBlockNumber: 1n, - currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), }; - const currentBlock = currentEpoch.currentEpochBlockNumber + 10n; + const currentBlock = currentEpoch.firstBlockNumber + 10n; vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); @@ -294,12 +291,12 @@ describe("EboProcessor", () => { const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); const currentEpoch = { - currentEpoch: 1n, - currentEpochBlockNumber: 1n, - currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), }; - const currentBlock = currentEpoch.currentEpochBlockNumber + 10n; + const currentBlock = currentEpoch.firstBlockNumber + 10n; vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); @@ -381,6 +378,96 @@ describe("EboProcessor", () => { it.skip("notifies if an actor throws while handling events"); + it("creates a request when no actor is handling a chain's current epoch", async () => { + const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); + + const currentEpoch = { + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + }; + + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(1n); + vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([]); + vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ + "eip155:1", + "eip155:42161", + ]); + + vi.spyOn(actorsManager, "getActorsRequests").mockReturnValue([ + { id: "0x01", chainId: "eip155:1", epoch: currentEpoch.number }, + ]); + + const mockProtocolProviderCreateRequest = vi + .spyOn(protocolProvider, "createRequest") + .mockImplementation(() => Promise.resolve()); + + await processor.start(); + + expect(mockProtocolProviderCreateRequest).toHaveBeenCalledWith(currentEpoch.number, [ + "eip155:42161", + ]); + }); + + it("does not create a new request if a corresponding actor already exist", async () => { + const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); + + const currentEpoch = { + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + }; + + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(1n); + vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([]); + vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ + "eip155:1", + "eip155:42161", + ]); + + vi.spyOn(actorsManager, "getActorsRequests").mockReturnValue([ + { id: "0x01", chainId: "eip155:1", epoch: currentEpoch.number }, + { id: "0x02", chainId: "eip155:42161", epoch: currentEpoch.number }, + ]); + + const mockProtocolProviderCreateRequest = vi + .spyOn(protocolProvider, "createRequest") + .mockImplementation(() => Promise.resolve()); + + await processor.start(); + + expect(mockProtocolProviderCreateRequest).not.toHaveBeenCalled(); + }); + + it("handles errors during request creation", async () => { + const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); + + const currentEpoch = { + epoch: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + }; + + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(1n); + vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([]); + vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ + "eip155:1", + "eip155:42161", + ]); + vi.spyOn(protocolProvider, "createRequest").mockImplementation(() => Promise.reject()); + + vi.spyOn(actorsManager, "getActorsRequests").mockReturnValue([ + { id: "0x01", chainId: "eip155:1", epoch: currentEpoch.epoch }, + ]); + + expect(processor.start()).resolves.not.toThrow(); + }); + + it.skip("notifies if a request failed to be created"); + it("removes the actor from registry when terminating", async () => { const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor(logger); diff --git a/packages/automated-dispute/tests/services/protocolProvider.spec.ts b/packages/automated-dispute/tests/services/protocolProvider.spec.ts index 337a9f7..7d92d43 100644 --- a/packages/automated-dispute/tests/services/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/services/protocolProvider.spec.ts @@ -186,9 +186,9 @@ describe("ProtocolProvider", () => { const result = await protocolProvider.getCurrentEpoch(); - expect(result.currentEpoch).toBe(mockEpoch); - expect(result.currentEpochBlockNumber).toBe(mockEpochBlock); - expect(result.currentEpochTimestamp).toBe(mockEpochTimestamp); + expect(result.number).toBe(mockEpoch); + expect(result.firstBlockNumber).toBe(mockEpochBlock); + expect(result.startTimestamp).toBe(mockEpochTimestamp); }); it("throws when current epoch request fails", async () => { const protocolProvider = new ProtocolProvider(