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

Replaced addresses by the address component on SignTypedData v4 signatures (#16018)

* Replaced addresses by the address component on SignTypedData v4 signatures

* Fixing signature-request e2e tests

* Modified scss file for signature-request message

* Using address component for rendering the addresses and bold label where hex address is not valid

* Modify the address component

* Added proper BEM syntax for class names and used Box and Typography

* FIxing e2e tests

* Commited requested changes from George and added storybook

* Review requested changes

* Created new component for rendering data in signature-request-message.js

* Fixing proper usage for getAccountName and getMetadataContractName selectors

* Fixing e2e tests
This commit is contained in:
Vladimir Saric 2022-11-10 11:28:34 +01:00 committed by GitHub
parent 4a247a95cb
commit 42be5a07d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 357 additions and 174 deletions

View File

@ -698,7 +698,7 @@
"ui/components/app/signature-request/signature-request-header/signature-request-header.component.js",
"ui/components/app/signature-request/signature-request-header/signature-request-header.stories.js",
"ui/components/app/signature-request/signature-request-message/index.js",
"ui/components/app/signature-request/signature-request-message/signature-request-message.component.js",
"ui/components/app/signature-request/signature-request-message/signature-request-message.js",
"ui/components/app/signature-request/signature-request.component.js",
"ui/components/app/signature-request/signature-request.component.test.js",
"ui/components/app/signature-request/signature-request.container.js",

View File

@ -56,7 +56,7 @@ describe('Sign Typed Data V4 Signature Request', function () {
const origin = content[0];
const address = content[1];
const message = await driver.findElement(
'.signature-request-message--node-value',
'.signature-request-data__node__value',
);
assert.equal(await title.getText(), 'Signature request');
assert.equal(await name.getText(), 'Ether Mail');
@ -140,7 +140,7 @@ describe('Sign Typed Data V3 Signature Request', function () {
const origin = content[0];
const address = content[1];
const messages = await driver.findElements(
'.signature-request-message--node-value',
'.signature-request-data__node__value',
);
assert.equal(await title.getText(), 'Signature request');
assert.equal(await name.getText(), 'Ether Mail');
@ -154,6 +154,10 @@ describe('Sign Typed Data V3 Signature Request', function () {
assert.equal(await messages[4].getText(), 'Hello, Bob!');
// Approve signing typed data
await driver.clickElement(
'[data-testid="signature-request-scroll-button"]',
);
await driver.delay(regularDelayMs);
await driver.clickElement({ text: 'Sign', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
windowHandles = await driver.getAllWindowHandles();

View File

@ -7,6 +7,7 @@ import {
getSwapsDefaultToken,
getMetadataContractName,
getAccountName,
getMetaMaskIdentities,
} from '../../../selectors';
import ConfirmPageContainer from './confirm-page-container.component';
@ -17,8 +18,9 @@ function mapStateToProps(state, ownProps) {
const networkIdentifier = getNetworkIdentifier(state);
const defaultToken = getSwapsDefaultToken(state);
const accountBalance = defaultToken.string;
const toName = getAccountName(state, to);
const toMetadataName = getMetadataContractName(to);
const identities = getMetaMaskIdentities(state);
const toName = getAccountName(identities, to);
const toMetadataName = getMetadataContractName(state, to);
return {
isBuyableChain,

View File

@ -1,6 +1,7 @@
@import 'signature-request-footer/index';
@import 'signature-request-header/index';
@import 'signature-request-message/index';
@import 'signature-request-data/index';
.signature-request {
display: flex;

View File

@ -0,0 +1 @@
export { default } from './signature-request-data';

View File

@ -0,0 +1,26 @@
.signature-request-data {
&__node {
&__value {
white-space: pre-line;
overflow: hidden;
word-wrap: break-word;
&__address {
[dir='rtl'] & {
/*rtl:ignore*/
direction: ltr;
/*rtl:ignore*/
text-align: right;
span {
display: block;
/*rtl:ignore*/
direction: rtl;
}
}
}
}
}
}

View File

@ -0,0 +1,79 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { getMetaMaskIdentities, getAccountName } from '../../../../selectors';
import Address from '../../transaction-decoding/components/decoding/address';
import {
isValidHexAddress,
toChecksumHexAddress,
} from '../../../../../shared/modules/hexstring-utils';
import Box from '../../../ui/box';
import Typography from '../../../ui/typography';
import {
DISPLAY,
COLORS,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
export default function SignatureRequestData({ data }) {
const identities = useSelector(getMetaMaskIdentities);
return (
<Box className="signature-request-data__node">
{Object.entries(data).map(([label, value], i) => (
<Box
className="signature-request-data__node"
key={i}
paddingLeft={2}
display={
typeof value !== 'object' || value === null ? DISPLAY.FLEX : null
}
>
<Typography
as="span"
color={COLORS.TEXT_DEFAULT}
marginLeft={4}
fontWeight={
typeof value === 'object' ? FONT_WEIGHT.BOLD : FONT_WEIGHT.NORMAL
}
>
{label.charAt(0).toUpperCase() + label.slice(1)}:{' '}
</Typography>
{typeof value === 'object' && value !== null ? (
<SignatureRequestData data={value} />
) : (
<Typography
as="span"
color={COLORS.TEXT_DEFAULT}
marginLeft={4}
className="signature-request-data__node__value"
>
{isValidHexAddress(value, {
mixedCaseUseChecksum: true,
}) ? (
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.INFO_DEFAULT}
className="signature-request-data__node__value__address"
>
<Address
addressOnly
checksummedRecipientAddress={toChecksumHexAddress(value)}
recipientName={getAccountName(identities, value)}
/>
</Typography>
) : (
`${value}`
)}
</Typography>
)}
</Box>
))}
</Box>
);
}
SignatureRequestData.propTypes = {
data: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
};

View File

@ -1 +1 @@
export { default } from './signature-request-message.component';
export { default } from './signature-request-message';

View File

@ -1,75 +1,24 @@
.signature-request-message {
flex: 1 60%;
display: flex;
max-height: 231px;
flex-direction: column;
position: relative;
&__title {
@include H6;
font-weight: 500;
color: var(--color-text-alternative);
margin-left: 12px;
}
h2 {
@include H6;
flex: 1 1 0;
text-align: left;
border-bottom: 1px solid var(--color-border-default);
padding: 0.5rem;
margin: 0;
color: var(--color-text-alternative);
}
&--root {
&__root {
flex: 1 100%;
background-color: var(--color-background-alternative);
padding-bottom: 0.5rem;
overflow: auto;
padding-left: 12px;
padding-right: 12px;
@include screen-sm-min {
width: auto;
}
}
&--node,
&--node-leaf {
padding-left: 0.3rem;
&-label {
color: var(--color-text-alternative);
margin-left: 0.5rem;
}
&-value {
color: var(--color-text-default);
margin-left: 0.5rem;
white-space: pre-line;
overflow: hidden;
word-wrap: break-word;
}
}
&--node-leaf {
display: flex;
}
&__scroll-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--color-border-default);
background: var(--color-background-alternative);
color: var(--color-icon-default);
position: absolute;
right: 24px;
right: 28px;
bottom: 12px;
border-radius: 50%;
height: 24px;
width: 24px;
cursor: pointer;
}
}

View File

@ -1,103 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import classnames from 'classnames';
export default class SignatureRequestMessage extends PureComponent {
static propTypes = {
data: PropTypes.object.isRequired,
onMessageScrolled: PropTypes.func,
setMessageRootRef: PropTypes.func,
messageRootRef: PropTypes.object,
messageIsScrollable: PropTypes.bool,
};
static contextTypes = {
t: PropTypes.func,
};
state = {
messageIsScrolled: false,
};
setMessageIsScrolled = () => {
if (!this.props.messageRootRef || this.state.messageIsScrolled) {
return;
}
const { scrollTop, offsetHeight, scrollHeight } = this.props.messageRootRef;
const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight;
if (isAtBottom) {
this.setState({ messageIsScrolled: true });
this.props.onMessageScrolled();
}
};
onScroll = debounce(this.setMessageIsScrolled, 25);
renderNode(data) {
return (
<div className="signature-request-message--node">
{Object.entries(data).map(([label, value], i) => (
<div
className={classnames('signature-request-message--node', {
'signature-request-message--node-leaf':
typeof value !== 'object' || value === null,
})}
key={i}
>
<span className="signature-request-message--node-label">
{label}:{' '}
</span>
{typeof value === 'object' && value !== null ? (
this.renderNode(value)
) : (
<span className="signature-request-message--node-value">
{`${value}`}
</span>
)}
</div>
))}
</div>
);
}
renderScrollButton() {
return (
<div
onClick={() => {
this.setState({ messageIsScrolled: true });
this.props.onMessageScrolled();
this.props.messageRootRef.scrollTo(
0,
this.props.messageRootRef.scrollHeight,
);
}}
className="signature-request-message__scroll-button"
data-testid="signature-request-scroll-button"
>
<i className="fa fa-arrow-down" title={this.context.t('scrollDown')} />
</div>
);
}
render() {
const { data, messageIsScrollable } = this.props;
return (
<div onScroll={this.onScroll} className="signature-request-message">
{messageIsScrollable ? this.renderScrollButton() : null}
<div className="signature-request-message__title">
{this.context.t('signatureRequest1')}
</div>
<div
className="signature-request-message--root"
ref={this.props.setMessageRootRef}
>
{this.renderNode(data)}
</div>
</div>
);
}
}

View File

@ -0,0 +1,98 @@
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import { I18nContext } from '../../../../contexts/i18n';
import Box from '../../../ui/box';
import Typography from '../../../ui/typography';
import {
DISPLAY,
ALIGN_ITEMS,
JUSTIFY_CONTENT,
COLORS,
FONT_WEIGHT,
FLEX_DIRECTION,
SIZES,
} from '../../../../helpers/constants/design-system';
import SignatureRequestData from '../signature-request-data';
export default function SignatureRequestMessage({
data,
onMessageScrolled,
setMessageRootRef,
messageRootRef,
messageIsScrollable,
}) {
const t = useContext(I18nContext);
const [messageIsScrolled, setMessageIsScrolled] = useState(false);
const setMessageIsScrolledAtBottom = () => {
if (!messageRootRef || messageIsScrolled) {
return;
}
const { scrollTop, offsetHeight, scrollHeight } = messageRootRef;
const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight;
if (isAtBottom) {
setMessageIsScrolled(true);
onMessageScrolled();
}
};
return (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
onScroll={debounce(setMessageIsScrolledAtBottom, 25)}
className="signature-request-message"
>
{messageIsScrollable ? (
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
borderColor={COLORS.BORDER_DEFAULT}
backgroundColor={COLORS.BACKGROUND_DEFAULT}
color={COLORS.ICON_DEFAULT}
onClick={() => {
setMessageIsScrolled(true);
onMessageScrolled();
messageRootRef?.scrollTo(0, messageRootRef?.scrollHeight);
}}
className="signature-request-message__scroll-button"
data-testid="signature-request-scroll-button"
>
<i className="fa fa-arrow-down" aria-label={t('scrollDown')} />
</Box>
) : null}
<Box
backgroundColor={COLORS.BACKGROUND_DEFAULT}
paddingBottom={3}
paddingTop={3}
paddingRight={3}
margin={2}
borderRadius={SIZES.XL}
borderColor={COLORS.BORDER_MUTED}
className="signature-request-message__root"
ref={setMessageRootRef}
>
<Typography
fontWeight={FONT_WEIGHT.BOLD}
color={COLORS.TEXT_DEFAULT}
marginLeft={4}
className="signature-request-message__title"
>
{t('signatureRequest1')}
</Typography>
<SignatureRequestData data={data} />
</Box>
</Box>
);
}
SignatureRequestMessage.propTypes = {
data: PropTypes.object.isRequired,
onMessageScrolled: PropTypes.func,
setMessageRootRef: PropTypes.func,
messageRootRef: PropTypes.object,
messageIsScrollable: PropTypes.bool,
};

View File

@ -0,0 +1,58 @@
import React from 'react';
import SignatureRequestMessage from './signature-request-message';
export default {
title: 'Components/App/SignatureRequestMessage',
id: __filename,
component: SignatureRequestMessage,
argTypes: {
data: { control: 'object' },
onMessageScrolled: { action: 'onMessageScrolled' },
setMessageRootRef: { action: 'setMessageRootRef' },
messageRootRef: { control: 'object' },
messageIsScrollable: { control: 'boolean' },
},
};
export const DefaultStory = (args) => {
return <SignatureRequestMessage {...args} />;
};
DefaultStory.storyName = 'Default';
DefaultStory.args = {
data: JSON.parse(
JSON.stringify({
domain: {
name: 'happydapp.website',
},
message: {
string: 'haay wuurl',
number: 42,
},
primaryType: 'Mail',
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
}),
),
messageIsScrollable: true,
};

View File

@ -8,6 +8,46 @@ import SignatureRequest from './signature-request.container';
describe('Signature Request', () => {
const mockStore = {
metamask: {
tokenList: {
'0x514910771af9ca656af840dff83e8264ecf986ca': {
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
symbol: 'LINK',
decimals: 18,
name: 'ChainLink Token',
iconUrl:
'https://crypto.com/price/coin-data/icon/LINK/color_icon.png',
aggregators: [
'Aave',
'Bancor',
'CMC',
'Crypto.com',
'CoinGecko',
'1inch',
'Paraswap',
'PMM',
'Zapper',
'Zerion',
'0x',
],
occurrences: 12,
unlisted: false,
},
},
identities: {
'0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e': {
name: 'Account 2',
address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e',
},
},
addressBook: {
undefined: {
0: {
address: '0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0',
name: '',
isEns: false,
},
},
},
provider: {
type: 'rpc',
},

View File

@ -5,7 +5,10 @@ import copyToClipboard from 'copy-to-clipboard';
import { shortenAddress } from '../../../../../../helpers/utils/util';
import Identicon from '../../../../../ui/identicon';
import { useI18nContext } from '../../../../../../hooks/useI18nContext';
import { getAddressBook } from '../../../../../../selectors';
import {
getMetadataContractName,
getAddressBook,
} from '../../../../../../selectors';
import NicknamePopovers from '../../../../modals/nickname-popovers';
const Address = ({
@ -20,15 +23,25 @@ const Address = ({
const addressBook = useSelector(getAddressBook);
const addressBookEntryObject = addressBook.find(
(entry) => entry.address === checksummedRecipientAddress,
(entry) =>
entry.address.toLowerCase() === checksummedRecipientAddress.toLowerCase(),
);
const recipientNickname = addressBookEntryObject?.name;
const recipientMetadataName = useSelector((state) =>
getMetadataContractName(state, checksummedRecipientAddress),
);
const recipientToRender = addressOnly
? recipientNickname ||
? recipientName ||
recipientNickname ||
recipientMetadataName ||
recipientEns ||
shortenAddress(checksummedRecipientAddress)
: recipientNickname || recipientEns || recipientName || t('newContract');
: recipientName ||
recipientNickname ||
recipientMetadataName ||
recipientEns ||
t('newContract');
return (
<div

View File

@ -7,6 +7,9 @@
.tx-insight {
overflow-x: hidden;
display: flex;
align-items: center;
cursor: pointer;
&-loading {
display: flex;
@ -167,6 +170,12 @@
}
}
}
.tx-insight-component-address {
&__sender-icon {
padding-right: 8px;
}
}
}

View File

@ -10,6 +10,7 @@ import {
getEnsResolutionByAddress,
getAccountName,
getMetadataContractName,
getMetaMaskIdentities,
} from '../../../selectors';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import TransactionListItemDetails from './transaction-list-item-details.component';
@ -22,8 +23,12 @@ const mapStateToProps = (state, ownProps) => {
recipientEns = getEnsResolutionByAddress(state, address);
}
const addressBook = getAddressBook(state);
const recipientName = getAccountName(state, recipientAddress);
const recipientMetadataName = getMetadataContractName(recipientAddress);
const identities = getMetaMaskIdentities(state);
const recipientName = getAccountName(identities, recipientAddress);
const recipientMetadataName = getMetadataContractName(
state,
recipientAddress,
);
const getNickName = (address) => {
const entry = addressBook.find((contact) => {

View File

@ -412,15 +412,16 @@ export function getAddressBookEntryOrAccountName(state, address) {
return entry && entry.name !== '' ? entry.name : address;
}
export function getAccountName(state, address) {
const entry = Object.values(state.metamask.identities).find((identity) =>
export function getAccountName(identities, address) {
const entry = Object.values(identities).find((identity) =>
isEqualCaseInsensitive(identity.address, toChecksumHexAddress(address)),
);
return entry && entry.name !== '' ? entry.name : '';
}
export function getMetadataContractName(address) {
const entry = Object.values(STATIC_MAINNET_TOKEN_LIST).find((identity) =>
export function getMetadataContractName(state, address) {
const tokenList = getTokenList(state);
const entry = Object.values(tokenList).find((identity) =>
isEqualCaseInsensitive(identity.address, toChecksumHexAddress(address)),
);
return entry && entry.name !== '' ? entry.name : '';