Commit 8388b41f authored by tom goriunov's avatar tom goriunov Committed by GitHub

App config: support multiple RPC URLs (#2498)

Fixes #2479
parent e0663888
import type { RollupType } from 'types/client/rollup'; import type { RollupType } from 'types/client/rollup';
import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks'; 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; const DEFAULT_CURRENCY_DECIMALS = 18;
...@@ -17,6 +19,19 @@ const verificationType: NetworkVerificationType = (() => { ...@@ -17,6 +19,19 @@ const verificationType: NetworkVerificationType = (() => {
return getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') as NetworkVerificationTypeEnvs || 'mining'; 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<Array<string>>(envValue);
return Array.isArray(parsedValue) ? parsedValue : [];
})();
const chain = Object.freeze({ const chain = Object.freeze({
id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'), id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'),
name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'), name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'),
...@@ -32,7 +47,7 @@ const chain = Object.freeze({ ...@@ -32,7 +47,7 @@ const chain = Object.freeze({
}, },
hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true', hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true',
tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC', tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC',
rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'), rpcUrls,
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
verificationType, verificationType,
}); });
......
...@@ -17,7 +17,7 @@ const config: Feature<{ walletConnect: { projectId: string } }> = (() => { ...@@ -17,7 +17,7 @@ const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
chain.currency.name && chain.currency.name &&
chain.currency.symbol && chain.currency.symbol &&
chain.currency.decimals && chain.currency.decimals &&
chain.rpcUrl && chain.rpcUrls.length > 0 &&
walletConnectProjectId walletConnectProjectId
) { ) {
return Object.freeze({ return Object.freeze({
......
...@@ -33,7 +33,7 @@ const config: Feature<( ...@@ -33,7 +33,7 @@ const config: Feature<(
rating: { airtableApiKey: string; airtableBaseId: string } | undefined; rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
graphLinksUrl: string | undefined; graphLinksUrl: string | undefined;
}> = (() => { }> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) {
const props = { const props = {
submitFormUrl, submitFormUrl,
categoriesUrl, categoriesUrl,
......
...@@ -587,7 +587,21 @@ const schema = yup ...@@ -587,7 +587,21 @@ const schema = yup
NEXT_PUBLIC_NETWORK_NAME: yup.string().required(), NEXT_PUBLIC_NETWORK_NAME: yup.string().required(),
NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(), NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(), 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_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(),
......
...@@ -5,4 +5,5 @@ NEXT_PUBLIC_HOMEPAGE_STATS=[] ...@@ -5,4 +5,5 @@ NEXT_PUBLIC_HOMEPAGE_STATS=[]
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
\ No newline at end of file NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com']
...@@ -95,7 +95,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -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_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_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_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<string>` | 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_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_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+ | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ |
......
...@@ -12,7 +12,7 @@ const currentChain = { ...@@ -12,7 +12,7 @@ const currentChain = {
}, },
rpcUrls: { rpcUrls: {
'default': { 'default': {
http: [ config.chain.rpcUrl ?? '' ], http: config.chain.rpcUrls,
}, },
}, },
blockExplorers: { blockExplorers: {
......
...@@ -37,7 +37,7 @@ export default function useAddOrSwitchChain() { ...@@ -37,7 +37,7 @@ export default function useAddOrSwitchChain() {
symbol: config.chain.currency.symbol, symbol: config.chain.currency.symbol,
decimals: config.chain.currency.decimals, decimals: config.chain.currency.decimals,
}, },
rpcUrls: [ config.chain.rpcUrl ], rpcUrls: config.chain.rpcUrls,
blockExplorerUrls: [ config.app.baseUrl ], blockExplorerUrls: [ config.app.baseUrl ],
} ] as never; } ] as never;
// in wagmi types for wallet_addEthereumChain method is not provided // in wagmi types for wallet_addEthereumChain method is not provided
......
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import { http } from 'viem'; import { fallback, http } from 'viem';
import { createConfig } from 'wagmi'; import { createConfig } from 'wagmi';
import config from 'configs/app'; import config from 'configs/app';
...@@ -13,7 +13,11 @@ const wagmi = (() => { ...@@ -13,7 +13,11 @@ const wagmi = (() => {
const wagmiConfig = createConfig({ const wagmiConfig = createConfig({
chains: [ currentChain ], chains: [ currentChain ],
transports: { 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, ssr: true,
batch: { multicall: { wait: 100 } }, batch: { multicall: { wait: 100 } },
...@@ -26,7 +30,7 @@ const wagmi = (() => { ...@@ -26,7 +30,7 @@ const wagmi = (() => {
networks: chains, networks: chains,
multiInjectedProviderDiscovery: true, multiInjectedProviderDiscovery: true,
transports: { transports: {
[currentChain.id]: http(), [currentChain.id]: fallback(config.chain.rpcUrls.map((url) => http(url))),
}, },
projectId: feature.walletConnect.projectId, projectId: feature.walletConnect.projectId,
ssr: true, ssr: true,
......
...@@ -51,7 +51,7 @@ export function app(): CspDev.DirectiveDescriptor { ...@@ -51,7 +51,7 @@ export function app(): CspDev.DirectiveDescriptor {
getFeaturePayload(config.features.rewards)?.api.endpoint, getFeaturePayload(config.features.rewards)?.api.endpoint,
// chain RPC server // chain RPC server
config.chain.rpcUrl, ...config.chain.rpcUrls,
'https://infragrid.v.network', // RPC providers 'https://infragrid.v.network', // RPC providers
// github (spec for api-docs page) // github (spec for api-docs page)
......
...@@ -15,7 +15,7 @@ const ContractVerificationSolidityFoundry = () => { ...@@ -15,7 +15,7 @@ const ContractVerificationSolidityFoundry = () => {
const address = watch('address'); const address = watch('address');
const codeSnippet = `forge verify-contract \\ 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 blockscout \\
--verifier-url '${ config.api.endpoint }/api/' \\ --verifier-url '${ config.api.endpoint }/api/' \\
${ address || '<address>' } \\ ${ address || '<address>' } \\
......
...@@ -22,7 +22,7 @@ const ContractVerificationSolidityHardhat = ({ config: formConfig }: { config: S ...@@ -22,7 +22,7 @@ const ContractVerificationSolidityHardhat = ({ config: formConfig }: { config: S
solidity: "${ latestSolidityVersion || '0.8.24' }", // replace if necessary solidity: "${ latestSolidityVersion || '0.8.24' }", // replace if necessary
networks: { networks: {
'${ chainNameSlug }': { '${ chainNameSlug }': {
url: '${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` }' url: '${ config.chain.rpcUrls[0] || `${ config.api.endpoint }/api/eth-rpc` }'
}, },
}, },
etherscan: { etherscan: {
......
...@@ -51,7 +51,7 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse ...@@ -51,7 +51,7 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse
}); });
const component = await render(<Address/>, { hooksConfig }); const component = await render(<Address/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
...@@ -28,7 +28,7 @@ test('degradation view, details tab', async({ render, mockApiResponse, mockRpcRe ...@@ -28,7 +28,7 @@ test('degradation view, details tab', async({ render, mockApiResponse, mockRpcRe
}); });
const component = await render(<Block/>, { hooksConfig }); const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -49,7 +49,7 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon ...@@ -49,7 +49,7 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon
}); });
const component = await render(<Block/>, { hooksConfig }); const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -71,7 +71,7 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR ...@@ -71,7 +71,7 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR
}); });
const component = await render(<Block/>, { hooksConfig }); const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -65,7 +65,7 @@ const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => { ...@@ -65,7 +65,7 @@ const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => {
blockscoutNetworkName: config.chain.name, blockscoutNetworkName: config.chain.name,
blockscoutNetworkId: Number(config.chain.id), blockscoutNetworkId: Number(config.chain.id),
blockscoutNetworkCurrency: config.chain.currency, blockscoutNetworkCurrency: config.chain.currency,
blockscoutNetworkRpc: config.chain.rpcUrl, blockscoutNetworkRpc: config.chain.rpcUrls[0],
}; };
iframeRef?.current?.contentWindow?.postMessage(message, data.url); iframeRef?.current?.contentWindow?.postMessage(message, data.url);
...@@ -159,7 +159,7 @@ const MarketplaceApp = () => { ...@@ -159,7 +159,7 @@ const MarketplaceApp = () => {
<DappscoutIframeProvider <DappscoutIframeProvider
address={ address } address={ address }
appUrl={ appUrl } appUrl={ appUrl }
rpcUrl={ config.chain.rpcUrl } rpcUrl={ config.chain.rpcUrls[0] }
sendTransaction={ sendTransaction } sendTransaction={ sendTransaction }
signMessage={ signMessage } signMessage={ signMessage }
signTypedData={ signTypedData } signTypedData={ signTypedData }
......
...@@ -50,7 +50,7 @@ const NetworkAddToWallet = () => { ...@@ -50,7 +50,7 @@ const NetworkAddToWallet = () => {
} }
}, [ addOrSwitchChain, provider, toast, wallet ]); }, [ addOrSwitchChain, provider, toast, wallet ]);
if (!provider || !wallet || !config.chain.rpcUrl || !feature.isEnabled) { if (!provider || !wallet || !config.chain.rpcUrls.length || !feature.isEnabled) {
return null; return null;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment