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('vitalik.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('vitalik.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"