Tx page: show historical $ value (#3272)

* Tx page: show historical $ value

* review fixes

* review fixes 2
This commit is contained in:
Igor Stuev
2026-03-02 14:37:43 +03:00
committed by GitHub
parent b6df49edd7
commit 5a37b28b9c
48 changed files with 252 additions and 32 deletions
+1
View File
@@ -58,6 +58,7 @@ export default function formatTxData(
token_transfers: null,
token_transfers_overflow: false,
exchange_rate: null,
historic_exchange_rate: null,
method: null,
transaction_types: [],
transaction_tag: null,
+1
View File
@@ -22,6 +22,7 @@ export const base: Transaction = {
created_contract: null,
decoded_input: decodedInputDataMock.withoutIndexedFields,
exchange_rate: '0.00254428',
historic_exchange_rate: '0.00254428',
fee: {
type: 'actual',
value: '7143168000000000',
+1
View File
@@ -20,6 +20,7 @@ export const TX: Transaction = {
confirmations: 71,
type: 0,
exchange_rate: '1828.71',
historic_exchange_rate: null,
to: ADDRESS_PARAMS,
transaction_burnt_fee: null,
max_fee_per_gas: null,
+1
View File
@@ -54,6 +54,7 @@ export type Transaction = {
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: string | null;
historic_exchange_rate: string | null;
method: string | null;
transaction_types: Array<TransactionType>;
transaction_tag: string | null;
+1
View File
@@ -118,6 +118,7 @@ export default function useBlockTxsQuery({ heightOrHash, blockQuery, tab }: Para
token_transfers: null,
token_transfers_overflow: false,
exchange_rate: null,
historic_exchange_rate: null,
method: null,
transaction_types: [],
transaction_tag: null,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+7 -2
View File
@@ -11,6 +11,7 @@ import TokenValue from 'ui/shared/value/TokenValue';
interface Props extends BoxProps {
loading?: boolean;
tx: Transaction | Pick<Transaction, WrappedTransactionFields>;
hasExchangeRateToggle?: boolean;
accuracy?: number;
accuracyUsd?: number;
noTooltip?: boolean;
@@ -19,7 +20,7 @@ interface Props extends BoxProps {
layout?: 'horizontal' | 'vertical';
}
const TxFee = ({ tx, accuracy, accuracyUsd, loading, noSymbol: noSymbolProp, noUsd, noTooltip, ...rest }: Props) => {
const TxFee = ({ tx, accuracy, accuracyUsd, loading, noSymbol: noSymbolProp, noUsd, noTooltip, hasExchangeRateToggle, ...rest }: Props) => {
if ('celo' in tx && tx.celo?.gas_token) {
return (
@@ -48,12 +49,16 @@ const TxFee = ({ tx, accuracy, accuracyUsd, loading, noSymbol: noSymbolProp, noU
}
const noSymbol = noSymbolProp || config.UI.views.tx.hiddenFields?.fee_currency;
const exchangeRate = 'exchange_rate' in tx ? tx.exchange_rate : null;
const historicalExchangeRate = 'historic_exchange_rate' in tx ? tx.historic_exchange_rate : null;
return (
<NativeCoinValue
amount={ tx.fee.value || '0' }
noSymbol={ noSymbol }
exchangeRate={ !noUsd && 'exchange_rate' in tx ? tx.exchange_rate : null }
exchangeRate={ noUsd ? null : exchangeRate }
historicalExchangeRate={ noUsd ? null : historicalExchangeRate }
hasExchangeRateToggle={ hasExchangeRateToggle }
accuracy={ accuracy }
accuracyUsd={ accuracyUsd }
loading={ loading }
+103
View File
@@ -0,0 +1,103 @@
import React from 'react';
import { test, expect } from 'playwright/lib';
import AssetValue from './AssetValue';
test.use({ viewport: { width: 300, height: 100 } });
test('no exchange rate', async({ render }) => {
const component = await render(
<AssetValue
amount="12345678901234567890"
decimals="18"
/>,
);
await expect(component).toHaveScreenshot();
});
test('current exchange rate, horizontal layout', async({ render }) => {
const component = await render(
<AssetValue
amount="12345678901234567890"
decimals="18"
exchangeRate="1234.56"
/>,
);
await expect(component).toHaveScreenshot();
});
test('current exchange rate, vertical layout', async({ render }) => {
const component = await render(
<AssetValue
amount="12345678901234567890"
decimals="18"
exchangeRate="1234.56"
layout="vertical"
/>,
);
await expect(component).toHaveScreenshot();
});
test('current + historic, default shows historic with tag', async({ render }) => {
const component = await render(
<AssetValue
amount="12345678901234567890"
decimals="18"
exchangeRate="1234.56"
historicalExchangeRate="1100.00"
hasExchangeRateToggle
/>,
);
await expect(component).toHaveScreenshot();
});
test('current + historic, tooltip shows historic label', async({ render, page }) => {
await render(
<AssetValue
amount="12345678901234567890"
decimals="18"
exchangeRate="1234.56"
historicalExchangeRate="1100.00"
hasExchangeRateToggle
/>,
);
// hover over the USD value to trigger the tooltip
await page.locator('text=$').first().hover();
await expect(page.getByText('Estimated value on day of txn')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('current + historic, toggle to current', async({ render, page }) => {
await render(
<AssetValue
amount="12345678901234567890"
decimals="18"
exchangeRate="1234.56"
historicalExchangeRate="1100.00"
hasExchangeRateToggle
/>,
);
// click the tag to toggle to current
await page.locator('text=$').first().click();
// verify tooltip shows current label
await page.locator('text=$').first().hover();
await expect(page.getByText('Current value')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('zero value, no tag shown', async({ render }) => {
const component = await render(
<AssetValue
amount="0"
decimals="18"
exchangeRate="1234.56"
historicalExchangeRate="1100.00"
hasExchangeRateToggle
/>,
);
await expect(component).toHaveScreenshot();
});
+86 -21
View File
@@ -1,8 +1,10 @@
import type { BoxProps } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
import { Tag } from 'toolkit/chakra/tag';
import { thinsp } from 'toolkit/utils/htmlEntities';
import IconSvg from 'ui/shared/IconSvg';
import type { Params as CalculateUsdValueParams } from './calculateUsdValue';
import calculateUsdValue from './calculateUsdValue';
@@ -18,6 +20,8 @@ export interface Props extends Omit<BoxProps, 'prefix' | 'suffix'>, Omit<Calcula
loading?: boolean;
layout?: 'horizontal' | 'vertical';
tooltipContent?: React.ReactNode;
historicalExchangeRate?: string | null;
hasExchangeRateToggle?: boolean;
}
const AssetValue = ({
@@ -31,18 +35,37 @@ const AssetValue = ({
noTooltip,
loading,
exchangeRate,
historicalExchangeRate,
hasExchangeRateToggle,
layout = 'horizontal',
tooltipContent,
...rest
}: Props) => {
const hasHistorical = Boolean(historicalExchangeRate);
const hasCurrent = Boolean(exchangeRate);
const hasToggle = hasHistorical && hasCurrent && hasExchangeRateToggle && amount !== '0';
const [ showHistorical, setShowHistorical ] = React.useState(hasHistorical);
React.useEffect(() => {
setShowHistorical(hasHistorical);
}, [ hasHistorical ]);
const activeExchangeRate = showHistorical ? historicalExchangeRate : exchangeRate;
const handleToggle = React.useCallback(() => {
if (hasToggle) {
setShowHistorical(prev => !prev);
}
}, [ hasToggle ]);
if (amount === null || amount === undefined) {
return <chakra.span { ...rest }>-</chakra.span>;
}
const { valueBn, usdBn } = calculateUsdValue({ amount, decimals, accuracy, accuracyUsd, exchangeRate });
const { valueBn, usdBn } = calculateUsdValue({ amount, decimals, accuracy, accuracyUsd, exchangeRate: activeExchangeRate });
if (!exchangeRate) {
if (!activeExchangeRate) {
return (
<SimpleValue
value={ valueBn }
@@ -57,6 +80,64 @@ const AssetValue = ({
);
}
const nativeValue = (
<SimpleValue
value={ valueBn }
accuracy={ accuracy }
startElement={ startElement }
endElement={ endElement ?? (typeof asset === 'string' ? `${ thinsp }${ asset }` : asset) }
tooltipContent={ tooltipContent }
noTooltip={ noTooltip }
loading={ loading }
/>
);
const clockIcon = showHistorical ? (
<IconSvg name="clock-light" boxSize="14px" color="icon.secondary"/>
) : undefined;
const tooltipContentBefore = (() => {
if (!amount || amount === '0') {
return undefined;
}
// for values where historic exchange rate doesn't exist, we show current value without tooltip
// for recent transactions, historicExchangeRate can be null, in this case we show current value with tooltip
if (historicalExchangeRate === undefined) {
return undefined;
}
return <Box>{ showHistorical ? 'Estimated value on day of txn' : 'Current value' }</Box>;
})();
const usdValue = hasToggle ? (
<Tag
size="md"
startElement={ clockIcon }
onClick={ handleToggle }
loading={ loading }
variant="clickable"
>
<SimpleValue
value={ usdBn }
accuracy={ accuracyUsd }
prefix="$"
tooltipContentBefore={ tooltipContentBefore }
/>
</Tag>
) : (
<SimpleValue
value={ usdBn }
accuracy={ accuracyUsd }
startElement={ layout === 'horizontal' ? <span>(</span> : undefined }
prefix="$"
endElement={ layout === 'horizontal' ? <span>)</span> : undefined }
tooltipContentBefore={ tooltipContentBefore }
loading={ loading }
color="text.secondary"
/>
);
return (
<chakra.span
display="inline-flex"
@@ -67,24 +148,8 @@ const AssetValue = ({
rowGap={ 1 }
{ ...rest }
>
<SimpleValue
value={ valueBn }
accuracy={ accuracy }
startElement={ startElement }
endElement={ endElement ?? (typeof asset === 'string' ? `${ thinsp }${ asset }` : asset) }
tooltipContent={ tooltipContent }
noTooltip={ noTooltip }
loading={ loading }
/>
<SimpleValue
value={ usdBn }
accuracy={ accuracyUsd }
startElement={ layout === 'horizontal' ? <span>(</span> : undefined }
prefix="$"
endElement={ layout === 'horizontal' ? <span>)</span> : undefined }
loading={ loading }
color="text.secondary"
/>
{ nativeValue }
{ usdValue }
</chakra.span>
);
};
+10 -5
View File
@@ -22,6 +22,7 @@ export interface Props extends Omit<BoxProps, 'prefix' | 'postfix'> {
noTooltip?: boolean;
loading?: boolean;
overflowed?: boolean;
tooltipContentBefore?: React.ReactNode;
}
const SimpleValue = ({
@@ -32,6 +33,7 @@ const SimpleValue = ({
startElement,
endElement,
tooltipContent: tooltipContentProp,
tooltipContentBefore,
noTooltip,
loading,
overflowed,
@@ -40,12 +42,15 @@ const SimpleValue = ({
const tooltipContent = React.useMemo(() => {
return (
<Box display="inline" whiteSpace="wrap" wordBreak="break-all">
{ prefix ?? '' }{ value.toFormat() }{ postfix ?? '' }
<CopyToClipboard text={ value.toFixed() } verticalAlign="bottom" noTooltip/>
</Box>
<>
{ tooltipContentBefore }
<Box display="inline" whiteSpace="wrap" wordBreak="break-all">
{ prefix ?? '' }{ value.toFormat() }{ postfix ?? '' }
<CopyToClipboard text={ value.toFixed() } verticalAlign="bottom" noTooltip/>
</Box>
</>
);
}, [ postfix, prefix, value ]);
}, [ postfix, prefix, value, tooltipContentBefore ]);
return (
<Skeleton loading={ loading } display="inline-flex" alignItems="center" whiteSpace="pre" maxW="100%" overflow="hidden" { ...rest }>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+1 -1
View File
@@ -77,7 +77,7 @@ const TxDetailsWrapped = ({ data }: Props) => {
Transaction fee
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<TxFee tx={ data }/>
<TxFee tx={ data } hasExchangeRateToggle/>
</DetailedInfo.ItemValue>
</>
) }
+2
View File
@@ -43,6 +43,8 @@ const TxDetailsBurntFees = ({ data, isLoading }: Props) => {
<DetailedInfoNativeCoinValue
amount={ value.toString() }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
startElement={ <IconSvg name="flame" boxSize={ 5 } color="icon.primary" isLoading={ isLoading } mr={{ base: 0, lg: 1 }}/> }
loading={ isLoading }
/>
@@ -30,6 +30,8 @@ const TxDetailsSetMaxGasLimit = ({ data }: Props) => {
<DetailedInfoNativeCoinValue
amount={ maxGasLimit.toString() }
exchangeRate={ 'exchange_rate' in data ? data.exchange_rate : null }
historicalExchangeRate={ 'historic_exchange_rate' in data ? data.historic_exchange_rate : null }
hasExchangeRateToggle
unitsTooltip="gwei"
copyOriginalValue
/>
+13 -3
View File
@@ -28,15 +28,21 @@ const TxDetailsTxFee = ({ isLoading, data }: Props) => {
loading={ isLoading }
accuracy={ 0 }
rowGap={ 0 }
hasExchangeRateToggle
/>
);
}
const exchangeRate = 'exchange_rate' in data ? data.exchange_rate : null;
const historicalExchangeRate = 'historic_exchange_rate' in data ? data.historic_exchange_rate : null;
return (
<>
<NativeCoinValue
amount={ data.fee.value }
exchangeRate={ 'exchange_rate' in data ? data.exchange_rate : null }
exchangeRate={ exchangeRate }
historicalExchangeRate={ historicalExchangeRate }
hasExchangeRateToggle
loading={ isLoading }
unitsTooltip="gwei"
copyOriginalValue
@@ -52,7 +58,9 @@ const TxDetailsTxFee = ({ isLoading, data }: Props) => {
>
<NativeCoinValue
amount={ BigNumber(data.base_fee_per_gas || 0).multipliedBy(data.gas_used || 0).toString() }
exchangeRate={ 'exchange_rate' in data ? data.exchange_rate : null }
exchangeRate={ exchangeRate }
historicalExchangeRate={ historicalExchangeRate }
hasExchangeRateToggle
accuracy={ 0 }
unitsTooltip="gwei"
copyOriginalValue
@@ -67,7 +75,9 @@ const TxDetailsTxFee = ({ isLoading, data }: Props) => {
>
<NativeCoinValue
amount={ data.priority_fee || '0' }
exchangeRate={ 'exchange_rate' in data ? data.exchange_rate : null }
exchangeRate={ exchangeRate }
historicalExchangeRate={ historicalExchangeRate }
hasExchangeRateToggle
accuracy={ 0 }
unitsTooltip="gwei"
copyOriginalValue
+12
View File
@@ -628,6 +628,8 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus, noTxActions }: P
<DetailedInfoNativeCoinValue
amount={ data.value }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
@@ -645,6 +647,8 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus, noTxActions }: P
<DetailedInfoNativeCoinValue
amount={ data.operator_fee }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
@@ -661,6 +665,8 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus, noTxActions }: P
<DetailedInfoNativeCoinValue
amount={ data.arbitrum.poster_fee }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
@@ -673,6 +679,8 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus, noTxActions }: P
<DetailedInfoNativeCoinValue
amount={ data.arbitrum.network_fee }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
@@ -832,6 +840,8 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus, noTxActions }: P
asset={ rollupFeature.parentChain.currency?.symbol || currencyUnits.ether }
decimals={ rollupFeature.parentChain.currency?.decimals ?? config.chain.currency.decimals }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
@@ -874,6 +884,8 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus, noTxActions }: P
amount={ BigNumber(data.blob_gas_used).multipliedBy(data.blob_gas_price).toString() }
noSymbol={ config.UI.views.tx.hiddenFields?.fee_currency }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
+8
View File
@@ -30,6 +30,8 @@ export const TxInfoScrollFees = ({ data, isLoading }: Props) => {
<DetailedInfoNativeCoinValue
amount={ data.scroll?.l1_fee }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
@@ -46,6 +48,8 @@ export const TxInfoScrollFees = ({ data, isLoading }: Props) => {
<DetailedInfoNativeCoinValue
amount={ data.scroll?.l2_fee.value }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
@@ -62,6 +66,8 @@ export const TxInfoScrollFees = ({ data, isLoading }: Props) => {
<DetailedInfoNativeCoinValue
amount={ String(data.scroll?.l1_fee_commit_scalar) }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
@@ -78,6 +84,8 @@ export const TxInfoScrollFees = ({ data, isLoading }: Props) => {
<DetailedInfoNativeCoinValue
amount={ String(data.scroll?.l1_fee_overhead) }
exchangeRate={ data.exchange_rate }
historicalExchangeRate={ data.historic_exchange_rate }
hasExchangeRateToggle
loading={ isLoading }
/>
</>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 205 KiB

+1
View File
@@ -63,6 +63,7 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
<NativeCoinValue
amount={ tx.value }
exchangeRate={ tx.exchange_rate }
historicalExchangeRate={ tx.historic_exchange_rate }
noTooltip
/>
</Box>
+1
View File
@@ -128,6 +128,7 @@ const TxsListItem = ({
<NativeCoinValue
amount={ tx.value }
exchangeRate={ tx.exchange_rate }
historicalExchangeRate={ tx.historic_exchange_rate }
loading={ isLoading }
color="text.secondary"
/>
+1
View File
@@ -139,6 +139,7 @@ const TxsTableItem = ({
noSymbol
loading={ isLoading }
exchangeRate={ tx.exchange_rate }
historicalExchangeRate={ tx.historic_exchange_rate }
layout="vertical"
rowGap={ 3 }
/>