Skip to content
This repository has been archived by the owner on Jan 15, 2021. It is now read-only.

Deterministic sorting #1513

Merged
merged 13 commits into from
Oct 9, 2020
12 changes: 10 additions & 2 deletions src/api/tokenList/tokenList.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/reducers-actions/trade.ts
Original file line number Diff line number Diff line change
@@ -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<TokenDex> | null
buyToken: Required<TokenDex> | null
sellToken: Required<TokenDetails> | null
buyToken: Required<TokenDetails> | null
validFrom: string | null
validUntil: string | null
}
Expand Down
3 changes: 2 additions & 1 deletion src/services/factories/addTokenToExchange.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -52,6 +52,7 @@ export function addTokenToExchangeFactory(
const token: TokenDetails = {
...erc20Info,
id,
label: safeTokenName(erc20Info),
}

// TODO: cache new token
Expand Down
4 changes: 3 additions & 1 deletion src/services/factories/getTokenFromExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,6 +86,7 @@ function getTokenFromExchangeByAddressFactory(
token = {
...erc20token,
id: tokenId,
label: safeTokenName(erc20token),
}
}

Expand Down Expand Up @@ -149,6 +150,7 @@ function getTokenFromExchangeByIdFactory(
return {
...erc20token,
id: tokenId,
label: safeTokenName(erc20token),
}
}

Expand Down
62 changes: 57 additions & 5 deletions src/services/factories/tokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -175,18 +176,56 @@ export function getTokensFactory(factoryParams: {
const tokenDetailsPromises: (Promise<TokenDetails | undefined> | TokenDetails)[] = []
addressToIdMap.forEach((id, tokenAddress) => {
// Resolve the details using the config, otherwise fetch the token
const token: TokenDetails | undefined | Promise<TokenDetails | undefined> = tokensConfigMap.has(tokenAddress)
? tokensConfigMap.get(tokenAddress)
const token: undefined | Promise<TokenDetails | undefined> = tokensConfigMap.has(tokenAddress)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we in the future call unresolved vars promised<varName>? I find it easier to read.

? 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
})
Comment on lines +185 to +190
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we don't await this specific promise, it could be a race condition
It won't be a race condition in our case because this microtask is scheduled before await Promise.all, but still a bad approach.


tokenDetailsPromises.push(token)
}
})

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
}
Comment on lines +199 to +209
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, no 😢
This is like 3 loops more than necessary


function _sortTokens(networkId: number, tokens: TokenDetails[]): TokenDetails[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do the same in one pass.

// 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)
Comment on lines +222 to +223
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, no no 😭

default:
return tokensSorted
}
}

async function _fetchToken(
networkId: number,
tokenId: number,
Expand All @@ -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
}
}

Expand All @@ -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)
Comment on lines +261 to +262
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is misleading, Sort is mutative, so we don't get a new tokenList out of that. One might be inclined to treat tokenDetails as unchanged after this line, though we don't do it.

Just writing

_sortTokens(networkId, tokenDetails)

explicitly shows that tokenDetails was mutated


// 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
Comment on lines +269 to +274
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

considering what a lable may be (unicode string)and for simpicity, I recommend String.localeCompare (or Intl.collator.compare if you fancy that)

}

async function updateTokens(networkId: number): Promise<void> {
Expand Down
3 changes: 2 additions & 1 deletion src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -181,6 +181,7 @@ export const fetchTokenData = async ({
const token: TokenDetails = {
...erc20Token,
id: tokenId,
label: safeTokenName(erc20Token),
}

return {
Expand Down
7 changes: 7 additions & 0 deletions src/storybook/data/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ 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',
decimals: 18,
}
export const DAI: DefaultTokenDetails = {
id: 2,
label: 'DAI',
name: 'DAI Stablecoin',
address: '0x2',
symbol: 'DAI',
Expand All @@ -23,6 +25,7 @@ export const DAI: DefaultTokenDetails = {

export const baseTokenDefault: DefaultTokenDetails = {
id: 3,
label: 'BASE',
name: 'Base Token',
symbol: 'BASE',
address: '0x3',
Expand All @@ -31,6 +34,7 @@ export const baseTokenDefault: DefaultTokenDetails = {

export const quoteTokenDefault: DefaultTokenDetails = {
id: 4,
label: 'QUOTE',
name: 'Quote Token',
symbol: 'QUOTE',
address: '0x4',
Expand All @@ -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',
Expand All @@ -47,6 +52,7 @@ export const longNamedToken: DefaultTokenDetails = {

export const emojiToken: DefaultTokenDetails = {
id: 6,
label: '🍤🍤🍤',
name: 'Emoji 🍤 Token',
symbol: '🍤🍤🍤',
address: '0x6',
Expand All @@ -55,6 +61,7 @@ export const emojiToken: DefaultTokenDetails = {

export const weirdSymbolToken: DefaultTokenDetails = {
id: 7,
label: '!._$[]{…<>}@#¢$%&/()=?',
name: 'Token-!._$[]{…<>}@#¢$%&/()=?',
symbol: '!._$[]{…<>}@#¢$%&/()=?',
address: '0x7',
Expand Down
17 changes: 10 additions & 7 deletions src/storybook/data/tokensConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/storybook/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ import { WithAddress, WithDecimals, WithId, WithSymbolAndName } from '@gnosis.pm
import { Network } from 'types'

export type NetworkMap = Record<keyof typeof Network, Network>
export type DefaultTokenDetails = Required<WithId & WithSymbolAndName & WithAddress & WithDecimals>
export interface DefaultTokenDetails extends Required<WithId & WithSymbolAndName & WithAddress & WithDecimals> {
label: string
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum Network {
}

export interface TokenDetails extends TokenDex {
label: string
disabled?: boolean
override?: TokenOverride
}
Expand Down
1 change: 1 addition & 0 deletions test/api/ExchangeApi/TokenListApiMock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ beforeEach(() => {

const NEW_TOKEN = {
id: 7,
label: 'NTK',
name: 'New Token',
symbol: 'NTK',
decimals: 18,
Expand Down
1 change: 1 addition & 0 deletions test/components/DepositWidget.components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const fakeRowState: Record<keyof TokenLocalState, boolean> = {
const initialEthBalance = TEN
const initialTokenBalanceDetails = {
id: 1,
label: 'TTT',
name: 'Test token',
symbol: 'TTT',
decimals: 18,
Expand Down
8 changes: 8 additions & 0 deletions test/data/tokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down