From 35bc10220c8845c0f507f619415e1cbd80a48f95 Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Wed, 25 Sep 2024 11:55:46 +0300 Subject: [PATCH 1/4] refactor: use the Failover Provider --- .env.template | 15 +++++ src/common/FailoverProvider.ts | 102 +++++++++++++++++++++++++++++++++ src/common/appSettings.ts | 13 +++++ src/common/dripsContracts.ts | 41 ++++++------- src/environment.d.ts | 10 ++++ 5 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 src/common/FailoverProvider.ts diff --git a/.env.template b/.env.template index ee22dde..4f7a3f7 100644 --- a/.env.template +++ b/.env.template @@ -3,16 +3,31 @@ PORT=string # Optional. The API server port. Defaults to 8080. NETWORK=string # Required. The network to connect to. See `SupportedChains` in `types.ts` for supported networks. # You should provide at least one of the following RPC URLs: + RPC_URL_MAINNET=string # The RPC URL for the Mainnet provider. RPC_ACCESS_TOKEN_MAINNET=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. +FALLBACK_RPC_URL_MAINNET=string # Optional. The fallback RPC URL for the Mainnet provider. If specified, it will be used if the main RPC URL fails. +FALLBACK_RPC_ACCESS_TOKEN_MAINNET=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. + RPC_URL_SEPOLIA=string # The RPC URL for the Sepolia provider. RPC_ACCESS_TOKEN_SEPOLIA=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. +FALLBACK_RPC_URL_SEPOLIA=string # Optional. The fallback RPC URL for the Sepolia provider. If specified, it will be used if the main RPC URL fails. +FALLBACK_RPC_ACCESS_TOKEN_SEPOLIA=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. + RPC_URL_OPTIMISM_SEPOLIA=string # The RPC URL for the Optimism Sepolia provider. RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. +FALLBACK_RPC_URL_OPTIMISM_SEPOLIA=string # Optional. The fallback RPC URL for the Optimism Sepolia provider. If specified, it will be used if the main RPC URL fails. +FALLBACK_RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. + RPC_URL_POLYGON_AMOY=string # The RPC URL for the Polygon Amoy provider. RPC_ACCESS_TOKEN_POLYGON_AMOY=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. +FALLBACK_RPC_URL_POLYGON_AMOY=string # Optional. The fallback RPC URL for the Polygon Amoy provider. If specified, it will be used if the main RPC URL fails. +FALLBACK_RPC_ACCESS_TOKEN_POLYGON_AMOY=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. + RPC_URL_FILECOIN=string # The RPC URL for the Filecoin provider. RPC_ACCESS_TOKEN_FILECOIN=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. +FALLBACK_RPC_URL_FILECOIN=string # Optional. The fallback RPC URL for the Filecoin provider. If specified, it will be used if the main RPC URL fails. +FALLBACK_RPC_ACCESS_TOKEN_FILECOIN=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. PUBLIC_API_KEYS=string # Required. Comma-separated list of strings API keys that rate limit is applied to. DRIPS_API_KEY=string # Required. API key withouth rate limit. diff --git a/src/common/FailoverProvider.ts b/src/common/FailoverProvider.ts new file mode 100644 index 0000000..cab37b0 --- /dev/null +++ b/src/common/FailoverProvider.ts @@ -0,0 +1,102 @@ +import type { + JsonRpcApiProviderOptions, + JsonRpcPayload, + JsonRpcResult, + Networkish, +} from 'ethers'; +import { JsonRpcProvider, FetchRequest } from 'ethers'; + +/** + * A `JsonRpcProvider` that transparently fails over to a list of backup JSON-RPC endpoints. + */ +export default class FailoverJsonRpcProvider extends JsonRpcProvider { + private readonly _rpcEndpoints: (string | FetchRequest)[]; + + /** + * @param rpcEndpoints An array of JSON-RPC endpoints. The order determines the failover order. + */ + constructor( + rpcEndpoints: (string | FetchRequest)[], + network?: Networkish, + options?: JsonRpcApiProviderOptions, + ) { + super(rpcEndpoints[0], network, options); + + this._rpcEndpoints = rpcEndpoints; + } + + /** + * Overrides the `_send` method to try each endpoint until the request succeeds. + * + * @param payload - The JSON-RPC payload or array of payloads to send. + * @returns A promise that resolves to the result of the first successful JSON-RPC call. + */ + public override async _send( + payload: JsonRpcPayload | Array, + ): Promise> { + // The actual sending of the request is the same as in the base class. + // The only difference is that we're creating a new `FetchRequest` for each endpoint, + // instead of getting the `request` from `_getConnection()`, which will return the *primary* (first) endpoint. + + const errors: { rpcEndpoint: string; error: any }[] = []; + + // Try each endpoint, in order. + for (const rpcEndpoint of this._rpcEndpoints) { + try { + let request: FetchRequest; + + if (typeof rpcEndpoint === 'string') { + request = new FetchRequest(rpcEndpoint); + } else { + request = rpcEndpoint.clone(); + } + + request.body = JSON.stringify(payload); + request.setHeader('content-type', 'application/json'); + const response = await request.send(); + response.assertOk(); + + let resp = response.bodyJson; + if (!Array.isArray(resp)) { + resp = [resp]; + } + + return resp; + } catch (error: any) { + const endpointUrl = this._getRpcEndpointUrl(rpcEndpoint); + errors.push({ rpcEndpoint: endpointUrl, error }); + } + } + + // All endpoints failed. Throw an error containing the details. + const errorMessages = errors + .map( + (e) => + `Endpoint '${e.rpcEndpoint}' failed with error: ${e.error.message}.`, + ) + .join('\n'); + + const aggregatedError = new Error( + `All RPC endpoints failed:\n${errorMessages}`, + ) as Error & { errors: { rpcEndpoint: string; error: any }[] }; + + aggregatedError.errors = errors; + + throw aggregatedError; + } + + /** + * Returns a copy of the endpoint URLs used by the provider. + */ + public getRpcEndpointUrls(): string[] { + return this._rpcEndpoints.map(this._getRpcEndpointUrl); + } + + private _getRpcEndpointUrl(rpcEndpoint: string | FetchRequest): string { + if (typeof rpcEndpoint === 'string') { + return rpcEndpoint; + } + + return rpcEndpoint.url; + } +} diff --git a/src/common/appSettings.ts b/src/common/appSettings.ts index 935946c..7e01d5e 100644 --- a/src/common/appSettings.ts +++ b/src/common/appSettings.ts @@ -6,6 +6,8 @@ dotenv.config(); type RpcConfig = { url: string; accessToken?: string; + fallbackUrl?: string; + fallbackAccessToken?: string; }; export default { @@ -14,22 +16,33 @@ export default { MAINNET: { url: process.env.RPC_URL_MAINNET, accessToken: process.env.RPC_ACCESS_TOKEN_MAINNET, + fallbackUrl: process.env.FALLBACK_RPC_URL_MAINNET, + fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_MAINNET, }, SEPOLIA: { url: process.env.RPC_URL_SEPOLIA, accessToken: process.env.RPC_ACCESS_TOKEN_SEPOLIA, + fallbackUrl: process.env.FALLBACK_RPC_URL_SEPOLIA, + fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_SEPOLIA, }, OPTIMISM_SEPOLIA: { url: process.env.RPC_URL_OPTIMISM_SEPOLIA, accessToken: process.env.RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA, + fallbackUrl: process.env.FALLBACK_RPC_URL_OPTIMISM_SEPOLIA, + fallbackAccessToken: + process.env.FALLBACK_RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA, }, POLYGON_AMOY: { url: process.env.RPC_URL_POLYGON_AMOY, + fallbackUrl: process.env.FALLBACK_RPC_URL_POLYGON_AMOY, accessToken: process.env.RPC_ACCESS_TOKEN_POLYGON_AMOY, + fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_POLYGON_AMOY, }, FILECOIN: { url: process.env.RPC_URL_FILECOIN, + fallbackUrl: process.env.FALLBACK_RPC_URL_FILECOIN, accessToken: process.env.RPC_ACCESS_TOKEN_FILECOIN, + fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_FILECOIN, }, } as Record, publicApiKeys: process.env.PUBLIC_API_KEYS?.split(',') || [], diff --git a/src/common/dripsContracts.ts b/src/common/dripsContracts.ts index c08af12..e95f7f3 100644 --- a/src/common/dripsContracts.ts +++ b/src/common/dripsContracts.ts @@ -1,9 +1,4 @@ -import { - FetchRequest, - JsonRpcProvider, - WebSocketProvider, - ethers, -} from 'ethers'; +import { FetchRequest, ethers } from 'ethers'; import appSettings from './appSettings'; import type { AddressDriver, Drips, RepoDriver } from '../generated/contracts'; import { @@ -21,6 +16,7 @@ import type { ProjectId, } from './types'; import queryableChains from './queryableChains'; +import FailoverJsonRpcProvider from './FailoverProvider'; const chainConfigs: Record< SupportedChain, @@ -65,7 +61,7 @@ const chainConfigs: Record< const { rpcConfigs } = appSettings; const providers: { - [network in SupportedChain]?: JsonRpcProvider | WebSocketProvider; + [network in SupportedChain]?: FailoverJsonRpcProvider; } = {}; function createAuthFetchRequest(rpcUrl: string, token: string): FetchRequest { @@ -79,25 +75,26 @@ function createAuthFetchRequest(rpcUrl: string, token: string): FetchRequest { Object.values(SupportedChain).forEach((network) => { const rpcConfig = rpcConfigs[network]; - if (rpcConfig?.url) { - const rpcUrl = rpcConfig.url; + if (!rpcConfig) { + return; + } + + const { url, accessToken, fallbackUrl, fallbackAccessToken } = rpcConfig; - let provider: JsonRpcProvider | WebSocketProvider | null = null; + const primaryEndpoint = accessToken + ? createAuthFetchRequest(url, accessToken) + : url; - if (rpcUrl.startsWith('http')) { - provider = rpcConfig?.accessToken - ? new JsonRpcProvider( - createAuthFetchRequest(rpcUrl, rpcConfig.accessToken), - ) - : new JsonRpcProvider(rpcUrl); - } else if (rpcUrl.startsWith('wss')) { - provider = new WebSocketProvider(rpcUrl); - } else { - shouldNeverHappen(`Invalid RPC URL: ${rpcUrl}`); - } + const rpcEndpoints = [primaryEndpoint]; - providers[network] = provider; + if (fallbackUrl) { + const fallbackEndpoint = fallbackAccessToken + ? createAuthFetchRequest(fallbackUrl, fallbackAccessToken) + : fallbackUrl; + rpcEndpoints.push(fallbackEndpoint); } + + providers[network] = new FailoverJsonRpcProvider(rpcEndpoints); }); const dripsContracts: { diff --git a/src/environment.d.ts b/src/environment.d.ts index b98572b..67ea91e 100644 --- a/src/environment.d.ts +++ b/src/environment.d.ts @@ -7,18 +7,28 @@ declare global { RPC_URL_MAINNET: string | undefined; RPC_ACCESS_TOKEN_MAINNET: string | undefined; + FALLBACK_RPC_URL_MAINNET: string | undefined; + FALLBACK_RPC_ACCESS_TOKEN_MAINNET: string | undefined; RPC_URL_SEPOLIA: string | undefined; + FALLBACK_RPC_URL_SEPOLIA: string | undefined; RPC_ACCESS_TOKEN_SEPOLIA: string | undefined; + FALLBACK_RPC_ACCESS_TOKEN_SEPOLIA: string | undefined; RPC_URL_OPTIMISM_SEPOLIA: string | undefined; + FALLBACK_RPC_URL_OPTIMISM_SEPOLIA: string | undefined; RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA: string | undefined; + FALLBACK_RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA: string | undefined; RPC_URL_POLYGON_AMOY: string | undefined; + FALLBACK_RPC_URL_POLYGON_AMOY: string | undefined; RPC_ACCESS_TOKEN_POLYGON_AMOY: string | undefined; + FALLBACK_RPC_ACCESS_TOKEN_POLYGON_AMOY: string | undefined; RPC_URL_FILECOIN: string | undefined; + FALLBACK_RPC_URL_FILECOIN: string; RPC_ACCESS_TOKEN_FILECOIN: string | undefined; + FALLBACK_RPC_ACCESS_TOKEN_FILECOIN: string | undefined; PUBLIC_API_KEYS: string; DRIPS_API_KEY: string; From 67c931d8842bcd09ecf2f07772d8ed55eb7cc104 Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Mon, 14 Oct 2024 12:06:27 +0200 Subject: [PATCH 2/4] refactor: use one rpc config env var --- .env.template | 47 +++++++++++--------------- package-lock.json | 2 +- package.json | 2 +- src/common/FailoverProvider.ts | 29 ++++++++-------- src/common/appSettings.ts | 62 ++++++++++++---------------------- src/common/dripsContracts.ts | 8 ++--- src/common/queryableChains.ts | 2 +- src/environment.d.ts | 27 +-------------- 8 files changed, 63 insertions(+), 116 deletions(-) diff --git a/.env.template b/.env.template index 4f7a3f7..be59797 100644 --- a/.env.template +++ b/.env.template @@ -1,33 +1,23 @@ PORT=string # Optional. The API server port. Defaults to 8080. -NETWORK=string # Required. The network to connect to. See `SupportedChains` in `types.ts` for supported networks. - -# You should provide at least one of the following RPC URLs: - -RPC_URL_MAINNET=string # The RPC URL for the Mainnet provider. -RPC_ACCESS_TOKEN_MAINNET=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. -FALLBACK_RPC_URL_MAINNET=string # Optional. The fallback RPC URL for the Mainnet provider. If specified, it will be used if the main RPC URL fails. -FALLBACK_RPC_ACCESS_TOKEN_MAINNET=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. - -RPC_URL_SEPOLIA=string # The RPC URL for the Sepolia provider. -RPC_ACCESS_TOKEN_SEPOLIA=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. -FALLBACK_RPC_URL_SEPOLIA=string # Optional. The fallback RPC URL for the Sepolia provider. If specified, it will be used if the main RPC URL fails. -FALLBACK_RPC_ACCESS_TOKEN_SEPOLIA=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. - -RPC_URL_OPTIMISM_SEPOLIA=string # The RPC URL for the Optimism Sepolia provider. -RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. -FALLBACK_RPC_URL_OPTIMISM_SEPOLIA=string # Optional. The fallback RPC URL for the Optimism Sepolia provider. If specified, it will be used if the main RPC URL fails. -FALLBACK_RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. - -RPC_URL_POLYGON_AMOY=string # The RPC URL for the Polygon Amoy provider. -RPC_ACCESS_TOKEN_POLYGON_AMOY=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. -FALLBACK_RPC_URL_POLYGON_AMOY=string # Optional. The fallback RPC URL for the Polygon Amoy provider. If specified, it will be used if the main RPC URL fails. -FALLBACK_RPC_ACCESS_TOKEN_POLYGON_AMOY=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. - -RPC_URL_FILECOIN=string # The RPC URL for the Filecoin provider. -RPC_ACCESS_TOKEN_FILECOIN=string # Optional. The access token to use for the RPC. If specified, it will be used as a bearer token in the `Authorization` header. -FALLBACK_RPC_URL_FILECOIN=string # Optional. The fallback RPC URL for the Filecoin provider. If specified, it will be used if the main RPC URL fails. -FALLBACK_RPC_ACCESS_TOKEN_FILECOIN=string # Optional. The access token to use for the fallback RPC. If specified, it will be used as a bearer token in the `Authorization` header. +NETWORK=string # Required. The network to connect to. See `SupportedChain` in `graphql.ts` for supported networks. + +# Required. The RPC configuration. See `SupportedChain` in `graphql.ts` for supported networks. +RPC_CONFIG='{ + "MAINNET": { + "url": "string", # The RPC URL. + "accessToken": "string", # Optional. The access token for the RPC URL. + "fallbackUrl": "string", # Optional. The fallback RPC URL. + "fallbackAccessToken": "string" # Optional. The access token for the fallback RPC URL. + }, + "SEPOLIA": { + "url": "string", # The RPC URL. + "accessToken": "string", # Optional. The access token for the RPC URL. + "fallbackUrl": "string", # Optional. The fallback RPC URL. + "fallbackAccessToken": "string" # Optional. The access token for the fallback RPC URL. + }, + ... # Add more networks as needed. +}' PUBLIC_API_KEYS=string # Required. Comma-separated list of strings API keys that rate limit is applied to. DRIPS_API_KEY=string # Required. API key withouth rate limit. @@ -44,3 +34,4 @@ MAX_QUERY_DEPTH=number # Optional. defaults to 4. TIMEOUT_IN_SECONDS=number # Optional. defaults to 20. IPFS_GATEWAY_URL=string # Optional. The IPFS gateway URL to use for fetching IPFS data. Defaults to 'https://drips.mypinata.cloud'. + diff --git a/package-lock.json b/package-lock.json index 49b376b..c22cf28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "graphql-tag": "^2.12.6", "pg": "^8.11.3", "sequelize": "^6.32.1", - "zod": "^3.23.4" + "zod": "^3.23.8" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.0", diff --git a/package.json b/package.json index c2dc15b..b9034b7 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,6 @@ "graphql-tag": "^2.12.6", "pg": "^8.11.3", "sequelize": "^6.32.1", - "zod": "^3.23.4" + "zod": "^3.23.8" } } diff --git a/src/common/FailoverProvider.ts b/src/common/FailoverProvider.ts index cab37b0..7c2c1ab 100644 --- a/src/common/FailoverProvider.ts +++ b/src/common/FailoverProvider.ts @@ -6,6 +6,19 @@ import type { } from 'ethers'; import { JsonRpcProvider, FetchRequest } from 'ethers'; +class AggregatedRpcError extends Error { + public readonly errors: { rpcEndpoint: string; error: any }[]; + + constructor(errors: { rpcEndpoint: string; error: any }[]) { + super( + `All RPC endpoints failed:\n${errors.map((e) => `Endpoint '${e.rpcEndpoint}' failed with error: ${e.error.message}.`).join('\n')}`, + ); + + this.name = 'AggregatedRpcError'; + this.errors = errors; + } +} + /** * A `JsonRpcProvider` that transparently fails over to a list of backup JSON-RPC endpoints. */ @@ -68,21 +81,7 @@ export default class FailoverJsonRpcProvider extends JsonRpcProvider { } } - // All endpoints failed. Throw an error containing the details. - const errorMessages = errors - .map( - (e) => - `Endpoint '${e.rpcEndpoint}' failed with error: ${e.error.message}.`, - ) - .join('\n'); - - const aggregatedError = new Error( - `All RPC endpoints failed:\n${errorMessages}`, - ) as Error & { errors: { rpcEndpoint: string; error: any }[] }; - - aggregatedError.errors = errors; - - throw aggregatedError; + throw new AggregatedRpcError(errors); } /** diff --git a/src/common/appSettings.ts b/src/common/appSettings.ts index 7e01d5e..9cdd6ad 100644 --- a/src/common/appSettings.ts +++ b/src/common/appSettings.ts @@ -1,50 +1,32 @@ import dotenv from 'dotenv'; -import type { SupportedChain } from '../generated/graphql'; +import { z } from 'zod'; +import { SupportedChain } from '../generated/graphql'; dotenv.config(); -type RpcConfig = { - url: string; - accessToken?: string; - fallbackUrl?: string; - fallbackAccessToken?: string; -}; +function missingEnvVar(name: string): never { + throw new Error(`Missing ${name} in .env file.`); +} + +const RpcConfigSchema = z.record( + z.nativeEnum(SupportedChain), + z + .object({ + url: z.string().url(), // The RPC URL for the provider. + accessToken: z.string().optional(), // Optional. The access token for the RPC URL. + fallbackUrl: z.string().optional(), // Optional. The fallback RPC URL. + fallbackAccessToken: z.string().optional(), // Optional. The access token for the fallback RPC URL. + }) + .optional(), +); + +console.log('process.env.RPC_CONFIG', process.env.RPC_CONFIG); export default { port: (process.env.PORT || 8080) as number, - rpcConfigs: { - MAINNET: { - url: process.env.RPC_URL_MAINNET, - accessToken: process.env.RPC_ACCESS_TOKEN_MAINNET, - fallbackUrl: process.env.FALLBACK_RPC_URL_MAINNET, - fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_MAINNET, - }, - SEPOLIA: { - url: process.env.RPC_URL_SEPOLIA, - accessToken: process.env.RPC_ACCESS_TOKEN_SEPOLIA, - fallbackUrl: process.env.FALLBACK_RPC_URL_SEPOLIA, - fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_SEPOLIA, - }, - OPTIMISM_SEPOLIA: { - url: process.env.RPC_URL_OPTIMISM_SEPOLIA, - accessToken: process.env.RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA, - fallbackUrl: process.env.FALLBACK_RPC_URL_OPTIMISM_SEPOLIA, - fallbackAccessToken: - process.env.FALLBACK_RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA, - }, - POLYGON_AMOY: { - url: process.env.RPC_URL_POLYGON_AMOY, - fallbackUrl: process.env.FALLBACK_RPC_URL_POLYGON_AMOY, - accessToken: process.env.RPC_ACCESS_TOKEN_POLYGON_AMOY, - fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_POLYGON_AMOY, - }, - FILECOIN: { - url: process.env.RPC_URL_FILECOIN, - fallbackUrl: process.env.FALLBACK_RPC_URL_FILECOIN, - accessToken: process.env.RPC_ACCESS_TOKEN_FILECOIN, - fallbackAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN_FILECOIN, - }, - } as Record, + rpcConfig: process.env.RPC_CONFIG + ? RpcConfigSchema.parse(JSON.parse(process.env.RPC_CONFIG)) + : missingEnvVar('RPC_CONFIG'), publicApiKeys: process.env.PUBLIC_API_KEYS?.split(',') || [], dripsApiKey: process.env.DRIPS_API_KEY, postgresConnectionString: process.env.POSTGRES_CONNECTION_STRING, diff --git a/src/common/dripsContracts.ts b/src/common/dripsContracts.ts index 8799419..9280710 100644 --- a/src/common/dripsContracts.ts +++ b/src/common/dripsContracts.ts @@ -58,7 +58,7 @@ const chainConfigs: Record< }, }; -const { rpcConfigs } = appSettings; +const { rpcConfig } = appSettings; const providers: { [network in SupportedChain]?: FailoverJsonRpcProvider; @@ -73,13 +73,13 @@ function createAuthFetchRequest(rpcUrl: string, token: string): FetchRequest { } Object.values(SupportedChain).forEach((network) => { - const rpcConfig = rpcConfigs[network]; + const config = rpcConfig[network]; - if (!rpcConfig) { + if (!config) { return; } - const { url, accessToken, fallbackUrl, fallbackAccessToken } = rpcConfig; + const { url, accessToken, fallbackUrl, fallbackAccessToken } = config; const primaryEndpoint = accessToken ? createAuthFetchRequest(url, accessToken) diff --git a/src/common/queryableChains.ts b/src/common/queryableChains.ts index ea7efe2..2b6e701 100644 --- a/src/common/queryableChains.ts +++ b/src/common/queryableChains.ts @@ -7,7 +7,7 @@ import appSettings from './appSettings'; const queryableChains: SupportedChain[] = []; Object.keys(SupportedChain).forEach((chain) => { - if (appSettings.rpcConfigs[chain as SupportedChain]?.url) { + if (appSettings.rpcConfig[chain as SupportedChain]?.url) { queryableChains.push(chain as SupportedChain); } }); diff --git a/src/environment.d.ts b/src/environment.d.ts index 67ea91e..2b7373f 100644 --- a/src/environment.d.ts +++ b/src/environment.d.ts @@ -4,32 +4,7 @@ declare global { namespace NodeJS { interface ProcessEnv { PORT: string; - - RPC_URL_MAINNET: string | undefined; - RPC_ACCESS_TOKEN_MAINNET: string | undefined; - FALLBACK_RPC_URL_MAINNET: string | undefined; - FALLBACK_RPC_ACCESS_TOKEN_MAINNET: string | undefined; - - RPC_URL_SEPOLIA: string | undefined; - FALLBACK_RPC_URL_SEPOLIA: string | undefined; - RPC_ACCESS_TOKEN_SEPOLIA: string | undefined; - FALLBACK_RPC_ACCESS_TOKEN_SEPOLIA: string | undefined; - - RPC_URL_OPTIMISM_SEPOLIA: string | undefined; - FALLBACK_RPC_URL_OPTIMISM_SEPOLIA: string | undefined; - RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA: string | undefined; - FALLBACK_RPC_ACCESS_TOKEN_OPTIMISM_SEPOLIA: string | undefined; - - RPC_URL_POLYGON_AMOY: string | undefined; - FALLBACK_RPC_URL_POLYGON_AMOY: string | undefined; - RPC_ACCESS_TOKEN_POLYGON_AMOY: string | undefined; - FALLBACK_RPC_ACCESS_TOKEN_POLYGON_AMOY: string | undefined; - - RPC_URL_FILECOIN: string | undefined; - FALLBACK_RPC_URL_FILECOIN: string; - RPC_ACCESS_TOKEN_FILECOIN: string | undefined; - FALLBACK_RPC_ACCESS_TOKEN_FILECOIN: string | undefined; - + RPC_CONFIG: string; PUBLIC_API_KEYS: string; DRIPS_API_KEY: string; NODE_ENV: 'development' | 'production'; From 834edc0488de130144c6fb08468cc187d4ae77c1 Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Mon, 14 Oct 2024 12:11:10 +0200 Subject: [PATCH 3/4] refactor: remove leftovers --- src/common/appSettings.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/common/appSettings.ts b/src/common/appSettings.ts index 9cdd6ad..13cf778 100644 --- a/src/common/appSettings.ts +++ b/src/common/appSettings.ts @@ -12,16 +12,14 @@ const RpcConfigSchema = z.record( z.nativeEnum(SupportedChain), z .object({ - url: z.string().url(), // The RPC URL for the provider. - accessToken: z.string().optional(), // Optional. The access token for the RPC URL. - fallbackUrl: z.string().optional(), // Optional. The fallback RPC URL. - fallbackAccessToken: z.string().optional(), // Optional. The access token for the fallback RPC URL. + url: z.string().url(), + accessToken: z.string().optional(), + fallbackUrl: z.string().optional(), + fallbackAccessToken: z.string().optional(), }) .optional(), ); -console.log('process.env.RPC_CONFIG', process.env.RPC_CONFIG); - export default { port: (process.env.PORT || 8080) as number, rpcConfig: process.env.RPC_CONFIG From a871221ca52777e9b8bbb6ee664fdffc5c6b2b2c Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Tue, 15 Oct 2024 11:16:45 +0200 Subject: [PATCH 4/4] build: add script for checking env vars on build --- .env.template | 2 -- package.json | 2 +- scripts/check-env-vars.sh | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100755 scripts/check-env-vars.sh diff --git a/.env.template b/.env.template index be59797..bb888de 100644 --- a/.env.template +++ b/.env.template @@ -1,7 +1,5 @@ PORT=string # Optional. The API server port. Defaults to 8080. -NETWORK=string # Required. The network to connect to. See `SupportedChain` in `graphql.ts` for supported networks. - # Required. The RPC configuration. See `SupportedChain` in `graphql.ts` for supported networks. RPC_CONFIG='{ "MAINNET": { diff --git a/package.json b/package.json index b9034b7..b1a0594 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test:e2e": "docker compose up --attach app --build --exit-code-from app && docker compose down", "build:graphql": "graphql-codegen", "build:contracts": "typechain --target=ethers-v6 --out-dir ./src/generated/contracts ./src/abi/**.json", - "build": "npm run build:contracts && npm run build:graphql && tsc", + "build": "./scripts/check-env-vars.sh && npm run build:contracts && npm run build:graphql && tsc", "dev": "npx nodemon", "start": "node dist/index.js", "check": "tsc --noEmit" diff --git a/scripts/check-env-vars.sh b/scripts/check-env-vars.sh new file mode 100755 index 0000000..f63a6ce --- /dev/null +++ b/scripts/check-env-vars.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +echo 🤓 Checking env vars... +echo + +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +required_vars=("RPC_CONFIG" "POSTGRES_CONNECTION_STRING" "DRIPS_API_KEY" "PUBLIC_API_KEYS") + +for var in "${required_vars[@]}"; do + if [ -z "${!var}" ]; then + echo "❌ Error: $var is not set." + exit 1 + fi +done + +echo "✅ All required environment variables are set."