Skip to content

feat(portfolio): multi-LST page with RPC snapshot and holdings API - #274

Open
EjembiEmmanuel wants to merge 5 commits into
Endur-fi:staging-mainfrom
EjembiEmmanuel:feat/portfolio-restore
Open

feat(portfolio): multi-LST page with RPC snapshot and holdings API#274
EjembiEmmanuel wants to merge 5 commits into
Endur-fi:staging-mainfrom
EjembiEmmanuel:feat/portfolio-restore

Conversation

@EjembiEmmanuel

@EjembiEmmanuel EjembiEmmanuel commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR restores and substantially upgrades the /portfolio experience so it works for every supported LST (xSTRK and all BTC LSTs), not only xSTRK on STRK. Balances and VIP valuation are driven by on-chain RPC reads instead of a GraphQL points backend that did not scale cleanly to multi-asset. A new snapshot API plus PostgreSQL cache reduces repeated RPC load and improves perceived performance. Historical holdings logic is extracted into shared server libraries so chart, block, and per-block routes stay consistent. The holdings chart is fixed so changing the time range (7d / 30d / 90d / 180d) no longer filters against a hardcoded November 2024 window or drops data incorrectly.


Motivation (why this work exists)

  1. Product: Endur now supports multiple BTC LSTs; the old portfolio page was effectively xSTRK-centric—copy, table columns, pie chart, and client atoms all assumed one token.
  2. Reliability: Portfolio balances depended on GraphQL (pointsApolloClient) for live positions. That path is harder to extend per LST and couples UI freshness to an indexer. RPC reads reuse the same contract helpers as the rest of the app and align balances with what users see on-chain.
  3. Performance: Aggregating all LSTs across all protocols per page load is RPC-heavy. Caching full snapshots in Postgres with TTL + stale-while-revalidate avoids hammering the node on every navigation.
  4. Maintainability: Block sampling, exchange-rate lookup, and historical holdings were duplicated inside API route files (~500+ lines). Centralizing them prevents drift when adding protocols or LSTs.
  5. UX bugs: The chart filtered client-side from a fixed 2024-11-25 anchor, so switching ranges often showed empty or wrong series. Server-side range selection and honest “N days available” messaging fix that.

Changes (what changed and why)

Environment and documentation

File What changed Why
.env.sample Added NEXT_PUBLIC_PORTFOLIO_RPC Optional dedicated RPC for portfolio aggregation so heavy historical/block queries do not compete with user-facing wallet RPC on the same endpoint. Falls back to NEXT_PUBLIC_RPC_URL when unset.
CLAUDE.md Documented portfolio RPC env and routing context Keeps agent/human contributors aligned on how portfolio APIs choose RPC and where portfolio fits in the app map.

Database and caching

File What changed Why
prisma/schema.prisma New PortfolioSnapshotCache model (address, snapshot JSON, blockNumber, fetchedAt) Persists precomputed portfolio payloads so repeat visits do not re-run full multi-LST RPC aggregation.
prisma/migrations/20260527120000_add_portfolio_snapshot_cache/migration.sql Creates portfolio_snapshot_cache table + unique address index + fetchedAt index Required for deploy; index on fetchedAt supports future cleanup or monitoring of stale rows.
src/app/api/portfolio/snapshot/[address]/route.ts New GET endpoint with 2 min fresh TTL, up to 10 min stale-while-revalidate background refresh, X-Portfolio-Cache: hit|stale|miss Single entry point for the portfolio page: one round-trip returns native balances, all LST protocol breakdowns, USD totals, and block metadata. Stale path returns immediately while refresh runs in the background so users are not blocked on slow RPC.

Shared server libraries (new)

File What changed Why
src/lib/portfolio-types.ts NewPortfolioData, PortfolioBalance, LST token keys (XSTRKXSTRKBTC), USD helpers (portfolioLstUsdValue, portfolioDefiPieSlices), asset→protocol mapping (isProtocolAllowedForAsset, getHoldingsKeyForProtocol) One source of truth for shapes and valuation math used by snapshot, VIP, chart transformation, and UI. Avoids copy-pasting BigInt summation and decimals per route.
src/lib/portfolio-rpc.ts NewbuildPortfolioDataForLst, getAllLstTokenBalancesRpc, getPortfolioBalanceRpc, getLstConfigBySymbol Encapsulates per-protocol RPC calls (Endur wallet, Ekubo, Vesu, Troves Sensei/Hyper/Ekubo, Nostra, Opus) parameterized by LST config from LST_CONFIG. Parallel fetch across all LST assets; per-LST failures degrade to emptyPortfolioData() instead of failing the whole page.
src/lib/portfolio-blocks.ts NewgetBlocksWithExchangeRatesForDays, BlockInfo, block-for-time estimation, exchange rate retries Previously lived only in /api/blocks/[nDays]. Shared so holdings history and blocks API use identical sampling gaps (7d→1d, 30d→3d, 90/180d→7d) and retry behavior.
src/lib/portfolio-holdings-history.ts NewfetchHistoricalHoldingsForAsset, transient RPC error detection, per-block protocol series Replaces ~300 lines inlined in the holdings route. Accepts lstSymbol so historical chart works for any LST. Retries on JSON/connection errors reduce flaky chart loads.
src/lib/portfolio.ts Re-exports types; adds getAllLstTokenBalancesRpc / getPortfolioBalanceRpc wrappers; buildPortfolioSnapshot composes RPC balances + native tokens + Pragma USD rates + category totals GraphQL-based getPortfolioBalance / getAllLstTokenBalances remain for legacy/points flows; snapshot and balance routes explicitly use RPC paths. buildPortfolioSnapshot is the canonical payload written to cache.

Constants and RPC provider

File What changed Why
src/constants/index.ts getPortfolioProvider() using NEXT_PUBLIC_PORTFOLIO_RPCRPC_URLNEXT_PUBLIC_RPC_URL Portfolio server code can target a higher-rate-limit or archive-capable node without changing the wallet provider used in the browser.

API route refactors

File What changed Why
src/app/api/blocks/[nDays]/route.ts Delegates to getBlocksWithExchangeRatesForDays (~150 lines removed) Thin HTTP layer; business logic testable and reusable.
src/app/api/holdings/[address]/[nDays]/route.ts Delegates to portfolio-blocks + portfolio-holdings-history; adds lstSymbol query param (default STRK) Chart and portfolio page request history for the selected asset. Removes internal axios self-calls to HOSTNAME for blocks—same process, fewer failure modes.
src/app/api/block-holdings/[address]/[block]/route.ts Imports helpers from @/lib/portfolio-holdings-history instead of holdings route API routes should not import from each other; lib is the stable boundary.
src/app/api/portfolio/balance/[address]/route.ts getAllLstTokenBalancesgetAllLstTokenBalancesRpc Balance endpoint returns on-chain positions consistent with snapshot/VIP.
src/app/api/portfolio/isVIP/[address]/route.ts Uses RPC balances; loops PORTFOLIO_LST_TOKEN_KEYS with portfolioLstUsdValue; includes strkbtc in native BTC USD sum VIP threshold ($50k) must count all LSTs and native BTC variants fairly. Removed ~100 lines of duplicated per-token BigInt math.

Client state

File What changed Why
src/store/portfolio.store.ts portfolioAssetSymbolAtom (default STRK); portfolioSnapshotAtom via TanStack Query → /api/portfolio/snapshot/:address (2 min stale) Page reads one cached snapshot instead of many client-side protocol atoms. Asset selection is global across stats, pie, and table.
src/store/ekubo.store.ts getEkuboHoldings accepts optional lstAddress (defaults xSTRK) Historical and RPC portfolio paths must query Ekubo positions for xWBTC, xtBTC, etc., not only xSTRK pools.
src/store/defi.store.ts Minor type/export adjustments for portfolio integration Keeps DAppHoldings / protocol types compatible with new holdings pipeline.

Portfolio UI

File What changed Why
src/app/portfolio/_components/portfolio-asset-selector.tsx New — tabs/chips for STRK + BTC LSTs; syncs portfolioAssetSymbolAtom and lstConfigAtom Users switch portfolio context without leaving the page; matches asset-selector pattern on stake pages.
src/app/portfolio/_components/portfolio-page.tsx Fetches holdings with lstSymbol + chartFilter; abort in-flight requests on asset/range change; protocol table filtered via isProtocolAllowedForAsset; uses createPortfolioColumns; snapshot-driven layout Prevents race conditions when switching asset or range quickly. Only shows protocols that support the selected LST (e.g. no xSTRK-only Troves row on xWBTC). Uses userAddressAtom for consistency with rest of app.
src/app/portfolio/_components/stats.tsx Reads totals from portfolioSnapshotAtom (USD, per-category, selected LST balance) Stats stay in sync with server snapshot and selected asset without N client RPC subscriptions.
src/app/portfolio/_components/defi-holding.tsx Pie chart from portfolioDefiPieSlices(snapshot.byLst[...]) instead of individual Jotai protocol atoms One data source; works for any LST; removes Haiko/Nostra split that was xSTRK-specific. Adds Troves Hyper slice. Title uses dynamic lstSymbol.
src/app/portfolio/_components/chart.tsx Removes client date filter; dynamic title/tooltip decimals; loading overlay; retry button; strkfarm / trovesHyper stack keys; merges Nostra into single nostra series Fixes time-range bug (see below). BTC assets use 8 decimal tooltips. Better empty/loading/error states.
src/app/portfolio/_components/table/columns.tsx createPortfolioColumns(lstSymbol); PortfolioDAppAmountCell picks correct token column and decimals; removed debug console.log Table header and amounts reflect selected LST (e.g. “Amount in xWBTC”).
src/app/portfolio/_components/table/data-table.tsx Wires dynamic columns from parent Passes LST-specific column factory from portfolio-page.

Chart time-range fix (commit c66cd95)

File What changed Why
src/lib/portfolio-holdings-history.ts Server requests only blocks/days needed for selected range; aligns returned series length with nDays Client no longer receives full 180d data and incorrectly slices it. Reduces RPC work for 7d/30d views.
src/app/portfolio/_components/portfolio-page.tsx Passes holdingsTimeRange / coordinates fetch with chartFilter UI and API agree on which range was fetched; avoids showing 90d label on 7d data.
src/app/portfolio/_components/chart.tsx availableDays vs requestedDays hint (“Showing N days available”) Honest UX when chain history is shorter than selected range (new wallets, indexer gaps).

Previous bug: Chart filtered with referenceDate = new Date("2024-11-25"), so after that calendar window, 7d/30d filters often returned [] even when the API had data.

Navigation and polish

File What changed Why
src/components/sidebar-menu-items.tsx Re-enables Portfolio nav entry Feature was hidden or missing while portfolio was broken; users need discoverability.
src/components/mobile-nav.tsx Portfolio link on mobile Parity with desktop navigation.
src/components/ui/user.tsx Hover animation fix for user icon Small UX fix bundled in original portfolio restore commit.
src/components/ui/chart.tsx Tooltip supports valueSuffix and valueDecimals Chart tooltips show “xWBTC” with correct precision instead of hardcoded xSTRK / 2 decimals.

Architecture (data flow)

flowchart LR
  UI["/portfolio UI"]
  Snap["GET /api/portfolio/snapshot/:address"]
  DB["portfolio_snapshot_cache"]
  RPC["portfolio-rpc + stores"]
  Hold["GET /api/holdings/:address/:nDays?lstSymbol="]
  Hist["portfolio-holdings-history"]

  UI --> Snap
  Snap --> DB
  Snap --> RPC
  DB --> Snap
  UI --> Hold
  Hold --> Hist
  Hist --> RPC
Loading

Add portfolio snapshot endpoint and RPC aggregation for all LSTs.
Extend holdings API with lstSymbol; share block helpers across routes.
Rebuild /portfolio UI with asset selector, snapshot stats, and per-LST
protocol table/chart. Switch balance route to RPC; parameterize Ekubo by LST.
Add Portfolio to sidebar and mobile nav; fix UserIcon hover animation.
@vercel

vercel Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

@EjembiEmmanuel is attempting to deploy a commit to the unwrap labs's projects Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread src/lib/portfolio-holdings-keys.ts Outdated
Comment thread src/lib/portfolio.ts Outdated
Comment thread src/lib/portfolio.ts Outdated
Comment thread src/app/portfolio/_components/stats.tsx Outdated
Comment thread src/lib/portfolio-snapshot.ts Outdated
XLBTC: rates.xlbtc,
XSBTC: rates.xsbtc,
XTBTC: rates.xtbtc,
XSTRKBTC: rates.xwbtc,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why are we proxying over here ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not sure I understand what you mean here

Comment thread src/lib/portfolio-types.ts
Comment thread src/lib/portfolio-rpc.ts Outdated
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.

2 participants