-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
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"; | ||
} | ||
} |
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"; | ||
} | ||
} |
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"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./external.js"; |
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"; |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty convoluted but just wanted to tell that Not sure if this is relevant but might be. [1] https://viem.sh/docs/actions/public/getBlockNumber.html#cachetime-optional There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ?? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./evmProvider.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./viem.types.js"; |
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]; |
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, | ||
}; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.