Skip to content

fix(portfolio): stop publishing an incomplete scan as a complete one (GH#2414) - #2438

Closed
dcccrypto wants to merge 1 commit into
mainfrom
fix/launch-2414-partial-portfolio-scan
Closed

fix(portfolio): stop publishing an incomplete scan as a complete one (GH#2414)#2438
dcccrypto wants to merge 1 commit into
mainfrom
fix/launch-2414-partial-portfolio-scan

Conversation

@dcccrypto

@dcccrypto dcccrypto commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Closes #2414[HIGH] Portfolio shared cache accepts partial v17 owner-scan results, hiding active positions and understating account value.

The bug

Every failure path inside usePortfolio's load() was swallowed into an empty result, which made "we could not read your positions" indistinguishable from "you have no positions":

Location Old behaviour Blast radius
batch slab fetch catch { slabAccountsInfo = [] } every market — one RPC failure rendered as an empty portfolio
per-market loop catch { /* Skip markets that fail to parse */ } covers the v17 getProgramAccounts owner scan — the exact path in the issue
outer wrapper catch { // ignore } discovery failure left stale/empty state rendering as a completed scan

The partial result was then published through the shared snapshot every usePortfolio() consumer reads, understating position count, deposited capital, portfolio value, unrealized PnL, and — most dangerously — liquidation risk. A user could believe a position had closed, or that their exposure was far smaller than it really was, while the same position stayed visible on the market page.

The fix

Count failed markets and publish completeness alongside the numbers it qualifiesisPartial and failedMarketCount on PortfolioData, set in the same if (!cancelled) block as the totals, so no consumer can read the totals without them. Each catch now logs what failed rather than discarding it.

Surfaced on /portfolio. When the scan is incomplete the page shows a warning that positions may be missing and totals may be understated, and points the user at the market page before acting on the numbers. A flag nobody renders would not have fixed the reported harm, which is precisely that partial data looked complete.

A disconnected wallet clears the flag — no wallet is a complete (empty) picture, not a failed scan.

Testing

__tests__/hooks/gh2414-portfolio-partial-scan.test.ts (5): v17 owner-scan failure flags partial; batch-slab failure flags partial and yields zero positions (the key distinction — zero positions plus a partial flag is what separates this from an empty wallet); a fully successful scan is not partial; a genuinely empty wallet is not partial; and the snapshot exposes both fields with the right types.

Verified non-vacuous: all 5 fail against the origin/main hook. To be precise about why — 3 fail on behaviour, and 2 (the "complete" and "empty wallet" cases) fail because the property does not exist on main at all. They are contract guards, not behavioural proofs.

No regressions, measured rather than assumed:

Test files Tests
clean origin/main, __tests__/hooks/ 3 failed / 19 passed 37 failed / 200 passed
this branch, same set + my file 3 failed / 20 passed 37 failed / 205 passed

Identical 37 pre-existing failures (useChartDrawingTool.test.ts and two others fail 10/10 on clean main); my change adds exactly the 5 new passing tests.

npx tsc --noEmit19 errors, same count as clean main, none referencing these files: 16 are stale .next/dev/types build artifacts and 3 are a missing resend module in unrelated waitlist routes. (Correcting myself: in #2437 I described all 19 as .next artifacts — 3 of them are the resend ones.)

Adjacent gap, deliberately not fixed here

discoverPortfolioMarkets also has .catch(() => []) on both discovery paths (lines ~52 and ~61). If the API and static-bundle paths both fail, it returns [], which still reads as "no markets" rather than "discovery failed". That is arguably the same bug class one level up — but those catches are a deliberate fallback chain (API → static → empty), so distinguishing exhausted-fallbacks from genuinely-no-markets is a separate change with its own design question. Flagging rather than folding it in silently.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Portfolio scans now indicate when results are incomplete and report how many markets failed to load.
    • Added a warning banner explaining that positions may be missing and totals may be understated.
  • Bug Fixes

    • Prevented failed or partial scans from appearing complete.
    • Improved handling of scan failures while preserving accurate loading and empty-wallet states.
  • Tests

    • Added coverage for partial scans, failed data retrieval, successful scans, and empty wallets.

…(GH#2414)

Every failure path inside usePortfolio's load() was swallowed into an
empty result, making "we could not read your positions" indistinguishable
from "you have no positions":

  - the batch slab fetch caught and set slabAccountsInfo = [], which
    covers EVERY market, so one RPC failure rendered as an empty portfolio
  - the per-market loop caught with "Skip markets that fail to parse".
    That path covers the v17 getProgramAccounts owner scan, so a failure
    there hid real positions while the same positions stayed visible on
    the market page — the symptom reported in the issue
  - the outer catch was a bare "ignore", so a discovery failure left the
    previous state rendering as a completed scan

The partial result was then published through the shared snapshot that
every usePortfolio() consumer reads, understating position count,
deposited capital, portfolio value, unrealized PnL and — most
dangerously — liquidation risk. A user could believe a position had been
closed, or that their exposure was far smaller than it actually was.

Count failed markets and publish completeness alongside the numbers it
qualifies, as isPartial + failedMarketCount on PortfolioData. Each catch
now logs what failed instead of discarding it silently.

Surface it on /portfolio: when the scan is incomplete, a warning states
that positions may be missing and the totals may be understated, rather
than rendering a partial portfolio as the whole picture. A flag no one
renders would not have fixed the reported harm.

A disconnected wallet clears the flag — no wallet is a complete (empty)
picture, not a failed scan.

Closes #2414

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
percolator-launch Ready Ready Preview, Comment Jul 19, 2026 1:03am
percolator-mainnet Ready Ready Preview, Comment Jul 19, 2026 1:03am
percolator-playground Error Error Jul 19, 2026 1:03am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The portfolio hook now reports incomplete scans and failed-market counts, while the portfolio page displays warnings when loading finishes with partial data. New tests cover RPC failures, successful scans, empty wallets, and the returned state contract.

Changes

Portfolio partial-scan reporting

Layer / File(s) Summary
Track scan failures and publish completeness
app/hooks/usePortfolio.ts
usePortfolio counts slab and per-market scan failures, resets state when no wallet is connected, and returns isPartial with failedMarketCount.
Render incomplete-scan warning
app/app/portfolio/page.tsx
The portfolio header warns after loading when markets failed to load and indicates that positions or totals may be incomplete.
Validate partial-scan states
app/__tests__/hooks/gh2414-portfolio-partial-scan.test.ts
Tests cover failed RPC calls, successful scans, empty wallets, and the runtime types of the new fields.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PortfolioPage
  participant usePortfolio
  participant SolanaRPC
  PortfolioPage->>usePortfolio: request portfolio snapshot
  usePortfolio->>SolanaRPC: scan markets and fetch slab accounts
  SolanaRPC-->>usePortfolio: accounts or RPC failure
  usePortfolio-->>PortfolioPage: positions, isPartial, failedMarketCount
  PortfolioPage-->>PortfolioPage: render warning when partial and not loading
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds partial-scan tracking and UI warnings, but it does not clearly satisfy cache protection and last-known-good preservation required by #2414. Add fail-closed owner-scan rejection, preserve the previous good snapshot on refresh errors, and block partial snapshots from being returned, published, or cached.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: preventing incomplete portfolio scans from being treated as complete.
Out of Scope Changes check ✅ Passed The changes stay focused on portfolio scan completeness, tests, and the related warning UI, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/launch-2414-partial-portfolio-scan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/app/portfolio/page.tsx (1)

56-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align failedMarketCount fallback with isPartial for mock mode.

For strict consistency with isPartial being forced to false in mock mode, consider forcing failedMarketCount to 0 as well. Even though the warning banner won't render due to the isPartial check, this keeps the derived local state completely clean.

💡 Proposed fix
   // GH#2414: an incomplete scan must not render as a complete picture — the
   // totals below can understate real exposure. Mock mode is always complete.
   const isPartial = mockPositions ? false : portfolio.isPartial;
-  const failedMarketCount = portfolio.failedMarketCount ?? 0;
+  const failedMarketCount = mockPositions ? 0 : (portfolio.failedMarketCount ?? 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/app/portfolio/page.tsx` around lines 56 - 57, Update the
failedMarketCount derivation alongside isPartial in the portfolio page so
mockPositions forces failedMarketCount to 0; otherwise preserve
portfolio.failedMarketCount with its existing nullish fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/app/portfolio/page.tsx`:
- Around line 56-57: Update the failedMarketCount derivation alongside isPartial
in the portfolio page so mockPositions forces failedMarketCount to 0; otherwise
preserve portfolio.failedMarketCount with its existing nullish fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5a213f1-6372-4a36-9c13-cffda0cd64c1

📥 Commits

Reviewing files that changed from the base of the PR and between 775a126 and 2ce534f.

📒 Files selected for processing (3)
  • app/__tests__/hooks/gh2414-portfolio-partial-scan.test.ts
  • app/app/portfolio/page.tsx
  • app/hooks/usePortfolio.ts

@dcccrypto

Copy link
Copy Markdown
Owner Author

Correction to my previous comment — I over-corrected, and I was wrong about #2414's status.

Two errors to fix on the record.

1. #2414 IS already fixed on playground (I said last cycle it was the one still live there). It is fixed with a better approach than mine — usePortfolio.ts on playground throws on a failed v17 owner scan and lets loadPortfolioShared keep serving the last-good cached snapshot:

"Fail closed on the RPC scan ONLY: a failed owner scan makes the aggregate snapshot incomplete, so reject this fetch and let loadPortfolioShared keep serving the last-good cached snapshot instead of caching/publishing partial positions, balances, PnL, or totals."

That is strictly better than my isPartial flag: it never displays understated numbers at all, and it isolates per-account parse failures so one bad account cannot blank the portfolio.

My verification was faulty. I grepped playground for isPartial — my own solution's fingerprint — and concluded the bug was unfixed. Checking whether my API exists is not the same as checking whether the problem is solved. That is the same error class as the branch mistake, one level down.

2. But "redundant → close" was too strong. percolator.trade is served by the percolator-launch Vercel project, whose last production deploy was ~2026-07-09 — matching main's last commit, not playground's (playground deploys to its own preview domain, last updated 3d ago).

So main appears to be the production branch, which means the defects these PRs fix are live on percolator.trade right now, and playground's better fixes have not shipped to production.

Revised status of my three launch PRs:

PR vs playground vs production (main)
#2437 (#2416 oracle cache) redundant fixes live production defect
#2438 (#2414 partial portfolio) redundant fixes live production defect
#2439 (#2362 worst-fill bound) redundant fixes live production defect

So the decision is purely about release intent, and I cannot make it:

  • If the next production deploy comes from playground → close all three; playground's versions are better.
  • If production keeps deploying from main → these three are worth merging, because production carries the bugs until playground ships.

I am not closing them. Flagging clearly instead so nobody reviews them under a false assumption in either direction.

@dcccrypto

Copy link
Copy Markdown
Owner Author

Closing this — #2414 partial portfolio scan.

I have asked twice which branch is authoritative (see #2440) without an answer. The evidence is now conclusive enough to act on rather than leave this open indefinitely:

1. Every other contributor targets playground. Recent PRs: #2442, #2436, #2434 all base=playground; #2431 and #2432 were merged into playground. My #2437/#2438/#2439 were the only open PRs anywhere with base=main.

2. playground already fixes this, and better than I did (verified in #2440).

3. The user impact is limited — mainnet is dormant (no collateral, last on-chain activity March; see #2443), so this path is not carrying real trading.

Taken together, keeping this open only adds to a review backlog I have repeatedly flagged as the binding constraint. Closing is reversible — the branch and history remain, and this can be reopened in seconds if main turns out to ship independently.

Not closing #2437. That one fixes an unauthenticated memory-exhaustion vector, and percolator.trade is currently serving a build from main (last production deploy ~2026-07-09), so that defect is live on the running site regardless of which branch development targets. It stays open until either playground is deployed or someone decides otherwise.

@dcccrypto dcccrypto closed this Jul 19, 2026
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.

[HIGH] Portfolio shared cache accepts partial v17 owner-scan results, hiding active positions and understating account value

1 participant