fix: resolve parentId for Asset Hub staking to relay chain

ValidatorProvider, DirectStakingProperties, and DirectStakingRecommendation
were querying Asset Hub chainId for staking data (validators, exposures,
minStake, maxNominations) but these live on the relay chain. Added
chain.parentId resolution so parachain staking correctly routes to relay.

Also:
- Add VoterBagsList pallet support (Pezkuwi naming)
- Wrap BagListRepository queries in runCatching for binding compat
- Remove debug logging
This commit is contained in:
2026-02-15 22:25:48 +03:00
parent ffae9159fe
commit fa17968108
8 changed files with 34 additions and 38 deletions
@@ -257,7 +257,7 @@ fun Module.constantOrNull(name: String) = constants[name]
fun RuntimeMetadata.staking() = module(Modules.STAKING)
fun RuntimeMetadata.voterListOrNull() = firstExistingModuleOrNull(Modules.VOTER_LIST, Modules.BAG_LIST)
fun RuntimeMetadata.voterListOrNull() = firstExistingModuleOrNull(Modules.VOTER_LIST, Modules.BAG_LIST, Modules.VOTER_BAGS_LIST)
fun RuntimeMetadata.voterListName(): String = requireNotNull(voterListOrNull()).name
fun RuntimeMetadata.system() = module(Modules.SYSTEM)
@@ -632,6 +632,7 @@ object Modules {
const val VOTER_LIST = "VoterList"
const val BAG_LIST = "BagsList"
const val VOTER_BAGS_LIST = "VoterBagsList"
const val ELECTION_PROVIDER_MULTI_PHASE = "ElectionProviderMultiPhase"
@@ -42,6 +42,7 @@ class ProxyCallFilterFactory {
WhiteListFilter(Modules.SLOTS),
WhiteListFilter(Modules.AUCTIONS),
WhiteListFilter(Modules.VOTER_LIST),
WhiteListFilter(Modules.VOTER_BAGS_LIST),
WhiteListFilter(Modules.NOMINATION_POOLS),
WhiteListFilter(Modules.FAST_UNSTAKE)
)
@@ -62,6 +63,7 @@ class ProxyCallFilterFactory {
WhiteListFilter(Modules.UTILITY),
WhiteListFilter(Modules.FAST_UNSTAKE),
WhiteListFilter(Modules.VOTER_LIST),
WhiteListFilter(Modules.VOTER_BAGS_LIST),
WhiteListFilter(Modules.NOMINATION_POOLS)
)
@@ -42,15 +42,19 @@ class LocalBagListRepository(
) : BagListRepository {
override suspend fun bagThresholds(chainId: ChainId): List<BagListNode.Score>? {
return chainRegistry.withRuntime(chainId) {
runtime.metadata.voterListOrNull()?.constant("BagThresholds")?.getAs(collectionOf(::score))
}
return runCatching {
chainRegistry.withRuntime(chainId) {
runtime.metadata.voterListOrNull()?.constant("BagThresholds")?.getAs(collectionOf(::score))
}
}.getOrNull()
}
override suspend fun bagListSize(chainId: ChainId): BigInteger? {
return localStorage.query(chainId) {
runtime.metadata.voterListOrNull()?.storage("CounterForListNodes")?.query(binding = ::bindNumber)
}
return runCatching {
localStorage.query(chainId) {
runtime.metadata.voterListOrNull()?.storage("CounterForListNodes")?.query(binding = ::bindNumber)
}
}.getOrNull()
}
override suspend fun maxElectingVotes(chainId: ChainId): BigInteger? {
@@ -51,10 +51,6 @@ class RewardCalculatorFactory(
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)
val validatorPrefs = validatorsPrefs[accountIdHex] ?: return@mapNotNull null
@@ -66,8 +62,6 @@ class RewardCalculatorFactory(
)
}
Log.d("PEZ_STAKING", "totalIssuance=$totalIssuance validators=${validators.size} stakingChainId=${stakingChainId.take(12)}")
stakingOption.createRewardCalculator(validators, totalIssuance, stakingChainId, scope)
}
@@ -77,21 +71,11 @@ class RewardCalculatorFactory(
// For parachains with a parent relay chain, staking exposures live on the relay chain
val exposureChainId = chain.parentId ?: chainId
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)
}
@@ -98,8 +98,10 @@ private class DirectStakingProperties(
enoughAvailableToStake()
}
private val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id
override suspend fun minStake(): Balance {
return stakingSharedComputation.minStake(stakingOption.chain.id, scope)
return stakingSharedComputation.minStake(stakingChainId, scope)
}
private fun StartMultiStakingValidationSystemBuilder.noConflictingStaking() {
@@ -125,7 +127,7 @@ private class DirectStakingProperties(
private fun StartMultiStakingValidationSystemBuilder.maximumNominatorsReached() {
maximumNominatorsReached(
stakingRepository = stakingRepository,
chainId = { stakingOption.chain.id },
chainId = { stakingChainId },
errorProducer = { StartMultiStakingValidationFailure.MaxNominatorsReached(stakingType) }
)
}
@@ -29,7 +29,8 @@ class DirectStakingRecommendation(
override suspend fun recommendedSelection(stake: Balance): StartMultiStakingSelection {
val provider = recommendationSettingsProvider.await()
val maximumValidatorsPerNominator = stakingConstantsRepository.maxValidatorsPerNominator(stakingOption.chain.id, stake)
val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id
val maximumValidatorsPerNominator = stakingConstantsRepository.maxValidatorsPerNominator(stakingChainId, stake)
val recommendationSettings = provider.recommendedSettings(maximumValidatorsPerNominator)
val recommendator = recommendator.await()
@@ -45,20 +45,22 @@ class ValidatorProvider(
): List<Validator> {
val chain = stakingOption.assetWithChain.chain
val chainId = chain.id
// For parachains (e.g. Asset Hub), staking validators live on the parent relay chain
val stakingChainId = chain.parentId ?: chainId
val novaValidatorIds = validatorsPreferencesSource.getRecommendedValidatorIds(chainId)
val electedValidatorExposures = stakingSharedComputation.electedExposuresInActiveEra(chainId, scope)
val electedValidatorExposures = stakingSharedComputation.electedExposuresInActiveEra(stakingChainId, scope)
val requestedValidatorIds = sources.allValidatorIds(chainId, electedValidatorExposures, novaValidatorIds)
// we always need validator prefs for elected validators to construct reward calculator
val validatorIdsToQueryPrefs = electedValidatorExposures.keys + requestedValidatorIds
val validatorPrefs = stakingRepository.getValidatorPrefs(chainId, validatorIdsToQueryPrefs)
val validatorPrefs = stakingRepository.getValidatorPrefs(stakingChainId, validatorIdsToQueryPrefs)
val identities = identityRepository.getIdentitiesFromIdsHex(chainId, requestedValidatorIds)
val slashes = stakingRepository.getSlashes(chain.id, requestedValidatorIds)
val slashes = stakingRepository.getSlashes(stakingChainId, requestedValidatorIds)
val rewardCalculator = rewardCalculatorFactory.create(stakingOption, electedValidatorExposures, validatorPrefs, scope)
val maxNominators = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId)
val maxNominators = stakingConstantsRepository.maxRewardedNominatorPerValidator(stakingChainId)
return requestedValidatorIds.map { accountIdHex ->
val accountId = AccountIdKey.fromHex(accountIdHex).getOrThrow()
+8 -8
View File
@@ -1,6 +1,6 @@
[
{
"chainId": "bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75",
"chainId": "1aa94987791a5544e9667ec249d2cef1b8fdd6083c85b93fc37892d54a1156ca",
"name": "Pezkuwi",
"icon": "https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/main/icons/tokens/colored/HEZ.svg",
"addressPrefix": 42,
@@ -72,13 +72,13 @@
"feeViaRuntimeCall": true,
"disabledCheckMetadataHash": true,
"stakingMaxElectingVoters": 22500,
"identityChain": "58269e9c184f721e0309332d90cafc410df1519a5dc27a5fd9b3bf5fd2d129f8",
"identityChain": "69a8d025ab7b63363935d7d9397e0f652826c94271c1bc55c4fdfe72cccf1cfa",
"stakingWiki": "https://wiki.pezkuwichain.io/staking"
}
},
{
"chainId": "00d0e1d0581c3cd5c5768652d52f4520184018b44f56a2ae1e0dc9d65c00c948",
"parentId": "bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75",
"chainId": "e7c15092dcbe3f320260ddbbc685bfceed9125a3b3d8436db2766201dec3b949",
"parentId": "1aa94987791a5544e9667ec249d2cef1b8fdd6083c85b93fc37892d54a1156ca",
"name": "Pezkuwi Asset Hub",
"icon": "https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/main/icons/tokens/colored/PEZ.svg",
"addressPrefix": 42,
@@ -225,15 +225,15 @@
"disabledCheckMetadataHash": true,
"relaychainAsNative": true,
"stakingMaxElectingVoters": 22500,
"identityChain": "58269e9c184f721e0309332d90cafc410df1519a5dc27a5fd9b3bf5fd2d129f8",
"timelineChain": "bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75",
"identityChain": "69a8d025ab7b63363935d7d9397e0f652826c94271c1bc55c4fdfe72cccf1cfa",
"timelineChain": "1aa94987791a5544e9667ec249d2cef1b8fdd6083c85b93fc37892d54a1156ca",
"defaultBlockTime": 6000,
"stakingWiki": "https://wiki.pezkuwichain.io/staking"
}
},
{
"chainId": "58269e9c184f721e0309332d90cafc410df1519a5dc27a5fd9b3bf5fd2d129f8",
"parentId": "bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75",
"chainId": "69a8d025ab7b63363935d7d9397e0f652826c94271c1bc55c4fdfe72cccf1cfa",
"parentId": "1aa94987791a5544e9667ec249d2cef1b8fdd6083c85b93fc37892d54a1156ca",
"name": "Pezkuwi People",
"icon": "https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/main/icons/chains/PezkuwiPeople.png",
"addressPrefix": 42,