mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Add currency symbol validation in the add network form (#12431)
* validate ticker symbol in add/edit network form
This commit is contained in:
parent
87daae708c
commit
48cc9d5ad3
@ -444,6 +444,10 @@
|
||||
"chainIdExistsErrorMsg": {
|
||||
"message": "This Chain ID is currently used by the $1 network."
|
||||
},
|
||||
"chainListReturnedDifferentTickerSymbol": {
|
||||
"message": "The network with chain ID $1 may use a different currency symbol ($2) than the one you have entered. Please verify before continuing.",
|
||||
"description": "$1 is the chain id currently entered in the network form and $2 is the return value of nativeCurrency.symbol from chainlist.network"
|
||||
},
|
||||
"chromeRequiredForHardwareWallets": {
|
||||
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
|
||||
},
|
||||
@ -1017,7 +1021,7 @@
|
||||
"message": "Learn more."
|
||||
},
|
||||
"endpointReturnedDifferentChainId": {
|
||||
"message": "The endpoint returned a different chain ID: $1",
|
||||
"message": "The RPC URL you have entered returned a different chain ID ($1). Please update the Chain ID to match the RPC URL of the network you are trying to add.",
|
||||
"description": "$1 is the return value of eth_chainId from an RPC endpoint"
|
||||
},
|
||||
"ensIllegalCharacter": {
|
||||
@ -1126,6 +1130,9 @@
|
||||
"failedToFetchChainId": {
|
||||
"message": "Could not fetch chain ID. Is your RPC URL correct?"
|
||||
},
|
||||
"failedToFetchTickerSymbolData": {
|
||||
"message": "Ticker symbol verification data is currently unavailable, make sure that the symbol you have entered is correct. It will impact the conversion rates that you see for this network"
|
||||
},
|
||||
"failureMessage": {
|
||||
"message": "Something went wrong, and we were unable to complete the action"
|
||||
},
|
||||
|
@ -14,6 +14,7 @@ describe('Stores custom RPC history', function () {
|
||||
it(`creates first custom RPC entry`, async function () {
|
||||
const port = 8546;
|
||||
const chainId = 1338;
|
||||
const symbol = 'TEST';
|
||||
await withFixtures(
|
||||
{
|
||||
fixtures: 'imported-account',
|
||||
@ -38,6 +39,7 @@ describe('Stores custom RPC history', function () {
|
||||
const networkNameInput = customRpcInputs[0];
|
||||
const rpcUrlInput = customRpcInputs[1];
|
||||
const chainIdInput = customRpcInputs[2];
|
||||
const symbolInput = customRpcInputs[3];
|
||||
|
||||
await networkNameInput.clear();
|
||||
await networkNameInput.sendKeys(networkName);
|
||||
@ -48,6 +50,9 @@ describe('Stores custom RPC history', function () {
|
||||
await chainIdInput.clear();
|
||||
await chainIdInput.sendKeys(chainId.toString());
|
||||
|
||||
await symbolInput.clear();
|
||||
await symbolInput.sendKeys(symbol);
|
||||
|
||||
await driver.clickElement(
|
||||
'.networks-tab__add-network-form-footer .btn-primary',
|
||||
);
|
||||
@ -70,7 +75,8 @@ describe('Stores custom RPC history', function () {
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
// duplicate network
|
||||
const duplicateRpcUrl = 'http://localhost:8545';
|
||||
const duplicateRpcUrl =
|
||||
'https://mainnet.infura.io/v3/00000000000000000000000000000000';
|
||||
|
||||
await driver.clickElement('.network-display');
|
||||
|
||||
@ -84,7 +90,7 @@ describe('Stores custom RPC history', function () {
|
||||
await rpcUrlInput.clear();
|
||||
await rpcUrlInput.sendKeys(duplicateRpcUrl);
|
||||
await driver.findElement({
|
||||
text: 'This URL is currently used by the Localhost 8545 network.',
|
||||
text: 'This URL is currently used by the mainnet network.',
|
||||
tag: 'h6',
|
||||
});
|
||||
},
|
||||
@ -97,6 +103,7 @@ describe('Stores custom RPC history', function () {
|
||||
fixtures: 'imported-account',
|
||||
ganacheOptions,
|
||||
title: this.test.title,
|
||||
failOnConsoleError: false,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await driver.navigate();
|
||||
@ -117,9 +124,6 @@ describe('Stores custom RPC history', function () {
|
||||
const rpcUrlInput = customRpcInputs[1];
|
||||
const chainIdInput = customRpcInputs[2];
|
||||
|
||||
await rpcUrlInput.clear();
|
||||
await rpcUrlInput.sendKeys(newRpcUrl);
|
||||
|
||||
await chainIdInput.clear();
|
||||
await chainIdInput.sendKeys(duplicateChainId);
|
||||
await driver.findElement({
|
||||
@ -127,6 +131,14 @@ describe('Stores custom RPC history', function () {
|
||||
'This Chain ID is currently used by the Localhost 8545 network.',
|
||||
tag: 'h6',
|
||||
});
|
||||
|
||||
await rpcUrlInput.clear();
|
||||
await rpcUrlInput.sendKeys(newRpcUrl);
|
||||
|
||||
await driver.findElement({
|
||||
text: 'Could not fetch chain ID. Is your RPC URL correct?',
|
||||
tag: 'h6',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -184,6 +196,7 @@ describe('Stores custom RPC history', function () {
|
||||
fixtures: 'custom-rpc',
|
||||
ganacheOptions,
|
||||
title: this.test.title,
|
||||
failOnConsoleError: false,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await driver.navigate();
|
||||
|
@ -31,6 +31,7 @@ export default function FormField({
|
||||
allowDecimals,
|
||||
disabled,
|
||||
placeholder,
|
||||
warning,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@ -92,6 +93,7 @@ export default function FormField({
|
||||
<input
|
||||
className={classNames('form-field__input', {
|
||||
'form-field__input--error': error,
|
||||
'form-field__input--warning': warning,
|
||||
})}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={value}
|
||||
@ -111,6 +113,15 @@ export default function FormField({
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
{warning && (
|
||||
<Typography
|
||||
color={COLORS.UI4}
|
||||
variant={TYPOGRAPHY.H7}
|
||||
className="form-field__warning"
|
||||
>
|
||||
{warning}
|
||||
</Typography>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
@ -141,6 +152,10 @@ FormField.propTypes = {
|
||||
* Show error message
|
||||
*/
|
||||
error: PropTypes.string,
|
||||
/**
|
||||
* Show warning message
|
||||
*/
|
||||
warning: PropTypes.string,
|
||||
/**
|
||||
* Handler when fields change
|
||||
*/
|
||||
|
@ -45,5 +45,9 @@
|
||||
&--error {
|
||||
border-color: var(--error-1);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
border-color: var(--alert-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@
|
||||
|
||||
&__network-form {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex: 1 0;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-height: 465px;
|
||||
|
@ -26,6 +26,8 @@ import {
|
||||
DEFAULT_ROUTE,
|
||||
NETWORKS_ROUTE,
|
||||
} from '../../../../helpers/constants/routes';
|
||||
import fetchWithCache from '../../../../helpers/utils/fetch-with-cache';
|
||||
import { usePrevious } from '../../../../hooks/usePrevious';
|
||||
|
||||
/**
|
||||
* Attempts to convert the given chainId to a decimal string, for display
|
||||
@ -83,6 +85,7 @@ const NetworksForm = ({
|
||||
selectedNetwork?.blockExplorerUrl || '',
|
||||
);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [warnings, setWarnings] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
@ -92,6 +95,7 @@ const NetworksForm = ({
|
||||
setTicker(selectedNetwork?.ticker);
|
||||
setBlockExplorerUrl(selectedNetwork?.blockExplorerUrl);
|
||||
setErrors({});
|
||||
setWarnings({});
|
||||
setIsSubmitting(false);
|
||||
}, [selectedNetwork, selectedNetworkName]);
|
||||
|
||||
@ -170,216 +174,297 @@ const NetworksForm = ({
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
const setErrorTo = (errorKey, errorVal) => {
|
||||
setErrors({ ...errors, [errorKey]: errorVal });
|
||||
};
|
||||
|
||||
const setErrorEmpty = (errorKey) => {
|
||||
setErrors({
|
||||
...errors,
|
||||
[errorKey]: {
|
||||
msg: '',
|
||||
key: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const hasError = (errorKey, errorKeyVal) => {
|
||||
return errors[errorKey]?.key === errorKeyVal;
|
||||
};
|
||||
|
||||
const hasErrors = () => {
|
||||
return Object.keys(errors).some((key) => {
|
||||
const error = errors[key];
|
||||
// Do not factor in duplicate chain id error for submission disabling
|
||||
if (key === 'chainId' && error.key === 'chainIdExistsErrorMsg') {
|
||||
if (key === 'chainId' && error?.key === 'chainIdExistsErrorMsg') {
|
||||
return false;
|
||||
}
|
||||
return error.key && error.msg;
|
||||
return error?.key && error?.msg;
|
||||
});
|
||||
};
|
||||
|
||||
const validateChainIdOnChange = (chainArg = '') => {
|
||||
const formChainId = chainArg.trim();
|
||||
let errorKey = '';
|
||||
let errorMessage = '';
|
||||
let radix = 10;
|
||||
let hexChainId = formChainId;
|
||||
const validateBlockExplorerURL = useCallback(
|
||||
(url) => {
|
||||
if (!validUrl.isWebUri(url) && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
|
||||
if (!hexChainId.startsWith('0x')) {
|
||||
try {
|
||||
hexChainId = `0x${decimalToHex(hexChainId)}`;
|
||||
} catch (err) {
|
||||
setErrorTo('chainId', {
|
||||
key: 'invalidHexNumber',
|
||||
msg: t('invalidHexNumber'),
|
||||
});
|
||||
return;
|
||||
if (isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidBlockExplorerURL';
|
||||
errorMessage = t('invalidBlockExplorerURL');
|
||||
}
|
||||
|
||||
return {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const [matchingChainId] = networksToRender.filter(
|
||||
(e) => e.chainId === hexChainId && e.rpcUrl !== rpcUrl,
|
||||
);
|
||||
const validateChainId = useCallback(
|
||||
async (chainArg = '') => {
|
||||
const formChainId = chainArg.trim();
|
||||
let errorKey = '';
|
||||
let errorMessage = '';
|
||||
let radix = 10;
|
||||
let hexChainId = formChainId;
|
||||
|
||||
if (formChainId === '') {
|
||||
setErrorEmpty('chainId');
|
||||
return;
|
||||
} else if (matchingChainId) {
|
||||
errorKey = 'chainIdExistsErrorMsg';
|
||||
errorMessage = t('chainIdExistsErrorMsg', [
|
||||
matchingChainId.label ?? matchingChainId.labelKey,
|
||||
]);
|
||||
} else if (formChainId.startsWith('0x')) {
|
||||
radix = 16;
|
||||
if (!/^0x[0-9a-f]+$/iu.test(formChainId)) {
|
||||
errorKey = 'invalidHexNumber';
|
||||
errorMessage = t('invalidHexNumber');
|
||||
} else if (!isPrefixedFormattedHexString(formChainId)) {
|
||||
errorMessage = t('invalidHexNumberLeadingZeros');
|
||||
}
|
||||
} else if (!/^[0-9]+$/u.test(formChainId)) {
|
||||
errorKey = 'invalidNumber';
|
||||
errorMessage = t('invalidNumber');
|
||||
} else if (formChainId.startsWith('0')) {
|
||||
errorKey = 'invalidNumberLeadingZeros';
|
||||
errorMessage = t('invalidNumberLeadingZeros');
|
||||
} else if (!isSafeChainId(parseInt(formChainId, radix))) {
|
||||
errorKey = 'invalidChainIdTooBig';
|
||||
errorMessage = t('invalidChainIdTooBig');
|
||||
}
|
||||
|
||||
setErrorTo('chainId', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the chain ID by checking it against the `eth_chainId` return
|
||||
* value from the given RPC URL.
|
||||
* Assumes that all strings are non-empty and correctly formatted.
|
||||
*
|
||||
* @param {string} formChainId - Non-empty, hex or decimal number string from
|
||||
* the form.
|
||||
* @param {string} parsedChainId - The parsed, hex string chain ID.
|
||||
* @param {string} formRpcUrl - The RPC URL from the form.
|
||||
*/
|
||||
const validateChainIdOnSubmit = async (
|
||||
formChainId,
|
||||
parsedChainId,
|
||||
formRpcUrl,
|
||||
) => {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
let endpointChainId;
|
||||
let providerError;
|
||||
|
||||
try {
|
||||
endpointChainId = await jsonRpcRequest(formRpcUrl, 'eth_chainId');
|
||||
} catch (err) {
|
||||
log.warn('Failed to fetch the chainId from the endpoint.', err);
|
||||
providerError = err;
|
||||
}
|
||||
|
||||
if (providerError || typeof endpointChainId !== 'string') {
|
||||
errorKey = 'failedToFetchChainId';
|
||||
errorMessage = t('failedToFetchChainId');
|
||||
} else if (parsedChainId !== endpointChainId) {
|
||||
// Here, we are in an error state. The endpoint should always return a
|
||||
// hexadecimal string. If the user entered a decimal string, we attempt
|
||||
// to convert the endpoint's return value to decimal before rendering it
|
||||
// in an error message in the form.
|
||||
if (!formChainId.startsWith('0x')) {
|
||||
if (!hexChainId.startsWith('0x')) {
|
||||
try {
|
||||
endpointChainId = parseInt(endpointChainId, 16).toString(10);
|
||||
hexChainId = `0x${decimalToHex(hexChainId)}`;
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
'Failed to convert endpoint chain ID to decimal',
|
||||
endpointChainId,
|
||||
);
|
||||
return {
|
||||
key: 'invalidHexNumber',
|
||||
msg: t('invalidHexNumber'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
errorKey = 'endpointReturnedDifferentChainId';
|
||||
errorMessage = t('endpointReturnedDifferentChainId', [
|
||||
endpointChainId.length <= 12
|
||||
? endpointChainId
|
||||
: `${endpointChainId.slice(0, 9)}...`,
|
||||
]);
|
||||
}
|
||||
const [matchingChainId] = networksToRender.filter(
|
||||
(e) => e.chainId === hexChainId && e.rpcUrl !== rpcUrl,
|
||||
);
|
||||
|
||||
if (errorKey) {
|
||||
setErrorTo('chainId', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
setErrorEmpty('chainId');
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateBlockExplorerURL = (url) => {
|
||||
if (!validUrl.isWebUri(url) && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
|
||||
if (isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidBlockExplorerURL';
|
||||
errorMessage = t('invalidBlockExplorerURL');
|
||||
if (formChainId === '') {
|
||||
return null;
|
||||
} else if (matchingChainId) {
|
||||
errorKey = 'chainIdExistsErrorMsg';
|
||||
errorMessage = t('chainIdExistsErrorMsg', [
|
||||
matchingChainId.label ?? matchingChainId.labelKey,
|
||||
]);
|
||||
} else if (formChainId.startsWith('0x')) {
|
||||
radix = 16;
|
||||
if (!/^0x[0-9a-f]+$/iu.test(formChainId)) {
|
||||
errorKey = 'invalidHexNumber';
|
||||
errorMessage = t('invalidHexNumber');
|
||||
} else if (!isPrefixedFormattedHexString(formChainId)) {
|
||||
errorMessage = t('invalidHexNumberLeadingZeros');
|
||||
}
|
||||
} else if (!/^[0-9]+$/u.test(formChainId)) {
|
||||
errorKey = 'invalidNumber';
|
||||
errorMessage = t('invalidNumber');
|
||||
} else if (formChainId.startsWith('0')) {
|
||||
errorKey = 'invalidNumberLeadingZeros';
|
||||
errorMessage = t('invalidNumberLeadingZeros');
|
||||
} else if (!isSafeChainId(parseInt(formChainId, radix))) {
|
||||
errorKey = 'invalidChainIdTooBig';
|
||||
errorMessage = t('invalidChainIdTooBig');
|
||||
}
|
||||
|
||||
setErrorTo('blockExplorerUrl', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
} else {
|
||||
setErrorEmpty('blockExplorerUrl');
|
||||
}
|
||||
};
|
||||
let endpointChainId;
|
||||
let providerError;
|
||||
|
||||
const validateUrlRpcUrl = (url) => {
|
||||
const isValidUrl = validUrl.isWebUri(url);
|
||||
const chainIdFetchFailed = hasError('chainId', 'failedToFetchChainId');
|
||||
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
|
||||
|
||||
if (!isValidUrl && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
if (isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidRPC';
|
||||
errorMessage = t('invalidRPC');
|
||||
try {
|
||||
endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId');
|
||||
} catch (err) {
|
||||
log.warn('Failed to fetch the chainId from the endpoint.', err);
|
||||
providerError = err;
|
||||
}
|
||||
setErrorTo('rpcUrl', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
} else if (matchingRPCUrl) {
|
||||
setErrorTo('rpcUrl', {
|
||||
key: 'urlExistsErrorMsg',
|
||||
msg: t('urlExistsErrorMsg', [
|
||||
matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
|
||||
]),
|
||||
});
|
||||
} else {
|
||||
setErrorEmpty('rpcUrl');
|
||||
|
||||
if (rpcUrl && formChainId) {
|
||||
if (providerError || typeof endpointChainId !== 'string') {
|
||||
errorKey = 'failedToFetchChainId';
|
||||
errorMessage = t('failedToFetchChainId');
|
||||
} else if (hexChainId !== endpointChainId) {
|
||||
// Here, we are in an error state. The endpoint should always return a
|
||||
// hexadecimal string. If the user entered a decimal string, we attempt
|
||||
// to convert the endpoint's return value to decimal before rendering it
|
||||
// in an error message in the form.
|
||||
if (!formChainId.startsWith('0x')) {
|
||||
try {
|
||||
endpointChainId = parseInt(endpointChainId, 16).toString(10);
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
'Failed to convert endpoint chain ID to decimal',
|
||||
endpointChainId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
errorKey = 'endpointReturnedDifferentChainId';
|
||||
errorMessage = t('endpointReturnedDifferentChainId', [
|
||||
endpointChainId.length <= 12
|
||||
? endpointChainId
|
||||
: `${endpointChainId.slice(0, 9)}...`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (errorKey) {
|
||||
return {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[rpcUrl, networksToRender, t],
|
||||
);
|
||||
|
||||
/**
|
||||
* Validates the ticker symbol by checking it against the nativeCurrency.symbol return
|
||||
* value from chainid.network trusted chain data
|
||||
* Assumes that all strings are non-empty and correctly formatted.
|
||||
*
|
||||
* @param {string} formChainId - The Chain ID currently entered in the form.
|
||||
* @param {string} formTickerSymbol - The ticker/currency symbol currently entered in the form.
|
||||
*/
|
||||
const validateTickerSymbol = useCallback(
|
||||
async (formChainId, formTickerSymbol) => {
|
||||
let warningKey;
|
||||
let warningMessage;
|
||||
let safeChainsList;
|
||||
let providerError;
|
||||
|
||||
if (!formChainId || !formTickerSymbol) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
safeChainsList = await fetchWithCache(
|
||||
'https://chainid.network/chains.json',
|
||||
);
|
||||
} catch (err) {
|
||||
log.warn('Failed to fetch the chainList from chainid.network', err);
|
||||
providerError = err;
|
||||
}
|
||||
|
||||
if (providerError || !Array.isArray(safeChainsList)) {
|
||||
warningKey = 'failedToFetchTickerSymbolData';
|
||||
warningMessage = t('failedToFetchTickerSymbolData');
|
||||
} else {
|
||||
const matchedChain = safeChainsList?.find(
|
||||
(chain) => chain.chainId.toString() === formChainId,
|
||||
);
|
||||
|
||||
if (matchedChain === undefined) {
|
||||
warningKey = 'failedToFetchTickerSymbolData';
|
||||
warningMessage = t('failedToFetchTickerSymbolData');
|
||||
} else {
|
||||
const returnedTickerSymbol = matchedChain.nativeCurrency?.symbol;
|
||||
if (returnedTickerSymbol !== formTickerSymbol) {
|
||||
warningKey = 'chainListReturnedDifferentTickerSymbol';
|
||||
warningMessage = t('chainListReturnedDifferentTickerSymbol', [
|
||||
formChainId,
|
||||
returnedTickerSymbol,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (warningKey) {
|
||||
return {
|
||||
key: warningKey,
|
||||
msg: warningMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const validateRPCUrl = useCallback(
|
||||
(url) => {
|
||||
const isValidUrl = validUrl.isWebUri(url);
|
||||
const [
|
||||
{
|
||||
rpcUrl: matchingRPCUrl = null,
|
||||
label: matchingRPCLabel,
|
||||
labelKey: matchingRPCLabelKey,
|
||||
} = {},
|
||||
] = networksToRender.filter((e) => e.rpcUrl === url);
|
||||
const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork;
|
||||
|
||||
if (!isValidUrl && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
if (isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidRPC';
|
||||
errorMessage = t('invalidRPC');
|
||||
}
|
||||
|
||||
return {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
};
|
||||
} else if (matchingRPCUrl && matchingRPCUrl !== selectedNetworkRpcUrl) {
|
||||
return {
|
||||
key: 'urlExistsErrorMsg',
|
||||
msg: t('urlExistsErrorMsg', [
|
||||
matchingRPCLabel ?? matchingRPCLabelKey,
|
||||
]),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[selectedNetwork, networksToRender, t],
|
||||
);
|
||||
|
||||
// validation effect
|
||||
const previousRpcUrl = usePrevious(rpcUrl);
|
||||
const previousChainId = usePrevious(chainId);
|
||||
const previousTicker = usePrevious(ticker);
|
||||
const previousBlockExplorerUrl = usePrevious(blockExplorerUrl);
|
||||
useEffect(() => {
|
||||
if (viewOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-validate the chain id if it could not be found with previous rpc url
|
||||
if (chainId && isValidUrl && chainIdFetchFailed) {
|
||||
const formChainId = chainId.trim().toLowerCase();
|
||||
const prefixedChainId = prefixChainId(formChainId);
|
||||
validateChainIdOnSubmit(formChainId, prefixedChainId, url);
|
||||
if (
|
||||
previousRpcUrl === rpcUrl &&
|
||||
previousChainId === chainId &&
|
||||
previousTicker === ticker &&
|
||||
previousBlockExplorerUrl === blockExplorerUrl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
async function validate() {
|
||||
const chainIdError = await validateChainId(chainId);
|
||||
const tickerWarning = await validateTickerSymbol(chainId, ticker);
|
||||
const blockExplorerError = validateBlockExplorerURL(blockExplorerUrl);
|
||||
const rpcUrlError = validateRPCUrl(rpcUrl);
|
||||
setErrors({
|
||||
...errors,
|
||||
chainId: chainIdError,
|
||||
blockExplorerUrl: blockExplorerError,
|
||||
rpcUrl: rpcUrlError,
|
||||
});
|
||||
setWarnings({
|
||||
...warnings,
|
||||
ticker: tickerWarning,
|
||||
});
|
||||
}
|
||||
|
||||
validate();
|
||||
}, [
|
||||
errors,
|
||||
warnings,
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
blockExplorerUrl,
|
||||
viewOnly,
|
||||
label,
|
||||
previousRpcUrl,
|
||||
previousChainId,
|
||||
previousTicker,
|
||||
previousBlockExplorerUrl,
|
||||
validateBlockExplorerURL,
|
||||
validateChainId,
|
||||
validateTickerSymbol,
|
||||
validateRPCUrl,
|
||||
]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
@ -387,13 +472,6 @@ const NetworksForm = ({
|
||||
const formChainId = chainId.trim().toLowerCase();
|
||||
const prefixedChainId = prefixChainId(formChainId);
|
||||
|
||||
if (
|
||||
!(await validateChainIdOnSubmit(formChainId, prefixedChainId, rpcUrl))
|
||||
) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// After this point, isSubmitting will be reset in componentDidUpdate
|
||||
if (selectedNetwork.rpcUrl && rpcUrl !== selectedNetwork.rpcUrl) {
|
||||
await dispatch(
|
||||
@ -453,7 +531,12 @@ const NetworksForm = ({
|
||||
const deletable = !isCurrentRpcTarget && !viewOnly && !addNewNetwork;
|
||||
const stateUnchanged = stateIsUnchanged();
|
||||
const isSubmitDisabled =
|
||||
hasErrors() || isSubmitting || stateUnchanged || !rpcUrl || !chainId;
|
||||
hasErrors() ||
|
||||
isSubmitting ||
|
||||
stateUnchanged ||
|
||||
!rpcUrl ||
|
||||
!chainId ||
|
||||
!ticker;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -488,39 +571,29 @@ const NetworksForm = ({
|
||||
/>
|
||||
<FormField
|
||||
error={errors.rpcUrl?.msg || ''}
|
||||
onChange={(value) => {
|
||||
setRpcUrl(value);
|
||||
validateUrlRpcUrl(value);
|
||||
}}
|
||||
onChange={setRpcUrl}
|
||||
titleText={t('rpcUrl')}
|
||||
value={rpcUrl}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<FormField
|
||||
error={errors.chainId?.msg || ''}
|
||||
onChange={(value) => {
|
||||
setChainId(value);
|
||||
validateChainIdOnChange(value);
|
||||
}}
|
||||
onChange={setChainId}
|
||||
titleText={t('chainId')}
|
||||
value={chainId}
|
||||
disabled={viewOnly}
|
||||
tooltipText={viewOnly ? null : t('networkSettingsChainIdDescription')}
|
||||
/>
|
||||
<FormField
|
||||
error={errors.ticker?.msg || ''}
|
||||
warning={warnings.ticker?.msg || ''}
|
||||
onChange={setTicker}
|
||||
titleText={t('currencySymbol')}
|
||||
titleUnit={t('optionalWithParanthesis')}
|
||||
value={ticker}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<FormField
|
||||
error={errors.blockExplorerUrl?.msg || ''}
|
||||
onChange={(value) => {
|
||||
setBlockExplorerUrl(value);
|
||||
validateBlockExplorerURL(value);
|
||||
}}
|
||||
onChange={setBlockExplorerUrl}
|
||||
titleText={t('blockExplorerUrl')}
|
||||
titleUnit={t('optionalWithParanthesis')}
|
||||
value={blockExplorerUrl}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import nock from 'nock';
|
||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||
import { defaultNetworksData } from '../networks-tab.constants';
|
||||
import { MAINNET_RPC_URL } from '../../../../../shared/constants/network';
|
||||
import NetworksForm from '.';
|
||||
|
||||
const renderComponent = (props) => {
|
||||
@ -36,7 +38,51 @@ const propNetworkDisplay = {
|
||||
};
|
||||
|
||||
describe('NetworkForm Component', () => {
|
||||
it('should render Add new network form correctly', () => {
|
||||
beforeAll(() => {
|
||||
nock.disableNetConnect();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nock('https://chainid.network:443', { encodedQueryParams: true })
|
||||
.get('/chains.json')
|
||||
.reply(200, [
|
||||
{
|
||||
name: 'Polygon Mainnet',
|
||||
chain: 'Polygon',
|
||||
rpc: [
|
||||
'https://polygon-rpc.com/',
|
||||
'https://rpc-mainnet.matic.network',
|
||||
'https://matic-mainnet.chainstacklabs.com',
|
||||
'https://rpc-mainnet.maticvigil.com',
|
||||
'https://rpc-mainnet.matic.quiknode.pro',
|
||||
'https://matic-mainnet-full-rpc.bwarelabs.com',
|
||||
],
|
||||
nativeCurrency: {
|
||||
name: 'MATIC',
|
||||
symbol: 'MATIC',
|
||||
decimals: 18,
|
||||
},
|
||||
shortName: 'MATIC',
|
||||
chainId: 137,
|
||||
},
|
||||
]);
|
||||
|
||||
nock('https://bsc-dataseed.binance.org:443', {
|
||||
encodedQueryParams: true,
|
||||
})
|
||||
.post('/')
|
||||
.reply(200, { jsonrpc: '2.0', id: '1643927040523', result: '0x38' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('should render add new network form correctly', async () => {
|
||||
const { queryByText, queryAllByText } = renderComponent(propNewNetwork);
|
||||
expect(
|
||||
queryByText(
|
||||
@ -48,9 +94,30 @@ describe('NetworkForm Component', () => {
|
||||
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||
expect(queryAllByText('(Optional)')).toHaveLength(2);
|
||||
expect(queryAllByText('(Optional)')).toHaveLength(1);
|
||||
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||
expect(queryByText('Save')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.change(screen.getByRole('textbox', { name: 'Chain ID' }), {
|
||||
target: { value: '1' },
|
||||
});
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'This Chain ID is currently used by the mainnet network.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await fireEvent.change(
|
||||
screen.getByRole('textbox', { name: 'New RPC URL' }),
|
||||
{
|
||||
target: { value: 'test' },
|
||||
},
|
||||
);
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'URLs require the appropriate HTTP/HTTPS prefix.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render network form correctly', () => {
|
||||
@ -81,31 +148,121 @@ describe('NetworkForm Component', () => {
|
||||
expect(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.blockExplorerUrl),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.label),
|
||||
{
|
||||
target: { value: 'LocalHost 8545' },
|
||||
},
|
||||
);
|
||||
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.chainId),
|
||||
{
|
||||
target: { value: '1' },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate RPC URL field correctly', async () => {
|
||||
renderComponent(propNewNetwork);
|
||||
|
||||
const rpcUrlField = screen.getByRole('textbox', { name: 'New RPC URL' });
|
||||
await fireEvent.change(rpcUrlField, {
|
||||
target: { value: 'test' },
|
||||
});
|
||||
expect(
|
||||
queryByText('This Chain ID is currently used by the mainnet network.'),
|
||||
await screen.findByText(
|
||||
'URLs require the appropriate HTTP/HTTPS prefix.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.rpcUrl),
|
||||
{
|
||||
target: { value: 'test' },
|
||||
},
|
||||
);
|
||||
await fireEvent.change(rpcUrlField, {
|
||||
target: { value: ' ' },
|
||||
});
|
||||
expect(await screen.findByText('Invalid RPC URL')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.change(rpcUrlField, {
|
||||
target: { value: MAINNET_RPC_URL },
|
||||
});
|
||||
|
||||
expect(
|
||||
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
|
||||
await screen.findByText(
|
||||
'This URL is currently used by the mainnet network.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate chain id field correctly', async () => {
|
||||
renderComponent(propNewNetwork);
|
||||
const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
|
||||
const rpcUrlField = screen.getByRole('textbox', { name: 'New RPC URL' });
|
||||
|
||||
fireEvent.change(chainIdField, {
|
||||
target: { value: '1' },
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'This Chain ID is currently used by the mainnet network.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(rpcUrlField, {
|
||||
target: { value: 'https://bsc-dataseed.binance.org/' },
|
||||
});
|
||||
|
||||
const expectedWarning =
|
||||
'The RPC URL you have entered returned a different chain ID (56). Please update the Chain ID to match the RPC URL of the network you are trying to add.';
|
||||
expect(await screen.findByText(expectedWarning)).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(chainIdField, {
|
||||
target: { value: 'a' },
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText('Invalid hexadecimal number.'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// reset RCP URL field
|
||||
fireEvent.change(rpcUrlField, {
|
||||
target: { value: '' },
|
||||
});
|
||||
|
||||
fireEvent.change(chainIdField, {
|
||||
target: { value: '00000012314' },
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText('Invalid number. Remove any leading zeros.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate currency symbol field correctly', async () => {
|
||||
renderComponent(propNewNetwork);
|
||||
const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
|
||||
const currencySymbolField = screen.getByRole('textbox', {
|
||||
name: 'Currency Symbol',
|
||||
});
|
||||
|
||||
fireEvent.change(chainIdField, {
|
||||
target: { value: '1234' },
|
||||
});
|
||||
|
||||
fireEvent.change(currencySymbolField, {
|
||||
target: { value: 'abcd' },
|
||||
});
|
||||
|
||||
const expectedWarning =
|
||||
'Ticker symbol verification data is currently unavailable, make sure that the symbol you have entered is correct. It will impact the conversion rates that you see for this network';
|
||||
expect(await screen.findByText(expectedWarning)).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(chainIdField, {
|
||||
target: { value: '137' },
|
||||
});
|
||||
const secondExpectedWarning =
|
||||
'The network with chain ID 137 may use a different currency symbol (MATIC) than the one you have entered. Please verify before continuing.';
|
||||
expect(await screen.findByText(secondExpectedWarning)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate block explorer url field correctly', async () => {
|
||||
renderComponent(propNewNetwork);
|
||||
const blockExplorerUrlField = screen.getByRole('textbox', {
|
||||
name: 'Block Explorer URL (Optional)',
|
||||
});
|
||||
fireEvent.change(blockExplorerUrlField, {
|
||||
target: { value: '1234' },
|
||||
});
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'URLs require the appropriate HTTP/HTTPS prefix.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||
import { defaultNetworksData } from '../networks-tab.constants';
|
||||
import NetworksTabContent from '.';
|
||||
@ -45,7 +45,7 @@ const props = {
|
||||
};
|
||||
|
||||
describe('NetworksTabContent Component', () => {
|
||||
it('should render networks tab content correctly', () => {
|
||||
it('should render networks tab content correctly', async () => {
|
||||
const { queryByText, getByDisplayValue } = renderComponent(props);
|
||||
|
||||
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||
@ -71,22 +71,29 @@ describe('NetworksTabContent Component', () => {
|
||||
expect(
|
||||
getByDisplayValue(props.selectedNetwork.blockExplorerUrl),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.label), {
|
||||
target: { value: 'LocalHost 8545' },
|
||||
});
|
||||
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.chainId), {
|
||||
target: { value: '1' },
|
||||
});
|
||||
expect(
|
||||
queryByText('This Chain ID is currently used by the mainnet network.'),
|
||||
).toBeInTheDocument();
|
||||
expect(await getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.rpcUrl), {
|
||||
target: { value: 'test' },
|
||||
});
|
||||
expect(
|
||||
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
|
||||
await screen.findByText(
|
||||
'URLs require the appropriate HTTP/HTTPS prefix.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.chainId), {
|
||||
target: { value: '1' },
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'Could not fetch chain ID. Is your RPC URL correct?',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user