(Feature) Balances enhancements (#1122)
* Fix ts error Add type return on fetchCurrencyValues * Add skeleton for loading balance value * Fix texts in uppercase * Adds ETH Icon in currencyValues dropdown * Adds getExchangeRatesUrlFallback Adds support for ETH as currency * Alphabetically sort currencies * Add types * Type formatAmount * Adds formatAmountInUsFormat util function * Add types Uses formatAmountInUsFormat for BALANCE_TABLE_BALANCE_ID * Updates max and min fraction digits on formatAmountInUsFormat Add tests * Updates max and min fraction digits on formatAmountInUsFormat Add tests * Add types * Fix currencyValues types * Adds safeFiatBalancesTotalSelector * Adds total balance to safe header * Fix types * Adds currentCurrency on header * Adds types to getTokenPriceInCurrency * Fix balance currency rate conversion * Add guards for modules * Add guards for modules * Uses console error for api * Remove anys * Redefine CurrencyRateValue types into CurrencyRateValueRecord * Redefine test texts * Use absolute imports * Add types to dispatch * Add guard for no balance value * Fix ESLINT warning * Add types * Fix no balance case * Use optional chaining * Absolute paths * Adds return types Uses BigNumber in safeFiatBalancesTotalSelector * Remove number as type for formatAmountInUsFormat * Uses createStyles to remove any types * Improve total balances display * Fix balances value column * formatAmountInUsFormat feedback * Force boolean evaluation * Fix totalBalance heading styles * Add types * Add types to fetchTokenCurrenciesBalances endpoint * Replaces coinbase dependency by backend for ETH price in USD * Absolute paths * Replaces RecordOf<TokenProps> with Token * Feedback * Trigger buid * Types * Fix tests order * Renames numberFormat to usNumberFormatter Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>
This commit is contained in:
parent
8a6b219781
commit
bbfa7d8166
|
@ -2,7 +2,7 @@ import { List } from 'immutable'
|
|||
|
||||
export const FIXED = 'fixed'
|
||||
|
||||
export const buildOrderFieldFrom = (attr) => `${attr}Order`
|
||||
export const buildOrderFieldFrom = (attr: string): string => `${attr}Order`
|
||||
|
||||
const desc = (a, b, orderBy, orderProp) => {
|
||||
const order = orderProp ? buildOrderFieldFrom(orderBy) : orderBy
|
||||
|
@ -38,5 +38,10 @@ export const stableSort = (dataArray, cmp, fixed) => {
|
|||
return fixedElems.concat(sortedElems)
|
||||
}
|
||||
|
||||
export const getSorting = (order, orderBy, orderProp) =>
|
||||
order === 'desc' ? (a, b) => desc(a, b, orderBy, orderProp) : (a, b) => -desc(a, b, orderBy, orderProp)
|
||||
export const getSorting = (
|
||||
order: string,
|
||||
orderBy?: string,
|
||||
orderProp?: boolean,
|
||||
): ((a: unknown, b: unknown) => number) => {
|
||||
return order === 'desc' ? (a, b) => desc(a, b, orderBy, orderProp) : (a, b) => -desc(a, b, orderBy, orderProp)
|
||||
}
|
||||
|
|
|
@ -87,6 +87,8 @@ export const getIntercomId = () =>
|
|||
|
||||
export const getExchangeRatesUrl = () => 'https://api.exchangeratesapi.io/latest'
|
||||
|
||||
export const getExchangeRatesUrlFallback = () => 'https://api.coinbase.com/v2/exchange-rates'
|
||||
|
||||
export const getSafeLastVersion = () => process.env.REACT_APP_LATEST_SAFE_VERSION || '1.1.1'
|
||||
|
||||
export const buildSafeCreationTxUrl = (safeAddress) => {
|
||||
|
|
|
@ -2,20 +2,38 @@ import axios from 'axios'
|
|||
|
||||
import { getExchangeRatesUrl } from 'src/config'
|
||||
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
|
||||
import fetchTokenCurrenciesBalances from './fetchTokenCurrenciesBalances'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
const fetchCurrenciesRates = async (
|
||||
baseCurrency: AVAILABLE_CURRENCIES,
|
||||
targetCurrencyValue: AVAILABLE_CURRENCIES,
|
||||
safeAddress: string,
|
||||
): Promise<number> => {
|
||||
let rate = 0
|
||||
const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
|
||||
|
||||
const result = await axios.get(url)
|
||||
if (result && result.data) {
|
||||
const { rates } = result.data
|
||||
rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0
|
||||
if (targetCurrencyValue === AVAILABLE_CURRENCIES.ETH) {
|
||||
try {
|
||||
const result = await fetchTokenCurrenciesBalances(safeAddress)
|
||||
if (result?.data?.length) {
|
||||
rate = new BigNumber(1).div(result.data[0].usdConversion).toNumber()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetching ETH data from the relayer errored', error)
|
||||
}
|
||||
return rate
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
|
||||
const result = await axios.get(url)
|
||||
if (result?.data) {
|
||||
const { rates } = result.data
|
||||
rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetching data from getExchangeRatesUrl errored', error)
|
||||
}
|
||||
return rate
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
import axios from 'axios'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
|
||||
import { getTxServiceHost } from 'src/config'
|
||||
import { TokenProps } from '../../tokens/store/model/token'
|
||||
|
||||
const fetchTokenCurrenciesBalances = (safeAddress) => {
|
||||
type BalanceEndpoint = {
|
||||
balance: string
|
||||
balanceUsd: string
|
||||
tokenAddress?: string
|
||||
token?: TokenProps
|
||||
usdConversion: string
|
||||
}
|
||||
|
||||
const fetchTokenCurrenciesBalances = (safeAddress?: string): Promise<AxiosResponse<BalanceEndpoint[]>> => {
|
||||
if (!safeAddress) {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates'
|
||||
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => async (dispatch) => {
|
||||
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => async (
|
||||
dispatch: Dispatch<typeof setCurrencyRate>,
|
||||
): Promise<void> => {
|
||||
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {
|
||||
return dispatch(setCurrencyRate(safeAddress, 1))
|
||||
}
|
||||
|
@ -10,6 +13,7 @@ const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURR
|
|||
const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates(
|
||||
AVAILABLE_CURRENCIES.USD,
|
||||
selectedCurrency,
|
||||
safeAddress,
|
||||
)
|
||||
|
||||
dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency))
|
||||
|
|
|
@ -6,10 +6,13 @@ import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurre
|
|||
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||
import { AVAILABLE_CURRENCIES, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { loadCurrencyValues } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
export const fetchCurrencyValues = (safeAddress: string) => async (dispatch) => {
|
||||
export const fetchCurrencyValues = (safeAddress: string) => async (
|
||||
dispatch: Dispatch<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const storedCurrencies: Map<string, CurrencyRateValue> | any = await loadCurrencyValues()
|
||||
const storedCurrencies: Map<string, CurrencyRateValue> | unknown = await loadCurrencyValues()
|
||||
const storedCurrency = storedCurrencies[safeAddress]
|
||||
if (!storedCurrency) {
|
||||
return batch(() => {
|
||||
|
|
|
@ -1,42 +1,43 @@
|
|||
import { Record } from 'immutable'
|
||||
import { List, Record, RecordOf } from 'immutable'
|
||||
|
||||
export enum AVAILABLE_CURRENCIES {
|
||||
ETH = 'ETH',
|
||||
USD = 'USD',
|
||||
EUR = 'EUR',
|
||||
CAD = 'CAD',
|
||||
HKD = 'HKD',
|
||||
ISK = 'ISK',
|
||||
PHP = 'PHP',
|
||||
DKK = 'DKK',
|
||||
HUF = 'HUF',
|
||||
CZK = 'CZK',
|
||||
AUD = 'AUD',
|
||||
RON = 'RON',
|
||||
SEK = 'SEK',
|
||||
IDR = 'IDR',
|
||||
INR = 'INR',
|
||||
BRL = 'BRL',
|
||||
RUB = 'RUB',
|
||||
HRK = 'HRK',
|
||||
JPY = 'JPY',
|
||||
THB = 'THB',
|
||||
CHF = 'CHF',
|
||||
SGD = 'SGD',
|
||||
PLN = 'PLN',
|
||||
BGN = 'BGN',
|
||||
TRY = 'TRY',
|
||||
BRL = 'BRL',
|
||||
CAD = 'CAD',
|
||||
CHF = 'CHF',
|
||||
CNY = 'CNY',
|
||||
CZK = 'CZK',
|
||||
DKK = 'DKK',
|
||||
GBP = 'GBP',
|
||||
HKD = 'HKD',
|
||||
HRK = 'HRK',
|
||||
HUF = 'HUF',
|
||||
IDR = 'IDR',
|
||||
ILS = 'ILS',
|
||||
INR = 'INR',
|
||||
ISK = 'ISK',
|
||||
JPY = 'JPY',
|
||||
KRW = 'KRW',
|
||||
MXN = 'MXN',
|
||||
MYR = 'MYR',
|
||||
NOK = 'NOK',
|
||||
NZD = 'NZD',
|
||||
PHP = 'PHP',
|
||||
PLN = 'PLN',
|
||||
RON = 'RON',
|
||||
RUB = 'RUB',
|
||||
SEK = 'SEK',
|
||||
SGD = 'SGD',
|
||||
THB = 'THB',
|
||||
TRY = 'TRY',
|
||||
ZAR = 'ZAR',
|
||||
MXN = 'MXN',
|
||||
ILS = 'ILS',
|
||||
GBP = 'GBP',
|
||||
KRW = 'KRW',
|
||||
MYR = 'MYR',
|
||||
}
|
||||
|
||||
type BalanceCurrencyRecord = {
|
||||
export type BalanceCurrencyRecord = {
|
||||
currencyName?: string
|
||||
tokenAddress?: string
|
||||
balanceInBaseCurrency: string
|
||||
|
@ -46,9 +47,11 @@ type BalanceCurrencyRecord = {
|
|||
export type CurrencyRateValue = {
|
||||
currencyRate?: number
|
||||
selectedCurrency?: AVAILABLE_CURRENCIES
|
||||
currencyBalances?: BalanceCurrencyRecord[]
|
||||
currencyBalances?: List<BalanceCurrencyRecord>
|
||||
}
|
||||
|
||||
export type CurrencyRateValueRecord = RecordOf<CurrencyRateValue>
|
||||
|
||||
export const makeBalanceCurrency = Record({
|
||||
currencyName: '',
|
||||
tokenAddress: '',
|
||||
|
|
|
@ -1,29 +1,58 @@
|
|||
import { List } from 'immutable'
|
||||
import { List, Map, RecordOf } from 'immutable'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { CURRENCY_VALUES_KEY } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { AppReduxState } from 'src/store/index'
|
||||
import {
|
||||
AVAILABLE_CURRENCIES,
|
||||
BalanceCurrencyRecord,
|
||||
CurrencyRateValue,
|
||||
CurrencyRateValueRecord,
|
||||
} from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
|
||||
export const currencyValuesSelector = (state) => state[CURRENCY_VALUES_KEY]
|
||||
export const currencyValuesSelector = (state: AppReduxState): Map<string, RecordOf<CurrencyRateValue>> =>
|
||||
state[CURRENCY_VALUES_KEY]
|
||||
|
||||
export const safeFiatBalancesSelector = createSelector(
|
||||
currencyValuesSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(currencyValues, safeAddress) => {
|
||||
(currencyValues, safeAddress): CurrencyRateValueRecord => {
|
||||
if (!currencyValues) return
|
||||
return currencyValues.get(safeAddress)
|
||||
},
|
||||
)
|
||||
|
||||
export const safeFiatBalancesListSelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap) => {
|
||||
if (!currencyValuesMap) return
|
||||
return currencyValuesMap.get('currencyBalances') ? currencyValuesMap.get('currencyBalances') : List([])
|
||||
})
|
||||
|
||||
export const currentCurrencySelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap) =>
|
||||
currencyValuesMap ? currencyValuesMap.get('selectedCurrency') : null,
|
||||
export const safeFiatBalancesListSelector = createSelector(
|
||||
safeFiatBalancesSelector,
|
||||
(currencyValuesMap): List<BalanceCurrencyRecord> => {
|
||||
if (!currencyValuesMap) return
|
||||
return currencyValuesMap.get('currencyBalances') ? currencyValuesMap.get('currencyBalances') : List([])
|
||||
},
|
||||
)
|
||||
|
||||
export const currencyRateSelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap) =>
|
||||
export const currentCurrencySelector = createSelector(
|
||||
safeFiatBalancesSelector,
|
||||
(currencyValuesMap): AVAILABLE_CURRENCIES | null =>
|
||||
currencyValuesMap ? currencyValuesMap.get('selectedCurrency') : null,
|
||||
)
|
||||
|
||||
export const currencyRateSelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap): number | null =>
|
||||
currencyValuesMap ? currencyValuesMap.get('currencyRate') : null,
|
||||
)
|
||||
|
||||
export const safeFiatBalancesTotalSelector = createSelector(
|
||||
safeFiatBalancesListSelector,
|
||||
currencyRateSelector,
|
||||
(currencyBalances, currencyRate): string | null => {
|
||||
if (!currencyBalances) return '0'
|
||||
if (!currencyRate) return null
|
||||
|
||||
const totalInBaseCurrency = currencyBalances.reduce((total, balanceCurrencyRecord) => {
|
||||
return total.plus(balanceCurrencyRecord.balanceInBaseCurrency)
|
||||
}, new BigNumber(0))
|
||||
|
||||
return totalInBaseCurrency.times(currencyRate).toFixed(2)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import { CurrencyRateValue } from '../model/currencyValues'
|
|||
import { Map } from 'immutable'
|
||||
|
||||
const CURRENCY_VALUES_STORAGE_KEY = 'CURRENCY_VALUES_STORAGE_KEY'
|
||||
export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyRateValue>) => {
|
||||
export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyRateValue>): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(CURRENCY_VALUES_STORAGE_KEY, currencyValues)
|
||||
} catch (err) {
|
||||
|
@ -11,6 +11,6 @@ export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyRat
|
|||
}
|
||||
}
|
||||
|
||||
export const loadCurrencyValues = async (): Promise<Map<string, CurrencyRateValue> | any> => {
|
||||
export const loadCurrencyValues = async (): Promise<Map<string, CurrencyRateValue> | unknown> => {
|
||||
return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ const lt100mFormatter = new Intl.NumberFormat([], { maximumFractionDigits: 0 })
|
|||
// same format for billions and trillions
|
||||
const lt1000tFormatter = new Intl.NumberFormat([], { maximumFractionDigits: 3, notation: 'compact' } as any)
|
||||
|
||||
export const formatAmount = (number) => {
|
||||
let numberFloat: any = parseFloat(number)
|
||||
export const formatAmount = (number: string): string => {
|
||||
let numberFloat: number | string = parseFloat(number)
|
||||
|
||||
if (numberFloat === 0) {
|
||||
numberFloat = '0'
|
||||
|
@ -39,3 +39,11 @@ export const formatAmount = (number) => {
|
|||
|
||||
return numberFloat
|
||||
}
|
||||
|
||||
const options = { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 8 }
|
||||
const usNumberFormatter = new Intl.NumberFormat('en-US', options)
|
||||
|
||||
export const formatAmountInUsFormat = (amount: string): string => {
|
||||
const numberFloat: number = parseFloat(amount)
|
||||
return usNumberFormatter.format(numberFloat).replace('$', '')
|
||||
}
|
||||
|
|
|
@ -2,9 +2,6 @@ import TableCell from '@material-ui/core/TableCell'
|
|||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
//import CallMade from '@material-ui/icons/CallMade'
|
||||
//import CallReceived from '@material-ui/icons/CallReceived'
|
||||
//import classNames from 'classnames/bind'
|
||||
import { List } from 'immutable'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
@ -30,10 +27,29 @@ import {
|
|||
getBalanceData,
|
||||
} from 'src/routes/safe/components/Balances/dataFetcher'
|
||||
import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
|
||||
const Coins = (props) => {
|
||||
type Props = {
|
||||
showReceiveFunds: () => void
|
||||
showSendFunds: (tokenAddress: string) => void
|
||||
}
|
||||
|
||||
export type BalanceDataRow = List<{
|
||||
asset: {
|
||||
name: string
|
||||
address: string
|
||||
logoUri: string
|
||||
}
|
||||
assetOrder: string
|
||||
balance: string
|
||||
balanceOrder: number
|
||||
fixed: boolean
|
||||
value: string
|
||||
}>
|
||||
|
||||
const Coins = (props: Props): React.ReactElement => {
|
||||
const { showReceiveFunds, showSendFunds } = props
|
||||
const classes = useStyles()
|
||||
const columns = generateColumns()
|
||||
|
@ -43,7 +59,7 @@ const Coins = (props) => {
|
|||
const activeTokens = useSelector(extendedSafeTokensSelector)
|
||||
const currencyValues = useSelector(safeFiatBalancesListSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const [filteredData, setFilteredData] = React.useState(List())
|
||||
const [filteredData, setFilteredData] = React.useState<BalanceDataRow>(List())
|
||||
|
||||
React.useMemo(() => {
|
||||
setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate))
|
||||
|
@ -64,7 +80,7 @@ const Coins = (props) => {
|
|||
sortedData.map((row, index) => (
|
||||
<TableRow className={classes.hide} data-testid={BALANCE_ROW_TEST_ID} key={index} tabIndex={-1}>
|
||||
{autoColumns.map((column) => {
|
||||
const { align, id, width }: any = column
|
||||
const { align, id, width } = column
|
||||
let cellItem
|
||||
switch (id) {
|
||||
case BALANCE_TABLE_ASSET_ID: {
|
||||
|
@ -76,7 +92,16 @@ const Coins = (props) => {
|
|||
break
|
||||
}
|
||||
case BALANCE_TABLE_VALUE_ID: {
|
||||
cellItem = <div className={classes.currencyValueRow}>{row[id]}</div>
|
||||
// If there are no values for that row but we have balances, we display as '0.00 {CurrencySelected}'
|
||||
// In case we don't have balances, we display a skeleton
|
||||
const showCurrencyValueRow = row[id] || row[BALANCE_TABLE_BALANCE_ID]
|
||||
|
||||
cellItem =
|
||||
showCurrencyValueRow && selectedCurrency ? (
|
||||
<div className={classes.currencyValueRow}>{row[id] ? row[id] : `0.00 ${selectedCurrency}`}</div>
|
||||
) : (
|
||||
<Skeleton animation="wave" />
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
import { BigNumber } from 'bignumber.js'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { FIXED, buildOrderFieldFrom } from 'src/components/Table/sorting'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { FIXED } from 'src/components/Table/sorting'
|
||||
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { TableColumn } from 'src/components/Table/types'
|
||||
import { AVAILABLE_CURRENCIES, BalanceCurrencyRecord } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { Token } from 'src/logic/tokens/store/model/token'
|
||||
import { BalanceDataRow } from './Coins'
|
||||
|
||||
export const BALANCE_TABLE_ASSET_ID = 'asset'
|
||||
export const BALANCE_TABLE_BALANCE_ID = 'balance'
|
||||
export const BALANCE_TABLE_VALUE_ID = 'value'
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const getTokenPriceInCurrency = (token, currencySelected, currencyValues, currencyRate) => {
|
||||
const getTokenPriceInCurrency = (
|
||||
token: Token,
|
||||
currencySelected: AVAILABLE_CURRENCIES,
|
||||
currencyValues: List<BalanceCurrencyRecord>,
|
||||
currencyRate: number | null,
|
||||
): string => {
|
||||
if (!currencySelected) {
|
||||
return ''
|
||||
}
|
||||
|
@ -31,26 +38,28 @@ const getTokenPriceInCurrency = (token, currencySelected, currencyValues, curren
|
|||
const { balanceInBaseCurrency } = currencyValue
|
||||
const balance = new BigNumber(balanceInBaseCurrency).times(currencyRate).toFixed(2)
|
||||
|
||||
return `${balance} ${currencySelected}`
|
||||
return `${formatAmountInUsFormat(balance)} ${currencySelected}`
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export const getBalanceData = (activeTokens, currencySelected, currencyValues, currencyRate) => {
|
||||
const rows = activeTokens.map((token) => ({
|
||||
export const getBalanceData = (
|
||||
activeTokens: List<Token>,
|
||||
currencySelected: AVAILABLE_CURRENCIES,
|
||||
currencyValues: List<BalanceCurrencyRecord>,
|
||||
currencyRate: number,
|
||||
): BalanceDataRow => {
|
||||
return activeTokens.map((token) => ({
|
||||
[BALANCE_TABLE_ASSET_ID]: {
|
||||
name: token.name,
|
||||
logoUri: token.logoUri,
|
||||
address: token.address,
|
||||
symbol: token.symbol,
|
||||
},
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name,
|
||||
[BALANCE_TABLE_BALANCE_ID]: `${formatAmount(token.balance)} ${token.symbol}`,
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.balance),
|
||||
[FIXED]: token.get('symbol') === 'ETH',
|
||||
assetOrder: token.name,
|
||||
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance.toString())} ${token.symbol}`,
|
||||
balanceOrder: Number(token.balance),
|
||||
[FIXED]: token.symbol === 'ETH',
|
||||
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate),
|
||||
}))
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export const generateColumns = (): List<TableColumn> => {
|
||||
|
@ -102,7 +111,3 @@ export const generateColumns = (): List<TableColumn> => {
|
|||
|
||||
return List([assetColumn, balanceColumn, value, actions])
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export const filterByZero = (data, hideZero) =>
|
||||
data.filter((row) => (hideZero ? row[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)] !== 0 : true))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
|
@ -41,7 +41,10 @@ const INITIAL_STATE = {
|
|||
export const COINS_LOCATION_REGEX = /\/balances\/?$/
|
||||
export const COLLECTIBLES_LOCATION_REGEX = /\/balances\/collectibles$/
|
||||
|
||||
const Balances = (props) => {
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Balances = (): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [state, setState] = useState(INITIAL_STATE)
|
||||
|
||||
const address = useSelector(safeParamAddressFromStateSelector)
|
||||
|
@ -66,7 +69,7 @@ const Balances = (props) => {
|
|||
setState((prevState) => ({ ...prevState, [`show${action}`]: false }))
|
||||
}
|
||||
|
||||
const showSendFunds = (tokenAddress) => {
|
||||
const showSendFunds = (tokenAddress: string): void => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
sendFunds: {
|
||||
|
@ -95,7 +98,7 @@ const Balances = (props) => {
|
|||
manageTokensButton,
|
||||
receiveModal,
|
||||
tokenControls,
|
||||
} = props.classes
|
||||
} = classes
|
||||
const { erc721Enabled, sendFunds, showManageCollectibleModal, showReceive, showToken } = state
|
||||
|
||||
return (
|
||||
|
@ -227,4 +230,4 @@ const Balances = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(Balances)
|
||||
export default Balances
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { md, screenSm, secondary, xs } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
controls: {
|
||||
alignItems: 'center',
|
||||
boxSizing: 'border-box',
|
||||
|
@ -10,7 +11,7 @@ export const styles = () => ({
|
|||
assetTabs: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
order: '2',
|
||||
order: 2,
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
order: '1',
|
||||
|
@ -41,7 +42,7 @@ export const styles = () => ({
|
|||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
order: '1',
|
||||
order: 1,
|
||||
padding: '0 0 10px',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
|
|
|
@ -18,8 +18,11 @@ import { currentCurrencySelector } from 'src/logic/currencyValues/store/selector
|
|||
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { DropdownListTheme } from 'src/theme/mui'
|
||||
import { setImageToPlaceholder } from '../Balances/utils'
|
||||
import Img from 'src/components/layout/Img/index'
|
||||
import etherIcon from 'src/assets/icons/icon_etherTokens.svg'
|
||||
|
||||
const CurrencyDropdown = () => {
|
||||
const CurrencyDropdown = (): React.ReactElement => {
|
||||
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
@ -96,14 +99,23 @@ const CurrencyDropdown = () => {
|
|||
value={currencyName}
|
||||
>
|
||||
<ListItemIcon className={classes.iconLeft}>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.localFlag,
|
||||
'currency-flag',
|
||||
'currency-flag-lg',
|
||||
`currency-flag-${currencyName.toLowerCase()}`,
|
||||
)}
|
||||
/>
|
||||
{currencyName === AVAILABLE_CURRENCIES.ETH ? (
|
||||
<Img
|
||||
alt="ether"
|
||||
onError={setImageToPlaceholder}
|
||||
src={etherIcon}
|
||||
className={classNames(classes.etherFlag)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
classes.localFlag,
|
||||
'currency-flag',
|
||||
'currency-flag-lg',
|
||||
`currency-flag-${currencyName.toLowerCase()}`,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={currencyName} />
|
||||
{currencyName === selectedCurrency ? (
|
||||
|
|
|
@ -18,6 +18,13 @@ export const useDropdownStyles = makeStyles({
|
|||
height: '20px !important',
|
||||
width: '26px !important',
|
||||
},
|
||||
etherFlag: {
|
||||
backgroundPosition: '50% 50%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'contain',
|
||||
width: '26px',
|
||||
height: '26px',
|
||||
},
|
||||
iconLeft: {
|
||||
marginRight: '10px',
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import CallMade from '@material-ui/icons/CallMade'
|
||||
import CallReceived from '@material-ui/icons/CallReceived'
|
||||
import classNames from 'classnames/bind'
|
||||
|
@ -19,13 +19,28 @@ import { SAFE_VIEW_NAME_HEADING_TEST_ID } from 'src/routes/safe/components/Layou
|
|||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
|
||||
const LayoutHeader = (props) => {
|
||||
const { classes, onShow, showSendFunds } = props
|
||||
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors/index'
|
||||
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type Props = {
|
||||
onShow: (modalName: string) => void
|
||||
showSendFunds: (modalName: string) => void
|
||||
}
|
||||
|
||||
const LayoutHeader = (props: Props): React.ReactElement => {
|
||||
const { onShow, showSendFunds } = props
|
||||
const classes = useStyles(styles)
|
||||
const address = useSelector(safeParamAddressFromStateSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const name = useSelector(safeNameSelector)
|
||||
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
|
||||
const currentCurrency = useSelector(currentCurrencySelector)
|
||||
if (!address) return null
|
||||
|
||||
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
||||
|
||||
return (
|
||||
<Block className={classes.container} margin="xl">
|
||||
<Row className={classes.userInfo}>
|
||||
|
@ -34,6 +49,12 @@ const LayoutHeader = (props) => {
|
|||
<Row>
|
||||
<Heading className={classes.nameText} color="primary" tag="h2" testId={SAFE_VIEW_NAME_HEADING_TEST_ID}>
|
||||
{name}
|
||||
{!!formattedTotalBalance && !!currentCurrency && (
|
||||
<span className={classes.totalBalance}>
|
||||
{' '}
|
||||
| {formattedTotalBalance} {currentCurrency}
|
||||
</span>
|
||||
)}
|
||||
</Heading>
|
||||
{!granted && <Block className={classes.readonly}>Read Only</Block>}
|
||||
</Row>
|
||||
|
@ -87,4 +108,4 @@ const LayoutHeader = (props) => {
|
|||
</Block>
|
||||
)
|
||||
}
|
||||
export default withStyles(styles as any)(LayoutHeader)
|
||||
export default LayoutHeader
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { screenSm, secondaryText, sm, smallFontSize, xs } from 'src/theme/variables'
|
||||
import { screenSm, secondaryText, sm, smallFontSize, xs, disabled, fontSizeHeadingSm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core/styles'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
|
@ -90,4 +91,8 @@ export const styles = () => ({
|
|||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
},
|
||||
totalBalance: {
|
||||
color: disabled,
|
||||
fontSize: fontSizeHeadingSm,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -112,10 +112,10 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
|
|||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
BACK
|
||||
Back
|
||||
</Button>
|
||||
<Button color="primary" minWidth={140} type="submit" variant="contained">
|
||||
CHANGE
|
||||
Change
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Map } from 'immutable'
|
||||
import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router'
|
||||
import { createHashHistory } from 'history'
|
||||
import { applyMiddleware, combineReducers, compose, createStore, CombinedState } from 'redux'
|
||||
|
@ -31,6 +30,8 @@ import incomingTransactions, {
|
|||
import safe, { SAFE_REDUCER_ID, SafeReducerMap } from 'src/routes/safe/store/reducer/safe'
|
||||
import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
|
||||
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
|
||||
import { Map } from 'immutable'
|
||||
import { CurrencyRateValueRecord } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
|
||||
export const history = createHashHistory()
|
||||
|
||||
|
@ -75,7 +76,7 @@ export type AppReduxState = CombinedState<{
|
|||
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: Map<string, any>
|
||||
[INCOMING_TRANSACTIONS_REDUCER_ID]: Map<string, any>
|
||||
[NOTIFICATIONS_REDUCER_ID]: Map<string, any>
|
||||
[CURRENCY_VALUES_KEY]: Map<string, any>
|
||||
[CURRENCY_VALUES_KEY]: Map<string, CurrencyRateValueRecord>
|
||||
[COOKIES_REDUCER_ID]: Map<string, any>
|
||||
[ADDRESS_BOOK_REDUCER_ID]: Map<string, any>
|
||||
[CURRENT_SESSION_REDUCER_ID]: Map<string, any>
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
import { formatAmount, formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||
|
||||
|
||||
describe('formatAmount', () => {
|
||||
it('Given 0 returns 0', () => {
|
||||
// given
|
||||
const input = '0'
|
||||
const expectedResult = '0'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given 1 returns 1', () => {
|
||||
// given
|
||||
const input = '1'
|
||||
const expectedResult = '1'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = '19797.899'
|
||||
const expectedResult = '19,797.899'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given number > 0.001 && < 1000 returns the same number as string', () => {
|
||||
// given
|
||||
const input = 999
|
||||
const expectedResult = '999'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input.toString())
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => {
|
||||
// given
|
||||
const input = 9999
|
||||
const expectedResult = '9,999'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input.toString())
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => {
|
||||
// given
|
||||
const input = 99999
|
||||
const expectedResult = '99,999'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input.toString())
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => {
|
||||
// given
|
||||
const input = 999999
|
||||
const expectedResult = '999,999'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input.toString())
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => {
|
||||
// given
|
||||
const input = 9999999
|
||||
const expectedResult = '9,999,999'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input.toString())
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given number < 0.001 returns < 0.001', () => {
|
||||
// given
|
||||
const input = 0.000001
|
||||
const expectedResult = '< 0.001'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input.toString())
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given number > 10 ** 15 returns > 1000T', () => {
|
||||
// given
|
||||
const input = 10 ** 15 * 2
|
||||
const expectedResult = '> 1000T'
|
||||
|
||||
// when
|
||||
const result = formatAmount(input.toString())
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('FormatsAmountsInUsFormat', () => {
|
||||
it('Given 0 returns 0.00', () => {
|
||||
// given
|
||||
const input = 0
|
||||
const expectedResult = '0.00'
|
||||
|
||||
// when
|
||||
const result = formatAmountInUsFormat(input.toString())
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given 1 returns 1.00', () => {
|
||||
// given
|
||||
const input = 1
|
||||
const expectedResult = '1.00'
|
||||
|
||||
// when
|
||||
const result = formatAmountInUsFormat(input.toString())
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => {
|
||||
// given
|
||||
const input = 311137.30
|
||||
const expectedResult = '311,137.30'
|
||||
|
||||
// when
|
||||
const result = formatAmountInUsFormat(input.toString())
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = 19797.899
|
||||
const expectedResult = '19,797.899'
|
||||
|
||||
// when
|
||||
const result = formatAmountInUsFormat(input.toString())
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = 19797899.479
|
||||
const expectedResult = '19,797,899.479'
|
||||
|
||||
// when
|
||||
const result = formatAmountInUsFormat(input.toString())
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = 19797899479.999
|
||||
const expectedResult = '19,797,899,479.999'
|
||||
|
||||
// when
|
||||
const result = formatAmountInUsFormat(input.toString())
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue