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/providers-and-logger #12

Merged
merged 4 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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: bigint)`
- `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: bigint): Promise<GetBlockReturnType> {
return this.client.getBlock({ 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