mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
fix(explore): nothing has been burned — stop labelling the fee a burn
explore.lux.network/block/1098234 renders "Burnt fees 0.000525 LUX" on the same page that names the recipient: Miner 0x0100000000000000000000000000000000000000. Both cannot be true. The block list repeats it per row, with a "Burnt fees / Txn fees * 100%" ratio that reads 100%, and the tx details page says it again. To an investor that is a deflation claim, and it is false. Measured on mainnet 96369 via https://api.lux.network/v1/bc/C/rpc at block 1098244 (eth_getChainConfig ANSWERS — chainId 96369 is in the result — so the absence below is a real absence, not a failed call): feeSplitTimestamp unset -> IsFeeSplit(t) is false for every t vault 0x0100..0002 0 LUX -> rewardShare has never been credited coinbase 0x0100..0000 3867.79 LUX and climbing (3867.55 an hour earlier) luxfi/evm core/fee_split.go only destroys wei while the split is active; before activation creditTxFee credits the WHOLE fee to the coinbase, which is an account, so total supply does not move. Every wei of every fee this chain has ever charged is sitting at 0x0100..0000. Zero has been burned. The label is now derived, in the ONE place fee-split policy already lives (lib/api/cchain/useFeeSplit.ts, alongside deriveStatus and burnedWei), from the same cached reading the fee panel uses — so a page of 50 block rows still issues one batch: active -> "Burnt fees", burn ratio shown inactive/scheduled-> "Fees to coinbase", ratio suppressed unknown -> "Fees (destination unknown)", ratio suppressed 'unknown' is a real third state: a node that will not answer eth_getChainConfig has not told us the fee was burned, and it has not told us it wasn't. Rendering either claim would be inventing one. Surfaces: block details (label, hint, ratio, and the reward-breakdown tooltip), blocks table header + rows, mobile blocks list, tx details. 5 new cases in useFeeSplit.spec.ts, 22/22 green; tsc clean on all seven touched files. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { FeeSplitReading, RpcEnvelope } from './useFeeSplit';
|
||||
import { burnedWei, deriveStatus, parseFeeSplitBatch, recordSample, toCoinSeries } from './useFeeSplit';
|
||||
import { burnedWei, deriveFeeDestination, deriveStatus, feeDestinationCopy, parseFeeSplitBatch, recordSample, toCoinSeries } from './useFeeSplit';
|
||||
|
||||
// Verbatim, trimmed only of chain-config fields the panel never reads.
|
||||
// id 1 = fee reward vault balance, id 2 = coinbase balance,
|
||||
@@ -183,3 +183,33 @@ describe('recordSample', () => {
|
||||
expect(series[series.length - 1].blockNumber).toBe(299);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveFeeDestination — "Burnt fees" is a claim, not a label', () => {
|
||||
it('only calls it burned while the split is active', () => {
|
||||
expect(deriveFeeDestination('active')).toBe('burned');
|
||||
});
|
||||
|
||||
// Mainnet 96369 today: eth_getChainConfig answers (chainId present) with no
|
||||
// feeSplitTimestamp, the vault holds 0 LUX and the coinbase 0x0100..0000
|
||||
// holds 3867.79 LUX. Every wei of every fee went to an account.
|
||||
it('says coinbase before activation, scheduled or not', () => {
|
||||
expect(deriveFeeDestination('inactive')).toBe('coinbase');
|
||||
expect(deriveFeeDestination('scheduled')).toBe('coinbase');
|
||||
});
|
||||
|
||||
it('makes no claim when the config could not be read', () => {
|
||||
expect(deriveFeeDestination('unknown')).toBe('unknown');
|
||||
});
|
||||
|
||||
it('never shows a burn ratio for a fee that was not burned', () => {
|
||||
expect(feeDestinationCopy('burned').showBurnRatio).toBe(true);
|
||||
expect(feeDestinationCopy('coinbase').showBurnRatio).toBe(false);
|
||||
expect(feeDestinationCopy('unknown').showBurnRatio).toBe(false);
|
||||
});
|
||||
|
||||
it('never labels an un-burned fee "Burnt fees"', () => {
|
||||
expect(feeDestinationCopy('coinbase').label).not.toContain('Burnt');
|
||||
expect(feeDestinationCopy('unknown').label).not.toContain('Burnt');
|
||||
expect(feeDestinationCopy('burned').label).toBe('Burnt fees');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,6 +110,88 @@ export function burnedWei(reading: FeeSplitReading | undefined): string | undefi
|
||||
return reading.status === 'active' ? reading.vaultWei : '0';
|
||||
}
|
||||
|
||||
// ── Where a fee actually goes ──
|
||||
|
||||
// Every "Burnt fees" label in this explorer is a claim about deflation, and on
|
||||
// this chain it is currently false. `creditTxFee` only destroys wei while the
|
||||
// split is active; before activation it credits the WHOLE fee to the block
|
||||
// coinbase, which is an account, so supply does not move. Measured on mainnet
|
||||
// 96369 at block 1098244: eth_getChainConfig returns a config object (chainId
|
||||
// 96369 present, so the call works) with NO feeSplitTimestamp, the vault
|
||||
// 0x0100…0002 holds 0 LUX, and the coinbase 0x0100…0000 holds 3867.79 LUX and
|
||||
// climbing. Not one wei has been burned, yet /block/1098234 rendered
|
||||
// "Burnt fees 0.000525 LUX".
|
||||
//
|
||||
// So the label is derived, in this one place, from the same reading the fee
|
||||
// panel uses. Callers ask once and render what they are told.
|
||||
export type FeeDestination =
|
||||
|
||||
/** creditTxFee destroys ceil(fee/2): "burnt" is accurate. */
|
||||
| 'burned' |
|
||||
|
||||
/** The whole fee is credited to the block coinbase: nothing is burned. */
|
||||
'coinbase' |
|
||||
|
||||
/** The node did not answer. Make no claim either way. */
|
||||
'unknown';
|
||||
|
||||
export function deriveFeeDestination(status: FeeSplitStatus): FeeDestination {
|
||||
if (status === 'unknown') {
|
||||
return 'unknown';
|
||||
}
|
||||
return status === 'active' ? 'burned' : 'coinbase';
|
||||
}
|
||||
|
||||
export interface FeeDestinationCopy {
|
||||
readonly label: string;
|
||||
readonly hint: string;
|
||||
|
||||
/** False when a "share of fees burnt" ratio would assert something untrue. */
|
||||
readonly showBurnRatio: boolean;
|
||||
}
|
||||
|
||||
const COIN = () => config.chain.currency.symbol || 'the native token';
|
||||
|
||||
export function feeDestinationCopy(destination: FeeDestination): FeeDestinationCopy {
|
||||
switch (destination) {
|
||||
case 'burned':
|
||||
return {
|
||||
label: 'Burnt fees',
|
||||
hint: `Amount of ${ COIN() } destroyed by the transactions in this block. ` +
|
||||
'The fee split is active, so half of every fee is credited to no account and ' +
|
||||
'the supply falls by that much. Equals Block Base Fee per Gas * Gas Used.',
|
||||
showBurnRatio: true,
|
||||
};
|
||||
case 'coinbase':
|
||||
return {
|
||||
label: 'Fees to coinbase',
|
||||
hint: `Base Fee per Gas * Gas Used, in ${ COIN() }. NOT burned: the fee split is ` +
|
||||
`not active on this chain, so the whole fee is credited to the block coinbase ` +
|
||||
`${ FEE_COINBASE }, an account with no code and no key. Total supply is unchanged.`,
|
||||
showBurnRatio: false,
|
||||
};
|
||||
case 'unknown':
|
||||
return {
|
||||
label: 'Fees (destination unknown)',
|
||||
hint: `Base Fee per Gas * Gas Used, in ${ COIN() }. This node did not answer ` +
|
||||
'eth_getChainConfig, so whether the fee was burned or credited to the block ' +
|
||||
'coinbase could not be established. No claim is made here.',
|
||||
showBurnRatio: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The fee destination for the current chain, read live.
|
||||
*
|
||||
* Shares `useFeeSplit`'s query key, so a page that renders many rows issues one
|
||||
* batch for all of them and the fee panel reuses the same cached reading.
|
||||
*/
|
||||
export function useFeeDestination(): FeeDestination {
|
||||
const { reading } = useFeeSplit();
|
||||
return reading ? deriveFeeDestination(reading.status) : 'unknown';
|
||||
}
|
||||
|
||||
// ── Series ──
|
||||
|
||||
// One observation per head block, kept for the browsing session.
|
||||
|
||||
+11
-10
@@ -1,3 +1,6 @@
|
||||
import { CollapsibleDetails } from '@luxfi/ui/collapsible';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { capitalize } from 'es-toolkit';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -8,16 +11,14 @@ import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
|
||||
import { route, routeParams } from 'nextjs/routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { feeDestinationCopy, useFeeDestination } from 'lib/api/cchain/useFeeSplit';
|
||||
import getBlockReward from 'lib/block/getBlockReward';
|
||||
import { useMultichainContext } from 'lib/contexts/multichain';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import * as arbitrum from 'lib/rollups/arbitrum';
|
||||
import { formatZkSyncL2TxnBatchStatus, layerLabels } from 'lib/rollups/utils';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { CollapsibleDetails } from '@luxfi/ui/collapsible';
|
||||
import { Link } from 'toolkit/next/link';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import { ZERO } from 'toolkit/utils/consts';
|
||||
import { space } from 'toolkit/utils/htmlEntities';
|
||||
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
|
||||
@@ -58,6 +59,9 @@ const BlockDetails = ({ query }: Props) => {
|
||||
const router = useRouter();
|
||||
const heightOrHash = getQueryParamString(router.query.height_or_hash);
|
||||
const multichainContext = useMultichainContext();
|
||||
// Whether the base fee is actually destroyed on this chain right now, read
|
||||
// live. Decides the label below; it must never be assumed.
|
||||
const feeCopy = feeDestinationCopy(useFeeDestination());
|
||||
|
||||
const { data, isPlaceholderData } = query;
|
||||
|
||||
@@ -105,7 +109,7 @@ const BlockDetails = ({ query }: Props) => {
|
||||
{ !burntFees.isEqualTo(ZERO) && (
|
||||
<>
|
||||
{ space }-{ space }
|
||||
<Tooltip content="Burnt fees">
|
||||
<Tooltip content={ feeCopy.label }>
|
||||
<span>{ burntFees.dividedBy(WEI).toFixed() }</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
@@ -498,13 +502,10 @@ const BlockDetails = ({ query }: Props) => {
|
||||
{ !config.UI.views.block.hiddenFields?.burnt_fees && !burntFees.isEqualTo(ZERO) && (
|
||||
<>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint={
|
||||
`Amount of ${ config.chain.currency.symbol || 'native token' } burned from transactions included in the block.
|
||||
Equals Block Base Fee per Gas * Gas Used`
|
||||
}
|
||||
hint={ feeCopy.hint }
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
Burnt fees
|
||||
{ feeCopy.label }
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue multiRow>
|
||||
<NativeCoinValue
|
||||
@@ -514,7 +515,7 @@ const BlockDetails = ({ query }: Props) => {
|
||||
startElement={ <IconSvg name="flame" className="w-5 h-5 text-[var(--color-icon-primary)] mr-1 lg:mr-2" isLoading={ isPlaceholderData }/> }
|
||||
mr={ 4 }
|
||||
/>
|
||||
{ !txFees.isEqualTo(ZERO) && (
|
||||
{ feeCopy.showBurnRatio && !txFees.isEqualTo(ZERO) && (
|
||||
<Tooltip content="Burnt fees / Txn fees * 100%">
|
||||
<Utilization
|
||||
value={ burntFees.dividedBy(txFees).toNumber() }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { capitalize } from 'es-toolkit';
|
||||
import React from 'react';
|
||||
@@ -8,12 +10,11 @@ import type { ClusterChainConfig } from 'types/multichain';
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { feeDestinationCopy, useFeeDestination } from 'lib/api/cchain/useFeeSplit';
|
||||
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import { Link } from 'toolkit/next/link';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
@@ -35,6 +36,9 @@ interface Props {
|
||||
const isRollup = config.features.rollup.isEnabled;
|
||||
|
||||
const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation, chainData }: Props) => {
|
||||
// The label for this column is a live read, not an assumption: base fees are
|
||||
// only destroyed while the fee split is active. See lib/api/cchain/useFeeSplit.
|
||||
const feeCopy = feeDestinationCopy(useFeeDestination());
|
||||
const totalReward = getBlockTotalReward(data);
|
||||
const burntFees = BigNumber(data.burnt_fees || 0);
|
||||
const txFees = BigNumber(data.transaction_fees || 0);
|
||||
@@ -118,7 +122,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation, chain
|
||||
) }
|
||||
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && (
|
||||
<div>
|
||||
<span className="font-medium">Burnt fees</span>
|
||||
<span className="font-medium">{ feeCopy.label }</span>
|
||||
<div className="flex gap-x-4 mt-2">
|
||||
<NativeCoinValue
|
||||
amount={ data.burnt_fees }
|
||||
@@ -128,7 +132,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation, chain
|
||||
display="flex"
|
||||
color="text.secondary"
|
||||
/>
|
||||
<Utilization value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
|
||||
{ feeCopy.showBurnRatio && <Utilization value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/> }
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from '@luxfi/ui/table';
|
||||
import { capitalize } from 'es-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
@@ -5,11 +6,11 @@ import type { Block } from 'types/api/block';
|
||||
import type { ClusterChainConfig } from 'types/multichain';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { feeDestinationCopy, useFeeDestination } from 'lib/api/cchain/useFeeSplit';
|
||||
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
|
||||
import useInitialList from 'lib/hooks/useInitialList';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from '@luxfi/ui/table';
|
||||
import BlocksTableItem from 'ui/blocks/BlocksTableItem';
|
||||
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
|
||||
import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle';
|
||||
@@ -33,6 +34,8 @@ const FEES_COL_WEIGHT = 22;
|
||||
const isRollup = config.features.rollup.isEnabled;
|
||||
|
||||
const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum, showSocketErrorAlert, chainData }: Props) => {
|
||||
// Column heading is a live read — see lib/api/cchain/useFeeSplit.
|
||||
const feeCopy = feeDestinationCopy(useFeeDestination());
|
||||
const initialList = useInitialList({
|
||||
data: data ?? [],
|
||||
idFn: (item) => item.height,
|
||||
@@ -66,7 +69,7 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
|
||||
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward &&
|
||||
<TableColumnHeader width={ `${ REWARD_COL_WEIGHT / widthBase * 100 }%` }>Reward { currencyUnits.ether }</TableColumnHeader> }
|
||||
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees &&
|
||||
<TableColumnHeader width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>Burnt fees { currencyUnits.ether }</TableColumnHeader> }
|
||||
<TableColumnHeader width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>{ feeCopy.label } { currencyUnits.ether }</TableColumnHeader> }
|
||||
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee &&
|
||||
<TableColumnHeader width="150px" isNumeric>Base fee</TableColumnHeader> }
|
||||
</TableRow>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { TableCell, TableRow } from '@luxfi/ui/table';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
@@ -7,11 +10,9 @@ import type { ClusterChainConfig } from 'types/multichain';
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { feeDestinationCopy, useFeeDestination } from 'lib/api/cchain/useFeeSplit';
|
||||
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import { Link } from 'toolkit/next/link';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { TableCell, TableRow } from '@luxfi/ui/table';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
|
||||
import BlockPendingUpdateHint from 'ui/shared/block/BlockPendingUpdateHint';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
@@ -34,6 +35,9 @@ interface Props {
|
||||
const isRollup = config.features.rollup.isEnabled;
|
||||
|
||||
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement, animation, chainData }: Props) => {
|
||||
// The label for this column is a live read, not an assumption: base fees are
|
||||
// only destroyed while the fee split is active. See lib/api/cchain/useFeeSplit.
|
||||
const feeCopy = feeDestinationCopy(useFeeDestination());
|
||||
const totalReward = getBlockTotalReward(data);
|
||||
const burntFees = BigNumber(data.burnt_fees || 0);
|
||||
const txFees = BigNumber(data.transaction_fees || 0);
|
||||
@@ -125,9 +129,11 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement, animation, chai
|
||||
loading={ isLoading }
|
||||
display="flex"
|
||||
/>
|
||||
<Tooltip content="Burnt fees / Txn fees * 100%" disabled={ isLoading }>
|
||||
<Utilization className="mt-2 w-min" value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
|
||||
</Tooltip>
|
||||
{ feeCopy.showBurnRatio && (
|
||||
<Tooltip content="Burnt fees / Txn fees * 100%" disabled={ isLoading }>
|
||||
<Utilization className="mt-2 w-min" value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
|
||||
</Tooltip>
|
||||
) }
|
||||
</TableCell>
|
||||
) }
|
||||
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee && data.base_fee_per_gas && (
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import { feeDestinationCopy, useFeeDestination } from 'lib/api/cchain/useFeeSplit';
|
||||
import { ZERO } from 'toolkit/utils/consts';
|
||||
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
|
||||
import DetailedInfoNativeCoinValue from 'ui/shared/DetailedInfo/DetailedInfoNativeCoinValue';
|
||||
@@ -18,6 +18,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const TxDetailsBurntFees = ({ data, isLoading }: Props) => {
|
||||
// Whether this fee was destroyed or credited to the block coinbase is a live
|
||||
// read, never an assumption. See lib/api/cchain/useFeeSplit.
|
||||
const feeCopy = feeDestinationCopy(useFeeDestination());
|
||||
|
||||
if (config.UI.views.tx.hiddenFields?.burnt_fees || (rollupFeature.isEnabled && rollupFeature.type === 'optimistic')) {
|
||||
return null;
|
||||
@@ -32,13 +35,12 @@ const TxDetailsBurntFees = ({ data, isLoading }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint={ `
|
||||
Amount of ${ currencyUnits.ether } burned for this transaction. Equals Block Base Fee per Gas * Gas Used
|
||||
${ data.blob_gas_price && data.blob_gas_used ? ' + Blob Gas Price * Blob Gas Used' : '' }
|
||||
` }
|
||||
hint={ `${ feeCopy.hint }${
|
||||
data.blob_gas_price && data.blob_gas_used ? ' Includes Blob Gas Price * Blob Gas Used.' : ''
|
||||
}` }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Burnt fees
|
||||
{ feeCopy.label }
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfoNativeCoinValue
|
||||
amount={ value.toString() }
|
||||
|
||||
Reference in New Issue
Block a user