Skip to content

Commit

Permalink
feat: add evm provider and logger
Browse files Browse the repository at this point in the history
  • Loading branch information
0xkenj1 committed Oct 15, 2024
1 parent 8113bcb commit d7d5c70
Show file tree
Hide file tree
Showing 26 changed files with 1,356 additions and 1 deletion.
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,
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();
}

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

/**
* 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

0 comments on commit d7d5c70

Please sign in to comment.