feat(tooltips): add helper tooltip system with knowledge-level support#326
Conversation
Add a reusable helper tooltip infrastructure that provides contextual explanations for blockchain data fields across all explorer pages. Infrastructure: - HelperTooltip component: hover/focus/tap with a11y support - FieldLabel wrapper: knowledge-level-aware tooltip visibility - Settings UI: enable/disable toggle + beginner/intermediate/advanced selector - Navbar quick-switcher: cycle through levels with colored border indicator - New `tooltips` i18n namespace with content in all 5 languages Coverage: - EVM transaction page: all fields including L2 (Arbitrum, OP Stack) and blob fields - EVM block page: all fields including More Details (advanced-only) and Arbitrum fields - Address/contract pages: verification, proxy, read/write, balance, nonce, ENS, EIP-7702 - Token/NFT pages: standard, decimals, supply, token ID, owner, approved, metadata URI - Bitcoin transaction/block/address pages: all fields with BTC-specific content - Settings page: knowledge-level selector tooltip
|
🚀 Preview: https://pr-326--openscan.netlify.app |
josealoha666
left a comment
There was a problem hiding this comment.
Nice addition overall — the coverage is thorough and the FieldLabel/HelperTooltip split makes the rollout easy to follow.
Two blockers before merge:
src/locales/ja/tooltips.jsonhas mojibake/corrupted text inaddress.implementationAddress(コント��クト). That will ship visibly broken copy in the Japanese locale.- Current CI is red: all E2E jobs on this PR timed out at the 15m limit. Even if that's partly unrelated/flaky, it needs to be green or explained before merging.
|
Thanks for the review Jose! Regarding the mojibake in Regarding E2E timeouts: These are pre-existing flaky timeouts unrelated to this PR (no runtime logic changed — only tooltip UI additions and i18n content). Will be addressed separately. |
MatiasOS
left a comment
There was a problem hiding this comment.
- Z-index is broken on Tx details - Datos de Entrada
- Missing tooltips on tx History table header
- Account type missing Tooltip
- Missing tooltips on tx details table header Call Tree, Gas Profiler, State changes
- Missing tooltip on block header status (Finalized)
- Arbitrum dificulty says it should be zero, but value is 1
- Contract details missing tooltips on Contract bytecode, Source Code and Raw ABI and Functions headers
- Bitcoin block headr missing tooltip next to confirmations
- Bitcoin tx header missing tooltip next to confirmations
- Bitcoin tx inputs and outputs missing all tooltips
-
All networks missing tootlip in netowrk page
-
Optional. Dont like the the begginer, personal, advanced strategy. I think this will be more clear if:
begginer: All tooltips
intermediare: Hide basic blockchain fields like txs, blocks, value, balnces
Advanced: Tx types, ENS, Kleros, integrations, debug tooltips, and developer mode info,etc. and chain specifc fields ( l2 fields, etc)
| "gasLimit": "Maximum gas allowed in this block, defining its capacity.", | ||
| "baseFeePerGas": "Minimum gas price for this block, set by the network based on demand (EIP-1559).", | ||
| "burntFees": "Transaction fees permanently removed from circulation via EIP-1559.", | ||
| "blobGasUsed": "Total blob gas consumed by blob transactions in this block.", |
There was a problem hiding this comment.
Defines Blobs with blobs. I think that is better to define Blob
There was a problem hiding this comment.
Some concepts are not easy or chain specific. We migth add an extra description to some fields introducing some concepts. Something like this:
{
"transaction": {
"hash": "A unique identifier for this transaction, generated from its contents.",
"from": "The address that initiated and signed this transaction.",
"status": "Whether the transaction executed successfully or reverted.",
"confirmations": "Blocks mined after this transaction. More confirmations means higher finality.",
"interactedWith": "The contract this transaction called, or the recipient address for simple transfers.",
"transactionFee": "Total fee paid to process this transaction (gas used × gas price). Gas measures computational work on the network, and under EIP-1559 fees include a base fee (burned) plus a priority fee (tip).",
"gasPrice": "Price per unit of gas, in Gwei. Higher gas prices incentivize faster inclusion.",
"gasLimitUsage": "Gas limit is the maximum gas authorized. Gas used is the actual amount consumed.",
"nonce": "Sender's transaction counter. Ensures transactions are processed in order and prevents replay attacks.",
"position": "The index of this transaction within its block. Lower positions were processed first.",
"type": "The transaction format (e.g. Legacy, EIP-1559, EIP-4844). Determines which fee fields apply.",
"decodedInput": "The function call and parameters sent to the contract, decoded into human-readable form.",
"maxFeePerBlobGas": "Maximum price per unit of blob gas the sender is willing to pay for data availability.",
"blobGasPrice": "Actual price per unit of blob gas paid in this block.",
"blobGasUsed": "Amount of blob gas consumed by this transaction's data blobs. Blobs are data containers introduced in EIP-4844 for rollup data availability, priced separately from execution gas.",
"blobCount": "Number of data blobs attached to this transaction for rollup data availability.",
"blobVersionedHashes": "Versioned hashes that uniquely identify each data blob committed by this transaction.",
"contractCreated": "The address of the new contract deployed by this transaction.",
"value": "Amount of native currency (e.g. ETH) transferred in this transaction.",
"effectiveGasPrice": "The actual gas price paid after EIP-1559 fee calculation, which may differ from the requested price.",
"l1BlockNumber": "The Ethereum L1 block that anchors this L2 transaction for security.",
"gasUsedForL1": "Gas consumed for posting this transaction's data to Ethereum L1.",
"l1Fee": "Fee paid for data availability on Ethereum L1. L2 networks rely on L1 to publish transaction data for security and verification.",
"l1GasPrice": "The L1 gas price used to calculate the data availability fee component.",
"l1GasUsed": "Estimated L1 gas required to post this transaction's calldata.",
"l1FeeScalar": "A multiplier applied to the L1 fee calculation by the L2 sequencer."
},
"block": {
"hash": "A unique fingerprint identifying this block, derived from its contents.",
"transactions": "The number of transactions included and executed in this block.",
"withdrawals": "Validator withdrawals processed in this block, returning staked ETH.",
"feeRecipient": "The address that receives priority fees from transactions in this block.",
"difficulty": "Mining difficulty for this block. Always zero on proof-of-stake networks after the Merge.",
"totalDifficulty": "Cumulative difficulty of the chain up to this block. Legacy field from proof-of-work.",
"size": "The total size of the block data in bytes.",
"extraData": "Arbitrary data set by the block producer. Often contains client or pool identifiers.",
"gasUsed": "Total gas consumed by all transactions in this block.",
"gasLimit": "Maximum gas allowed in this block, defining its capacity.",
"baseFeePerGas": "Minimum gas price for this block, set by the network based on demand (EIP-1559). This base fee is burned.",
"burntFees": "Transaction fees permanently removed from circulation via EIP-1559.",
"blobGasUsed": "Total blob gas consumed by blob transactions in this block.",
"excessBlobGas": "Blob gas above the target, used to calculate the next block's blob base fee.",
"blobCount": "Number of data blobs included in this block for rollup data availability.",
"l1BlockNumber": "The Ethereum L1 block number associated with this Arbitrum block.",
"sendCount": "Number of outgoing L2-to-L1 messages sent in this Arbitrum block.",
"sendRoot": "Merkle root of all outgoing L2-to-L1 messages, used for cross-layer verification.",
"parentHash": "Hash of the previous block in the chain, linking blocks together.",
"stateRoot": "Merkle root of the entire blockchain state after processing this block (a cryptographic summary of all account data).",
"transactionsRoot": "Merkle root of all transactions included in this block.",
"receiptsRoot": "Merkle root of all transaction receipts from this block.",
"withdrawalsRoot": "Merkle root of all validator withdrawals processed in this block.",
"logsBloom": "A bloom filter (probabilistic data structure) for efficiently searching event logs emitted in this block.",
"nonce": "A value used in proof-of-work mining. Always zero after the Merge.",
"mixHash": "A hash used in the proof-of-work algorithm. Replaced by RANDAO value after the Merge.",
"sha3Uncles": "Hash of the uncle blocks list. Always the empty list hash after the Merge.",
"validator": "The validator index that initiated this withdrawal from the beacon chain."
},
"address": {
"verification": "A verified contract has its source code publicly confirmed to match the deployed bytecode.",
"proxyType": "This contract uses an upgradeable proxy pattern, where execution is delegated to a separate implementation contract, allowing upgrades without changing the address.",
"readContract": "Query contract data without spending gas or connecting a wallet. Read-only calls.",
"writeContract": "Send a transaction that changes contract state. Requires a connected wallet and gas.",
"balance": "Native currency balance held by this address.",
"usdValue": "Estimated fiat value based on current market price.",
"nonce": "Number of transactions sent from this address. Used for transaction ordering.",
"eip7702Delegate": "This account delegates its code execution to another address via EIP-7702.",
"implementationAddress": "The contract that contains the actual logic for this proxy.",
"ensName": "Ethereum Name Service name associated with this address (maps human-readable names to addresses).",
"ensApp": "Application or resolver linked to this ENS name."
},
"token": {
"tokenStandard": "The token interface standard (e.g. ERC-20, ERC-721, ERC-1155) that defines how this token behaves.",
"decimals": "Number of decimal places used to display token amounts. Most ERC-20 tokens use 18.",
"totalSupply": "Total number of tokens that have been created for this contract.",
"tokenId": "A unique numeric identifier for this specific token within its collection.",
"owner": "The address that currently holds this NFT.",
"approved": "An address authorized to transfer this specific NFT on behalf of the owner.",
"metadataUri": "The URI where this token's metadata (name, image, attributes) is stored."
},
"bitcoin": {
"txid": "A unique identifier for this transaction, computed from its serialized data.",
"witnessHash": "Transaction hash including witness (SegWit) data. Differs from TXID for SegWit transactions.",
"block": "The block that includes this transaction.",
"status": "Whether this transaction has been confirmed in a block or is still waiting in the mempool.",
"inputs": "The number of previous outputs being spent. Each input references a UTXO (Unspent Transaction Output). Bitcoin uses a UTXO model where transactions consume and create discrete outputs.",
"outputs": "The number of new outputs created. Each output locks BTC to an address.",
"fee": "The difference between total inputs and total outputs, paid to the miner.",
"feePerByte": "Fee rate in satoshis per byte of raw transaction data.",
"feePerVByte": "Fee rate in satoshis per virtual byte. The standard fee metric for SegWit transactions.",
"feePerWU": "Fee rate in satoshis per weight unit. The most granular fee metric.",
"size": "Raw transaction size in bytes, including witness data.",
"virtualSize": "Adjusted transaction size that discounts witness data. Used for fee calculation.",
"weight": "Transaction weight in weight units (WU). 1 vByte = 4 WU.",
"coinbase": "Whether this is a coinbase transaction that creates new BTC as a block reward.",
"witness": "Whether this transaction uses Segregated Witness (SegWit) format.",
"rbf": "Replace-By-Fee. If enabled, this transaction can be replaced by one with a higher fee before confirmation.",
"version": "Transaction format version number. Determines which features are available.",
"locktime": "Earliest block height or time when this transaction can be included in a block.",
"blockHash": "The unique hash identifying the block that contains this transaction.",
"minedBy": "The mining pool or entity that produced this block.",
"blockReward": "New BTC created as a reward for mining this block (subsidy + fees).",
"totalFees": "Sum of all transaction fees collected by the miner in this block.",
"feeRate": "Average and median fee rates of transactions in this block.",
"transactions": "Number of transactions included in this block.",
"totalOutput": "Total BTC value of all outputs created in this block.",
"difficulty": "How hard it is to find a valid block hash. Adjusts every 2016 blocks.",
"blockSize": "Total size of the serialized block data.",
"blockWeight": "Block weight in weight units. Maximum is 4,000,000 WU.",
"previousBlock": "Hash of the preceding block, linking this block to the chain.",
"nextBlock": "Hash of the following block, if one exists.",
"coinbaseMessage": "Arbitrary text embedded in the coinbase transaction by the miner.",
"merkleRoot": "Root hash of the Merkle tree of all transactions in this block (a structure that allows efficient verification).",
"blockVersion": "Block format version. Encodes which consensus rules and soft forks are signaled.",
"bits": "Compact encoding of the target threshold for a valid block hash.",
"blockNonce": "A value miners iterate to find a block hash below the target.",
"coinbaseHex": "Raw hexadecimal data of the coinbase transaction input.",
"address": "A Bitcoin address derived from a public key, used to receive BTC.",
"balance": "Total BTC held by this address across all unspent outputs.",
"totalReceived": "Cumulative BTC received by this address across all transactions.",
"utxos": "Unspent Transaction Outputs. Individual coins available to be spent by this address.",
"txCount": "Total number of transactions involving this address."
},
"settings": {
"knowledgeLevel": "Controls how much explanatory help is shown throughout the explorer."
}
}- Fix tooltip z-index clipping by rendering bubbles via React Portal to document.body, escaping all ancestor overflow containers - Enrich tooltip descriptions with EIP-1559, EIP-4844, UTXO model, Merkle tree, and proxy pattern explanations across all 5 locales - Fix Arbitrum difficulty tooltip and circular blob definition - Add missing tooltips: tx history table headers, account type, analyser tabs, finalized badge, contract details, Bitcoin confirmations, inputs/outputs columns, network stats, state changes - Reclassify knowledge levels: beginner=all tooltips, intermediate=hides basic fields, advanced=complex/chain-specific only - Update knowledge level descriptions in all locale settings files
Move coinbase and witness to left column, RBF to right column for better visual grouping of related fields.
Fetch block height from block header when displaying confirmed transactions. Block number is shown as plain text before the clickable block hash link.
Support all four directions (top, bottom, left, right) for tooltip bubbles. Use left placement for confirmation and finalized badges in the top-right corner. Add vertical viewport clamping.
Compute final pixel position for both axes before removing transform, so vertical translation is preserved when only horizontal clamping is needed and vice versa.
josealoha666
left a comment
There was a problem hiding this comment.
Re-reviewing the new head 1a787a8:
- the broken Japanese
implementationAddressstring I flagged earlier is fixed in the committed file - the follow-up patch covers the missing tooltip surfaces raised in review and the tooltip portal/z-index change looks directionally right
- no new code issues jumped out in the incremental diff
I'm not approving yet because CI is still running, but from the code side my previous blockers look addressed.
Render a CSS triangle arrow inside the tooltip bubble that points toward the trigger. Arrow position is dynamically calculated after viewport clamping to always aim at the trigger center.
josealoha666
left a comment
There was a problem hiding this comment.
Quick re-review for head cfc9998:
- the new arrow-pointer addition in
HelperTooltiplooks consistent with the existing portal/clamp logic - I checked the incremental diff only; no new code issues stood out to me in this follow-up
- CI is still pending on this head, so I'm leaving this as a comment rather than approving yet
josealoha666
left a comment
There was a problem hiding this comment.
I checked the current head and I do not see an obvious blocking issue from the diff/API view. If CI is green, this looks fine from my side.

Description
Add a reusable helper tooltip system that provides contextual explanations for blockchain data fields across all explorer pages. Tooltips are gated by a user-configurable knowledge level (Beginner / Intermediate / Advanced) with an enable/disable toggle.
Related Issue
N/A — new feature
Type of Change
Changes Made
Infrastructure
src/components/common/HelperTooltip.tsx): Reusable tooltip primitive with 350ms hover delay, keyboard focus support, Escape dismiss, tap-to-toggle for mobile, top/bottom collision detection,aria-describedbyaccessibilitysrc/components/common/FieldLabel.tsx): Thin wrapper that reads knowledge level from settings and conditionally renders the tooltip icon based onvisibleForpropsrc/styles/helper-tooltip.css): Styling using existing CSS variables, mobile-responsive, dark/light theme support?icon button (left of theme toggle) that cycles through levels. Border color indicates current level (green = beginner, blue = intermediate, purple = advanced). Shows notification on switchtooltipsi18n namespace: Content registry with all tooltip strings in 5 languages (en, es, zh, ja, pt-BR)KnowledgeLeveltype +knowledgeLevelandshowHelperTooltipsfields added toUserSettingsPage Coverage (every visible field label)
Visibility Levels
Screenshots (if applicable)
N/A — tooltips are interactive, best tested locally
Checklist
npm run format:fixandnpm run lint:fixnpm run typecheckwith no errorsnpm run test:runAdditional Notes
width: fit-contenton.notificationin styles.css)onMouseEnter/onMouseLeave(not pointer events) to cleanly separate desktop hover from mobile tap behavior