From c3b79bb358d0f35fa222d5f20cf3d2dac8b81991 Mon Sep 17 00:00:00 2001
From: Daniel <80175477+dan437@users.noreply.github.com>
Date: Thu, 3 Jun 2021 18:08:37 +0200
Subject: [PATCH] Show custom tokens in Swaps, add a custom token in Swaps
(#11200)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Show custom tokens in Swaps
* Add messages for adding a custom token in Swaps
* Add the first version of importing custom tokens in swaps
* Fix lint rules
* Create a new component: ImportToken
* Remove a pointer cursor from regular heading
* Fix a CSS issue for tokens with long names
* Update a comment
* Don’t return a custom token if it doesn’t have symbol or decimals
* Only search by contract address if nothing was found
* Track “Token Imported” event
* Fix unit tests
* Import tracking for “Token Imported”, increase token icon font size
* Disable token import for Source Token
* Update logic and content for notifications, update tests
* Do not hide a dropdown placeholder on click, so a user can click on a link
* Update a key name
* Update styling for the “danger” type notification in Swaps
* Show either a warning or danger notification based on token verification occurences
* Remove testnets from SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP
* Use the “shouldSearchForImports” prop
* Create a new function for handling token import: “onOpenImportTokenModalClick”
* Filter token duplicities before iterating over tokens
* Use “address” instead of “symbol” for checking uniqueness
* Trigger Build
* Use a new API (/token) to get token data for importing in Swaps
* Temporarily decrese Jest threshold for functions
---
app/_locales/en/messages.json | 23 +++-
jest.config.js | 2 +-
shared/constants/swaps.js | 2 +
ui/hooks/useTokensToSearch.js | 12 +-
ui/pages/swaps/actionable-message/index.scss | 12 +-
.../swaps/awaiting-swap/awaiting-swap.test.js | 2 +-
ui/pages/swaps/build-quote/build-quote.js | 57 +++++----
ui/pages/swaps/build-quote/index.scss | 16 +++
.../dropdown-input-pair.test.js | 8 +-
.../dropdown-search-list.js | 111 ++++++++++++++++++
.../dropdown-search-list.test.js | 8 +-
.../swaps/dropdown-search-list/index.scss | 8 +-
ui/pages/swaps/import-token/import-token.js | 89 ++++++++++++++
.../swaps/import-token/import-token.test.js | 27 +++++
ui/pages/swaps/import-token/index.js | 1 +
ui/pages/swaps/import-token/index.scss | 30 +++++
ui/pages/swaps/index.scss | 1 +
.../swaps/searchable-item-list/index.scss | 36 +++++-
.../item-list/item-list.component.js | 81 ++++++++++++-
.../list-item-search.component.js | 50 +++++++-
.../searchable-item-list.js | 6 +
.../searchable-item-list.test.js | 12 +-
ui/pages/swaps/slippage-buttons/index.scss | 1 -
ui/pages/swaps/swaps.util.js | 18 ++-
24 files changed, 562 insertions(+), 51 deletions(-)
create mode 100644 ui/pages/swaps/import-token/import-token.js
create mode 100644 ui/pages/swaps/import-token/import-token.test.js
create mode 100644 ui/pages/swaps/import-token/index.js
create mode 100644 ui/pages/swaps/import-token/index.scss
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index e864d40f2..7bdda532f 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -52,6 +52,10 @@
"addContact": {
"message": "Add contact"
},
+ "addCustomTokenByContractAddress": {
+ "message": "Can’t find a token? You can manually add any token by pasting its address. Token contract addresses can be found on $1.",
+ "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum"
+ },
"addEthereumChainConfirmationDescription": {
"message": "This will allow this network to be used within MetaMask."
},
@@ -410,6 +414,9 @@
"continueToWyre": {
"message": "Continue to Wyre"
},
+ "contract": {
+ "message": "Contract"
+ },
"contractAddressError": {
"message": "You are sending tokens to the token's contract address. This may result in the loss of these tokens."
},
@@ -893,6 +900,12 @@
"message": "or $1",
"description": "$1 represents the text from `importAccountLinkText` as a link"
},
+ "importTokenQuestion": {
+ "message": "Import token?"
+ },
+ "importTokenWarning": {
+ "message": "Anyone can create a token with any name, including fake versions of existing tokens. Add and trade at your own risk!"
+ },
"importWallet": {
"message": "Import wallet"
},
@@ -2066,13 +2079,13 @@
"message": "Swap $1 to $2",
"description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap."
},
+ "swapTokenVerificationAddedManually": {
+ "message": "This token has been added manually."
+ },
"swapTokenVerificationMessage": {
"message": "Always confirm the token address on $1.",
"description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover."
},
- "swapTokenVerificationNoSource": {
- "message": "This token has not been verified."
- },
"swapTokenVerificationOnlyOneSource": {
"message": "Only verified on 1 source."
},
@@ -2358,6 +2371,10 @@
"message": "Verify this token on $1",
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
},
+ "verifyThisUnconfirmedTokenOn": {
+ "message": "Verify this token on $1 and make sure this is the token you want to trade.",
+ "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
+ },
"viewAccount": {
"message": "View Account"
},
diff --git a/jest.config.js b/jest.config.js
index e186bc814..6fce2ce4d 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -6,7 +6,7 @@ module.exports = {
coverageThreshold: {
global: {
branches: 32.75,
- functions: 43.31,
+ functions: 42.9,
lines: 43.12,
statements: 43.67,
},
diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js
index 2843821e5..42a2167f3 100644
--- a/shared/constants/swaps.js
+++ b/shared/constants/swaps.js
@@ -63,6 +63,7 @@ const SWAPS_TESTNET_CHAIN_ID = '0x539';
const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network';
const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/';
+const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/';
export const ALLOWED_SWAPS_CHAIN_IDS = {
[MAINNET_CHAIN_ID]: true,
@@ -90,4 +91,5 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL,
+ [MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL,
};
diff --git a/ui/hooks/useTokensToSearch.js b/ui/hooks/useTokensToSearch.js
index 5d6f8dd1a..02dd6097f 100644
--- a/ui/hooks/useTokensToSearch.js
+++ b/ui/hooks/useTokensToSearch.js
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import contractMap from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js';
-import { isEqual, shuffle } from 'lodash';
+import { isEqual, shuffle, uniqBy } from 'lodash';
import { getTokenFiatAmount } from '../helpers/utils/token-util';
import {
getTokenExchangeRates,
@@ -119,7 +119,12 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
others: [],
};
- memoizedTokensToSearch.forEach((token) => {
+ const memoizedSwapsAndUserTokensWithoutDuplicities = uniqBy(
+ [...memoizedTokensToSearch, ...memoizedUsersToken],
+ 'address',
+ );
+
+ memoizedSwapsAndUserTokensWithoutDuplicities.forEach((token) => {
const renderableDataToken = getRenderableTokenData(
{ ...usersTokensAddressMap[token.address], ...token },
tokenConversionRates,
@@ -129,8 +134,7 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
);
if (
isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) ||
- (usersTokensAddressMap[token.address] &&
- Number(renderableDataToken.balance ?? 0) !== 0)
+ usersTokensAddressMap[token.address]
) {
tokensToSearchBuckets.owned.push(renderableDataToken);
} else if (memoizedTopTokens[token.address]) {
diff --git a/ui/pages/swaps/actionable-message/index.scss b/ui/pages/swaps/actionable-message/index.scss
index 50e871613..4838489d4 100644
--- a/ui/pages/swaps/actionable-message/index.scss
+++ b/ui/pages/swaps/actionable-message/index.scss
@@ -51,12 +51,18 @@
}
&--danger {
- background: $Red-100;
- border: 1px solid $Red-500;
+ background: $Red-000;
+ border: 1px solid $Red-300;
justify-content: flex-start;
.actionable-message__message {
- color: $Red-500;
+ color: $Black-100;
+ text-align: left;
+ }
+
+ button {
+ background: $Red-500;
+ color: #fff;
}
}
diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js
index af9850752..bdd91c871 100644
--- a/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js
+++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js
@@ -27,7 +27,7 @@ describe('AwaitingSwap', () => {
store,
);
expect(getByText('Processing')).toBeInTheDocument();
- expect(getByText('View on Etherscan')).toBeInTheDocument();
+ expect(getByText('ETH')).toBeInTheDocument();
expect(getByText('View in activity')).toBeInTheDocument();
expect(
document.querySelector('.awaiting-swap__main-descrption'),
diff --git a/ui/pages/swaps/build-quote/build-quote.js b/ui/pages/swaps/build-quote/build-quote.js
index 71e887b5c..a3ee1b047 100644
--- a/ui/pages/swaps/build-quote/build-quote.js
+++ b/ui/pages/swaps/build-quote/build-quote.js
@@ -333,6 +333,38 @@ export default function BuildQuote({
dispatch(resetSwapsPostFetchState());
}, [dispatch]);
+ const BlockExplorerLink = () => {
+ return (
+ {
+ blockExplorerLinkClickedEvent();
+ global.platform.openTab({
+ url: blockExplorerTokenLink,
+ });
+ }}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {blockExplorerLabel}
+
+ );
+ };
+
+ let tokenVerificationDescription = '';
+ if (blockExplorerTokenLink) {
+ if (occurances === 1) {
+ tokenVerificationDescription = t('verifyThisTokenOn', [
+ ,
+ ]);
+ } else if (occurances === 0) {
+ tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [
+ ,
+ ]);
+ }
+ }
+
return (