From 827b90c1aa74b5c0b7b1be04e39151e8421564f8 Mon Sep 17 00:00:00 2001 From: jahabeebs <47253537+jahabeebs@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:18:59 -0500 Subject: [PATCH] feat: add new methods to protocolProvider --- .../automated-dispute/src/protocolProvider.ts | 195 +++++++++++++++--- .../src/services/errorFactory.ts | 30 +++ .../tests/protocolProvider.spec.ts | 78 +++++-- 3 files changed, 259 insertions(+), 44 deletions(-) diff --git a/packages/automated-dispute/src/protocolProvider.ts b/packages/automated-dispute/src/protocolProvider.ts index 90b96c6..7fb8339 100644 --- a/packages/automated-dispute/src/protocolProvider.ts +++ b/packages/automated-dispute/src/protocolProvider.ts @@ -304,23 +304,96 @@ export class ProtocolProvider implements IProtocolProvider { } } + /** + * Proposes a response for a given request. + * + * @param {string} requestId - The ID of the request. + * @param {bigint} epoch - The epoch for which the response is proposed. + * @param {Caip2ChainId} chainId - The chain ID for which the response is proposed. + * @param {bigint} blockNumber - The block number proposed as the response. + * @throws {Error} If the transaction fails or if there's a contract revert. + * @returns {Promise} + */ async proposeResponse( - _requestId: string, - _epoch: bigint, - _chainId: Caip2ChainId, - _blockNumber: bigint, + requestId: string, + epoch: bigint, + chainId: Caip2ChainId, + blockNumber: bigint, ): Promise { - // TODO: implement actual method - return; + try { + const { request } = await this.readClient.simulateContract({ + address: this.oracleContract.address, + abi: oracleAbi, + functionName: "proposeResponse", + args: [requestId, { epoch, chainId, block: blockNumber }], + account: this.writeClient.account, + }); + + const hash = await this.writeClient.writeContract(request); + + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: TRANSACTION_RECEIPT_CONFIRMATIONS, + }); + + if (receipt.status !== "success") { + throw new Error("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; + } } - async disputeResponse( - _requestId: string, - _responseId: string, - _proposer: Address, - ): Promise { - // TODO: implement actual method - return; + /** + * Disputes a proposed response. + * + * @param {string} requestId - The ID of the request. + * @param {string} responseId - The ID of the response being disputed. + * @param {Address} proposer - The address of the proposer of the disputed response. + * @throws {Error} If the transaction fails or if there's a contract revert. + * @returns {Promise} + */ + async disputeResponse(requestId: string, responseId: string, proposer: Address): Promise { + try { + const { request } = await this.readClient.simulateContract({ + address: this.oracleContract.address, + abi: oracleAbi, + functionName: "disputeResponse", + args: [requestId, responseId, proposer], + account: this.writeClient.account, + }); + + const hash = await this.writeClient.writeContract(request); + + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: TRANSACTION_RECEIPT_CONFIRMATIONS, + }); + + if (receipt.status !== "success") { + throw new Error("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; + } } async pledgeForDispute( @@ -348,23 +421,95 @@ export class ProtocolProvider implements IProtocolProvider { return; } + /** + * Escalates a dispute to a higher authority. + * + * @param {Request["prophetData"]} request - The request data. + * @param {Response["prophetData"]} response - The response data. + * @param {Dispute["prophetData"]} dispute - The dispute data. + * @throws {Error} If the transaction fails or if there's a contract revert. + * @returns {Promise} + */ async escalateDispute( - _request: Request["prophetData"], - _response: Response["prophetData"], - _dispute: Dispute["prophetData"], + request: Request["prophetData"], + response: Response["prophetData"], + dispute: Dispute["prophetData"], ): Promise { - // TODO: implement actual method - return; - } + 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 hash = await this.writeClient.writeContract(simulatedRequest); - // Pending confirmation from onchain team - // releasePledge(args):void; + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: TRANSACTION_RECEIPT_CONFIRMATIONS, + }); + if (receipt.status !== "success") { + throw new Error("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; + } + } + + /** + * Finalizes a request with a given response. + * + * @param {Request["prophetData"]} request - The request data. + * @param {Response["prophetData"]} response - The response data to finalize. + * @throws {Error} If the transaction fails or if there's a contract revert. + * @returns {Promise} + */ async finalize( - _request: Request["prophetData"], - _response: Response["prophetData"], + request: Request["prophetData"], + response: Response["prophetData"], ): Promise { - //TODO: implement actual method - return; + try { + 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 receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: TRANSACTION_RECEIPT_CONFIRMATIONS, + }); + + if (receipt.status !== "success") { + throw new Error("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; + } } } diff --git a/packages/automated-dispute/src/services/errorFactory.ts b/packages/automated-dispute/src/services/errorFactory.ts index 37fd125..108dd06 100644 --- a/packages/automated-dispute/src/services/errorFactory.ts +++ b/packages/automated-dispute/src/services/errorFactory.ts @@ -18,6 +18,7 @@ export class ErrorFactory { // TODO: need to define structure of each error // TODO: Need to define some base contract reverted error to distinguish from other errors switch (errorName) { + // Existing errors case "EBORequestCreator_InvalidEpoch": return new EBORequestCreator_InvalidEpoch(); case "Oracle_InvalidRequestBody": @@ -26,6 +27,35 @@ export class ErrorFactory { return new EBORequestModule_InvalidRequester(); case "EBORequestCreator_ChainNotAdded": return new EBORequestCreator_ChainNotAdded(); + // TODO: refactor 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": + return new Error(`Contract reverted: ${errorName}`); default: return new Error(`Unknown error: ${errorName}`); } diff --git a/packages/automated-dispute/tests/protocolProvider.spec.ts b/packages/automated-dispute/tests/protocolProvider.spec.ts index 083d794..5685076 100644 --- a/packages/automated-dispute/tests/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/protocolProvider.spec.ts @@ -1,7 +1,9 @@ +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; import { createPublicClient, createWalletClient, fallback, getContract, http } from "viem"; import { arbitrum } from "viem/chains"; import { afterEach, beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import type { Dispute, Request, Response } from "../src/types/prophet.js"; import { eboRequestCreatorAbi } from "../src/abis/eboRequestCreator.js"; import { epochManagerAbi } from "../src/abis/epochManager.js"; import { oracleAbi } from "../src/abis/oracle.js"; @@ -221,29 +223,55 @@ describe("ProtocolProvider", () => { it("returns false if the address has 0 staked assets"); }); - describe.skip("createRequest", () => { - it("succeeds if the RPC client sent the request"); - // NOTE: Should we validate if the request was created by - // tracking the transaction result somehow? I feel like it's - // somewhat brittle to just wish for the tx to be processed. - it("throws if the epoch is not current"); - it("throws if chains is empty"); - it("throws if the RPC client fails"); - }); - describe.skip("getAvailableChains", () => { it("returns an array of available chains in CAIP-2 compliant format"); it("throws if the RPC client fails"); }); - describe.skip("proposeResponse", () => { - it("returns if the RPC client sent the response"); - it("throws if the RPC client fails"); + describe("proposeResponse", () => { + it("should successfully propose a response", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcUrls, + mockContractAddress, + mockedPrivateKey, + ); + + await expect( + protocolProvider.proposeResponse("0x123", 1n, "eip155:1" as Caip2ChainId, 100n), + ).resolves.not.toThrow(); + }); }); - describe.skip("disputeResponse", () => { - it("returns if the RPC client sent the dispute"); - it("throws if the RPC client fails"); + describe("disputeResponse", () => { + it("should successfully dispute a response", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcUrls, + mockContractAddress, + mockedPrivateKey, + ); + + await expect( + protocolProvider.disputeResponse("0x123", "0x456", "0x789"), + ).resolves.not.toThrow(); + }); + }); + + describe("escalateDispute", () => { + it("should successfully escalate a dispute", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcUrls, + mockContractAddress, + mockedPrivateKey, + ); + + const mockRequest = {} as Request["prophetData"]; + const mockResponse = {} as Response["prophetData"]; + const mockDispute = {} as Dispute["prophetData"]; + + await expect( + protocolProvider.escalateDispute(mockRequest, mockResponse, mockDispute), + ).resolves.not.toThrow(); + }); }); describe.skip("pledgeForDispute", () => { @@ -256,9 +284,21 @@ describe("ProtocolProvider", () => { it("throws if the RPC client fails"); }); - describe.skip("finalize", () => { - it("returns if the RPC client finalizes the pledge"); - it("throws if the RPC client fails"); + describe("finalize", () => { + it("should successfully finalize a request", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcUrls, + mockContractAddress, + mockedPrivateKey, + ); + + const mockRequest = {} as Request["prophetData"]; + const mockResponse = {} as Response["prophetData"]; + + await expect( + protocolProvider.finalize(mockRequest, mockResponse), + ).resolves.not.toThrow(); + }); }); describe("createRequest", () => {