From 5454d777e9f6d01326a8108d110de9c6688b31ba Mon Sep 17 00:00:00 2001 From: valentunn <70131744+valentunn@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:44:40 +0200 Subject: [PATCH] Fix/nomination pools (#1587) * Show holds * Show holds on balance details * Reduce shown reserved amount by sum of all holds * Fix conflicts * Code style --- .../nova/common/utils/FearlessLibExt.kt | 6 ++ .../nova/common/utils/FlowExt.kt | 15 ++++ common/src/main/res/values/strings.xml | 7 ++ .../nova/core_db/AppDatabase.kt | 12 ++- .../nova/core_db/dao/HoldsDao.kt | 37 +++++++++ .../novafoundation/nova/core_db/di/DbApi.kt | 3 + .../nova/core_db/di/DbModule.kt | 7 ++ .../migrations/60_61_AddBalanceHolds.kt | 25 ++++++ .../nova/core_db/model/BalanceHoldLocal.kt | 44 ++++++++++ .../di/AssetsFeatureDependencies.kt | 6 ++ .../breakdown/BalanceBreakdownInteractor.kt | 46 +++++++++-- .../domain/locks/BalanceLocksInteractor.kt | 3 + .../locks/BalanceLocksInteractorImpl.kt | 22 ++++- .../balance/detail/BalanceDetailViewModel.kt | 25 ++++-- .../balance/detail/di/BalanceDetailModule.kt | 12 ++- .../balance/list/di/BalanceListModule.kt | 22 ++--- ...RealNewDelegationChooseAmountInteractor.kt | 4 +- .../unlock/GovernanceUnlockInteractor.kt | 4 +- .../vote/RealVoteReferendumInteractor.kt | 5 +- .../blockhain/api/DelegatedStakingApi.kt | 28 +++++++ ...cBuilderExt.kt => NominationPoolsCalls.kt} | 10 +++ .../models/DelegatedStakingDelegation.kt | 17 ++++ .../updater/DelegatedStakeUpdater.kt | 29 +++++++ .../blockhain/updater/PooledBalanceUpdater.kt | 29 ++++++- .../NominationPoolDelegatedStakeRepository.kt | 49 ++++++++++++ .../NominationPoolMembersRepository.kt | 9 +++ .../di/StakingFeatureModule.kt | 17 ++++ .../nominationPool/NominationPoolModule.kt | 29 ++++++- .../NominationPoolStakingUpdatersModule.kt | 20 ++++- .../StartMultiStakingModule.kt | 12 ++- .../NominationPoolsBondMoreInteractor.kt | 14 +++- .../bondMore/validations/Declarations.kt | 12 +++ ...ominationPoolsBondMoreValidationFailure.kt | 2 + .../validations/ValidationFailureUi.kt | 7 +- .../NominationPoolsClaimRewardsInteractor.kt | 16 ++-- .../claimRewards/validations/Declarations.kt | 14 +++- ...ationPoolsClaimRewardsValidationFailure.kt | 2 + ...ationPoolsClaimRewardsValidationPayload.kt | 4 +- .../validations/ValidationFailureUi.kt | 3 + .../DelegatedStakeMigrationUseCase.kt | 35 ++++++++ .../StakingTypesConflictValidation.kt | 80 +++++++++++++++++++ .../model/DelegatedStakeMigrationState.kt | 6 ++ .../redeem/NominationPoolsRedeemInteractor.kt | 4 + .../redeem/validations/Declarations.kt | 15 +++- .../NominationPoolsRedeemValidationFailure.kt | 2 + .../NominationPoolsRedeemValidationPayload.kt | 4 +- .../redeem/validations/ValidationFailureUi.kt | 3 + .../unbond/NominationPoolsUnbondInteractor.kt | 14 ++-- .../unbond/validations/Declarations.kt | 14 +++- .../NominationPoolsUnbondValidationFailure.kt | 2 + .../unbond/validations/ValidationFailureUi.kt | 7 +- .../StartMultiStakingValidationFailure.kt | 2 + .../StartMultiStakingValidationFailureUi.kt | 11 +++ .../start/landing/StartStakingInteractor.kt | 0 .../landing/StartStakingInteractorFactory.kt | 0 .../direct/DirectStakingProperties.kt | 23 +++++- .../pools/NominationPoolStakingProperties.kt | 22 +++++ .../di/NominationPoolsCommonBondMoreModule.kt | 11 ++- .../NominationPoolsClaimRewardsViewModel.kt | 9 ++- .../claimRewards/PoolPendingRewards.kt | 9 +++ .../di/NominationPoolsClaimRewardsModule.kt | 11 ++- .../redeem/NominationPoolsRedeemViewModel.kt | 3 +- .../redeem/di/NominationPoolsRedeemModule.kt | 11 ++- .../di/NominationPoolsCommonUnbondModule.kt | 19 ++++- .../confirm/ConfirmMultiStakingViewModel.kt | 2 +- .../blockhain/assets/balances/AssetBalance.kt | 9 +++ .../data/repository/BalanceHoldsRepository.kt | 12 +++ .../data/repository/BalanceLocksRepository.kt | 2 +- .../feature_wallet_api/di/WalletFeatureApi.kt | 3 + .../feature_wallet_api/domain/model/Asset.kt | 9 +++ .../domain/model/BalanceBreakdownIds.kt | 2 + .../domain/model/BalanceHold.kt | 35 ++++++++ .../formatters/BalanceIdMapper.kt | 3 +- .../balances/utility/NativeAssetBalance.kt | 64 ++++++++++++++- .../updaters/locks/BalanceLocksUpdater.kt | 20 +++-- .../repository/RealBalanceHoldsRepository.kt | 35 ++++++++ .../repository/RealBalanceLocksRepository.kt | 6 +- .../di/WalletFeatureDependencies.kt | 3 + .../di/WalletFeatureModule.kt | 12 +++ .../di/modules/NativeAssetsModule.kt | 7 +- 80 files changed, 1068 insertions(+), 101 deletions(-) create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/dao/HoldsDao.kt create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/migrations/60_61_AddBalanceHolds.kt create mode 100644 core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceHoldLocal.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/api/DelegatedStakingApi.kt rename feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/calls/{ExtrinsicBuilderExt.kt => NominationPoolsCalls.kt} (89%) create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/models/DelegatedStakingDelegation.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/DelegatedStakeUpdater.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolDelegatedStakeRepository.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/delegatedStake/DelegatedStakeMigrationUseCase.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/StakingTypesConflictValidation.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/model/DelegatedStakeMigrationState.kt delete mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/landing/StartStakingInteractor.kt delete mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/landing/StartStakingInteractorFactory.kt create mode 100644 feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/PoolPendingRewards.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceHoldsRepository.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceHold.kt create mode 100644 feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceHoldsRepository.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt index e3d74d026c..e567a88275 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt @@ -264,6 +264,10 @@ fun RuntimeMetadata.preImage() = module(Modules.PREIMAGE) fun RuntimeMetadata.nominationPools() = module(Modules.NOMINATION_POOLS) +fun RuntimeMetadata.delegatedStakingOrNull() = moduleOrNull(Modules.DELEGATED_STAKING) + +fun RuntimeMetadata.delegatedStaking() = module(Modules.DELEGATED_STAKING) + fun RuntimeMetadata.nominationPoolsOrNull() = moduleOrNull(Modules.NOMINATION_POOLS) fun RuntimeMetadata.assetConversionOrNull() = moduleOrNull(Modules.ASSET_CONVERSION) @@ -437,6 +441,8 @@ object Modules { const val NOMINATION_POOLS = "NominationPools" + const val DELEGATED_STAKING = "DelegatedStaking" + const val ASSET_CONVERSION = "AssetConversion" const val TRANSACTION_PAYMENT = "TransactionPayment" diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt index 3caad489ba..c0ebdf200a 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt @@ -301,6 +301,7 @@ fun Flow>.transformLatestDiffed(transform: suspend private class SendingCollector( private val channel: SendChannel ) : FlowCollector { + override suspend fun emit(value: T): Unit = channel.send(value) } @@ -554,6 +555,20 @@ fun unite(flowA: Flow, flowB: Flow, flowC: Flow, transform ).map { transform(aResult, bResult, cResult) } } +fun unite(flowA: Flow, flowB: Flow, flowC: Flow, flowD: Flow, transform: (A?, B?, C?, D?) -> R): Flow { + var aResult: A? = null + var bResult: B? = null + var cResult: C? = null + var dResult: D? = null + + return merge( + flowA.onEach { aResult = it }, + flowB.onEach { bResult = it }, + flowC.onEach { cResult = it }, + flowD.onEach { dResult = it } + ).map { transform(aResult, bResult, cResult, dResult) } +} + fun firstNonEmpty( vararg sources: Flow> ): Flow> = accumulate(*sources) diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 1160b8fcbf..e236b41cb5 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,12 @@ + Already staking + You cannot stake with Direct Staking and Nomination Pools at the same time + + + Pool operations are not available + You can no longer use both Direct Staking and Pool Staking from the same account. To manage your Pool Staking you first need to unstake your tokens from Direct Staking. + Auto-balance nodes Enable connection diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt index 26f41b0627..6b94110c72 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt @@ -30,6 +30,7 @@ import io.novafoundation.nova.core_db.dao.DappAuthorizationDao import io.novafoundation.nova.core_db.dao.ExternalBalanceDao import io.novafoundation.nova.core_db.dao.FavouriteDAppsDao import io.novafoundation.nova.core_db.dao.GovernanceDAppsDao +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.NftDao @@ -44,6 +45,7 @@ import io.novafoundation.nova.core_db.dao.StorageDao import io.novafoundation.nova.core_db.dao.TokenDao import io.novafoundation.nova.core_db.dao.WalletConnectSessionsDao import io.novafoundation.nova.core_db.migrations.AddAdditionalFieldToChains_12_13 +import io.novafoundation.nova.core_db.migrations.AddBalanceHolds_60_61 import io.novafoundation.nova.core_db.migrations.AddBalanceModesToAssets_51_52 import io.novafoundation.nova.core_db.migrations.AddBrowserHostSettings_34_35 import io.novafoundation.nova.core_db.migrations.AddBuyProviders_7_8 @@ -59,7 +61,6 @@ import io.novafoundation.nova.core_db.migrations.AddExtrinsicContentField_37_38 import io.novafoundation.nova.core_db.migrations.AddFavouriteDApps_9_10 import io.novafoundation.nova.core_db.migrations.AddFungibleNfts_55_56 import io.novafoundation.nova.core_db.migrations.AddGloballyUniqueIdToMetaAccounts_58_59 -import io.novafoundation.nova.core_db.migrations.ChainNetworkManagement_59_60 import io.novafoundation.nova.core_db.migrations.AddGovernanceDapps_25_26 import io.novafoundation.nova.core_db.migrations.AddGovernanceExternalApiToChain_27_28 import io.novafoundation.nova.core_db.migrations.AddGovernanceFlagToChains_24_25 @@ -84,6 +85,7 @@ import io.novafoundation.nova.core_db.migrations.AddVersioningToGovernanceDapps_ import io.novafoundation.nova.core_db.migrations.AddWalletConnectSessions_39_40 import io.novafoundation.nova.core_db.migrations.AssetTypes_2_3 import io.novafoundation.nova.core_db.migrations.BetterChainDiffing_8_9 +import io.novafoundation.nova.core_db.migrations.ChainNetworkManagement_59_60 import io.novafoundation.nova.core_db.migrations.ChainPushSupport_56_57 import io.novafoundation.nova.core_db.migrations.ChangeAsset_3_4 import io.novafoundation.nova.core_db.migrations.ChangeChainNodes_20_21 @@ -105,6 +107,7 @@ import io.novafoundation.nova.core_db.migrations.WatchOnlyChainAccounts_16_17 import io.novafoundation.nova.core_db.model.AccountLocal import io.novafoundation.nova.core_db.model.AccountStakingLocal import io.novafoundation.nova.core_db.model.AssetLocal +import io.novafoundation.nova.core_db.model.BalanceHoldLocal import io.novafoundation.nova.core_db.model.BalanceLockLocal import io.novafoundation.nova.core_db.model.BrowserHostSettingsLocal import io.novafoundation.nova.core_db.model.CoinPriceLocal @@ -142,7 +145,7 @@ import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal @Database( - version = 60, + version = 61, entities = [ AccountLocal::class, NodeLocal::class, @@ -181,6 +184,7 @@ import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal StakingRewardPeriodLocal::class, ExternalBalanceLocal::class, ProxyAccountLocal::class, + BalanceHoldLocal::class, NodeSelectionPreferencesLocal::class ], ) @@ -236,7 +240,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(ChangeSessionTopicToParing_52_53, AddConnectionStateToChains_53_54, AddProxyAccount_54_55) .addMigrations(AddFungibleNfts_55_56, ChainPushSupport_56_57) .addMigrations(AddLocalMigratorVersionToChainRuntimes_57_58, AddGloballyUniqueIdToMetaAccounts_58_59) - .addMigrations(ChainNetworkManagement_59_60) + .addMigrations(ChainNetworkManagement_59_60, AddBalanceHolds_60_61) .build() } return instance!! @@ -294,4 +298,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun stakingRewardPeriodDao(): StakingRewardPeriodDao abstract fun externalBalanceDao(): ExternalBalanceDao + + abstract fun holdsDao(): HoldsDao } diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/dao/HoldsDao.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/dao/HoldsDao.kt new file mode 100644 index 0000000000..d85f4f7177 --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/dao/HoldsDao.kt @@ -0,0 +1,37 @@ +package io.novafoundation.nova.core_db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import io.novafoundation.nova.core_db.model.BalanceHoldLocal +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class HoldsDao { + + @Transaction + open suspend fun updateHolds( + holds: List, + metaId: Long, + chainId: String, + chainAssetId: Int + ) { + deleteHolds(metaId, chainId, chainAssetId) + + insert(holds) + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + protected abstract fun insert(holds: List) + + @Query("DELETE FROM holds WHERE metaId = :metaId AND chainId = :chainId AND assetId = :chainAssetId") + protected abstract fun deleteHolds(metaId: Long, chainId: String, chainAssetId: Int) + + @Query("SELECT * FROM holds WHERE metaId = :metaId") + abstract fun observeHoldsForMetaAccount(metaId: Long): Flow> + + @Query("SELECT * FROM holds WHERE metaId = :metaId AND chainId = :chainId AND assetId = :chainAssetId") + abstract fun observeBalanceHolds(metaId: Long, chainId: String, chainAssetId: Int): Flow> +} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbApi.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbApi.kt index cdd1a42058..59ea12f82e 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbApi.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbApi.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.core_db.dao.DappAuthorizationDao import io.novafoundation.nova.core_db.dao.ExternalBalanceDao import io.novafoundation.nova.core_db.dao.FavouriteDAppsDao import io.novafoundation.nova.core_db.dao.GovernanceDAppsDao +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.NftDao @@ -83,4 +84,6 @@ interface DbApi { val stakingDashboardDao: StakingDashboardDao val externalBalanceDao: ExternalBalanceDao + + val holdsDao: HoldsDao } diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbModule.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbModule.kt index bad50411d0..4198333fbf 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbModule.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/di/DbModule.kt @@ -18,6 +18,7 @@ import io.novafoundation.nova.core_db.dao.DappAuthorizationDao import io.novafoundation.nova.core_db.dao.ExternalBalanceDao import io.novafoundation.nova.core_db.dao.FavouriteDAppsDao import io.novafoundation.nova.core_db.dao.GovernanceDAppsDao +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.NftDao @@ -198,4 +199,10 @@ class DbModule { fun provideExternalBalanceDao(appDatabase: AppDatabase): ExternalBalanceDao { return appDatabase.externalBalanceDao() } + + @Provides + @ApplicationScope + fun provideHoldsDao(appDatabase: AppDatabase): HoldsDao { + return appDatabase.holdsDao() + } } diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/60_61_AddBalanceHolds.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/60_61_AddBalanceHolds.kt new file mode 100644 index 0000000000..7a9751d31b --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/60_61_AddBalanceHolds.kt @@ -0,0 +1,25 @@ +package io.novafoundation.nova.core_db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val AddBalanceHolds_60_61 = object : Migration(60, 61) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `holds` ( + `metaId` INTEGER NOT NULL, + `chainId` TEXT NOT NULL, + `assetId` INTEGER NOT NULL, + `amount` TEXT NOT NULL, + `id_module` TEXT NOT NULL, + `id_reason` TEXT NOT NULL, + PRIMARY KEY(`metaId`, `chainId`, `assetId`, `id_module`, `id_reason`), + FOREIGN KEY(`metaId`) REFERENCES `meta_accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , + FOREIGN KEY(`chainId`) REFERENCES `chains`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , + FOREIGN KEY(`assetId`, `chainId`) REFERENCES `chain_assets`(`id`, `chainId`) ON UPDATE NO ACTION ON DELETE CASCADE + ) + """.trimIndent() + ) + } +} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceHoldLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceHoldLocal.kt new file mode 100644 index 0000000000..d769de1482 --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/BalanceHoldLocal.kt @@ -0,0 +1,44 @@ +package io.novafoundation.nova.core_db.model + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal +import io.novafoundation.nova.core_db.model.chain.ChainLocal +import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal +import java.math.BigInteger + +@Entity( + tableName = "holds", + primaryKeys = ["metaId", "chainId", "assetId", "id_module", "id_reason"], + foreignKeys = [ + ForeignKey( + entity = MetaAccountLocal::class, + parentColumns = ["id"], + childColumns = ["metaId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = ChainLocal::class, + parentColumns = ["id"], + childColumns = ["chainId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = ChainAssetLocal::class, + parentColumns = ["id", "chainId"], + childColumns = ["assetId", "chainId"], + onDelete = ForeignKey.CASCADE + ) + ], +) +class BalanceHoldLocal( + val metaId: Long, + val chainId: String, + val assetId: Int, + @Embedded(prefix = "id_") val id: HoldIdLocal, + val amount: BigInteger +) { + + class HoldIdLocal(val module: String, val reason: String) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt index da8a1954ca..1ce1f7e787 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt @@ -21,6 +21,7 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.QrCodeGenerator import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService @@ -56,6 +57,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.coingecko.Coingeck import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryTokenUseCase @@ -243,5 +245,9 @@ interface AssetsFeatureDependencies { val chainStateRepository: ChainStateRepository + val holdsRepository: BalanceHoldsRepository + + val holdsDao: HoldsDao + val coinGeckoLinkParser: CoinGeckoLinkParser } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/breakdown/BalanceBreakdownInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/breakdown/BalanceBreakdownInteractor.kt index 5f39f86c26..12f9fdb615 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/breakdown/BalanceBreakdownInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/breakdown/BalanceBreakdownInteractor.kt @@ -1,17 +1,23 @@ package io.novafoundation.nova.feature_assets.domain.breakdown import io.novafoundation.nova.common.utils.formatting.ABBREVIATED_SCALE +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.percentage +import io.novafoundation.nova.common.utils.sumByBigInteger import io.novafoundation.nova.common.utils.unite import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdown.PercentageAmount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceBreakdownIds +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.domain.model.unlabeledReserves import io.novafoundation.nova.feature_wallet_api.presentation.formatters.balanceId import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -48,7 +54,8 @@ class BalanceBreakdown( class BalanceBreakdownInteractor( private val accountRepository: AccountRepository, - private val balanceLocksRepository: BalanceLocksRepository + private val balanceLocksRepository: BalanceLocksRepository, + private val balanceHoldsRepository: BalanceHoldsRepository, ) { private class TotalAmount( @@ -65,17 +72,23 @@ class BalanceBreakdownInteractor( unite( assetsFlow, balanceLocksRepository.observeLocksForMetaAccount(metaAccount), + balanceHoldsRepository.observeHoldsForMetaAccount(metaAccount.id), externalBalancesFlow - ) { assets, locks, externalBalances -> + ) { assets, locks, holds, externalBalances -> if (assets == null) { BalanceBreakdown.empty() } else { val assetsByChainId = assets.associateBy { it.token.configuration.fullId } val locksItems = mapLocks(assetsByChainId, locks.orEmpty()) + val holdsItems = mapHolds(assetsByChainId, holds.orEmpty()) val externalBalancesItems = mapExternalBalances(assetsByChainId, externalBalances.orEmpty()) - val reserved = getReservedBreakdown(assets) - val breakdown = locksItems + externalBalancesItems + reserved + val holdsByAsset = holds.orEmpty() + .groupBy { it.chainAsset.fullId } + .mapValues { (_, holds) -> holds.sumByBigInteger { it.amountInPlanks } } + val reserved = getReservedBreakdown(assets, holdsByAsset) + + val breakdown = locksItems + holdsItems + externalBalancesItems + reserved val totalAmount = calculateTotalBalance(assets, externalBalancesItems) val (transferablePercentage, locksPercentage) = percentage( @@ -109,6 +122,21 @@ class BalanceBreakdownInteractor( } } + private fun mapHolds( + assetsByChainId: Map, + holds: List + ): List { + return holds.mapNotNull { hold -> + assetsByChainId[hold.chainAsset.fullId]?.let { asset -> + BalanceBreakdown.BreakdownItem( + id = hold.identifier, + token = asset.token, + amountInPlanks = hold.amountInPlanks, + ) + } + } + } + private fun mapExternalBalances( assetsByChainId: Map, externalBalances: List @@ -142,14 +170,18 @@ class BalanceBreakdownInteractor( return TotalAmount(total, transferable, locks) } - private fun getReservedBreakdown(assets: List): List { + private fun getReservedBreakdown(assets: List, holds: Map): List { return assets .filter { it.reservedInPlanks > BigInteger.ZERO } - .map { + .mapNotNull { + val labeledReserves = holds[it.token.configuration.fullId].orZero() + val unlabeledReserves = it.unlabeledReserves(labeledReserves) + if (unlabeledReserves <= BigInteger.ZERO) return@mapNotNull null + BalanceBreakdown.BreakdownItem( id = BalanceBreakdownIds.RESERVED, token = it.token, - amountInPlanks = it.reservedInPlanks + amountInPlanks = unlabeledReserves ) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractor.kt index 8d2bae90ec..29784f4e5d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractor.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_assets.domain.locks +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import kotlinx.coroutines.flow.Flow @@ -7,4 +8,6 @@ import kotlinx.coroutines.flow.Flow interface BalanceLocksInteractor { fun balanceLocksFlow(chainId: ChainId, chainAssetId: Int): Flow> + + fun balanceHoldsFlow(chainId: ChainId, chainAssetId: Int): Flow> } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractorImpl.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractorImpl.kt index d76daa2fcf..9bf2cb8d54 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractorImpl.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/locks/BalanceLocksInteractorImpl.kt @@ -1,23 +1,37 @@ package io.novafoundation.nova.feature_assets.domain.locks +import io.novafoundation.nova.common.utils.flowOfAll +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow class BalanceLocksInteractorImpl( private val chainRegistry: ChainRegistry, private val balanceLocksRepository: BalanceLocksRepository, + private val balanceHoldsRepository: BalanceHoldsRepository, + private val accountRepository: AccountRepository, ) : BalanceLocksInteractor { override fun balanceLocksFlow(chainId: ChainId, chainAssetId: Int): Flow> { - return flow { + return flowOfAll { val (chain, chainAsset) = chainRegistry.chainWithAsset(chainId, chainAssetId) - emitAll(balanceLocksRepository.observeBalanceLocks(chain, chainAsset)) + val selectedAccount = accountRepository.getSelectedMetaAccount() + balanceLocksRepository.observeBalanceLocks(selectedAccount.id, chain, chainAsset) + } + } + + override fun balanceHoldsFlow(chainId: ChainId, chainAssetId: Int): Flow> { + return flowOfAll { + val chainAsset = chainRegistry.asset(chainId, chainAssetId) + val selectedAccount = accountRepository.getSelectedMetaAccount() + balanceHoldsRepository.observeBalanceHolds(selectedAccount.id, chainAsset) } } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt index 0d4aef5c50..f16e3389f5 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/BalanceDetailViewModel.kt @@ -27,9 +27,11 @@ import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.domain.model.unlabeledReserves import io.novafoundation.nova.feature_wallet_api.presentation.formatters.balanceId import io.novafoundation.nova.feature_wallet_api.presentation.formatters.mapBalanceIdToUi import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload @@ -78,8 +80,10 @@ class BalanceDetailViewModel( .share() private val balanceLocksFlow = balanceLocksInteractor.balanceLocksFlow(assetPayload.chainId, assetPayload.chainAssetId) - .inBackground() - .share() + .shareInBackground() + + private val balanceHoldsFlow = balanceLocksInteractor.balanceHoldsFlow(assetPayload.chainId, assetPayload.chainAssetId) + .shareInBackground() private val selectedAccountFlow = accountUseCase.selectedMetaAccountFlow() .share() @@ -94,8 +98,8 @@ class BalanceDetailViewModel( .inBackground() .share() - private val lockedBalanceModel = combine(balanceLocksFlow, externalBalancesFlow, assetFlow) { locks, externalBalances, asset -> - mapBalanceLocksToUi(locks, externalBalances, asset) + private val lockedBalanceModel = combine(balanceLocksFlow, balanceHoldsFlow, externalBalancesFlow, assetFlow) { locks, holds, externalBalances, asset -> + mapBalanceLocksToUi(locks, holds, externalBalances, asset) } .inBackground() .share() @@ -204,6 +208,7 @@ class BalanceDetailViewModel( private fun mapBalanceLocksToUi( balanceLocks: List, + holds: List, externalBalances: List, asset: Asset ): BalanceLocksModel { @@ -214,9 +219,18 @@ class BalanceDetailViewModel( ) } + val mappedHolds = holds.map { + BalanceLocksModel.Lock( + mapBalanceIdToUi(resourceManager, it.identifier), + mapAmountToAmountModel(it.amountInPlanks, asset) + ) + } + + val unlabeledReserves = asset.unlabeledReserves(holds) + val reservedBalance = BalanceLocksModel.Lock( resourceManager.getString(R.string.wallet_balance_reserved), - mapAmountToAmountModel(asset.reserved, asset) + mapAmountToAmountModel(unlabeledReserves, asset) ) val external = externalBalances.map { externalBalance -> @@ -228,6 +242,7 @@ class BalanceDetailViewModel( val locks = buildList { addAll(mappedLocks) + addAll(mappedHolds) add(reservedBalance) addAll(external) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt index ad7f3b52f5..224b1b101d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/di/BalanceDetailModule.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.common.di.scope.ScreenScope 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.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase import io.novafoundation.nova.feature_assets.domain.WalletInteractor @@ -28,6 +29,7 @@ import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -39,11 +41,15 @@ class BalanceDetailModule { @ScreenScope fun provideBalanceLocksInteractor( chainRegistry: ChainRegistry, - balanceLocksRepository: BalanceLocksRepository + balanceLocksRepository: BalanceLocksRepository, + balanceHoldsRepository: BalanceHoldsRepository, + accountRepository: AccountRepository ): BalanceLocksInteractor { return BalanceLocksInteractorImpl( - chainRegistry, - balanceLocksRepository + chainRegistry = chainRegistry, + balanceLocksRepository = balanceLocksRepository, + balanceHoldsRepository = balanceHoldsRepository, + accountRepository = accountRepository ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt index 467f97240b..779ddb96eb 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt @@ -17,32 +17,18 @@ import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdownInteractor -import io.novafoundation.nova.feature_assets.domain.locks.BalanceLocksInteractor -import io.novafoundation.nova.feature_assets.domain.locks.BalanceLocksInteractorImpl import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.list.BalanceListViewModel import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module(includes = [ViewModelModule::class]) class BalanceListModule { - @Provides - @ScreenScope - fun provideBalanceLocksInteractor( - chainRegistry: ChainRegistry, - balanceLocksRepository: BalanceLocksRepository - ): BalanceLocksInteractor { - return BalanceLocksInteractorImpl( - chainRegistry, - balanceLocksRepository - ) - } - @Provides @ScreenScope fun provideInteractor( @@ -55,11 +41,13 @@ class BalanceListModule { @ScreenScope fun provideBalanceBreakdownInteractor( accountRepository: AccountRepository, - balanceLocksRepository: BalanceLocksRepository + balanceLocksRepository: BalanceLocksRepository, + balanceHoldsRepository: BalanceHoldsRepository ): BalanceBreakdownInteractor { return BalanceBreakdownInteractor( accountRepository, - balanceLocksRepository + balanceLocksRepository, + balanceHoldsRepository ) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt index 2882a786e3..d0ce741495 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt @@ -143,7 +143,9 @@ class RealNewDelegationChooseAmountInteractor( val blockDurationEstimator = chainStateRepository.blockDurationEstimator(chain.id) - val balanceLocksFlow = locksRepository.observeBalanceLocks(chain, chainAsset) + val metaId = accountRepository.getSelectedMetaAccount().id + + val balanceLocksFlow = locksRepository.observeBalanceLocks(metaId, chain, chainAsset) return balanceLocksFlow.map { locks -> RealDelegateAssistant( diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt index dc7a4387e3..a3a9987cc8 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt @@ -134,9 +134,11 @@ class RealGovernanceUnlockInteractor( val governanceSource = governanceSourceRegistry.sourceFor(governanceSelectedOption) + val metaAccount = accountRepository.getSelectedMetaAccount() + combine( assetFlow, - balanceLocksRepository.observeBalanceLocks(chain, chainAsset), + balanceLocksRepository.observeBalanceLocks(metaAccount.id, chain, chainAsset), locksOverviewFlow(scope) ) { assetFlow, balanceLocks, locksOverview -> governanceSource.constructGovernanceUnlockAffects(assetFlow, balanceLocks, locksOverview) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt index 6b9044f0b9..e380b90d92 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/RealVoteReferendumInteractor.kt @@ -66,7 +66,7 @@ class RealVoteReferendumInteractor( val voterAccountId = metaAccount.accountIdIn(governanceOption.assetWithChain.chain)!! - voteAssistantFlowSuspend(governanceOption, voterAccountId, referendumId) + voteAssistantFlowSuspend(governanceOption, voterAccountId, metaAccount.id, referendumId) } } @@ -101,6 +101,7 @@ class RealVoteReferendumInteractor( private suspend fun voteAssistantFlowSuspend( selectedGovernanceOption: SupportedGovernanceOption, voterAccountId: AccountId, + metaId: Long, referendumId: ReferendumId ): Flow { val chain = selectedGovernanceOption.assetWithChain.chain @@ -122,7 +123,7 @@ class RealVoteReferendumInteractor( val selectedReferendumFlow = governanceSource.referenda.onChainReferendumFlow(chain.id, referendumId) .filterNotNull() - val balanceLocksFlow = locksRepository.observeBalanceLocks(chain, chainAsset) + val balanceLocksFlow = locksRepository.observeBalanceLocks(metaId, chain, chainAsset) return combine(votingInformation, selectedReferendumFlow, balanceLocksFlow) { (locksByTrack, voting, votedReferenda), selectedReferendum, locks -> val blockDurationEstimator = chainStateRepository.blockDurationEstimator(chain.id) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/api/DelegatedStakingApi.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/api/DelegatedStakingApi.kt new file mode 100644 index 0000000000..0dc62d1839 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/api/DelegatedStakingApi.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api + +import io.novafoundation.nova.common.utils.delegatedStaking +import io.novafoundation.nova.common.utils.delegatedStakingOrNull +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.DelegatedStakingDelegation +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.bindDelegatedStakingDelegation +import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext +import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule +import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 +import io.novafoundation.nova.runtime.storage.source.query.api.storage1 +import io.novasama.substrate_sdk_android.runtime.AccountId +import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata +import io.novasama.substrate_sdk_android.runtime.metadata.module.Module + +@JvmInline +value class DelegatedStakingApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.delegatedStaking: DelegatedStakingApi + get() = DelegatedStakingApi(delegatedStaking()) + +context(StorageQueryContext) +val RuntimeMetadata.delegatedStakingOrNull: DelegatedStakingApi? + get() = delegatedStakingOrNull()?.let(::DelegatedStakingApi) + +context(StorageQueryContext) +val DelegatedStakingApi.delegators: QueryableStorageEntry1 + get() = storage1("Delegators", binding = { decoded, _ -> bindDelegatedStakingDelegation(decoded) }) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/calls/ExtrinsicBuilderExt.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/calls/NominationPoolsCalls.kt similarity index 89% rename from feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/calls/ExtrinsicBuilderExt.kt rename to feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/calls/NominationPoolsCalls.kt index a0129d9836..37c1bf459a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/calls/ExtrinsicBuilderExt.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/calls/NominationPoolsCalls.kt @@ -71,6 +71,16 @@ fun NominationPoolsCalls.claimPayout() { ) } +fun NominationPoolsCalls.migrateDelegation(memberAccount: AccountId) { + extrinsicBuilder.call( + moduleName = Modules.NOMINATION_POOLS, + callName = "migrate_delegation", + arguments = mapOf( + "member_account" to AddressInstanceConstructor.constructInstance(extrinsicBuilder.runtime.typeRegistry, memberAccount), + ) + ) +} + private fun NominationPoolBondExtraSource.prepareForEncoding(): DictEnum.Entry<*> { return when (this) { is NominationPoolBondExtraSource.FreeBalance -> DictEnum.Entry("FreeBalance", amount) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/models/DelegatedStakingDelegation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/models/DelegatedStakingDelegation.kt new file mode 100644 index 0000000000..98d1c851b0 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/models/DelegatedStakingDelegation.kt @@ -0,0 +1,17 @@ +package io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models + +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance + +class DelegatedStakingDelegation( + val amount: Balance +) + +fun bindDelegatedStakingDelegation(decoded: Any): DelegatedStakingDelegation { + val asStruct = decoded.castToStruct() + + return DelegatedStakingDelegation( + amount = bindNumber(asStruct["amount"]) + ) +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/DelegatedStakeUpdater.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/DelegatedStakeUpdater.kt new file mode 100644 index 0000000000..cb87a90eb9 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/DelegatedStakeUpdater.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater + +import io.novafoundation.nova.common.utils.delegatedStakingOrNull +import io.novafoundation.nova.core.storage.StorageCache +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope +import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater.base.NominationPoolUpdater +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater +import io.novafoundation.nova.runtime.state.chain +import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot +import io.novasama.substrate_sdk_android.runtime.metadata.storage +import io.novasama.substrate_sdk_android.runtime.metadata.storageKey + +class DelegatedStakeUpdater( + override val scope: AccountUpdateScope, + private val stakingSharedState: StakingSharedState, + chainRegistry: ChainRegistry, + storageCache: StorageCache +) : SingleStorageKeyUpdater(scope, stakingSharedState, chainRegistry, storageCache), NominationPoolUpdater { + + override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: MetaAccount): String? { + val chain = stakingSharedState.chain() + val accountId = scopeValue.accountIdIn(chain) ?: return null + + return runtime.metadata.delegatedStakingOrNull()?.storage("Delegators")?.storageKey(runtime, accountId) + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/PooledBalanceUpdater.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/PooledBalanceUpdater.kt index 454c58b959..7475e17dc0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/PooledBalanceUpdater.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/network/blockhain/updater/PooledBalanceUpdater.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.core.updater.Updater import io.novafoundation.nova.core_db.dao.ExternalBalanceDao @@ -15,6 +16,8 @@ import io.novafoundation.nova.feature_staking_api.domain.nominationPool.model.Po import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.ledger import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.staking import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.bondedPools +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.delegatedStakingOrNull +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.delegators import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.nominationPools import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.poolMembers import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.subPoolsStorage @@ -30,8 +33,10 @@ import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull import io.novafoundation.nova.runtime.storage.source.query.metadata +import io.novasama.substrate_sdk_android.runtime.AccountId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -88,13 +93,15 @@ class PooledBalanceUpdater( val poolMemberFlow = metadata.nominationPools.poolMembers .observe(accountId) + val inAccountPoolStakeFlow = observeInAccountPoolStake(accountId) + val poolWithBalance = poolMemberFlow .map { it?.poolId } .distinctUntilChanged() .flatMapLatest(::subscribeToPoolWithBalance) - combine(poolMemberFlow, poolWithBalance) { poolMember, totalPoolBalances -> - insertExternalBalance(poolMember, totalPoolBalances, metaAccount) + combine(poolMemberFlow, poolWithBalance, inAccountPoolStakeFlow) { poolMember, totalPoolBalances, inAccountPoolStake -> + insertExternalBalance(poolMember, totalPoolBalances, metaAccount, inAccountPoolStake) } }.noSideAffects() } @@ -116,13 +123,29 @@ class PooledBalanceUpdater( } } + context(StorageQueryContext) + @Suppress("IfThenToElvis") + private fun observeInAccountPoolStake(accountId: AccountId): Flow { + val delegatedStakingPallet = metadata.delegatedStakingOrNull + + return if (delegatedStakingPallet != null) { + delegatedStakingPallet.delegators.observe(accountId).map { + it?.amount.orZero() + } + } else { + flowOf(Balance.ZERO) + } + } + private suspend fun insertExternalBalance( poolMember: PoolMember?, totalPoolBalances: TotalPoolBalances?, metaAccount: MetaAccount, + inAccountPoolStake: Balance, ) { val totalStake = if (poolMember != null && totalPoolBalances != null) { - totalPoolBalances.totalStakeOf(poolMember) + // Only use stake part that is not in account as external balance entry + totalPoolBalances.totalStakeOf(poolMember) - inAccountPoolStake } else { Balance.ZERO } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolDelegatedStakeRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolDelegatedStakeRepository.kt new file mode 100644 index 0000000000..4867bb2478 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolDelegatedStakeRepository.kt @@ -0,0 +1,49 @@ +package io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository + +import io.novafoundation.nova.common.utils.delegatedStakingOrNull +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.delegatedStaking +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.api.delegators +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.model.DelegatedStakeMigrationState +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.multiNetwork.getRuntime +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.metadata +import io.novasama.substrate_sdk_android.runtime.AccountId + +interface NominationPoolDelegatedStakeRepository { + + suspend fun hasMigratedToDelegatedStake(chainId: ChainId): Boolean + + suspend fun poolMemberMigrationState(chainId: ChainId, accountId: AccountId): DelegatedStakeMigrationState +} + +suspend fun NominationPoolDelegatedStakeRepository.shouldMigrateToDelegatedStake(chainId: ChainId, accountId: AccountId): Boolean { + return poolMemberMigrationState(chainId, accountId) == DelegatedStakeMigrationState.NEEDS_MIGRATION +} + +class RealNominationPoolDelegatedStakeRepository( + private val localStorageSource: StorageDataSource, + private val chainRegistry: ChainRegistry, +) : NominationPoolDelegatedStakeRepository { + + override suspend fun hasMigratedToDelegatedStake(chainId: ChainId): Boolean { + return chainRegistry.getRuntime(chainId).metadata.delegatedStakingOrNull() != null + } + + override suspend fun poolMemberMigrationState(chainId: ChainId, accountId: AccountId): DelegatedStakeMigrationState { + return when { + !hasMigratedToDelegatedStake(chainId) -> DelegatedStakeMigrationState.NOT_SUPPORTED + hasDelegatedStake(chainId, accountId) -> DelegatedStakeMigrationState.MIGRATED + else -> DelegatedStakeMigrationState.NEEDS_MIGRATION + } + } + + private suspend fun hasDelegatedStake(chainId: ChainId, accountId: AccountId): Boolean { + val delegatedStake = localStorageSource.query(chainId) { + metadata.delegatedStaking.delegators.query(accountId) + } + + return delegatedStake != null + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolMembersRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolMembersRepository.kt index 5209cf4096..2ef0fa4f21 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolMembersRepository.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/nominationPools/repository/NominationPoolMembersRepository.kt @@ -16,11 +16,14 @@ interface NominationPoolMembersRepository { fun observePoolMember(chainId: ChainId, accountId: AccountId): Flow + suspend fun getPoolMember(chainId: ChainId, accountId: AccountId): PoolMember? + suspend fun getPendingRewards(poolMemberAccountId: AccountId, chainId: ChainId): Balance } class RealNominationPoolMembersRepository( private val localStorageSource: StorageDataSource, + private val remoteStorageSource: StorageDataSource, private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, ) : NominationPoolMembersRepository { @@ -30,6 +33,12 @@ class RealNominationPoolMembersRepository( } } + override suspend fun getPoolMember(chainId: ChainId, accountId: AccountId): PoolMember? { + return remoteStorageSource.query(chainId) { + metadata.nominationPools.poolMembers.query(accountId) + } + } + override suspend fun getPendingRewards(poolMemberAccountId: AccountId, chainId: ChainId): Balance { return multiChainRuntimeCallsApi.forChain(chainId).call( section = "NominationPoolsApi", diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt index 207bf08991..79016e0fd9 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt @@ -35,6 +35,8 @@ import io.novafoundation.nova.feature_staking_impl.data.dashboard.repository.Sta import io.novafoundation.nova.feature_staking_impl.data.network.subquery.StakingApi import io.novafoundation.nova.feature_staking_impl.data.network.subquery.SubQueryValidatorSetFetcher import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater.RealPooledBalanceUpdaterFactory +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolDelegatedStakeRepository +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolMembersRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolStateRepository import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.RoundDurationEstimator import io.novafoundation.nova.feature_staking_impl.data.repository.BagListRepository @@ -78,6 +80,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.alerts.AlertsInteracto import io.novafoundation.nova.feature_staking_impl.domain.common.EraTimeCalculatorFactory import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.era.StakingEraInteractorFactory +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.payout.PayoutInteractor import io.novafoundation.nova.feature_staking_impl.domain.period.RealStakingRewardPeriodInteractor import io.novafoundation.nova.feature_staking_impl.domain.period.StakingRewardPeriodInteractor @@ -692,4 +695,18 @@ class StakingFeatureModule { ): StakingGlobalConfigRepository { return RealStakingGlobalConfigRepository(api) } + + @Provides + @FeatureScope + fun provideStakingConflictsValidationFactory( + stakingRepository: StakingRepository, + delegatedStakeRepository: NominationPoolDelegatedStakeRepository, + nominationPoolStakingRepository: NominationPoolMembersRepository + ): StakingTypesConflictValidationFactory { + return StakingTypesConflictValidationFactory( + stakingRepository = stakingRepository, + delegatedStakeRepository = delegatedStakeRepository, + nominationPoolStakingRepository = nominationPoolStakingRepository + ) + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolModule.kt index 7f0392d6ed..36fec3c266 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolModule.kt @@ -7,19 +7,21 @@ import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.nominationPools.datasource.KnownMaxUnlockingOverwrites import io.novafoundation.nova.feature_staking_impl.data.nominationPools.datasource.RealKnownMaxUnlockingOverwrites import io.novafoundation.nova.feature_staking_impl.data.nominationPools.pool.FixedKnownNovaPools import io.novafoundation.nova.feature_staking_impl.data.nominationPools.pool.KnownNovaPools -import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_staking_impl.data.nominationPools.pool.PoolImageDataSource import io.novafoundation.nova.feature_staking_impl.data.nominationPools.pool.PredefinedPoolImageDataSource import io.novafoundation.nova.feature_staking_impl.data.nominationPools.pool.RealPoolAccountDerivation +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolDelegatedStakeRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolGlobalsRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolMembersRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolStateRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolUnbondRepository +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolDelegatedStakeRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolGlobalsRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolMembersRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolStateRepository @@ -31,6 +33,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedCo import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolMemberUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.RealNominationPoolMemberUseCase +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.RealDelegatedStakeMigrationUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.hints.NominationPoolHintsUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.hints.RealNominationPoolHintsUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.rewards.NominationPoolRewardCalculatorFactory @@ -54,6 +58,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.nominationPools. import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.storage.source.StorageDataSource import javax.inject.Named @@ -112,9 +117,10 @@ class NominationPoolModule { @FeatureScope fun provideNominationPoolMembersRepository( @Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource, + @Named(REMOTE_STORAGE_SOURCE) remoteDataSource: StorageDataSource, multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, ): NominationPoolMembersRepository { - return RealNominationPoolMembersRepository(localStorageSource, multiChainRuntimeCallsApi) + return RealNominationPoolMembersRepository(localStorageSource, remoteDataSource, multiChainRuntimeCallsApi) } @Provides @@ -274,4 +280,23 @@ class NominationPoolModule { nominationPoolStateRepository = nominationPoolStateRepository, poolStateRepository = poolStateRepository ) + + @Provides + @FeatureScope + fun provideNominationPoolDelegatedStakeRepository( + @Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource, + chainRegistry: ChainRegistry + ): NominationPoolDelegatedStakeRepository { + return RealNominationPoolDelegatedStakeRepository(localStorageSource, chainRegistry) + } + + @Provides + @FeatureScope + fun provideDelegatedStakeMigrationUseCase( + delegatedStakeRepository: NominationPoolDelegatedStakeRepository, + stakingSharedState: StakingSharedState, + accountRepository: AccountRepository + ): DelegatedStakeMigrationUseCase { + return RealDelegatedStakeMigrationUseCase(delegatedStakeRepository, stakingSharedState, accountRepository) + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolStakingUpdatersModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolStakingUpdatersModule.kt index 35bad3ee62..97bade0c1b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolStakingUpdatersModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/nominationPool/NominationPoolStakingUpdatersModule.kt @@ -4,6 +4,7 @@ import dagger.Module import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.core.storage.StorageCache +import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.ActiveEraUpdater import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.CurrentEraUpdater @@ -16,6 +17,7 @@ import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.update import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.session.EraStartSessionIndexUpdater import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.session.GenesisSlotUpdater import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater.CounterForPoolMembersUpdater +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater.DelegatedStakeUpdater import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater.LastPoolIdUpdater import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater.MaxPoolMembersPerPoolUpdater import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.updater.MaxPoolMembersUpdater @@ -48,6 +50,20 @@ class NominationPoolStakingUpdatersModule { chainRegistry = chainRegistry ) + @Provides + @FeatureScope + fun provideDelegatedStakeUpdater( + storageCache: StorageCache, + stakingSharedState: StakingSharedState, + chainRegistry: ChainRegistry, + scope: AccountUpdateScope + ) = DelegatedStakeUpdater( + storageCache = storageCache, + stakingSharedState = stakingSharedState, + chainRegistry = chainRegistry, + scope = scope + ) + @Provides @FeatureScope fun provideMinJoinBondUpdater( @@ -144,6 +160,7 @@ class NominationPoolStakingUpdatersModule { currentSessionIndexUpdater: CurrentSessionIndexUpdater, eraStartSessionIndexUpdater: EraStartSessionIndexUpdater, parachainsUpdater: ParachainsUpdater, + delegatedStakeUpdater: DelegatedStakeUpdater, ) = StakingUpdaters.Group( lastPoolIdUpdater, minJoinBondUpdater, @@ -160,6 +177,7 @@ class NominationPoolStakingUpdatersModule { genesisSlotUpdater, currentSessionIndexUpdater, eraStartSessionIndexUpdater, - parachainsUpdater + parachainsUpdater, + delegatedStakeUpdater ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/startMultiStaking/StartMultiStakingModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/startMultiStaking/StartMultiStakingModule.kt index 606ff7aa75..9bf78a12f0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/startMultiStaking/StartMultiStakingModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/startMultiStaking/StartMultiStakingModule.kt @@ -7,6 +7,7 @@ import dagger.multibindings.IntoMap import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.pool.KnownNovaPools import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolGlobalsRepository @@ -14,6 +15,7 @@ import io.novafoundation.nova.feature_staking_impl.data.repository.StakingConsta import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.PoolAvailableBalanceValidationFactory +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.pools.NominationPoolProvider import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.pools.recommendation.NominationPoolRecommenderFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.selecting.SearchNominationPoolInteractor @@ -166,13 +168,17 @@ class StartMultiStakingModule { stakingSharedComputation: StakingSharedComputation, stakingRepository: StakingRepository, stakingConstantsRepository: StakingConstantsRepository, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory, + selectedAccountUseCase: SelectedAccountUseCase, ): SingleStakingPropertiesFactory { return DirectStakingPropertiesFactory( validatorRecommenderFactory = validatorRecommenderFactory, recommendationSettingsProviderFactory = recommendationSettingsProviderFactory, stakingSharedComputation = stakingSharedComputation, stakingRepository = stakingRepository, - stakingConstantsRepository = stakingConstantsRepository + stakingConstantsRepository = stakingConstantsRepository, + stakingTypesConflictValidationFactory = stakingTypesConflictValidationFactory, + selectedAccountUseCase = selectedAccountUseCase ) } @@ -186,6 +192,8 @@ class StartMultiStakingModule { availableBalanceResolver: NominationPoolsAvailableBalanceResolver, nominationPoolGlobalsRepository: NominationPoolGlobalsRepository, poolAvailableBalanceValidationFactory: PoolAvailableBalanceValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory, + selectedAccountUseCase: SelectedAccountUseCase, ): SingleStakingPropertiesFactory { return NominationPoolStakingPropertiesFactory( nominationPoolSharedComputation = nominationPoolSharedComputation, @@ -193,6 +201,8 @@ class StartMultiStakingModule { poolsAvailableBalanceResolver = availableBalanceResolver, nominationPoolGlobalsRepository = nominationPoolGlobalsRepository, poolAvailableBalanceValidationFactory = poolAvailableBalanceValidationFactory, + stakingTypesConflictValidationFactory = stakingTypesConflictValidationFactory, + selectedAccountUseCase = selectedAccountUseCase ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt index 8a0f4b66f5..0c60ba3244 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/NominationPoolsBondMoreInteractor.kt @@ -5,10 +5,13 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.NominationPoolBondExtraSource import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.bondExtra import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.nominationPools +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.state.chain +import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -22,12 +25,13 @@ interface NominationPoolsBondMoreInteractor { class RealNominationPoolsBondMoreInteractor( private val extrinsicService: ExtrinsicService, private val stakingSharedState: StakingSharedState, + private val migrationUseCase: DelegatedStakeMigrationUseCase ) : NominationPoolsBondMoreInteractor { override suspend fun estimateFee(bondMoreAmount: Balance): Fee { return withContext(Dispatchers.IO) { extrinsicService.estimateFee(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { - nominationPools.bondExtra(bondMoreAmount) + bondExtra(bondMoreAmount) } } } @@ -35,8 +39,14 @@ class RealNominationPoolsBondMoreInteractor( override suspend fun bondMore(bondMoreAmount: Balance): Result { return withContext(Dispatchers.IO) { extrinsicService.submitExtrinsic(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { - nominationPools.bondExtra(bondMoreAmount) + bondExtra(bondMoreAmount) } } } + + private suspend fun ExtrinsicBuilder.bondExtra(amount: Balance) { + migrationUseCase.migrateToDelegatedStakeIfNeeded() + + nominationPools.bondExtra(NominationPoolBondExtraSource.FreeBalance(amount)) + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt index 3002b7b804..ffdc1f9671 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/Declarations.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.PoolAvailableBalanceValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.PoolStateValidationFactory +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.validateNotDestroying import io.novafoundation.nova.feature_wallet_api.domain.validation.positiveAmount @@ -16,7 +17,10 @@ typealias NominationPoolsBondMoreValidationSystemBuilder = fun ValidationSystem.Companion.nominationPoolsBondMore( poolStateValidationFactory: PoolStateValidationFactory, poolAvailableBalanceValidationFactory: PoolAvailableBalanceValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory ): NominationPoolsBondMoreValidationSystem = ValidationSystem { + noStakingTypesConflict(stakingTypesConflictValidationFactory) + poolIsNotDestroying(poolStateValidationFactory) notUnstakingAll() @@ -26,6 +30,14 @@ fun ValidationSystem.Companion.nominationPoolsBondMore( positiveBond() } +private fun NominationPoolsBondMoreValidationSystemBuilder.noStakingTypesConflict(factory: StakingTypesConflictValidationFactory) { + factory.noStakingTypesConflict( + accountId = { it.poolMember.accountId }, + chainId = { it.asset.token.configuration.chainId }, + error = { NominationPoolsBondMoreValidationFailure.StakingTypesConflict } + ) +} + private fun NominationPoolsBondMoreValidationSystemBuilder.poolIsNotDestroying(factory: PoolStateValidationFactory) { factory.validateNotDestroying( poolId = { it.poolMember.poolId }, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationFailure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationFailure.kt index 2a77a97023..0be7caf8b7 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationFailure.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationFailure.kt @@ -13,4 +13,6 @@ sealed class NominationPoolsBondMoreValidationFailure { object PoolIsDestroying : NominationPoolsBondMoreValidationFailure() object UnstakingAll : NominationPoolsBondMoreValidationFailure() + + object StakingTypesConflict : NominationPoolsBondMoreValidationFailure() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/ValidationFailureUi.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/ValidationFailureUi.kt index 5d4e7e8e39..82cdb0eee4 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/ValidationFailureUi.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/ValidationFailureUi.kt @@ -8,7 +8,10 @@ import io.novafoundation.nova.feature_staking_impl.R import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.validations.NominationPoolsBondMoreValidationFailure.NotEnoughToBond import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.validations.NominationPoolsBondMoreValidationFailure.NotPositiveAmount import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.validations.NominationPoolsBondMoreValidationFailure.PoolIsDestroying +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.validations.NominationPoolsBondMoreValidationFailure.StakingTypesConflict +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.validations.NominationPoolsBondMoreValidationFailure.UnstakingAll import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.handlePoolAvailableBalanceError +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.handlePoolStakingTypesConflictValidationFailure import io.novafoundation.nova.feature_wallet_api.domain.validation.zeroAmount import java.math.BigDecimal @@ -34,9 +37,11 @@ fun nominationPoolsBondMoreValidationFailure( resourceManager.getString(R.string.nomination_pools_pool_destroying_error_message) ) - NominationPoolsBondMoreValidationFailure.UnstakingAll -> TransformedFailure.Default( + UnstakingAll -> TransformedFailure.Default( resourceManager.getString(R.string.staking_unable_to_stake_more_title) to resourceManager.getString(R.string.staking_unable_to_stake_more_message) ) + + StakingTypesConflict -> TransformedFailure.Default(handlePoolStakingTypesConflictValidationFailure(resourceManager)) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt index cbf1d56a9a..f7b84ad898 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/NominationPoolsClaimRewardsInteractor.kt @@ -11,7 +11,8 @@ import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network. import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.nominationPools import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolMembersRepository import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolMemberUseCase -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase +import io.novafoundation.nova.feature_staking_impl.presentation.nominationPools.claimRewards.PoolPendingRewards import io.novafoundation.nova.runtime.state.chain import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.Dispatchers @@ -23,7 +24,7 @@ import kotlinx.coroutines.withContext interface NominationPoolsClaimRewardsInteractor { - fun pendingRewardsFlow(): Flow + fun pendingRewardsFlow(): Flow suspend fun estimateFee(shouldRestake: Boolean): Fee @@ -35,14 +36,17 @@ class RealNominationPoolsClaimRewardsInteractor( private val poolMembersRepository: NominationPoolMembersRepository, private val stakingSharedState: StakingSharedState, private val extrinsicService: ExtrinsicService, + private val migrationUseCase: DelegatedStakeMigrationUseCase ) : NominationPoolsClaimRewardsInteractor { - override fun pendingRewardsFlow(): Flow { + override fun pendingRewardsFlow(): Flow { return poolMemberUseCase.currentPoolMemberFlow() .filterNotNull() .distinctUntilChangedBy { it.lastRecordedRewardCounter } .mapLatest { poolMember -> - poolMembersRepository.getPendingRewards(poolMember.accountId, stakingSharedState.chainId()) + val rewards = poolMembersRepository.getPendingRewards(poolMember.accountId, stakingSharedState.chainId()) + + PoolPendingRewards(rewards, poolMember) } } @@ -62,7 +66,9 @@ class RealNominationPoolsClaimRewardsInteractor( } } - private fun ExtrinsicBuilder.claimRewards(shouldRestake: Boolean) { + private suspend fun ExtrinsicBuilder.claimRewards(shouldRestake: Boolean) { + migrationUseCase.migrateToDelegatedStakeIfNeeded() + if (shouldRestake) { nominationPools.bondExtra(NominationPoolBondExtraSource.Rewards) } else { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt index 9d2d5ac58a..16324b030a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/Declarations.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claim import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_staking_impl.domain.common.validation.profitableAction +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.model.balanceCountedTowardsED import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance @@ -17,8 +18,11 @@ typealias NominationPoolsClaimRewardsValidationSystemBuilder = ValidationSystemBuilder fun ValidationSystem.Companion.nominationPoolsClaimRewards( - enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory + enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory, ): NominationPoolsClaimRewardsValidationSystem = ValidationSystem { + noStakingTypesConflict(stakingTypesConflictValidationFactory) + enoughToPayFees() sufficientCommissionBalanceToStayAboveED(enoughTotalToStayAboveEDValidationFactory) @@ -26,6 +30,14 @@ fun ValidationSystem.Companion.nominationPoolsClaimRewards( profitableClaim() } +private fun NominationPoolsClaimRewardsValidationSystemBuilder.noStakingTypesConflict(factory: StakingTypesConflictValidationFactory) { + factory.noStakingTypesConflict( + accountId = { it.poolMember.accountId }, + chainId = { it.asset.token.configuration.chainId }, + error = { NominationPoolsClaimRewardsValidationFailure.StakingTypesConflict } + ) +} + private fun NominationPoolsClaimRewardsValidationSystemBuilder.sufficientCommissionBalanceToStayAboveED( enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory ) { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationFailure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationFailure.kt index 9edea53ecc..55617e2c1f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationFailure.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationFailure.kt @@ -17,4 +17,6 @@ sealed class NominationPoolsClaimRewardsValidationFailure { class ToStayAboveED(override val asset: Chain.Asset, override val errorModel: InsufficientBalanceToStayAboveEDError.ErrorModel) : NominationPoolsClaimRewardsValidationFailure(), InsufficientBalanceToStayAboveEDError + + object StakingTypesConflict : NominationPoolsClaimRewardsValidationFailure() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt index 123515249c..55e58fafce 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimRewards.validations +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks @@ -11,7 +12,8 @@ class NominationPoolsClaimRewardsValidationPayload( val fee: DecimalFee, val pendingRewardsPlanks: Balance, val asset: Asset, - val chain: Chain + val chain: Chain, + val poolMember: PoolMember ) val NominationPoolsClaimRewardsValidationPayload.pendingRewards: BigDecimal diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/ValidationFailureUi.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/ValidationFailureUi.kt index a8eaf25520..37a9046f36 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/ValidationFailureUi.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/ValidationFailureUi.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_staking_impl.R import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimRewards.validations.NominationPoolsClaimRewardsValidationFailure.NotEnoughBalanceToPayFees +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.handlePoolStakingTypesConflictValidationFailure import io.novafoundation.nova.feature_wallet_api.domain.validation.handleNotEnoughFeeError import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleInsufficientBalanceCommission @@ -21,5 +22,7 @@ fun nominationPoolsClaimRewardsValidationFailure( failure, resourceManager ) + + NominationPoolsClaimRewardsValidationFailure.StakingTypesConflict -> handlePoolStakingTypesConflictValidationFailure(resourceManager) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/delegatedStake/DelegatedStakeMigrationUseCase.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/delegatedStake/DelegatedStakeMigrationUseCase.kt new file mode 100644 index 0000000000..a556c0633d --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/delegatedStake/DelegatedStakeMigrationUseCase.kt @@ -0,0 +1,35 @@ +package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake + +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_staking_impl.data.StakingSharedState +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.migrateDelegation +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.nominationPools +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolDelegatedStakeRepository +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.shouldMigrateToDelegatedStake +import io.novafoundation.nova.runtime.state.chain +import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder + +interface DelegatedStakeMigrationUseCase { + + context(ExtrinsicBuilder) + suspend fun migrateToDelegatedStakeIfNeeded() +} + +class RealDelegatedStakeMigrationUseCase( + private val delegatedStakeRepository: NominationPoolDelegatedStakeRepository, + private val stakingSharedState: StakingSharedState, + private val accountRepository: AccountRepository +) : DelegatedStakeMigrationUseCase { + + context(ExtrinsicBuilder) + override suspend fun migrateToDelegatedStakeIfNeeded() { + val chain = stakingSharedState.chain() + val account = accountRepository.getSelectedMetaAccount() + val accountId = account.requireAccountIdIn(chain) + + if (delegatedStakeRepository.shouldMigrateToDelegatedStake(stakingSharedState.chainId(), accountId)) { + nominationPools.migrateDelegation(accountId) + } + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/StakingTypesConflictValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/StakingTypesConflictValidation.kt new file mode 100644 index 0000000000..0dfedcc007 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/StakingTypesConflictValidation.kt @@ -0,0 +1,80 @@ +package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations + +import io.novafoundation.nova.common.base.TitleAndMessage +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.validation.Validation +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.common.validation.isFalseOrError +import io.novafoundation.nova.common.validation.valid +import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository +import io.novafoundation.nova.feature_staking_impl.R +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolDelegatedStakeRepository +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolMembersRepository +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidation.ConflictingStakingType +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novasama.substrate_sdk_android.runtime.AccountId + +class StakingTypesConflictValidationFactory( + private val stakingRepository: StakingRepository, + private val delegatedStakeRepository: NominationPoolDelegatedStakeRepository, + private val nominationPoolStakingRepository: NominationPoolMembersRepository +) { + + context(ValidationSystemBuilder) + fun noStakingTypesConflict( + accountId: suspend (P) -> AccountId, + chainId: (P) -> ChainId, + error: () -> E, + checkStakingTypeNotPresent: ConflictingStakingType = ConflictingStakingType.DIRECT + ) { + validate( + StakingTypesConflictValidation( + accountId = accountId, + chainId = chainId, + error = error, + stakingRepository = stakingRepository, + delegatedStakeRepository = delegatedStakeRepository, + nominationPoolStakingRepository = nominationPoolStakingRepository, + checkStakingTypeNotPresent = checkStakingTypeNotPresent + ) + ) + } +} + +class StakingTypesConflictValidation( + private val accountId: suspend (P) -> AccountId, + private val chainId: (P) -> ChainId, + private val error: () -> E, + private val stakingRepository: StakingRepository, + private val nominationPoolStakingRepository: NominationPoolMembersRepository, + private val delegatedStakeRepository: NominationPoolDelegatedStakeRepository, + private val checkStakingTypeNotPresent: ConflictingStakingType +) : Validation { + + enum class ConflictingStakingType { + POOLS, DIRECT + } + + override suspend fun validate(value: P): ValidationStatus { + val chainId = chainId(value) + val delegatedStakeSupported = delegatedStakeRepository.hasMigratedToDelegatedStake(chainId) + if (!delegatedStakeSupported) return valid() + + val isConflictingTypePresent = checkStakingTypeNotPresent.checkPresent(chainId, accountId(value)) + + return isConflictingTypePresent isFalseOrError error + } + + private suspend fun ConflictingStakingType.checkPresent(chainId: ChainId, accountId: AccountId): Boolean { + return when (this) { + ConflictingStakingType.POOLS -> nominationPoolStakingRepository.getPoolMember(chainId, accountId) != null + ConflictingStakingType.DIRECT -> stakingRepository.ledger(chainId, accountId) != null + } + } +} + +fun handlePoolStakingTypesConflictValidationFailure(resourceManager: ResourceManager): TitleAndMessage { + return resourceManager.getString(R.string.pool_staking_conflict_title) to + resourceManager.getString(R.string.pool_staking_conflict_message) +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/model/DelegatedStakeMigrationState.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/model/DelegatedStakeMigrationState.kt new file mode 100644 index 0000000000..4b6fddb161 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/model/DelegatedStakeMigrationState.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.model + +enum class DelegatedStakeMigrationState { + + NOT_SUPPORTED, NEEDS_MIGRATION, MIGRATED +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt index 4517c4a305..f7d30d1612 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/NominationPoolsRedeemInteractor.kt @@ -18,6 +18,7 @@ import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network. import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.unlockChunksFor import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase 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.runtime.state.chain @@ -44,6 +45,7 @@ class RealNominationPoolsRedeemInteractor( private val stakingSharedState: StakingSharedState, private val nominationPoolSharedComputation: NominationPoolSharedComputation, private val stakingSharedComputation: StakingSharedComputation, + private val migrationUseCase: DelegatedStakeMigrationUseCase ) : NominationPoolsRedeemInteractor { override fun redeemAmountFlow(poolMember: PoolMember, computationScope: CoroutineScope): Flow { @@ -83,6 +85,8 @@ class RealNominationPoolsRedeemInteractor( } private suspend fun ExtrinsicBuilder.redeem(poolMember: PoolMember) { + migrationUseCase.migrateToDelegatedStakeIfNeeded() + val chainId = stakingSharedState.chainId() val poolStash = poolAccountDerivation.bondedAccountOf(poolMember.poolId, chainId) val slashingSpans = stakingRepository.getSlashingSpan(chainId, poolStash).numberOfSlashingSpans() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt index 33178c84a7..cc01fe3758 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/Declarations.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redee import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.model.balanceCountedTowardsED import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance @@ -13,12 +14,24 @@ typealias NominationPoolsRedeemValidationSystem = ValidationSystem fun ValidationSystem.Companion.nominationPoolsRedeem( - enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory + enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory ): NominationPoolsRedeemValidationSystem = ValidationSystem { + noStakingTypesConflict(stakingTypesConflictValidationFactory) + enoughToPayFees() + sufficientCommissionBalanceToStayAboveED(enoughTotalToStayAboveEDValidationFactory) } +private fun NominationPoolsRedeemValidationSystemBuilder.noStakingTypesConflict(factory: StakingTypesConflictValidationFactory) { + factory.noStakingTypesConflict( + accountId = { it.poolMember.accountId }, + chainId = { it.asset.token.configuration.chainId }, + error = { NominationPoolsRedeemValidationFailure.StakingTypesConflict } + ) +} + private fun NominationPoolsRedeemValidationSystemBuilder.enoughToPayFees() { sufficientBalance( fee = { it.fee }, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationFailure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationFailure.kt index dbbf1b7133..9fa103ce1d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationFailure.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationFailure.kt @@ -15,4 +15,6 @@ sealed class NominationPoolsRedeemValidationFailure { class ToStayAboveED(override val asset: Chain.Asset, override val errorModel: InsufficientBalanceToStayAboveEDError.ErrorModel) : NominationPoolsRedeemValidationFailure(), InsufficientBalanceToStayAboveEDError + + object StakingTypesConflict : NominationPoolsRedeemValidationFailure() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt index 9069b16696..1553fe3853 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem.validations +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -7,5 +8,6 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain class NominationPoolsRedeemValidationPayload( val fee: DecimalFee, val asset: Asset, - val chain: Chain + val chain: Chain, + val poolMember: PoolMember, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/ValidationFailureUi.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/ValidationFailureUi.kt index 15dbaf6206..e72b22f6a7 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/ValidationFailureUi.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/ValidationFailureUi.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redee import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.handlePoolStakingTypesConflictValidationFailure import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem.validations.NominationPoolsRedeemValidationFailure.NotEnoughBalanceToPayFees import io.novafoundation.nova.feature_wallet_api.domain.validation.handleNotEnoughFeeError import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleInsufficientBalanceCommission @@ -16,5 +17,7 @@ fun nominationPoolsRedeemValidationFailure( failure, resourceManager ) + + NominationPoolsRedeemValidationFailure.StakingTypesConflict -> handlePoolStakingTypesConflictValidationFailure(resourceManager) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt index 909e2e2a79..355a2143ab 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/NominationPoolsUnbondInteractor.kt @@ -7,18 +7,19 @@ import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.bondedAccountOf import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState -import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.NominationPoolsCalls import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.nominationPools import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.calls.unbond import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolMemberUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.getParticipatingBondedPoolState import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.participatingBondedPoolStateFlow import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.model.pointsOf import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.state.chain +import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -42,6 +43,7 @@ class RealNominationPoolsUnbondInteractor( private val nominationPoolSharedComputation: NominationPoolSharedComputation, private val poolAccountDerivation: PoolAccountDerivation, private val poolMemberUseCase: NominationPoolMemberUseCase, + private val migrationUseCase: DelegatedStakeMigrationUseCase ) : NominationPoolsUnbondInteractor { override fun poolMemberStateFlow(computationScope: CoroutineScope): Flow { @@ -63,7 +65,7 @@ class RealNominationPoolsUnbondInteractor( val chain = stakingSharedState.chain() extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { - nominationPools.unbond(poolMember, amount, chain.id) + unbond(poolMember, amount, chain.id) } } } @@ -73,17 +75,19 @@ class RealNominationPoolsUnbondInteractor( val chain = stakingSharedState.chain() extrinsicService.submitExtrinsic(stakingSharedState.chain(), TransactionOrigin.SelectedWallet) { - nominationPools.unbond(poolMember, amount, chain.id) + unbond(poolMember, amount, chain.id) } } } - private suspend fun NominationPoolsCalls.unbond(poolMember: PoolMember, amount: Balance, chainId: ChainId) { + private suspend fun ExtrinsicBuilder.unbond(poolMember: PoolMember, amount: Balance, chainId: ChainId) { + migrationUseCase.migrateToDelegatedStakeIfNeeded() + val poolAccount = poolAccountDerivation.bondedAccountOf(poolMember.poolId, chainId) val bondedPoolState = nominationPoolSharedComputation.getParticipatingBondedPoolState(poolAccount, poolMember.poolId, chainId) val unbondPoints = bondedPoolState.pointsOf(amount).coerceAtMost(poolMember.points) - unbond(poolMember.accountId, unbondPoints) + nominationPools.unbond(poolMember.accountId, unbondPoints) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt index 663692c2d2..9c3cfb9489 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/Declarations.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.unbon 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.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.model.balanceCountedTowardsED import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.positiveAmount @@ -19,8 +20,11 @@ typealias NominationPoolsUnbondValidation = Validation handleInsufficientBalanceCommission( + is ToStayAboveED -> handleInsufficientBalanceCommission( failure, resourceManager ).asDefault() + + StakingTypesConflict -> handlePoolStakingTypesConflictValidationFailure(resourceManager).asDefault() } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailure.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailure.kt index 25c4547109..f2b9e93f6c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailure.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailure.kt @@ -35,4 +35,6 @@ sealed class StartMultiStakingValidationFailure { ) : PoolAvailableBalanceValidation.ValidationError, StartMultiStakingValidationFailure() object InactivePool : StartMultiStakingValidationFailure() + + object HasConflictingStakingType : StartMultiStakingValidationFailure() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailureUi.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailureUi.kt index 3fb38236a1..d3b0090696 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailureUi.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationFailureUi.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations +import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.ValidationFlowActions @@ -10,6 +11,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.selection.copyWith import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure.AmountLessThanMinimum import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure.AvailableBalanceGap +import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure.HasConflictingStakingType import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure.InactivePool import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure.MaxNominatorsReached import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure.NonPositiveAmount @@ -81,5 +83,14 @@ fun handleStartMultiStakingValidationFailure( }, updateAmountInUi = updateAmountInUi ) + + is HasConflictingStakingType -> handleStartStakingConflictingTypesFailure(resourceManager).asDefault() } } + +private fun handleStartStakingConflictingTypesFailure( + resourceManager: ResourceManager +): TitleAndMessage { + return resourceManager.getString(R.string.setup_staking_conflict_title) to + resourceManager.getString(R.string.setup_staking_conflict_message) +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/landing/StartStakingInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/landing/StartStakingInteractor.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/landing/StartStakingInteractorFactory.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/landing/StartStakingInteractorFactory.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt index 24be148225..93b16a0ceb 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt @@ -1,6 +1,8 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmount.direct import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_impl.data.StakingOption import io.novafoundation.nova.feature_staking_impl.data.asset @@ -9,6 +11,8 @@ import io.novafoundation.nova.feature_staking_impl.data.repository.StakingConsta import io.novafoundation.nova.feature_staking_impl.data.stakingType import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.common.minStake +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidation.ConflictingStakingType +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.recommendations.ValidatorRecommenderFactory import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settings.RecommendationSettingsProviderFactory import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure @@ -33,6 +37,8 @@ class DirectStakingPropertiesFactory( private val stakingSharedComputation: StakingSharedComputation, private val stakingRepository: StakingRepository, private val stakingConstantsRepository: StakingConstantsRepository, + private val stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory, + private val selectedAccountUseCase: SelectedAccountUseCase, ) : SingleStakingPropertiesFactory { override fun createProperties(scope: CoroutineScope, stakingOption: StakingOption): SingleStakingProperties { @@ -43,7 +49,9 @@ class DirectStakingPropertiesFactory( scope = scope, stakingSharedComputation = stakingSharedComputation, stakingRepository = stakingRepository, - stakingConstantsRepository = stakingConstantsRepository + stakingConstantsRepository = stakingConstantsRepository, + stakingTypesConflictValidationFactory = stakingTypesConflictValidationFactory, + selectedAccountUseCase = selectedAccountUseCase ) } } @@ -56,6 +64,8 @@ private class DirectStakingProperties( private val stakingSharedComputation: StakingSharedComputation, private val stakingRepository: StakingRepository, private val stakingConstantsRepository: StakingConstantsRepository, + private val stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory, + private val selectedAccountUseCase: SelectedAccountUseCase, ) : SingleStakingProperties { override val stakingType: Chain.Asset.StakingType = stakingOption.stakingType @@ -77,6 +87,8 @@ private class DirectStakingProperties( ) override val validationSystem: StartMultiStakingValidationSystem = ValidationSystem { + noConflictingStaking() + maximumNominatorsReached() positiveBond() @@ -90,6 +102,15 @@ private class DirectStakingProperties( return stakingSharedComputation.minStake(stakingOption.chain.id, scope) } + private fun StartMultiStakingValidationSystemBuilder.noConflictingStaking() { + stakingTypesConflictValidationFactory.noStakingTypesConflict( + accountId = { selectedAccountUseCase.getSelectedMetaAccount().requireAccountIdIn(it.recommendableSelection.selection.stakingOption.chain) }, + chainId = { it.asset.token.configuration.chainId }, + error = { StartMultiStakingValidationFailure.HasConflictingStakingType }, + checkStakingTypeNotPresent = ConflictingStakingType.POOLS + ) + } + private fun StartMultiStakingValidationSystemBuilder.enoughForMinimumStake() { minimumBondValidation( stakingRepository = stakingRepository, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt index e0a5743a11..ae3f5348ec 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolStakingProperties.kt @@ -1,17 +1,22 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmount.pools import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn 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.nominationPools.repository.NominationPoolGlobalsRepository import io.novafoundation.nova.feature_staking_impl.data.stakingType import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.PoolAvailableBalanceValidationFactory +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidation.ConflictingStakingType +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.pools.recommendation.NominationPoolRecommenderFactory import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.NominationPoolsAvailableBalanceResolver import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.selection.stakeAmount import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationFailure import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationSystem +import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.StartMultiStakingValidationSystemBuilder import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.nominationPools.activePool import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.nominationPools.enoughForMinJoinBond import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.validations.nominationPools.maxPoolMembersNotReached @@ -30,6 +35,8 @@ class NominationPoolStakingPropertiesFactory( private val poolsAvailableBalanceResolver: NominationPoolsAvailableBalanceResolver, private val nominationPoolGlobalsRepository: NominationPoolGlobalsRepository, private val poolAvailableBalanceValidationFactory: PoolAvailableBalanceValidationFactory, + private val stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory, + private val selectedAccountUseCase: SelectedAccountUseCase, ) : SingleStakingPropertiesFactory { override fun createProperties(scope: CoroutineScope, stakingOption: StakingOption): SingleStakingProperties { @@ -41,6 +48,8 @@ class NominationPoolStakingPropertiesFactory( poolsAvailableBalanceResolver = poolsAvailableBalanceResolver, nominationPoolGlobalsRepository = nominationPoolGlobalsRepository, poolAvailableBalanceValidationFactory = poolAvailableBalanceValidationFactory, + stakingTypesConflictValidationFactory = stakingTypesConflictValidationFactory, + selectedAccountUseCase = selectedAccountUseCase ) } } @@ -53,6 +62,8 @@ private class NominationPoolStakingProperties( private val poolsAvailableBalanceResolver: NominationPoolsAvailableBalanceResolver, private val nominationPoolGlobalsRepository: NominationPoolGlobalsRepository, private val poolAvailableBalanceValidationFactory: PoolAvailableBalanceValidationFactory, + private val stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory, + private val selectedAccountUseCase: SelectedAccountUseCase, ) : SingleStakingProperties { override val stakingType: Chain.Asset.StakingType = stakingOption.stakingType @@ -72,6 +83,8 @@ private class NominationPoolStakingProperties( ) override val validationSystem: StartMultiStakingValidationSystem = ValidationSystem { + noConflictingStaking() + maxPoolMembersNotReached(nominationPoolGlobalsRepository) activePool() @@ -88,6 +101,15 @@ private class NominationPoolStakingProperties( ) } + private fun StartMultiStakingValidationSystemBuilder.noConflictingStaking() { + stakingTypesConflictValidationFactory.noStakingTypesConflict( + accountId = { selectedAccountUseCase.getSelectedMetaAccount().requireAccountIdIn(it.recommendableSelection.selection.stakingOption.chain) }, + chainId = { it.asset.token.configuration.chainId }, + error = { StartMultiStakingValidationFailure.HasConflictingStakingType }, + checkStakingTypeNotPresent = ConflictingStakingType.DIRECT + ) + } + override suspend fun minStake(): Balance { return nominationPoolSharedComputation.minJoinBond(stakingOption.chain.id, sharedComputationScope) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/common/di/NominationPoolsCommonBondMoreModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/common/di/NominationPoolsCommonBondMoreModule.kt index fa46439bbf..42ba9b6402 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/common/di/NominationPoolsCommonBondMoreModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/common/di/NominationPoolsCommonBondMoreModule.kt @@ -11,9 +11,11 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMo import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.RealNominationPoolsBondMoreInteractor import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.validations.NominationPoolsBondMoreValidationSystem import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondMore.validations.nominationPoolsBondMore +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.hints.NominationPoolHintsUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.PoolAvailableBalanceValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.PoolStateValidationFactory +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.presentation.nominationPools.bondMore.hints.NominationPoolsBondMoreHintsFactory @Module @@ -23,9 +25,10 @@ class NominationPoolsCommonBondMoreModule { @ScreenScope fun provideInteractor( extrinsicService: ExtrinsicService, - stakingSharedState: StakingSharedState + stakingSharedState: StakingSharedState, + migrationUseCase: DelegatedStakeMigrationUseCase ): NominationPoolsBondMoreInteractor { - return RealNominationPoolsBondMoreInteractor(extrinsicService, stakingSharedState) + return RealNominationPoolsBondMoreInteractor(extrinsicService, stakingSharedState, migrationUseCase) } @Provides @@ -33,9 +36,11 @@ class NominationPoolsCommonBondMoreModule { fun provideValidationSystem( poolStateValidationFactory: PoolStateValidationFactory, poolAvailableBalanceValidationFactory: PoolAvailableBalanceValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory ): NominationPoolsBondMoreValidationSystem = ValidationSystem.nominationPoolsBondMore( poolStateValidationFactory = poolStateValidationFactory, - poolAvailableBalanceValidationFactory = poolAvailableBalanceValidationFactory + poolAvailableBalanceValidationFactory = poolAvailableBalanceValidationFactory, + stakingTypesConflictValidationFactory = stakingTypesConflictValidationFactory ) @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt index 9c9b329d13..fbb695455d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class NominationPoolsClaimRewardsViewModel( @@ -57,7 +58,7 @@ class NominationPoolsClaimRewardsViewModel( private val pendingRewards = interactor.pendingRewardsFlow() .shareInBackground() - val pendingRewardsAmountModel = combine(pendingRewards, assetFlow, ::mapAmountToAmountModel) + val pendingRewardsAmountModel = combine(pendingRewards.map { it.amount }, assetFlow, ::mapAmountToAmountModel) .shareInBackground() val walletUiFlow = walletUiUseCase.selectedWalletUiFlow() @@ -99,12 +100,14 @@ class NominationPoolsClaimRewardsViewModel( _showNextProgress.value = true val shouldRestake = shouldRestakeInput.first() + val pendingRewards = pendingRewards.first() val payload = NominationPoolsClaimRewardsValidationPayload( fee = feeLoaderMixin.awaitDecimalFee(), - pendingRewardsPlanks = pendingRewards.first(), + pendingRewardsPlanks = pendingRewards.amount, asset = assetFlow.first(), - chain = stakingSharedState.chain() + chain = stakingSharedState.chain(), + poolMember = pendingRewards.poolMember ) validationExecutor.requireValid( diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/PoolPendingRewards.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/PoolPendingRewards.kt new file mode 100644 index 0000000000..07bdc9ac49 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/PoolPendingRewards.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_staking_impl.presentation.nominationPools.claimRewards + +import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance + +class PoolPendingRewards( + val amount: Balance, + val poolMember: PoolMember, +) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/di/NominationPoolsClaimRewardsModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/di/NominationPoolsClaimRewardsModule.kt index 14af0ddc69..7a54df4f1e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/di/NominationPoolsClaimRewardsModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/di/NominationPoolsClaimRewardsModule.kt @@ -23,6 +23,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimR import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimRewards.validations.NominationPoolsClaimRewardsValidationSystem import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimRewards.validations.nominationPoolsClaimRewards import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolMemberUseCase +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.presentation.NominationPoolsRouter import io.novafoundation.nova.feature_staking_impl.presentation.nominationPools.claimRewards.NominationPoolsClaimRewardsViewModel import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase @@ -39,19 +41,22 @@ class NominationPoolsClaimRewardsModule { poolMembersRepository: NominationPoolMembersRepository, stakingSharedState: StakingSharedState, extrinsicService: ExtrinsicService, + migrationUseCase: DelegatedStakeMigrationUseCase ): NominationPoolsClaimRewardsInteractor = RealNominationPoolsClaimRewardsInteractor( poolMemberUseCase = poolMemberUseCase, poolMembersRepository = poolMembersRepository, stakingSharedState = stakingSharedState, - extrinsicService = extrinsicService + extrinsicService = extrinsicService, + migrationUseCase = migrationUseCase ) @Provides @ScreenScope fun provideValidationSystem( - enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory + enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory ): NominationPoolsClaimRewardsValidationSystem { - return ValidationSystem.nominationPoolsClaimRewards(enoughTotalToStayAboveEDValidationFactory) + return ValidationSystem.nominationPoolsClaimRewards(enoughTotalToStayAboveEDValidationFactory, stakingTypesConflictValidationFactory) } @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt index 94ee117ac1..ee58bc0738 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt @@ -108,7 +108,8 @@ class NominationPoolsRedeemViewModel( val payload = NominationPoolsRedeemValidationPayload( fee = feeLoaderMixin.awaitDecimalFee(), asset = assetFlow.first(), - chain = stakingSharedState.chain() + chain = stakingSharedState.chain(), + poolMember = poolMemberFlow.first() ) validationExecutor.requireValid( diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/di/NominationPoolsRedeemModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/di/NominationPoolsRedeemModule.kt index 8b5caaa513..dc40b7db7c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/di/NominationPoolsRedeemModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/di/NominationPoolsRedeemModule.kt @@ -22,6 +22,8 @@ import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolMemberUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem.NominationPoolsRedeemInteractor import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem.RealNominationPoolsRedeemInteractor import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem.validations.NominationPoolsRedeemValidationSystem @@ -44,21 +46,24 @@ class NominationPoolsRedeemModule { stakingSharedState: StakingSharedState, nominationPoolSharedComputation: NominationPoolSharedComputation, stakingSharedComputation: StakingSharedComputation, + migrationUseCase: DelegatedStakeMigrationUseCase ): NominationPoolsRedeemInteractor = RealNominationPoolsRedeemInteractor( extrinsicService = extrinsicService, stakingRepository = stakingRepository, poolAccountDerivation = poolAccountDerivation, stakingSharedState = stakingSharedState, nominationPoolSharedComputation = nominationPoolSharedComputation, - stakingSharedComputation = stakingSharedComputation + stakingSharedComputation = stakingSharedComputation, + migrationUseCase = migrationUseCase ) @Provides @ScreenScope fun provideValidationSystem( - enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory + enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory ): NominationPoolsRedeemValidationSystem { - return ValidationSystem.nominationPoolsRedeem(enoughTotalToStayAboveEDValidationFactory) + return ValidationSystem.nominationPoolsRedeem(enoughTotalToStayAboveEDValidationFactory, stakingTypesConflictValidationFactory) } @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/common/di/NominationPoolsCommonUnbondModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/common/di/NominationPoolsCommonUnbondModule.kt index 1721a1dca1..1624a44c14 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/common/di/NominationPoolsCommonUnbondModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/common/di/NominationPoolsCommonUnbondModule.kt @@ -5,15 +5,17 @@ import dagger.Provides import io.novafoundation.nova.common.di.scope.ScreenScope import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState -import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolGlobalsRepository import io.novafoundation.nova.feature_staking_impl.data.repository.StakingConstantsRepository import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolMemberUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.delegatedStake.DelegatedStakeMigrationUseCase import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.hints.NominationPoolHintsUseCase +import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.validations.StakingTypesConflictValidationFactory import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.unbond.NominationPoolsUnbondInteractor import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.unbond.RealNominationPoolsUnbondInteractor import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.unbond.validations.NominationPoolsUnbondValidationFactory @@ -52,13 +54,15 @@ class NominationPoolsCommonUnbondModule { nominationPoolSharedComputation: NominationPoolSharedComputation, poolAccountDerivation: PoolAccountDerivation, poolMemberUseCase: NominationPoolMemberUseCase, + migrationUseCase: DelegatedStakeMigrationUseCase ): NominationPoolsUnbondInteractor { return RealNominationPoolsUnbondInteractor( extrinsicService = extrinsicService, stakingSharedState = stakingSharedState, nominationPoolSharedComputation = nominationPoolSharedComputation, poolAccountDerivation = poolAccountDerivation, - poolMemberUseCase = poolMemberUseCase + poolMemberUseCase = poolMemberUseCase, + migrationUseCase = migrationUseCase ) } @@ -66,8 +70,15 @@ class NominationPoolsCommonUnbondModule { @ScreenScope fun provideValidationSystem( validationFactory: NominationPoolsUnbondValidationFactory, - enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory - ): NominationPoolsUnbondValidationSystem = ValidationSystem.nominationPoolsUnbond(validationFactory, enoughTotalToStayAboveEDValidationFactory) + enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, + stakingTypesConflictValidationFactory: StakingTypesConflictValidationFactory + ): NominationPoolsUnbondValidationSystem { + return ValidationSystem.nominationPoolsUnbond( + unbondValidationFactory = validationFactory, + enoughTotalToStayAboveEDValidationFactory = enoughTotalToStayAboveEDValidationFactory, + stakingTypesConflictValidationFactory = stakingTypesConflictValidationFactory + ) + } @Provides @ScreenScope diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt index c19ff42e87..485ba0204e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt @@ -139,7 +139,7 @@ class ConfirmMultiStakingViewModel( val payload = StartMultiStakingValidationPayload( recommendableSelection = recommendableSelection, asset = assetFlow.first(), - fee = decimalFee + fee = decimalFee, ) validationExecutor.requireValid( diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt index deeb40ea15..c362318087 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import java.math.BigInteger sealed class BalanceSyncUpdate { @@ -30,6 +31,14 @@ interface AssetBalance { subscriptionBuilder: SharedRequestsBuilder ): Flow<*> + suspend fun startSyncingBalanceHolds( + metaAccount: MetaAccount, + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow<*> = emptyFlow() + suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean suspend fun existentialDeposit( diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceHoldsRepository.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceHoldsRepository.kt new file mode 100644 index 0000000000..36cb099d3f --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/repository/BalanceHoldsRepository.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_wallet_api.data.repository + +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow + +interface BalanceHoldsRepository { + + suspend fun observeBalanceHolds(metaInt: Long, chainAsset: Chain.Asset): Flow> + + fun observeHoldsForMetaAccount(metaInt: Long): Flow> +} 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 2633ae3473..b871926850 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 @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow interface BalanceLocksRepository { - suspend fun observeBalanceLocks(chain: Chain, chainAsset: Chain.Asset): Flow> + suspend fun observeBalanceLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow> suspend fun getBiggestLock(chain: Chain, chainAsset: Chain.Asset): BalanceLock? diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt index 5e5bea33c3..051c18c3ac 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.coingecko.Coingeck import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase @@ -75,4 +76,6 @@ interface WalletFeatureApi { val arbitraryTokenUseCase: ArbitraryTokenUseCase val hydraDxAssetIdConverter: HydraDxAssetIdConverter + + val holdsRepository: BalanceHoldsRepository } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt index 3627fd2af4..df5f52cd80 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_api.domain.model import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.utils.atLeastZero +import io.novafoundation.nova.common.utils.sumByBigInteger import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.holdAndFreezesTransferable @@ -111,6 +112,14 @@ data class Asset( val unbonding = token.amountFromPlanks(unbondingInPlanks) } +fun Asset.unlabeledReserves(holds: Collection): Balance { + return unlabeledReserves(holds.sumByBigInteger { it.amountInPlanks }) +} + +fun Asset.unlabeledReserves(labeledReserves: Balance): Balance { + return reservedInPlanks - labeledReserves +} + fun Asset.balanceCountedTowardsED(): BigDecimal { return token.amountFromPlanks(balanceCountedTowardsEDInPlanks) } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceBreakdownIds.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceBreakdownIds.kt index e0380c8ce0..880c2d7807 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceBreakdownIds.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceBreakdownIds.kt @@ -7,4 +7,6 @@ object BalanceBreakdownIds { const val CROWDLOAN = "crowdloan" const val NOMINATION_POOL = "nomination-pool" + + const val NOMINATION_POOL_DELEGATED = "DelegatedStaking: StakingDelegation" } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceHold.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceHold.kt new file mode 100644 index 0000000000..047fd17f2b --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/BalanceHold.kt @@ -0,0 +1,35 @@ +package io.novafoundation.nova.feature_wallet_api.domain.model + +import io.novafoundation.nova.common.utils.Identifiable +import io.novafoundation.nova.core_db.model.BalanceHoldLocal +import io.novafoundation.nova.core_db.model.BalanceHoldLocal.HoldIdLocal +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold.HoldId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class BalanceHold( + val id: HoldId, + val amountInPlanks: Balance, + val chainAsset: Chain.Asset +) : Identifiable { + + class HoldId(val module: String, val reason: String) + + // Keep in tact with `BalanceBreakdownIds` + override val identifier: String = "${id.module}: ${id.reason}" +} + +fun mapBalanceHoldFromLocal( + asset: Chain.Asset, + hold: BalanceHoldLocal +): BalanceHold { + return BalanceHold( + id = hold.id.toDomain(), + amountInPlanks = hold.amount, + chainAsset = asset + ) +} + +private fun HoldIdLocal.toDomain(): HoldId { + return HoldId(module, reason) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/BalanceIdMapper.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/BalanceIdMapper.kt index a444fb0661..2defe8e057 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/BalanceIdMapper.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/BalanceIdMapper.kt @@ -15,7 +15,8 @@ fun mapBalanceIdToUi(resourceManager: ResourceManager, id: String): String { "phrelect" -> resourceManager.getString(R.string.assets_balance_details_locks_phrelect) BalanceBreakdownIds.RESERVED -> resourceManager.getString(R.string.wallet_balance_reserved) BalanceBreakdownIds.CROWDLOAN -> resourceManager.getString(R.string.assets_balance_details_locks_crowdloans) - BalanceBreakdownIds.NOMINATION_POOL -> resourceManager.getString(R.string.setup_staking_type_pool_staking) + BalanceBreakdownIds.NOMINATION_POOL, + BalanceBreakdownIds.NOMINATION_POOL_DELEGATED -> resourceManager.getString(R.string.setup_staking_type_pool_staking) else -> id.capitalize() } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt index ccc5815d06..33bf79593c 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt @@ -3,13 +3,19 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import android.util.Log import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo +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.castToDictEnum +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.balances import io.novafoundation.nova.common.utils.decodeValue import io.novafoundation.nova.common.utils.numberConstant import io.novafoundation.nova.common.utils.system import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao +import io.novafoundation.nova.core_db.model.BalanceHoldLocal import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.cache.bindAccountInfoOrDefault @@ -19,12 +25,15 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.b import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.SubstrateRemoteSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bindBalanceLocks import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.updateLocks import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.getRuntime import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.metadata @@ -33,6 +42,7 @@ import io.novafoundation.nova.runtime.storage.typed.system import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.metadata.storage import io.novasama.substrate_sdk_android.runtime.metadata.storageKey +import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map @@ -43,7 +53,8 @@ class NativeAssetBalance( private val assetCache: AssetCache, private val substrateRemoteSource: SubstrateRemoteSource, private val remoteStorage: StorageDataSource, - private val lockDao: LockDao + private val lockDao: LockDao, + private val holdsDao: HoldsDao, ) : AssetBalance { override suspend fun startSyncingBalanceLocks( @@ -64,6 +75,24 @@ class NativeAssetBalance( } } + override suspend fun startSyncingBalanceHolds( + metaAccount: MetaAccount, + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow<*> { + val runtime = chainRegistry.getRuntime(chain.id) + val storage = runtime.metadata.balances().storageOrNull("Holds") ?: return emptyFlow() + val key = storage.storageKey(runtime, accountId) + + return subscriptionBuilder.subscribe(key) + .map { change -> + val holds = bindBalanceHolds(storage.decodeValue(change.value, runtime)).orEmpty() + holdsDao.updateHolds(holds, metaAccount.id, chain.id, chainAsset.id) + } + } + override suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { return true } @@ -139,4 +168,37 @@ class NativeAssetBalance( } else { Asset.TransferableMode.REGULAR } + + private fun bindBalanceHolds(dynamicInstance: Any?): List? { + if (dynamicInstance == null) return null + + return bindList(dynamicInstance) { + BlockchainHold( + id = bindHoldId(it.castToStruct()["id"]), + amount = bindNumber(it.castToStruct()["amount"]) + ) + } + } + + private fun bindHoldId(id: Any?): BalanceHold.HoldId { + val module = id.castToDictEnum() + val reason = module.value.castToDictEnum() + + return BalanceHold.HoldId(module.name, reason.name) + } + + private suspend fun HoldsDao.updateHolds(holds: List, metaId: Long, chainId: ChainId, chainAssetId: ChainAssetId) { + val balanceLocksLocal = holds.map { + BalanceHoldLocal( + metaId = metaId, + chainId = chainId, + assetId = chainAssetId, + id = BalanceHoldLocal.HoldIdLocal(module = it.id.module, reason = it.id.reason), + amount = it.amount + ) + } + updateHolds(balanceLocksLocal, metaId, chainId, chainAssetId) + } + + private class BlockchainHold(val id: BalanceHold.HoldId, val amount: Balance) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/locks/BalanceLocksUpdater.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/locks/BalanceLocksUpdater.kt index 76394f70fd..1bb2f84e91 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/locks/BalanceLocksUpdater.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/updaters/locks/BalanceLocksUpdater.kt @@ -38,12 +38,22 @@ class BalanceLocksUpdater( storageSubscriptionBuilder: SharedRequestsBuilder, scopeValue: MetaAccount, ): Flow { - return chain.enabledAssets().map { chainAsset -> - val metaAccount = scopeValue - val accountId = metaAccount.accountIdIn(chain) ?: return emptyFlow() - val assetSource = assetSourceRegistry.sourceFor(chainAsset) - assetSource.balance.startSyncingBalanceLocks(metaAccount, chain, chainAsset, accountId, storageSubscriptionBuilder) + val metaAccount = scopeValue + val accountId = metaAccount.accountIdIn(chain) ?: return emptyFlow() + + val flows = buildList { + chain.enabledAssets().forEach { chainAsset -> + val assetSource = assetSourceRegistry.sourceFor(chainAsset) + + val locksFlow = assetSource.balance.startSyncingBalanceLocks(metaAccount, chain, chainAsset, accountId, storageSubscriptionBuilder) + val holdsFlow = assetSource.balance.startSyncingBalanceHolds(metaAccount, chain, chainAsset, accountId, storageSubscriptionBuilder) + + add(locksFlow) + add(holdsFlow) + } } + + return flows .merge() .noSideAffects() } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceHoldsRepository.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceHoldsRepository.kt new file mode 100644 index 0000000000..b545814120 --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealBalanceHoldsRepository.kt @@ -0,0 +1,35 @@ +package io.novafoundation.nova.feature_wallet_impl.data.repository + +import io.novafoundation.nova.common.utils.mapList +import io.novafoundation.nova.core_db.dao.HoldsDao +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository +import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold +import io.novafoundation.nova.feature_wallet_api.domain.model.mapBalanceHoldFromLocal +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chainsById +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class RealBalanceHoldsRepository( + private val chainRegistry: ChainRegistry, + private val holdsDao: HoldsDao, +) : BalanceHoldsRepository { + + override suspend fun observeBalanceHolds(metaInt: Long, chainAsset: Chain.Asset): Flow> { + return holdsDao.observeBalanceHolds(metaInt, chainAsset.chainId, chainAsset.id).mapList { hold -> + mapBalanceHoldFromLocal(chainAsset, hold) + } + } + + override fun observeHoldsForMetaAccount(metaInt: Long): Flow> { + return holdsDao.observeHoldsForMetaAccount(metaInt).map { holds -> + val chainsById = chainRegistry.chainsById() + holds.mapNotNull { holdLocal -> + val asset = chainsById[holdLocal.chainId]?.assetsById?.get(holdLocal.assetId) ?: return@mapNotNull null + + mapBalanceHoldFromLocal(asset, holdLocal) + } + } + } +} 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 3bee3e5b99..1b90841428 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 @@ -14,14 +14,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map class RealBalanceLocksRepository( + // TODO refactoring - repository should not depend on other repository. MetaId should be passed to repository arguments private val accountRepository: AccountRepository, private val chainRegistry: ChainRegistry, private val lockDao: LockDao ) : BalanceLocksRepository { - override suspend fun observeBalanceLocks(chain: Chain, chainAsset: Chain.Asset): Flow> { - val metaAccount = accountRepository.getSelectedMetaAccount() - return lockDao.observeBalanceLocks(metaAccount.id, chain.id, chainAsset.id) + override suspend fun observeBalanceLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow> { + return lockDao.observeBalanceLocks(metaId, chain.id, chainAsset.id) .mapList { lock -> mapBalanceLockFromLocal(chainAsset, lock) } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt index 7a6d49b72f..1ea23de8b2 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt @@ -23,6 +23,7 @@ import io.novafoundation.nova.core_db.dao.CoinPriceDao import io.novafoundation.nova.core_db.dao.ContributionDao import io.novafoundation.nova.core_db.dao.CurrencyDao import io.novafoundation.nova.core_db.dao.ExternalBalanceDao +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.core_db.dao.PhishingAddressDao @@ -144,4 +145,6 @@ interface WalletFeatureDependencies { val multiLocationConverterFactory: MultiLocationConverterFactory val extrinsicWalk: ExtrinsicWalk + + val holdsDao: HoldsDao } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt index 59d62196ae..c364bedc2a 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.core_db.dao.AssetDao import io.novafoundation.nova.core_db.dao.ChainAssetDao import io.novafoundation.nova.core_db.dao.CoinPriceDao import io.novafoundation.nova.core_db.dao.ExternalBalanceDao +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.core_db.dao.PhishingAddressDao @@ -32,6 +33,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.coingecko.Coingeck import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher +import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository import io.novafoundation.nova.feature_wallet_api.data.source.CoinPriceLocalDataSource @@ -66,6 +68,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.RealPa import io.novafoundation.nova.feature_wallet_impl.data.network.phishing.PhishingApi import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi import io.novafoundation.nova.feature_wallet_impl.data.repository.CoinPriceRepositoryImpl +import io.novafoundation.nova.feature_wallet_impl.data.repository.RealBalanceHoldsRepository import io.novafoundation.nova.feature_wallet_impl.data.repository.RealBalanceLocksRepository import io.novafoundation.nova.feature_wallet_impl.data.repository.RealChainAssetRepository import io.novafoundation.nova.feature_wallet_impl.data.repository.RealCrossChainTransfersRepository @@ -272,6 +275,15 @@ class WalletFeatureModule { return RealBalanceLocksRepository(accountRepository, chainRegistry, lockDao) } + @Provides + @FeatureScope + fun provideBalanceHoldsRepository( + chainRegistry: ChainRegistry, + holdsDao: HoldsDao + ): BalanceHoldsRepository { + return RealBalanceHoldsRepository(chainRegistry, holdsDao) + } + @Provides @FeatureScope fun provideChainAssetRepository( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt index 51ac18bc61..156cc3f24f 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_wallet_impl.di.modules import dagger.Module import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.core_db.dao.HoldsDao import io.novafoundation.nova.core_db.dao.LockDao import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -40,13 +41,15 @@ class NativeAssetsModule { assetCache: AssetCache, substrateRemoteSource: SubstrateRemoteSource, @Named(REMOTE_STORAGE_SOURCE) remoteSource: StorageDataSource, - lockDao: LockDao + lockDao: LockDao, + holdsDao: HoldsDao, ) = NativeAssetBalance( chainRegistry = chainRegistry, assetCache = assetCache, substrateRemoteSource = substrateRemoteSource, remoteStorage = remoteSource, - lockDao = lockDao + lockDao = lockDao, + holdsDao = holdsDao ) @Provides