diff --git a/packages/automated-dispute/package.json b/packages/automated-dispute/package.json index 1fc9a72..a23558c 100644 --- a/packages/automated-dispute/package.json +++ b/packages/automated-dispute/package.json @@ -17,7 +17,8 @@ "author": "", "license": "ISC", "dependencies": { - "viem": "2.17.11", - "@ebo-agent/blocknumber": "workspace:*" + "@ebo-agent/blocknumber": "workspace:*", + "@ebo-agent/shared": "workspace:*", + "viem": "2.17.11" } } diff --git a/packages/automated-dispute/src/eboActor.ts b/packages/automated-dispute/src/eboActor.ts index f3ff7f5..ffc91c3 100644 --- a/packages/automated-dispute/src/eboActor.ts +++ b/packages/automated-dispute/src/eboActor.ts @@ -1,23 +1,121 @@ import { BlockNumberService } from "@ebo-agent/blocknumber"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; +import { ILogger } from "@ebo-agent/shared"; +import { ContractFunctionRevertedError } from "viem"; +import { InvalidActorState } from "./exceptions/invalidActorState.exception.js"; +import { RequestMismatch } from "./exceptions/requestMismatch.js"; +import { EboRegistry } from "./interfaces/eboRegistry.js"; import { ProtocolProvider } from "./protocolProvider.js"; import { EboEvent } from "./types/events.js"; import { Dispute, Response } from "./types/prophet.js"; export class EboActor { - private requestActivity: unknown[]; - constructor( private readonly protocolProvider: ProtocolProvider, private readonly blockNumberService: BlockNumberService, + private readonly registry: EboRegistry, private readonly requestId: string, - ) { - this.requestActivity = []; + private readonly logger: ILogger, + ) {} + + /** + * Handle RequestCreated event. + * + * @param event RequestCreated event + */ + public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise { + if (event.metadata.requestId != this.requestId) + throw new RequestMismatch(this.requestId, event.metadata.requestId); + + if (this.registry.getRequest(event.metadata.requestId)) { + this.logger.error( + `The request ${event.metadata.requestId} was already being handled by an actor.`, + ); + + throw new InvalidActorState(); + } + + this.registry.addRequest(event.metadata.requestId, event.metadata.request); + + if (this.anyActiveProposal()) { + // Skipping new proposal until the actor receives a ResponseDisputed event; + // at that moment, it will be possible to re-propose again. + this.logger.info( + `There is an active proposal for request ${this.requestId}. Skipping...`, + ); + + return; + } + + const { chainId } = event.metadata; + const { currentEpoch, currentEpochTimestamp } = + await this.protocolProvider.getCurrentEpoch(); + + const epochBlockNumber = await this.blockNumberService.getEpochBlockNumber( + currentEpochTimestamp, + chainId, + ); + + if (this.alreadyProposed(currentEpoch, chainId, epochBlockNumber)) return; + + try { + await this.protocolProvider.proposeResponse( + this.requestId, + currentEpoch, + chainId, + epochBlockNumber, + ); + } catch (err) { + if (err instanceof ContractFunctionRevertedError) { + this.logger.warn( + `Block ${epochBlockNumber} for epoch ${currentEpoch} and ` + + `chain ${chainId} was not proposed. Skipping proposal...`, + ); + } else { + this.logger.error( + `Actor handling request ${this.requestId} is not able to continue.`, + ); + + throw err; + } + } } - public async onRequestCreated(_event: EboEvent<"RequestCreated">): Promise { - // TODO: implement - return; + /** + * Check if there's at least one proposal that has not received any dispute yet. + * + * @returns + */ + private anyActiveProposal() { + // TODO: implement this function + return false; + } + + /** + * Check if the same proposal has already been made in the past. + * + * @param epoch epoch of the request + * @param chainId chain id of the request + * @param blockNumber proposed block number + * @returns true if there's a registry of a proposal with the same attributes, false otherwise + */ + private alreadyProposed(epoch: bigint, chainId: Caip2ChainId, blockNumber: bigint) { + const responses = this.registry.getResponses(); + + for (const [responseId, response] of responses) { + if (response.response.block != blockNumber) continue; + if (response.response.chainId != chainId) continue; + if (response.response.epoch != epoch) continue; + + this.logger.info( + `Block ${blockNumber} for epoch ${epoch} and chain ${chainId} already proposed on response ${responseId}. Skipping...`, + ); + + return true; + } + + return false; } public async onResponseProposed(_event: EboEvent<"ResponseDisputed">): Promise { diff --git a/packages/automated-dispute/src/eboMemoryRegistry.ts b/packages/automated-dispute/src/eboMemoryRegistry.ts new file mode 100644 index 0000000..85c5b49 --- /dev/null +++ b/packages/automated-dispute/src/eboMemoryRegistry.ts @@ -0,0 +1,25 @@ +import { EboRegistry } from "./interfaces/eboRegistry.js"; +import { Dispute, Request, Response } from "./types/prophet.js"; + +export class EboMemoryRegistry implements EboRegistry { + constructor( + private requests: Map = new Map(), + private responses: Map = new Map(), + private dispute: Map = new Map(), + ) {} + + /** @inheritdoc */ + public addRequest(requestId: string, request: Request) { + this.requests.set(requestId, request); + } + + /** @inheritdoc */ + public getRequest(requestId: string) { + return this.requests.get(requestId); + } + + /** @inheritdoc */ + public getResponses() { + return this.responses; + } +} diff --git a/packages/automated-dispute/src/exceptions/index.ts b/packages/automated-dispute/src/exceptions/index.ts index 7e1aa47..4050512 100644 --- a/packages/automated-dispute/src/exceptions/index.ts +++ b/packages/automated-dispute/src/exceptions/index.ts @@ -1 +1,2 @@ export * from "./rpcUrlsEmpty.exception.js"; +export * from "./invalidActorState.exception.js"; diff --git a/packages/automated-dispute/src/exceptions/invalidActorState.exception.ts b/packages/automated-dispute/src/exceptions/invalidActorState.exception.ts new file mode 100644 index 0000000..74f8d47 --- /dev/null +++ b/packages/automated-dispute/src/exceptions/invalidActorState.exception.ts @@ -0,0 +1,8 @@ +export class InvalidActorState extends Error { + constructor() { + // TODO: we'll want to dump the Actor state into stderr at this point + super("The actor is in an invalid state."); + + this.name = "InvalidActorState"; + } +} diff --git a/packages/automated-dispute/src/exceptions/requestMismatch.ts b/packages/automated-dispute/src/exceptions/requestMismatch.ts new file mode 100644 index 0000000..24b3225 --- /dev/null +++ b/packages/automated-dispute/src/exceptions/requestMismatch.ts @@ -0,0 +1,6 @@ +export class RequestMismatch extends Error { + constructor(requestId: string, eventRequestId: string) { + super(`Actor handling request ${requestId} received a request ${eventRequestId} event.`); + this.name = "RequestMismatch"; + } +} diff --git a/packages/automated-dispute/src/interfaces/eboRegistry.ts b/packages/automated-dispute/src/interfaces/eboRegistry.ts new file mode 100644 index 0000000..2bc51f7 --- /dev/null +++ b/packages/automated-dispute/src/interfaces/eboRegistry.ts @@ -0,0 +1,27 @@ +import { Request, Response } from "../types/prophet.js"; + +/** Registry that stores Prophet entities (ie. requests, responses and disputes) */ +export interface EboRegistry { + /** + * Add a `Request` by ID. + * + * @param requestId the ID of the `Request` + * @param request the `Request` + */ + addRequest(requestId: string, request: Request): void; + + /** + * Get a `Request` by ID. + * + * @param requestId request ID + * @returns the request if already added into registry, `undefined` otherwise + */ + getRequest(requestId: string): Request | undefined; + + /** + * Return all responses + * + * @returns responses map + */ + getResponses(): Map; +} diff --git a/packages/automated-dispute/src/protocolProvider.ts b/packages/automated-dispute/src/protocolProvider.ts index 001124b..0fa469f 100644 --- a/packages/automated-dispute/src/protocolProvider.ts +++ b/packages/automated-dispute/src/protocolProvider.ts @@ -1,3 +1,5 @@ +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; +import { Timestamp } from "@ebo-agent/shared"; import { Address, createPublicClient, @@ -53,17 +55,28 @@ export class ProtocolProvider { } /** - * Gets the current epoch and the block number of the current epoch - * @returns The current epoch and the block number of the current epoch + * Gets the current epoch, the block number and its timestamp of the current epoch + * + * @returns The current epoch, its block number and its timestamp */ - async getCurrentEpoch(): Promise<{ currentEpoch: bigint; currentEpochBlock: bigint }> { - const [currentEpoch, currentEpochBlock] = await Promise.all([ + async getCurrentEpoch(): Promise<{ + currentEpoch: bigint; + currentEpochBlockNumber: bigint; + currentEpochTimestamp: Timestamp; + }> { + const [currentEpoch, currentEpochBlockNumber] = await Promise.all([ this.epochManagerContract.read.currentEpoch(), this.epochManagerContract.read.currentEpochBlock(), ]); + + const currentEpochBlock = await this.client.getBlock({ + blockNumber: currentEpochBlockNumber, + }); + return { currentEpoch, - currentEpochBlock, + currentEpochBlockNumber, + currentEpochTimestamp: currentEpochBlock.timestamp, }; } @@ -79,6 +92,8 @@ export class ProtocolProvider { logIndex: 1, metadata: { requestId: "0x01", + chainId: "eip155:1", + epoch: 1n, request: { requester: "0x12345678901234567890123456789012", requestModule: "0x12345678901234567890123456789012", @@ -102,7 +117,11 @@ export class ProtocolProvider { response: { proposer: "0x12345678901234567890123456789012", requestId: "0x01", - response: "0x01234", + response: { + block: 1n, + chainId: "eip155:1", + epoch: 20n, + }, }, }, } as EboEvent<"ResponseProposed">, @@ -173,7 +192,12 @@ export class ProtocolProvider { return; } - async proposeResponse(_request: Request, _response: Response): Promise { + async proposeResponse( + _requestId: string, + _epoch: bigint, + _chainId: Caip2ChainId, + _blockNumber: bigint, + ): Promise { // TODO: implement actual method return; } diff --git a/packages/automated-dispute/src/types/events.ts b/packages/automated-dispute/src/types/events.ts index 60d194e..31cf2e0 100644 --- a/packages/automated-dispute/src/types/events.ts +++ b/packages/automated-dispute/src/types/events.ts @@ -1,6 +1,7 @@ +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; import { Log } from "viem"; -import { Dispute, Request } from "./prophet.js"; +import { Dispute, Request, Response } from "./prophet.js"; export type EboEventName = | "NewEpoch" @@ -17,14 +18,17 @@ export interface NewEpoch { epochBlockNumber: bigint; } -export interface ResponseCreated { +export interface ResponseProposed { requestId: string; - request: Request; + responseId: string; + response: Response; } export interface RequestCreated { - requestId: string; + epoch: bigint; + chainId: Caip2ChainId; request: Request; + requestId: string; } export interface ResponseDisputed { @@ -60,8 +64,8 @@ export type EboEventData = E extends "NewEpoch" ? NewEpoch : E extends "RequestCreated" ? RequestCreated - : E extends "ResponseCreated" - ? ResponseCreated + : E extends "ResponseProposed" + ? ResponseProposed : E extends "ResponseDisputed" ? ResponseDisputed : E extends "DisputeStatusChanged" diff --git a/packages/automated-dispute/src/types/prophet.ts b/packages/automated-dispute/src/types/prophet.ts index 81b3abf..c7f2364 100644 --- a/packages/automated-dispute/src/types/prophet.ts +++ b/packages/automated-dispute/src/types/prophet.ts @@ -1,3 +1,4 @@ +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; import { Address } from "viem"; export interface Request { @@ -7,13 +8,18 @@ export interface Request { disputeModule: Address; resolutionModule: Address; finalityModule: Address; - // We might need here modules' data too } export interface Response { proposer: Address; requestId: string; - response: Uint8Array; + + // To be byte-encode when sending it to Prophet + response: { + chainId: Caip2ChainId; // TODO: Pending on-chain definition on CAIP-2 usage + block: bigint; + epoch: bigint; + }; } export interface Dispute { diff --git a/packages/automated-dispute/tests/eboActor.spec.ts b/packages/automated-dispute/tests/eboActor.spec.ts index 96a7345..88d9df3 100644 --- a/packages/automated-dispute/tests/eboActor.spec.ts +++ b/packages/automated-dispute/tests/eboActor.spec.ts @@ -1,7 +1,223 @@ -import { describe } from "vitest"; +import { BlockNumberService } from "@ebo-agent/blocknumber"; +import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; +import { Logger } from "@ebo-agent/shared"; +import { Address } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { EboActor } from "../src/eboActor.js"; +import { EboMemoryRegistry } from "../src/eboMemoryRegistry.js"; +import { InvalidActorState } from "../src/exceptions/invalidActorState.exception.js"; +import { RequestMismatch } from "../src/exceptions/requestMismatch.js"; +import { ProtocolProvider } from "../src/protocolProvider.js"; +import { EboEvent } from "../src/types/events.js"; +import { Response } from "../src/types/prophet.js"; + +const logger = Logger.getInstance(); + +const protocolContracts = { + oracle: "0x123456" as Address, + epochManager: "0x654321" as Address, +}; + +const BASE_REQUEST = { + disputeModule: "0x01" as Address, + finalityModule: "0x02" as Address, + requestModule: "0x03" as Address, + resolutionModule: "0x04" as Address, + responseModule: "0x05" as Address, + requester: "0x10" as Address, +}; describe("EboActor", () => { - describe.skip("onRequestCreated"); + describe("onRequestCreated", () => { + const requestId: Address = "0x12345"; + const indexedChainId: Caip2ChainId = "eip155:137"; + + const protocolEpoch = { + currentEpoch: 1n, + currentEpochBlockNumber: 1n, + currentEpochTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)), + }; + + const requestCreatedEvent: EboEvent<"RequestCreated"> = { + blockNumber: 34n, + logIndex: 1, + name: "RequestCreated", + metadata: { + chainId: "eip155:10", + epoch: protocolEpoch.currentEpoch, + requestId: requestId, + request: BASE_REQUEST, + }, + }; + + let protocolProvider: ProtocolProvider; + let blockNumberService: BlockNumberService; + let registry: EboMemoryRegistry; + + beforeEach(() => { + protocolProvider = new ProtocolProvider(["http://localhost:8538"], protocolContracts); + + const chainRpcUrls = new Map(); + chainRpcUrls.set(indexedChainId, ["http://localhost:8539"]); + + blockNumberService = new BlockNumberService(chainRpcUrls, logger); + registry = new EboMemoryRegistry(); + }); + + it("proposes a response", async () => { + const indexedEpochBlockNumber = 48n; + + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( + indexedEpochBlockNumber, + ); + + const proposeResponseMock = vi.spyOn(protocolProvider, "proposeResponse"); + + proposeResponseMock.mockImplementation( + ( + _requestId: string, + _epoch: bigint, + _chainId: Caip2ChainId, + _blockNumbre: bigint, + ) => Promise.resolve(), + ); + + const actor = new EboActor( + protocolProvider, + blockNumberService, + registry, + requestId, + logger, + ); + + await actor.onRequestCreated(requestCreatedEvent); + + expect(proposeResponseMock).toHaveBeenCalledWith( + requestCreatedEvent.metadata.requestId, + protocolEpoch.currentEpoch, + requestCreatedEvent.metadata.chainId, + indexedEpochBlockNumber, + ); + }); + + it("does not propose when already proposed the same block", async () => { + const indexedEpochBlockNumber = 48n; + + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockResolvedValue( + indexedEpochBlockNumber, + ); + + const proposeResponseMock = vi.spyOn(protocolProvider, "proposeResponse"); + + proposeResponseMock.mockImplementation( + ( + _requestId: string, + _epoch: bigint, + _chainId: Caip2ChainId, + _blockNumbre: bigint, + ) => Promise.resolve(), + ); + + const actor = new EboActor( + protocolProvider, + blockNumberService, + registry, + requestId, + logger, + ); + + const previousResponses = new Map(); + previousResponses.set("0x01", { + proposer: "0x02", + requestId: requestId, + response: { + block: indexedEpochBlockNumber, + chainId: requestCreatedEvent.metadata.chainId, + epoch: protocolEpoch.currentEpoch, + }, + }); + + vi.spyOn(registry, "getResponses").mockReturnValue(previousResponses); + + await actor.onRequestCreated(requestCreatedEvent); + + expect(proposeResponseMock).not.toHaveBeenCalled(); + }); + + it("throws if the event's request id does not match with actor's", () => { + const noMatchRequestCreatedEvent: EboEvent<"RequestCreated"> = { + blockNumber: 34n, + logIndex: 1, + name: "RequestCreated", + metadata: { + chainId: "eip155:10", + epoch: protocolEpoch.currentEpoch, + requestId: "0x000000" as Address, + }, + }; + + const actor = new EboActor( + protocolProvider, + blockNumberService, + registry, + requestId, + logger, + ); + + expect(actor.onRequestCreated(noMatchRequestCreatedEvent)).rejects.toThrowError( + RequestMismatch, + ); + }); + + it("throws if the request was already handled by the actor", () => { + const actor = new EboActor( + protocolProvider, + blockNumberService, + registry, + requestId, + logger, + ); + + vi.spyOn(registry, "getRequest").mockReturnValue(BASE_REQUEST); + + expect(actor.onRequestCreated(requestCreatedEvent)).rejects.toThrowError( + InvalidActorState, + ); + }); + + it("throws if current epoch cannot be fetched", () => { + vi.spyOn(protocolProvider, "getCurrentEpoch").mockRejectedValue(new Error()); + + const actor = new EboActor( + protocolProvider, + blockNumberService, + registry, + requestId, + logger, + ); + + expect(actor.onRequestCreated(requestCreatedEvent)).rejects.toBeDefined(); + }); + + it("throws if the indexed chain block number cannot be fetched", () => { + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(protocolEpoch); + vi.spyOn(blockNumberService, "getEpochBlockNumber").mockRejectedValue(new Error()); + + const actor = new EboActor( + protocolProvider, + blockNumberService, + registry, + requestId, + logger, + ); + + expect(actor.onRequestCreated(requestCreatedEvent)).rejects.toBeDefined(); + }); + }); + describe.skip("onResponseProposed"); describe.skip("onResponseDisputed"); describe.skip("onFinalizeRequest"); diff --git a/packages/automated-dispute/tests/protocolProvider.spec.ts b/packages/automated-dispute/tests/protocolProvider.spec.ts index 2cf64b4..337c649 100644 --- a/packages/automated-dispute/tests/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/protocolProvider.spec.ts @@ -73,14 +73,20 @@ describe("ProtocolProvider", () => { }); describe("getCurrentEpoch", () => { it("returns currentEpoch and currentEpochBlock successfully", async () => { - const protocolProvider = new ProtocolProvider(mockRpcUrls, mockContractAddress); - const mockEpoch = BigInt(1); const mockEpochBlock = BigInt(12345); + const mockEpochTimestamp = BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)); + + (createPublicClient as Mock).mockReturnValue({ + getBlock: vi.fn().mockResolvedValue({ timestamp: mockEpochTimestamp }), + }); + + const protocolProvider = new ProtocolProvider(mockRpcUrls, mockContractAddress); (protocolProvider["epochManagerContract"].read.currentEpoch as Mock).mockResolvedValue( mockEpoch, ); + ( protocolProvider["epochManagerContract"].read.currentEpochBlock as Mock ).mockResolvedValue(mockEpochBlock); @@ -88,7 +94,7 @@ describe("ProtocolProvider", () => { const result = await protocolProvider.getCurrentEpoch(); expect(result.currentEpoch).toBe(mockEpoch); - expect(result.currentEpochBlock).toBe(mockEpochBlock); + expect(result.currentEpochBlockNumber).toBe(mockEpochBlock); }); it("throws when current epoch request fails", async () => { const protocolProvider = new ProtocolProvider(mockRpcUrls, mockContractAddress); diff --git a/packages/blocknumber/src/exceptions/unsupportedBlockNumber.ts b/packages/blocknumber/src/exceptions/unsupportedBlockNumber.ts index ace46c2..b49c356 100644 --- a/packages/blocknumber/src/exceptions/unsupportedBlockNumber.ts +++ b/packages/blocknumber/src/exceptions/unsupportedBlockNumber.ts @@ -1,5 +1,7 @@ +import { Timestamp } from "@ebo-agent/shared"; + export class UnsupportedBlockNumber extends Error { - constructor(timestamp: bigint) { + constructor(timestamp: Timestamp) { super(`Block with null block number at ${timestamp}`); this.name = "UnsupportedBlockNumber"; diff --git a/packages/blocknumber/src/providers/blockNumberProvider.ts b/packages/blocknumber/src/providers/blockNumberProvider.ts index 0b3b74e..65c3e39 100644 --- a/packages/blocknumber/src/providers/blockNumberProvider.ts +++ b/packages/blocknumber/src/providers/blockNumberProvider.ts @@ -1,3 +1,5 @@ +import { Timestamp } from "@ebo-agent/shared"; + export interface BlockNumberProvider { /** * Get the block number corresponding to the beginning of the epoch. @@ -9,5 +11,5 @@ export interface BlockNumberProvider { * * @returns the corresponding block number of a chain at a specific timestamp */ - getEpochBlockNumber(timestamp: number): Promise; + getEpochBlockNumber(timestamp: Timestamp): Promise; } diff --git a/packages/blocknumber/src/providers/evmBlockNumberProvider.ts b/packages/blocknumber/src/providers/evmBlockNumberProvider.ts index b986625..0ec9d41 100644 --- a/packages/blocknumber/src/providers/evmBlockNumberProvider.ts +++ b/packages/blocknumber/src/providers/evmBlockNumberProvider.ts @@ -1,4 +1,4 @@ -import { ILogger } from "@ebo-agent/shared"; +import { ILogger, Timestamp } from "@ebo-agent/shared"; import { Block, FallbackTransport, HttpTransport, PublicClient } from "viem"; import { @@ -56,9 +56,8 @@ export class EvmBlockNumberProvider implements BlockNumberProvider { this.firstBlock = null; } - async getEpochBlockNumber(timestamp: number): Promise { + async getEpochBlockNumber(timestamp: Timestamp): Promise { // An optimized binary search is used to look for the epoch block. - const _timestamp = BigInt(timestamp); // The EBO agent looks only for finalized blocks to avoid handling reorgs const upperBoundBlock = await this.client.getBlock({ blockTag: "finalized" }); @@ -71,16 +70,16 @@ export class EvmBlockNumberProvider implements BlockNumberProvider { const firstBlock = await this.getFirstBlock(); - if (_timestamp < firstBlock.timestamp) throw new InvalidTimestamp(_timestamp); - if (_timestamp >= upperBoundBlock.timestamp) throw new LastBlockEpoch(upperBoundBlock); + if (timestamp < firstBlock.timestamp) throw new InvalidTimestamp(timestamp); + if (timestamp >= upperBoundBlock.timestamp) throw new LastBlockEpoch(upperBoundBlock); // Reduces the search space by estimating a lower bound for the binary search. // // Performing a binary search between block 0 and last block is not efficient. - const lowerBoundBlock = await this.calculateLowerBoundBlock(_timestamp, upperBoundBlock); + const lowerBoundBlock = await this.calculateLowerBoundBlock(timestamp, upperBoundBlock); // Searches for the timestamp with a binary search - return this.searchTimestamp(_timestamp, { + return this.searchTimestamp(timestamp, { fromBlock: lowerBoundBlock.number, toBlock: upperBoundBlock.number, }); @@ -126,7 +125,7 @@ export class EvmBlockNumberProvider implements BlockNumberProvider { * @param lastBlock last block of the chain * @returns an optimized lower bound for a binary search space */ - private async calculateLowerBoundBlock(timestamp: bigint, lastBlock: BlockWithNumber) { + private async calculateLowerBoundBlock(timestamp: Timestamp, lastBlock: BlockWithNumber) { const { blocksLookback, deltaMultiplier } = this.searchConfig; const estimatedBlockTime = await this.estimateBlockTime(lastBlock, blocksLookback); @@ -192,7 +191,7 @@ export class EvmBlockNumberProvider implements BlockNumberProvider { * @returns the block number */ private async searchTimestamp( - timestamp: bigint, + timestamp: Timestamp, between: { fromBlock: bigint; toBlock: bigint }, ) { let currentBlockNumber: bigint; diff --git a/packages/blocknumber/src/services/blockNumberService.ts b/packages/blocknumber/src/services/blockNumberService.ts index 84b8aa1..1dbd1e2 100644 --- a/packages/blocknumber/src/services/blockNumberService.ts +++ b/packages/blocknumber/src/services/blockNumberService.ts @@ -1,4 +1,4 @@ -import { EBO_SUPPORTED_CHAIN_IDS, ILogger } from "@ebo-agent/shared"; +import { EBO_SUPPORTED_CHAIN_IDS, ILogger, Timestamp } from "@ebo-agent/shared"; import { createPublicClient, fallback, http } from "viem"; import { ChainWithoutProvider, EmptyRpcUrls, UnsupportedChain } from "../exceptions/index.js"; @@ -32,7 +32,7 @@ export class BlockNumberService { * @param chainId the CAIP-2 chain id * @returns the block number corresponding to the timestamp */ - public async getEpochBlockNumber(timestamp: number, chainId: Caip2ChainId): Promise { + public async getEpochBlockNumber(timestamp: Timestamp, chainId: Caip2ChainId): Promise { const provider = this.blockNumberProviders.get(chainId); if (!provider) throw new ChainWithoutProvider(chainId); @@ -49,7 +49,7 @@ export class BlockNumberService { * @param chains a list of CAIP-2 chain ids * @returns a map of CAIP-2 chain ids */ - public async getEpochBlockNumbers(timestamp: number, chains: Caip2ChainId[]) { + public async getEpochBlockNumbers(timestamp: Timestamp, chains: Caip2ChainId[]) { const epochBlockNumbers = await Promise.all( chains.map(async (chain) => ({ chainId: chain, diff --git a/packages/blocknumber/test/providers/evmBlockNumberProvider.spec.ts b/packages/blocknumber/test/providers/evmBlockNumberProvider.spec.ts index 5bf18fd..78e7c7b 100644 --- a/packages/blocknumber/test/providers/evmBlockNumberProvider.spec.ts +++ b/packages/blocknumber/test/providers/evmBlockNumberProvider.spec.ts @@ -25,7 +25,7 @@ describe("EvmBlockNumberProvider", () => { evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger); - const day5 = Date.UTC(2024, 1, 5, 2, 0, 0, 0); + const day5 = BigInt(Date.UTC(2024, 1, 5, 2, 0, 0, 0)); const epochBlockNumber = await evmProvider.getEpochBlockNumber(day5); expect(epochBlockNumber).toEqual(4n); @@ -39,7 +39,7 @@ describe("EvmBlockNumberProvider", () => { evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger); - const exactDay5 = Date.UTC(2024, 1, 1, 0, 0, 5, 0); + const exactDay5 = BigInt(Date.UTC(2024, 1, 1, 0, 0, 5, 0)); const epochBlockNumber = await evmProvider.getEpochBlockNumber(exactDay5); expect(epochBlockNumber).toEqual(4n); @@ -53,7 +53,7 @@ describe("EvmBlockNumberProvider", () => { evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger); - const futureTimestamp = Date.UTC(2025, 1, 1, 0, 0, 0, 0); + const futureTimestamp = BigInt(Date.UTC(2025, 1, 1, 0, 0, 0, 0)); expect(evmProvider.getEpochBlockNumber(futureTimestamp)).rejects.toBeInstanceOf( LastBlockEpoch, @@ -68,7 +68,7 @@ describe("EvmBlockNumberProvider", () => { evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger); - const futureTimestamp = Date.UTC(1970, 1, 1, 0, 0, 0, 0); + const futureTimestamp = BigInt(Date.UTC(1970, 1, 1, 0, 0, 0, 0)); expect(evmProvider.getEpochBlockNumber(futureTimestamp)).rejects.toBeInstanceOf( InvalidTimestamp, @@ -88,7 +88,7 @@ describe("EvmBlockNumberProvider", () => { evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger); - expect(evmProvider.getEpochBlockNumber(Number(timestamp))).rejects.toBeInstanceOf( + expect(evmProvider.getEpochBlockNumber(timestamp)).rejects.toBeInstanceOf( UnsupportedBlockTimestamps, ); }); @@ -101,7 +101,7 @@ describe("EvmBlockNumberProvider", () => { evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger); - expect(evmProvider.getEpochBlockNumber(Number(timestamp))).rejects.toBeInstanceOf( + expect(evmProvider.getEpochBlockNumber(timestamp)).rejects.toBeInstanceOf( UnsupportedBlockNumber, ); }); @@ -112,7 +112,7 @@ describe("EvmBlockNumberProvider", () => { client.getBlock = vi.fn().mockRejectedValue(null); evmProvider = new EvmBlockNumberProvider(client, searchConfig, logger); - const timestamp = Date.UTC(2024, 1, 1, 0, 0, 0, 0); + const timestamp = BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)); expect(evmProvider.getEpochBlockNumber(timestamp)).rejects.toBeDefined(); }); diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index d81cc32..4dcf4e8 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1 +1,2 @@ export * from "./logger.js"; +export * from "./timestamp.js"; diff --git a/packages/shared/src/types/timestamp.ts b/packages/shared/src/types/timestamp.ts new file mode 100644 index 0000000..f6d6d82 --- /dev/null +++ b/packages/shared/src/types/timestamp.ts @@ -0,0 +1 @@ +export type Timestamp = bigint; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97fa462..eebb37e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: "@ebo-agent/blocknumber": specifier: workspace:* version: link:../blocknumber + "@ebo-agent/shared": + specifier: workspace:* + version: link:../shared viem: specifier: 2.17.11 version: 2.17.11(typescript@5.5.3)