diff --git a/components/Tooltip/StyledTooltip.tsx b/components/Tooltip/StyledTooltip.tsx index 70318c9495..478de99e64 100644 --- a/components/Tooltip/StyledTooltip.tsx +++ b/components/Tooltip/StyledTooltip.tsx @@ -7,6 +7,7 @@ interface ToolTipProps { content?: any; children?: React.ReactNode; width?: string; + height?: string; preset?: string; top?: string; bottom?: string; diff --git a/components/Tooltip/TooltipStyles.tsx b/components/Tooltip/TooltipStyles.tsx index 78646fce37..85804ae5f8 100644 --- a/components/Tooltip/TooltipStyles.tsx +++ b/components/Tooltip/TooltipStyles.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; interface ToolTipStyleProps { preset?: string; width?: string; + height?: string; top?: string; bottom?: string; left?: string; @@ -12,8 +13,9 @@ interface ToolTipStyleProps { } export const Tooltip = styled.div` - height: 56px; - width: ${(props) => props.width || '189px'}; + height: ${(props) => props.height || '56px'}; + width: max-content; + max-width: ${(props) => props.width || '472.5px'}; background: linear-gradient(180deg, #1E1D1D 0%, #161515 100%); border: 1px solid rgba(255, 255, 255, 0.1); box-sizing: border-box; @@ -56,6 +58,7 @@ export const Tooltip = styled.div` transform: translate(-25%, 125%); `} + ${(props) => props.preset === 'left' && ` diff --git a/queries/synths/useBaseFeeRateQuery.ts b/queries/synths/useBaseFeeRateQuery.ts index 83e3a21f77..e9eb983605 100644 --- a/queries/synths/useBaseFeeRateQuery.ts +++ b/queries/synths/useBaseFeeRateQuery.ts @@ -6,7 +6,6 @@ import { ethers } from 'ethers'; import { CurrencyKey } from 'constants/currency'; import { appReadyState } from 'store/app'; import QUERY_KEYS from 'constants/queryKeys'; -import Wei from '@synthetixio/wei'; const useBaseFeeRateQuery = ( sourceCurrencyKey: CurrencyKey | null, @@ -20,18 +19,14 @@ const useBaseFeeRateQuery = ( async () => { const { SystemSettings } = synthetixjs!.contracts; - const [sourceCurrencyFeeRate, destinationCurrencyFeeRate] = (await Promise.all([ - new Wei( - SystemSettings.exchangeFeeRate( - ethers.utils.formatBytes32String(sourceCurrencyKey as string) - ) + const [sourceCurrencyFeeRate, destinationCurrencyFeeRate] = await Promise.all([ + SystemSettings.exchangeFeeRate( + ethers.utils.formatBytes32String(sourceCurrencyKey as string) ), - new Wei( - SystemSettings.exchangeFeeRate( - ethers.utils.formatBytes32String(destinationCurrencyKey as string) - ) + SystemSettings.exchangeFeeRate( + ethers.utils.formatBytes32String(destinationCurrencyKey as string) ), - ])) as [Wei, Wei]; + ]); return sourceCurrencyFeeRate && destinationCurrencyFeeRate ? sourceCurrencyFeeRate.add(destinationCurrencyFeeRate) diff --git a/sections/dashboard/Overview/Overview.tsx b/sections/dashboard/Overview/Overview.tsx index 2acca08347..e3b8556efa 100644 --- a/sections/dashboard/Overview/Overview.tsx +++ b/sections/dashboard/Overview/Overview.tsx @@ -8,7 +8,16 @@ import useGetFuturesMarkets from 'queries/futures/useGetFuturesMarkets'; import useGetFuturesPositionForAccount from 'queries/futures/useGetFuturesPositionForAccount'; import FuturesPositionsTable from '../FuturesPositionsTable'; import FuturesMarketsTable from '../FuturesMarketsTable'; -import useExchangeRatesQuery from 'queries/rates/useExchangeRatesQuery'; +import { useRecoilValue } from 'recoil'; +import { walletAddressState } from 'store/wallet'; +import useSynthetixQueries from '@synthetixio/queries'; +import SynthBalancesTable from '../SynthBalancesTable'; +import { wei } from '@synthetixio/wei'; +import { formatCurrency, zeroBN } from 'utils/formatters/number'; +import { Synths } from '@synthetixio/contracts-interface'; +import { getMarketKey } from 'utils/futures'; +import useGetCurrentPortfolioValue from 'queries/futures/useGetCurrentPortfolioValue'; +import Connector from 'containers/Connector'; import SpotMarketsTable from '../SpotMarketsTable'; enum PositionsTab { @@ -25,18 +34,44 @@ enum MarketsTab { const Overview: FC = () => { const { t } = useTranslation(); + const { useExchangeRatesQuery, useSynthsBalancesQuery } = useSynthetixQueries(); + const futuresMarketsQuery = useGetFuturesMarkets(); const futuresMarkets = futuresMarketsQuery?.data ?? []; + const { network } = Connector.useContainer(); + const markets = futuresMarkets.map(({ asset }) => getMarketKey(asset, network.id)); + const portfolioValueQuery = useGetCurrentPortfolioValue(markets); + const portfolioValue = portfolioValueQuery?.data ?? null; + const futuresPositionQuery = useGetFuturesPositionForAccount(); const futuresPositionHistory = futuresPositionQuery?.data ?? []; const exchangeRatesQuery = useExchangeRatesQuery(); const exchangeRates = exchangeRatesQuery.isSuccess ? exchangeRatesQuery.data ?? null : null; + const walletAddress = useRecoilValue(walletAddressState); + const synthsBalancesQuery = useSynthsBalancesQuery(walletAddress); + const synthBalances = + synthsBalancesQuery.isSuccess && synthsBalancesQuery.data != null + ? synthsBalancesQuery.data + : null; + const [activePositionsTab, setActivePositionsTab] = useState(PositionsTab.FUTURES); const [activeMarketsTab, setActiveMarketsTab] = useState(MarketsTab.FUTURES); + const totalSpotBalancesValue = formatCurrency( + Synths.sUSD, + wei(synthBalances?.totalUSDBalance ?? zeroBN), + { + sign: '$', + } + ); + + const totalFuturesPortfolioValue = formatCurrency(Synths.sUSD, wei(portfolioValue ?? zeroBN), { + sign: '$', + }); + const POSITIONS_TABS = useMemo( () => [ { @@ -44,32 +79,37 @@ const Overview: FC = () => { label: t('dashboard.overview.positions-tabs.futures'), badge: futuresPositionQuery?.data?.length, active: activePositionsTab === PositionsTab.FUTURES, + detail: totalFuturesPortfolioValue, onClick: () => { setActivePositionsTab(PositionsTab.FUTURES); }, }, { - name: PositionsTab.SHORTS, - label: t('dashboard.overview.positions-tabs.shorts'), - badge: 3, - disabled: true, - active: activePositionsTab === PositionsTab.SHORTS, + name: PositionsTab.SPOT, + label: t('dashboard.overview.positions-tabs.spot'), + active: activePositionsTab === PositionsTab.SPOT, + detail: totalSpotBalancesValue, onClick: () => { - setActivePositionsTab(PositionsTab.SHORTS); + setActivePositionsTab(PositionsTab.SPOT); }, }, { - name: PositionsTab.SPOT, - label: t('dashboard.overview.positions-tabs.spot'), - badge: 3, + name: PositionsTab.SHORTS, + label: t('dashboard.overview.positions-tabs.shorts'), disabled: true, - active: activePositionsTab === PositionsTab.SPOT, + active: activePositionsTab === PositionsTab.SHORTS, onClick: () => { - setActivePositionsTab(PositionsTab.SPOT); + setActivePositionsTab(PositionsTab.SHORTS); }, }, ], - [activePositionsTab, futuresPositionQuery?.data, t] + [ + activePositionsTab, + futuresPositionQuery?.data?.length, + t, + totalFuturesPortfolioValue, + totalSpotBalancesValue, + ] ); const MARKETS_TABS = useMemo( @@ -96,16 +136,21 @@ const Overview: FC = () => { return ( <> - - - - {POSITIONS_TABS.map(({ name, label, badge, active, disabled, onClick }) => ( + + + + {POSITIONS_TABS.map(({ name, label, badge, active, disabled, detail, onClick }) => ( ))} @@ -117,9 +162,14 @@ const Overview: FC = () => { /> - + + + - + {MARKETS_TABS.map(({ name, label, active, onClick }) => ( @@ -137,13 +187,13 @@ const Overview: FC = () => { ); }; -const TabButtonsContainer = styled.div` +const TabButtonsContainer = styled.div<{ hasDetail?: boolean }>` display: flex; margin-top: 16px; margin-bottom: 16px; & > button { - height: 38px; + height: ${(props) => (props.hasDetail ? '48px' : '38px')}; font-size: 13px; &:not(:last-of-type) { diff --git a/sections/dashboard/PortfolioChart/PortfolioChart.tsx b/sections/dashboard/PortfolioChart/PortfolioChart.tsx index 0a73d6775e..78ff603359 100644 --- a/sections/dashboard/PortfolioChart/PortfolioChart.tsx +++ b/sections/dashboard/PortfolioChart/PortfolioChart.tsx @@ -1,30 +1,25 @@ import { FC } from 'react'; import styled from 'styled-components'; -import useGetCurrentPortfolioValue from 'queries/futures/useGetCurrentPortfolioValue'; -import { FuturesMarket } from 'queries/futures/types'; import { Synths } from 'constants/currency'; import Currency from 'components/Currency'; import { zeroBN } from 'utils/formatters/number'; -import { getMarketKey } from 'utils/futures'; -import Connector from 'containers/Connector'; +import Wei from '@synthetixio/wei'; type PortfolioChartProps = { - futuresMarkets: FuturesMarket[]; + totalFuturesPortfolioValue: Wei; + totalSpotBalanceValue: Wei; + totalShortsValue: Wei; }; -const PortfolioChart: FC = ({ futuresMarkets }: PortfolioChartProps) => { - const { network } = Connector.useContainer(); - - const markets = futuresMarkets.map(({ asset }) => getMarketKey(asset, network.id)); - const portfolioValueQuery = useGetCurrentPortfolioValue(markets); - const portfolioValue = portfolioValueQuery?.data ?? null; - +const PortfolioChart: FC = (props: PortfolioChartProps) => { + const { totalFuturesPortfolioValue, totalSpotBalanceValue, totalShortsValue } = props; + const total = totalFuturesPortfolioValue.add(totalSpotBalanceValue).add(totalShortsValue); return ( - Futures Portfolio Value + Portfolio Value diff --git a/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx new file mode 100644 index 0000000000..d1ffc7b02f --- /dev/null +++ b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx @@ -0,0 +1,268 @@ +import { Rates, SynthBalance } from '@synthetixio/queries'; +import { FC, ReactElement, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CellProps, Row } from 'react-table'; +import styled from 'styled-components'; +import Currency from 'components/Currency'; +import { NO_VALUE } from 'constants/placeholder'; +import Connector from 'containers/Connector'; +import { DEFAULT_DATA } from './constants'; +import Table from 'components/Table'; +import { Price } from 'queries/rates/types'; +import * as _ from 'lodash/fp'; +import { CurrencyKey, Synths } from '@synthetixio/contracts-interface'; +import Wei, { wei } from '@synthetixio/wei'; +import { formatNumber } from 'utils/formatters/number'; +import useLaggedDailyPrice from 'queries/rates/useLaggedDailyPrice'; +import ChangePercent from 'components/ChangePercent'; +import { isEurForex } from 'utils/futures'; + +type SynthBalancesTableProps = { + exchangeRates: Rates | null; + synthBalances: SynthBalance[]; +}; + +type Cell = { + synth: CurrencyKey; + description: string | undefined; + balance: Wei; + usdBalance: Wei; + price: Wei | null; + priceChange: number | undefined; +}; + +const calculatePriceChange = (current: Wei | null, past: Price | undefined): number | undefined => { + if (_.isNil(current) || _.isNil(past)) { + return undefined; + } + const currentPrice = current.toNumber(); + const pastPrice = past.price; + const priceChange = (currentPrice - pastPrice) / currentPrice; + return priceChange; +}; + +const conditionalRender = (prop: T, children: ReactElement): ReactElement => + _.isNil(prop) ? {NO_VALUE} : children; + +const SynthBalancesTable: FC = ({ + exchangeRates, + synthBalances, +}: SynthBalancesTableProps) => { + const { t } = useTranslation(); + const { synthsMap } = Connector.useContainer(); + + const synthNames = synthBalances.map(({ currencyKey }) => currencyKey); + const dailyPriceChangesQuery = useLaggedDailyPrice(synthNames); + + let data = useMemo(() => { + const dailyPriceChanges: Price[] = dailyPriceChangesQuery?.data ?? []; + return synthBalances.length > 0 + ? synthBalances.map((synthBalance: SynthBalance, i: number) => { + const { currencyKey, balance, usdBalance } = synthBalance; + + const price = exchangeRates && exchangeRates[currencyKey]; + const pastPrice = dailyPriceChanges.find((price: Price) => price.synth === currencyKey); + + const description = synthsMap != null ? synthsMap[currencyKey]?.description : ''; + return { + synth: currencyKey, + description, + balance, + usdBalance, + price, + priceChange: calculatePriceChange(price, pastPrice), + }; + }) + : DEFAULT_DATA; + }, [dailyPriceChangesQuery?.data, exchangeRates, synthBalances, synthsMap]); + + return ( + + {t('dashboard.overview.synth-balances-table.market')} + ), + accessor: 'market', + Cell: (cellProps: CellProps) => { + return conditionalRender( + cellProps.row.original.synth, + + + + + {cellProps.row.original.synth} + + {t('common.currency.synthetic-currency-name', { + currencyName: cellProps.row.original.description, + })} + + + ); + }, + width: 198, + }, + { + Header: ( + {t('dashboard.overview.synth-balances-table.amount')} + ), + accessor: 'amount', + Cell: (cellProps: CellProps) => { + return conditionalRender( + cellProps.row.original.balance, + +

{formatNumber(cellProps.row.original.balance ?? 0)}

+
+ ); + }, + width: 198, + sortable: true, + sortType: useMemo( + () => (rowA: Row, rowB: Row) => { + const rowOne = rowA.original.balance ?? wei(0); + const rowTwo = rowB.original.balance ?? wei(0); + return rowOne.toSortable() > rowTwo.toSortable() ? 1 : -1; + }, + [] + ), + }, + { + Header: ( + {t('dashboard.overview.synth-balances-table.value-in-usd')} + ), + accessor: 'valueInUSD', + Cell: (cellProps: CellProps) => { + return conditionalRender( + cellProps.row.original.usdBalance, + + ); + }, + width: 198, + sortable: true, + sortType: useMemo( + () => (rowA: Row, rowB: Row) => { + const rowOne = rowA.original.usdBalance ?? wei(0); + const rowTwo = rowB.original.usdBalance ?? wei(0); + return rowOne.toSortable() > rowTwo.toSortable() ? 1 : -1; + }, + [] + ), + }, + { + Header: ( + {t('dashboard.overview.synth-balances-table.oracle-price')} + ), + accessor: 'price', + Cell: (cellProps: CellProps) => { + return conditionalRender( + cellProps.row.original.price, + + ); + }, + width: 198, + sortable: true, + sortType: useMemo( + () => (rowA: Row, rowB: Row) => { + const rowOne = rowA.original.price ?? wei(0); + const rowTwo = rowB.original.price ?? wei(0); + return rowOne.toSortable() > rowTwo.toSortable() ? 1 : -1; + }, + [] + ), + }, + { + Header: ( + {t('dashboard.overview.synth-balances-table.daily-change')} + ), + accessor: 'priceChange', + Cell: (cellProps: CellProps) => { + return conditionalRender( + cellProps.row.original.priceChange, + + ); + }, + sortable: true, + sortType: useMemo( + () => (rowA: Row, rowB: Row) => { + const rowOne = rowA.original.priceChange ?? 0; + const rowTwo = rowB.original.priceChange ?? 0; + return rowOne > rowTwo ? 1 : -1; + }, + [] + ), + width: 105, + }, + ]} + /> +
+ ); +}; + +const StyledCurrencyIcon = styled(Currency.Icon)` + width: 30px; + height: 30px; + margin-right: 8px; +`; + +const IconContainer = styled.div` + grid-column: 1; + grid-row: 1 / span 2; +`; + +const StyledValue = styled.div` + color: ${(props) => props.theme.colors.common.secondaryGray}; + font-family: ${(props) => props.theme.fonts.regular}; + font-size: 12px; + grid-column: 2; + grid-row: 2; +`; + +const DefaultCell = styled.p``; + +const TableContainer = styled.div``; + +const StyledTable = styled(Table)` + /* margin-top: 20px; */ +`; + +const TableHeader = styled.div``; + +const StyledText = styled.div` + display: flex; + align-items: center; + grid-column: 2; + grid-row: 1; + margin-bottom: -4px; +`; + +const MarketContainer = styled.div` + display: grid; + grid-template-rows: auto auto; + grid-template-columns: auto auto; + align-items: center; +`; + +const AmountCol = styled.div` + justify-self: flex-end; +`; + +export default SynthBalancesTable; diff --git a/sections/dashboard/SynthBalancesTable/constants.ts b/sections/dashboard/SynthBalancesTable/constants.ts new file mode 100644 index 0000000000..d0d4d1cf18 --- /dev/null +++ b/sections/dashboard/SynthBalancesTable/constants.ts @@ -0,0 +1,10 @@ +export const DEFAULT_DATA = [ + { + synth: undefined, + description: undefined, + balance: undefined, + usdBalance: undefined, + price: undefined, + priceChange: undefined, + }, +]; diff --git a/sections/dashboard/SynthBalancesTable/index.ts b/sections/dashboard/SynthBalancesTable/index.ts new file mode 100644 index 0000000000..b4f00535ce --- /dev/null +++ b/sections/dashboard/SynthBalancesTable/index.ts @@ -0,0 +1 @@ +export { default } from './SynthBalancesTable'; diff --git a/sections/exchange/TradeCard/CurrencyCard/CurrencyCard.tsx b/sections/exchange/TradeCard/CurrencyCard/CurrencyCard.tsx index f95b5b78d3..7818580630 100644 --- a/sections/exchange/TradeCard/CurrencyCard/CurrencyCard.tsx +++ b/sections/exchange/TradeCard/CurrencyCard/CurrencyCard.tsx @@ -26,7 +26,6 @@ import { Side } from '../types'; import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import { TxProvider } from 'sections/shared/modals/TxConfirmationModal/TxConfirmationModal'; import Wei, { wei } from '@synthetixio/wei'; -import { DEFAULT_CRYPTO_DECIMALS } from 'constants/defaults'; import CurrencyIcon from 'components/Currency/CurrencyIcon'; import Connector from 'containers/Connector'; import Button from 'components/Button'; @@ -108,18 +107,14 @@ const CurrencyCard: FC = ({ > 0 - ? Number(amount).toFixed(DEFAULT_CRYPTO_DECIMALS).toString() - : amount - } + value={amount} onChange={(_, value) => onAmountChange(value)} placeholder={t('exchange.currency-card.amount-placeholder')} data-testid="currency-amount" /> {!isBase && ( - max + {t('exchange.currency-card.max-button')} )} diff --git a/sections/exchange/hooks/useExchange.tsx b/sections/exchange/hooks/useExchange.tsx index 864f1045af..ff7a4a8825 100644 --- a/sections/exchange/hooks/useExchange.tsx +++ b/sections/exchange/hooks/useExchange.tsx @@ -71,6 +71,7 @@ import { useGetL1SecurityFee } from 'hooks/useGetL1SecurityGasFee'; import useGas from 'hooks/useGas'; import { KWENTA_TRACKING_CODE } from 'queries/futures/constants'; import useExchangeFeeRateQuery from 'queries/synths/useExchangeFeeRateQuery'; +import { DEFAULT_CRYPTO_DECIMALS } from 'constants/defaults'; type ExchangeCardProps = { defaultBaseCurrencyKey?: string | null; @@ -492,7 +493,6 @@ const useExchange = ({ : false; const handleCurrencySwap = () => { - const baseAmount = baseCurrencyAmount; const quoteAmount = quoteCurrencyAmount; setCurrencyPair({ @@ -501,7 +501,7 @@ const useExchange = ({ }); setBaseCurrencyAmount(quoteAmount); - setQuoteCurrencyAmount(baseAmount); + setQuoteCurrencyAmount(''); if (quoteCurrencyKey != null && baseCurrencyKey != null) { routeToMarketPair(quoteCurrencyKey, baseCurrencyKey); @@ -562,19 +562,24 @@ const useExchange = ({ } if (txProvider === 'synthetix' && quoteCurrencyAmount !== '' && baseCurrencyKey != null) { const baseCurrencyAmountNoFee = wei(quoteCurrencyAmount).mul(rate); - const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 1); - setBaseCurrencyAmount(baseCurrencyAmountNoFee.sub(fee).toString()); + const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 0); + setBaseCurrencyAmount( + baseCurrencyAmountNoFee.sub(fee).toNumber().toFixed(DEFAULT_CRYPTO_DECIMALS).toString() + ); } - }, [ - rate, - baseCurrencyKey, - quoteCurrencyAmount, - baseCurrencyAmount, - exchangeFeeRate, - txProvider, - oneInchQuoteQuery.data, - oneInchQuoteQuery.isSuccess, - ]); + // eslint-disable-next-line + }, [quoteCurrencyKey, exchangeFeeRate, oneInchQuoteQuery.isSuccess, oneInchQuoteQuery.data]); + + useEffect(() => { + if (txProvider === 'synthetix' && baseCurrencyAmount !== '' && quoteCurrencyKey != null) { + const quoteCurrencyAmountNoFee = wei(baseCurrencyAmount).mul(inverseRate); + const fee = quoteCurrencyAmountNoFee.mul(exchangeFeeRate ?? 0); + setQuoteCurrencyAmount( + quoteCurrencyAmountNoFee.add(fee).toNumber().toFixed(DEFAULT_CRYPTO_DECIMALS).toString() + ); + } + // eslint-disable-next-line + }, [baseCurrencyKey, exchangeFeeRate]); const getExchangeParams = useCallback( (isAtomic: boolean) => { @@ -936,12 +941,19 @@ const useExchange = ({ onAmountChange={async (value) => { if (value === '') { setQuoteCurrencyAmount(''); + setBaseCurrencyAmount(''); } else { setQuoteCurrencyAmount(value); if (txProvider === 'synthetix' && baseCurrencyKey != null) { const baseCurrencyAmountNoFee = wei(value).mul(rate); - const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 1); - setBaseCurrencyAmount(baseCurrencyAmountNoFee.sub(fee).toString()); + const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 0); + setBaseCurrencyAmount( + baseCurrencyAmountNoFee + .sub(fee) + .toNumber() + .toFixed(DEFAULT_CRYPTO_DECIMALS) + .toString() + ); } } }} @@ -951,14 +963,26 @@ const useExchange = ({ if (quoteCurrencyKey === 'ETH') { const ETH_TX_BUFFER = 0.1; const balanceWithBuffer = quoteCurrencyBalance.sub(wei(ETH_TX_BUFFER)); - setQuoteCurrencyAmount(balanceWithBuffer.lt(0) ? '0' : balanceWithBuffer.toString()); + setQuoteCurrencyAmount( + balanceWithBuffer.lt(0) + ? '0' + : balanceWithBuffer.toNumber().toFixed(DEFAULT_CRYPTO_DECIMALS).toString() + ); } else { - setQuoteCurrencyAmount(quoteCurrencyBalance.toString()); + setQuoteCurrencyAmount( + quoteCurrencyBalance.toNumber().toFixed(DEFAULT_CRYPTO_DECIMALS).toString() + ); } if (txProvider === 'synthetix') { const baseCurrencyAmountNoFee = quoteCurrencyBalance.mul(rate); - const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 1); - setBaseCurrencyAmount(baseCurrencyAmountNoFee.sub(fee).toString()); + const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 0); + setBaseCurrencyAmount( + baseCurrencyAmountNoFee + .sub(fee) + .toNumber() + .toFixed(DEFAULT_CRYPTO_DECIMALS) + .toString() + ); } } }} @@ -1032,24 +1056,39 @@ const useExchange = ({ onAmountChange={async (value) => { if (value === '') { setBaseCurrencyAmount(''); + setQuoteCurrencyAmount(''); } else { setBaseCurrencyAmount(value); if (txProvider === 'synthetix' && baseCurrencyKey != null) { const quoteCurrencyAmountNoFee = wei(value).mul(inverseRate); - const fee = quoteCurrencyAmountNoFee.mul(exchangeFeeRate ?? 1); - setQuoteCurrencyAmount(quoteCurrencyAmountNoFee.add(fee).toString()); + const fee = quoteCurrencyAmountNoFee.mul(exchangeFeeRate ?? 0); + setQuoteCurrencyAmount( + quoteCurrencyAmountNoFee + .add(fee) + .toNumber() + .toFixed(DEFAULT_CRYPTO_DECIMALS) + .toString() + ); } } }} walletBalance={baseCurrencyBalance} onBalanceClick={async () => { if (baseCurrencyBalance != null) { - setBaseCurrencyAmount(baseCurrencyBalance.toString()); + setBaseCurrencyAmount( + baseCurrencyBalance.toNumber().toFixed(DEFAULT_CRYPTO_DECIMALS).toString() + ); if (txProvider === 'synthetix') { const baseCurrencyAmountNoFee = baseCurrencyBalance.mul(inverseRate); - const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 1); - setQuoteCurrencyAmount(baseCurrencyAmountNoFee.add(fee).toString()); + const fee = baseCurrencyAmountNoFee.mul(exchangeFeeRate ?? 0); + setQuoteCurrencyAmount( + baseCurrencyAmountNoFee + .add(fee) + .toNumber() + .toFixed(DEFAULT_CRYPTO_DECIMALS) + .toString() + ); } } }} @@ -1071,7 +1110,7 @@ const useExchange = ({ setSelectBaseCurrencyModalOpen(false)} onSelect={(currencyKey) => { - setBaseCurrencyAmount(''); + setQuoteCurrencyAmount(''); // @ts-ignore setCurrencyPair((pair) => ({ base: currencyKey, @@ -1092,7 +1131,7 @@ const useExchange = ({ setSelectBaseTokenModalOpen(false)} onSelect={(currencyKey) => { - setBaseCurrencyAmount(''); + setQuoteCurrencyAmount(''); // @ts-ignore setCurrencyPair((pair) => ({ base: currencyKey, diff --git a/sections/futures/MarketDetails/MarketDetails.tsx b/sections/futures/MarketDetails/MarketDetails.tsx index 1342695f9e..db7ed21b46 100644 --- a/sections/futures/MarketDetails/MarketDetails.tsx +++ b/sections/futures/MarketDetails/MarketDetails.tsx @@ -24,6 +24,7 @@ import { DEFAULT_FIAT_EURO_DECIMALS } from 'constants/defaults'; import useExternalPriceQuery from 'queries/rates/useExternalPriceQuery'; import useRateUpdateQuery from 'queries/rates/useRateUpdateQuery'; import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; type MarketDetailsProps = { baseCurrencyKey: CurrencyKey; @@ -32,6 +33,7 @@ type MarketDetailsProps = { type MarketData = Record; const MarketDetails: React.FC = ({ baseCurrencyKey }) => { + const { t } = useTranslation(); const { network } = Connector.useContainer(); const futuresMarketsQuery = useGetFuturesMarkets({ refetchInterval: 6000 }); @@ -121,24 +123,44 @@ const MarketDetails: React.FC = ({ baseCurrencyKey }) => { }, 'External Price': { value: - externalPrice === 0 - ? '-' - : formatCurrency(selectedPriceCurrency.name, externalPrice, { - sign: '$', - minDecimals, - }), + externalPrice === 0 ? ( + '-' + ) : ( + + + {formatCurrency(selectedPriceCurrency.name, externalPrice, { + sign: '$', + minDecimals, + })} + + + ), }, '24H Change': { value: - marketSummary?.price && pastPrice?.price - ? `${formatCurrency( - selectedPriceCurrency.name, - marketSummary?.price.sub(pastPrice?.price) ?? zeroBN, - { sign: '$', minDecimals } - )} (${formatPercent( - marketSummary?.price.sub(pastPrice?.price).div(marketSummary?.price) ?? zeroBN - )})` - : NO_VALUE, + marketSummary?.price && pastPrice?.price ? ( + + + {`${formatCurrency( + selectedPriceCurrency.name, + marketSummary?.price.sub(pastPrice?.price) ?? zeroBN, + { sign: '$', minDecimals } + )} (${formatPercent( + marketSummary?.price.sub(pastPrice?.price).div(marketSummary?.price) ?? zeroBN + )})`} + + + ) : ( + NO_VALUE + ), color: marketSummary?.price && pastPrice?.price ? marketSummary?.price.sub(pastPrice?.price).gt(zeroBN) @@ -149,20 +171,39 @@ const MarketDetails: React.FC = ({ baseCurrencyKey }) => { : undefined, }, '24H Volume': { - value: !!futuresTradingVolume - ? formatCurrency(selectedPriceCurrency.name, futuresTradingVolume ?? zeroBN, { - sign: '$', - }) - : NO_VALUE, + value: !!futuresTradingVolume ? ( + + + {formatCurrency(selectedPriceCurrency.name, futuresTradingVolume ?? zeroBN, { + sign: '$', + })} + + + ) : ( + NO_VALUE + ), }, '24H Trades': { - value: !!futuresDailyTradeStats ? `${futuresDailyTradeStats ?? 0}` : NO_VALUE, + value: !!futuresDailyTradeStats ? ( + + {`${futuresDailyTradeStats ?? 0}`} + + ) : ( + NO_VALUE + ), }, 'Open Interest': { value: marketSummary?.marketSize?.mul(wei(basePriceRate)) ? ( = ({ baseCurrencyKey }) => { ), }, [fundingTitle]: { - value: fundingValue ? formatPercent(fundingValue ?? zeroBN, { minDecimals: 6 }) : NO_VALUE, + value: fundingValue ? ( + + + {formatPercent(fundingValue ?? zeroBN, { minDecimals: 6 })} + + + ) : ( + NO_VALUE + ), color: fundingValue?.gt(zeroBN) ? 'green' : fundingValue?.lt(zeroBN) ? 'red' : undefined, }, }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ baseCurrencyKey, marketSummary, @@ -234,6 +287,12 @@ const MarketDetails: React.FC = ({ baseCurrencyKey }) => { ); }; +const OneHrFundingRateTooltip = styled(StyledTooltip)` + bottom: -145px; + z-index: 2; + left: -200px; +`; + const MarketDetailsContainer = styled.div` width: 100%; height: 55px; @@ -279,7 +338,7 @@ const MarketDetailsContainer = styled.div` } `; -const HoverTransform = styled.div` +export const HoverTransform = styled.div` :hover { transform: scale(1.03); } diff --git a/sections/futures/PositionCard/PositionCard.tsx b/sections/futures/PositionCard/PositionCard.tsx index ab18081331..190bd26e39 100644 --- a/sections/futures/PositionCard/PositionCard.tsx +++ b/sections/futures/PositionCard/PositionCard.tsx @@ -3,6 +3,8 @@ import styled, { css } from 'styled-components'; import { FlexDivCol } from 'styles/common'; import { useTranslation } from 'react-i18next'; +import StyledTooltip from 'components/Tooltip/StyledTooltip'; +import { HoverTransform } from '../MarketDetails/MarketDetails'; import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import { formatCurrency, formatPercent, zeroBN } from 'utils/formatters/number'; import { isFiatCurrency } from 'utils/currencies'; @@ -36,17 +38,17 @@ type PositionData = { marketPrice: string; price24h: Wei; positionSide: JSX.Element; - positionSize: string; - leverage: string; - liquidationPrice: string; - pnl: Wei; + positionSize: string | React.ReactNode; + leverage: string | React.ReactNode; + liquidationPrice: string | JSX.Element; + pnl: string | Wei | JSX.Element; realizedPnl: Wei; - pnlText: string; - realizedPnlText: string; + pnlText: string | null | JSX.Element; + realizedPnlText: string | JSX.Element; netFunding: Wei; - netFundingText: string; - fees: string; - avgEntryPrice: string; + netFundingText: string | null | JSX.Element; + fees: string | JSX.Element; + avgEntryPrice: string | JSX.Element; }; const PositionCard: React.FC = ({ currencyKey, position, currencyKeyRate }) => { @@ -98,63 +100,149 @@ const PositionCard: React.FC = ({ currencyKey, position, curr }), price24h: lastPriceWei.sub(pastPriceWei), positionSide: positionDetails ? ( - - {positionDetails.side === 'long' ? PositionSide.LONG : PositionSide.SHORT} - + + + {positionDetails.side === 'long' ? PositionSide.LONG : PositionSide.SHORT} + + + ) : ( {NO_VALUE} ), - positionSize: positionDetails - ? `${formatNumber(positionDetails.size ?? 0, { - minDecimals: positionDetails.size.abs().lt(0.01) ? 4 : 2, - })} (${formatCurrency(Synths.sUSD, positionDetails.notionalValue.abs() ?? zeroBN, { - sign: '$', - minDecimals: positionDetails.notionalValue.abs().lt(0.01) ? 4 : 2, - })})` - : NO_VALUE, - leverage: positionDetails - ? formatNumber(positionDetails?.leverage ?? zeroBN) + 'x' - : NO_VALUE, - liquidationPrice: positionDetails - ? formatCurrency(Synths.sUSD, positionDetails?.liquidationPrice ?? zeroBN, { - sign: '$', - minDecimals, - }) - : NO_VALUE, - pnl: pnl, + positionSize: positionDetails ? ( + + + {`${formatNumber(positionDetails.size ?? 0, { + minDecimals: positionDetails.size.abs().lt(0.01) ? 4 : 2, + })} (${formatCurrency(Synths.sUSD, positionDetails.notionalValue.abs() ?? zeroBN, { + sign: '$', + minDecimals: positionDetails.notionalValue.abs().lt(0.01) ? 4 : 2, + })})`} + + + ) : ( + NO_VALUE + ), + leverage: positionDetails ? ( + + {formatNumber(positionDetails?.leverage ?? zeroBN) + 'x'} + + ) : ( + NO_VALUE + ), + liquidationPrice: positionDetails ? ( + + + {formatCurrency(Synths.sUSD, positionDetails?.liquidationPrice ?? zeroBN, { + sign: '$', + minDecimals, + })} + + + ) : ( + NO_VALUE + ), + pnl: pnl ?? NO_VALUE, realizedPnl: realizedPnl, pnlText: - positionDetails && pnl - ? `${formatCurrency(Synths.sUSD, pnl, { - sign: '$', - minDecimals: pnl.abs().lt(0.01) ? 4 : 2, - })} (${formatPercent(positionDetails.profitLoss.div(positionDetails.initialMargin))})` - : NO_VALUE, + positionDetails && pnl ? ( + + + {`${formatCurrency(Synths.sUSD, pnl, { + sign: '$', + minDecimals: pnl.abs().lt(0.01) ? 4 : 2, + })} (${formatPercent( + positionDetails.profitLoss.div(positionDetails.initialMargin) + )})`} + + + ) : ( + NO_VALUE + ), realizedPnlText: - positionHistory && realizedPnl - ? `${formatCurrency(Synths.sUSD, realizedPnl, { - sign: '$', - minDecimals: 2, - })}` - : NO_VALUE, + positionHistory && realizedPnl ? ( + + + {`${formatCurrency(Synths.sUSD, realizedPnl, { + sign: '$', + minDecimals: 2, + })}`} + + + ) : ( + NO_VALUE + ), netFunding: netFunding, - netFundingText: formatCurrency(Synths.sUSD, netFunding, { - sign: '$', - minDecimals: netFunding.abs().lt(0.01) ? 4 : 2, - }), - fees: positionDetails - ? formatCurrency(Synths.sUSD, positionHistory?.feesPaid ?? zeroBN, { - sign: '$', - }) - : NO_VALUE, - avgEntryPrice: positionDetails - ? formatCurrency(Synths.sUSD, positionHistory?.entryPrice ?? zeroBN, { + netFundingText: netFunding ? ( + + {`${formatCurrency(Synths.sUSD, netFunding, { sign: '$', - minDecimals, - }) - : NO_VALUE, + minDecimals: netFunding.abs().lt(0.01) ? 4 : 2, + })}`} + + ) : null, + fees: positionDetails ? ( + + + {formatCurrency(Synths.sUSD, positionHistory?.feesPaid ?? zeroBN, { + sign: '$', + })} + + + ) : ( + NO_VALUE + ), + avgEntryPrice: positionDetails ? ( + + + {formatCurrency(Synths.sUSD, positionHistory?.entryPrice ?? zeroBN, { + sign: '$', + minDecimals, + })} + + + ) : ( + NO_VALUE + ), }; }, [ currencyKey, @@ -297,6 +385,15 @@ const StyledSubtitle = styled.p` margin: 0; `; +const PositionCardTooltip = styled(StyledTooltip)` + z-index: 2; +`; + +const LeftMarginTooltip = styled(StyledTooltip)` + left: -60px; + z-index: 2; +`; + const StyledValue = styled.p` font-family: ${(props) => props.theme.fonts.mono}; font-size: 13px; diff --git a/sections/futures/Trades/Trades.tsx b/sections/futures/Trades/Trades.tsx index c5d648da4d..296bc0dfde 100644 --- a/sections/futures/Trades/Trades.tsx +++ b/sections/futures/Trades/Trades.tsx @@ -1,5 +1,5 @@ import { wei } from '@synthetixio/wei'; -import LinkIcon from 'assets/svg/app/link.svg'; +import LinkIcon from 'assets/svg/app/link-blue.svg'; import Card from 'components/Card'; import Table from 'components/Table'; import TimeDisplay from './TimeDisplay'; @@ -291,11 +291,22 @@ const TableNoResults = styled(GridDivCenteredRow)` `; const StyledExternalLink = styled(ExternalLink)` - margin-left: auto; + padding: 10px; + &:hover { + svg { + path { + fill: ${(props) => props.theme.colors.common.primaryWhite}; + } + } + } `; const StyledLinkIcon = styled(LinkIcon)` color: ${(props) => props.theme.colors.common.secondaryGray}; width: 14px; height: 14px; + + path { + fill: ${(props) => props.theme.colors.common.secondaryGray}; + } `; diff --git a/sections/futures/TradingHistory/SkewInfo.tsx b/sections/futures/TradingHistory/SkewInfo.tsx index 478642db97..d7f4d4c59f 100644 --- a/sections/futures/TradingHistory/SkewInfo.tsx +++ b/sections/futures/TradingHistory/SkewInfo.tsx @@ -1,3 +1,4 @@ +import StyledTooltip from 'components/Tooltip/StyledTooltip'; import { FuturesMarket } from 'queries/futures/types'; import useGetFuturesMarkets from 'queries/futures/useGetFuturesMarkets'; import { useMemo } from 'react'; @@ -5,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { CapitalizedText, NumericValue } from 'styles/common'; import { formatPercent } from 'utils/formatters/number'; +import { HoverTransform } from '../MarketDetails/MarketDetails'; import OpenInterestBar from './OpenInterestBar'; type SkewInfoProps = { @@ -42,7 +44,16 @@ const SkewInfo: React.FC = ({ currencyKey }) => { {formatPercent(data[0].short, { minDecimals: 0 })} - {t('futures.market.history.skew-label')} + + + {t('futures.market.history.skew-label')} + + {formatPercent(data[0].long, { minDecimals: 0 })} @@ -52,6 +63,11 @@ const SkewInfo: React.FC = ({ currencyKey }) => { export default SkewInfo; +const SkewTooltip = styled(StyledTooltip)` + left: -30px; + z-index: 2; +`; + const SkewContainer = styled.div` display: flex; flex-direction: column; diff --git a/translations/en.json b/translations/en.json index ff43eb0b71..a4a176c27a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -172,13 +172,14 @@ "page-title-currency-pair": "{{baseCurrencyKey}}/{{quoteCurrencyKey}} - {{rate}} | Kwenta", "synth-exchange": "Synth Exchange", "currency-card": { - "synth-name": "Synth Name", + "synth-name": "-", "amount-placeholder": "0.0000", "wallet-balance": "balance:", "currency-selector": { "select-synth": "select synth", "select-token": "select token" - } + }, + "max-button": "max" }, "market-details-card": { "title": "Market Details", @@ -194,6 +195,13 @@ "minute-ago": "min ago", "minutes-ago": "mins ago", "seconds-ago": "sec ago" + }, + "tooltips": { + "external-price": "Aggregated market price provided by CoinGecko", + "24h-change": "Total change in asset value within the past 24 hours", + "24h-vol": "Total trade volume within the past 24 hours", + "24h-trades": "Total amount of trades on selected pair within the past 24h", + "1h-funding-rate": "Funding applies to all open positions. A positive rate means longs pay shorts, a negative rate means shorts pay longs. The rate is calculated as the average funding paid by open positions for the last hour. Funding accrues on a position every time it is opened, modified, or closed. Funding accrues on open positions." } }, "price-chart-card": { @@ -394,6 +402,13 @@ "notionalValue": "Size", "liquidationPrice": "Liq. Price" }, + "synth-balances-table": { + "market": "Market", + "amount": "Amount", + "value-in-usd": "Value in USD", + "oracle-price": "Oracle Price", + "daily-change": "24H Change" + }, "futures-markets-table": { "market": "Market", "oracle-price": "Oracle Price", @@ -642,7 +657,18 @@ "liquidation-price": "Liq. Price", "net-funding": "Net Funding", "fees": "Fees", - "avg-entry-price": "Avg. Entry" + "avg-entry-price": "Avg. Entry", + "tooltips": { + "position-side": "Direction of current trade", + "position-size": "Notional value of the open position", + "u-pnl": "The current unrealized profit or loss on the open position", + "r-pnl": "Profit or loss achieved by closing some or all of a position", + "leverage": "Individual position leverage which will vary based on the balance of margin deposited into the market", + "liquidation-price": "The estimated oracle price that will trigger a liquidation", + "net-funding": "Total funding due at the close of the position", + "fees": "Net fees paid to open and/or modify the position", + "avg-entry-price": "The average price at which the position was opened" + } }, "state": { "opens-soon": "opens soon", @@ -743,10 +769,10 @@ } } }, - "next-price": { - "description": "Next-Price orders are subject to volatility and execute at the very next on-chain price.", - "learn-more": "Learn more" - }, + "next-price": { + "description": "Next-Price orders are subject to volatility and execute at the very next on-chain price.", + "learn-more": "Learn more" + }, "confirmation": { "modal": { "confirm-order": "Confirm order", @@ -758,6 +784,7 @@ "history-label": "trading history", "last-n-trades": "last {{ numberOfTrades }} trades", "skew-label": "skew", + "skew-tooltip": "Skew is the total amount of outstanding long and short contracts expressed as a percentage", "amount-label": "amount", "price-label": "price", "time-label": "time" @@ -1191,4 +1218,4 @@ }, "min": "min" } -} +} \ No newline at end of file