diff --git a/configs/app/chain.ts b/configs/app/chain.ts index 56971abda6..e50647530f 100644 --- a/configs/app/chain.ts +++ b/configs/app/chain.ts @@ -1,7 +1,9 @@ import type { RollupType } from 'types/client/rollup'; import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks'; -import { getEnvValue } from './utils'; +import { urlValidator } from 'ui/shared/forms/validators/url'; + +import { getEnvValue, parseEnvJson } from './utils'; const DEFAULT_CURRENCY_DECIMALS = 18; @@ -17,6 +19,19 @@ const verificationType: NetworkVerificationType = (() => { return getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') as NetworkVerificationTypeEnvs || 'mining'; })(); +const rpcUrls = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'); + const isUrl = urlValidator(envValue); + + if (envValue && isUrl === true) { + return [ envValue ]; + } + + const parsedValue = parseEnvJson>(envValue); + + return Array.isArray(parsedValue) ? parsedValue : []; +})(); + const chain = Object.freeze({ id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'), name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'), @@ -32,7 +47,7 @@ const chain = Object.freeze({ }, hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true', tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC', - rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'), + rpcUrls, isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', verificationType, }); diff --git a/configs/app/features/blockchainInteraction.ts b/configs/app/features/blockchainInteraction.ts index 788e059ec9..6700126089 100644 --- a/configs/app/features/blockchainInteraction.ts +++ b/configs/app/features/blockchainInteraction.ts @@ -17,7 +17,7 @@ const config: Feature<{ walletConnect: { projectId: string } }> = (() => { chain.currency.name && chain.currency.symbol && chain.currency.decimals && - chain.rpcUrl && + chain.rpcUrls.length > 0 && walletConnectProjectId ) { return Object.freeze({ diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts index ab5ab4a965..f0af944b10 100644 --- a/configs/app/features/marketplace.ts +++ b/configs/app/features/marketplace.ts @@ -33,7 +33,7 @@ const config: Feature<( rating: { airtableApiKey: string; airtableBaseId: string } | undefined; graphLinksUrl: string | undefined; }> = (() => { - if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { + if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) { const props = { submitFormUrl, categoriesUrl, diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 6ed8487d94..bed33149c7 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -587,7 +587,21 @@ const schema = yup NEXT_PUBLIC_NETWORK_NAME: yup.string().required(), NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(), NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(), - NEXT_PUBLIC_NETWORK_RPC_URL: yup.string().test(urlTest), + NEXT_PUBLIC_NETWORK_RPC_URL: yup + .mixed() + .test( + 'shape', + 'Invalid schema were provided for NEXT_PUBLIC_NETWORK_RPC_URL, it should be either array of URLs or URL string', + (data) => { + const isUrlSchema = yup.string().test(urlTest); + const isArrayOfUrlsSchema = yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().test(urlTest)); + + return isUrlSchema.isValidSync(data) || isArrayOfUrlsSchema.isValidSync(data); + }), NEXT_PUBLIC_NETWORK_CURRENCY_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), diff --git a/deploy/tools/envs-validator/test/.env.alt b/deploy/tools/envs-validator/test/.env.alt index 172a809a56..11e6bf78fb 100644 --- a/deploy/tools/envs-validator/test/.env.alt +++ b/deploy/tools/envs-validator/test/.env.alt @@ -5,4 +5,5 @@ NEXT_PUBLIC_HOMEPAGE_STATS=[] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx -NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated \ No newline at end of file +NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated +NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com'] diff --git a/docs/ENVS.md b/docs/ENVS.md index c96fd7a319..e0c1147eb1 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -95,7 +95,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` | Used for SEO attributes (e.g, page description) | - | - | `OoG` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` | v1.0.x+ | -| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference | - | - | `https://core.poa.network` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_RPC_URL | `string \| Array` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference. Can contain a single string value, or an array of urls. | - | - | `https://core.poa.network` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | v1.23.0+ | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ | diff --git a/lib/web3/currentChain.ts b/lib/web3/currentChain.ts index dd3892859f..bdb35cfd8d 100644 --- a/lib/web3/currentChain.ts +++ b/lib/web3/currentChain.ts @@ -12,7 +12,7 @@ const currentChain = { }, rpcUrls: { 'default': { - http: [ config.chain.rpcUrl ?? '' ], + http: config.chain.rpcUrls, }, }, blockExplorers: { diff --git a/lib/web3/useAddOrSwitchChain.tsx b/lib/web3/useAddOrSwitchChain.tsx index 71114f5642..adba7a6d4e 100644 --- a/lib/web3/useAddOrSwitchChain.tsx +++ b/lib/web3/useAddOrSwitchChain.tsx @@ -37,7 +37,7 @@ export default function useAddOrSwitchChain() { symbol: config.chain.currency.symbol, decimals: config.chain.currency.decimals, }, - rpcUrls: [ config.chain.rpcUrl ], + rpcUrls: config.chain.rpcUrls, blockExplorerUrls: [ config.app.baseUrl ], } ] as never; // in wagmi types for wallet_addEthereumChain method is not provided diff --git a/lib/web3/wagmiConfig.ts b/lib/web3/wagmiConfig.ts index 387a9bdd16..f5e28b811e 100644 --- a/lib/web3/wagmiConfig.ts +++ b/lib/web3/wagmiConfig.ts @@ -1,5 +1,5 @@ import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; -import { http } from 'viem'; +import { fallback, http } from 'viem'; import { createConfig } from 'wagmi'; import config from 'configs/app'; @@ -13,7 +13,11 @@ const wagmi = (() => { const wagmiConfig = createConfig({ chains: [ currentChain ], transports: { - [currentChain.id]: http(config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc`), + [currentChain.id]: fallback( + config.chain.rpcUrls + .map((url) => http(url)) + .concat(http(`${ config.api.endpoint }/api/eth-rpc`)), + ), }, ssr: true, batch: { multicall: { wait: 100 } }, @@ -26,7 +30,7 @@ const wagmi = (() => { networks: chains, multiInjectedProviderDiscovery: true, transports: { - [currentChain.id]: http(), + [currentChain.id]: fallback(config.chain.rpcUrls.map((url) => http(url))), }, projectId: feature.walletConnect.projectId, ssr: true, diff --git a/nextjs/csp/policies/app.ts b/nextjs/csp/policies/app.ts index 2ba5f68f5d..ae0fbf7ca2 100644 --- a/nextjs/csp/policies/app.ts +++ b/nextjs/csp/policies/app.ts @@ -51,7 +51,7 @@ export function app(): CspDev.DirectiveDescriptor { getFeaturePayload(config.features.rewards)?.api.endpoint, // chain RPC server - config.chain.rpcUrl, + ...config.chain.rpcUrls, 'https://infragrid.v.network', // RPC providers // github (spec for api-docs page) diff --git a/ui/contractVerification/methods/ContractVerificationSolidityFoundry.tsx b/ui/contractVerification/methods/ContractVerificationSolidityFoundry.tsx index a481631c3c..d91ca04e24 100644 --- a/ui/contractVerification/methods/ContractVerificationSolidityFoundry.tsx +++ b/ui/contractVerification/methods/ContractVerificationSolidityFoundry.tsx @@ -15,7 +15,7 @@ const ContractVerificationSolidityFoundry = () => { const address = watch('address'); const codeSnippet = `forge verify-contract \\ - --rpc-url ${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` } \\ + --rpc-url ${ config.chain.rpcUrls[0] || `${ config.api.endpoint }/api/eth-rpc` } \\ --verifier blockscout \\ --verifier-url '${ config.api.endpoint }/api/' \\ ${ address || '
' } \\ diff --git a/ui/contractVerification/methods/ContractVerificationSolidityHardhat.tsx b/ui/contractVerification/methods/ContractVerificationSolidityHardhat.tsx index d779aea9e4..9dda8e2e25 100644 --- a/ui/contractVerification/methods/ContractVerificationSolidityHardhat.tsx +++ b/ui/contractVerification/methods/ContractVerificationSolidityHardhat.tsx @@ -22,7 +22,7 @@ const ContractVerificationSolidityHardhat = ({ config: formConfig }: { config: S solidity: "${ latestSolidityVersion || '0.8.24' }", // replace if necessary networks: { '${ chainNameSlug }': { - url: '${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` }' + url: '${ config.chain.rpcUrls[0] || `${ config.api.endpoint }/api/eth-rpc` }' }, }, etherscan: { diff --git a/ui/pages/Address.pw.tsx b/ui/pages/Address.pw.tsx index e958c71e96..28b668fde1 100644 --- a/ui/pages/Address.pw.tsx +++ b/ui/pages/Address.pw.tsx @@ -51,7 +51,7 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse }); const component = await render(
, { hooksConfig }); - await page.waitForResponse(config.chain.rpcUrl as string); + await page.waitForResponse(config.chain.rpcUrls[0]); await expect(component).toHaveScreenshot({ mask: [ page.locator(pwConfig.adsBannerSelector) ], diff --git a/ui/pages/Block.pw.tsx b/ui/pages/Block.pw.tsx index 48a69ee034..7501f74379 100644 --- a/ui/pages/Block.pw.tsx +++ b/ui/pages/Block.pw.tsx @@ -28,7 +28,7 @@ test('degradation view, details tab', async({ render, mockApiResponse, mockRpcRe }); const component = await render(, { hooksConfig }); - await page.waitForResponse(config.chain.rpcUrl as string); + await page.waitForResponse(config.chain.rpcUrls[0]); await expect(component).toHaveScreenshot(); }); @@ -49,7 +49,7 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon }); const component = await render(, { hooksConfig }); - await page.waitForResponse(config.chain.rpcUrl as string); + await page.waitForResponse(config.chain.rpcUrls[0]); await expect(component).toHaveScreenshot(); }); @@ -71,7 +71,7 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR }); const component = await render(, { hooksConfig }); - await page.waitForResponse(config.chain.rpcUrl as string); + await page.waitForResponse(config.chain.rpcUrls[0]); await expect(component).toHaveScreenshot(); }); diff --git a/ui/pages/MarketplaceApp.tsx b/ui/pages/MarketplaceApp.tsx index 78e14ca4e3..bd15c08eae 100644 --- a/ui/pages/MarketplaceApp.tsx +++ b/ui/pages/MarketplaceApp.tsx @@ -65,7 +65,7 @@ const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => { blockscoutNetworkName: config.chain.name, blockscoutNetworkId: Number(config.chain.id), blockscoutNetworkCurrency: config.chain.currency, - blockscoutNetworkRpc: config.chain.rpcUrl, + blockscoutNetworkRpc: config.chain.rpcUrls[0], }; iframeRef?.current?.contentWindow?.postMessage(message, data.url); @@ -159,7 +159,7 @@ const MarketplaceApp = () => { { } }, [ addOrSwitchChain, provider, toast, wallet ]); - if (!provider || !wallet || !config.chain.rpcUrl || !feature.isEnabled) { + if (!provider || !wallet || !config.chain.rpcUrls.length || !feature.isEnabled) { return null; }