From 54b34ef230c9737bf7c3ba1b2c816ce7a4ac7015 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 28 Jan 2025 13:39:38 +0300 Subject: [PATCH 1/2] Redeem flow --- .../staking/MythosStakingNavigationModule.kt | 4 +- .../staking/mythos/MythosStakingNavigator.kt | 15 ++- .../res/navigation/staking_main_graph.xml | 13 ++ .../nova/common/presentation/LoadingState.kt | 8 ++ .../nova/core_db/dao/LockDao.kt | 3 + .../blockchain/calls/CollatorStakingCalls.kt | 8 ++ .../blockchain/model/MythReleaseRequest.kt | 9 ++ .../data/mythos/repository/MythosLocks.kt | 20 ++- .../repository/MythosStakingRepository.kt | 6 + .../repository/MythosUserStakeRepository.kt | 11 ++ .../di/StakingFeatureComponent.kt | 3 + .../di/staking/mythos/MythosBindsModule.kt | 5 + .../di/staking/mythos/MythosModule.kt | 14 +- .../unbonding/MythosUnbondingInteractor.kt | 5 +- .../mythos/redeem/MythosRedeemInteractor.kt | 85 ++++++++++++ .../RedeemMythosStakingValidationFailure.kt | 14 ++ .../RedeemMythosStakingValidationPayload.kt | 13 ++ .../RedeemMythosValidationSystem.kt | 26 ++++ ...ReleaseRequestLimitNotReachedValidation.kt | 51 +++++++ .../UnbondMythosStakingValidationFailure.kt | 2 + .../UnbondMythosValidationSystem.kt | 7 +- .../presentation/MythosStakingRouter.kt | 3 + ...MythosStakingValidationFailureFormatter.kt | 17 +++ .../mythos/redeem/MythosRedeemFragment.kt | 69 ++++++++++ .../mythos/redeem/MythosRedeemViewModel.kt | 124 ++++++++++++++++++ .../mythos/redeem/di/MythosRedeemComponent.kt | 26 ++++ .../mythos/redeem/di/MythosRedeemModule.kt | 68 ++++++++++ .../res/layout/fragment_mythos_redeem.xml | 49 +++++++ .../data/repository/BalanceLocksRepository.kt | 2 + .../repository/RealBalanceLocksRepository.kt | 5 + 30 files changed, 671 insertions(+), 14 deletions(-) create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationFailure.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationPayload.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemFragment.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemViewModel.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemComponent.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemModule.kt create mode 100644 feature-staking-impl/src/main/res/layout/fragment_mythos_redeem.xml diff --git a/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/staking/MythosStakingNavigationModule.kt b/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/staking/MythosStakingNavigationModule.kt index d3e63fdaaa..8205692dcd 100644 --- a/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/staking/MythosStakingNavigationModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/di/app/navigation/staking/MythosStakingNavigationModule.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.app.root.navigation.navigators.staking.mythos.Sele import io.novafoundation.nova.app.root.navigation.navigators.staking.mythos.SelectMythosCollatorInterScreenCommunicatorImpl import io.novafoundation.nova.common.di.scope.ApplicationScope import io.novafoundation.nova.feature_staking_impl.presentation.MythosStakingRouter +import io.novafoundation.nova.feature_staking_impl.presentation.StakingDashboardRouter import io.novafoundation.nova.feature_staking_impl.presentation.mythos.SelectMythosInterScreenCommunicator import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.selectCollatorSettings.SelectMythCollatorSettingsInterScreenCommunicator @@ -18,8 +19,9 @@ class MythosStakingNavigationModule { @ApplicationScope fun provideMythosStakingRouter( navigationHoldersRegistry: NavigationHoldersRegistry, + stakingDashboardRouter: StakingDashboardRouter, ): MythosStakingRouter { - return MythosStakingNavigator(navigationHoldersRegistry) + return MythosStakingNavigator(navigationHoldersRegistry, stakingDashboardRouter) } @Provides diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/navigators/staking/mythos/MythosStakingNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/navigators/staking/mythos/MythosStakingNavigator.kt index d43b967b02..11dd291c92 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/navigators/staking/mythos/MythosStakingNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/navigators/staking/mythos/MythosStakingNavigator.kt @@ -3,7 +3,9 @@ package io.novafoundation.nova.app.root.navigation.navigators.staking.mythos import io.novafoundation.nova.app.R import io.novafoundation.nova.app.root.navigation.navigators.BaseNavigator import io.novafoundation.nova.app.root.navigation.navigators.NavigationHoldersRegistry +import io.novafoundation.nova.feature_staking_impl.domain.staking.redeem.RedeemConsequences import io.novafoundation.nova.feature_staking_impl.presentation.MythosStakingRouter +import io.novafoundation.nova.feature_staking_impl.presentation.StakingDashboardRouter import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.confirm.ConfirmStartMythosStakingFragment import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.confirm.ConfirmStartMythosStakingPayload import io.novafoundation.nova.feature_staking_impl.presentation.mythos.unbond.confirm.ConfirmUnbondMythosFragment @@ -13,6 +15,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.validators.detai class MythosStakingNavigator( navigationHoldersRegistry: NavigationHoldersRegistry, + private val stakingDashboardRouter: StakingDashboardRouter, ) : BaseNavigator(navigationHoldersRegistry), MythosStakingRouter { override fun openCollatorDetails(payload: StakeTargetDetailsPayload) { @@ -53,7 +56,17 @@ class MythosStakingNavigator( } override fun openRedeem() { - // TODO + navigationBuilder() + .action(R.id.action_stakingFragment_to_mythosRedeemFragment) + .navigateInFirstAttachedContext() + } + + override fun finishRedeemFlow(redeemConsequences: RedeemConsequences) { + if (redeemConsequences.willKillStash) { + stakingDashboardRouter.returnToStakingDashboard() + } else { + returnToStakingMain() + } } override fun returnToStartStaking() { diff --git a/app/src/main/res/navigation/staking_main_graph.xml b/app/src/main/res/navigation/staking_main_graph.xml index 31f7f1ae6a..b3f5836e1a 100644 --- a/app/src/main/res/navigation/staking_main_graph.xml +++ b/app/src/main/res/navigation/staking_main_graph.xml @@ -231,6 +231,14 @@ app:exitAnim="@anim/fragment_open_exit" app:popEnterAnim="@anim/fragment_close_enter" app:popExitAnim="@anim/fragment_close_exit" /> + + + + \ No newline at end of file diff --git a/common/src/main/java/io/novafoundation/nova/common/presentation/LoadingState.kt b/common/src/main/java/io/novafoundation/nova/common/presentation/LoadingState.kt index 9edaaadd51..cb834f318a 100644 --- a/common/src/main/java/io/novafoundation/nova/common/presentation/LoadingState.kt +++ b/common/src/main/java/io/novafoundation/nova/common/presentation/LoadingState.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.common.presentation +import io.novafoundation.nova.common.domain.ExtendedLoadingState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -26,6 +27,13 @@ fun LoadingView.showLoadingState(loadingState: LoadingState) { } } +fun LoadingView.showLoadingState(loadingState: ExtendedLoadingState) { + when (loadingState) { + is ExtendedLoadingState.Loaded -> showData(loadingState.data) + is ExtendedLoadingState.Loading, is ExtendedLoadingState.Error -> showLoading() + } +} + @Suppress("UNCHECKED_CAST") inline fun LoadingState.map(mapper: (T) -> R): LoadingState { return when (this) { diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/dao/LockDao.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/dao/LockDao.kt index 956f422d35..3bb46342e2 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/dao/LockDao.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/dao/LockDao.kt @@ -35,6 +35,9 @@ abstract class LockDao { @Query("SELECT * FROM locks WHERE metaId = :metaId AND chainId = :chainId AND assetId = :chainAssetId") abstract fun observeBalanceLocks(metaId: Long, chainId: String, chainAssetId: Int): Flow> + @Query("SELECT * FROM locks WHERE metaId = :metaId AND chainId = :chainId AND assetId = :chainAssetId") + abstract suspend fun getBalanceLocks(metaId: Long, chainId: String, chainAssetId: Int): List + @Query( """ SELECT * FROM locks diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/calls/CollatorStakingCalls.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/calls/CollatorStakingCalls.kt index 77edced8b2..90f1e12406 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/calls/CollatorStakingCalls.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/calls/CollatorStakingCalls.kt @@ -62,6 +62,14 @@ fun CollatorStakingCalls.unlock(amount: Balance) { ) } +fun CollatorStakingCalls.release() { + builder.call( + moduleName = Modules.COLLATOR_STAKING, + callName = "release", + arguments = emptyMap() + ) +} + private fun StakingIntent.toEncodableInstance(): Any { return structOf( "candidate" to candidate.value, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/model/MythReleaseRequest.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/model/MythReleaseRequest.kt index 78c8a0234c..0efde4ead2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/model/MythReleaseRequest.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/network/blockchain/model/MythReleaseRequest.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.bindBlockNumbe import io.novafoundation.nova.common.data.network.runtime.binding.bindList import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.common.utils.sumByBigInteger import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance class MythReleaseRequest( @@ -12,6 +13,14 @@ class MythReleaseRequest( val amount: Balance ) +fun MythReleaseRequest.isRedeemableAt(at: BlockNumber): Boolean { + return at >= block +} + +fun List.totalRedeemable(at: BlockNumber): Balance { + return sumByBigInteger { if (it.isRedeemableAt(at)) it.amount else Balance.ZERO } +} + fun bindMythReleaseRequest(decoded: Any?): MythReleaseRequest { val asStruct = decoded.castToStruct() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosLocks.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosLocks.kt index 9c2c5bc472..19072cf30b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosLocks.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosLocks.kt @@ -21,12 +21,20 @@ val MythosLocks.total: Balance get() = releasing + staked fun BalanceLocksRepository.observeMythosLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow { - return observeBalanceLocks(metaId, chain, chainAsset).map { locks -> - MythosLocks( - releasing = locks.findAmountOrZero(MythosStakingFreezeIds.RELEASING), - staked = locks.findAmountOrZero(MythosStakingFreezeIds.STAKING) - ) - }.distinctUntilChanged() + return observeBalanceLocks(metaId, chain, chainAsset) + .map { locks -> locks.findMythosLocks() } + .distinctUntilChanged() +} + +suspend fun BalanceLocksRepository.getMythosLocks(metaId: Long, chainAsset: Chain.Asset): MythosLocks { + return getBalanceLocks(metaId, chainAsset).findMythosLocks() +} + +private fun List.findMythosLocks(): MythosLocks { + return MythosLocks( + releasing = findAmountOrZero(MythosStakingFreezeIds.RELEASING), + staked = findAmountOrZero(MythosStakingFreezeIds.STAKING) + ) } private fun List.findAmountOrZero(id: BalanceLockId): Balance { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosStakingRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosStakingRepository.kt index 09a0e88dfd..fee9729b7c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosStakingRepository.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosStakingRepository.kt @@ -29,6 +29,8 @@ interface MythosStakingRepository { suspend fun maxDelegatorsPerCollator(chainId: ChainId): Int suspend fun unstakeDurationInSessions(chainId: ChainId): Int + + suspend fun maxReleaseRequests(chainId: ChainId): Int } @FeatureScope @@ -67,4 +69,8 @@ class RealMythosStakingRepository @Inject constructor( metadata.collatorStaking().numberConstant("StakeUnlockDelay").toInt() } } + + override suspend fun maxReleaseRequests(chainId: ChainId): Int { + return maxCollatorsPerDelegator(chainId) + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosUserStakeRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosUserStakeRepository.kt index b8a14017a5..393386c84d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosUserStakeRepository.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/mythos/repository/MythosUserStakeRepository.kt @@ -43,6 +43,11 @@ interface MythosUserStakeRepository { chainId: ChainId, accountId: AccountIdKey ): Flow> + + suspend fun releaseQueues( + chainId: ChainId, + accountId: AccountIdKey + ): List } @FeatureScope @@ -84,6 +89,12 @@ class RealMythosUserStakeRepository @Inject constructor( } } + override suspend fun releaseQueues(chainId: ChainId, accountId: AccountIdKey): List { + return localStorageDataSource.query(chainId) { + metadata.collatorStaking.releaseQueues.query(accountId.value).orEmpty() + } + } + private suspend fun RuntimeCallsApi.shouldClaimPendingRewards(accountId: AccountIdKey): Boolean { return call( section = "CollatorStakingApi", diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureComponent.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureComponent.kt index d69d2f3555..97c7d73794 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureComponent.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureComponent.kt @@ -31,6 +31,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.bagList.rebag.di import io.novafoundation.nova.feature_staking_impl.presentation.dashboard.main.di.StakingDashboardComponent import io.novafoundation.nova.feature_staking_impl.presentation.dashboard.more.di.MoreStakingOptionsComponent import io.novafoundation.nova.feature_staking_impl.presentation.mythos.SelectMythosInterScreenCommunicator +import io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem.di.MythosRedeemComponent import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.confirm.di.ConfirmStartMythosStakingComponent import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.selectCollator.di.SelectMythosCollatorComponent import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.selectCollatorSettings.SelectMythCollatorSettingsInterScreenCommunicator @@ -258,6 +259,8 @@ interface StakingFeatureComponent : StakingFeatureApi { fun confirmUnbondMythosFactory(): ConfirmUnbondMythosComponent.Factory + fun redeemMythosFactory(): MythosRedeemComponent.Factory + @Component.Factory interface Factory { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosBindsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosBindsModule.kt index 85f128c9a5..885b716ad0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosBindsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosBindsModule.kt @@ -20,6 +20,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.stakeSumma import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.stakeSummary.RealMythosStakeSummaryInteractor import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.unbonding.MythosUnbondingInteractor import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.unbonding.RealMythosUnbondingInteractor +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.MythosRedeemInteractor +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.RealMythosRedeemInteractor import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.RealStartMythosStakingInteractor import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.StartMythosStakingInteractor import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.RealUnbondMythosStakingInteractor @@ -56,6 +58,9 @@ interface MythosBindsModule { @Binds fun bindUnbondingInteractor(implementation: RealMythosUnbondingInteractor): MythosUnbondingInteractor + @Binds + fun bindRedeemInteractor(implementation: RealMythosRedeemInteractor): MythosRedeemInteractor + @Binds fun bindStartStakingInteractor(implementation: RealStartMythosStakingInteractor): StartMythosStakingInteractor diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosModule.kt index c6737890a8..fd4e85670b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/mythos/MythosModule.kt @@ -5,9 +5,12 @@ import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_staking_impl.domain.mythos.common.validations.MythosNoPendingRewardsValidationFactory +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.RedeemMythosValidationSystem +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.mythosRedeem import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.validations.MythosMinimumDelegationValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.validations.StartMythosStakingValidationSystem import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.validations.mythosStakingStart +import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations.MythosReleaseRequestLimitNotReachedValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations.UnbondMythosValidationSystem import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations.mythosUnbond @@ -26,8 +29,15 @@ class MythosModule { @Provides @FeatureScope fun provideUnbondValidationSystem( - hasPendingRewardsValidationFactory: MythosNoPendingRewardsValidationFactory + hasPendingRewardsValidationFactory: MythosNoPendingRewardsValidationFactory, + releaseRequestLimitNotReachedValidation: MythosReleaseRequestLimitNotReachedValidationFactory ): UnbondMythosValidationSystem { - return ValidationSystem.mythosUnbond(hasPendingRewardsValidationFactory) + return ValidationSystem.mythosUnbond(hasPendingRewardsValidationFactory, releaseRequestLimitNotReachedValidation) + } + + @Provides + @FeatureScope + fun provideRedeemValidationSystem(): RedeemMythosValidationSystem { + return ValidationSystem.mythosRedeem() } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/main/unbonding/MythosUnbondingInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/main/unbonding/MythosUnbondingInteractor.kt index e0443bd4ca..22ecd44922 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/main/unbonding/MythosUnbondingInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/main/unbonding/MythosUnbondingInteractor.kt @@ -4,7 +4,7 @@ import io.novafoundation.nova.common.address.intoKey import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn import io.novafoundation.nova.feature_staking_impl.data.StakingOption import io.novafoundation.nova.feature_staking_impl.data.chain import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythReleaseRequest @@ -42,8 +42,7 @@ class RealMythosUnbondingInteractor @Inject constructor( val chainId = stakingOption.chain.id return flowOfAll { - val metaAccount = accountRepository.getSelectedMetaAccount() - val accountId = metaAccount.requireAccountIdIn(stakingOption.chain).intoKey() + val accountId = accountRepository.requireIdOfSelectedMetaAccountIn(stakingOption.chain).intoKey() combine( userStakeRepository.releaseQueuesFlow(chainId, accountId), diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt new file mode 100644 index 0000000000..dd40867d85 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt @@ -0,0 +1,85 @@ +package io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem + +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn +import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState +import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.calls.collatorStaking +import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.calls.release +import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.totalRedeemable +import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.MythosUserStakeRepository +import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.getMythosLocks +import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.total +import io.novafoundation.nova.feature_staking_impl.domain.staking.redeem.RedeemConsequences +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository +import io.novafoundation.nova.runtime.repository.ChainStateRepository +import io.novafoundation.nova.runtime.state.chain +import io.novafoundation.nova.runtime.state.chainAndAsset +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + + +interface MythosRedeemInteractor { + + fun redeemAmountFlow(): Flow + + suspend fun estimateFee(): Fee + + suspend fun redeem(redeemAmount: Balance): Result +} + +@FeatureScope +class RealMythosRedeemInteractor @Inject constructor( + private val userStakeRepository: MythosUserStakeRepository, + private val chainStateRepository: ChainStateRepository, + private val stakingSharedState: StakingSharedState, + private val accountRepository: AccountRepository, + private val extrinsicService: ExtrinsicService, + private val balanceLocksRepository: BalanceLocksRepository, +) : MythosRedeemInteractor { + + override fun redeemAmountFlow(): Flow { + return flowOfAll { + val chain = stakingSharedState.chain() + val accountId = accountRepository.requireIdOfSelectedMetaAccountIn(chain).intoKey() + + combine( + userStakeRepository.releaseQueuesFlow(chain.id, accountId), + chainStateRepository.currentBlockNumberFlow(chain.id) + ) { releaseRequests, blockNumber -> + releaseRequests.totalRedeemable(at = blockNumber) + } + } + } + + override suspend fun estimateFee(): Fee { + val chain = stakingSharedState.chain() + + return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { + collatorStaking.release() + } + } + + override suspend fun redeem(redeemAmount: Balance): Result { + val (chain, chainAsset) = stakingSharedState.chainAndAsset() + val metaAccount = accountRepository.getSelectedMetaAccount() + val mythStakingFreezes = balanceLocksRepository.getMythosLocks(metaAccount.id, chainAsset) + + return extrinsicService.submitExtrinsicAndAwaitExecution(chain, TransactionOrigin.SelectedWallet) { + collatorStaking.release() + } + .requireOk() + .map { + val redeemedAll = mythStakingFreezes.total == redeemAmount + RedeemConsequences(willKillStash = redeemedAll) + } + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationFailure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationFailure.kt new file mode 100644 index 0000000000..d65d798d67 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationFailure.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations + +import io.novafoundation.nova.feature_wallet_api.domain.validation.NotEnoughToPayFeesError +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import java.math.BigDecimal + +sealed class RedeemMythosStakingValidationFailure { + + class NotEnoughBalanceToPayFees( + override val chainAsset: Chain.Asset, + override val maxUsable: BigDecimal, + override val fee: BigDecimal + ) : RedeemMythosStakingValidationFailure(), NotEnoughToPayFeesError +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationPayload.kt new file mode 100644 index 0000000000..81fb19b721 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosStakingValidationPayload.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations + +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId + +class RedeemMythosStakingValidationPayload( + val fee: Fee, + val asset: Asset, +) + +val RedeemMythosStakingValidationPayload.chainId: ChainId + get() = asset.token.configuration.chainId diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt new file mode 100644 index 0000000000..d5295f7133 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations + +import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance + +typealias RedeemMythosValidationSystem = ValidationSystem +typealias RedeemMythosValidationSystemBuilder = ValidationSystemBuilder + +fun ValidationSystem.Companion.mythosRedeem( +): RedeemMythosValidationSystem = ValidationSystem { + enoughToPayFees() +} +private fun RedeemMythosValidationSystemBuilder.enoughToPayFees() { + sufficientBalance( + fee = { it.fee }, + available = { it.asset.transferable }, + error = { + RedeemMythosStakingValidationFailure.NotEnoughBalanceToPayFees( + chainAsset = it.payload.asset.token.configuration, + maxUsable = it.maxUsable, + fee = it.fee + ) + } + ) +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt new file mode 100644 index 0000000000..8661ea1016 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt @@ -0,0 +1,51 @@ +package io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations + +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.common.validation.isTrueOrError +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn +import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.MythosStakingRepository +import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.MythosUserStakeRepository +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import javax.inject.Inject + +@FeatureScope +class MythosReleaseRequestLimitNotReachedValidationFactory @Inject constructor( + private val stakingRepository: MythosStakingRepository, + private val userStakeRepository: MythosUserStakeRepository, + private val accountRepository: AccountRepository, + private val chainRegistry: ChainRegistry, +) { + + context(UnbondMythosValidationSystemBuilder) + fun releaseRequestsLimitNotReached() { + validate(ReleaseRequestLimitNotReachedValidation( + stakingRepository = stakingRepository, + userStakeRepository = userStakeRepository, + accountRepository = accountRepository, + chainRegistry = chainRegistry + )) + } +} + +private class ReleaseRequestLimitNotReachedValidation( + private val stakingRepository: MythosStakingRepository, + private val userStakeRepository: MythosUserStakeRepository, + private val accountRepository: AccountRepository, + private val chainRegistry: ChainRegistry, +): UnbondMythosValidation { + + override suspend fun validate(value: UnbondMythosStakingValidationPayload): ValidationStatus { + val chain = chainRegistry.getChain(value.chainId) + val accountId = accountRepository.requireIdOfSelectedMetaAccountIn(chain).intoKey() + + val releaseQueues = userStakeRepository.releaseQueues(chain.id, accountId) + val limit = stakingRepository.maxReleaseRequests(chain.id) + + return (releaseQueues.size < limit) isTrueOrError { + UnbondMythosStakingValidationFailure.ReleaseRequestsLimitReached(limit) + } + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt index 186bbd3460..e791a65c8a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt @@ -13,4 +13,6 @@ sealed class UnbondMythosStakingValidationFailure { ) : UnbondMythosStakingValidationFailure(), NotEnoughToPayFeesError object HasNotClaimedRewards : UnbondMythosStakingValidationFailure() + + class ReleaseRequestsLimitReached(val limit: Int): UnbondMythosStakingValidationFailure() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosValidationSystem.kt index 523cff3b7f..aa71dbd094 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosValidationSystem.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations +import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_staking_impl.domain.mythos.common.validations.MythosNoPendingRewardsValidationFactory @@ -7,10 +8,14 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBal typealias UnbondMythosValidationSystem = ValidationSystem typealias UnbondMythosValidationSystemBuilder = ValidationSystemBuilder +typealias UnbondMythosValidation = Validation fun ValidationSystem.Companion.mythosUnbond( - hasPendingRewardsValidationFactory: MythosNoPendingRewardsValidationFactory + hasPendingRewardsValidationFactory: MythosNoPendingRewardsValidationFactory, + releaseRequestLimitNotReachedValidation: MythosReleaseRequestLimitNotReachedValidationFactory ): UnbondMythosValidationSystem = ValidationSystem { + releaseRequestLimitNotReachedValidation.releaseRequestsLimitNotReached() + hasPendingRewardsValidationFactory.noPendingRewards() enoughToPayFees() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/MythosStakingRouter.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/MythosStakingRouter.kt index a1aa8af4c5..f3a62f1721 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/MythosStakingRouter.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/MythosStakingRouter.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.presentation +import io.novafoundation.nova.feature_staking_impl.domain.staking.redeem.RedeemConsequences import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.confirm.ConfirmStartMythosStakingPayload import io.novafoundation.nova.feature_staking_impl.presentation.mythos.unbond.confirm.ConfirmUnbondMythosPayload import io.novafoundation.nova.feature_staking_impl.presentation.validators.details.StakeTargetDetailsPayload @@ -21,4 +22,6 @@ interface MythosStakingRouter : StarkingReturnableRouter { fun openUnbondConfirm(payload: ConfirmUnbondMythosPayload) fun openRedeem() + + fun finishRedeemFlow(redeemConsequences: RedeemConsequences) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/common/validations/MythosStakingValidationFailureFormatter.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/common/validations/MythosStakingValidationFailureFormatter.kt index 164c885303..c4e44ca210 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/common/validations/MythosStakingValidationFailureFormatter.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/common/validations/MythosStakingValidationFailureFormatter.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.presentation.mythos.common.validations +import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer.Payload.DialogAction @@ -8,6 +9,7 @@ import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.asDefault import io.novafoundation.nova.feature_staking_impl.R +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.RedeemMythosStakingValidationFailure import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.validations.StartMythosStakingValidationFailure import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations.UnbondMythosStakingValidationFailure import io.novafoundation.nova.feature_staking_impl.presentation.MythosStakingRouter @@ -22,6 +24,8 @@ interface MythosStakingValidationFailureFormatter { fun formatStartStaking(failure: ValidationStatus.NotValid): TransformedFailure fun formatUnbond(failure: ValidationStatus.NotValid): TransformedFailure + + fun formatRedeem(reason: RedeemMythosStakingValidationFailure): TitleAndMessage } @FeatureScope @@ -56,6 +60,19 @@ class RealMythosStakingValidationFailureFormatter @Inject constructor( UnbondMythosStakingValidationFailure.HasNotClaimedRewards -> hasPendingRewardFailure() is UnbondMythosStakingValidationFailure.NotEnoughBalanceToPayFees -> handleNotEnoughFeeError(reason, resourceManager).asDefault() + + is UnbondMythosStakingValidationFailure.ReleaseRequestsLimitReached -> { + val content = resourceManager.getString(R.string.staking_unbonding_limit_reached_title) to + resourceManager.getString(R.string.staking_unbonding_limit_reached_message, reason.limit) + + content.asDefault() + } + } + } + + override fun formatRedeem(reason: RedeemMythosStakingValidationFailure): TitleAndMessage { + return when (reason) { + is RedeemMythosStakingValidationFailure.NotEnoughBalanceToPayFees -> handleNotEnoughFeeError(reason, resourceManager) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemFragment.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemFragment.kt new file mode 100644 index 0000000000..edac9a171a --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemFragment.kt @@ -0,0 +1,69 @@ +package io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import io.novafoundation.nova.common.base.BaseFragment +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.mixin.impl.observeRetries +import io.novafoundation.nova.common.mixin.impl.observeValidations +import io.novafoundation.nova.common.presentation.showLoadingState +import io.novafoundation.nova.common.utils.applyStatusBarInsets +import io.novafoundation.nova.common.view.setProgressState +import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions +import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi +import io.novafoundation.nova.feature_staking_impl.R +import io.novafoundation.nova.feature_staking_impl.di.StakingFeatureComponent +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading +import kotlinx.android.synthetic.main.fragment_mythos_redeem.mythosRedeemAmount +import kotlinx.android.synthetic.main.fragment_mythos_redeem.mythosRedeemConfirm +import kotlinx.android.synthetic.main.fragment_mythos_redeem.mythosRedeemContainer +import kotlinx.android.synthetic.main.fragment_mythos_redeem.mythosRedeemExtrinsicInfo +import kotlinx.android.synthetic.main.fragment_mythos_redeem.mythosRedeemToolbar + +class MythosRedeemFragment : BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.fragment_mythos_redeem, container, false) + } + + override fun initViews() { + mythosRedeemContainer.applyStatusBarInsets() + + mythosRedeemToolbar.setHomeButtonListener { viewModel.backClicked() } + + mythosRedeemExtrinsicInfo.setOnAccountClickedListener { viewModel.originAccountClicked() } + + mythosRedeemConfirm.prepareForProgress(viewLifecycleOwner) + mythosRedeemConfirm.setOnClickListener { viewModel.confirmClicked() } + } + + override fun inject() { + FeatureUtils.getFeature( + requireContext(), + StakingFeatureApi::class.java + ) + .redeemMythosFactory() + .create(this) + .inject(this) + } + + override fun subscribe(viewModel: MythosRedeemViewModel) { + observeRetries(viewModel) + observeValidations(viewModel) + setupExternalActions(viewModel) + setupFeeLoading(viewModel, mythosRedeemExtrinsicInfo.fee) + + viewModel.showNextProgress.observe(mythosRedeemConfirm::setProgressState) + + viewModel.currentAccountModelFlow.observe(mythosRedeemExtrinsicInfo::setAccount) + viewModel.walletFlow.observe(mythosRedeemExtrinsicInfo::setWallet) + + viewModel.redeemableAmountModelFlow.observe(mythosRedeemAmount::showLoadingState) + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemViewModel.kt new file mode 100644 index 0000000000..82be8a9aab --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/MythosRedeemViewModel.kt @@ -0,0 +1,124 @@ +package io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem + +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.mixin.api.Retriable +import io.novafoundation.nova.common.mixin.api.Validatable +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.launchUnit +import io.novafoundation.nova.common.utils.withSafeLoading +import io.novafoundation.nova.common.validation.ValidationExecutor +import io.novafoundation.nova.common.validation.progressConsumer +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase +import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions +import io.novafoundation.nova.feature_staking_impl.R +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.MythosRedeemInteractor +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.RedeemMythosStakingValidationPayload +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.RedeemMythosValidationSystem +import io.novafoundation.nova.feature_staking_impl.presentation.MythosStakingRouter +import io.novafoundation.nova.feature_staking_impl.presentation.mythos.common.validations.MythosStakingValidationFailureFormatter +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState +import io.novafoundation.nova.runtime.state.chain +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class MythosRedeemViewModel( + private val router: MythosStakingRouter, + private val resourceManager: ResourceManager, + private val validationSystem: RedeemMythosValidationSystem, + private val validationFailureFormatter: MythosStakingValidationFailureFormatter, + private val interactor: MythosRedeemInteractor, + private val feeLoaderMixin: FeeLoaderMixin.Presentation, + private val externalActions: ExternalActions.Presentation, + private val selectedAssetState: AnySelectedAssetOptionSharedState, + private val validationExecutor: ValidationExecutor, + selectedAccountUseCase: SelectedAccountUseCase, + assetUseCase: AssetUseCase, + walletUiUseCase: WalletUiUseCase, +) : BaseViewModel(), + Retriable, + Validatable by validationExecutor, + FeeLoaderMixin by feeLoaderMixin, + ExternalActions by externalActions { + + private val assetFlow = assetUseCase.currentAssetFlow() + .shareInBackground() + + private val redeemableAmountFlow = interactor.redeemAmountFlow() + .shareInBackground() + + val redeemableAmountModelFlow = combine(redeemableAmountFlow, assetFlow, ::mapAmountToAmountModel) + .withSafeLoading() + .shareInBackground() + + val currentAccountModelFlow = selectedAccountUseCase.selectedAddressModelFlow { selectedAssetState.chain() } + .shareInBackground() + + val walletFlow = walletUiUseCase.selectedWalletUiFlow() + .shareInBackground() + + private val _showNextProgress = MutableStateFlow(false) + val showNextProgress: StateFlow = _showNextProgress + + init { + feeLoaderMixin.loadFee( + coroutineScope = this, + feeConstructor = { interactor.estimateFee() }, + onRetryCancelled = {} + ) + } + + fun confirmClicked() { + sendTransactionIfValid() + } + + fun backClicked() { + router.back() + } + + fun originAccountClicked() = launch { + val address = currentAccountModelFlow.first().address + + externalActions.showExternalActions(ExternalActions.Type.Address(address), selectedAssetState.chain()) + } + + private fun sendTransactionIfValid() = launchUnit { + _showNextProgress.value = true + + val payload = RedeemMythosStakingValidationPayload( + fee = feeLoaderMixin.awaitFee(), + asset = assetFlow.first() + ) + + val redeemAmount = redeemableAmountFlow.first() + + validationExecutor.requireValid( + validationSystem = validationSystem, + payload = payload, + validationFailureTransformer = validationFailureFormatter::formatRedeem, + progressConsumer = _showNextProgress.progressConsumer() + ) { + sendTransaction(redeemAmount) + } + } + + private fun sendTransaction(redeemAmount: Balance) = launch { + interactor.redeem(redeemAmount) + .onFailure(::showError) + .onSuccess { redeemConsequences -> + showMessage(resourceManager.getString(R.string.common_transaction_submitted)) + + router.finishRedeemFlow(redeemConsequences) + } + + _showNextProgress.value = false + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemComponent.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemComponent.kt new file mode 100644 index 0000000000..646109d5b3 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemComponent.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem.MythosRedeemFragment + +@Subcomponent( + modules = [ + MythosRedeemModule::class + ] +) +@ScreenScope +interface MythosRedeemComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment, + ): MythosRedeemComponent + } + + fun inject(fragment: MythosRedeemFragment) +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemModule.kt new file mode 100644 index 0000000000..75266bb518 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/mythos/redeem/di/MythosRedeemModule.kt @@ -0,0 +1,68 @@ +package io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.validation.ValidationExecutor +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase +import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.MythosRedeemInteractor +import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.RedeemMythosValidationSystem +import io.novafoundation.nova.feature_staking_impl.presentation.MythosStakingRouter +import io.novafoundation.nova.feature_staking_impl.presentation.mythos.common.validations.MythosStakingValidationFailureFormatter +import io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem.MythosRedeemViewModel +import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState + +@Module(includes = [ViewModelModule::class]) +class MythosRedeemModule { + + @Provides + @IntoMap + @ViewModelKey(MythosRedeemViewModel::class) + fun provideViewModel( + router: MythosStakingRouter, + resourceManager: ResourceManager, + validationSystem: RedeemMythosValidationSystem, + validationFailureFormatter: MythosStakingValidationFailureFormatter, + interactor: MythosRedeemInteractor, + feeLoaderMixin: FeeLoaderMixin.Presentation, + externalActions: ExternalActions.Presentation, + selectedAssetState: AnySelectedAssetOptionSharedState, + validationExecutor: ValidationExecutor, + selectedAccountUseCase: SelectedAccountUseCase, + assetUseCase: AssetUseCase, + walletUiUseCase: WalletUiUseCase, + ): ViewModel { + return MythosRedeemViewModel( + router = router, + resourceManager = resourceManager, + validationSystem = validationSystem, + validationFailureFormatter = validationFailureFormatter, + interactor = interactor, + feeLoaderMixin = feeLoaderMixin, + externalActions = externalActions, + selectedAssetState = selectedAssetState, + validationExecutor = validationExecutor, + selectedAccountUseCase = selectedAccountUseCase, + assetUseCase = assetUseCase, + walletUiUseCase = walletUiUseCase + ) + } + + @Provides + fun provideViewModelCreator( + fragment: Fragment, + viewModelFactory: ViewModelProvider.Factory, + ): MythosRedeemViewModel { + return ViewModelProvider(fragment, viewModelFactory).get(MythosRedeemViewModel::class.java) + } +} diff --git a/feature-staking-impl/src/main/res/layout/fragment_mythos_redeem.xml b/feature-staking-impl/src/main/res/layout/fragment_mythos_redeem.xml new file mode 100644 index 0000000000..4057e245f2 --- /dev/null +++ b/feature-staking-impl/src/main/res/layout/fragment_mythos_redeem.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceLocksRepository.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceLocksRepository.kt index 46b5f0e71d..de9c369700 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceLocksRepository.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceLocksRepository.kt @@ -10,6 +10,8 @@ interface BalanceLocksRepository { fun observeBalanceLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow> + suspend fun getBalanceLocks(metaId: Long, chainAsset: Chain.Asset): List + suspend fun getBiggestLock(chain: Chain, chainAsset: Chain.Asset): BalanceLock? suspend fun observeBalanceLock(chainAsset: Chain.Asset, lockId: BalanceLockId): Flow diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceLocksRepository.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceLocksRepository.kt index 1984b149ca..73bc8c6646 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceLocksRepository.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceLocksRepository.kt @@ -26,6 +26,11 @@ class RealBalanceLocksRepository( .mapList { lock -> mapBalanceLockFromLocal(chainAsset, lock) } } + override suspend fun getBalanceLocks(metaId: Long, chainAsset: Chain.Asset): List { + return lockDao.getBalanceLocks(metaId, chainAsset.chainId, chainAsset.id) + .map { lock -> mapBalanceLockFromLocal(chainAsset, lock) } + } + override suspend fun getBiggestLock(chain: Chain, chainAsset: Chain.Asset): BalanceLock? { val metaAccount = accountRepository.getSelectedMetaAccount() From cf3b0d27261e16a117381b52f615c7e0c1f31b5f Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 28 Jan 2025 14:15:07 +0300 Subject: [PATCH 2/2] Code style --- .../mythos/redeem/MythosRedeemInteractor.kt | 1 - .../validations/RedeemMythosValidationSystem.kt | 3 +-- .../ReleaseRequestLimitNotReachedValidation.kt | 16 +++++++++------- .../UnbondMythosStakingValidationFailure.kt | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt index dd40867d85..27c7dae7a2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/MythosRedeemInteractor.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import javax.inject.Inject - interface MythosRedeemInteractor { fun redeemAmountFlow(): Flow diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt index d5295f7133..75cc4e3141 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/redeem/validations/RedeemMythosValidationSystem.kt @@ -7,8 +7,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBal typealias RedeemMythosValidationSystem = ValidationSystem typealias RedeemMythosValidationSystemBuilder = ValidationSystemBuilder -fun ValidationSystem.Companion.mythosRedeem( -): RedeemMythosValidationSystem = ValidationSystem { +fun ValidationSystem.Companion.mythosRedeem(): RedeemMythosValidationSystem = ValidationSystem { enoughToPayFees() } private fun RedeemMythosValidationSystemBuilder.enoughToPayFees() { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt index 8661ea1016..a83c0f4df7 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/ReleaseRequestLimitNotReachedValidation.kt @@ -21,12 +21,14 @@ class MythosReleaseRequestLimitNotReachedValidationFactory @Inject constructor( context(UnbondMythosValidationSystemBuilder) fun releaseRequestsLimitNotReached() { - validate(ReleaseRequestLimitNotReachedValidation( - stakingRepository = stakingRepository, - userStakeRepository = userStakeRepository, - accountRepository = accountRepository, - chainRegistry = chainRegistry - )) + validate( + ReleaseRequestLimitNotReachedValidation( + stakingRepository = stakingRepository, + userStakeRepository = userStakeRepository, + accountRepository = accountRepository, + chainRegistry = chainRegistry + ) + ) } } @@ -35,7 +37,7 @@ private class ReleaseRequestLimitNotReachedValidation( private val userStakeRepository: MythosUserStakeRepository, private val accountRepository: AccountRepository, private val chainRegistry: ChainRegistry, -): UnbondMythosValidation { +) : UnbondMythosValidation { override suspend fun validate(value: UnbondMythosStakingValidationPayload): ValidationStatus { val chain = chainRegistry.getChain(value.chainId) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt index e791a65c8a..44544be170 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/mythos/unbond/validations/UnbondMythosStakingValidationFailure.kt @@ -14,5 +14,5 @@ sealed class UnbondMythosStakingValidationFailure { object HasNotClaimedRewards : UnbondMythosStakingValidationFailure() - class ReleaseRequestsLimitReached(val limit: Int): UnbondMythosStakingValidationFailure() + class ReleaseRequestsLimitReached(val limit: Int) : UnbondMythosStakingValidationFailure() }