From d7d5c70d31e4879e33f83910fa9821adbec77341 Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Tue, 15 Oct 2024 16:45:39 -0300 Subject: [PATCH 1/4] feat: add evm provider and logger --- packages/chain-providers/README.md | 72 +++ packages/chain-providers/package.json | 22 + .../src/exceptions/dataDecode.exception.ts | 6 + .../chain-providers/src/exceptions/index.ts | 4 + .../exceptions/invalidArgument.exception.ts | 6 + .../exceptions/multicallNotFound.exception.ts | 5 + .../src/exceptions/rpcUrlsEmpty.exception.ts | 6 + packages/chain-providers/src/external.ts | 8 + packages/chain-providers/src/index.ts | 1 + packages/chain-providers/src/internal.ts | 3 + .../src/providers/evmProvider.ts | 208 ++++++++ .../chain-providers/src/providers/index.ts | 1 + packages/chain-providers/src/types/index.ts | 1 + .../chain-providers/src/types/viem.types.ts | 3 + .../test/fixtures/batchRequest.fixture.ts | 33 ++ .../providers/evmProvider.service.spec.ts | 381 ++++++++++++++ packages/chain-providers/tsconfig.build.json | 11 + packages/chain-providers/tsconfig.json | 4 + packages/chain-providers/vitest.config.ts | 28 + packages/shared/package.json | 3 +- packages/shared/src/external.ts | 1 + packages/shared/src/internal.ts | 1 + packages/shared/src/logger/index.ts | 2 + .../shared/src/logger/logger.interface.ts | 9 + packages/shared/src/logger/logger.ts | 59 +++ pnpm-lock.yaml | 479 ++++++++++++++++++ 26 files changed, 1356 insertions(+), 1 deletion(-) create mode 100644 packages/chain-providers/README.md create mode 100644 packages/chain-providers/package.json create mode 100644 packages/chain-providers/src/exceptions/dataDecode.exception.ts create mode 100644 packages/chain-providers/src/exceptions/index.ts create mode 100644 packages/chain-providers/src/exceptions/invalidArgument.exception.ts create mode 100644 packages/chain-providers/src/exceptions/multicallNotFound.exception.ts create mode 100644 packages/chain-providers/src/exceptions/rpcUrlsEmpty.exception.ts create mode 100644 packages/chain-providers/src/external.ts create mode 100644 packages/chain-providers/src/index.ts create mode 100644 packages/chain-providers/src/internal.ts create mode 100644 packages/chain-providers/src/providers/evmProvider.ts create mode 100644 packages/chain-providers/src/providers/index.ts create mode 100644 packages/chain-providers/src/types/index.ts create mode 100644 packages/chain-providers/src/types/viem.types.ts create mode 100644 packages/chain-providers/test/fixtures/batchRequest.fixture.ts create mode 100644 packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts create mode 100644 packages/chain-providers/tsconfig.build.json create mode 100644 packages/chain-providers/tsconfig.json create mode 100644 packages/chain-providers/vitest.config.ts create mode 100644 packages/shared/src/logger/index.ts create mode 100644 packages/shared/src/logger/logger.interface.ts create mode 100644 packages/shared/src/logger/logger.ts diff --git a/packages/chain-providers/README.md b/packages/chain-providers/README.md new file mode 100644 index 0000000..bf09f87 --- /dev/null +++ b/packages/chain-providers/README.md @@ -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, constructorReturnParams: ReturnType)` +- `multicall(args: MulticallParameters)` + +For more details on both providers, refer to their implementations. diff --git a/packages/chain-providers/package.json b/packages/chain-providers/package.json new file mode 100644 index 0000000..e48ce2a --- /dev/null +++ b/packages/chain-providers/package.json @@ -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" + } +} diff --git a/packages/chain-providers/src/exceptions/dataDecode.exception.ts b/packages/chain-providers/src/exceptions/dataDecode.exception.ts new file mode 100644 index 0000000..6fbfaa4 --- /dev/null +++ b/packages/chain-providers/src/exceptions/dataDecode.exception.ts @@ -0,0 +1,6 @@ +export class DataDecodeException extends Error { + constructor(message: string) { + super(message); + this.name = "DataDecodeException"; + } +} diff --git a/packages/chain-providers/src/exceptions/index.ts b/packages/chain-providers/src/exceptions/index.ts new file mode 100644 index 0000000..3c7b4f8 --- /dev/null +++ b/packages/chain-providers/src/exceptions/index.ts @@ -0,0 +1,4 @@ +export * from "./invalidArgument.exception.js"; +export * from "./dataDecode.exception.js"; +export * from "./multicallNotFound.exception.js"; +export * from "./rpcUrlsEmpty.exception.js"; diff --git a/packages/chain-providers/src/exceptions/invalidArgument.exception.ts b/packages/chain-providers/src/exceptions/invalidArgument.exception.ts new file mode 100644 index 0000000..84b6087 --- /dev/null +++ b/packages/chain-providers/src/exceptions/invalidArgument.exception.ts @@ -0,0 +1,6 @@ +export class InvalidArgumentException extends Error { + constructor(message: string) { + super(message); + this.name = "InvalidArgumentException"; + } +} diff --git a/packages/chain-providers/src/exceptions/multicallNotFound.exception.ts b/packages/chain-providers/src/exceptions/multicallNotFound.exception.ts new file mode 100644 index 0000000..45b3902 --- /dev/null +++ b/packages/chain-providers/src/exceptions/multicallNotFound.exception.ts @@ -0,0 +1,5 @@ +export class MulticallNotFound extends Error { + constructor() { + super("Multicall contract address not found"); + } +} diff --git a/packages/chain-providers/src/exceptions/rpcUrlsEmpty.exception.ts b/packages/chain-providers/src/exceptions/rpcUrlsEmpty.exception.ts new file mode 100644 index 0000000..c57457f --- /dev/null +++ b/packages/chain-providers/src/exceptions/rpcUrlsEmpty.exception.ts @@ -0,0 +1,6 @@ +export class RpcUrlsEmpty extends Error { + constructor() { + super("RPC URLs array cannot be empty"); + this.name = "RpcUrlsEmpty"; + } +} diff --git a/packages/chain-providers/src/external.ts b/packages/chain-providers/src/external.ts new file mode 100644 index 0000000..bf158a8 --- /dev/null +++ b/packages/chain-providers/src/external.ts @@ -0,0 +1,8 @@ +export { + DataDecodeException, + InvalidArgumentException, + MulticallNotFound, + RpcUrlsEmpty, +} from "./internal.js"; + +export { EvmProvider } from "./internal.js"; diff --git a/packages/chain-providers/src/index.ts b/packages/chain-providers/src/index.ts new file mode 100644 index 0000000..a5a2748 --- /dev/null +++ b/packages/chain-providers/src/index.ts @@ -0,0 +1 @@ +export * from "./external.js"; diff --git a/packages/chain-providers/src/internal.ts b/packages/chain-providers/src/internal.ts new file mode 100644 index 0000000..40dbafa --- /dev/null +++ b/packages/chain-providers/src/internal.ts @@ -0,0 +1,3 @@ +export * from "./types/index.js"; +export * from "./exceptions/index.js"; +export * from "./providers/index.js"; diff --git a/packages/chain-providers/src/providers/evmProvider.ts b/packages/chain-providers/src/providers/evmProvider.ts new file mode 100644 index 0000000..5ceda9e --- /dev/null +++ b/packages/chain-providers/src/providers/evmProvider.ts @@ -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, 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} A Promise that resolves to the balance of the address. + */ + async getBalance(address: Address): Promise { + return this.client.getBalance({ address }); + } + + /** + * Retrieves the current block number. + * @returns {Promise} A Promise that resolves to the latest block number. + */ + async getBlockNumber(): Promise { + return this.client.getBlockNumber(); + } + + /** + * Retrieves the current block number. + * @returns {Promise} Latest block number. + */ + async getBlockByNumber(blockNumber: number): Promise { + return this.client.getBlock({ blockNumber: BigInt(blockNumber) }); + } + + /** + * Retrieves the current estimated gas price on the chain. + * @returns {Promise} A Promise that resolves to the current gas price. + */ + async getGasPrice(): Promise { + return this.client.getGasPrice(); + } + + async estimateGas(args: EstimateGasParameters): Promise { + 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} 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 { + 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 = ContractFunctionName< + TAbi, + "pure" | "view" + >, + TArgs extends ContractFunctionArgs< + TAbi, + "pure" | "view", + TFunctionName + > = ContractFunctionArgs, + >( + contractAddress: Address, + abi: TAbi, + functionName: TFunctionName, + args?: TArgs, + ): Promise> { + 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} 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( + abi: AbiWithConstructor, + bytecode: Hex, + args: ContractConstructorArgs, + constructorReturnParams: ReturnType, + ): Promise> { + 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, + ): Promise> { + if (!this.chain?.contracts?.multicall3?.address) throw new MulticallNotFound(); + + return this.client.multicall(args); + } +} diff --git a/packages/chain-providers/src/providers/index.ts b/packages/chain-providers/src/providers/index.ts new file mode 100644 index 0000000..25e7bcb --- /dev/null +++ b/packages/chain-providers/src/providers/index.ts @@ -0,0 +1 @@ +export * from "./evmProvider.js"; diff --git a/packages/chain-providers/src/types/index.ts b/packages/chain-providers/src/types/index.ts new file mode 100644 index 0000000..12bd81d --- /dev/null +++ b/packages/chain-providers/src/types/index.ts @@ -0,0 +1 @@ +export * from "./viem.types.js"; diff --git a/packages/chain-providers/src/types/viem.types.ts b/packages/chain-providers/src/types/viem.types.ts new file mode 100644 index 0000000..d5fc5ab --- /dev/null +++ b/packages/chain-providers/src/types/viem.types.ts @@ -0,0 +1,3 @@ +import { Abi, AbiConstructor } from "abitype"; + +export type AbiWithConstructor = readonly [AbiConstructor, ...Abi]; diff --git a/packages/chain-providers/test/fixtures/batchRequest.fixture.ts b/packages/chain-providers/test/fixtures/batchRequest.fixture.ts new file mode 100644 index 0000000..de52860 --- /dev/null +++ b/packages/chain-providers/test/fixtures/batchRequest.fixture.ts @@ -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, +}; diff --git a/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts new file mode 100644 index 0000000..8944d6c --- /dev/null +++ b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts @@ -0,0 +1,381 @@ +import { parseAbi } from "abitype"; +import * as viem from "viem"; +import { localhost } from "viem/chains"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { ILogger } from "@grants-stack-indexer/shared"; + +import { + DataDecodeException, + EvmProvider, + MulticallNotFound, + RpcUrlsEmpty, +} from "../../../src/internal.js"; +import { arrayAbiFixture, structAbiFixture } from "../../fixtures/batchRequest.fixture.js"; + +const mockClient = { + getBalance: vi.fn(), + getBlockNumber: vi.fn(), + getGasPrice: vi.fn(), + estimateGas: vi.fn(), + getStorageAt: vi.fn(), + readContract: vi.fn(), + multicall: vi.fn(), + call: vi.fn(), +}; + +vi.mock("viem", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createPublicClient: vi.fn().mockImplementation(() => mockClient), + http: vi.fn(), + }; +}); + +export const mockLogger: ILogger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), +}; +const testAbi = parseAbi([ + "constructor(uint256 totalSupply)", + "function balanceOf(address owner) view returns (uint256)", + "function tokenURI(uint256 tokenId) pure returns (string)", +]); + +describe("EvmProvider", () => { + let viemProvider: EvmProvider | null = null; + const defaultMockChain: viem.Chain = vi.mocked({ + ...localhost, + contracts: { multicall3: undefined }, + }); + const defaultRpcUrls = ["http://localhost:8545"]; + + afterEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + viemProvider = null; + }); + + it("has a client property defined", () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + expect(viemProvider["client"]).toBeDefined(); + }); + + it("throws RpcUrlsEmpty error if rpcUrls is empty", () => { + expect(() => { + new EvmProvider([], defaultMockChain, mockLogger); + }).toThrowError(RpcUrlsEmpty); + }); + + describe("getBalance", () => { + it("should return the balance of the specified address", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const address = "0x123456789"; + const expectedBalance = 100n; + vi.spyOn(mockClient, "getBalance").mockResolvedValue(expectedBalance); + + const balance = await viemProvider.getBalance(address); + + expect(balance).toBe(expectedBalance); + expect(mockClient.getBalance).toHaveBeenCalledWith({ address }); + }); + }); + + describe("getBlockNumber", () => { + it("should return the current block number", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const expectedBlockNumber = 1000n; + vi.spyOn(mockClient, "getBlockNumber").mockResolvedValue(expectedBlockNumber); + + const blockNumber = await viemProvider.getBlockNumber(); + + expect(blockNumber).toBe(expectedBlockNumber); + }); + }); + + describe("getGasPrice", () => { + it("should return the current gas price", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const expectedGasPrice = BigInt(100); + // Mock the getGasPrice method of the Viem client + vi.spyOn(mockClient, "getGasPrice").mockResolvedValue(expectedGasPrice); + + const gasPrice = await viemProvider.getGasPrice(); + + expect(gasPrice).toBe(expectedGasPrice); + }); + }); + + describe("estimateGas", () => { + it("return the estimated gas for the given transaction", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const args = vi.mocked>({ + account: "0xffff", + to: viem.zeroAddress, + value: 100n, + }); + + const expectedGas = 50000n; + vi.spyOn(mockClient, "estimateGas").mockResolvedValue(expectedGas); + + const gas = await viemProvider.estimateGas(args); + + expect(gas).toBe(expectedGas); + expect(mockClient.estimateGas).toHaveBeenCalledWith(args); + }); + }); + + describe("getStorageAt", () => { + it("should return the value of the storage slot at the given address and slot number", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const address = "0x123456789"; + const slot = 1; + const expectedValue = "0xabcdef"; + vi.spyOn(mockClient, "getStorageAt").mockResolvedValue(expectedValue); + + const value = await viemProvider.getStorageAt(address, slot); + + expect(value).toBe(expectedValue); + expect(mockClient.getStorageAt).toHaveBeenCalledWith({ address, slot: "0x1" }); + }); + + it("should return the value of the storage slot at the given address and slot value", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const address = "0x123456789"; + const slot = "0x12"; + const expectedValue = "0xabcdef"; + vi.spyOn(mockClient, "getStorageAt").mockResolvedValue(expectedValue); + + const value = await viemProvider.getStorageAt(address, slot); + + expect(value).toBe(expectedValue); + expect(mockClient.getStorageAt).toHaveBeenCalledWith({ address, slot: "0x12" }); + }); + + it("should throw an error if the slot is not a positive integer", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const address = "0x123456789"; + const slot = -1; + + await expect(viemProvider.getStorageAt(address, slot)).rejects.toThrowError( + "Slot must be a positive integer number. Received: -1", + ); + }); + }); + + describe("readContract", () => { + it("should call the readContract method of the Viem client with the correct arguments", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const contractAddress = "0x123456789"; + const abi = testAbi; + const functionName = "balanceOf"; + const expectedReturnValue = 5n; + + // Mock the readContract method of the Viem client + vi.spyOn(mockClient, "readContract").mockResolvedValue(expectedReturnValue); + + const returnValue = await viemProvider.readContract(contractAddress, abi, functionName); + + expect(returnValue).toBe(expectedReturnValue); + expect(mockClient.readContract).toHaveBeenCalledWith({ + address: contractAddress, + abi, + functionName, + }); + }); + + it("should call the readContract method of the Viem client with the correct arguments when args are provided", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const contractAddress = "0x123456789"; + const functionName = "tokenURI"; + const args = [1n] as const; + const expectedReturnValue = "tokenUri"; + + // Mock the readContract method of the Viem client + vi.spyOn(mockClient, "readContract").mockResolvedValue(expectedReturnValue); + + const returnValue = await viemProvider.readContract( + contractAddress, + testAbi, + functionName, + args, + ); + + expect(returnValue).toBe(expectedReturnValue); + expect(mockClient.readContract).toHaveBeenCalledWith({ + address: contractAddress, + abi: testAbi, + functionName, + args, + }); + }); + }); + + describe("batchRequest", () => { + it("should properly encode bytecode data and decode return data from batch request call", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const returnAbiParams = viem.parseAbiParameters([ + "TokenData[] returnData", + "struct TokenData { uint8 tokenDecimals; string tokenSymbol; string tokenName; }", + ]); + vi.spyOn(mockClient, "call").mockResolvedValue({ data: structAbiFixture.returnData }); + + const [returnValue] = await viemProvider.batchRequest( + structAbiFixture.abi, + structAbiFixture.bytecode, + structAbiFixture.args, + returnAbiParams, + ); + + expect(returnValue).toStrictEqual([ + { + tokenDecimals: 18, + tokenSymbol: "WETH", + tokenName: "Wrapped Ether", + }, + { + tokenDecimals: 6, + tokenSymbol: "USDC", + tokenName: "USD Coin", + }, + ]); + }); + + it("should fail if no data is returned", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const returnAbiParams = viem.parseAbiParameters([ + "TokenData[] returnData", + "struct TokenData { uint8 tokenDecimals; string tokenSymbol; string tokenName; }", + ]); + + vi.spyOn(mockClient, "call").mockResolvedValue({ data: undefined }); + + await expect( + viemProvider.batchRequest( + structAbiFixture.abi, + structAbiFixture.bytecode, + structAbiFixture.args, + returnAbiParams, + ), + ).rejects.toThrowError(DataDecodeException); + }); + + it("should fail if decoded data does not match validator (missing struct fields)", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + // this schema is incorrect, it should have 3 fields instead of 2 + const returnAbiParams = viem.parseAbiParameters([ + "WrongTokenData[] returnData", + "struct WrongTokenData { string tokenSymbol; string tokenName; }", + ]); + + vi.spyOn(mockClient, "call").mockResolvedValue({ data: structAbiFixture.returnData }); + + await expect( + viemProvider.batchRequest( + structAbiFixture.abi, + structAbiFixture.bytecode, + structAbiFixture.args, + returnAbiParams, + ), + ).rejects.toThrowError( + new DataDecodeException("Error decoding return data with given AbiParameters"), + ); + }); + + it("should fail if decoded data does not match validator (not struct vs struct)", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + // this schema is incorrect, it should have 3 fields instead of 2 + const returnAbiParams = viem.parseAbiParameters("uint8 decimals, address[] owners"); + + vi.spyOn(mockClient, "call").mockResolvedValue({ data: structAbiFixture.returnData }); + + await expect( + viemProvider.batchRequest( + structAbiFixture.abi, + structAbiFixture.bytecode, + structAbiFixture.args, + returnAbiParams, + ), + ).rejects.toThrowError( + new DataDecodeException("Error decoding return data with given AbiParameters"), + ); + }); + + it("should properly decode address[]", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const returnAbiParams = viem.parseAbiParameters("address[]"); + + vi.spyOn(mockClient, "call").mockResolvedValue({ data: arrayAbiFixture.returnData }); + + const [returnValue] = await viemProvider.batchRequest( + arrayAbiFixture.abi, + arrayAbiFixture.bytecode, + arrayAbiFixture.args, + returnAbiParams, + ); + + expect(returnValue).toEqual(arrayAbiFixture.args[0]); + }); + }); + describe("multicall", () => { + it("calls the multicall method of the Viem client with the correct arguments", async () => { + const mockChain: viem.Chain = vi.mocked({ + ...localhost, + contracts: { multicall3: { address: "0x123456789" } }, + }); + + viemProvider = new EvmProvider(defaultRpcUrls, mockChain, mockLogger); + const contracts = [ + { + address: "0x123456789", + abi: testAbi, + functionName: "balanceOf", + args: ["0x987654321"], + } as const, + { + address: "0x123456789", + abi: testAbi, + functionName: "tokenURI", + args: [1n], + } as const, + { + address: "0x987654321", + abi: testAbi, + functionName: "totalSupply", + args: [], + } as const, + ]; + + const expectedReturnValue = [ + { result: 100n, status: true }, + { result: "tokenUri", status: true }, + { result: 1000n, status: true }, + ]; + vi.spyOn(mockClient, "multicall").mockResolvedValue(expectedReturnValue); + + const returnValue = await viemProvider.multicall({ contracts }); + + expect(returnValue).toEqual(expectedReturnValue); + expect(mockClient.multicall).toHaveBeenCalledWith({ contracts }); + }); + + it("throws a MulticallNotFound error if the Multicall contract is not found for the chain", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const contracts = [ + { + address: "0x123456789", + abi: testAbi, + functionName: "balanceOf", + args: ["0x987654321"], + } as const, + ]; + + await expect(viemProvider.multicall({ contracts })).rejects.toThrowError( + MulticallNotFound, + ); + }); + }); +}); diff --git a/packages/chain-providers/tsconfig.build.json b/packages/chain-providers/tsconfig.build.json new file mode 100644 index 0000000..0be123c --- /dev/null +++ b/packages/chain-providers/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "declaration": true, + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/packages/chain-providers/tsconfig.json b/packages/chain-providers/tsconfig.json new file mode 100644 index 0000000..83e2c6c --- /dev/null +++ b/packages/chain-providers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*", "test/fixtures"] +} diff --git a/packages/chain-providers/vitest.config.ts b/packages/chain-providers/vitest.config.ts new file mode 100644 index 0000000..7f9df03 --- /dev/null +++ b/packages/chain-providers/vitest.config.ts @@ -0,0 +1,28 @@ +import path from "path"; +import { configDefaults, defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, // Use Vitest's global API without importing it in each file + environment: "node", // Use the Node.js environment + include: ["test/**/*.spec.ts"], // Include test files + exclude: ["node_modules", "dist"], // Exclude certain directories + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], // Coverage reporters + exclude: [ + "node_modules", + "dist", + "src/index.ts", + "**/external.ts", + ...configDefaults.exclude, + ], // Files to exclude from coverage + }, + }, + resolve: { + alias: { + // Setup path alias based on tsconfig paths + "@": path.resolve(__dirname, "src"), + }, + }, +}); diff --git a/packages/shared/package.json b/packages/shared/package.json index 45674ad..2faaa5b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -28,6 +28,7 @@ "test:cov": "vitest run --config vitest.config.ts --coverage" }, "dependencies": { - "viem": "2.21.19" + "viem": "2.21.19", + "winston": "3.15.0" } } diff --git a/packages/shared/src/external.ts b/packages/shared/src/external.ts index 6547286..8613945 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -1,3 +1,4 @@ export type * from "./types/index.js"; export type { Address } from "./internal.js"; +export type { ILogger } from "./internal.js"; export { NATIVE_TOKEN_ADDRESS, isNativeToken } from "./constants/index.js"; diff --git a/packages/shared/src/internal.ts b/packages/shared/src/internal.ts index 7dc65a3..872e172 100644 --- a/packages/shared/src/internal.ts +++ b/packages/shared/src/internal.ts @@ -1,3 +1,4 @@ export type { Address } from "viem"; export * from "./types/index.js"; export * from "./constants/index.js"; +export * from "./logger/index.js"; diff --git a/packages/shared/src/logger/index.ts b/packages/shared/src/logger/index.ts new file mode 100644 index 0000000..fadf281 --- /dev/null +++ b/packages/shared/src/logger/index.ts @@ -0,0 +1,2 @@ +export * from "./logger.interface.js"; +export * from "./logger.js"; diff --git a/packages/shared/src/logger/logger.interface.ts b/packages/shared/src/logger/logger.interface.ts new file mode 100644 index 0000000..b647e39 --- /dev/null +++ b/packages/shared/src/logger/logger.interface.ts @@ -0,0 +1,9 @@ +/** + * Generic logger interface. + */ +export interface ILogger { + error: (error: Error | string) => void; + info: (message: string) => void; + warn: (message: string) => void; + debug: (message: string) => void; +} diff --git a/packages/shared/src/logger/logger.ts b/packages/shared/src/logger/logger.ts new file mode 100644 index 0000000..4ba6656 --- /dev/null +++ b/packages/shared/src/logger/logger.ts @@ -0,0 +1,59 @@ +import { createLogger, format, transports, Logger as WinstonLogger } from "winston"; + +import { ILogger } from "./logger.interface.js"; + +type LogLevel = "error" | "warn" | "info" | "debug"; + +const validLogLevels: LogLevel[] = ["error", "warn", "info", "debug"]; + +export class Logger implements ILogger { + private logger: WinstonLogger; + private static instance: Logger | null; + private level: LogLevel; + private constructor() { + this.level = this.isValidLogLevel(process.env.LOG_LEVEL) ? process.env.LOG_LEVEL : "info"; + this.logger = createLogger({ + level: this.level, + format: format.combine( + format.colorize(), + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.errors({ stack: true }), + format.printf(({ level, message, timestamp, stack }) => { + return `${timestamp} ${level}: ${stack ?? message ?? ""}`; + }), + ), + transports: [new transports.Console()], + }); + } + /** + * Returns the instance of the Logger class. + * @param level The log level to be used by the logger. + * @returns The instance of the Logger class. + */ + public static getInstance(): ILogger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + isValidLogLevel(level?: string): level is LogLevel { + return validLogLevels.includes(level as LogLevel); + } + + info(message: string): void { + this.logger.info(message); + } + error(error: Error | string): void { + if (error instanceof Error) { + this.logger.error(error); + } else { + this.logger.error(new Error(error)); + } + } + warn(message: string): void { + this.logger.warn(message); + } + debug(message: string): void { + this.logger.debug(message); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 442ab68..7cd70c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,18 @@ importers: specifier: 5.2.2 version: 5.2.2 + packages/chain-providers: + dependencies: + "@grants-stack-indexer/shared": + specifier: workspace:* + version: link:../shared + abitype: + specifier: 1.0.6 + version: 1.0.6(typescript@5.5.4)(zod@3.23.8) + viem: + specifier: 2.19.6 + version: 2.19.6(typescript@5.5.4)(zod@3.23.8) + packages/data-flow: dependencies: "@grants-stack-indexer/indexer-client": @@ -185,6 +197,9 @@ importers: viem: specifier: 2.21.19 version: 2.21.19(typescript@5.5.4)(zod@3.23.8) + winston: + specifier: 3.15.0 + version: 3.15.0 packages: "@adraffy/ens-normalize@1.10.0": @@ -334,6 +349,13 @@ packages: integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==, } + "@colors/colors@1.6.0": + resolution: + { + integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==, + } + engines: { node: ">=0.1.90" } + "@commitlint/cli@19.4.1": resolution: { @@ -461,6 +483,12 @@ packages: } engines: { node: ">=12" } + "@dabh/diagnostics@2.0.3": + resolution: + { + integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==, + } + "@esbuild/aix-ppc64@0.21.5": resolution: { @@ -811,6 +839,12 @@ packages: integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==, } + "@noble/curves@1.4.0": + resolution: + { + integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==, + } + "@noble/curves@1.6.0": resolution: { @@ -825,6 +859,13 @@ packages: } engines: { node: ">= 16" } + "@noble/hashes@1.4.0": + resolution: + { + integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==, + } + engines: { node: ">= 16" } + "@noble/hashes@1.5.0": resolution: { @@ -1001,12 +1042,24 @@ packages: integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==, } + "@scure/bip32@1.4.0": + resolution: + { + integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==, + } + "@scure/bip32@1.5.0": resolution: { integrity: sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==, } + "@scure/bip39@1.3.0": + resolution: + { + integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==, + } + "@scure/bip39@1.4.0": resolution: { @@ -1091,6 +1144,12 @@ packages: integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==, } + "@types/triple-beam@1.3.5": + resolution: + { + integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==, + } + "@typescript-eslint/eslint-plugin@7.18.0": resolution: { @@ -1230,6 +1289,20 @@ packages: } hasBin: true + abitype@1.0.5: + resolution: + { + integrity: sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==, + } + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abitype@1.0.6: resolution: { @@ -1244,6 +1317,13 @@ packages: zod: optional: true + abort-controller@3.0.0: + resolution: + { + integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, + } + engines: { node: ">=6.5" } + acorn-jsx@5.3.2: resolution: { @@ -1392,6 +1472,12 @@ packages: } engines: { node: ">=12" } + async@3.2.6: + resolution: + { + integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==, + } + asynckit@0.4.0: resolution: { @@ -1418,6 +1504,12 @@ packages: integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, } + base64-js@1.5.1: + resolution: + { + integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, + } + binary-extensions@2.3.0: resolution: { @@ -1464,6 +1556,12 @@ packages: integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, } + buffer@6.0.3: + resolution: + { + integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, + } + cac@6.7.14: resolution: { @@ -1598,12 +1696,30 @@ packages: integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, } + color-string@1.9.1: + resolution: + { + integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==, + } + + color@3.2.1: + resolution: + { + integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==, + } + colorette@2.0.20: resolution: { integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, } + colorspace@1.1.4: + resolution: + { + integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==, + } + combined-stream@1.0.8: resolution: { @@ -1853,6 +1969,12 @@ packages: integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, } + enabled@2.0.0: + resolution: + { + integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==, + } + env-paths@2.2.1: resolution: { @@ -2037,12 +2159,26 @@ packages: } engines: { node: ">=14.0.0" } + event-target-shim@5.0.1: + resolution: + { + integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, + } + engines: { node: ">=6" } + eventemitter3@5.0.1: resolution: { integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==, } + events@3.3.0: + resolution: + { + integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==, + } + engines: { node: ">=0.8.x" } + execa@8.0.1: resolution: { @@ -2093,6 +2229,12 @@ packages: integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, } + fecha@4.2.3: + resolution: + { + integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==, + } + file-entry-cache@6.0.1: resolution: { @@ -2141,6 +2283,12 @@ packages: integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==, } + fn.name@1.1.0: + resolution: + { + integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==, + } + follow-redirects@1.15.9: resolution: { @@ -2380,6 +2528,12 @@ packages: engines: { node: ">=18" } hasBin: true + ieee754@1.2.1: + resolution: + { + integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, + } + ignore@5.3.2: resolution: { @@ -2433,6 +2587,12 @@ packages: integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, } + is-arrayish@0.3.2: + resolution: + { + integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==, + } + is-binary-path@2.1.0: resolution: { @@ -2517,6 +2677,13 @@ packages: } engines: { node: ">=12" } + is-stream@2.0.1: + resolution: + { + integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, + } + engines: { node: ">=8" } + is-stream@3.0.0: resolution: { @@ -2544,6 +2711,14 @@ packages: integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, } + isows@1.0.4: + resolution: + { + integrity: sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==, + } + peerDependencies: + ws: "*" + isows@1.0.6: resolution: { @@ -2672,6 +2847,12 @@ packages: integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, } + kuler@2.0.0: + resolution: + { + integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==, + } + kysely@0.27.4: resolution: { @@ -2802,6 +2983,13 @@ packages: } engines: { node: ">=18" } + logform@2.6.1: + resolution: + { + integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==, + } + engines: { node: ">= 12.0.0" } + loupe@2.3.7: resolution: { @@ -3020,6 +3208,12 @@ packages: integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, } + one-time@1.0.0: + resolution: + { + integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==, + } + onetime@6.0.0: resolution: { @@ -3341,6 +3535,13 @@ packages: engines: { node: ">=14" } hasBin: true + process@0.11.10: + resolution: + { + integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==, + } + engines: { node: ">= 0.6.0" } + proxy-from-env@1.1.0: resolution: { @@ -3366,6 +3567,20 @@ packages: integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==, } + readable-stream@3.6.2: + resolution: + { + integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, + } + engines: { node: ">= 6" } + + readable-stream@4.5.2: + resolution: + { + integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + readdirp@3.6.0: resolution: { @@ -3462,6 +3677,13 @@ packages: integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, } + safe-stable-stringify@2.5.0: + resolution: + { + integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==, + } + engines: { node: ">=10" } + semver@6.3.1: resolution: { @@ -3510,6 +3732,12 @@ packages: } engines: { node: ">=14" } + simple-swizzle@0.2.2: + resolution: + { + integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==, + } + slash@3.0.0: resolution: { @@ -3578,6 +3806,12 @@ packages: } engines: { node: ">= 10.x" } + stack-trace@0.0.10: + resolution: + { + integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==, + } + stackback@0.0.2: resolution: { @@ -3625,6 +3859,12 @@ packages: } engines: { node: ">=18" } + string_decoder@1.3.0: + resolution: + { + integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, + } + strip-ansi@6.0.1: resolution: { @@ -3702,6 +3942,12 @@ packages: } engines: { node: ">=8" } + text-hex@1.0.0: + resolution: + { + integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==, + } + text-table@0.2.0: resolution: { @@ -3755,6 +4001,13 @@ packages: } engines: { node: ">=8.0" } + triple-beam@1.4.1: + resolution: + { + integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==, + } + engines: { node: ">= 14.0.0" } + ts-api-utils@1.3.0: resolution: { @@ -3950,12 +4203,29 @@ packages: integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, } + util-deprecate@1.0.2: + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } + v8-compile-cache-lib@3.0.1: resolution: { integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==, } + viem@2.19.6: + resolution: + { + integrity: sha512-7jiuX+ZncPZE8iEzgW/iO7EaAMRJEW8hYeJy9YFTMK34Wa9aWzqOm4lrORgemlsqJ/kpQG4InzhG0jc2HY67FA==, + } + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + viem@2.21.19: resolution: { @@ -4043,6 +4313,12 @@ packages: integrity: sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA==, } + webauthn-p256@0.0.5: + resolution: + { + integrity: sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg==, + } + which@2.0.2: resolution: { @@ -4059,6 +4335,20 @@ packages: engines: { node: ">=8" } hasBin: true + winston-transport@4.8.0: + resolution: + { + integrity: sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==, + } + engines: { node: ">= 12.0.0" } + + winston@3.15.0: + resolution: + { + integrity: sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==, + } + engines: { node: ">= 12.0.0" } + word-wrap@1.2.5: resolution: { @@ -4099,6 +4389,21 @@ packages: integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, } + ws@8.17.1: + resolution: + { + integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==, + } + engines: { node: ">=10.0.0" } + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: { @@ -4350,6 +4655,8 @@ snapshots: "@bcoe/v8-coverage@0.2.3": {} + "@colors/colors@1.6.0": {} + "@commitlint/cli@19.4.1(@types/node@20.3.1)(typescript@5.5.4)": dependencies: "@commitlint/format": 19.3.0 @@ -4465,6 +4772,12 @@ snapshots: dependencies: "@jridgewell/trace-mapping": 0.3.9 + "@dabh/diagnostics@2.0.3": + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + "@esbuild/aix-ppc64@0.21.5": optional: true @@ -4640,12 +4953,18 @@ snapshots: dependencies: "@noble/hashes": 1.3.2 + "@noble/curves@1.4.0": + dependencies: + "@noble/hashes": 1.4.0 + "@noble/curves@1.6.0": dependencies: "@noble/hashes": 1.5.0 "@noble/hashes@1.3.2": {} + "@noble/hashes@1.4.0": {} + "@noble/hashes@1.5.0": {} "@nodelib/fs.scandir@2.1.5": @@ -4715,12 +5034,23 @@ snapshots: "@scure/base@1.1.9": {} + "@scure/bip32@1.4.0": + dependencies: + "@noble/curves": 1.4.0 + "@noble/hashes": 1.4.0 + "@scure/base": 1.1.9 + "@scure/bip32@1.5.0": dependencies: "@noble/curves": 1.6.0 "@noble/hashes": 1.5.0 "@scure/base": 1.1.9 + "@scure/bip39@1.3.0": + dependencies: + "@noble/hashes": 1.4.0 + "@scure/base": 1.1.9 + "@scure/bip39@1.4.0": dependencies: "@noble/hashes": 1.5.0 @@ -4761,6 +5091,8 @@ snapshots: pg-protocol: 1.7.0 pg-types: 4.0.2 + "@types/triple-beam@1.3.5": {} + "@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.56.0)(typescript@5.5.4))(eslint@8.56.0)(typescript@5.5.4)": dependencies: "@eslint-community/regexpp": 4.11.0 @@ -4900,11 +5232,20 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + abitype@1.0.5(typescript@5.5.4)(zod@3.23.8): + optionalDependencies: + typescript: 5.5.4 + zod: 3.23.8 + abitype@1.0.6(typescript@5.5.4)(zod@3.23.8): optionalDependencies: typescript: 5.5.4 zod: 3.23.8 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 @@ -4977,6 +5318,8 @@ snapshots: assertion-error@2.0.1: {} + async@3.2.6: {} + asynckit@0.4.0: {} axios-mock-adapter@2.0.0(axios@1.7.7): @@ -4995,6 +5338,8 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.11: @@ -5021,6 +5366,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + cac@6.7.14: {} callsites@3.1.0: {} @@ -5111,8 +5461,23 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + colorette@2.0.20: {} + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -5231,6 +5596,8 @@ snapshots: emoji-regex@9.2.2: {} + enabled@2.0.0: {} + env-paths@2.2.1: {} envio-darwin-arm64@2.4.1: @@ -5388,8 +5755,12 @@ snapshots: - bufferutil - utf-8-validate + event-target-shim@5.0.1: {} + eventemitter3@5.0.1: {} + events@3.3.0: {} + execa@8.0.1: dependencies: cross-spawn: 7.0.3 @@ -5424,6 +5795,8 @@ snapshots: dependencies: reusify: 1.0.4 + fecha@4.2.3: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -5453,6 +5826,8 @@ snapshots: flatted@3.3.1: {} + fn.name@1.1.0: {} + follow-redirects@1.15.9: {} foreground-child@3.3.0: @@ -5576,6 +5951,8 @@ snapshots: husky@9.1.5: {} + ieee754@1.2.1: {} + ignore@5.3.2: {} import-fresh@3.3.0: @@ -5598,6 +5975,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -5628,6 +6007,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-stream@2.0.1: {} + is-stream@3.0.0: {} is-text-path@2.0.0: @@ -5638,6 +6019,10 @@ snapshots: isexe@2.0.0: {} + isows@1.0.4(ws@8.17.1): + dependencies: + ws: 8.17.1 + isows@1.0.6(ws@8.18.0): dependencies: ws: 8.18.0 @@ -5702,6 +6087,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kuler@2.0.0: {} + kysely@0.27.4: {} levn@0.4.1: @@ -5778,6 +6165,15 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + logform@2.6.1: + dependencies: + "@colors/colors": 1.6.0 + "@types/triple-beam": 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + loupe@2.3.7: dependencies: get-func-name: 2.0.2 @@ -5897,6 +6293,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -6055,6 +6455,8 @@ snapshots: prettier@3.3.3: {} + process@0.11.10: {} + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -6065,6 +6467,20 @@ snapshots: dependencies: safe-buffer: 5.2.1 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -6122,6 +6538,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + semver@6.3.1: {} semver@7.6.3: {} @@ -6140,6 +6558,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + slash@3.0.0: {} slash@4.0.0: {} @@ -6178,6 +6600,8 @@ snapshots: split2@4.2.0: {} + stack-trace@0.0.10: {} + stackback@0.0.2: {} std-env@3.7.0: {} @@ -6206,6 +6630,10 @@ snapshots: get-east-asian-width: 1.2.0 strip-ansi: 7.1.0 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -6246,6 +6674,8 @@ snapshots: text-extensions@2.4.0: {} + text-hex@1.0.0: {} + text-table@0.2.0: {} through@2.3.8: {} @@ -6264,6 +6694,8 @@ snapshots: dependencies: is-number: 7.0.0 + triple-beam@1.4.1: {} + ts-api-utils@1.3.0(typescript@5.5.4): dependencies: typescript: 5.5.4 @@ -6373,8 +6805,28 @@ snapshots: dependencies: punycode: 2.3.1 + util-deprecate@1.0.2: {} + v8-compile-cache-lib@3.0.1: {} + viem@2.19.6(typescript@5.5.4)(zod@3.23.8): + dependencies: + "@adraffy/ens-normalize": 1.10.0 + "@noble/curves": 1.4.0 + "@noble/hashes": 1.4.0 + "@scure/bip32": 1.4.0 + "@scure/bip39": 1.3.0 + abitype: 1.0.5(typescript@5.5.4)(zod@3.23.8) + isows: 1.0.4(ws@8.17.1) + webauthn-p256: 0.0.5 + ws: 8.17.1 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + viem@2.21.19(typescript@5.5.4)(zod@3.23.8): dependencies: "@adraffy/ens-normalize": 1.11.0 @@ -6458,6 +6910,11 @@ snapshots: "@noble/curves": 1.6.0 "@noble/hashes": 1.5.0 + webauthn-p256@0.0.5: + dependencies: + "@noble/curves": 1.6.0 + "@noble/hashes": 1.5.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -6467,6 +6924,26 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + winston-transport@4.8.0: + dependencies: + logform: 2.6.1 + readable-stream: 4.5.2 + triple-beam: 1.4.1 + + winston@3.15.0: + dependencies: + "@colors/colors": 1.6.0 + "@dabh/diagnostics": 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.6.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.8.0 + word-wrap@1.2.5: {} workerpool@6.2.1: {} @@ -6491,6 +6968,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.17.1: {} + ws@8.18.0: {} ws@8.5.0: {} From 44d8c71968934e36348b83968bcdd222b992f1ab Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Tue, 15 Oct 2024 17:31:09 -0300 Subject: [PATCH 2/4] fix: export logger --- packages/shared/src/external.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/external.ts b/packages/shared/src/external.ts index 8613945..29e4880 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -1,4 +1,4 @@ export type * from "./types/index.js"; export type { Address } from "./internal.js"; -export type { ILogger } from "./internal.js"; +export type { ILogger, Logger } from "./internal.js"; export { NATIVE_TOKEN_ADDRESS, isNativeToken } from "./constants/index.js"; From dd1a92212cd1b413632b02ba1b595b5bf31065e7 Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Wed, 16 Oct 2024 14:07:16 -0300 Subject: [PATCH 3/4] fix: pr comments --- packages/chain-providers/README.md | 2 +- .../src/providers/evmProvider.ts | 4 +- .../providers/evmProvider.service.spec.ts | 26 ++++---- packages/chain-providers/tsconfig.build.json | 2 +- .../data-flow/test/unit/eventsFetcher.spec.ts | 32 +++++----- packages/data-flow/tsconfig.build.json | 2 +- packages/data-flow/tsconfig.json | 2 +- packages/indexer-client/tsconfig.build.json | 2 +- packages/pricing/package.json | 2 +- .../test/providers/coingecko.provider.spec.ts | 61 ++++++++++++++----- packages/pricing/tsconfig.json | 2 +- packages/processors/tsconfig.json | 2 +- packages/repository/tsconfig.json | 2 +- packages/shared/tsconfig.build.json | 2 +- packages/shared/tsconfig.json | 2 +- pnpm-lock.yaml | 18 +++++- 16 files changed, 104 insertions(+), 59 deletions(-) diff --git a/packages/chain-providers/README.md b/packages/chain-providers/README.md index bf09f87..b330313 100644 --- a/packages/chain-providers/README.md +++ b/packages/chain-providers/README.md @@ -64,7 +64,7 @@ Available methods - `getMulticall3Address()` - `getBlockNumber()` -- `getBlockByNumber(blockNumber: number)` +- `getBlockByNumber(blockNumber: bigint)` - `readContract(contractAddress: Address, abi: TAbi functionName: TFunctionName, args?: TArgs)` - `batchRequest(abi: AbiWithConstructor,bytecode: Hex, args: ContractConstructorArgs, constructorReturnParams: ReturnType)` - `multicall(args: MulticallParameters)` diff --git a/packages/chain-providers/src/providers/evmProvider.ts b/packages/chain-providers/src/providers/evmProvider.ts index 5ceda9e..22c048a 100644 --- a/packages/chain-providers/src/providers/evmProvider.ts +++ b/packages/chain-providers/src/providers/evmProvider.ts @@ -86,8 +86,8 @@ export class EvmProvider { * Retrieves the current block number. * @returns {Promise} Latest block number. */ - async getBlockByNumber(blockNumber: number): Promise { - return this.client.getBlock({ blockNumber: BigInt(blockNumber) }); + async getBlockByNumber(blockNumber: bigint): Promise { + return this.client.getBlock({ blockNumber }); } /** diff --git a/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts index 8944d6c..da4681a 100644 --- a/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts +++ b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts @@ -71,7 +71,7 @@ describe("EvmProvider", () => { }); describe("getBalance", () => { - it("should return the balance of the specified address", async () => { + it("returns the balance of the specified address", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const address = "0x123456789"; const expectedBalance = 100n; @@ -85,7 +85,7 @@ describe("EvmProvider", () => { }); describe("getBlockNumber", () => { - it("should return the current block number", async () => { + it("returns the current block number", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const expectedBlockNumber = 1000n; vi.spyOn(mockClient, "getBlockNumber").mockResolvedValue(expectedBlockNumber); @@ -97,7 +97,7 @@ describe("EvmProvider", () => { }); describe("getGasPrice", () => { - it("should return the current gas price", async () => { + it("returns the current gas price", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const expectedGasPrice = BigInt(100); // Mock the getGasPrice method of the Viem client @@ -129,7 +129,7 @@ describe("EvmProvider", () => { }); describe("getStorageAt", () => { - it("should return the value of the storage slot at the given address and slot number", async () => { + it("returns the value of the storage slot at the given address and slot number", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const address = "0x123456789"; const slot = 1; @@ -142,7 +142,7 @@ describe("EvmProvider", () => { expect(mockClient.getStorageAt).toHaveBeenCalledWith({ address, slot: "0x1" }); }); - it("should return the value of the storage slot at the given address and slot value", async () => { + it("returns the value of the storage slot at the given address and slot value", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const address = "0x123456789"; const slot = "0x12"; @@ -155,7 +155,7 @@ describe("EvmProvider", () => { expect(mockClient.getStorageAt).toHaveBeenCalledWith({ address, slot: "0x12" }); }); - it("should throw an error if the slot is not a positive integer", async () => { + it("throws an error if the slot is not a positive integer", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const address = "0x123456789"; const slot = -1; @@ -167,7 +167,7 @@ describe("EvmProvider", () => { }); describe("readContract", () => { - it("should call the readContract method of the Viem client with the correct arguments", async () => { + it("calls the readContract method of the Viem client with the correct arguments", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const contractAddress = "0x123456789"; const abi = testAbi; @@ -187,7 +187,7 @@ describe("EvmProvider", () => { }); }); - it("should call the readContract method of the Viem client with the correct arguments when args are provided", async () => { + it("calls the readContract method of the Viem client with the correct arguments when args are provided", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const contractAddress = "0x123456789"; const functionName = "tokenURI"; @@ -215,7 +215,7 @@ describe("EvmProvider", () => { }); describe("batchRequest", () => { - it("should properly encode bytecode data and decode return data from batch request call", async () => { + it("encodes bytecode data and decode return data from batch request call", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const returnAbiParams = viem.parseAbiParameters([ "TokenData[] returnData", @@ -244,7 +244,7 @@ describe("EvmProvider", () => { ]); }); - it("should fail if no data is returned", async () => { + it("fails if no data is returned", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const returnAbiParams = viem.parseAbiParameters([ "TokenData[] returnData", @@ -263,7 +263,7 @@ describe("EvmProvider", () => { ).rejects.toThrowError(DataDecodeException); }); - it("should fail if decoded data does not match validator (missing struct fields)", async () => { + it("fails if decoded data does not match validator (missing struct fields)", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); // this schema is incorrect, it should have 3 fields instead of 2 const returnAbiParams = viem.parseAbiParameters([ @@ -285,7 +285,7 @@ describe("EvmProvider", () => { ); }); - it("should fail if decoded data does not match validator (not struct vs struct)", async () => { + it("fails if decoded data does not match validator (not struct vs struct)", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); // this schema is incorrect, it should have 3 fields instead of 2 const returnAbiParams = viem.parseAbiParameters("uint8 decimals, address[] owners"); @@ -304,7 +304,7 @@ describe("EvmProvider", () => { ); }); - it("should properly decode address[]", async () => { + it("decodes address[]", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); const returnAbiParams = viem.parseAbiParameters("address[]"); diff --git a/packages/chain-providers/tsconfig.build.json b/packages/chain-providers/tsconfig.build.json index 0be123c..32768e3 100644 --- a/packages/chain-providers/tsconfig.build.json +++ b/packages/chain-providers/tsconfig.build.json @@ -7,5 +7,5 @@ "outDir": "dist" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] + "exclude": ["node_modules", "dist", "test"] } diff --git a/packages/data-flow/test/unit/eventsFetcher.spec.ts b/packages/data-flow/test/unit/eventsFetcher.spec.ts index a547b5e..371f616 100644 --- a/packages/data-flow/test/unit/eventsFetcher.spec.ts +++ b/packages/data-flow/test/unit/eventsFetcher.spec.ts @@ -17,26 +17,26 @@ describe("EventsFetcher", () => { eventsFetcher = new EventsFetcher(indexerClientMock); }); - it("should fetch events by block number and log index", async () => { + it("fetches events by block number and log index", async () => { const mockEvents: AnyProtocolEvent[] = [ { - chain_id: 1, - block_number: 12345, - block_timestamp: 123123123, - contract_name: "Allo", - event_name: "PoolCreated", - src_address: "0x1234567890123456789012345678901234567890", - log_index: 0, + chainId: 1, + blockNumber: 12345, + blockTimestamp: 123123123, + contractName: "Allo", + eventName: "PoolCreated", + srcAddress: "0x1234567890123456789012345678901234567890", + logIndex: 0, params: { contractAddress: "0x1234" }, }, { - chain_id: 1, - block_number: 12345, - block_timestamp: 123123123, - contract_name: "Allo", - event_name: "PoolCreated", - src_address: "0x1234567890123456789012345678901234567890", - log_index: 0, + chainId: 1, + blockNumber: 12345, + blockTimestamp: 123123123, + contractName: "Allo", + eventName: "PoolCreated", + srcAddress: "0x1234567890123456789012345678901234567890", + logIndex: 0, params: { contractAddress: "0x1234" }, }, ]; @@ -62,7 +62,7 @@ describe("EventsFetcher", () => { expect(result).toEqual(mockEvents); }); - it("should handle errors thrown by indexer client", async () => { + it("handles errors thrown by indexer client", async () => { const chainId = 1n; const blockNumber = 1000n; const logIndex = 0; diff --git a/packages/data-flow/tsconfig.build.json b/packages/data-flow/tsconfig.build.json index 56d3112..a9bfa3d 100644 --- a/packages/data-flow/tsconfig.build.json +++ b/packages/data-flow/tsconfig.build.json @@ -9,5 +9,5 @@ "outDir": "dist" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] + "exclude": ["node_modules", "dist", "test"] } diff --git a/packages/data-flow/tsconfig.json b/packages/data-flow/tsconfig.json index 66bb87a..21c1c5b 100644 --- a/packages/data-flow/tsconfig.json +++ b/packages/data-flow/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } diff --git a/packages/indexer-client/tsconfig.build.json b/packages/indexer-client/tsconfig.build.json index 56d3112..a9bfa3d 100644 --- a/packages/indexer-client/tsconfig.build.json +++ b/packages/indexer-client/tsconfig.build.json @@ -9,5 +9,5 @@ "outDir": "dist" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] + "exclude": ["node_modules", "dist", "test"] } diff --git a/packages/pricing/package.json b/packages/pricing/package.json index a6bfd4f..69e12af 100644 --- a/packages/pricing/package.json +++ b/packages/pricing/package.json @@ -32,6 +32,6 @@ "axios": "1.7.7" }, "devDependencies": { - "axios-mock-adapter": "2.0.0" + "axios-mock-adapter": "2.1.0" } } diff --git a/packages/pricing/test/providers/coingecko.provider.spec.ts b/packages/pricing/test/providers/coingecko.provider.spec.ts index 7210197..126da8b 100644 --- a/packages/pricing/test/providers/coingecko.provider.spec.ts +++ b/packages/pricing/test/providers/coingecko.provider.spec.ts @@ -1,5 +1,5 @@ -import MockAdapter from "axios-mock-adapter"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import axios, { isAxiosError } from "axios"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Address, NATIVE_TOKEN_ADDRESS } from "@grants-stack-indexer/shared"; @@ -10,20 +10,40 @@ import { UnsupportedChainException, } from "../../src/external.js"; +const mock = vi.hoisted(() => ({ + get: vi.fn(), + post: vi.fn(), +})); + +vi.mock("axios", async (importActual) => { + const actual = await importActual(); + + const mockAxios = { + default: { + ...actual.default, + create: vi.fn(() => ({ + ...actual.default.create(), + get: mock.get, + post: mock.post, + })), + }, + isAxiosError: actual.isAxiosError, // Return it directly from the mock + }; + + return mockAxios; +}); describe("CoingeckoProvider", () => { let provider: CoingeckoProvider; - let mock: MockAdapter; beforeEach(() => { provider = new CoingeckoProvider({ apiKey: "test-api-key", apiType: "demo", }); - mock = new MockAdapter(provider["axios"]); }); afterEach(() => { - mock.reset(); + vi.restoreAllMocks(); }); describe("getTokenPrice", () => { @@ -31,7 +51,7 @@ describe("CoingeckoProvider", () => { const mockResponse = { prices: [[1609459200000, 100]], }; - mock.onGet().reply(200, mockResponse); + mock.get.mockResolvedValueOnce({ status: 200, data: mockResponse }); const result = await provider.getTokenPrice( 1, @@ -46,7 +66,7 @@ describe("CoingeckoProvider", () => { }; expect(result).toEqual(expectedPrice); - expect(mock.history.get[0].url).toContain( + expect(mock.get).toHaveBeenCalledWith( "/coins/ethereum/contract/0x1234567890123456789012345678901234567890/market_chart/range?vs_currency=usd&from=1609459200&to=1609545600&precision=full", ); }); @@ -55,7 +75,7 @@ describe("CoingeckoProvider", () => { const mockResponse = { prices: [[1609459200000, 100]], }; - mock.onGet().reply(200, mockResponse); + mock.get.mockResolvedValueOnce({ status: 200, data: mockResponse }); const result = await provider.getTokenPrice( 10, @@ -70,7 +90,7 @@ describe("CoingeckoProvider", () => { }; expect(result).toEqual(expectedPrice); - expect(mock.history.get[0].url).toContain( + expect(mock.get).toHaveBeenCalledWith( "/coins/ethereum/market_chart/range?vs_currency=usd&from=1609459200&to=1609545600&precision=full", ); }); @@ -79,7 +99,7 @@ describe("CoingeckoProvider", () => { const mockResponse = { prices: [], }; - mock.onGet().reply(200, mockResponse); + mock.get.mockResolvedValueOnce({ status: 200, data: mockResponse }); const result = await provider.getTokenPrice( 1, @@ -103,7 +123,11 @@ describe("CoingeckoProvider", () => { }); it("return undefined if 400 family error", async () => { - mock.onGet().replyOnce(400, "Bad Request"); + mock.get.mockRejectedValueOnce({ + status: 400, + data: "Bad Request", + isAxiosError: true, + }); const result = await provider.getTokenPrice( 1, @@ -126,9 +150,12 @@ describe("CoingeckoProvider", () => { ).rejects.toThrow(UnsupportedChainException); }); - it("should throw NetworkException for 500 family errors", async () => { - mock.onGet().reply(500, "Internal Server Error"); - + it("throws NetworkException for 500 family errors", async () => { + mock.get.mockRejectedValueOnce({ + status: 500, + data: "Internal Server Error", + isAxiosError: true, + }); await expect( provider.getTokenPrice( 1, @@ -140,7 +167,11 @@ describe("CoingeckoProvider", () => { }); it("throw NetworkException for network errors", async () => { - mock.onGet().networkErrorOnce(); + mock.get.mockRejectedValueOnce({ + status: 500, + data: "Network Error", + isAxiosError: true, + }); await expect( provider.getTokenPrice( diff --git a/packages/pricing/tsconfig.json b/packages/pricing/tsconfig.json index 66bb87a..21c1c5b 100644 --- a/packages/pricing/tsconfig.json +++ b/packages/pricing/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } diff --git a/packages/processors/tsconfig.json b/packages/processors/tsconfig.json index 66bb87a..21c1c5b 100644 --- a/packages/processors/tsconfig.json +++ b/packages/processors/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } diff --git a/packages/repository/tsconfig.json b/packages/repository/tsconfig.json index 66bb87a..21c1c5b 100644 --- a/packages/repository/tsconfig.json +++ b/packages/repository/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } diff --git a/packages/shared/tsconfig.build.json b/packages/shared/tsconfig.build.json index 56d3112..a9bfa3d 100644 --- a/packages/shared/tsconfig.build.json +++ b/packages/shared/tsconfig.build.json @@ -9,5 +9,5 @@ "outDir": "dist" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] + "exclude": ["node_modules", "dist", "test"] } diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 66bb87a..21c1c5b 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cd70c6..0051848 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,8 +155,8 @@ importers: version: 1.7.7 devDependencies: axios-mock-adapter: - specifier: 2.0.0 - version: 2.0.0(axios@1.7.7) + specifier: 2.1.0 + version: 2.1.0(axios@1.7.7) packages/processors: dependencies: @@ -1492,6 +1492,14 @@ packages: peerDependencies: axios: ">= 0.17.0" + axios-mock-adapter@2.1.0: + resolution: + { + integrity: sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==, + } + peerDependencies: + axios: ">= 0.17.0" + axios@1.7.7: resolution: { @@ -5328,6 +5336,12 @@ snapshots: fast-deep-equal: 3.1.3 is-buffer: 2.0.5 + axios-mock-adapter@2.1.0(axios@1.7.7): + dependencies: + axios: 1.7.7 + fast-deep-equal: 3.1.3 + is-buffer: 2.0.5 + axios@1.7.7: dependencies: follow-redirects: 1.15.9 From a38745749ff0db726dfbec275804553d7dc1e3cc Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Wed, 16 Oct 2024 14:12:08 -0300 Subject: [PATCH 4/4] fix: lint --- packages/pricing/test/providers/coingecko.provider.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pricing/test/providers/coingecko.provider.spec.ts b/packages/pricing/test/providers/coingecko.provider.spec.ts index 126da8b..ee7a5a3 100644 --- a/packages/pricing/test/providers/coingecko.provider.spec.ts +++ b/packages/pricing/test/providers/coingecko.provider.spec.ts @@ -1,4 +1,3 @@ -import axios, { isAxiosError } from "axios"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Address, NATIVE_TOKEN_ADDRESS } from "@grants-stack-indexer/shared";