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 { 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<Array<string>>(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,
});
......
......@@ -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({
......
......@@ -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,
......
......@@ -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(),
......
......@@ -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']
......@@ -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<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_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+ |
......
......@@ -12,7 +12,7 @@ const currentChain = {
},
rpcUrls: {
'default': {
http: [ config.chain.rpcUrl ?? '' ],
http: config.chain.rpcUrls,
},
},
blockExplorers: {
......
......@@ -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
......
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,
......
......@@ -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)
......
......@@ -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 || '<address>' } \\
......
......@@ -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: {
......
......@@ -51,7 +51,7 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse
});
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({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
......@@ -28,7 +28,7 @@ test('degradation view, details tab', async({ render, mockApiResponse, mockRpcRe
});
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();
});
......@@ -49,7 +49,7 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon
});
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();
});
......@@ -71,7 +71,7 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR
});
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();
});
......@@ -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 = () => {
<DappscoutIframeProvider
address={ address }
appUrl={ appUrl }
rpcUrl={ config.chain.rpcUrl }
rpcUrl={ config.chain.rpcUrls[0] }
sendTransaction={ sendTransaction }
signMessage={ signMessage }
signTypedData={ signTypedData }
......
......@@ -50,7 +50,7 @@ const NetworkAddToWallet = () => {
}
}, [ addOrSwitchChain, provider, toast, wallet ]);
if (!provider || !wallet || !config.chain.rpcUrl || !feature.isEnabled) {
if (!provider || !wallet || !config.chain.rpcUrls.length || !feature.isEnabled) {
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