Initial commit: Pezkuwi Wallet Android

Complete rebrand of Nova Wallet for Pezkuwichain ecosystem.

## Features
- Full Pezkuwichain support (HEZ & PEZ tokens)
- Polkadot ecosystem compatibility
- Staking, Governance, DeFi, NFTs
- XCM cross-chain transfers
- Hardware wallet support (Ledger, Polkadot Vault)
- WalletConnect v2
- Push notifications

## Languages
- English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese

Based on Nova Wallet by Novasama Technologies GmbH
© Dijital Kurdistan Tech Institute 2026
This commit is contained in:
2026-01-23 01:31:12 +03:00
commit 31c8c5995f
7621 changed files with 425838 additions and 0 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,36 @@
package io.novafoundation.nova.feature_staking_impl.data
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.MultiStakingOptionIds
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset
val StakingOption.fullId
get() = StakingOptionId(chainId = assetWithChain.chain.id, assetWithChain.asset.id, additional.stakingType)
val StakingOption.components: Triple<Chain, Chain.Asset, Chain.Asset.StakingType>
get() = Triple(assetWithChain.chain, assetWithChain.asset, additional.stakingType)
val StakingOption.chain: Chain
get() = assetWithChain.chain
val StakingOption.asset: Chain.Asset
get() = assetWithChain.asset
val StakingOption.stakingType: Chain.Asset.StakingType
get() = additional.stakingType
suspend fun ChainRegistry.constructStakingOptions(stakingOptionId: MultiStakingOptionIds): List<StakingOption> {
val (chain, asset) = chainWithAsset(stakingOptionId.chainId, stakingOptionId.chainAssetId)
return stakingOptionId.stakingTypes.map { stakingType ->
createStakingOption(chain, asset, stakingType)
}
}
suspend fun ChainRegistry.constructStakingOption(stakingOptionId: StakingOptionId): StakingOption {
val (chain, asset) = chainWithAsset(stakingOptionId.chainId, stakingOptionId.chainAssetId)
return createStakingOption(chain, asset, stakingOptionId.stakingType)
}
@@ -0,0 +1,56 @@
package io.novafoundation.nova.feature_staking_impl.data
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.findStakingTypeBackingNominationPools
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState.SupportedAssetOption
import kotlinx.coroutines.flow.Flow
typealias StakingOption = SupportedAssetOption<StakingSharedState.OptionAdditionalData>
class StakingSharedState : SelectedAssetOptionSharedState<StakingSharedState.OptionAdditionalData> {
class OptionAdditionalData(val stakingType: Chain.Asset.StakingType)
private val _selectedOption = singleReplaySharedFlow<StakingOption>()
override val selectedOption: Flow<StakingOption> = _selectedOption
suspend fun setSelectedOption(
chain: Chain,
chainAsset: Chain.Asset,
stakingType: Chain.Asset.StakingType
) {
val selectedOption = createStakingOption(chain, chainAsset, stakingType)
setSelectedOption(selectedOption)
}
suspend fun setSelectedOption(option: StakingOption) {
_selectedOption.emit(option)
}
}
fun createStakingOption(chainWithAsset: ChainWithAsset, stakingType: Chain.Asset.StakingType): StakingOption {
return StakingOption(
assetWithChain = chainWithAsset,
additional = StakingSharedState.OptionAdditionalData(stakingType)
)
}
fun createStakingOption(chain: Chain, chainAsset: Chain.Asset, stakingType: Chain.Asset.StakingType): StakingOption {
return createStakingOption(
chainWithAsset = ChainWithAsset(chain, chainAsset),
stakingType = stakingType
)
}
fun StakingOption.unwrapNominationPools(): StakingOption {
return if (stakingType == Chain.Asset.StakingType.NOMINATION_POOLS) {
val backingType = assetWithChain.asset.findStakingTypeBackingNominationPools()
copy(additional = StakingSharedState.OptionAdditionalData(backingType))
} else {
this
}
}
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.cache
import io.novafoundation.nova.core_db.dao.StakingDashboardDao
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
interface StakingDashboardCache {
suspend fun update(
chainId: ChainId,
assetId: ChainAssetId,
stakingTypeLocal: String,
metaAccountId: Long,
updating: (previousValue: StakingDashboardItemLocal?) -> StakingDashboardItemLocal
)
}
class RealStakingDashboardCache(
private val dao: StakingDashboardDao
) : StakingDashboardCache {
override suspend fun update(
chainId: ChainId,
assetId: ChainAssetId,
stakingTypeLocal: String,
metaAccountId: Long,
updating: (previousValue: StakingDashboardItemLocal?) -> StakingDashboardItemLocal
) {
val fromCache = dao.getDashboardItem(chainId, assetId, stakingTypeLocal, metaAccountId)
val toInsert = updating(fromCache)
dao.insertItem(toInsert)
}
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.model
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.common.utils.Percent
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
class StakingDashboardItem(
val fullChainAssetId: FullChainAssetId,
val stakingType: Chain.Asset.StakingType,
val stakeState: StakeState,
) {
sealed interface StakeState {
val stats: ExtendedLoadingState<CommonStats>
class HasStake(
val stake: Balance,
override val stats: ExtendedLoadingState<Stats>,
) : StakeState {
class Stats(
val rewards: Balance,
val status: StakingStatus,
override val estimatedEarnings: Percent
) : CommonStats
enum class StakingStatus {
ACTIVE, INACTIVE, WAITING
}
}
class NoStake(override val stats: ExtendedLoadingState<Stats>) : StakeState {
class Stats(override val estimatedEarnings: Percent) : CommonStats
}
interface CommonStats {
val estimatedEarnings: Percent
}
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.utils.Identifiable
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
data class StakingDashboardOptionAccounts(
val stakingOptionId: StakingOptionId,
val stakingStatusAccount: AccountIdKey?,
val rewardsAccount: AccountIdKey?
) : Identifiable {
override val identifier: String = "${stakingOptionId.chainId}:${stakingOptionId.chainAssetId}:${stakingOptionId.stakingType}"
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats
import io.novafoundation.nova.common.utils.Percent
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
typealias MultiChainStakingStats = Map<StakingOptionId, ChainStakingStats>
class ChainStakingStats(
val estimatedEarnings: Percent,
val accountPresentInActiveStakers: Boolean,
val rewards: Balance
)
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
typealias StakingAccounts = Map<StakingOptionId, StakingOptionAccounts?>
data class StakingOptionAccounts(val rewards: AccountIdKey, val stakingStatus: AccountIdKey)
@@ -0,0 +1,106 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats
import io.novafoundation.nova.common.data.config.GlobalConfigDataSource
import io.novafoundation.nova.common.data.network.subquery.SubQueryNodes
import io.novafoundation.nova.common.utils.asPerbill
import io.novafoundation.nova.common.utils.atLeastZero
import io.novafoundation.nova.common.utils.orZero
import io.novafoundation.nova.common.utils.removeHexPrefix
import io.novafoundation.nova.common.utils.retryUntilDone
import io.novafoundation.nova.common.utils.toPercent
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api.StakingStatsApi
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api.StakingStatsRequest
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api.StakingStatsResponse
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api.StakingStatsResponse.AccumulatedReward
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api.StakingStatsResponse.WithStakingId
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api.StakingStatsRewards
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api.mapSubQueryIdToStakingType
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.ext.UTILITY_ASSET_ID
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface StakingStatsDataSource {
suspend fun fetchStakingStats(stakingAccounts: StakingAccounts, stakingChains: List<Chain>): MultiChainStakingStats
}
class RealStakingStatsDataSource(
private val api: StakingStatsApi,
private val globalConfigDataSource: GlobalConfigDataSource
) : StakingStatsDataSource {
override suspend fun fetchStakingStats(
stakingAccounts: StakingAccounts,
stakingChains: List<Chain>
): MultiChainStakingStats = withContext(Dispatchers.IO) {
retryUntilDone {
val request = StakingStatsRequest(stakingAccounts, stakingChains)
val globalConfig = globalConfigDataSource.getGlobalConfig()
val response = api.fetchStakingStats(request, globalConfig.multiStakingApiUrl).data
val earnings = response.stakingApies.associatedById()
val rewards = response.rewards?.associatedById() ?: emptyMap()
val slashes = response.slashes?.associatedById() ?: emptyMap()
val activeStakers = response.activeStakers?.groupedById() ?: emptyMap()
request.stakingKeysMapping.mapValues { (originalStakingOptionId, stakingKeys) ->
val totalReward = rewards.getPlanks(originalStakingOptionId) - slashes.getPlanks(originalStakingOptionId)
val stakingStatusAddress = stakingKeys.stakingStatusAddress
val stakingOptionActiveStakers = activeStakers[stakingKeys.stakingStatusOptionId].orEmpty()
val isStakingActive = stakingStatusAddress != null && stakingStatusAddress in stakingOptionActiveStakers
ChainStakingStats(
estimatedEarnings = earnings[originalStakingOptionId]?.maxAPY.orZero().asPerbill().toPercent(),
accountPresentInActiveStakers = isStakingActive,
rewards = totalReward.atLeastZero()
)
}
}
}
private fun Map<StakingOptionId, AccumulatedReward>.getPlanks(key: StakingOptionId): Balance {
return get(key)?.amount?.toBigInteger().orZero()
}
private fun <T : WithStakingId> SubQueryNodes<T>.associatedById(): Map<StakingOptionId, T> {
return nodes.associateBy {
StakingOptionId(
chainId = it.networkId.removeHexPrefix(),
chainAssetId = UTILITY_ASSET_ID,
stakingType = mapSubQueryIdToStakingType(it.stakingType)
)
}
}
private fun SubQueryNodes<StakingStatsResponse.ActiveStaker>.groupedById(): Map<StakingOptionId, List<String>> {
return nodes.groupBy(
keySelector = {
StakingOptionId(
chainId = it.networkId.removeHexPrefix(),
chainAssetId = UTILITY_ASSET_ID,
stakingType = mapSubQueryIdToStakingType(it.stakingType)
)
},
valueTransform = { it.address }
)
}
private fun StakingStatsRewards.associatedById(): Map<StakingOptionId, AccumulatedReward> {
return groupedAggregates.associateBy(
keySelector = { rewardAggregate ->
val (networkId, stakingTypeRaw) = rewardAggregate.keys
StakingOptionId(
chainId = networkId.removeHexPrefix(),
chainAssetId = UTILITY_ASSET_ID,
stakingType = mapSubQueryIdToStakingType(stakingTypeRaw)
)
},
valueTransform = { rewardAggregate -> rewardAggregate.sum }
)
}
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Url
interface StakingStatsApi {
@POST
suspend fun fetchStakingStats(
@Body request: StakingStatsRequest,
@Url url: String
): SubQueryResponse<StakingStatsResponse>
}
@@ -0,0 +1,166 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api
import io.novafoundation.nova.common.data.network.subquery.SubQueryFilters
import io.novafoundation.nova.common.data.network.subquery.SubqueryExpressions.and
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import io.novafoundation.nova.feature_staking_impl.data.createStakingOption
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.StakingAccounts
import io.novafoundation.nova.feature_staking_impl.data.fullId
import io.novafoundation.nova.feature_staking_impl.data.stakingType
import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.supportedStakingOptions
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
class StakingStatsRequest(stakingAccounts: StakingAccounts, chains: List<Chain>) {
@Transient
val stakingKeysMapping: Map<StakingOptionId, StakingKeys> = constructStakingTypeOverrides(chains, stakingAccounts)
val query = """
{
activeStakers${constructFilters(chains, FilterParent.STAKING_STATUS)} {
nodes {
networkId
stakingType
address
}
}
stakingApies {
nodes {
networkId
stakingType
maxAPY
}
}
rewards: rewards${constructFilters(chains, FilterParent.REWARD)} {
groupedAggregates(groupBy: [NETWORK_ID, STAKING_TYPE]) {
sum {
amount
}
keys
}
}
slashes: rewards${constructFilters(chains, FilterParent.SLASH)} {
groupedAggregates(groupBy: [NETWORK_ID, STAKING_TYPE]) {
sum {
amount
}
keys
}
}
}
""".trimIndent()
private fun constructStakingTypeOverrides(
chains: List<Chain>,
stakingAccounts: StakingAccounts
): Map<StakingOptionId, StakingKeys> {
return chains.flatMap { chain ->
val utilityAsset = chain.utilityAsset
utilityAsset.supportedStakingOptions().mapNotNull { stakingType ->
val stakingOption = createStakingOption(chain, utilityAsset, stakingType)
val stakingOptionId = stakingOption.fullId
val stakingOptionAccounts = stakingAccounts[stakingOptionId]
val stakingKeys = StakingKeys(
otherStakingOptionId = stakingOptionId,
stakingStatusAddress = stakingOptionAccounts?.stakingStatus?.value?.let(chain::addressOf),
rewardsAddress = stakingOptionAccounts?.rewards?.value?.let(chain::addressOf),
stakingStatusOptionId = stakingOptionId.copy(stakingType = stakingOption.unwrapNominationPools().stakingType)
)
stakingOptionId to stakingKeys
}
}.toMap()
}
private fun constructFilters(chains: List<Chain>, filterParent: FilterParent): String = with(SubQueryFilters) {
val targetAddresses = mutableSetOf<String>()
val targetNetworks = mutableSetOf<String>()
val targetStakingTypes = mutableSetOf<String>()
chains.forEach { chain ->
val utilityAsset = chain.utilityAsset
utilityAsset.supportedStakingOptions().forEach { stakingType ->
val stakingOption = createStakingOption(chain, utilityAsset, stakingType)
val stakingOptionId = stakingOption.fullId
val stakingKeys = stakingKeysMapping[stakingOptionId] ?: return@forEach
val address = stakingKeys.addressFor(filterParent) ?: return@forEach
val requestStakingType = stakingKeys.stakingTypeFor(filterParent)
val requestStakingTypeId = mapStakingTypeToSubQueryId(requestStakingType) ?: return@forEach
targetAddresses.add(address)
targetNetworks.add(chain.id.requireHexPrefix())
targetStakingTypes.add(requestStakingTypeId)
}
}
if (targetAddresses.isEmpty() || targetNetworks.isEmpty() || targetStakingTypes.isEmpty()) {
return@with ""
}
val addressFilter = "address" containedIn targetAddresses
val networkFilter = "networkId" containedIn targetNetworks
val typeFilter = "stakingType" containedIn targetStakingTypes
val aggregatedFilters = and(addressFilter, networkFilter, typeFilter)
val finalFilters = appendFiltersSpecificToParent(baseFilters = aggregatedFilters, filterParent)
queryParams(filter = finalFilters)
}
private fun SubQueryFilters.hasRewardType(type: String): String {
return "type" equalToEnum type
}
private fun SubQueryFilters.Companion.appendFiltersSpecificToParent(baseFilters: String, filterParent: FilterParent): String {
return when (filterParent) {
FilterParent.REWARD -> baseFilters and hasRewardType("reward")
FilterParent.SLASH -> baseFilters and hasRewardType("slash")
FilterParent.STAKING_STATUS -> baseFilters
}
}
private fun StakingKeys.addressFor(filterParent: FilterParent): String? {
return when (filterParent) {
FilterParent.REWARD, FilterParent.SLASH -> rewardsAddress
FilterParent.STAKING_STATUS -> stakingStatusAddress
}
}
private fun StakingKeys.stakingTypeFor(filterParent: FilterParent): Chain.Asset.StakingType {
return when (filterParent) {
FilterParent.REWARD, FilterParent.SLASH -> otherStakingOptionId.stakingType
FilterParent.STAKING_STATUS -> stakingStatusOptionId.stakingType
}
}
private infix fun String.containedIn(values: Set<String>): String {
val joinedValues = values.joinToString(separator = "\", \"", prefix = "\"", postfix = "\"")
return "$this: { in: [$joinedValues] }"
}
private enum class FilterParent {
REWARD, SLASH, STAKING_STATUS
}
class StakingKeys(
val otherStakingOptionId: StakingOptionId,
val stakingStatusOptionId: StakingOptionId,
val stakingStatusAddress: String?,
val rewardsAddress: String?,
)
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api
import io.novafoundation.nova.common.data.network.subquery.GroupedAggregate
import io.novafoundation.nova.common.data.network.subquery.SubQueryGroupedAggregates
import io.novafoundation.nova.common.data.network.subquery.SubQueryNodes
import java.math.BigDecimal
typealias StakingStatsRewards = SubQueryGroupedAggregates<GroupedAggregate.Sum<StakingStatsResponse.AccumulatedReward>>
class StakingStatsResponse(
val activeStakers: SubQueryNodes<ActiveStaker>?,
val stakingApies: SubQueryNodes<StakingApy>,
val rewards: SubQueryGroupedAggregates<GroupedAggregate.Sum<AccumulatedReward>>?,
val slashes: SubQueryGroupedAggregates<GroupedAggregate.Sum<AccumulatedReward>>?
) {
interface WithStakingId {
val networkId: String
val stakingType: String
}
class ActiveStaker(override val networkId: String, override val stakingType: String, val address: String) : WithStakingId
class StakingApy(override val networkId: String, override val stakingType: String, val maxAPY: Double) : WithStakingId
class AccumulatedReward(val amount: BigDecimal) // We use BigDecimal to support scientific notations
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.api
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
fun mapStakingTypeToSubQueryId(stakingType: Chain.Asset.StakingType): String? {
return when (stakingType) {
Chain.Asset.StakingType.UNSUPPORTED -> null
Chain.Asset.StakingType.RELAYCHAIN -> "relaychain"
Chain.Asset.StakingType.PARACHAIN -> "parachain"
Chain.Asset.StakingType.RELAYCHAIN_AURA -> "aura-relaychain"
Chain.Asset.StakingType.TURING -> "turing"
Chain.Asset.StakingType.ALEPH_ZERO -> "aleph-zero"
Chain.Asset.StakingType.NOMINATION_POOLS -> "nomination-pool"
Chain.Asset.StakingType.MYTHOS -> "mythos"
}
}
fun mapSubQueryIdToStakingType(subQueryStakingTypeId: String?): Chain.Asset.StakingType {
return when (subQueryStakingTypeId) {
null -> Chain.Asset.StakingType.UNSUPPORTED
"relaychain" -> Chain.Asset.StakingType.RELAYCHAIN
"parachain" -> Chain.Asset.StakingType.PARACHAIN
"aura-relaychain" -> Chain.Asset.StakingType.RELAYCHAIN_AURA
"turing" -> Chain.Asset.StakingType.TURING
"aleph-zero" -> Chain.Asset.StakingType.ALEPH_ZERO
"nomination-pool" -> Chain.Asset.StakingType.NOMINATION_POOLS
"mythos" -> Chain.Asset.StakingType.MYTHOS
else -> Chain.Asset.StakingType.UNSUPPORTED
}
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.MultiChainStakingStats
data class MultiChainOffChainSyncResult(
val index: Int,
val multiChainStakingStats: MultiChainStakingStats,
)
@@ -0,0 +1,188 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters
import android.util.Log
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.utils.CollectionDiffer
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.inserted
import io.novafoundation.nova.common.utils.mergeIfMultiple
import io.novafoundation.nova.common.utils.throttleLast
import io.novafoundation.nova.common.utils.zipWithPrevious
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_staking_api.data.dashboard.StakingDashboardUpdateSystem
import io.novafoundation.nova.feature_staking_api.data.dashboard.SyncingStageMap
import io.novafoundation.nova.feature_staking_api.data.dashboard.getSyncingStage
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.AggregatedStakingDashboardOption.SyncingStage
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import io.novafoundation.nova.feature_staking_api.data.dashboard.common.stakingChains
import io.novafoundation.nova.feature_staking_impl.data.dashboard.model.StakingDashboardOptionAccounts
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.StakingAccounts
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.StakingOptionAccounts
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.StakingStatsDataSource
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain.StakingDashboardUpdaterEvent
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain.StakingDashboardUpdaterFactory
import io.novafoundation.nova.feature_staking_impl.data.dashboard.repository.StakingDashboardRepository
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.ext.supportedStakingOptions
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.withIndex
import kotlin.coroutines.coroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
private const val EMPTY_OFF_CHAIN_SYNC_INDEX = -1
class RealStakingDashboardUpdateSystem(
private val stakingStatsDataSource: StakingStatsDataSource,
private val accountRepository: AccountRepository,
private val chainRegistry: ChainRegistry,
private val updaterFactory: StakingDashboardUpdaterFactory,
private val sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
private val stakingDashboardRepository: StakingDashboardRepository,
private val offChainSyncDebounceRate: Duration = 1.seconds
) : StakingDashboardUpdateSystem {
override val syncedItemsFlow: MutableStateFlow<SyncingStageMap> = MutableStateFlow(emptyMap())
private val latestOffChainSyncIndex: MutableStateFlow<Int> = MutableStateFlow(EMPTY_OFF_CHAIN_SYNC_INDEX)
override fun start(): Flow<Updater.SideEffect> {
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
val accountScope = CoroutineScope(coroutineContext)
syncedItemsFlow.emit(emptyMap())
latestOffChainSyncIndex.emit(EMPTY_OFF_CHAIN_SYNC_INDEX)
val stakingChains = chainRegistry.stakingChains()
val stakingOptionsWithChain = stakingChains.associateWithStakingOptions()
val offChainSyncFlow = debouncedOffChainSyncFlow(metaAccount, stakingOptionsWithChain, stakingChains)
.shareIn(accountScope, started = SharingStarted.Eagerly, replay = 1)
val updateFlows = stakingChains.map { stakingChain ->
flowOfAll {
val sharedRequestsBuilder = sharedRequestsBuilderFactory.create(stakingChain.id)
val chainUpdates = stakingChain.utilityAsset.supportedStakingOptions().mapNotNull { stakingType ->
val updater = updaterFactory.createUpdater(stakingChain, stakingType, metaAccount, offChainSyncFlow)
?: return@mapNotNull null
updater.listenForUpdates(sharedRequestsBuilder, Unit)
}
sharedRequestsBuilder.subscribe(accountScope)
chainUpdates.mergeIfMultiple()
}.catch {
Log.d("StakingDashboardUpdateSystem", "Failed to sync staking dashboard status for ${stakingChain.name}")
}
}
updateFlows.merge()
.filterIsInstance<StakingDashboardUpdaterEvent>()
.onEach(::handleUpdaterEvent)
}
.onCompletion {
syncedItemsFlow.emit(emptyMap())
}
}
private fun debouncedOffChainSyncFlow(
metaAccount: MetaAccount,
stakingOptionsWithChain: Map<StakingOptionId, Chain>,
stakingChains: List<Chain>
): Flow<MultiChainOffChainSyncResult> {
return stakingDashboardRepository.stakingAccountsFlow(metaAccount.id)
.map { stakingPrimaryAccounts -> constructStakingAccounts(stakingOptionsWithChain, metaAccount, stakingPrimaryAccounts) }
.zipWithPrevious()
.transform { (previousAccounts, currentAccounts) ->
if (previousAccounts != null) {
val diff = CollectionDiffer.findDiff(previousAccounts, currentAccounts, forceUseNewItems = false)
if (diff.newOrUpdated.isNotEmpty()) {
markSyncingSecondaryFor(diff.newOrUpdated)
emit(currentAccounts)
}
} else {
emit(currentAccounts)
}
}
.withIndex()
.onEach { latestOffChainSyncIndex.value = it.index }
.throttleLast(offChainSyncDebounceRate)
.mapLatest { (index, stakingAccounts) ->
MultiChainOffChainSyncResult(
index = index,
multiChainStakingStats = stakingStatsDataSource.fetchStakingStats(stakingAccounts, stakingChains),
)
}
}
private fun markSyncingSecondaryFor(changedPrimaryAccounts: List<Map.Entry<StakingOptionId, StakingOptionAccounts?>>) {
val result = syncedItemsFlow.value.toMutableMap()
changedPrimaryAccounts.forEach { (stakingOptionId, _) ->
result[stakingOptionId] = result.getSyncingStage(stakingOptionId).coerceAtMost(SyncingStage.SYNCING_SECONDARY)
}
syncedItemsFlow.value = result
}
private fun List<Chain>.associateWithStakingOptions(): Map<StakingOptionId, Chain> {
return flatMap { chain ->
chain.assets.flatMap { asset ->
asset.supportedStakingOptions().map {
StakingOptionId(chain.id, asset.id, it) to chain
}
}
}.toMap()
}
private fun constructStakingAccounts(
stakingOptionIds: Map<StakingOptionId, Chain>,
metaAccount: MetaAccount,
knownPrimaryAccounts: List<StakingDashboardOptionAccounts>
): StakingAccounts {
val knownStakingAccountsByOptionId = knownPrimaryAccounts.associateBy(StakingDashboardOptionAccounts::stakingOptionId)
return stakingOptionIds.mapValues { (optionId, chain) ->
val knownPrimaryAccount = knownStakingAccountsByOptionId[optionId]
val default = metaAccount.accountIdIn(chain) ?: return@mapValues null
val stakeStatusAccount = knownPrimaryAccount?.stakingStatusAccount?.value ?: default
val rewardsAccount = knownPrimaryAccount?.rewardsAccount?.value ?: default
StakingOptionAccounts(rewards = rewardsAccount.intoKey(), stakingStatus = stakeStatusAccount.intoKey())
}
}
private fun handleUpdaterEvent(event: StakingDashboardUpdaterEvent) {
when (event) {
is StakingDashboardUpdaterEvent.AllSynced -> {
// we only mark option as synced if there are no fresher syncs
if (event.indexOfUsedOffChainSync >= latestOffChainSyncIndex.value) {
syncedItemsFlow.value = syncedItemsFlow.value.inserted(event.option, SyncingStage.SYNCED)
}
}
is StakingDashboardUpdaterEvent.PrimarySynced -> {
syncedItemsFlow.value = syncedItemsFlow.value.inserted(event.option, SyncingStage.SYNCING_SECONDARY)
}
}
}
}
@@ -0,0 +1,49 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain
import io.novafoundation.nova.core.updater.GlobalScopeUpdater
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import io.novafoundation.nova.feature_staking_impl.data.dashboard.cache.StakingDashboardCache
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapStakingTypeToStakingString
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
abstract class BaseStakingDashboardUpdater(
protected val chain: Chain,
protected val chainAsset: Chain.Asset,
protected val stakingType: Chain.Asset.StakingType,
protected val metaAccount: MetaAccount,
) : GlobalScopeUpdater {
protected val stakingTypeLocal = requireNotNull(mapStakingTypeToStakingString(stakingType))
override val requiredModules: List<String> = emptyList()
abstract suspend fun listenForUpdates(storageSubscriptionBuilder: SharedRequestsBuilder): Flow<Updater.SideEffect>
override suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder,
scopeValue: Unit
): Flow<Updater.SideEffect> {
return listenForUpdates(storageSubscriptionBuilder)
}
protected fun primarySynced(): StakingDashboardUpdaterEvent {
return StakingDashboardUpdaterEvent.PrimarySynced(stakingOptionId())
}
protected fun secondarySynced(indexOfUsedOffChainSync: Int): StakingDashboardUpdaterEvent {
return StakingDashboardUpdaterEvent.AllSynced(stakingOptionId(), indexOfUsedOffChainSync)
}
protected fun stakingOptionId(): StakingOptionId {
return StakingOptionId(chain.id, chainAsset.id, stakingType)
}
protected suspend fun StakingDashboardCache.update(updating: (StakingDashboardItemLocal?) -> StakingDashboardItemLocal) {
update(chain.id, chainAsset.id, stakingTypeLocal, metaAccount.id, updating)
}
}
@@ -0,0 +1,180 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.utils.isZero
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.common.utils.orZero
import io.novafoundation.nova.common.utils.takeUnlessZero
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.accountIdKeyIn
import io.novafoundation.nova.feature_staking_impl.data.dashboard.cache.StakingDashboardCache
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.MultiChainStakingStats
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.MultiChainOffChainSyncResult
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.userStake
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.UserStakeInfo
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.hasActiveCollators
import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.observeMythosLocks
import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.total
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.session
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.validators
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.SessionValidators
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.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.cache.StorageCachingContext
import io.novafoundation.nova.runtime.storage.cache.cacheValues
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
class StakingDashboardMythosUpdater(
chain: Chain,
chainAsset: Chain.Asset,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
private val stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
private val balanceLocksRepository: BalanceLocksRepository,
private val stakingDashboardCache: StakingDashboardCache,
override val storageCache: StorageCache,
private val remoteStorageSource: StorageDataSource,
) : BaseStakingDashboardUpdater(chain, chainAsset, stakingType, metaAccount),
StorageCachingContext by StorageCachingContext(storageCache) {
override suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder
): Flow<Updater.SideEffect> {
return subscribeToOnChainState(storageSubscriptionBuilder).transformLatest { onChainState ->
saveItem(onChainState, secondaryInfo = null)
emit(primarySynced())
val secondarySyncFlow = stakingStatsFlow.map { (index, stakingStats) ->
val secondaryInfo = constructSecondaryInfo(onChainState, stakingStats)
saveItem(onChainState, secondaryInfo)
secondarySynced(index)
}
emitAll(secondarySyncFlow)
}
}
private suspend fun subscribeToOnChainState(storageSubscriptionBuilder: SharedRequestsBuilder): Flow<OnChainInfo?> {
val accountId = metaAccount.accountIdKeyIn(chain) ?: return flowOf(null)
return combine(
subscribeToTotalStake(),
subscribeToUserStake(storageSubscriptionBuilder, accountId),
sessionValidatorsFlow(storageSubscriptionBuilder)
) { totalStake, userStakeInfo, sessionValidators ->
constructOnChainInfo(totalStake, userStakeInfo, accountId, sessionValidators)
}
}
private suspend fun sessionValidatorsFlow(storageSubscriptionBuilder: SharedRequestsBuilder): Flow<Set<AccountIdKey>> {
return remoteStorageSource.subscribe(chain.id, storageSubscriptionBuilder) {
metadata.session.validators.observeNonNull()
}
}
private fun constructOnChainInfo(
totalStake: Balance?,
userStakeInfo: UserStakeInfo?,
accountId: AccountIdKey,
sessionValidators: SessionValidators,
): OnChainInfo? {
if (totalStake == null) return null
val hasActiveValidators = userStakeInfo.hasActiveCollators(sessionValidators)
val activeStake = userStakeInfo?.balance.orZero()
return OnChainInfo(activeStake, accountId, hasActiveValidators)
}
private fun constructSecondaryInfo(
baseInfo: OnChainInfo?,
multiChainStakingStats: MultiChainStakingStats,
): SecondaryInfo? {
val chainStakingStats = multiChainStakingStats[stakingOptionId()] ?: return null
return SecondaryInfo(
rewards = chainStakingStats.rewards,
estimatedEarnings = chainStakingStats.estimatedEarnings.value,
status = determineStakingStatus(baseInfo)
)
}
private fun determineStakingStatus(baseInfo: OnChainInfo?): StakingDashboardItemLocal.Status? {
return when {
baseInfo == null -> null
baseInfo.activeStake.isZero -> StakingDashboardItemLocal.Status.INACTIVE
baseInfo.hasActiveCollators -> StakingDashboardItemLocal.Status.ACTIVE
else -> StakingDashboardItemLocal.Status.INACTIVE
}
}
private fun subscribeToTotalStake(): Flow<Balance?> {
return balanceLocksRepository.observeMythosLocks(metaAccount.id, chain, chainAsset).map { mythosLocks ->
mythosLocks.total.takeUnlessZero()
}
}
private suspend fun subscribeToUserStake(
storageSubscriptionBuilder: SharedRequestsBuilder,
accountId: AccountIdKey
): Flow<UserStakeInfo?> {
return remoteStorageSource.subscribe(chain.id, storageSubscriptionBuilder) {
metadata.collatorStaking.userStake.observeWithRaw(accountId.value)
.cacheValues()
}
}
private suspend fun saveItem(
onChainInfo: OnChainInfo?,
secondaryInfo: SecondaryInfo?
) = stakingDashboardCache.update { fromCache ->
if (onChainInfo != null) {
StakingDashboardItemLocal.staking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
stake = onChainInfo.activeStake,
status = secondaryInfo?.status ?: fromCache?.status,
rewards = secondaryInfo?.rewards ?: fromCache?.rewards,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings,
stakeStatusAccount = onChainInfo.accountId.value,
rewardsAccount = onChainInfo.accountId.value
)
} else {
StakingDashboardItemLocal.notStaking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings
)
}
}
private class OnChainInfo(
val activeStake: Balance,
val accountId: AccountIdKey,
val hasActiveCollators: Boolean
)
private class SecondaryInfo(
val rewards: Balance,
val estimatedEarnings: Double,
val status: StakingDashboardItemLocal.Status?
)
}
@@ -0,0 +1,229 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain
import io.novafoundation.nova.common.utils.combineToPair
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.isZero
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation
import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex
import io.novafoundation.nova.feature_staking_api.domain.model.Nominations
import io.novafoundation.nova.feature_staking_api.domain.model.activeBalance
import io.novafoundation.nova.feature_staking_api.domain.nominationPool.model.PoolId
import io.novafoundation.nova.feature_staking_impl.data.dashboard.cache.StakingDashboardCache
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.ChainStakingStats
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.MultiChainStakingStats
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.MultiChainOffChainSyncResult
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.activeEra
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.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.models.BondedPool
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolPoints
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolStateRepository
import io.novafoundation.nova.feature_staking_impl.domain.common.isWaiting
import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.model.PoolBalanceConvertable
import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.model.amountOf
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.cache.StorageCachingContext
import io.novafoundation.nova.runtime.storage.cache.cacheValues
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
import io.novafoundation.nova.common.utils.metadata
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.transformLatest
import kotlin.coroutines.coroutineContext
class StakingDashboardNominationPoolsUpdater(
chain: Chain,
chainAsset: Chain.Asset,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
private val stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
private val stakingDashboardCache: StakingDashboardCache,
private val remoteStorageSource: StorageDataSource,
private val nominationPoolStateRepository: NominationPoolStateRepository,
private val poolAccountDerivation: PoolAccountDerivation,
storageCache: StorageCache,
) : BaseStakingDashboardUpdater(chain, chainAsset, stakingType, metaAccount),
StorageCachingContext by StorageCachingContext(storageCache) {
override suspend fun listenForUpdates(storageSubscriptionBuilder: SharedRequestsBuilder): Flow<Updater.SideEffect> {
return remoteStorageSource.subscribe(chain.id, storageSubscriptionBuilder) {
val stakingStateFlow = subscribeToStakingState()
val activeEraFlow = metadata.staking.activeEra
.observeWithRaw()
.cacheValues()
.filterNotNull()
combineToPair(stakingStateFlow, activeEraFlow)
}
.transformLatest { (onChainInfo, activeEra) ->
saveItem(onChainInfo, secondaryInfo = null)
emit(primarySynced())
val secondarySyncFlow = stakingStatsFlow.map { (index, stakingStats) ->
val secondaryInfo = constructSecondaryInfo(onChainInfo, activeEra, stakingStats)
saveItem(onChainInfo, secondaryInfo)
secondarySynced(index)
}
emitAll(secondarySyncFlow)
}
}
private suspend fun StorageQueryContext.subscribeToStakingState(): Flow<PoolsOnChainInfo?> {
val accountId = metaAccount.accountIdIn(chain) ?: return flowOf(null)
val poolMemberFlow = metadata.nominationPools.poolMembers
.observeWithRaw(accountId)
.cacheValues()
return flowOfAll {
val poolMemberFlowShared = poolMemberFlow
.shareIn(CoroutineScope(coroutineContext), SharingStarted.Lazily, replay = 1)
val poolAggregatedStateFlow = poolMemberFlowShared
.map { it?.poolId }
.distinctUntilChanged()
.flatMapLatest(::subscribeToPoolWithBalance)
combine(poolMemberFlow, poolAggregatedStateFlow) { poolMember, poolWithBalance ->
if (poolMember != null && poolWithBalance != null) {
PoolsOnChainInfo(poolMember, poolWithBalance)
} else {
null
}
}
}
}
private suspend fun subscribeToPoolWithBalance(poolId: PoolId?): Flow<PoolAggregatedState?> {
if (poolId == null) return flowOf(null)
val bondedPoolAccountId = poolAccountDerivation.derivePoolAccount(poolId, PoolAccountDerivation.PoolAccountType.BONDED, chain.id)
return remoteStorageSource.subscribeBatched(chain.id) {
val bondedPoolFlow = metadata.nominationPools.bondedPools.observeWithRaw(poolId.value)
.cacheValues()
.filterNotNull()
val poolNominationsFlow = nominationPoolStateRepository.observePoolNominations(bondedPoolAccountId)
.cacheValues()
val activeStakeFlow = nominationPoolStateRepository.observeBondedPoolLedger(bondedPoolAccountId)
.cacheValues()
.map { it.activeBalance() }
combine(
bondedPoolFlow,
poolNominationsFlow,
activeStakeFlow,
) { bondedPool, nominations, balance ->
PoolAggregatedState(bondedPool, nominations, balance, bondedPoolAccountId)
}
}
}
private suspend fun saveItem(
relaychainStakingBaseInfo: PoolsOnChainInfo?,
secondaryInfo: NominationPoolsSecondaryInfo?,
) = stakingDashboardCache.update { fromCache ->
if (relaychainStakingBaseInfo != null) {
StakingDashboardItemLocal.staking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
stake = relaychainStakingBaseInfo.stakedBalance(),
status = secondaryInfo?.status ?: fromCache?.status,
rewards = secondaryInfo?.rewards ?: fromCache?.rewards,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings,
stakeStatusAccount = relaychainStakingBaseInfo.poolAggregatedState.poolStash,
rewardsAccount = relaychainStakingBaseInfo.poolMember.accountId
)
} else {
StakingDashboardItemLocal.notStaking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings
)
}
}
private fun constructSecondaryInfo(
baseInfo: PoolsOnChainInfo?,
activeEra: EraIndex,
multiChainStakingStats: MultiChainStakingStats,
): NominationPoolsSecondaryInfo? {
val chainStakingStats = multiChainStakingStats[stakingOptionId()] ?: return null
return NominationPoolsSecondaryInfo(
rewards = chainStakingStats.rewards,
estimatedEarnings = chainStakingStats.estimatedEarnings.value,
status = determineStakingStatus(baseInfo, activeEra, chainStakingStats)
)
}
private fun determineStakingStatus(
baseInfo: PoolsOnChainInfo?,
activeEra: EraIndex,
chainStakingStats: ChainStakingStats,
): StakingDashboardItemLocal.Status? {
return when {
baseInfo == null -> null
baseInfo.poolMember.points.value.isZero -> StakingDashboardItemLocal.Status.INACTIVE
chainStakingStats.accountPresentInActiveStakers -> StakingDashboardItemLocal.Status.ACTIVE
baseInfo.poolAggregatedState.poolNominations != null && baseInfo.poolAggregatedState.poolNominations.isWaiting(activeEra) -> {
StakingDashboardItemLocal.Status.WAITING
}
else -> StakingDashboardItemLocal.Status.INACTIVE
}
}
private class PoolsOnChainInfo(
val poolMember: PoolMember,
val poolAggregatedState: PoolAggregatedState
) {
fun stakedBalance(): Balance {
return poolAggregatedState.amountOf(poolMember.points)
}
}
private class PoolAggregatedState(
val pool: BondedPool,
val poolNominations: Nominations?,
override val poolBalance: Balance,
val poolStash: AccountId
) : PoolBalanceConvertable {
override val poolPoints: PoolPoints = pool.points
}
private class NominationPoolsSecondaryInfo(
val rewards: Balance,
val estimatedEarnings: Double,
val status: StakingDashboardItemLocal.Status?
)
}
@@ -0,0 +1,164 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain
import io.novafoundation.nova.common.address.get
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.utils.isZero
import io.novafoundation.nova.common.utils.parachainStaking
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal
import io.novafoundation.nova.feature_account_api.data.model.AccountIdKeyMap
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState
import io.novafoundation.nova.feature_staking_api.domain.model.parachain.activeBonded
import io.novafoundation.nova.feature_staking_impl.data.dashboard.cache.StakingDashboardCache
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.ChainStakingStats
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.MultiChainStakingStats
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.MultiChainOffChainSyncResult
import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.network.bindings.CandidateMetadata
import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.network.bindings.bindCandidateMetadata
import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.network.bindings.bindDelegatorState
import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.network.bindings.isActive
import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.network.bindings.isStakeEnoughToEarnRewards
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
class StakingDashboardParachainStakingUpdater(
chain: Chain,
chainAsset: Chain.Asset,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
private val stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
private val stakingDashboardCache: StakingDashboardCache,
private val remoteStorageSource: StorageDataSource
) : BaseStakingDashboardUpdater(chain, chainAsset, stakingType, metaAccount) {
override suspend fun listenForUpdates(storageSubscriptionBuilder: SharedRequestsBuilder): Flow<Updater.SideEffect> {
return remoteStorageSource.subscribe(chain.id, storageSubscriptionBuilder) { subscribeToStakingState() }
.transformLatest { parachainStakingBaseInfo ->
saveItem(parachainStakingBaseInfo, secondaryInfo = null)
emit(primarySynced())
val secondarySyncFlow = stakingStatsFlow.map { (index, stakingStats) ->
val secondaryInfo = constructSecondaryInfo(parachainStakingBaseInfo, stakingStats)
saveItem(parachainStakingBaseInfo, secondaryInfo)
secondarySynced(index)
}
emitAll(secondarySyncFlow)
}
}
private suspend fun StorageQueryContext.subscribeToStakingState(): Flow<ParachainStakingBaseInfo?> {
val accountId = metaAccount.accountIdIn(chain) ?: return flowOf(null)
val delegatorStateFlow = runtime.metadata.parachainStaking().storage("DelegatorState").observe(
accountId,
binding = { bindDelegatorState(it, accountId, chain, chainAsset) }
)
return delegatorStateFlow.map { delegatorState ->
if (delegatorState is DelegatorState.Delegator) {
val delegationKeys = delegatorState.delegations.map { listOf(it.owner) }
val collatorMetadatas = remoteStorageSource.query(chain.id) {
runtime.metadata.parachainStaking().storage("CandidateInfo").entries(
keysArguments = delegationKeys,
keyExtractor = { (candidateId: AccountId) -> candidateId.intoKey() },
binding = { decoded, _ -> bindCandidateMetadata(decoded) }
)
}
ParachainStakingBaseInfo(delegatorState, collatorMetadatas)
} else {
null
}
}
}
private suspend fun saveItem(
parachainStakingBaseInfo: ParachainStakingBaseInfo?,
secondaryInfo: ParachainStakingSecondaryInfo?
) = stakingDashboardCache.update { fromCache ->
if (parachainStakingBaseInfo != null) {
StakingDashboardItemLocal.staking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
stake = parachainStakingBaseInfo.delegatorState.activeBonded,
status = secondaryInfo?.status ?: fromCache?.status,
rewards = secondaryInfo?.rewards ?: fromCache?.rewards,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings,
stakeStatusAccount = parachainStakingBaseInfo.delegatorState.accountId,
rewardsAccount = parachainStakingBaseInfo.delegatorState.accountId
)
} else {
StakingDashboardItemLocal.notStaking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings
)
}
}
private fun constructSecondaryInfo(
baseInfo: ParachainStakingBaseInfo?,
multiChainStakingStats: MultiChainStakingStats,
): ParachainStakingSecondaryInfo? {
val chainStakingStats = multiChainStakingStats[stakingOptionId()] ?: return null
return ParachainStakingSecondaryInfo(
rewards = chainStakingStats.rewards,
estimatedEarnings = chainStakingStats.estimatedEarnings.value,
status = determineStakingStatus(baseInfo, chainStakingStats)
)
}
private fun determineStakingStatus(
baseInfo: ParachainStakingBaseInfo?,
chainStakingStats: ChainStakingStats,
): StakingDashboardItemLocal.Status? {
return when {
baseInfo == null -> null
baseInfo.delegatorState.activeBonded.isZero -> StakingDashboardItemLocal.Status.INACTIVE
chainStakingStats.accountPresentInActiveStakers -> StakingDashboardItemLocal.Status.ACTIVE
baseInfo.hasWaitingCollators() -> StakingDashboardItemLocal.Status.WAITING
else -> StakingDashboardItemLocal.Status.INACTIVE
}
}
private fun ParachainStakingBaseInfo.hasWaitingCollators(): Boolean {
return delegatorState.delegations.any { delegatorBond ->
val delegateMetadata = delegatesMetadata[delegatorBond.owner]
delegateMetadata != null && delegateMetadata.isActive && delegateMetadata.isStakeEnoughToEarnRewards(delegatorBond.balance)
}
}
}
private class ParachainStakingBaseInfo(
val delegatorState: DelegatorState.Delegator,
val delegatesMetadata: AccountIdKeyMap<CandidateMetadata>
) {
companion object
}
private class ParachainStakingSecondaryInfo(
val rewards: Balance,
val estimatedEarnings: Double,
val status: StakingDashboardItemLocal.Status?
)
@@ -0,0 +1,176 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain
import io.novafoundation.nova.common.utils.combineToPair
import io.novafoundation.nova.common.utils.isZero
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal.Status
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex
import io.novafoundation.nova.feature_staking_api.domain.model.Nominations
import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger
import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs
import io.novafoundation.nova.feature_staking_impl.data.dashboard.cache.StakingDashboardCache
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.ChainStakingStats
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.stats.MultiChainStakingStats
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.MultiChainOffChainSyncResult
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.activeEra
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.bonded
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.ledger
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.nominators
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.staking
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.validators
import io.novafoundation.nova.feature_staking_impl.domain.common.isWaiting
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull
import io.novafoundation.nova.common.utils.metadata
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
class StakingDashboardRelayStakingUpdater(
chain: Chain,
chainAsset: Chain.Asset,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
private val stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
private val stakingDashboardCache: StakingDashboardCache,
private val remoteStorageSource: StorageDataSource
) : BaseStakingDashboardUpdater(chain, chainAsset, stakingType, metaAccount) {
override suspend fun listenForUpdates(storageSubscriptionBuilder: SharedRequestsBuilder): Flow<Updater.SideEffect> {
val accountId = metaAccount.accountIdIn(chain)
return remoteStorageSource.subscribe(chain.id, storageSubscriptionBuilder) {
val activeEraFlow = metadata.staking.activeEra.observeNonNull()
val baseInfo = if (accountId != null) {
val bondedFlow = metadata.staking.bonded.observe(accountId)
bondedFlow.flatMapLatest { maybeController ->
val controllerId = maybeController ?: accountId
subscribeToStakingState(controllerId)
}
} else {
flowOf(null)
}
combineToPair(baseInfo, activeEraFlow)
}.transformLatest { (relaychainStakingState, activeEra) ->
saveItem(relaychainStakingState, secondaryInfo = null)
emit(primarySynced())
val secondarySyncFlow = stakingStatsFlow.map { (index, stakingStats) ->
val secondaryInfo = constructSecondaryInfo(relaychainStakingState, activeEra, stakingStats)
saveItem(relaychainStakingState, secondaryInfo)
secondarySynced(index)
}
emitAll(secondarySyncFlow)
}
}
private fun subscribeToStakingState(controllerId: AccountId): Flow<RelaychainStakingBaseInfo?> {
return remoteStorageSource.subscribe(chain.id) {
metadata.staking.ledger.observe(controllerId).flatMapLatest { ledger ->
if (ledger != null) {
subscribeToStakerIntentions(ledger.stashId).map { (nominations, validatorPrefs) ->
RelaychainStakingBaseInfo(ledger, nominations, validatorPrefs)
}
} else {
flowOf(null)
}
}
}
}
private suspend fun subscribeToStakerIntentions(stashId: AccountId): Flow<Pair<Nominations?, ValidatorPrefs?>> {
return remoteStorageSource.subscribeBatched(chain.id) {
combineToPair(
metadata.staking.nominators.observe(stashId),
metadata.staking.validators.observe(stashId)
)
}
}
private suspend fun saveItem(
relaychainStakingBaseInfo: RelaychainStakingBaseInfo?,
secondaryInfo: RelaychainStakingSecondaryInfo?
) = stakingDashboardCache.update { fromCache ->
if (relaychainStakingBaseInfo != null) {
StakingDashboardItemLocal.staking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
stake = relaychainStakingBaseInfo.stakingLedger.active,
status = secondaryInfo?.status ?: fromCache?.status,
rewards = secondaryInfo?.rewards ?: fromCache?.rewards,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings,
stakeStatusAccount = relaychainStakingBaseInfo.stakingLedger.stashId,
rewardsAccount = relaychainStakingBaseInfo.stakingLedger.stashId,
)
} else {
StakingDashboardItemLocal.notStaking(
chainId = chain.id,
chainAssetId = chainAsset.id,
stakingType = stakingTypeLocal,
metaId = metaAccount.id,
estimatedEarnings = secondaryInfo?.estimatedEarnings ?: fromCache?.estimatedEarnings
)
}
}
private fun constructSecondaryInfo(
baseInfo: RelaychainStakingBaseInfo?,
activeEra: EraIndex,
multiChainStakingStats: MultiChainStakingStats,
): RelaychainStakingSecondaryInfo? {
val chainStakingStats = multiChainStakingStats[stakingOptionId()] ?: return null
return RelaychainStakingSecondaryInfo(
rewards = chainStakingStats.rewards,
estimatedEarnings = chainStakingStats.estimatedEarnings.value,
status = determineStakingStatus(baseInfo, activeEra, chainStakingStats)
)
}
private fun determineStakingStatus(
baseInfo: RelaychainStakingBaseInfo?,
activeEra: EraIndex,
chainStakingStats: ChainStakingStats,
): Status? {
return when {
baseInfo == null -> null
baseInfo.stakingLedger.active.isZero -> Status.INACTIVE
baseInfo.nominations == null && baseInfo.validatorPrefs == null -> Status.INACTIVE
chainStakingStats.accountPresentInActiveStakers -> Status.ACTIVE
baseInfo.nominations != null && baseInfo.nominations.isWaiting(activeEra) -> Status.WAITING
else -> Status.INACTIVE
}
}
}
private class RelaychainStakingBaseInfo(
val stakingLedger: StakingLedger,
val nominations: Nominations?,
val validatorPrefs: ValidatorPrefs?,
)
private class RelaychainStakingSecondaryInfo(
val rewards: Balance,
val estimatedEarnings: Double,
val status: Status?
)
@@ -0,0 +1,114 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScopeUpdater
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_staking_impl.data.dashboard.cache.StakingDashboardCache
import io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.MultiChainOffChainSyncResult
import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolStateRepository
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
import io.novafoundation.nova.runtime.ext.StakingTypeGroup
import io.novafoundation.nova.runtime.ext.group
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import kotlinx.coroutines.flow.Flow
class StakingDashboardUpdaterFactory(
private val stakingDashboardCache: StakingDashboardCache,
private val remoteStorageSource: StorageDataSource,
private val nominationPoolBalanceRepository: NominationPoolStateRepository,
private val poolAccountDerivation: PoolAccountDerivation,
private val storageCache: StorageCache,
private val balanceLocksRepository: BalanceLocksRepository,
) {
fun createUpdater(
chain: Chain,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
): GlobalScopeUpdater? {
return when (stakingType.group()) {
StakingTypeGroup.RELAYCHAIN -> relayChain(chain, stakingType, metaAccount, stakingStatsFlow)
StakingTypeGroup.PARACHAIN -> parachain(chain, stakingType, metaAccount, stakingStatsFlow)
StakingTypeGroup.NOMINATION_POOL -> nominationPools(chain, stakingType, metaAccount, stakingStatsFlow)
StakingTypeGroup.MYTHOS -> mythos(chain, stakingType, metaAccount, stakingStatsFlow)
StakingTypeGroup.UNSUPPORTED -> null
}
}
private fun relayChain(
chain: Chain,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
): GlobalScopeUpdater {
return StakingDashboardRelayStakingUpdater(
chain = chain,
chainAsset = chain.utilityAsset,
stakingType = stakingType,
metaAccount = metaAccount,
stakingStatsFlow = stakingStatsFlow,
stakingDashboardCache = stakingDashboardCache,
remoteStorageSource = remoteStorageSource
)
}
private fun parachain(
chain: Chain,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
): GlobalScopeUpdater {
return StakingDashboardParachainStakingUpdater(
chain = chain,
chainAsset = chain.utilityAsset,
stakingType = stakingType,
metaAccount = metaAccount,
stakingStatsFlow = stakingStatsFlow,
stakingDashboardCache = stakingDashboardCache,
remoteStorageSource = remoteStorageSource
)
}
private fun nominationPools(
chain: Chain,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
): GlobalScopeUpdater {
return StakingDashboardNominationPoolsUpdater(
chain = chain,
chainAsset = chain.utilityAsset,
stakingType = stakingType,
metaAccount = metaAccount,
stakingStatsFlow = stakingStatsFlow,
stakingDashboardCache = stakingDashboardCache,
remoteStorageSource = remoteStorageSource,
nominationPoolStateRepository = nominationPoolBalanceRepository,
poolAccountDerivation = poolAccountDerivation,
storageCache = storageCache
)
}
private fun mythos(
chain: Chain,
stakingType: Chain.Asset.StakingType,
metaAccount: MetaAccount,
stakingStatsFlow: Flow<MultiChainOffChainSyncResult>,
): GlobalScopeUpdater {
return StakingDashboardMythosUpdater(
chain = chain,
chainAsset = chain.utilityAsset,
stakingType = stakingType,
metaAccount = metaAccount,
stakingDashboardCache = stakingDashboardCache,
balanceLocksRepository = balanceLocksRepository,
storageCache = storageCache,
remoteStorageSource = remoteStorageSource,
stakingStatsFlow = stakingStatsFlow
)
}
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.network.updaters.chain
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
sealed class StakingDashboardUpdaterEvent : Updater.SideEffect {
class AllSynced(val option: StakingOptionId, val indexOfUsedOffChainSync: Int) : StakingDashboardUpdaterEvent()
class PrimarySynced(val option: StakingOptionId) : StakingDashboardUpdaterEvent()
}
@@ -0,0 +1,109 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.repository
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.common.domain.fromOption
import io.novafoundation.nova.common.utils.asPercent
import io.novafoundation.nova.common.utils.mapList
import io.novafoundation.nova.core_db.dao.StakingDashboardDao
import io.novafoundation.nova.core_db.model.StakingDashboardAccountsView
import io.novafoundation.nova.core_db.model.StakingDashboardItemLocal
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.MultiStakingOptionIds
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import io.novafoundation.nova.feature_staking_impl.data.dashboard.model.StakingDashboardItem
import io.novafoundation.nova.feature_staking_impl.data.dashboard.model.StakingDashboardItem.StakeState.HasStake
import io.novafoundation.nova.feature_staking_impl.data.dashboard.model.StakingDashboardItem.StakeState.NoStake
import io.novafoundation.nova.feature_staking_impl.data.dashboard.model.StakingDashboardOptionAccounts
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapStakingStringToStakingType
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapStakingTypeToStakingString
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
import kotlinx.coroutines.flow.Flow
interface StakingDashboardRepository {
fun dashboardItemsFlow(metaAccountId: Long): Flow<List<StakingDashboardItem>>
fun dashboardItemsFlow(metaAccountId: Long, multiStakingOptionIds: MultiStakingOptionIds): Flow<List<StakingDashboardItem>>
fun stakingAccountsFlow(metaAccountId: Long): Flow<List<StakingDashboardOptionAccounts>>
}
class RealStakingDashboardRepository(
private val dao: StakingDashboardDao
) : StakingDashboardRepository {
override fun dashboardItemsFlow(metaAccountId: Long): Flow<List<StakingDashboardItem>> {
return dao.dashboardItemsFlow(metaAccountId).mapList(::mapDashboardItemFromLocal)
}
override fun dashboardItemsFlow(metaAccountId: Long, multiStakingOptionIds: MultiStakingOptionIds): Flow<List<StakingDashboardItem>> {
val stakingTypes = multiStakingOptionIds.stakingTypes.mapNotNull(::mapStakingTypeToStakingString)
return dao.dashboardItemsFlow(metaAccountId, multiStakingOptionIds.chainId, multiStakingOptionIds.chainAssetId, stakingTypes)
.mapList(::mapDashboardItemFromLocal)
}
override fun stakingAccountsFlow(metaAccountId: Long): Flow<List<StakingDashboardOptionAccounts>> {
return dao.stakingAccountsViewFlow(metaAccountId).mapList(::mapStakingAccountViewFromLocal)
}
private fun mapDashboardItemFromLocal(localItem: StakingDashboardItemLocal): StakingDashboardItem {
return StakingDashboardItem(
fullChainAssetId = FullChainAssetId(
chainId = localItem.chainId,
assetId = localItem.chainAssetId,
),
stakingType = mapStakingStringToStakingType(localItem.stakingType),
stakeState = if (localItem.hasStake) hasStakeState(localItem) else noStakeState(localItem)
)
}
private fun mapStakingAccountViewFromLocal(localItem: StakingDashboardAccountsView): StakingDashboardOptionAccounts {
return StakingDashboardOptionAccounts(
stakingOptionId = StakingOptionId(
chainId = localItem.chainId,
chainAssetId = localItem.chainAssetId,
stakingType = mapStakingStringToStakingType(localItem.stakingType),
),
stakingStatusAccount = localItem.stakeStatusAccount?.intoKey(),
rewardsAccount = localItem.rewardsAccount?.intoKey()
)
}
private fun hasStakeState(localItem: StakingDashboardItemLocal): HasStake {
val estimatedEarnings = localItem.estimatedEarnings
val rewards = localItem.rewards
val status = localItem.status
val stats = if (estimatedEarnings != null && rewards != null && status != null) {
HasStake.Stats(
rewards = rewards,
status = mapStakingStatusFromLocal(status),
estimatedEarnings = estimatedEarnings.asPercent()
)
} else {
null
}
return HasStake(
stake = requireNotNull(localItem.stake),
stats = ExtendedLoadingState.fromOption(stats)
)
}
private fun noStakeState(localItem: StakingDashboardItemLocal): NoStake {
val stats = localItem.estimatedEarnings?.let { estimatedEarnings ->
NoStake.Stats(estimatedEarnings.asPercent())
}
return NoStake(ExtendedLoadingState.fromOption(stats))
}
private fun mapStakingStatusFromLocal(localStatus: StakingDashboardItemLocal.Status): HasStake.StakingStatus {
return when (localStatus) {
StakingDashboardItemLocal.Status.ACTIVE -> HasStake.StakingStatus.ACTIVE
StakingDashboardItemLocal.Status.INACTIVE -> HasStake.StakingStatus.INACTIVE
StakingDashboardItemLocal.Status.WAITING -> HasStake.StakingStatus.WAITING
}
}
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_staking_impl.data.dashboard.repository
import io.novafoundation.nova.common.utils.associateWithIndex
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface TotalStakeChainComparatorProvider {
suspend fun getTotalStakeComparator(): Comparator<Chain>
}
class RealTotalStakeChainComparatorProvider : TotalStakeChainComparatorProvider {
private val positionByGenesisHash by lazy {
listOf(
Chain.Geneses.POLKADOT,
Chain.Geneses.KUSAMA,
Chain.Geneses.ALEPH_ZERO,
Chain.Geneses.MOONBEAM,
Chain.Geneses.MOONRIVER,
Chain.Geneses.TERNOA,
Chain.Geneses.POLKADEX,
Chain.Geneses.CALAMARI,
Chain.Geneses.ZEITGEIST,
Chain.Geneses.TURING
).associateWithIndex()
}
override suspend fun getTotalStakeComparator(): Comparator<Chain> {
return compareBy {
positionByGenesisHash[it.id] ?: Int.MAX_VALUE
}
}
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_staking_impl.data.mappers
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
import io.novafoundation.nova.feature_staking_api.domain.model.StakingAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
fun mapAccountToStakingAccount(chain: Chain, metaAccount: MetaAccount): StakingAccount? = with(metaAccount) {
val address = addressIn(chain)
address?.let {
StakingAccount(
address = address,
name = name,
)
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_staking_impl.data.mappers
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import io.novafoundation.nova.feature_staking_api.domain.model.RewardDestination
import io.novafoundation.nova.feature_staking_impl.presentation.common.rewardDestination.RewardDestinationModel
fun mapRewardDestinationModelToRewardDestination(
rewardDestinationModel: RewardDestinationModel,
): RewardDestination {
return when (rewardDestinationModel) {
is RewardDestinationModel.Restake -> RewardDestination.Restake
is RewardDestinationModel.Payout -> RewardDestination.Payout(rewardDestinationModel.destination.address.toAccountId())
}
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_staking_impl.data.mappers
import io.novafoundation.nova.core_db.model.TotalRewardLocal
import io.novafoundation.nova.feature_staking_impl.domain.model.TotalReward
fun mapTotalRewardLocalToTotalReward(reward: TotalRewardLocal): TotalReward {
return reward.totalReward
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_staking_impl.data.model
import io.novafoundation.nova.runtime.ext.allExternalApis
import io.novafoundation.nova.runtime.ext.externalApi
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
fun Chain.stakingRewardsExternalApi(): List<Chain.ExternalApi.StakingRewards> = allExternalApis<Chain.ExternalApi.StakingRewards>()
fun Chain.stakingExternalApi(): Chain.ExternalApi.Staking? = externalApi<Chain.ExternalApi.Staking>()
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_staking_impl.data.model
import io.novafoundation.nova.common.address.AccountIdKey
import java.math.BigInteger
class Payout(
val validatorStash: AccountIdKey,
val era: BigInteger,
val amount: BigInteger,
val pagesToClaim: List<Int>
)
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.domain.account.system.AccountSystemAccountMatcher
import io.novafoundation.nova.feature_account_api.domain.account.system.SystemAccountMatcher
import io.novafoundation.nova.feature_staking_api.data.mythos.MythosMainPotMatcherFactory
import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.MythosStakingRepository
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.MYTHOS
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
@FeatureScope
class RealMythosMainPotMatcherFactory @Inject constructor(
private val mythosStakingRepository: MythosStakingRepository
) : MythosMainPotMatcherFactory {
private val fetchMutex = Mutex()
private var cache: SystemAccountMatcher? = null
override suspend fun create(chainAsset: Chain.Asset): SystemAccountMatcher? {
if (MYTHOS !in chainAsset.staking) return null
return fetchMutex.withLock {
if (cache == null) {
cache = mythosStakingRepository.getMainStakingPot(chainAsset.chainId)
.map(::AccountSystemAccountMatcher)
.getOrNull()
}
cache
}
}
}
@@ -0,0 +1,79 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.duration
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.toDuration
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.mythos.repository.RealMythosSessionRepository
import io.novafoundation.nova.feature_staking_impl.domain.common.EraRewardCalculatorComparable
import io.novafoundation.nova.feature_staking_impl.domain.common.ignoreInsignificantTimeChanges
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import java.math.BigInteger
import javax.inject.Inject
import kotlin.time.Duration
interface MythosSessionDurationCalculator : EraRewardCalculatorComparable {
val blockTime: BigInteger
fun sessionDuration(): Duration
/**
* Remaining time of the current session
*/
fun remainingSessionDuration(): Duration
}
@FeatureScope
class MythosSessionDurationCalculatorFactory @Inject constructor(
private val mythosSessionRepository: RealMythosSessionRepository,
private val chainStateRepository: ChainStateRepository,
) {
fun create(stakingOption: StakingOption): Flow<MythosSessionDurationCalculator> {
val chainId = stakingOption.chain.id
return flowOfAll {
val sessionLength = mythosSessionRepository.sessionLength(stakingOption.chain)
combine(
mythosSessionRepository.currentSlotFlow(chainId),
chainStateRepository.predictedBlockTimeFlow(chainId)
) { currentSlot, blockTime ->
RealMythosSessionDurationCalculator(
blockTime = blockTime,
currentSlot = currentSlot,
slotsInSession = sessionLength
)
}
}.ignoreInsignificantTimeChanges()
}
}
private class RealMythosSessionDurationCalculator(
override val blockTime: BigInteger,
private val currentSlot: BigInteger,
private val slotsInSession: BigInteger
) : MythosSessionDurationCalculator {
override fun sessionDuration(): Duration {
return (slotsInSession * blockTime).toDuration()
}
override fun remainingSessionDuration(): Duration {
val remainingBlocks = slotsInSession - sessionProgress()
return (remainingBlocks * blockTime).toDuration()
}
override fun derivedTimestamp(): Duration {
return (currentSlot * blockTime).toDuration()
}
private fun sessionProgress(): BigInteger {
// Mythos has 0 offset for sessions, so first block number of a session is divisible by session length
return currentSlot % slotsInSession
}
}
@@ -0,0 +1,86 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.bindPercentFraction
import io.novafoundation.nova.common.utils.Fraction
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.Invulnerables
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythCandidateInfo
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythDelegation
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythReleaseRequest
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.UserStakeInfo
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.bindDelegationInfo
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.bindInvulnerables
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.bindMythCandidateInfo
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.bindMythReleaseQueues
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.bindUserStakeInfo
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry0
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry2
import io.novafoundation.nova.runtime.storage.source.query.api.converters.scaleDecoder
import io.novafoundation.nova.runtime.storage.source.query.api.converters.scaleEncoder
import io.novafoundation.nova.runtime.storage.source.query.api.storage0
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
import io.novafoundation.nova.runtime.storage.source.query.api.storage2
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 CollatorStakingRuntimeApi(override val module: Module) : QueryableModule
context(RuntimeContext)
val RuntimeMetadata.collatorStaking: CollatorStakingRuntimeApi
get() = CollatorStakingRuntimeApi(collatorStaking())
context(RuntimeContext)
val CollatorStakingRuntimeApi.userStake: QueryableStorageEntry1<AccountId, UserStakeInfo>
get() = storage1("UserStake", binding = { decoded, _ -> bindUserStakeInfo(decoded) })
context(RuntimeContext)
val CollatorStakingRuntimeApi.minStake: QueryableStorageEntry0<Balance>
get() = storage0("MinStake", binding = ::bindNumber)
context(RuntimeContext)
val CollatorStakingRuntimeApi.extraReward: QueryableStorageEntry0<Balance>
get() = storage0("ExtraReward", binding = ::bindNumber)
context(RuntimeContext)
val CollatorStakingRuntimeApi.collatorRewardPercentage: QueryableStorageEntry0<Fraction>
get() = storage0("CollatorRewardPercentage", binding = ::bindPercentFraction)
context(RuntimeContext)
val CollatorStakingRuntimeApi.invulnerables: QueryableStorageEntry0<Invulnerables>
get() = storage0("Invulnerables", binding = ::bindInvulnerables)
context(RuntimeContext)
val CollatorStakingRuntimeApi.candidates: QueryableStorageEntry1<AccountIdKey, MythCandidateInfo>
get() = storage1(
"Candidates",
binding = { decoded, _ -> bindMythCandidateInfo(decoded) },
keyBinding = ::bindAccountIdKey
)
context(RuntimeContext)
val CollatorStakingRuntimeApi.candidateStake: QueryableStorageEntry2<AccountIdKey, AccountIdKey, MythDelegation>
get() = storage2(
"CandidateStake",
binding = { decoded, _, _, -> bindDelegationInfo(decoded) },
key1ToInternalConverter = AccountIdKey.scaleEncoder,
key2ToInternalConverter = AccountIdKey.scaleEncoder,
key1FromInternalConverter = AccountIdKey.scaleDecoder,
key2FromInternalConverter = AccountIdKey.scaleDecoder
)
context(RuntimeContext)
val CollatorStakingRuntimeApi.releaseQueues: QueryableStorageEntry1<AccountId, List<MythReleaseRequest>>
get() = storage1("ReleaseQueues", binding = { decoded, _ -> bindMythReleaseQueues(decoded) })
context(RuntimeContext)
val CollatorStakingRuntimeApi.autoCompound: QueryableStorageEntry1<AccountId, Fraction>
get() = storage1("AutoCompound", binding = { decoded, _ -> bindPercentFraction(decoded) })
@@ -0,0 +1,98 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.calls
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.utils.Fraction
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.common.utils.structOf
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
@JvmInline
value class CollatorStakingCalls(val builder: ExtrinsicBuilder)
val ExtrinsicBuilder.collatorStaking: CollatorStakingCalls
get() = CollatorStakingCalls(this)
fun CollatorStakingCalls.lock(amount: Balance) {
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "lock",
arguments = mapOf(
"amount" to amount
)
)
}
data class StakingIntent(val candidate: AccountIdKey, val stake: Balance) {
companion object {
fun zero(candidate: AccountIdKey) = StakingIntent(candidate, Balance.ZERO)
}
}
fun CollatorStakingCalls.stake(intents: List<StakingIntent>) {
val targets = intents.map(StakingIntent::toEncodableInstance)
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "stake",
arguments = mapOf(
"targets" to targets
)
)
}
fun CollatorStakingCalls.unstakeFrom(collatorId: AccountIdKey) {
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "unstake_from",
arguments = mapOf(
"account" to collatorId.value
)
)
}
fun CollatorStakingCalls.unlock(amount: Balance) {
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "unlock",
arguments = mapOf(
"maybe_amount" to amount
)
)
}
fun CollatorStakingCalls.release() {
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "release",
arguments = emptyMap()
)
}
fun CollatorStakingCalls.claimRewards() {
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "claim_rewards",
arguments = emptyMap()
)
}
fun CollatorStakingCalls.setAutoCompoundPercentage(percent: Fraction) {
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "set_autocompound_percentage",
arguments = mapOf(
"percent" to percent.inWholePercents
)
)
}
private fun StakingIntent.toEncodableInstance(): Any {
return structOf(
"candidate" to candidate.value,
"stake" to stake
)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindSet
typealias Invulnerables = Set<AccountIdKey>
fun bindInvulnerables(decoded: Any?): Invulnerables {
return bindSet(decoded, ::bindAccountIdKey)
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model
import io.novafoundation.nova.common.data.network.runtime.binding.bindInt
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_account_api.data.model.AccountIdKeyMap
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
class MythCandidateInfo(
val stake: Balance,
val stakers: Int
)
typealias MythCandidateInfos = AccountIdKeyMap<MythCandidateInfo>
fun bindMythCandidateInfo(decoded: Any?): MythCandidateInfo {
val asStruct = decoded.castToStruct()
return MythCandidateInfo(
stake = bindNumber(asStruct["stake"]),
stakers = bindInt(asStruct["stakers"])
)
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model
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_staking_api.domain.model.SessionIndex
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSessionIndex
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
class MythDelegation(
val session: SessionIndex,
val stake: Balance
)
fun bindDelegationInfo(decoded: Any?): MythDelegation {
val asStruct = decoded.castToStruct()
return MythDelegation(
session = bindSessionIndex(asStruct["session"]),
stake = bindNumber(asStruct["stake"])
)
}
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.bindBlockNumber
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.castToStruct
import io.novafoundation.nova.common.utils.sumByBigInteger
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
class MythReleaseRequest(
val block: BlockNumber,
val amount: Balance
)
fun MythReleaseRequest.isRedeemableAt(at: BlockNumber): Boolean {
return at >= block
}
fun List<MythReleaseRequest>.totalRedeemable(at: BlockNumber): Balance {
return sumByBigInteger { if (it.isRedeemableAt(at)) it.amount else Balance.ZERO }
}
fun bindMythReleaseRequest(decoded: Any?): MythReleaseRequest {
val asStruct = decoded.castToStruct()
return MythReleaseRequest(
block = bindBlockNumber(asStruct["block"]),
amount = bindNumber(asStruct["amount"])
)
}
fun bindMythReleaseQueues(decoded: Any): List<MythReleaseRequest> {
return bindList(decoded, ::bindMythReleaseRequest)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLockId
object MythosStakingFreezeIds {
val STAKING = BalanceLockId.fromPath(Modules.COLLATOR_STAKING, "Staking")
val RELEASING = BalanceLockId.fromPath(Modules.COLLATOR_STAKING, "Releasing")
}
@@ -0,0 +1,64 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindBlockNumber
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.castToList
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.SessionValidators
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import java.math.BigInteger
class UserStakeInfo(
val balance: Balance,
val maybeLastUnstake: LastUnstake?,
val candidates: List<AccountIdKey>,
val maybeLastRewardSession: SessionIndex?
)
fun UserStakeInfo.hasActiveCollators(sessionValidators: SessionValidators): Boolean {
return candidates.any { it in sessionValidators }
}
fun UserStakeInfo.hasInactiveCollators(sessionValidators: SessionValidators): Boolean {
return candidates.any { it !in sessionValidators }
}
@JvmName("hasActiveCollatorsOrFalse")
fun UserStakeInfo?.hasActiveCollators(sessionValidators: SessionValidators): Boolean {
if (this == null) return false
return hasActiveCollators(sessionValidators)
}
class LastUnstake(
val amount: Balance,
val availableForRestakeAt: BlockNumber
)
typealias SessionIndex = BigInteger
fun bindUserStakeInfo(decoded: Any?): UserStakeInfo {
val asStruct = decoded.castToStruct()
return UserStakeInfo(
balance = bindNumber(asStruct["stake"]),
maybeLastUnstake = bindLastUnstake(asStruct["maybeLastUnstake"]),
candidates = bindList(asStruct["candidates"], ::bindAccountIdKey),
maybeLastRewardSession = asStruct.get<Any?>("maybeLastRewardSession")?.let(::bindNumber)
)
}
private fun bindLastUnstake(decoded: Any?): LastUnstake? {
if (decoded == null) return null
// Tuple
val (amountRaw, availableForRestakeAtRaw) = decoded.castToList()
return LastUnstake(
amount = bindNumber(amountRaw),
availableForRestakeAt = bindBlockNumber(availableForRestakeAtRaw)
)
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.repository
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.candidates
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythCandidateInfos
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Inject
import javax.inject.Named
interface MythosCandidatesRepository {
suspend fun getCandidateInfos(chainId: ChainId): MythCandidateInfos
}
@FeatureScope
class RealMythosCandidatesRepository @Inject constructor(
@Named(REMOTE_STORAGE_SOURCE)
private val remoteStorageSource: StorageDataSource
) : MythosCandidatesRepository {
override suspend fun getCandidateInfos(chainId: ChainId): MythCandidateInfos {
return remoteStorageSource.query(chainId) {
metadata.collatorStaking.candidates.entries()
}
}
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.repository
import io.novafoundation.nova.common.utils.findById
import io.novafoundation.nova.common.utils.orZero
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythosStakingFreezeIds
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.BalanceLock
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLockId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
data class MythosLocks(
val releasing: Balance,
val staked: Balance
)
val MythosLocks.total: Balance
get() = releasing + staked
fun BalanceLocksRepository.observeMythosLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow<MythosLocks> {
return observeBalanceLocks(metaId, chain, chainAsset)
.map { locks -> locks.findMythosLocks() }
.distinctUntilChanged()
}
suspend fun BalanceLocksRepository.getMythosLocks(metaId: Long, chainAsset: Chain.Asset): MythosLocks {
return getBalanceLocks(metaId, chainAsset).findMythosLocks()
}
private fun List<BalanceLock>.findMythosLocks(): MythosLocks {
return MythosLocks(
releasing = findAmountOrZero(MythosStakingFreezeIds.RELEASING),
staked = findAmountOrZero(MythosStakingFreezeIds.STAKING)
)
}
private fun List<BalanceLock>.findAmountOrZero(id: BalanceLockId): Balance {
return findById(id)?.amountInPlanks.orZero()
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.repository
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_staking_impl.data.repository.consensus.AuraSession
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
interface MythosSessionRepository {
suspend fun sessionLength(chain: Chain): BlockNumber
fun currentSlotFlow(chainId: ChainId): Flow<BlockNumber>
}
@FeatureScope
class RealMythosSessionRepository @Inject constructor(
private val auraSession: AuraSession,
) : MythosSessionRepository {
override suspend fun sessionLength(chain: Chain): BlockNumber {
return chain.additional?.sessionLength?.toBigInteger()
?: auraSession.sessionLength(chain.id)
}
override fun currentSlotFlow(chainId: ChainId): Flow<BlockNumber> {
return auraSession.currentSlotFlow(chainId)
}
}
@@ -0,0 +1,136 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.repository
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.Fraction
import io.novafoundation.nova.common.utils.collatorStaking
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.common.utils.numberConstant
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorRewardPercentage
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.extraReward
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.invulnerables
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.minStake
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.Invulnerables
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
import io.novafoundation.nova.runtime.call.RuntimeCallsApi
import io.novafoundation.nova.runtime.call.callCatching
import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.withRuntime
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull
import io.novafoundation.nova.runtime.storage.source.query.api.queryNonNull
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Named
interface MythosStakingRepository {
fun minStakeFlow(chainId: ChainId): Flow<Balance>
suspend fun minStake(chainId: ChainId): Balance
suspend fun maxCollatorsPerDelegator(chainId: ChainId): Int
suspend fun maxDelegatorsPerCollator(chainId: ChainId): Int
suspend fun unstakeDurationInBlocks(chainId: ChainId): BlockNumber
suspend fun maxReleaseRequests(chainId: ChainId): Int
suspend fun perBlockReward(chainId: ChainId): Balance
suspend fun collatorCommission(chainId: ChainId): Fraction
suspend fun getMainStakingPot(chainId: ChainId): Result<AccountIdKey>
suspend fun getInvulnerableCollators(chainId: ChainId): Invulnerables
suspend fun autoCompoundThreshold(chainId: ChainId): Balance
}
@FeatureScope
class RealMythosStakingRepository @Inject constructor(
@Named(LOCAL_STORAGE_SOURCE)
private val localStorageDataSource: StorageDataSource,
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi,
private val chainRegistry: ChainRegistry
) : MythosStakingRepository {
override fun minStakeFlow(chainId: ChainId): Flow<Balance> {
return localStorageDataSource.subscribe(chainId) {
metadata.collatorStaking.minStake.observeNonNull()
}
}
override suspend fun minStake(chainId: ChainId): Balance {
return localStorageDataSource.query(chainId) {
metadata.collatorStaking.minStake.queryNonNull()
}
}
override suspend fun maxCollatorsPerDelegator(chainId: ChainId): Int {
return chainRegistry.withRuntime(chainId) {
metadata.collatorStaking().numberConstant("MaxStakedCandidates").toInt()
}
}
override suspend fun maxDelegatorsPerCollator(chainId: ChainId): Int {
return chainRegistry.withRuntime(chainId) {
metadata.collatorStaking().numberConstant("MaxStakers").toInt()
}
}
override suspend fun unstakeDurationInBlocks(chainId: ChainId): BlockNumber {
return chainRegistry.withRuntime(chainId) {
metadata.collatorStaking().numberConstant("StakeUnlockDelay")
}
}
override suspend fun maxReleaseRequests(chainId: ChainId): Int {
return maxCollatorsPerDelegator(chainId)
}
override suspend fun perBlockReward(chainId: ChainId): Balance {
return localStorageDataSource.query(chainId, applyStorageDefault = true) {
metadata.collatorStaking.extraReward.queryNonNull()
}
}
override suspend fun collatorCommission(chainId: ChainId): Fraction {
return localStorageDataSource.query(chainId) {
metadata.collatorStaking.collatorRewardPercentage.queryNonNull()
}
}
override suspend fun getMainStakingPot(chainId: ChainId): Result<AccountIdKey> {
return multiChainRuntimeCallsApi.forChain(chainId).mainStakingPot()
}
override suspend fun getInvulnerableCollators(chainId: ChainId): Invulnerables {
return localStorageDataSource.query(chainId) {
metadata.collatorStaking.invulnerables.query().orEmpty()
}
}
override suspend fun autoCompoundThreshold(chainId: ChainId): Balance {
return chainRegistry.withRuntime(chainId) {
metadata.collatorStaking().numberConstant("AutoCompoundingThreshold")
}
}
private suspend fun RuntimeCallsApi.mainStakingPot(): Result<AccountIdKey> {
return callCatching(
section = "CollatorStakingApi",
method = "main_pot_account",
arguments = emptyMap(),
returnBinding = ::bindAccountIdKey
)
}
}
@@ -0,0 +1,190 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.repository
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindBoolean
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.Fraction
import io.novafoundation.nova.common.utils.filterNotNull
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.autoCompound
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.candidateStake
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.releaseQueues
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.userStake
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythDelegation
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.MythReleaseRequest
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.UserStakeInfo
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
import io.novafoundation.nova.runtime.call.RuntimeCallsApi
import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull
import io.novafoundation.nova.runtime.storage.source.query.api.queryNonNull
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Named
interface MythosUserStakeRepository {
fun userStakeOrDefaultFlow(chainId: ChainId, accountId: AccountId): Flow<UserStakeInfo>
suspend fun userStakeOrDefault(chainId: ChainId, accountId: AccountId): UserStakeInfo
fun userDelegationsFlow(
chainId: ChainId,
userId: AccountIdKey,
delegationIds: List<AccountIdKey>
): Flow<Map<AccountIdKey, MythDelegation>>
suspend fun userDelegations(
chainId: ChainId,
userId: AccountIdKey,
delegationIds: List<AccountIdKey>
): Map<AccountIdKey, MythDelegation>
suspend fun shouldClaimRewards(
chainId: ChainId,
accountId: AccountIdKey
): Boolean
suspend fun getpPendingRewards(
chainId: ChainId,
accountId: AccountIdKey
): Balance
fun releaseQueuesFlow(
chainId: ChainId,
accountId: AccountIdKey
): Flow<List<MythReleaseRequest>>
suspend fun releaseQueues(
chainId: ChainId,
accountId: AccountIdKey
): List<MythReleaseRequest>
suspend fun getAutoCompoundPercentage(
chainId: ChainId,
accountId: AccountIdKey
): Fraction
suspend fun lastShouldRestakeSelection(): Boolean?
suspend fun setLastShouldRestakeSelection(shouldRestake: Boolean)
}
private const val SHOULD_RESTAKE_KEY = "RealMythosUserStakeRepository.COMPOUND_MODIFIED_KEY"
@FeatureScope
class RealMythosUserStakeRepository @Inject constructor(
@Named(LOCAL_STORAGE_SOURCE)
private val localStorageDataSource: StorageDataSource,
private val callApi: MultiChainRuntimeCallsApi,
private val preferences: Preferences,
) : MythosUserStakeRepository {
override fun userStakeOrDefaultFlow(chainId: ChainId, accountId: AccountId): Flow<UserStakeInfo> {
return localStorageDataSource.subscribe(chainId, applyStorageDefault = true) {
metadata.collatorStaking.userStake.observeNonNull(accountId)
}
}
override suspend fun userStakeOrDefault(chainId: ChainId, accountId: AccountId): UserStakeInfo {
return localStorageDataSource.query(chainId, applyStorageDefault = true) {
metadata.collatorStaking.userStake.queryNonNull(accountId)
}
}
override fun userDelegationsFlow(
chainId: ChainId,
userId: AccountIdKey,
delegationIds: List<AccountIdKey>
): Flow<Map<AccountIdKey, MythDelegation>> {
return localStorageDataSource.subscribe(chainId) {
val allKeys = delegationIds.map { it to userId }
metadata.collatorStaking.candidateStake.observe(allKeys).map { resultMap ->
resultMap.filterNotNull().mapKeys { (keys, _) -> keys.first }
}
}
}
override suspend fun userDelegations(
chainId: ChainId,
userId: AccountIdKey,
delegationIds: List<AccountIdKey>
): Map<AccountIdKey, MythDelegation> {
return localStorageDataSource.query(chainId) {
val allKeys = delegationIds.map { it to userId }
metadata.collatorStaking.candidateStake.entries(allKeys)
.mapKeys { (keys, _) -> keys.first }
}
}
override suspend fun shouldClaimRewards(chainId: ChainId, accountId: AccountIdKey): Boolean {
return callApi.forChain(chainId).shouldClaimPendingRewards(accountId)
}
override suspend fun getpPendingRewards(chainId: ChainId, accountId: AccountIdKey): Balance {
return callApi.forChain(chainId).pendingRewards(accountId)
}
override fun releaseQueuesFlow(chainId: ChainId, accountId: AccountIdKey): Flow<List<MythReleaseRequest>> {
return localStorageDataSource.subscribe(chainId) {
metadata.collatorStaking.releaseQueues.observe(accountId.value)
.map { it.orEmpty() }
}
}
override suspend fun releaseQueues(chainId: ChainId, accountId: AccountIdKey): List<MythReleaseRequest> {
return localStorageDataSource.query(chainId) {
metadata.collatorStaking.releaseQueues.query(accountId.value).orEmpty()
}
}
override suspend fun getAutoCompoundPercentage(chainId: ChainId, accountId: AccountIdKey): Fraction {
return localStorageDataSource.query(chainId, applyStorageDefault = true) {
metadata.collatorStaking.autoCompound.queryNonNull(accountId.value)
}
}
override suspend fun lastShouldRestakeSelection(): Boolean? {
if (preferences.contains(SHOULD_RESTAKE_KEY)) {
return preferences.getBoolean(SHOULD_RESTAKE_KEY, true)
}
return null
}
override suspend fun setLastShouldRestakeSelection(shouldRestake: Boolean) {
preferences.putBoolean(SHOULD_RESTAKE_KEY, shouldRestake)
}
private suspend fun RuntimeCallsApi.shouldClaimPendingRewards(accountId: AccountIdKey): Boolean {
return call(
section = "CollatorStakingApi",
method = "should_claim",
arguments = mapOf(
"account" to accountId.value
),
returnBinding = ::bindBoolean
)
}
private suspend fun RuntimeCallsApi.pendingRewards(accountId: AccountIdKey): Balance {
return call(
section = "CollatorStakingApi",
method = "total_rewards",
arguments = mapOf(
"account" to accountId.value
),
returnBinding = ::bindNumber
)
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.updaters
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorRewardPercentage
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
class MythosCollatorRewardPercentageUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String {
return with(RuntimeContext(runtime)) {
metadata.collatorStaking.collatorRewardPercentage.storageKey()
}
}
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.updaters
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.metadata
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.mythos.network.blockchain.api.autoCompound
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
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
class MythosCompoundPercentageUpdater(
private val stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache,
scope: AccountUpdateScope,
) : SingleStorageKeyUpdater<MetaAccount>(scope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<MetaAccount> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: MetaAccount): String? {
return with(RuntimeContext(runtime)) {
val chain = stakingSharedState.chain()
val accountId = scopeValue.accountIdIn(chain) ?: return@with null
metadata.collatorStaking.autoCompound.storageKey(accountId)
}
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.updaters
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.extraReward
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
class MythosExtraRewardUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String {
return with(RuntimeContext(runtime)) {
metadata.collatorStaking.extraReward.storageKey()
}
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.updaters
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.minStake
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
class MythosMinStakeUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String {
return with(RuntimeContext(runtime)) {
metadata.collatorStaking.minStake.storageKey()
}
}
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.updaters
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.metadata
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.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.releaseQueues
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
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
class MythosReleaseQueuesUpdater(
private val stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache,
scope: AccountUpdateScope,
) : SingleStorageKeyUpdater<MetaAccount>(scope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<MetaAccount> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: MetaAccount): String? {
return with(RuntimeContext(runtime)) {
val chain = stakingSharedState.chain()
val accountId = scopeValue.accountIdIn(chain) ?: return@with null
metadata.collatorStaking.releaseQueues.storageKey(accountId)
}
}
}
@@ -0,0 +1,58 @@
package io.novafoundation.nova.feature_staking_impl.data.mythos.updaters
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.common.utils.onEachLatest
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.storage.insert
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
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.mythos.network.blockchain.api.candidateStake
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.api.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.UserStakeInfo
import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.MythosUserStakeRepository
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.state.chain
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
class MythosSelectedCandidatesUpdater(
override val scope: AccountUpdateScope,
private val stakingSharedState: StakingSharedState,
private val storageCache: StorageCache,
private val mythosUserStakeRepository: MythosUserStakeRepository,
private val remoteStorageDataSource: StorageDataSource,
) : SharedStateBasedUpdater<MetaAccount> {
override suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder,
scopeValue: MetaAccount
): Flow<Updater.SideEffect> {
val chain = stakingSharedState.chain()
val accountId = scopeValue.accountIdIn(chain) ?: return emptyFlow()
return mythosUserStakeRepository.userStakeOrDefaultFlow(chain.id, accountId).onEachLatest { userStake ->
syncUserDelegations(chain.id, userStake, accountId.intoKey())
}.noSideAffects()
}
private suspend fun syncUserDelegations(
chainId: ChainId,
userStake: UserStakeInfo,
userAccountId: AccountIdKey,
) {
val allKeys = userStake.candidates.map { it to userAccountId }
val entries = remoteStorageDataSource.query(chainId) {
metadata.collatorStaking.candidateStake.entriesRaw(allKeys)
}
storageCache.insert(entries, chainId)
}
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.babe
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSlot
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry0
import io.novafoundation.nova.runtime.storage.source.query.api.storage0
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import java.math.BigInteger
@JvmInline
value class BabeRuntimeApi(override val module: Module) : QueryableModule
context(RuntimeContext)
val RuntimeMetadata.babe: BabeRuntimeApi
get() = BabeRuntimeApi(babe())
context(RuntimeContext)
val BabeRuntimeApi.currentSlot: QueryableStorageEntry0<BigInteger>
get() = storage0("CurrentSlot", binding = ::bindSlot)
context(RuntimeContext)
val BabeRuntimeApi.genesisSlot: QueryableStorageEntry0<BigInteger>
get() = storage0("GenesisSlot", binding = ::bindSlot)
context(RuntimeContext)
val BabeRuntimeApi.epochIndex: QueryableStorageEntry0<BigInteger>
get() = storage0("EpochIndex", binding = ::bindNumber)
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.session
import io.novafoundation.nova.feature_staking_api.domain.model.SessionIndex
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.SessionValidators
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSessionIndex
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSessionValidators
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry0
import io.novafoundation.nova.runtime.storage.source.query.api.storage0
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
@JvmInline
value class SessionRuntimeApi(override val module: Module) : QueryableModule
context(RuntimeContext)
val RuntimeMetadata.session: SessionRuntimeApi
get() = SessionRuntimeApi(session())
context(RuntimeContext)
val SessionRuntimeApi.currentIndex: QueryableStorageEntry0<SessionIndex>
get() = storage0("CurrentIndex", binding = ::bindSessionIndex)
context(RuntimeContext)
val SessionRuntimeApi.validators: QueryableStorageEntry0<SessionValidators>
get() = storage0("Validators", binding = ::bindSessionValidators)
@@ -0,0 +1,77 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.feature_staking_api.domain.model.BondedEras
import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex
import io.novafoundation.nova.feature_staking_api.domain.model.Nominations
import io.novafoundation.nova.feature_staking_api.domain.model.SessionIndex
import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger
import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.UnappliedSlashKey
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bind
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindActiveEra
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindEraIndex
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindNominations
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSessionIndex
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindStakingLedger
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindValidatorPrefs
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry0
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry2
import io.novafoundation.nova.runtime.storage.source.query.api.storage0
import io.novafoundation.nova.runtime.storage.source.query.api.storage0OrNull
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
import io.novafoundation.nova.runtime.storage.source.query.api.storage1OrNull
import io.novafoundation.nova.runtime.storage.source.query.api.storage2
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 StakingRuntimeApi(override val module: Module) : QueryableModule
context(RuntimeContext)
val RuntimeMetadata.staking: StakingRuntimeApi
get() = StakingRuntimeApi(staking())
context(RuntimeContext)
val StakingRuntimeApi.ledger: QueryableStorageEntry1<AccountId, StakingLedger>
get() = storage1("Ledger", binding = { decoded, _ -> bindStakingLedger(decoded) })
context(RuntimeContext)
val StakingRuntimeApi.nominators: QueryableStorageEntry1<AccountId, Nominations>
get() = storage1("Nominators", binding = { decoded, _ -> bindNominations(decoded) })
context(RuntimeContext)
val StakingRuntimeApi.validators: QueryableStorageEntry1<AccountId, ValidatorPrefs>
get() = storage1("Validators", binding = { decoded, _ -> bindValidatorPrefs(decoded) })
context(RuntimeContext)
val StakingRuntimeApi.bonded: QueryableStorageEntry1<AccountId, AccountId>
get() = storage1("Bonded", binding = { decoded, _ -> bindAccountId(decoded) })
context(RuntimeContext)
val StakingRuntimeApi.activeEra: QueryableStorageEntry0<EraIndex>
get() = storage0("ActiveEra", binding = ::bindActiveEra)
context(RuntimeContext)
val StakingRuntimeApi.erasStartSessionIndexOrNull: QueryableStorageEntry1<EraIndex, SessionIndex>?
get() = storage1OrNull("ErasStartSessionIndex", binding = { decoded, _ -> bindSessionIndex(decoded) })
context(RuntimeContext)
val StakingRuntimeApi.bondedErasOrNull: QueryableStorageEntry0<BondedEras>?
get() = storage0OrNull("BondedEras", binding = BondedEras.Companion::bind)
context(RuntimeContext)
val StakingRuntimeApi.unappliedSlashes: QueryableStorageEntry2<EraIndex, UnappliedSlashKey, Unit>
get() = storage2(
name = "UnappliedSlashes",
binding = { _, _, _ -> },
key1ToInternalConverter = { it },
key2ToInternalConverter = { TODO("Not yet needed") },
key1FromInternalConverter = ::bindEraIndex,
key2FromInternalConverter = UnappliedSlashKey.Companion::bind
)
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
import io.novafoundation.nova.feature_staking_api.domain.model.BondedEra
import io.novafoundation.nova.feature_staking_api.domain.model.BondedEras
fun BondedEras.Companion.bind(decoded: Any?): BondedEras {
val value = bindList(decoded, BondedEra.Companion::bind)
return BondedEras(value)
}
private fun BondedEra.Companion.bind(decoded: Any?): BondedEra {
val (eraIndex, sessionIndex) = decoded.castToList()
return BondedEra(
bindEraIndex(dynamicInstance = eraIndex),
bindSessionIndex(sessionIndex)
)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
import io.novafoundation.nova.common.utils.mapToSet
typealias ClaimedRewardsPages = Set<Int>
fun bindClaimedPages(decoded: Any?): Set<Int> {
val asList = decoded.castToList()
return asList.mapToSet { item ->
bindNumber(item).toInt()
}
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberConstant
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.metadata.module.Constant
import java.math.BigInteger
/*
SlashDeferDuration = EraIndex
*/
@UseCaseBinding
fun bindSlashDeferDuration(
constant: Constant,
runtime: RuntimeSnapshot
): BigInteger = bindNumberConstant(constant, runtime)
@UseCaseBinding
fun bindMaximumRewardedNominators(
constant: Constant,
runtime: RuntimeSnapshot
): BigInteger = bindNumberConstant(constant, runtime)
@@ -0,0 +1,65 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.getTyped
import io.novafoundation.nova.common.data.network.runtime.binding.storageReturnType
import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex
import io.novafoundation.nova.feature_staking_api.domain.model.SessionIndex
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
import java.math.BigInteger
/*
"ActiveEraInfo": {
"type": "struct",
"type_mapping": [
[
"index",
"EraIndex"
],
[
"start",
"Option<Moment>"
]
]
}
*/
@UseCaseBinding
fun bindActiveEra(
scale: String,
runtime: RuntimeSnapshot
): BigInteger {
val returnType = runtime.metadata.storageReturnType("Staking", "ActiveEra")
val decoded = returnType.fromHexOrNull(runtime, scale)
return bindActiveEra(decoded)
}
fun bindActiveEra(decoded: Any?): BigInteger {
return bindEraIndex(decoded.castToStruct().getTyped("index"))
}
/*
EraIndex
*/
@UseCaseBinding
fun bindCurrentEra(
scale: String,
runtime: RuntimeSnapshot
): BigInteger {
val returnType = runtime.metadata.storageReturnType("Staking", "CurrentEra")
return bindEraIndex(returnType.fromHexOrNull(runtime, scale))
}
@HelperBinding
fun bindEraIndex(dynamicInstance: Any?): EraIndex = bindNumber(dynamicInstance)
@HelperBinding
fun bindSessionIndex(dynamicInstance: Any?): SessionIndex = bindNumber(dynamicInstance)
@HelperBinding
fun bindSlot(dynamicInstance: Any?): BigInteger = bindNumber(dynamicInstance)
@@ -0,0 +1,43 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.getList
import io.novafoundation.nova.common.data.network.runtime.binding.requireType
import io.novafoundation.nova.common.utils.second
import io.novasama.substrate_sdk_android.runtime.AccountId
import java.math.BigInteger
typealias RewardPoints = BigInteger
class EraRewardPoints(
val totalPoints: RewardPoints,
val individual: List<Individual>
) {
class Individual(val accountId: AccountId, val rewardPoints: RewardPoints)
}
@UseCaseBinding
fun bindEraRewardPoints(
decoded: Any?
): EraRewardPoints {
val dynamicInstance = decoded.castToStruct()
return EraRewardPoints(
totalPoints = bindRewardPoint(dynamicInstance["total"]),
individual = dynamicInstance.getList("individual").map {
requireType<List<*>>(it) // (AccountId, RewardPoint)
EraRewardPoints.Individual(
accountId = bindAccountId(it.first()),
rewardPoints = bindRewardPoint(it.second())
)
}
)
}
@HelperBinding
fun bindRewardPoint(dynamicInstance: Any?): RewardPoints = bindNumber(dynamicInstance)
@@ -0,0 +1,64 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
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.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novafoundation.nova.common.data.network.runtime.binding.requireType
import io.novafoundation.nova.feature_staking_api.domain.model.Exposure
import io.novafoundation.nova.feature_staking_api.domain.model.ExposureOverview
import io.novafoundation.nova.feature_staking_api.domain.model.ExposurePage
import io.novafoundation.nova.feature_staking_api.domain.model.IndividualExposure
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import java.math.BigInteger
/*
IndividualExposure: {
who: AccountId; // account id of the nominator
value: Compact<Balance>; // nominators stake
}
*/
@HelperBinding
private fun bindIndividualExposure(dynamicInstance: Any?): IndividualExposure {
requireType<Struct.Instance>(dynamicInstance)
val who = dynamicInstance.get<ByteArray>("who") ?: incompatible()
val value = dynamicInstance.get<BigInteger>("value") ?: incompatible()
return IndividualExposure(who, value)
}
@UseCaseBinding
fun bindExposure(instance: Any?): Exposure {
val decoded = instance.castToStruct()
val total = decoded.get<BigInteger>("total") ?: incompatible()
val own = decoded.get<BigInteger>("own") ?: incompatible()
val others = decoded.get<List<*>>("others")?.map { bindIndividualExposure(it) } ?: incompatible()
return Exposure(total, own, others)
}
@UseCaseBinding
fun bindExposureOverview(instance: Any?): ExposureOverview {
val decoded = instance.castToStruct()
return ExposureOverview(
total = bindNumber(decoded["total"]),
own = bindNumber(decoded["own"]),
nominatorCount = bindNumber(decoded["nominatorCount"]),
pageCount = bindNumber(decoded["pageCount"])
)
}
@UseCaseBinding
fun bindExposurePage(instance: Any?): ExposurePage {
val decoded = instance.castToStruct()
return ExposurePage(
others = bindList(decoded["others"], ::bindIndividualExposure)
)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.fromHexOrIncompatible
import io.novafoundation.nova.common.data.network.runtime.binding.storageReturnType
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import java.math.BigInteger
@UseCaseBinding
fun bindHistoryDepth(scale: String, runtime: RuntimeSnapshot): BigInteger {
val type = runtime.metadata.storageReturnType("Staking", "HistoryDepth")
return bindNumber(type.fromHexOrIncompatible(scale, runtime))
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.getList
import io.novafoundation.nova.common.data.network.runtime.binding.getTyped
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novafoundation.nova.common.data.network.runtime.binding.requireType
import io.novafoundation.nova.common.data.network.runtime.binding.returnType
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.feature_staking_api.domain.model.Nominations
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
import io.novasama.substrate_sdk_android.runtime.metadata.storage
@UseCaseBinding
fun bindNominations(scale: String, runtime: RuntimeSnapshot): Nominations {
val type = runtime.metadata.staking().storage("Nominators").returnType()
val dynamicInstance = type.fromHexOrNull(runtime, scale) ?: incompatible()
return bindNominations(dynamicInstance)
}
fun bindNominations(dynamicInstance: Any): Nominations {
requireType<Struct.Instance>(dynamicInstance)
return Nominations(
targets = dynamicInstance.getList("targets").map { it as AccountId },
submittedInEra = bindEraIndex(dynamicInstance["submittedIn"]),
suppressed = dynamicInstance.getTyped("suppressed")
)
}
fun bindNominationsOrNull(dynamicInstance: Any?): Nominations? {
return dynamicInstance?.let(::bindNominations)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.fromHexOrIncompatible
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
import java.math.BigInteger
@UseCaseBinding
fun bindTotalValidatorEraReward(scale: String?, runtime: RuntimeSnapshot, type: Type<*>): BigInteger {
val result = scale?.let { bindNumber(type.fromHexOrIncompatible(it, runtime)) }
return result ?: incompatible()
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.cast
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novafoundation.nova.common.data.network.runtime.binding.returnType
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.feature_staking_api.domain.model.RewardDestination
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
import io.novasama.substrate_sdk_android.runtime.metadata.storage
private const val TYPE_STAKED = "Staked"
private const val TYPE_ACCOUNT = "Account"
fun bindRewardDestination(rewardDestination: RewardDestination) = when (rewardDestination) {
is RewardDestination.Restake -> DictEnum.Entry(TYPE_STAKED, null)
is RewardDestination.Payout -> DictEnum.Entry(TYPE_ACCOUNT, rewardDestination.targetAccountId)
}
fun bindRewardDestination(
scale: String,
runtime: RuntimeSnapshot,
stashId: AccountId,
controllerId: AccountId,
): RewardDestination {
val type = runtime.metadata.staking().storage("Payee").returnType()
val dynamicInstance = type.fromHexOrNull(runtime, scale).cast<DictEnum.Entry<*>>()
return when (dynamicInstance.name) {
TYPE_STAKED -> RewardDestination.Restake
TYPE_ACCOUNT -> RewardDestination.Payout(dynamicInstance.value.cast())
"Stash" -> RewardDestination.Payout(stashId)
"Controller" -> RewardDestination.Payout(controllerId)
else -> incompatible()
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindSet
typealias SessionValidators = Set<AccountIdKey>
fun bindSessionValidators(dynamicInstance: Any?) = bindSet(dynamicInstance, ::bindAccountIdKey)
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.getList
import io.novafoundation.nova.common.data.network.runtime.binding.storageReturnType
import io.novafoundation.nova.feature_staking_api.domain.model.SlashingSpans
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
fun bindSlashingSpans(
decoded: Any?,
): SlashingSpans {
val asStruct = decoded.castToStruct()
return SlashingSpans(
lastNonZeroSlash = bindEraIndex(asStruct["lastNonzeroSlash"]),
prior = asStruct.getList("prior").map(::bindEraIndex)
)
}
@UseCaseBinding
fun bindSlashingSpans(
scale: String,
runtime: RuntimeSnapshot,
returnType: Type<*> = runtime.metadata.storageReturnType("Staking", "SlashingSpans")
): SlashingSpans {
val decoded = returnType.fromHexOrNull(runtime, scale)
return bindSlashingSpans(decoded)
}
@@ -0,0 +1,56 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.getList
import io.novafoundation.nova.common.data.network.runtime.binding.getTyped
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novafoundation.nova.common.data.network.runtime.binding.requireType
import io.novafoundation.nova.common.data.network.runtime.binding.returnType
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger
import io.novafoundation.nova.feature_staking_api.domain.model.UnlockChunk
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
import io.novasama.substrate_sdk_android.runtime.metadata.storage
@UseCaseBinding
fun bindStakingLedger(scale: String, runtime: RuntimeSnapshot): StakingLedger {
val type = runtime.metadata.staking().storage("Ledger").returnType()
val dynamicInstance = type.fromHexOrNull(runtime, scale) ?: incompatible()
return bindStakingLedger(dynamicInstance)
}
@UseCaseBinding
fun bindStakingLedger(decoded: Any): StakingLedger {
requireType<Struct.Instance>(decoded)
return StakingLedger(
stashId = decoded.getTyped("stash"),
total = decoded.getTyped("total"),
active = decoded.getTyped("active"),
unlocking = decoded.getList("unlocking").map(::bindUnlockChunk),
claimedRewards = bindList(
dynamicInstance = decoded["claimedRewards"] ?: decoded["legacyClaimedRewards"] ?: emptyList<Nothing>(),
itemBinder = ::bindEraIndex
)
)
}
@UseCaseBinding
fun bindStakingLedgerOrNull(dynamicInstance: Any?): StakingLedger? {
return dynamicInstance?.let(::bindStakingLedger)
}
@HelperBinding
fun bindUnlockChunk(dynamicInstance: Any?): UnlockChunk {
requireType<Struct.Instance>(dynamicInstance)
return UnlockChunk(
amount = dynamicInstance.getTyped("value"),
era = dynamicInstance.getTyped("era")
)
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.fromHexOrIncompatible
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
import java.math.BigInteger
fun bindMinBond(scale: String, runtimeSnapshot: RuntimeSnapshot, type: Type<*>): BigInteger {
return bindNumber(scale, runtimeSnapshot, type)
}
fun bindMaxNominators(scale: String, runtimeSnapshot: RuntimeSnapshot, type: Type<*>): BigInteger {
return bindNumber(scale, runtimeSnapshot, type)
}
fun bindNominatorsCount(scale: String, runtimeSnapshot: RuntimeSnapshot, type: Type<*>): BigInteger {
return bindNumber(scale, runtimeSnapshot, type)
}
private fun bindNumber(scale: String, runtimeSnapshot: RuntimeSnapshot, type: Type<*>): BigInteger {
return bindNumber(type.fromHexOrIncompatible(scale, runtimeSnapshot))
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
data class UnappliedSlashKey(val validator: AccountIdKey) {
companion object {
fun bind(decoded: Any?): UnappliedSlashKey {
val (validator) = decoded.castToList()
return UnappliedSlashKey(
validator = bindAccountIdKey(validator),
)
}
}
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings
import io.novafoundation.nova.common.data.network.runtime.binding.bindPerbillNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.getTyped
import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs
private const val BLOCKED_DEFAULT = false
fun bindValidatorPrefs(decoded: Any?): ValidatorPrefs {
val asStruct = decoded.castToStruct()
return ValidatorPrefs(
commission = bindPerbillNumber(asStruct.getTyped("commission")),
blocked = asStruct["blocked"] ?: BLOCKED_DEFAULT
)
}
@@ -0,0 +1,116 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.calls
import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress
import io.novafoundation.nova.common.data.network.runtime.binding.bindMultiAddress
import io.novafoundation.nova.common.utils.voterListName
import io.novafoundation.nova.feature_staking_api.domain.model.RewardDestination
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindRewardDestination
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.definitions.types.instances.AddressInstanceConstructor
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
import java.math.BigInteger
fun ExtrinsicBuilder.setController(controllerAddress: MultiAddress): ExtrinsicBuilder {
return call(
"Staking",
"set_controller",
mapOf(
// `controller` argument is missing on newer versions of staking pallets
// but we don`t sanitize it since library will ignore unknown arguments on encoding stage
"controller" to bindMultiAddress(controllerAddress)
)
)
}
fun ExtrinsicBuilder.bond(
controllerAddress: MultiAddress,
amount: BigInteger,
payee: RewardDestination,
): ExtrinsicBuilder {
return call(
"Staking",
"bond",
mapOf(
// `controller` argument is missing on newer versions of staking pallets
// but we don`t sanitize it since library will ignore unknown arguments on encoding stage
"controller" to bindMultiAddress(controllerAddress),
"value" to amount,
"payee" to bindRewardDestination(payee)
)
)
}
fun ExtrinsicBuilder.nominate(targets: List<MultiAddress>): ExtrinsicBuilder {
return call(
"Staking",
"nominate",
mapOf(
"targets" to targets.map(::bindMultiAddress)
)
)
}
fun ExtrinsicBuilder.bondMore(amount: BigInteger): ExtrinsicBuilder {
return call(
"Staking",
"bond_extra",
mapOf(
"max_additional" to amount
)
)
}
fun ExtrinsicBuilder.chill(): ExtrinsicBuilder {
return call("Staking", "chill", emptyMap())
}
fun ExtrinsicBuilder.unbond(amount: BigInteger): ExtrinsicBuilder {
return call(
"Staking",
"unbond",
mapOf(
"value" to amount
)
)
}
fun ExtrinsicBuilder.withdrawUnbonded(numberOfSlashingSpans: BigInteger): ExtrinsicBuilder {
return call(
"Staking",
"withdraw_unbonded",
mapOf(
"num_slashing_spans" to numberOfSlashingSpans
)
)
}
fun ExtrinsicBuilder.rebond(amount: BigInteger): ExtrinsicBuilder {
return call(
"Staking",
"rebond",
mapOf(
"value" to amount
)
)
}
fun ExtrinsicBuilder.setPayee(rewardDestination: RewardDestination): ExtrinsicBuilder {
return call(
"Staking",
"set_payee",
mapOf(
"payee" to bindRewardDestination(rewardDestination)
)
)
}
fun ExtrinsicBuilder.rebag(dislocated: AccountId): ExtrinsicBuilder {
return call(
moduleName = runtime.metadata.voterListName(),
callName = "rebag",
arguments = mapOf(
"dislocated" to AddressInstanceConstructor.constructInstance(runtime.typeRegistry, dislocated)
)
)
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core_db.model.AccountStakingLocal
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.AccountStakingScope
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 AccountNominationsUpdater(
scope: AccountStakingScope,
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<AccountStakingLocal>(scope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<AccountStakingLocal> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: AccountStakingLocal): String? {
val stakingAccessInfo = scopeValue.stakingAccessInfo ?: return null
val stashId = stakingAccessInfo.stashId
return runtime.metadata.staking().storage("Nominators").storageKey(runtime, stashId)
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core_db.model.AccountStakingLocal
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.AccountStakingScope
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 AccountRewardDestinationUpdater(
scope: AccountStakingScope,
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<AccountStakingLocal>(scope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<AccountStakingLocal> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: AccountStakingLocal): String? {
val stakingAccessInfo = scopeValue.stakingAccessInfo ?: return null
val stashId = stakingAccessInfo.stashId
return runtime.metadata.staking().storage("Payee").storageKey(runtime, stashId)
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core_db.model.AccountStakingLocal
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.AccountStakingScope
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 AccountValidatorPrefsUpdater(
scope: AccountStakingScope,
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<AccountStakingLocal>(scope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<AccountStakingLocal> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: AccountStakingLocal): String? {
val stakingAccessInfo = scopeValue.stakingAccessInfo ?: return null
val stashId = stakingAccessInfo.stashId
return runtime.metadata.staking().storage("Validators").storageKey(runtime, stashId)
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 ActiveEraUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String {
return runtime.metadata.staking().storage("ActiveEra").storageKey()
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.voterListOrNull
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core_db.model.AccountStakingLocal
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.AccountStakingScope
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 BagListNodeUpdater(
scope: AccountStakingScope,
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<AccountStakingLocal>(scope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<AccountStakingLocal> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: AccountStakingLocal): String? {
val stakingAccessInfo = scopeValue.stakingAccessInfo ?: return null
val stashId = stakingAccessInfo.stashId
return runtime.metadata.voterListOrNull()?.storage("ListNodes")?.storageKey(runtime, stashId)
}
}
@@ -0,0 +1,43 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.data.network.rpc.BulkRetriever
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.model.StorageEntry
import io.novafoundation.nova.core.storage.StorageCache
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import io.novasama.substrate_sdk_android.wsrpc.SocketService
fun RuntimeMetadata.activeEraStorageKey() = staking().storage("ActiveEra").storageKey()
suspend fun BulkRetriever.fetchValuesToCache(
socketService: SocketService,
keys: List<String>,
storageCache: StorageCache,
chainId: String,
) {
val allValues = queryKeys(socketService, keys)
val toInsert = allValues.map { (key, value) -> StorageEntry(key, value) }
storageCache.insert(toInsert, chainId)
}
/**
* @return number of fetched keys
*/
suspend fun BulkRetriever.fetchPrefixValuesToCache(
socketService: SocketService,
prefix: String,
storageCache: StorageCache,
chainId: String
): Int {
val allKeys = retrieveAllKeys(socketService, prefix)
if (allKeys.isNotEmpty()) {
fetchValuesToCache(socketService, allKeys, storageCache, chainId)
}
return allKeys.size
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.voterListOrNull
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 CounterForListNodesUpdater(
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String? {
return runtime.metadata.voterListOrNull()?.storage("CounterForListNodes")?.storageKey(runtime)
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
class CounterForNominatorsUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String? {
return runtime.metadata.staking().storageOrNull("CounterForNominators")?.storageKey()
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 CurrentEraUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache,
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String {
return runtime.metadata.staking().storage("CurrentEra").storageKey()
}
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.defaultInHex
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
class HistoryDepthUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache,
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override fun fallbackValue(runtime: RuntimeSnapshot): String? {
return storageEntry(runtime)?.defaultInHex()
}
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String? {
return storageEntry(runtime)?.storageKey()
}
private fun storageEntry(runtime: RuntimeSnapshot) = runtime.metadata.staking().storageOrNull("HistoryDepth")
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
class MaxNominatorsUpdater(
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String? {
return runtime.metadata.staking().storageOrNull("MaxNominatorsCount")?.storageKey()
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
class MinBondUpdater(
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache,
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String? {
return runtime.metadata.staking().storageOrNull("MinNominatorBond")?.storageKey()
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.parasOrNull
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
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 ParachainsUpdater(
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String? {
return runtime.metadata.parasOrNull()?.storage("Parachains")?.storageKey(runtime)
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.proxyOrNull
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core_db.model.AccountStakingLocal
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.AccountStakingScope
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
class ProxiesUpdater(
scope: AccountStakingScope,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : SingleStorageKeyUpdater<AccountStakingLocal>(scope, stakingSharedState, chainRegistry, storageCache), SharedStateBasedUpdater<AccountStakingLocal> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: AccountStakingLocal): String? {
val accountId = scopeValue.accountId
return runtime.metadata.proxyOrNull()?.storageOrNull("Proxies")?.storageKey(runtime, accountId)
}
}
@@ -0,0 +1,50 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.mergeIfMultiple
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.network.updaters.ChainUpdaterGroupUpdateSystem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
class StakingLandingInfoUpdateSystemFactory(
private val stakingUpdaters: StakingUpdaters,
private val chainRegistry: ChainRegistry,
private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
) {
fun create(chainId: ChainId, stakingTypes: List<Chain.Asset.StakingType>): StakingLandingInfoUpdateSystem {
return StakingLandingInfoUpdateSystem(
stakingUpdaters,
chainId,
stakingTypes,
chainRegistry,
storageSharedRequestsBuilderFactory,
)
}
}
class StakingLandingInfoUpdateSystem(
private val stakingUpdaters: StakingUpdaters,
private val chainId: ChainId,
private val stakingTypes: List<Chain.Asset.StakingType>,
private val chainRegistry: ChainRegistry,
storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
) : ChainUpdaterGroupUpdateSystem(chainRegistry, storageSharedRequestsBuilderFactory) {
override fun start(): Flow<Updater.SideEffect> = flowOfAll {
val stakingChain = chainRegistry.getChain(chainId)
val updatersBySyncChainId = stakingUpdaters.getUpdaters(stakingChain, stakingTypes)
updatersBySyncChainId.map { (syncChainId, updaters) ->
val syncChain = chainRegistry.getChain(syncChainId)
runUpdaters(syncChain, updaters)
}.mergeIfMultiple()
}.flowOn(Dispatchers.Default)
}
@@ -0,0 +1,182 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.model.StorageChange
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.dao.AccountStakingDao
import io.novafoundation.nova.core_db.model.AccountStakingLocal
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_api.domain.api.StakingRepository
import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger
import io.novafoundation.nova.feature_staking_api.domain.model.isRedeemableIn
import io.novafoundation.nova.feature_staking_api.domain.model.isUnbondingIn
import io.novafoundation.nova.feature_staking_api.domain.model.sumStaking
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindStakingLedger
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
import io.novafoundation.nova.runtime.ext.disabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novafoundation.nova.runtime.network.updaters.insert
import io.novafoundation.nova.runtime.state.assetWithChain
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.AccountId
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
import io.novasama.substrate_sdk_android.wsrpc.SocketService
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.storage.SubscribeStorageRequest
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.storage.storageChange
import io.novasama.substrate_sdk_android.wsrpc.subscriptionFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import java.math.BigInteger
class LedgerWithController(
val ledger: StakingLedger,
val controllerId: AccountId,
)
class StakingLedgerUpdater(
private val stakingRepository: StakingRepository,
private val stakingSharedState: StakingSharedState,
private val chainRegistry: ChainRegistry,
private val accountStakingDao: AccountStakingDao,
private val storageCache: StorageCache,
private val assetCache: AssetCache,
override val scope: AccountUpdateScope,
) : SharedStateBasedUpdater<MetaAccount> {
override suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder,
scopeValue: MetaAccount
): Flow<Updater.SideEffect> {
val (chain, chainAsset) = stakingSharedState.assetWithChain.first()
if (chainAsset.disabled) return emptyFlow()
val runtime = chainRegistry.getRuntime(chain.id)
val currentAccountId = scopeValue.accountIdIn(chain) ?: return emptyFlow()
val socketService = storageSubscriptionBuilder.socketService ?: return emptyFlow()
val key = runtime.metadata.staking().storage("Bonded").storageKey(runtime, currentAccountId)
return storageSubscriptionBuilder.subscribe(key)
.flatMapLatest { change ->
// assume we're controller, if no controller found
val controllerId = change.value?.fromHex() ?: currentAccountId
subscribeToLedger(socketService, runtime, chain.id, controllerId)
}.onEach { ledgerWithController ->
updateAccountStaking(chain.id, chainAsset.id, currentAccountId, ledgerWithController)
ledgerWithController?.let {
val era = stakingRepository.getActiveEraIndex(chain.id)
val stashId = it.ledger.stashId
val controllerId = it.controllerId
updateAssetStaking(it.ledger.stashId, chainAsset, it.ledger, era)
if (!stashId.contentEquals(controllerId)) {
updateAssetStaking(controllerId, chainAsset, it.ledger, era)
}
} ?: updateAssetStakingForEmptyLedger(currentAccountId, chainAsset)
}
.flowOn(Dispatchers.IO)
.noSideAffects()
}
private suspend fun updateAccountStaking(
chainId: String,
chainAssetId: Int,
accountId: AccountId,
ledgerWithController: LedgerWithController?,
) {
val accountStaking = AccountStakingLocal(
chainId = chainId,
chainAssetId = chainAssetId,
accountId = accountId,
stakingAccessInfo = ledgerWithController?.let {
AccountStakingLocal.AccessInfo(
stashId = it.ledger.stashId,
controllerId = it.controllerId,
)
}
)
accountStakingDao.insert(accountStaking)
}
private suspend fun subscribeToLedger(
socketService: SocketService,
runtime: RuntimeSnapshot,
chainId: String,
controllerId: AccountId,
): Flow<LedgerWithController?> {
val key = runtime.metadata.staking().storage("Ledger").storageKey(runtime, controllerId)
val request = SubscribeStorageRequest(key)
return socketService.subscriptionFlow(request)
.map { it.storageChange() }
.onEach {
val storageChange = StorageChange(it.block, key, it.getSingleChange())
storageCache.insert(storageChange, chainId)
}
.map {
val change = it.getSingleChange()
if (change != null) {
val ledger = bindStakingLedger(change, runtime)
LedgerWithController(ledger, controllerId)
} else {
null
}
}
}
private suspend fun updateAssetStaking(
accountId: AccountId,
chainAsset: Chain.Asset,
stakingLedger: StakingLedger,
era: BigInteger,
) {
assetCache.updateAsset(accountId, chainAsset) { cached ->
val redeemable = stakingLedger.unlocking.sumStaking { it.isRedeemableIn(era) }
val unbonding = stakingLedger.unlocking.sumStaking { it.isUnbondingIn(era) }
cached.copy(
redeemableInPlanks = redeemable,
unbondingInPlanks = unbonding,
bondedInPlanks = stakingLedger.active
)
}
}
private suspend fun updateAssetStakingForEmptyLedger(
accountId: AccountId,
chainAsset: Chain.Asset,
) {
assetCache.updateAsset(accountId, chainAsset) { cached ->
cached.copy(
redeemableInPlanks = BigInteger.ZERO,
unbondingInPlanks = BigInteger.ZERO,
bondedInPlanks = BigInteger.ZERO
)
}
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.MultiMap
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_staking_impl.data.StakingOption
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.chain
import io.novafoundation.nova.feature_staking_impl.data.stakingType
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.network.updaters.multiChain.MultiChainUpdateSystem
class StakingUpdateSystem(
private val chainRegistry: ChainRegistry,
private val stakingUpdaters: StakingUpdaters,
stakingSharedState: StakingSharedState,
storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
) : MultiChainUpdateSystem<StakingSharedState.OptionAdditionalData>(chainRegistry, stakingSharedState, storageSharedRequestsBuilderFactory) {
override fun getUpdaters(option: StakingOption): MultiMap<ChainId, Updater<*>> {
return stakingUpdaters.getUpdaters(option.chain, option.stakingType)
}
}
@@ -0,0 +1,67 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.utils.MultiMap
import io.novafoundation.nova.common.utils.buildMultiMap
import io.novafoundation.nova.common.utils.putAll
import io.novafoundation.nova.core.updater.Updater
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.multiNetwork.chain.model.Chain.Asset.StakingType.ALEPH_ZERO
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.MYTHOS
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.NOMINATION_POOLS
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.PARACHAIN
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.RELAYCHAIN
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.RELAYCHAIN_AURA
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.TURING
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.UNSUPPORTED
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.network.updaters.multiChain.groupBySyncingChain
class StakingUpdaters(
private val relaychainUpdaters: Group,
private val parachainUpdaters: Group,
private val commonUpdaters: Group,
private val turingExtraUpdaters: Group,
private val nominationPoolsUpdaters: Group,
private val mythosUpdaters: Group,
) {
class Group(val updaters: List<SharedStateBasedUpdater<*>>) {
constructor(vararg updaters: SharedStateBasedUpdater<*>) : this(updaters.toList())
}
fun getUpdaters(stakingChain: Chain, stakingType: StakingType): MultiMap<ChainId, Updater<*>> {
return buildMultiMap {
putAll(getCommonUpdaters(stakingChain))
putAll(getUpdatersByType(stakingChain, stakingType))
}
}
fun getUpdaters(stakingChain: Chain, stakingTypes: List<StakingType>): MultiMap<ChainId, Updater<*>> {
return buildMultiMap {
putAll(getCommonUpdaters(stakingChain))
stakingTypes.forEach {
putAll(getUpdatersByType(stakingChain, it))
}
}
}
private fun getCommonUpdaters(stakingChain: Chain): MultiMap<ChainId, SharedStateBasedUpdater<*>> {
return commonUpdaters.updaters.groupBySyncingChain(stakingChain)
}
private fun getUpdatersByType(stakingChain: Chain, stakingType: StakingType): MultiMap<ChainId, SharedStateBasedUpdater<*>> {
val byTypeUpdaters = when (stakingType) {
RELAYCHAIN, RELAYCHAIN_AURA, ALEPH_ZERO -> relaychainUpdaters.updaters
PARACHAIN -> parachainUpdaters.updaters
TURING -> parachainUpdaters.updaters + turingExtraUpdaters.updaters
NOMINATION_POOLS -> nominationPoolsUpdaters.updaters
MYTHOS -> mythosUpdaters.updaters
UNSUPPORTED -> emptyList()
}
return byTypeUpdaters.groupBySyncingChain(stakingChain)
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.UpdateScope
import io.novafoundation.nova.runtime.ext.timelineChainIdOrSelf
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novafoundation.nova.runtime.network.updaters.multiChain.DelegateToTimelineChainIdHolder
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
abstract class TimelineDelegatingSingleKeyUpdater<V>(
scope: UpdateScope<V>,
chainRegistry: ChainRegistry,
storageCache: StorageCache,
timelineDelegatingChainIdHolder: DelegateToTimelineChainIdHolder
) : SingleStorageKeyUpdater<V>(scope, timelineDelegatingChainIdHolder, chainRegistry, storageCache), SharedStateBasedUpdater<V> {
override fun getSyncChainId(sharedStateChain: Chain): ChainId {
return sharedStateChain.timelineChainIdOrSelf()
}
}
@@ -0,0 +1,251 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters
import io.novafoundation.nova.common.data.network.rpc.BulkRetriever
import io.novafoundation.nova.common.utils.hasStorage
import io.novafoundation.nova.common.utils.staking
import io.novafoundation.nova.core.model.StorageEntry
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.ValidatorExposureUpdater.Companion.STORAGE_KEY_PAGED_EXPOSURES
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.ActiveEraScope
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
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
import io.novasama.substrate_sdk_android.wsrpc.SocketService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import java.math.BigInteger
/**
* Manages sync for validators exposures
* Depending on the version of the staking pallet, exposures might be stored differently:
*
* 1. Legacy version - exposures are stored in `EraStakers` storage
* 2. Current version - exposures are paged and stored in two storages: `EraStakersPaged` and `EraStakersOverview`
*
* Note that during the transition from Legacy to Current version during next [Staking.historyDepth]
* eras older storage will still be present and filled with previous era information. Old storage will be cleared on era-by-era basis once new era happens.
* Also, right after chain has just upgraded, `EraStakersPaged` will be empty untill next era happens.
*
* The updater takes care of that all also storing special [STORAGE_KEY_PAGED_EXPOSURES] indicating whether
* paged exposures are actually present for the latest era or not
*/
class ValidatorExposureUpdater(
private val bulkRetriever: BulkRetriever,
private val stakingSharedState: StakingSharedState,
private val chainRegistry: ChainRegistry,
private val storageCache: StorageCache,
override val scope: ActiveEraScope,
) : SharedStateBasedUpdater<EraIndex> {
companion object {
const val STORAGE_KEY_PAGED_EXPOSURES = "NovaWallet.Staking.PagedExposuresUsed"
fun isPagedExposuresValue(enabled: Boolean) = enabled.toString()
fun decodeIsPagedExposuresValue(value: String?) = value.toBoolean()
}
override suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder,
scopeValue: EraIndex,
): Flow<Updater.SideEffect> {
@Suppress("UnnecessaryVariable")
val activeEra = scopeValue
val socketService = storageSubscriptionBuilder.socketService ?: return emptyFlow()
return flow<Updater.SideEffect> {
val chainId = stakingSharedState.chainId()
val runtime = chainRegistry.getRuntime(chainId)
if (checkValuesInCache(activeEra, chainId, runtime)) {
return@flow
}
cleanupOutdatedEras(chainId, runtime)
syncNewExposures(activeEra, runtime, socketService, chainId)
}.noSideAffects()
}
private suspend fun checkValuesInCache(era: BigInteger, chainId: String, runtimeSnapshot: RuntimeSnapshot): Boolean {
if (!isPagedExposuresFlagInCache(chainId)) return false
return isPagedExposuresInCache(era, chainId, runtimeSnapshot) || isLegacyExposuresInCache(era, chainId, runtimeSnapshot)
}
private suspend fun isPagedExposuresFlagInCache(chainId: String): Boolean {
return storageCache.isFullKeyInCache(STORAGE_KEY_PAGED_EXPOSURES, chainId)
}
private suspend fun isPagedExposuresInCache(era: BigInteger, chainId: String, runtimeSnapshot: RuntimeSnapshot): Boolean {
if (!runtimeSnapshot.pagedExposuresEnabled()) return false
val prefix = runtimeSnapshot.eraStakersPagedPrefixFor(era)
return storageCache.isPrefixInCache(prefix, chainId)
}
private suspend fun isLegacyExposuresInCache(era: BigInteger, chainId: String, runtimeSnapshot: RuntimeSnapshot): Boolean {
// We cannot construct storage key to remove legacy exposures
// There is a little chance they are still there given that removing of legacy exposures should
// happen at least when all legacy exposures are removed from the chain
if (runtimeSnapshot.legacyExposuresFullyRemoved()) {
return false
}
val prefix = runtimeSnapshot.eraStakersPrefixFor(era)
return storageCache.isPrefixInCache(prefix, chainId)
}
private suspend fun cleanupOutdatedEras(chainId: String, runtimeSnapshot: RuntimeSnapshot) {
cleanupPagedExposures(runtimeSnapshot, chainId)
cleanupLegacyExposures(runtimeSnapshot, chainId)
}
private suspend fun cleanupLegacyExposures(runtimeSnapshot: RuntimeSnapshot, chainId: String) {
if (runtimeSnapshot.legacyExposuresFullyRemoved()) return
storageCache.removeByPrefix(runtimeSnapshot.eraStakersPrefix(), chainId)
}
private suspend fun cleanupPagedExposures(runtimeSnapshot: RuntimeSnapshot, chainId: String) {
if (!runtimeSnapshot.pagedExposuresEnabled()) return
storageCache.removeByPrefix(runtimeSnapshot.eraStakersPagedPrefix(), chainId)
storageCache.removeByPrefix(runtimeSnapshot.eraStakersOverviewPrefix(), chainId)
}
private suspend fun syncNewExposures(era: BigInteger, runtimeSnapshot: RuntimeSnapshot, socketService: SocketService, chainId: String) {
var pagedExposureState = runtimeSnapshot.detectExposureStateFromPallets()
if (pagedExposureState.shouldTrySyncingPagedExposures()) {
val pagedExposuresPresent = tryFetchingPagedExposures(era, runtimeSnapshot, socketService, chainId)
if (pagedExposuresPresent) {
pagedExposureState = ExposureState.CERTAIN_PAGED
}
}
if (pagedExposureState.shouldTrySyncingLegacyExposures()) {
val legacyExposuresPresent = fetchLegacyExposures(era, runtimeSnapshot, socketService, chainId)
if (legacyExposuresPresent) {
pagedExposureState = ExposureState.CERTAIN_LEGACY
}
}
saveIsExposuresUsedFlag(pagedExposureState, chainId)
}
private fun RuntimeSnapshot.detectExposureStateFromPallets(): ExposureState {
return when {
legacyExposuresFullyRemoved() -> ExposureState.CERTAIN_PAGED
// Just because paged exposures are enabled does not mean they are actually used yet
pagedExposuresEnabled() -> ExposureState.UNCERTAIN
else -> ExposureState.CERTAIN_LEGACY
}
}
private enum class ExposureState {
CERTAIN_PAGED, UNCERTAIN, CERTAIN_LEGACY
}
private fun ExposureState.shouldTrySyncingPagedExposures(): Boolean {
return when (this) {
ExposureState.CERTAIN_PAGED, ExposureState.UNCERTAIN -> true
ExposureState.CERTAIN_LEGACY -> false
}
}
private fun ExposureState.shouldTrySyncingLegacyExposures(): Boolean {
return when (this) {
ExposureState.CERTAIN_LEGACY, ExposureState.UNCERTAIN -> true
ExposureState.CERTAIN_PAGED -> false
}
}
private suspend fun tryFetchingPagedExposures(
era: BigInteger,
runtimeSnapshot: RuntimeSnapshot,
socketService: SocketService,
chainId: String
): Boolean {
val overviewPrefix = runtimeSnapshot.eraStakersOverviewPrefixFor(era)
val numberOfKeysSynced = bulkRetriever.fetchPrefixValuesToCache(socketService, overviewPrefix, storageCache, chainId)
val pagedExposuresPresent = numberOfKeysSynced > 0
if (pagedExposuresPresent) {
val pagedExposuresPrefix = runtimeSnapshot.eraStakersPagedPrefixFor(era)
bulkRetriever.fetchPrefixValuesToCache(socketService, pagedExposuresPrefix, storageCache, chainId)
}
return pagedExposuresPresent
}
private suspend fun fetchLegacyExposures(
era: BigInteger,
runtimeSnapshot: RuntimeSnapshot,
socketService: SocketService,
chainId: String
): Boolean {
val prefix = runtimeSnapshot.eraStakersPrefixFor(era)
val keysFetched = bulkRetriever.fetchPrefixValuesToCache(socketService, prefix, storageCache, chainId)
return keysFetched > 0
}
private suspend fun saveIsExposuresUsedFlag(state: ExposureState, chainId: String) {
val isUsed = when (state) {
ExposureState.CERTAIN_PAGED -> true
ExposureState.CERTAIN_LEGACY -> false
ExposureState.UNCERTAIN -> return
}
val encodedValue = isPagedExposuresValue(isUsed)
val entry = StorageEntry(STORAGE_KEY_PAGED_EXPOSURES, encodedValue)
storageCache.insert(entry, chainId)
}
private fun RuntimeSnapshot.pagedExposuresEnabled(): Boolean {
return metadata.staking().hasStorage("ErasStakersPaged")
}
private fun RuntimeSnapshot.legacyExposuresFullyRemoved(): Boolean {
return !metadata.staking().hasStorage("ErasStakers")
}
private fun RuntimeSnapshot.eraStakersPrefixFor(era: BigInteger): String {
return metadata.staking().storage("ErasStakers").storageKey(this, era)
}
private fun RuntimeSnapshot.eraStakersOverviewPrefixFor(era: BigInteger): String {
return metadata.staking().storage("ErasStakersOverview").storageKey(this, era)
}
private fun RuntimeSnapshot.eraStakersOverviewPrefix(): String {
return metadata.staking().storage("ErasStakersOverview").storageKey(this)
}
private fun RuntimeSnapshot.eraStakersPagedPrefixFor(era: BigInteger): String {
return metadata.staking().storage("ErasStakersPaged").storageKey(this, era)
}
private fun RuntimeSnapshot.eraStakersPagedPrefix(): String {
return metadata.staking().storage("ErasStakersPaged").storageKey(this)
}
private fun RuntimeSnapshot.eraStakersPrefix(): String {
return metadata.staking().storage("ErasStakers").storageKey(this)
}
}
@@ -0,0 +1,70 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.controller
import io.novafoundation.nova.common.utils.system
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.model.AccountStakingLocal
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.AccountStakingScope
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
import io.novafoundation.nova.feature_wallet_api.data.cache.bindAccountInfoOrDefault
import io.novafoundation.nova.feature_wallet_api.data.cache.updateAsset
import io.novafoundation.nova.runtime.ext.disabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novafoundation.nova.runtime.state.chainAndAsset
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
class AccountControllerBalanceUpdater(
override val scope: AccountStakingScope,
private val sharedState: StakingSharedState,
private val chainRegistry: ChainRegistry,
private val assetCache: AssetCache,
) : SharedStateBasedUpdater<AccountStakingLocal> {
override suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder,
scopeValue: AccountStakingLocal
): Flow<Updater.SideEffect> {
val (chain, chainAsset) = sharedState.chainAndAsset()
if (chainAsset.disabled) return emptyFlow()
val runtime = chainRegistry.getRuntime(chain.id)
val accountStaking = scopeValue
val stakingAccessInfo = accountStaking.stakingAccessInfo ?: return emptyFlow()
val controllerId = stakingAccessInfo.controllerId
val stashId = stakingAccessInfo.stashId
val accountId = accountStaking.accountId
if (controllerId.contentEquals(stashId)) {
// balance is already observed, no need to do it twice
return emptyFlow()
}
val companionAccountId = when {
accountId.contentEquals(controllerId) -> stashId
accountId.contentEquals(stashId) -> controllerId
else -> throw IllegalArgumentException()
}
val key = runtime.metadata.system().storage("Account").storageKey(runtime, companionAccountId)
return storageSubscriptionBuilder.subscribe(key)
.onEach { change ->
val newAccountInfo = bindAccountInfoOrDefault(change.value, runtime)
assetCache.updateAsset(companionAccountId, chainAsset, newAccountInfo)
}
.flowOn(Dispatchers.IO)
.noSideAffects()
}
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.historical
import io.novafoundation.nova.common.utils.staking
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 HistoricalTotalValidatorRewardUpdater : HistoricalUpdater {
override fun constructKeyPrefix(runtime: RuntimeSnapshot): String {
return runtime.metadata.staking().storage("ErasValidatorReward").storageKey(runtime)
}
}
@@ -0,0 +1,58 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.historical
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.utils.flowOf
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope.ActiveEraScope
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.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import kotlinx.coroutines.flow.Flow
interface HistoricalUpdater {
fun constructKeyPrefix(runtime: RuntimeSnapshot): String
}
class HistoricalUpdateMediator(
override val scope: ActiveEraScope,
private val historicalUpdaters: List<HistoricalUpdater>,
private val stakingSharedState: StakingSharedState,
private val storageCache: StorageCache,
private val chainRegistry: ChainRegistry,
private val preferences: Preferences,
) : Updater<EraIndex>, SharedStateBasedUpdater<EraIndex> {
override suspend fun listenForUpdates(storageSubscriptionBuilder: SharedRequestsBuilder, scopeValue: EraIndex): Flow<Updater.SideEffect> {
val chainId = stakingSharedState.chainId()
val runtime = chainRegistry.getRuntime(chainId)
return flowOf {
if (isHistoricalDataCleared(chainId)) return@flowOf null
val prefixes = historicalUpdaters.map { it.constructKeyPrefix(runtime) }
prefixes.onEach { storageCache.removeByPrefix(prefixKey = it, chainId) }
setHistoricalDataCleared(chainId)
}
.noSideAffects()
}
private fun isHistoricalDataCleared(chainId: ChainId): Boolean {
return preferences.contains(isHistoricalDataClearedKey(chainId))
}
private fun setHistoricalDataCleared(chainId: ChainId) {
preferences.putBoolean(isHistoricalDataClearedKey(chainId), true)
}
private fun isHistoricalDataClearedKey(chainId: ChainId): String {
return "HistoricalUpdateMediator.HistoricalDataCleared::$chainId"
}
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.historical
import io.novafoundation.nova.common.utils.staking
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 HistoricalValidatorRewardPointsUpdater : HistoricalUpdater {
override fun constructKeyPrefix(runtime: RuntimeSnapshot): String {
return runtime.metadata.staking().storage("ErasRewardPoints").storageKey(runtime)
}
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope
import io.novafoundation.nova.common.utils.combineToPair
import io.novafoundation.nova.core.updater.UpdateScope
import io.novafoundation.nova.core_db.dao.AccountStakingDao
import io.novafoundation.nova.core_db.model.AccountStakingLocal
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.runtime.state.assetWithChain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
class AccountStakingScope(
private val accountRepository: AccountRepository,
private val accountStakingDao: AccountStakingDao,
private val sharedStakingState: StakingSharedState
) : UpdateScope<AccountStakingLocal> {
override fun invalidationFlow(): Flow<AccountStakingLocal> {
return combineToPair(
sharedStakingState.assetWithChain,
accountRepository.selectedMetaAccountFlow()
).flatMapLatest { (chainWithAsset, account) ->
val (chain, chainAsset) = chainWithAsset
val accountId = account.accountIdIn(chain) ?: return@flatMapLatest emptyFlow()
accountStakingDao.observeDistinct(chain.id, chainAsset.id, accountId)
}
}
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.scope
import io.novafoundation.nova.common.utils.withFlowScope
import io.novafoundation.nova.core.updater.UpdateScope
import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation
import kotlinx.coroutines.flow.Flow
class ActiveEraScope(
private val stakingSharedComputation: StakingSharedComputation,
private val stakingSharedState: StakingSharedState,
) : UpdateScope<EraIndex> {
override fun invalidationFlow(): Flow<EraIndex?> {
return withFlowScope { flowScope ->
val chainId = stakingSharedState.chainId()
stakingSharedComputation.activeEraFlow(chainId, flowScope)
}
}
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.session
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.common.utils.provideContext
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.bondedErasOrNull
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.staking
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SingleStorageKeyUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
class BondedErasUpdaterUpdater(
storageCache: StorageCache,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
) : SingleStorageKeyUpdater<Unit>(GlobalScope, stakingSharedState, chainRegistry, storageCache),
SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String? {
return runtime.provideContext { metadata.staking.bondedErasOrNull?.storageKey() }
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.session
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.repository.consensus.ElectionsSession
import io.novafoundation.nova.feature_staking_impl.data.repository.consensus.ElectionsSessionRegistry
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.network.updaters.multiChain.DelegateToTimelineChainIdHolder
class CurrentEpochIndexUpdater(
electionsSessionRegistry: ElectionsSessionRegistry,
stakingSharedState: StakingSharedState,
timelineDelegatingChainIdHolder: DelegateToTimelineChainIdHolder,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : ElectionsSessionParameterUpdater(
electionsSessionRegistry = electionsSessionRegistry,
stakingSharedState = stakingSharedState,
timelineDelegatingChainIdHolder = timelineDelegatingChainIdHolder,
chainRegistry = chainRegistry,
storageCache = storageCache
) {
override suspend fun ElectionsSession.updaterStorageKey(chainId: ChainId): String? {
return currentEpochIndexStorageKey(chainId)
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.session
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.common.utils.provideContext
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.GlobalScope
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.currentIndex
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.session
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.TimelineDelegatingSingleKeyUpdater
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.multiChain.DelegateToTimelineChainIdHolder
import io.novafoundation.nova.runtime.network.updaters.multiChain.SharedStateBasedUpdater
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
class CurrentSessionIndexUpdater(
timelineDelegatingChainIdHolder: DelegateToTimelineChainIdHolder,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : TimelineDelegatingSingleKeyUpdater<Unit>(GlobalScope, chainRegistry, storageCache, timelineDelegatingChainIdHolder), SharedStateBasedUpdater<Unit> {
override suspend fun storageKey(runtime: RuntimeSnapshot, scopeValue: Unit): String {
return runtime.provideContext {
metadata.session.currentIndex.storageKey()
}
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.session
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.repository.consensus.ElectionsSession
import io.novafoundation.nova.feature_staking_impl.data.repository.consensus.ElectionsSessionRegistry
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.network.updaters.multiChain.DelegateToTimelineChainIdHolder
class CurrentSlotUpdater(
electionsSessionRegistry: ElectionsSessionRegistry,
timelineDelegatingChainIdHolder: DelegateToTimelineChainIdHolder,
stakingSharedState: StakingSharedState,
chainRegistry: ChainRegistry,
storageCache: StorageCache
) : ElectionsSessionParameterUpdater(
electionsSessionRegistry = electionsSessionRegistry,
stakingSharedState = stakingSharedState,
timelineDelegatingChainIdHolder = timelineDelegatingChainIdHolder,
chainRegistry = chainRegistry,
storageCache = storageCache
) {
override suspend fun ElectionsSession.updaterStorageKey(chainId: ChainId): String? {
return currentSlotStorageKey(chainId)
}
}

Some files were not shown because too many files have changed in this diff Show More