Commit 91c20135 authored by Jack Short's avatar Jack Short Committed by GitHub

chore: only exposing useFormatter (#7308)

parent cf09e809
......@@ -2,6 +2,7 @@ import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/
import { useEffect, useMemo } from 'react'
import { usePendingOrders } from 'state/signatures/hooks'
import { usePendingTransactions, useTransactionCanceller } from 'state/transactions/hooks'
import { useFormatter } from 'utils/formatNumbers'
import { useLocalActivities } from './parseLocal'
import { parseRemoteActivities } from './parseRemote'
......@@ -55,6 +56,7 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
}
export function useAllActivities(account: string) {
const { formatNumberOrString } = useFormatter()
const { data, loading, refetch } = useActivityQuery({
variables: { account },
errorPolicy: 'all',
......@@ -62,7 +64,10 @@ export function useAllActivities(account: string) {
})
const localMap = useLocalActivities(account)
const remoteMap = useMemo(() => parseRemoteActivities(data?.portfolios?.[0].assetActivities), [data?.portfolios])
const remoteMap = useMemo(
() => parseRemoteActivities(formatNumberOrString, data?.portfolios?.[0].assetActivities),
[data?.portfolios, formatNumberOrString]
)
const updateCancelledTx = useTransactionCanceller()
/* Updates locally stored pendings tx's when remote data contains a conflicting cancellation tx */
......
......@@ -11,6 +11,7 @@ import {
TransactionType as MockTxType,
} from 'state/transactions/types'
import { renderHook } from 'test-utils/render'
import { useFormatter } from 'utils/formatNumbers'
import { UniswapXOrderStatus } from '../../../../lib/hooks/orders/types'
import { SignatureDetails, SignatureType } from '../../../../state/signatures/types'
......@@ -237,6 +238,8 @@ jest.mock('../../../../state/transactions/hooks', () => {
describe('parseLocalActivity', () => {
it('returns swap activity fields with known tokens, exact input', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
......@@ -251,7 +254,7 @@ describe('parseLocalActivity', () => {
},
} as TransactionDetails
const chainId = ChainId.MAINNET
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toEqual({
expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toEqual({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
......@@ -264,6 +267,8 @@ describe('parseLocalActivity', () => {
})
it('returns swap activity fields with known tokens, exact output', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_OUTPUT,
......@@ -278,7 +283,7 @@ describe('parseLocalActivity', () => {
},
} as TransactionDetails
const chainId = ChainId.MAINNET
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toMatchObject({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
......@@ -288,6 +293,8 @@ describe('parseLocalActivity', () => {
})
it('returns swap activity fields with unknown tokens', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
......@@ -303,7 +310,7 @@ describe('parseLocalActivity', () => {
} as TransactionDetails
const chainId = ChainId.MAINNET
const tokens = {} as ChainTokenMap
expect(transactionToActivity(details, chainId, tokens)).toMatchObject({
expect(transactionToActivity(details, chainId, tokens, formatNumber)).toMatchObject({
chainId: 1,
currencies: [undefined, undefined],
descriptor: 'Unknown for Unknown',
......@@ -496,13 +503,16 @@ describe('parseLocalActivity', () => {
})
it('Signature to activity - returns undefined if is on chain order', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
expect(
signatureToActivity(
{
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.FILLED,
} as SignatureDetails,
{}
{},
formatNumber
)
).toBeUndefined()
......@@ -512,7 +522,8 @@ describe('parseLocalActivity', () => {
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.CANCELLED,
} as SignatureDetails,
{}
{},
formatNumber
)
).toBeUndefined()
})
......
......@@ -2,7 +2,6 @@ import { BigNumber } from '@ethersproject/bignumber'
import { t } from '@lingui/macro'
import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import UniswapXBolt from 'assets/svg/bolt.svg'
import { SupportedLocale } from 'constants/locales'
import { nativeOnChain } from 'constants/tokens'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
......@@ -24,11 +23,13 @@ import {
TransactionType,
WrapTransactionInfo,
} from 'state/transactions/types'
import { formatCurrencyAmount, useFormatterLocales } from 'utils/formatNumbers'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { CancelledTransactionTitleTable, getActivityTitle, OrderTextTable } from '../constants'
import { Activity, ActivityMap } from './types'
type FormatNumberFunctionType = ReturnType<typeof useFormatter>['formatNumber']
function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined {
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]
}
......@@ -38,15 +39,21 @@ function buildCurrencyDescriptor(
amtA: string,
currencyB: Currency | undefined,
amtB: string,
delimiter = t`for`,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType,
delimiter = t`for`
) {
const formattedA = currencyA
? formatCurrencyAmount({ amount: CurrencyAmount.fromRawAmount(currencyA, amtA), locale })
? formatNumber({
input: parseFloat(CurrencyAmount.fromRawAmount(currencyA, amtA).toSignificant()),
type: NumberType.TokenNonTx,
})
: t`Unknown`
const symbolA = currencyA?.symbol ?? ''
const formattedB = currencyB
? formatCurrencyAmount({ amount: CurrencyAmount.fromRawAmount(currencyB, amtB), locale })
? formatNumber({
input: parseFloat(CurrencyAmount.fromRawAmount(currencyB, amtB).toSignificant()),
type: NumberType.TokenNonTx,
})
: t`Unknown`
const symbolB = currencyB?.symbol ?? ''
return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ')
......@@ -56,7 +63,7 @@ function parseSwap(
swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo,
chainId: ChainId,
tokens: ChainTokenMap,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
const tokenIn = getCurrency(swap.inputCurrencyId, chainId, tokens)
const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens)
......@@ -66,18 +73,29 @@ function parseSwap(
: [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw]
return {
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw, undefined, locale),
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw, formatNumber, undefined),
currencies: [tokenIn, tokenOut],
prefixIconSrc: swap.isUniswapXOrder ? UniswapXBolt : undefined,
}
}
function parseWrap(wrap: WrapTransactionInfo, chainId: ChainId, status: TransactionStatus): Partial<Activity> {
function parseWrap(
wrap: WrapTransactionInfo,
chainId: ChainId,
status: TransactionStatus,
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
const native = nativeOnChain(chainId)
const wrapped = native.wrapped
const [input, output] = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
const descriptor = buildCurrencyDescriptor(input, wrap.currencyAmountRaw, output, wrap.currencyAmountRaw)
const descriptor = buildCurrencyDescriptor(
input,
wrap.currencyAmountRaw,
output,
wrap.currencyAmountRaw,
formatNumber
)
const title = getActivityTitle(TransactionType.WRAP, status, wrap.unwrapped)
const currencies = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
......@@ -107,11 +125,16 @@ type GenericLPInfo = Omit<
AddLiquidityV3PoolTransactionInfo | RemoveLiquidityV3TransactionInfo | AddLiquidityV2PoolTransactionInfo,
'type'
>
function parseLP(lp: GenericLPInfo, chainId: ChainId, tokens: ChainTokenMap): Partial<Activity> {
function parseLP(
lp: GenericLPInfo,
chainId: ChainId,
tokens: ChainTokenMap,
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
const [baseRaw, quoteRaw] = [lp.expectedAmountBaseRaw, lp.expectedAmountQuoteRaw]
const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, t`and`)
const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, formatNumber, t`and`)
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
......@@ -119,7 +142,8 @@ function parseLP(lp: GenericLPInfo, chainId: ChainId, tokens: ChainTokenMap): Pa
function parseCollectFees(
collect: CollectFeesTransactionInfo,
chainId: ChainId,
tokens: ChainTokenMap
tokens: ChainTokenMap,
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
// Adapts CollectFeesTransactionInfo to generic LP type
const {
......@@ -128,7 +152,12 @@ function parseCollectFees(
expectedCurrencyOwed0: expectedAmountBaseRaw,
expectedCurrencyOwed1: expectedAmountQuoteRaw,
} = collect
return parseLP({ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, chainId, tokens)
return parseLP(
{ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
chainId,
tokens,
formatNumber
)
}
function parseMigrateCreateV3(
......@@ -157,7 +186,7 @@ export function transactionToActivity(
details: TransactionDetails,
chainId: ChainId,
tokens: ChainTokenMap,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType
): Activity | undefined {
try {
const status = getTransactionStatus(details)
......@@ -176,19 +205,19 @@ export function transactionToActivity(
let additionalFields: Partial<Activity> = {}
const info = details.info
if (info.type === TransactionType.SWAP) {
additionalFields = parseSwap(info, chainId, tokens, locale)
additionalFields = parseSwap(info, chainId, tokens, formatNumber)
} else if (info.type === TransactionType.APPROVAL) {
additionalFields = parseApproval(info, chainId, tokens, status)
} else if (info.type === TransactionType.WRAP) {
additionalFields = parseWrap(info, chainId, status)
additionalFields = parseWrap(info, chainId, status, formatNumber)
} else if (
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
) {
additionalFields = parseLP(info, chainId, tokens)
additionalFields = parseLP(info, chainId, tokens, formatNumber)
} else if (info.type === TransactionType.COLLECT_FEES) {
additionalFields = parseCollectFees(info, chainId, tokens)
additionalFields = parseCollectFees(info, chainId, tokens, formatNumber)
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
}
......@@ -210,7 +239,7 @@ export function transactionToActivity(
export function signatureToActivity(
signature: SignatureDetails,
tokens: ChainTokenMap,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType
): Activity | undefined {
switch (signature.type) {
case SignatureType.SIGN_UNISWAPX_ORDER: {
......@@ -229,7 +258,7 @@ export function signatureToActivity(
from: signature.offerer,
statusMessage,
prefixIconSrc: UniswapXBolt,
...parseSwap(signature.swapInfo, signature.chainId, tokens, locale),
...parseSwap(signature.swapInfo, signature.chainId, tokens, formatNumber),
}
}
default:
......@@ -241,24 +270,24 @@ export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions()
const allSignatures = useAllSignatures()
const tokens = useAllTokensMultichain()
const { formatterLocale } = useFormatterLocales()
const { formatNumber } = useFormatter()
return useMemo(() => {
const activityMap: ActivityMap = {}
for (const [transaction, chainId] of allTransactions) {
if (transaction.from !== account) continue
const activity = transactionToActivity(transaction, chainId, tokens, formatterLocale)
const activity = transactionToActivity(transaction, chainId, tokens, formatNumber)
if (activity) activityMap[transaction.hash] = activity
}
for (const signature of Object.values(allSignatures)) {
if (signature.offerer !== account) continue
const activity = signatureToActivity(signature, tokens, formatterLocale)
const activity = signatureToActivity(signature, tokens, formatNumber)
if (activity) activityMap[signature.id] = activity
}
return activityMap
}, [account, allSignatures, allTransactions, formatterLocale, tokens])
}, [account, allSignatures, allTransactions, formatNumber, tokens])
}
......@@ -21,7 +21,7 @@ import { gqlToCurrency, logSentryErrorForUnsupportedChain, supportedChainIdFromG
import ms from 'ms'
import { useEffect, useState } from 'react'
import { isAddress } from 'utils'
import { formatFiatPrice, formatNumberOrString, NumberType } from 'utils/formatNumbers'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants'
import { Activity } from './types'
......@@ -34,6 +34,8 @@ type TransactionChanges = {
NftApproveForAll: NftApproveForAllPartsFragment[]
}
type FormatNumberOrStringFunctionType = ReturnType<typeof useFormatter>['formatNumberOrString']
// TODO: Move common contract metadata to a backend service
const UNI_IMG =
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png'
......@@ -140,13 +142,13 @@ function getSwapDescriptor({
* @param transactedValue Transacted value amount from TokenTransfer API response
* @returns parsed & formatted USD value as a string if currency is of type USD
*/
function formatTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): string {
if (!transactedValue) return '-'
function getTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): number | undefined {
if (!transactedValue) return undefined
const price = transactedValue?.currency === GQLCurrency.Usd ? transactedValue.value ?? undefined : undefined
return formatFiatPrice(price)
return price
}
function parseSwap(changes: TransactionChanges) {
function parseSwap(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
if (changes.NftTransfer.length > 0 && changes.TokenTransfer.length === 1) {
const collectionCounts = getCollectionCounts(changes.NftTransfer)
......@@ -168,8 +170,8 @@ function parseSwap(changes: TransactionChanges) {
if (sent && received) {
const adjustedInput = parseFloat(sent.quantity) - parseFloat(refund?.quantity ?? '0')
const inputAmount = formatNumberOrString(adjustedInput, NumberType.TokenNonTx)
const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx)
const inputAmount = formatNumberOrString({ input: adjustedInput, type: NumberType.TokenNonTx })
const outputAmount = formatNumberOrString({ input: received.quantity, type: NumberType.TokenNonTx })
return {
title: getSwapTitle(sent, received),
descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }),
......@@ -180,8 +182,8 @@ function parseSwap(changes: TransactionChanges) {
return { title: t`Unknown Swap` }
}
function parseSwapOrder(changes: TransactionChanges) {
return { ...parseSwap(changes), prefixIconSrc: UniswapXBolt }
function parseSwapOrder(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
return { ...parseSwap(changes, formatNumberOrString), prefixIconSrc: UniswapXBolt }
}
function parseApprove(changes: TransactionChanges) {
......@@ -194,12 +196,12 @@ function parseApprove(changes: TransactionChanges) {
return { title: t`Unknown Approval` }
}
function parseLPTransfers(changes: TransactionChanges) {
function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
const poolTokenA = changes.TokenTransfer[0]
const poolTokenB = changes.TokenTransfer[1]
const tokenAQuanitity = formatNumberOrString(poolTokenA.quantity, NumberType.TokenNonTx)
const tokenBQuantity = formatNumberOrString(poolTokenB.quantity, NumberType.TokenNonTx)
const tokenAQuanitity = formatNumberOrString({ input: poolTokenA.quantity, type: NumberType.TokenNonTx })
const tokenBQuantity = formatNumberOrString({ input: poolTokenB.quantity, type: NumberType.TokenNonTx })
return {
descriptor: `${tokenAQuanitity} ${poolTokenA.asset.symbol} and ${tokenBQuantity} ${poolTokenB.asset.symbol}`,
......@@ -211,11 +213,15 @@ function parseLPTransfers(changes: TransactionChanges) {
type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }
type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment }
function parseSendReceive(changes: TransactionChanges, assetActivity: TransactionActivity) {
function parseSendReceive(
changes: TransactionChanges,
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) {
// TODO(cartcrom): remove edge cases after backend implements
// Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
return { title: t`Removed Liquidity`, ...parseLPTransfers(changes) }
return { title: t`Removed Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) }
}
let transfer: NftTransferPartsFragment | TokenTransferPartsFragment | undefined
......@@ -230,7 +236,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
} else if (changes.TokenTransfer.length === 1) {
transfer = changes.TokenTransfer[0]
assetName = transfer.asset.symbol
amount = formatNumberOrString(transfer.quantity, NumberType.TokenNonTx)
amount = formatNumberOrString({ input: transfer.quantity, type: NumberType.TokenNonTx })
currencies = [gqlToCurrency(transfer.asset)]
}
......@@ -241,7 +247,10 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
return isMoonpayPurchase && transfer.__typename === 'TokenTransfer'
? {
title: t`Purchased`,
descriptor: `${amount} ${assetName} ${t`for`} ${formatTransactedValue(transfer.transactedValue)}`,
descriptor: `${amount} ${assetName} ${t`for`} ${formatNumberOrString({
input: getTransactedValue(transfer.transactedValue),
type: NumberType.FiatTokenPrice,
})}`,
logos: [moonpayLogoSrc],
currencies,
}
......@@ -263,25 +272,37 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
return { title: t`Unknown Send` }
}
function parseMint(changes: TransactionChanges, assetActivity: TransactionActivity) {
function parseMint(
changes: TransactionChanges,
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) {
const collectionMap = getCollectionCounts(changes.NftTransfer)
if (Object.keys(collectionMap).length === 1) {
const collectionName = Object.keys(collectionMap)[0]
// Edge case: Minting a v3 positon represents adding liquidity
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
return { title: t`Added Liquidity`, ...parseLPTransfers(changes) }
return { title: t`Added Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) }
}
return { title: t`Minted`, descriptor: `${collectionMap[collectionName]} ${collectionName}` }
}
return { title: t`Unknown Mint` }
}
function parseUnknown(_changes: TransactionChanges, assetActivity: TransactionActivity) {
function parseUnknown(
_changes: TransactionChanges,
_formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] }
}
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: TransactionActivity) => Partial<Activity>
type ActivityTypeParser = (
changes: TransactionChanges,
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) => Partial<Activity>
const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = {
[ActivityType.Swap]: parseSwap,
[ActivityType.SwapOrder]: parseSwapOrder,
......@@ -345,7 +366,10 @@ function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activ
}
}
function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activity | undefined {
function parseRemoteActivity(
assetActivity: AssetActivityPartsFragment,
formatNumberOrString: FormatNumberOrStringFunctionType
): Activity | undefined {
try {
if (assetActivity.details.__typename === 'SwapOrderDetails') {
return parseUniswapXOrder(assetActivity as OrderActivity)
......@@ -385,6 +409,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
const parsedFields = ActivityParserByType[assetActivity.details.type]?.(
changes,
formatNumberOrString,
assetActivity as TransactionActivity
)
return { ...defaultFields, ...parsedFields }
......@@ -394,9 +419,12 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
}
}
export function parseRemoteActivities(assetActivities?: readonly AssetActivityPartsFragment[]) {
export function parseRemoteActivities(
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivities?: readonly AssetActivityPartsFragment[]
) {
return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => {
const activity = parseRemoteActivity(assetActivity)
const activity = parseRemoteActivity(assetActivity, formatNumberOrString)
if (activity) acc[activity.hash] = activity
return acc
}, {})
......
......@@ -18,7 +18,7 @@ import { useCallback, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { formatUSDPrice } from 'utils/formatNumbers'
import { useFormatter } from 'utils/formatNumbers'
import { DeltaArrow, DeltaText } from '../Tokens/TokenDetails/Delta'
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
......@@ -128,6 +128,7 @@ interface TokenRowProps {
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
const navigate = useNavigate()
const { formatFiatPrice } = useFormatter()
const handleClick = useCallback(() => {
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
......@@ -184,7 +185,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
{!!token.market?.price?.value && (
<>
<Row gap="4">
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
<Box className={styles.primaryText}>{formatFiatPrice({ price: token.market.price.value })}</Box>
</Row>
<PriceChangeContainer>
<DeltaArrow delta={token.market?.pricePercentChange?.value} />
......
......@@ -17,6 +17,7 @@ import { useOrder } from 'state/signatures/hooks'
import { useTransaction } from 'state/transactions/hooks'
import styled from 'styled-components'
import { EllipsisStyle, ThemedText } from 'theme'
import { useFormatter } from 'utils/formatNumbers'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
const StyledClose = styled(X)<{ $padding: number }>`
......@@ -137,9 +138,10 @@ export function TransactionPopupContent({
}) {
const transaction = useTransaction(hash)
const tokens = useAllTokensMultichain()
const { formatNumber } = useFormatter()
if (!transaction) return null
const activity = transactionToActivity(transaction, chainId, tokens)
const activity = transactionToActivity(transaction, chainId, tokens, formatNumber)
if (!activity) return null
......@@ -153,9 +155,10 @@ export function UniswapXOrderPopupContent({ orderHash, onClose }: { orderHash: s
const order = useOrder(orderHash)
const tokens = useAllTokensMultichain()
const openOffchainActivityModal = useOpenOffchainActivityModal()
const { formatNumber } = useFormatter()
if (!order) return null
const activity = signatureToActivity(order, tokens)
const activity = signatureToActivity(order, tokens, formatNumber)
if (!activity) return null
......
......@@ -15,7 +15,7 @@ import { Link } from 'react-router-dom'
import { Bound } from 'state/mint/v3/actions'
import styled from 'styled-components'
import { HideSmall, MEDIA_WIDTHS, SmallOnly, ThemedText } from 'theme'
import { formatTickPrice } from 'utils/formatTickPrice'
import { useFormatter } from 'utils/formatNumbers'
import { unwrappedToken } from 'utils/unwrappedToken'
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
......@@ -172,6 +172,8 @@ export default function PositionListItem({
tickLower,
tickUpper,
}: PositionListItemProps) {
const { formatTickPrice } = useFormatter()
const token0 = useToken(token0Address)
const token1 = useToken(token1Address)
......
......@@ -14,7 +14,7 @@ import { ReactNode, useCallback, useState } from 'react'
import { Bound } from 'state/mint/v3/actions'
import { useTheme } from 'styled-components'
import { ThemedText } from 'theme'
import { formatTickPrice } from 'utils/formatTickPrice'
import { useFormatter } from 'utils/formatNumbers'
import { unwrappedToken } from 'utils/unwrappedToken'
export const PositionPreview = ({
......@@ -31,6 +31,7 @@ export const PositionPreview = ({
ticksAtLimit: { [bound: string]: boolean | undefined }
}) => {
const theme = useTheme()
const { formatTickPrice } = useFormatter()
const currency0 = unwrappedToken(position.pool.token0)
const currency1 = unwrappedToken(position.pool.token1)
......
......@@ -17,7 +17,7 @@ import { Info, TrendingUp } from 'react-feather'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme'
import { textFadeIn } from 'theme/styles'
import { formatUSDPrice } from 'utils/formatNumbers'
import { useFormatter } from 'utils/formatNumbers'
import { calculateDelta, DeltaArrow, formatDelta } from './Delta'
......@@ -87,6 +87,7 @@ interface PriceChartProps {
export function PriceChart({ width, height, prices: originalPrices, timePeriod }: PriceChartProps) {
const locale = useActiveLocale()
const theme = useTheme()
const { formatFiatPrice } = useFormatter()
const { prices, blanks } = useMemo(
() =>
......@@ -208,13 +209,13 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
<ChartHeader data-cy="chart-header">
{displayPrice.value ? (
<>
<TokenPrice>{formatUSDPrice(displayPrice.value)}</TokenPrice>
<TokenPrice>{formatFiatPrice({ price: displayPrice.value })}</TokenPrice>
<ChartDelta startingPrice={startingPrice} endingPrice={displayPrice} />
</>
) : lastPrice.value ? (
<OutdatedContainer>
<OutdatedPriceContainer>
<TokenPrice>{formatUSDPrice(lastPrice.value)}</TokenPrice>
<TokenPrice>{formatFiatPrice({ price: lastPrice.value })}</TokenPrice>
<MouseoverTooltip text={tooltipMessage}>
<Info size={16} />
</MouseoverTooltip>
......
......@@ -16,7 +16,7 @@ import { CSSProperties, ReactNode } from 'react'
import { Link, useParams } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components'
import { BREAKPOINTS, ClickableStyle } from 'theme'
import { formatUSDPrice, NumberType, useFormatter } from 'utils/formatNumbers'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import {
LARGE_MEDIA_BREAKPOINT,
......@@ -440,7 +440,7 @@ interface LoadedRowProps {
/* Loaded State: row component with token information */
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
const { formatNumber } = useFormatter()
const { formatFiatPrice, formatNumber } = useFormatter()
const { tokenListIndex, tokenListLength, token, sortRank } = props
const filterString = useAtomValue(filterStringAtom)
......@@ -463,7 +463,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
}
// A simple 0 price indicates the price is not currently available from the api
const price = token.market?.price?.value === 0 ? '-' : formatUSDPrice(token.market?.price?.value)
const price = token.market?.price?.value === 0 ? '-' : formatFiatPrice({ price: token.market?.price?.value })
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return (
......
......@@ -17,7 +17,6 @@ import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import styled, { DefaultTheme, useTheme } from 'styled-components'
import { ExternalLink, ThemedText } from 'theme'
import { FormatterRule, NumberType, SIX_SIG_FIGS_NO_COMMAS, useFormatter } from 'utils/formatNumbers'
import { priceToPreciseFloat } from 'utils/formatNumbers'
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters'
import { getPriceImpactColor } from 'utils/prices'
......@@ -85,7 +84,7 @@ export default function SwapModalFooter({
const label = `${trade.executionPrice.baseCurrency?.symbol} `
const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}`
const formattedPrice = formatNumber({
input: priceToPreciseFloat(trade.executionPrice),
input: trade.executionPrice ? parseFloat(trade.executionPrice.toFixed(9)) : undefined,
type: NumberType.TokenTx,
})
const txCount = getTransactionCount(trade)
......
......@@ -6,7 +6,6 @@ import {
TEST_TRADE_EXACT_OUTPUT,
} from 'test-utils/constants'
import { render, screen } from 'test-utils/render'
import { formatCurrencyAmount, NumberType } from 'utils/formatNumbers'
import SwapModalHeader from './SwapModalHeader'
......@@ -17,16 +16,8 @@ describe('SwapModalHeader.tsx', () => {
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText(/Output is estimated. You will receive at least /i)).toBeInTheDocument()
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount({ amount: TEST_TRADE_EXACT_INPUT.inputAmount, type: NumberType.TokenTx })} ${
TEST_TRADE_EXACT_INPUT.inputAmount.currency.symbol ?? ''
}`
)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount({ amount: TEST_TRADE_EXACT_INPUT.outputAmount, type: NumberType.TokenTx })} ${
TEST_TRADE_EXACT_INPUT.outputAmount.currency.symbol ?? ''
}`
)
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(`<0.00001 ABC`)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(`<0.00001 DEF`)
})
it('renders ETH input token for an ETH input UniswapX swap', () => {
......@@ -39,16 +30,8 @@ describe('SwapModalHeader.tsx', () => {
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText(/Output is estimated. You will receive at least /i)).toBeInTheDocument()
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount({ amount: TEST_DUTCH_TRADE_ETH_INPUT.inputAmount, type: NumberType.TokenTx })} ${
ETH_MAINNET.symbol
}`
)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount({ amount: TEST_DUTCH_TRADE_ETH_INPUT.outputAmount, type: NumberType.TokenTx })} ${
TEST_DUTCH_TRADE_ETH_INPUT.outputAmount.currency.symbol ?? ''
}`
)
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(`<0.00001 ETH`)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(`<0.00001 DEF`)
})
it('test trade exact output, no recipient', () => {
......@@ -58,15 +41,7 @@ describe('SwapModalHeader.tsx', () => {
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText(/Input is estimated. You will sell at most/i)).toBeInTheDocument()
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount({ amount: TEST_TRADE_EXACT_OUTPUT.inputAmount, type: NumberType.TokenTx })} ${
TEST_TRADE_EXACT_OUTPUT.inputAmount.currency.symbol ?? ''
}`
)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount({ amount: TEST_TRADE_EXACT_OUTPUT.outputAmount, type: NumberType.TokenTx })} ${
TEST_TRADE_EXACT_OUTPUT.outputAmount.currency.symbol ?? ''
}`
)
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(`<0.00001 ABC`)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(`<0.00001 GHI`)
})
})
......@@ -6,7 +6,7 @@ import { Markets, TrendingCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils'
import styled from 'styled-components'
import { ThemedText } from 'theme/components/text'
import { formatNumberOrString, NumberType } from 'utils/formatNumbers'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const CarouselCardBorder = styled.div`
width: 100%;
......@@ -198,6 +198,8 @@ interface MarketplaceRowProps {
}
const MarketplaceRow = ({ marketplace, floorInEth, listings }: MarketplaceRowProps) => {
const { formatNumberOrString } = useFormatter()
return (
<>
<TableElement>
......@@ -212,7 +214,7 @@ const MarketplaceRow = ({ marketplace, floorInEth, listings }: MarketplaceRowPro
<TableElement>
<ThemedText.BodySmall color="neutral2">
{Number(floorInEth) > 0
? `${formatNumberOrString(floorInEth, NumberType.NFTTokenFloorPriceTrailingZeros)} ETH`
? `${formatNumberOrString({ input: floorInEth, type: NumberType.NFTTokenFloorPriceTrailingZeros })} ETH`
: '-'}
</ThemedText.BodySmall>
</TableElement>
......
......@@ -38,7 +38,6 @@ import { currencyId } from 'utils/currencyId'
import { WrongChainError } from 'utils/errors'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { formatTickPrice } from 'utils/formatTickPrice'
import { unwrappedToken } from 'utils/unwrappedToken'
import RangeBadge from '../../components/Badge/RangeBadge'
......@@ -389,6 +388,7 @@ function PositionPageContent() {
const { tokenId: tokenIdFromUrl } = useParams<{ tokenId?: string }>()
const { chainId, account, provider } = useWeb3React()
const theme = useTheme()
const { formatTickPrice } = useFormatter()
const parsedTokenId = tokenIdFromUrl ? BigNumber.from(tokenIdFromUrl) : undefined
const { loading, position: positionDetails } = useV3PositionFromTokenId(parsedTokenId)
......
This diff is collapsed.
import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core'
import {
DEFAULT_LOCAL_CURRENCY,
LOCAL_CURRENCY_SYMBOL_DISPLAY_TYPE,
......@@ -12,6 +12,7 @@ import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency'
import { useActiveLocale } from 'hooks/useActiveLocale'
import usePrevious from 'hooks/usePrevious'
import { useCallback, useMemo } from 'react'
import { Bound } from 'state/mint/v3/actions'
type Nullish<T> = T | null | undefined
type NumberFormatOptions = Intl.NumberFormatOptions
......@@ -392,7 +393,7 @@ interface FormatNumberOptions {
conversionRate?: number
}
export function formatNumber({
function formatNumber({
input,
type = NumberType.TokenNonTx,
placeholder = '-',
......@@ -434,7 +435,7 @@ interface FormatCurrencyAmountOptions {
conversionRate?: number
}
export function formatCurrencyAmount({
function formatCurrencyAmount({
amount,
type = NumberType.TokenNonTx,
placeholder,
......@@ -452,7 +453,7 @@ export function formatCurrencyAmount({
})
}
export function formatPriceImpact(priceImpact: Percent | undefined, locale: SupportedLocale = DEFAULT_LOCALE): string {
function formatPriceImpact(priceImpact: Percent | undefined, locale: SupportedLocale = DEFAULT_LOCALE): string {
if (!priceImpact) return '-'
return `${Number(priceImpact.multiply(-1).toFixed(3)).toLocaleString(locale, {
......@@ -462,13 +463,17 @@ export function formatPriceImpact(priceImpact: Percent | undefined, locale: Supp
})}%`
}
export function formatSlippage(slippage: Percent | undefined) {
function formatSlippage(slippage: Percent | undefined, locale: SupportedLocale = DEFAULT_LOCALE) {
if (!slippage) return '-'
return `${slippage.toFixed(3)}%`
return `${Number(slippage.toFixed(3)).toLocaleString(locale, {
minimumFractionDigits: 3,
maximumFractionDigits: 3,
useGrouping: false,
})}%`
}
interface FormatPriceProps {
interface FormatPriceOptions {
price: Nullish<Price<Currency, Currency>>
type: FormatterType
locale?: SupportedLocale
......@@ -476,13 +481,13 @@ interface FormatPriceProps {
conversionRate?: number
}
export function formatPrice({
function formatPrice({
price,
type = NumberType.FiatTokenPrice,
locale = DEFAULT_LOCALE,
localCurrency = DEFAULT_LOCAL_CURRENCY,
conversionRate,
}: FormatPriceProps): string {
}: FormatPriceOptions): string {
if (price === null || price === undefined) {
return '-'
}
......@@ -490,45 +495,80 @@ export function formatPrice({
return formatNumber({ input: parseFloat(price.toSignificant()), type, locale, localCurrency, conversionRate })
}
export function formatNumberOrString(price: Nullish<number | string>, type: FormatterType): string {
if (price === null || price === undefined) return '-'
if (typeof price === 'string') return formatNumber({ input: parseFloat(price), type })
return formatNumber({ input: price, type })
interface FormatTickPriceOptions {
price?: Price<Token, Token>
atLimit: { [bound in Bound]?: boolean | undefined }
direction: Bound
placeholder?: string
numberType?: NumberType
locale?: SupportedLocale
localCurrency?: SupportedLocalCurrency
conversionRate?: number
}
export function formatUSDPrice(price: Nullish<number | string>, type: NumberType = NumberType.FiatTokenPrice): string {
return formatNumberOrString(price, type)
function formatTickPrice({
price,
atLimit,
direction,
placeholder,
numberType,
locale,
localCurrency,
conversionRate,
}: FormatTickPriceOptions) {
if (atLimit[direction]) {
return direction === Bound.LOWER ? '0' : ''
}
if (!price && placeholder !== undefined) {
return placeholder
}
return formatPrice({ price, type: numberType ?? NumberType.TokenNonTx, locale, localCurrency, conversionRate })
}
/** Formats USD and non-USD prices */
export function formatFiatPrice(price: Nullish<number>, currency = 'USD'): string {
if (price === null || price === undefined) return '-'
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(price)
interface FormatNumberOrStringOptions {
input: Nullish<number | string>
type: FormatterType
locale?: SupportedLocale
localCurrency?: SupportedLocalCurrency
conversionRate?: number
}
// Convert [CurrencyAmount] to number with necessary precision for price formatting.
export const currencyAmountToPreciseFloat = (currencyAmount: CurrencyAmount<Currency> | undefined) => {
if (!currencyAmount) return undefined
const floatForLargerNumbers = parseFloat(currencyAmount.toExact())
if (floatForLargerNumbers < 0.1) {
return parseFloat(currencyAmount.toSignificant(6))
}
return floatForLargerNumbers
function formatNumberOrString({
input,
type,
locale,
localCurrency,
conversionRate,
}: FormatNumberOrStringOptions): string {
if (input === null || input === undefined) return '-'
if (typeof input === 'string')
return formatNumber({ input: parseFloat(input), type, locale, localCurrency, conversionRate })
return formatNumber({ input, type, locale, localCurrency, conversionRate })
}
// Convert [Price] to number with necessary precision for price formatting.
export const priceToPreciseFloat = (price: Price<Currency, Currency> | undefined) => {
if (!price) return undefined
const floatForLargerNumbers = parseFloat(price.toFixed(9))
if (floatForLargerNumbers < 0.1) {
return parseFloat(price.toSignificant(6))
}
return floatForLargerNumbers
interface FormatFiatPriceOptions {
price: Nullish<number | string>
type?: FormatterType
locale?: SupportedLocale
localCurrency?: SupportedLocalCurrency
conversionRate?: number
}
function formatFiatPrice({
price,
type = NumberType.FiatTokenPrice,
locale,
localCurrency,
conversionRate,
}: FormatFiatPriceOptions): string {
return formatNumberOrString({ input: price, type, locale, localCurrency, conversionRate })
}
const MAX_AMOUNT_STR_LENGTH = 9
export function formatReviewSwapCurrencyAmount(
function formatReviewSwapCurrencyAmount(
amount: CurrencyAmount<Currency>,
locale: SupportedLocale = DEFAULT_LOCALE
): string {
......@@ -539,7 +579,7 @@ export function formatReviewSwapCurrencyAmount(
return formattedAmount
}
export function useFormatterLocales(): {
function useFormatterLocales(): {
formatterLocale: SupportedLocale
formatterLocalCurrency: SupportedLocalCurrency
} {
......@@ -620,7 +660,7 @@ export function useFormatter() {
)
const formatPriceWithLocales = useCallback(
(options: Omit<FormatPriceProps, LocalesType>) =>
(options: Omit<FormatPriceOptions, LocalesType>) =>
formatPrice({
...options,
locale: formatterLocale,
......@@ -640,20 +680,66 @@ export function useFormatter() {
[formatterLocale]
)
const formatSlippageWithLocales = useCallback(
(slippage: Percent | undefined) => formatSlippage(slippage, formatterLocale),
[formatterLocale]
)
const formatTickPriceWithLocales = useCallback(
(options: Omit<FormatTickPriceOptions, LocalesType>) =>
formatTickPrice({
...options,
locale: formatterLocale,
localCurrency: currencyToFormatWith,
conversionRate: localCurrencyConversionRateToFormatWith,
}),
[currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith]
)
const formatNumberOrStringWithLocales = useCallback(
(options: Omit<FormatNumberOrStringOptions, LocalesType>) =>
formatNumberOrString({
...options,
locale: formatterLocale,
localCurrency: currencyToFormatWith,
conversionRate: localCurrencyConversionRateToFormatWith,
}),
[currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith]
)
const formatFiatPriceWithLocales = useCallback(
(options: Omit<FormatFiatPriceOptions, LocalesType>) =>
formatFiatPrice({
...options,
locale: formatterLocale,
localCurrency: currencyToFormatWith,
conversionRate: localCurrencyConversionRateToFormatWith,
}),
[currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith]
)
return useMemo(
() => ({
formatCurrencyAmount: formatCurrencyAmountWithLocales,
formatFiatPrice: formatFiatPriceWithLocales,
formatNumber: formatNumberWithLocales,
formatNumberOrString: formatNumberOrStringWithLocales,
formatPrice: formatPriceWithLocales,
formatPriceImpact: formatPriceImpactWithLocales,
formatReviewSwapCurrencyAmount: formatReviewSwapCurrencyAmountWithLocales,
formatSlippage: formatSlippageWithLocales,
formatTickPrice: formatTickPriceWithLocales,
}),
[
formatCurrencyAmountWithLocales,
formatFiatPriceWithLocales,
formatNumberOrStringWithLocales,
formatNumberWithLocales,
formatPriceImpactWithLocales,
formatPriceWithLocales,
formatReviewSwapCurrencyAmountWithLocales,
formatSlippageWithLocales,
formatTickPriceWithLocales,
]
)
}
import { Price, Token } from '@uniswap/sdk-core'
import { formatPrice, NumberType } from 'utils/formatNumbers'
import { Bound } from '../state/mint/v3/actions'
interface FormatTickPriceArgs {
price?: Price<Token, Token>
atLimit: { [bound in Bound]?: boolean | undefined }
direction: Bound
placeholder?: string
numberType?: NumberType
}
export function formatTickPrice({ price, atLimit, direction, placeholder, numberType }: FormatTickPriceArgs) {
if (atLimit[direction]) {
return direction === Bound.LOWER ? '0' : ''
}
if (!price && placeholder !== undefined) {
return placeholder
}
return formatPrice({ price, type: numberType ?? NumberType.TokenNonTx })
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment