-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implements BlockNumberService (#13)
# 🤖 Linear Closes GRT-38 ## Description - Refactors the `CAIP2` chain id class, using it as a static module that only validates a string being compliant. - Implements `BlockNumberService`, enabling the agent to get from multiple chains their block numbers at a particular timestamp.
- Loading branch information
Showing
23 changed files
with
526 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
38 changes: 38 additions & 0 deletions
38
packages/blocknumber/src/providers/blockNumberProviderFactory.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
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 { | ||
/** | ||
* Build a `BlockNumberProvider` to handle communication with the specified chain. | ||
* | ||
* @param chainId CAIP-2 chain id | ||
* @param client a viem public client | ||
* @param logger a ILogger instance | ||
* @returns | ||
*/ | ||
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
packages/blocknumber/src/services/blockNumberService.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
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>; | ||
|
||
/** | ||
* Create a `BlockNumberService` instance that will handle the interaction with a collection | ||
* of chains. | ||
* | ||
* @param chainRpcUrls a map of CAIP-2 chain ids with their RPC urls that this service will handle | ||
* @param logger a `ILogger` instance | ||
*/ | ||
constructor( | ||
chainRpcUrls: Map<Caip2ChainId, RpcUrl[]>, | ||
private readonly logger: ILogger, | ||
) { | ||
this.blockNumberProviders = this.buildBlockNumberProviders(chainRpcUrls); | ||
} | ||
|
||
/** | ||
* Get a chain epoch block number based on a timestamp. | ||
* | ||
* @param timestamp UTC timestamp in ms since UNIX epoch | ||
* @param chainId the CAIP-2 chain id | ||
* @returns the block number corresponding to the timestamp | ||
*/ | ||
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; | ||
} | ||
|
||
/** | ||
* Get the epoch block number for all the specified chains based on a timestamp. | ||
* | ||
* @param timestamp UTC timestamp in ms since UNIX epoch | ||
* @param chains a list of CAIP-2 chain ids | ||
* @returns a map of CAIP-2 chain ids | ||
*/ | ||
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>()); | ||
} | ||
|
||
/** | ||
* Build a collection of `BlockNumberProvider`s instances respective to each | ||
* CAIP-2 chain id. | ||
* | ||
* @param chainRpcUrls a map containing chain ids with their respective list of RPC urls | ||
* @returns a map of CAIP-2 chain ids and their respective `BlockNumberProvider` instances | ||
*/ | ||
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; | ||
} | ||
|
||
/** | ||
* Check if a chain is supported by the service. | ||
* | ||
* @param chainId CAIP-2 chain id | ||
* @returns true if the chain is supported, false otherwise | ||
*/ | ||
private isChainSupported(chainId: Caip2ChainId) { | ||
return EBO_SUPPORTED_CHAIN_IDS.includes(chainId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./blockNumberService.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export type Caip2ChainId = `${string}:${string}`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./caip2Utils.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file was deleted.
Oops, something went wrong.
30 changes: 30 additions & 0 deletions
30
packages/blocknumber/test/providers/blockNumberProviderFactory.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.