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

[FLASK] Snaps Insight (#15814)

Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>
Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com>
This commit is contained in:
Guillaume Roux 2022-09-20 19:00:07 +02:00 committed by GitHub
parent 8b5630025b
commit c9dc59ea2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 541 additions and 8 deletions

View File

@ -2684,6 +2684,10 @@
"message": "Show notifications.", "message": "Show notifications.",
"description": "The description for the `snap_notify` permission" "description": "The description for the `snap_notify` permission"
}, },
"permission_transactionInsight": {
"message": "Fetch and display transaction insights.",
"description": "The description for 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."
@ -3239,6 +3243,10 @@
"message": "Added on $1 from $2", "message": "Added on $1 from $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap." "description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
}, },
"snapContent": {
"message": "This content is coming from $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
},
"snapError": { "snapError": {
"message": "Snap Error: '$1'. Error Code: '$2'", "message": "Snap Error: '$1'. Error Code: '$2'",
"description": "This is shown when a snap encounters an error. $1 is the error message from the snap, and $2 is the error code." "description": "This is shown when a snap encounters an error. $1 is the error message from the snap, and $2 is the error code."
@ -3269,6 +3277,12 @@
"snaps": { "snaps": {
"message": "Snaps" "message": "Snaps"
}, },
"snapsInsightLoading": {
"message": "Loading transaction insight..."
},
"snapsNoInsight": {
"message": "The snap didn't return any insight"
},
"snapsSettingsDescription": { "snapsSettingsDescription": {
"message": "Manage your Snaps" "message": "Manage your Snaps"
}, },

View File

@ -1867,6 +1867,10 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger, this.controllerMessenger,
'SnapController:remove', 'SnapController:remove',
), ),
handleSnapRequest: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:handleRequest',
),
dismissNotifications: this.dismissNotifications.bind(this), dismissNotifications: this.dismissNotifications.bind(this),
markNotificationsAsRead: this.markNotificationsAsRead.bind(this), markNotificationsAsRead: this.markNotificationsAsRead.bind(this),
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN

View File

@ -32,6 +32,7 @@
@import 'edit-gas-fee-popover/edit-gas-tooltip/index'; @import 'edit-gas-fee-popover/edit-gas-tooltip/index';
@import 'flask/experimental-area/index'; @import 'flask/experimental-area/index';
@import 'flask/snaps-authorship-pill/index'; @import 'flask/snaps-authorship-pill/index';
@import 'flask/snap-content-footer/index';
@import 'flask/snap-install-warning/index'; @import 'flask/snap-install-warning/index';
@import 'flask/snap-remove-warning/index'; @import 'flask/snap-remove-warning/index';
@import 'flask/snap-settings-card/index'; @import 'flask/snap-settings-card/index';

View File

@ -23,6 +23,9 @@ export default class ConfirmPageContainerContent extends Component {
dataComponent: PropTypes.node, dataComponent: PropTypes.node,
dataHexComponent: PropTypes.node, dataHexComponent: PropTypes.node,
detailsComponent: PropTypes.node, detailsComponent: PropTypes.node,
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent: PropTypes.node,
///: END:ONLY_INCLUDE_IN
errorKey: PropTypes.string, errorKey: PropTypes.string,
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
hideSubtitle: PropTypes.bool, hideSubtitle: PropTypes.bool,
@ -59,15 +62,37 @@ export default class ConfirmPageContainerContent extends Component {
renderContent() { renderContent() {
const { detailsComponent, dataComponent } = this.props; const { detailsComponent, dataComponent } = this.props;
///: BEGIN:ONLY_INCLUDE_IN(flask)
const { insightComponent } = this.props;
if (insightComponent && (detailsComponent || dataComponent)) {
return this.renderTabs();
}
///: END:ONLY_INCLUDE_IN
if (detailsComponent && dataComponent) { if (detailsComponent && dataComponent) {
return this.renderTabs(); return this.renderTabs();
} }
return detailsComponent || dataComponent;
return (
detailsComponent ||
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent ||
///: END:ONLY_INCLUDE_IN
dataComponent
);
} }
renderTabs() { renderTabs() {
const { t } = this.context; const { t } = this.context;
const { detailsComponent, dataComponent, dataHexComponent } = this.props; const {
detailsComponent,
dataComponent,
dataHexComponent,
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent,
///: END:ONLY_INCLUDE_IN
} = this.props;
return ( return (
<Tabs> <Tabs>
@ -88,6 +113,12 @@ export default class ConfirmPageContainerContent extends Component {
{dataHexComponent} {dataHexComponent}
</Tab> </Tab>
)} )}
{
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent
///: END:ONLY_INCLUDE_IN
}
</Tabs> </Tabs>
); );
} }

View File

@ -82,12 +82,28 @@
color: var(--color-text-alternative); color: var(--color-text-alternative);
text-transform: uppercase; text-transform: uppercase;
margin: 0 8px;
& button { & button {
font-size: unset; font-size: unset;
color: var(--color-text-alternative); color: var(--color-text-alternative);
text-transform: uppercase; text-transform: uppercase;
max-width: 170px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
& .dropdown__select {
color: var(--color-text-alternative);
text-transform: uppercase;
option {
text-transform: none;
}
}
& .dropdown__icon-caret-down {
top: 40%;
} }
} }

View File

@ -64,6 +64,9 @@ export default class ConfirmPageContainer extends Component {
dataComponent: PropTypes.node, dataComponent: PropTypes.node,
dataHexComponent: PropTypes.node, dataHexComponent: PropTypes.node,
detailsComponent: PropTypes.node, detailsComponent: PropTypes.node,
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent: PropTypes.node,
///: END:ONLY_INCLUDE_IN
tokenAddress: PropTypes.string, tokenAddress: PropTypes.string,
nonce: PropTypes.string, nonce: PropTypes.string,
warning: PropTypes.string, warning: PropTypes.string,
@ -155,6 +158,9 @@ export default class ConfirmPageContainer extends Component {
isBuyableChain, isBuyableChain,
networkIdentifier, networkIdentifier,
setApproveForAllArg, setApproveForAllArg,
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent,
///: END:ONLY_INCLUDE_IN
} = this.props; } = this.props;
const showAddToAddressDialog = const showAddToAddressDialog =
@ -242,6 +248,9 @@ export default class ConfirmPageContainer extends Component {
detailsComponent={detailsComponent} detailsComponent={detailsComponent}
dataComponent={dataComponent} dataComponent={dataComponent}
dataHexComponent={dataHexComponent} dataHexComponent={dataHexComponent}
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent={insightComponent}
///: END:ONLY_INCLUDE_IN
errorMessage={errorMessage} errorMessage={errorMessage}
errorKey={errorKey} errorKey={errorKey}
tokenAddress={tokenAddress} tokenAddress={tokenAddress}

View File

@ -0,0 +1 @@
export { SnapInsight } from './snap-insight';

View File

@ -0,0 +1,3 @@
.snap-insight {
word-wrap: break-word;
}

View File

@ -0,0 +1,111 @@
import React from 'react';
import PropTypes from 'prop-types';
import Preloader from '../../../ui/icon/preloader/preloader-icon.component';
import Typography from '../../../ui/typography/typography';
import {
ALIGN_ITEMS,
COLORS,
FLEX_DIRECTION,
JUSTIFY_CONTENT,
TEXT_ALIGN,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { useTransactionInsightSnap } from '../../../../hooks/flask/useTransactionInsightSnap';
import SnapContentFooter from '../../flask/snap-content-footer/snap-content-footer';
import Box from '../../../ui/box/box';
export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
const t = useI18nContext();
const response = useTransactionInsightSnap({
transaction,
chainId,
snapId: selectedSnap.id,
});
const data = response?.insights;
const hasNoData = !data || !Object.keys(data).length;
return (
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
height="full"
marginTop={hasNoData && 12}
marginBottom={hasNoData && 12}
alignItems={hasNoData && ALIGN_ITEMS.CENTER}
justifyContent={hasNoData && JUSTIFY_CONTENT.CENTER}
textAlign={hasNoData && TEXT_ALIGN.CENTER}
className="snap-insight"
>
{data ? (
<Box
height="full"
flexDirection={FLEX_DIRECTION.COLUMN}
className="snap-insight__container"
>
{Object.keys(data).length ? (
<>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
paddingTop={0}
paddingRight={6}
paddingBottom={3}
paddingLeft={6}
className="snap-insight__container__data"
>
{Object.keys(data).map((key, i) => (
<div key={i}>
<Typography
fontWeight="bold"
marginTop={3}
variant={TYPOGRAPHY.H6}
>
{key}
</Typography>
<Typography variant={TYPOGRAPHY.H6}>{data[key]}</Typography>
</div>
))}
</Box>
<SnapContentFooter
snapName={selectedSnap.manifest.proposedName}
snapId={selectedSnap.id}
/>
</>
) : (
<Typography color={COLORS.TEXT_ALTERNATIVE} variant={TYPOGRAPHY.H6}>
{t('snapsNoInsight')}
</Typography>
)}
</Box>
) : (
<>
<Preloader size={40} />
<Typography
marginTop={3}
color={COLORS.TEXT_ALTERNATIVE}
variant={TYPOGRAPHY.H6}
>
{t('snapsInsightLoading')}
</Typography>
</>
)}
</Box>
);
};
SnapInsight.propTypes = {
/*
* The transaction data object
*/
transaction: PropTypes.object,
/*
* CAIP2 Chain ID
*/
chainId: PropTypes.string,
/*
* The insight snap selected
*/
selectedSnap: PropTypes.object,
};

View File

@ -7,3 +7,6 @@ export {
default as ConfirmPageContainerContent, default as ConfirmPageContainerContent,
ConfirmPageContainerSummary, ConfirmPageContainerSummary,
} from './confirm-page-container-content'; } from './confirm-page-container-content';
///: BEGIN:ONLY_INCLUDE_IN(flask)
export { SnapInsight } from './flask/snap-insight';
///: END:ONLY_INCLUDE_IN

View File

@ -2,6 +2,9 @@
@import 'confirm-page-container-header/index'; @import 'confirm-page-container-header/index';
@import 'confirm-detail-row/index'; @import 'confirm-detail-row/index';
@import 'confirm-page-container-navigation/index'; @import 'confirm-page-container-navigation/index';
///: BEGIN:ONLY_INCLUDE_IN(flask)
@import 'flask/index';
///: END:ONLY_INCLUDE_IN
.confirm-page-container { .confirm-page-container {
&__dialog { &__dialog {

View File

@ -0,0 +1 @@
export { default } from './snap-content-footer';

View File

@ -0,0 +1,13 @@
.snap-content-footer {
i {
color: var(--color-icon-muted);
padding-right: 4px;
}
.button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100px;
}
}

View File

@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import Typography from '../../../ui/typography/typography';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { SNAPS_VIEW_ROUTE } from '../../../../helpers/constants/routes';
import {
COLORS,
TYPOGRAPHY,
JUSTIFY_CONTENT,
ALIGN_ITEMS,
} from '../../../../helpers/constants/design-system';
import Button from '../../../ui/button';
import Box from '../../../ui/box/box';
export default function SnapContentFooter({ snapName, snapId }) {
const t = useI18nContext();
const history = useHistory();
const handleNameClick = (e) => {
e.stopPropagation();
history.push(`${SNAPS_VIEW_ROUTE}/${encodeURIComponent(snapId)}`);
};
// TODO: add truncation to the snap name, need to pick a character length at which to cut off
return (
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
alignItems={ALIGN_ITEMS.CENTER}
paddingTop={4}
paddingBottom={4}
className="snap-content-footer"
>
<i className="fas fa-exclamation-circle fa-sm" />
<Typography color={COLORS.TEXT_MUTED} variant={TYPOGRAPHY.H7}>
{t('snapContent', [
<Button type="inline" onClick={handleNameClick} key="button">
{snapName}
</Button>,
])}
</Typography>
</Box>
);
}
SnapContentFooter.propTypes = {
/**
* The name of the snap who's content is displayed
*/
snapName: PropTypes.string,
/**
* The id of the snap
*/
snapId: PropTypes.string,
};

View File

@ -0,0 +1,17 @@
import React from 'react';
import SnapContentFooter from '.';
export default {
title: 'Components/App/Flask/SnapContentFooter',
id: __filename,
component: SnapContentFooter,
args: {
snapName: 'Test Snap',
snapId: 'local:test-snap',
},
};
export const DefaultStory = (args) => <SnapContentFooter {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Dropdown from '../../dropdown';
export const DropdownTab = (props) => {
const {
activeClassName,
className,
'data-testid': dataTestId,
isActive,
onClick,
onChange,
tabIndex,
options,
selectedOption,
} = props;
return (
<li
className={classnames('tab', className, {
'tab--active': isActive,
[activeClassName]: activeClassName && isActive,
})}
data-testid={dataTestId}
onClick={(event) => {
event.preventDefault();
onClick(tabIndex);
}}
>
<Dropdown
options={options}
selectedOption={selectedOption}
onChange={onChange}
/>
</li>
);
};
DropdownTab.propTypes = {
activeClassName: PropTypes.string,
className: PropTypes.string,
'data-testid': PropTypes.string,
isActive: PropTypes.bool, // required, but added using React.cloneElement
options: PropTypes.arrayOf(
PropTypes.exact({
name: PropTypes.string,
value: PropTypes.string.isRequired,
}),
).isRequired,
selectedOption: PropTypes.string,
onChange: PropTypes.func,
onClick: PropTypes.func,
tabIndex: PropTypes.number, // required, but added using React.cloneElement
};
DropdownTab.defaultProps = {
activeClassName: undefined,
className: undefined,
onChange: undefined,
onClick: undefined,
selectedOption: undefined,
};

View File

@ -0,0 +1,3 @@
import { DropdownTab } from './dropdown-tab';
export default DropdownTab;

View File

@ -0,0 +1,23 @@
.tab {
.dropdown__select {
border: none;
font-size: unset;
width: 100%;
background-color: unset;
padding-left: 8px;
padding-right: 20px;
line-height: unset;
option {
background-color: var(--color-background-default);
}
&:focus-visible {
outline: none;
}
}
.dropdown__icon-caret-down {
right: 0;
}
}

View File

@ -1,4 +1,5 @@
import Tabs from './tabs.component'; import Tabs from './tabs.component';
import Tab from './tab'; import Tab from './tab';
import DropdownTab from './dropdown-tab';
export { Tabs, Tab }; export { Tabs, Tab, DropdownTab };

View File

@ -1,4 +1,5 @@
@import 'tab/index'; @import 'tab/index';
@import 'dropdown-tab/index';
.tabs { .tabs {
flex-grow: 1; flex-grow: 1;

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import DropdownTab from './dropdown-tab';
import Tab from './tab/tab.component'; import Tab from './tab/tab.component';
import Tabs from './tabs.component'; import Tabs from './tabs.component';
@ -41,6 +42,14 @@ export const DefaultStory = (args) => {
onTabClick={args.onTabClick} onTabClick={args.onTabClick}
> >
{args.tabs.map((tabProps, i) => renderTab(tabProps, i))} {args.tabs.map((tabProps, i) => renderTab(tabProps, i))}
<DropdownTab
options={[
{ name: 'Insight Snap', value: 'Insight Snap' },
{ name: 'Tenderly Insight', value: 'Tenderly Insight' },
]}
>
This is a dropdown Tab
</DropdownTab>
</Tabs> </Tabs>
); );
}; };

View File

@ -94,6 +94,11 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
leftIcon: 'fas fa-infinity', leftIcon: 'fas fa-infinity',
rightIcon: null, rightIcon: null,
}, },
[EndowmentPermissions['endowment:transaction-insight']]: {
label: (t) => t('permission_transactionInsight'),
leftIcon: 'fas fa-info',
rightIcon: null,
},
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
[UNKNOWN_PERMISSION]: { [UNKNOWN_PERMISSION]: {
label: (t, permissionName) => label: (t, permissionName) =>

View File

@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { handleSnapRequest } from '../../store/actions';
import { getPermissionSubjects } from '../../selectors';
const INSIGHT_PERMISSION = 'endowment:transaction-insight';
export function useTransactionInsightSnap({ transaction, chainId, snapId }) {
const subjects = useSelector(getPermissionSubjects);
if (!subjects[snapId]?.permissions[INSIGHT_PERMISSION]) {
throw new Error(
'This snap does not have the transaction insight endowment.',
);
}
const [data, setData] = useState(undefined);
useEffect(() => {
async function fetchInsight() {
const d = await handleSnapRequest({
snapId,
origin: 'test',
handler: 'onTransaction',
request: {
jsonrpc: '2.0',
method: ' ',
params: { transaction, chainId },
},
});
setData(d);
}
if (transaction) {
fetchInsight();
}
}, [snapId, transaction, chainId]);
return data;
}

View File

@ -1,5 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { stripHexPrefix } from 'ethereumjs-util';
///: END:ONLY_INCLUDE_IN
import ConfirmPageContainer from '../../components/app/confirm-page-container'; import ConfirmPageContainer from '../../components/app/confirm-page-container';
import TransactionDecoding from '../../components/app/transaction-decoding'; import TransactionDecoding from '../../components/app/transaction-decoding';
import { isBalanceSufficient } from '../send/send.utils'; import { isBalanceSufficient } from '../send/send.utils';
@ -42,7 +45,7 @@ import GasDetailsItem from '../../components/app/gas-details-item';
import GasTiming from '../../components/app/gas-timing/gas-timing.component'; import GasTiming from '../../components/app/gas-timing/gas-timing.component';
import LedgerInstructionField from '../../components/app/ledger-instruction-field'; import LedgerInstructionField from '../../components/app/ledger-instruction-field';
import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message'; import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message';
import Typography from '../../components/ui/typography/typography';
import { import {
COLORS, COLORS,
FONT_STYLE, FONT_STYLE,
@ -55,12 +58,21 @@ import {
removePollingTokenFromAppState, removePollingTokenFromAppState,
} from '../../store/actions'; } from '../../store/actions';
import Typography from '../../components/ui/typography/typography';
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants'; import { MIN_GAS_LIMIT_DEC } from '../send/send.constants';
import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
import { hexToDecimal } from '../../../shared/lib/metamask-controller-utils'; import { hexToDecimal } from '../../../shared/lib/metamask-controller-utils';
import { hexWEIToDecGWEI } from '../../../shared/lib/transactions-controller-utils'; import { hexWEIToDecGWEI } from '../../../shared/lib/transactions-controller-utils';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { SnapInsight } from '../../components/app/confirm-page-container/flask/snap-insight';
import { DropdownTab, Tab } from '../../components/ui/tabs';
///: END:ONLY_INCLUDE_IN
import {
NETWORK_TO_NAME_MAP,
///: BEGIN:ONLY_INCLUDE_IN(flask)
CHAIN_ID_TO_NETWORK_ID_MAP,
///: END:ONLY_INCLUDE_IN
} from '../../../shared/constants/network';
import TransactionAlerts from './transaction-alerts'; import TransactionAlerts from './transaction-alerts';
const renderHeartBeatIfNotInTest = () => const renderHeartBeatIfNotInTest = () =>
@ -150,6 +162,9 @@ export default class ConfirmTransactionBase extends Component {
showBuyModal: PropTypes.func, showBuyModal: PropTypes.func,
isBuyableChain: PropTypes.bool, isBuyableChain: PropTypes.bool,
setApproveForAllArg: PropTypes.bool, setApproveForAllArg: PropTypes.bool,
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightSnaps: PropTypes.arrayOf(PropTypes.object),
///: END:ONLY_INCLUDE_IN
}; };
state = { state = {
@ -159,6 +174,9 @@ export default class ConfirmTransactionBase extends Component {
ethGasPriceWarning: '', ethGasPriceWarning: '',
editingGas: false, editingGas: false,
userAcknowledgedGasMissing: false, userAcknowledgedGasMissing: false,
///: BEGIN:ONLY_INCLUDE_IN(flask)
selectedInsightSnapId: this.props.insightSnaps[0]?.id,
///: END:ONLY_INCLUDE_IN
}; };
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -302,6 +320,12 @@ export default class ConfirmTransactionBase extends Component {
this.setState({ editingGas: false }); this.setState({ editingGas: false });
} }
///: BEGIN:ONLY_INCLUDE_IN(flask)
handleSnapSelected(snapId) {
this.setState({ selectedInsightSnapId: snapId });
}
///: END:ONLY_INCLUDE_IN
setUserAcknowledgedGasMissing() { setUserAcknowledgedGasMissing() {
this.setState({ userAcknowledgedGasMissing: true }); this.setState({ userAcknowledgedGasMissing: true });
} }
@ -729,6 +753,61 @@ export default class ConfirmTransactionBase extends Component {
); );
} }
///: BEGIN:ONLY_INCLUDE_IN(flask)
renderInsight() {
const { txData, insightSnaps } = this.props;
const { selectedInsightSnapId } = this.state;
const { txParams, chainId } = txData;
const selectedSnap = insightSnaps.find(
({ id }) => id === selectedInsightSnapId,
);
const networkId = CHAIN_ID_TO_NETWORK_ID_MAP[chainId];
const caip2ChainId = `eip155:${networkId ?? stripHexPrefix(chainId)}`;
if (
txData.type !== TRANSACTION_TYPES.CONTRACT_INTERACTION ||
!insightSnaps.length
) {
return null;
}
const dropdownOptions = insightSnaps.map(
({ id, manifest: { proposedName } }) => ({
value: id,
name: proposedName,
}),
);
return insightSnaps.length > 1 ? (
<DropdownTab
className="confirm-page-container-content__tab"
options={dropdownOptions}
selectedOption={selectedInsightSnapId}
onChange={(snapId) => this.handleSnapSelected(snapId)}
>
<SnapInsight
transaction={txParams}
chainId={caip2ChainId}
selectedSnap={selectedSnap}
/>
</DropdownTab>
) : (
<Tab
className="confirm-page-container-content__tab"
name={selectedSnap.manifest.proposedName}
>
<SnapInsight
transaction={txParams}
chainId={caip2ChainId}
selectedSnap={selectedSnap}
/>
</Tab>
);
}
///: END:ONLY_INCLUDE_IN
handleEdit() { handleEdit() {
const { const {
txData, txData,
@ -1111,6 +1190,9 @@ export default class ConfirmTransactionBase extends Component {
detailsComponent={this.renderDetails()} detailsComponent={this.renderDetails()}
dataComponent={this.renderData(functionType)} dataComponent={this.renderData(functionType)}
dataHexComponent={this.renderDataHex(functionType)} dataHexComponent={this.renderDataHex(functionType)}
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightComponent={this.renderInsight()}
///: END:ONLY_INCLUDE_IN
contentComponent={contentComponent} contentComponent={contentComponent}
nonce={customNonceValue || nonce} nonce={customNonceValue || nonce}
unapprovedTxCount={unapprovedTxCount} unapprovedTxCount={unapprovedTxCount}

View File

@ -35,6 +35,9 @@ import {
getEIP1559V2Enabled, getEIP1559V2Enabled,
getIsBuyableChain, getIsBuyableChain,
getEnsResolutionByAddress, getEnsResolutionByAddress,
///: BEGIN:ONLY_INCLUDE_IN(flask)
getInsightSnaps,
///: END:ONLY_INCLUDE_IN
} from '../../selectors'; } from '../../selectors';
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { import {
@ -198,6 +201,10 @@ const mapStateToProps = (state, ownProps) => {
const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state); const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state);
const eip1559V2Enabled = getEIP1559V2Enabled(state); const eip1559V2Enabled = getEIP1559V2Enabled(state);
///: BEGIN:ONLY_INCLUDE_IN(flask)
const insightSnaps = getInsightSnaps(state);
///: END:ONLY_INCLUDE_IN
return { return {
balance, balance,
fromAddress, fromAddress,
@ -250,6 +257,9 @@ const mapStateToProps = (state, ownProps) => {
chainId, chainId,
eip1559V2Enabled, eip1559V2Enabled,
isBuyableChain, isBuyableChain,
///: BEGIN:ONLY_INCLUDE_IN(flask)
insightSnaps,
///: END:ONLY_INCLUDE_IN
}; };
}; };

View File

@ -62,10 +62,11 @@ import {
getLedgerTransportStatus, getLedgerTransportStatus,
} from '../ducks/app/app'; } from '../ducks/app/app';
import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils';
import { hexToDecimal } from '../../shared/lib/metamask-controller-utils';
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes'; import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes';
import { getPermissionSubjects } from './permissions';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
import { hexToDecimal } from '../../shared/lib/metamask-controller-utils';
/** /**
* One of the only remaining valid uses of selecting the network subkey of the * One of the only remaining valid uses of selecting the network subkey of the
@ -754,6 +755,17 @@ export function getSnaps(state) {
return state.metamask.snaps; return state.metamask.snaps;
} }
export function getInsightSnaps(state) {
const snaps = Object.values(state.metamask.snaps);
const subjects = getPermissionSubjects(state);
const insightSnaps = snaps.filter(
({ id }) => subjects[id]?.permissions['endowment:transaction-insight'],
);
return insightSnaps;
}
export const getSnapsRouteObjects = createSelector(getSnaps, (snaps) => { export const getSnapsRouteObjects = createSelector(getSnaps, (snaps) => {
return Object.values(snaps).map((snap) => { return Object.values(snaps).map((snap) => {
return { return {

View File

@ -1056,6 +1056,10 @@ export async function removeSnapError(msgData) {
return submitRequestToBackground('removeSnapError', [msgData]); return submitRequestToBackground('removeSnapError', [msgData]);
} }
export async function handleSnapRequest(args) {
return submitRequestToBackground('handleSnapRequest', [args]);
}
export function dismissNotifications(ids) { export function dismissNotifications(ids) {
return async (dispatch) => { return async (dispatch) => {
await submitRequestToBackground('dismissNotifications', [ids]); await submitRequestToBackground('dismissNotifications', [ids]);