Skip to content

feat(portfolio): fold Solana staking into get_portfolio_summary (roadmap #2) - #143

Merged
szhygulin merged 1 commit into
mainfrom
feat/portfolio-solana-staking-integration
Apr 25, 2026
Merged

feat(portfolio): fold Solana staking into get_portfolio_summary (roadmap #2)#143
szhygulin merged 1 commit into
mainfrom
feat/portfolio-solana-staking-integration

Conversation

@szhygulin

@szhygulin szhygulin commented Apr 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes roadmap item #2 (section 2 of 2) from claude-work/HIGH-solana-roadmap.md. Follow-up to PR #141 which shipped the standalone reader + get_solana_staking_positions tool. This PR folds the SOL-equivalent subtotals into get_portfolio_summary, paralleling how MarginFi lending and TRON staking are surfaced today.

Wire-up

Layer Change
Types SolanaStakingPositionSlice (thin projection of the reader), PortfolioCoverage.solanaStaking, PortfolioSummary.solanaStakingUsd, SolanaPortfolioSlice.staking / stakingNetUsd
Aggregator getSolanaStakingPositions added to the solanaAddress-gated Promise.all; graceful-degradation via .catch(() => null) mirrors the MarginFi reader's pattern
USD math Uses the same SOL price that valued the native-SOL balance line — extracted via solanaSlice.native.find(b => b.token === "native").priceUsd. No duplicate price fetch; guarantees SOL-in-wallet and staked-SOL use identical SOL price
totalUsd Now includes solanaStakingUsd alongside solanaLendingUsd and tronStakingUsd
Conditional rendering Staking fields only appear when totalSolEquivalent > 0 — no dangling empty-stake fields on wallets without staking

Tests

Before After
Full suite 802 805 (+3)
test/solana-portfolio.test.ts 3 6

New cases:

  1. Happy path — mSOL (10 × 1.2) + jitoSOL (10 × 1.1) = 23 SOL-equivalent × $100 SOL price = $2300 solanaStakingUsd. Verifies math end-to-end through the aggregator.
  2. Failure path — reader throws → coverage.solanaStaking.errored: true; coverage.solana still covered: true (independent split); no staking key on the breakdown; no top-level carve-out.
  3. Zero-holdings — reader succeeds but empty → coverage.solanaStaking.covered: true; no dangling staking fields anywhere.

Test plan

  • npm run build — clean.
  • npm test — 805/805 pass.
  • Live sanity: call get_portfolio_summary({ wallet: <EVM>, solanaAddress: <wallet with staking> }) against a wallet that holds mSOL/jitoSOL/native stakes. Confirm breakdown.solana.staking.totalSolEquivalent matches cross-checks on explorers (Marinade dashboard for mSOL, Jito dashboard for jitoSOL, solscan for native). Confirm solanaStakingUsd + totalUsd include the staking USD.

Roadmap status after this PR

…map #2 section 2)

Follow-up to PR #141. Surfaces the SOL-equivalent subtotals from
`getSolanaStakingPositions` (Marinade + Jito + native stakes) in the
aggregate portfolio summary, paralleling how MarginFi lending and TRON
staking are folded in today.

## New types (src/types/index.ts)

- `SolanaStakingPositionSlice` — thin projection of the reader's
  consolidated shape (`marinade`, `jito`, `nativeStakes`,
  `totalSolEquivalent`). Drops per-reader wrapper metadata the
  portfolio JSON doesn't need.
- `PortfolioCoverage.solanaStaking?: CoverageStatus` — tracks the
  staking-reader fetch independently of `coverage.solana` and
  `coverage.marginfi`. Mirror of the tron/tronStaking split: a staking
  failure doesn't mask a successful balance read.
- `PortfolioSummary.solanaStakingUsd?: number` — top-level carve-out,
  already included in `totalUsd`. Parallels `solanaLendingUsd` and
  `tronStakingUsd`.
- `SolanaPortfolioSlice.staking?` + `stakingNetUsd?` — per-wallet
  breakdown fields inside the Solana slice.

## Aggregator wiring (src/modules/portfolio/index.ts)

- `getSolanaStakingPositions` added to the parallel Promise.all,
  gated on `solanaAddress` like the MarginFi reader. Graceful-
  degradation: `.catch(() => null)` flips `errors.solanaStaking = true`
  without aborting balance + MarginFi reads.
- USD conversion uses the SAME SOL price that valued the native-SOL
  line in `solanaSlice`, via
  `solanaSlice.native.find(b => b.token === "native").priceUsd`.
  Guarantees the two USD numbers (SOL-in-wallet vs. staked-SOL) stay
  consistent + avoids a duplicate price fetch.
- The `solanaStakingUsd` carve-out + `breakdown.solana.staking` entry
  only render when `totalSolEquivalent > 0` (no empty-stake dangling
  fields on wallets without staking).
- `totalUsd` now includes `solanaStakingUsd` alongside `solanaLendingUsd`
  and `tronStakingUsd`.

## Tests

805/805 pass (was 802; +3 new on `test/solana-portfolio.test.ts`):

1. Happy-path — wallet with mSOL + jitoSOL folds into
   `breakdown.solana.staking`, `stakingNetUsd`, top-level
   `solanaStakingUsd`. Verifies totalSolEquivalent math (23 SOL) and
   USD math (× $100 SOL price = $2300).
2. Failure path — reader throws → `coverage.solanaStaking.errored:
   true`; `coverage.solana` stays `covered: true`; no `staking` key on
   `breakdown.solana`; no `solanaStakingUsd` at top level.
3. Zero-holdings path — reader succeeds but returns empty →
   `coverage.solanaStaking.covered: true`; no `staking` key; no
   `solanaStakingUsd` (avoids dangling empty fields).

## Reused

- `getSolanaStakingPositions` — `src/modules/positions/solana-staking.ts`
  (shipped in PR #141)
- SOL price — extracted from the existing `solanaSlice.native` entry
  (no new price fetch)

## What's next

Closes roadmap item #2. Next per the plan: roadmap #3 (Solana staking
writes — Marinade stake/unstake, Jito stake/unstake, native stake
delegate/deactivate/withdraw). That's 2-3 PRs per the roadmap's budget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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