feat: add Pezkuwi Dashboard card with live People Chain data

- Dashboard card on Assets page showing roles, trust score, referral,
  staking, and perwerde points from People Chain pallets
- Repository queries: Tiki, Trust, Referral, StakingScore, Perwerde
- CachedStakingDetails double map query (RelayChain + AssetHub sources)
- Full i18n support across all 15 locales including new Turkish locale
- "Apply & Actions" button opens Telegram bot
- Staking improvements for split ecosystem multi-endpoint stats
This commit is contained in:
2026-02-17 00:10:23 +03:00
parent 9899bb5c40
commit 93e94cbf15
37 changed files with 741 additions and 48 deletions
@@ -38,6 +38,7 @@ suspend fun BagListRepository.bagListLocatorOrThrow(chainId: ChainId): BagListLo
class LocalBagListRepository(
private val localStorage: StorageDataSource,
private val remoteStorage: StorageDataSource,
private val chainRegistry: ChainRegistry
) : BagListRepository {
@@ -51,7 +52,7 @@ class LocalBagListRepository(
override suspend fun bagListSize(chainId: ChainId): BigInteger? {
return runCatching {
localStorage.query(chainId) {
remoteStorage.query(chainId) {
runtime.metadata.voterListOrNull()?.storage("CounterForListNodes")?.query(binding = ::bindNumber)
}
}.getOrNull()
@@ -346,7 +346,7 @@ class StakingRepositoryImpl(
val runtime = runtimeFor(chainId)
return runtime.metadata.staking().storageOrNull(storageName)?.let { storageEntry ->
localStorage.query(
remoteStorage.query(
keyBuilder = { storageEntry.storageKey() },
binding = { scale, _ -> scale?.let { binder(scale, runtime, storageEntry.returnType()) } },
chainId = chainId
@@ -225,8 +225,9 @@ class StakingFeatureModule {
@FeatureScope
fun provideBagListRepository(
@Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource,
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
chainRegistry: ChainRegistry
): BagListRepository = LocalBagListRepository(localStorageSource, chainRegistry)
): BagListRepository = LocalBagListRepository(localStorageSource, remoteStorageSource, chainRegistry)
@Provides
@FeatureScope
@@ -22,10 +22,12 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.repository.TotalIssuanceRepository
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
@@ -61,7 +63,15 @@ class StakingSharedComputation(
val key = "ACTIVE_ERA:$chainId"
return computationalCache.useSharedFlow(key, scope) {
stakingRepository.observeActiveEraIndex(chainId)
flow {
Log.d("PEZ_STAKE", "activeEraFlow: fetching remote activeEra for chainId=$chainId")
val era = stakingRepository.getActiveEraIndex(chainId)
Log.d("PEZ_STAKE", "activeEraFlow: got remote activeEra=$era")
emit(era)
Log.d("PEZ_STAKE", "activeEraFlow: starting local observation for chainId=$chainId")
emitAll(stakingRepository.observeActiveEraIndex(chainId))
}
}
}
@@ -70,7 +80,15 @@ class StakingSharedComputation(
return computationalCache.useSharedFlow(key, scope) {
activeEraFlow(chainId, scope).map { eraIndex ->
stakingRepository.getElectedValidatorsExposure(chainId, eraIndex) to eraIndex
Log.d("PEZ_STAKE", "electedExposures: fetching validators for chainId=$chainId, era=$eraIndex")
try {
val exposures = stakingRepository.getElectedValidatorsExposure(chainId, eraIndex)
Log.d("PEZ_STAKE", "electedExposures: got ${exposures.size} validators for chainId=$chainId")
exposures to eraIndex
} catch (e: Exception) {
Log.e("PEZ_STAKE", "electedExposures: FAILED for chainId=$chainId, era=$eraIndex", e)
throw e
}
}
}
}
@@ -79,24 +97,33 @@ class StakingSharedComputation(
val key = "MIN_STAKE:$chainId"
return computationalCache.useSharedFlow(key, scope) {
val minBond = stakingRepository.minimumNominatorBond(chainId)
val bagListLocator = bagListRepository.bagListLocatorOrNull(chainId)
val totalIssuance = totalIssuanceRepository.getTotalIssuance(chainId)
val bagListScoreConverter = BagListScoreConverter.U128(totalIssuance)
val maxElectingVoters = bagListRepository.maxElectingVotes(chainId)
val bagListSize = bagListRepository.bagListSize(chainId)
electedExposuresWithActiveEraFlow(chainId, scope).map { (exposures, activeEraIndex) ->
val minStake = minimumStake(
exposures = exposures.values,
minimumNominatorBond = minBond,
bagListLocator = bagListLocator,
bagListScoreConverter = bagListScoreConverter,
bagListSize = bagListSize,
maxElectingVoters = maxElectingVoters
)
Log.d("PEZ_STAKE", "activeEraInfo: calculating minStake for chainId=$chainId, era=$activeEraIndex, validators=${exposures.size}")
try {
val minBond = stakingRepository.minimumNominatorBond(chainId)
Log.d("PEZ_STAKE", "activeEraInfo: minBond=$minBond")
val bagListLocator = bagListRepository.bagListLocatorOrNull(chainId)
val totalIssuance = totalIssuanceRepository.getTotalIssuance(chainId)
val bagListScoreConverter = BagListScoreConverter.U128(totalIssuance)
val maxElectingVoters = bagListRepository.maxElectingVotes(chainId)
val bagListSize = bagListRepository.bagListSize(chainId)
Log.d("PEZ_STAKE", "activeEraInfo: bagListSize=$bagListSize, maxElectingVoters=$maxElectingVoters")
ActiveEraInfo(activeEraIndex, exposures, minStake)
val minStake = minimumStake(
exposures = exposures.values,
minimumNominatorBond = minBond,
bagListLocator = bagListLocator,
bagListScoreConverter = bagListScoreConverter,
bagListSize = bagListSize,
maxElectingVoters = maxElectingVoters
)
Log.d("PEZ_STAKE", "activeEraInfo: minStake=$minStake")
ActiveEraInfo(activeEraIndex, exposures, minStake)
} catch (e: Exception) {
Log.e("PEZ_STAKE", "activeEraInfo: FAILED for chainId=$chainId", e)
throw e
}
}
}
}
@@ -33,20 +33,16 @@ class NominationPoolRewardCalculatorFactory(
// 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,
@@ -26,7 +26,8 @@ class RealStartMultiStakingInteractor(
override suspend fun calculateFee(selection: StartMultiStakingSelection): Fee {
return withContext(Dispatchers.IO) {
extrinsicService.estimateFee(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) {
val chain = selection.stakingOption.chain
extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) {
startStaking(selection)
}
}
@@ -34,7 +35,8 @@ class RealStartMultiStakingInteractor(
override suspend fun startStaking(selection: StartMultiStakingSelection): Result<ExtrinsicExecutionResult> {
return withContext(Dispatchers.IO) {
extrinsicService.submitExtrinsicAndAwaitExecution(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) {
val chain = selection.stakingOption.chain
extrinsicService.submitExtrinsicAndAwaitExecution(chain, TransactionOrigin.SelectedWallet) {
startStaking(selection)
}.requireOk()
}
@@ -29,6 +29,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import android.util.Log
import kotlinx.coroutines.CoroutineScope
class DirectStakingPropertiesFactory(
@@ -101,7 +102,10 @@ private class DirectStakingProperties(
private val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id
override suspend fun minStake(): Balance {
return stakingSharedComputation.minStake(stakingChainId, scope)
Log.d("PEZ_STAKE", "DirectStaking.minStake() called, stakingChainId=$stakingChainId")
val result = stakingSharedComputation.minStake(stakingChainId, scope)
Log.d("PEZ_STAKE", "DirectStaking.minStake() returned: $result")
return result
}
private fun StartMultiStakingValidationSystemBuilder.noConflictingStaking() {
@@ -8,6 +8,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settin
import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.selection.StartMultiStakingSelection
import io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmount.SingleStakingRecommendation
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@@ -20,21 +21,42 @@ class DirectStakingRecommendation(
) : SingleStakingRecommendation {
private val recommendator = scope.async {
validatorRecommenderFactory.create(scope)
Log.d("PEZ_STAKE", "DirectRecommendation: creating validator recommender...")
try {
val result = validatorRecommenderFactory.create(scope)
Log.d("PEZ_STAKE", "DirectRecommendation: validator recommender created")
result
} catch (e: Exception) {
Log.e("PEZ_STAKE", "DirectRecommendation: validator recommender FAILED", e)
throw e
}
}
private val recommendationSettingsProvider = scope.async {
recommendationSettingsProviderFactory.create(scope)
Log.d("PEZ_STAKE", "DirectRecommendation: creating settings provider...")
try {
val result = recommendationSettingsProviderFactory.create(scope)
Log.d("PEZ_STAKE", "DirectRecommendation: settings provider created")
result
} catch (e: Exception) {
Log.e("PEZ_STAKE", "DirectRecommendation: settings provider FAILED", e)
throw e
}
}
override suspend fun recommendedSelection(stake: Balance): StartMultiStakingSelection {
Log.d("PEZ_STAKE", "DirectRecommendation: awaiting settings provider...")
val provider = recommendationSettingsProvider.await()
Log.d("PEZ_STAKE", "DirectRecommendation: got settings provider")
val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id
val maximumValidatorsPerNominator = stakingConstantsRepository.maxValidatorsPerNominator(stakingChainId, stake)
val recommendationSettings = provider.recommendedSettings(maximumValidatorsPerNominator)
Log.d("PEZ_STAKE", "DirectRecommendation: awaiting recommender...")
val recommendator = recommendator.await()
Log.d("PEZ_STAKE", "DirectRecommendation: got recommender, getting recommendations...")
val recommendedValidators = recommendator.recommendations(recommendationSettings)
Log.d("PEZ_STAKE", "DirectRecommendation: got ${recommendedValidators.size} recommended validators")
return DirectStakingSelection(
validators = recommendedValidators,
@@ -5,6 +5,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.pools.
import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.selection.StartMultiStakingSelection
import io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmount.SingleStakingRecommendation
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@@ -15,11 +16,24 @@ class NominationPoolRecommendation(
) : SingleStakingRecommendation {
private val recommendator = scope.async {
nominationPoolRecommenderFactory.create(stakingOption, scope)
Log.d("PEZ_STAKE", "NomPoolRecommendation: creating recommender...")
try {
val result = nominationPoolRecommenderFactory.create(stakingOption, scope)
Log.d("PEZ_STAKE", "NomPoolRecommendation: recommender created successfully")
result
} catch (e: Exception) {
Log.e("PEZ_STAKE", "NomPoolRecommendation: recommender creation FAILED", e)
throw e
}
}
override suspend fun recommendedSelection(stake: Balance): StartMultiStakingSelection? {
val recommendedPool = recommendator.await().recommendedPool() ?: return null
Log.d("PEZ_STAKE", "NomPoolRecommendation: awaiting recommender...")
val recommendedPool = recommendator.await().recommendedPool() ?: run {
Log.d("PEZ_STAKE", "NomPoolRecommendation: no recommended pool found")
return null
}
Log.d("PEZ_STAKE", "NomPoolRecommendation: recommended pool=${recommendedPool.id}")
return NominationPoolSelection(recommendedPool, stakingOption, stake)
}
@@ -13,6 +13,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmo
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
import android.util.Log
import kotlin.coroutines.cancellation.CancellationException
class AutomaticMultiStakingSelectionType(
private val candidates: List<SingleStakingProperties>,
@@ -44,8 +46,14 @@ class AutomaticMultiStakingSelectionType(
}
override suspend fun updateSelectionFor(stake: Balance) {
Log.d("PEZ_STAKE", "updateSelectionFor: stake=$stake")
val stakingProperties = typePropertiesFor(stake)
val candidates = stakingProperties.recommendation.recommendedSelection(stake) ?: return
Log.d("PEZ_STAKE", "updateSelectionFor: got properties type=${stakingProperties.stakingType}")
val candidates = stakingProperties.recommendation.recommendedSelection(stake) ?: run {
Log.d("PEZ_STAKE", "updateSelectionFor: recommendedSelection returned null, returning")
return
}
Log.d("PEZ_STAKE", "updateSelectionFor: got recommended selection")
val recommendableSelection = RecommendableMultiStakingSelection(
source = SelectionTypeSource.Automatic,
@@ -54,10 +62,26 @@ class AutomaticMultiStakingSelectionType(
)
selectionStore.updateSelection(recommendableSelection)
Log.d("PEZ_STAKE", "updateSelectionFor: selection updated successfully")
}
private suspend fun typePropertiesFor(stake: Balance): SingleStakingProperties {
return candidates.firstAllowingToStake(stake) ?: candidates.findWithMinimumStake()
Log.d("PEZ_STAKE", "typePropertiesFor: trying ${candidates.size} candidates")
for ((index, candidate) in candidates.withIndex()) {
Log.d("PEZ_STAKE", "typePropertiesFor: checking candidate $index type=${candidate.stakingType}")
try {
val minStake = candidate.minStake()
Log.d("PEZ_STAKE", "typePropertiesFor: candidate $index minStake=$minStake, stake=$stake, allows=${minStake <= stake}")
if (minStake <= stake) return candidate
} catch (e: CancellationException) {
Log.d("PEZ_STAKE", "typePropertiesFor: candidate $index cancelled, rethrowing")
throw e
} catch (e: Exception) {
Log.e("PEZ_STAKE", "typePropertiesFor: candidate $index minStake() threw", e)
}
}
Log.d("PEZ_STAKE", "typePropertiesFor: no candidate allows, finding minimum")
return candidates.findWithMinimumStake()
}
private suspend fun List<SingleStakingProperties>.firstAllowingToStake(stake: Balance): SingleStakingProperties? {