1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

[FLASK] Expose transaction origin to transaction insight snaps (#16671)

* Expose transaction origin to transaction insight snaps

* Support multiple icons for each label for a permission

* Add transaction insight origin permission

* Fix fencing

* Fix import and permission crash

* Use function properly for connected accounts
This commit is contained in:
Frederik Bolding 2022-12-01 14:38:56 +01:00 committed by GitHub
parent 6416936eec
commit 025ee2cb48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 188 additions and 143 deletions

View File

@ -2763,6 +2763,10 @@
"message": "Fetch and display transaction insights.", "message": "Fetch and display transaction insights.",
"description": "The description for the `endowment:transaction-insight` permission" "description": "The description for the `endowment:transaction-insight` permission"
}, },
"permission_transactionInsightOrigin": {
"message": "See the origins of websites that suggest transactions",
"description": "The description for the `transactionOrigin` caveat, to be used with the `endowment:transaction-insight` permission"
},
"permission_unknown": { "permission_unknown": {
"message": "Unknown permission: $1", "message": "Unknown permission: $1",
"description": "$1 is the name of a requested permission that is not recognized." "description": "$1 is the name of a requested permission that is not recognized."

View File

@ -17,7 +17,7 @@ import SnapContentFooter from '../../flask/snap-content-footer/snap-content-foot
import Box from '../../../ui/box/box'; import Box from '../../../ui/box/box';
import ActionableMessage from '../../../ui/actionable-message/actionable-message'; import ActionableMessage from '../../../ui/actionable-message/actionable-message';
export const SnapInsight = ({ transaction, chainId, selectedSnap }) => { export const SnapInsight = ({ transaction, origin, chainId, selectedSnap }) => {
const t = useI18nContext(); const t = useI18nContext();
const { const {
data: response, data: response,
@ -26,6 +26,7 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
} = useTransactionInsightSnap({ } = useTransactionInsightSnap({
transaction, transaction,
chainId, chainId,
origin,
snapId: selectedSnap.id, snapId: selectedSnap.id,
}); });
@ -146,6 +147,10 @@ SnapInsight.propTypes = {
* CAIP2 Chain ID * CAIP2 Chain ID
*/ */
chainId: PropTypes.string, chainId: PropTypes.string,
/*
* The origin of the transaction
*/
origin: PropTypes.string,
/* /*
* The insight snap selected * The insight snap selected
*/ */

View File

@ -1,6 +1,7 @@
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { flatten } from 'lodash';
import CheckBox from '../../ui/check-box'; import CheckBox from '../../ui/check-box';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { getPermissionDescription } from '../../../helpers/utils/permission'; import { getPermissionDescription } from '../../../helpers/utils/permission';
@ -17,6 +18,12 @@ const ConnectedAccountsPermissions = ({ permissions }) => {
return null; return null;
} }
const permissionLabels = flatten(
permissions.map(({ key, value }) =>
getPermissionDescription(t, key, value),
),
);
return ( return (
<div className="connected-accounts-permissions"> <div className="connected-accounts-permissions">
<p <p
@ -43,20 +50,18 @@ const ConnectedAccountsPermissions = ({ permissions }) => {
> >
<p>{t('authorizedPermissions')}:</p> <p>{t('authorizedPermissions')}:</p>
<ul className="connected-accounts-permissions__list"> <ul className="connected-accounts-permissions__list">
{permissions.map(({ key: permissionName }) => ( {permissionLabels.map(({ label }, idx) => (
<li <li
key={permissionName} key={`connected-permission-${idx}`}
className="connected-accounts-permissions__list-item" className="connected-accounts-permissions__list-item"
> >
<CheckBox <CheckBox
checked checked
disabled disabled
id={permissionName} id={`connected-permission-${idx}`}
className="connected-accounts-permissions__checkbox" className="connected-accounts-permissions__checkbox"
/> />
<label htmlFor={permissionName}> <label htmlFor={`connected-permission-${idx}`}>{label}</label>
{getPermissionDescription(t, permissionName).label}
</label>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -16,14 +16,14 @@ export default function UpdateSnapPermissionList({
const ApprovedPermissions = () => { const ApprovedPermissions = () => {
return Object.entries(approvedPermissions).map( return Object.entries(approvedPermissions).map(
([permissionName, permissionValue]) => { ([permissionName, permissionValue]) => {
const { label, rightIcon } = getPermissionDescription( const permissions = getPermissionDescription(
t, t,
permissionName, permissionName,
permissionValue, permissionValue,
); );
const { date } = permissionValue; const { date } = permissionValue;
const formattedDate = formatDate(date, 'yyyy-MM-dd'); const formattedDate = formatDate(date, 'yyyy-MM-dd');
return ( return permissions.map(({ label, rightIcon }) => (
<div className="approved-permission" key={permissionName}> <div className="approved-permission" key={permissionName}>
<i className="fas fa-check" /> <i className="fas fa-check" />
<div className="permission-description"> <div className="permission-description">
@ -38,7 +38,7 @@ export default function UpdateSnapPermissionList({
</div> </div>
{rightIcon && <i className={rightIcon} />} {rightIcon && <i className={rightIcon} />}
</div> </div>
); ));
}, },
); );
}; };
@ -46,12 +46,12 @@ export default function UpdateSnapPermissionList({
const RevokedPermissions = () => { const RevokedPermissions = () => {
return Object.entries(revokedPermissions).map( return Object.entries(revokedPermissions).map(
([permissionName, permissionValue]) => { ([permissionName, permissionValue]) => {
const { label, rightIcon } = getPermissionDescription( const permissions = getPermissionDescription(
t, t,
permissionName, permissionName,
permissionValue, permissionValue,
); );
return ( return permissions.map(({ label, rightIcon }) => (
<div className="revoked-permission" key={permissionName}> <div className="revoked-permission" key={permissionName}>
<i className="fas fa-x" /> <i className="fas fa-x" />
<div className="permission-description"> <div className="permission-description">
@ -66,7 +66,7 @@ export default function UpdateSnapPermissionList({
</div> </div>
{rightIcon && <i className={rightIcon} />} {rightIcon && <i className={rightIcon} />}
</div> </div>
); ));
}, },
); );
}; };
@ -74,12 +74,12 @@ export default function UpdateSnapPermissionList({
const NewPermissions = () => { const NewPermissions = () => {
return Object.entries(newPermissions).map( return Object.entries(newPermissions).map(
([permissionName, permissionValue]) => { ([permissionName, permissionValue]) => {
const { label, rightIcon } = getPermissionDescription( const permissions = getPermissionDescription(
t, t,
permissionName, permissionName,
permissionValue, permissionValue,
); );
return ( return permissions.map(({ label, rightIcon }) => (
<div className="new-permission" key={permissionName}> <div className="new-permission" key={permissionName}>
<i className="fas fa-arrow-right" /> <i className="fas fa-arrow-right" />
<div className="permission-description"> <div className="permission-description">
@ -94,7 +94,7 @@ export default function UpdateSnapPermissionList({
</div> </div>
{rightIcon && <i className={rightIcon} />} {rightIcon && <i className={rightIcon} />}
</div> </div>
); ));
}, },
); );
}; };

View File

@ -12,29 +12,19 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
* @returns {JSX.Element[]} An array of permission description nodes. * @returns {JSX.Element[]} An array of permission description nodes.
*/ */
function getDescriptionNodes(t, permissionName, permissionValue) { function getDescriptionNodes(t, permissionName, permissionValue) {
const { label, leftIcon, rightIcon } = getPermissionDescription( const permissions = getPermissionDescription(
t, t,
permissionName, permissionName,
permissionValue, permissionValue,
); );
if (Array.isArray(label)) { return permissions.map(({ label, leftIcon, rightIcon }, index) => (
return label.map((labelValue, index) => ( <div className="permission" key={`${permissionName}-${index}`}>
<div className="permission" key={`${permissionName}-${index}`}>
<i className={leftIcon} />
{labelValue}
{rightIcon && <i className={rightIcon} />}
</div>
));
}
return [
<div className="permission" key={permissionName}>
<i className={leftIcon} /> <i className={leftIcon} />
{label} {label}
{rightIcon && <i className={rightIcon} />} {rightIcon && <i className={rightIcon} />}
</div>, </div>
]; ));
} }
export default function PermissionsConnectPermissionList({ permissions }) { export default function PermissionsConnectPermissionList({ permissions }) {

View File

@ -2,6 +2,8 @@ import deepFreeze from 'deep-freeze-strict';
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
import React from 'react'; import React from 'react';
import { getRpcCaveatOrigins } from '@metamask/snaps-controllers/dist/snaps/endowments/rpc'; import { getRpcCaveatOrigins } from '@metamask/snaps-controllers/dist/snaps/endowments/rpc';
import { SnapCaveatType } from '@metamask/snaps-utils';
import { isNonEmptyArray } from '@metamask/controller-utils';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
import { import {
RestrictedMethods, RestrictedMethods,
@ -17,132 +19,140 @@ import { coinTypeToProtocolName } from './util';
const UNKNOWN_PERMISSION = Symbol('unknown'); const UNKNOWN_PERMISSION = Symbol('unknown');
const PERMISSION_DESCRIPTIONS = deepFreeze({ const PERMISSION_DESCRIPTIONS = deepFreeze({
[RestrictedMethods.eth_accounts]: { [RestrictedMethods.eth_accounts]: (t) => ({
label: (t) => t('permission_ethereumAccounts'), label: t('permission_ethereumAccounts'),
leftIcon: 'fas fa-eye', leftIcon: 'fas fa-eye',
rightIcon: null, rightIcon: null,
}, }),
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
[RestrictedMethods.snap_confirm]: { [RestrictedMethods.snap_confirm]: (t) => ({
label: (t) => t('permission_customConfirmation'), label: t('permission_customConfirmation'),
leftIcon: 'fas fa-user-check', leftIcon: 'fas fa-user-check',
rightIcon: null, rightIcon: null,
}, }),
[RestrictedMethods.snap_notify]: { [RestrictedMethods.snap_notify]: (t) => ({
leftIcon: 'fas fa-bell', leftIcon: 'fas fa-bell',
label: (t) => t('permission_notifications'), label: t('permission_notifications'),
rightIcon: null, rightIcon: null,
}, }),
[RestrictedMethods.snap_getBip32PublicKey]: { [RestrictedMethods.snap_getBip32PublicKey]: (t, _, permissionValue) =>
label: (t, _, permissionValue) => { permissionValue.caveats[0].value.map(({ path, curve }) => ({
return permissionValue.caveats[0].value.map(({ path, curve }) => label: t('permission_viewBip32PublicKeys', [
t('permission_viewBip32PublicKeys', [ <span className="permission-label-item" key={path.join('/')}>
<span className="permission-label-item" key={path.join('/')}> {path.join('/')}
{path.join('/')} </span>,
</span>, curve,
curve, ]),
]), leftIcon: 'fas fa-eye',
); rightIcon: null,
}, })),
leftIcon: 'fas fa-eye', [RestrictedMethods.snap_getBip32Entropy]: (t, _, permissionValue) =>
rightIcon: null, permissionValue.caveats[0].value.map(({ path, curve }) => ({
}, label: t('permission_manageBip32Keys', [
[RestrictedMethods.snap_getBip32Entropy]: { <span className="permission-label-item" key={path.join('/')}>
label: (t, _, permissionValue) => { {path.join('/')}
return permissionValue.caveats[0].value.map(({ path, curve }) => </span>,
t('permission_manageBip32Keys', [ curve,
<span className="permission-label-item" key={path.join('/')}> ]),
{path.join('/')} leftIcon: 'fas fa-door-open',
</span>, rightIcon: null,
curve, })),
]), [RestrictedMethods.snap_getBip44Entropy]: (t, _, permissionValue) =>
); permissionValue.caveats[0].value.map(({ coinType }) => ({
}, label: t('permission_manageBip44Keys', [
leftIcon: 'fas fa-door-open', <span className="permission-label-item" key={`coin-type-${coinType}`}>
rightIcon: null, {coinTypeToProtocolName(coinType) ||
}, `${coinType} (Unrecognized protocol)`}
[RestrictedMethods.snap_getBip44Entropy]: { </span>,
label: (t, _, permissionValue) => { ]),
return permissionValue.caveats[0].value.map(({ coinType }) => leftIcon: 'fas fa-door-open',
t('permission_manageBip44Keys', [ rightIcon: null,
<span className="permission-label-item" key={`coin-type-${coinType}`}> })),
{coinTypeToProtocolName(coinType) || [RestrictedMethods.snap_getEntropy]: (t) => ({
`${coinType} (Unrecognized protocol)`} label: t('permission_getEntropy'),
</span>,
]),
);
},
leftIcon: 'fas fa-door-open',
rightIcon: null,
},
[RestrictedMethods.snap_getEntropy]: {
label: (t) => t('permission_getEntropy'),
leftIcon: 'fas fa-key', leftIcon: 'fas fa-key',
rightIcon: null, rightIcon: null,
}, }),
[RestrictedMethods.snap_manageState]: { [RestrictedMethods.snap_manageState]: (t) => ({
label: (t) => t('permission_manageState'), label: t('permission_manageState'),
leftIcon: 'fas fa-download', leftIcon: 'fas fa-download',
rightIcon: null, rightIcon: null,
}, }),
[RestrictedMethods['wallet_snap_*']]: { [RestrictedMethods['wallet_snap_*']]: (t, permissionName) => ({
label: (t, permissionName) => { label: t('permission_accessSnap', [permissionName.split('_').slice(-1)]),
const snapId = permissionName.split('_').slice(-1);
return t('permission_accessSnap', [snapId]);
},
leftIcon: 'fas fa-bolt', leftIcon: 'fas fa-bolt',
rightIcon: null, rightIcon: null,
}, }),
[EndowmentPermissions['endowment:network-access']]: { [EndowmentPermissions['endowment:network-access']]: (t) => ({
label: (t) => t('permission_accessNetwork'), label: t('permission_accessNetwork'),
leftIcon: 'fas fa-wifi', leftIcon: 'fas fa-wifi',
rightIcon: null, rightIcon: null,
}, }),
[EndowmentPermissions['endowment:long-running']]: { [EndowmentPermissions['endowment:long-running']]: (t) => ({
label: (t) => t('permission_longRunning'), label: t('permission_longRunning'),
leftIcon: 'fas fa-infinity', leftIcon: 'fas fa-infinity',
rightIcon: null, rightIcon: null,
}),
[EndowmentPermissions['endowment:transaction-insight']]: (
t,
_,
permissionValue,
) => {
const result = [
{
label: t('permission_transactionInsight'),
leftIcon: 'fas fa-info',
rightIcon: null,
},
];
if (
isNonEmptyArray(permissionValue.caveats) &&
permissionValue.caveats[0].type === SnapCaveatType.TransactionOrigin &&
permissionValue.caveats[0].value
) {
result.push({
label: t('permission_transactionInsightOrigin'),
leftIcon: 'fas fa-compass',
rightIcon: null,
});
}
return result;
}, },
[EndowmentPermissions['endowment:transaction-insight']]: { [EndowmentPermissions['endowment:cronjob']]: (t) => ({
label: (t) => t('permission_transactionInsight'), label: t('permission_cronjob'),
leftIcon: 'fas fa-info',
rightIcon: null,
},
[EndowmentPermissions['endowment:cronjob']]: {
label: (t) => t('permission_cronjob'),
leftIcon: 'fas fa-clock', leftIcon: 'fas fa-clock',
rightIcon: null, rightIcon: null,
}, }),
[EndowmentPermissions['endowment:ethereum-provider']]: { [EndowmentPermissions['endowment:ethereum-provider']]: (t) => ({
label: (t) => t('permission_ethereumProvider'), label: t('permission_ethereumProvider'),
leftIcon: 'fab fa-ethereum', leftIcon: 'fab fa-ethereum',
rightIcon: null, rightIcon: null,
}, }),
[EndowmentPermissions['endowment:rpc']]: { [EndowmentPermissions['endowment:rpc']]: (t, _, permissionValue) => {
label: (t, _, permissionValue) => { const { snaps, dapps } = getRpcCaveatOrigins(permissionValue);
const { snaps, dapps } = getRpcCaveatOrigins(permissionValue);
const messages = []; const labels = [];
if (snaps) { if (snaps) {
messages.push(t('permission_rpc', [t('otherSnaps')])); labels.push(t('permission_rpc', [t('otherSnaps')]));
} }
if (dapps) { if (dapps) {
messages.push(t('permission_rpc', [t('websites')])); labels.push(t('permission_rpc', [t('websites')]));
} }
return messages; return labels.map((label) => ({
}, label,
leftIcon: 'fas fa-plug', leftIcon: 'fas fa-plug',
rightIcon: null, rightIcon: null,
}));
}, },
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
[UNKNOWN_PERMISSION]: { [UNKNOWN_PERMISSION]: (t, permissionName) => ({
label: (t, permissionName) => label: t('permission_unknown', [permissionName ?? 'undefined']),
t('permission_unknown', [permissionName ?? 'undefined']),
leftIcon: 'fas fa-times-circle', leftIcon: 'fas fa-times-circle',
rightIcon: null, rightIcon: null,
}, }),
}); });
/** /**
@ -156,7 +166,7 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
* @param {Function} t - The translation function * @param {Function} t - The translation function
* @param {string} permissionName - The name of the permission to request * @param {string} permissionName - The name of the permission to request
* @param {object} permissionValue - The value of the permission to request * @param {object} permissionValue - The value of the permission to request
* @returns {(permissionName:string) => PermissionLabelObject} * @returns {PermissionLabelObject[]}
*/ */
export const getPermissionDescription = ( export const getPermissionDescription = (
t, t,
@ -176,5 +186,9 @@ export const getPermissionDescription = (
} }
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
return { ...value, label: value.label(t, permissionName, permissionValue) }; const result = value(t, permissionName, permissionValue);
if (!Array.isArray(result)) {
return [result];
}
return result;
}; };

View File

@ -1,18 +1,28 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getTransactionOriginCaveat } from '@metamask/snaps-controllers';
import { handleSnapRequest } from '../../store/actions'; import { handleSnapRequest } from '../../store/actions';
import { getPermissionSubjects } from '../../selectors'; import { getPermissionSubjects } from '../../selectors';
const INSIGHT_PERMISSION = 'endowment:transaction-insight'; const INSIGHT_PERMISSION = 'endowment:transaction-insight';
export function useTransactionInsightSnap({ transaction, chainId, snapId }) { export function useTransactionInsightSnap({
transaction,
chainId,
origin,
snapId,
}) {
const subjects = useSelector(getPermissionSubjects); const subjects = useSelector(getPermissionSubjects);
if (!subjects[snapId]?.permissions[INSIGHT_PERMISSION]) { const permission = subjects[snapId]?.permissions[INSIGHT_PERMISSION];
if (!permission) {
throw new Error( throw new Error(
'This snap does not have the transaction insight endowment.', 'This snap does not have the transaction insight endowment.',
); );
} }
const hasTransactionOriginCaveat = getTransactionOriginCaveat(permission);
const transactionOrigin = hasTransactionOriginCaveat ? origin : null;
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [data, setData] = useState(undefined); const [data, setData] = useState(undefined);
const [error, setError] = useState(undefined); const [error, setError] = useState(undefined);
@ -30,7 +40,7 @@ export function useTransactionInsightSnap({ transaction, chainId, snapId }) {
request: { request: {
jsonrpc: '2.0', jsonrpc: '2.0',
method: ' ', method: ' ',
params: { transaction, chainId }, params: { transaction, chainId, transactionOrigin },
}, },
}); });
setData(d); setData(d);
@ -43,7 +53,7 @@ export function useTransactionInsightSnap({ transaction, chainId, snapId }) {
if (transaction) { if (transaction) {
fetchInsight(); fetchInsight();
} }
}, [snapId, transaction, chainId]); }, [snapId, transaction, chainId, transactionOrigin]);
return { data, error, loading }; return { data, error, loading };
} }

View File

@ -755,7 +755,7 @@ export default class ConfirmTransactionBase extends Component {
renderInsight() { renderInsight() {
const { txData, insightSnaps } = this.props; const { txData, insightSnaps } = this.props;
const { selectedInsightSnapId } = this.state; const { selectedInsightSnapId } = this.state;
const { txParams, chainId } = txData; const { txParams, chainId, origin } = txData;
const selectedSnap = insightSnaps.find( const selectedSnap = insightSnaps.find(
({ id }) => id === selectedInsightSnapId, ({ id }) => id === selectedInsightSnapId,
@ -791,6 +791,7 @@ export default class ConfirmTransactionBase extends Component {
> >
<SnapInsight <SnapInsight
transaction={txParams} transaction={txParams}
origin={origin}
chainId={caip2ChainId} chainId={caip2ChainId}
selectedSnap={selectedSnap} selectedSnap={selectedSnap}
/> />
@ -802,6 +803,7 @@ export default class ConfirmTransactionBase extends Component {
> >
<SnapInsight <SnapInsight
transaction={txParams} transaction={txParams}
origin={origin}
chainId={caip2ChainId} chainId={caip2ChainId}
selectedSnap={selectedSnap} selectedSnap={selectedSnap}
/> />

View File

@ -242,13 +242,13 @@ export function getPermissionsForActiveTab(state) {
const { activeTab, metamask } = state; const { activeTab, metamask } = state;
const { subjects = {} } = metamask; const { subjects = {} } = metamask;
return Object.keys(subjects[activeTab.origin]?.permissions || {}).map( const permissions = subjects[activeTab.origin]?.permissions ?? {};
(parentCapability) => { return Object.keys(permissions).map((parentCapability) => {
return { return {
key: parentCapability, key: parentCapability,
}; value: permissions[parentCapability],
}, };
); });
} }
export function activeTabHasPermissions(state) { export function activeTabHasPermissions(state) {

View File

@ -425,10 +425,25 @@ describe('selectors', () => {
}, },
}; };
it('should return a list of permissions strings', () => { it('should return a list of permissions keys and values', () => {
expect(getPermissionsForActiveTab(mockState)).toStrictEqual([ expect(getPermissionsForActiveTab(mockState)).toStrictEqual([
{ {
key: 'eth_accounts', key: 'eth_accounts',
value: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
],
},
],
date: 1586359844177,
id: '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
invoker: 'https://remix.ethereum.org',
parentCapability: 'eth_accounts',
},
}, },
]); ]);
}); });