diff --git a/apps/agent/src/index.ts b/apps/agent/src/index.ts index cd7548d4..7494172f 100644 --- a/apps/agent/src/index.ts +++ b/apps/agent/src/index.ts @@ -14,18 +14,19 @@ import { config } from "./config/index.js"; const logger = Logger.getInstance(); const main = async (): Promise => { - const protocolProvider = new ProtocolProvider( - config.protocolProvider.rpcsConfig, - config.protocolProvider.contracts, - config.protocolProvider.privateKey, - ); - const blockNumberService = new BlockNumberService( config.blockNumberService.chainRpcUrls, config.blockNumberService.blockmetaConfig, logger, ); + const protocolProvider = new ProtocolProvider( + config.protocolProvider.rpcsConfig, + config.protocolProvider.contracts, + config.protocolProvider.privateKey, + blockNumberService, + ); + const actorsManager = new EboActorsManager(); const notifier = await DiscordNotifier.create( diff --git a/packages/automated-dispute/src/exceptions/blockNumberServiceRequired.exception.ts b/packages/automated-dispute/src/exceptions/blockNumberServiceRequired.exception.ts new file mode 100644 index 00000000..ef8f5150 --- /dev/null +++ b/packages/automated-dispute/src/exceptions/blockNumberServiceRequired.exception.ts @@ -0,0 +1,6 @@ +export class BlockNumberServiceRequiredError extends Error { + constructor() { + super("BlockNumberService is required to get the current epoch"); + this.name = "BlockNumberServiceRequiredError"; + } +} diff --git a/packages/automated-dispute/src/exceptions/index.ts b/packages/automated-dispute/src/exceptions/index.ts index f93c3605..e043d0a0 100644 --- a/packages/automated-dispute/src/exceptions/index.ts +++ b/packages/automated-dispute/src/exceptions/index.ts @@ -16,5 +16,6 @@ export * from "./invalidBlockRangeError.exception.js"; export * from "./unknownCustomError.exception.js"; export * from "./invalidBlockHash.exception.js"; export * from "./unknownDisputeStatus.exception.js"; +export * from "./blockNumberServiceRequired.exception.js"; export * from "./customContractError.js"; export * from "./errorFactory.js"; diff --git a/packages/automated-dispute/src/providers/protocolProvider.ts b/packages/automated-dispute/src/providers/protocolProvider.ts index 40f9d9ba..e5179e9e 100644 --- a/packages/automated-dispute/src/providers/protocolProvider.ts +++ b/packages/automated-dispute/src/providers/protocolProvider.ts @@ -1,4 +1,4 @@ -import { UnsupportedChain } from "@ebo-agent/blocknumber"; +import { BlockNumberService, UnsupportedChain } from "@ebo-agent/blocknumber"; import { Caip2ChainId, HexUtils, UnixTimestamp } from "@ebo-agent/shared"; import { Address, @@ -41,6 +41,7 @@ import { oracleAbi, } from "../abis/index.js"; import { + BlockNumberServiceRequiredError, ErrorFactory, InvalidAccountOnClient, InvalidBlockHashError, @@ -75,6 +76,7 @@ export class ProtocolProvider implements IProtocolProvider { private l1ReadClient: PublicClient>; private l2ReadClient: PublicClient>; private l2WriteClient: WalletClient>; + private readonly blockNumberService?: BlockNumberService; private oracleContract: GetContractReturnType< typeof oracleAbi, @@ -111,11 +113,13 @@ export class ProtocolProvider implements IProtocolProvider { * @param rpcConfig The configuration for RPC connections including URLs, timeout, retry interval, and transaction receipt confirmations * @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 + * @param blockNumberService The service that will be used to fetch block numbers */ constructor( private readonly rpcConfig: ProtocolRpcConfig, contracts: ProtocolContractsAddresses, privateKey: Hex, + blockNumberService?: BlockNumberService, ) { const l1Chain = this.getViemChain(rpcConfig.l1.chainId); const l2Chain = this.getViemChain(rpcConfig.l2.chainId); @@ -123,6 +127,7 @@ export class ProtocolProvider implements IProtocolProvider { this.l1ReadClient = this.createReadClient(rpcConfig.l1, l1Chain); this.l2ReadClient = this.createReadClient(rpcConfig.l2, l2Chain); this.l2WriteClient = this.createWriteClient(rpcConfig.l2, l2Chain, privateKey); + this.blockNumberService = blockNumberService; // Instantiate all the protocol contracts this.oracleContract = getContract({ @@ -269,6 +274,10 @@ export class ProtocolProvider implements IProtocolProvider { * @returns {Promise} The current epoch, its block number, and its timestamp. */ async getCurrentEpoch(): Promise { + if (!this.blockNumberService) { + throw new BlockNumberServiceRequiredError(); + } + const [epoch, epochFirstBlockNumber] = await Promise.all([ this.epochManagerContract.read.currentEpoch(), this.epochManagerContract.read.currentEpochBlock(), @@ -278,10 +287,18 @@ export class ProtocolProvider implements IProtocolProvider { blockNumber: epochFirstBlockNumber, }); + const startTimestamp = epochFirstBlock.timestamp as UnixTimestamp; + + const l2ChainId = this.rpcConfig.l2.chainId; + const l2FirstBlockNumber = await this.blockNumberService.getEpochBlockNumber( + startTimestamp, + l2ChainId, + ); + return { number: epoch, - firstBlockNumber: epochFirstBlockNumber, - startTimestamp: epochFirstBlock.timestamp as UnixTimestamp, + firstBlockNumber: l2FirstBlockNumber, + startTimestamp: startTimestamp, }; } diff --git a/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts b/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts index 7249c31a..b93ceb53 100644 --- a/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts +++ b/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts @@ -1,9 +1,10 @@ -import { BlockNumberService, Caip2ChainId } from "@ebo-agent/blocknumber"; -import { ILogger } from "@ebo-agent/shared"; +import { BlockNumberService } from "@ebo-agent/blocknumber"; +import { Caip2ChainId, ILogger } from "@ebo-agent/shared"; import { vi } from "vitest"; +import { NotificationService } from "../../src/index.js"; import { ProtocolProvider } from "../../src/providers/index.js"; -import { EboProcessor, NotificationService } from "../../src/services"; +import { EboProcessor } from "../../src/services"; import { EboActorsManager } from "../../src/services/index.js"; import { AccountingModules } from "../../src/types/prophet.js"; import { @@ -20,6 +21,24 @@ export function buildEboProcessor( }, notifier?: NotificationService, ) { + const blockNumberRpcUrls = new Map([ + ["eip155:1" as Caip2ChainId, ["http://localhost:8539"]], + ]); + + const blockNumberService = new BlockNumberService( + blockNumberRpcUrls, + { + baseUrl: new URL("http://localhost"), + bearerToken: "secret-token", + bearerTokenExpirationWindow: 10, + servicePaths: { + block: "/block", + blockByTime: "/blockbytime", + }, + }, + logger, + ); + const protocolProvider = new ProtocolProvider( { l1: { @@ -39,23 +58,7 @@ export function buildEboProcessor( }, DEFAULT_MOCKED_PROTOCOL_CONTRACTS, mockedPrivateKey, - ); - - const blockNumberRpcUrls = new Map([ - ["eip155:1" as Caip2ChainId, ["http://localhost:8539"]], - ]); - const blockNumberService = new BlockNumberService( - blockNumberRpcUrls, - { - baseUrl: new URL("http://localhost"), - bearerToken: "secret-token", - bearerTokenExpirationWindow: 10, - servicePaths: { - block: "/block", - blockByTime: "/blockbytime", - }, - }, - logger, + blockNumberService, ); const actorsManager = new EboActorsManager(); diff --git a/packages/automated-dispute/tests/services/protocolProvider.spec.ts b/packages/automated-dispute/tests/services/protocolProvider.spec.ts index ef4326a1..ba62d243 100644 --- a/packages/automated-dispute/tests/services/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/services/protocolProvider.spec.ts @@ -1,3 +1,4 @@ +import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Caip2ChainId, HexUtils } from "@ebo-agent/shared"; import { Address, @@ -22,12 +23,13 @@ import { oracleAbi, } from "../../src/abis/index.js"; import { + BlockNumberServiceRequiredError, InvalidAccountOnClient, RpcUrlsEmpty, TransactionExecutionError, } from "../../src/exceptions/index.js"; +import { ProtocolProvider } from "../../src/index.js"; import { ProtocolContractsAddresses } from "../../src/interfaces/index.js"; -import { ProtocolProvider } from "../../src/providers/index.js"; import { EboEvent } from "../../src/types/index.js"; import { DEFAULT_MOCKED_DISPUTE_DATA, @@ -51,14 +53,14 @@ vi.mock("viem", async () => { describe("ProtocolProvider", () => { const mockRpcConfig = { l1: { - chainId: "eip155:1", + chainId: "eip155:1" as Caip2ChainId, urls: ["http://localhost:8545"], retryInterval: 1, timeout: 100, transactionReceiptConfirmations: 1, }, l2: { - chainId: "eip155:42161", + chainId: "eip155:42161" as Caip2ChainId, urls: ["http://localhost:8546"], retryInterval: 1, timeout: 100, @@ -74,6 +76,10 @@ describe("ProtocolProvider", () => { horizonAccountingExtension: "0x1234567890123456789012345678901234567890", }; + const mockBlockNumberService = { + getEpochBlockNumber: vi.fn().mockResolvedValue(100n), + } as unknown as BlockNumberService; + beforeEach(() => { (getContract as Mock).mockImplementation(({ address, abi }) => { if (abi === oracleAbi && address === mockContractAddress.oracle) { @@ -164,6 +170,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); expect(createPublicClient).toHaveBeenCalledWith({ @@ -219,6 +226,7 @@ describe("ProtocolProvider", () => { { ...mockRpcConfig, l1: { ...mockRpcConfig.l1, urls: [] } }, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ), ).toThrowError(RpcUrlsEmpty); }); @@ -230,6 +238,7 @@ describe("ProtocolProvider", () => { { ...mockRpcConfig, l2: { ...mockRpcConfig.l2, urls: [] } }, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ), ).toThrowError(RpcUrlsEmpty); }); @@ -243,13 +252,19 @@ describe("ProtocolProvider", () => { describe("getCurrentEpoch", () => { it("returns currentEpoch and currentEpochBlock successfully", async () => { const mockEpoch = BigInt(1); - const mockEpochBlock = BigInt(12345); + const mockEpochBlock_L1 = BigInt(12345); + const mockEpochBlock_L2 = BigInt(67890); const mockEpochTimestamp = BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) / 1000n; + const mockBlockNumberService = { + getEpochBlockNumber: vi.fn().mockResolvedValue(mockEpochBlock_L2), + } as unknown as BlockNumberService; + const protocolProvider = new ProtocolProvider( mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["epochManagerContract"].read.currentEpoch as Mock).mockResolvedValue( @@ -258,7 +273,7 @@ describe("ProtocolProvider", () => { ( protocolProvider["epochManagerContract"].read.currentEpochBlock as Mock - ).mockResolvedValue(mockEpochBlock); + ).mockResolvedValue(mockEpochBlock_L1); (protocolProvider["l1ReadClient"].getBlock as Mock).mockResolvedValue({ timestamp: mockEpochTimestamp, @@ -267,8 +282,26 @@ describe("ProtocolProvider", () => { const result = await protocolProvider.getCurrentEpoch(); expect(result.number).toBe(mockEpoch); - expect(result.firstBlockNumber).toBe(mockEpochBlock); + expect(result.firstBlockNumber).toBe(mockEpochBlock_L2); expect(result.startTimestamp).toBe(mockEpochTimestamp); + + expect(mockBlockNumberService.getEpochBlockNumber).toHaveBeenCalledWith( + mockEpochTimestamp, + mockRpcConfig.l2.chainId, + ); + }); + + it("throws BlockNumberServiceRequiredError when blockNumberService is not provided", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + undefined, + ); + + await expect(protocolProvider.getCurrentEpoch()).rejects.toThrow( + BlockNumberServiceRequiredError, + ); }); it("throws when current epoch request fails", async () => { @@ -276,6 +309,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const error = new Error("Failed to get current epoch"); const mockEpochBlock = BigInt(12345); @@ -295,6 +329,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const error = new Error("Failed to get current epoch block"); const mockEpoch = BigInt(12345); @@ -321,6 +356,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockRequestProphetData = DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData; @@ -336,6 +372,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -355,6 +392,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2WriteClient"].writeContract as Mock).mockRejectedValue( @@ -374,6 +412,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].simulateContract as Mock).mockRejectedValue( @@ -396,6 +435,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockRejectedValue( @@ -417,6 +457,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockRequestProphetData = DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData; @@ -437,6 +478,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -463,6 +505,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockRequestProphetData = DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData; @@ -483,6 +526,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -519,6 +563,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockRequestProphetData = DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData; @@ -534,6 +579,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -555,6 +601,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockEpoch = 1n; @@ -590,6 +637,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const expectedAddress = privateKeyToAccount(mockedPrivateKey).address; @@ -601,6 +649,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2WriteClient"] as any).account = undefined; @@ -615,6 +664,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockRequestProphetData = DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData; @@ -630,6 +680,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -651,6 +702,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockRequestProphetData = DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData; @@ -669,6 +721,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -693,6 +746,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockRequestProphetData = DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData; @@ -713,6 +767,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -739,6 +794,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockModuleAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; @@ -766,6 +822,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); (protocolProvider["l2ReadClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ @@ -786,6 +843,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockUserAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; @@ -811,6 +869,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockUserAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; @@ -832,6 +891,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); const mockResponseProposedEvents = [ @@ -900,6 +960,7 @@ describe("ProtocolProvider", () => { mockRpcConfig, mockContractAddress, mockedPrivateKey, + mockBlockNumberService, ); vi.spyOn(HexUtils, "normalize").mockReturnValue("0x01");