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

feat: implements BlockNumberService #13

merged 37 commits into from
Aug 2, 2024

Conversation

0xyaco
Copy link
Collaborator

@0xyaco 0xyaco commented Jul 26, 2024

🤖 Linear

Closes GRT-38
Part of GRT-77

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.

Copy link

linear bot commented Jul 26, 2024

GRT-38 Implement BlocknumberService

Follow Figma diagrams: https://www.figma.com/board/BbciqJb5spg35ZglTsRRBb/Offchain?node-id=233-2491&t=fNurQJG1q77rduKS-4to implement BlocknumberService.

AC:

  • BlocknumberService successfully implemented
  • CAIP-2 standard is used for blockchains ids (https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md)
  • BlocknumberService should have the capability to distinguish between an EVM or nonEVM chain to use the corresponding provider to fetch blocknumbers in the future (EVMProvider, BlockmetaProvider)
  • Unit tests

@0xyaco 0xyaco changed the title Feat/bn service feat: implements BlockNumberService Jul 26, 2024
Comment on lines 11 to 15
export const chains = {
mainnet: "eip155:1",
polygon: "eip155:137",
arbitrum: "eip155:42161",
} as Record<SupportedChains, Caip2ChainId>;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is something that feels weird for me. It works, but feels super weird. Any suggestions?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Supported chains should be part of the config file from the runtime or on a shared module, not here i think , this is just an abstraction of the CAIP2

Comment on lines 68 to 77
switch (chainId) {
case "eip155:1":
return new EvmBlockNumberProvider(client, {
blocksLookback: 10_000n,
deltaMultiplier: 2n,
});

default:
throw new UnsupportedChain(chainId);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@0xkenj1 what do you think about this one? Should we have some kind of config file/constant value with the following attributes per chain?

  • Provider class to use
  • blocksLookback
  • deltaMultiplier

Something like:

providers:
  - "eip155:1":
    - provider: EvmBlockNumberProvider
      blocksLookback: 10000
      deltaMultiplier: 2

Copy link
Collaborator

Choose a reason for hiding this comment

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

2 things here:

  1. i would just use the same config for all the evm's for now , to avoid extra complexity. Lets leave some default values, and in the future if we need it we can do an easy refactor to inject the config.

  2. on the switch statement, we can just pick the namespace from the CAIP2 in order to decide which blockNumberService we should instantiate. Lets avoid hardcoding each of the chains.

Copy link
Collaborator

Choose a reason for hiding this comment

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

After reading the code below, the switch statement should run after a validation, like:

if( chainId is not in SupportedChains ){
throw ``;
}

we can inject SupportedChains on the constructor, or having them on the shared module as constants would be good also

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Given that we'll be deciding which provider to instantiate based on the namespace, what do you think about building a functionstatic getNamespace(chainId: string | Caip2ChainId) inside Caip2?

And I agree, let's go with a one-size-fits-all config for every chain and later we can add more customization.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea, perfect, thats what we need :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

And regarding the supported chains, with this convo I think it'd make more sense to validate the supported chains with a shared module/config file inside the constructor, yeah.

This buildProvider method being a static one and using (at the moment) only the namespace to operate seems like a not so good place. Probably in the future if we define a custom config per chain (for its provider) we'll be able to do something like:

if (config.getProviderConfig(chainId) == null) throw new Error();

switch (namespace) {
   ...
}

@0xyaco 0xyaco force-pushed the feat/bn-service branch from 7a2782e to 841dc96 Compare July 26, 2024 19:53
Comment on lines 68 to 77
switch (chainId) {
case "eip155:1":
return new EvmBlockNumberProvider(client, {
blocksLookback: 10_000n,
deltaMultiplier: 2n,
});

default:
throw new UnsupportedChain(chainId);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

2 things here:

  1. i would just use the same config for all the evm's for now , to avoid extra complexity. Lets leave some default values, and in the future if we need it we can do an easy refactor to inject the config.

  2. on the switch statement, we can just pick the namespace from the CAIP2 in order to decide which blockNumberService we should instantiate. Lets avoid hardcoding each of the chains.

Comment on lines 11 to 15
export const chains = {
mainnet: "eip155:1",
polygon: "eip155:137",
arbitrum: "eip155:42161",
} as Record<SupportedChains, Caip2ChainId>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Supported chains should be part of the config file from the runtime or on a shared module, not here i think , this is just an abstraction of the CAIP2

Comment on lines 68 to 77
switch (chainId) {
case "eip155:1":
return new EvmBlockNumberProvider(client, {
blocksLookback: 10_000n,
deltaMultiplier: 2n,
});

default:
throw new UnsupportedChain(chainId);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

After reading the code below, the switch statement should run after a validation, like:

if( chainId is not in SupportedChains ){
throw ``;
}

we can inject SupportedChains on the constructor, or having them on the shared module as constants would be good also

@0xyaco
Copy link
Collaborator Author

0xyaco commented Jul 30, 2024

Pendingshared modules integration yet

@0xyaco 0xyaco force-pushed the feat/bn-service branch from 213aae4 to 776ba42 Compare July 31, 2024 00:24
@0xyaco 0xyaco marked this pull request as ready for review July 31, 2024 00:40
Copy link
Collaborator

@0xkenj1 0xkenj1 left a comment

Choose a reason for hiding this comment

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

some findings, but we are almost there :)

GJ ser! 💯

export const supportedChains = {
evm: {
namespace: "eip155",
chains: {
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
chains: {
reference: {

Comment on lines 5 to 7
ethereum: "eip155:1",
polygon: "eip155:137",
arbitrum: "eip155:42161",
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
ethereum: "eip155:1",
polygon: "eip155:137",
arbitrum: "eip155:42161",
ethereum: "1",
polygon: "137",
arbitrum: "42161",

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 {

??

@@ -0,0 +1,10 @@
export const supportedChains = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think maybe this should be an env var or configurable by the user insted of part of a shared package.

Each user may want to choose which blockchains they support.

Maybe the input structure should be something like

{
    evm: {
        namespace: "eip155",
        chains: {
           ethereum: {
              reference: "1",
              rpcs: ["...", "..."]
           }
      }
}          
              

Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe i would rather pick reference as key of "chains" we are not gona be looking up using human readable, wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

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

thinking again here. maybe what we need is a user input structure and also another structure of validChains (what we have now) to validate user input against it. make sense ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You envision something like:

MONITORED_CHAINS=eip155:1,eip155:137 ebo-agent run

(I guess also via a config file apart from env vars)

And that user-input list of chains should be a subset of our supportedChains set of values?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yea, exactly, a subset of the supported chains defined by us. The user should be also able to set rpcUrls for each of the chains.

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.

}, [] 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.

}

public async getEpochBlockNumbers(timestamp: number, chains: Caip2ChainId[]) {
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.

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), ...]
}

Comment on lines 19 to 22
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.

@0xyaco
Copy link
Collaborator Author

0xyaco commented Jul 31, 2024

@0xkenj1 most significant changes are:

  • Differentiation between the namespace > reference structure for each chain (EBO_SUPPORTED_CHAINS_CONFIG) as suggested
  • Built the EBO_SUPPORTED_CHAIN_IDS list of CAIP-2 compliant chain ids using EBO_SUPPORTED_CHAINS_CONFIG
  • Encapsulated the isChainSupported behavior inside BlockNumberService.isChainSupported(chainId) to be ready for whenever we decide to tackle the mechanism to let the user decide which (out of the supported chains defined in these new constants) will be monitoring with the EBO agent.

Let me know what you think!

@0xkenj1 0xkenj1 requested review from 0xnigir1 and 0xkenj1 August 1, 2024 16:23
0xkenj1
0xkenj1 previously approved these changes Aug 1, 2024
Copy link
Collaborator

@0xkenj1 0xkenj1 left a comment

Choose a reason for hiding this comment

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

GJ!!!!! This is huge!!! 🥇

Copy link
Collaborator

Choose a reason for hiding this comment

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

🚀

Comment on lines +22 to +28
switch (chainNamespace) {
case EBO_SUPPORTED_CHAINS_CONFIG.evm.namespace:
return new EvmBlockNumberProvider(client, DEFAULT_PROVIDER_CONFIG, logger);

default:
throw new UnsupportedChain(chainId);
}
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

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 👌

@0xyaco 0xyaco merged commit 995a84a into dev Aug 2, 2024
5 checks passed
@0xyaco 0xyaco deleted the feat/bn-service branch August 2, 2024 14:01
Copy link

linear bot commented Aug 28, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants