Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI-673 Vault validations and withdrawal slippage checkbox #237

Merged
merged 2 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.14'
abacusVersion = '1.12.16'
carteraVersion = '0.1.15'
kollectionsVersion = '2.0.16'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ object DydxValidationView : DydxComponent {
modifier: Modifier,
viewState: ViewState
) {
if (viewState.title.isNullOrEmpty() && viewState.message.isNullOrEmpty()) {
return
}

var size by remember { mutableStateOf(IntSize.Zero) }

val shape = RoundedCornerShape(8.dp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import exchange.dydx.abacus.functional.vault.VaultFormAccountData
import exchange.dydx.abacus.functional.vault.VaultFormAction
import exchange.dydx.abacus.functional.vault.VaultFormData
import exchange.dydx.abacus.functional.vault.VaultFormValidationResult
import exchange.dydx.abacus.output.input.ErrorType
import exchange.dydx.abacus.output.input.ValidationError
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol
import indexer.models.chain.OnChainVaultDepositWithdrawSlippageResponse
import kotlinx.coroutines.flow.Flow
Expand All @@ -29,6 +32,7 @@ enum class VaultInputStage {
@ActivityRetainedScoped
class VaultInputState @Inject constructor(
private val abacusStateManager: AbacusStateManagerProtocol,
private val localizer: LocalizerProtocol,
) {
val type: MutableStateFlow<VaultInputType?> = MutableStateFlow(null)
val amount: MutableStateFlow<Double?> = MutableStateFlow(null)
Expand Down Expand Up @@ -81,6 +85,7 @@ class VaultInputState @Inject constructor(
accountData = accountData,
vaultAccount = account,
slippageResponse = slippageResponse,
localizer = localizer,
)
}
.distinctUntilChanged()
Expand All @@ -93,3 +98,21 @@ class VaultInputState @Inject constructor(
slippageResponse.value = null
}
}

val VaultFormValidationResult.firstError: ValidationError?
get() = errors.firstOrNull() { it.type == ErrorType.error }

val VaultFormValidationResult.firstWarning: ValidationError?
get() = errors.firstOrNull() { it.type == ErrorType.warning }

val VaultFormValidationResult.displayedError: ValidationError?
get() = firstError ?: firstWarning

val VaultFormValidationResult.hasBlockingError: Boolean
get() = firstError != null

val VaultFormValidationResult.canDeposit: Boolean
get() = submissionData?.deposit != null

val VaultFormValidationResult.canWithdraw: Boolean
get() = submissionData?.withdraw != null
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package exchange.dydx.trading.feature.vault.depositwithdraw

import exchange.dydx.abacus.output.input.ErrorType
import exchange.dydx.abacus.output.input.ValidationError
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.trading.feature.receipt.validation.DydxValidationView

internal fun ValidationError.createViewModel(
localizer: LocalizerProtocol
): DydxValidationView.ViewState {
return DydxValidationView.ViewState(
localizer = localizer,
state = when (this.type) {
ErrorType.error -> DydxValidationView.State.Error
ErrorType.warning -> DydxValidationView.State.Warning
ErrorType.required -> DydxValidationView.State.None
},
title = this.resources.title?.localized ?: this.resources.title?.stringKey,
message = this.resources.text?.localized ?: this.resources.text?.stringKey,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package exchange.dydx.trading.feature.vault.depositwithdraw.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.platformui.components.icons.PlatformSelectedIcon
import exchange.dydx.platformui.components.icons.PlatformUnselectedIcon
import exchange.dydx.platformui.designSystem.theme.ThemeColor
import exchange.dydx.platformui.designSystem.theme.ThemeFont
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

@Preview
@Composable
fun Preview_VaultSlippageCheckbox() {
DydxThemedPreviewSurface {
VaultSlippageCheckbox.Content(Modifier, VaultSlippageCheckbox.ViewState.preview)
}
}

object VaultSlippageCheckbox {
data class ViewState(
val localizer: LocalizerProtocol,
val text: String?,
val checked: Boolean = false,
val onCheckedChange: (Boolean) -> Unit = {},
) {
companion object {
val preview = ViewState(
localizer = MockLocalizer(),
text = "1.0M",
)
}
}

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

Row(
modifier = modifier
.clickable {
state.onCheckedChange(!state.checked)
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
if (state.checked) {
PlatformSelectedIcon(size = 24.dp)
} else {
PlatformUnselectedIcon(size = 24.dp)
}

Text(
text = state.text ?: "",
style = TextStyle.dydxDefault
.themeColor(ThemeColor.SemanticColor.text_secondary)
.themeFont(fontSize = ThemeFont.FontSize.base),
modifier = Modifier.padding(start = 8.dp).weight(1f),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import exchange.dydx.trading.common.component.DydxComponent
import exchange.dydx.trading.feature.shared.R
import exchange.dydx.trading.feature.shared.views.HeaderView
import exchange.dydx.trading.feature.shared.views.InputCtaButton
import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultSlippageCheckbox
import exchange.dydx.trading.feature.vault.receipt.DydxVaultReceiptView

@Preview
Expand All @@ -62,6 +63,7 @@ object DydxVaultConfirmationView : DydxComponent {
val ctaButton: InputCtaButton.ViewState? = null,
val backAction: (() -> Unit)? = null,
val direction: Direction? = null,
val slippage: VaultSlippageCheckbox.ViewState? = null,
) {
companion object {
val preview = ViewState(
Expand All @@ -72,6 +74,7 @@ object DydxVaultConfirmationView : DydxComponent {
destinationValue = "Vault",
ctaButton = InputCtaButton.ViewState.preview,
direction = Direction.Deposit,
slippage = VaultSlippageCheckbox.ViewState.preview,
)
}
}
Expand Down Expand Up @@ -139,6 +142,13 @@ object DydxVaultConfirmationView : DydxComponent {
modifier = Modifier.offset(y = ThemeShapes.VerticalPadding),
)

if (state.slippage != null) {
VaultSlippageCheckbox.Content(
modifier = Modifier.padding(ThemeShapes.HorizontalPadding),
state = state.slippage,
)
}

InputCtaButton.Content(
modifier = Modifier
.fillMaxWidth()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package exchange.dydx.trading.feature.vault.depositwithdraw.confirmation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import exchange.dydx.abacus.functional.vault.VaultDepositData
import exchange.dydx.abacus.functional.vault.VaultFormValidationResult
Expand All @@ -9,6 +10,7 @@ import exchange.dydx.abacus.output.Vault
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.abacus.protocols.ParserProtocol
import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol
import exchange.dydx.dydxstatemanager.localizeWithParams
import exchange.dydx.trading.common.DydxViewModel
import exchange.dydx.trading.common.formatter.DydxFormatter
import exchange.dydx.trading.common.navigation.DydxRouter
Expand All @@ -17,10 +19,15 @@ import exchange.dydx.trading.feature.shared.views.InputCtaButton
import exchange.dydx.trading.feature.vault.VaultInputStage
import exchange.dydx.trading.feature.vault.VaultInputState
import exchange.dydx.trading.feature.vault.VaultInputType
import exchange.dydx.trading.feature.vault.canDeposit
import exchange.dydx.trading.feature.vault.canWithdraw
import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultSlippageCheckbox
import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
Expand All @@ -34,21 +41,25 @@ class DydxVaultConfirmationViewModel @Inject constructor(
private val parser: ParserProtocol,
) : ViewModel(), DydxViewModel {

private val isSubmitting: MutableStateFlow<Boolean> = MutableStateFlow(false)

val state: Flow<DydxVaultConfirmationView.ViewState?> =
combine(
abacusStateManager.state.vault,
inputState.amount,
inputState.type.filterNotNull(),
inputState.result,
) { vault, amount, type, result ->
createViewState(vault, amount, type, result)
isSubmitting,
) { vault, amount, type, result, isSubmitting ->
createViewState(vault, amount, type, result, isSubmitting)
}

private fun createViewState(
vault: Vault?,
amount: Double?,
type: VaultInputType,
result: VaultFormValidationResult?
result: VaultFormValidationResult?,
isSubmitting: Boolean,
): DydxVaultConfirmationView.ViewState {
return DydxVaultConfirmationView.ViewState(
localizer = localizer,
Expand Down Expand Up @@ -77,44 +88,108 @@ class DydxVaultConfirmationViewModel @Inject constructor(
router.navigateBack()
inputState.stage.value = VaultInputStage.EDIT
},
ctaButton = InputCtaButton.ViewState(
ctaButton = createInputCtaButton(type, result, isSubmitting),
slippage = createSlippage(type, result),
)
}

private fun createSlippage(
type: VaultInputType,
result: VaultFormValidationResult?
): VaultSlippageCheckbox.ViewState? {
if (type == VaultInputType.WITHDRAW && result?.summaryData?.needSlippageAck == true) {
val slippage = formatter.percent(result?.summaryData?.estimatedSlippage, digits = 2) ?: ""
val slippageText = localizer.localizeWithParams(
path = "APP.VAULTS.SLIPPAGE_ACK",
params = mapOf("AMOUNT" to slippage),
)
return VaultSlippageCheckbox.ViewState(
localizer = localizer,
ctaButtonState = InputCtaButton.State.Enabled(
when (type) {
VaultInputType.DEPOSIT -> localizer.localize("APP.VAULTS.CONFIRM_DEPOSIT_CTA")
VaultInputType.WITHDRAW -> localizer.localize("APP.VAULTS.CONFIRM_WITHDRAW_CTA")
text = slippageText,
checked = inputState.slippageAcked.value,
onCheckedChange = { inputState.slippageAcked.value = it },
)
} else {
return null
}
}

private fun createInputCtaButton(
type: VaultInputType,
result: VaultFormValidationResult?,
isSubmitting: Boolean,
): InputCtaButton.ViewState {
when (type) {
VaultInputType.DEPOSIT -> {
val ctaButtonTitle = localizer.localize("APP.VAULTS.CONFIRM_DEPOSIT_CTA")
return InputCtaButton.ViewState(
localizer = localizer,
ctaButtonState = if (isSubmitting) {
InputCtaButton.State.Disabled(localizer.localize("APP.TRADE.SUBMITTING"))
} else if (result?.canDeposit == true) {
InputCtaButton.State.Enabled(ctaButtonTitle)
} else {
InputCtaButton.State.Disabled(ctaButtonTitle)
},
),
ctaAction = {
when (type) {
VaultInputType.DEPOSIT -> submitDeposit(result?.submissionData?.deposit)
VaultInputType.WITHDRAW -> submitWithdraw(result?.submissionData?.withdraw)
}
},
),
)
ctaAction = {
submitDeposit(result?.submissionData?.deposit)
},
)
}
VaultInputType.WITHDRAW -> {
val ctaButtonTitle = localizer.localize("APP.VAULTS.CONFIRM_WITHDRAW_CTA")
return InputCtaButton.ViewState(
localizer = localizer,
ctaButtonState = if (isSubmitting) {
InputCtaButton.State.Disabled(localizer.localize("APP.TRADE.SUBMITTING"))
} else if (result?.canWithdraw == true) {
InputCtaButton.State.Enabled(ctaButtonTitle)
} else {
InputCtaButton.State.Disabled(ctaButtonTitle)
},
ctaAction = {
submitWithdraw(result?.submissionData?.withdraw)
},
)
}
}
}

private fun submitDeposit(depositData: VaultDepositData?) {
val depositData = depositData ?: return
isSubmitting.value = true
cosmosClient.depositToMegavault(
subaccountNumber = parser.asInt(depositData.subaccountFrom) ?: 0,
amountUsdc = depositData.amount,
completion = { response ->
print(response)
abacusStateManager.refreshVaultAccount()
inputState.reset()
routeToVault()
},
)
}

private fun submitWithdraw(withdrawData: VaultWithdrawData?) {
val withdrawData = withdrawData ?: return
isSubmitting.value = true
cosmosClient.withdrawFromMegavault(
subaccountNumber = parser.asInt(withdrawData.subaccountTo) ?: 0,
shares = withdrawData.shares.toLong(),
minAmount = withdrawData.minAmount.toLong(),
completion = { response ->
print(response)
abacusStateManager.refreshVaultAccount()
inputState.reset()
routeToVault()
},
)
}

private fun routeToVault() {
viewModelScope.launch {
router.navigateBack()
router.navigateBack()
}
}
}
Loading
Loading