diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 27bdc23e8..ebe1cbc07 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3665,6 +3665,9 @@ "message": "Sending collectible (ERC-721) tokens is not currently supported", "description": "This is an error message we show the user if they attempt to send a collectible asset type, for which currently don't support sending" }, + "unverifiedContractAddressMessage": { + "message": "We cannot verify this contract. Make sure you trust this address." + }, "updatedWithDate": { "message": "Updated $1" }, diff --git a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js index 92fa00dc4..da2025e12 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js @@ -42,6 +42,7 @@ describe('Confirm Page Container Container Test', () => { identities: [], featureFlags: {}, enableEIP1559V2NoticeDismissed: true, + tokenList: {}, }, }; diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 54dcb6d62..9723f4ce5 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -55,6 +55,8 @@ export default class ConfirmPageContainerContent extends Component { nativeCurrency: PropTypes.string, networkName: PropTypes.string, showBuyModal: PropTypes.func, + toAddress: PropTypes.string, + transactionType: PropTypes.string, }; renderContent() { @@ -128,6 +130,8 @@ export default class ConfirmPageContainerContent extends Component { nativeCurrency, networkName, showBuyModal, + toAddress, + transactionType, } = this.props; const primaryAction = hideUserAcknowledgedGasMissing @@ -179,6 +183,8 @@ export default class ConfirmPageContainerContent extends Component { nonce={nonce} origin={origin} hideTitle={hideTitle} + toAddress={toAddress} + transactionType={transactionType} /> {this.renderContent()} {!supportsEIP1559V2 && diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js index bff595911..89c855a71 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js @@ -1,6 +1,7 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { TRANSACTION_TYPES } from '../../../../../shared/constants/transaction'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { TRANSACTION_ERROR_KEY } from '../../../../helpers/constants/error-keys'; import ConfirmPageContainerContent from './confirm-page-container-content.component'; @@ -10,8 +11,18 @@ describe('Confirm Page Container Content', () => { metamask: { provider: { type: 'test', + chainId: '0x3', }, eip1559V2Enabled: false, + addressBook: { + '0x3': { + '0x06195827297c7A80a443b6894d3BDB8824b43896': { + address: '0x06195827297c7A80a443b6894d3BDB8824b43896', + name: 'Address Book Account 1', + chainId: '0x3', + }, + }, + }, }, }; @@ -125,4 +136,30 @@ describe('Confirm Page Container Content', () => { fireEvent.click(cancelButton); expect(props.onCancel).toHaveBeenCalledTimes(1); }); + + it('render contract address name from addressBook in title for contract', async () => { + props.hasSimulationError = false; + props.disabled = false; + props.toAddress = '0x06195827297c7A80a443b6894d3BDB8824b43896'; + props.transactionType = TRANSACTION_TYPES.CONTRACT_INTERACTION; + const { queryByText } = renderWithProvider( + , + store, + ); + + expect(queryByText('Address Book Account 1')).toBeInTheDocument(); + }); + + it('render simple title without address name for simple send', async () => { + props.hasSimulationError = false; + props.disabled = false; + props.toAddress = '0x06195827297c7A80a443b6894d3BDB8824b43896'; + props.transactionType = TRANSACTION_TYPES.SIMPLE_SEND; + const { queryByText } = renderWithProvider( + , + store, + ); + + expect(queryByText('Address Book Account 1')).not.toBeInTheDocument(); + }); }); diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index d4878442f..e941568e9 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -1,8 +1,16 @@ /* eslint-disable no-negated-condition */ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; + +import { TRANSACTION_TYPES } from '../../../../../../shared/constants/transaction'; +import { toChecksumHexAddress } from '../../../../../../shared/modules/hexstring-utils'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import useAddressDetails from '../../../../../hooks/useAddressDetails'; + import Identicon from '../../../../ui/identicon'; +import InfoTooltip from '../../../../ui/info-tooltip'; +import NicknamePopovers from '../../../modals/nickname-popovers'; const ConfirmPageContainerSummary = (props) => { const { @@ -17,8 +25,18 @@ const ConfirmPageContainerSummary = (props) => { origin, hideTitle, image, + transactionType, + toAddress, } = props; + const [showNicknamePopovers, setShowNicknamePopovers] = useState(false); + const t = useI18nContext(); + const { toName, isTrusted } = useAddressDetails(toAddress); + + const isContractTypeTransaction = + transactionType === TRANSACTION_TYPES.CONTRACT_INTERACTION; + const checksummedAddress = toChecksumHexAddress(toAddress); + const renderImage = () => { if (image) { return ( @@ -47,7 +65,29 @@ const ConfirmPageContainerSummary = (props) => {
{origin}
)}
-
{action}
+
+ {isContractTypeTransaction && toName && ( + + + : + + )} + + {action} + + {isContractTypeTransaction && isTrusted === false && ( + + )} +
{nonce && (
{`#${nonce}`} @@ -69,6 +109,12 @@ const ConfirmPageContainerSummary = (props) => {
)} + {showNicknamePopovers && ( + setShowNicknamePopovers(false)} + address={checksummedAddress} + /> + )}
); }; @@ -85,6 +131,8 @@ ConfirmPageContainerSummary.propTypes = { nonce: PropTypes.string, origin: PropTypes.string.isRequired, hideTitle: PropTypes.bool, + toAddress: PropTypes.string, + transactionType: PropTypes.string, }; export default ConfirmPageContainerSummary; diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss index d8f7a307c..0f4941976 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss @@ -23,12 +23,36 @@ &__action { @include H7; - text-transform: uppercase; color: var(--oslo-gray); padding: 3px 8px; border: 1px solid var(--oslo-gray); border-radius: 4px; - display: inline-block; + display: inline-flex; + align-items: center; + + &__name { + text-transform: uppercase; + } + + .info-tooltip { + margin-inline-start: 4px; + + &__tooltip-container { + margin-bottom: -3px; + } + } + + &__contract-address { + margin-inline-end: 4px; + } + + &__contract-address-btn { + background: none; + border: none; + padding: 0; + margin-inline-end: 4px; + color: var(--primary-1); + } } &__nonce { diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 25c9ff937..270055889 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -256,6 +256,8 @@ export default class ConfirmPageContainer extends Component { nativeCurrency={nativeCurrency} networkName={networkName} showBuyModal={showBuyModal} + toAddress={toAddress} + transactionType={currentTransaction.type} /> )} {shouldDisplayWarning && errorKey === INSUFFICIENT_FUNDS_ERROR_KEY && ( diff --git a/ui/hooks/useAddressDetails.js b/ui/hooks/useAddressDetails.js new file mode 100644 index 000000000..9755f3bf1 --- /dev/null +++ b/ui/hooks/useAddressDetails.js @@ -0,0 +1,48 @@ +import { useSelector } from 'react-redux'; + +import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; +import { + getAddressBook, + getMetaMaskIdentities, + getTokenList, + getUseTokenDetection, +} from '../selectors'; +import { shortenAddress } from '../helpers/utils/util'; + +const useAddressDetails = (toAddress) => { + const addressBook = useSelector(getAddressBook); + const identities = useSelector(getMetaMaskIdentities); + const tokenList = useSelector(getTokenList); + const useTokenDetection = useSelector(getUseTokenDetection); + const checksummedAddress = toChecksumHexAddress(toAddress); + + if (!toAddress) { + return {}; + } + const addressBookEntryObject = addressBook.find( + (entry) => entry.address === checksummedAddress, + ); + if (addressBookEntryObject?.name) { + return { toName: addressBookEntryObject.name, isTrusted: true }; + } + if (identities[toAddress]?.name) { + return { toName: identities[toAddress].name, isTrusted: true }; + } + const casedTokenList = useTokenDetection + ? tokenList + : Object.keys(tokenList).reduce((acc, base) => { + return { + ...acc, + [base.toLowerCase()]: tokenList[base], + }; + }, {}); + if (casedTokenList[toAddress]?.name) { + return { toName: casedTokenList[toAddress].name, isTrusted: true }; + } + return { + toName: shortenAddress(checksummedAddress), + isTrusted: false, + }; +}; + +export default useAddressDetails; diff --git a/ui/hooks/useAddressDetails.test.js b/ui/hooks/useAddressDetails.test.js new file mode 100644 index 000000000..89fb38d12 --- /dev/null +++ b/ui/hooks/useAddressDetails.test.js @@ -0,0 +1,102 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { renderHook } from '@testing-library/react-hooks'; + +import configureStore from '../store/store'; +import useAddressDetails from './useAddressDetails'; + +const renderUseAddressDetails = (toAddress, stateVariables = {}) => { + const mockState = { + metamask: { + provider: { + type: 'test', + chainId: '0x3', + }, + tokenList: {}, + ...stateVariables, + }, + }; + + const wrapper = ({ children }) => ( + {children} + ); + + return renderHook(() => useAddressDetails(toAddress), { wrapper }); +}; + +describe('useAddressDetails', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return empty object if no address is passed', () => { + const { result } = renderUseAddressDetails(); + expect(result.current).toStrictEqual({}); + }); + + it('should return name from addressBook if address is present in addressBook', () => { + const { result } = renderUseAddressDetails( + '0x06195827297c7A80a443b6894d3BDB8824b43896', + { + addressBook: { + '0x3': { + '0x06195827297c7A80a443b6894d3BDB8824b43896': { + address: '0x06195827297c7A80a443b6894d3BDB8824b43896', + name: 'Address Book Account 1', + chainId: '0x3', + }, + }, + }, + }, + ); + const { toName, isTrusted } = result.current; + expect(toName).toBe('Address Book Account 1'); + expect(isTrusted).toBe(true); + }); + + it('should return name from identities if address is present in identities', () => { + const { result } = renderUseAddressDetails( + '0x06195827297c7A80a443b6894d3BDB8824b43896', + { + identities: { + '0x06195827297c7A80a443b6894d3BDB8824b43896': { + address: '0x06195827297c7A80a443b6894d3BDB8824b43896', + name: 'Account 1', + }, + }, + }, + ); + const { toName, isTrusted } = result.current; + expect(toName).toBe('Account 1'); + expect(isTrusted).toBe(true); + }); + + it('should return name from tokenlist if address is present in tokens', () => { + const { result } = renderUseAddressDetails( + '0x06195827297c7A80a443b6894d3BDB8824b43896', + { + useTokenDetection: true, + tokenList: { + '0x06195827297c7A80a443b6894d3BDB8824b43896': { + address: '0x06195827297c7A80a443b6894d3BDB8824b43896', + symbol: 'LINK', + decimals: 18, + name: 'TOKEN-ABC', + }, + }, + }, + ); + const { toName, isTrusted } = result.current; + expect(toName).toBe('TOKEN-ABC'); + expect(isTrusted).toBe(true); + }); + + it('should return shortened address if address is not presend in any of above sources', () => { + const { result } = renderUseAddressDetails( + '0x06195827297c7A80a443b6894d3BDB8824b43896', + ); + const { toName, isTrusted } = result.current; + expect(toName).toBe('0x061...3896'); + expect(isTrusted).toBe(false); + }); +}); diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index a7202b6ca..7669eb59f 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -1045,10 +1045,7 @@ export default class ConfirmTransactionBase extends Component { }; let functionType; - if ( - txData.type === TRANSACTION_TYPES.DEPLOY_CONTRACT || - txData.type === TRANSACTION_TYPES.CONTRACT_INTERACTION - ) { + if (txData.type === TRANSACTION_TYPES.CONTRACT_INTERACTION) { functionType = getMethodName(name); }