From b67902ee18025bbfd32d38a730184d672df913b0 Mon Sep 17 00:00:00 2001 From: Beebs <47253537+jahabeebs@users.noreply.github.com> Date: Tue, 8 Oct 2024 09:07:30 -0500 Subject: [PATCH] feat: add foundation of error handling logic (#51) --- apps/agent/src/config/schemas.ts | 3 +- .../src/exceptions/chainNotAdded.exception.ts | 6 - .../src/exceptions/customContractError.ts | 67 ++++ .../src/exceptions/errorFactory.ts | 379 ++++++++++++++++++ .../src/exceptions/errorHandler.ts | 42 ++ .../automated-dispute/src/exceptions/index.ts | 7 +- .../src/exceptions/invalidEpoch.exception.ts | 6 - .../invalidRequestBody.exception.ts | 6 - .../exceptions/invalidRequester.exception.ts | 6 - .../unknownCustomError.exception.ts | 6 + packages/automated-dispute/src/guards.ts | 8 +- .../src/providers/protocolProvider.ts | 269 +++++-------- .../src/services/eboActor.ts | 235 +++++++---- .../src/services/eboProcessor.ts | 3 +- .../src/services/errorFactory.ts | 83 ---- .../automated-dispute/src/services/index.ts | 1 - .../src/types/actorRequest.ts | 2 +- .../automated-dispute/src/types/errorTypes.ts | 69 ++++ .../automated-dispute/src/types/events.ts | 2 +- packages/automated-dispute/src/types/index.ts | 1 + .../automated-dispute/src/types/prophet.ts | 4 +- .../tests/exceptions/errorFactory.spec.ts | 57 +++ .../tests/mocks/eboActor.mocks.ts | 16 +- .../tests/services/eboActor.spec.ts | 195 +++++++-- .../tests/services/eboActor/fixtures.ts | 28 +- .../eboActor/onLastBlockupdated.spec.ts | 36 +- .../eboActor/onRequestCreated.spec.ts | 51 +-- .../eboActor/onResponseDisputed.spec.ts | 12 +- .../eboActor/onResponseProposed.spec.ts | 77 +++- .../tests/services/errorFactory.spec.ts | 37 -- .../tests/services/protocolProvider.spec.ts | 4 +- 31 files changed, 1173 insertions(+), 545 deletions(-) delete mode 100644 packages/automated-dispute/src/exceptions/chainNotAdded.exception.ts create mode 100644 packages/automated-dispute/src/exceptions/customContractError.ts create mode 100644 packages/automated-dispute/src/exceptions/errorFactory.ts create mode 100644 packages/automated-dispute/src/exceptions/errorHandler.ts delete mode 100644 packages/automated-dispute/src/exceptions/invalidEpoch.exception.ts delete mode 100644 packages/automated-dispute/src/exceptions/invalidRequestBody.exception.ts delete mode 100644 packages/automated-dispute/src/exceptions/invalidRequester.exception.ts create mode 100644 packages/automated-dispute/src/exceptions/unknownCustomError.exception.ts create mode 100644 packages/automated-dispute/src/types/errorTypes.ts create mode 100644 packages/automated-dispute/tests/exceptions/errorFactory.spec.ts delete mode 100644 packages/automated-dispute/tests/services/errorFactory.spec.ts diff --git a/apps/agent/src/config/schemas.ts b/apps/agent/src/config/schemas.ts index 89089bf..6e5a896 100644 --- a/apps/agent/src/config/schemas.ts +++ b/apps/agent/src/config/schemas.ts @@ -1,5 +1,4 @@ -import { Caip2Utils } from "@ebo-agent/blocknumber"; -import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; +import { Caip2ChainId, Caip2Utils } from "@ebo-agent/blocknumber/src/index.js"; import { isAddress, isHex } from "viem"; import { z } from "zod"; diff --git a/packages/automated-dispute/src/exceptions/chainNotAdded.exception.ts b/packages/automated-dispute/src/exceptions/chainNotAdded.exception.ts deleted file mode 100644 index 2e5912d..0000000 --- a/packages/automated-dispute/src/exceptions/chainNotAdded.exception.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class EBORequestCreator_ChainNotAdded extends Error { - constructor() { - super("Chain not added"); - this.name = "EBORequestCreator_ChainNotAdded"; - } -} diff --git a/packages/automated-dispute/src/exceptions/customContractError.ts b/packages/automated-dispute/src/exceptions/customContractError.ts new file mode 100644 index 0000000..9e8269b --- /dev/null +++ b/packages/automated-dispute/src/exceptions/customContractError.ts @@ -0,0 +1,67 @@ +import { + EboEvent, + EboEventName, + ErrorContext, + ErrorHandlingStrategy, + ErrorName, +} from "../types/index.js"; + +export class CustomContractError extends Error { + public override name: ErrorName; + public strategy: ErrorHandlingStrategy; + public context!: ErrorContext; + private customActions: Map Promise | void> = + new Map(); + + constructor(name: ErrorName, strategy: ErrorHandlingStrategy) { + super(`Contract reverted: ${name}`); + this.name = name; + this.strategy = strategy; + } + + public setContext(context: ErrorContext): this { + this.context = context; + return this; + } + + public getContext(): ErrorContext { + return this.context; + } + + /** + * Sets the context specific to processEvents. + * This method replaces addContext for processEvents-related context updates. + * + * @param event The event being processed. + * @param reenqueueEvent Callback to reenqueue the event. + * @param terminateActor Callback to terminate the actor. + * @returns The error instance. + */ + public setProcessEventsContext( + event: EboEvent, + reenqueueEvent: () => void, + terminateActor: () => void, + ): this { + this.context = { + ...this.context, + event, + reenqueueEvent, + terminateActor, + }; + return this; + } + + public on(errorName: string, action: (context: ErrorContext) => Promise | void): this { + if (this.name === errorName) { + this.customActions.set(errorName, action); + } + return this; + } + + public async executeCustomAction(): Promise { + const action = this.customActions.get(this.name) || this.strategy.customAction; + if (action) { + await action(this.context); + } + } +} diff --git a/packages/automated-dispute/src/exceptions/errorFactory.ts b/packages/automated-dispute/src/exceptions/errorFactory.ts new file mode 100644 index 0000000..e67d51d --- /dev/null +++ b/packages/automated-dispute/src/exceptions/errorFactory.ts @@ -0,0 +1,379 @@ +import { CustomContractError } from "../exceptions/index.js"; +import { isDispute } from "../guards.js"; +import { ErrorContext, ErrorHandlingStrategy, ErrorName } from "../types/index.js"; + +const errorStrategiesEntries: [ErrorName, ErrorHandlingStrategy][] = [ + [ + "ValidatorLib_InvalidResponseBody", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationAccounting_InsufficientFunds", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + }, + ], + [ + "BondEscalationAccounting_AlreadySettled", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_InvalidDispute", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + customAction: async (context: ErrorContext) => { + if (isDispute(context.dispute)) { + context.registry.removeDispute(context.dispute.id); + } + }, + }, + ], + [ + "AccountingExtension_InsufficientFunds", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + }, + ], + [ + "Oracle_InvalidDisputeId", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + customAction: async (context: ErrorContext) => { + if (isDispute(context.dispute)) { + context.registry.removeDispute(context.dispute.id); + } + }, + }, + ], + [ + "Oracle_InvalidDispute", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + customAction: async (context: ErrorContext) => { + if (isDispute(context.dispute)) { + context.registry.removeDispute(context.dispute.id); + } + }, + }, + ], + [ + "Oracle_InvalidDisputeBody", + { + shouldNotify: true, + shouldTerminate: true, + shouldReenqueue: false, + }, + ], + [ + "Oracle_AlreadyFinalized", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "EBORequestModule_InvalidRequester", + { + shouldNotify: true, + shouldTerminate: true, + shouldReenqueue: false, + }, + ], + [ + "EBORequestModule_ChainNotAdded", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + }, + ], + [ + "EBORequestCreator_InvalidEpoch", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "Oracle_InvalidRequestBody", + { + shouldNotify: true, + shouldTerminate: true, + shouldReenqueue: false, + }, + ], + [ + "EBORequestCreator_RequestAlreadyCreated", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "Oracle_InvalidRequest", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + }, + ], + [ + "Oracle_InvalidResponseBody", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "AccountingExtension_NotAllowed", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + }, + ], + [ + "BondedResponseModule_AlreadyResponded", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondedResponseModule_TooLateToPropose", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "Oracle_CannotEscalate", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "Validator_InvalidDispute", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + customAction: async (context: ErrorContext) => { + if (isDispute(context.dispute)) { + context.registry.removeDispute(context.dispute.id); + } + }, + }, + ], + [ + "ValidatorLib_InvalidDisputeBody", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + customAction: async (context: ErrorContext) => { + if (isDispute(context.dispute)) { + context.registry.removeDispute(context.dispute.id); + } + }, + }, + ], + [ + "EBOFinalityModule_InvalidRequester", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + }, + ], + [ + "Oracle_InvalidFinalizedResponse", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "Oracle_InvalidResponse", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + customAction: async (context: ErrorContext) => { + if (context.response) { + context.registry.removeResponse(context.response.id); + } + }, + }, + ], + [ + "Oracle_FinalizableResponseExists", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "ArbitratorModule_InvalidArbitrator", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + customAction: async (context: ErrorContext) => { + if (isDispute(context.dispute)) { + context.registry.removeDispute(context.dispute.id); + } + }, + }, + ], + [ + "AccountingExtension_UnauthorizedModule", + { + shouldNotify: true, + shouldTerminate: true, + shouldReenqueue: false, + }, + ], + [ + "BondEscalationModule_NotEscalatable", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_BondEscalationNotOver", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_BondEscalationOver", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_DisputeWindowOver", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "Oracle_ResponseAlreadyDisputed", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_CannotBreakTieDuringTyingBuffer", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_CanOnlySurpassByOnePledge", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_MaxNumberOfEscalationsReached", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "Oracle_NotDisputeOrResolutionModule", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + }, + ], + [ + "BondEscalationModule_ShouldBeEscalated", + { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: false, + // Custom action to escalate dispute is implemented in eboActor.ts + }, + ], + [ + "BondEscalationModule_BondEscalationCantBeSettled", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], + [ + "BondEscalationModule_BondEscalationNotOver", + { + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }, + ], +]; + +const errorStrategies = new Map(errorStrategiesEntries); + +export class ErrorFactory { + public static createError(errorName: ErrorName | string): CustomContractError { + if (!errorStrategies.has(errorName as ErrorName)) { + return new CustomContractError("UnknownError", { + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + }); + } + const strategy = errorStrategies.get(errorName as ErrorName)!; + + return new CustomContractError(errorName as ErrorName, strategy); + } +} diff --git a/packages/automated-dispute/src/exceptions/errorHandler.ts b/packages/automated-dispute/src/exceptions/errorHandler.ts new file mode 100644 index 0000000..68f4b9c --- /dev/null +++ b/packages/automated-dispute/src/exceptions/errorHandler.ts @@ -0,0 +1,42 @@ +import { Logger } from "@ebo-agent/shared"; + +import { CustomContractError } from "../exceptions/index.js"; +import { ErrorContext } from "../types/index.js"; + +export class ErrorHandler { + private static logger = Logger.getInstance(); + + public static async handle(error: CustomContractError): Promise { + const strategy = error.strategy; + const context = error.getContext(); + + this.logger.error(`Error occurred: ${error.message}`); + + if (strategy.shouldNotify) { + await this.notifyError(error, context); + } + + try { + await error.executeCustomAction(); + } catch (actionError) { + this.logger.error(`Error executing custom action: ${actionError}`); + // Continue without rethrowing + } + + if (strategy.shouldReenqueue && context.reenqueueEvent) { + context.reenqueueEvent(); + } + + if (strategy.shouldTerminate && context.terminateActor) { + context.terminateActor(); + } + } + + private static async notifyError( + error: CustomContractError, + context: ErrorContext, + ): Promise { + // TODO: notification logic + console.log(error, context); + } +} diff --git a/packages/automated-dispute/src/exceptions/index.ts b/packages/automated-dispute/src/exceptions/index.ts index 1785170..b7906c1 100644 --- a/packages/automated-dispute/src/exceptions/index.ts +++ b/packages/automated-dispute/src/exceptions/index.ts @@ -8,9 +8,8 @@ export * from "./requestAlreadyHandled.exception.js"; export * from "./requestMismatch.exception.js"; export * from "./responseAlreadyProposed.exception.js"; export * from "./rpcUrlsEmpty.exception.js"; -export * from "./chainNotAdded.exception.js"; -export * from "./invalidEpoch.exception.js"; -export * from "./invalidRequestBody.exception.js"; -export * from "./invalidRequester.exception.js"; export * from "./transactionExecutionError.exception.js"; export * from "./invalidAccountOnClient.exception.js"; +export * from "./unknownCustomError.exception.js"; +export * from "./customContractError.js"; +export * from "./errorFactory.js"; diff --git a/packages/automated-dispute/src/exceptions/invalidEpoch.exception.ts b/packages/automated-dispute/src/exceptions/invalidEpoch.exception.ts deleted file mode 100644 index f5dd1d1..0000000 --- a/packages/automated-dispute/src/exceptions/invalidEpoch.exception.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class EBORequestCreator_InvalidEpoch extends Error { - constructor() { - super("Invalid epoch"); - this.name = "EBORequestCreator_InvalidEpoch"; - } -} diff --git a/packages/automated-dispute/src/exceptions/invalidRequestBody.exception.ts b/packages/automated-dispute/src/exceptions/invalidRequestBody.exception.ts deleted file mode 100644 index 969e261..0000000 --- a/packages/automated-dispute/src/exceptions/invalidRequestBody.exception.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class Oracle_InvalidRequestBody extends Error { - constructor() { - super("Invalid request body"); - this.name = "Oracle_InvalidRequestBody"; - } -} diff --git a/packages/automated-dispute/src/exceptions/invalidRequester.exception.ts b/packages/automated-dispute/src/exceptions/invalidRequester.exception.ts deleted file mode 100644 index fdf831b..0000000 --- a/packages/automated-dispute/src/exceptions/invalidRequester.exception.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class EBORequestModule_InvalidRequester extends Error { - constructor() { - super("Invalid requester"); - this.name = "EBORequestModule_InvalidRequester"; - } -} diff --git a/packages/automated-dispute/src/exceptions/unknownCustomError.exception.ts b/packages/automated-dispute/src/exceptions/unknownCustomError.exception.ts new file mode 100644 index 0000000..22bb14c --- /dev/null +++ b/packages/automated-dispute/src/exceptions/unknownCustomError.exception.ts @@ -0,0 +1,6 @@ +export class UnknownCustomError extends Error { + constructor(errorName?: string) { + super(`Unknown custom error: ${errorName}`); + this.name = "UnknownCustomError"; + } +} diff --git a/packages/automated-dispute/src/guards.ts b/packages/automated-dispute/src/guards.ts index 6a72adb..4ad9210 100644 --- a/packages/automated-dispute/src/guards.ts +++ b/packages/automated-dispute/src/guards.ts @@ -1,7 +1,13 @@ -import { EboEvent, EboEventName } from "./types/index.js"; +import { Dispute, EboEvent, EboEventName } from "./types/index.js"; export const isRequestCreatedEvent = ( event: EboEvent, ): event is EboEvent<"RequestCreated"> => { return event.name === "RequestCreated"; }; + +export function isDispute( + dispute: Dispute | Dispute["prophetData"] | undefined, +): dispute is Dispute { + return !!dispute && "id" in dispute; +} diff --git a/packages/automated-dispute/src/providers/protocolProvider.ts b/packages/automated-dispute/src/providers/protocolProvider.ts index f8c2022..b3659d0 100644 --- a/packages/automated-dispute/src/providers/protocolProvider.ts +++ b/packages/automated-dispute/src/providers/protocolProvider.ts @@ -1,4 +1,4 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; import { Address, BaseError, @@ -39,6 +39,7 @@ import { oracleAbi, } from "../abis/index.js"; import { + ErrorFactory, InvalidAccountOnClient, RpcUrlsEmpty, TransactionExecutionError, @@ -49,7 +50,6 @@ import { IWriteProvider, ProtocolContractsAddresses, } from "../interfaces/index.js"; -import { ErrorFactory } from "../services/errorFactory.js"; type ProtocolRpcConfig = { urls: string[]; @@ -109,7 +109,7 @@ export class ProtocolProvider implements IProtocolProvider { /** * Creates a new ProtocolProvider instance - * @param rpcUrls The RPC URLs to connect to the Arbitrum chain + * @param rpcConfig The configuration for RPC connections including URLs, timeout, retry interval, and transaction receipt confirmations * @param contracts The addresses of the protocol contracts that will be instantiated * @param privateKey The private key of the account that will be used to interact with the contracts */ @@ -215,6 +215,7 @@ export class ProtocolProvider implements IProtocolProvider { * Returns the address of the account used for transactions. * * @returns {Address} The account address. + * @throws {InvalidAccountOnClient} Throws if the write client does not have an assigned account. */ public getAccountAddress(): Address { if (!this.writeClient.account) { @@ -224,9 +225,9 @@ export class ProtocolProvider implements IProtocolProvider { } /** - * Gets the current epoch, the block number and its timestamp of the current epoch + * Gets the current epoch, including the block number and its timestamp. * - * @returns The current epoch, its block number and its timestamp + * @returns {Promise} The current epoch, its block number, and its timestamp. */ async getCurrentEpoch(): Promise { const [epoch, epochFirstBlockNumber] = await Promise.all([ @@ -245,12 +246,24 @@ export class ProtocolProvider implements IProtocolProvider { }; } + /** + * Gets the number of the last finalized block. + * + * @returns {Promise} The block number of the last finalized block. + */ async getLastFinalizedBlock(): Promise { const { number } = await this.readClient.getBlock({ blockTag: "finalized" }); return number; } + /** + * Gets a list of events between two blocks. + * + * @param {bigint} _fromBlock - The starting block number. + * @param {bigint} _toBlock - The ending block number. + * @returns {Promise[]>} A list of EBO events. + */ async getEvents(_fromBlock: bigint, _toBlock: bigint): Promise[]> { // TODO: implement actual method. // @@ -285,9 +298,8 @@ export class ProtocolProvider implements IProtocolProvider { * Merge multiple streams of events considering the chain order, based on their block numbers * and log indexes. * - * @param streams a collection of EboEvent[] arrays. - * @returns the EboEvent[] arrays merged in a single array and sorted by ascending blockNumber - * and logIndex + * @param {EboEvent[][]} streams - A collection of EboEvent[] arrays. + * @returns {EboEvent[]} The merged and sorted event array. */ private mergeEventStreams(...streams: EboEvent[][]) { return streams @@ -317,7 +329,7 @@ export class ProtocolProvider implements IProtocolProvider { /** * Approves a module in the accounting extension contract. * - * @param module The address of the module to approve. + * @param {Address} module - The address of the module to approve. * @throws {TransactionExecutionError} Throws if the transaction fails during execution. * @returns {Promise} A promise that resolves when the module is approved. */ @@ -345,8 +357,8 @@ export class ProtocolProvider implements IProtocolProvider { /** * Gets the list of approved modules' addresses for a given user. * - * @param user The address of the user. - * @returns A promise that resolves with an array of approved modules for the user. + * @param {Address} user - The address of the user. + * @returns {Promise} A promise that resolves with an array of approved modules for the user. */ async getApprovedModules(user: Address): Promise { return [...(await this.horizonAccountingExtensionContract.read.approvedModules([user]))]; @@ -362,11 +374,10 @@ export class ProtocolProvider implements IProtocolProvider { } /** - * Decodes the Prophet's request responseModuleData bytes into an object. + * Decodes the request's response module data bytes into an object. * - * @param responseModuleData responseModuleData bytes - * @throws {BaseErrorType} when the responseModuleData decoding fails - * @returns a decoded object with responseModuleData properties + * @param {Request["prophetData"]["responseModuleData"]} responseModuleData - The response module data bytes. + * @returns {Request["decodedData"]["responseModuleData"]} A decoded object with responseModuleData properties. */ static decodeRequestResponseModuleData( responseModuleData: Request["prophetData"]["responseModuleData"], @@ -386,11 +397,10 @@ export class ProtocolProvider implements IProtocolProvider { } /** - * Decodes the Prophet's request disputeModuelData bytes into an object. + * Decodes the request's dispute module data bytes into an object. * - * @param disputeModuelData disputeModuelData bytes - * @throws {BaseErrorType} when the disputeModuelData decoding fails - * @returns a decoded object with disputeModuelData properties + * @param {Request["prophetData"]["disputeModuleData"]} disputeModuleData - The dispute module data bytes. + * @returns {Request["decodedData"]["disputeModuleData"]} A decoded object with disputeModuleData properties. */ static decodeRequestDisputeModuleData( disputeModuleData: Request["prophetData"]["disputeModuleData"], @@ -412,10 +422,10 @@ export class ProtocolProvider implements IProtocolProvider { } /** - * Encodes a Prophet's response body object into bytes. + * Encodes a response object into bytes. * - * @param response response body object - * @returns byte-encode response body + * @param {Response["decodedData"]["response"]} response - The response object to encode. + * @returns {Response["prophetData"]["response"]} Byte-encoded response body. */ static encodeResponse( response: Response["decodedData"]["response"], @@ -424,10 +434,10 @@ export class ProtocolProvider implements IProtocolProvider { } /** - * Decodes a Prophet's response body bytes into an object. + * Decodes a response body bytes into an object. * - * @param response response body bytes - * @returns decoded response body object + * @param {Response["prophetData"]["response"]} response - The response body bytes. + * @returns {Response["decodedData"]["response"]} Decoded response body object. */ static decodeResponse( response: Response["prophetData"]["response"], @@ -447,43 +457,26 @@ export class ProtocolProvider implements IProtocolProvider { * @param {bigint} epoch - The epoch for which the request is being created. * @param {Caip2ChainId} chain - A chain identifier for which the request should be created. * @throws {Error} Throws an error if the chains array is empty or if the transaction fails. - * @throws {EBORequestCreator_InvalidEpoch} Throws if the epoch is invalid. - * @throws {Oracle_InvalidRequestBody} Throws if the request body is invalid. - * @throws {EBORequestModule_InvalidRequester} Throws if the requester is invalid. - * @throws {EBORequestCreator_ChainNotAdded} Throws if the specified chain is not added. * @returns {Promise} A promise that resolves when the request is successfully created. */ async createRequest(epoch: bigint, chain: Caip2ChainId): Promise { - try { - const { request } = await this.readClient.simulateContract({ - address: this.eboRequestCreatorContract.address, - abi: eboRequestCreatorAbi, - functionName: "createRequest", - args: [epoch, chain], - account: this.writeClient.account, - }); + const { request } = await this.readClient.simulateContract({ + address: this.eboRequestCreatorContract.address, + abi: eboRequestCreatorAbi, + functionName: "createRequest", + args: [epoch, chain], + account: this.writeClient.account, + }); - const hash = await this.writeClient.writeContract(request); + const hash = await this.writeClient.writeContract(request); - const receipt = await this.readClient.waitForTransactionReceipt({ - hash, - confirmations: this.rpcConfig.transactionReceiptConfirmations, - }); + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: this.rpcConfig.transactionReceiptConfirmations, + }); - if (receipt.status !== "success") { - throw new TransactionExecutionError("createRequest transaction failed"); - } - } catch (error) { - if (error instanceof BaseError) { - const revertError = error.walk( - (err) => err instanceof ContractFunctionRevertedError, - ); - if (revertError instanceof ContractFunctionRevertedError) { - const errorName = revertError.data?.errorName ?? ""; - throw ErrorFactory.createError(errorName); - } - } - throw error; + if (receipt.status !== "success") { + throw new TransactionExecutionError("createRequest transaction failed"); } } @@ -500,36 +493,23 @@ export class ProtocolProvider implements IProtocolProvider { request: Request["prophetData"], response: Response["prophetData"], ): Promise { - try { - const { request: simulatedRequest } = await this.readClient.simulateContract({ - address: this.oracleContract.address, - abi: oracleAbi, - functionName: "proposeResponse", - args: [request, response], - account: this.writeClient.account, - }); + const { request: simulatedRequest } = await this.readClient.simulateContract({ + address: this.oracleContract.address, + abi: oracleAbi, + functionName: "proposeResponse", + args: [request, response], + account: this.writeClient.account, + }); - const hash = await this.writeClient.writeContract(simulatedRequest); + const hash = await this.writeClient.writeContract(simulatedRequest); - const receipt = await this.readClient.waitForTransactionReceipt({ - hash, - confirmations: this.rpcConfig.transactionReceiptConfirmations, - }); + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: this.rpcConfig.transactionReceiptConfirmations, + }); - if (receipt.status !== "success") { - throw new TransactionExecutionError("proposeResponse transaction failed"); - } - } catch (error) { - if (error instanceof BaseError) { - const revertError = error.walk( - (err) => err instanceof ContractFunctionRevertedError, - ); - if (revertError instanceof ContractFunctionRevertedError) { - const errorName = revertError.data?.errorName ?? ""; - throw ErrorFactory.createError(errorName); - } - } - throw error; + if (receipt.status !== "success") { + throw new TransactionExecutionError("proposeResponse transaction failed"); } } @@ -548,36 +528,23 @@ export class ProtocolProvider implements IProtocolProvider { response: Response["prophetData"], dispute: Dispute["prophetData"], ): Promise { - try { - const { request: simulatedRequest } = await this.readClient.simulateContract({ - address: this.oracleContract.address, - abi: oracleAbi, - functionName: "disputeResponse", - args: [request, response, dispute], - account: this.writeClient.account, - }); + const { request: simulatedRequest } = await this.readClient.simulateContract({ + address: this.oracleContract.address, + abi: oracleAbi, + functionName: "disputeResponse", + args: [request, response, dispute], + account: this.writeClient.account, + }); - const hash = await this.writeClient.writeContract(simulatedRequest); + const hash = await this.writeClient.writeContract(simulatedRequest); - const receipt = await this.readClient.waitForTransactionReceipt({ - hash, - confirmations: this.rpcConfig.transactionReceiptConfirmations, - }); + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: this.rpcConfig.transactionReceiptConfirmations, + }); - if (receipt.status !== "success") { - throw new TransactionExecutionError("disputeResponse transaction failed"); - } - } catch (error) { - if (error instanceof BaseError) { - const revertError = error.walk( - (err) => err instanceof ContractFunctionRevertedError, - ); - if (revertError instanceof ContractFunctionRevertedError) { - const errorName = revertError.data?.errorName ?? ""; - throw ErrorFactory.createError(errorName); - } - } - throw error; + if (receipt.status !== "success") { + throw new TransactionExecutionError("disputeResponse transaction failed"); } } @@ -741,36 +708,23 @@ export class ProtocolProvider implements IProtocolProvider { response: Response["prophetData"], dispute: Dispute["prophetData"], ): Promise { - try { - const { request: simulatedRequest } = await this.readClient.simulateContract({ - address: this.oracleContract.address, - abi: oracleAbi, - functionName: "escalateDispute", - args: [request, response, dispute], - account: this.writeClient.account, - }); + const { request: simulatedRequest } = await this.readClient.simulateContract({ + address: this.oracleContract.address, + abi: oracleAbi, + functionName: "escalateDispute", + args: [request, response, dispute], + account: this.writeClient.account, + }); - const hash = await this.writeClient.writeContract(simulatedRequest); + const hash = await this.writeClient.writeContract(simulatedRequest); - const receipt = await this.readClient.waitForTransactionReceipt({ - hash, - confirmations: this.rpcConfig.transactionReceiptConfirmations, - }); + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: this.rpcConfig.transactionReceiptConfirmations, + }); - if (receipt.status !== "success") { - throw new TransactionExecutionError("escalateDispute transaction failed"); - } - } catch (error) { - if (error instanceof BaseError) { - const revertError = error.walk( - (err) => err instanceof ContractFunctionRevertedError, - ); - if (revertError instanceof ContractFunctionRevertedError) { - const errorName = revertError.data?.errorName ?? ""; - throw ErrorFactory.createError(errorName); - } - } - throw error; + if (receipt.status !== "success") { + throw new TransactionExecutionError("escalateDispute transaction failed"); } } @@ -793,36 +747,23 @@ export class ProtocolProvider implements IProtocolProvider { request: Request["prophetData"], response: Response["prophetData"], ): Promise { - try { - const { request: simulatedRequest } = await this.readClient.simulateContract({ - address: this.oracleContract.address, - abi: oracleAbi, - functionName: "finalize", - args: [request, response], - account: this.writeClient.account, - }); + const { request: simulatedRequest } = await this.readClient.simulateContract({ + address: this.oracleContract.address, + abi: oracleAbi, + functionName: "finalize", + args: [request, response], + account: this.writeClient.account, + }); - const hash = await this.writeClient.writeContract(simulatedRequest); + const hash = await this.writeClient.writeContract(simulatedRequest); - const receipt = await this.readClient.waitForTransactionReceipt({ - hash, - confirmations: this.rpcConfig.transactionReceiptConfirmations, - }); + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: this.rpcConfig.transactionReceiptConfirmations, + }); - if (receipt.status !== "success") { - throw new TransactionExecutionError("finalize transaction failed"); - } - } catch (error) { - if (error instanceof BaseError) { - const revertError = error.walk( - (err) => err instanceof ContractFunctionRevertedError, - ); - if (revertError instanceof ContractFunctionRevertedError) { - const errorName = revertError.data?.errorName ?? ""; - throw ErrorFactory.createError(errorName); - } - } - throw error; + if (receipt.status !== "success") { + throw new TransactionExecutionError("finalize transaction failed"); } } } diff --git a/packages/automated-dispute/src/services/eboActor.ts b/packages/automated-dispute/src/services/eboActor.ts index f5a4d3f..80c065d 100644 --- a/packages/automated-dispute/src/services/eboActor.ts +++ b/packages/automated-dispute/src/services/eboActor.ts @@ -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"; @@ -10,19 +11,19 @@ import type { EboEvent, EboEventName, Epoch, + ErrorContext, Request, Response, ResponseBody, ResponseId, } from "../types/index.js"; +import { ErrorHandler } from "../exceptions/errorHandler.js"; import { + CustomContractError, DisputeWithoutResponse, - EBORequestCreator_ChainNotAdded, - EBORequestCreator_InvalidEpoch, - EBORequestModule_InvalidRequester, + ErrorFactory, InvalidActorState, InvalidDisputeStatus, - Oracle_InvalidRequestBody, PastEventEnqueueError, RequestMismatch, ResponseAlreadyProposed, @@ -156,13 +157,28 @@ export class EboActor { } catch (err) { this.logger.error(`Error processing event ${event.name}: ${err}`); - // Enqueue the event again as it's supposed to be reprocessed - this.eventsQueue.push(event); - - // Undo last state update - updateStateCommand.undo(); - - return; + if (err instanceof CustomContractError) { + err.setProcessEventsContext( + event, + () => { + this.eventsQueue.push(event!); + updateStateCommand.undo(); + }, + () => { + throw err; + }, + ); + + await ErrorHandler.handle(err); + + if (err.strategy.shouldNotify) { + // TODO: add notification logic + continue; + } + return; + } else { + throw err; + } } } }); @@ -309,7 +325,7 @@ export class EboActor { if (!response) { this.logger.error( - `While trying to settle dispute ${dispute.id} its response with` + + `While trying to settle dispute ${dispute.id}, its response with ` + `id ${dispute.prophetData.responseId} was not found in the registry.`, ); @@ -348,50 +364,58 @@ export class EboActor { * @param response the dispute's response * @param dispute the dispute */ - private settleDispute(request: Request, response: Response, dispute: Dispute): Promise { - return Promise.resolve() - .then(async () => { - this.logger.info(`Settling dispute ${dispute.id}...`); - - // OPTIMIZE: check for pledges to potentially save the ShouldBeEscalated error - - await this.protocolProvider.settleDispute( - request.prophetData, - response.prophetData, - dispute.prophetData, - ); - - this.logger.info(`Dispute ${dispute.id} settled.`); - }) - .catch(async (err) => { - this.logger.warn(`Dispute ${dispute.id} was not settled.`); - - // TODO: use custom errors to be developed while implementing ProtocolProvider - if (!(err instanceof ContractFunctionRevertedError)) throw err; - - this.logger.warn(`Call reverted for ${dispute.id} due to: ${err.data?.errorName}`); - - if (err.data?.errorName === "BondEscalationModule_ShouldBeEscalated") { - this.logger.warn(`Escalating dispute ${dispute.id}...`); - - await this.protocolProvider.escalateDispute( - request.prophetData, - response.prophetData, - dispute.prophetData, - ); - - // TODO: notify + private async settleDispute( + request: Request, + response: Response, + dispute: Dispute, + ): Promise { + try { + this.logger.info(`Settling dispute ${dispute.id}...`); - this.logger.warn(`Dispute ${dispute.id} was escalated.`); - } - }) - .catch((err) => { - this.logger.error(`Failed to escalate dispute ${dispute.id}.`); + // OPTIMIZE: check for pledges to potentially save the ShouldBeEscalated error - // TODO: notify + await this.protocolProvider.settleDispute( + request.prophetData, + response.prophetData, + dispute.prophetData, + ); + this.logger.info(`Dispute ${dispute.id} settled.`); + } catch (err) { + if (err instanceof ContractFunctionRevertedError) { + const errorName = err.data?.errorName || err.name; + this.logger.warn(`Call reverted for dispute ${dispute.id} due to: ${errorName}`); + + const customError = ErrorFactory.createError(errorName); + customError.setContext({ + request, + response, + dispute, + registry: this.registry, + }); + + customError.on("BondEscalationModule_ShouldBeEscalated", async () => { + try { + await this.protocolProvider.escalateDispute( + request.prophetData, + response.prophetData, + dispute.prophetData, + ); + this.logger.info(`Dispute ${dispute.id} escalated.`); + + await ErrorHandler.handle(customError); + } catch (escalationError) { + this.logger.error( + `Failed to escalate dispute ${dispute.id}: ${escalationError}`, + ); + throw escalationError; + } + }); + } else { + this.logger.error(`Failed to escalate dispute ${dispute.id}: ${err}`); throw err; - }); + } + } } /** @@ -487,15 +511,17 @@ export class EboActor { try { await this.proposeResponse(chainId); } catch (err) { - if (err instanceof ResponseAlreadyProposed) this.logger.info(err.message); - else if (err instanceof EBORequestCreator_InvalidEpoch) { - // TODO: Handle error - } else if (err instanceof Oracle_InvalidRequestBody) { - // TODO: Handle error - } else if (err instanceof EBORequestModule_InvalidRequester) { - // TODO: Handle error - } else if (err instanceof EBORequestCreator_ChainNotAdded) { - // TODO: Handle error + if (err instanceof ContractFunctionRevertedError) { + const request = this.getActorRequest(); + const customError = ErrorFactory.createError(err.name); + + customError.setContext({ + request, + event, + registry: this.registry, + }); + + throw customError; } else { throw err; } @@ -586,6 +612,15 @@ export class EboActor { await this.protocolProvider.proposeResponse(request.prophetData, response); } catch (err) { if (err instanceof ContractFunctionRevertedError) { + const customError = ErrorFactory.createError(err.name); + const context: ErrorContext = { + request, + registry: this.registry, + }; + customError.setContext(context); + + await ErrorHandler.handle(customError); + this.logger.warn( `Block ${responseBody.block} for epoch ${request.epoch} and ` + `chain ${chainId} was not proposed. Skipping proposal...`, @@ -634,12 +669,29 @@ export class EboActor { responseId: Address.normalize(event.metadata.responseId) as ResponseId, requestId: request.id, }; + try { + await this.protocolProvider.disputeResponse( + request.prophetData, + proposedResponse.prophetData, + dispute, + ); + } catch (err) { + if (err instanceof ContractFunctionRevertedError) { + const customError = ErrorFactory.createError(err.name); + const response = this.registry.getResponse(event.metadata.responseId); - await this.protocolProvider.disputeResponse( - request.prophetData, - proposedResponse.prophetData, - dispute, - ); + customError.setContext({ + request, + response, + event, + registry: this.registry, + }); + + throw customError; + } else { + throw err; + } + } } /** @@ -736,24 +788,31 @@ export class EboActor { */ private async pledgeFor(request: Request, dispute: Dispute) { try { - this.logger.info(`Pledging against dispute ${dispute.id}`); + this.logger.info(`Pledging for dispute ${dispute.id}`); await this.protocolProvider.pledgeForDispute(request.prophetData, dispute.prophetData); } catch (err) { if (err instanceof ContractFunctionRevertedError) { - // TODO: handle each error appropriately - this.logger.warn(`Pledging for dispute ${dispute.id} was reverted. Skipping...`); + const errorName = err.data?.errorName || err.name; + this.logger.warn(`Pledge for dispute ${dispute.id} reverted due to: ${errorName}`); + + const customError = ErrorFactory.createError(errorName); + const context: ErrorContext = { + request, + dispute, + registry: this.registry, + terminateActor: () => { + throw customError; + }, + }; + customError.setContext(context); + + await ErrorHandler.handle(customError); } else { - // TODO: handle each error appropriately - this.logger.error( - `Actor handling request ${this.actorRequest.id} is not able to continue.`, - ); - throw err; } } } - /** * Pledge against the dispute. * @@ -762,7 +821,7 @@ export class EboActor { */ private async pledgeAgainst(request: Request, dispute: Dispute) { try { - this.logger.info(`Pledging for dispute ${dispute.id}`); + this.logger.info(`Pledging against dispute ${dispute.id}`); await this.protocolProvider.pledgeAgainstDispute( request.prophetData, @@ -770,14 +829,24 @@ export class EboActor { ); } catch (err) { if (err instanceof ContractFunctionRevertedError) { - // TODO: handle each error appropriately - this.logger.warn(`Pledging on dispute ${dispute.id} was reverted. Skipping...`); - } else { - // TODO: handle each error appropriately - this.logger.error( - `Actor handling request ${this.actorRequest.id} is not able to continue.`, + const errorName = err.data?.errorName || err.name; + this.logger.warn( + `Pledge against dispute ${dispute.id} reverted due to: ${errorName}`, ); + const customError = ErrorFactory.createError(errorName); + const context: ErrorContext = { + request, + dispute, + registry: this.registry, + terminateActor: () => { + throw customError; + }, + }; + customError.setContext(context); + + await ErrorHandler.handle(customError); + } else { throw err; } } diff --git a/packages/automated-dispute/src/services/eboProcessor.ts b/packages/automated-dispute/src/services/eboProcessor.ts index 2f0dfe1..13b8944 100644 --- a/packages/automated-dispute/src/services/eboProcessor.ts +++ b/packages/automated-dispute/src/services/eboProcessor.ts @@ -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"; diff --git a/packages/automated-dispute/src/services/errorFactory.ts b/packages/automated-dispute/src/services/errorFactory.ts index 3481af0..e69de29 100644 --- a/packages/automated-dispute/src/services/errorFactory.ts +++ b/packages/automated-dispute/src/services/errorFactory.ts @@ -1,83 +0,0 @@ -import { EBORequestCreator_ChainNotAdded } from "../exceptions/chainNotAdded.exception.js"; -import { EBORequestCreator_InvalidEpoch } from "../exceptions/invalidEpoch.exception.js"; -import { Oracle_InvalidRequestBody } from "../exceptions/invalidRequestBody.exception.js"; -import { EBORequestModule_InvalidRequester } from "../exceptions/invalidRequester.exception.js"; - -/** - * A factory class for creating specific error instances based on the provided error name. - */ -export class ErrorFactory { - /** - * Creates an instance of a specific error class based on the provided error name. - * - * @param {string} errorName - The name of the error to create. - * @returns {Error} An instance of the corresponding error class. - * @throws {Error} If the provided error name is unknown. - */ - public static createError(errorName: string): Error { - // TODO: need to define structure of each error - // TODO: Need to define some base contract reverted error to distinguish from other errors - switch (errorName) { - case "EBORequestCreator_InvalidEpoch": - return new EBORequestCreator_InvalidEpoch(); - case "Oracle_InvalidRequestBody": - return new Oracle_InvalidRequestBody(); - case "EBORequestModule_InvalidRequester": - return new EBORequestModule_InvalidRequester(); - case "EBORequestCreator_ChainNotAdded": - return new EBORequestCreator_ChainNotAdded(); - // TODO: refactor all errors to be in a map & use new error factory rather than a new class for each - // case "AccountExtension_InsufficientFunds": - // case "AccountingExtensions_NotAllowed": - // case "BondedResponseModule_AlreadyResponded": - // case "BondedResponseModule_TooLateToPropose": - // case "Oracle_AlreadyFinalized": - // case "ValidatorLib_InvalidResponseBody": - // case "ArbitratorModule_InvalidArbitrator": - // case "BondEscalationAccounting_AlreadySettled": - // case "BondEscalationAccounting_InsufficientFunds": - // case "AccountingExtension_UnauthorizedModule": - // case "Oracle_CannotEscalate": - // case "Oracle_InvalidDisputeId": - // case "Oracle_InvalidDispute": - // case "BondEscalationModule_NotEscalatable": - // case "BondEscalationModule_BondEscalationNotOver": - // case "BondEscalationModule_BondEscalationOver": - // case "AccountingExtension_InsufficientFunds": - // case "BondEscalationModule_DisputeWindowOver": - // case "Oracle_ResponseAlreadyDisputed": - // case "Oracle_InvalidDisputeBody": - // case "Oracle_InvalidResponse": - // case "ValidatorLib_InvalidDisputeBody": - // case "Validator_InvalidDispute": - // case "EBORequestModule_InvalidRequest": - // case "EBOFinalityModule_InvalidRequester": - // case "Oracle_InvalidFinalizedResponse": - // case "Oracle_FinalizableResponseExists": - // case "ValidatorLib_InvalidDispute": - // case "BondEscalationModule_CannotBreakTieDuringTyingBuffer": - // case "BondEscalationModule_CanOnlySurpassByOnePledge": - // case "BondEscalationModule_MaxNumberOfEscalationsReached": - // case "BondEscalationModule_BondEscalationOver": - // case "BondEscalationModule_InvalidDispute": - // case "Validator_InvalidDispute": - // case "ArbitratorModule_InvalidArbitrator": - // case "BondEscalationAccounting_AlreadySettled": - // case "BondEscalationAccounting_InsufficientFunds": - // case "AccountingExtension_InsufficientFunds": - // case "AccountingExtension_NotAllowed": - // case "AccountingExtension_UnauthorizedModule": - // case "Oracle_InvalidDisputeId": - // case "Oracle_InvalidDispute": - // case "BondEscalationModule_NotEscalatable": - // case "BondEscalationModule_BondEscalationNotOver": - // case "Oracle_NotDisputeOrResolutionModule": - // case "BondEscalationModule_ShouldBeEscalated": - // case "BondEscalationModule_BondEscalationCantBeSettled": - // case "ValidatorLib_InvalidResponseBody": - return new Error(`Contract reverted: ${errorName}`); - default: - return new Error(`Unknown error: ${errorName}`); - } - } -} diff --git a/packages/automated-dispute/src/services/index.ts b/packages/automated-dispute/src/services/index.ts index d6885f4..b5ad964 100644 --- a/packages/automated-dispute/src/services/index.ts +++ b/packages/automated-dispute/src/services/index.ts @@ -2,4 +2,3 @@ export * from "./eboActor.js"; export * from "./eboActorsManager.js"; export * from "./eboProcessor.js"; export * from "./eboRegistry/index.js"; -export * from "./errorFactory.js"; diff --git a/packages/automated-dispute/src/types/actorRequest.ts b/packages/automated-dispute/src/types/actorRequest.ts index a46e168..dd954f6 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"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; import { RequestId } from "./prophet.js"; diff --git a/packages/automated-dispute/src/types/errorTypes.ts b/packages/automated-dispute/src/types/errorTypes.ts new file mode 100644 index 0000000..32b1797 --- /dev/null +++ b/packages/automated-dispute/src/types/errorTypes.ts @@ -0,0 +1,69 @@ +import { EboRegistry } from "../interfaces/index.js"; +import { Dispute, EboEvent, EboEventName, Request, Response } from "./index.js"; + +export type BaseErrorStrategy = { + shouldNotify: boolean; + shouldTerminate: boolean; + shouldReenqueue: boolean; + customAction?: (context: any) => Promise | void; +}; + +export type EventReactError = BaseErrorStrategy & { + shouldReenqueue?: boolean; +}; + +export type TimeBasedError = BaseErrorStrategy; + +export type ErrorHandlingStrategy = BaseErrorStrategy; + +export interface ErrorContext { + request: Request; + response?: Response; + dispute?: Dispute; + event?: EboEvent; + registry: EboRegistry; + reenqueueEvent?: () => void; + terminateActor?: () => void; +} + +export type ErrorName = + | "UnknownError" + | "ValidatorLib_InvalidResponseBody" + | "BondEscalationAccounting_InsufficientFunds" + | "BondEscalationAccounting_AlreadySettled" + | "BondEscalationModule_InvalidDispute" + | "AccountingExtension_InsufficientFunds" + | "Oracle_InvalidDisputeId" + | "Oracle_InvalidDispute" + | "Oracle_InvalidDisputeBody" + | "Oracle_AlreadyFinalized" + | "EBORequestModule_InvalidRequester" + | "EBORequestModule_ChainNotAdded" + | "EBORequestCreator_InvalidEpoch" + | "Oracle_InvalidRequestBody" + | "EBORequestCreator_RequestAlreadyCreated" + | "Oracle_InvalidRequest" + | "Oracle_InvalidResponseBody" + | "AccountingExtension_NotAllowed" + | "BondedResponseModule_AlreadyResponded" + | "BondedResponseModule_TooLateToPropose" + | "Oracle_CannotEscalate" + | "Validator_InvalidDispute" + | "ValidatorLib_InvalidDisputeBody" + | "EBOFinalityModule_InvalidRequester" + | "Oracle_InvalidFinalizedResponse" + | "Oracle_InvalidResponse" + | "Oracle_FinalizableResponseExists" + | "ArbitratorModule_InvalidArbitrator" + | "AccountingExtension_UnauthorizedModule" + | "BondEscalationModule_NotEscalatable" + | "BondEscalationModule_BondEscalationNotOver" + | "BondEscalationModule_BondEscalationOver" + | "BondEscalationModule_DisputeWindowOver" + | "Oracle_ResponseAlreadyDisputed" + | "BondEscalationModule_CannotBreakTieDuringTyingBuffer" + | "BondEscalationModule_CanOnlySurpassByOnePledge" + | "BondEscalationModule_MaxNumberOfEscalationsReached" + | "Oracle_NotDisputeOrResolutionModule" + | "BondEscalationModule_ShouldBeEscalated" + | "BondEscalationModule_BondEscalationCantBeSettled"; diff --git a/packages/automated-dispute/src/types/events.ts b/packages/automated-dispute/src/types/events.ts index 310f28e..03b5e1f 100644 --- a/packages/automated-dispute/src/types/events.ts +++ b/packages/automated-dispute/src/types/events.ts @@ -1,4 +1,4 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; import { Address, Hex, Log } from "viem"; import { diff --git a/packages/automated-dispute/src/types/index.ts b/packages/automated-dispute/src/types/index.ts index 68d5e64..aece55d 100644 --- a/packages/automated-dispute/src/types/index.ts +++ b/packages/automated-dispute/src/types/index.ts @@ -2,3 +2,4 @@ export * from "./actorRequest.js"; export * from "./epochs.js"; export * from "./events.js"; export * from "./prophet.js"; +export * from "./errorTypes.js"; diff --git a/packages/automated-dispute/src/types/prophet.ts b/packages/automated-dispute/src/types/prophet.ts index 076fe62..6741528 100644 --- a/packages/automated-dispute/src/types/prophet.ts +++ b/packages/automated-dispute/src/types/prophet.ts @@ -1,5 +1,5 @@ -import type { Branded, NormalizedAddress } from "@ebo-agent/shared"; -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"; export type RequestId = Branded; diff --git a/packages/automated-dispute/tests/exceptions/errorFactory.spec.ts b/packages/automated-dispute/tests/exceptions/errorFactory.spec.ts new file mode 100644 index 0000000..ae0c602 --- /dev/null +++ b/packages/automated-dispute/tests/exceptions/errorFactory.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; + +import { CustomContractError, ErrorFactory } from "../../src/exceptions/index.js"; + +describe("ErrorFactory", () => { + it("creates a CustomContractError with the correct name and strategy for known errors", () => { + const errorName = "ValidatorLib_InvalidResponseBody"; + const error = ErrorFactory.createError(errorName); + + expect(error).toBeInstanceOf(CustomContractError); + expect(error.name).toBe(errorName); + expect(error.strategy).toEqual({ + shouldNotify: false, + shouldTerminate: false, + shouldReenqueue: true, + }); + }); + + it("creates a CustomContractError with default strategy for unknown errors", () => { + const errorName = "UnknownError"; + const error = ErrorFactory.createError(errorName); + + expect(error).toBeInstanceOf(CustomContractError); + expect(error.name).toBe(errorName); + expect(error.strategy).toEqual({ + shouldNotify: true, + shouldTerminate: false, + shouldReenqueue: true, + }); + }); + + it("creates a CustomContractError with custom action for specific errors", () => { + const errorName = "BondEscalationModule_InvalidDispute"; + const error = ErrorFactory.createError(errorName); + + expect(error).toBeInstanceOf(CustomContractError); + expect(error.name).toBe(errorName); + expect(error.strategy).toHaveProperty("customAction"); + expect(typeof error.strategy.customAction).toBe("function"); + }); + + it("creates different CustomContractErrors for different error names", () => { + const error1 = ErrorFactory.createError("ValidatorLib_InvalidResponseBody"); + const error2 = ErrorFactory.createError("BondEscalationAccounting_InsufficientFunds"); + + expect(error1.name).not.toBe(error2.name); + expect(error1.strategy).not.toEqual(error2.strategy); + }); + + it("creates CustomContractErrors with consistent strategies for the same error name", () => { + const errorName = "ValidatorLib_InvalidResponseBody"; + const error1 = ErrorFactory.createError(errorName); + const error2 = ErrorFactory.createError(errorName); + + expect(error1.strategy).toEqual(error2.strategy); + }); +}); diff --git a/packages/automated-dispute/tests/mocks/eboActor.mocks.ts b/packages/automated-dispute/tests/mocks/eboActor.mocks.ts index 915ed6e..bc107ce 100644 --- a/packages/automated-dispute/tests/mocks/eboActor.mocks.ts +++ b/packages/automated-dispute/tests/mocks/eboActor.mocks.ts @@ -1,6 +1,7 @@ import { BlockNumberService, Caip2ChainId } from "@ebo-agent/blocknumber"; import { ILogger } from "@ebo-agent/shared"; import { Mutex } from "async-mutex"; +import { vi } from "vitest"; import { ProtocolProvider } from "../../src/providers/index.js"; import { EboActor, EboMemoryRegistry } from "../../src/services/index.js"; @@ -31,6 +32,15 @@ export function buildEboActor(request: Request, logger: ILogger) { mockedPrivateKey, ); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + number: BigInt(1), + firstBlockNumber: BigInt(100), + startTimestamp: BigInt(Date.now()), + }); + vi.spyOn(protocolProvider, "proposeResponse").mockResolvedValue(undefined); + vi.spyOn(protocolProvider, "disputeResponse").mockResolvedValue(undefined); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(BigInt(1000)); + const blockNumberRpcUrls = new Map([ [chainId, ["http://localhost:8539"]], ]); @@ -48,6 +58,8 @@ export function buildEboActor(request: Request, logger: ILogger) { logger, ); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue(BigInt(12345)); + const registry = new EboMemoryRegistry(); const eventProcessingMutex = new Mutex(); @@ -84,13 +96,13 @@ export function buildResponse(request: Request, attributes: Partial = }; const baseResponse: Response = { - id: "0x01", + id: "0x0111111111111111111111111111111111111111", createdAt: request.createdAt + 1n, decodedData: { response: responseBody, }, prophetData: { - proposer: "0x01", + proposer: "0x0111111111111111111111111111111111111111", requestId: request.id, response: ProtocolProvider.encodeResponse(responseBody), }, diff --git a/packages/automated-dispute/tests/services/eboActor.spec.ts b/packages/automated-dispute/tests/services/eboActor.spec.ts index 4d3fb75..870c6af 100644 --- a/packages/automated-dispute/tests/services/eboActor.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor.spec.ts @@ -1,6 +1,13 @@ +import { Abi, ContractFunctionRevertedError, encodeErrorResult } from "viem"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { PastEventEnqueueError, RequestMismatch } from "../../src/exceptions/index.js"; +import { ErrorHandler } from "../../src/exceptions/errorHandler.js"; +import { + CustomContractError, + ErrorFactory, + PastEventEnqueueError, + RequestMismatch, +} from "../../src/exceptions/index.js"; import { ProtocolProvider } from "../../src/providers/index.js"; import { EboEvent, Request, RequestId } from "../../src/types/index.js"; import mocks from "../mocks/index.js"; @@ -110,14 +117,18 @@ describe("EboActor", () => { const mockEventsQueuePush = vi.spyOn(queue, "push"); - actor["onLastEvent"] = vi.fn().mockImplementation(() => Promise.reject()); + actor["onLastEvent"] = vi + .fn() + .mockImplementation(() => Promise.reject(new Error("Test Error"))); actor.enqueue(event); - await actor.processEvents(); + // Expect processEvents to throw and handle the rejection + await expect(actor.processEvents()).rejects.toThrow("Test Error"); - expect(mockEventsQueuePush).toHaveBeenNthCalledWith(1, event); - expect(mockEventsQueuePush).toHaveBeenNthCalledWith(2, event); + // The event should not be re-enqueued because it was the only event in the queue + expect(mockEventsQueuePush).toHaveBeenCalledTimes(1); + expect(queue.size()).toBe(0); }); it("enqueues again an event at the top if its processing throws", async () => { @@ -127,40 +138,38 @@ describe("EboActor", () => { const firstEvent = { ...event }; const secondEvent = { ...firstEvent, blockNumber: firstEvent.blockNumber + 1n }; + const mockError = new CustomContractError("UnknownError", { + shouldReenqueue: true, + shouldTerminate: false, + shouldNotify: false, + }); + // Mock ErrorHandler.handle to prevent re-enqueueing + const errorHandlerSpy = vi + .spyOn(ErrorHandler, "handle") + .mockImplementation(async (error) => { + if (error.strategy.shouldReenqueue && error.getContext()?.reenqueueEvent) { + error.getContext().reenqueueEvent(); + } + }); + actor["onLastEvent"] = vi.fn().mockImplementation(() => { return new Promise((resolve, reject) => { setTimeout(() => { - reject(); + reject(mockError); }, 10); }); }); - setTimeout(async () => { - actor.enqueue(firstEvent); - - await actor.processEvents(); - }, 5); - - setTimeout(() => { - actor.enqueue(secondEvent); - }, 10); - - // First enqueue + actor.enqueue(firstEvent); await vi.advanceTimersByTimeAsync(5); + actor.enqueue(secondEvent); - expect(queue.size()).toEqual(0); - - // Second enqueue - await vi.advanceTimersByTimeAsync(5); - - expect(queue.size()).toEqual(1); - expect(queue.peek()).toEqual(secondEvent); - - // processEvents throws and re-enqueues first event - await vi.advanceTimersByTime(10); + await vi.advanceTimersByTimeAsync(10); expect(queue.size()).toEqual(2); expect(queue.peek()).toEqual(firstEvent); + + errorHandlerSpy.mockRestore(); }); it("does not allow interleaved event processing", async () => { @@ -415,4 +424,136 @@ describe("EboActor", () => { expect(canBeTerminated).toBe(true); }); }); + + describe("onRequestCreated", () => { + it("throws a CustomContractError when proposeResponse fails with ContractFunctionRevertedError", async () => { + const { actor } = mocks.buildEboActor(request, logger); + const event: EboEvent<"RequestCreated"> = { + name: "RequestCreated", + blockNumber: 1n, + logIndex: 0, + requestId: request.id, + metadata: { + chainId: request.chainId, + epoch: request.epoch, + requestId: request.id, + request: request.prophetData, + }, + }; + + vi.spyOn(actor["registry"], "getRequest").mockReturnValue(request); + + const abi: Abi = [ + { + type: "error", + name: "SomeError", + inputs: [{ name: "reason", type: "string" }], + }, + ]; + + const data = encodeErrorResult({ + abi, + errorName: "SomeError", + args: ["Test error message"], + }); + + const contractError = new ContractFunctionRevertedError({ + abi, + data, + functionName: "proposeResponse", + }); + + actor["proposeResponse"] = vi.fn().mockRejectedValue(contractError); + + const customError = new CustomContractError("SomeError", { + shouldNotify: false, + shouldReenqueue: true, + shouldTerminate: false, + }); + + const errorFactorySpy = vi + .spyOn(ErrorFactory, "createError") + .mockReturnValue(customError); + + await expect(actor["onRequestCreated"](event)).rejects.toThrow(customError); + + expect(errorFactorySpy).toHaveBeenCalledWith(contractError.name); + + errorFactorySpy.mockRestore(); + }); + }); + + describe("settleDispute", () => { + it("escalates dispute when BondEscalationModule_ShouldBeEscalated error occurs", async () => { + const { actor, protocolProvider } = mocks.buildEboActor(request, logger); + const response = mocks.buildResponse(request); + const dispute = mocks.buildDispute(request, response); + + const abi: Abi = [ + { + type: "error", + name: "BondEscalationModule_ShouldBeEscalated", + inputs: [], + }, + ]; + + const errorName = "BondEscalationModule_ShouldBeEscalated"; + const data = encodeErrorResult({ + abi, + errorName, + args: [], + }); + + const contractError = new ContractFunctionRevertedError({ + abi, + data, + functionName: "settleDispute", + }); + + vi.spyOn(protocolProvider, "settleDispute").mockRejectedValue(contractError); + const escalateDisputeMock = vi + .spyOn(protocolProvider, "escalateDispute") + .mockResolvedValue(); + + const customError = new CustomContractError(errorName, { + shouldNotify: false, + shouldReenqueue: false, + shouldTerminate: false, + }); + + const onSpy = vi.spyOn(customError, "on").mockImplementation((eventName, handler) => { + if (eventName === errorName) { + handler(); + } + return customError; + }); + + vi.spyOn(ErrorFactory, "createError").mockReturnValue(customError); + + await actor["settleDispute"](request, response, dispute); + + expect(onSpy).toHaveBeenCalledWith(errorName, expect.any(Function)); + + expect(escalateDisputeMock).toHaveBeenCalledWith( + request.prophetData, + response.prophetData, + dispute.prophetData, + ); + expect(logger.info).toHaveBeenCalledWith(`Dispute ${dispute.id} escalated.`); + }); + + it("rethrows error when settleDispute fails", async () => { + const { actor, protocolProvider } = mocks.buildEboActor(request, logger); + const response = mocks.buildResponse(request); + const dispute = mocks.buildDispute(request, response); + + const settleError = new Error("SettleDispute failed"); + + vi.spyOn(protocolProvider, "settleDispute").mockRejectedValue(settleError); + + await expect(actor["settleDispute"](request, response, dispute)).rejects.toThrow( + settleError, + ); + }); + }); }); diff --git a/packages/automated-dispute/tests/services/eboActor/fixtures.ts b/packages/automated-dispute/tests/services/eboActor/fixtures.ts index c93a152..086dce8 100644 --- a/packages/automated-dispute/tests/services/eboActor/fixtures.ts +++ b/packages/automated-dispute/tests/services/eboActor/fixtures.ts @@ -14,9 +14,9 @@ export const mockedPrivateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; export const DEFAULT_MOCKED_PROTOCOL_CONTRACTS: ProtocolContractsAddresses = { - oracle: "0x123456" as Address, - epochManager: "0x654321" as Address, - eboRequestCreator: "0xabcdef" as Address, + oracle: "0x1234560000000000000000000000000000000000" as Address, + epochManager: "0x6543210000000000000000000000000000000000" as Address, + eboRequestCreator: "0x9999990000000000000000000000000000000000" as Address, bondEscalationModule: "0x1a2b3c" as Address, }; @@ -63,17 +63,17 @@ export const DEFAULT_MOCKED_REQUEST_CREATED_DATA: Request = { }, prophetData: { nonce: 1n, - disputeModule: "0x01" as Address, - finalityModule: "0x02" as Address, - requestModule: "0x03" as Address, - resolutionModule: "0x04" as Address, - responseModule: "0x05" as Address, - requester: "0x10" as Address, - responseModuleData: "0x11" as Hex, // TODO: use the corresponding encoded data - disputeModuleData: "0x12" as Hex, // TODO: use the corresponding encoded data - finalityModuleData: "0x13" as Hex, - requestModuleData: "0x14" as Hex, - resolutionModuleData: "0x15" as Hex, + disputeModule: "0x0111111111111111111111111111111111111111" as Address, + finalityModule: "0x0211111111111111111111111111111111111111" as Address, + requestModule: "0x0311111111111111111111111111111111111111" as Address, + resolutionModule: "0x0411111111111111111111111111111111111111" as Address, + responseModule: "0x0511111111111111111111111111111111111111" as Address, + requester: "0x1011111111111111111111111111111111111111" as Address, + responseModuleData: "0x1111111111111111111111111111111111111111" as Hex, // TODO: use the corresponding encoded data + disputeModuleData: "0x1211111111111111111111111111111111111111" as Hex, // TODO: use the corresponding encoded data + finalityModuleData: "0x1311111111111111111111111111111111111111" as Hex, + requestModuleData: "0x1411111111111111111111111111111111111111" as Hex, + resolutionModuleData: "0x1511111111111111111111111111111111111111" as Hex, }, }; diff --git a/packages/automated-dispute/tests/services/eboActor/onLastBlockupdated.spec.ts b/packages/automated-dispute/tests/services/eboActor/onLastBlockupdated.spec.ts index f935359..f031ece 100644 --- a/packages/automated-dispute/tests/services/eboActor/onLastBlockupdated.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onLastBlockupdated.spec.ts @@ -1,4 +1,3 @@ -import { ContractFunctionRevertedError } from "viem"; import { describe, expect, it, vi } from "vitest"; import { DisputeWithoutResponse } from "../../../src/exceptions/index.js"; @@ -47,7 +46,7 @@ describe("EboActor", () => { ); }); - it("escalates dispute if cannot settle", async () => { + it("throws error when settleDispute fails during onLastBlockUpdated", async () => { const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; const { disputeModuleData } = request.decodedData; @@ -60,43 +59,18 @@ describe("EboActor", () => { vi.spyOn(registry, "getRequest").mockReturnValue(request); vi.spyOn(registry, "getResponse").mockImplementation((id) => { - switch (id) { - case response.id: - return response; - } + if (id === response.id) return response; }); - // Skipping finalize flow with this mock vi.spyOn(registry, "getResponses").mockReturnValue([]); vi.spyOn(registry, "getDisputes").mockReturnValue([dispute]); - const error = Object.create(ContractFunctionRevertedError.prototype); - error.data = { errorName: "BondEscalationModule_ShouldBeEscalated" }; + const settleError = new Error("SettleDispute failed"); - const mockSettleDispute = vi - .spyOn(protocolProvider, "settleDispute") - .mockImplementation(async () => { - throw error; - }); - - const mockEscalateDispute = vi - .spyOn(protocolProvider, "escalateDispute") - .mockImplementation(() => Promise.resolve()); + vi.spyOn(protocolProvider, "settleDispute").mockRejectedValue(settleError); const newBlockNumber = disputeDeadline + 1n; - await actor.onLastBlockUpdated(newBlockNumber); - - expect(mockSettleDispute).toHaveBeenCalledWith( - request.prophetData, - response.prophetData, - dispute.prophetData, - ); - - expect(mockEscalateDispute).toHaveBeenCalledWith( - request.prophetData, - response.prophetData, - dispute.prophetData, - ); + await expect(actor.onLastBlockUpdated(newBlockNumber)).rejects.toThrow(settleError); }); it("throws if the dispute has no response in registry", async () => { diff --git a/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts b/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts index 412b268..9da3552 100644 --- a/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts @@ -1,7 +1,8 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; import { ILogger } from "@ebo-agent/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ResponseAlreadyProposed } from "../../../src/exceptions/index.js"; import { ProtocolProvider } from "../../../src/providers/index.js"; import { EboEvent, @@ -58,56 +59,30 @@ describe("EboActor", () => { }); it("stores the new request", async () => { - const { actor, blockNumberService, protocolProvider, registry } = + const { actor, protocolProvider, blockNumberService, registry } = mocks.buildEboActor(request, logger); - const indexedEpochBlockNumber = 48n; - - vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( - indexedEpochBlockNumber, - ); - - vi.spyOn(protocolProvider, "proposeResponse").mockImplementation(() => - Promise.resolve(), - ); - - const mockRegistryAddRequest = vi - .spyOn(registry, "addRequest") - .mockImplementation(() => {}); - - actor.enqueue(requestCreatedEvent); - - await actor.processEvents(); - - expect(mockRegistryAddRequest).toHaveBeenCalledWith( - expect.objectContaining({ - id: requestId, - }), - ); - }); - - it("proposes a response", async () => { - const { actor, blockNumberService, protocolProvider } = mocks.buildEboActor( - request, - logger, - ); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch); - const indexedEpochBlockNumber = 48n; const proposerAddress = "0x1234567890123456789012345678901234567890"; + vi.spyOn(protocolProvider, "getAccountAddress").mockReturnValue(proposerAddress); + const indexedEpochBlockNumber = 48n; vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( indexedEpochBlockNumber, ); - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch); - vi.spyOn(protocolProvider, "getAccountAddress").mockReturnValue(proposerAddress); - - const proposeResponseMock = vi.spyOn(protocolProvider, "proposeResponse"); + const proposeResponseMock = vi + .spyOn(protocolProvider, "proposeResponse") + .mockResolvedValue(); actor.enqueue(requestCreatedEvent); await actor.processEvents(); + const storedRequest = registry.getRequest(request.id); + expect(storedRequest).toBeDefined(); + expect(proposeResponseMock).toHaveBeenCalledWith( expect.objectContaining(request.prophetData), expect.objectContaining({ @@ -158,7 +133,7 @@ describe("EboActor", () => { actor.enqueue(requestCreatedEvent); - await actor.processEvents(); + await expect(actor.processEvents()).rejects.toThrow(ResponseAlreadyProposed); expect(proposeResponseMock).not.toHaveBeenCalled(); }); diff --git a/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts b/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts index 15f8fc8..2ba2127 100644 --- a/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts @@ -23,8 +23,8 @@ describe("onResponseDisputed", () => { dispute: { requestId: actorRequest.id, responseId: response.id, - disputer: "0x11", - proposer: "0x12", + disputer: "0x1111111111111111111111111111111111111111", + proposer: "0x2222222222222222222222222222222222222222", }, }, }; @@ -48,13 +48,13 @@ describe("onResponseDisputed", () => { response.decodedData.response.block + 1n, ); - const mockPledgeForDispute = vi.spyOn(protocolProvider, "pledgeForDispute"); + vi.spyOn(protocolProvider, "pledgeForDispute").mockResolvedValue(); actor.enqueue(event); await actor.processEvents(); - expect(mockPledgeForDispute).toHaveBeenCalled(); + expect(protocolProvider.pledgeForDispute).toHaveBeenCalled(); }); it("pledges against dispute if proposal is ok", async () => { @@ -76,13 +76,13 @@ describe("onResponseDisputed", () => { response.decodedData.response.block, ); - const mockPledgeAgainstDispute = vi.spyOn(protocolProvider, "pledgeAgainstDispute"); + vi.spyOn(protocolProvider, "pledgeAgainstDispute").mockResolvedValue(); actor.enqueue(event); await actor.processEvents(); - expect(mockPledgeAgainstDispute).toHaveBeenCalled(); + expect(protocolProvider.pledgeAgainstDispute).toHaveBeenCalled(); }); it("adds dispute to registry", async () => { diff --git a/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts b/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts index 9eedecf..5bb9b6b 100644 --- a/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts @@ -1,6 +1,9 @@ import { ILogger } from "@ebo-agent/shared"; +import { ContractFunctionRevertedError } from "viem"; import { describe, expect, it, vi } from "vitest"; +import { ErrorHandler } from "../../../src/exceptions/errorHandler.js"; +import { ErrorFactory } from "../../../src/exceptions/index.js"; import { ProtocolProvider } from "../../../src/providers/index.js"; import { EboEvent, ResponseBody } from "../../../src/types/index.js"; import mocks from "../../mocks/index.js"; @@ -28,35 +31,59 @@ describe("EboActor", () => { requestId: actorRequest.id, responseId: "0x02", response: { - proposer: "0x03", + proposer: "0x0000000000000000000000000000000000000003", requestId: actorRequest.id, response: ProtocolProvider.encodeResponse(proposedResponseBody), }, }, }; - it("adds the response to the registry", async () => { - const { actor, registry, blockNumberService } = mocks.buildEboActor( - actorRequest, - logger, - ); + it("handles error when disputing the response", async () => { + expect.assertions(3); + + const { actor, registry, blockNumberService, protocolProvider } = + mocks.buildEboActor(actorRequest, logger); vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + number: proposedResponseBody.epoch, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + }); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( - proposedResponseBody.block, + proposedResponseBody.block + 1n, ); - const addResponseMock = vi.spyOn(registry, "addResponse"); + const disputeResponseMock = vi + .spyOn(protocolProvider, "disputeResponse") + .mockRejectedValue( + new ContractFunctionRevertedError({ + data: { + errorName: "SomeRevertedError", + }, + } as any), + ); + + const errorFactorySpy = vi.spyOn(ErrorFactory, "createError"); + const errorHandlerSpy = vi.spyOn(ErrorHandler, "handle").mockResolvedValue(); actor.enqueue(responseProposedEvent); await actor.processEvents(); - expect(addResponseMock).toHaveBeenCalled(); + expect(disputeResponseMock).toHaveBeenCalled(); + expect(errorFactorySpy).toHaveBeenCalledWith("ContractFunctionRevertedError"); + expect(errorHandlerSpy).toHaveBeenCalled(); + + errorFactorySpy.mockRestore(); + errorHandlerSpy.mockRestore(); }); - it("does not dispute the response if seems valid", async () => { + const proposeData = responseProposedEvent.metadata.response.response; + + it("adds the response to the registry", async () => { const { actor, registry, blockNumberService, protocolProvider } = mocks.buildEboActor(actorRequest, logger); @@ -66,38 +93,46 @@ describe("EboActor", () => { proposedResponseBody.block, ); - const mockDisputeResponse = vi.spyOn(protocolProvider, "disputeResponse"); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ + number: proposeData.epoch, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + }); + + const addResponseMock = vi.spyOn(registry, "addResponse"); actor.enqueue(responseProposedEvent); await actor.processEvents(); - expect(mockDisputeResponse).not.toHaveBeenCalled(); + expect(addResponseMock).toHaveBeenCalled(); }); - it("disputes the response if it should be different", async () => { + it("does not dispute the response if seems valid", async () => { + expect.assertions(1); + const { actor, registry, blockNumberService, protocolProvider } = mocks.buildEboActor(actorRequest, logger); vi.spyOn(registry, "getRequest").mockReturnValue(actorRequest); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( + proposedResponseBody.block, + ); + + vi.spyOn(protocolProvider, "disputeResponse").mockResolvedValue(undefined); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({ - number: proposedResponseBody.epoch, + number: proposeData.epoch, firstBlockNumber: 1n, startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), }); - vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( - proposedResponseBody.block + 1n, - ); - - const mockDisputeResponse = vi.spyOn(protocolProvider, "disputeResponse"); - actor.enqueue(responseProposedEvent); await actor.processEvents(); - expect(mockDisputeResponse).toHaveBeenCalled(); + expect(protocolProvider.disputeResponse).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/automated-dispute/tests/services/errorFactory.spec.ts b/packages/automated-dispute/tests/services/errorFactory.spec.ts deleted file mode 100644 index d0a09a4..0000000 --- a/packages/automated-dispute/tests/services/errorFactory.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - EBORequestCreator_ChainNotAdded, - EBORequestCreator_InvalidEpoch, - EBORequestModule_InvalidRequester, - Oracle_InvalidRequestBody, -} from "../../src/exceptions/index.js"; -import { ErrorFactory } from "../../src/services/index.js"; - -describe("ErrorFactory", () => { - it("creates EBORequestCreator_InvalidEpoch error", () => { - const error = ErrorFactory.createError("EBORequestCreator_InvalidEpoch"); - expect(error).toBeInstanceOf(EBORequestCreator_InvalidEpoch); - }); - - it("creates Oracle_InvalidRequestBody error", () => { - const error = ErrorFactory.createError("Oracle_InvalidRequestBody"); - expect(error).toBeInstanceOf(Oracle_InvalidRequestBody); - }); - - it("creates EBORequestModule_InvalidRequester error", () => { - const error = ErrorFactory.createError("EBORequestModule_InvalidRequester"); - expect(error).toBeInstanceOf(EBORequestModule_InvalidRequester); - }); - - it("creates EBORequestCreator_ChainNotAdded error", () => { - const error = ErrorFactory.createError("EBORequestCreator_ChainNotAdded"); - expect(error).toBeInstanceOf(EBORequestCreator_ChainNotAdded); - }); - - it("creates generic Error for unknown error name", () => { - const error = ErrorFactory.createError("UnknownError"); - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe("Unknown error: UnknownError"); - }); -}); diff --git a/packages/automated-dispute/tests/services/protocolProvider.spec.ts b/packages/automated-dispute/tests/services/protocolProvider.spec.ts index 77ca076..5cf574b 100644 --- a/packages/automated-dispute/tests/services/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/services/protocolProvider.spec.ts @@ -1,4 +1,4 @@ -import { Caip2ChainId } from "@ebo-agent/blocknumber"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/src/index.js"; import { ContractFunctionRevertedError, createPublicClient, @@ -390,7 +390,7 @@ describe("ProtocolProvider", () => { await expect( protocolProvider.proposeResponse(mockRequestProphetData, mockResponseProphetData), - ).rejects.toThrow("Unknown error: "); + ).rejects.toThrow('The contract function "proposeResponse" reverted.'); }); it("throws WaitForTransactionReceiptTimeoutError when waitForTransactionReceipt times out", async () => {