1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +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.",
"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": {
"message": "Unknown permission: $1",
"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 ActionableMessage from '../../../ui/actionable-message/actionable-message';
export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
export const SnapInsight = ({ transaction, origin, chainId, selectedSnap }) => {
const t = useI18nContext();
const {
data: response,
@ -26,6 +26,7 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
} = useTransactionInsightSnap({
transaction,
chainId,
origin,
snapId: selectedSnap.id,
});
@ -146,6 +147,10 @@ SnapInsight.propTypes = {
* CAIP2 Chain ID
*/
chainId: PropTypes.string,
/*
* The origin of the transaction
*/
origin: PropTypes.string,
/*
* The insight snap selected
*/

View File

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

View File

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

View File

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

View File

@ -2,6 +2,8 @@ import deepFreeze from 'deep-freeze-strict';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import React from 'react';
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
import {
RestrictedMethods,
@ -17,132 +19,140 @@ import { coinTypeToProtocolName } from './util';
const UNKNOWN_PERMISSION = Symbol('unknown');
const PERMISSION_DESCRIPTIONS = deepFreeze({
[RestrictedMethods.eth_accounts]: {
label: (t) => t('permission_ethereumAccounts'),
[RestrictedMethods.eth_accounts]: (t) => ({
label: t('permission_ethereumAccounts'),
leftIcon: 'fas fa-eye',
rightIcon: null,
},
}),
///: BEGIN:ONLY_INCLUDE_IN(flask)
[RestrictedMethods.snap_confirm]: {
label: (t) => t('permission_customConfirmation'),
[RestrictedMethods.snap_confirm]: (t) => ({
label: t('permission_customConfirmation'),
leftIcon: 'fas fa-user-check',
rightIcon: null,
},
[RestrictedMethods.snap_notify]: {
}),
[RestrictedMethods.snap_notify]: (t) => ({
leftIcon: 'fas fa-bell',
label: (t) => t('permission_notifications'),
label: t('permission_notifications'),
rightIcon: null,
},
[RestrictedMethods.snap_getBip32PublicKey]: {
label: (t, _, permissionValue) => {
return permissionValue.caveats[0].value.map(({ path, curve }) =>
t('permission_viewBip32PublicKeys', [
<span className="permission-label-item" key={path.join('/')}>
{path.join('/')}
</span>,
curve,
]),
);
},
leftIcon: 'fas fa-eye',
rightIcon: null,
},
[RestrictedMethods.snap_getBip32Entropy]: {
label: (t, _, permissionValue) => {
return permissionValue.caveats[0].value.map(({ path, curve }) =>
t('permission_manageBip32Keys', [
<span className="permission-label-item" key={path.join('/')}>
{path.join('/')}
</span>,
curve,
]),
);
},
leftIcon: 'fas fa-door-open',
rightIcon: null,
},
[RestrictedMethods.snap_getBip44Entropy]: {
label: (t, _, permissionValue) => {
return permissionValue.caveats[0].value.map(({ coinType }) =>
t('permission_manageBip44Keys', [
<span className="permission-label-item" key={`coin-type-${coinType}`}>
{coinTypeToProtocolName(coinType) ||
`${coinType} (Unrecognized protocol)`}
</span>,
]),
);
},
leftIcon: 'fas fa-door-open',
rightIcon: null,
},
[RestrictedMethods.snap_getEntropy]: {
label: (t) => t('permission_getEntropy'),
}),
[RestrictedMethods.snap_getBip32PublicKey]: (t, _, permissionValue) =>
permissionValue.caveats[0].value.map(({ path, curve }) => ({
label: t('permission_viewBip32PublicKeys', [
<span className="permission-label-item" key={path.join('/')}>
{path.join('/')}
</span>,
curve,
]),
leftIcon: 'fas fa-eye',
rightIcon: null,
})),
[RestrictedMethods.snap_getBip32Entropy]: (t, _, permissionValue) =>
permissionValue.caveats[0].value.map(({ path, curve }) => ({
label: t('permission_manageBip32Keys', [
<span className="permission-label-item" key={path.join('/')}>
{path.join('/')}
</span>,
curve,
]),
leftIcon: 'fas fa-door-open',
rightIcon: null,
})),
[RestrictedMethods.snap_getBip44Entropy]: (t, _, permissionValue) =>
permissionValue.caveats[0].value.map(({ coinType }) => ({
label: t('permission_manageBip44Keys', [
<span className="permission-label-item" key={`coin-type-${coinType}`}>
{coinTypeToProtocolName(coinType) ||
`${coinType} (Unrecognized protocol)`}
</span>,
]),
leftIcon: 'fas fa-door-open',
rightIcon: null,
})),
[RestrictedMethods.snap_getEntropy]: (t) => ({
label: t('permission_getEntropy'),
leftIcon: 'fas fa-key',
rightIcon: null,
},
[RestrictedMethods.snap_manageState]: {
label: (t) => t('permission_manageState'),
}),
[RestrictedMethods.snap_manageState]: (t) => ({
label: t('permission_manageState'),
leftIcon: 'fas fa-download',
rightIcon: null,
},
[RestrictedMethods['wallet_snap_*']]: {
label: (t, permissionName) => {
const snapId = permissionName.split('_').slice(-1);
return t('permission_accessSnap', [snapId]);
},
}),
[RestrictedMethods['wallet_snap_*']]: (t, permissionName) => ({
label: t('permission_accessSnap', [permissionName.split('_').slice(-1)]),
leftIcon: 'fas fa-bolt',
rightIcon: null,
},
[EndowmentPermissions['endowment:network-access']]: {
label: (t) => t('permission_accessNetwork'),
}),
[EndowmentPermissions['endowment:network-access']]: (t) => ({
label: t('permission_accessNetwork'),
leftIcon: 'fas fa-wifi',
rightIcon: null,
},
[EndowmentPermissions['endowment:long-running']]: {
label: (t) => t('permission_longRunning'),
}),
[EndowmentPermissions['endowment:long-running']]: (t) => ({
label: t('permission_longRunning'),
leftIcon: 'fas fa-infinity',
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']]: {
label: (t) => t('permission_transactionInsight'),
leftIcon: 'fas fa-info',
rightIcon: null,
},
[EndowmentPermissions['endowment:cronjob']]: {
label: (t) => t('permission_cronjob'),
[EndowmentPermissions['endowment:cronjob']]: (t) => ({
label: t('permission_cronjob'),
leftIcon: 'fas fa-clock',
rightIcon: null,
},
[EndowmentPermissions['endowment:ethereum-provider']]: {
label: (t) => t('permission_ethereumProvider'),
}),
[EndowmentPermissions['endowment:ethereum-provider']]: (t) => ({
label: t('permission_ethereumProvider'),
leftIcon: 'fab fa-ethereum',
rightIcon: null,
},
[EndowmentPermissions['endowment:rpc']]: {
label: (t, _, permissionValue) => {
const { snaps, dapps } = getRpcCaveatOrigins(permissionValue);
}),
[EndowmentPermissions['endowment:rpc']]: (t, _, permissionValue) => {
const { snaps, dapps } = getRpcCaveatOrigins(permissionValue);
const messages = [];
if (snaps) {
messages.push(t('permission_rpc', [t('otherSnaps')]));
}
const labels = [];
if (snaps) {
labels.push(t('permission_rpc', [t('otherSnaps')]));
}
if (dapps) {
messages.push(t('permission_rpc', [t('websites')]));
}
if (dapps) {
labels.push(t('permission_rpc', [t('websites')]));
}
return messages;
},
leftIcon: 'fas fa-plug',
rightIcon: null,
return labels.map((label) => ({
label,
leftIcon: 'fas fa-plug',
rightIcon: null,
}));
},
///: END:ONLY_INCLUDE_IN
[UNKNOWN_PERMISSION]: {
label: (t, permissionName) =>
t('permission_unknown', [permissionName ?? 'undefined']),
[UNKNOWN_PERMISSION]: (t, permissionName) => ({
label: t('permission_unknown', [permissionName ?? 'undefined']),
leftIcon: 'fas fa-times-circle',
rightIcon: null,
},
}),
});
/**
@ -156,7 +166,7 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
* @param {Function} t - The translation function
* @param {string} permissionName - The name of the permission to request
* @param {object} permissionValue - The value of the permission to request
* @returns {(permissionName:string) => PermissionLabelObject}
* @returns {PermissionLabelObject[]}
*/
export const getPermissionDescription = (
t,
@ -176,5 +186,9 @@ export const getPermissionDescription = (
}
///: 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 { useSelector } from 'react-redux';
import { getTransactionOriginCaveat } from '@metamask/snaps-controllers';
import { handleSnapRequest } from '../../store/actions';
import { getPermissionSubjects } from '../../selectors';
const INSIGHT_PERMISSION = 'endowment:transaction-insight';
export function useTransactionInsightSnap({ transaction, chainId, snapId }) {
export function useTransactionInsightSnap({
transaction,
chainId,
origin,
snapId,
}) {
const subjects = useSelector(getPermissionSubjects);
if (!subjects[snapId]?.permissions[INSIGHT_PERMISSION]) {
const permission = subjects[snapId]?.permissions[INSIGHT_PERMISSION];
if (!permission) {
throw new Error(
'This snap does not have the transaction insight endowment.',
);
}
const hasTransactionOriginCaveat = getTransactionOriginCaveat(permission);
const transactionOrigin = hasTransactionOriginCaveat ? origin : null;
const [loading, setLoading] = useState(true);
const [data, setData] = useState(undefined);
const [error, setError] = useState(undefined);
@ -30,7 +40,7 @@ export function useTransactionInsightSnap({ transaction, chainId, snapId }) {
request: {
jsonrpc: '2.0',
method: ' ',
params: { transaction, chainId },
params: { transaction, chainId, transactionOrigin },
},
});
setData(d);
@ -43,7 +53,7 @@ export function useTransactionInsightSnap({ transaction, chainId, snapId }) {
if (transaction) {
fetchInsight();
}
}, [snapId, transaction, chainId]);
}, [snapId, transaction, chainId, transactionOrigin]);
return { data, error, loading };
}

View File

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

View File

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