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 36 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.

30 changes: 30 additions & 0 deletions packages/blocknumber/src/providers/blockNumberProviderFactory.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

🚀

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { EBO_SUPPORTED_CHAINS_CONFIG, ILogger } from "@ebo-agent/shared";
import { FallbackTransport, HttpTransport, PublicClient } from "viem";

import { UnsupportedChain } from "../exceptions/unsupportedChain.js";
import { Caip2ChainId } from "../types.js";
import { Caip2Utils } from "../utils/index.js";
import { EvmBlockNumberProvider } from "./evmBlockNumberProvider.js";

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

export class BlockNumberProviderFactory {
public static buildProvider(
chainId: Caip2ChainId,
client: PublicClient<FallbackTransport<HttpTransport[]>>,
logger: ILogger,
) {
const chainNamespace = Caip2Utils.getNamespace(chainId);

switch (chainNamespace) {
case EBO_SUPPORTED_CHAINS_CONFIG.evm.namespace:
return new EvmBlockNumberProvider(client, DEFAULT_PROVIDER_CONFIG, logger);

default:
throw new UnsupportedChain(chainId);
}
Comment on lines +30 to +36
Copy link
Collaborator

Choose a reason for hiding this comment

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

a switch for just two cases is too much i think but if this is expected to grow then feel free to ignore this and leave it as it is

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It will grow, yep

}
}
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
69 changes: 69 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,69 @@
import { EBO_SUPPORTED_CHAIN_IDS, ILogger } from "@ebo-agent/shared";
import { createPublicClient, fallback, http } from "viem";

import { ChainWithoutProvider, EmptyRpcUrls, UnsupportedChain } from "../exceptions/index.js";
import { BlockNumberProvider } from "../providers/blockNumberProvider.js";
import { BlockNumberProviderFactory } from "../providers/blockNumberProviderFactory.js";
import { Caip2ChainId } from "../types.js";

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

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

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

public async getEpochBlockNumber(timestamp: number, chainId: Caip2ChainId): Promise<bigint> {
const provider = this.blockNumberProviders.get(chainId);

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

const blockNumber = await provider.getEpochBlockNumber(timestamp);

return blockNumber;
}

public async getEpochBlockNumbers(timestamp: number, chains: Caip2ChainId[]) {
const epochBlockNumbers = await Promise.all(
chains.map(async (chain) => ({
chainId: chain,
blockNumber: await this.getEpochBlockNumber(timestamp, chain),
})),
);

return epochBlockNumbers.reduce((epochBlockNumbersMap, epoch) => {
return epochBlockNumbersMap.set(epoch.chainId, epoch.blockNumber);
}, new Map<Caip2ChainId, bigint>());
}

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

const providers = new Map<Caip2ChainId, BlockNumberProvider>();

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

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

const provider = BlockNumberProviderFactory.buildProvider(chainId, client, this.logger);

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

providers.set(chainId, provider);
}

return providers;
}

private isChainSupported(chainId: Caip2ChainId) {
return EBO_SUPPORTED_CHAIN_IDS.includes(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 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 "./caip2Utils.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/caip2Utils.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
@@ -0,0 +1,30 @@
import { Logger } from "@ebo-agent/shared";
import { createPublicClient, fallback, http } from "viem";
import { describe, expect, it } from "vitest";

import { UnsupportedChain } from "../../src/exceptions";
import { BlockNumberProviderFactory } from "../../src/providers/blockNumberProviderFactory";
import { EvmBlockNumberProvider } from "../../src/providers/evmBlockNumberProvider";
import { Caip2ChainId } from "../../src/types";

describe("BlockNumberProviderFactory", () => {
const logger = Logger.getInstance();

describe("buildProvider", () => {
const client = createPublicClient({ transport: fallback([http("http://localhost:8545")]) });

it("builds a provider", () => {
const provider = BlockNumberProviderFactory.buildProvider("eip155:1", client, logger);

expect(provider).toBeInstanceOf(EvmBlockNumberProvider);
});

it("fails if chain is not supported", () => {
const unsupportedChainId = "solana:80085" as Caip2ChainId;

expect(() => {
BlockNumberProviderFactory.buildProvider(unsupportedChainId, client, logger);
}).toThrow(UnsupportedChain);
});
});
});
Loading