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 EBORequestCreator.createRequests function #35

Merged
merged 18 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/automated-dispute/src/abis/eboRequestCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,4 @@ export const eboRequestCreatorAbi = [
{ type: "error", name: "EBORequestCreator_ChainNotAdded", inputs: [] },
{ type: "error", name: "EBORequestCreator_InvalidEpoch", inputs: [] },
{ type: "error", name: "EBORequestCreator_InvalidNonce", inputs: [] },
];
] as const;
Copy link
Collaborator

Choose a reason for hiding this comment

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

As a sidenote, as @0xkenj1 said somewhere in the offline chat, by using the Prophet packages we won't have a "simple" way to leverage the abi as const to feed viem with a typed ABI.

TypeScript refuses to cooperate with types while importing the JSON ABIs from the Prophet package so this dependency might be at first a dev dependency to just copy/paste the JSON ABIs values into our own:

// abis/Oracle.ts
export const abi = <copy paste from package>;

Later we might get to preprocess the package's JSON ABIs to automate the generation of TypeScript abi/** const values but it's not high priority right now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm ok, for now I've left it with a const assertion and added the contracts as dev dependencies

143 changes: 103 additions & 40 deletions packages/automated-dispute/src/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Timestamp } from "@ebo-agent/shared";
import {
Address,
BaseError,
ContractFunctionRevertedError,
createPublicClient,
createWalletClient,
fallback,
FallbackTransport,
getContract,
GetContractReturnType,
Hex,
http,
HttpTransport,
PublicClient,
Expand All @@ -26,20 +29,29 @@ import {
Oracle_InvalidRequestBody,
} from "./exceptions/index.js";
import { RpcUrlsEmpty } from "./exceptions/rpcUrlsEmpty.exception.js";
import { ProtocolContractsAddresses } from "./types/protocolProvider.js";
import {
IProtocolProvider,
IReadProvider,
IWriteProvider,
ProtocolContractsAddresses,
} from "./types/protocolProvider.js";

export class ProtocolProvider {
private client: PublicClient<FallbackTransport<HttpTransport[]>>;
private walletClient: WalletClient<FallbackTransport<HttpTransport[]>>;
private oracleContract: GetContractReturnType<typeof oracleAbi, typeof this.client, Address>;
export class ProtocolProvider implements IProtocolProvider {
private readClient: PublicClient<FallbackTransport<HttpTransport[]>>;
private writeClient: WalletClient<FallbackTransport<HttpTransport[]>>;
private oracleContract: GetContractReturnType<
typeof oracleAbi,
typeof this.readClient,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oracle will need to write (it might end up not needing read):

Suggested change
typeof this.readClient,
typeof this.writeClient,

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Address
>;
private epochManagerContract: GetContractReturnType<
typeof epochManagerAbi,
typeof this.client,
typeof this.readClient,
Address
>;
private eboRequestCreatorContract: GetContractReturnType<
typeof eboRequestCreatorAbi,
typeof this.walletClient,
typeof this.writeClient,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seeing the initialization of the eboRequestCreatorContract in the constructor, it's using both the read and write client.

Does this typeof this.writeClient include both read and write clients?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, the write client (WalletClient) is just the read client (PublicClient) with an account basically.

For example, I wrote this because I wanted to be explicit:

  this.eboRequestCreatorContract = getContract({
            address: contracts.eboRequestCreator,
            abi: eboRequestCreatorAbi,
            client: {
                public: this.readClient,
                wallet: this.writeClient,
            },
        });

but I could have just changed the client line to

client: this.writeClient

Address
>;

Expand All @@ -49,23 +61,19 @@ export class ProtocolProvider {
* @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
*/
constructor(
rpcUrls: string[],
contracts: ProtocolContractsAddresses,
privateKey: `0x${string}`,
) {
constructor(rpcUrls: string[], contracts: ProtocolContractsAddresses, privateKey: Hex) {
if (rpcUrls.length === 0) {
throw new RpcUrlsEmpty();
}

this.client = createPublicClient({
this.readClient = createPublicClient({
Copy link
Collaborator

Choose a reason for hiding this comment

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

Now that I think, every client should have:

Let's define consts at the top of the class and add for each of them the TODO comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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 them with their viem-specified defaults for now

chain: arbitrum,
transport: fallback(rpcUrls.map((url) => http(url))),
});

const account = privateKeyToAccount(privateKey);

this.walletClient = createWalletClient({
this.writeClient = createWalletClient({
chain: arbitrum,
transport: fallback(rpcUrls.map((url) => http(url))),
account: account,
Expand All @@ -75,23 +83,42 @@ export class ProtocolProvider {
this.oracleContract = getContract({
address: contracts.oracle,
abi: oracleAbi,
client: this.client,
client: this.readClient,
});
this.epochManagerContract = getContract({
address: contracts.epochManager,
abi: epochManagerAbi,
client: this.client,
client: this.readClient,
});
this.eboRequestCreatorContract = getContract({
address: contracts.eboRequestCreator,
abi: eboRequestCreatorAbi,
client: {
public: this.client,
wallet: this.walletClient,
public: this.readClient,
wallet: this.writeClient,
},
});
}

public write: IWriteProvider = {
createRequest: this.createRequest.bind(this),
proposeResponse: this.proposeResponse.bind(this),
disputeResponse: this.disputeResponse.bind(this),
pledgeForDispute: this.pledgeForDispute.bind(this),
pledgeAgainstDispute: this.pledgeAgainstDispute.bind(this),
settleDispute: this.settleDispute.bind(this),
escalateDispute: this.escalateDispute.bind(this),
finalize: this.finalize.bind(this),
};

public read: IReadProvider = {
getCurrentEpoch: this.getCurrentEpoch.bind(this),
getLastFinalizedBlock: this.getLastFinalizedBlock.bind(this),
getEvents: this.getEvents.bind(this),
hasStakedAssets: this.hasStakedAssets.bind(this),
getAvailableChains: this.getAvailableChains.bind(this),
};

/**
* Gets the current epoch, the block number and its timestamp of the current epoch
*
Expand All @@ -107,7 +134,7 @@ export class ProtocolProvider {
this.epochManagerContract.read.currentEpochBlock(),
]);

const currentEpochBlock = await this.client.getBlock({
const currentEpochBlock = await this.readClient.getBlock({
blockNumber: currentEpochBlockNumber,
});

Expand All @@ -119,7 +146,7 @@ export class ProtocolProvider {
}

async getLastFinalizedBlock(): Promise<bigint> {
const { number } = await this.client.getBlock({ blockTag: "finalized" });
const { number } = await this.readClient.getBlock({ blockTag: "finalized" });

return number;
}
Expand Down Expand Up @@ -208,33 +235,69 @@ export class ProtocolProvider {

// TODO: waiting for ChainId to be merged for _chains parameter
/**
* Creates a new request for the specified epoch and chains.
* Creates a request on the EBO Request Creator contract by simulating the transaction
* and then executing it if the simulation is successful.
*
* This function first simulates the `createRequests` call on the EBO Request Creator contract
* to validate that the transaction will succeed. If the simulation is successful, the transaction
* is executed by the `writeContract` method of the wallet client. The function also handles any
* potential errors that may occur during the simulation or transaction execution.
*
* @param epoch The epoch for which to create the request
* @param chains An array of chain IDs for which to create the request
* @throws {EBORequestCreator_InvalidEpoch} If the epoch is invalid
* @throws {Oracle_InvalidRequestBody} If the request body is invalid
* @throws {EBORequestModule_InvalidRequester} If the requester is invalid
* @throws {EBORequestCreator_ChainNotAdded} If one of the specified chains is not added
* @param {bigint} epoch - The epoch for which the request is being created.
* @param {string[]} chains - An array of chain identifiers where 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<void>} A promise that resolves when the request is successfully created.
*/
async createRequest(epoch: bigint, chains: string[]): Promise<void> {
if (chains.length === 0) {
throw new Error("Chains array cannot be empty");
}

try {
if (!this.eboRequestCreatorContract?.write?.createRequests) {
throw new Error("createRequests function is not available on the ABI");
const { request } = await this.readClient.simulateContract({
address: this.eboRequestCreatorContract.address,
abi: eboRequestCreatorAbi,
functionName: "createRequests",
args: [epoch, chains],
account: this.writeClient.account,
});

const hash = await this.writeClient.writeContract(request);

const receipt = await this.readClient.waitForTransactionReceipt({
hash,
confirmations: 1,
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 create a const value with this 1:

// TODO: env var
const TRANSACTION_RECEIPT_CONFIRMATIONS = 1;

at the top of this class. It'll be one of the EBO agent config values.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

});

if (receipt.status !== "success") {
throw new Error("Transaction failed");
}
await this.eboRequestCreatorContract.write.createRequests([epoch, chains]);
} catch (error) {
if (error instanceof EBORequestCreator_InvalidEpoch) {
throw new EBORequestCreator_InvalidEpoch();
} else if (error instanceof Oracle_InvalidRequestBody) {
throw new Oracle_InvalidRequestBody();
} else if (error instanceof EBORequestModule_InvalidRequester) {
throw new EBORequestModule_InvalidRequester();
} else if (error instanceof EBORequestCreator_ChainNotAdded) {
throw new EBORequestCreator_ChainNotAdded();
} else {
throw error;
if (error instanceof BaseError) {
const revertError = error.walk(
(err) => err instanceof ContractFunctionRevertedError,
);
if (revertError instanceof ContractFunctionRevertedError) {
const errorName = revertError.data?.errorName ?? "";
switch (errorName) {
case "EBORequestCreator_InvalidEpoch":
throw new EBORequestCreator_InvalidEpoch();
case "Oracle_InvalidRequestBody":
throw new Oracle_InvalidRequestBody();
case "EBORequestModule_InvalidRequester":
throw new EBORequestModule_InvalidRequester();
case "EBORequestCreator_ChainNotAdded":
throw new EBORequestCreator_ChainNotAdded();
default:
throw new Error(`Unknown error: ${errorName}`);
}
}
}
throw error;
}
}

Expand Down
51 changes: 51 additions & 0 deletions packages/automated-dispute/src/types/protocolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,57 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Timestamp } from "@ebo-agent/shared";
import { Address } from "viem";

import type { EboEvent, EboEventName } from "../types/events.js";
import type { Dispute, Request, Response } from "../types/prophet.js";
import { ProtocolContractsNames } from "../constants.js";

export type ProtocolContract = (typeof ProtocolContractsNames)[number];
export type ProtocolContractsAddresses = Record<ProtocolContract, Address>;

export interface IReadProvider {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We've got the interfaces/ folder for these

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

getCurrentEpoch(): Promise<{
currentEpoch: bigint;
currentEpochBlockNumber: bigint;
currentEpochTimestamp: Timestamp;
}>;
getLastFinalizedBlock(): Promise<bigint>;
getEvents(_fromBlock: bigint, _toBlock: bigint): Promise<EboEvent<EboEventName>[]>;
hasStakedAssets(_address: Address): Promise<boolean>;
getAvailableChains(): Promise<string[]>;
}

export interface IWriteProvider {
createRequest(epoch: bigint, chains: string[]): Promise<void>;
proposeResponse(
_requestId: string,
_epoch: bigint,
_chainId: Caip2ChainId,
_blockNumber: bigint,
): Promise<void>;
disputeResponse(_requestId: string, _responseId: string, _proposer: Address): Promise<void>;
pledgeForDispute(
_request: Request["prophetData"],
_dispute: Dispute["prophetData"],
): Promise<void>;
pledgeAgainstDispute(
_request: Request["prophetData"],
_dispute: Dispute["prophetData"],
): Promise<void>;
settleDispute(
_request: Request["prophetData"],
_response: Response["prophetData"],
_dispute: Dispute["prophetData"],
): Promise<void>;
escalateDispute(
_request: Request["prophetData"],
_response: Response["prophetData"],
_dispute: Dispute["prophetData"],
): Promise<void>;
finalize(_request: Request["prophetData"], _response: Response["prophetData"]): Promise<void>;
}

export interface IProtocolProvider {
write: IWriteProvider;
read: IReadProvider;
}
3 changes: 2 additions & 1 deletion packages/automated-dispute/tests/eboActor/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Address } from "viem";

import { Request, RequestId } from "../../src/types/prophet";

export const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
export const mockedPrivateKey =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

export const DEFAULT_MOCKED_PROTOCOL_CONTRACTS = {
oracle: "0x123456" as Address,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import mocks from "../mocks/index.js";
import {
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
DEFAULT_MOCKED_REQUEST_CREATED_DATA,
privateKey,
mockedPrivateKey,
} from "./fixtures.js";

const logger: ILogger = mocks.mockLogger();
Expand Down Expand Up @@ -53,7 +53,7 @@ describe("EboActor", () => {
protocolProvider = new ProtocolProvider(
["http://localhost:8538"],
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
privateKey,
mockedPrivateKey,
);

const chainRpcUrls = new Map<Caip2ChainId, string[]>();
Expand Down
4 changes: 2 additions & 2 deletions packages/automated-dispute/tests/eboActorsManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ProtocolProvider } from "../src/protocolProvider.js";
import {
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
DEFAULT_MOCKED_REQUEST_CREATED_DATA,
privateKey,
mockedPrivateKey,
} from "./eboActor/fixtures.js";
import mocks from "./mocks/index.js";

Expand All @@ -32,7 +32,7 @@ describe("EboActorsManager", () => {
protocolProvider = new ProtocolProvider(
protocolProviderRpcUrls,
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
privateKey,
mockedPrivateKey,
);

const blockNumberRpcUrls = new Map<Caip2ChainId, string[]>([
Expand Down
4 changes: 2 additions & 2 deletions packages/automated-dispute/tests/mocks/eboActor.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { EboActor } from "../../src/eboActor.js";
import { ProtocolProvider } from "../../src/protocolProvider.js";
import { EboMemoryRegistry } from "../../src/services/index.js";
import { Dispute, Request, Response } from "../../src/types/index.js";
import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS, privateKey } from "../eboActor/fixtures.js";
import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS, mockedPrivateKey } from "../eboActor/fixtures.js";

/**
* Builds a base `EboActor` scaffolded with all its dependencies.
Expand All @@ -23,7 +23,7 @@ export function buildEboActor(request: Request, logger: ILogger) {
const protocolProvider = new ProtocolProvider(
protocolProviderRpcUrls,
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
privateKey,
mockedPrivateKey,
);

const blockNumberRpcUrls = new Map<Caip2ChainId, string[]>([
Expand Down
4 changes: 2 additions & 2 deletions packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { ILogger } from "@ebo-agent/shared";
import { EboActorsManager } from "../../src/eboActorsManager";
import { ProtocolProvider } from "../../src/protocolProvider";
import { EboProcessor } from "../../src/services";
import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS, privateKey } from "../eboActor/fixtures";
import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS, mockedPrivateKey } from "../eboActor/fixtures";

export function buildEboProcessor(logger: ILogger) {
const protocolProviderRpcUrls = ["http://localhost:8538"];
const protocolProvider = new ProtocolProvider(
protocolProviderRpcUrls,
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
privateKey,
mockedPrivateKey,
);

const blockNumberRpcUrls = new Map<Caip2ChainId, string[]>([
Expand Down
Loading