1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

delay chain validation (#17413)

This commit is contained in:
witmicko 2023-03-08 16:33:27 +00:00 committed by GitHub
parent b231b091b9
commit 75801e9502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 355 additions and 53 deletions

View File

@ -263,6 +263,9 @@
"addToken": { "addToken": {
"message": "Add token" "message": "Add token"
}, },
"addingCustomNetwork": {
"message": "Adding Network"
},
"address": { "address": {
"message": "Address" "message": "Address"
}, },
@ -1308,6 +1311,9 @@
"message": "Stack:", "message": "Stack:",
"description": "Title for error stack, which is displayed for debugging purposes" "description": "Title for error stack, which is displayed for debugging purposes"
}, },
"errorWhileConnectingToRPC": {
"message": "Error while connecting to the custom network."
},
"ethGasPriceFetchWarning": { "ethGasPriceFetchWarning": {
"message": "Backup gas price is provided as the main gas estimation service is unavailable right now." "message": "Backup gas price is provided as the main gas estimation service is unavailable right now."
}, },
@ -2033,6 +2039,9 @@
"mismatchedNetworkSymbol": { "mismatchedNetworkSymbol": {
"message": "The submitted currency symbol does not match what we expect for this chain ID." "message": "The submitted currency symbol does not match what we expect for this chain ID."
}, },
"mismatchedRpcChainId": {
"message": "Chain ID returned by the custom network does not match the submitted chain ID."
},
"mismatchedRpcUrl": { "mismatchedRpcUrl": {
"message": "According to our records the submitted RPC URL value does not match a known provider for this chain ID." "message": "According to our records the submitted RPC URL value does not match a known provider for this chain ID."
}, },

View File

@ -10,7 +10,6 @@ import {
isPrefixedFormattedHexString, isPrefixedFormattedHexString,
isSafeChainId, isSafeChainId,
} from '../../../../../shared/modules/network.utils'; } from '../../../../../shared/modules/network.utils';
import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
const addEthereumChain = { const addEthereumChain = {
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN], methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
@ -155,6 +154,7 @@ async function addEthereumChainHandler(
if (currentChainId === _chainId && currentRpcUrl === firstValidRPCUrl) { if (currentChainId === _chainId && currentRpcUrl === firstValidRPCUrl) {
return end(); return end();
} }
// If this network is already added with but is not the currently selected network // If this network is already added with but is not the currently selected network
// Ask the user to switch the network // Ask the user to switch the network
try { try {
@ -182,28 +182,6 @@ async function addEthereumChainHandler(
return end(); return end();
} }
let endpointChainId;
try {
endpointChainId = await jsonRpcRequest(firstValidRPCUrl, 'eth_chainId');
} catch (err) {
return end(
ethErrors.rpc.internal({
message: `Request for method 'eth_chainId on ${firstValidRPCUrl} failed`,
data: { networkErr: err },
}),
);
}
if (_chainId !== endpointChainId) {
return end(
ethErrors.rpc.invalidParams({
message: `Chain ID returned by RPC URL ${firstValidRPCUrl} does not match ${_chainId}`,
data: { chainId: endpointChainId },
}),
);
}
if (typeof chainName !== 'string' || !chainName) { if (typeof chainName !== 'string' || !chainName) {
return end( return end(
ethErrors.rpc.invalidParams({ ethErrors.rpc.invalidParams({
@ -266,8 +244,7 @@ async function addEthereumChainHandler(
} }
try { try {
await addCustomRpc( const customRpc = await requestUserApproval({
await requestUserApproval({
origin, origin,
type: MESSAGE_TYPE.ADD_ETHEREUM_CHAIN, type: MESSAGE_TYPE.ADD_ETHEREUM_CHAIN,
requestData: { requestData: {
@ -277,9 +254,8 @@ async function addEthereumChainHandler(
rpcUrl: firstValidRPCUrl, rpcUrl: firstValidRPCUrl,
ticker, ticker,
}, },
}), });
); await addCustomRpc(customRpc);
sendMetrics({ sendMetrics({
event: 'Custom Network Added', event: 'Custom Network Added',
category: EVENT.CATEGORIES.NETWORK, category: EVENT.CATEGORIES.NETWORK,

View File

@ -6,10 +6,10 @@
// subset of files to check against these targets. // subset of files to check against these targets.
module.exports = { module.exports = {
global: { global: {
lines: 64, lines: 64.5,
branches: 52.5, branches: 53,
statements: 63.1, statements: 63,
functions: 56.1, functions: 56.5,
}, },
transforms: { transforms: {
branches: 100, branches: 100,

View File

@ -32,6 +32,20 @@ async function setupMocking(server, testSpecificMock) {
return {}; return {};
}, },
}); });
await server
.forPost(
'https://arbitrum-mainnet.infura.io/v3/00000000000000000000000000000000',
)
.thenCallback(() => {
return {
statusCode: 200,
json: {
jsonrpc: '2.0',
id: '1675864782845',
result: '0xa4b1',
},
};
});
await server.forPost('https://api.segment.io/v1/batch').thenCallback(() => { await server.forPost('https://api.segment.io/v1/batch').thenCallback(() => {
return { return {
@ -372,6 +386,19 @@ async function setupMocking(server, testSpecificMock) {
json: emptyHotlist, json: emptyHotlist,
}; };
}); });
await server
.forPost('https://customnetwork.com/api/customRPC')
.thenCallback(() => {
return {
statusCode: 200,
json: {
jsonrpc: '2.0',
id: '1675864782845',
result: '0x122',
},
};
});
} }
module.exports = { setupMocking }; module.exports = { setupMocking };

View File

@ -17,6 +17,213 @@ describe('Custom network', function () {
}, },
], ],
}; };
it('should show warning when adding chainId 0x1(ethereum) and be followed by an wrong chainId error', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
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);
await driver.openNewPage('http://127.0.0.1:8080/');
await driver.executeScript(`
var params = [{
chainId: "0x1",
chainName: "Fake Ethereum Network",
nativeCurrency: {
name: "",
symbol: "ETH",
decimals: 18
},
rpcUrls: ["https://customnetwork.com/api/customRPC"],
blockExplorerUrls: [ "http://localhost:8080/api/customRPC" ]
}]
window.ethereum.request({
method: 'wallet_addEthereumChain',
params
})
`);
const windowHandles = await driver.waitUntilXWindowHandles(3);
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
tag: 'button',
text: 'Approve',
});
const warningTxt =
'You are adding a new RPC provider for Ethereum Mainnet';
await driver.findElement({
tag: 'h4',
text: warningTxt,
});
await driver.clickElement({
tag: 'button',
text: 'Approve',
});
const errMsg =
'Chain ID returned by the custom network does not match the submitted chain ID.';
await driver.findElement({
tag: 'span',
text: errMsg,
});
const approveBtn = await driver.findElement({
tag: 'button',
text: 'Approve',
});
assert.equal(await approveBtn.isEnabled(), false);
await driver.clickElement({
tag: 'button',
text: 'Cancel',
});
},
);
});
it("don't add bad rpc custom network", async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
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);
await driver.openNewPage('http://127.0.0.1:8080/');
await driver.executeScript(`
var params = [{
chainId: "0x123",
chainName: "Antani",
nativeCurrency: {
name: "",
symbol: "ANTANI",
decimals: 18
},
rpcUrls: ["https://customnetwork.com/api/customRPC"],
blockExplorerUrls: [ "http://localhost:8080/api/customRPC" ]
}]
window.ethereum.request({
method: 'wallet_addEthereumChain',
params
})
`);
const windowHandles = await driver.waitUntilXWindowHandles(3);
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
tag: 'button',
text: 'Approve',
});
const errMsg =
'Chain ID returned by the custom network does not match the submitted chain ID.';
await driver.findElement({
tag: 'span',
text: errMsg,
});
const approveBtn = await driver.findElement({
tag: 'button',
text: 'Approve',
});
assert.equal(await approveBtn.isEnabled(), false);
await driver.clickElement({
tag: 'button',
text: 'Cancel',
});
},
);
});
it("don't add unreachable custom network", async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
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);
await driver.openNewPage('http://127.0.0.1:8080/');
await driver.executeScript(`
var params = [{
chainId: "0x123",
chainName: "Antani",
nativeCurrency: {
name: "",
symbol: "ANTANI",
decimals: 18
},
rpcUrls: ["https://doesntexist.abc/customRPC"],
blockExplorerUrls: [ "http://localhost:8080/api/customRPC" ]
}]
window.ethereum.request({
method: 'wallet_addEthereumChain',
params
})
`);
const windowHandles = await driver.waitUntilXWindowHandles(3);
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
tag: 'button',
text: 'Approve',
});
await driver.findElement({
tag: 'span',
text: 'Error while connecting to the custom network.',
});
const approveBtn = await driver.findElement({
tag: 'button',
text: 'Approve',
});
assert.equal(await approveBtn.isEnabled(), false);
await driver.clickElement({
tag: 'button',
text: 'Cancel',
});
},
);
});
it('add custom network and switch the network', async function () { it('add custom network and switch the network', async function () {
await withFixtures( await withFixtures(
{ {
@ -85,7 +292,6 @@ describe('Custom network', function () {
await driver.clickElement({ tag: 'button', text: 'Close' }); await driver.clickElement({ tag: 'button', text: 'Close' });
await driver.clickElement({ tag: 'button', text: 'Approve' }); await driver.clickElement({ tag: 'button', text: 'Approve' });
await driver.clickElement({ await driver.clickElement({
tag: 'h6', tag: 'h6',
text: 'Switch to Arbitrum One', text: 'Switch to Arbitrum One',

View File

@ -8,11 +8,15 @@ export default function ConfirmationFooter({
onCancel, onCancel,
submitText, submitText,
cancelText, cancelText,
loadingText,
alerts, alerts,
loading,
submitAlerts,
}) { }) {
return ( return (
<div className="confirmation-footer"> <div className="confirmation-footer">
{alerts} {alerts}
{submitAlerts}
<div className="confirmation-footer__actions"> <div className="confirmation-footer__actions">
{onCancel ? ( {onCancel ? (
<Button type="secondary" onClick={onCancel}> <Button type="secondary" onClick={onCancel}>
@ -20,13 +24,14 @@ export default function ConfirmationFooter({
</Button> </Button>
) : null} ) : null}
<Button <Button
disabled={Boolean(loading)}
type="primary" type="primary"
onClick={onSubmit} onClick={onSubmit}
className={classnames({ className={classnames({
centered: !onCancel, centered: !onCancel,
})} })}
> >
{submitText} {loading ? loadingText : submitText}
</Button> </Button>
</div> </div>
</div> </div>
@ -39,4 +44,7 @@ ConfirmationFooter.propTypes = {
cancelText: PropTypes.string, cancelText: PropTypes.string,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
submitText: PropTypes.string.isRequired, submitText: PropTypes.string.isRequired,
loadingText: PropTypes.string,
loading: PropTypes.bool,
submitAlerts: PropTypes.node,
}; };

View File

@ -177,6 +177,10 @@ export default function ConfirmationPage({
const setInputState = (key, value) => { const setInputState = (key, value) => {
setInputStates((currentState) => ({ ...currentState, [key]: value })); setInputStates((currentState) => ({ ...currentState, [key]: value }));
}; };
const [loading, setLoading] = useState(false);
const [loadingText, setLoadingText] = useState();
const [submitAlerts, setSubmitAlerts] = useState([]);
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
const snap = useSelector((state) => const snap = useSelector((state) =>
@ -258,14 +262,28 @@ export default function ConfirmationPage({
return INPUT_STATE_CONFIRMATIONS.includes(type); return INPUT_STATE_CONFIRMATIONS.includes(type);
}; };
const handleSubmit = () => const handleSubmitResult = (submitResult) => {
templateState[pendingConfirmation.id]?.useWarningModal if (submitResult?.length > 0) {
? setShowWarningModal(true) setLoadingText(templatedValues.submitText);
: templatedValues.onSubmit( setSubmitAlerts(submitResult);
hasInputState(pendingConfirmation.type) setLoading(true);
} else {
setLoading(false);
}
};
const handleSubmit = async () => {
setLoading(true);
if (templateState[pendingConfirmation.id]?.useWarningModal) {
setShowWarningModal(true);
} else {
const inputState = hasInputState(pendingConfirmation.type)
? inputStates[MESSAGE_TYPE.SNAP_DIALOG_PROMPT] ? inputStates[MESSAGE_TYPE.SNAP_DIALOG_PROMPT]
: null, : null;
); // submit result is an array of errors or empty on success
const submitResult = await templatedValues.onSubmit(inputState);
handleSubmitResult(submitResult);
}
};
return ( return (
<div className="confirmation-page"> <div className="confirmation-page">
@ -332,7 +350,8 @@ export default function ConfirmationPage({
{showWarningModal && ( {showWarningModal && (
<ConfirmationWarningModal <ConfirmationWarningModal
onSubmit={async () => { onSubmit={async () => {
await templatedValues.onSubmit(); const res = await templatedValues.onSubmit();
await handleSubmitResult(res);
setShowWarningModal(false); setShowWarningModal(false);
}} }}
onCancel={templatedValues.onCancel} onCancel={templatedValues.onCancel}
@ -362,6 +381,13 @@ export default function ConfirmationPage({
onCancel={templatedValues.onCancel} onCancel={templatedValues.onCancel}
submitText={templatedValues.submitText} submitText={templatedValues.submitText}
cancelText={templatedValues.cancelText} cancelText={templatedValues.cancelText}
loadingText={loadingText || templatedValues.loadingText}
loading={loading}
submitAlerts={submitAlerts.map((alert, idx) => (
<Callout key={alert.id} severity={alert.severity} isFirst={idx === 0}>
<MetaMaskTemplateRenderer sections={alert.content} />
</Callout>
))}
/> />
</div> </div>
); );

View File

@ -14,6 +14,7 @@ import {
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import fetchWithCache from '../../../../shared/lib/fetch-with-cache';
import { jsonRpcRequest } from '../../../../shared/modules/rpc.utils';
const UNRECOGNIZED_CHAIN = { const UNRECOGNIZED_CHAIN = {
id: 'UNRECOGNIZED_CHAIN', id: 'UNRECOGNIZED_CHAIN',
@ -101,6 +102,34 @@ const MISMATCHED_NETWORK_RPC = {
}, },
}; };
const MISMATCHED_NETWORK_RPC_CHAIN_ID = {
id: 'MISMATCHED_NETWORK_RPC_CHAIN_ID',
severity: SEVERITIES.DANGER,
content: {
element: 'span',
children: {
element: 'MetaMaskTranslation',
props: {
translationKey: 'mismatchedRpcChainId',
},
},
},
};
const ERROR_CONNECTING_TO_RPC = {
id: 'ERROR_CONNECTING_TO_RPC',
severity: SEVERITIES.DANGER,
content: {
element: 'span',
children: {
element: 'MetaMaskTranslation',
props: {
translationKey: 'errorWhileConnectingToRPC',
},
},
},
};
async function getAlerts(pendingApproval) { async function getAlerts(pendingApproval) {
const alerts = []; const alerts = [];
const safeChainsList = const safeChainsList =
@ -154,7 +183,7 @@ function getState(pendingApproval) {
function getValues(pendingApproval, t, actions, history) { function getValues(pendingApproval, t, actions, history) {
const originIsMetaMask = pendingApproval.origin === 'metamask'; const originIsMetaMask = pendingApproval.origin === 'metamask';
const customRpcUrl = pendingApproval.requestData.rpcUrl;
return { return {
content: [ content: [
{ {
@ -180,6 +209,7 @@ function getValues(pendingApproval, t, actions, history) {
}, },
], ],
}, },
{ {
element: 'Typography', element: 'Typography',
key: 'title', key: 'title',
@ -331,7 +361,25 @@ function getValues(pendingApproval, t, actions, history) {
], ],
cancelText: t('cancel'), cancelText: t('cancel'),
submitText: t('approveButtonText'), submitText: t('approveButtonText'),
loadingText: t('addingCustomNetwork'),
onSubmit: async () => { onSubmit: async () => {
let endpointChainId;
try {
endpointChainId = await jsonRpcRequest(customRpcUrl, 'eth_chainId');
} catch (err) {
console.error(
`Request for method 'eth_chainId on ${customRpcUrl} failed`,
);
return [ERROR_CONNECTING_TO_RPC];
}
if (pendingApproval.requestData.chainId !== endpointChainId) {
console.error(
`Chain ID returned by RPC URL ${customRpcUrl} does not match ${endpointChainId}`,
);
return [MISMATCHED_NETWORK_RPC_CHAIN_ID];
}
await actions.resolvePendingApproval( await actions.resolvePendingApproval(
pendingApproval.id, pendingApproval.id,
pendingApproval.requestData, pendingApproval.requestData,
@ -340,6 +388,7 @@ function getValues(pendingApproval, t, actions, history) {
actions.addCustomNetwork(pendingApproval.requestData); actions.addCustomNetwork(pendingApproval.requestData);
history.push(DEFAULT_ROUTE); history.push(DEFAULT_ROUTE);
} }
return [];
}, },
onCancel: () => onCancel: () =>
actions.rejectPendingApproval( actions.rejectPendingApproval(

View File

@ -33,6 +33,7 @@ const ALLOWED_TEMPLATE_KEYS = [
'onSubmit', 'onSubmit',
'networkDisplay', 'networkDisplay',
'submitText', 'submitText',
'loadingText',
]; ];
/** /**