From c461e61895b4e5e10b9d151a8258ae6ae7d2d3f9 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sat, 14 Feb 2026 05:38:47 +0300 Subject: [PATCH] fix: resolve staking reward calculation for parachain (Asset Hub) - Use remote RPC instead of local storage for era/exposure queries - Resolve relay chain via parentId for exposure, totalIssuance, and parachains - Remove StorageCache dependency, detect paged exposures via runtime metadata - Add StakingRepository to NominationPoolRewardCalculatorFactory for direct queries - Use storageOrNull for null-safety on chains without Paras pallet --- .../data/repository/ParasRepository.kt | 4 +- .../data/repository/StakingRepositoryImpl.kt | 14 +++---- .../di/StakingFeatureModule.kt | 3 -- .../nominationPool/NominationPoolModule.kt | 5 ++- .../RealNominationPoolRewardCalculator.kt | 18 +++++++-- .../domain/rewards/RewardCalculatorFactory.kt | 37 +++++++++++++++---- .../nova/runtime/di/RuntimeModule.kt | 4 +- 7 files changed, 57 insertions(+), 28 deletions(-) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/ParasRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/ParasRepository.kt index 48df5cc..6d6577f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/ParasRepository.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/ParasRepository.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.parasOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novasama.substrate_sdk_android.runtime.metadata.storage +import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull interface ParasRepository { @@ -21,7 +21,7 @@ class RealParasRepository( override suspend fun activePublicParachains(chainId: ChainId): Int? { return localSource.query(chainId) { - val parachains = runtime.metadata.parasOrNull()?.storage("Parachains") + val parachains = runtime.metadata.parasOrNull()?.storageOrNull("Parachains") ?.query(binding = ::bindParachains) ?: return@query null parachains.count { it >= LOWEST_PUBLIC_ID } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt index 1a0dc29..15b9671 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt @@ -13,7 +13,6 @@ import io.novafoundation.nova.common.utils.metadata import io.novafoundation.nova.common.utils.numberConstant import io.novafoundation.nova.common.utils.numberConstantOrNull import io.novafoundation.nova.common.utils.staking -import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.AccountStakingDao import io.novafoundation.nova.core_db.model.AccountStakingLocal import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap @@ -50,7 +49,6 @@ import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindin import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSlashDeferDuration import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSlashingSpans import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindValidatorPrefs -import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.ValidatorExposureUpdater import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.activeEraStorageKey import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstants import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi @@ -88,7 +86,6 @@ class StakingRepositoryImpl( private val localStorage: StorageDataSource, private val walletConstants: WalletConstants, private val chainRegistry: ChainRegistry, - private val storageCache: StorageCache, private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, ) : StakingRepository { @@ -127,7 +124,7 @@ class StakingRepositoryImpl( return runtime.metadata.staking().numberConstant("SessionsPerEra", runtime) // How many sessions per era } - override suspend fun getActiveEraIndex(chainId: ChainId): EraIndex = localStorage.query(chainId) { + override suspend fun getActiveEraIndex(chainId: ChainId): EraIndex = remoteStorage.query(chainId) { metadata.staking.activeEra.queryNonNull() } @@ -164,7 +161,7 @@ class StakingRepositoryImpl( } } - private suspend fun fetchPagedEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap = localStorage.query(chainId) { + private suspend fun fetchPagedEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap = remoteStorage.query(chainId) { val eraStakersOverview = metadata.staking().storage("ErasStakersOverview").entries( eraIndex, keyExtractor = { (_: BigInteger, accountId: ByteArray) -> accountId.toHexString() }, @@ -207,7 +204,7 @@ class StakingRepositoryImpl( } } - private suspend fun fetchLegacyEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap = localStorage.query(chainId) { + private suspend fun fetchLegacyEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap = remoteStorage.query(chainId) { runtime.metadata.staking().storage("ErasStakers").entries( eraIndex, keyExtractor = { (_: BigInteger, accountId: ByteArray) -> accountId.toHexString() }, @@ -405,9 +402,8 @@ class StakingRepositoryImpl( } private suspend fun isPagedExposuresUsed(chainId: ChainId): Boolean { - val isPagedExposuresValue = storageCache.getEntry(ValidatorExposureUpdater.STORAGE_KEY_PAGED_EXPOSURES, chainId) - - return ValidatorExposureUpdater.decodeIsPagedExposuresValue(isPagedExposuresValue.content) + val runtime = runtimeFor(chainId) + return runtime.metadata.staking().storageOrNull("ErasStakersOverview") != null } private fun observeAccountValidatorPrefs(chainId: ChainId, stashId: AccountId): Flow { 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 d025836..aeca5c0 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 @@ -11,7 +11,6 @@ import io.novafoundation.nova.common.data.network.rpc.BulkRetriever import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.AccountStakingDao import io.novafoundation.nova.core_db.dao.ExternalBalanceDao import io.novafoundation.nova.core_db.dao.StakingRewardPeriodDao @@ -190,7 +189,6 @@ class StakingFeatureModule { @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, walletConstants: WalletConstants, chainRegistry: ChainRegistry, - storageCache: StorageCache, multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi ): StakingRepository = StakingRepositoryImpl( accountStakingDao = accountStakingDao, @@ -198,7 +196,6 @@ class StakingFeatureModule { localStorage = localStorageSource, walletConstants = walletConstants, chainRegistry = chainRegistry, - storageCache = storageCache, multiChainRuntimeCallsApi = multiChainRuntimeCallsApi ) 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 36fec3c..3acf5bb 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 @@ -26,6 +26,7 @@ import io.novafoundation.nova.feature_staking_impl.data.nominationPools.reposito import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolMembersRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolStateRepository import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolUnbondRepository +import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_impl.data.repository.StakingConstantsRepository import io.novafoundation.nova.feature_staking_impl.data.repository.StakingRewardsRepository import io.novafoundation.nova.feature_staking_impl.domain.StakingInteractor @@ -186,10 +187,12 @@ class NominationPoolModule { fun provideNominationPoolRewardCalculatorFactory( stakingSharedComputation: StakingSharedComputation, nominationPoolSharedComputation: NominationPoolSharedComputation, + stakingRepository: StakingRepository, ): NominationPoolRewardCalculatorFactory { return NominationPoolRewardCalculatorFactory( sharedStakingSharedComputation = stakingSharedComputation, - nominationPoolSharedComputation = nominationPoolSharedComputation + nominationPoolSharedComputation = nominationPoolSharedComputation, + stakingRepository = stakingRepository ) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt index 4cec59f..2c7aa85 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt @@ -11,12 +11,12 @@ import io.novafoundation.nova.common.utils.reversed import io.novafoundation.nova.feature_account_api.data.model.AccountIdKeyMap import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap import io.novafoundation.nova.feature_staking_api.domain.model.Exposure +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.chain import io.novafoundation.nova.feature_staking_api.domain.nominationPool.model.PoolId import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation -import io.novafoundation.nova.feature_staking_impl.domain.common.electedExposuresInActiveEra import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.rewards.RewardCalculator import kotlinx.coroutines.CoroutineScope @@ -24,21 +24,33 @@ import kotlinx.coroutines.CoroutineScope class NominationPoolRewardCalculatorFactory( private val sharedStakingSharedComputation: StakingSharedComputation, private val nominationPoolSharedComputation: NominationPoolSharedComputation, + private val stakingRepository: StakingRepository, ) { suspend fun create(stakingOption: StakingOption, sharedComputationScope: CoroutineScope): NominationPoolRewardCalculator { - val chainId = stakingOption.chain.id + val chain = stakingOption.chain + val chainId = chain.id + // For parachains, staking exposures live on the parent relay chain + val exposureChainId = chain.parentId ?: chainId + + android.util.Log.d("PEZ_STAKING", "NomPoolRewardCalcFactory.create() chainId=${chainId.take(12)} exposureChainId=${exposureChainId.take(12)}") val delegateOption = stakingOption.unwrapNominationPools() val delegate = sharedStakingSharedComputation.rewardCalculator(delegateOption, sharedComputationScope) val allPoolAccounts = nominationPoolSharedComputation.allBondedPoolAccounts(chainId, sharedComputationScope) + android.util.Log.d("PEZ_STAKING", "Pool accounts: ${allPoolAccounts.size}") + val poolCommissions = nominationPoolSharedComputation.allBondedPools(chainId, sharedComputationScope) .mapValues { (_, pool) -> pool.commission?.current?.perbill } + val activeEra = stakingRepository.getActiveEraIndex(exposureChainId) + val exposures = stakingRepository.getElectedValidatorsExposure(exposureChainId, activeEra) + android.util.Log.d("PEZ_STAKING", "NomPool exposures: ${exposures.size} (era=$activeEra)") + return RealNominationPoolRewardCalculator( directStakingDelegate = delegate, - exposures = sharedStakingSharedComputation.electedExposuresInActiveEra(stakingOption.assetWithChain.chain.id, sharedComputationScope), + exposures = exposures, commissions = poolCommissions, poolStashesById = allPoolAccounts ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt index 997761a..16a152a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt @@ -13,7 +13,6 @@ import io.novafoundation.nova.feature_staking_impl.data.repository.VaraRepositor import io.novafoundation.nova.feature_staking_impl.data.stakingType import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation -import io.novafoundation.nova.feature_staking_impl.domain.common.electedExposuresInActiveEra import io.novafoundation.nova.feature_staking_impl.domain.common.eraTimeCalculator import io.novafoundation.nova.feature_staking_impl.domain.error.accountIdNotFound import io.novafoundation.nova.runtime.ext.Geneses @@ -47,7 +46,14 @@ class RewardCalculatorFactory( validatorsPrefs: AccountIdMap, scope: CoroutineScope ): RewardCalculator = withContext(Dispatchers.Default) { - val totalIssuance = totalIssuanceRepository.getTotalIssuance(stakingOption.assetWithChain.chain.id) + // For parachains (e.g. Asset Hub), staking lives on the parent relay chain. + // TotalIssuance must come from there, not from the parachain. + val stakingChainId = stakingOption.assetWithChain.chain.parentId ?: stakingOption.assetWithChain.chain.id + val totalIssuance = totalIssuanceRepository.getTotalIssuance(stakingChainId) + + Log.d("PEZ_STAKING", "create(4-param) exposures=${exposures.size} validatorsPrefs=${validatorsPrefs.size}") + Log.d("PEZ_STAKING", "exposureKeys=${exposures.keys.take(3).map { it.take(16) }}") + Log.d("PEZ_STAKING", "prefKeys=${validatorsPrefs.keys.take(3).map { it.take(16) }}") val validators = exposures.keys.mapNotNull { accountIdHex -> val exposure = exposures[accountIdHex] ?: accountIdNotFound(accountIdHex) @@ -60,14 +66,27 @@ class RewardCalculatorFactory( ) } - stakingOption.createRewardCalculator(validators, totalIssuance, scope) + Log.d("PEZ_STAKING", "totalIssuance=$totalIssuance validators=${validators.size} stakingChainId=${stakingChainId.take(12)}") + + stakingOption.createRewardCalculator(validators, totalIssuance, stakingChainId, scope) } suspend fun create(stakingOption: StakingOption, scope: CoroutineScope): RewardCalculator = withContext(Dispatchers.Default) { - val chainId = stakingOption.assetWithChain.chain.id + val chain = stakingOption.assetWithChain.chain + val chainId = chain.id + // For parachains with a parent relay chain, staking exposures live on the relay chain + val exposureChainId = chain.parentId ?: chainId - val exposures = shareStakingSharedComputation.get().electedExposuresInActiveEra(chainId, scope) - val validatorsPrefs = stakingRepository.getValidatorPrefs(chainId, exposures.keys) + Log.d("PEZ_STAKING", "RewardCalculatorFactory.create() chainId=${chainId.take(12)} exposureChainId=${exposureChainId.take(12)} stakingType=${stakingOption.additional.stakingType}") + + val activeEra = stakingRepository.getActiveEraIndex(exposureChainId) + Log.d("PEZ_STAKING", "ActiveEra: $activeEra for ${exposureChainId.take(12)}") + + val exposures = stakingRepository.getElectedValidatorsExposure(exposureChainId, activeEra) + Log.d("PEZ_STAKING", "Exposures: ${exposures.size}") + + val validatorsPrefs = stakingRepository.getValidatorPrefs(exposureChainId, exposures.keys) + Log.d("PEZ_STAKING", "ValidatorPrefs: ${validatorsPrefs.size}") create(stakingOption, exposures, validatorsPrefs, scope) } @@ -75,6 +94,7 @@ class RewardCalculatorFactory( private suspend fun StakingOption.createRewardCalculator( validators: List, totalIssuance: BigInteger, + stakingChainId: ChainId, scope: CoroutineScope ): RewardCalculator { return when (unwrapNominationPools().stakingType) { @@ -82,8 +102,9 @@ class RewardCalculatorFactory( val custom = customRelayChainCalculator(validators, totalIssuance, scope) if (custom != null) return custom - val activePublicParachains = parasRepository.activePublicParachains(assetWithChain.chain.id) - val inflationConfig = InflationConfig.create(chain.id, activePublicParachains) + // Query parachains from the relay chain, not from Asset Hub + val activePublicParachains = parasRepository.activePublicParachains(stakingChainId) + val inflationConfig = InflationConfig.create(stakingChainId, activePublicParachains) RewardCurveInflationRewardCalculator(validators, totalIssuance, inflationConfig) } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt index c484816..4e11d77 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt @@ -184,8 +184,8 @@ class RuntimeModule { @Provides @ApplicationScope fun provideTotalIssuanceRepository( - @Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource, - ): TotalIssuanceRepository = RealTotalIssuanceRepository(localStorageSource) + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, + ): TotalIssuanceRepository = RealTotalIssuanceRepository(remoteStorageSource) @Provides @ApplicationScope