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: add evm provider and logger #10

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
72 changes: 72 additions & 0 deletions packages/chain-providers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# @grants-stack-indexer/chain-providers

## Overview

The `@grants-stack-indexer/chain-providers` package provides wrappers of the `Viem` library to interact with EVM-based blockchains.

## 📋 Prerequisites

- Ensure you have `node >= 20.0.0` and `pnpm >= 9.5.0` installed.

## Installation

```bash
$ pnpm install
```

## Building

To build the monorepo packages, run:

```bash
$ pnpm build
```

## Test

```bash
# unit tests
$ pnpm run test

# test coverage
$ pnpm run test:cov
```

## Usage

### Importing the Package

You can import the package in your TypeScript or JavaScript files as follows:

```typescript
import { EvmProvider } from "@grants-stack-indexer/chain-providers";
```

### Example

```typescript
// EVM-provider
const rpcUrls = [...]; //non-empty
const chain = mainnet; // from viem/chains

const evmProvider = new EvmProvider(rpcUrls, chain, logger);

const gasPrice = await evmProvider.getGasPrice();

const result = await evmProvider.readContract(address, abi, "myfunction", [arg1, arg2]);
```

## API

### [EvmProvider](./src/providers/evmProvider.ts)

Available methods

- `getMulticall3Address()`
- `getBlockNumber()`
- `getBlockByNumber(blockNumber: number)`
- `readContract(contractAddress: Address, abi: TAbi functionName: TFunctionName, args?: TArgs)`
- `batchRequest(abi: AbiWithConstructor,bytecode: Hex, args: ContractConstructorArgs<typeof abi>, constructorReturnParams: ReturnType)`
- `multicall(args: MulticallParameters<contracts, allowFailure>)`

For more details on both providers, refer to their implementations.
22 changes: 22 additions & 0 deletions packages/chain-providers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@grants-stack-indexer/chain-providers",
"version": "1.0.0",
"type": "module",
"main": "./dist/src/index.js",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"check-types": "tsc --noEmit -p ./tsconfig.json",
"clean": "rm -rf dist/",
"format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"",
"format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"",
"lint": "eslint \"{src,test}/**/*.{js,ts,json}\"",
"lint:fix": "pnpm lint --fix",
"test": "vitest run --config vitest.config.ts --passWithNoTests",
"test:cov": "vitest run --config vitest.config.ts --coverage"
},
"dependencies": {
"@grants-stack-indexer/shared": "workspace:*",
"abitype": "1.0.6",
"viem": "2.19.6"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class DataDecodeException extends Error {
constructor(message: string) {
super(message);
this.name = "DataDecodeException";
}
}
4 changes: 4 additions & 0 deletions packages/chain-providers/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./invalidArgument.exception.js";
export * from "./dataDecode.exception.js";
export * from "./multicallNotFound.exception.js";
export * from "./rpcUrlsEmpty.exception.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class InvalidArgumentException extends Error {
constructor(message: string) {
super(message);
this.name = "InvalidArgumentException";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class MulticallNotFound extends Error {
constructor() {
super("Multicall contract address not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class RpcUrlsEmpty extends Error {
constructor() {
super("RPC URLs array cannot be empty");
this.name = "RpcUrlsEmpty";
}
}
8 changes: 8 additions & 0 deletions packages/chain-providers/src/external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
DataDecodeException,
InvalidArgumentException,
MulticallNotFound,
RpcUrlsEmpty,
} from "./internal.js";

export { EvmProvider } from "./internal.js";
1 change: 1 addition & 0 deletions packages/chain-providers/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./external.js";
3 changes: 3 additions & 0 deletions packages/chain-providers/src/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./types/index.js";
export * from "./exceptions/index.js";
export * from "./providers/index.js";
208 changes: 208 additions & 0 deletions packages/chain-providers/src/providers/evmProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { AbiParameter } from "abitype";
import {
Abi,
Address,
Chain,
ContractConstructorArgs,
ContractFunctionArgs,
ContractFunctionName,
ContractFunctionParameters,
ContractFunctionReturnType,
createPublicClient,
decodeAbiParameters,
DecodeAbiParametersReturnType,
encodeDeployData,
EstimateGasParameters,
fallback,
FallbackTransport,
GetBlockReturnType,
Hex,
http,
HttpTransport,
MulticallParameters,
MulticallReturnType,
toHex,
} from "viem";

import { ILogger } from "@grants-stack-indexer/shared";

import {
AbiWithConstructor,
DataDecodeException,
InvalidArgumentException,
MulticallNotFound,
RpcUrlsEmpty,
} from "../internal.js";

/**
* Acts as a wrapper around Viem library to provide methods to interact with an EVM-based blockchain.
*/
export class EvmProvider {
private client: ReturnType<
typeof createPublicClient<FallbackTransport<HttpTransport[]>, Chain | undefined>
>;

constructor(
rpcUrls: string[],
readonly chain: Chain | undefined,
private readonly logger: ILogger,
) {
if (rpcUrls.length === 0) {
throw new RpcUrlsEmpty();
}

this.client = createPublicClient({
chain,
Copy link
Collaborator

Choose a reason for hiding this comment

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

optional: do we need any validation of the chain? It's completely optional here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

maybe we should add a validation to ensure that multicall works, thats the only thing that conserns me, just to avoid throwing in the case multicall address doesn't exist. Or maybe providing another way as fallback mechanism like a Promise.all in case there is no multicall contract (we already talk about the last one with nigiri).

Lets move forward as it is and keep track of this in linear, to tackle the issue in the future.

transport: fallback(rpcUrls.map((rpcUrl) => http(rpcUrl))),
});
}

/**
* Retrieves the address of the Multicall3 contract.
* @returns {Address | undefined} The address of the Multicall3 contract, or undefined if not found.
*/
getMulticall3Address(): Address | undefined {
return this.chain?.contracts?.multicall3?.address;
}

/**
* Retrieves the balance of the specified address.
* @param {Address} address The address for which to retrieve the balance.
* @returns {Promise<bigint>} A Promise that resolves to the balance of the address.
*/
async getBalance(address: Address): Promise<bigint> {
return this.client.getBalance({ address });
}

/**
* Retrieves the current block number.
* @returns {Promise<bigint>} A Promise that resolves to the latest block number.
*/
async getBlockNumber(): Promise<bigint> {
return this.client.getBlockNumber();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pretty convoluted but just wanted to tell that getBlockNumber by default caches the result by 4 seconds (ie default client's polling interval). [1]

Not sure if this is relevant but might be.

[1] https://viem.sh/docs/actions/public/getBlockNumber.html#cachetime-optional

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not sure if this is that relevant, we typically have all the events linked to the blocknumbers where they were emitted

cc @0xnigir1 ??

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 it's unlinkely we have an issue with this, its not that we call this method for every event. anyway, lets keep an eye an eventually we set it to 0

}

/**
* Retrieves the current block number.
* @returns {Promise<GetBlockReturnType>} Latest block number.
*/
async getBlockByNumber(blockNumber: number): Promise<GetBlockReturnType> {
return this.client.getBlock({ blockNumber: BigInt(blockNumber) });
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any specific reason to use number over bigint here? With this interface, someone could pass blockNumber = 5.5 as an argument and BigInt(blockNumber) would explode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good catch king


/**
* Retrieves the current estimated gas price on the chain.
* @returns {Promise<bigint>} A Promise that resolves to the current gas price.
*/
async getGasPrice(): Promise<bigint> {
return this.client.getGasPrice();
}

async estimateGas(args: EstimateGasParameters<typeof this.chain>): Promise<bigint> {
return this.client.estimateGas(args);
}

/**
* Retrieves the value from a storage slot at a given address.
* @param {Address} address The address of the contract.
* @param {number} slot The slot number to read.
* @returns {Promise<Hex>} A Promise that resolves to the value of the storage slot.
* @throws {InvalidArgumentException} If the slot is not a positive integer.
*/
async getStorageAt(address: Address, slot: number | Hex): Promise<Hex | undefined> {
if (typeof slot === "number" && (slot <= 0 || !Number.isInteger(slot))) {
throw new InvalidArgumentException(
`Slot must be a positive integer number. Received: ${slot}`,
);
}

return this.client.getStorageAt({
address,
slot: typeof slot === "string" ? slot : toHex(slot),
});
}

/**
* Reads a contract "pure" or "view" function with the specified arguments using readContract from Viem.
* @param {Address} contractAddress - The address of the contract.
* @param {TAbi} abi - The ABI (Application Binary Interface) of the contract.
* @param {TFunctionName} functionName - The name of the function to call.
* @param {TArgs} [args] - The arguments to pass to the function (optional).
* @returns A promise that resolves to the return value of the contract function.
*/
async readContract<
TAbi extends Abi,
TFunctionName extends ContractFunctionName<TAbi, "pure" | "view"> = ContractFunctionName<
TAbi,
"pure" | "view"
>,
TArgs extends ContractFunctionArgs<
TAbi,
"pure" | "view",
TFunctionName
> = ContractFunctionArgs<TAbi, "pure" | "view", TFunctionName>,
>(
contractAddress: Address,
abi: TAbi,
functionName: TFunctionName,
args?: TArgs,
): Promise<ContractFunctionReturnType<TAbi, "pure" | "view", TFunctionName, TArgs>> {
return this.client.readContract({
address: contractAddress,
abi,
functionName,
args,
});
}

/**
* Executes a batch request to deploy a contract and returns the decoded constructor return parameters.
* @param {AbiWithConstructor} abi - The ABI (Application Binary Interface) of the contract. Must contain a constructor.
* @param {Hex} bytecode - The bytecode of the contract.
* @param {ContractConstructorArgs<typeof abi>} args - The constructor arguments for the contract.
* @param constructorReturnParams - The return parameters of the contract's constructor.
* @returns The decoded constructor return parameters.
* @throws {DataDecodeException} if there is no return data or if the return data does not match the expected type.
*/
async batchRequest<ReturnType extends readonly AbiParameter[]>(
abi: AbiWithConstructor,
bytecode: Hex,
args: ContractConstructorArgs<typeof abi>,
constructorReturnParams: ReturnType,
): Promise<DecodeAbiParametersReturnType<ReturnType>> {
const deploymentData = args ? encodeDeployData({ abi, bytecode, args }) : bytecode;

const { data: returnData } = await this.client.call({
data: deploymentData,
});

if (!returnData) {
throw new DataDecodeException("No return data");
}

try {
const decoded = decodeAbiParameters(constructorReturnParams, returnData);
return decoded;
} catch (e) {
throw new DataDecodeException("Error decoding return data with given AbiParameters");
}
}

/**
* Similar to readContract, but batches up multiple functions
* on a contract in a single RPC call via the multicall3 contract.
* @param {MulticallParameters} args - The parameters for the multicall.
* @returns — An array of results. If allowFailure is true, with accompanying status
* @throws {MulticallNotFound} if the Multicall contract is not found.
*/
async multicall<
contracts extends readonly unknown[] = readonly ContractFunctionParameters[],
allowFailure extends boolean = true,
>(
args: MulticallParameters<contracts, allowFailure>,
): Promise<MulticallReturnType<contracts, allowFailure>> {
if (!this.chain?.contracts?.multicall3?.address) throw new MulticallNotFound();

return this.client.multicall<contracts, allowFailure>(args);
}
}
1 change: 1 addition & 0 deletions packages/chain-providers/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./evmProvider.js";
1 change: 1 addition & 0 deletions packages/chain-providers/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./viem.types.js";
3 changes: 3 additions & 0 deletions packages/chain-providers/src/types/viem.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Abi, AbiConstructor } from "abitype";

export type AbiWithConstructor = readonly [AbiConstructor, ...Abi];
33 changes: 33 additions & 0 deletions packages/chain-providers/test/fixtures/batchRequest.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Hex } from "viem";

export const structAbiFixture = {
abi: [
{
inputs: [
{
internalType: "address[]",
name: "_tokenAddresses",
type: "address[]",
},
],
stateMutability: "nonpayable",
type: "constructor",
},
] as const,
bytecode:
`0x608060405234801561001057600080fd5b506040516108aa3803806108aa833981810160405281019061003291906104f2565b60008151905060008167ffffffffffffffff81111561007a577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280602002602001820160405280156100b357816020015b6100a06103a6565b8152602001906001900390816100985790505b50905060005b828110156103775760008482815181106100fc577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b602002602001015190508073ffffffffffffffffffffffffffffffffffffffff1663313ce5676040518163ffffffff1660e01b815260040160206040518083038186803b15801561014c57600080fd5b505afa158015610160573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101849190610574565b8383815181106101bd577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200260200101516000019060ff16908160ff16815250508073ffffffffffffffffffffffffffffffffffffffff166395d89b416040518163ffffffff1660e01b815260040160006040518083038186803b15801561021b57600080fd5b505afa15801561022f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906102589190610533565b838381518110610291577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6020026020010151602001819052508073ffffffffffffffffffffffffffffffffffffffff166306fdde036040518163ffffffff1660e01b815260040160006040518083038186803b1580156102e657600080fd5b505afa1580156102fa573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103239190610533565b83838151811061035c577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b602002602001015160400181905250816001019150506100b9565b5060008160405160200161038b91906106c5565b60405160208183030381529060405290506020810180590381f35b6040518060600160405280600060ff16815260200160608152602001606081525090565b60006103dd6103d884610718565b6106e7565b905080838252602082019050828560208602820111156103fc57600080fd5b60005b8581101561042c57816104128882610474565b8452602084019350602083019250506001810190506103ff565b5050509392505050565b600061044961044484610744565b6106e7565b90508281526020810184848401111561046157600080fd5b61046c848285610808565b509392505050565b6000815190506104838161087b565b92915050565b600082601f83011261049a57600080fd5b81516104aa8482602086016103ca565b91505092915050565b600082601f8301126104c457600080fd5b81516104d4848260208601610436565b91505092915050565b6000815190506104ec81610892565b92915050565b60006020828403121561050457600080fd5b600082015167ffffffffffffffff81111561051e57600080fd5b61052a84828501610489565b91505092915050565b60006020828403121561054557600080fd5b600082015167ffffffffffffffff81111561055f57600080fd5b61056b848285016104b3565b91505092915050565b60006020828403121561058657600080fd5b6000610594848285016104dd565b91505092915050565b60006105a9838361065f565b905092915050565b60006105bc82610784565b6105c681856107a7565b9350836020820285016105d885610774565b8060005b8581101561061457848403895281516105f5858261059d565b94506106008361079a565b925060208a019950506001810190506105dc565b50829750879550505050505092915050565b60006106318261078f565b61063b81856107b8565b935061064b818560208601610808565b6106548161086a565b840191505092915050565b600060608301600083015161067760008601826106b6565b506020830151848203602086015261068f8282610626565b915050604083015184820360408601526106a98282610626565b9150508091505092915050565b6106bf816107fb565b82525050565b600060208201905081810360008301526106df81846105b1565b905092915050565b6000604051905081810181811067ffffffffffffffff8211171561070e5761070d61083b565b5b8060405250919050565b600067ffffffffffffffff8211156107335761073261083b565b5b602082029050602081019050919050565b600067ffffffffffffffff82111561075f5761075e61083b565b5b601f19601f8301169050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b60006107d4826107db565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600060ff82169050919050565b60005b8381101561082657808201518184015260208101905061080b565b83811115610835576000848401525b50505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b610884816107c9565b811461088f57600080fd5b50565b61089b816107fb565b81146108a657600080fd5b5056fe` as Hex,
args: [
[
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
],
] as const,
returnData:
`0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000045745544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d57726170706564204574686572000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000045553444300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000855534420436f696e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000` as Hex,
};

export const arrayAbiFixture = {
...structAbiFixture,
returnData:
`0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48` as Hex,
};
Loading