Skip to content

Commit d7d5c70

Browse files
committed
feat: add evm provider and logger
1 parent 8113bcb commit d7d5c70

26 files changed

+1356
-1
lines changed

packages/chain-providers/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# @grants-stack-indexer/chain-providers
2+
3+
## Overview
4+
5+
The `@grants-stack-indexer/chain-providers` package provides wrappers of the `Viem` library to interact with EVM-based blockchains.
6+
7+
## 📋 Prerequisites
8+
9+
- Ensure you have `node >= 20.0.0` and `pnpm >= 9.5.0` installed.
10+
11+
## Installation
12+
13+
```bash
14+
$ pnpm install
15+
```
16+
17+
## Building
18+
19+
To build the monorepo packages, run:
20+
21+
```bash
22+
$ pnpm build
23+
```
24+
25+
## Test
26+
27+
```bash
28+
# unit tests
29+
$ pnpm run test
30+
31+
# test coverage
32+
$ pnpm run test:cov
33+
```
34+
35+
## Usage
36+
37+
### Importing the Package
38+
39+
You can import the package in your TypeScript or JavaScript files as follows:
40+
41+
```typescript
42+
import { EvmProvider } from "@grants-stack-indexer/chain-providers";
43+
```
44+
45+
### Example
46+
47+
```typescript
48+
// EVM-provider
49+
const rpcUrls = [...]; //non-empty
50+
const chain = mainnet; // from viem/chains
51+
52+
const evmProvider = new EvmProvider(rpcUrls, chain, logger);
53+
54+
const gasPrice = await evmProvider.getGasPrice();
55+
56+
const result = await evmProvider.readContract(address, abi, "myfunction", [arg1, arg2]);
57+
```
58+
59+
## API
60+
61+
### [EvmProvider](./src/providers/evmProvider.ts)
62+
63+
Available methods
64+
65+
- `getMulticall3Address()`
66+
- `getBlockNumber()`
67+
- `getBlockByNumber(blockNumber: number)`
68+
- `readContract(contractAddress: Address, abi: TAbi functionName: TFunctionName, args?: TArgs)`
69+
- `batchRequest(abi: AbiWithConstructor,bytecode: Hex, args: ContractConstructorArgs<typeof abi>, constructorReturnParams: ReturnType)`
70+
- `multicall(args: MulticallParameters<contracts, allowFailure>)`
71+
72+
For more details on both providers, refer to their implementations.

packages/chain-providers/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@grants-stack-indexer/chain-providers",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"main": "./dist/src/index.js",
6+
"scripts": {
7+
"build": "tsc -p tsconfig.build.json",
8+
"check-types": "tsc --noEmit -p ./tsconfig.json",
9+
"clean": "rm -rf dist/",
10+
"format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"",
11+
"format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"",
12+
"lint": "eslint \"{src,test}/**/*.{js,ts,json}\"",
13+
"lint:fix": "pnpm lint --fix",
14+
"test": "vitest run --config vitest.config.ts --passWithNoTests",
15+
"test:cov": "vitest run --config vitest.config.ts --coverage"
16+
},
17+
"dependencies": {
18+
"@grants-stack-indexer/shared": "workspace:*",
19+
"abitype": "1.0.6",
20+
"viem": "2.19.6"
21+
}
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class DataDecodeException extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = "DataDecodeException";
5+
}
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./invalidArgument.exception.js";
2+
export * from "./dataDecode.exception.js";
3+
export * from "./multicallNotFound.exception.js";
4+
export * from "./rpcUrlsEmpty.exception.js";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class InvalidArgumentException extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = "InvalidArgumentException";
5+
}
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class MulticallNotFound extends Error {
2+
constructor() {
3+
super("Multicall contract address not found");
4+
}
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class RpcUrlsEmpty extends Error {
2+
constructor() {
3+
super("RPC URLs array cannot be empty");
4+
this.name = "RpcUrlsEmpty";
5+
}
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export {
2+
DataDecodeException,
3+
InvalidArgumentException,
4+
MulticallNotFound,
5+
RpcUrlsEmpty,
6+
} from "./internal.js";
7+
8+
export { EvmProvider } from "./internal.js";

packages/chain-providers/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./external.js";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./types/index.js";
2+
export * from "./exceptions/index.js";
3+
export * from "./providers/index.js";
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { AbiParameter } from "abitype";
2+
import {
3+
Abi,
4+
Address,
5+
Chain,
6+
ContractConstructorArgs,
7+
ContractFunctionArgs,
8+
ContractFunctionName,
9+
ContractFunctionParameters,
10+
ContractFunctionReturnType,
11+
createPublicClient,
12+
decodeAbiParameters,
13+
DecodeAbiParametersReturnType,
14+
encodeDeployData,
15+
EstimateGasParameters,
16+
fallback,
17+
FallbackTransport,
18+
GetBlockReturnType,
19+
Hex,
20+
http,
21+
HttpTransport,
22+
MulticallParameters,
23+
MulticallReturnType,
24+
toHex,
25+
} from "viem";
26+
27+
import { ILogger } from "@grants-stack-indexer/shared";
28+
29+
import {
30+
AbiWithConstructor,
31+
DataDecodeException,
32+
InvalidArgumentException,
33+
MulticallNotFound,
34+
RpcUrlsEmpty,
35+
} from "../internal.js";
36+
37+
/**
38+
* Acts as a wrapper around Viem library to provide methods to interact with an EVM-based blockchain.
39+
*/
40+
export class EvmProvider {
41+
private client: ReturnType<
42+
typeof createPublicClient<FallbackTransport<HttpTransport[]>, Chain | undefined>
43+
>;
44+
45+
constructor(
46+
rpcUrls: string[],
47+
readonly chain: Chain | undefined,
48+
private readonly logger: ILogger,
49+
) {
50+
if (rpcUrls.length === 0) {
51+
throw new RpcUrlsEmpty();
52+
}
53+
54+
this.client = createPublicClient({
55+
chain,
56+
transport: fallback(rpcUrls.map((rpcUrl) => http(rpcUrl))),
57+
});
58+
}
59+
60+
/**
61+
* Retrieves the address of the Multicall3 contract.
62+
* @returns {Address | undefined} The address of the Multicall3 contract, or undefined if not found.
63+
*/
64+
getMulticall3Address(): Address | undefined {
65+
return this.chain?.contracts?.multicall3?.address;
66+
}
67+
68+
/**
69+
* Retrieves the balance of the specified address.
70+
* @param {Address} address The address for which to retrieve the balance.
71+
* @returns {Promise<bigint>} A Promise that resolves to the balance of the address.
72+
*/
73+
async getBalance(address: Address): Promise<bigint> {
74+
return this.client.getBalance({ address });
75+
}
76+
77+
/**
78+
* Retrieves the current block number.
79+
* @returns {Promise<bigint>} A Promise that resolves to the latest block number.
80+
*/
81+
async getBlockNumber(): Promise<bigint> {
82+
return this.client.getBlockNumber();
83+
}
84+
85+
/**
86+
* Retrieves the current block number.
87+
* @returns {Promise<GetBlockReturnType>} Latest block number.
88+
*/
89+
async getBlockByNumber(blockNumber: number): Promise<GetBlockReturnType> {
90+
return this.client.getBlock({ blockNumber: BigInt(blockNumber) });
91+
}
92+
93+
/**
94+
* Retrieves the current estimated gas price on the chain.
95+
* @returns {Promise<bigint>} A Promise that resolves to the current gas price.
96+
*/
97+
async getGasPrice(): Promise<bigint> {
98+
return this.client.getGasPrice();
99+
}
100+
101+
async estimateGas(args: EstimateGasParameters<typeof this.chain>): Promise<bigint> {
102+
return this.client.estimateGas(args);
103+
}
104+
105+
/**
106+
* Retrieves the value from a storage slot at a given address.
107+
* @param {Address} address The address of the contract.
108+
* @param {number} slot The slot number to read.
109+
* @returns {Promise<Hex>} A Promise that resolves to the value of the storage slot.
110+
* @throws {InvalidArgumentException} If the slot is not a positive integer.
111+
*/
112+
async getStorageAt(address: Address, slot: number | Hex): Promise<Hex | undefined> {
113+
if (typeof slot === "number" && (slot <= 0 || !Number.isInteger(slot))) {
114+
throw new InvalidArgumentException(
115+
`Slot must be a positive integer number. Received: ${slot}`,
116+
);
117+
}
118+
119+
return this.client.getStorageAt({
120+
address,
121+
slot: typeof slot === "string" ? slot : toHex(slot),
122+
});
123+
}
124+
125+
/**
126+
* Reads a contract "pure" or "view" function with the specified arguments using readContract from Viem.
127+
* @param {Address} contractAddress - The address of the contract.
128+
* @param {TAbi} abi - The ABI (Application Binary Interface) of the contract.
129+
* @param {TFunctionName} functionName - The name of the function to call.
130+
* @param {TArgs} [args] - The arguments to pass to the function (optional).
131+
* @returns A promise that resolves to the return value of the contract function.
132+
*/
133+
async readContract<
134+
TAbi extends Abi,
135+
TFunctionName extends ContractFunctionName<TAbi, "pure" | "view"> = ContractFunctionName<
136+
TAbi,
137+
"pure" | "view"
138+
>,
139+
TArgs extends ContractFunctionArgs<
140+
TAbi,
141+
"pure" | "view",
142+
TFunctionName
143+
> = ContractFunctionArgs<TAbi, "pure" | "view", TFunctionName>,
144+
>(
145+
contractAddress: Address,
146+
abi: TAbi,
147+
functionName: TFunctionName,
148+
args?: TArgs,
149+
): Promise<ContractFunctionReturnType<TAbi, "pure" | "view", TFunctionName, TArgs>> {
150+
return this.client.readContract({
151+
address: contractAddress,
152+
abi,
153+
functionName,
154+
args,
155+
});
156+
}
157+
158+
/**
159+
* Executes a batch request to deploy a contract and returns the decoded constructor return parameters.
160+
* @param {AbiWithConstructor} abi - The ABI (Application Binary Interface) of the contract. Must contain a constructor.
161+
* @param {Hex} bytecode - The bytecode of the contract.
162+
* @param {ContractConstructorArgs<typeof abi>} args - The constructor arguments for the contract.
163+
* @param constructorReturnParams - The return parameters of the contract's constructor.
164+
* @returns The decoded constructor return parameters.
165+
* @throws {DataDecodeException} if there is no return data or if the return data does not match the expected type.
166+
*/
167+
async batchRequest<ReturnType extends readonly AbiParameter[]>(
168+
abi: AbiWithConstructor,
169+
bytecode: Hex,
170+
args: ContractConstructorArgs<typeof abi>,
171+
constructorReturnParams: ReturnType,
172+
): Promise<DecodeAbiParametersReturnType<ReturnType>> {
173+
const deploymentData = args ? encodeDeployData({ abi, bytecode, args }) : bytecode;
174+
175+
const { data: returnData } = await this.client.call({
176+
data: deploymentData,
177+
});
178+
179+
if (!returnData) {
180+
throw new DataDecodeException("No return data");
181+
}
182+
183+
try {
184+
const decoded = decodeAbiParameters(constructorReturnParams, returnData);
185+
return decoded;
186+
} catch (e) {
187+
throw new DataDecodeException("Error decoding return data with given AbiParameters");
188+
}
189+
}
190+
191+
/**
192+
* Similar to readContract, but batches up multiple functions
193+
* on a contract in a single RPC call via the multicall3 contract.
194+
* @param {MulticallParameters} args - The parameters for the multicall.
195+
* @returns — An array of results. If allowFailure is true, with accompanying status
196+
* @throws {MulticallNotFound} if the Multicall contract is not found.
197+
*/
198+
async multicall<
199+
contracts extends readonly unknown[] = readonly ContractFunctionParameters[],
200+
allowFailure extends boolean = true,
201+
>(
202+
args: MulticallParameters<contracts, allowFailure>,
203+
): Promise<MulticallReturnType<contracts, allowFailure>> {
204+
if (!this.chain?.contracts?.multicall3?.address) throw new MulticallNotFound();
205+
206+
return this.client.multicall<contracts, allowFailure>(args);
207+
}
208+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./evmProvider.js";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./viem.types.js";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Abi, AbiConstructor } from "abitype";
2+
3+
export type AbiWithConstructor = readonly [AbiConstructor, ...Abi];
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Hex } from "viem";
2+
3+
export const structAbiFixture = {
4+
abi: [
5+
{
6+
inputs: [
7+
{
8+
internalType: "address[]",
9+
name: "_tokenAddresses",
10+
type: "address[]",
11+
},
12+
],
13+
stateMutability: "nonpayable",
14+
type: "constructor",
15+
},
16+
] as const,
17+
bytecode:
18+
`0x608060405234801561001057600080fd5b506040516108aa3803806108aa833981810160405281019061003291906104f2565b60008151905060008167ffffffffffffffff81111561007a577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280602002602001820160405280156100b357816020015b6100a06103a6565b8152602001906001900390816100985790505b50905060005b828110156103775760008482815181106100fc577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b602002602001015190508073ffffffffffffffffffffffffffffffffffffffff1663313ce5676040518163ffffffff1660e01b815260040160206040518083038186803b15801561014c57600080fd5b505afa158015610160573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101849190610574565b8383815181106101bd577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200260200101516000019060ff16908160ff16815250508073ffffffffffffffffffffffffffffffffffffffff166395d89b416040518163ffffffff1660e01b815260040160006040518083038186803b15801561021b57600080fd5b505afa15801561022f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906102589190610533565b838381518110610291577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6020026020010151602001819052508073ffffffffffffffffffffffffffffffffffffffff166306fdde036040518163ffffffff1660e01b815260040160006040518083038186803b1580156102e657600080fd5b505afa1580156102fa573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103239190610533565b83838151811061035c577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b602002602001015160400181905250816001019150506100b9565b5060008160405160200161038b91906106c5565b60405160208183030381529060405290506020810180590381f35b6040518060600160405280600060ff16815260200160608152602001606081525090565b60006103dd6103d884610718565b6106e7565b905080838252602082019050828560208602820111156103fc57600080fd5b60005b8581101561042c57816104128882610474565b8452602084019350602083019250506001810190506103ff565b5050509392505050565b600061044961044484610744565b6106e7565b90508281526020810184848401111561046157600080fd5b61046c848285610808565b509392505050565b6000815190506104838161087b565b92915050565b600082601f83011261049a57600080fd5b81516104aa8482602086016103ca565b91505092915050565b600082601f8301126104c457600080fd5b81516104d4848260208601610436565b91505092915050565b6000815190506104ec81610892565b92915050565b60006020828403121561050457600080fd5b600082015167ffffffffffffffff81111561051e57600080fd5b61052a84828501610489565b91505092915050565b60006020828403121561054557600080fd5b600082015167ffffffffffffffff81111561055f57600080fd5b61056b848285016104b3565b91505092915050565b60006020828403121561058657600080fd5b6000610594848285016104dd565b91505092915050565b60006105a9838361065f565b905092915050565b60006105bc82610784565b6105c681856107a7565b9350836020820285016105d885610774565b8060005b8581101561061457848403895281516105f5858261059d565b94506106008361079a565b925060208a019950506001810190506105dc565b50829750879550505050505092915050565b60006106318261078f565b61063b81856107b8565b935061064b818560208601610808565b6106548161086a565b840191505092915050565b600060608301600083015161067760008601826106b6565b506020830151848203602086015261068f8282610626565b915050604083015184820360408601526106a98282610626565b9150508091505092915050565b6106bf816107fb565b82525050565b600060208201905081810360008301526106df81846105b1565b905092915050565b6000604051905081810181811067ffffffffffffffff8211171561070e5761070d61083b565b5b8060405250919050565b600067ffffffffffffffff8211156107335761073261083b565b5b602082029050602081019050919050565b600067ffffffffffffffff82111561075f5761075e61083b565b5b601f19601f8301169050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b60006107d4826107db565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600060ff82169050919050565b60005b8381101561082657808201518184015260208101905061080b565b83811115610835576000848401525b50505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b610884816107c9565b811461088f57600080fd5b50565b61089b816107fb565b81146108a657600080fd5b5056fe` as Hex,
19+
args: [
20+
[
21+
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
22+
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
23+
],
24+
] as const,
25+
returnData:
26+
`0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000045745544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d57726170706564204574686572000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000045553444300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000855534420436f696e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000` as Hex,
27+
};
28+
29+
export const arrayAbiFixture = {
30+
...structAbiFixture,
31+
returnData:
32+
`0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48` as Hex,
33+
};

0 commit comments

Comments
 (0)