diff --git a/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigDataSource.kt b/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigDataSource.kt index 98e1f73..b2ab21e 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigDataSource.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigDataSource.kt @@ -32,6 +32,7 @@ class RealGlobalConfigDataSource( private fun GlobalConfigRemote.toDomain() = GlobalConfig( multisigsApiUrl = multisigsApiUrl, proxyApiUrl = proxyApiUrl, - multiStakingApiUrl = multiStakingApiUrl + multiStakingApiUrl = multiStakingApiUrl, + stakingApiOverrides = stakingApiOverrides ?: emptyMap() ) } diff --git a/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigRemote.kt b/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigRemote.kt index 5c07cfb..00f8f2b 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigRemote.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/config/GlobalConfigRemote.kt @@ -3,5 +3,6 @@ package io.novafoundation.nova.common.data.config class GlobalConfigRemote( val multisigsApiUrl: String, val proxyApiUrl: String, - val multiStakingApiUrl: String + val multiStakingApiUrl: String, + val stakingApiOverrides: Map>? ) diff --git a/common/src/main/java/io/novafoundation/nova/common/domain/config/GlobalConfig.kt b/common/src/main/java/io/novafoundation/nova/common/domain/config/GlobalConfig.kt index 2d45168..d347b00 100644 --- a/common/src/main/java/io/novafoundation/nova/common/domain/config/GlobalConfig.kt +++ b/common/src/main/java/io/novafoundation/nova/common/domain/config/GlobalConfig.kt @@ -3,5 +3,6 @@ package io.novafoundation.nova.common.domain.config class GlobalConfig( val multisigsApiUrl: String, val proxyApiUrl: String, - val multiStakingApiUrl: String + val multiStakingApiUrl: String, + val stakingApiOverrides: Map> ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt index f2042a4..5ee63bb 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt @@ -2,6 +2,7 @@ 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.domain.config.GlobalConfig import io.novafoundation.nova.common.utils.asPerbill import io.novafoundation.nova.common.utils.atLeastZero import io.novafoundation.nova.common.utils.orZero @@ -20,6 +21,8 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba 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.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext interface StakingStatsDataSource { @@ -37,31 +40,63 @@ class RealStakingStatsDataSource( stakingChains: List ): MultiChainStakingStats = withContext(Dispatchers.IO) { retryUntilDone { - val request = StakingStatsRequest(stakingAccounts, stakingChains) val globalConfig = globalConfigDataSource.getGlobalConfig() - val response = api.fetchStakingStats(request, globalConfig.multiStakingApiUrl).data + val chainsByEndpoint = splitChainsByEndpoint(stakingChains, globalConfig) - 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() - ) - } + coroutineScope { + chainsByEndpoint.map { (url, chains) -> + async { fetchStatsFromEndpoint(stakingAccounts, chains, url) } + }.map { it.await() } + }.fold(emptyMap()) { acc, map -> acc + map } } } + private suspend fun fetchStatsFromEndpoint( + stakingAccounts: StakingAccounts, + chains: List, + url: String + ): MultiChainStakingStats { + if (chains.isEmpty()) return emptyMap() + + val request = StakingStatsRequest(stakingAccounts, chains) + val response = api.fetchStakingStats(request, url).data + + val earnings = response.stakingApies.associatedById() + val rewards = response.rewards?.associatedById() ?: emptyMap() + val slashes = response.slashes?.associatedById() ?: emptyMap() + val activeStakers = response.activeStakers?.groupedById() ?: emptyMap() + + return 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 splitChainsByEndpoint( + chains: List, + globalConfig: GlobalConfig + ): Map> { + val overrideUrlByChainId = globalConfig.stakingApiOverrides.flatMap { (url, chainIds) -> + chainIds.map { chainId -> chainId to url } + }.toMap() + + val result = mutableMapOf>() + for (chain in chains) { + val url = overrideUrlByChainId[chain.id] ?: globalConfig.multiStakingApiUrl + result.getOrPut(url) { mutableListOf() }.add(chain) + } + return result + } + private fun Map.getPlanks(key: StakingOptionId): Balance { return get(key)?.amount?.toBigInteger().orZero() }