feat(txn): add enhanced Decibel transaction details to existing transaction page#1508
feat(txn): add enhanced Decibel transaction details to existing transaction page#1508gregnazario wants to merge 7 commits intomainfrom
Conversation
✅ Deploy Preview for aptos-explorer ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
- New route at /transactions/decibel for browsing Decibel network transactions - DecibelNetworkStatus component showing chain ID, epoch, ledger version, block height - DecibelTransactionCard with expandable detailed view including: - Function display with module highlighting - Events summary with type chips and full event data - Payload preview with type arguments and decoded arguments - State changes summary with change type breakdown - VM status display - Auto-refresh every 10 seconds - Pagination support
- Add /transactions/decibel to llms.txt and llms-full.txt - Add sitemap.xml entry for Decibel transactions page - Add drift test coverage for /transactions/decibel path - Bump llms-full.txt revision to 10
Will instead enhance the existing transaction detail page with Decibel-specific parsing and display.
When viewing transactions on the Decibel network (?network=decibel):
- Transaction title shows a 'Decibel Network' badge chip
- Page metadata includes Decibel-specific title, description, and keywords
- UserTransactionOverviewTab shows a DecibelDetailsPanel with:
- Event Timeline: categorized events with color-coded type chips
(Deposit/Withdraw/Transfer/Swap/Mint/Burn/Fee/etc), shortened
type names, contract address links, and inline decoded data
with amount formatting (raw + human-readable for Octa values)
- Payload Breakdown: function displayed with module highlighting
and linked contract address, type arguments with struct name
emphasis, and indexed arguments with JSON viewer for complex values
- State Changes summary: change type counts and list of touched
modules extracted from write-set resources
The panel is visually distinct with a primary-color border and
appears above the standard overview content.
Instead of gating the enhanced details to ?network=decibel, detect Decibel transactions by checking whether the sender, payload function target, event sources, or change addresses match the known Decibel contract address (0x50ead...). This makes the enhanced panel appear on mainnet, testnet, and decibel alike.
The Decibel enhanced details section is now an Accordion that starts collapsed. Users click the header row (Decibel chip + label + chevron) to expand and reveal the Event Timeline, Payload Breakdown, and State Changes sections.
The Decibel details accordion now shows a plain-English summary: - Collapsed header: first summary line + brief preview of next lines - Expanded: full bulleted 'What Happened' section above the technical details The summariser parses the function call, events, and state changes into sentences like 'Called Transfer on the coin module', 'Moved 1.5 APT out and 1.2 APT in across 4 operations', 'Performed a token swap', etc. Amounts are auto-converted from Octas to APT for readability.
3354737 to
ecdf750
Compare
There was a problem hiding this comment.
Pull request overview
This PR enhances the existing transaction detail page (/txn/:id/:tab) by detecting transactions that involve the Decibel contract and surfacing a collapsible “Decibel” details panel with human-readable summaries, event timeline, payload breakdown, and state-change aggregation.
Changes:
- Add Decibel transaction detection (
isDecibelTransaction) and a newDecibelDetailsPanelUI. - Update transaction title/metadata to show a “Decibel” badge and Decibel-specific metadata when applicable.
- Inject the Decibel details panel into the user transaction overview tab when a transaction is detected as Decibel-related.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| app/pages/Transaction/Title.tsx | Adds Decibel badge + Decibel-specific metadata based on detection logic. |
| app/pages/Transaction/Tabs/UserTransactionOverviewTab.tsx | Conditionally renders DecibelDetailsPanel for Decibel-related transactions. |
| app/pages/Transaction/Tabs/Components/DecibelDetailsPanel.tsx | New accordion panel that summarizes and displays Decibel-related transaction details. |
| function SectionHeader({ | ||
| icon, | ||
| title, | ||
| count, | ||
| }: { | ||
| icon: React.ReactNode; | ||
| title: string; | ||
| count?: number; | ||
| }) { |
There was a problem hiding this comment.
SectionHeader references React.ReactNode, but this file doesn't import the React types. This will fail TypeScript compilation unless React is in scope; add import type React from "react" (or switch to ReactNode from react).
| storageRefundOctas !== undefined | ||
| ? gasFeeOctas - storageRefundOctas | ||
| : gasFeeOctas; | ||
| lines.push(`Gas fee: ${octasToApt(net.toString())} APT.`); |
There was a problem hiding this comment.
octasToApt() already appends the "APT" unit for non-zero values, but the gas fee line adds "APT" again (Gas fee: ${octasToApt(...)} APT.), which will render as "... APT APT.". Either have octasToApt return a unitless number string or remove the extra "APT" suffix in this sentence.
| lines.push(`Gas fee: ${octasToApt(net.toString())} APT.`); | |
| lines.push(`Gas fee: ${octasToApt(net.toString())}.`); |
| {summary.map((line) => ( | ||
| <Typography | ||
| component="li" | ||
| key={line} | ||
| variant="body2" | ||
| sx={{pl: 0.5}} | ||
| > | ||
| {line} | ||
| </Typography> | ||
| ))} |
There was a problem hiding this comment.
The summary list uses key={line} when rendering summary.map(...). If two summary lines are identical (e.g., multiple "Minted tokens." events), React keys will collide and can cause incorrect reconciliation/warnings. Use a stable unique key (e.g., include the index) instead of the line text alone.
| function octasToApt(octas: string): string { | ||
| try { | ||
| const n = BigInt(octas); | ||
| const decimal = Number(n) / 1e8; | ||
| if (decimal === 0) return "0"; | ||
| if (Math.abs(decimal) < 0.0001) return `${decimal.toExponential(2)} APT`; | ||
| return `${decimal.toLocaleString(undefined, {maximumFractionDigits: 8})} APT`; | ||
| } catch { |
There was a problem hiding this comment.
octasToApt converts BigInt to Number (Number(n) / 1e8), which can lose precision for values > 2^53 and can misformat large on-chain amounts. Consider formatting purely as strings/BigInt (e.g., reuse getFormattedBalanceStr / APTCurrencyValue logic) instead of converting through Number.
| function octasToApt(octas: string): string { | |
| try { | |
| const n = BigInt(octas); | |
| const decimal = Number(n) / 1e8; | |
| if (decimal === 0) return "0"; | |
| if (Math.abs(decimal) < 0.0001) return `${decimal.toExponential(2)} APT`; | |
| return `${decimal.toLocaleString(undefined, {maximumFractionDigits: 8})} APT`; | |
| } catch { | |
| /** | |
| * Format a non-negative integer string with thousands separators. | |
| * E.g. "1234567" -> "1,234,567". | |
| */ | |
| function formatIntegerWithSeparators(value: string): string { | |
| if (value.length <= 3) return value; | |
| const parts: string[] = []; | |
| let i = value.length; | |
| while (i > 3) { | |
| parts.unshift(value.slice(i - 3, i)); | |
| i -= 3; | |
| } | |
| if (i > 0) { | |
| parts.unshift(value.slice(0, i)); | |
| } | |
| return parts.join(","); | |
| } | |
| function octasToApt(octas: string): string { | |
| try { | |
| const n = BigInt(octas); | |
| const OCTA_PER_APT = 100000000n; | |
| if (n === 0n) { | |
| // Preserve existing behaviour: plain "0" without units. | |
| return "0"; | |
| } | |
| const negative = n < 0n; | |
| const abs = negative ? -n : n; | |
| // For very small amounts (< 0.0001 APT), keep exponential notation as before. | |
| // 0.0001 APT = 10_000 octas. These values are safely representable as Number. | |
| if (abs < 10000n) { | |
| const decimal = Number(abs) / 1e8; | |
| const expStr = decimal.toExponential(2); | |
| return `${negative ? "-" : ""}${expStr} APT`; | |
| } | |
| const integerPart = abs / OCTA_PER_APT; | |
| const fractionalPart = abs % OCTA_PER_APT; | |
| let integerStr = integerPart.toString(); | |
| integerStr = formatIntegerWithSeparators(integerStr); | |
| let fractionStr = fractionalPart.toString().padStart(8, "0"); | |
| // Trim trailing zeros to respect a maximum of 8 fraction digits. | |
| fractionStr = fractionStr.replace(/0+$/, ""); | |
| let result = integerStr; | |
| if (fractionStr.length > 0) { | |
| result = `${integerStr}.${fractionStr}`; | |
| } | |
| if (negative) { | |
| result = `-${result}`; | |
| } | |
| return `${result} APT`; | |
| } catch { | |
| // If parsing fails, fall back to the original string. |
| function formatEventValue(value: string, key: string): string { | ||
| const lowerKey = key.toLowerCase(); | ||
| if ( | ||
| (lowerKey.includes("amount") || lowerKey === "apt" || lowerKey === "fee") && | ||
| /^\d+$/.test(value) && | ||
| value.length > 4 | ||
| ) { | ||
| const num = BigInt(value); | ||
| const decimal = Number(num) / 1e8; | ||
| if (decimal >= 0.0001 && decimal < 1e15) { | ||
| return `${decimal.toLocaleString(undefined, {maximumFractionDigits: 8})} (${value})`; | ||
| } | ||
| } |
There was a problem hiding this comment.
formatEventValue also converts BigInt to Number for display, which will lose precision for large amounts and may show an incorrect decimal representation. Prefer BigInt/string-based formatting (consistent with CurrencyValue.getFormattedBalanceStr) and/or only apply decimal conversion when you know the asset decimals.
| import TitleHashButton, {HashType} from "../../components/TitleHashButton"; | ||
| import {TransactionType} from "../../components/TransactionType"; | ||
| import {truncateAddress} from "../../utils"; | ||
| import {isDecibelTransaction} from "./Tabs/Components/DecibelDetailsPanel"; |
There was a problem hiding this comment.
Title.tsx imports isDecibelTransaction from DecibelDetailsPanel.tsx, which also imports MUI Accordion + several icon components. This risks pulling the whole panel (and icons) into the title bundle even when only the detection helper is needed. Consider moving DECIBEL_ADDRESS + isDecibelTransaction into a lightweight decibelTransaction.ts utility module (no UI imports) that both Title and DecibelDetailsPanel can import.
| import {isDecibelTransaction} from "./Tabs/Components/DecibelDetailsPanel"; | |
| import {isDecibelTransaction} from "./Tabs/Components/decibelTransaction"; |
|
|
||
| /** | ||
| * Detect whether a transaction involves the Decibel contract. | ||
| * Checks the sender, the payload function target, and event sources. | ||
| */ | ||
| export function isDecibelTransaction(transaction: Types.Transaction): boolean { | ||
| const normalizedDecibel = tryStandardizeAddress(DECIBEL_ADDRESS); | ||
| if (!normalizedDecibel) return false; |
There was a problem hiding this comment.
isDecibelTransaction recomputes tryStandardizeAddress(DECIBEL_ADDRESS) on every call. Since DECIBEL_ADDRESS is constant, consider hoisting the standardized value to module scope (and handling the undefined case once) to avoid repeated parsing work on each render.
| /** | |
| * Detect whether a transaction involves the Decibel contract. | |
| * Checks the sender, the payload function target, and event sources. | |
| */ | |
| export function isDecibelTransaction(transaction: Types.Transaction): boolean { | |
| const normalizedDecibel = tryStandardizeAddress(DECIBEL_ADDRESS); | |
| if (!normalizedDecibel) return false; | |
| const NORMALIZED_DECIBEL_ADDRESS = tryStandardizeAddress(DECIBEL_ADDRESS); | |
| /** | |
| * Detect whether a transaction involves the Decibel contract. | |
| * Checks the sender, the payload function target, and event sources. | |
| */ | |
| export function isDecibelTransaction(transaction: Types.Transaction): boolean { | |
| if (!NORMALIZED_DECIBEL_ADDRESS) return false; | |
| const normalizedDecibel = NORMALIZED_DECIBEL_ADDRESS; |
Description
Enhances the existing transaction detail page (
/txn/:id/:tab) with richer parsing and display for transactions that involve the Decibel contract (0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06). Works on mainnet, testnet, and the decibel network — the panel appears whenever the transaction's sender, payload function, events, or state changes touch the Decibel address.Detection logic (
isDecibelTransaction):Changes to TransactionTitle:
Changes to UserTransactionOverviewTab:
DecibelDetailsPanelabove the standard overview content (collapsed by default)"What Happened" Summary — Plain-English description of the transaction:
Event Timeline — Events displayed as a categorized timeline with:
token::MintEventinstead of full address)HashButtonPayload Breakdown — Function call with module highlighting, type arguments, and indexed arguments
State Changes Summary — Change type counts and "Modules Touched" list
New file:
app/pages/Transaction/Tabs/Components/DecibelDetailsPanel.tsxModified files:
app/pages/Transaction/Title.tsx— Decibel badge + metadataapp/pages/Transaction/Tabs/UserTransactionOverviewTab.tsx— DecibelDetailsPanel integrationRelated Links
N/A
Checklist
tsc --noEmit)pnpm test --run)pnpm build)