diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5587b3d85526..a3c3e7c9e479 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2395,6 +2395,7 @@ export default class MetamaskController extends EventEmitter { assetsContractController, backup, approvalController, + phishingController, } = this; return { @@ -2822,6 +2823,11 @@ export default class MetamaskController extends EventEmitter { dismissNotifications: this.dismissNotifications.bind(this), markNotificationsAsRead: this.markNotificationsAsRead.bind(this), updateCaveat: this.updateCaveat.bind(this), + getPhishingResult: async (website) => { + await phishingController.maybeUpdateState(); + + return phishingController.test(website); + }, ///: END:ONLY_INCLUDE_IN ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) updateSnapRegistry: this.preferencesController.updateSnapRegistry.bind( diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 6c2064108972..8431ea490caa 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1407,7 +1407,40 @@ "requestState": { "test": "value" } } }, - "pendingApprovalCount": 1 + "pendingApprovalCount": 1, + "database": { + "verifiedSnaps": { + "npm:@metamask/test-snap-bip44": { + "id": "npm:@metamask/test-snap-bip44", + "metadata": { + "name": "BIP-44", + "author": { + "name": "Consensys", + "website": "https://consensys.io/" + }, + "website": "https://snaps.consensys.io/", + "summary": "An example Snap that signs messages using BLS.", + "description": "An example Snap that signs messages using BLS.", + "audits": [ + { + "auditor": "Consensys Diligence", + "report": "https://consensys.io/diligence/audits/" + } + ], + "category": "interoperability", + "support": { + "contact": "https://github.com/MetaMask" + }, + "sourceCode": "https://github.com/MetaMask/test-snaps" + }, + "versions": { + "5.1.2": { + "checksum": "L1k+dT9Q+y3KfIqzaH09MpDZVPS9ZowEh9w01ZMTWMU=" + } + } + } + } + } }, "send": { "amountMode": "INPUT", diff --git a/ui/components/app/snaps/snap-authorship-expanded/snap-authorship-expanded.js b/ui/components/app/snaps/snap-authorship-expanded/snap-authorship-expanded.js index 0c3eb1f0bfac..8b7b6384ff71 100644 --- a/ui/components/app/snaps/snap-authorship-expanded/snap-authorship-expanded.js +++ b/ui/components/app/snaps/snap-authorship-expanded/snap-authorship-expanded.js @@ -1,7 +1,7 @@ import { getSnapPrefix } from '@metamask/snaps-utils'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { AlignItems, @@ -15,6 +15,7 @@ import { FlexDirection, FontWeight, JustifyContent, + OverflowWrap, TextColor, TextVariant, } from '../../../../helpers/constants/design-system'; @@ -25,8 +26,15 @@ import { } from '../../../../helpers/utils/util'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; -import { getTargetSubjectMetadata } from '../../../../selectors'; -import { disableSnap, enableSnap } from '../../../../store/actions'; +import { + getSnapRegistryData, + getTargetSubjectMetadata, +} from '../../../../selectors'; +import { + disableSnap, + enableSnap, + getPhishingResult, +} from '../../../../store/actions'; import { Box, ButtonLink, Text } from '../../../component-library'; import ToggleButton from '../../../ui/toggle-button'; import Tooltip from '../../../ui/tooltip/tooltip'; @@ -53,6 +61,24 @@ const SnapAuthorshipExpanded = ({ snapId, className, snap }) => { const subjectMetadata = useSelector((state) => getTargetSubjectMetadata(state, snapId), ); + const snapRegistryData = useSelector((state) => + getSnapRegistryData(state, snapId), + ); + const { website = undefined } = snapRegistryData?.metadata ?? {}; + const [safeWebsite, setSafeWebsite] = useState(null); + + useEffect(() => { + const performPhishingCheck = async () => { + const phishingResult = await getPhishingResult(website); + + if (!phishingResult.result) { + setSafeWebsite(website); + } + }; + if (website) { + performPhishingCheck(); + } + }, [website]); const friendlyName = snapId && getSnapName(snapId, subjectMetadata); @@ -134,6 +160,33 @@ const SnapAuthorshipExpanded = ({ snapId, className, snap }) => { + {safeWebsite && ( + + + {t('snapDetailWebsite')} + + + + {safeWebsite} + + + + )} {installOrigin && installInfo && ( { flexDirection={FlexDirection.Column} alignItems={AlignItems.flexEnd} > - - {installOrigin.host} - + {installOrigin.host} {t('installedOn', [ formatDate(installInfo.date, 'dd MMM yyyy'), diff --git a/ui/pages/settings/snaps/view-snap/index.scss b/ui/pages/settings/snaps/view-snap/index.scss index caec7ad5a869..c074c154c1fa 100644 --- a/ui/pages/settings/snaps/view-snap/index.scss +++ b/ui/pages/settings/snaps/view-snap/index.scss @@ -69,7 +69,6 @@ &__subject-name { font-size: 14px; - color: var(--color-primary-default); } } } diff --git a/ui/pages/settings/snaps/view-snap/view-snap.test.js b/ui/pages/settings/snaps/view-snap/view-snap.test.js index a303613dfbf9..47ff4447cf81 100644 --- a/ui/pages/settings/snaps/view-snap/view-snap.test.js +++ b/ui/pages/settings/snaps/view-snap/view-snap.test.js @@ -1,6 +1,7 @@ import * as React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { waitFor, screen } from '@testing-library/react'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import mockState from '../../../../../test/data/mock-state.json'; import ViewSnap from './view-snap'; @@ -12,6 +13,11 @@ jest.mock('../../../../store/actions.ts', () => { removeSnap: jest.fn(), removePermissionsFor: jest.fn(), updateCaveat: jest.fn(), + getPhishingResult: jest.fn().mockImplementation(() => { + return { + result: false, + }; + }), }; }); @@ -45,6 +51,12 @@ describe('ViewSnap', () => { expect( getByText('An example Snap that signs messages using BLS.'), ).toBeDefined(); + // Snap website + await waitFor(() => { + const websiteElement = screen.queryByText('https://snaps.consensys.io/'); + expect(websiteElement).toBeDefined(); + expect(getByText('https://snaps.consensys.io/')).toBeDefined(); + }); // Snap version info expect(getByText('5.1.2')).toBeDefined(); // Enable Snap diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 1a0bb0ad229a..041fc765b182 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -723,6 +723,18 @@ export function getTargetSubjectMetadata(state, origin) { return metadata; } +/** + * Retrieve registry data for requested Snap. + * + * @param state - Redux state object. + * @param snapId - ID of a Snap. + * @returns Object containing metadata stored in Snaps registry for requested Snap. + */ +export function getSnapRegistryData(state, snapId) { + const snapsRegistryData = state.metamask.database.verifiedSnaps; + return snapsRegistryData ? snapsRegistryData[snapId] : null; +} + export function getRpcPrefsForCurrentProvider(state) { const { rpcPrefs } = getProviderConfig(state); return rpcPrefs || {}; diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 9ffc3b375320..a057667a1b1a 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -780,4 +780,22 @@ describe('Selectors', () => { isInfuraBlocked = selectors.getInfuraBlocked(modifiedMockState); expect(isInfuraBlocked).toBe(true); }); + + it('#getSnapRegistryData', () => { + const mockSnapId = 'npm:@metamask/test-snap-bip44'; + expect(selectors.getSnapRegistryData(mockState, mockSnapId)).toStrictEqual( + expect.objectContaining({ + id: mockSnapId, + versions: { + '5.1.2': { + checksum: 'L1k+dT9Q+y3KfIqzaH09MpDZVPS9ZowEh9w01ZMTWMU=', + }, + }, + metadata: expect.objectContaining({ + website: 'https://snaps.consensys.io/', + name: 'BIP-44', + }), + }), + ); + }); }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 4346513ee431..3b13d774a124 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -1136,6 +1136,10 @@ export function enableSnap( await forceUpdateMetamaskState(dispatch); }; } + +export async function getPhishingResult(website: string) { + return await submitRequestToBackground('getPhishingResult', [website]); +} ///: END:ONLY_INCLUDE_IN // TODO: Clean this up.