diff --git a/app/components/Base/Text.js b/app/components/Base/Text.js index 906ba541319..1478cb60198 100644 --- a/app/components/Base/Text.js +++ b/app/components/Base/Text.js @@ -16,10 +16,13 @@ const style = StyleSheet.create({ right: { textAlign: 'right' }, - bold: fontStyles.bold, + red: { + color: colors.red + }, black: { color: colors.black }, + bold: fontStyles.bold, blue: { color: colors.blue }, @@ -66,6 +69,7 @@ const Text = ({ green, black, blue, + red, primary, small, upper, @@ -87,6 +91,8 @@ const Text = ({ green && style.green, black && style.black, blue && style.blue, + red && style.red, + black && style.black, primary && style.primary, disclaimer && [style.small, style.disclaimer], small && style.small, @@ -110,6 +116,7 @@ Text.defaultProps = { green: false, black: false, blue: false, + red: false, primary: false, disclaimer: false, modal: false, @@ -150,6 +157,10 @@ Text.propTypes = { * Makes text blue */ blue: PropTypes.bool, + /** + * Makes text red + */ + red: PropTypes.bool, /** * Makes text fontPrimary color */ diff --git a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap index 19c45c50f29..7d82f7e0e3d 100644 --- a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap @@ -26,6 +26,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -55,6 +56,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -83,6 +85,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -109,6 +112,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -132,6 +136,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` noMargin={true} onPress={[Function]} primary={true} + red={false} reset={false} right={false} small={false} @@ -174,6 +179,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={true} @@ -199,6 +205,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={true} small={false} @@ -232,6 +239,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={true} @@ -257,6 +265,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={true} small={false} @@ -289,6 +298,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={true} @@ -314,6 +324,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={true} small={false} @@ -350,6 +361,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap b/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap index 7e4369c5da1..08125e83109 100644 --- a/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap +++ b/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap @@ -40,6 +40,7 @@ exports[`AssetActionButtons should render correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -97,6 +98,7 @@ exports[`AssetActionButtons should render type add correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -154,6 +156,7 @@ exports[`AssetActionButtons should render type information correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -211,6 +214,7 @@ exports[`AssetActionButtons should render type receive correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -268,6 +272,7 @@ exports[`AssetActionButtons should render type send correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -325,6 +330,7 @@ exports[`AssetActionButtons should render type swap correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap b/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap index 5177dd71ee4..79962ceefa0 100644 --- a/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap +++ b/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap @@ -88,6 +88,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -150,6 +151,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -176,6 +178,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -280,6 +283,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -309,6 +313,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -336,6 +341,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap index 71e670559c0..84e57f358d0 100644 --- a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap @@ -28,6 +28,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -70,6 +71,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -104,6 +106,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -126,6 +129,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={true} diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js index d578eadd56c..66e66ce4e8e 100644 --- a/app/components/UI/Swaps/QuotesView.js +++ b/app/components/UI/Swaps/QuotesView.js @@ -199,6 +199,9 @@ const styles = StyleSheet.create({ termsButton: { marginTop: 10, marginBottom: 6 + }, + text: { + lineHeight: 20 } }); @@ -1303,20 +1306,20 @@ function SwapsQuotesView({ isVisible={isUpdateModalVisible} toggleModal={toggleUpdateModal} title={strings('swaps.quotes_update_often')} - body={{strings('swaps.quotes_update_often_text')}} + body={{strings('swaps.quotes_update_often_text')}} /> {strings('swaps.price_difference_body')}} + body={{strings('swaps.price_difference_body')}} /> + {strings('swaps.fee_text.get_the')} {strings('swaps.fee_text.best_price')}{' '} {strings('swaps.fee_text.from_the')} {strings('swaps.fee_text.top_liquidity')}{' '} {strings('swaps.fee_text.fee_is_applied', { diff --git a/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap b/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap index db85e8e40db..2e5a01e3d1d 100644 --- a/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap +++ b/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap @@ -38,6 +38,7 @@ exports[`TokenIcon component should Render correctly 3`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -134,6 +135,7 @@ exports[`TokenIcon component should Render correctly 7`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap b/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap index 60cadcd3f84..a609ae4002a 100644 --- a/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap +++ b/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap @@ -34,6 +34,7 @@ exports[`TokenSelectButton component should Render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -96,6 +97,7 @@ exports[`TokenSelectButton component should Render correctly 2`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -158,6 +160,7 @@ exports[`TokenSelectButton component should Render correctly 3`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -221,6 +224,7 @@ exports[`TokenSelectButton component should Render correctly 4`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -286,6 +290,7 @@ exports[`TokenSelectButton component should Render correctly 5`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap index cab6e467afd..b1c7d1a29d4 100644 --- a/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap @@ -24,6 +24,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -53,6 +54,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -79,6 +81,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -97,6 +100,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -117,6 +121,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap index 554bf0e3fc0..bd9bdbcb3fe 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap @@ -30,6 +30,7 @@ exports[`TransactionDetails should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={true} @@ -58,6 +59,7 @@ exports[`TransactionDetails should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={true} @@ -87,6 +89,7 @@ exports[`TransactionDetails should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={true} diff --git a/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap index c4aede01781..e0a24358079 100644 --- a/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap @@ -20,6 +20,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -39,6 +40,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -59,6 +61,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -81,6 +84,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={true} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -121,6 +125,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} diff --git a/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap b/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap index 0305a7201aa..528c58446c4 100644 --- a/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap +++ b/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap @@ -50,6 +50,7 @@ exports[`OfflineMode should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -78,6 +79,7 @@ exports[`OfflineMode should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/Views/SendFlow/AddressInputs/index.js b/app/components/Views/SendFlow/AddressInputs/index.js index 24e7ccf9cf7..7a59d533301 100644 --- a/app/components/Views/SendFlow/AddressInputs/index.js +++ b/app/components/Views/SendFlow/AddressInputs/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, View, Text, TextInput, TouchableOpacity } from 'react-native'; +import { StyleSheet, View, TextInput, TouchableOpacity } from 'react-native'; import { colors, fontStyles, baseStyles } from '../../../../styles/common'; import AntIcon from 'react-native-vector-icons/AntDesign'; import FontAwesome from 'react-native-vector-icons/FontAwesome'; @@ -7,6 +7,8 @@ import PropTypes from 'prop-types'; import Identicon from '../../../UI/Identicon'; import { renderShortAddress } from '../../../../util/address'; import { strings } from '../../../../../locales/i18n'; +import Text from '../../../Base/Text'; +import { hasZeroWidthPoints } from '../../../../util/validators'; const styles = StyleSheet.create({ wrapper: { @@ -45,7 +47,15 @@ const styles = StyleSheet.create({ addressToInformation: { flex: 1, flexDirection: 'row', - alignItems: 'center' + alignItems: 'center', + position: 'relative' + }, + exclamation: { + backgroundColor: colors.white, + borderRadius: 12, + position: 'absolute', + bottom: 8, + left: 20 }, address: { flexDirection: 'column', @@ -119,6 +129,43 @@ const styles = StyleSheet.create({ } }); +const AddressName = ({ toAddressName, confusableCollection = [] }) => { + if (confusableCollection.length) { + const texts = toAddressName.split('').map((char, index) => { + // if text has a confusable highlight it red + if (confusableCollection.includes(char)) { + // if the confusable is zero width, replace it with `?` + const replacement = hasZeroWidthPoints(char) ? '?' : char; + return ( + + {replacement} + + ); + } + return ( + + {char} + + ); + }); + return ( + + {texts} + + ); + } + return ( + + {toAddressName} + + ); +}; + +AddressName.propTypes = { + toAddressName: PropTypes.string, + confusableCollection: PropTypes.array +}; + export const AddressTo = props => { const { addressToReady, @@ -132,7 +179,9 @@ export const AddressTo = props => { onInputFocus, onSubmit, onInputBlur, - inputWidth + inputWidth, + confusableCollection, + displayExclamation } = props; return ( @@ -173,12 +222,18 @@ export const AddressTo = props => { + {displayExclamation && ( + + + + )} {toAddressName && ( - - {toAddressName} - + )} { diff --git a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap index 30049b78f87..9ef917d8156 100644 --- a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap @@ -24,11 +24,40 @@ exports[`Confirm should render correctly 1`] = ` fromAccountAddress="0x1" onPressIcon={null} /> - + + + We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam. + + } + isVisible={false} + title="Check the recipient address" + toggleModal={[Function]} + /> balance */ @@ -313,6 +323,7 @@ class Confirm extends PureComponent { }; state = { + confusableCollection: [], gasSpeedSelected: 'average', gasEstimationReady: false, customGas: undefined, @@ -331,6 +342,7 @@ class Confirm extends PureComponent { transactionTotalAmountFiat: undefined, errorMessage: undefined, fromAccountModalVisible: false, + warningModalVisible: false, mode: REVIEW, over: false }; @@ -368,6 +380,13 @@ class Confirm extends PureComponent { } }; + handleConfusables = async () => { + const { transactionToName } = this.props.transactionState; + await this.setState({ confusableCollection: collectConfusables(transactionToName) }); + }; + + toggleWarningModal = () => this.setState(state => ({ warningModalVisible: !state.warningModalVisible })); + componentDidMount = async () => { // For analytics AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SEND_TRANSACTION_STARTED, this.getAnalyticsParams()); @@ -375,6 +394,7 @@ class Confirm extends PureComponent { const { showCustomNonce, navigation, providerType } = this.props; await this.handleFetchBasicEstimates(); showCustomNonce && (await this.setNetworkNonce()); + await this.handleConfusables(); navigation.setParams({ providerType }); this.parseTransactionData(); this.prepareTransaction(); @@ -911,7 +931,7 @@ class Confirm extends PureComponent { render = () => { const { transactionToName, selectedAsset, paymentRequest } = this.props.transactionState; - const { showHexData, showCustomNonce, primaryCurrency, network, chainId } = this.props; + const { addressBook, showHexData, showCustomNonce, primaryCurrency, network, chainId } = this.props; const { nonce } = this.props.transaction; const { gasEstimationReady, @@ -928,10 +948,36 @@ class Confirm extends PureComponent { errorMessage, transactionConfirmed, warningGasPriceHigh, + confusableCollection, mode, - over + over, + warningModalVisible } = this.state; + const checksummedAddress = transactionTo && toChecksumAddress(transactionTo); + const existingContact = checksummedAddress && addressBook[network] && addressBook[network][checksummedAddress]; + const displayExclamation = !existingContact && !!confusableCollection.length; + + const AdressToComponent = () => ( + + ); + + const AdressToComponentWrap = () => + !existingContact && confusableCollection.length ? ( + + + + ) : ( + + ); + const is_main_net = isMainNet(network); const errorPress = is_main_net ? this.buyEth : this.gotoFaucet; const networkName = capitalize(getNetworkName(network)); @@ -947,14 +993,16 @@ class Confirm extends PureComponent { fromAccountName={fromAccountName} fromAccountBalance={fromAccountBalance} /> - + + {strings('transaction.confusable_msg')}} + /> + {!selectedAsset.tokenId ? ( @@ -1046,6 +1094,7 @@ class Confirm extends PureComponent { const mapStateToProps = state => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, + addressBook: state.engine.backgroundState.AddressBookController?.addressBook, contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, diff --git a/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap index 57413a9c970..621c6cfde6f 100644 --- a/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap @@ -32,6 +32,7 @@ exports[`ErrorMessage should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={true} diff --git a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap index d6855203729..c9965feb7e4 100644 --- a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap @@ -27,6 +27,7 @@ exports[`SendTo should render correctly 1`] = ` /> Add to address book Enter an alias diff --git a/app/components/Views/SendFlow/SendTo/index.js b/app/components/Views/SendFlow/SendTo/index.js index 54a4e6adda7..bd0f0b7bd42 100644 --- a/app/components/Views/SendFlow/SendTo/index.js +++ b/app/components/Views/SendFlow/SendTo/index.js @@ -7,7 +7,6 @@ import { StyleSheet, View, TouchableOpacity, - Text, TextInput, SafeAreaView, InteractionManager, @@ -34,6 +33,9 @@ import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; import { allowedToBuy } from '../../../UI/FiatOrders'; import NetworkList from '../../../../util/networks'; +import Text from '../../../Base/Text'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import { collectConfusables, hasZeroWidthPoints } from '../../../../util/validators'; const { hexToBN } = util; const styles = StyleSheet.create({ @@ -125,12 +127,41 @@ const styles = StyleSheet.create({ marginBottom: 32 }, buyEth: { - ...fontStyles.bold, color: colors.black, textDecorationLine: 'underline' }, - bold: { - ...fontStyles.bold + confusabeError: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + margin: 16, + padding: 16, + borderWidth: 1, + borderColor: colors.red, + backgroundColor: colors.red000, + borderRadius: 8 + }, + confusabeWarning: { + borderColor: colors.yellow, + backgroundColor: colors.yellow100 + }, + confusableTitle: { + marginTop: -3, + color: colors.red, + ...fontStyles.bold, + fontSize: 14 + }, + confusableMsg: { + color: colors.red, + fontSize: 12, + lineHeight: 16, + paddingRight: 10 + }, + black: { + color: colors.black + }, + warningIcon: { + marginRight: 8 } }); @@ -210,6 +241,7 @@ class SendFlow extends PureComponent { toEnsName: undefined, addToAddressToAddressBook: false, alias: undefined, + confusableCollection: [], inputWidth: { width: '99%' } }; @@ -274,7 +306,7 @@ class SendFlow extends PureComponent { const { AssetsContractController } = Engine.context; const { addressBook, network, identities, providerType } = this.props; const networkAddressBook = addressBook[network] || {}; - let addressError, toAddressName, toEnsName, errorContinue, isOnlyWarning; + let addressError, toAddressName, toEnsName, errorContinue, isOnlyWarning, confusableCollection; let [addToAddressToAddressBook, toSelectedAddressReady] = [false, false]; if (isValidAddress(toSelectedAddress)) { const checksummedToSelectedAddress = toChecksumAddress(toSelectedAddress); @@ -304,7 +336,7 @@ class SendFlow extends PureComponent { addressError = ( {strings('transaction.tokenContractAddressWarning_1')} - {strings('transaction.tokenContractAddressWarning_2')} + {strings('transaction.tokenContractAddressWarning_2')} {strings('transaction.tokenContractAddressWarning_3')} ); @@ -329,6 +361,7 @@ class SendFlow extends PureComponent { */ } else if (isENS(toSelectedAddress)) { toEnsName = toSelectedAddress; + confusableCollection = collectConfusables(toEnsName); const resolvedAddress = await doENSLookup(toSelectedAddress, network); if (resolvedAddress) { const checksummedResolvedAddress = toChecksumAddress(resolvedAddress); @@ -352,7 +385,8 @@ class SendFlow extends PureComponent { toSelectedAddressName: toAddressName, toEnsName, errorContinue, - isOnlyWarning + isOnlyWarning, + confusableCollection }); }; @@ -510,7 +544,7 @@ class SendFlow extends PureComponent { return ( <> {'\n'} - + {strings('fiat_on_ramp.buy_eth')} @@ -519,6 +553,7 @@ class SendFlow extends PureComponent { render = () => { const { ticker } = this.props; + const { addressBook, network } = this.props; const { fromSelectedAddress, fromAccountName, @@ -532,8 +567,16 @@ class SendFlow extends PureComponent { toInputHighlighted, inputWidth, errorContinue, - isOnlyWarning + isOnlyWarning, + confusableCollection } = this.state; + + const checksummedAddress = toSelectedAddress && toChecksumAddress(toSelectedAddress); + const existingContact = checksummedAddress && addressBook[network] && addressBook[network][checksummedAddress]; + const displayConfusableWarning = !existingContact && confusableCollection && !!confusableCollection.length; + const displayAsWarning = + confusableCollection && confusableCollection.length && !confusableCollection.some(hasZeroWidthPoints); + return ( @@ -556,6 +599,7 @@ class SendFlow extends PureComponent { onInputBlur={this.onToInputFocus} onSubmit={this.onTransactionDirectionSet} inputWidth={inputWidth} + confusableCollection={(!existingContact && confusableCollection) || []} /> @@ -578,6 +622,25 @@ class SendFlow extends PureComponent { /> )} + {displayConfusableWarning && ( + + + + + + + {strings('transaction.confusable_title')} + + + {strings('transaction.confusable_msg')} + + + + )} {addToAddressToAddressBook && ( { const wordCount = seed.split(/\s/u).length; @@ -13,3 +14,23 @@ export const parseSeedPhrase = seedPhrase => ?.join(' ') || ''; export const { isValidMnemonic } = ethers.utils; + +export const collectConfusables = ensName => { + const key = 'similarTo'; + const collection = confusables(ensName).reduce( + (total, current) => (key in current ? [...total, current.point] : total), + [] + ); + return collection; +}; + +const zeroWidthPoints = new Set([ + '\u200b', // zero width space + '\u200c', // zero width non-joiner + '\u200d', // zero width joiner + '\ufeff', // zero width no-break space + '\u2028', // line separator + '\u2029' // paragraph separator, +]); + +export const hasZeroWidthPoints = char => zeroWidthPoints.has(char); diff --git a/app/util/validators.test.js b/app/util/validators.test.js index a9f8b7c90e6..0f676b55953 100644 --- a/app/util/validators.test.js +++ b/app/util/validators.test.js @@ -1,4 +1,4 @@ -import { failedSeedPhraseRequirements, parseSeedPhrase } from './validators'; +import { failedSeedPhraseRequirements, parseSeedPhrase, hasZeroWidthPoints, collectConfusables } from './validators'; const VALID_24 = 'verb middle giant soon wage common wide tool gentle garlic issue nut retreat until album recall expire bronze bundle live accident expect dry cook'; @@ -37,3 +37,23 @@ describe('parseSeedPhrase', () => { expect(parseSeedPhrase(` ${String(VALID_12).toUpperCase()}`)).toEqual(VALID_12); }); }); + +describe('hasZeroWidthPoints', () => { + it('should detect zero-width unicode', () => { + expect('vita‍lik.eth'.split('').some(hasZeroWidthPoints)).toEqual(true); + }); + it('should not detect zero-width unicode', () => { + expect('vitalik.eth'.split('').some(hasZeroWidthPoints)).toEqual(false); + }); +}); + +describe('collectConfusables', () => { + it('should detect homoglyphic unicode points', () => { + expect(collectConfusables('vita‍lik.eth')).toHaveLength(1); + expect(collectConfusables('faceboоk.eth')).toHaveLength(1); + }); + + it('should detect multiple homoglyphic unicode points', () => { + expect(collectConfusables('ѕсоре.eth')).toHaveLength(5); + }); +}); diff --git a/locales/languages/en.json b/locales/languages/en.json index 0fcc69a1245..2067e452115 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -670,7 +670,9 @@ "tokenContractAddressWarning_2": "token contract address", "tokenContractAddressWarning_3": ". If you send tokens to this address, you will lose them.", "smartContractAddressWarning": "This address is a smart contract address. Please make sure you understand what this address is for, otherwise you risk losing your funds.", - "continueError": "I understand the risks, continue" + "continueError": "I understand the risks, continue", + "confusable_title": "Check the recipient address", + "confusable_msg": "We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam." }, "custom_gas": { "total": "Total", diff --git a/package.json b/package.json index 0cecb3b6bd1..96ed4b44b85 100644 --- a/package.json +++ b/package.json @@ -198,6 +198,7 @@ "rn-fetch-blob": "^0.12.0", "stream-browserify": "1.0.0", "through2": "3.0.1", + "unicode-confusables": "^0.1.1", "url": "0.11.0", "url-parse": "1.4.4", "valid-url": "1.0.9", diff --git a/patches/unicode-confusables+0.1.1.patch b/patches/unicode-confusables+0.1.1.patch new file mode 100644 index 00000000000..9b90e6b4c18 --- /dev/null +++ b/patches/unicode-confusables+0.1.1.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/unicode-confusables/data/confusables.json b/node_modules/unicode-confusables/data/confusables.json +index 855e49c..b0b8a0b 100644 +--- a/node_modules/unicode-confusables/data/confusables.json ++++ b/node_modules/unicode-confusables/data/confusables.json +@@ -157,8 +157,8 @@ + "໊": "๊", + "໋": "๋", + "꙯": "⃩", +- "
": " ", +- "
": " ", ++ "\u2028": " ", ++ "\u2029": " ", + " ": " ", + " ": " ", + " ": " ", diff --git a/yarn.lock b/yarn.lock index 9ff086d9574..582ba06b42b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13250,6 +13250,11 @@ unicode-canonical-property-names-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== +unicode-confusables@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/unicode-confusables/-/unicode-confusables-0.1.1.tgz#17f14e8dc53ff81c12e92fd86e836ebdf14ea0c2" + integrity sha512-XTPBWmT88BDpXz9NycWk4KxDn+/AJmJYYaYBwuIH9119sopwk2E9GxU9azc+JNbhEsfiPul78DGocEihCp6MFQ== + unicode-match-property-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"