Skip to content

feat: enable raw data for all account headers#884

Draft
askov wants to merge 1 commit intosolana-foundation:masterfrom
hoodieshq:refactor/extract-account-card-component
Draft

feat: enable raw data for all account headers#884
askov wants to merge 1 commit intosolana-foundation:masterfrom
hoodieshq:refactor/extract-account-card-component

Conversation

@askov
Copy link
Contributor

@askov askov commented Mar 18, 2026

Description

  • Extracts a reusable AccountCard component (@features/account) that encapsulates the card shell (header, Raw/Refresh buttons, raw account data view), eliminating duplicated card markup across 13 account section files
  • Every account detail card now gets a "Raw" toggle showing address, balance, owner, data size, executable flag, and hex-encoded account data

Type of change

  • New feature

Screenshots

localhost_3000_address_EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

Testing

Related Issues

Checklist

  • My code follows the project's style guidelines
  • I have added tests that prove my fix/feature works
  • All tests pass locally and in CI
  • I have run build:info script to update build information
  • CI/CD checks pass
  • I have included screenshots for protocol screens (if applicable)

@vercel
Copy link

vercel bot commented Mar 18, 2026

@askov is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@askov askov marked this pull request as draft March 18, 2026 13:44
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 18, 2026

Greptile Summary

This PR introduces a reusable AccountCard component under app/features/account that encapsulates the card shell (header, Raw/Refresh buttons, raw account data view) for all account detail pages. The 13 account section components are refactored to use this new abstraction, eliminating duplicated card markup throughout the codebase.

Key changes and observations:

  • AccountCardBase handles the card layout and the Raw toggle state; it conditionally renders either parsed content (children) or raw content (rawContent) — raw data is fetched lazily and only after the user clicks "Raw".
  • useRawAccountDataOnMount (in app/entities/account) fetches raw account bytes via getAccountInfo using useSWRImmutable. There is a bug here: data ?? null collapses SWR's initial undefined (still loading) to null, making it indistinguishable from the case where getAccountInfo returns null (account doesn't exist). This causes an infinite spinner in BaseRawAccountRows for non-existent accounts.
  • fetchRawAccountData creates a brand-new Connection object on each cache-miss invocation rather than reusing the shared cluster connection.
  • The overall refactoring is clean and consistent; all migrated components remove their duplicated card boilerplate and unused imports correctly.

Confidence Score: 3/5

  • Safe to merge after addressing the infinite spinner bug for non-existent accounts in the Raw view.
  • The refactoring across 13 components is mechanically correct and consistent. The main concern is in the new use-raw-account-data.ts hook where null is used for both the loading state and the "account not found" state, leading to a spinner that never resolves for accounts that no longer exist on-chain.
  • app/entities/account/model/use-raw-account-data.ts and app/features/account/ui/AccountCardBase.tsx (the rendering logic for the three rawData states)

Important Files Changed

Filename Overview
app/features/account/ui/AccountCard.tsx New reusable AccountCard wrapper that composes AccountCardBase with a RawAccountRows raw-content renderer; lazily mounts RawAccountRows only when the Raw button is toggled.
app/features/account/ui/AccountCardBase.tsx New AccountCardBase presentational component handling card shell, Raw/Refresh buttons, and toggling between parsed children and rawContent; well-structured and re-uses e-gap-2 utility class consistent with the rest of the codebase.
app/entities/account/model/use-raw-account-data.ts New hook for fetching raw account bytes; has a critical issue where data ?? null collapses SWR's initial undefined (loading) into null (not found), making the two states indistinguishable and causing an infinite spinner for non-existent accounts. Also creates a new Connection object on every cache-miss fetch instead of reusing a shared instance.
app/components/account/UnknownAccountCard.tsx Migrated to AccountCard; maintains existing behavior for unknown/non-existent accounts.
app/components/account/TokenAccountSection.tsx Three token sub-cards (fungible mint, NFT mint, token account, multisig) migrated to AccountCard; logic and rendering preserved correctly.
app/features/account/ui/stories/RawAccountRows.stories.tsx Story for BaseRawAccountRows includes a Loading story that passes rawData: null, but this same state is also used for "account not found" — there is no story representing the true loading state (undefined) vs. the not-found state (null).

Sequence Diagram

sequenceDiagram
    participant User
    participant AccountCard
    participant AccountCardBase
    participant RawAccountRows
    participant useRawAccountDataOnMount
    participant SWR
    participant RPC

    User->>AccountCard: renders page
    AccountCard->>AccountCardBase: rawContent=<RawAccountRows/> (not yet mounted)
    AccountCardBase-->>User: shows parsed children (showRaw=false)

    User->>AccountCardBase: clicks "Raw" button
    AccountCardBase->>AccountCardBase: setShowRaw(true)
    AccountCardBase->>RawAccountRows: mounts rawContent
    RawAccountRows->>useRawAccountDataOnMount: pubkey
    useRawAccountDataOnMount->>SWR: useSWRImmutable(key, fetcher)
    SWR->>RPC: getAccountInfo(pubkey)
    RawAccountRows-->>User: spinner (rawData = null/undefined)

    alt Account exists
        RPC-->>SWR: AccountInfo with data
        SWR-->>useRawAccountDataOnMount: Uint8Array
        useRawAccountDataOnMount-->>RawAccountRows: Uint8Array
        RawAccountRows-->>User: HexData display
    else Account not found
        RPC-->>SWR: null
        SWR-->>useRawAccountDataOnMount: null (data ?? null = null)
        useRawAccountDataOnMount-->>RawAccountRows: null (same as loading!)
        RawAccountRows-->>User: ⚠️ infinite spinner (bug)
    end
Loading

Last reviewed commit: "refactor: extract Ac..."

Comment on lines +29 to +33
async function fetchRawAccountData([, url, address]: [string, string, string]): Promise<Uint8Array | null> {
const connection = new Connection(url, 'confirmed');
const info = await connection.getAccountInfo(new PublicKey(address));
return info ? Uint8Array.from(info.data) : null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New Connection instantiated on every fetch

A brand-new Connection object is created inside the SWR fetcher on every invocation. While useSWRImmutable prevents redundant calls for the same cache key, this still creates an extra connection object every time the cache key changes (different cluster or account). The existing cluster providers already manage a shared Connection instance.

Consider consuming the Connection from the cluster context, or at minimum memoizing it at module level keyed by URL, rather than constructing one inside the fetcher.

)}
</div>

<TableCardBody {...tableProps}>{showRaw ? rawContent : children}</TableCardBody>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 rawContent is always mounted despite showRaw being false at start

AccountCardBase passes rawContent as a prop to AccountCard which immediately evaluates the JSX expression <RawAccountRows account={account} /> and creates the React element. However, since rawContent is only rendered in the tree when showRaw is true (via the ternary), RawAccountRows itself isn't mounted until the button is clicked — so the hook fires correctly on demand.

This is fine as-is, but it's worth noting that passing rawContent as a render prop () => <RawAccountRows /> instead of a pre-evaluated element would make the lazy-mounting intent more explicit and guard against future accidental eager rendering.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +17 to +23
export function useRawAccountDataOnMount(pubkey: PublicKey): Uint8Array | null {
const { url } = useCluster();

const { data } = useSWRImmutable(getRawAccountDataKey(url, pubkey), fetchRawAccountData);

return data ?? null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Indistinguishable loading vs. not-found state

useRawAccountDataOnMount returns null for both the initial loading state (SWR data is undefined, and undefined ?? null evaluates to null) and the case where fetchRawAccountData itself returns null (i.e., getAccountInfo found no account on chain).

BaseRawAccountRows treats any falsy rawData as "still loading" and shows a spinning indicator indefinitely. If a user visits a closed or non-existent account and clicks "Raw", the spinner will never resolve — there is no terminal state for "account not found".

The fix is to remove the ?? null coercion from the return value so that SWR's initial undefined (in-flight) remains distinct from the null that signals "account not found". The BaseRawAccountRowsProps type already allows rawData to be undefined, so the rendering logic just needs a third branch:

  • undefined → loading spinner
  • null → "Account data unavailable" message
  • Uint8Array → render HexData

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant