Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement oracle rpc calls #39

Merged
merged 13 commits into from
Sep 16, 2024
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 }],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old function signature might have been a little bit deprecated.

Based on the Oracle function we might be ok using Request["prophetData"] and Response["prophetData"]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I double checked the interfaces and had to update a couple of their protocolProvider interfaces and implementations

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");
Copy link
Collaborator

@0xyaco 0xyaco Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also need to handle this scenario too when processing the event that triggered this action.

Let's add a named error for this (probably TransactionExecutionError) as it needs to be handled by the EboProcessor by retrying the transaction.


Also, given that this error is most likely not going to be a child class of ContractFunctionRevertedError we'll (probably) end up modifying the method we have/adding a new method to the ErrorFactory class:

// ErrorFactory.ts
static async createTransactionError(error) {
  ...
}

static async createContractRevertedError(error) {
  ...
}

For these last ErrorFactory changes mentioned, let's wait until we have a solid design for the error handling and we'll come back to it after that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, for now I added a TransactionExecutionError w/test cases

}
} 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],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, based on the contract function signatures we should use Request["prophetData"], Response["prophetData'}, Dispute["prophetData"].

I'm surprised that the types are not being enforced by viem with the help of the TypeScript.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom error here!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
} 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");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom error here!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
} 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");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom error here!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
} 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;
}
}
}
29 changes: 29 additions & 0 deletions packages/automated-dispute/src/services/errorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ export class ErrorFactory {
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":
return new Error(`Contract reverted: ${errorName}`);
default:
return new Error(`Unknown error: ${errorName}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ describe("EboActor", () => {
vi.spyOn(registry, "getResponses").mockReturnValue(reverseResponses);
vi.spyOn(registry, "getDispute").mockReturnValue(firstResponseDispute);

const mockFinalize = vi.spyOn(protocolProvider, "finalize");
const mockFinalize = vi.spyOn(protocolProvider, "finalize").mockImplementation(() => {
return Promise.resolve();
});

const newBlock =
secondResponse.createdAt + request.prophetData.responseModuleData.disputeWindow;
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();
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's test failures too when:

  • Transaction couldn't be confirmed
  • A ContractFunctionRevertedError is thrown by viem
  • Can the waitForTransactionReceipt throw a timeout error or something like that? Let's test that too

Testing failure behavior (specially in this class) is almost as important as the happy path. This applies to every Oracle provider method implemented.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ added these for proposeResponse

});

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