diff --git a/.env.example b/.env.example index eab91031..1d295fcb 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ -NEXT_PUBLIC_WALLET_CONNECT_ID=12345678901234567890123456789012 \ No newline at end of file +NEXT_PUBLIC_WALLET_CONNECT_ID=12345678901234567890123456789012 +NEXT_PUBLIC_RPC_OVERRIDES='{"chain1":{"http":"https://..."}}' \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index dcc1fd5e..3f64c5fc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,12 +15,12 @@ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", - "next", - "next/core-web-vitals", + "next", + "next/core-web-vitals", "prettier" ], "rules": { - "no-console": ["warn"], + "no-console": ["error"], "no-eval": ["error"], "no-ex-assign": ["error"], "no-constant-condition": ["off"], diff --git a/src/consts/config.ts b/src/consts/config.ts index 2f09dfc6..3b9096e7 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -7,6 +7,7 @@ const explorerApiKeys = JSON.parse(process?.env?.EXPLORER_API_KEYS || '{}'); const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || ''; const withdrawalWhitelist = process?.env?.NEXT_PUBLIC_BLOCK_WITHDRAWAL_WHITELIST || ''; const transferBlacklist = process?.env?.NEXT_PUBLIC_TRANSFER_BLACKLIST || ''; +const rpcOverrides = process?.env?.NEXT_PUBLIC_RPC_OVERRIDES || ''; interface Config { isDevMode: boolean; // Enables some debug features in the app @@ -20,6 +21,7 @@ interface Config { transferBlacklist: string; // comma-separated list of routes between which transfers are disabled. Expects Caip2Id-Caip2Id (e.g. ethereum:1-sealevel:1399811149) enableExplorerLink: boolean; // Include a link to the hyperlane explorer in the transfer modal addressBlacklist: string[]; // A list of addresses that are blacklisted and cannot be used in the app + rpcOverrides: string; } export const config: Config = Object.freeze({ @@ -34,4 +36,5 @@ export const config: Config = Object.freeze({ transferBlacklist, enableExplorerLink: false, addressBlacklist: ADDRESS_BLACKLIST.map((address) => address.toLowerCase()), + rpcOverrides, }); diff --git a/src/context/chains.ts b/src/context/chains.ts index 3c404a6b..66d2c882 100644 --- a/src/context/chains.ts +++ b/src/context/chains.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { GithubRegistry, chainMetadata } from '@hyperlane-xyz/registry'; -import { ChainMap, ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk'; +import { ChainMap, ChainMetadata, ChainMetadataSchema, RpcUrlSchema } from '@hyperlane-xyz/sdk'; import { chains as ChainsTS } from '../consts/chains.ts'; import ChainsYaml from '../consts/chains.yaml'; import { config } from '../consts/config.ts'; import { cosmosDefaultChain } from '../features/chains/cosmosDefault'; +import { tryParseJson } from '../utils/json.ts'; import { logger } from '../utils/logger'; export async function assembleChainMetadata() { @@ -20,6 +21,12 @@ export async function assembleChainMetadata() { logger.warn('Invalid chain config', result.error); throw new Error(`Invalid chain config: ${result.error.toString()}`); } + + const rpcOverrides = z.record(RpcUrlSchema).safeParse(tryParseJson(config.rpcOverrides)); + if (config.rpcOverrides && !rpcOverrides.success) { + logger.warn('Invalid RPC overrides config', rpcOverrides.error); + } + const customChainMetadata = result.data as ChainMap; const registry = new GithubRegistry({ uri: config.registryUrl }); @@ -34,6 +41,22 @@ export async function assembleChainMetadata() { await registry.listRegistryContent(); } - const chains = { ...defaultChainMetadata, ...customChainMetadata }; + const chains: ChainMap = Object.entries({ + ...defaultChainMetadata, + ...customChainMetadata, + }).reduce( + (accum, [name, chain]) => ({ + ...accum, + [name]: + rpcOverrides.success && rpcOverrides.data[name] + ? { + ...chain, + rpcUrls: [rpcOverrides.data[name]], + } + : chain, + }), + {} as ChainMap, + ); + return { chains, registry }; } diff --git a/src/utils/json.ts b/src/utils/json.ts new file mode 100644 index 00000000..7fbacb94 --- /dev/null +++ b/src/utils/json.ts @@ -0,0 +1,10 @@ +import { logger } from './logger'; + +export function tryParseJson(input: string): unknown | null { + try { + return JSON.parse(input); + } catch (e) { + logger.warn('unable to parse JSON', e); + return null; + } +}