Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 48 additions & 9 deletions src/pages/ethereum/execution/gas-profiler/BlockPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ import {
TRANSACTION_BUCKETS,
BlockOpcodeHeatmap,
ContractActionPopover,
ResourceGasBreakdown,
ResourceGasTable,
ResourceGasHelp,
} from './components';
import type { ContractInteractionItem, TopGasItem } from './components';
import { useBlockResourceGas } from './hooks/useBlockResourceGas';
import { useCallFrameResourceGas } from './hooks/useCallFrameResourceGas';
import { useNetwork } from '@/hooks/useNetwork';
import { CATEGORY_COLORS, CALL_TYPE_COLORS, getOpcodeCategory, getEtherscanBaseUrl, isMainnet } from './utils';
import type { GasProfilerBlockSearch } from './IndexPage.types';
Expand Down Expand Up @@ -97,7 +102,7 @@ function filterAndSortTransactions(
}

// Tab hash values for URL-based navigation
const BLOCK_TAB_HASHES = ['overview', 'opcodes', 'transactions', 'calls'] as const;
const BLOCK_TAB_HASHES = ['overview', 'opcodes', 'resources', 'transactions', 'calls'] as const;

/**
* Block detail page - shows all transactions in a block with analytics
Expand Down Expand Up @@ -165,6 +170,21 @@ export function BlockPage(): JSX.Element {
enabled: !isNaN(blockNumber),
});

// Fetch block-level resource gas breakdown
const {
entries: resourceEntries,
refund: resourceRefund,
total: resourceTotal,
isLoading: resourceLoading,
} = useBlockResourceGas({ blockNumber: isNaN(blockNumber) ? null : blockNumber });

// Fetch per-opcode resource gas for the block (all transactions)
const { opcodeRows: blockOpcodeResourceRows, isLoading: blockResourceOpcodeLoading } = useCallFrameResourceGas({
transactionHash: null,
blockNumber: isNaN(blockNumber) ? null : blockNumber,
callFrameId: null,
});

// Handle sort change (uses local state to avoid scroll reset)
const handleSortChange = useCallback(
(newSort: TransactionSortField) => {
Expand Down Expand Up @@ -1057,17 +1077,21 @@ export function BlockPage(): JSX.Element {
<HeadlessTab.List className="flex gap-1">
<Tab hash="overview">Overview</Tab>
<Tab hash="opcodes">Opcodes</Tab>
<Tab hash="resources">Resources</Tab>
<Tab hash="transactions">Transactions</Tab>
<Tab hash="calls">Calls</Tab>
</HeadlessTab.List>
<Link
to="/ethereum/execution/gas-profiler/simulate"
search={{ block: blockNumber }}
className="mb-1 flex items-center gap-1.5 rounded-xs px-3 py-1.5 text-sm text-muted transition-colors hover:bg-primary/10 hover:text-primary"
>
<BeakerIcon className="size-4" />
Simulate
</Link>
<div className="mb-1 flex items-center gap-2">
{selectedTabIndex === 2 && <ResourceGasHelp />}
<Link
to="/ethereum/execution/gas-profiler/simulate"
search={{ block: blockNumber }}
className="flex items-center gap-1.5 rounded-xs px-3 py-1.5 text-sm text-muted transition-colors hover:bg-primary/10 hover:text-primary"
>
<BeakerIcon className="size-4" />
Simulate
</Link>
</div>
</div>

<HeadlessTab.Panels>
Expand Down Expand Up @@ -1220,6 +1244,21 @@ export function BlockPage(): JSX.Element {
)}
</HeadlessTab.Panel>

{/* Resources Tab */}
<HeadlessTab.Panel>
<ResourceGasBreakdown
entries={resourceEntries}
refund={resourceRefund}
total={resourceTotal}
loading={resourceLoading}
title="Gas by Resource Category"
subtitle="What system resources consumed gas?"
className="mb-6"
/>

<ResourceGasTable rows={blockOpcodeResourceRows} loading={blockResourceOpcodeLoading} />
</HeadlessTab.Panel>

{/* Transactions Tab */}
<HeadlessTab.Panel>
{/* Filter bar */}
Expand Down
35 changes: 31 additions & 4 deletions src/pages/ethereum/execution/gas-profiler/CallPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import {
GasFormula,
CallTraceView,
ContractStorageCTA,
ResourceGasBreakdown,
ResourceGasTable,
ResourceGasHelp,
} from './components';
import { useCallFrameResourceGas } from './hooks/useCallFrameResourceGas';
import { useNetwork } from '@/hooks/useNetwork';
import { CATEGORY_COLORS, getOpcodeCategory, getEtherscanBaseUrl, isMainnet } from './utils';
import { isPrecompileAddress } from './utils/precompileNames';
Expand Down Expand Up @@ -171,6 +175,17 @@ export function CallPage(): JSX.Element {
callFrameId: callIdNum,
});

// Fetch per-opcode resource gas for this call frame
const {
entries: frameResourceEntries,
opcodeRows: frameOpcodeResourceRows,
isLoading: frameResourceLoading,
} = useCallFrameResourceGas({
transactionHash: txHash,
blockNumber,
callFrameId: callIdNum,
});

// Find the current frame from all frames
const currentFrame = useMemo<IntTransactionCallFrame | null>(() => {
if (!txData?.callFrames) return null;
Expand Down Expand Up @@ -292,6 +307,7 @@ export function CallPage(): JSX.Element {
const hash = window.location.hash.slice(1);
if (hash === 'overview') return 0;
if (hash === 'opcodes') return 1;
if (hash === 'resources') return 2;
return 0;
}, []);
const [selectedTabIndex, setSelectedTabIndex] = useState(getTabIndexFromHash);
Expand Down Expand Up @@ -635,10 +651,14 @@ export function CallPage(): JSX.Element {

{/* Tabbed Content */}
<HeadlessTab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<HeadlessTab.List className="mb-6 flex gap-1 border-b border-border">
<Tab hash="overview">Overview</Tab>
<Tab hash="opcodes">Opcodes</Tab>
</HeadlessTab.List>
<div className="mb-6 flex items-center justify-between border-b border-border">
<HeadlessTab.List className="flex gap-1">
<Tab hash="overview">Overview</Tab>
<Tab hash="opcodes">Opcodes</Tab>
<Tab hash="resources">Resources</Tab>
</HeadlessTab.List>
{selectedTabIndex === 2 && <ResourceGasHelp />}
</div>

<HeadlessTab.Panels>
{/* Overview Tab */}
Expand Down Expand Up @@ -788,6 +808,13 @@ export function CallPage(): JSX.Element {
</Card>
)}
</HeadlessTab.Panel>

{/* Resources Tab */}
<HeadlessTab.Panel>
<ResourceGasBreakdown entries={frameResourceEntries} loading={frameResourceLoading} className="mb-6" />

<ResourceGasTable rows={frameOpcodeResourceRows} loading={frameResourceLoading} />
</HeadlessTab.Panel>
</HeadlessTab.Panels>
</HeadlessTab.Group>
</Container>
Expand Down
51 changes: 43 additions & 8 deletions src/pages/ethereum/execution/gas-profiler/TransactionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ import {
TopItemsByGasTable,
CallTraceView,
ContractActionPopover,
ResourceGasBreakdown,
ResourceGasTable,
ResourceGasHelp,
} from './components';
import type { TopGasItem, CallFrameData } from './components';
import { getCallLabel } from './hooks/useTransactionGasData';
import { useCallFrameResourceGas } from './hooks/useCallFrameResourceGas';
import { useNetwork } from '@/hooks/useNetwork';
import {
CATEGORY_COLORS,
Expand Down Expand Up @@ -114,6 +118,17 @@ export function TransactionPage(): JSX.Element {
blockNumber,
});

// Fetch per-opcode resource gas for the resource view
const {
entries: txResourceCategoryEntries,
opcodeRows: txOpcodeResourceRows,
isLoading: txResourceOpcodeLoading,
} = useCallFrameResourceGas({
transactionHash: txHash,
blockNumber,
callFrameId: null,
});

// Derive badge stats from full opcode data (SSTORE, SLOAD, LOG*, CREATE*, SELFDESTRUCT)
const callFrameOpcodeStats = useMemo((): Map<number, CallFrameOpcodeStats> => {
const statsMap = new Map<number, CallFrameOpcodeStats>();
Expand Down Expand Up @@ -501,8 +516,9 @@ export function TransactionPage(): JSX.Element {
if (hash === 'overview') return 0;
if (hash === 'trace') return 1;
if (hash === 'opcodes') return 2;
if (hash === 'calls') return 3;
if (hash === 'contracts') return 4;
if (hash === 'resources') return 3;
if (hash === 'calls') return 4;
if (hash === 'contracts') return 5;
return 0;
}, []);
const [selectedTabIndex, setSelectedTabIndex] = useState(getInitialTabIndex);
Expand Down Expand Up @@ -818,12 +834,16 @@ export function TransactionPage(): JSX.Element {

{/* Tabbed Content */}
<HeadlessTab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<HeadlessTab.List className="mb-6 flex gap-1 border-b border-border">
<Tab hash="overview">Overview</Tab>
{!isSimpleTransfer && <Tab hash="trace">Trace</Tab>}
{!isSimpleTransfer && <Tab hash="opcodes">Opcodes</Tab>}
{!isSimpleTransfer && callTypeChartData.length > 0 && <Tab hash="calls">Internal Txs</Tab>}
</HeadlessTab.List>
<div className="mb-6 flex items-center justify-between border-b border-border">
<HeadlessTab.List className="flex gap-1">
<Tab hash="overview">Overview</Tab>
{!isSimpleTransfer && <Tab hash="trace">Trace</Tab>}
{!isSimpleTransfer && <Tab hash="opcodes">Opcodes</Tab>}
{!isSimpleTransfer && <Tab hash="resources">Resources</Tab>}
{!isSimpleTransfer && callTypeChartData.length > 0 && <Tab hash="calls">Internal Txs</Tab>}
</HeadlessTab.List>
{selectedTabIndex === 3 && <ResourceGasHelp />}
</div>

<HeadlessTab.Panels>
{/* Overview Tab */}
Expand Down Expand Up @@ -999,6 +1019,21 @@ export function TransactionPage(): JSX.Element {
</HeadlessTab.Panel>
)}

{/* Resources Tab - only show if not a simple transfer */}
{!isSimpleTransfer && (
<HeadlessTab.Panel>
<ResourceGasBreakdown
entries={txResourceCategoryEntries}
loading={txResourceOpcodeLoading}
title="Gas by Resource Category"
subtitle="What system resources consumed gas?"
className="mb-6"
/>

<ResourceGasTable rows={txOpcodeResourceRows} loading={txResourceOpcodeLoading} />
</HeadlessTab.Panel>
)}

{/* Internal Txs Tab - only show if not a simple transfer and there are internal txs */}
{!isSimpleTransfer && callTypeChartData.length > 0 && (
<HeadlessTab.Panel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,15 +360,19 @@ function TraceRow({
[onToggleExpand, callId]
);

// Display label
const displayLabel =
node.functionName ||
node.contractName ||
(frame.target_address
? `${frame.target_address.slice(0, 10)}...${frame.target_address.slice(-8)}`
: isContractCreation
? 'Contract Creation'
: 'Unknown');
// Display label — for precompiles, prefer the precompile name (contractName) over functionName
// because the "function selector" is just the first 4 bytes of calldata, not a real selector.
const displayLabel = isPrecompile
? node.contractName ||
node.functionName ||
(frame.target_address ? `${frame.target_address.slice(0, 10)}...${frame.target_address.slice(-8)}` : 'Unknown')
: node.functionName ||
node.contractName ||
(frame.target_address
? `${frame.target_address.slice(0, 10)}...${frame.target_address.slice(-8)}`
: isContractCreation
? 'Contract Creation'
: 'Unknown');

return (
<>
Expand Down Expand Up @@ -411,6 +415,13 @@ function TraceRow({
</span>
)}

{/* Precompile badge */}
{isPrecompile && (
<span className="shrink-0 rounded-xs bg-rose-500/20 px-1.5 py-0.5 text-xs font-medium text-rose-400">
precompile
</span>
)}

{/* Contract/Function name */}
<span className={clsx('truncate', hasError ? 'text-danger' : 'text-foreground', 'group-hover:text-primary')}>
{node.contractName && !isPrecompile && (
Expand Down
Loading