Skip to content

Commit

Permalink
Portfolio Vault view (#238)
Browse files Browse the repository at this point in the history
Also fix vault charts and sort the positions by usdc size.  

Merge after #237


![Screenshot_1727979967](https://github.com/user-attachments/assets/4e140e8a-3ddd-4e45-83ce-0fc27037be30)
  • Loading branch information
ruixhuang authored Oct 4, 2024
1 parent 46fa98a commit 3a46d4b
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 17 deletions.
2 changes: 1 addition & 1 deletion v4/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ ext {
compileSdkVersion = 34

// App dependencies
abacusVersion = '1.12.16'
abacusVersion = '1.12.17'
carteraVersion = '0.1.15'
kollectionsVersion = '2.0.16'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
Expand Down Expand Up @@ -38,6 +39,7 @@ import exchange.dydx.trading.feature.portfolio.components.pendingpositions.DydxP
import exchange.dydx.trading.feature.portfolio.components.pendingpositions.DydxPortfolioPendingPositionsViewModel
import exchange.dydx.trading.feature.portfolio.components.positions.DydxPortfolioPositionsView.positionsListContent
import exchange.dydx.trading.feature.portfolio.components.positions.DydxPortfolioPositionsViewModel
import exchange.dydx.trading.feature.portfolio.components.vault.DydxPortfolioVaultView
import exchange.dydx.trading.feature.shared.bottombar.DydxBottomBarScaffold

@Preview
Expand Down Expand Up @@ -154,6 +156,10 @@ object DydxPortfolioView : DydxComponent {
else -> {
}
}

item(key = "vault") {
DydxPortfolioVaultView.Content(Modifier)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package exchange.dydx.trading.feature.portfolio.components.vault

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.platformui.components.gradient.GradientType
import exchange.dydx.platformui.components.icons.PlatformImage
import exchange.dydx.platformui.compose.collectAsStateWithLifecycle
import exchange.dydx.platformui.designSystem.theme.ThemeColor
import exchange.dydx.platformui.designSystem.theme.ThemeFont
import exchange.dydx.platformui.designSystem.theme.ThemeShapes
import exchange.dydx.platformui.designSystem.theme.dydxDefault
import exchange.dydx.platformui.designSystem.theme.themeColor
import exchange.dydx.platformui.designSystem.theme.themeFont
import exchange.dydx.platformui.theme.DydxThemedPreviewSurface
import exchange.dydx.platformui.theme.MockLocalizer
import exchange.dydx.trading.common.component.DydxComponent
import exchange.dydx.trading.feature.shared.R
import exchange.dydx.trading.feature.shared.views.SignedAmountView

@Preview
@Composable
fun Preview_DydxPortfolioVaultView() {
DydxThemedPreviewSurface {
DydxPortfolioVaultView.Content(Modifier, DydxPortfolioVaultView.ViewState.preview)
}
}

object DydxPortfolioVaultView : DydxComponent {
data class ViewState(
val localizer: LocalizerProtocol,
val gradientType: GradientType = GradientType.NONE,
val onTapAction: () -> Unit = {},
val balance: String? = null,
val apr: SignedAmountView.ViewState? = null,
) {
companion object {
val preview = ViewState(
localizer = MockLocalizer(),
balance = "$1,234.56",
apr = SignedAmountView.ViewState.preview,
)
}
}

@Composable
override fun Content(modifier: Modifier) {
val viewModel: DydxPortfolioVaultViewModel = hiltViewModel()

val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value
Content(modifier, state)
}

@Composable
fun Content(modifier: Modifier, state: ViewState?) {
if (state == null) {
return
}

Column(
modifier = modifier
.padding(horizontal = ThemeShapes.HorizontalPadding, vertical = ThemeShapes.VerticalPadding),
verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding),
) {
Text(
text = state.localizer.localize("APP.VAULTS.MEGAVAULT"),
modifier = Modifier
.padding(vertical = ThemeShapes.VerticalPadding),
style = TextStyle.dydxDefault
.themeColor(foreground = ThemeColor.SemanticColor.text_primary)
.themeFont(fontSize = ThemeFont.FontSize.large, fontType = ThemeFont.FontType.plus),
)

Row(
modifier = Modifier.fillMaxWidth()
.padding(horizontal = ThemeShapes.HorizontalPadding),
) {
Text(
text = state.localizer.localize("APP.GENERAL.DETAILS"),
modifier = Modifier
.weight(1.25f),
style = TextStyle.dydxDefault
.themeColor(foreground = ThemeColor.SemanticColor.text_tertiary)
.themeFont(fontSize = ThemeFont.FontSize.small),
)

Text(
text = state.localizer.localize("APP.VAULTS.VAULT_THIRTY_DAY_APR"),
modifier = Modifier
.weight(1f),
style = TextStyle.dydxDefault
.themeColor(foreground = ThemeColor.SemanticColor.text_tertiary)
.themeFont(fontSize = ThemeFont.FontSize.small),
)

Text(
text = state.localizer.localize("APP.VAULTS.YOUR_VAULT_BALANCE"),
textAlign = TextAlign.End,
modifier = Modifier
.weight(1f),
style = TextStyle.dydxDefault
.themeColor(foreground = ThemeColor.SemanticColor.text_tertiary)
.themeFont(fontSize = ThemeFont.FontSize.small),
)
}

val shape = RoundedCornerShape(10.dp)

Row(
modifier = Modifier.fillMaxWidth()
.height(64.dp)
.background(
brush = state.gradientType.brush(ThemeColor.SemanticColor.layer_3),
shape = shape,
)
.clip(shape)
.clickable { state.onTapAction.invoke() }
.padding(
// inner paddings after clipping
horizontal = ThemeShapes.HorizontalPadding,
vertical = ThemeShapes.VerticalPadding,
),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier
.weight(1.25f),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
PlatformImage(
modifier = Modifier
.size(32.dp),
icon = R.drawable.vault_account_token,
)

Text(
text = state.localizer.localize("APP.VAULTS.MEGAVAULT"),
modifier = Modifier,
style = TextStyle.dydxDefault
.themeColor(foreground = ThemeColor.SemanticColor.text_secondary)
.themeFont(fontSize = ThemeFont.FontSize.base),
)
}

SignedAmountView.Content(
modifier = Modifier.weight(1f),
state = state.apr,
textStyle = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.mini),
)

Text(
text = state.balance ?: "-",
textAlign = TextAlign.End,
modifier = Modifier.weight(1f),
style = TextStyle.dydxDefault
.themeColor(foreground = ThemeColor.SemanticColor.text_secondary)
.themeFont(fontSize = ThemeFont.FontSize.base),
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package exchange.dydx.trading.feature.portfolio.components.vault

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import exchange.dydx.abacus.output.Vault
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol
import exchange.dydx.platformui.components.PlatformUISign
import exchange.dydx.trading.common.DydxViewModel
import exchange.dydx.trading.common.formatter.DydxFormatter
import exchange.dydx.trading.common.navigation.DydxRouter
import exchange.dydx.trading.common.navigation.VaultRoutes
import exchange.dydx.trading.feature.shared.views.SignedAmountView
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import javax.inject.Inject

@HiltViewModel
class DydxPortfolioVaultViewModel @Inject constructor(
private val localizer: LocalizerProtocol,
private val abacusStateManager: AbacusStateManagerProtocol,
private val formatter: DydxFormatter,
private val router: DydxRouter
) : ViewModel(), DydxViewModel {

val state: Flow<DydxPortfolioVaultView.ViewState?> = abacusStateManager.state.vault
.map {
createViewState(it)
}
.distinctUntilChanged()

private fun createViewState(vault: Vault?): DydxPortfolioVaultView.ViewState? {
val account = vault?.account ?: return null
val apr = vault?.details?.thirtyDayReturnPercent
return DydxPortfolioVaultView.ViewState(
localizer = localizer,
balance = formatter.dollar(account.balanceUsdc, digits = 2),
apr = SignedAmountView.ViewState(
text = formatter.percent(apr, digits = 2),
sign = if ((apr ?: 0.0) > 0) {
PlatformUISign.Plus
} else if ((apr ?: 0.0) < 0) {
PlatformUISign.Minus
} else {
PlatformUISign.None
},
),
onTapAction = {
router.tabTo(VaultRoutes.main)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class DydxBottomBarModel @Inject constructor(

private fun vaultItem(router: DydxRouter) = BottomBarItem(
route = NewsAlertsRoutes.main,
label = "APP.GENERAL.EARN_SHORT",
label = "APP.VAULTS.VAULT",
icon = R.drawable.ic_tab_vault,
selected = router.routeIsInBackStack(VaultRoutes.main),
onTapAction = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ object SparklineView {
lineColor = when (sign) {
PlatformUISign.Plus -> ThemeColor.SemanticColor.positiveColor.color.toArgb()
PlatformUISign.Minus -> ThemeColor.SemanticColor.negativeColor.color.toArgb()
PlatformUISign.None -> ThemeColor.SemanticColor.text_primary.color.toArgb()
PlatformUISign.None -> ThemeColor.SemanticColor.text_tertiary.color.toArgb()
},
)
// Embed regular Android View using AndroidView composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.github.mikephil.charting.data.Entry
import dagger.hilt.android.lifecycle.HiltViewModel
import exchange.dydx.abacus.functional.vault.ThirtyDayPnl
import exchange.dydx.abacus.functional.vault.VaultPosition
import exchange.dydx.abacus.functional.vault.VaultPositions
import exchange.dydx.abacus.output.Asset
import exchange.dydx.abacus.output.PerpetualMarket
import exchange.dydx.abacus.output.Vault
Expand Down Expand Up @@ -47,7 +48,7 @@ class DydxVaultViewModel @Inject constructor(
marketMap: Map<String, PerpetualMarket>?,
assetMap: Map<String, Asset>?,
): DydxVaultView.ViewState {
val items: List<DydxVaultPositionItemView.ViewState> = vault?.positions?.positions?.mapNotNull { position ->
val items: List<DydxVaultPositionItemView.ViewState> = vault?.positions?.sortedBySize?.mapNotNull { position ->
val marketId = position.marketId ?: return@mapNotNull null
val market = marketMap?.get(marketId) ?: return@mapNotNull null
val asset = assetMap?.get(market.assetId) ?: return@mapNotNull null
Expand Down Expand Up @@ -75,15 +76,15 @@ class DydxVaultViewModel @Inject constructor(
side = position.side,
),
leverage = formatter.raw(position.currentLeverageMultiple?.absoluteValue, digits = 2),
notionalValue = formatter.dollar(position.currentPosition?.usdc?.absoluteValue, digits = 0),
positionSize = formatter.raw(position.currentPosition?.asset?.absoluteValue, digits = 2),
notionalValue = formatter.dollar((position.currentPosition?.usdc?.absoluteValue ?: 0.0), digits = 0),
positionSize = formatter.raw((position.currentPosition?.asset?.absoluteValue ?: 0.0), digits = 2),
token = TokenTextView.ViewState(
symbol = asset.id,
),
pnlAmount = if (position.thirtyDayPnl?.absolute != null) {
SignedAmountView.ViewState(
sign = position.pnlSign,
text = formatter.dollar(position.thirtyDayPnl?.absolute, digits = 0) ?: "-",
text = formatter.dollar(position.thirtyDayPnl?.absolute?.absoluteValue, digits = 0) ?: "-",
)
} else {
SignedAmountView.ViewState(
Expand All @@ -107,7 +108,7 @@ class DydxVaultViewModel @Inject constructor(
val total = pnl.absolute ?: 0.0
SparklineView.ViewState(
sparkline = LineChartDataSet(lines, "Sparkline"),
sign = if (total >= 0.0) PlatformUISign.Plus else PlatformUISign.Minus,
sign = PlatformUISign.from(total),
)
} else {
null
Expand Down Expand Up @@ -138,3 +139,14 @@ private val VaultPosition.pnlSign: PlatformUISign
PlatformUISign.None
}
}

private val VaultPositions.sortedBySize: List<VaultPosition>?
get() = this.positions?.sortedWith { p1, p2 ->
val size1 = p1.currentPosition?.usdc ?: 0.0
val size2 = p2.currentPosition?.usdc ?: 0.0
if (size1 == size2) {
p2.thirtyDayPnl?.absolute?.compareTo(p1.thirtyDayPnl?.absolute ?: 0.0) ?: 0
} else {
size2.compareTo(size1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import exchange.dydx.trading.common.DydxViewModel
import exchange.dydx.trading.common.formatter.DydxFormatter
import exchange.dydx.trading.common.navigation.DydxRouter
import exchange.dydx.trading.common.navigation.VaultRoutes
import exchange.dydx.trading.feature.vault.VaultInputState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
Expand All @@ -18,6 +19,7 @@ class DydxVaultButtonsViewModel @Inject constructor(
private val abacusStateManager: AbacusStateManagerProtocol,
private val formatter: DydxFormatter,
private val router: DydxRouter,
private val inputState: VaultInputState,
) : ViewModel(), DydxViewModel {

val state: Flow<DydxVaultButtonsView.ViewState?> = flowOf(createViewState())
Expand All @@ -26,9 +28,11 @@ class DydxVaultButtonsViewModel @Inject constructor(
return DydxVaultButtonsView.ViewState(
localizer = localizer,
depositAction = {
inputState.reset()
router.navigateTo(route = VaultRoutes.deposit, presentation = DydxRouter.Presentation.Modal)
},
withdrawAction = {
inputState.reset()
router.navigateTo(route = VaultRoutes.withdraw, presentation = DydxRouter.Presentation.Modal)
},
)
Expand Down
Loading

0 comments on commit 3a46d4b

Please sign in to comment.