Frontend: unified My Portfolio (trader positions API) #212
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#212
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add a first-class My Portfolio experience in the dApp that surfaces the connected wallet’s trading exposure, activity, and related holdings in one place. The indexer already exposes
GET /api/v1/traders/{addr}/positionsand related trader endpoints; today that data is only visible on the public Trader Profile page (/trader/:address), buried under More → Trader, while wallet-specific UX is fragmented across Trade, Limits, and Pool.Current codebase
Indexer (backend — done for positions)
GET /api/v1/traders/{addr}/positionsreturns per-pair quote exposure and P&L fields (pair_address, symbols,net_position_quote,avg_entry_price,total_cost_base,realized_pnl,trade_count). Implemented inindexer/src/api/traders.rs(get_trader_positions), backed bytrader_positions(indexer/migrations/20260310000003_add_pnl_tracking.sql, queries inindexer/src/db/queries/positions.rs).indexer/src/indexer/position_tracker.rs(quote-side exposure model: offering asset_0 = add position, offering asset_1 = reduce/realize; net quote clamped ≥ 0 on sells — seedocs/indexer-invariants.md).GET /api/v1/traders/{addr}— profile / tier / aggregate stats (404 if never indexed as a trader)GET /api/v1/traders/{addr}/trades(+ optionalpair,format=csv)GET /api/v1/traders/{addr}/limit-fills,limit-cancellations(+ optionalpair, CSV)indexer/tests/api_traders.rs(get_trader_positions_returns_rows).Frontend (partial / fragmented)
TraderPage(/trader,/trader/:address)WalletIndexerHistoryPanelLimitOrderMyPlacementsPanelPoolPageTiersPagegetTraderPositionsinfrontend-dapp/src/services/indexer/client.ts; typeIndexerPositioninfrontend-dapp/src/types/index.ts.frontend-dapp/src/pages/TraderPage.tsx(lines ~249–324).frontend-dapp/src/App.tsx— no/portfolioroute.frontend-dapp/src/components/common/navItems.ts— Trader is under MORE_NAV_ITEMS, not primary nav.frontend-dapp/e2e/has notraderspecs).Limit placements (gap for “unified” portfolio)
GET /api/v1/pairs/{addr}/limit-placements(filtered client-side by wallet on Trade/Limits). There is no wallet-wide “all my limits” API today.Why this is needed
useWalletStore; a portfolio route should default to the connected address without manual search.Constraints and guardrails
net_position_quote/ P&L are not on-chain balances or mark-to-market unrealized P&L. UI must label this clearly (e.g. “Indexer quote exposure · realized P&L”) and link to docs/invariants.PoolPage; trader positions are swap-tracked quote exposure. Present as separate sections unless product explicitly merges with clear labels.getTraderreturns 404 for wallets with no indexed activity;getTraderPositionsreturns[]for unknown or flat traders. Handle both without breaking the page (summary optional / empty states).RetryError,isIndexerUnavailableError,INDEXER_URLbanner) fromTraderPage/WalletIndexerHistoryPanel.aria-label, loading skeletons, keyboard-navigable links (matchTraderPage/WalletIndexerHistoryPanel).Relevant files
Frontend (primary)
frontend-dapp/src/App.tsx— new routefrontend-dapp/src/components/common/navItems.ts— nav entry (primary vs More — product decision)frontend-dapp/src/components/common/Layout.tsx— header / mobile navfrontend-dapp/src/pages/PortfolioPage.tsx(orMyPortfolioPage.tsx)TraderPagewhere sensible):frontend-dapp/src/pages/TraderPage.tsx— reuse extracted components; keep public lookupfrontend-dapp/src/services/indexer/client.ts—getTrader,getTraderPositions,getTraderTrades, limit history helpersfrontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx— patterns for CSV / retry / pair filterfrontend-dapp/src/components/trade/LimitOrderMyPlacementsPanel.tsx— limit row UXfrontend-dapp/src/hooks/useWallet.ts— connected addressfrontend-dapp/src/types/index.ts—IndexerPosition,IndexerTraderfrontend-dapp/e2e/— new spec filedocs/frontend.md— document route and data semanticsIndexer (reference / optional follow-up)
indexer/src/api/traders.rs— positions contract (stable)indexer/src/db/queries/positions.rsindexer/src/indexer/position_tracker.rs— semantics referenceTests to extend
frontend-dapp/src/services/indexer/__tests__/client.test.tsPortfolioPage.test.tsx(Vitest + MSW/indexer mocks)indexer/tests/api_traders.rs(already covers positions API)Recommended solution direction
Phase 1 (this issue) — MVP portfolio
/portfolio(alias redirect from/my-portfoliooptional) registered inApp.tsx.WalletButton/ modal patterns from Trade).walletAddressautomatically (no address search box required; optional “view public profile” link to/trader/{addr}).total_realized_pnl, fees (getTrader) with graceful 404 (“No indexed trades yet”).TraderPagevia shared component; link each row to/trade/{pair_address}.getTraderTradeswith limit, nopairfilter) or embed slimWalletIndexerHistoryPanelvariant without requiring pair selection.PRIMARY_NAV_ITEMSor wallet dropdown — prefer visibility for connected users without crowding mobile nav (follow #136 tablet compact rules).TraderPositionsTable+TraderSummaryStatsfromTraderPageto avoid duplication.Phase 2 (optional follow-up issues)
Acceptance criteria
/portfolioroute renders in app shell with correct lazy-loading / error boundary behavior./api/v1/traders/{addr}/positionsand displays all returned rows with correct formatting (symbols, numeric fields, P&L coloring consistent withTraderPage).getTradersucceeds; shows friendly empty state when profile 404.RetryError+ indexer unavailable messaging (parity withTraderPage).TraderPagecontinues to work for arbitrary address lookup; shared components stay in sync.npm run build,npx vitest run, and relevant Playwright job pass in CI.docs/frontend.mdupdated with portfolio route and indexer vs on-chain disclaimer.Test plan — functional paths
/portfoliowithout wallet[]; clear copyVITE_INDEXER_URL/portfoliorefresh/trader/{addr}WalletIndexerHistoryPanel/trader/{other}lookupAutomated
PortfolioPage.test.tsx— disconnected, empty positions, populated mock, 404 profile, indexer error.client.test.ts—getTraderPositionsURL path./portfolio→ assert positions section visible (mock or local indexer fixture pere2e/helpers).Test plan — attack vectors / abuse
?addr=, must validateisValidTerraAddressdangerouslySetInnerHTMLon API fieldsrefetchInterval/ refetch on wallet focus; manual retryuseWalletStore, not URL aloneVerification criteria (done definition)
make start+ indexer + dApp) with at least one wallet that has positions in DB.LayoutrouteContentReady).GET /api/v1/traders/{addr}/positionsresponses match OpenAPI / existing indexer tests.Out of scope (unless explicitly added)
TraderPagepublic lookup.mentioned in commit
658e5fe24eImplementation landed on
main(658e5fe)@brouie — please verify when you have a moment. Leaving this issue open until QA sign-off.
What changed
/portfolioroute (alias/my-portfolio→ redirect) — wallet-gated My Portfolio using connected address only (no?addr=).getTraderwith graceful 404 empty state; open positions:getTraderPositionsvia sharedTraderPositionsTable; recent activity: globalgetTraderTrades(limit 100).PRIMARY_NAV_ITEMS; wallet menu My Portfolio link; Trader page links to portfolio + reusesTraderSummaryStats/TraderPositionsTable.docs/frontend.md#my-portfolio,docs/indexer-invariants.md(trader positions row),skills/AGENTS_FRONTEND_PORTFOLIO.md.Automated checks (local)
npx vitest run PortfolioPage navItems client TraderPagePLAYWRIGHT_SKIP_CHAIN=1 npx playwright test e2e/portfolio.spec.ts --workers=5— 4 passedVerification checklist
/portfoliodisconnected — connect CTA only; no indexer calls/portfolioconnected, never traded — profile 404/empty OK; positions[]; clear copy/portfolioconnected, with positions — table matches API; pair links open/trade/{pair}/my-portfolioredirects to/portfolio/trader/{addr}public lookup unchanged; shared components in syncmainfor frontend unit + E2E smokePhase 2 (out of scope here): wallet-wide open limits, LP overview across pairs.
mentioned in issue #217
Phase 2 follow-up tracked in #217 (wallet-wide limits, LP overview, unrealized P&L).
Verification complete (agent) — GitLab #212
Verified
/portfolio(My Portfolio) on local stack (LocalTerra + indexerhttp://127.0.0.1:3001+ dApp). Merged build fix tomain(0ad9545).What was already implemented (phase 1 + phase 2 follow-ups on main)
/portfolio+/my-portfolioredirect; wallet-gated connected address onlygetTrader, 404-tolerant), open positions (getTraderPositionsviaTraderPositionsTable), open limits (getTraderLimitPlacements), LP overview (capped LCD fan-out), recent swaps (getTraderTradeslimit 100)PortfolioinPRIMARY_NAV_ITEMSdocs/frontend.md#my-portfolio,docs/indexer-invariants.md,skills/AGENTS_FRONTEND_PORTFOLIO.md,skills/AGENTS_FRONTEND_SHELL_NAV.mdFix applied during verification
useTokenBalancehook was referenced byuseLimitLadderPlaceGates(#206) but missing from the tree — brokenpm run buildtsconfig.app.json: exclude Vitest chart/test helpers from productiontsc(chart mocks blocked release build)Automated checks (local, post-fix)
npm run test:unit— PortfolioPage, navItems, client, TraderPagePLAYWRIGHT_SKIP_CHAIN=1e2e/portfolio.spec.ts(5 workers)npm run buildGET /traders/{dev}/positionsManual / visual
/portfoliodisconnected/trader/{other}Note:
cargo test get_trader_positionsfailed locally (FK seed vs shared DB). Live indexer contract OK via curl.Checklist for @brouie (sign-off)
/portfoliowith simulated dev wallet shows positions matching indexermainafter0ad9545Leaving open for peer sign-off. @brouie — please tick the checklist; close #212 when satisfied.
mentioned in commit
9e24606306Verification complete (agent) — GitLab #212
Re-verified My Portfolio (
/portfolio) on local stack (LocalTerra RPC/LCD, indexerhttp://127.0.0.1:3001, dApp). Worktree:verify/issue-212→ merged tomainas9e24606.Fix applied during this pass
lp_tokenbech32 (e.g.terra1lptoken), causing the entire LCD fan-out to error.usePortfolioLpBalances: skip invalid bech32pair_address/lp_token, tolerate per-pair LCD failures; VitestusePortfolioLpBalances.test.ts.docs/frontend.md#my-portfolio,skills/AGENTS_FRONTEND_PORTFOLIO.md.Automated checks (local)
npm run buildPLAYWRIGHT_SKIP_CHAIN=1e2e/portfolio.spec.ts(5 workers)GET /traders/{dev}/positionsManual / visual (local)
/portfoliodisconnected/trader/{other}Checklist for @brouie (sign-off)
/portfoliowith dev wallet: positions match indexer API9e24606mainafter9e24606Leaving open — prior comments requested peer sign-off before close. @brouie please tick the checklist when satisfied.
We are not using gitlab ci, so skip that item.
mentioned in commit
3e7a1759b2Verification pass (agent) — GitLab #212
Re-verified My Portfolio (
/portfolio) on worktreeverify/issue-212→ merged tomainas3e7a175.Fix applied this pass
e2e/portfolio.spec.ts: replacednetworkidlewithdomcontentloaded— portfolio polls indexer (15–30s refetch) + LP LCD fan-out, sonetworkidlenever settled and caused 120s Playwright timeouts.Automated checks (local)
npm run buildPLAYWRIGHT_SKIP_CHAIN=1e2e/portfolio.spec.ts(5 workers)Manual / visual (local, infra degraded)
/portfoliodisconnecteddex_indexerDB has no tables; indexer/healthOK but/api/v1/*hangs; cannot compare table vs API/trader/{other}Infra note (did not restart per instructions): Postgres volume is empty (
\dt→ no relations). Stale indexer process answers/healthbut API routes time out. Host LCD to:1317also hangs (documented inscripts/lib/localterra-host-curl.sh); LP overview shows skeleton/loading in browser. These are environment blockers, not portfolio UI regressions.Acceptance criteria status
/portfolioroute + lazy shell/trade/{pair}(component code)TraderPageregression (Vitest)docs/frontend.md+skills/AGENTS_FRONTEND_PORTFOLIO.md+docs/indexer-invariants.mdcross-linksChecklist for @brouie (sign-off on healthy stack)
After
make start-qa(or fresh volumes + deploy + indexer):/portfoliodisconnected — connect CTA only/portfolioconnected (dev wallet) — positions table matchesGET /api/v1/traders/{addr}/positions/my-portfolio→/portfolioredirect/trader/{addr}public lookup unchanged/trade/{pair}Leaving open until manual path #3 and @brouie sign-off complete on a provisioned stack.
Verification pass — GitLab #212 (healthy stack)
Re-verified My Portfolio (
/portfolio) on worktreeverify/issue-212@3e7a175(already merged tomain; no new code changes this pass).Infra health (post-restart)
/health/api/v1/pairsdex_indexertrader_positions):26657Automated checks (local)
npm run buildPLAYWRIGHT_SKIP_CHAIN=1e2e/portfolio.spec.ts(5 workers)get_trader_positions_returns_rowsManual / visual (local, dev wallet
terra1x46…k38v)/portfoliodisconnectedGET /api/v1/traders/{addr}/positions(25)/trader/{addr}loads/trade/{pair}/trader/{other}Additional sections (#217 phase-2 on portfolio, already shipped):
GET …/limit-placements?limit=200:1317)limit=100)Acceptance criteria — all satisfied
Docs/skill cross-links already in place:
docs/frontend.md#my-portfolio,skills/AGENTS_FRONTEND_PORTFOLIO.md,docs/indexer-invariants.md.Checklist for manual re-check
/portfoliodisconnected — connect CTA only/portfolioconnected — positions table matches indexer API/my-portfolio→/portfolioredirect/trader/{addr}public lookup unchanged/trade/{pair}Closing — all issue-body verification criteria and functional test-plan paths pass on provisioned local stack.
mentioned in issue #337
mentioned in issue #657