-
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
Changes from 2 commits
b506597
97bff8c
6f2a0de
4e85924
20c6636
24ca7b1
9bd4286
e548352
1696d48
f296f4d
4e42502
0432968
e6bdfcc
ff5ab29
d813f46
4bd8c86
848efa7
f77aad9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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, | ||||||
|
@@ -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, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seeing the initialization of the Does this There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
but I could have just changed the client line to
|
||||||
Address | ||||||
>; | ||||||
|
||||||
|
@@ -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({ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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, | ||||||
|
@@ -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 | ||||||
* | ||||||
|
@@ -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, | ||||||
}); | ||||||
|
||||||
|
@@ -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; | ||||||
} | ||||||
|
@@ -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, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's create a const value with this // 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 commentThe 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; | ||||||
} | ||||||
} | ||||||
|
||||||
|
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've got the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
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 feedviem
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:
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