diff --git a/packages/util/src/config.ts b/packages/util/src/config.ts index 318c21324..211adf6fe 100644 --- a/packages/util/src/config.ts +++ b/packages/util/src/config.ts @@ -194,6 +194,7 @@ export interface ServerConfig { port: number; mode: string; kind: string; + enableConfigValidation: boolean; checkpointing: boolean; checkpointInterval: number; subgraphPath: string; diff --git a/packages/util/src/constants.ts b/packages/util/src/constants.ts index 5cf1d0103..ba1b464a5 100644 --- a/packages/util/src/constants.ts +++ b/packages/util/src/constants.ts @@ -27,3 +27,5 @@ export const KIND_LAZY = 'lazy'; export const DEFAULT_PREFETCH_BATCH_SIZE = 10; export const DEFAULT_MAX_GQL_CACHE_SIZE = Math.pow(2, 20) * 8; // 8 MB + +export const SUPPORTED_PAID_RPC_METHODS = ['eth_getBlockByHash', 'eth_getStorageAt', 'eth_getBlockByNumber']; diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 5a4399c54..679c1537d 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -26,3 +26,4 @@ export * from './graph/types'; export * from './payments'; export * from './eth'; export * from './consensus'; +export * from './validate-config'; diff --git a/packages/util/src/validate-config.ts b/packages/util/src/validate-config.ts new file mode 100644 index 000000000..9603bd31d --- /dev/null +++ b/packages/util/src/validate-config.ts @@ -0,0 +1,128 @@ +import { ethers } from 'ethers'; +import { Client } from 'pg'; +import debug from 'debug'; +import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; +import WebSocket from 'ws'; +import path from 'path'; +import fs from 'fs-extra'; + +import { SUPPORTED_PAID_RPC_METHODS } from './constants'; + +const log = debug('vulcanize:server'); + +async function validateContractDeployment (rpcEndpoint: string, contractInfo: {address:string, name?:string}, isWs: boolean): Promise { + try { + let provider; + if (isWs) { + provider = new ethers.providers.WebSocketProvider(rpcEndpoint); + } else { + provider = new ethers.providers.JsonRpcProvider(rpcEndpoint); + } + const code = await provider.getCode(contractInfo.address); + if (code === '0x') { + log(`WARNING: Contract ${contractInfo.name ? contractInfo.name : ''} is not deployed at ${contractInfo.address}`); + } else { + log(`SUCCESS: Contract ${contractInfo.name ? contractInfo.name : ''} is deployed at ${contractInfo.address}`); + } + } catch (error) { + log(error); + } +} + +function validateContractAddressFormat (contractInfo: {address:string, name?:string}): boolean { + if (ethers.utils.isAddress(contractInfo.address)) { + log(`SUCCESS: Address ${contractInfo.address} ${contractInfo.name ? `for ${contractInfo.name}` : ''} is in a valid format`); + return true; + } else { + log(`WARNING: Address ${contractInfo.address} ${contractInfo.name ? `for ${contractInfo.name}` : ''} is not in a valid format`); + return false; + } +} + +export async function validateContracts (contractsArr: {address:string, name?:string}[], rpcProviderMutationEndpoint: string, isWs: boolean): Promise { + contractsArr.forEach((contract) => { + const isValidFormat = validateContractAddressFormat(contract); + + if (isValidFormat) { + validateContractDeployment(rpcProviderMutationEndpoint, contract, isWs); + } + }); +} + +export async function validateHttpEndpoint (endPoint: string, kind: string): Promise { + try { + const response = await fetch(endPoint); + log(`SUCCESS: The ${endPoint} is up. Status ${response.status}`); + } catch (error:any) { + log(`WARNING: could not connect to ${endPoint}. Please check if the ${kind} is correct and up.`); + log(error); + } +} + +async function checkDBEndpoint (connectionString: string, dbKind: string): Promise { + const client = new Client({ + connectionString + }); + + try { + await client.connect(); + log(`SUCCESS: ${dbKind} endpoint is up.`); + } catch (error) { + log(`WARNING: Error connecting to ${dbKind} database. Please check if job queue config is setup and database is running \n`, error); + } finally { + await client.end(); + } +} + +export async function validateDatabaseEndpoint (database: PostgresConnectionOptions): Promise { + const connectionString = `${database.type}://${database.username}:${database.password}@${database.host}:${database.port}/${database.database}`; + await checkDBEndpoint(connectionString, 'postgresQL'); +} + +export async function validateJobQueueEndpoint (connString: string): Promise { + await checkDBEndpoint(connString, 'Job queue database'); +} + +async function checkWebSocket (wsEndpoint: string) { + const socket = new WebSocket(wsEndpoint); + + return new Promise((resolve, reject) => { + socket.on('open', () => { + socket.close(); + resolve(true); + }); + + socket.on('error', (error) => { + reject(error); + }); + }); +} + +export async function validateWebSocketEndpoint (wsEndpoint: string): Promise { + try { + await checkWebSocket(wsEndpoint); + log(`SUCCESS: The WebSocket endpoint ${wsEndpoint} is up.`); + } catch (error) { + log(`WARNING: Error connecting to websocket endpoint ${wsEndpoint}. Please check if server.p2p.nitro.chainUrl is correct.`, error); + } +} + +export async function validatePaidRPCMethods (paidRPCMethods: string[]): Promise { + paidRPCMethods.forEach((method) => { + if (SUPPORTED_PAID_RPC_METHODS.includes(method)) { + log(`SUCCESS: ${method} is a supported paid RPC method`); + } else { + log(`WARNING: ${method} is not a supported paid RPC method`); + } + }); +} + +export async function validateFilePath (configFile: string): Promise { + const configFilePath = path.resolve(configFile); + const fileExists = await fs.pathExists(configFilePath); + if (!fileExists) { + log(`WARNING: Config file not found: ${configFilePath}`); + } else { + log(`SUCCESS: Config file found: ${configFilePath}`); + } +}