diff --git a/src/api/tokenList/tokenList.ts b/src/api/tokenList/tokenList.ts index 841446cbc..8d436768b 100644 --- a/src/api/tokenList/tokenList.ts +++ b/src/api/tokenList/tokenList.ts @@ -1,6 +1,6 @@ import { TokenDetails, Network } from 'types' import { DEFAULT_PRECISION } from 'const' -import { TokenDetailsConfigLegacy } from '@gnosis.pm/dex-js' +import { safeTokenName, TokenDetailsConfigLegacy } from '@gnosis.pm/dex-js' export function getTokensByNetwork(networkId: number, tokenList: TokenDetailsConfigLegacy[]): TokenDetails[] { // Return token details @@ -11,7 +11,15 @@ export function getTokensByNetwork(networkId: number, tokenList: TokenDetailsCon const { id, name, symbol, decimals = DEFAULT_PRECISION } = token const addressMainnet = token.addressByNetwork[Network.Mainnet] - acc.push({ id, name, symbol, decimals, address, addressMainnet }) + acc.push({ + id, + label: safeTokenName({ address, name, symbol }), + name, + symbol, + decimals, + address, + addressMainnet, + }) return acc } diff --git a/src/const.ts b/src/const.ts index a0bfb72fa..0e5348661 100644 --- a/src/const.ts +++ b/src/const.ts @@ -160,6 +160,7 @@ export const INPUT_PRECISION_SIZE = 6 export const WETH_ADDRESS_MAINNET = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' export const WETH_ADDRESS_RINKEBY = '0xc778417E063141139Fce010982780140Aa0cD5Ab' export const WXDAI_ADDRESS_XDAI = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d' +export const WETH_ADDRESS_XDAI = '0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1' export const ORDER_BOOK_HOPS_DEFAULT = 2 export const ORDER_BOOK_HOPS_MAX = 2 diff --git a/src/reducers-actions/trade.ts b/src/reducers-actions/trade.ts index fdff5bac9..11002c750 100644 --- a/src/reducers-actions/trade.ts +++ b/src/reducers-actions/trade.ts @@ -1,11 +1,11 @@ -import { TokenDex } from '@gnosis.pm/dex-js' import { Actions } from 'reducers-actions' +import { TokenDetails } from 'types' export interface TradeState { price: string | null sellAmount: string | null - sellToken: Required | null - buyToken: Required | null + sellToken: Required | null + buyToken: Required | null validFrom: string | null validUntil: string | null } diff --git a/src/services/factories/addTokenToExchange.ts b/src/services/factories/addTokenToExchange.ts index cc3543629..4b6fdf460 100644 --- a/src/services/factories/addTokenToExchange.ts +++ b/src/services/factories/addTokenToExchange.ts @@ -1,6 +1,6 @@ import Web3 from 'web3' -import { logDebug } from 'utils' +import { logDebug, safeTokenName } from 'utils' import { getErc20Info } from '../helpers' import { ExchangeApi } from 'api/exchange/ExchangeApi' @@ -52,6 +52,7 @@ export function addTokenToExchangeFactory( const token: TokenDetails = { ...erc20Info, id, + label: safeTokenName(erc20Info), } // TODO: cache new token diff --git a/src/services/factories/getTokenFromExchange.ts b/src/services/factories/getTokenFromExchange.ts index cced9eccd..fe56d1cac 100644 --- a/src/services/factories/getTokenFromExchange.ts +++ b/src/services/factories/getTokenFromExchange.ts @@ -7,7 +7,7 @@ import { Erc20Api } from 'api/erc20/Erc20Api' import { ExchangeApi } from 'api/exchange/ExchangeApi' import { TokenList } from 'api/tokenList/TokenListApi' import { TokenFromErc20Params } from './' -import { TokenErc20 } from '@gnosis.pm/dex-js' +import { safeTokenName, TokenErc20 } from '@gnosis.pm/dex-js' interface FactoryParams { tokenListApi: TokenList @@ -86,6 +86,7 @@ function getTokenFromExchangeByAddressFactory( token = { ...erc20token, id: tokenId, + label: safeTokenName(erc20token), } } @@ -149,6 +150,7 @@ function getTokenFromExchangeByIdFactory( return { ...erc20token, id: tokenId, + label: safeTokenName(erc20token), } } diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 07b12506b..7522b61c4 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -2,11 +2,12 @@ import { TokenList } from 'api/tokenList/TokenListApi' import { SubscriptionCallback } from 'api/tokenList/Subscriptions' import { ExchangeApi } from 'api/exchange/ExchangeApi' import { TcrApi } from 'api/tcr/TcrApi' -import { TokenDetails, Command } from 'types' +import { TokenDetails, Command, Network } from 'types' import { logDebug, notEmpty, retry } from 'utils' import { TokenFromErc20Params } from './' -import { TokenErc20 } from '@gnosis.pm/dex-js' +import { safeTokenName, TokenErc20 } from '@gnosis.pm/dex-js' +import { WETH_ADDRESS_MAINNET, WETH_ADDRESS_RINKEBY, WETH_ADDRESS_XDAI, WXDAI_ADDRESS_XDAI } from 'const' export function getTokensFactory(factoryParams: { tokenListApi: TokenList @@ -175,11 +176,19 @@ export function getTokensFactory(factoryParams: { const tokenDetailsPromises: (Promise | TokenDetails)[] = [] addressToIdMap.forEach((id, tokenAddress) => { // Resolve the details using the config, otherwise fetch the token - const token: TokenDetails | undefined | Promise = tokensConfigMap.has(tokenAddress) - ? tokensConfigMap.get(tokenAddress) + const token: undefined | Promise = tokensConfigMap.has(tokenAddress) + ? Promise.resolve(tokensConfigMap.get(tokenAddress)) : _fetchToken(networkId, id, tokenAddress) if (token) { + // Add a label for convenience + token.then((token) => { + if (token) { + token.label = safeTokenName(token) + } + return token + }) + tokenDetailsPromises.push(token) } }) @@ -187,6 +196,36 @@ export function getTokensFactory(factoryParams: { return (await Promise.all(tokenDetailsPromises)).filter(notEmpty) } + function _moveTokenToHeadOfArray(tokenAddress: string, tokenList: TokenDetails[]): TokenDetails[] { + const tokenIndex = tokenList.findIndex((t) => t.address === tokenAddress) + if (tokenIndex !== -1) { + const token = tokenList[tokenIndex] + tokenList.splice(tokenIndex, 1) + + return [token, ...tokenList] + } + + return tokenList + } + + function _sortTokens(networkId: number, tokens: TokenDetails[]): TokenDetails[] { + // Sort tokens + let tokensSorted = tokens.sort(_tokenComparer) + + // Make sure wxDAI and WETH are the first tokens + switch (networkId) { + case Network.Mainnet: + return _moveTokenToHeadOfArray(WETH_ADDRESS_MAINNET, tokensSorted) + case Network.Rinkeby: + return _moveTokenToHeadOfArray(WETH_ADDRESS_RINKEBY, tokensSorted) + case Network.xDAI: + tokensSorted = _moveTokenToHeadOfArray(WETH_ADDRESS_XDAI, tokensSorted) + return _moveTokenToHeadOfArray(WXDAI_ADDRESS_XDAI, tokensSorted) + default: + return tokensSorted + } + } + async function _fetchToken( networkId: number, tokenId: number, @@ -206,6 +245,7 @@ export function getTokensFactory(factoryParams: { return { ...partialToken, id: tokenId, + label: '', // Label is not nullable for convenience, but it's added later. This adds a default for making TS happy } } @@ -218,8 +258,20 @@ export function getTokensFactory(factoryParams: { // Get token details for each filtered token const tokenDetails = await _fetchTokenDetails(networkId, filteredAddressesAndIds, tokensConfig) + // Sort tokens + const tokenList = _sortTokens(networkId, tokenDetails) + // Persist it - tokenListApi.persistTokens({ networkId, tokenList: tokenDetails }) + tokenListApi.persistTokens({ networkId, tokenList }) + } + + function _tokenComparer(a: TokenDetails, b: TokenDetails): number { + if (a.label < b.label) { + return -1 + } else if (a.label > b.label) { + return 1 + } + return 0 } async function updateTokens(networkId: number): Promise { diff --git a/src/services/index.ts b/src/services/index.ts index d47633aae..8aa993868 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -16,7 +16,7 @@ import { import { logDebug } from 'utils' import { TokenDetails } from 'types' import { toChecksumAddress } from 'web3-utils' -import { TokenErc20 } from '@gnosis.pm/dex-js' +import { safeTokenName, TokenErc20 } from '@gnosis.pm/dex-js' const apis = { tokenListApi, @@ -181,6 +181,7 @@ export const fetchTokenData = async ({ const token: TokenDetails = { ...erc20Token, id: tokenId, + label: safeTokenName(erc20Token), } return { diff --git a/src/storybook/data/tokens.ts b/src/storybook/data/tokens.ts index f505ee245..fbe3d44b6 100644 --- a/src/storybook/data/tokens.ts +++ b/src/storybook/data/tokens.ts @@ -8,6 +8,7 @@ export const ADDRESS_WXDAI = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d' // and when there's no Token found for symbol export const GNO: DefaultTokenDetails = { id: 1, + label: 'GNO', name: 'Gnosis Token', address: ADDRESS_GNO, symbol: 'GNO', @@ -15,6 +16,7 @@ export const GNO: DefaultTokenDetails = { } export const DAI: DefaultTokenDetails = { id: 2, + label: 'DAI', name: 'DAI Stablecoin', address: '0x2', symbol: 'DAI', @@ -23,6 +25,7 @@ export const DAI: DefaultTokenDetails = { export const baseTokenDefault: DefaultTokenDetails = { id: 3, + label: 'BASE', name: 'Base Token', symbol: 'BASE', address: '0x3', @@ -31,6 +34,7 @@ export const baseTokenDefault: DefaultTokenDetails = { export const quoteTokenDefault: DefaultTokenDetails = { id: 4, + label: 'QUOTE', name: 'Quote Token', symbol: 'QUOTE', address: '0x4', @@ -39,6 +43,7 @@ export const quoteTokenDefault: DefaultTokenDetails = { export const longNamedToken: DefaultTokenDetails = { id: 5, + label: 'TOKEN', name: 'Super super very ultra mega hyper long token name', symbol: 'TOKEN', address: '0x5', @@ -47,6 +52,7 @@ export const longNamedToken: DefaultTokenDetails = { export const emojiToken: DefaultTokenDetails = { id: 6, + label: '🍤🍤🍤', name: 'Emoji 🍤 Token', symbol: '🍤🍤🍤', address: '0x6', @@ -55,6 +61,7 @@ export const emojiToken: DefaultTokenDetails = { export const weirdSymbolToken: DefaultTokenDetails = { id: 7, + label: '!._$[]{…<>}@#¢$%&/()=?', name: 'Token-!._$[]{…<>}@#¢$%&/()=?', symbol: '!._$[]{…<>}@#¢$%&/()=?', address: '0x7', diff --git a/src/storybook/data/tokensConfig.ts b/src/storybook/data/tokensConfig.ts index ceb0a9170..617771903 100644 --- a/src/storybook/data/tokensConfig.ts +++ b/src/storybook/data/tokensConfig.ts @@ -2,17 +2,20 @@ import { TokenDetails } from 'types' import { defaultNetworkId } from 'storybook/data' import { baseTokenDefault, quoteTokenDefault } from 'storybook/data' import { findFromListHoC } from 'storybook/utils' +import { safeTokenName } from '@gnosis.pm/dex-js' // All Default Tokens -export const tokenList: TokenDetails[] = CONFIG.initialTokenList.map( - ({ id, name, symbol, addressByNetwork, decimals = 18 }) => ({ +export const tokenList: TokenDetails[] = CONFIG.initialTokenList.map((token) => { + const { id, name, symbol, addressByNetwork, decimals = 18 } = token + const address = addressByNetwork[defaultNetworkId] || '0x' + return { id, - name, + label: safeTokenName({ address, name, symbol }), symbol, - address: addressByNetwork[defaultNetworkId] || '0x', - decimals: decimals, - }), -) + address, + decimals, + } +}) // Token symbols to use in control selector function toTokenSymbol(token: TokenDetails): string { diff --git a/src/storybook/data/types.ts b/src/storybook/data/types.ts index 611c15a90..f0eec911e 100644 --- a/src/storybook/data/types.ts +++ b/src/storybook/data/types.ts @@ -2,4 +2,6 @@ import { WithAddress, WithDecimals, WithId, WithSymbolAndName } from '@gnosis.pm import { Network } from 'types' export type NetworkMap = Record -export type DefaultTokenDetails = Required +export interface DefaultTokenDetails extends Required { + label: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 6ade8176a..cddfa2c24 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -19,6 +19,7 @@ export enum Network { } export interface TokenDetails extends TokenDex { + label: string disabled?: boolean override?: TokenOverride } diff --git a/test/api/ExchangeApi/TokenListApiMock.test.ts b/test/api/ExchangeApi/TokenListApiMock.test.ts index 310c9c22d..3eae47dca 100644 --- a/test/api/ExchangeApi/TokenListApiMock.test.ts +++ b/test/api/ExchangeApi/TokenListApiMock.test.ts @@ -23,6 +23,7 @@ beforeEach(() => { const NEW_TOKEN = { id: 7, + label: 'NTK', name: 'New Token', symbol: 'NTK', decimals: 18, diff --git a/test/components/DepositWidget.components.test.tsx b/test/components/DepositWidget.components.test.tsx index 720106f33..3d682e8cf 100644 --- a/test/components/DepositWidget.components.test.tsx +++ b/test/components/DepositWidget.components.test.tsx @@ -22,6 +22,7 @@ const fakeRowState: Record = { const initialEthBalance = TEN const initialTokenBalanceDetails = { id: 1, + label: 'TTT', name: 'Test token', symbol: 'TTT', decimals: 18, diff --git a/test/data/tokenList.ts b/test/data/tokenList.ts index b5bc43989..74a131eab 100644 --- a/test/data/tokenList.ts +++ b/test/data/tokenList.ts @@ -8,6 +8,7 @@ const tokens: TokenDetails[] = [ // Wrapper of Ether to make it ERC-20 compliant { id: 1, + label: 'WETH', name: 'Wrapped Ether', symbol: 'WETH', decimals: 18, @@ -20,6 +21,7 @@ const tokens: TokenDetails[] = [ // Fiat enabled collateralized stable coin, that is backed by the most popular fiat currency, USD (US Dollar) in a 1:1 ratio { id: 2, + label: 'USDT', name: 'Tether USD', symbol: 'USDT', decimals: 6, @@ -32,6 +34,7 @@ const tokens: TokenDetails[] = [ // US Dollar backed stable coin which is totally fiat-collateralized { id: 3, + label: 'DAI', name: 'TrueUSD', symbol: 'TUSD', decimals: 18, @@ -46,6 +49,7 @@ const tokens: TokenDetails[] = [ // launched by cryptocurrency finance firm circle Internet financial Ltd and the CENTRE open source consortium launched { id: 4, + label: 'USDC', name: 'USD Coin', symbol: 'USDC', decimals: 6, @@ -60,6 +64,7 @@ const tokens: TokenDetails[] = [ // approved by the New York State Department of Financial Services { id: 5, + label: 'PAX', name: 'Paxos Standard', symbol: 'PAX', decimals: 18, @@ -73,6 +78,7 @@ const tokens: TokenDetails[] = [ // launched same day as PAX by Gemini Trust Company. backed by USD { id: 6, + label: 'GUSD', name: 'Gemini Dollar', symbol: 'GUSD', decimals: 2, @@ -85,6 +91,7 @@ const tokens: TokenDetails[] = [ // crypto-collateralized cryptocurrency: stable coin which is pegged to USD { id: 7, + label: 'DAI', name: 'DAI Stablecoin', symbol: 'DAI', decimals: 18, @@ -96,6 +103,7 @@ const tokens: TokenDetails[] = [ // for testing token with problematic characters for the URL { id: 77, + label: 'FTK /21/10-DAI $99 & +|-', name: 'Fake token', symbol: 'FTK /21/10-DAI $99 & +|-', decimals: 18,