Android wallet/perps portfolio#616
Open
gemdev111 wants to merge 17 commits into
Open
Conversation
Backend endpoint + coordinator for the wallet portfolio value history: - GemDeviceApiClient: POST /v2/devices/portfolio/assets - GetPortfolioData reads held balances, POSTs them, applies the fiat rate, and returns the generated PortfolioData (mirrors iOS PortfolioDataService) - Hilt binding + GetPortfolioDataImplTest
Prepare the shared chart components so the portfolio scene can reuse them: - PeriodsPanel/ChartStateView take an optional periods list (defaults to all entries) so a chart can restrict the selector; asset and perpetual charts are unchanged - allTimeProperties is now internal and drops a dead asset parameter, so it can render the portfolio all-time high/low rows too
PortfolioChartViewModel + PortfolioChartScene showing the wallet value history plus all-time high/low: - ViewModel holds the selected period in-memory (default All, matching iOS PortfolioState); maps PortfolioData's ChartDateValue series onto ChartUIModel via a new date-value overload (no seconds/millis round-trip); timeout and min-points are named constants - Scene reuses ChartStateView / GemLineChart / chartHeader / allTimeProperties and renders the generated PortfolioStatistic; the period set is a local constant - PortfolioChartViewModelTest keeps a collector on the WhileSubscribed flow and awaits the refetched data, the way the scene subscribes via collectAsStateWithLifecycle
- PortfolioChartRoute + portfolioChartScreen entry (Navigation3) - navigator.openPortfolioChart(), mirroring openAssetChart - register the entry in WalletNavGraph
- AmountListHead gains an optional onSubtitleClick (Role.Button); the clip, ripple, and a show-chart icon only apply when it is set, so asset/ perpetual/confirm headers are unchanged. The icon mirrors iOS WalletHeaderViewModel.subtitleImage (chart.line.uptrend.xyaxis in footnote size and secondary color) - AssetsHead threads it through as onPortfolio, dispatched as AssetsAction.Portfolio through the assets screen's sealed action and wired to navigator.openPortfolioChart in MainScreen, mirroring iOS WalletHeaderView.onSubtitleAction
- PerpetualService.getPortfolio(address) -> gateway.getPerpetualPortfolio, with GemPerpetualPortfolio/timeframe/summary/date-value DTO mappers - GetPortfolioData takes a PortfolioType and dispatches wallet vs perpetuals, like iOS PortfolioDataService.getPortfoliData(input:); the perpetuals side mirrors iOS getPerpetualData (pnl + value charts, unrealizedPnl/leverage/ margin/allTimePnl/volume statistics, availablePeriods from the account timeframes) - Failures surface like iOS: a gateway error propagates and a missing hyperliquid account throws, so the chart shows the error state instead of a silent empty portfolio - The wallet side now returns data-driven availablePeriods - Hilt DI + coordinator test updated; ViewModel still requests Wallet (perpetuals UI lands in the follow-up commits)
- selectedType (wallet/perpetuals) + selectedChartType (value/pnl) drive the coordinator fetch; the chart renders the selected chartType's series and perpetuals values format in USD like iOS's perpetualFormatter (the gateway data carries no fiat rate) - showSegmentedControl gated on ObservePerpetualWallet (mirrors iOS showPerpetuals(for:)); showChartTypePicker when perpetuals is selected - availablePeriods sourced from the data per type; when the fetched data does not offer the selected period it resets to the first available one, like iOS PortfolioSceneViewModel.fetch (a new perpetuals account has no all-time history, so Year/All disappear) - chartState lives in one place: fewer than two points or a flat series with no variation is Empty (iOS ChartValues.hasVariation), a failed request is Error, and toggling the chart type recomputes it for the newly selected series - ViewModel tests cover switching, the period reset, the flat series, and the error state
- Scene title slot shows the Portfolio/Perpetuals segmented control when perpetuals is enabled; actions slot shows the value/PnL chart-type menu; periods are data-driven (viewModel.availablePeriods) - One portfolioStatistics renderer mirrors iOS's single statistics list under an Info section header: the wallet all-time high/low reuse the existing allTimeProperties rows, and the perpetual rows (unrealized/all-time PnL as signed amounts colored by direction, leverage, margin usage with an unsigned percent, volume) reuse the shared property rows and formatters, matching iOS statisticModel
Header: build a priceChange-style header (ChartValueType.PriceChange) like iOS ChartValuesViewModel instead of reusing the asset price header. It shows the absolute value as the big header value, the signed change amount colored by the amount's sign (so a negative perpetuals PnL reads red), and the percentage as an unsigned value in parentheses that is hidden when there is no header value (perpetuals PnL). showHeaderValue follows iOS: wallet, or the value chart type. The change amount uses the raw first point as the base so the perpetuals PnL change equals the full PnL, matching iOS. Footer: render the statistics section only when the chart is loaded (ready or empty), mirroring iOS which sources statistics from the data state, rather than leaving the previous type's rows visible during a refetch. The header-model change is additive: the asset price chart passes no type (defaults to Price) and renders exactly as before.
Mirror iOS: tapping the balance on the perpetuals screen opens the portfolio with the Perpetuals type preselected (PerpetualsScene balanceActionType -> .portfolio(.perpetuals)), while the wallet balance subtitle keeps opening it on Wallet. - PortfolioChartRoute carries a PortfolioType (default Wallet) passed to the ViewModel via the Type route argument, like WalletPhraseRoute - PerpetualMarketScene dispatches OpenPortfolio from AmountListHead onClick through the screen's sealed action - PortfolioChartViewModel takes the initial type via SavedStateHandle with the same two-constructor pattern as ChartViewModel - Scene title follows the selected type when the segmented control is hidden, matching iOS typeTitle(for:)
The portfolio chart duplicated the asset Chart composable body (period-keyed selection state, the loading fallback while a new period's data arrives, and the ChartStateView/GemLineChart wiring). Extract that block as ChartSection with the periods and the header builder as parameters; Chart and PortfolioChart are now thin wrappers that collect their ViewModel and choose the header.
The Portfolio/Perpetuals switch was the app's only Material3 SegmentedButton; replace it with the shared TabsBar the buy/sell switch already uses in its scene title. The chart-type menu label matches the wallet selector in AssetsTopBar: onSurface text with an ExpandMore affordance instead of a bare primary-colored label.
Review findings: viewState was a MutableStateFlow written from five call sites, with the ready/empty rule computed from a stale _selectedChartType read inside mapLatest and re-derived in setChartType from a lagging portfolio.value; the fetch result also carried no request identity, so a type switch could pair Ready with the previous type's points, and the cancelled block's finally hid the refresh indicator while the replacing fetch was still loading. The fetch now emits tagged with its request (type + period): the chart state, model, and statistics derive reactively from the tag matching the current selection, the period reset uses compareAndSet so a cancelled fetch cannot override a newer user selection, and the refresh flag clears only when a fetch actually finishes. showChartTypePicker/showHeaderValue StateFlows are gone - the picker derives from selectedType at the call site and the header value is baked into ChartUIModel like iOS ChartValuesViewModel.headerValue, which also removes their duplicated initial-value predicates. The chart percentage base now uses the raw first point like iOS ChartValuesViewModel.priceChange (was first non-zero).
Review findings: the PnL rows hand-built ListItem/PropertyTitleText/ PropertyDataText although PropertyItem(title, data, dataColor) renders the same row and is how TransactionDetailsScene shows PnL today; leverage was formatted with a locale-fragile String.format while gemcore owns leverage display (formatLeverage), which gains a locale-aware Double overload with iOS's two fraction digits; and the all-time/perpetual split was an either/or branch that would silently drop non-all-time statistics from a mixed list - the statistics now partition so both kinds render.
Review findings: the wallet fetch read the currency rate only after the portfolio POST and mapped a missing rate to an empty result, wasting the round-trip and diverging from iOS, which reads the rate first and fails into the error state - the rate is now checked before the request and throws; seconds-to-millis conversions used TimeUnit and a magic multiply instead of the shared secondsToMillis extension; the wallet period list existed twice (coordinator constant + ViewModel fallback) and is now one constant beside the GetPortfolioData interface; the portfolio sub-mappers follow the file's private-mapper precedent; new imports are sorted into their blocks.
The tagged-fetch pattern from the portfolio ViewModel moves to ui-models as ChartFetch(request, data) with a shared viewState derivation - the Android counterpart of iOS StateViewType<PortfolioData>, where the fetch result and its identity are one value. The asset ChartViewModel adopts it, deleting its imperative viewState writes, which had the same cancelled-refresh and unordered Ready-vs-data races as the portfolio chart. The candlestick chart keeps its streaming flow: flatMapLatest already binds candles to their period, so there is no identity to tag. The chart type picker flag returns as a named value in the scene, and the duplicated PnL PropertyItem calls collapse into a shared PnlPropertyItem next to the other property rows.
Port iOS StateViewType (noData/loading/data/error) to ui-models and make it the single chart state primitive: ChartViewState and the interim ChartFetch are gone, and the state now carries its payload the way iOS StateViewType<PortfolioData> does. Each chart fetch is a transformLatest that emits its request state first and the result after, so a stale result can never render under a newer selection and no imperative state writes remain. The request snapshot is a feature model like iOS Types/PortfolioState: PortfolioState(type, period, currency, data) with displayCurrency computed on it, and AssetChartState for the asset chart - the currency rides in the request, not beside the result. The portfolio and asset chart ViewModels expose one ChartUIModel.State(period, chart); the candlestick flow emits its states in-band on the stream that merges websocket candles; ChartStateView is generic over the payload. The chart ViewModels share the named StopTimeoutMillis instead of a raw 5_000.
4cd5efd to
4534e25
Compare
DRadmir
approved these changes
Jul 3, 2026
| StateViewType.Data(ChartUIModel.from(it, priceInfo, state.period, state.currency)) | ||
| }, | ||
| ) | ||
| }.stateIn(viewModelScope, SharingStarted.Eagerly, ChartUIModel.State()) |
Collaborator
There was a problem hiding this comment.
should we use WhileSubscribed?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes #403
images:
Both entry points — balance change + Perpetuals header
Line + area, high/low bounds, ATH/ATL stats
Selected point value + date
PnL chart + perps stats
Value / PnL
Same scene, Value mode