diff --git a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt index dafab10cb..f08195e02 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.remember import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import com.gemwallet.android.application.confirm.coordinators.ConfirmTransaction.FinishRoute -import com.gemwallet.android.domains.transaction.isPerpetual import com.gemwallet.android.features.asset_select.presents.navigation.AssetsManageRoute import com.gemwallet.android.features.asset_select.presents.navigation.AssetsSearchRoute import com.gemwallet.android.features.create_wallet.navigation.CreateWalletAlertRoute @@ -46,7 +45,6 @@ import com.gemwallet.android.ui.navigation.routes.NftAssetRoute import com.gemwallet.android.ui.navigation.routes.NftCollectionRoute import com.gemwallet.android.ui.navigation.routes.NftUnverifiedCollectionsRoute import com.gemwallet.android.ui.navigation.routes.NotificationsRoute -import com.gemwallet.android.ui.navigation.routes.PerpetualAmountRoute import com.gemwallet.android.ui.navigation.routes.PerpetualPositionRoute import com.gemwallet.android.ui.navigation.routes.PerpetualRoute import com.gemwallet.android.ui.navigation.routes.PreferencesRoute @@ -203,7 +201,7 @@ class WalletNavigator( fun openNftRecipient(assetId: AssetId, nftAssetId: String) = push(RecipientInputRoute(assetId, nftAssetId)) fun openAmount(params: AmountParams) { val pack = params.pack() ?: return - push(if (params.txType.isPerpetual) PerpetualAmountRoute(pack) else AmountRoute(pack)) + push(AmountRoute(pack)) } fun openSwap() { clearSwapSelections() @@ -315,7 +313,6 @@ internal fun NavKey.isConfirmFlowSegmentRoute(): Boolean { SwapRoute -> true is AmountRoute, is ConfirmRoute, - is PerpetualAmountRoute, is RecipientInputRoute, is StakeRoute, is SwapPairRoute, diff --git a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/TransferAmount.kt b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/TransferAmount.kt index e223339b3..1a3466170 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/TransferAmount.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/TransferAmount.kt @@ -2,9 +2,8 @@ package com.gemwallet.android.ui.navigation.routes import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import com.gemwallet.android.model.ConfirmParams -import com.gemwallet.android.features.transfer_amount.presents.AmountPerpetualNavScreen import com.gemwallet.android.features.transfer_amount.presents.AmountScreen +import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.ui.navigation.paramsArgument import com.gemwallet.android.ui.navigation.routeArguments import kotlinx.serialization.Serializable @@ -12,9 +11,6 @@ import kotlinx.serialization.Serializable @Serializable data class AmountRoute(val params: String) : NavKey -@Serializable -data class PerpetualAmountRoute(val params: String) : NavKey - fun EntryProviderScope.amount( onCancel: () -> Unit, onConfirm: (ConfirmParams) -> Unit, @@ -24,13 +20,4 @@ fun EntryProviderScope.amount( ) { AmountScreen(onCancel = onCancel, onConfirm = onConfirm) } - - entry( - metadata = { key -> routeArguments(paramsArgument(key.params)) }, - ) { - AmountPerpetualNavScreen( - onConfirm = onConfirm, - onClose = onCancel - ) - } } diff --git a/android/app/src/test/kotlin/com/gemwallet/android/ui/navigation/WalletNavigatorTest.kt b/android/app/src/test/kotlin/com/gemwallet/android/ui/navigation/WalletNavigatorTest.kt index efa1c788f..91e7e4a55 100644 --- a/android/app/src/test/kotlin/com/gemwallet/android/ui/navigation/WalletNavigatorTest.kt +++ b/android/app/src/test/kotlin/com/gemwallet/android/ui/navigation/WalletNavigatorTest.kt @@ -28,7 +28,6 @@ import com.gemwallet.android.ui.navigation.routes.FiatInputRoute import com.gemwallet.android.ui.navigation.routes.FiatSelectRoute import com.gemwallet.android.ui.navigation.routes.NftAssetRoute import com.gemwallet.android.ui.navigation.routes.NftCollectionRoute -import com.gemwallet.android.ui.navigation.routes.PerpetualAmountRoute import com.gemwallet.android.ui.navigation.routes.PriceAlertsRoute import com.gemwallet.android.ui.navigation.routes.RecipientInputRoute import com.gemwallet.android.ui.navigation.routes.ReceiveRoute @@ -197,7 +196,7 @@ class WalletNavigatorTest { WalletPhraseRoute(walletId, WalletType.Multicoin), RecipientInputRoute(assetId, nftAssetId = null), AmountRoute("amount"), - PerpetualAmountRoute("perpetual"), + AmountRoute("perpetual"), ConfirmRoute("confirm"), ).dropNonRestorableRoutes(WalletRootRoute) diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceService.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceService.kt index de1f85079..3af4cc922 100644 --- a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceService.kt +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceService.kt @@ -59,7 +59,7 @@ class TransactionBalanceService @Inject constructor( resource: Resource? = null, ): BigInteger { return assetInfo.balance( - txType = params.txType, + txType = params.transactionType, context = getContext(assetInfo, params, delegation, resource), ) } @@ -70,7 +70,7 @@ class TransactionBalanceService @Inject constructor( delegation: Delegation? = null, resource: Resource? = null, ): TransactionBalanceContext { - return when (params.txType) { + return when (params.transactionType) { TransactionType.StakeRewards -> TransactionBalanceContext( rewardsBalance = delegation?.rewardsBalance() ?: getRewardsBalance(assetInfo), ) diff --git a/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceServiceTest.kt b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceServiceTest.kt index d1ec9c4b9..d73646f50 100644 --- a/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceServiceTest.kt +++ b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/transactions/TransactionBalanceServiceTest.kt @@ -10,7 +10,6 @@ import com.gemwallet.android.testkit.mockAssetInfo import com.gemwallet.android.testkit.mockDelegation import com.gemwallet.android.testkit.mockDelegationValidator import com.gemwallet.android.testkit.mockAssetMonad -import com.wallet.core.primitives.TransactionType import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -43,7 +42,7 @@ class TransactionBalanceServiceTest { ) coEvery { stakeRepository.getRewards(asset.id, "wallet-address") } returns rewards - val amountParams = AmountParams.buildStake(asset.id, TransactionType.StakeRewards) + val amountParams = AmountParams.Stake.Rewards(asset.id) val confirmParams = ConfirmParams.Builder( asset = asset, from = requireNotNull(assetInfo.owner), diff --git a/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt b/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt index a3ef8a4e4..eca7febaa 100644 --- a/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt +++ b/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt @@ -27,7 +27,6 @@ import com.gemwallet.android.features.earn.delegation.models.DelegationProperty import com.gemwallet.android.features.earn.delegation.models.HeadDelegationInfo import com.wallet.core.primitives.DelegationState import com.wallet.core.primitives.StakeChain -import com.wallet.core.primitives.TransactionType import com.wallet.core.primitives.WalletType import dagger.hilt.android.lifecycle.HiltViewModel import uniffi.gemstone.Explorer @@ -173,14 +172,14 @@ class DelegationViewModel @Inject constructor( .stateIn(viewModelScope, SharingStarted.Eagerly, null) fun onStake(call: AmountTransactionAction) { - buildStake(TransactionType.StakeDelegate)?.let { call(it) } + buildDelegate()?.let { call(it) } } fun onUnstake(amountCall: AmountTransactionAction, confirmCall: ConfirmTransactionAction) { val assetInfo = assetInfo.value ?: return val delegation = delegation.value ?: return if (assetInfo.chain.changeAmountOnUnstake) { - buildStake(TransactionType.StakeUndelegate)?.let { amountCall(it) } + buildUndelegate()?.let { amountCall(it) } return } val from = assetInfo.owner ?: return @@ -191,7 +190,7 @@ class DelegationViewModel @Inject constructor( } fun onRedelegate(call: AmountTransactionAction) { - buildStake(TransactionType.StakeRedelegate)?.let { call(it) } + buildRedelegate()?.let { call(it) } } fun onWithdraw(call: ConfirmTransactionAction) { @@ -218,12 +217,27 @@ class DelegationViewModel @Inject constructor( ) } - private fun buildStake(type: TransactionType): AmountParams? { + private fun buildDelegate(): AmountParams.Stake.Delegate? { val assetId = assetInfo.value?.asset?.id ?: return null val delegation = delegation.value ?: return null - return AmountParams.buildStake( + return AmountParams.Stake.Delegate(assetId, validatorId = delegation.validator.id) + } + + private fun buildUndelegate(): AmountParams.Stake.Undelegate? { + val assetId = assetInfo.value?.asset?.id ?: return null + val delegation = delegation.value ?: return null + return AmountParams.Stake.Undelegate( + assetId = assetId, + validatorId = delegation.validator.id, + delegationId = delegation.base.delegationId, + ) + } + + private fun buildRedelegate(): AmountParams.Stake.Redelegate? { + val assetId = assetInfo.value?.asset?.id ?: return null + val delegation = delegation.value ?: return null + return AmountParams.Stake.Redelegate( assetId = assetId, - txType = type, validatorId = delegation.validator.id, delegationId = delegation.base.delegationId, ) diff --git a/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/components/StakeActions.kt b/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/components/StakeActions.kt index 08ccaff23..acc314543 100644 --- a/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/components/StakeActions.kt +++ b/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/components/StakeActions.kt @@ -36,17 +36,14 @@ internal fun LazyListScope.stakeActions( StakeAction.Unfreeze -> R.string.transfer_unfreeze_title } val onClick = when (item) { - StakeAction.Stake, - StakeAction.Freeze, + StakeAction.Stake -> { + { amountAction(AmountParams.Stake.Delegate(assetId)) } + } + StakeAction.Freeze -> { + { amountAction(AmountParams.Freeze(assetId, AmountParams.Freeze.Direction.Freeze)) } + } StakeAction.Unfreeze -> { - { - amountAction( - AmountParams.Companion.buildStake( - assetId = assetId, - txType = item.transactionType, - ) - ) - } + { amountAction(AmountParams.Freeze(assetId, AmountParams.Freeze.Direction.Unfreeze)) } } is StakeAction.Rewards -> onRewards } diff --git a/android/features/earn/stake/viewmodels/src/main/kotlin/com/gemwallet/android/features/stake/viewmodels/StakeViewModel.kt b/android/features/earn/stake/viewmodels/src/main/kotlin/com/gemwallet/android/features/stake/viewmodels/StakeViewModel.kt index 9ab113c2a..bf5e521ac 100644 --- a/android/features/earn/stake/viewmodels/src/main/kotlin/com/gemwallet/android/features/stake/viewmodels/StakeViewModel.kt +++ b/android/features/earn/stake/viewmodels/src/main/kotlin/com/gemwallet/android/features/stake/viewmodels/StakeViewModel.kt @@ -29,7 +29,6 @@ import com.gemwallet.android.ui.models.navigation.RouteArgument import com.gemwallet.android.features.stake.models.StakeAction import com.wallet.core.primitives.Delegation import com.wallet.core.primitives.DelegationState -import com.wallet.core.primitives.TransactionType import com.wallet.core.primitives.WalletType import com.gemwallet.android.ext.isViewOnly import dagger.hilt.android.lifecycle.HiltViewModel @@ -186,10 +185,7 @@ class StakeViewModel @Inject constructor( ) } else { onAmount( - AmountParams.buildStake( - assetId = assetInfo.asset.id, - txType = TransactionType.StakeRewards, - ) + AmountParams.Stake.Rewards(assetInfo.asset.id) ) } } diff --git a/android/features/perpetual/presents/src/main/kotlin/com/gemwallet/android/features/perpetual/views/position/PerpetualPositionNavScreen.kt b/android/features/perpetual/presents/src/main/kotlin/com/gemwallet/android/features/perpetual/views/position/PerpetualPositionNavScreen.kt index bd11537aa..05a0dd622 100644 --- a/android/features/perpetual/presents/src/main/kotlin/com/gemwallet/android/features/perpetual/views/position/PerpetualPositionNavScreen.kt +++ b/android/features/perpetual/presents/src/main/kotlin/com/gemwallet/android/features/perpetual/views/position/PerpetualPositionNavScreen.kt @@ -28,10 +28,10 @@ fun PerpetualPositionNavScreen( onOpenPosition = { direction -> val currentPerpetual = perpetual ?: return@PerpetualPositionScene onOpenPosition( - AmountParams.buildPerpetualOpenPosition( - currentPerpetual.asset.id, - currentPerpetual.id, - direction, + AmountParams.Perpetual( + assetId = currentPerpetual.asset.id, + perpetualId = currentPerpetual.id, + direction = direction, ) ) } diff --git a/android/features/recipient/viewmodels/src/main/kotlin/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt b/android/features/recipient/viewmodels/src/main/kotlin/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt index 2c15bdebd..eac8ec6cf 100644 --- a/android/features/recipient/viewmodels/src/main/kotlin/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt +++ b/android/features/recipient/viewmodels/src/main/kotlin/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt @@ -170,7 +170,7 @@ class RecipientViewModel @Inject constructor( when (type) { is RecipientType.Nft -> onNftConfirm(type.nftAsset, destination, confirmAction) is RecipientType.Asset -> amountAction( - AmountParams.buildTransfer(type.assetInfo.id(), destination, memo.value) + AmountParams.Transfer(type.assetInfo.id(), destination, memo.value) ) } } diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountPerpetualNavScreen.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountPerpetualNavScreen.kt deleted file mode 100644 index 8192a7d8e..000000000 --- a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountPerpetualNavScreen.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.presents - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.gemwallet.android.model.ConfirmParams -import com.gemwallet.android.ui.R -import com.gemwallet.android.ui.components.clickable -import com.gemwallet.android.ui.components.list_item.property.DataBadgeChevron -import com.gemwallet.android.ui.components.list_item.property.PropertyDataText -import com.gemwallet.android.ui.components.list_item.property.PropertyItem -import com.gemwallet.android.ui.components.list_item.property.PropertyTitleText -import com.gemwallet.android.ui.models.ListPosition -import com.gemwallet.android.features.transfer_amount.presents.dialogs.SelectLeverageDialog -import com.gemwallet.android.features.transfer_amount.viewmodels.PerpetualAmountViewModel -import com.wallet.core.primitives.PerpetualDirection - -@Composable -fun AmountPerpetualNavScreen( - onConfirm: (ConfirmParams) -> Unit, - onClose: () -> Unit, - viewModel: PerpetualAmountViewModel = hiltViewModel() -) { - val params by viewModel.params.collectAsStateWithLifecycle() - val assetInfo by viewModel.assetInfo.collectAsStateWithLifecycle() - val error by viewModel.amountError.collectAsStateWithLifecycle() - val equivalent by viewModel.amountEquivalent.collectAsStateWithLifecycle() - val availableBalance by viewModel.availableBalanceFormatted.collectAsStateWithLifecycle() - val reserveForFee by viewModel.reserveForFee.collectAsStateWithLifecycle() - val amountInputType by viewModel.amountInputType.collectAsStateWithLifecycle() - val availableLeverages by viewModel.availableLeverages.collectAsStateWithLifecycle() - val leverage by viewModel.leverage.collectAsStateWithLifecycle() - - var showLeverageSelect by remember { mutableStateOf(false) } - val currentAssetInfo = assetInfo ?: return - val currency = currentAssetInfo.price?.currency ?: return - - AmountScene( - title = when (params.perpetualDirection) { - PerpetualDirection.Short -> stringResource(R.string.perpetual_short) - else -> stringResource(R.string.perpetual_long) - }, - amount = viewModel.amount, - amountInputType = amountInputType, - txType = params.txType, - asset = currentAssetInfo.asset, - currency = currency, - error = error, - equivalent = equivalent, - availableBalance = availableBalance, - reserveForFee = reserveForFee?.toString(), - onNext = { viewModel.onNext(onConfirm) }, - onInputAmount = viewModel::updateAmount, - onInputTypeClick = viewModel::switchInputType, - onMaxAmount = viewModel::onMaxAmount, - onCancel = onClose, - ) { - PropertyItem( - modifier = Modifier.clickable { showLeverageSelect = true }, - title = { PropertyTitleText(R.string.perpetual_leverage) }, - data = { PropertyDataText("${leverage}x", badge = { DataBadgeChevron() }) }, - listPosition = ListPosition.Single, - ) - } - - SelectLeverageDialog( - isVisible = showLeverageSelect, - leverages = availableLeverages, - onDismiss = { showLeverageSelect = false }, - onSelect = viewModel::setLeverage - ) -} diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScene.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScene.kt index 17e8dd6f1..cb15413f5 100644 --- a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScene.kt +++ b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScene.kt @@ -22,6 +22,8 @@ import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.gemwallet.android.domains.asset.getIconUrl +import com.gemwallet.android.features.transfer_amount.models.AmountError +import com.gemwallet.android.features.transfer_amount.presents.components.amountErrorString import com.gemwallet.android.ui.R import com.gemwallet.android.ui.components.InfoButton import com.gemwallet.android.ui.components.InfoSheetEntity @@ -35,28 +37,19 @@ import com.gemwallet.android.ui.models.AmountInputType import com.gemwallet.android.ui.theme.Spacer16 import com.gemwallet.android.ui.theme.paddingDefault import com.gemwallet.android.ui.theme.paddingSmall -import com.gemwallet.android.features.transfer_amount.models.AmountError -import com.gemwallet.android.features.transfer_amount.presents.components.amountErrorString -import com.gemwallet.android.features.transfer_amount.presents.components.resourceSelect -import com.gemwallet.android.features.transfer_amount.presents.components.transactionTypeTitle -import com.gemwallet.android.features.transfer_amount.presents.components.validatorView import com.wallet.core.primitives.Asset import com.wallet.core.primitives.Currency -import com.wallet.core.primitives.DelegationValidator -import com.wallet.core.primitives.Resource -import com.wallet.core.primitives.TransactionType @Composable fun AmountScene( - txType: TransactionType, - title: String = transactionTypeTitle(txType), + title: String, amount: String, - amountPrefill: String? = null, amountInputType: AmountInputType, asset: Asset, currency: Currency, - validatorState: DelegationValidator? = null, - resource: Resource = Resource.Bandwidth, + canSwitchInputType: Boolean, + readOnly: Boolean, + showsAssetBalance: Boolean, error: AmountError, equivalent: String, availableBalance: String, @@ -66,8 +59,6 @@ fun AmountScene( onInputTypeClick: () -> Unit, onMaxAmount: () -> Unit, onCancel: () -> Unit, - onResourceSelect: (Resource) -> Unit = {}, - onValidator: () -> Unit = {}, additionParams: (@Composable () -> Unit)? = null, ) { val focusRequester = remember { FocusRequester() } @@ -89,41 +80,39 @@ fun AmountScene( } }, actions = { - TextButton(onClick = onNext, - colors = ButtonDefaults.textButtonColors() - .copy(contentColor = MaterialTheme.colorScheme.primary) - ) { - Text(stringResource(R.string.common_continue).uppercase()) - } - } + TextButton( + onClick = onNext, + colors = ButtonDefaults.textButtonColors().copy(contentColor = MaterialTheme.colorScheme.primary), + ) { Text(stringResource(R.string.common_continue).uppercase()) } + }, ) { LazyColumn(horizontalAlignment = Alignment.CenterHorizontally) { item { Spacer16() AmountField( modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), - amount = amountPrefill ?: amount, + amount = amount, assetSymbol = asset.symbol, currency = currency, inputType = amountInputType, - onInputTypeClick = if (txType.canSwitchAmountInputTypeOnAmountScreen()) onInputTypeClick else null, + onInputTypeClick = if (canSwitchInputType) onInputTypeClick else null, equivalent = equivalent, - readOnly = !amountPrefill.isNullOrEmpty(), + readOnly = readOnly, error = amountErrorString(error = error), onValueChange = onInputAmount, - onNext = onNext - ) - } - item { - PropertyAssetInfoItem( - asset = asset, - availableAmount = availableBalance, - onMaxAmount = onMaxAmount + onNext = onNext, ) } - item { - additionParams?.invoke() + if (showsAssetBalance) { + item { + PropertyAssetInfoItem( + asset = asset, + availableAmount = availableBalance, + onMaxAmount = onMaxAmount, + ) + } } + item { additionParams?.invoke() } reserveForFee?.let { item { Row( @@ -132,25 +121,14 @@ fun AmountScene( horizontalArrangement = Arrangement.spacedBy(paddingSmall), ) { InfoButton(InfoSheetEntity.ReserveForFee(asset.getIconUrl())) - Text( - text = stringResource(R.string.transfer_reserved_fees, it) - ) + Text(text = stringResource(R.string.transfer_reserved_fees, it)) } - } } - validatorView(txType, validatorState, onValidator) - resourceSelect(txType, resource, onResourceSelect) } } LaunchedEffect(Unit) { - try { - focusRequester.requestFocus() - } catch (_: Throwable) {} + try { focusRequester.requestFocus() } catch (_: Throwable) {} } } - -internal fun TransactionType.canSwitchAmountInputTypeOnAmountScreen(): Boolean { - return this == TransactionType.Transfer -} diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScreen.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScreen.kt index a344df392..762840d1c 100644 --- a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScreen.kt +++ b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountScreen.kt @@ -7,17 +7,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountViewModel +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountStakeProvider import com.gemwallet.android.model.ConfirmParams -import com.gemwallet.android.ui.R import com.gemwallet.android.ui.components.animation.navigationSlideTransition import com.gemwallet.android.ui.components.screen.LoadingScene -import com.gemwallet.android.features.transfer_amount.models.ValidatorsSource -import com.gemwallet.android.features.transfer_amount.viewmodels.AmountViewModel import com.wallet.core.primitives.Currency -import com.wallet.core.primitives.TransactionType @Composable fun AmountScreen( @@ -25,77 +22,65 @@ fun AmountScreen( onConfirm: (ConfirmParams) -> Unit, viewModel: AmountViewModel = hiltViewModel(), ) { - val params by viewModel.params.collectAsStateWithLifecycle() - val assetInfo by viewModel.assetInfo.collectAsStateWithLifecycle() - val validatorState by viewModel.validatorState.collectAsStateWithLifecycle() - val error by viewModel.errorUIState.collectAsStateWithLifecycle() - val equivalent by viewModel.equivalentState.collectAsStateWithLifecycle() - val availableBalance by viewModel.availableBalance.collectAsStateWithLifecycle() - val reserveForFee by viewModel.reserveForFee.collectAsStateWithLifecycle() - val amountPrefill by viewModel.prefillAmount.collectAsStateWithLifecycle() - val amountInputType by viewModel.amountInputType.collectAsStateWithLifecycle() - val resources by viewModel.resource.collectAsStateWithLifecycle() - - var isSelectValidator by remember { - mutableStateOf(false) - } - - BackHandler(isSelectValidator) { - isSelectValidator = false - } + val provider = viewModel.provider + val assetInfo by provider.assetInfo.collectAsStateWithLifecycle() + val title = provider.title.asString() if (assetInfo == null) { - LoadingScene(stringResource(id = R.string.transfer_amount_title), onCancel) + LoadingScene(title, onCancel) return } + var isSelectValidator by remember { mutableStateOf(false) } + val canPickValidator = provider is AmountStakeProvider && provider.canSelectValidator + BackHandler(isSelectValidator && canPickValidator) { isSelectValidator = false } + + val amountInputType by viewModel.amountInputType.collectAsStateWithLifecycle() + val error by viewModel.amountError.collectAsStateWithLifecycle() + val equivalent by viewModel.amountEquivalent.collectAsStateWithLifecycle() + val available by viewModel.availableBalanceFormatted.collectAsStateWithLifecycle() + val reserve by viewModel.reserveForFeeFormatted.collectAsStateWithLifecycle() + AnimatedContent( - isSelectValidator, - transitionSpec = { - navigationSlideTransition(forward = targetState) - }, - label = "stake" - ) { state -> - when (state) { - true -> { - val asset = assetInfo?.asset ?: return@AnimatedContent - val source = when (params.txType) { - TransactionType.StakeRewards -> ValidatorsSource.Rewards( - assetId = asset.id, - owner = assetInfo?.owner?.address ?: return@AnimatedContent, - ) - else -> ValidatorsSource.ChainValidators(chain = asset.id.chain) - } + isSelectValidator && canPickValidator, + transitionSpec = { navigationSlideTransition(forward = targetState) }, + label = "amount-validator-pick", + ) { showingPicker -> + if (showingPicker && provider is AmountStakeProvider) { + val validator by provider.validatorState.collectAsStateWithLifecycle() + val source by provider.validatorSource.collectAsStateWithLifecycle() + source?.let { resolved -> ValidatorsScreen( - source = source, - selectedValidatorId = validatorState?.id ?: "", + source = resolved, + selectedValidatorId = validator?.id.orEmpty(), onCancel = { isSelectValidator = false }, onSelect = { + provider.selectValidator(it) isSelectValidator = false - viewModel.setDelegatorValidator(it) - } + }, ) } - false -> AmountScene( + } else { + val asset = assetInfo!!.asset + AmountScene( + title = title, amount = viewModel.amount, - amountPrefill = amountPrefill, - asset = assetInfo?.asset ?: return@AnimatedContent, - currency = assetInfo?.price?.currency ?: Currency.USD, amountInputType = amountInputType, - txType = params.txType, - validatorState = validatorState, + asset = asset, + currency = assetInfo!!.price?.currency ?: Currency.USD, + canSwitchInputType = provider.canSwitchInputType, + readOnly = !provider.canChangeValue, + showsAssetBalance = provider.showsAssetBalance, error = error, equivalent = equivalent, - availableBalance = availableBalance, - reserveForFee = reserveForFee, - resource = resources, + availableBalance = available, + reserveForFee = reserve, onNext = { viewModel.onNext(onConfirm) }, onInputAmount = viewModel::updateAmount, - onMaxAmount = viewModel::onMaxAmount, onInputTypeClick = viewModel::switchInputType, + onMaxAmount = viewModel::onMaxAmount, onCancel = onCancel, - onResourceSelect = viewModel::setResource, - onValidator = { isSelectValidator = !isSelectValidator }, + additionParams = { ProviderExtras(provider, onPickValidator = { isSelectValidator = true }) }, ) } } diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountTitleString.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountTitleString.kt new file mode 100644 index 000000000..b92f10a70 --- /dev/null +++ b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountTitleString.kt @@ -0,0 +1,28 @@ +package com.gemwallet.android.features.transfer_amount.presents + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.ui.R +import com.wallet.core.primitives.PerpetualDirection + +@Composable +fun AmountTitle.asString(): String = when (this) { + AmountTitle.Send -> stringResource(R.string.transfer_send_title) + is AmountTitle.Stake -> stringResource(when (action) { + is AmountParams.Stake.Delegate -> R.string.transfer_stake_title + is AmountParams.Stake.Undelegate -> R.string.transfer_unstake_title + is AmountParams.Stake.Redelegate -> R.string.transfer_redelegate_title + is AmountParams.Stake.Withdraw -> R.string.transfer_withdraw_title + is AmountParams.Stake.Rewards -> R.string.transfer_rewards_title + }) + is AmountTitle.Freeze -> stringResource(when (direction) { + AmountParams.Freeze.Direction.Freeze -> R.string.transfer_freeze_title + AmountParams.Freeze.Direction.Unfreeze -> R.string.transfer_unfreeze_title + }) + is AmountTitle.Perpetual -> stringResource(when (direction) { + PerpetualDirection.Short -> R.string.perpetual_short + else -> R.string.perpetual_long + }) +} diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/ProviderExtras.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/ProviderExtras.kt new file mode 100644 index 000000000..6b92997a3 --- /dev/null +++ b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/ProviderExtras.kt @@ -0,0 +1,91 @@ +package com.gemwallet.android.features.transfer_amount.presents + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.gemwallet.android.features.transfer_amount.presents.dialogs.SelectLeverageDialog +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountDataProvider +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountFreezeProvider +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountPerpetualProvider +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountStakeProvider +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountTransferProvider +import com.gemwallet.android.ui.R +import com.gemwallet.android.ui.components.TabsBar +import com.gemwallet.android.ui.components.clickable +import com.gemwallet.android.ui.components.list_item.SubheaderItem +import com.gemwallet.android.ui.components.list_item.property.DataBadgeChevron +import com.gemwallet.android.ui.components.list_item.property.PropertyDataText +import com.gemwallet.android.ui.components.list_item.property.PropertyItem +import com.gemwallet.android.ui.components.list_item.property.PropertyTitleText +import com.gemwallet.android.ui.components.list_item.property.PropertyValidatorItem +import com.gemwallet.android.ui.models.ListPosition +import com.wallet.core.primitives.Resource + +@Composable +fun ProviderExtras( + provider: AmountDataProvider, + onPickValidator: () -> Unit, +) { + Column { + when (provider) { + is AmountStakeProvider -> StakeValidatorSection(provider, onPickValidator) + is AmountFreezeProvider -> FreezeResourceSection(provider) + is AmountPerpetualProvider -> PerpetualLeverageSection(provider) + is AmountTransferProvider -> Unit + } + } +} + +@Composable +private fun StakeValidatorSection(provider: AmountStakeProvider, onPickValidator: () -> Unit) { + val validator by provider.validatorState.collectAsStateWithLifecycle() + validator?.let { current -> + SubheaderItem(R.string.stake_validator) + PropertyValidatorItem( + validator = current, + listPosition = ListPosition.Single, + onClick = if (provider.canSelectValidator) onPickValidator else null, + ) + } +} + +@Composable +private fun FreezeResourceSection(provider: AmountFreezeProvider) { + val resource by provider.resource.collectAsStateWithLifecycle() + TabsBar( + tabs = listOf(Resource.Bandwidth, Resource.Energy), + selected = resource, + onSelect = provider::setResource, + ) { item -> + Text(stringResource(when (item) { + Resource.Bandwidth -> R.string.stake_resource_bandwidth + Resource.Energy -> R.string.stake_resource_energy + })) + } +} + +@Composable +private fun PerpetualLeverageSection(provider: AmountPerpetualProvider) { + var showLeverageSelect by remember { mutableStateOf(false) } + val leverage by provider.leverage.collectAsStateWithLifecycle() + val available by provider.availableLeverages.collectAsStateWithLifecycle() + PropertyItem( + modifier = Modifier.clickable { showLeverageSelect = true }, + title = { PropertyTitleText(R.string.perpetual_leverage) }, + data = { PropertyDataText("${leverage}x", badge = { DataBadgeChevron() }) }, + listPosition = ListPosition.Single, + ) + SelectLeverageDialog( + isVisible = showLeverageSelect, + leverages = available, + onDismiss = { showLeverageSelect = false }, + onSelect = provider::setLeverage, + ) +} diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/AmountErrorString.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/AmountErrorString.kt index 4f6df6e5a..24ab58f22 100644 --- a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/AmountErrorString.kt +++ b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/AmountErrorString.kt @@ -29,4 +29,6 @@ fun amountErrorString(error: AmountError): String = when (error) { ) AmountError.IncorrectAddress -> stringResource(id = R.string.errors_invalid_address_name) is AmountError.Unknown -> "${stringResource(id = R.string.errors_unknown)}: ${error.data}" + AmountError.NoValidatorSelected -> stringResource(id = R.string.errors_unknown) + AmountError.NoDelegationSelected -> stringResource(id = R.string.errors_unknown) } \ No newline at end of file diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/FreezeVariant.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/FreezeVariant.kt deleted file mode 100644 index cfb08a85a..000000000 --- a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/FreezeVariant.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.presents.components - -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.gemwallet.android.ui.R -import com.gemwallet.android.ui.components.TabsBar -import com.gemwallet.android.ui.theme.WalletTheme -import com.wallet.core.primitives.Resource -import com.wallet.core.primitives.TransactionType - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun LazyListScope.resourceSelect( - txType: TransactionType, - selected: Resource, - onSelect: (Resource) -> Unit -) { - if (txType != TransactionType.StakeFreeze && txType != TransactionType.StakeUnfreeze) { - return - } - item { - TabsBar(listOf(Resource.Bandwidth, Resource.Energy), selected, onSelect) { item -> - Text( - stringResource( - when (item) { - Resource.Bandwidth -> R.string.stake_resource_bandwidth - Resource.Energy -> R.string.stake_resource_energy - } - ), - ) - } - } -} - -@Preview -@Composable -fun PreviewFreezeVarian() { - WalletTheme { - LazyColumn { - resourceSelect(TransactionType.StakeFreeze, selected = Resource.Energy) {} - } - } -} \ No newline at end of file diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/TransactionTypeTitle.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/TransactionTypeTitle.kt deleted file mode 100644 index e1f81f5e9..000000000 --- a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/TransactionTypeTitle.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.presents.components - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.gemwallet.android.ui.R -import com.wallet.core.primitives.TransactionType - -@Composable -internal fun transactionTypeTitle(txType: TransactionType) = when (txType) { - TransactionType.Transfer -> stringResource(id = R.string.transfer_send_title) - TransactionType.StakeDelegate -> stringResource(id = R.string.transfer_stake_title) - TransactionType.StakeUndelegate -> stringResource(id = R.string.transfer_unstake_title) - TransactionType.StakeRedelegate -> stringResource(id = R.string.transfer_redelegate_title) - TransactionType.StakeWithdraw -> stringResource(id = R.string.transfer_withdraw_title) - TransactionType.TransferNFT -> stringResource(id = R.string.nft_collection) - TransactionType.Swap -> stringResource(id = R.string.wallet_swap) - TransactionType.TokenApproval -> stringResource(id = R.string.transfer_approve_title) - TransactionType.StakeRewards -> stringResource(id = R.string.transfer_rewards_title) - TransactionType.AssetActivation -> stringResource(id = R.string.transfer_activate_asset_title) - TransactionType.SmartContractCall -> stringResource(id = R.string.transfer_amount_title) - TransactionType.PerpetualOpenPosition -> stringResource(R.string.perpetual_position) - TransactionType.PerpetualClosePosition -> stringResource(R.string.perpetual_close_position) - TransactionType.StakeFreeze -> stringResource(R.string.transfer_freeze_title) - TransactionType.StakeUnfreeze -> stringResource(R.string.transfer_unfreeze_title) - TransactionType.PerpetualModifyPosition -> stringResource(R.string.perpetual_modify) - TransactionType.EarnDeposit -> stringResource(id = R.string.transfer_stake_title) - TransactionType.EarnWithdraw -> stringResource(id = R.string.transfer_withdraw_title) -} \ No newline at end of file diff --git a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/ValidatorView.kt b/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/ValidatorView.kt deleted file mode 100644 index dbb196938..000000000 --- a/android/features/transfer_amount/presents/src/main/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/ValidatorView.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.presents.components - -import androidx.compose.foundation.lazy.LazyListScope -import com.gemwallet.android.ui.R -import com.gemwallet.android.ui.components.list_item.SubheaderItem -import com.gemwallet.android.ui.components.list_item.property.PropertyValidatorItem -import com.gemwallet.android.ui.models.ListPosition -import com.wallet.core.primitives.DelegationValidator -import com.wallet.core.primitives.TransactionType - -internal fun LazyListScope.validatorView( - txType: TransactionType, - validatorState: DelegationValidator?, - onValidator: () -> Unit -) { - validatorState ?: return - val isValidatorSelectable = txType.canSelectValidatorOnAmountScreen() - item { - SubheaderItem(R.string.stake_validator) - } - item { - PropertyValidatorItem( - validator = validatorState, - listPosition = ListPosition.Single, - onClick = if (isValidatorSelectable) { - { onValidator() } - } else { - null - } - ) - } -} - -internal fun TransactionType.canSelectValidatorOnAmountScreen(): Boolean { - return this != TransactionType.StakeUndelegate -} diff --git a/android/features/transfer_amount/presents/src/test/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountSceneTest.kt b/android/features/transfer_amount/presents/src/test/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountSceneTest.kt deleted file mode 100644 index 3b37e0a2b..000000000 --- a/android/features/transfer_amount/presents/src/test/kotlin/com/gemwallet/android/features/transfer_amount/presents/AmountSceneTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.presents - -import com.wallet.core.primitives.TransactionType -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class AmountSceneTest { - @Test - fun transfer_keepsAmountInputTypeSwitch() { - assertTrue(TransactionType.Transfer.canSwitchAmountInputTypeOnAmountScreen()) - } - - @Test - fun stakeFlows_hideAmountInputTypeSwitch() { - assertFalse(TransactionType.StakeDelegate.canSwitchAmountInputTypeOnAmountScreen()) - assertFalse(TransactionType.StakeUndelegate.canSwitchAmountInputTypeOnAmountScreen()) - assertFalse(TransactionType.StakeRedelegate.canSwitchAmountInputTypeOnAmountScreen()) - } -} diff --git a/android/features/transfer_amount/presents/src/test/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/ValidatorViewTest.kt b/android/features/transfer_amount/presents/src/test/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/ValidatorViewTest.kt deleted file mode 100644 index 77b4d6a5c..000000000 --- a/android/features/transfer_amount/presents/src/test/kotlin/com/gemwallet/android/features/transfer_amount/presents/components/ValidatorViewTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.presents.components - -import com.wallet.core.primitives.TransactionType -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class ValidatorViewTest { - @Test - fun stakeUndelegate_disablesValidatorSelection() { - assertFalse(TransactionType.StakeUndelegate.canSelectValidatorOnAmountScreen()) - } - - @Test - fun stakeDelegateAndRedelegate_enableValidatorSelection() { - assertTrue(TransactionType.StakeDelegate.canSelectValidatorOnAmountScreen()) - assertTrue(TransactionType.StakeRedelegate.canSelectValidatorOnAmountScreen()) - } -} diff --git a/android/features/transfer_amount/viewmodels/build.gradle.kts b/android/features/transfer_amount/viewmodels/build.gradle.kts index 86ba197b2..bb4c55138 100644 --- a/android/features/transfer_amount/viewmodels/build.gradle.kts +++ b/android/features/transfer_amount/viewmodels/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { testImplementation(testFixtures(project(":gemcore"))) testImplementation(libs.junit) + testImplementation(libs.mockk.android) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) } \ No newline at end of file diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/models/AmountError.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/models/AmountError.kt index 00d08853e..5a5a6bee7 100644 --- a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/models/AmountError.kt +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/models/AmountError.kt @@ -20,4 +20,8 @@ sealed class AmountError : Exception() { object IncorrectAddress : AmountError() class Unknown(val data: String) : AmountError() + + object NoValidatorSelected : AmountError() + + object NoDelegationSelected : AmountError() } \ No newline at end of file diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountBaseViewModel.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountBaseViewModel.kt deleted file mode 100644 index 2d3def12c..000000000 --- a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountBaseViewModel.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.viewmodels - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.gemwallet.android.math.parseNumber -import com.gemwallet.android.model.AmountParams -import com.gemwallet.android.model.AssetInfo -import com.gemwallet.android.model.ConfirmParams -import com.gemwallet.android.model.Crypto -import com.gemwallet.android.model.format -import com.gemwallet.android.ui.models.AmountInputType -import com.gemwallet.android.features.transfer_amount.models.AmountError -import com.wallet.core.primitives.Asset -import com.wallet.core.primitives.Currency -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.math.BigDecimal -import java.math.BigInteger -import java.math.MathContext - -@OptIn(ExperimentalCoroutinesApi::class) -abstract class AmountBaseViewModel( - savedStateHandle: SavedStateHandle -) : ViewModel() { - val params = MutableStateFlow(savedStateHandle.requireAmountParams()) - - val txType = params.mapLatest { it.txType } - .stateIn(viewModelScope, SharingStarted.Eagerly, params.value.txType) - - var amount by mutableStateOf("") - private set - - open val inputTypeToggleable - get() = true - val amountInputType = MutableStateFlow(AmountInputType.Crypto) - - val amountError = MutableStateFlow(AmountError.None) - - abstract val assetInfo: StateFlow - - val amountEquivalent: StateFlow - get() = combine( - snapshotFlow { amount }, - amountInputType, - assetInfo, - ) { input, direction, assetInfo -> - val priceInfo = assetInfo?.price ?: return@combine "" - calcEquivalent( - inputAmount = input, - inputDirection = direction, - asset = assetInfo.asset, - price = priceInfo.price.price, - currency = priceInfo.currency - ) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, "") - - internal val maxAmount = MutableStateFlow(false) - - val reserveForFee: StateFlow get() = MutableStateFlow(null) - - abstract val availableBalance: StateFlow - abstract val availableBalanceFormatted: StateFlow - - fun updateAmount(input: String, isMax: Boolean = false) { - amount = input - maxAmount.update { isMax } - } - - abstract fun onMaxAmount() - - fun switchInputType() { - amountInputType.update { - when (it) { - AmountInputType.Crypto -> AmountInputType.Fiat - AmountInputType.Fiat -> AmountInputType.Crypto - } - } - amount = "" - } - - fun onNext(onConfirm: (ConfirmParams) -> Unit) { - viewModelScope.launch { - try { - onNext(params.value, amount, onConfirm) - } catch (err: Throwable) { - when (err) { - is AmountError -> amountError.update { err } - else -> amountError.update { AmountError.Unknown(err.message ?: "Unknown error") } - } - } - } - } - - internal abstract fun onNext( - params: AmountParams, - rawAmount: String, - onConfirm: (ConfirmParams) -> Unit - ) - - internal fun calcEquivalent( - inputAmount: String, - inputDirection: AmountInputType, - asset: Asset, - price: Double, - currency: Currency - ): String { - return try { - when (inputDirection) { - AmountInputType.Crypto -> { - AmountValidation.validateAmount(asset, inputAmount, BigInteger.ZERO) - val amount = inputAmount.parseNumber() - val decimals = asset.decimals - val unit = Crypto(amount, decimals).convert(decimals, price) - currency.format(unit.atomicValue) - } - AmountInputType.Fiat -> { - val value = inputAmount.parseNumber() - val crypto = value.divide(price.toBigDecimal(), MathContext.DECIMAL128) - AmountValidation.validateAmount(asset, crypto.toString(), BigInteger.ZERO) - asset.format(crypto, dynamicPlace = true) - } - } - } catch (_: Throwable) { - when (inputDirection) { - AmountInputType.Crypto -> { - currency.format(0.0) - } - AmountInputType.Fiat -> { - asset.format(Crypto(BigInteger.ZERO), dynamicPlace = true) - } - } - } - } -} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountTitle.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountTitle.kt new file mode 100644 index 000000000..77795c1ec --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountTitle.kt @@ -0,0 +1,11 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels + +import com.gemwallet.android.model.AmountParams +import com.wallet.core.primitives.PerpetualDirection + +sealed interface AmountTitle { + data object Send : AmountTitle + data class Stake(val action: AmountParams.Stake) : AmountTitle + data class Freeze(val direction: AmountParams.Freeze.Direction) : AmountTitle + data class Perpetual(val direction: PerpetualDirection) : AmountTitle +} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountViewModel.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountViewModel.kt index 17f290cd8..63712bd0c 100644 --- a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountViewModel.kt +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountViewModel.kt @@ -7,51 +7,31 @@ import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.gemwallet.android.data.repositories.assets.AssetsRepository -import com.gemwallet.android.data.repositories.stake.StakeRepository -import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService -import com.gemwallet.android.domains.asset.chain -import com.gemwallet.android.domains.asset.stakeChain -import com.gemwallet.android.domains.stake.hasRewards -import com.gemwallet.android.domains.stake.rewardsBalance -import com.gemwallet.android.domains.transaction.TransactionBalanceContext -import com.gemwallet.android.domains.transaction.balance -import com.gemwallet.android.ext.freezed +import com.gemwallet.android.features.transfer_amount.models.AmountError +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountDataProvider +import com.gemwallet.android.features.transfer_amount.viewmodels.providers.AmountProviderFactory import com.gemwallet.android.math.parseNumber import com.gemwallet.android.math.parseNumberOrNull import com.gemwallet.android.model.AmountParams -import com.gemwallet.android.model.AssetInfo import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Crypto import com.gemwallet.android.model.format import com.gemwallet.android.ui.models.AmountInputType -import com.gemwallet.android.features.transfer_amount.models.AmountError import com.wallet.core.primitives.Asset -import com.wallet.core.primitives.Chain import com.wallet.core.primitives.Currency -import com.wallet.core.primitives.Delegation -import com.wallet.core.primitives.Resource -import com.wallet.core.primitives.TransactionType import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import uniffi.gemstone.Config import java.math.BigInteger import java.math.MathContext import javax.inject.Inject @@ -59,401 +39,156 @@ import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class AmountViewModel @Inject constructor( - private val assetsRepository: AssetsRepository, - private val stakeRepository: StakeRepository, - private val transactionBalanceService: TransactionBalanceService, - savedStateHandle: SavedStateHandle + factory: AmountProviderFactory, + savedStateHandle: SavedStateHandle, ) : ViewModel() { - val params = MutableStateFlow(savedStateHandle.requireAmountParams()) + private val params: AmountParams = savedStateHandle.requireAmountParams() + val provider: AmountDataProvider = factory.create(params, viewModelScope) var amount by mutableStateOf("") private set - var amountInputType = MutableStateFlow(AmountInputType.Crypto) - - val errorUIState = MutableStateFlow(AmountError.None) - + val amountInputType = MutableStateFlow(AmountInputType.Crypto) + val amountError = MutableStateFlow(AmountError.None) private val maxAmount = MutableStateFlow(false) - val resource = MutableStateFlow(Resource.Bandwidth) - - val assetInfo = params.flatMapLatest { - assetsRepository.getAssetInfo(it.assetId).filterNotNull() - } - .flowOn(Dispatchers.IO) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val reserveForFee = combine(params, assetInfo.filterNotNull(), maxAmount) { params, assetInfo, maxAmount -> - if (!maxAmount) { - return@combine null - } - val value = getReserveForFee(params.txType, assetInfo.asset.chain) - if (value == BigInteger.ZERO) { - return@combine null - } - assetInfo.asset.format(value, decimalPlace = 4) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - - private val selectedValidatorId = MutableStateFlow(null) - - private val delegation: StateFlow = combine(params, assetInfo, selectedValidatorId) { params, assetInfo, selectedId -> - Triple(params, assetInfo, selectedId) - }.flatMapLatest { (params, assetInfo, selectedId) -> - when (params.txType) { - TransactionType.StakeUndelegate, - TransactionType.StakeRedelegate, - TransactionType.StakeWithdraw -> { - val validatorId = params.validatorId ?: return@flatMapLatest flowOf(null) - stakeRepository.getDelegation(validatorId, params.delegationId ?: "") - } - TransactionType.StakeRewards -> { - val owner = assetInfo?.owner?.address ?: return@flatMapLatest flowOf(null) - stakeRepository.getDelegations(assetInfo.asset.id, owner).map { list -> - val rewards = list.filter { it.hasRewards() } - rewards.firstOrNull { it.validator.id == selectedId } ?: rewards.firstOrNull() - } - } - else -> flowOf(null) - } - } - .flowOn(Dispatchers.IO) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - private val recommendedValidator = params.flatMapLatest { - stakeRepository.getRecommended(it.assetId.chain) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - private val srcValidator = combine(params, delegation, recommendedValidator) { params, delegation, recommended -> - when (params.txType) { - TransactionType.StakeWithdraw, - TransactionType.StakeUndelegate, - TransactionType.StakeRewards -> delegation?.validator - TransactionType.StakeDelegate, - TransactionType.StakeRedelegate -> recommended - else -> null - } - } - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - private val selectedValidator = combine(assetInfo, selectedValidatorId) { assetInfo, validatorId -> - val assetId = assetInfo?.asset?.id ?: return@combine null - validatorId ?: return@combine null - - stakeRepository.getStakeValidator(assetId, validatorId) - ?: stakeRepository.getRecommended(assetId.chain).firstOrNull() - } - .flowOn(Dispatchers.IO) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val validatorState = selectedValidator.combine(srcValidator) { selected, src -> - selected ?: src + val availableBalanceFormatted: StateFlow = combine( + provider.availableBalance, + provider.assetInfo, + ) { balance, current -> + current?.asset?.format(Crypto(balance), 8).orEmpty() + }.stateIn(viewModelScope, SharingStarted.Eagerly, "") + + val reserveForFeeFormatted: StateFlow = combine( + provider.assetInfo, + maxAmount, + ) { current, isMax -> + if (!provider.shouldReserveFee(isMax) || provider.reserveForFee.signum() == 0) null + else current?.asset?.format(Crypto(provider.reserveForFee), 4) }.stateIn(viewModelScope, SharingStarted.Eagerly, null) - private val balanceContext = combine(params, assetInfo, delegation, resource) { params, assetInfo, delegation, resource -> - BalanceRequest(params, assetInfo, delegation, resource) - } - .mapLatest { request -> - val assetInfo = request.assetInfo ?: return@mapLatest null - transactionBalanceService.getContext( - assetInfo = assetInfo, - params = request.params, - delegation = request.delegation, - resource = request.resource, - ) - } - .flowOn(Dispatchers.IO) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val availableBalance = combine(params, assetInfo, balanceContext) { params, assetInfo, balanceContext -> - assetInfo ?: return@combine "" - val value = Crypto( - assetInfo.balance( - txType = params.txType, - context = balanceContext ?: TransactionBalanceContext(), - ) - ) - assetInfo.asset.format(value, 8) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, "") - - var prefillAmount = combine( - params, - assetInfo, - delegation - ) { params, assetInfo, delegation -> - assetInfo ?: return@combine null - - when (params.txType) { - TransactionType.StakeWithdraw -> { - val balance = Crypto(delegation?.base?.balance?.toBigIntegerOrNull() ?: BigInteger.ZERO) - val value = balance.value(assetInfo.asset.decimals).stripTrailingZeros().toPlainString() - amount = value - value - } - TransactionType.StakeRewards -> { - val balance = Crypto(delegation?.rewardsBalance() ?: BigInteger.ZERO) - val value = balance.value(assetInfo.asset.decimals).stripTrailingZeros().toPlainString() - amount = value - value - } - else -> null - } - } - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val equivalentState = combine( + val amountEquivalent: StateFlow = combine( snapshotFlow { amount }, amountInputType, - assetInfo - ) { input, direction, assetInfo -> - val priceInfo = assetInfo?.price ?: return@combine "" - calcEquivalent( - inputAmount = input, - inputDirection = direction, - asset = assetInfo.asset, - price = priceInfo.price.price, - currency = priceInfo.currency - ) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, "") + provider.assetInfo, + ) { input, direction, current -> + val price = current?.price ?: return@combine "" + calculateEquivalent(input, direction, current.asset, price.price.price, price.currency) + }.stateIn(viewModelScope, SharingStarted.Eagerly, "") init { combine( snapshotFlow { amount }, amountInputType, - assetInfo, - params, - combine(delegation, resource) { d, r -> d to r }, - ) { input, inputType, asset, amountParams, (delegation, resource) -> - ValidationInputs(input, inputType, asset, amountParams, delegation, resource) + provider.assetInfo, + provider.availableBalance, + ) { input, type, current, balance -> + ValidationInputs(input, type, current?.asset, balance) } - .mapLatest { validate(it) } - .onEach { errorUIState.value = it } - .launchIn(viewModelScope) - } - - private suspend fun validate(inputs: ValidationInputs): AmountError { - if (inputs.amount.isEmpty()) return AmountError.None - if (inputs.amount.parseNumberOrNull()?.signum() == 0) return AmountError.None - val assetInfo = inputs.assetInfo ?: return AmountError.None - return try { - val asset = assetInfo.asset - val price = assetInfo.price?.price?.price ?: 0.0 - AmountValidation.validateAmount(asset, inputs.amount, getMinAmount(inputs.params.txType, asset.id.chain)) - val cryptoAmount = inputs.inputType.getAmount(inputs.amount, asset.decimals, price) - checkBalance(assetInfo, inputs.params, inputs.delegation, inputs.resource, cryptoAmount) - AmountError.None - } catch (err: Throwable) { - err as? AmountError ?: AmountError.None + .mapLatest { validate(it) } + .onEach { amountError.value = it } + .launchIn(viewModelScope) + + if (!provider.canChangeValue) { + provider.assetInfo.filterNotNull() + .combine(provider.availableBalance) { _, balance -> balance } + .onEach { onMaxAmount() } + .launchIn(viewModelScope) } } - fun setDelegatorValidator(validatorId: String?) { - selectedValidatorId.update { validatorId } - } - fun updateAmount(input: String, isMax: Boolean = false) { amount = input maxAmount.update { isMax } } fun onMaxAmount() = viewModelScope.launch { - val assetInfo = this@AmountViewModel.assetInfo.value ?: return@launch - val params = params.value - val txType = params.txType - val reserveForFee = getReserveForFee(txType = txType, assetInfo.asset.chain) - val baseBalance = transactionBalanceService.getBalance( - assetInfo = assetInfo, - params = params, - delegation = delegation.value, - resource = resource.value, - ) - - val balance = when (txType) { - TransactionType.EarnDeposit, - TransactionType.StakeDelegate -> if (assetInfo.stakeChain?.freezed() == true) { - Crypto(baseBalance) - } else { - Crypto(maxAmountAfterReserve(baseBalance, reserveForFee)) - } - TransactionType.StakeFreeze -> Crypto(maxAmountAfterReserve(baseBalance, reserveForFee)) - else -> Crypto(baseBalance) + val current = provider.assetInfo.value ?: return@launch + val balance = provider.availableBalance.value + val final = if (provider.shouldReserveFee(isMaxAmount = true)) { + (balance - provider.reserveForFee).max(BigInteger.ZERO) + } else { + balance } - - updateAmount(balance.value(assetInfo.asset.decimals).stripTrailingZeros().toPlainString(), true) + updateAmount(Crypto(final).value(current.asset.decimals).stripTrailingZeros().toPlainString(), isMax = true) } fun switchInputType() { - amountInputType.update { - when (it) { - AmountInputType.Crypto -> AmountInputType.Fiat - AmountInputType.Fiat -> AmountInputType.Crypto - } - } + amountInputType.update { if (it == AmountInputType.Crypto) AmountInputType.Fiat else AmountInputType.Crypto } amount = "" } - fun setResource(resource: Resource) { - this.resource.update { resource } - } - fun onNext(onConfirm: (ConfirmParams) -> Unit) { viewModelScope.launch { try { - onNext(params.value, amount, onConfirm) + val current = provider.assetInfo.value ?: return@launch + val asset = current.asset + AmountValidation.validateAmount(asset, amount, provider.minimumValue) + val price = current.price?.price?.price ?: 0.0 + val crypto = amountInputType.value.getAmount(amount, asset.decimals, price) + val available = provider.availableBalance.value.toBigDecimal().movePointLeft(asset.decimals) + AmountValidation.validateBalance(current, crypto, available) + amountError.value = AmountError.None + val isMax = maxAmount.value || crypto.atomicValue == provider.availableBalance.value + onConfirm(provider.buildConfirmParams(crypto, isMax)) + } catch (err: AmountError) { + amountError.value = err } catch (err: Throwable) { - when (err) { - is AmountError -> errorUIState.update { err } - else -> errorUIState.update { AmountError.Unknown(err.message ?: "Unknown error") } - } + amountError.value = AmountError.Unknown(err.message ?: "Unknown error") } } } - private suspend fun onNext( - params: AmountParams, - rawAmount: String, - onConfirm: (ConfirmParams) -> Unit - ) { - val assetInfo = assetInfo.value - val owner = assetInfo?.owner ?: return - val validator = validatorState.value - val delegation = delegation.value - val asset = assetInfo.asset - val decimals = asset.decimals - val price = assetInfo.price?.price?.price ?: 0.0 - val destination = params.destination - val memo = params.memo - val inputType = amountInputType.value - - val minimumValue = getMinAmount(params.txType, asset.id.chain) - AmountValidation.validateAmount(asset, rawAmount, minimumValue) - - val amount = inputType.getAmount(rawAmount, decimals, price) - val balance = checkBalance(assetInfo, params, delegation, resource.value, amount) - - errorUIState.update { AmountError.None } - - val isMax = maxAmount.value || amount.atomicValue == balance - val builder = ConfirmParams.Builder(asset, owner, amount.atomicValue, isMax) - val nextParams = when (params.txType) { - TransactionType.Transfer -> builder.transfer(destination!!, memo) - TransactionType.EarnDeposit, - TransactionType.StakeDelegate -> builder.delegate(validator ?: return) - TransactionType.StakeUndelegate -> builder.undelegate(delegation ?: return) - TransactionType.StakeRewards -> builder.rewards(listOfNotNull(validator)) - TransactionType.StakeRedelegate -> builder.redelegate(validator!!, delegation!!) - TransactionType.EarnWithdraw, - TransactionType.StakeWithdraw -> builder.withdraw(delegation!!) - TransactionType.AssetActivation -> builder.activate() - TransactionType.StakeFreeze -> builder.freeze(resource.value) - TransactionType.StakeUnfreeze -> builder.unfreeze(resource.value) - TransactionType.Swap, - TransactionType.TransferNFT, - TransactionType.SmartContractCall, - TransactionType.TokenApproval, - TransactionType.PerpetualOpenPosition, - TransactionType.PerpetualModifyPosition, - TransactionType.PerpetualClosePosition -> throw IllegalArgumentException() + private fun validate(inputs: ValidationInputs): AmountError { + if (inputs.amount.isEmpty()) return AmountError.None + if (inputs.amount.parseNumberOrNull()?.signum() == 0) return AmountError.None + val asset = inputs.asset ?: return AmountError.None + val current = provider.assetInfo.value ?: return AmountError.None + return try { + AmountValidation.validateAmount(asset, inputs.amount, provider.minimumValue) + val price = current.price?.price?.price ?: 0.0 + val crypto = inputs.inputType.getAmount(inputs.amount, asset.decimals, price) + val available = inputs.availableBalance.toBigDecimal().movePointLeft(asset.decimals) + AmountValidation.validateBalance(current, crypto, available) + AmountError.None + } catch (err: Throwable) { + err as? AmountError ?: AmountError.None } - onConfirm(nextParams) } - private fun calcEquivalent( - inputAmount: String, - inputDirection: AmountInputType, + private fun calculateEquivalent( + input: String, + direction: AmountInputType, asset: Asset, price: Double, - currency: Currency - ): String { - return try { - when (inputDirection) { - AmountInputType.Crypto -> { - AmountValidation.validateAmount(asset, inputAmount, BigInteger.ZERO) - val amount = inputAmount.parseNumber() - val decimals = asset.decimals - val unit = Crypto(amount, decimals).convert(decimals, price) - currency.format(unit.atomicValue) - } - AmountInputType.Fiat -> { - val value = inputAmount.parseNumber() - val crypto = value.divide(price.toBigDecimal(), MathContext.DECIMAL128) - AmountValidation.validateAmount(asset, crypto.toString(), BigInteger.ZERO) - asset.format(crypto, dynamicPlace = true) - } + currency: Currency, + ): String = try { + when (direction) { + AmountInputType.Crypto -> { + AmountValidation.validateAmount(asset, input, BigInteger.ZERO) + val parsed = input.parseNumber() + val unit = Crypto(parsed, asset.decimals).convert(asset.decimals, price) + currency.format(unit.atomicValue) } - } catch (_: Throwable) { - when (inputDirection) { - AmountInputType.Crypto -> { - currency.format(0.0) - } - AmountInputType.Fiat -> { - asset.format(Crypto(BigInteger.ZERO), dynamicPlace = true) - } + AmountInputType.Fiat -> { + val value = input.parseNumber() + val crypto = value.divide(price.toBigDecimal(), MathContext.DECIMAL128) + AmountValidation.validateAmount(asset, crypto.toString(), BigInteger.ZERO) + asset.format(crypto, dynamicPlace = true) } } - } - - private suspend fun checkBalance( - assetInfo: AssetInfo, - params: AmountParams, - delegation: Delegation?, - resource: Resource?, - amount: Crypto, - ): BigInteger { - val balance = transactionBalanceService.getBalance( - assetInfo = assetInfo, - params = params, - delegation = delegation, - resource = resource, - ) - val availableBalance = balance.toBigDecimal().movePointLeft(assetInfo.asset.decimals) - AmountValidation.validateBalance(assetInfo, amount, availableBalance) - return balance - } - - private fun getMinAmount(txType: TransactionType, chain: Chain): BigInteger { - return when (txType) { - TransactionType.StakeRedelegate, - TransactionType.StakeFreeze, - TransactionType.StakeDelegate -> BigInteger.valueOf( - Config().getStakeConfig(chain.string).minAmount.toLong() - ) - else -> BigInteger.ZERO + } catch (_: Throwable) { + when (direction) { + AmountInputType.Crypto -> currency.format(0.0) + AmountInputType.Fiat -> asset.format(Crypto(BigInteger.ZERO), dynamicPlace = true) } } - - companion object { - fun maxAmountAfterReserve(balance: BigInteger, reserve: BigInteger): BigInteger = - maxOf(balance - reserve, BigInteger.ZERO) - } - - private fun getReserveForFee(txType: TransactionType, chain: Chain) = when (txType) { - TransactionType.StakeFreeze -> BigInteger.valueOf(Config().getStakeConfig(chain.string).reservedForFees.toLong()) - TransactionType.StakeDelegate -> when (chain) { - Chain.Tron -> BigInteger.ZERO - else -> BigInteger.valueOf(Config().getStakeConfig(chain.string).reservedForFees.toLong()) - } - else -> BigInteger.ZERO - } } -private data class BalanceRequest( - val params: AmountParams, - val assetInfo: AssetInfo?, - val delegation: Delegation?, - val resource: Resource, -) - private data class ValidationInputs( val amount: String, val inputType: AmountInputType, - val assetInfo: AssetInfo?, - val params: AmountParams, - val delegation: Delegation?, - val resource: Resource, + val asset: Asset?, + val availableBalance: BigInteger, ) diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/PerpetualAmountViewModel.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/PerpetualAmountViewModel.kt deleted file mode 100644 index 3334c507e..000000000 --- a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/PerpetualAmountViewModel.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.viewmodels - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import com.gemwallet.android.application.perpetual.coordinators.GetPerpetual -import com.gemwallet.android.application.perpetual.coordinators.GetPerpetualBalance -import com.gemwallet.android.data.repositories.assets.AssetsRepository -import com.gemwallet.android.data.repositories.session.SessionRepository -import com.gemwallet.android.data.repositories.tokens.TokensRepository -import com.gemwallet.android.domains.asset.chain -import com.gemwallet.android.ext.getAccount -import com.gemwallet.android.ext.walletId -import com.gemwallet.android.model.AmountParams -import com.gemwallet.android.model.AssetInfo -import com.gemwallet.android.model.ConfirmParams -import com.gemwallet.android.model.format -import com.gemwallet.android.features.transfer_amount.models.AmountError -import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.Chain -import com.wallet.core.primitives.Currency -import com.wallet.core.primitives.PerpetualDirection -import com.wallet.core.primitives.TransactionType -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import java.math.BigDecimal -import java.math.BigInteger -import javax.inject.Inject -import kotlin.math.min - -@OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class PerpetualAmountViewModel @Inject constructor( - sessionRepository: SessionRepository, - assetsRepository: AssetsRepository, - tokenRepository: TokensRepository, - getPerpetual: GetPerpetual, - getPerpetualBalance: GetPerpetualBalance, - savedStateHandle: SavedStateHandle -) : AmountBaseViewModel(savedStateHandle) { - - val perpetual = params - .map { it.perpetualId } - .filterNotNull() - .flatMapLatest { getPerpetual.getPerpetual(it) } - .onEach { - val session = sessionRepository.session().firstOrNull() ?: return@onEach - val assetId = getAssetId(it?.asset?.chain ?: return@onEach) - tokenRepository.search(assetId, session.currency) - session.wallet.getAccount(assetId.chain) ?: return@onEach - assetsRepository.switchVisibility(session.wallet.walletId, assetId, false) - } - .onEach { perpetual -> leverage.update { min(perpetual?.maxLeverage ?: 0, 5) } } - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val availableLeverages = perpetual.filterNotNull().map { - val list = mutableListOf() - val minLeverage = min(it.maxLeverage, 5) - for (i in minLeverage .. it.maxLeverage step 5) { - list.add(i) - } - list - }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - - val leverage = MutableStateFlow(0) - - override val assetInfo: StateFlow = perpetual /// TODO: ??? - .filterNotNull() - .flatMapLatest { - val assetId = getAssetId(it.asset.chain) - assetsRepository.getAssetInfo(assetId) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - override val availableBalance: StateFlow = combine( - sessionRepository.session().filterNotNull(), - perpetual.filterNotNull() - ) { session, perpetual -> - val chain = perpetual.asset.id.chain - session.wallet.getAccount(chain) - } - .filterNotNull() - .flatMapLatest { account -> - getPerpetualBalance.getBalance(account.chain, account.address) - } - .mapLatest { it?.available?.toBigDecimal() ?: BigDecimal.ZERO } - .stateIn(viewModelScope, SharingStarted.Eagerly, BigDecimal.ZERO) - - override val availableBalanceFormatted: StateFlow = availableBalance - .mapLatest { Currency.USD.format(it.toDouble()) } - .stateIn(viewModelScope, SharingStarted.Eagerly, "") - - override fun onMaxAmount() { - updateAmount(availableBalance.value.toString(), true) - } - - override fun onNext( - params: AmountParams, - rawAmount: String, - onConfirm: (ConfirmParams) -> Unit - ) { - val assetInfo = assetInfo.value - val owner = assetInfo?.owner ?: return - val perpetual = perpetual.value ?: return - val asset = assetInfo.asset - val decimals = asset.decimals - val price = assetInfo.price?.price?.price ?: 0.0 - val inputType = amountInputType.value - AmountValidation.validateAmount(asset, rawAmount, BigInteger.ZERO) - - val amount = inputType.getAmount(rawAmount, decimals, price) - AmountValidation.validateBalance(assetInfo, amount, availableBalance.value) - - amountError.update { AmountError.None } - - val builder = ConfirmParams.Builder(asset, owner, amount.atomicValue, maxAmount.value) - val nextParams = when (params.txType) { - TransactionType.PerpetualOpenPosition -> builder.perpetualOrder( - perpetualId = perpetual.id, - perpetualPrice = perpetual.price, - perpetualProvider = perpetual.provider, - perpetualIdentifier = perpetual.identifier, - action = ConfirmParams.PerpetualParams.OrderAction.Open, - leverage = leverage.value, - baseAsset = asset, // USD - direction = params.perpetualDirection ?: PerpetualDirection.Long, - marginType = perpetual.marginType, - ) - else -> throw IllegalArgumentException() - } - onConfirm(nextParams) - } - - private fun getAssetId(chain: Chain): AssetId { // TODO: How to get asset info for perpetual asset + perpetual base asset - return when (chain) { - Chain.HyperCore -> AssetId(chain = chain, tokenId = "perpetual::USDC") - else -> throw IllegalArgumentException() - } - } - - fun setLeverage(value: Int) { - leverage.update { value } - } -} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountDataProvider.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountDataProvider.kt new file mode 100644 index 000000000..6b2c632a9 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountDataProvider.kt @@ -0,0 +1,23 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AssetInfo +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import kotlinx.coroutines.flow.StateFlow +import java.math.BigInteger + +sealed interface AmountDataProvider { + val title: AmountTitle + val canChangeValue: Boolean + val showsAssetBalance: Boolean get() = canChangeValue + val canSwitchInputType: Boolean + val minimumValue: BigInteger + val reserveForFee: BigInteger + + val assetInfo: StateFlow + val availableBalance: StateFlow + + fun shouldReserveFee(isMaxAmount: Boolean): Boolean + suspend fun buildConfirmParams(amount: Crypto, isMax: Boolean): ConfirmParams +} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountFreezeProvider.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountFreezeProvider.kt new file mode 100644 index 000000000..1391d8d73 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountFreezeProvider.kt @@ -0,0 +1,90 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.AssetInfo +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import com.wallet.core.primitives.Resource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import uniffi.gemstone.Config +import java.math.BigInteger + +@OptIn(ExperimentalCoroutinesApi::class) +class AmountFreezeProvider( + private val params: AmountParams.Freeze, + assetsRepository: AssetsRepository, + private val transactionBalanceService: TransactionBalanceService, + scope: CoroutineScope, +) : AmountDataProvider { + + override val title: AmountTitle = AmountTitle.Freeze(params.direction) + override val canChangeValue: Boolean = true + override val canSwitchInputType: Boolean = false + + override val minimumValue: BigInteger + get() = when (params.direction) { + AmountParams.Freeze.Direction.Freeze -> + BigInteger.valueOf(Config().getStakeConfig(params.assetId.chain.string).minAmount.toLong()) + AmountParams.Freeze.Direction.Unfreeze -> BigInteger.ZERO + } + + override val reserveForFee: BigInteger + get() = when (params.direction) { + AmountParams.Freeze.Direction.Freeze -> + BigInteger.valueOf(Config().getStakeConfig(params.assetId.chain.string).reservedForFees.toLong()) + AmountParams.Freeze.Direction.Unfreeze -> BigInteger.ZERO + } + + private val selectedResource = MutableStateFlow(Resource.Bandwidth) + val resource: StateFlow = selectedResource.asStateFlow() + + fun setResource(value: Resource) { + selectedResource.update { value } + } + + override val assetInfo: StateFlow = + assetsRepository.getAssetInfo(params.assetId) + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, null) + + override val availableBalance: StateFlow = + combine(assetInfo.filterNotNull(), selectedResource) { current, currentResource -> current to currentResource } + .mapLatest { (current, currentResource) -> + transactionBalanceService.getBalance(current, params, resource = currentResource) + } + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, BigInteger.ZERO) + + override fun shouldReserveFee(isMaxAmount: Boolean): Boolean = when (params.direction) { + AmountParams.Freeze.Direction.Freeze -> { + if (!isMaxAmount || reserveForFee.signum() == 0) false + else (availableBalance.value - reserveForFee).max(BigInteger.ZERO) > minimumValue + } + AmountParams.Freeze.Direction.Unfreeze -> false + } + + override suspend fun buildConfirmParams(amount: Crypto, isMax: Boolean): ConfirmParams { + val current = assetInfo.value ?: error("assetInfo not loaded") + val owner = current.owner ?: error("owner missing") + val builder = ConfirmParams.Builder(current.asset, owner, amount.atomicValue, isMax) + return when (params.direction) { + AmountParams.Freeze.Direction.Freeze -> builder.freeze(selectedResource.value) + AmountParams.Freeze.Direction.Unfreeze -> builder.unfreeze(selectedResource.value) + } + } +} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountPerpetualProvider.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountPerpetualProvider.kt new file mode 100644 index 000000000..9f9e08860 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountPerpetualProvider.kt @@ -0,0 +1,112 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetual +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetualBalance +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.session.SessionRepository +import com.gemwallet.android.data.repositories.tokens.TokensRepository +import com.gemwallet.android.domains.asset.chain +import com.gemwallet.android.ext.getAccount +import com.gemwallet.android.ext.walletId +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.AssetInfo +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import java.math.BigInteger +import kotlin.math.min + +@OptIn(ExperimentalCoroutinesApi::class) +class AmountPerpetualProvider( + private val params: AmountParams.Perpetual, + assetsRepository: AssetsRepository, + tokenRepository: TokensRepository, + sessionRepository: SessionRepository, + getPerpetual: GetPerpetual, + getPerpetualBalance: GetPerpetualBalance, + scope: CoroutineScope, +) : AmountDataProvider { + + override val title: AmountTitle = AmountTitle.Perpetual(params.direction) + override val canChangeValue: Boolean = true + override val canSwitchInputType: Boolean = false + override val minimumValue: BigInteger = BigInteger.ZERO + override val reserveForFee: BigInteger = BigInteger.ZERO + + private val perpetual = getPerpetual.getPerpetual(params.perpetualId) + .onEach { current -> + val session = sessionRepository.session().firstOrNull() ?: return@onEach + val perpetualAssetId = perpetualUsdcAssetId(current?.asset?.chain ?: return@onEach) + tokenRepository.search(perpetualAssetId, session.currency) + session.wallet.getAccount(perpetualAssetId.chain) ?: return@onEach + assetsRepository.switchVisibility(session.wallet.walletId, perpetualAssetId, false) + } + .onEach { current -> selectedLeverage.update { min(current?.maxLeverage ?: 0, 5) } } + .stateIn(scope, SharingStarted.Eagerly, null) + + private val selectedLeverage = MutableStateFlow(0) + val leverage: StateFlow = selectedLeverage.asStateFlow() + fun setLeverage(value: Int) { selectedLeverage.update { value } } + + val availableLeverages: StateFlow> = perpetual.filterNotNull().map { current -> + val minLeverage = min(current.maxLeverage, 5) + (minLeverage..current.maxLeverage step 5).toList() + }.stateIn(scope, SharingStarted.Eagerly, emptyList()) + + override val assetInfo: StateFlow = perpetual.filterNotNull() + .flatMapLatest { current -> assetsRepository.getAssetInfo(perpetualUsdcAssetId(current.asset.id.chain)) } + .stateIn(scope, SharingStarted.Eagerly, null) + + override val availableBalance: StateFlow = combine( + sessionRepository.session().filterNotNull(), + perpetual.filterNotNull(), + ) { session, current -> session.wallet.getAccount(current.asset.id.chain) } + .filterNotNull() + .flatMapLatest { account -> getPerpetualBalance.getBalance(account.chain, account.address) } + .combine(assetInfo.filterNotNull()) { balance, current -> + val available = balance?.available ?: 0.0 + Crypto(available.toBigDecimal(), current.asset.decimals).atomicValue + } + .stateIn(scope, SharingStarted.Eagerly, BigInteger.ZERO) + + override fun shouldReserveFee(isMaxAmount: Boolean): Boolean = false + + override suspend fun buildConfirmParams(amount: Crypto, isMax: Boolean): ConfirmParams { + val current = assetInfo.value ?: error("assetInfo not loaded") + val owner = current.owner ?: error("owner missing") + val currentPerpetual = perpetual.value ?: error("perpetual not loaded") + val builder = ConfirmParams.Builder(current.asset, owner, amount.atomicValue, isMax) + return builder.perpetualOrder( + perpetualId = currentPerpetual.id, + perpetualPrice = currentPerpetual.price, + perpetualProvider = currentPerpetual.provider, + perpetualIdentifier = currentPerpetual.identifier, + action = ConfirmParams.PerpetualParams.OrderAction.Open, + leverage = selectedLeverage.value, + baseAsset = current.asset, + direction = params.direction, + marginType = currentPerpetual.marginType, + ) + } + + private fun perpetualUsdcAssetId(chain: Chain): AssetId = when (chain) { + Chain.HyperCore -> AssetId(chain = chain, tokenId = "perpetual::USDC") + else -> error("Unsupported perpetual chain: $chain") + } +} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountProviderFactory.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountProviderFactory.kt new file mode 100644 index 000000000..c67cde8b5 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountProviderFactory.kt @@ -0,0 +1,53 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetual +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetualBalance +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.session.SessionRepository +import com.gemwallet.android.data.repositories.stake.StakeRepository +import com.gemwallet.android.data.repositories.tokens.TokensRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.model.AmountParams +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject + +class AmountProviderFactory @Inject constructor( + private val assetsRepository: AssetsRepository, + private val stakeRepository: StakeRepository, + private val transactionBalanceService: TransactionBalanceService, + private val getPerpetual: GetPerpetual, + private val getPerpetualBalance: GetPerpetualBalance, + private val sessionRepository: SessionRepository, + private val tokenRepository: TokensRepository, +) { + fun create(params: AmountParams, scope: CoroutineScope): AmountDataProvider = when (params) { + is AmountParams.Transfer -> AmountTransferProvider( + params = params, + assetsRepository = assetsRepository, + transactionBalanceService = transactionBalanceService, + scope = scope, + ) + is AmountParams.Stake -> AmountStakeProvider( + params = params, + assetsRepository = assetsRepository, + stakeRepository = stakeRepository, + transactionBalanceService = transactionBalanceService, + scope = scope, + ) + is AmountParams.Freeze -> AmountFreezeProvider( + params = params, + assetsRepository = assetsRepository, + transactionBalanceService = transactionBalanceService, + scope = scope, + ) + is AmountParams.Perpetual -> AmountPerpetualProvider( + params = params, + assetsRepository = assetsRepository, + tokenRepository = tokenRepository, + sessionRepository = sessionRepository, + getPerpetual = getPerpetual, + getPerpetualBalance = getPerpetualBalance, + scope = scope, + ) + } +} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountStakeProvider.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountStakeProvider.kt new file mode 100644 index 000000000..21ca509d2 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountStakeProvider.kt @@ -0,0 +1,200 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.stake.StakeRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.domains.stake.hasRewards +import com.gemwallet.android.ext.byChain +import com.gemwallet.android.ext.freezed +import com.gemwallet.android.features.transfer_amount.models.AmountError +import com.gemwallet.android.features.transfer_amount.models.ValidatorsSource +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.AssetInfo +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import com.wallet.core.primitives.Delegation +import com.wallet.core.primitives.DelegationValidator +import com.wallet.core.primitives.StakeChain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import uniffi.gemstone.Config +import java.math.BigInteger + +@OptIn(ExperimentalCoroutinesApi::class) +class AmountStakeProvider( + private val params: AmountParams.Stake, + assetsRepository: AssetsRepository, + private val stakeRepository: StakeRepository, + private val transactionBalanceService: TransactionBalanceService, + scope: CoroutineScope, +) : AmountDataProvider { + + override val title: AmountTitle = AmountTitle.Stake(params) + override val canSwitchInputType: Boolean = false + + override val canChangeValue: Boolean = when (params) { + is AmountParams.Stake.Delegate, + is AmountParams.Stake.Redelegate, + is AmountParams.Stake.Undelegate -> true + is AmountParams.Stake.Withdraw, + is AmountParams.Stake.Rewards -> false + } + + override val showsAssetBalance: Boolean = when (params) { + is AmountParams.Stake.Rewards -> true + else -> canChangeValue + } + + override val minimumValue: BigInteger + get() = when (params) { + is AmountParams.Stake.Delegate, + is AmountParams.Stake.Redelegate -> + BigInteger.valueOf(Config().getStakeConfig(params.assetId.chain.string).minAmount.toLong()) + is AmountParams.Stake.Undelegate, + is AmountParams.Stake.Withdraw, + is AmountParams.Stake.Rewards -> BigInteger.ZERO + } + + override val reserveForFee: BigInteger + get() = when (params) { + is AmountParams.Stake.Delegate -> when (StakeChain.byChain(params.assetId.chain)?.freezed()) { + true -> BigInteger.ZERO + else -> BigInteger.valueOf(Config().getStakeConfig(params.assetId.chain.string).reservedForFees.toLong()) + } + is AmountParams.Stake.Undelegate, + is AmountParams.Stake.Redelegate, + is AmountParams.Stake.Withdraw, + is AmountParams.Stake.Rewards -> BigInteger.ZERO + } + + override val assetInfo: StateFlow = + assetsRepository.getAssetInfo(params.assetId) + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, null) + + private val selectedValidatorId = MutableStateFlow(initialValidatorId(params)) + + private val delegation: StateFlow = run { + val source = when (params) { + is AmountParams.Stake.Undelegate -> + stakeRepository.getDelegation(validatorId = params.validatorId, delegationId = params.delegationId) + is AmountParams.Stake.Redelegate -> + stakeRepository.getDelegation(validatorId = params.validatorId, delegationId = params.delegationId) + is AmountParams.Stake.Withdraw -> + stakeRepository.getDelegation(validatorId = params.validatorId, delegationId = params.delegationId) + is AmountParams.Stake.Rewards -> { + val rewardsList = assetInfo.filterNotNull().flatMapLatest { current -> + val owner = current.owner?.address ?: return@flatMapLatest flowOf>(emptyList()) + stakeRepository.getDelegations(current.asset.id, owner) + } + combine(rewardsList, selectedValidatorId) { delegations, pickedId -> + val withRewards = delegations.filter { it.hasRewards() } + withRewards.firstOrNull { it.validator.id == pickedId } ?: withRewards.firstOrNull() + } + } + else -> flowOf(null) + } + source.flowOn(Dispatchers.IO).stateIn(scope, SharingStarted.Eagerly, null) + } + + private val recommendedValidator: StateFlow = when (params) { + is AmountParams.Stake.Delegate, + is AmountParams.Stake.Redelegate -> stakeRepository.getRecommended(params.assetId.chain) + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, null) + else -> MutableStateFlow(null) + } + + val validatorState: StateFlow = + combine(assetInfo, delegation, selectedValidatorId, recommendedValidator) { current, currentDelegation, pickedId, recommended -> + val byId = if (current != null && pickedId != null) { + stakeRepository.getStakeValidator(current.asset.id, pickedId) + } else { + null + } + byId ?: currentDelegation?.validator ?: recommended + }.flowOn(Dispatchers.IO).stateIn(scope, SharingStarted.Eagerly, null) + + val validatorSource: StateFlow = assetInfo.mapLatest { current -> + when (params) { + is AmountParams.Stake.Rewards -> + current?.owner?.address?.let { ValidatorsSource.Rewards(params.assetId, it) } + else -> ValidatorsSource.ChainValidators(chain = params.assetId.chain) + } + }.stateIn(scope, SharingStarted.Eagerly, null) + + val canSelectValidator: Boolean = when (params) { + is AmountParams.Stake.Delegate, + is AmountParams.Stake.Redelegate, + is AmountParams.Stake.Rewards -> true + is AmountParams.Stake.Undelegate, + is AmountParams.Stake.Withdraw -> false + } + + fun selectValidator(id: String?) { + selectedValidatorId.update { id } + } + + override val availableBalance: StateFlow = + combine(assetInfo.filterNotNull(), delegation) { current, currentDelegation -> current to currentDelegation } + .mapLatest { (current, currentDelegation) -> + transactionBalanceService.getBalance(current, params, delegation = currentDelegation) + } + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, BigInteger.ZERO) + + override fun shouldReserveFee(isMaxAmount: Boolean): Boolean { + if (!isMaxAmount || reserveForFee.signum() == 0) return false + if (params !is AmountParams.Stake.Delegate) return false + val maxAfterFee = (availableBalance.value - reserveForFee).max(BigInteger.ZERO) + return maxAfterFee > minimumValue + } + + override suspend fun buildConfirmParams(amount: Crypto, isMax: Boolean): ConfirmParams { + val current = assetInfo.value ?: error("assetInfo not loaded") + val owner = current.owner ?: error("owner missing") + val builder = ConfirmParams.Builder(current.asset, owner, amount.atomicValue, isMax) + return when (params) { + is AmountParams.Stake.Delegate -> { + val validator = validatorState.value ?: throw AmountError.NoValidatorSelected + builder.delegate(validator) + } + is AmountParams.Stake.Redelegate -> { + val validator = validatorState.value ?: throw AmountError.NoValidatorSelected + val currentDelegation = delegation.value ?: throw AmountError.NoDelegationSelected + builder.redelegate(validator, currentDelegation) + } + is AmountParams.Stake.Undelegate -> { + val currentDelegation = delegation.value ?: throw AmountError.NoDelegationSelected + builder.undelegate(currentDelegation) + } + is AmountParams.Stake.Withdraw -> { + val currentDelegation = delegation.value ?: throw AmountError.NoDelegationSelected + builder.withdraw(currentDelegation) + } + is AmountParams.Stake.Rewards -> { + val validator = validatorState.value ?: throw AmountError.NoValidatorSelected + builder.rewards(listOf(validator)) + } + } + } + + private fun initialValidatorId(params: AmountParams.Stake): String? = when (params) { + is AmountParams.Stake.Delegate -> params.validatorId + is AmountParams.Stake.Redelegate -> params.validatorId + else -> null + } +} diff --git a/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountTransferProvider.kt b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountTransferProvider.kt new file mode 100644 index 000000000..7fe4b3587 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/main/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountTransferProvider.kt @@ -0,0 +1,54 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.AssetInfo +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import java.math.BigInteger + +@OptIn(ExperimentalCoroutinesApi::class) +class AmountTransferProvider( + private val params: AmountParams.Transfer, + assetsRepository: AssetsRepository, + private val transactionBalanceService: TransactionBalanceService, + scope: CoroutineScope, +) : AmountDataProvider { + + override val title: AmountTitle = AmountTitle.Send + override val canChangeValue: Boolean = true + override val canSwitchInputType: Boolean = true + override val minimumValue: BigInteger = BigInteger.ZERO + override val reserveForFee: BigInteger = BigInteger.ZERO + + override val assetInfo: StateFlow = + assetsRepository.getAssetInfo(params.assetId) + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, null) + + override val availableBalance: StateFlow = + assetInfo.filterNotNull() + .mapLatest { current -> transactionBalanceService.getBalance(current, params) } + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, BigInteger.ZERO) + + override fun shouldReserveFee(isMaxAmount: Boolean): Boolean = false + + override suspend fun buildConfirmParams(amount: Crypto, isMax: Boolean): ConfirmParams { + val current = assetInfo.value ?: error("assetInfo not loaded") + val owner = current.owner ?: error("owner missing") + return ConfirmParams.Builder(current.asset, owner, amount.atomicValue, isMax) + .transfer(params.destination, params.memo) + } +} diff --git a/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountViewModelTest.kt b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountViewModelTest.kt deleted file mode 100644 index 91bdc40ff..000000000 --- a/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/AmountViewModelTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.gemwallet.android.features.transfer_amount.viewmodels - -import org.junit.Assert.assertEquals -import org.junit.Test -import java.math.BigInteger - -class AmountViewModelTest { - - @Test - fun maxAmountAfterReserveReturnsZeroWhenReserveExceedsBalance() { - val balance = BigInteger.valueOf(100) - val reserve = BigInteger.valueOf(500) - - val result = AmountViewModel.maxAmountAfterReserve(balance, reserve) - - assertEquals(BigInteger.ZERO, result) - } - - @Test - fun maxAmountAfterReserveReturnsRemainderWhenBalanceSufficient() { - val balance = BigInteger.valueOf(1000) - val reserve = BigInteger.valueOf(300) - - val result = AmountViewModel.maxAmountAfterReserve(balance, reserve) - - assertEquals(BigInteger.valueOf(700), result) - } - - @Test - fun maxAmountAfterReserveReturnsZeroWhenEqual() { - val balance = BigInteger.valueOf(500) - val reserve = BigInteger.valueOf(500) - - val result = AmountViewModel.maxAmountAfterReserve(balance, reserve) - - assertEquals(BigInteger.ZERO, result) - } -} diff --git a/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountFreezeProviderTest.kt b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountFreezeProviderTest.kt new file mode 100644 index 000000000..4f74d9290 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountFreezeProviderTest.kt @@ -0,0 +1,70 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import com.gemwallet.android.testkit.mockAssetCosmos +import com.gemwallet.android.testkit.mockAssetInfo +import com.wallet.core.primitives.Resource +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.math.BigInteger + +class AmountFreezeProviderTest { + + private val asset = mockAssetCosmos() + private val assetInfo = mockAssetInfo(asset = asset) + private val assetsRepository = mockk { + every { getAssetInfo(asset.id) } returns flowOf(assetInfo) + } + private val balanceService = mockk { + coEvery { getBalance(any(), any(), any(), any()) } returns BigInteger("1000") + } + private val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + private fun makeProvider(direction: AmountParams.Freeze.Direction) = AmountFreezeProvider( + params = AmountParams.Freeze(asset.id, direction), + assetsRepository = assetsRepository, + transactionBalanceService = balanceService, + scope = scope, + ) + + @Test + fun `freeze builds Stake Freeze ConfirmParams with selected resource`() = runBlocking { + val provider = makeProvider(AmountParams.Freeze.Direction.Freeze) + provider.assetInfo.filterNotNull().first() + provider.setResource(Resource.Bandwidth) + val confirm = provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.Stake.Freeze) + assertEquals(Resource.Bandwidth, (confirm as ConfirmParams.Stake.Freeze).resource) + } + + @Test + fun `unfreeze builds Stake Unfreeze ConfirmParams`() = runBlocking { + val provider = makeProvider(AmountParams.Freeze.Direction.Unfreeze) + provider.assetInfo.filterNotNull().first() + provider.setResource(Resource.Energy) + val confirm = provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.Stake.Unfreeze) + } + + @Test + fun `unfreeze direction has zero minimum and zero reserve`() { + val provider = makeProvider(AmountParams.Freeze.Direction.Unfreeze) + assertEquals(BigInteger.ZERO, provider.minimumValue) + assertEquals(BigInteger.ZERO, provider.reserveForFee) + } +} diff --git a/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountPerpetualProviderTest.kt b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountPerpetualProviderTest.kt new file mode 100644 index 000000000..98ce92aa7 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountPerpetualProviderTest.kt @@ -0,0 +1,63 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetual +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetualBalance +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.session.SessionRepository +import com.gemwallet.android.data.repositories.tokens.TokensRepository +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.testkit.mockAssetCosmos +import com.wallet.core.primitives.PerpetualDirection +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import org.junit.Assert.assertEquals +import org.junit.Test + +class AmountPerpetualProviderTest { + + @Test + fun `setLeverage updates the leverage flow`() { + val provider = makeProvider() + provider.setLeverage(10) + assertEquals(10, provider.leverage.value) + } + + @Test + fun `title carries the direction`() { + val provider = makeProvider(direction = PerpetualDirection.Short) + val title = provider.title as AmountTitle.Perpetual + assertEquals(PerpetualDirection.Short, title.direction) + } + + private fun makeProvider(direction: PerpetualDirection = PerpetualDirection.Long): AmountPerpetualProvider { + val asset = mockAssetCosmos() + val assetsRepository = mockk(relaxed = true) { + every { getAssetInfo(any()) } returns flowOf(null) + } + val sessionRepository = mockk(relaxed = true) { + every { session() } returns MutableStateFlow(null) + } + val tokenRepository = mockk(relaxed = true) + val getPerpetual = mockk(relaxed = true) { + every { getPerpetual(any()) } returns flowOf(null) + } + val getPerpetualBalance = mockk(relaxed = true) { + every { getBalance(any(), any()) } returns flowOf(null) + } + return AmountPerpetualProvider( + params = AmountParams.Perpetual(asset.id, "BTC-PERP", direction), + assetsRepository = assetsRepository, + tokenRepository = tokenRepository, + sessionRepository = sessionRepository, + getPerpetual = getPerpetual, + getPerpetualBalance = getPerpetualBalance, + scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()), + ) + } +} diff --git a/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountProviderFactoryTest.kt b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountProviderFactoryTest.kt new file mode 100644 index 000000000..54c8cd0e4 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountProviderFactoryTest.kt @@ -0,0 +1,79 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetual +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetualBalance +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.session.SessionRepository +import com.gemwallet.android.data.repositories.stake.StakeRepository +import com.gemwallet.android.data.repositories.tokens.TokensRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.testkit.mockAssetCosmos +import com.wallet.core.primitives.PerpetualDirection +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import org.junit.Assert.assertTrue +import org.junit.Test + +class AmountProviderFactoryTest { + + private val asset = mockAssetCosmos() + private val factory = AmountProviderFactory( + assetsRepository = mockk(relaxed = true) { + every { getAssetInfo(any()) } returns flowOf(null) + }, + stakeRepository = mockk(relaxed = true), + transactionBalanceService = mockk(relaxed = true), + getPerpetual = mockk(relaxed = true) { + every { getPerpetual(any()) } returns flowOf(null) + }, + getPerpetualBalance = mockk(relaxed = true) { + every { getBalance(any(), any()) } returns flowOf(null) + }, + sessionRepository = mockk(relaxed = true) { + every { session() } returns MutableStateFlow(null) + }, + tokenRepository = mockk(relaxed = true), + ) + private val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + @Test + fun `Transfer params produce TransferProvider`() { + val provider = factory.create( + AmountParams.Transfer(asset.id, DestinationAddress("to", null), null), + scope, + ) + assertTrue(provider is AmountTransferProvider) + } + + @Test + fun `Stake variants produce StakeProvider`() { + assertTrue(factory.create(AmountParams.Stake.Delegate(asset.id), scope) is AmountStakeProvider) + assertTrue(factory.create(AmountParams.Stake.Rewards(asset.id), scope) is AmountStakeProvider) + assertTrue(factory.create(AmountParams.Stake.Withdraw(asset.id, "v1", "d1"), scope) is AmountStakeProvider) + } + + @Test + fun `Freeze params produce FreezeProvider`() { + val provider = factory.create( + AmountParams.Freeze(asset.id, AmountParams.Freeze.Direction.Freeze), + scope, + ) + assertTrue(provider is AmountFreezeProvider) + } + + @Test + fun `Perpetual params produce PerpetualProvider`() { + val provider = factory.create( + AmountParams.Perpetual(asset.id, "BTC-PERP", PerpetualDirection.Long), + scope, + ) + assertTrue(provider is AmountPerpetualProvider) + } +} diff --git a/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountStakeProviderTest.kt b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountStakeProviderTest.kt new file mode 100644 index 000000000..470a05e24 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountStakeProviderTest.kt @@ -0,0 +1,134 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.stake.StakeRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.features.transfer_amount.models.AmountError +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import com.gemwallet.android.testkit.mockAssetCosmos +import com.gemwallet.android.testkit.mockAssetInfo +import com.gemwallet.android.testkit.mockDelegation +import com.gemwallet.android.testkit.mockDelegationValidator +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test +import java.math.BigInteger + +class AmountStakeProviderTest { + + private val asset = mockAssetCosmos() + private val assetInfo = mockAssetInfo(asset = asset) + private val validator = mockDelegationValidator(chain = asset.id.chain, id = "v1") + private val delegation = mockDelegation( + assetId = asset.id, + balance = "100", + rewards = "5", + validatorId = "v1", + delegationId = "d1", + ) + + private val assetsRepository = mockk { + every { getAssetInfo(asset.id) } returns flowOf(assetInfo) + } + private val stakeRepository = mockk { + every { getDelegation(any(), any()) } returns flowOf(delegation) + coEvery { getStakeValidator(asset.id, "v1") } returns validator + every { getDelegations(any(), any()) } returns flowOf(listOf(delegation)) + every { getRecommended(any()) } returns flowOf(null) + } + private val balanceService = mockk { + coEvery { getBalance(any(), any(), any(), any()) } returns BigInteger("100") + } + private val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + private fun makeProvider(params: AmountParams.Stake) = AmountStakeProvider( + params = params, + assetsRepository = assetsRepository, + stakeRepository = stakeRepository, + transactionBalanceService = balanceService, + scope = scope, + ) + + @Test + fun `delegate builds DelegateParams`() = runBlocking { + val provider = makeProvider(AmountParams.Stake.Delegate(asset.id, validatorId = "v1")) + provider.validatorState.filterNotNull().first() + val confirm = provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.Stake.DelegateParams) + } + + @Test + fun `delegate without validator throws NoValidatorSelected`() = runBlocking { + coEvery { stakeRepository.getStakeValidator(any(), any()) } returns null + every { stakeRepository.getDelegation(any(), any()) } returns flowOf(null) + val provider = makeProvider(AmountParams.Stake.Delegate(asset.id, validatorId = null)) + provider.assetInfo.filterNotNull().first() + assertThrows(AmountError.NoValidatorSelected::class.java) { + runBlocking { provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) } + } + Unit + } + + @Test + fun `undelegate builds UndelegateParams`() = runBlocking { + val provider = makeProvider(AmountParams.Stake.Undelegate(asset.id, validatorId = "v1", delegationId = "d1")) + provider.assetInfo.filterNotNull().first() + val confirm = provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.Stake.UndelegateParams) + } + + @Test + fun `redelegate builds RedelegateParams`() = runBlocking { + val provider = makeProvider(AmountParams.Stake.Redelegate(asset.id, "v1", "d1")) + provider.validatorState.filterNotNull().first() + val confirm = provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.Stake.RedelegateParams) + } + + @Test + fun `withdraw builds WithdrawParams`() = runBlocking { + val provider = makeProvider(AmountParams.Stake.Withdraw(asset.id, validatorId = "v1", delegationId = "d1")) + provider.assetInfo.filterNotNull().first() + val confirm = provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.Stake.WithdrawParams) + } + + @Test + fun `rewards builds RewardsParams`() = runBlocking { + val provider = makeProvider(AmountParams.Stake.Rewards(asset.id)) + provider.validatorState.filterNotNull().first() + val confirm = provider.buildConfirmParams(Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.Stake.RewardsParams) + } + + @Test + fun `canChangeValue is false for Withdraw and Rewards`() { + assertEquals(true, makeProvider(AmountParams.Stake.Delegate(asset.id)).canChangeValue) + assertEquals(true, makeProvider(AmountParams.Stake.Redelegate(asset.id, "v", "d")).canChangeValue) + assertEquals(true, makeProvider(AmountParams.Stake.Undelegate(asset.id, "v", "d")).canChangeValue) + assertEquals(false, makeProvider(AmountParams.Stake.Withdraw(asset.id, "v", "d")).canChangeValue) + assertEquals(false, makeProvider(AmountParams.Stake.Rewards(asset.id)).canChangeValue) + } + + @Test + fun `canSelectValidator is false for Undelegate and Withdraw`() { + assertEquals(true, makeProvider(AmountParams.Stake.Delegate(asset.id)).canSelectValidator) + assertEquals(true, makeProvider(AmountParams.Stake.Redelegate(asset.id, "v", "d")).canSelectValidator) + assertEquals(true, makeProvider(AmountParams.Stake.Rewards(asset.id)).canSelectValidator) + assertEquals(false, makeProvider(AmountParams.Stake.Undelegate(asset.id, "v", "d")).canSelectValidator) + assertEquals(false, makeProvider(AmountParams.Stake.Withdraw(asset.id, "v", "d")).canSelectValidator) + } +} diff --git a/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountTransferProviderTest.kt b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountTransferProviderTest.kt new file mode 100644 index 000000000..f3f7b9be7 --- /dev/null +++ b/android/features/transfer_amount/viewmodels/src/test/kotlin/com/gemwallet/android/features/transfer_amount/viewmodels/providers/AmountTransferProviderTest.kt @@ -0,0 +1,81 @@ +package com.gemwallet.android.features.transfer_amount.viewmodels.providers + +import com.gemwallet.android.data.repositories.assets.AssetsRepository +import com.gemwallet.android.data.repositories.transactions.TransactionBalanceService +import com.gemwallet.android.features.transfer_amount.viewmodels.AmountTitle +import com.gemwallet.android.model.AmountParams +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Crypto +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.testkit.mockAssetCosmos +import com.gemwallet.android.testkit.mockAssetInfo +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.math.BigInteger + +class AmountTransferProviderTest { + + private val asset = mockAssetCosmos() + private val assetInfo = mockAssetInfo(asset = asset) + private val assetsRepository = mockk { + every { getAssetInfo(asset.id) } returns flowOf(assetInfo) + } + private val balanceService = mockk { + coEvery { getBalance(any(), any(), any(), any()) } returns BigInteger("1000000") + } + private val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + private val params = AmountParams.Transfer( + assetId = asset.id, + destination = DestinationAddress(address = "to", name = null), + memo = "memo", + ) + + private fun makeProvider() = AmountTransferProvider( + params = params, + assetsRepository = assetsRepository, + transactionBalanceService = balanceService, + scope = scope, + ) + + @Test + fun `title is Send`() { + assertEquals(AmountTitle.Send, makeProvider().title) + } + + @Test + fun `canChangeValue and canSwitchInputType are both true`() { + val provider = makeProvider() + assertTrue(provider.canChangeValue) + assertTrue(provider.canSwitchInputType) + } + + @Test + fun `minimumValue and reserveForFee are zero`() { + val provider = makeProvider() + assertEquals(BigInteger.ZERO, provider.minimumValue) + assertEquals(BigInteger.ZERO, provider.reserveForFee) + } + + @Test + fun `buildConfirmParams produces TransferParams with destination and memo`() = runBlocking { + val provider = makeProvider() + provider.assetInfo.filterNotNull().first() + val confirm = provider.buildConfirmParams(amount = Crypto(BigInteger.ONE), isMax = false) + assertTrue(confirm is ConfirmParams.TransferParams) + confirm as ConfirmParams.TransferParams + assertEquals(BigInteger.ONE, confirm.amount) + assertEquals("to", confirm.destination.address) + assertEquals("memo", confirm.memo) + } +} diff --git a/android/gemcore/src/main/kotlin/com/gemwallet/android/model/AmountParams.kt b/android/gemcore/src/main/kotlin/com/gemwallet/android/model/AmountParams.kt index 34d248ed6..79bfc9839 100644 --- a/android/gemcore/src/main/kotlin/com/gemwallet/android/model/AmountParams.kt +++ b/android/gemcore/src/main/kotlin/com/gemwallet/android/model/AmountParams.kt @@ -6,66 +6,105 @@ import com.gemwallet.android.serializer.jsonEncoder import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.PerpetualDirection import com.wallet.core.primitives.TransactionType +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.Base64 @Serializable -data class AmountParams( - val assetId: AssetId, - val txType: TransactionType, - val destination: DestinationAddress? = null, - val memo: String? = null, - val validatorId: String? = null, // TODO: Separate to special subtype - val delegationId: String? = null, // TODO: Separate to special subtype - val perpetualId: String? = null, // TODO: Separate to special subtype - val perpetualDirection: PerpetualDirection? = null, // TODO: Separate to special subtype -) { +sealed interface AmountParams { + val assetId: AssetId + val transactionType: TransactionType - fun pack(): String? { + fun pack(): String? = runCatching { val json = jsonEncoder.encodeToString(this) - return Base64.getEncoder().encodeToString(json.toByteArray()).urlEncode() + Base64.getEncoder().encodeToString(json.toByteArray()).urlEncode() + }.getOrNull() + + @Serializable + @SerialName("transfer") + data class Transfer( + override val assetId: AssetId, + val destination: DestinationAddress, + val memo: String? = null, + ) : AmountParams { + override val transactionType: TransactionType get() = TransactionType.Transfer } - companion object { - fun unpack(input: String): AmountParams? { - return runCatching { - val json = String(Base64.getDecoder().decode(input.urlDecode())) - jsonEncoder.decodeFromString(json) - }.getOrNull() + @Serializable + sealed interface Stake : AmountParams { + + @Serializable @SerialName("stake.delegate") + data class Delegate( + override val assetId: AssetId, + val validatorId: String? = null, + ) : Stake { + override val transactionType: TransactionType get() = TransactionType.StakeDelegate } - fun buildTransfer( - assetId: AssetId, - destination: DestinationAddress?, - memo: String, - ): AmountParams = AmountParams( - assetId = assetId, - txType = TransactionType.Transfer, - destination = destination, - memo = memo, - ) + @Serializable @SerialName("stake.undelegate") + data class Undelegate( + override val assetId: AssetId, + val validatorId: String, + val delegationId: String, + ) : Stake { + override val transactionType: TransactionType get() = TransactionType.StakeUndelegate + } - fun buildStake( - assetId: AssetId, - txType: TransactionType, - validatorId: String? = null, - delegationId: String? = null, - ): AmountParams = AmountParams( - assetId = assetId, - txType = txType, - delegationId = delegationId, - validatorId = validatorId, - ) + @Serializable @SerialName("stake.redelegate") + data class Redelegate( + override val assetId: AssetId, + val validatorId: String, + val delegationId: String, + ) : Stake { + override val transactionType: TransactionType get() = TransactionType.StakeRedelegate + } - fun buildPerpetualOpenPosition( - assetId: AssetId, - perpetualId: String, - direction: PerpetualDirection, - ): AmountParams = AmountParams( - assetId = assetId, - txType = TransactionType.PerpetualOpenPosition, - perpetualId = perpetualId, - perpetualDirection = direction, - ) + @Serializable @SerialName("stake.withdraw") + data class Withdraw( + override val assetId: AssetId, + val validatorId: String, + val delegationId: String, + ) : Stake { + override val transactionType: TransactionType get() = TransactionType.StakeWithdraw + } + + @Serializable @SerialName("stake.rewards") + data class Rewards( + override val assetId: AssetId, + ) : Stake { + override val transactionType: TransactionType get() = TransactionType.StakeRewards + } + } + + @Serializable + @SerialName("freeze") + data class Freeze( + override val assetId: AssetId, + val direction: Direction, + ) : AmountParams { + @Serializable + enum class Direction { Freeze, Unfreeze } + + override val transactionType: TransactionType get() = when (direction) { + Direction.Freeze -> TransactionType.StakeFreeze + Direction.Unfreeze -> TransactionType.StakeUnfreeze + } + } + + @Serializable + @SerialName("perpetual") + data class Perpetual( + override val assetId: AssetId, + val perpetualId: String, + val direction: PerpetualDirection, + ) : AmountParams { + override val transactionType: TransactionType get() = TransactionType.PerpetualOpenPosition + } + + companion object { + fun unpack(input: String): AmountParams? = runCatching { + val json = String(Base64.getDecoder().decode(input.urlDecode())) + jsonEncoder.decodeFromString(json) + }.getOrNull() } }