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