Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dock staking apy #1356

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.novafoundation.nova.common.data.network.runtime.binding

import io.novafoundation.nova.common.utils.Percent
import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot
import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromByteArrayOrNull
import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Constant
Expand All @@ -20,3 +21,15 @@ fun bindNullableNumberConstant(

return decoded as BigInteger?
}

fun Constant.decodePercentOrThrow(runtime: RuntimeSnapshot): Percent {
return decodeAsOrThrow(runtime) { Percent(bindNumber(it).toDouble()) }
}

fun <T> Constant.decodeAsOrThrow(runtime: RuntimeSnapshot, binding: (Any?) -> T): T {
return binding(decodeOrThrow(runtime))
}

fun Constant.decodeOrThrow(runtime: RuntimeSnapshot): Any? {
return requireNotNull(type).fromByteArrayOrIncompatible(value, runtime)
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ fun Module.optionalNumberConstant(name: String, runtimeSnapshot: RuntimeSnapshot

fun Constant.asNumber(runtimeSnapshot: RuntimeSnapshot) = bindNumberConstant(this, runtimeSnapshot)

fun Constant.asPerbill(runtimeSnapshot: RuntimeSnapshot) = bindNumberConstant(this, runtimeSnapshot)

fun Module.constantOrNull(name: String) = constants[name]

fun RuntimeMetadata.staking() = module(Modules.STAKING)
Expand Down Expand Up @@ -235,6 +237,8 @@ fun RuntimeMetadata.assetConversionOrNull() = moduleOrNull(Modules.ASSET_CONVERS

fun RuntimeMetadata.assetConversion() = module(Modules.ASSET_CONVERSION)

fun RuntimeMetadata.stakingRewards() = module(Modules.STAKING_REWARDS)

fun RuntimeMetadata.proxy() = module(Modules.PROXY)

fun RuntimeMetadata.firstExistingModuleName(vararg options: String): String {
Expand Down Expand Up @@ -394,4 +398,6 @@ object Modules {
const val MULTISIG = "Multisig"
const val REGISTRAR = "Registrar"
const val FAST_UNSTAKE = "FastUnstake"

const val STAKING_REWARDS = "StakingRewards"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.rpc

import io.novafoundation.nova.common.utils.asGsonParsedNumber
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.getSocket
import jp.co.soramitsu.fearless_utils.wsrpc.executeAsync
import jp.co.soramitsu.fearless_utils.wsrpc.request.runtime.RuntimeRequest

interface DockRpc {

suspend fun stakingRewardYearlyEmission(chainId: ChainId, totalStaked: Balance, totalIssuance: Balance): Balance
}

class RealDockRpc(
private val chainRegistry: ChainRegistry,
) : DockRpc {

override suspend fun stakingRewardYearlyEmission(chainId: ChainId, totalStaked: Balance, totalIssuance: Balance): Balance {
val request = YearlyEmissionRequest(totalStaked, totalIssuance)

val response = chainRegistry.getSocket(chainId).executeAsync(request)

return response.result.asGsonParsedNumber()
}
}

private class YearlyEmissionRequest(
totalStaked: Balance,
totalIssuance: Balance
) : RuntimeRequest(
method = "staking_rewards_yearlyEmission",
params = listOf(totalStaked, totalIssuance)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.novafoundation.nova.feature_staking_impl.data.repository

import io.novafoundation.nova.common.data.network.runtime.binding.decodePercentOrThrow
import io.novafoundation.nova.common.utils.Percent
import io.novafoundation.nova.common.utils.constant
import io.novafoundation.nova.common.utils.stakingRewards
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.rpc.DockRpc
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
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.metadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigInteger

interface DockStakingRepository {

suspend fun getEarlyEmission(chainId: ChainId, totalStaked: Balance, totalIssuance: Balance): BigInteger

suspend fun getTreasuryRewardsPercentage(chainId: ChainId): Percent
}

class RealDockStakingRepository(
private val dockRpc: DockRpc,
private val storageDataSource: StorageDataSource,
) : DockStakingRepository {

override suspend fun getEarlyEmission(chainId: ChainId, totalStaked: Balance, totalIssuance: Balance): BigInteger = withContext(Dispatchers.IO) {
dockRpc.stakingRewardYearlyEmission(chainId, totalStaked, totalIssuance)
}

override suspend fun getTreasuryRewardsPercentage(chainId: ChainId): Percent {
return storageDataSource.query(chainId) {
metadata.stakingRewards().constant("TreasuryRewardsPct").decodePercentOrThrow(runtime)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.NominationPoolStateRepository
import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.RoundDurationEstimator
import io.novafoundation.nova.feature_staking_impl.data.repository.BagListRepository
import io.novafoundation.nova.feature_staking_impl.data.repository.DockStakingRepository
import io.novafoundation.nova.feature_staking_impl.data.repository.LocalBagListRepository
import io.novafoundation.nova.feature_staking_impl.data.repository.ParasRepository
import io.novafoundation.nova.feature_staking_impl.data.repository.PayoutRepository
Expand Down Expand Up @@ -61,6 +62,7 @@ import io.novafoundation.nova.feature_staking_impl.data.validators.FixedKnownNov
import io.novafoundation.nova.feature_staking_impl.data.validators.KnownNovaValidators
import io.novafoundation.nova.feature_staking_impl.di.staking.DefaultBulkRetriever
import io.novafoundation.nova.feature_staking_impl.di.staking.PayoutsBulkRetriever
import io.novafoundation.nova.feature_staking_impl.di.staking.relaychain.dock.DockStakingModule
import io.novafoundation.nova.feature_staking_impl.domain.StakingInteractor
import io.novafoundation.nova.feature_staking_impl.domain.alerts.AlertsInteractor
import io.novafoundation.nova.feature_staking_impl.domain.common.EraTimeCalculatorFactory
Expand Down Expand Up @@ -113,7 +115,7 @@ import javax.inject.Named
const val PAYOUTS_BULK_RETRIEVER_PAGE_SIZE = 500
const val DEFAULT_BULK_RETRIEVER_PAGE_SIZE = 1000

@Module(includes = [AssetUseCaseModule::class])
@Module(includes = [AssetUseCaseModule::class, DockStakingModule::class])
class StakingFeatureModule {

@Provides
Expand Down Expand Up @@ -289,7 +291,14 @@ class StakingFeatureModule {
totalIssuanceRepository: TotalIssuanceRepository,
stakingSharedComputation: dagger.Lazy<StakingSharedComputation>,
parasRepository: ParasRepository,
) = RewardCalculatorFactory(repository, totalIssuanceRepository, stakingSharedComputation, parasRepository)
dockStakingRepository: DockStakingRepository
) = RewardCalculatorFactory(
stakingRepository = repository,
totalIssuanceRepository = totalIssuanceRepository,
shareStakingSharedComputation = stakingSharedComputation,
parasRepository = parasRepository,
dockStakingRepository = dockStakingRepository
)

@Provides
@FeatureScope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_impl.di.staking.relaychain.dock

import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.rpc.DockRpc
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.rpc.RealDockRpc
import io.novafoundation.nova.feature_staking_impl.data.repository.DockStakingRepository
import io.novafoundation.nova.feature_staking_impl.data.repository.RealDockStakingRepository
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Named

@Module
class DockStakingModule {

@Provides
@FeatureScope
fun provideDockRpc(chainRegistry: ChainRegistry): DockRpc = RealDockRpc(chainRegistry)

@Provides
@FeatureScope
fun provideDockRepository(
dockRpc: DockRpc,
@Named(REMOTE_STORAGE_SOURCE) storageDataSource: StorageDataSource
): DockStakingRepository = RealDockStakingRepository(dockRpc, storageDataSource)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_staking_impl.domain.rewards

import io.novafoundation.nova.common.utils.Perbill
import java.math.BigInteger

class DockRewardCalculator(
validators: List<RewardCalculationTarget>,
private val totalIssuance: BigInteger,
private val yearlyEmission: BigInteger,
private val treasuryRewardsPercentage: Perbill,
) : InflationBasedRewardCalculator(validators, totalIssuance) {

override fun calculateYearlyInflation(stakedPortion: Double): Double {
val inflation = yearlyEmission.toDouble() / totalIssuance.toDouble()

return inflation * (1 - treasuryRewardsPercentage.value)
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package io.novafoundation.nova.feature_staking_impl.domain.rewards

import io.novafoundation.nova.common.utils.sumByBigInteger
import io.novafoundation.nova.common.utils.toPerbill
import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap
import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository
import io.novafoundation.nova.feature_staking_api.domain.model.Exposure
import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs
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.repository.DockStakingRepository
import io.novafoundation.nova.feature_staking_impl.data.repository.ParasRepository
import io.novafoundation.nova.feature_staking_impl.data.stakingType
import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools
Expand Down Expand Up @@ -33,6 +36,7 @@ class RewardCalculatorFactory(
private val totalIssuanceRepository: TotalIssuanceRepository,
private val shareStakingSharedComputation: dagger.Lazy<StakingSharedComputation>,
private val parasRepository: ParasRepository,
private val dockStakingRepository: DockStakingRepository,
) {

suspend fun create(
Expand Down Expand Up @@ -67,17 +71,45 @@ class RewardCalculatorFactory(

private suspend fun StakingOption.createRewardCalculator(validators: List<RewardCalculationTarget>, totalIssuance: BigInteger): RewardCalculator {
return when (unwrapNominationPools().stakingType) {
RELAYCHAIN, RELAYCHAIN_AURA -> {
RELAYCHAIN, RELAYCHAIN_AURA -> createRelaychainCalculator(validators, totalIssuance)
ALEPH_ZERO -> AlephZeroRewardCalculator(validators, chainAsset = assetWithChain.asset)
NOMINATION_POOLS, UNSUPPORTED, PARACHAIN, TURING -> throw IllegalStateException("Unknown staking type in RelaychainRewardFactory")
}
}

private suspend fun StakingOption.createRelaychainCalculator(
validators: List<RewardCalculationTarget>,
totalIssuance: BigInteger
): RewardCalculator {
return when (chain.id) {
Chain.Geneses.DOCK -> createDockRewardCalculator(validators, totalIssuance)

else -> {
val activePublicParachains = parasRepository.activePublicParachains(assetWithChain.chain.id)
val inflationConfig = InflationConfig.create(chain.id, activePublicParachains)

RewardCurveInflationRewardCalculator(validators, totalIssuance, inflationConfig)
}
ALEPH_ZERO -> AlephZeroRewardCalculator(validators, chainAsset = assetWithChain.asset)
NOMINATION_POOLS, UNSUPPORTED, PARACHAIN, TURING -> throw IllegalStateException("Unknown staking type in RelaychainRewardFactory")
}
}

private suspend fun StakingOption.createDockRewardCalculator(
validators: List<RewardCalculationTarget>,
totalIssuance: BigInteger
): RewardCalculator {
val totalStaked = validators.sumByBigInteger(RewardCalculationTarget::totalStake)

val yearlyEmission = dockStakingRepository.getEarlyEmission(chain.id, totalStaked, totalIssuance)
val treasuryRewardsPercentage = dockStakingRepository.getTreasuryRewardsPercentage(chain.id).toPerbill()

return DockRewardCalculator(
validators = validators,
totalIssuance = totalIssuance,
yearlyEmission = yearlyEmission,
treasuryRewardsPercentage = treasuryRewardsPercentage
)
}

private fun InflationConfig.Companion.create(chainId: ChainId, activePublicParachains: Int?): InflationConfig {
return when (chainId) {
Chain.Geneses.POLKADOT -> Polkadot(activePublicParachains)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ object ChainGeneses {
const val ZEITGEIST = "1bf2a2ecb4a868de66ea8610f2ce7c8c43706561b6476031315f6640fe38e060"

const val WESTMINT = "67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9"

const val DOCK = "6bfe24dca2a3be10f22212678ac13a6446ec764103c0f3471c71609eac384aae"
}

object ChainIds {
Expand Down
Loading