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: implements BlockNumberService #13

Merged
merged 37 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
06ef8af
build: separate tsconfig for build stage
0xyaco Jul 18, 2024
9802a89
feat: set up blocknumber module tests
0xyaco Jul 18, 2024
546d76e
chore: set up blocknumber module linting
0xyaco Jul 18, 2024
5b57a2d
Merge remote-tracking branch 'origin/dev' into chore/blocknumber-setup
0xyaco Jul 18, 2024
f291b45
style: remove total-typescript on non-base tsconfig
0xyaco Jul 18, 2024
0e8fc5a
build: set up build script
0xyaco Jul 18, 2024
baa144a
fix: blocknumber module package config
0xyaco Jul 19, 2024
9965c35
fix: dummy class import module
0xyaco Jul 19, 2024
6215697
chore: add coverage script and tasks
0xyaco Jul 19, 2024
2748ca2
chore: remove dummy classes
0xyaco Jul 19, 2024
ab869a4
feat: implement caip-2 compliant chain ids
0xyaco Jul 19, 2024
212960a
refactor: create exceptions/ folder
0xyaco Jul 24, 2024
21f025b
Merge remote-tracking branch 'origin/dev' into feat/bn-service
0xyaco Jul 24, 2024
ecd91e9
feat: search block by timestamp with binsearch
0xyaco Jul 23, 2024
4a38c03
fix: fixed winston dependency version
0xyaco Jul 24, 2024
711216d
refactor: rename evmProvider to evmBlockNumberProvider
0xyaco Jul 24, 2024
c7a9035
refactor: client and search params in evm provider constructor
0xyaco Jul 25, 2024
18d537a
Merge remote-tracking branch 'origin/dev' into feat/evm-block-search
0xyaco Jul 25, 2024
5ad1369
fix: pnpm dependecies mismatch
0xyaco Jul 25, 2024
6875aea
fix: throw InvalidTimestamp for timestamps prior first block
0xyaco Jul 25, 2024
80da90d
chore: remove dummy classes
0xyaco Jul 25, 2024
9a60076
refactor: move and redefine caip2 chain id
0xyaco Jul 26, 2024
60a57e1
feat: implements BlockNumberService
0xyaco Jul 26, 2024
841dc96
Merge remote-tracking branch 'origin/dev' into feat/bn-service
0xyaco Jul 26, 2024
cc0a6f0
chore: fix vitest tests location config
0xyaco Jul 26, 2024
0b44abe
feat: build provider based on chain namespace only
0xyaco Jul 30, 2024
32a49b7
Merge remote-tracking branch 'origin/dev' into feat/bn-service
0xyaco Jul 30, 2024
557a073
chore: remove old logger class
0xyaco Jul 30, 2024
163669b
fix: remove references from old logger
0xyaco Jul 31, 2024
776ba42
refactor: use shared chains
0xyaco Jul 31, 2024
ea13674
chore: fix turbo config
0xyaco Jul 31, 2024
0c01a4b
refactor: use logger with dependency injection
0xyaco Jul 31, 2024
472dab2
refactor: rename Caip2 to Caip2Utils
0xyaco Jul 31, 2024
0954b57
refactor: created BlockNumberProviderFactory
0xyaco Jul 31, 2024
7f0fa3a
feat: add single-chain getEpochBlockNumber
0xyaco Jul 31, 2024
e97d367
refactor: reorganize chains config structure and values
0xyaco Jul 31, 2024
927d237
docs: document BlockNumberService and BlockNumberProviderFactory
0xyaco Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/blocknumber/src/exceptions/chainWithoutProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Caip2ChainId } from "../types.js";

export class ChainWithoutProvider extends Error {
constructor(chainId: Caip2ChainId) {
super(`Chain ${chainId} has no provider defined.`);

this.name = "ChainWithoutProvider";
}
}
7 changes: 7 additions & 0 deletions packages/blocknumber/src/exceptions/emptyRpcUrls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class EmptyRpcUrls extends Error {
constructor() {
super(`At least one chain with its RPC endpoint must be defined.`);

this.name = "EmptyRpcUrls";
}
}
3 changes: 3 additions & 0 deletions packages/blocknumber/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export * from "./chainWithoutProvider.js";
export * from "./emptyRpcUrls.js";
export * from "./invalidChain.js";
export * from "./invalidTimestamp.js";
export * from "./lastBlockEpoch.js";
export * from "./timestampNotFound.js";
export * from "./unexpectedSearchRange.js";
export * from "./unsupportedBlockNumber.js";
export * from "./unsupportedBlockTimestamps.js";
export * from "./unsupportedChain.js";
9 changes: 9 additions & 0 deletions packages/blocknumber/src/exceptions/unsupportedChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Caip2ChainId } from "../types.js";

export class UnsupportedChain extends Error {
constructor(chainId: Caip2ChainId) {
super(`Chain ${chainId} is not supported.`);

this.name = "UnsupportedChain";
}
}
11 changes: 0 additions & 11 deletions packages/blocknumber/src/helloWorld.spec.ts

This file was deleted.

3 changes: 0 additions & 3 deletions packages/blocknumber/src/helloWorld.ts

This file was deleted.

13 changes: 6 additions & 7 deletions packages/blocknumber/src/providers/evmBlockNumberProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ILogger } from "@ebo-agent/shared";
import { Block, PublicClient } from "viem";
import { Block, FallbackTransport, HttpTransport, PublicClient } from "viem";

import {
InvalidTimestamp,
Expand All @@ -9,7 +9,6 @@ import {
UnsupportedBlockNumber,
UnsupportedBlockTimestamps,
} from "../exceptions/index.js";
import logger from "../utils/logger.js";
import { BlockNumberProvider } from "./blockNumberProvider.js";

const BINARY_SEARCH_BLOCKS_LOOKBACK = 10_000n;
Expand All @@ -31,7 +30,7 @@ interface SearchConfig {
}

export class EvmBlockNumberProvider implements BlockNumberProvider {
private client: PublicClient;
private client: PublicClient<FallbackTransport<HttpTransport[]>>;
private searchConfig: SearchConfig;
private firstBlock: Block | null;

Expand All @@ -45,7 +44,7 @@ export class EvmBlockNumberProvider implements BlockNumberProvider {
* while scanning blocks backwards during lower bound search. Defaults to 2.
*/
constructor(
client: PublicClient,
client: PublicClient<FallbackTransport<HttpTransport[]>>,
searchConfig: { blocksLookback?: bigint; deltaMultiplier?: bigint },
private logger: ILogger,
) {
Expand Down Expand Up @@ -201,15 +200,15 @@ export class EvmBlockNumberProvider implements BlockNumberProvider {

if (low > high) throw new UnexpectedSearchRange(low, high);

logger.debug(`Starting block binary search for timestamp ${timestamp}...`);
this.logger.debug(`Starting block binary search for timestamp ${timestamp}...`);

while (low <= high) {
currentBlockNumber = (high + low) / 2n;

const currentBlock = await this.client.getBlock({ blockNumber: currentBlockNumber });
const nextBlock = await this.client.getBlock({ blockNumber: currentBlockNumber + 1n });

logger.debug(
this.logger.debug(
`Analyzing block number #${currentBlock.number} with timestamp ${currentBlock.timestamp}`,
);

Expand All @@ -223,7 +222,7 @@ export class EvmBlockNumberProvider implements BlockNumberProvider {
currentBlock.timestamp <= timestamp && nextBlock.timestamp > timestamp;

if (blockContainsTimestamp) {
logger.debug(`Block #${currentBlock.number} contains timestamp.`);
this.logger.debug(`Block #${currentBlock.number} contains timestamp.`);

return currentBlock.number;
} else if (currentBlock.timestamp <= timestamp) {
Expand Down
100 changes: 100 additions & 0 deletions packages/blocknumber/src/services/blockNumberService.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing natspec

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 adding docs to the BlockNumberProviderFactory too 👌

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Logger, supportedChains } from "@ebo-agent/shared";
import {
createPublicClient,
fallback,
FallbackTransport,
http,
HttpTransport,
PublicClient,
} from "viem";

import { ChainWithoutProvider, EmptyRpcUrls, UnsupportedChain } from "../exceptions/index.js";
import { BlockNumberProvider } from "../providers/blockNumberProvider.js";
import { EvmBlockNumberProvider } from "../providers/evmBlockNumberProvider.js";
import { Caip2ChainId } from "../types.js";
import { Caip2 } from "../utils/index.js";

type RpcUrl = NonNullable<Parameters<typeof http>[0]>;

const DEFAULT_PROVIDER_CONFIG = {
blocksLookback: 10_000n,
deltaMultiplier: 2n,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

shared package? wdyt?

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 see where you are going, yes. Probably we could define a type ProviderConfig modeling this config object that goes in the shared package but keep the const here as I suspect these default values will only be used by the blockNumberService internals? Makes sense?

Copy link
Collaborator

@0xkenj1 0xkenj1 Jul 31, 2024

Choose a reason for hiding this comment

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

Makes totally sense, i was thinking that maybe it is better having almost all the constants on the centralized shared package, to only update values there when is needed and avoid modifying the class file's.

Lets leave it as it is.


export class BlockNumberService {
private blockNumberProviders: Map<Caip2ChainId, BlockNumberProvider>;

constructor(chainRpcUrls: Map<Caip2ChainId, RpcUrl[]>) {
this.blockNumberProviders = this.buildBlockNumberProviders(chainRpcUrls);
}

public async getEpochBlockNumbers(timestamp: number, chains: Caip2ChainId[]) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

also remember that will be 1 request -> 1 chain

so lets also add a getEpochBlockNumber method

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh yes.

Do you think leveraging the multiple chains method would be ok? ie:

getEpochBlockNumber(timestamp, chain) {
  getEpochBlockNumbers(timestamp, [chain])
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would do it the other way round, like:

getEpochBlockNumbers(timestamp, [chain]) {
Promise.all([..., getEpochBlockNumbers(timestamp, chain), ...]
}

const epochBlockNumbers = await Promise.all(
Copy link
Collaborator

Choose a reason for hiding this comment

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

probably here we will need a ts-retry, to implement a retry policy.

chains.map(async (chainId) => {
const provider = this.blockNumberProviders.get(chainId);

if (!provider) throw new ChainWithoutProvider(chainId);

const blockNumber = await provider.getEpochBlockNumber(timestamp);

return [chainId, blockNumber] as [Caip2ChainId, bigint];
}),
);

const e = epochBlockNumbers.filter(
(entry): entry is [Caip2ChainId, bigint] => entry !== null,
);

return new Map(e);
}

private buildBlockNumberProviders(chainRpcUrls: Map<Caip2ChainId, RpcUrl[]>) {
if (chainRpcUrls.size == 0) throw new EmptyRpcUrls();

const supportedChainIds = this.getSupportedChainIds(supportedChains);
const providers = new Map<Caip2ChainId, BlockNumberProvider>();

for (const [chainId, urls] of chainRpcUrls) {
if (!supportedChainIds.includes(chainId)) throw new UnsupportedChain(chainId);

const client = createPublicClient({
transport: fallback(urls.map((url) => http(url))),
});

const provider = BlockNumberService.buildProvider(chainId, client);

if (!provider) throw new ChainWithoutProvider(chainId);

providers.set(chainId, provider);
}

return providers;
}

private getSupportedChainIds(chainsConfig: typeof supportedChains) {
const namespacesChains = Object.values(chainsConfig);

return namespacesChains.reduce((acc, namespaceChains) => {
return [...acc, ...Object.values(namespaceChains.chains)];
}, [] as string[]);
}

public static buildProvider(
Copy link
Collaborator

Choose a reason for hiding this comment

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

this smells like a Factory method pattern pattern

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Whoops missed to answer this one; it is indeed a Factory method pattern! Want me to do some special tweaks on it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I was wondering if we should remove this method from the BlockNumberService class, i thing it has nothing to do with the main purpose of the class.

Also the static modifier is telling me that this method doesn't belong to this class 🤣

Probably creating a new class called BlocknumberProviderFactory to have an abstraction on the instantiation of the objects

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 see. We can extract this to a new class, absolutely, makes testing cleaner too.

chainId: Caip2ChainId,
client: PublicClient<FallbackTransport<HttpTransport[]>>,
) {
const chainNamespace = Caip2.getNamespace(chainId);

switch (chainNamespace) {
case supportedChains.evm.namespace:
return new EvmBlockNumberProvider(
client,
DEFAULT_PROVIDER_CONFIG,
Logger.getInstance(), // Should we drop this arg?
Copy link
Collaborator

Choose a reason for hiding this comment

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

dont think we should, maybe we can use this.logger but its the same at the end 🤣

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Aaah yesss let's add a logger member to this service.

);

default:
throw new UnsupportedChain(chainId);
}
}
}
1 change: 1 addition & 0 deletions packages/blocknumber/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./blockNumberService.js";
1 change: 1 addition & 0 deletions packages/blocknumber/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Caip2ChainId = `${string}:${string}`;
Original file line number Diff line number Diff line change
@@ -1,41 +1,19 @@
// Based on https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md

import { InvalidChainId } from "../exceptions/invalidChain.js";

type ChainNamespace = string;
type ChainReference = string;

interface ChainIdParams {
namespace: ChainNamespace;
reference: ChainReference;
}
import { InvalidChainId } from "../../exceptions/invalidChain.js";
import { Caip2ChainId } from "../../types.js";

const NAMESPACE_FORMAT = /^[-a-z0-9]{3,8}$/;
const REFERENCE_FORMAT = /^[-_a-zA-Z0-9]{1,32}$/;

export class ChainId {
private namespace: string;
private reference: string;

/**
* Creates a validated CAIP-2 compliant chain ID.
*
* @param chainId a CAIP-2 compliant string.
*/
constructor(chainId: string) {
const params = ChainId.parse(chainId);

this.namespace = params.namespace;
this.reference = params.reference;
}

export class Caip2 {
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 class Caip2 {
export abstract class Caip2 {

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 class Caip2 {
export class Caip2Utils {

??

/**
* Parses a CAIP-2 compliant string.
*
* @param chainId {string} a CAIP-2 compliant string
* @returns an object containing the namespace and the reference of the chain id
* @returns the CAIP-2 validated chain id string
*/
public static parse(chainId: string): ChainIdParams {
public static validateChainId(chainId: string): chainId is Caip2ChainId {
const elements = chainId.split(":");

if (elements.length !== 2) {
Expand All @@ -54,13 +32,14 @@ export class ChainId {
const isValidReference = REFERENCE_FORMAT.test(reference);
if (!isValidReference) throw new InvalidChainId("Chain ID reference is not valid.");

return {
namespace,
reference,
};
return true;
}

public toString() {
return `${this.namespace}:${this.reference}`;
public static getNamespace(chainId: string | Caip2ChainId) {
this.validateChainId(chainId);

const namespace = chainId.split(":")[0] as string;

return namespace;
}
}
1 change: 1 addition & 0 deletions packages/blocknumber/src/utils/caip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./caip2.js";
3 changes: 1 addition & 2 deletions packages/blocknumber/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./chainId.js";
export * from "./logger.js";
export * from "./caip/caip2.js";
15 changes: 0 additions & 15 deletions packages/blocknumber/src/utils/logger.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Logger } from "@ebo-agent/shared";
import { Block, createPublicClient, GetBlockParameters, http } from "viem";
import { mainnet } from "viem/chains";
import { describe, expect, it, vi } from "vitest";
Expand All @@ -13,6 +14,7 @@ import { EvmBlockNumberProvider } from "../../src/providers/evmBlockNumberProvid
describe("EvmBlockNumberProvider", () => {
describe("getEpochBlockNumber", () => {
const searchConfig = { blocksLookback: 2n, deltaMultiplier: 2n };
const logger = Logger.getInstance();
let evmProvider: EvmBlockNumberProvider;

it("returns the first of two consecutive blocks when their timestamp contains the searched timestamp", async () => {
Expand All @@ -21,7 +23,7 @@ describe("EvmBlockNumberProvider", () => {
const endTimestamp = Date.UTC(2024, 1, 11, 0, 0, 0, 0);
const rpcProvider = mockRpcProvider(blockNumber, startTimestamp, endTimestamp);

evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig);
evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger);

const day5 = Date.UTC(2024, 1, 5, 2, 0, 0, 0);
const epochBlockNumber = await evmProvider.getEpochBlockNumber(day5);
Expand All @@ -35,7 +37,7 @@ describe("EvmBlockNumberProvider", () => {
const endTimestamp = Date.UTC(2024, 1, 1, 0, 0, 11, 0);
const rpcProvider = mockRpcProvider(lastBlockNumber, startTimestamp, endTimestamp);

evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig);
evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger);

const exactDay5 = Date.UTC(2024, 1, 1, 0, 0, 5, 0);
const epochBlockNumber = await evmProvider.getEpochBlockNumber(exactDay5);
Expand All @@ -49,7 +51,7 @@ describe("EvmBlockNumberProvider", () => {
const endTimestamp = Date.UTC(2024, 1, 1, 0, 0, 11, 0);
const rpcProvider = mockRpcProvider(lastBlockNumber, startTimestamp, endTimestamp);

evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig);
evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger);

const futureTimestamp = Date.UTC(2025, 1, 1, 0, 0, 0, 0);

Expand All @@ -64,7 +66,7 @@ describe("EvmBlockNumberProvider", () => {
const endTimestamp = Date.UTC(2024, 1, 1, 0, 0, 11, 0);
const rpcProvider = mockRpcProvider(lastBlockNumber, startTimestamp, endTimestamp);

evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig);
evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger);

const futureTimestamp = Date.UTC(1970, 1, 1, 0, 0, 0, 0);

Expand All @@ -84,7 +86,7 @@ describe("EvmBlockNumberProvider", () => {
{ number: 4n, timestamp: afterTimestamp },
]);

evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig);
evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger);

expect(evmProvider.getEpochBlockNumber(Number(timestamp))).rejects.toBeInstanceOf(
UnsupportedBlockTimestamps,
Expand All @@ -97,7 +99,7 @@ describe("EvmBlockNumberProvider", () => {
{ number: null, timestamp: BigInt(timestamp) },
]);

evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig);
evmProvider = new EvmBlockNumberProvider(rpcProvider, searchConfig, logger);

expect(evmProvider.getEpochBlockNumber(Number(timestamp))).rejects.toBeInstanceOf(
UnsupportedBlockNumber,
Expand All @@ -109,7 +111,7 @@ describe("EvmBlockNumberProvider", () => {

client.getBlock = vi.fn().mockRejectedValue(null);

evmProvider = new EvmBlockNumberProvider(client, searchConfig);
evmProvider = new EvmBlockNumberProvider(client, searchConfig, logger);
const timestamp = Date.UTC(2024, 1, 1, 0, 0, 0, 0);

expect(evmProvider.getEpochBlockNumber(timestamp)).rejects.toBeDefined();
Expand Down
Loading