-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: Implement EBORequestCreator.createRequests function #35
Conversation
GRT-140 Implement EBORequestCreator RPC calls
Create the RPC call to handle the following functions:
|
There was a problem hiding this 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: [] }, | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
]; | |
] as const; |
without const assertion, you're missing viem type inference
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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}`, |
There was a problem hiding this comment.
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?
if (!this.eboRequestCreatorContract?.write?.createRequests) { | ||
throw new Error("createRequests function is not available on the ABI"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this actually possible?
There was a problem hiding this comment.
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
There was a problem hiding this 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[]>>; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
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; | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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 😛
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; | |
export const mockedPrivateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; |
} 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; | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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 😉
There was a problem hiding this comment.
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");
}
}
}```
Changes
Left to doStill 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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):
typeof this.readClient, | |
typeof this.writeClient, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
const mockRpcUrls = ["http://localhost:8545"]; | ||
const mockContractAddress: ProtocolContractsAddresses = { | ||
oracle: "0x1234567890123456789012345678901234567890", | ||
epochManager: "0x1234567890123456789012345678901234567890", | ||
eboRequestCreator: "0x1234567890123456789012345678901234567890", | ||
}; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
There was a problem hiding this 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"; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
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}`); | ||
} |
There was a problem hiding this comment.
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...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
// TODO: these constants should be env vars | ||
const TRANSACTION_RECEIPT_CONFIRMATIONS = 1; | ||
const TIMEOUT = 10000; | ||
const RETRY_INTERVAL = 150; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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}`); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
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 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GO GO GO 🫱 🫱 🫱
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cograts! First PR 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🫡🫡
🤖 Linear
Closes GRT-140
Description
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.