mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 16:08:06 +00:00
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:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
+36
@@ -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)
|
||||
}
|
||||
+56
@@ -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
|
||||
}
|
||||
}
|
||||
+35
@@ -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)
|
||||
}
|
||||
}
|
||||
+45
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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}"
|
||||
}
|
||||
+13
@@ -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
|
||||
)
|
||||
+8
@@ -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)
|
||||
+106
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
+15
@@ -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>
|
||||
}
|
||||
+166
@@ -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?,
|
||||
)
|
||||
}
|
||||
+27
@@ -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
|
||||
}
|
||||
+30
@@ -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
|
||||
}
|
||||
}
|
||||
+8
@@ -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,
|
||||
)
|
||||
+188
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -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)
|
||||
}
|
||||
}
|
||||
+180
@@ -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?
|
||||
)
|
||||
}
|
||||
+229
@@ -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?
|
||||
)
|
||||
}
|
||||
+164
@@ -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?
|
||||
)
|
||||
+176
@@ -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?
|
||||
)
|
||||
+114
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+11
@@ -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()
|
||||
}
|
||||
+109
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+14
@@ -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())
|
||||
}
|
||||
}
|
||||
+8
@@ -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
|
||||
}
|
||||
+9
@@ -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>()
|
||||
+11
@@ -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>
|
||||
)
|
||||
+35
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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
|
||||
}
|
||||
}
|
||||
+86
@@ -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) })
|
||||
+98
@@ -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
|
||||
)
|
||||
}
|
||||
+11
@@ -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)
|
||||
}
|
||||
+22
@@ -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"])
|
||||
)
|
||||
}
|
||||
+21
@@ -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"])
|
||||
)
|
||||
}
|
||||
+35
@@ -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)
|
||||
}
|
||||
+11
@@ -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")
|
||||
}
|
||||
+64
@@ -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)
|
||||
)
|
||||
}
|
||||
+30
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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()
|
||||
}
|
||||
+31
@@ -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)
|
||||
}
|
||||
}
|
||||
+136
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+190
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+26
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
}
|
||||
+31
@@ -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)
|
||||
+28
@@ -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)
|
||||
+77
@@ -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
|
||||
)
|
||||
+20
@@ -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)
|
||||
)
|
||||
}
|
||||
+15
@@ -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()
|
||||
}
|
||||
}
|
||||
+22
@@ -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)
|
||||
+65
@@ -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)
|
||||
+43
@@ -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)
|
||||
+64
@@ -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>; // nominator’s 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)
|
||||
)
|
||||
}
|
||||
+15
@@ -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))
|
||||
}
|
||||
+38
@@ -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)
|
||||
}
|
||||
+15
@@ -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()
|
||||
}
|
||||
+39
@@ -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()
|
||||
}
|
||||
}
|
||||
+9
@@ -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)
|
||||
+32
@@ -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)
|
||||
}
|
||||
+56
@@ -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")
|
||||
)
|
||||
}
|
||||
+23
@@ -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))
|
||||
}
|
||||
+19
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
)
|
||||
}
|
||||
+116
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
+43
@@ -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
|
||||
}
|
||||
+23
@@ -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)
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
+30
@@ -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")
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
+23
@@ -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)
|
||||
}
|
||||
}
|
||||
+26
@@ -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)
|
||||
}
|
||||
}
|
||||
+50
@@ -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)
|
||||
}
|
||||
+182
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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)
|
||||
}
|
||||
}
|
||||
+67
@@ -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)
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
+251
@@ -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)
|
||||
}
|
||||
}
|
||||
+70
@@ -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()
|
||||
}
|
||||
}
|
||||
+13
@@ -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)
|
||||
}
|
||||
}
|
||||
+58
@@ -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"
|
||||
}
|
||||
}
|
||||
+13
@@ -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)
|
||||
}
|
||||
}
|
||||
+31
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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() }
|
||||
}
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
+26
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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
Reference in New Issue
Block a user