Skip to content

Commit

Permalink
feat: add new methods to protocolProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
jahabeebs committed Sep 11, 2024
1 parent baeea02 commit 827b90c
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 44 deletions.
195 changes: 170 additions & 25 deletions packages/automated-dispute/src/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>}
*/
async proposeResponse(
_requestId: string,
_epoch: bigint,
_chainId: Caip2ChainId,
_blockNumber: bigint,
requestId: string,
epoch: bigint,
chainId: Caip2ChainId,
blockNumber: bigint,
): Promise<void> {
// 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<void> {
// 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<void>}
*/
async disputeResponse(requestId: string, responseId: string, proposer: Address): Promise<void> {
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(
Expand Down Expand Up @@ -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<void>}
*/
async escalateDispute(
_request: Request["prophetData"],
_response: Response["prophetData"],
_dispute: Dispute["prophetData"],
request: Request["prophetData"],
response: Response["prophetData"],
dispute: Dispute["prophetData"],
): Promise<void> {
// 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<void>}
*/
async finalize(
_request: Request["prophetData"],
_response: Response["prophetData"],
request: Request["prophetData"],
response: Response["prophetData"],
): Promise<void> {
//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;
}
}
}
30 changes: 30 additions & 0 deletions packages/automated-dispute/src/services/errorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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}`);
}
Expand Down
78 changes: 59 additions & 19 deletions packages/automated-dispute/tests/protocolProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down

0 comments on commit 827b90c

Please sign in to comment.