Allowing custom rpc form submission when chainId is a duplicate (#11363)

feature/default_network_editable
ryanml 3 years ago committed by GitHub
parent e10ddbe3a3
commit a171ac7b34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      test/e2e/tests/custom-rpc-history.spec.js
  2. 128
      ui/pages/settings/networks-tab/network-form/network-form.component.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( await withFixtures(
{ {
fixtures: 'imported-account', fixtures: 'imported-account',
@ -68,7 +68,6 @@ describe('Stores custom RPC history', function () {
// duplicate network // duplicate network
const duplicateRpcUrl = 'http://localhost:8545'; const duplicateRpcUrl = 'http://localhost:8545';
const duplicateChainId = '0x539';
await driver.clickElement('.network-display'); await driver.clickElement('.network-display');
@ -78,7 +77,6 @@ describe('Stores custom RPC history', function () {
const customRpcInputs = await driver.findElements('input[type="text"]'); const customRpcInputs = await driver.findElements('input[type="text"]');
const rpcUrlInput = customRpcInputs[1]; const rpcUrlInput = customRpcInputs[1];
const chainIdInput = customRpcInputs[2];
await rpcUrlInput.clear(); await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(duplicateRpcUrl); 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.', text: 'This URL is currently used by the Localhost 8545 network.',
tag: 'p', 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.clear();
await chainIdInput.sendKeys(duplicateChainId); await chainIdInput.sendKeys(duplicateChainId);

@ -280,6 +280,7 @@ export default class NetworkForm extends PureComponent {
}) { }) {
const { errors } = this.state; const { errors } = this.state;
const { viewOnly } = this.props; const { viewOnly } = this.props;
const errorMessage = errors[fieldKey]?.msg || '';
return ( return (
<div className="networks-tab__network-form-row"> <div className="networks-tab__network-form-row">
@ -305,7 +306,7 @@ export default class NetworkForm extends PureComponent {
margin="dense" margin="dense"
value={value} value={value}
disabled={viewOnly} disabled={viewOnly}
error={errors[fieldKey]} error={errorMessage}
autoFocus={autoFocus} autoFocus={autoFocus}
/> />
</div> </div>
@ -328,45 +329,78 @@ export default class NetworkForm extends PureComponent {
}); });
}; };
hasError = (errorKey, errorVal) => { setErrorEmpty = (errorKey) => {
return this.state.errors[errorKey] === errorVal; 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 { networksToRender } = this.props;
const chainId = chainIdArg.trim(); const chainId = chainIdArg.trim();
let errorKey = '';
let errorMessage = ''; let errorMessage = '';
let radix = 10; let radix = 10;
const hexChainId = chainId.startsWith('0x') const hexChainId = chainId.startsWith('0x')
? chainId ? chainId
: `0x${decimalToHex(chainId)}`; : `0x${decimalToHex(chainId)}`;
const [matchingChainId] = networksToRender.filter( const [matchingChainId] = networksToRender.filter(
(e) => e.chainId === hexChainId, (e) => e.chainId === hexChainId && e.rpcUrl !== selfRpcUrl,
); );
if (chainId === '') { if (chainId === '') {
this.setErrorTo('chainId', ''); this.setErrorEmpty('chainId');
return; return;
} else if (matchingChainId) { } else if (matchingChainId) {
errorKey = 'chainIdExistsErrorMsg';
errorMessage = this.context.t('chainIdExistsErrorMsg', [ errorMessage = this.context.t('chainIdExistsErrorMsg', [
matchingChainId.label ?? matchingChainId.labelKey, matchingChainId.label ?? matchingChainId.labelKey,
]); ]);
} else if (chainId.startsWith('0x')) { } else if (chainId.startsWith('0x')) {
radix = 16; radix = 16;
if (!/^0x[0-9a-f]+$/iu.test(chainId)) { if (!/^0x[0-9a-f]+$/iu.test(chainId)) {
errorKey = 'invalidHexNumber';
errorMessage = this.context.t('invalidHexNumber'); errorMessage = this.context.t('invalidHexNumber');
} else if (!isPrefixedFormattedHexString(chainId)) { } else if (!isPrefixedFormattedHexString(chainId)) {
errorMessage = this.context.t('invalidHexNumberLeadingZeros'); errorMessage = this.context.t('invalidHexNumberLeadingZeros');
} }
} else if (!/^[0-9]+$/u.test(chainId)) { } else if (!/^[0-9]+$/u.test(chainId)) {
errorKey = 'invalidNumber';
errorMessage = this.context.t('invalidNumber'); errorMessage = this.context.t('invalidNumber');
} else if (chainId.startsWith('0')) { } else if (chainId.startsWith('0')) {
errorKey = 'invalidNumberLeadingZeros';
errorMessage = this.context.t('invalidNumberLeadingZeros'); errorMessage = this.context.t('invalidNumberLeadingZeros');
} else if (!isSafeChainId(parseInt(chainId, radix))) { } else if (!isSafeChainId(parseInt(chainId, radix))) {
errorKey = 'invalidChainIdTooBig';
errorMessage = this.context.t('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) => { validateChainIdOnSubmit = async (formChainId, parsedChainId, rpcUrl) => {
const { t } = this.context; const { t } = this.context;
let errorKey;
let errorMessage; let errorMessage;
let endpointChainId; let endpointChainId;
let providerError; let providerError;
@ -393,6 +428,7 @@ export default class NetworkForm extends PureComponent {
} }
if (providerError || typeof endpointChainId !== 'string') { if (providerError || typeof endpointChainId !== 'string') {
errorKey = 'failedToFetchChainId';
errorMessage = t('failedToFetchChainId'); errorMessage = t('failedToFetchChainId');
} else if (parsedChainId !== endpointChainId) { } else if (parsedChainId !== endpointChainId) {
// Here, we are in an error state. The endpoint should always return a // 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', [ errorMessage = t('endpointReturnedDifferentChainId', [
endpointChainId.length <= 12 endpointChainId.length <= 12
? endpointChainId ? endpointChainId
@ -417,12 +454,15 @@ export default class NetworkForm extends PureComponent {
]); ]);
} }
if (errorMessage) { if (errorKey) {
this.setErrorTo('chainId', errorMessage); this.setErrorTo('chainId', {
key: errorKey,
msg: errorMessage,
});
return false; return false;
} }
this.setErrorTo('chainId', ''); this.setErrorEmpty('chainId');
return true; return true;
}; };
@ -432,17 +472,25 @@ export default class NetworkForm extends PureComponent {
}; };
validateBlockExplorerURL = (url, stateKey) => { validateBlockExplorerURL = (url, stateKey) => {
const { t } = this.context;
if (!validUrl.isWebUri(url) && url !== '') { if (!validUrl.isWebUri(url) && url !== '') {
this.setErrorTo( let errorKey;
stateKey, let errorMessage;
this.context.t(
this.isValidWhenAppended(url) if (this.isValidWhenAppended(url)) {
? 'urlErrorMsg' errorKey = 'urlErrorMsg';
: 'invalidBlockExplorerURL', errorMessage = t('urlErrorMsg');
), } else {
); errorKey = 'invalidBlockExplorerURL';
errorMessage = t('invalidBlockExplorerURL');
}
this.setErrorTo(stateKey, {
key: errorKey,
msg: errorMessage,
});
} else { } else {
this.setErrorTo(stateKey, ''); this.setErrorEmpty(stateKey);
} }
}; };
@ -451,28 +499,32 @@ export default class NetworkForm extends PureComponent {
const { networksToRender } = this.props; const { networksToRender } = this.props;
const { chainId: stateChainId } = this.state; const { chainId: stateChainId } = this.state;
const isValidUrl = validUrl.isWebUri(url); const isValidUrl = validUrl.isWebUri(url);
const chainIdFetchFailed = this.hasError( const chainIdFetchFailed = this.hasError('chainId', 'failedToFetchChainId');
'chainId',
t('failedToFetchChainId'),
);
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url); const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
if (!isValidUrl && url !== '') { if (!isValidUrl && url !== '') {
this.setErrorTo( let errorKey;
stateKey, let errorMessage;
this.context.t( if (this.isValidWhenAppended(url)) {
this.isValidWhenAppended(url) ? 'urlErrorMsg' : 'invalidRPC', errorKey = 'urlErrorMsg';
), errorMessage = t('urlErrorMsg');
); } else {
errorKey = 'invalidRPC';
errorMessage = t('invalidRPC');
}
this.setErrorTo(stateKey, {
key: errorKey,
msg: errorMessage,
});
} else if (matchingRPCUrl) { } else if (matchingRPCUrl) {
this.setErrorTo( this.setErrorTo(stateKey, {
stateKey, key: 'urlExistsErrorMsg',
this.context.t('urlExistsErrorMsg', [ msg: t('urlExistsErrorMsg', [
matchingRPCUrl.label ?? matchingRPCUrl.labelKey, matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
]), ]),
); });
} else { } else {
this.setErrorTo(stateKey, ''); this.setErrorEmpty(stateKey);
} }
// Re-validate the chain id if it could not be found with previous rpc url // 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 = '', chainId = '',
ticker, ticker,
blockExplorerUrl, blockExplorerUrl,
errors,
} = this.state; } = this.state;
const deletable = const deletable =
!networksTabIsInAddMode && !isCurrentRpcTarget && !viewOnly; !networksTabIsInAddMode && !isCurrentRpcTarget && !viewOnly;
const isSubmitDisabled = const isSubmitDisabled =
this.hasErrors() ||
this.isSubmitting() || this.isSubmitting() ||
this.stateIsUnchanged() || this.stateIsUnchanged() ||
!rpcUrl || !rpcUrl ||
!chainId || !chainId;
Object.values(errors).some((x) => x);
return ( return (
<div className="networks-tab__network-form"> <div className="networks-tab__network-form">
@ -535,7 +585,7 @@ export default class NetworkForm extends PureComponent {
textFieldId: 'chainId', textFieldId: 'chainId',
onChange: this.setStateWithValue( onChange: this.setStateWithValue(
'chainId', 'chainId',
this.validateChainIdOnChange, this.validateChainIdOnChange.bind(this, rpcUrl),
), ),
value: chainId, value: chainId,
tooltipText: viewOnly ? null : t('networkSettingsChainIdDescription'), tooltipText: viewOnly ? null : t('networkSettingsChainIdDescription'),

Loading…
Cancel
Save