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

Conversation

jahabeebs
Copy link
Collaborator

🤖 Linear

Closes GRT-140

Description

  • Adds a new createWalletClient that we initialize with a new privateKey argument to the constructor

My thinking for this is it's better to have separate read and write clients so that if we end up switching to different read RPCs or something it doesn't affect the write operations--or do you think it's better to consolidate?

^ Note we're going to do a big error refactoring later so that we can handle the 80+ errors more efficiently

  • Vitest tests for the new exceptions & createRequest function

  • Added the new errors to eboActor.ts since these errors could occur when the createRequest event is emitted--not sure if we're ready to actually implement these errors other than the current logging though.

@jahabeebs jahabeebs self-assigned this Sep 4, 2024
Copy link

linear bot commented Sep 4, 2024

GRT-140 Implement EBORequestCreator RPC calls

Create the RPC call to handle the following functions:

  • Implement request creation using EBORequestCreator.createRequests function (link)
  • Model all revert errors thrown by the smart contract function as they will be needed during the process handling

Copy link
Collaborator

@0xnigir1 0xnigir1 left a comment

Choose a reason for hiding this comment

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

left some comments, also i wanted to raise that we probably need to add Wonder Crypto Checks to the repo now the we are working with PKs: https://github.com/defi-wonderland/crypto-husky-checks

{ type: "error", name: "EBORequestCreator_ChainNotAdded", inputs: [] },
{ type: "error", name: "EBORequestCreator_InvalidEpoch", inputs: [] },
{ type: "error", name: "EBORequestCreator_InvalidNonce", inputs: [] },
];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
];
] as const;

without const assertion, you're missing viem type inference

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The crypto-husky-checks install instructions didn't work, might need to add it manually--the key I added is a mock from the viem docs just fyi

added the const assertion

Copy link
Collaborator

Choose a reason for hiding this comment

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

it failed? i think we have to tell Security team
cc @0xkenj1

constructor(
rpcUrls: string[],
contracts: ProtocolContractsAddresses,
privateKey: `0x${string}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

it's nicer to use Hex type from Viem, wdyt?

Comment on lines 222 to 224
if (!this.eboRequestCreatorContract?.write?.createRequests) {
throw new Error("createRequests function is not available on the ABI");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this actually possible?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ended up not needing this validationwith the new way we structured it

Copy link
Collaborator

@0xyaco 0xyaco left a comment

Choose a reason for hiding this comment

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

Nice start! Left some comments

EBORequestCreator_InvalidEpoch,
EBORequestModule_InvalidRequester,
Oracle_InvalidRequestBody,
} from "./exceptions/index.js";
import { RpcUrlsEmpty } from "./exceptions/rpcUrlsEmpty.exception.js";
import { ProtocolContractsAddresses } from "./types/protocolProvider.js";

export class ProtocolProvider {
private client: PublicClient<FallbackTransport<HttpTransport[]>>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we should rename this to publicClient just to make it even more explicit

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I changed it to readClient and writeClient--thoughts?

if (!this.eboRequestCreatorContract?.write?.createRequests) {
throw new Error("createRequests function is not available on the ABI");
}
await this.eboRequestCreatorContract.write.createRequests([epoch, chains]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I might be wrong but ideally, we should first simulate[1] the transaction and then, if everything went as expected (ie no reverts) write it[2].

I think this can be achieved with something along the lines of:

const { result } = await this.eboRequestCreatorContract.simulate.createRequest([epoch, chains])

[1] https://viem.sh/docs/contract/simulateContract
[2] https://viem.sh/docs/contract/writeContract

Copy link
Collaborator

Choose a reason for hiding this comment

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

We also need to wait for tx receipt to be sure things went ok! Parameters as timeout or number of confirms can be decided later. [3]

[3] https://viem.sh/docs/actions/public/waitForTransactionReceipt#waitfortransactionreceipt

Copy link
Collaborator

Choose a reason for hiding this comment

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

Regarding first comment, there is no need to simulate, that might be useful for staticCalls, but we actually want to write on the contract.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@0xkenj1 there's a yellow block warning in the link [2] that states the following:

The writeContract internally sends a transaction – it does not validate if the contract write will succeed (the contract may throw an error). It is highly recommended to simulate the contract write with simulateContract before you execute it.

I could be getting things wrong here but what I understand from that piece of doc is that viem allows you to submit a write transaction regarding the final result (successful or reverted). The RPC will surely not modify any contract's internal state but it might consume gas + leave some transaction traces on-chain. Simulating could help us avoid that gas consumption (as an extra, simulating write calls uses the public client so definitely no gas consumed).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree with @0xyaco it's best to simulate the contract and this lets us just pass in that request to a write method afterward--there's no downside. I added the logic for this following the viem docs

Comment on lines 227 to 237
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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Throwing the idea out in the wild, open to discussion: what do you think about wrapping all these errors under one single error class that contains the revert data when the error is instanceof ContractFunctionRevertedError?[4]

I can only think about the revertName right now as the relevant properties for this single error but we might end up adding things like block number, RPC timestamp, stuff like that.

[4] https://viem.sh/docs/contract/simulateContract.html#handling-custom-errors

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree we need to wrap these errors--and particularly in a way that is easier to mock because the way that viem suggests is pretty awkward:

catch (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;
        }

I think we should do the error refactoring in a separate issue because it'll become a lot if we start adding our existing errors to a new error class too

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sounds good!

@@ -2,9 +2,12 @@ import { Address } from "viem";

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

export const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hoping this is not a real private key 😛

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

viem uses this private key in their docs. I changed the name to mockedPrivateKey to avoid heart attacks 😉

@@ -2,9 +2,12 @@ import { Address } from "viem";

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

export const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
Copy link
Collaborator

Choose a reason for hiding this comment

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

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

Comment on lines 226 to 237
} 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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if this actually works. Take a look at this .

https://viem.sh/docs/contract/simulateContract#handling-custom-errors

Copy link
Collaborator

@0xkenj1 0xkenj1 Sep 4, 2024

Choose a reason for hiding this comment

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

In your code your are considering that viem already knows about errors, and thats imposible 🤣 . Probably viem when throws just uses a generic error like ContractRevertError with the error name of the custom error and the arguments. :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In your code your are considering that viem already knows about errors, and thats imposible 🤣 . Probably viem when throws just uses a generic error like ContractRevertError with the error name of the custom error and the arguments. :)

Yes I thought viem was smart enough to bubble up the error since the custom error is in the ABI, and they do indeed pass the error name back--I changed my code to implement it the way recommended by viem:

catch (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;
        }

Copy link
Collaborator

@0xkenj1 0xkenj1 Sep 5, 2024

Choose a reason for hiding this comment

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

What about creating an error factory, just an idea 😉

Copy link
Collaborator

Choose a reason for hiding this comment

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

Take a look at this example, we can do sth similar i think

class TransportFactory {
  public static createTransport(type: string): Transport {
    if (type === "car") {
      return new Car();
    } else if (type === "bike") {
      return new Bike();
    } else {
      throw new Error("Unknown transport type");
    }
  }
}```

@jahabeebs
Copy link
Collaborator Author

Changes

  • Error handling now using simulations and viem should now properly bubble up the errors--I think we'll tackle a separate error handling class in a different PR since we have errors throughout all the monorepos
  • Refactored to have a readClient and writeClient so we can make requests like this:
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);
  • Similarly, added IWriteProvider and IReadProvider interfaces and the IProtocolProvider interface suggested by Kenji:
export interface IProtocolProvider {
    write: IWriteProvider;
    read: IReadProvider;
}

Left to do

Still need to replace the ABI with the canary versions of the prophet library, need to add the husky key sh script manually (package install instructions not working correctly) and any other suggestions you have

@@ -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

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 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


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 (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

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.

Comment on lines 251 to 256
const mockRpcUrls = ["http://localhost:8545"];
const mockContractAddress: ProtocolContractsAddresses = {
oracle: "0x1234567890123456789012345678901234567890",
epochManager: "0x1234567890123456789012345678901234567890",
eboRequestCreator: "0x1234567890123456789012345678901234567890",
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

You could declare these values outside the it calls but inside the describe so you can reutilize them

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jahabeebs jahabeebs requested a review from 0xyaco September 6, 2024 21:29
Copy link
Collaborator

@0xyaco 0xyaco left a comment

Choose a reason for hiding this comment

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

Just some minor comments and after that the PR is ready to go! 🚀

@@ -0,0 +1,31 @@
import { EBORequestCreator_ChainNotAdded } from "./chainNotAdded.exception.js";
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a service, even if it's an "error" related service. Let's move this class into the src/services folder, keeping the error classes inside src/exceptions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines 18 to 29
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();
default:
return new Error(`Unknown error: ${errorName}`);
}
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 add a TODO here as we have not defined yet the structure of each error (should they have properties inside? should they be merged into a single EboContractReverted (or similar) class? etc...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines +34 to +37
// TODO: these constants should be env vars
const TRANSACTION_RECEIPT_CONFIRMATIONS = 1;
const TIMEOUT = 10000;
const RETRY_INTERVAL = 150;
Copy link
Collaborator

Choose a reason for hiding this comment

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

🫶

default:
throw new Error(`Unknown error: ${errorName}`);
}
throw ErrorFactory.createError(errorName);
Copy link
Collaborator

Choose a reason for hiding this comment

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

👌 👌

case "EBORequestCreator_ChainNotAdded":
return new EBORequestCreator_ChainNotAdded();
default:
return new Error(`Unknown error: ${errorName}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

As an extra, we'll probably want to define a "base" contract reverted error here so we are able to distinguish it from other errors. Feel free to add another TODO as we'll tackle this during the core error handling implementation in another PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@0xnigir1
Copy link
Collaborator

0xnigir1 commented Sep 9, 2024

it's looking good to me 💎 but let's wait for Yaco and Kenji reviews too that digged deeper on the PR

regarding the husky-check, add a Linear issue to not forget about it but we'll have to wait for it to be fixed as you said 👍

@jahabeebs jahabeebs requested a review from 0xyaco September 10, 2024 15:48
Copy link
Collaborator

@0xyaco 0xyaco left a comment

Choose a reason for hiding this comment

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

GO GO GO 🫱 🫱 🫱

Copy link
Collaborator

@0xkenj1 0xkenj1 left a comment

Choose a reason for hiding this comment

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

Cograts! First PR 🚀

Copy link
Collaborator

@0xnigir1 0xnigir1 left a comment

Choose a reason for hiding this comment

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

🫡🫡

@jahabeebs jahabeebs merged commit baeea02 into dev Sep 11, 2024
5 checks passed
@jahabeebs jahabeebs deleted the feat/grt-140-implement-eborequestcreator-rpc-calls branch September 11, 2024 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants