From 346b1d0ea27bf5dab72dd87b6cbc9591f43c704e Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 5 Oct 2020 19:47:43 +0200 Subject: [PATCH 01/11] Refactor and allow to filter in networks with no TCR --- src/services/factories/tokenList.ts | 51 +++++++++++++++++++---------- src/utils/miscellaneous.ts | 4 +++ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index fc7872cc1..ada5c7beb 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -3,7 +3,7 @@ import { SubscriptionCallback } from 'api/tokenList/Subscriptions' import { ExchangeApi } from 'api/exchange/ExchangeApi' import { TcrApi } from 'api/tcr/TcrApi' import { TokenDetails, Command } from 'types' -import { logDebug, retry } from 'utils' +import { logDebug, notEmpty, retry } from 'utils' import { TokenFromErc20Params } from './' import { TokenErc20 } from '@gnosis.pm/dex-js' @@ -130,34 +130,49 @@ export function getTokensFactory(factoryParams: { return new Set(tcrApi && (await tcrApi.getTokens(networkId))) } - async function updateTokenDetails(networkId: number, numTokens: number): Promise { - const [tcrAddressesSet, addressesAndIds] = await Promise.all([ + async function _getFilteredIdsMap(networkId: number, numTokens: number): Promise> { + const [tcrAddressesSet, listedAddressesAndIds] = await Promise.all([ // Fetch addresses from TCR retry(() => fetchTcrAddresses(networkId)), // Fetch addresses from contract given numTokens count retry(() => fetchAddressesAndIds(networkId, numTokens)), ]) - logDebug(`[tokenListFactory][${networkId}] TCR contains ${tcrAddressesSet.size} addresses`) + if (tcrAddressesSet.size > 0) { + // If filtering by TCR + logDebug(`[tokenListFactory][${networkId}] TCR contains ${tcrAddressesSet.size} addresses`) + logDebug(`[tokenListFactory][${networkId}] Token id and address mapping:`) + const filteredAddressAndIds = Array.from(listedAddressesAndIds.entries()).filter(([address, id]) => { + // Remove addresses that are not on the TCR, if any + const isInTcr = tcrAddressesSet.has(address) + logDebug(`[tokenListFactory][${networkId}] ${id} : ${address}. On TCR? ${isInTcr}`) + }) + return new Map(filteredAddressAndIds) + } else { + // If not using a TCR, filter by default list + logDebug(`[tokenListFactory][${networkId}] Not using a TCR. Filtering by tokens in the config file`) + const tokensConfig = tokenListApi.getTokens(networkId) + const filteredAddressAndIds = tokensConfig + .map((token): [string, number] | null => { + const tokenId = listedAddressesAndIds[token.address] + return [token.address, tokenId] || null + }) + .filter(notEmpty) + + return new Map(filteredAddressAndIds) + } + } - logDebug(`[tokenListFactory][${networkId}] Token id and address mapping:`) - addressesAndIds.forEach((id, address) => { - // Remove addresses that are not on the TCR, if any - let removed = false - if (tcrAddressesSet.size > 0 && !tcrAddressesSet.has(address)) { - addressesAndIds.delete(address) - removed = true - } - logDebug(`[tokenListFactory][${networkId}] ${id} : ${address}. On TCR? ${!removed}`) - }) + async function updateTokenDetails(networkId: number, numTokens: number): Promise { + // Get filtered ids and addresses + const filteredAddressesAndIds = await _getFilteredIdsMap(networkId, numTokens) // Create a map of current token list addresses and tokens const localAddressesMap = new Map( tokenListApi - // Get a copy of the current token list .getTokens(networkId) // Remove tokens that are on the list but not registered on the contract neither on TCR - .filter(({ address }) => addressesAndIds.has(address)) + .filter(({ address }) => filteredAddressesAndIds.has(address)) // Map it with token address as key for easy access .map((token) => [token.address, token]), ) @@ -165,7 +180,7 @@ export function getTokensFactory(factoryParams: { const promises: Promise[] = [] // Go over all value (id) and key (tokenAddress) pairs registered on the contract - addressesAndIds.forEach((id, tokenAddress) => { + filteredAddressesAndIds.forEach((id, tokenAddress) => { if (!localAddressesMap.has(tokenAddress)) { // New token not in our local list, fetch erc20 details for it logDebug(`[tokenListFactory][${networkId}] Address ${tokenAddress} with id ${id} not in local list, fetching`) @@ -191,7 +206,7 @@ export function getTokensFactory(factoryParams: { `[tokenListFactory][${networkId}] Got details for address ${partialToken.address}: symbol '${partialToken.symbol}' name '${partialToken.name}'`, ) // Get id from address/id mapping - const id = addressesAndIds.get(partialToken.address) as number + const id = filteredAddressesAndIds.get(partialToken.address) as number // build token object const token: TokenDetails = { ...partialToken, id } // add to local address map diff --git a/src/utils/miscellaneous.ts b/src/utils/miscellaneous.ts index e05faf0ac..9bbc2d64d 100644 --- a/src/utils/miscellaneous.ts +++ b/src/utils/miscellaneous.ts @@ -198,3 +198,7 @@ export const RequireContextMock: __WebpackModuleApi.RequireContext = Object.assi resolve: () => '', id: '', }) + +export function notEmpty(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined +} From 5b8e19c3e83925d232bdfb2ac7dcbab98bb0b40f Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 6 Oct 2020 11:35:19 +0200 Subject: [PATCH 02/11] Refactor map creation --- src/services/factories/tokenList.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index ada5c7beb..1594210b6 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -138,29 +138,29 @@ export function getTokensFactory(factoryParams: { retry(() => fetchAddressesAndIds(networkId, numTokens)), ]) + let filteredAddressAndIds: [string, number][] if (tcrAddressesSet.size > 0) { // If filtering by TCR logDebug(`[tokenListFactory][${networkId}] TCR contains ${tcrAddressesSet.size} addresses`) logDebug(`[tokenListFactory][${networkId}] Token id and address mapping:`) - const filteredAddressAndIds = Array.from(listedAddressesAndIds.entries()).filter(([address, id]) => { + filteredAddressAndIds = Array.from(listedAddressesAndIds.entries()).filter(([address, id]) => { // Remove addresses that are not on the TCR, if any const isInTcr = tcrAddressesSet.has(address) logDebug(`[tokenListFactory][${networkId}] ${id} : ${address}. On TCR? ${isInTcr}`) }) - return new Map(filteredAddressAndIds) } else { // If not using a TCR, filter by default list logDebug(`[tokenListFactory][${networkId}] Not using a TCR. Filtering by tokens in the config file`) const tokensConfig = tokenListApi.getTokens(networkId) - const filteredAddressAndIds = tokensConfig + filteredAddressAndIds = tokensConfig .map((token): [string, number] | null => { const tokenId = listedAddressesAndIds[token.address] return [token.address, tokenId] || null }) .filter(notEmpty) - - return new Map(filteredAddressAndIds) } + + return new Map(filteredAddressAndIds) } async function updateTokenDetails(networkId: number, numTokens: number): Promise { From 421ddf2f5c4da36e3dcd78c372b39de53c9572b5 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 6 Oct 2020 18:09:21 +0200 Subject: [PATCH 03/11] Return filter flag Co-authored-by: Velenir --- src/services/factories/tokenList.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 1594210b6..44a82f83a 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -147,6 +147,7 @@ export function getTokensFactory(factoryParams: { // Remove addresses that are not on the TCR, if any const isInTcr = tcrAddressesSet.has(address) logDebug(`[tokenListFactory][${networkId}] ${id} : ${address}. On TCR? ${isInTcr}`) + return isInTcr }) } else { // If not using a TCR, filter by default list From c8c2941328e66220eeed25d825ff1272319103b9 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 6 Oct 2020 18:10:57 +0200 Subject: [PATCH 04/11] Fix issue accessing Map Co-authored-by: Velenir --- src/services/factories/tokenList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 44a82f83a..464d12ea2 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -155,7 +155,7 @@ export function getTokensFactory(factoryParams: { const tokensConfig = tokenListApi.getTokens(networkId) filteredAddressAndIds = tokensConfig .map((token): [string, number] | null => { - const tokenId = listedAddressesAndIds[token.address] + const tokenId = listedAddressesAndIds.get(token.address) return [token.address, tokenId] || null }) .filter(notEmpty) From 11a7eeb20965df55c6ae1a8a9737d97f1ba48101 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 6 Oct 2020 18:13:48 +0200 Subject: [PATCH 05/11] Filter tokenList --- src/services/factories/tokenList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 464d12ea2..8a17b24b3 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -156,7 +156,7 @@ export function getTokensFactory(factoryParams: { filteredAddressAndIds = tokensConfig .map((token): [string, number] | null => { const tokenId = listedAddressesAndIds.get(token.address) - return [token.address, tokenId] || null + return tokenId !== undefined ? [token.address, tokenId] : null }) .filter(notEmpty) } From 760dedbb90d4086568fa669b30cd886d505cf098 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Wed, 7 Oct 2020 12:46:26 +0200 Subject: [PATCH 06/11] Improve fetch token details --- src/services/factories/tokenList.ts | 79 +++++++++++------------------ 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 8a17b24b3..3b9c87af8 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -130,7 +130,11 @@ export function getTokensFactory(factoryParams: { return new Set(tcrApi && (await tcrApi.getTokens(networkId))) } - async function _getFilteredIdsMap(networkId: number, numTokens: number): Promise> { + async function _getFilteredIdsMap( + networkId: number, + numTokens: number, + tokensConfig: TokenDetails[], + ): Promise> { const [tcrAddressesSet, listedAddressesAndIds] = await Promise.all([ // Fetch addresses from TCR retry(() => fetchTcrAddresses(networkId)), @@ -152,7 +156,6 @@ export function getTokensFactory(factoryParams: { } else { // If not using a TCR, filter by default list logDebug(`[tokenListFactory][${networkId}] Not using a TCR. Filtering by tokens in the config file`) - const tokensConfig = tokenListApi.getTokens(networkId) filteredAddressAndIds = tokensConfig .map((token): [string, number] | null => { const tokenId = listedAddressesAndIds.get(token.address) @@ -165,61 +168,41 @@ export function getTokensFactory(factoryParams: { } async function updateTokenDetails(networkId: number, numTokens: number): Promise { - // Get filtered ids and addresses - const filteredAddressesAndIds = await _getFilteredIdsMap(networkId, numTokens) - - // Create a map of current token list addresses and tokens - const localAddressesMap = new Map( - tokenListApi - .getTokens(networkId) - // Remove tokens that are on the list but not registered on the contract neither on TCR - .filter(({ address }) => filteredAddressesAndIds.has(address)) - // Map it with token address as key for easy access - .map((token) => [token.address, token]), - ) + const tokensConfig = tokenListApi.getTokens(networkId) - const promises: Promise[] = [] + // Get filtered ids and addresses + const filteredAddressesAndIds = await _getFilteredIdsMap(networkId, numTokens, tokensConfig) - // Go over all value (id) and key (tokenAddress) pairs registered on the contract + const tokensConfigMap = new Map(tokensConfig.map((t) => [t.address, t])) + const tokenDetailsPromises: (Promise | TokenDetails)[] = [] filteredAddressesAndIds.forEach((id, tokenAddress) => { - if (!localAddressesMap.has(tokenAddress)) { - // New token not in our local list, fetch erc20 details for it - logDebug(`[tokenListFactory][${networkId}] Address ${tokenAddress} with id ${id} not in local list, fetching`) - promises.push(getErc20DetailsOrAddress(networkId, tokenAddress)) + const token = tokensConfigMap.get(tokenAddress) + if (token) { + tokenDetailsPromises.push(token) } else { - // Token already exists, update id - logDebug(`[tokenListFactory][${networkId}] Address ${tokenAddress} already in the list, updating id to ${id}`) - const token = localAddressesMap.get(tokenAddress) as TokenDetails - token.id = id - } - }) - - const partialTokens = await Promise.all(promises) - - // For all newly fetched tokens - partialTokens.forEach((partialToken) => { - if (typeof partialToken === 'string') { - // We replaced potential null responses with original tokenAddress string for logging purposes - logDebug(`[tokenListFactory][${networkId}] Address ${partialToken} is not a valid ERC20 token`) - } else if (partialToken) { - // If we got a valid response - logDebug( - `[tokenListFactory][${networkId}] Got details for address ${partialToken.address}: symbol '${partialToken.symbol}' name '${partialToken.name}'`, - ) - // Get id from address/id mapping - const id = filteredAddressesAndIds.get(partialToken.address) as number - // build token object - const token: TokenDetails = { ...partialToken, id } - // add to local address map - localAddressesMap.set(partialToken.address, token) + const fullTokenPromise = getErc20DetailsOrAddress(networkId, tokenAddress).then((partialToken) => { + if (typeof partialToken === 'string') { + // We replaced potential null responses with original tokenAddress string for logging purposes + logDebug(`[tokenListFactory][${networkId}] Address ${partialToken} is not a valid ERC20 token`) + return + } + // If we got a valid response + logDebug( + `[tokenListFactory][${networkId}] Got details for address ${partialToken.address}: symbol '${partialToken.symbol}' name '${partialToken.name}'`, + ) + // build token object + const token: TokenDetails = { ...partialToken, id } + + return token + }) + tokenDetailsPromises.push(fullTokenPromise) } }) - // Convert map values to a list. Map keeps insertion order, so new tokens are added at the end - const tokenList = Array.from(localAddressesMap.values()) + const tokenDetails = (await Promise.all(tokenDetailsPromises)).filter(notEmpty) // Persist it \o/ - tokenListApi.persistTokens({ networkId, tokenList }) + tokenListApi.persistTokens({ networkId, tokenList: tokenDetails }) } async function updateTokens(networkId: number): Promise { From b8ce9b5dd60a8a98a890cfdc861823ec293b7480 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Wed, 7 Oct 2020 12:50:52 +0200 Subject: [PATCH 07/11] Refactor fetch into it's own function --- src/services/factories/tokenList.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 3b9c87af8..983f5d468 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -167,15 +167,14 @@ export function getTokensFactory(factoryParams: { return new Map(filteredAddressAndIds) } - async function updateTokenDetails(networkId: number, numTokens: number): Promise { - const tokensConfig = tokenListApi.getTokens(networkId) - - // Get filtered ids and addresses - const filteredAddressesAndIds = await _getFilteredIdsMap(networkId, numTokens, tokensConfig) - + async function _fetchTokenDetails( + networkId: number, + addressToIdMap: Map, + tokensConfig: TokenDetails[], + ): Promise { const tokensConfigMap = new Map(tokensConfig.map((t) => [t.address, t])) const tokenDetailsPromises: (Promise | TokenDetails)[] = [] - filteredAddressesAndIds.forEach((id, tokenAddress) => { + addressToIdMap.forEach((id, tokenAddress) => { const token = tokensConfigMap.get(tokenAddress) if (token) { tokenDetailsPromises.push(token) @@ -199,9 +198,19 @@ export function getTokensFactory(factoryParams: { } }) - const tokenDetails = (await Promise.all(tokenDetailsPromises)).filter(notEmpty) + return (await Promise.all(tokenDetailsPromises)).filter(notEmpty) + } + + async function updateTokenDetails(networkId: number, numTokens: number): Promise { + const tokensConfig = tokenListApi.getTokens(networkId) + + // Get filtered ids and addresses + const filteredAddressesAndIds = await _getFilteredIdsMap(networkId, numTokens, tokensConfig) + + // Get token details for each filtered token + const tokenDetails = await _fetchTokenDetails(networkId, filteredAddressesAndIds, tokensConfig) - // Persist it \o/ + // Persist it tokenListApi.persistTokens({ networkId, tokenList: tokenDetails }) } From e6782f2008a23e83bd40b79dd8c7745b94681376 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Wed, 7 Oct 2020 13:02:37 +0200 Subject: [PATCH 08/11] Simplify logic --- src/services/factories/tokenList.ts | 50 +++++++++++++++++------------ 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/src/services/factories/tokenList.ts b/src/services/factories/tokenList.ts index 983f5d468..07b12506b 100644 --- a/src/services/factories/tokenList.ts +++ b/src/services/factories/tokenList.ts @@ -104,10 +104,9 @@ export function getTokensFactory(factoryParams: { } } - async function getErc20DetailsOrAddress(networkId: number, tokenAddress: string): Promise { + async function getErc20DetailsOrAddress(networkId: number, tokenAddress: string): Promise { // Simple wrapper function to return original address instead of null for make logging easier - const erc20Details = await getTokenFromErc20({ networkId, tokenAddress }) - return erc20Details || tokenAddress + return getTokenFromErc20({ networkId, tokenAddress }) } async function fetchAddressesAndIds(networkId: number, numTokens: number): Promise> { @@ -175,32 +174,41 @@ export function getTokensFactory(factoryParams: { const tokensConfigMap = new Map(tokensConfig.map((t) => [t.address, t])) const tokenDetailsPromises: (Promise | TokenDetails)[] = [] addressToIdMap.forEach((id, tokenAddress) => { - const token = tokensConfigMap.get(tokenAddress) + // Resolve the details using the config, otherwise fetch the token + const token: TokenDetails | undefined | Promise = tokensConfigMap.has(tokenAddress) + ? tokensConfigMap.get(tokenAddress) + : _fetchToken(networkId, id, tokenAddress) + if (token) { tokenDetailsPromises.push(token) - } else { - const fullTokenPromise = getErc20DetailsOrAddress(networkId, tokenAddress).then((partialToken) => { - if (typeof partialToken === 'string') { - // We replaced potential null responses with original tokenAddress string for logging purposes - logDebug(`[tokenListFactory][${networkId}] Address ${partialToken} is not a valid ERC20 token`) - return - } - // If we got a valid response - logDebug( - `[tokenListFactory][${networkId}] Got details for address ${partialToken.address}: symbol '${partialToken.symbol}' name '${partialToken.name}'`, - ) - // build token object - const token: TokenDetails = { ...partialToken, id } - - return token - }) - tokenDetailsPromises.push(fullTokenPromise) } }) return (await Promise.all(tokenDetailsPromises)).filter(notEmpty) } + async function _fetchToken( + networkId: number, + tokenId: number, + tokenAddress: string, + ): Promise { + const partialToken = await getErc20DetailsOrAddress(networkId, tokenAddress) + + if (!partialToken) { + logDebug(`[tokenListFactory][${networkId}] Address ${partialToken} is not a valid ERC20 token`) + return + } + + logDebug( + `[tokenListFactory][${networkId}] Got details for address ${partialToken.address}: symbol '${partialToken.symbol}' name '${partialToken.name}'`, + ) + + return { + ...partialToken, + id: tokenId, + } + } + async function updateTokenDetails(networkId: number, numTokens: number): Promise { const tokensConfig = tokenListApi.getTokens(networkId) From f349ded2cd313e89c693a6ef4ae23ea0397e6a37 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 8 Oct 2020 17:25:22 +0200 Subject: [PATCH 09/11] Add label --- src/types/index.ts | 1 + 1 file changed, 1 insertion(+) 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 } From e0466896167c32fb019694a6017b2efddf06c339 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 8 Oct 2020 17:29:24 +0200 Subject: [PATCH 10/11] Provide a deterministic sorting --- src/const.ts | 1 + src/services/factories/tokenList.ts | 62 ++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) 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/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 { From 8ff5c713a7830d596eba9957be779b307866283e Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Fri, 9 Oct 2020 12:35:16 +0200 Subject: [PATCH 11/11] Add label in missing places --- src/api/tokenList/tokenList.ts | 12 ++++++++++-- src/reducers-actions/trade.ts | 6 +++--- src/services/factories/addTokenToExchange.ts | 3 ++- src/services/factories/getTokenFromExchange.ts | 4 +++- src/services/index.ts | 3 ++- src/storybook/data/tokens.ts | 7 +++++++ src/storybook/data/tokensConfig.ts | 17 ++++++++++------- src/storybook/data/types.ts | 4 +++- test/api/ExchangeApi/TokenListApiMock.test.ts | 1 + .../DepositWidget.components.test.tsx | 1 + test/data/tokenList.ts | 8 ++++++++ 11 files changed, 50 insertions(+), 16 deletions(-) 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/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/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/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,