diff --git a/test/e2e/tests/custom-rpc-history.spec.js b/test/e2e/tests/custom-rpc-history.spec.js
index 3405b9a5d..792bfa980 100644
--- a/test/e2e/tests/custom-rpc-history.spec.js
+++ b/test/e2e/tests/custom-rpc-history.spec.js
@@ -54,7 +54,7 @@ describe('Stores custom RPC history', function () {
);
});
- it('warns user when they enter url or chainId for an already configured network', async function () {
+ it('warns user when they enter url for an already configured network', async function () {
await withFixtures(
{
fixtures: 'imported-account',
@@ -68,7 +68,6 @@ describe('Stores custom RPC history', function () {
// duplicate network
const duplicateRpcUrl = 'http://localhost:8545';
- const duplicateChainId = '0x539';
await driver.clickElement('.network-display');
@@ -78,7 +77,6 @@ describe('Stores custom RPC history', function () {
const customRpcInputs = await driver.findElements('input[type="text"]');
const rpcUrlInput = customRpcInputs[1];
- const chainIdInput = customRpcInputs[2];
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(duplicateRpcUrl);
@@ -86,6 +84,38 @@ describe('Stores custom RPC history', function () {
text: 'This URL is currently used by the Localhost 8545 network.',
tag: 'p',
});
+ },
+ );
+ });
+
+ it('warns user when they enter chainId for an already configured network', async function () {
+ await withFixtures(
+ {
+ fixtures: 'imported-account',
+ ganacheOptions,
+ title: this.test.title,
+ },
+ async ({ driver }) => {
+ await driver.navigate();
+ await driver.fill('#password', 'correct horse battery staple');
+ await driver.press('#password', driver.Key.ENTER);
+
+ // duplicate network
+ const newRpcUrl = 'http://localhost:8544';
+ const duplicateChainId = '0x539';
+
+ await driver.clickElement('.network-display');
+
+ await driver.clickElement({ text: 'Custom RPC', tag: 'span' });
+
+ await driver.findElement('.settings-page__sub-header-text');
+
+ const customRpcInputs = await driver.findElements('input[type="text"]');
+ const rpcUrlInput = customRpcInputs[1];
+ const chainIdInput = customRpcInputs[2];
+
+ await rpcUrlInput.clear();
+ await rpcUrlInput.sendKeys(newRpcUrl);
await chainIdInput.clear();
await chainIdInput.sendKeys(duplicateChainId);
diff --git a/ui/pages/settings/networks-tab/network-form/network-form.component.js b/ui/pages/settings/networks-tab/network-form/network-form.component.js
index eeaebeeaa..8381c921d 100644
--- a/ui/pages/settings/networks-tab/network-form/network-form.component.js
+++ b/ui/pages/settings/networks-tab/network-form/network-form.component.js
@@ -280,6 +280,7 @@ export default class NetworkForm extends PureComponent {
}) {
const { errors } = this.state;
const { viewOnly } = this.props;
+ const errorMessage = errors[fieldKey]?.msg || '';
return (
@@ -305,7 +306,7 @@ export default class NetworkForm extends PureComponent {
margin="dense"
value={value}
disabled={viewOnly}
- error={errors[fieldKey]}
+ error={errorMessage}
autoFocus={autoFocus}
/>
@@ -328,45 +329,78 @@ export default class NetworkForm extends PureComponent {
});
};
- hasError = (errorKey, errorVal) => {
- return this.state.errors[errorKey] === errorVal;
+ setErrorEmpty = (errorKey) => {
+ this.setState({
+ errors: {
+ ...this.state.errors,
+ [errorKey]: {
+ msg: '',
+ key: '',
+ },
+ },
+ });
+ };
+
+ hasError = (errorKey, errorKeyVal) => {
+ return this.state.errors[errorKey]?.key === errorKeyVal;
};
- validateChainIdOnChange = (chainIdArg = '') => {
+ hasErrors = () => {
+ const { errors } = this.state;
+ 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') {
+ return false;
+ }
+ return error.key && error.msg;
+ });
+ };
+
+ validateChainIdOnChange = (selfRpcUrl, chainIdArg = '') => {
const { networksToRender } = this.props;
const chainId = chainIdArg.trim();
+ let errorKey = '';
let errorMessage = '';
let radix = 10;
const hexChainId = chainId.startsWith('0x')
? chainId
: `0x${decimalToHex(chainId)}`;
const [matchingChainId] = networksToRender.filter(
- (e) => e.chainId === hexChainId,
+ (e) => e.chainId === hexChainId && e.rpcUrl !== selfRpcUrl,
);
if (chainId === '') {
- this.setErrorTo('chainId', '');
+ this.setErrorEmpty('chainId');
return;
} else if (matchingChainId) {
+ errorKey = 'chainIdExistsErrorMsg';
errorMessage = this.context.t('chainIdExistsErrorMsg', [
matchingChainId.label ?? matchingChainId.labelKey,
]);
} else if (chainId.startsWith('0x')) {
radix = 16;
if (!/^0x[0-9a-f]+$/iu.test(chainId)) {
+ errorKey = 'invalidHexNumber';
errorMessage = this.context.t('invalidHexNumber');
} else if (!isPrefixedFormattedHexString(chainId)) {
errorMessage = this.context.t('invalidHexNumberLeadingZeros');
}
} else if (!/^[0-9]+$/u.test(chainId)) {
+ errorKey = 'invalidNumber';
errorMessage = this.context.t('invalidNumber');
} else if (chainId.startsWith('0')) {
+ errorKey = 'invalidNumberLeadingZeros';
errorMessage = this.context.t('invalidNumberLeadingZeros');
} else if (!isSafeChainId(parseInt(chainId, radix))) {
+ errorKey = 'invalidChainIdTooBig';
errorMessage = this.context.t('invalidChainIdTooBig');
}
- this.setErrorTo('chainId', errorMessage);
+ this.setErrorTo('chainId', {
+ key: errorKey,
+ msg: errorMessage,
+ });
};
/**
@@ -381,6 +415,7 @@ export default class NetworkForm extends PureComponent {
*/
validateChainIdOnSubmit = async (formChainId, parsedChainId, rpcUrl) => {
const { t } = this.context;
+ let errorKey;
let errorMessage;
let endpointChainId;
let providerError;
@@ -393,6 +428,7 @@ export default class NetworkForm extends PureComponent {
}
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
@@ -410,6 +446,7 @@ export default class NetworkForm extends PureComponent {
}
}
+ errorKey = 'endpointReturnedDifferentChainId';
errorMessage = t('endpointReturnedDifferentChainId', [
endpointChainId.length <= 12
? endpointChainId
@@ -417,12 +454,15 @@ export default class NetworkForm extends PureComponent {
]);
}
- if (errorMessage) {
- this.setErrorTo('chainId', errorMessage);
+ if (errorKey) {
+ this.setErrorTo('chainId', {
+ key: errorKey,
+ msg: errorMessage,
+ });
return false;
}
- this.setErrorTo('chainId', '');
+ this.setErrorEmpty('chainId');
return true;
};
@@ -432,17 +472,25 @@ export default class NetworkForm extends PureComponent {
};
validateBlockExplorerURL = (url, stateKey) => {
+ const { t } = this.context;
if (!validUrl.isWebUri(url) && url !== '') {
- this.setErrorTo(
- stateKey,
- this.context.t(
- this.isValidWhenAppended(url)
- ? 'urlErrorMsg'
- : 'invalidBlockExplorerURL',
- ),
- );
+ let errorKey;
+ let errorMessage;
+
+ if (this.isValidWhenAppended(url)) {
+ errorKey = 'urlErrorMsg';
+ errorMessage = t('urlErrorMsg');
+ } else {
+ errorKey = 'invalidBlockExplorerURL';
+ errorMessage = t('invalidBlockExplorerURL');
+ }
+
+ this.setErrorTo(stateKey, {
+ key: errorKey,
+ msg: errorMessage,
+ });
} else {
- this.setErrorTo(stateKey, '');
+ this.setErrorEmpty(stateKey);
}
};
@@ -451,28 +499,32 @@ export default class NetworkForm extends PureComponent {
const { networksToRender } = this.props;
const { chainId: stateChainId } = this.state;
const isValidUrl = validUrl.isWebUri(url);
- const chainIdFetchFailed = this.hasError(
- 'chainId',
- t('failedToFetchChainId'),
- );
+ const chainIdFetchFailed = this.hasError('chainId', 'failedToFetchChainId');
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
if (!isValidUrl && url !== '') {
- this.setErrorTo(
- stateKey,
- this.context.t(
- this.isValidWhenAppended(url) ? 'urlErrorMsg' : 'invalidRPC',
- ),
- );
+ let errorKey;
+ let errorMessage;
+ if (this.isValidWhenAppended(url)) {
+ errorKey = 'urlErrorMsg';
+ errorMessage = t('urlErrorMsg');
+ } else {
+ errorKey = 'invalidRPC';
+ errorMessage = t('invalidRPC');
+ }
+ this.setErrorTo(stateKey, {
+ key: errorKey,
+ msg: errorMessage,
+ });
} else if (matchingRPCUrl) {
- this.setErrorTo(
- stateKey,
- this.context.t('urlExistsErrorMsg', [
+ this.setErrorTo(stateKey, {
+ key: 'urlExistsErrorMsg',
+ msg: t('urlExistsErrorMsg', [
matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
]),
- );
+ });
} else {
- this.setErrorTo(stateKey, '');
+ this.setErrorEmpty(stateKey);
}
// Re-validate the chain id if it could not be found with previous rpc url
@@ -501,18 +553,16 @@ export default class NetworkForm extends PureComponent {
chainId = '',
ticker,
blockExplorerUrl,
- errors,
} = this.state;
const deletable =
!networksTabIsInAddMode && !isCurrentRpcTarget && !viewOnly;
-
const isSubmitDisabled =
+ this.hasErrors() ||
this.isSubmitting() ||
this.stateIsUnchanged() ||
!rpcUrl ||
- !chainId ||
- Object.values(errors).some((x) => x);
+ !chainId;
return (
@@ -535,7 +585,7 @@ export default class NetworkForm extends PureComponent {
textFieldId: 'chainId',
onChange: this.setStateWithValue(
'chainId',
- this.validateChainIdOnChange,
+ this.validateChainIdOnChange.bind(this, rpcUrl),
),
value: chainId,
tooltipText: viewOnly ? null : t('networkSettingsChainIdDescription'),