Skip to content

Commit 7bfd112

Browse files
authored
Merge pull request #922 from soramitsu/develop
develop
2 parents 6f60d2d + a42b588 commit 7bfd112

File tree

13 files changed

+207
-61
lines changed

13 files changed

+207
-61
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ apply from: './scripts/secrets.gradle'
33
buildscript {
44
ext {
55
// App version
6-
versionName = '2.2.3'
7-
versionCode = 92
6+
versionName = '2.2.4'
7+
versionCode = 93
88

99
// SDK and tools
1010
compileSdkVersion = 33

core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
55

66
val Migration_54_55 = object : Migration(54, 55) {
77
override fun migrate(database: SupportSQLiteDatabase) {
8+
database.execSQL("DROP TABLE IF EXISTS `_address_book`")
9+
database.execSQL("CREATE TABLE `_address_book` AS SELECT * FROM `address_book`")
10+
database.execSQL("DELETE FROM `address_book` where `id` NOT IN (SELECT `id` FROM `_address_book` GROUP BY `address`, `chainId`)")
11+
database.execSQL("DROP TABLE IF EXISTS `_address_book`")
12+
813
database.execSQL(
914
"""
1015
CREATE UNIQUE INDEX IF NOT EXISTS `index_address_book_address_chainId` ON `address_book` (`address`, `chainId`)

feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/di/StakingFeatureModule.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,15 +378,17 @@ class StakingFeatureModule {
378378
stakingRelayChainScenarioInteractor: StakingRelayChainScenarioInteractor,
379379
iconGenerator: AddressIconGenerator,
380380
accountDisplayUseCase: AddressDisplayUseCase,
381-
sharedState: StakingSharedState
381+
sharedState: StakingSharedState,
382+
soraStakingRewardsScenario: SoraStakingRewardsScenario
382383
): RewardDestinationMixin.Presentation = RewardDestinationProvider(
383384
resourceManager,
384385
stakingInteractor,
385386
stakingRelayChainScenarioInteractor,
386387
iconGenerator,
387388
appLinksProvider,
388389
sharedState,
389-
accountDisplayUseCase
390+
accountDisplayUseCase,
391+
soraStakingRewardsScenario
390392
)
391393

392394
@Provides

feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/AlertsInteractor.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ import jp.co.soramitsu.wallet.impl.domain.model.Asset
1616
import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks
1717
import kotlinx.coroutines.flow.Flow
1818
import kotlinx.coroutines.flow.combine
19-
import kotlinx.coroutines.flow.emitAll
20-
import kotlinx.coroutines.flow.first
21-
import kotlinx.coroutines.flow.flow
19+
import kotlinx.coroutines.flow.flatMapLatest
20+
import kotlinx.coroutines.flow.flowOf
2221
import java.math.BigDecimal
2322
import java.math.BigInteger
2423
import jp.co.soramitsu.core.models.Asset as CoreAsset
@@ -116,12 +115,9 @@ class AlertsInteractor(
116115
::produceSetValidatorsAlert
117116
)
118117

119-
fun getAlertsFlow(stakingState: StakingState): Flow<List<Alert>> = flow {
120-
val (chain, chainAsset) = sharedState.assetWithChain.first()
121-
118+
fun getAlertsFlow(stakingState: StakingState): Flow<List<Alert>> = sharedState.assetWithChain.flatMapLatest { (chain, chainAsset) ->
122119
if (chainAsset.staking != CoreAsset.StakingType.RELAYCHAIN) {
123-
emit(emptyList())
124-
return@flow
120+
return@flatMapLatest flowOf(emptyList())
125121
}
126122

127123
val maxRewardedNominatorsPerValidator = stakingConstantsRepository.maxRewardedNominatorPerValidator(chain.id)
@@ -146,7 +142,7 @@ class AlertsInteractor(
146142
alertProducers.mapNotNull { it.invoke(context) }
147143
}
148144

149-
emitAll(alertsFlow)
145+
alertsFlow
150146
}
151147

152148
private inline fun <reified T : StakingState, R> requireState(

feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/ManualRewardCalculator.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package jp.co.soramitsu.staking.impl.domain.rewards
22

3+
import java.math.BigDecimal
4+
import java.math.BigInteger
35
import jp.co.soramitsu.common.utils.fractionToPercentage
46
import jp.co.soramitsu.common.utils.median
57
import jp.co.soramitsu.common.utils.sumByBigInteger
68
import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId
79
import jp.co.soramitsu.shared_utils.extensions.toHexString
10+
import kotlin.math.pow
811
import kotlinx.coroutines.Dispatchers
912
import kotlinx.coroutines.withContext
10-
import java.math.BigDecimal
11-
import java.math.BigInteger
12-
import kotlin.math.pow
1313

1414
private const val PARACHAINS_ENABLED = false
1515

@@ -71,7 +71,7 @@ open class ManualRewardCalculator(
7171
}
7272
}
7373

74-
protected val maxAPY = apyByValidator.values.maxOrNull() ?: 0.0
74+
private val maxAPY = apyByValidator.values.maxOrNull() ?: 0.0
7575

7676
override suspend fun calculateMaxAPY(chainId: ChainId) = calculateReturns(
7777
amount = BigDecimal.ONE,
@@ -136,11 +136,11 @@ open class ManualRewardCalculator(
136136
)
137137
}
138138

139-
protected fun calculateSimpleReward(amount: Double, days: Int, dailyPercentage: Double): BigDecimal {
139+
private fun calculateSimpleReward(amount: Double, days: Int, dailyPercentage: Double): BigDecimal {
140140
return amount.toBigDecimal() * dailyPercentage.toBigDecimal() * days.toBigDecimal()
141141
}
142142

143-
protected fun calculateCompoundReward(amount: Double, days: Int, dailyPercentage: Double): BigDecimal {
143+
private fun calculateCompoundReward(amount: Double, days: Int, dailyPercentage: Double): BigDecimal {
144144
return amount.toBigDecimal() * ((1 + dailyPercentage).toBigDecimal().pow(days)) - amount.toBigDecimal()
145145
}
146146
}

feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/RewardCalculatorFactory.kt

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,48 @@ class RewardCalculatorFactory(
5454
)
5555
}
5656

57+
suspend fun createSoraWithCustomValidatorsSettings(
58+
exposures: AccountIdMap<Exposure>,
59+
validatorsPrefs: AccountIdMap<ValidatorPrefs?>,
60+
asset: Asset
61+
): RewardCalculator = withContext(Dispatchers.Default) {
62+
val chainId = asset.chainId
63+
64+
val cached = calculators[chainId]
65+
if (cached != null) {
66+
return@withContext cached
67+
}
68+
69+
val validatorsPayouts = relayChainRepository.getErasValidatorRewards(chainId).values.filterNotNull().map { asset.amountFromPlanks(it).toDouble() }
70+
val averageValidatorPayout: Double = validatorsPayouts.average()
71+
72+
val validators = exposures.keys.mapNotNull { accountIdHex ->
73+
val exposure = exposures[accountIdHex] ?: accountIdNotFound(accountIdHex)
74+
val validatorPrefs = validatorsPrefs[accountIdHex] ?: return@mapNotNull null
75+
76+
RewardCalculationTarget(
77+
accountIdHex = accountIdHex,
78+
totalStake = exposure.total,
79+
nominatorStakes = exposure.others,
80+
ownStake = exposure.own,
81+
commission = validatorPrefs.commission
82+
)
83+
}
84+
85+
val rateInPlanks = soraStakingRewardsScenario.mainAssetToRewardAssetRate()
86+
val rate = asset.amountFromPlanks(rateInPlanks).toDouble()
87+
val calculator = SoraRewardCalculator(
88+
validators = validators,
89+
xorValRate = rate,
90+
averageValidatorPayout = averageValidatorPayout,
91+
asset = asset
92+
)
93+
94+
calculators[chainId] = calculator
95+
96+
return@withContext calculator
97+
}
98+
5799
private suspend fun createManual(chainId: ChainId): RewardCalculator = withContext(Dispatchers.Default) {
58100
val cached = calculators[chainId]
59101
if (cached != null) {
@@ -76,13 +118,15 @@ class RewardCalculatorFactory(
76118

77119
val cached = calculators[chainId]
78120
if (cached != null) {
79-
return cached as ManualRewardCalculator
121+
return cached
80122
}
123+
124+
val validatorsPayouts = relayChainRepository.getErasValidatorRewards(chainId).values.filterNotNull().map { asset.amountFromPlanks(it).toDouble() }
125+
val averageValidatorPayout: Double = validatorsPayouts.average()
126+
81127
val exposures = relayChainRepository.getActiveElectedValidatorsExposures(chainId)
82128
val validatorsPrefs = relayChainRepository.getValidatorPrefs(chainId, exposures.keys.toList())
83129

84-
val totalIssuance = stakingRepository.getTotalIssuance(chainId)
85-
86130
val validators = exposures.keys.mapNotNull { accountIdHex ->
87131
val exposure = exposures[accountIdHex] ?: accountIdNotFound(accountIdHex)
88132
val validatorPrefs = validatorsPrefs[accountIdHex] ?: return@mapNotNull null
@@ -97,11 +141,12 @@ class RewardCalculatorFactory(
97141
}
98142

99143
val rateInPlanks = soraStakingRewardsScenario.mainAssetToRewardAssetRate()
100-
val rate = asset.amountFromPlanks(rateInPlanks)
144+
val rate = asset.amountFromPlanks(rateInPlanks).toDouble()
101145
val calculator = SoraRewardCalculator(
102146
validators = validators,
103-
totalIssuance = totalIssuance,
104-
xorValRate = rate
147+
xorValRate = rate,
148+
averageValidatorPayout = averageValidatorPayout,
149+
asset = asset
105150
)
106151

107152
calculators[chainId] = calculator
@@ -114,7 +159,6 @@ class RewardCalculatorFactory(
114159
val chainId = asset.chainId
115160
val syntheticType = asset.syntheticStakingType()
116161

117-
hashCode()
118162
return when {
119163
syntheticType == SyntheticStakingType.SORA -> createSora(asset)
120164
stakingType == Asset.StakingType.RELAYCHAIN -> createManual(chainId)
Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,109 @@
11
package jp.co.soramitsu.staking.impl.domain.rewards
22

33
import java.math.BigDecimal
4-
import java.math.BigInteger
54
import jp.co.soramitsu.common.utils.fractionToPercentage
5+
import jp.co.soramitsu.common.utils.median
6+
import jp.co.soramitsu.common.utils.sumByBigInteger
7+
import jp.co.soramitsu.core.models.Asset
8+
import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId
9+
import jp.co.soramitsu.shared_utils.extensions.toHexString
10+
import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.withContext
613

714
class SoraRewardCalculator(
8-
validators: List<RewardCalculationTarget>,
9-
totalIssuance: BigInteger,
10-
private val xorValRate: BigDecimal
11-
) : ManualRewardCalculator(validators, totalIssuance) {
15+
private val validators: List<RewardCalculationTarget>,
16+
private val xorValRate: Double,
17+
private val averageValidatorPayout: Double,
18+
private val asset: Asset
19+
) : RewardCalculator {
20+
companion object {
21+
private const val ERAS_PER_DAY = 4
22+
}
23+
24+
private val apyByValidator = validators.associateBy(
25+
keySelector = RewardCalculationTarget::accountIdHex,
26+
valueTransform = ::calculateValidatorAPY
27+
)
28+
29+
private val maxAPY = apyByValidator.values.maxOrNull() ?: 0.0
30+
private val expectedAPY = calculateExpectedAPY()
31+
32+
private fun calculateExpectedAPY(): Double {
33+
val prices = validators.map { it.commission.toDouble() }.filter { it < 1.0 }
34+
35+
val medianCommission = when {
36+
prices.isEmpty() -> 0.0
37+
else -> prices.median()
38+
}
39+
val averageValidatorRewardPercentage = apyByValidator.values.average()
40+
return averageValidatorRewardPercentage * (1 - medianCommission)
41+
}
42+
43+
private fun calculateValidatorAPY(validator: RewardCalculationTarget): Double {
44+
val validatorOwnStake = asset.amountFromPlanks(validator.totalStake).toDouble()
45+
val totalStaked = validators.sumByBigInteger(RewardCalculationTarget::totalStake)
46+
47+
val portion = validatorOwnStake / asset.amountFromPlanks(totalStaked).toDouble()
48+
val averageValidatorRewardInVal = averageValidatorPayout * portion
49+
val ownStakeInVal = validatorOwnStake * xorValRate
50+
val result = averageValidatorRewardInVal / ownStakeInVal * (1 - validator.commission.toDouble())
51+
52+
return result * ERAS_PER_DAY * DAYS_IN_YEAR
53+
}
54+
55+
override suspend fun calculateMaxAPY(chainId: ChainId): BigDecimal {
56+
return calculateReturns(
57+
amount = BigDecimal.ONE,
58+
days = DAYS_IN_YEAR,
59+
isCompound = false,
60+
chainId = chainId
61+
).gainPercentage
62+
}
63+
64+
override suspend fun calculateAvgAPY(): BigDecimal {
65+
val average = apyByValidator.values.average()
66+
val dailyPercentage = average / DAYS_IN_YEAR
67+
return calculateReward(
68+
amount = BigDecimal.ONE.toDouble(),
69+
days = DAYS_IN_YEAR,
70+
dailyPercentage = dailyPercentage
71+
).gainPercentage
72+
}
1273

13-
override fun calculateReward(
74+
override suspend fun getApyFor(targetId: ByteArray): BigDecimal {
75+
val apy = apyByValidator[targetId.toHexString()] ?: expectedAPY
76+
77+
return apy.toBigDecimal()
78+
}
79+
80+
override suspend fun calculateReturns(amount: BigDecimal, days: Int, isCompound: Boolean, chainId: ChainId) = withContext(Dispatchers.Default) {
81+
val dailyPercentage = maxAPY / DAYS_IN_YEAR
82+
calculateReward(amount.toDouble(), days, dailyPercentage)
83+
}
84+
85+
override suspend fun calculateReturns(amount: Double, days: Int, isCompound: Boolean, targetIdHex: String) = withContext(Dispatchers.Default) {
86+
val validatorAPY =
87+
apyByValidator[targetIdHex] ?: error("Validator with $targetIdHex was not found")
88+
val dailyPercentage = validatorAPY / DAYS_IN_YEAR
89+
90+
calculateReward(amount, days, dailyPercentage)
91+
}
92+
93+
private fun calculateReward(
1494
amount: Double,
1595
days: Int,
16-
dailyPercentage: Double,
17-
isCompound: Boolean
96+
dailyPercentage: Double
1897
): PeriodReturns {
19-
val gainAmount = if (isCompound) {
20-
calculateCompoundReward(amount, days, dailyPercentage)
21-
} else {
22-
calculateSimpleReward(amount, days, dailyPercentage)
23-
}
98+
val gainAmount = amount.toBigDecimal() * dailyPercentage.toBigDecimal() * days.toBigDecimal()
2499
val gainPercentage = if (amount == 0.0) {
25100
BigDecimal.ZERO
26101
} else {
27-
(gainAmount / xorValRate / amount.toBigDecimal()).fractionToPercentage()
102+
(gainAmount / amount.toBigDecimal()).fractionToPercentage()
28103
}
29104
return PeriodReturns(
30-
gainAmount = gainAmount,
105+
gainAmount = gainAmount * xorValRate.toBigDecimal(),
31106
gainPercentage = gainPercentage
32107
)
33108
}
34-
35-
override suspend fun calculateAvgAPY(): BigDecimal {
36-
val dailyPercentage = maxAPY / DAYS_IN_YEAR
37-
return calculateReward(1.0, DAYS_IN_YEAR, dailyPercentage, false).gainPercentage
38-
}
39109
}

feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validators/ValidatorProvider.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package jp.co.soramitsu.staking.impl.domain.validators
22

33
import jp.co.soramitsu.common.utils.toHexAccountId
4+
import jp.co.soramitsu.core.models.utilityAsset
45
import jp.co.soramitsu.runtime.ext.addressOf
56
import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain
7+
import jp.co.soramitsu.runtime.multiNetwork.chain.model.soraMainChainId
68
import jp.co.soramitsu.shared_utils.extensions.fromHex
79
import jp.co.soramitsu.staking.api.domain.api.AccountIdMap
810
import jp.co.soramitsu.staking.api.domain.api.IdentityRepository
@@ -48,7 +50,12 @@ class ValidatorProvider(
4850
val identities = identityRepository.getIdentitiesFromIds(chain, requestedValidatorIds)
4951
val slashes = stakingRepository.getSlashes(chainId, requestedValidatorIds)
5052

51-
val rewardCalculator = rewardCalculatorFactory.createManual(electedValidatorExposures, validatorPrefs, chainId)
53+
val rewardCalculator = if (chainId == soraMainChainId) {
54+
rewardCalculatorFactory.createSoraWithCustomValidatorsSettings(electedValidatorExposures, validatorPrefs, chain.utilityAsset)
55+
} else {
56+
rewardCalculatorFactory.createManual(electedValidatorExposures, validatorPrefs, chainId)
57+
}
58+
5259
val maxNominators = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId)
5360

5461
return requestedValidatorIds.map { accountIdHex ->

0 commit comments

Comments
 (0)