From a5b258ebc67549ecb27dabc556836d513280d3be Mon Sep 17 00:00:00 2001 From: Isa Ozler Date: Tue, 12 Aug 2025 12:43:17 +0200 Subject: [PATCH] Block reward display fix --- ui/block/BlockDetails.tsx | 36 +--- ui/block/BlockRewards.tsx | 360 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+), 31 deletions(-) create mode 100644 ui/block/BlockRewards.tsx diff --git a/ui/block/BlockDetails.tsx b/ui/block/BlockDetails.tsx index 91b733f1a0..8afbc1fe54 100644 --- a/ui/block/BlockDetails.tsx +++ b/ui/block/BlockDetails.tsx @@ -40,6 +40,7 @@ import Utilization from 'ui/shared/Utilization/Utilization'; import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo'; +import KDABlockRewards from './BlockRewards'; import BlockDetailsBaseFeeCelo from './details/BlockDetailsBaseFeeCelo'; import BlockDetailsBlobInfo from './details/BlockDetailsBlobInfo'; import BlockDetailsZilliqaQuorumCertificate from './details/BlockDetailsZilliqaQuorumCertificate'; @@ -73,7 +74,7 @@ const BlockDetails = ({ query }: Props) => { return null; } - const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data); + const { totalReward, burntFees, txFees } = getBlockReward(data); const validatorTitle = getNetworkValidatorTitle(); @@ -82,33 +83,7 @@ const BlockDetails = ({ query }: Props) => { return null; } - if (isPlaceholderData) { - return ; - } - - return ( - - - { staticReward.dividedBy(WEI).toFixed() } - - { !txFees.isEqualTo(ZERO) && ( - <> - { space }+{ space } - - { txFees.dividedBy(WEI).toFixed() } - - - ) } - { !burntFees.isEqualTo(ZERO) && ( - <> - { space }-{ space } - - { burntFees.dividedBy(WEI).toFixed() } - - - ) } - - ); + return ; })(); const verificationTitle = `${ capitalize(getNetworkValidationActionText()) } by`; @@ -382,10 +357,9 @@ const BlockDetails = ({ query }: Props) => { Block reward - - { totalReward.dividedBy(WEI).toFixed() } { currencyUnits.ether } + + { rewardBreakDown } - { rewardBreakDown } ) } diff --git a/ui/block/BlockRewards.tsx b/ui/block/BlockRewards.tsx new file mode 100644 index 0000000000..6c5a7e6926 --- /dev/null +++ b/ui/block/BlockRewards.tsx @@ -0,0 +1,360 @@ +import { chakra, Text } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React, { useCallback, useMemo } from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { currencyUnits } from 'lib/units'; +import { Skeleton } from 'toolkit/chakra/skeleton'; +import { Tooltip } from 'toolkit/chakra/tooltip'; +import { Hint } from 'toolkit/components/Hint/Hint'; +import { WEI, ZERO } from 'toolkit/utils/consts'; +import { space } from 'toolkit/utils/htmlEntities'; + +import getBlockReward from '../../lib/block/getBlockReward'; +import RawDataSnippet from '../shared/RawDataSnippet'; +import type { BlockQuery } from './useBlockQuery'; + +interface Props { + query: BlockQuery; +} + +type MouseEnterLeaveHandler = (event: React.MouseEvent) => void; +type MouseEnterLeaveLabelHandler = (event: React.MouseEvent) => void; + +type TKDABlockRewardsData = { + isLoading: boolean; + loading: { + queryIsLoading: boolean; + withdrawalsIsLoading: boolean; + }; + data: { + blockData?: BlockQuery['data']; + withdrawalsData?: { items: Array<{ amount?: string }> }; + }; + calculatedValues: { + rewardAmount: BigNumber; + rewardBaseFee: BigNumber; + txFees: BigNumber; + burntFees: BigNumber; + hasRewardBaseFee: boolean; + hasTxFees: boolean; + hasBurntFees: boolean; + } | null; + formattedValues: { + rewardAmount?: string; + rewardBaseFee?: string; + txFees?: string; + burntFees?: string; + }; +}; + +interface TKDABreakdownItemProps { + isLoading: boolean; + onMouseEnter?: MouseEnterLeaveHandler; + onMouseLeave?: MouseEnterLeaveHandler; + children: React.ReactNode; +} + +const KDABreakdownItem = React.forwardRef(({ + children, + isLoading, + onMouseEnter, + onMouseLeave, +}, ref) => { + return ( + + + + ); +}); + +KDABreakdownItem.displayName = 'KDABreakdownItem'; + +interface BreakdownLabelProps { + onMouseEnterValues: MouseEnterLeaveLabelHandler; + onMouseLeaveValues: MouseEnterLeaveLabelHandler; + children: React.ReactNode; +} + +const BreakdownLabel = React.forwardRef(({ + onMouseEnterValues, + onMouseLeaveValues, + children, +}, ref) => ( + + { children } + +)); + +BreakdownLabel.displayName = 'BreakdownLabel'; + +export const KDABlockRewardsData = (query: BlockQuery): TKDABlockRewardsData => { + const { data, isLoading: queryIsLoading } = query; + + const { data: withdrawalsData, isLoading: withdrawalsIsLoading } = useApiQuery('general:block_withdrawals', { + queryParams: { height_or_hash: `${ data?.height }` }, + }); + const { totalReward, burntFees, txFees } = data ? getBlockReward(data) : { totalReward: ZERO, burntFees: ZERO, txFees: ZERO }; + + const calculatedValues = useMemo(() => { + if (!withdrawalsData?.items?.length) { + return null; + } + + const [ reward ] = withdrawalsData.items; + const rewardAmount = BigNumber(reward.amount ?? 0); + const rewardBaseFee = BigNumber(rewardAmount.toNumber() - totalReward.toNumber() + burntFees.toNumber()); + + return { + rewardAmount: rewardAmount.dividedBy(WEI), + rewardBaseFee: rewardBaseFee.dividedBy(WEI), + txFees: txFees.dividedBy(WEI), + burntFees: burntFees.dividedBy(WEI), + hasRewardBaseFee: !rewardBaseFee.isEqualTo(ZERO), + hasTxFees: !txFees.isEqualTo(ZERO), + hasBurntFees: !burntFees.isEqualTo(ZERO), + }; + }, [ withdrawalsData, totalReward, burntFees, txFees ]); + + return { + isLoading: withdrawalsIsLoading || queryIsLoading, + loading: { + queryIsLoading, + withdrawalsIsLoading, + }, + data: { + blockData: data, + withdrawalsData, + }, + calculatedValues, + formattedValues: { + rewardAmount: calculatedValues?.rewardAmount ? BigNumber(calculatedValues.rewardAmount).toFixed() : undefined, + rewardBaseFee: calculatedValues?.rewardBaseFee ? BigNumber(calculatedValues.rewardBaseFee).toFixed() : undefined, + txFees: calculatedValues?.txFees ? BigNumber(calculatedValues.txFees).toFixed() : undefined, + burntFees: calculatedValues?.burntFees ? BigNumber(calculatedValues.burntFees).toFixed() : undefined, + }, + }; +}; + +const KDABlockRewards = ({ query /* data, txFees, burntFees, totalReward */ }: Props) => { + const blockRewardValuesRef = React.useRef(null); + const blockRewardLabelRef = React.useRef(null); + const txFeesValuesRef = React.useRef(null); + const txFeesLabelRef = React.useRef(null); + const burntFeesValuesRef = React.useRef(null); + const burntFeesLabelRef = React.useRef(null); + + const { + isLoading, + calculatedValues, + formattedValues: { + rewardAmount, + rewardBaseFee, + txFees, + burntFees, + }, + } = KDABlockRewardsData(query); + const { + hasRewardBaseFee, + hasTxFees, + hasBurntFees, + } = calculatedValues ?? {}; + + const onMouseEnterValues = useCallback((ref: React.RefObject) => { + const element = ref.current?.querySelector('.chakra-skeleton') as HTMLElement; + + if (element) { + element.style.backgroundColor = 'var(--kda-explorer-alert-background-semantic-info) !important'; + } + }, []); + + const onMouseLeaveValues = useCallback((ref: React.RefObject) => { + const element = ref.current?.querySelector('.chakra-skeleton') as HTMLElement; + + if (element) { + element.style.backgroundColor = ''; + } + }, []); + + const onMouseEnterLabels = useCallback((ref: React.RefObject) => { + if (ref.current) { + ref.current.style.color = 'var(--kda-color-accent-blue)'; + } + }, []); + + const onMouseLeaveLabels = useCallback((ref: React.RefObject) => { + if (ref.current) { + ref.current.style.color = ''; + } + }, []); + + const handleBlockRewardMouseEnter = useCallback(() => onMouseEnterLabels(blockRewardLabelRef), [ onMouseEnterLabels ]); + const handleBlockRewardMouseLeave = useCallback(() => onMouseLeaveLabels(blockRewardLabelRef), [ onMouseLeaveLabels ]); + + const handleTxFeesMouseEnter = useCallback(() => onMouseEnterLabels(txFeesLabelRef), [ onMouseEnterLabels ]); + const handleTxFeesMouseLeave = useCallback(() => onMouseLeaveLabels(txFeesLabelRef), [ onMouseLeaveLabels ]); + + const handleBurntFeesMouseEnter = useCallback(() => onMouseEnterLabels(burntFeesLabelRef), [ onMouseEnterLabels ]); + const handleBurntFeesMouseLeave = useCallback(() => onMouseLeaveLabels(burntFeesLabelRef), [ onMouseLeaveLabels ]); + + const handleBlockRewardValuesMouseEnter = useCallback(() => onMouseEnterValues(blockRewardValuesRef), [ onMouseEnterValues ]); + const handleBlockRewardValuesMouseLeave = useCallback(() => onMouseLeaveValues(blockRewardValuesRef), [ onMouseLeaveValues ]); + + const handleTxFeesValuesMouseEnter = useCallback(() => onMouseEnterValues(txFeesValuesRef), [ onMouseEnterValues ]); + const handleTxFeesValuesMouseLeave = useCallback(() => onMouseLeaveValues(txFeesValuesRef), [ onMouseLeaveValues ]); + + const handleBurntFeesValuesMouseEnter = useCallback(() => onMouseEnterValues(burntFeesValuesRef), [ onMouseEnterValues ]); + const handleBurntFeesValuesMouseLeave = useCallback(() => onMouseLeaveValues(burntFeesValuesRef), [ onMouseLeaveValues ]); + + if (isLoading) { + return ; + } + + if (!calculatedValues) { + return null; + } + + return ( + + + { rewardAmount } { currencyUnits.ether } + + + + Breakdown{ space } + + + + + + + Block reward + + + + + + + + + + Transaction fees + + + + - + + + + + Burnt fees + + + + + + { hasRewardBaseFee && ( + + + + Reward fee: + + { rewardBaseFee } + + + ) + } + { space }+{ space } + { hasTxFees && ( + + + + Transaction fees: + + { txFees } + + + ) } + { space }-{ space } + { hasBurntFees && ( + + + + Burnt fees:{ ' ' } + + { burntFees } + + + ) } + + ) } + isLoading={ isLoading } + /> + + + ); +}; + +KDABlockRewards.displayName = 'KDABlockRewards'; + +export default KDABlockRewards;